MIDIを作れるJavaアプレットを作っているのだ。
前回でピアノロールを作り始めた!
ノート(音符)を作る
前回予告したとおり、音符を置くことを考えてみます。
まず、ミディビではフルスペックのMIDIに対応させるつもりはありません。なぜなら、MIDIの全ての機能を使えるようにするということはすなわち、それを前提にしたつくりにしなくてはならなくて、それがそんなに扱いやすいものではないからです(開発者にとっても利用者にとっても)。
そんなわけである程度制限を設けることで構造を簡略化してゆくことを考えます。
例えば、理論上音符は無制限に細かくできて、一億分音符なんてのも作ればできるでしょうが、そんなものは現実的にはありえず、よく使われるのは経験的に言ってせいぜい64分音符程度までです(例)。
ついでに三連符が時々使われるので、それを考慮しても192分音符が表現できればもうそれで充分なわけです。
MIDIの分解能は四分音符をどれだけ分けるかで指定するので、実際の分解能はせいぜい48で事足りることになります。
そんなことも考慮しながら、MIDI音符に必要な情報を挙げると、鳴り始める時間・鳴り終わる時間(または持続時間)・音の高さ・音の大きさの4つにまとめることができます。
これらの情報を含み、MIDI作成ソフトとしてこれは必要だろうという機能を加えたクラスを作ると次のようになります。
public class MIDIAPNote { protected int m_start; protected int m_duration; protected int m_note; protected int m_velocity; public MIDIAPNote(int start,int duration,int note,int velocity) { m_start = start; m_duration = duration; m_note = note; m_velocity = velocity; // 念のため情報を表示しておく(完成したら消すけど) System.out.println(toString()); } public ShortMessage getNoteOnMessage(int channel) { ShortMessage msg = new ShortMessage(); try { msg.setMessage(ShortMessage.NOTE_ON, channel, m_note, m_velocity); }catch (InvalidMidiDataException e) { return null; } return msg; } public ShortMessage getNoteOffMessage(int channel) { ShortMessage msg = new ShortMessage(); try { msg.setMessage(ShortMessage.NOTE_OFF, channel, m_note, m_velocity); }catch (InvalidMidiDataException e) { return null; } return msg; } // MIDIファイルに書き出すとき必要 public MidiEvent getNoteOnEvent(int channel) { return new MidiEvent(getNoteOnMessage(channel),m_start); } public MidiEvent getNoteOffEvent(int channel) { return new MidiEvent(getNoteOffMessage(channel),m_start+m_duration); } public int hashCode() { // 同じ場所に同じノートを置くのを防ぐため必要 return (m_start<<7)|(m_note&0x7f); } public int getStart() { return m_start; } public int getDuration() { return m_duration; } public int getEnd() { return m_start+m_duration; } public int getNote() { return m_note; } public String toString() { return "開始:"+m_start+" 長さ:"+m_duration+" 高さ:"+m_note+" 大きさ:"+m_velocity+" ハッシュ:"+hashCode(); } }
トラック(いや、車ぢゃなくて)
使い古されたギャグですみません。
トラックはノートの寄せ集めを一まとめにして管理するものだと考えましょう。
本来トラックとチャンネルは全くの別物なのですが、現実問題として一つのトラックに一つのチャンネルを割り当てるほうが扱いやすいため、トラック番号とチャンネル番号は一致するように作ることにします。
もちろん、寄せ集めを管理する機能も必要です。
public class MIDIAPTrack { int program = 0; TreeSet notes = new TreeSet(new Comparator(){ public int compare(Object o1, Object o2){ return o1.hashCode()-o2.hashCode(); } }); public void generateTrack(Sequence s, int i) { Track track = s.createTrack(); track.add(new MidiEvent(getProgramMessage(i),0)); Iterator it = notes.iterator(); MIDIAPNote note; while (it.hasNext()) { note = (MIDIAPNote)it.next(); track.add(note.getNoteOnEvent(i)); track.add(note.getNoteOffEvent(i)); } } public ShortMessage getProgramMessage(int i) { ShortMessage msg = new ShortMessage(); try { msg.setMessage(ShortMessage.PROGRAM_CHANGE, i, program, 0); }catch (InvalidMidiDataException e) { return null; } return msg; } public void addNote(MIDIAPNote n) { notes.add(n); } }
見てください。TreeSetの初期化にいかにも難しそうな、それでいて便利そうな書き方をして、generateTrackではイテレータなんて高級そうなものを使っています。
私だって伊達に2ヶ月間開発サボってないです。別のプログラム作りながら勉強してたんです。本業の学校の勉強がおろそかになりましたが。
ドキュメントクラス
今回はデータを作るので、ついに今まで全く触れられていなかったドキュメントクラスにも手を出すことになります。
ビュークラスと違い、今回はぶっつけ本番で、同じものを後からも引き続き使うことになります。
とはいえ、実際の作業はほとんどトラックとノートがしてくれているので、実装する機能は今のところMIDIの保存唯一つです。
public class MIDIDocument { MIDIAPTrack tracks[] ={ new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), new MIDIAPTrack(), }; // 強引極まる方法だ // この後CGIに受け渡すことを考えSendというメソッドにしている public void Send() { Sequence seq; try { seq = new Sequence(Sequence.PPQ,48); }catch (InvalidMidiDataException e) { System.out.println("Sequence生成失敗!"); return; } for (int i=0; i<16; i++) { tracks[i].generateTrack(seq,i); } try { MidiSystem.write(seq,1,new File("test.mid")); }catch (IOException e) { System.out.println("Sequence.writeに失敗!"); return; } } }
ビューも手直し
とりあえずダブルクリックで八分音符を置き、右クリックで保存することにしましょう。以下のようなコードになります。
public void mousePressed(MouseEvent e){ 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()[0].noteOn(note,100); }else if (e.getClickCount()==2) { if (e.getX()-50>=0) { applet.doc.tracks[0].addNote(new MIDIAPNote(((e.getX()-50)/24)*24,24,note,100)); applet.repaint(); } } } System.out.println(e.getClickCount()); }else { applet.doc.Send(); } }
要点
- 重複を許さない順序付けされたデータを扱うにはTreeSetが便利そう。
- 順序付けにはComparator。
- TreeSetなどの各要素にアクセスするにはIterator。
例に拠って例の如く例の物。
ピアノロールへの道2
しかし、事はそううまくは運ばない。