MIDIを作れるJavaアプレットを作っているのだ。
前回でなんと作りかけの音楽を聞いて確かめられるようになったのだ。
ちらつかせない2
前回で一応ちらつきはなくなったのですが、後で「ある御方」から指摘を頂きまして、paintメソッドを直接呼び出すのはよろしくないとのことでしたので変更することにします。
前回「bgDirty = true;」と「paint(getGraphics());」をあわせてdirtyメソッドを作っていましたが、この「paint(getGraphics());」は「repaint();」としてやって、updateをオーバーライドしてrepaintを呼び出すのがよいらしいです。
本当によいのかどうかは経験の浅い私にはわかりませんが少なくとも悪くなることはないでしょう。
public void update(Graphics g) { paint(g); }
音が鳴らない?
実を言うと、開発のある段階から私のコンピュータではブラウザ上のアプレットでnoteOnメソッドによって音を鳴らすことができなくなっていました。
どうせ自分のところだけだろうと思ってほっといたんですがちょっと他の人数人に見てもらったところ全員が演奏ボタンでないと音が鳴らないといっていたので改めてアンケート方式で統計を取ってみたところ、実際多くの人が演奏でしか、つまりSequencerを使わないと音がならない状態だったので、できるだけ多くの環境で音が鳴るように、Sequencer.startにより音を鳴らす方法を検討してみようと思います。
というわけでSequencer.startで音を鳴らすためにその音を鳴らすためだけのSequenceを作ることにします。
音を鳴らす機能をどこに、つまり今回の場合MIDIAPTrackとMIDIViewTrackのどちらに置いておくかですが、音を鳴らすためには音に関するデータを多く使うので、楽譜の表示と編集を担うMIDIViewTrackよりデータ置き場のMIDIAPTrackに置いておくのがよさそうです。
Sequencerを使って音を鳴らすので当然Sequencerが必要なのですがMIDIAPTrackにはSequencerが入っていないので音を鳴らす段階でSequencerを渡してやることにしましょう。
では第6回でやったことを参考に作ってみます。
// トラック内の音を鳴らす public void noteOn(Sequencer sequencer, int channel, int note) { Sequence sequence; try { sequence = new Sequence(Sequence.PPQ,48); }catch (InvalidMidiDataException e) { System.out.println("Sequence生成失敗!"); return; } // トラックを作る Track track = sequence.createTrack(); insertInitMessage(track, channel); MIDIAPNote n = new MIDIAPNote(0,192,note,100); track.add(n.getNoteOnEvent(channel)); track.add(n.getNoteOffEvent(channel)); // 演奏 if (sequencer!=null) { try { sequencer.setSequence(sequence); sequencer.start(); }catch (InvalidMidiDataException e) { System.out.println("演奏失敗!"); return; } } } public void insertInitMessage(Track track, int channel) { track.add(new MidiEvent(getProgramMessage(channel),0)); }
insertInitMessageメソッドを追加していますがこれは最初に挿入されるトラック初期化情報がgenerateTrackと共通であるため将来初期化情報を増やす可能性を考えて別の場所に置くことにしています。
また、new MIDIAPNote(0,192,note,100) で音の長さを192にしているのは、いつまでその音がなり続けるかわからないため音を長めに取らなければいけないのですがあまりこの値を大きくするとMIDIイベント生成に時間がかかりすぎてしまうためにそれなりに長くて且つ生成に時間がかからない全音符の長さにしているためです。
そして忘れぬうちにnoteOffも作ります。
使われていない引数がありますがnoteOnと引数を共通にしたほうが一貫性があるので一応入れています。
// トラック内の音を消す public void noteOff(Sequencer sequencer, int channel, int note) { if (sequencer!=null) { if (sequencer.isRunning()) { sequencer.stop(); } } }
最後にこれを呼び出す必要がありますが簡単な置き換えだけなので記載しません。
スクロール
前回言ったように、このアプレットは音楽を一曲作り上げるにはあまりにも狭すぎます。
だからといって大きさを増やしてもせいぜい画面いっぱいが限度ですので、小さくても全部を見て編集できるように、つまりスクロールできるようにしなければなりません。
java.awt.Scrollbarのドキュメントでも見ながら作っていきましょう。
今度はAdjustmentListenerでイベントを受け取るようですね。
MIDIViewTrackに実装していきましょう。
public void adjustmentValueChanged(AdjustmentEvent e) { if (e.getSource()==vscroll) { }else if(e.getSource()==hscroll) { } }
さて、とりあえず垂直スクロールバーと水平スクロールバーで場合分けだけしてみましたが、実際どうするのかはまだ考えてませんでした。
確かpiano.scrollは基準となる音の高さを表していました。
ということはvscrollの値を使うことになります。
ピアノロールの鍵盤は下から0番、1番と並んでいるのでその点注意しておきます。
public void adjustmentValueChanged(AdjustmentEvent e) { if (e.getSource()==vscroll) { piano.scroll = 127-e.getValue(); piano.dirty(); }else if(e.getSource()==hscroll) { } }
しかし水平スクロールバーでスクロールできる時間軸のほうは本格的に何も考えてませんでした。
とりあえずpiano.starttimeという変数でも作ってそれによって表示を変えるようにしてみましょう。
public void adjustmentValueChanged(AdjustmentEvent e) { if (e.getSource()==vscroll) { piano.scroll = 127-e.getValue(); piano.dirty(); }else if(e.getSource()==hscroll) { piano.starttime = e.getValue(); piano.dirty(); } }
ピアノロールのほうも変更します。
表示部分で音符の位置をstarttime分左にずらすのと、逆にクリックされた部分のX座標は右にずらします。
マウスのほうは本当にただ単にstarttimeを足しているだけなので載せませんが音符のほうは鍵盤部分と重ならないようにしたりついでに画面外にある音符を表示しないようにしていたりするので載せておきます。
public void paint(Graphics g){ (中略。第10回を参照のこと) 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; int l,r; while (it.hasNext()) { note = (MIDIAPNote)it.next(); l = 50+note.getStart()-starttime; r = l+note.getDuration(); if (l<50) l=50; if (r>bgWidth) r=bgWidth; if (l<r) { g2.setColor(noteColorN); g2.fill3DRect(l,(scroll-note.getNote())*10,r-l,10,true); } } bgDirty = false; } g.drawImage(bgImage,0,0,null); }
忘れぬうちにリスナーを追加します。
ついでにスクロールバーのパラメータもいじってます。
public MIDIViewTrack(MIDIApplet parent, int track) { applet = parent; this.track=track; piano = new PanelPianoRoll(); vscroll = new Scrollbar(Scrollbar.VERTICAL,127-piano.scroll,1,0,128); hscroll = new Scrollbar(Scrollbar.HORIZONTAL,0,1,0,1920); Panel p = new Panel(new BorderLayout()); p.add("Center",piano); p.add("East",vscroll); p.add("South",hscroll); vscroll.addAdjustmentListener(this); hscroll.addAdjustmentListener(this); removeAll(); setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); add(p); }
更に別の場所で可視範囲を調整したりもしていますが詳しいことは省略します。
要点
- ユーザー側のJava実行環境のバージョンによってMidiChannel.noteOnで音がならないことがある。
- だから非効率的だけどSequencerで無理矢理鳴らす。
- MIDIイベントを置く位置にあまり無茶な値を指定すると時間がかかる。
- スクロールバーのスクロールを検出するにはAdjustmentListener。