CMX API ver.0.21チュートリアル
第3回「XMLドキュメントを生成する,SMFを生成する」

北原 鉄朗(JST CrestMuse/関西学院大学理工学研究科)
2007年9月14日

はじめに

前回は,既存のMusicXMLドキュメントを読んでそこから情報を取得する方法を学びました.今回は,新たにXMLドキュメントを生成する方法について学びましょう.

CMX APIにおけるXMLドキュメントの生成方法

CMX APIでは,既存のドキュメントに対するアクセスと新たなドキュメントの生成(および要素の追加)は完全に別のメソッド群を用いて行います.たとえば,MusicXMLドキュメントからpart要素の情報を取り出すためのMusicXMLWrapper.Partクラスやnote要素の情報を取り出すためのMusicXMLWrapper.Noteクラスには,それらに新たな要素を追加するメソッドは用意されていません.このように情報を「読む」ことしかできないことをイミュータブルであるといいます.ただし,これらのクラスがラップする要素のDOM表現であるNodeオブジェクトを取得できてしまう(現状では便宜上そうしてある)ので,完全なイミュータブルにはなっていません.この取得を許すかどうかは今後の議論を経て決めていく予定です.

XMLドキュメントの生成はCMXFileWrapperクラスのcreateDocumentメソッドを用いて行います.このメソッドには生成したいXMLフォーマットのトップレベルタグ名を引数として渡します.これにより,どのファイルラッパクラスを生成すべきかを自動的に判別して,適切なファイルラッパクラスを生成します.

createDocumentメソッドで空のドキュメントを生成したら,そこに次々と要素を追加していきます.CMXFileWrapperでは要素の追加用に以下のメソッドが用意されています.

ちなみに,addChildとaddSiblingでは付与された要素が新たなカレント要素になります.

もう少しわかりやすく具体例をあげて見ていきましょう.まず,createDocumentメソッドを使って空ドキュメントを生成したとします.ここではわかりやすくするため,架空のXMLフォーマットを想定して説明をしていきます.いま,"my-address-book-xml"というトップレベルタグを持つXMLフォーマットを考えてみましょう.空ドキュメントはこのようにして生成できます.

CMXFileWrapper f = CMXFileWrapper.createDocument("my-address-book-xml");
ただし,このXMLフォーマットに対応するファイルラッパクラスがないとファイルラッパオブジェクトを生成できませんので,現状でこのままこれを実行してもエラーになります.あくまで説明のためと思って試さずに読みつづけてみてください(ちなみに,自分で作ったXMLフォーマットのための自前のファイルラッパクラスを登録する方法もちゃんと用意されています.今回はそれは省略します).この時点では
  <my-address-book-xml></my-address-book-xml>
となり,my-address-book-xml要素がカレント要素になります.現在の要素にperson要素を子要素として付与しましょう.次のようにします.
f.addChild("person");
そうすると,XMLドキュメントは
  <my-address-book-xml>
    <person></person>
  <my-address-book-xml>
となり,person要素がカレント要素に変わります.次にperson要素の子要素としてname要素を付与してみましょう.
f.addChild("name");
XMLドキュメントは
  <my-address-book-xml>
    <person>
      <name></name>
    </person>
  </my-address-book-xml>
となり,name要素がカレント要素になります.name要素にテキストを付与してみましょう.
f.addText("Tetsuro Kitahara");
XMLドキュメントは
  <my-address-book-xml>
    <person>
      <name>Tetsuro Kitahara</name>
    </person>
  </my-address-book-xml>
となります.カレント要素はname要素のままです.次に,name要素の下に(弟として)address要素を付与してみましょう.カレント要素がname要素のまま
f.addSibling("address");
とするか,
f.returnToParent();
として一旦カレント要素をperson要素に戻してから f.addChild("address"); とします.続けて
f.addText("XX, YY, Osaka, Japan");
とすれば
  <my-address-book-xml>
    <person>
      <name>Tetsuro Kitahara</name>
      <address>XX, YY, Osaka, Japan</address>
    </person>
  </my-address-book-xml>
となります.このようにして次々と要素を加えていきます.ちなみに,
f.addChild("name");
f.addText("Tetsuro Kitahara");
f.returnToParent();
の一連の処理は
f.addChildAndText("name", "Tetsuro Kitahara");
と簡潔に書くこともできます.

以上が,CMX APIでXMLドキュメントを生成する基本的な方法です.

MIDI XMLドキュメントを生成する

基本的には以上の方法を使えばどんなXMLドキュメントでも生成できますが,ちょっと面倒です.それぞれのXMLフォーマットには決められた書き方があるわけですから,ある程度のことは自動でやってほしいわけです.ですので,おおかたのファイルラッパクラスでは上で述べたメソッドを使って要素を付与するための,もう少し使い易いメソッドが用意されています.ここでは,MIDI XMLを例にあげて説明していきたいと思います.

第1回でも述べましたように,MIDI XMLはSMFをそのままXML化したものです.いま,次のMIDI XMLドキュメントを生成してみましょう.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE MIDIFile PUBLIC "-//Recordare//DTD MusicXML 1.1 MIDI//EN"
                          "http://www.musicxml.org/dtds/midixml.dtd">
<MIDIFile>
  <Format>0</Format>
  <TrackCount>1</TrackCount>
  <TicksPerBeat>480</TicksPerBeat>
  <TimestampType>Delta</TimestampType>
  <Track Number="1">
    <Event>
      <Delta>480</Delta>
      <NoteOn Channel="1" Note="60" Velocity="100"/>
    </Event>
    <Event>
      <Delta>480</Delta>
      <NoteOff Channel="1" Note="60" Velocity="100"/>
    </Event>
    <Event>
      <Delta>1920</Delta>
      <EndOfTrack/>
    </Event>
  </Track>
</MIDIFile>
ここで,Format要素はSMFのフォーマットを表します.SMFのフォーマットには0, 1, 2の3種類ありますが,ほとんどのソフトウェアでは0と1しか対応していません.CMX APIでも0と1だけ対応しています.フォーマット0というのはトラックを1つしか含むことができないもの,フォーマット1はトラックを複数含むことができるものです.TrackCountはトラック数,TicksPerBeatは四分音符あたりのティック数,TimestampTypeは時間表記の方法でMIDI XMLではDeltaの他にAbsoluteが許されていますが,CMX APIではDeltaしか対応していません.あとは見ればだいたいわかると思いますが,トラックが1つあり,その中で480ティック(四分音符1個分)してからノートナンバー60(中央のド)のNoteOnメッセージがあり,さらにその480ティック後にNoteOffメッセージがあります.最後に4拍分余裕を持たせた後にEndOfTrackがあります.

では,早速このMIDI XMLドキュメントを作成してみましょう.まず,MIDIXMLWrapperオブジェクトを生成します.

MIDIXMLWrapper midixml 
  = (MIDIXMLWrapper)CMXFileWrapper.createDocument(MIDIXMLWrapper.TOP_TAG);
つぎに,Format, TrackCount, TicksPerBeat, TimestampType要素を追加します.これを行うためのメソッドとして が用意されています.文字どおり前者はフォーマット0,後者はフォーマット1で用いるものです.フォーマット0ではTrackCountは固定で,TimestampTypeはCMX APIではDeltaしか扱いませんので,addElementsFirstForFormat0メソッドではTicksPerBeatだけ引数で指定します.フォーマット1ではTrackCountとTicksPerBeatを指定します.ここではフォーマット0でTicksPerBeatを480にするので
midixml.addElementsFirstForFormat0(480);
とします.次にTrack要素を追加します.トラック番号である1を引数で指定して
midixml.newTrack(1);
とします.なお,newTrackメソッドはaddElementsFirstForFormat0かaddElementsFirstForFormat1が呼び出された後でないとエラーが出るようになっており,安全にXMLドキュメントを生成することができるようになっています.次にNoteOnメッセージを付与してみましょう.次のようにします.
midixml.addMIDIChannelMessage("NoteOn", 480, (byte)1, 60, 100);
同様にNoteOffメッセージも付与してみましょう.
midixml.addMIDIChannelMessage("NoteOff", 480, (byte)1, 60, 100);
最後にトラックを閉じます.
midixml.endTrack();
現在の実装ではEndOfTrackメッセージは自動で付与されるようになっています.EndOfTrackメッセージを自動で付与すべきかどうかは議論の余地があるところかもしれません.

以上で完成です.下に完全に動くサンプルプログラムを示しますので,ぜひ試してみてください.


import jp.crestmuse.cmx.filewrappers.*;

public class MyMIDIXMLTest {
  public static void main(String[] args) {
    try {
      MIDIXMLWrapper midixml = (MIDIXMLWrapper)
          CMXFileWrapper.createDocument(MIDIXMLWrapper.TOP_TAG);
      midixml.addElementsFirstForFormat0(480);
      midixml.newTrack(1);
      midixml.addMIDIChannelMessage("NoteOn", 480, (byte)1, 60, 100);
      midixml.addMIDIChannelMessage("NoteOff", 480, (byte)1, 60, 100);
      midixml.endTrack();
      midixml.finalizeDocument();
      midixml.write(System.out);
      midixml.writefileAsSMF("test.mid");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
上でのサンプルプログラムではmidixml.write(System.out)とすることで結果を画面表示しています.また,midixml.writefileAsSMF("test.mid")とすることで"test.mid"という名前のSMFとして保存しています.

自由自在にSMFを生成できるようになったところで,いろいろなSMFを生成して遊んでみましょう.

ちなみに,現時点でSMFのすべてに対応できているわけではありません.エクスクルーシブメッセージやRPN,NRPNなど未対応の部分はいろいろあります.詳細な説明は省略しますが,どこが対応できていないか知りたい方はお気軽にお問い合わせください.

おわりに

前回までは既存のXMLドキュメントからの情報の取りだし方を学んだのに対して,今回は新たにXMLドキュメントを生成する方法について学びました.次回は,(Renconエントリー希望者には最も知りたいはずの)DeviaionInstanceXMLドキュメントの生成方法について学んでいきます.