10.演奏

MIDIを作れるJavaアプレットを作っているのだ。
前回でより使いやすいインターフェースを実現した。

ちらつかせない

ちらつきについてはActiveBasicの項で述べたのと理由も対処法も似ていますので今回も見えない部分にせこせこ図を描いてまとめて表示するということにしましょう。

// Before
for (int i=0;i<applet.getSize().height/10;i++) {
	drawKeyboard(g,i*10,scroll-i);
}
Iterator it=applet.doc.tracks[track].notes.iterator();
MIDIAPNote note;
while (it.hasNext()) {
	note = (MIDIAPNote)it.next();
	g.setColor(noteColorN);
	g.fill3DRect(50+note.getStart(),(scroll-note.getNote())*10,note.getDuration(),10,true);
}

これを書き換えると次のようになります。

// After
if (bgWidth!=getWidth()||bgHeight!=getHeight()) {
	// 大きさが変わっているらしいので裏画面を作り直す
	bgWidth = getWidth();
	bgHeight = getHeight();
	bgImage = new BufferedImage(bgWidth, bgHeight, BufferedImage.TYPE_INT_RGB);
	bgDirty = true;
}
if (bgDirty) {
	Graphics g2 = bgImage.getGraphics();
	for (int i=0;i<applet.getSize().height/10;i++) {
		drawKeyboard(g2,i*10,scroll-i);
	}
	Iterator it=applet.doc.tracks[track].notes.iterator();
	MIDIAPNote note;
	while (it.hasNext()) {
		note = (MIDIAPNote)it.next();
		g2.setColor(noteColorN);
		g2.fill3DRect(50+note.getStart(),(scroll-note.getNote())*10,note.getDuration(),10,true);
	}
	bgDirty = false;
}
g.drawImage(bgImage,0,0,null);

今回画面の書き換えがゲームほど多くはないのでbgDirtyで描きなおす必要があるかどうかを判定してから描くようにしています。
しかしこの状態で音符を何度も置いてみればわかるのですがこれでもちらつきは消えないのです!
少し音符作成の部分を見てみます。

if (e.getButton()==MouseEvent.BUTTON1){
	note = scroll-e.getY()/10;
	// 0以上128未満じゃなくて0から127の間ね。そういう定義だった気がする
	if (0<=note && note<=127) {
		if (e.getClickCount()==1) {
			applet.synthesizer.getChannels()[track].noteOn(note,100);
		}else if (e.getClickCount()==2) {
			if (e.getX()-50>=0) {
				applet.doc.tracks[track].addNote(new MIDIAPNote(((e.getX()-50)/24)*24,24,note,100));
				bgDirty = true;
				repaint();
			}
		}
	}
}else {
}

第8回で既にrepaintの対象はアプレットからピアノロール自身に変更していますし、今回でbgDirtyもきちんと変更しているので、問題はrepaintそのものの挙動ということになります。
ちょっとrepaintが何をやっているかドキュメントで見てみると、「このメソッドは、このコンポーネントの update メソッドを可能なかぎり速やかに呼び出します。」と書いてありました。
今度はupdateを見なければなりません。
しかしPanelのupdateメソッドのページには参考になりそうなことは書いてありません。
少し内容的には離れたところにありますがCanvasのupdateメソッドを見ると「キャンバスは最初に背景色でクリアされ、このキャンバスの paint メソッドを呼び出して完全に再描画されます。」と書いてあります。
どうもPanelでも同じ現象が起こっているようです。
結局ちらつかせないためにはpaintメソッドを直接呼び出す必要があるようです。

if (e.getButton()==MouseEvent.BUTTON1){
	note = scroll-e.getY()/10;
	// 0以上128未満じゃなくて0から127の間ね。そういう定義だった気がする
	if (0<=note && note<=127) {
		if (e.getClickCount()==1) {
			applet.synthesizer.getChannels()[track].noteOn(note,100);
		}else if (e.getClickCount()==2) {
			if (e.getX()-50>=0) {
				applet.doc.tracks[track].addNote(new MIDIAPNote(((e.getX()-50)/24)*24,24,note,100));
				bgDirty = true;
				paint(getGraphics());
			}
		}
	}
}else {
}

実際には「bgDirty = true;」と「paint(getGraphics());」をあわせてdirtyメソッドを作っています。

演奏

しかし、演奏に必要なことは既に第2回で調べているのです。
準備のいい自分に惚れ惚れします。<ナルシシストメ!
それではMIDIを鳴らす手順についてもう少し詳しく見てみます。

  1. MidiSystem.getSequencer()でSequencer取得(そしてSynthesizerのときと同じく使いまわす)
  2. Synthesizerのときと同じく忘れずopen()
  3. setSequenceでSequenceセット(もちろんSequenceはきちっと作っておく)
  4. startで鳴りはじめる
  5. stopで止まる

3番の「Sequenceはきちっと作っておく」というのはドキュメントクラスでやっているのでSequencerもそちらに置くことにしましょう。
よってコンストラクタで1・2、演奏時に3・4、終わったら5、という手順でよさそうです。
Sequence作成は送信のときと同じなのでまとめてしまいましょう。

public MIDIDocument(MIDIApplet parent) {
	applet = parent;
	try {
		sequencer = MidiSystem.getSequencer();
		sequencer.open();
	} catch (MidiUnavailableException e) {
		System.out.println("Sequencer取得に失敗!");
		sequencer = null;
		return;
	}
}
// データをCGIに受け渡す
public void Send() {
	// 送信データ作成
	Sequence seq = getSequence();
	if (seq==null) {
		System.out.println("Sequence取得に失敗!");
		return;
	}
	(中略。第7回を参照のこと)
}
// Sequence生成
Sequence getSequence() {
	Sequence seq;
	(中略。第6回を参照のこと)
	return seq;
}
// 演奏
void Play() {
	if (sequencer!=null) {
		try {
			sequencer.setSequence(getSequence());
			sequencer.start();
		}catch (InvalidMidiDataException e) {
			System.out.println("演奏失敗!");
			return;
		}
	}
}
// 停止
void Stop() {
	if (sequencer!=null) {
		if (sequencer.isRunning()) {
			sequencer.stop();
		}
	}
}

だけど演奏できるようになってもスクロールバーが働かないからまだ大きな曲は作れないんですよね…。

要点

見所はどちらかというとちらつき防止の部分です。
いつもの途中バージョン
そうそう、書き忘れてたけど音符の削除も作りました。TreeSet.removeね。

続く