CMX API ver.0.21チュートリアル
第2回「MusicXMLから楽譜情報を取得する」

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

はじめに

本連載では,CrestMuseXML (CMX) APIを使ったプログラミングについてチュートリアル形式で紹介しています.第1回目の前回は,CrestMuseXMLおよびCMX APIの簡単な説明およびXerces,Xalanの準備について述べ,ごくごく簡単なコマンドを作ってみました.今回は,MusicXMLドキュメントから楽譜情報を取得するのをやってみます.

MusicXMLの構造

MusicXMLにはpartwiseとtimewiseの2通りの書き方がありますが,CMX APIではpartwiseのみ扱います.MusicXMLの構造を理解するには,Recordareが提供しているチュートリアル(HTML版PDF版)を読むのが一番です.チュートリアルの「"Hello World" in MusicXML」を見るとscore-partwiseというのがトップレベルタグであることが分かります.score-partwiseタグの中にはpart-listタグがあり,その後にpartタグが続きます.partタグ1つが1つの楽器パートを表します.partタグの中にはmeasureタグがあり,これが小節を表します.その中にnoteタグがあり,これが音符1つを表します.noteタグの前にはattributesタグがあり,ここで4/4拍子などの指定をしています.もちろん,partタグを複数書いたり,1つのpartタグの中にmeasureタグを複数書いたり,1つのmeasureタグの中にnoteタグを複数書いたりできます.

MusicXMLドキュメントからnote要素を取得しよう

まず,MusicXMLドキュメントからpart要素の列を取り出すことを考えます.これはMusicXMLWrapperクラスのgetPartListメソッドを使って行えます.

MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
MusicXMLWrapper.Partとは,MusicXMLWrapperクラスの内部クラスとして定義されているPartクラスという意味で,各part要素からの情報の取りだしはこのPartクラスを介して行います.MusicXMLではpart要素が複数現れる可能性がありますので,配列として返されます.パート毎に処理を行いたいときはfor文を用いてループにします.

MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
for (MusicXMLWrapper.Part part : partlist) {
  // パート毎の処理
}
上のコード例ではJava 5.0で導入された拡張for文を利用しています.

part要素の中にはmeasure要素が並んでいるはずです.measure要素を取り出すには,上と同様にgetMeasureListメソッドを用います.ただし,これはPartクラス内で定義されています.


MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
for (MusicXMLWrapper.Part part : partlist) {
  MusicXMLWrapper.Measure[] measurelist = part.getMeasureList();
  for (MusicXMLWrapper.Measure measure : measurelist) {
    // 小節ごとの処理
  }
}

各measure要素に入っているのはnote要素(音符データ),attributes要素(各種属性に関するデータ),direction要素(演奏指示データ)などで,これらはMeasureクラスで定義されているgetMusicDataListメソッドを使って取得できます.


MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
for (MusicXMLWrapper.Part part : partlist) {
  MusicXMLWrapper.Measure[] measurelist = part.getMeasureList();
  for (MusicXMLWrapper.Measure measure : measurelist) {
    MusicXMLWrapper.MusicData[] mdlist = measure.getMusicDataList();
    for (MusicXMLWrapper.MusicData md : mdlist) {
      // 音楽データごとの処理
    }
  }
}
getMusicDataListメソッドで得られるのはMusicXMLWrapper.MusicDataオブジェクトの配列ですが,実際の要素の種類によっては,MusicXMLWrapper.MusicDataのサブクラスが実体だったりします.たとえば,note要素の場合は,MusicXMLWrapper.Noteオブジェクトです.つまり,各々のオブジェクトがnote要素をラップしたものかどうかは次のようにすれば確かめられることになります.

MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
for (MusicXMLWrapper.Part part : partlist) {
  MusicXMLWrapper.Measure[] measurelist = part.getMeasureList();
  for (MusicXMLWrapper.Measure measure : measurelist) {
    MusicXMLWrapper.MusicData[] mdlist = measure.getMusicDataList();
    for (MusicXMLWrapper.MusicData md : mdlist) {
      if (md instanceof MusicXMLWrapper.Note) {
        // note要素だったらここを実行
      } else {
        // note要素でなかったらここを実行
      }
    }
  }
}

例として,得られた音楽データがnote要素かどうかを判断してnote要素だったらその音高を画面に出力するというプログラムを書いてみましょう.


import jp.crestmuse.cmx.commands.*;
import jp.crestmuse.cmx.filewrappers.*;

public class MyCommand2 extends CMXCommand {
  protected void run() {
    MusicXMLWrapper musicxml = (MusicXMLWrapper)indata();
    MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
    for (MusicXMLWrapper.Part part : partlist) {
      MusicXMLWrapper.Measure[] measurelist = part.getMeasureList();
      for (MusicXMLWrapper.Measure measure : measurelist) {
        MusicXMLWrapper.MusicData[] mdlist = measure.getMusicDataList();
        for (MusicXMLWrapper.MusicData md : mdlist) {
          if (md instanceof MusicXMLWrapper.Note)
            System.out.println
              ("Note: " + ((MusicXMLWrapper.Note)md).noteName());
        }
      }
    }
  }

  public static void main(String[] args) {
    MyCommand2 c = new MyCommand2();
    try {
      c.start(args);
    } catch (Exception e) {
      c.showErrorMessage(e);
      System.exit(1);
    }
  }
}
この例では,得られたMusicDataオブジェクトがNoteクラスのインスタンスかどうかを検査し,そうだったらダウンキャストしてnoteNameメソッドを呼び出しています(ちなみに,上のmainメソッドの中のMyCommand1になっているとMyCommand1クラスが実行されるので注意しましょう.僕はたまにそういうミスをします).

note要素から様々なデータを取り出そう

note要素から情報を取得するには,上で試したようにNoteクラスで定義されたメソッドを用います.たとえば,あるnote要素が

  <note>
    <pitch>
      <step>C</step>
      <octave>4</octave>
    </pitch>
    <duration>4</duration>
    <type>whole</type>
  </note>
だとすると,pitch要素の中のstep要素の値はpitchStepメソッド,pitch要素の中のoctave要素の値はpitchOctaveメソッド,duration要素の値はdurationメソッドで取得することができます.duration要素の値はある基準に対して何個分の長さかを表し,基準の長さによって実際の音長は変化します.この基準の長さはattributes要素内のdivisions要素で指定します.直前のdivisions指定を読み取って実際の音長を計算して返すメソッドとしてactualDurationというのが用意されています.現在の仕様では四分音符を1.0としたときの実数値が返されます.

ある音符に対して演奏上の特別な指示をする場合,note要素にnotations要素を作り,そこに記述します.たとえば,フェルマータは次のように記述されます.

  <note>
    <pitch>
      <step>C</step>
      <octave>4</octave>
    </pitch>
    <duration>4</duration>
    <type>quarter</type>
    <notations>
      <fermata type="inverted"/>
    </notations>
  </note>
notations要素へのアクセスはMusicXMLWrapper.Notationsクラスを用います.MusicXMLWrapper.Notationsオブジェクトは,MusicXMLWrapper.NoteクラスにあるgetFirstNotationsメソッドを用います.たとえば,あるMusicXMLWrapper.NoteオブジェクトnoteからNotationsオブジェクトを得るには
MusicXMLWrapper.Notations notations = note.getFirstNotations();
とします.MusicXMLのDTDファイルによると,note要素の中に複数のnotations要素が書けることになっていますが,現バージョンでは最初のnotations要素を取ってくるメソッドのみ用意されています.たとえば,fermata要素のtype属性を取得したいときはfermataTypeメソッドを使用します.fermata要素がないときはnullが返ってきますので,fermataがあるかどうかの検査にも利用できます.

スタッカートもnotations要素の中で書かれます.たとえば,こんな具合です.

  <note>
    <pitch>
      <step>C</step>
      <octave>4</octave>
    </pitch>
    <duration>4</duration>
    <type>quarter</type>
    <notations>
      <articulations>
        <staccato placement="below"/>
      </articulations>
    </notations>
  </note>
NotationsクラスにはhasArticulationというメソッドが用意されており,指定した名前の要素がarticulations要素の子要素として存在するかどうかを調べることができます.この場合,
notations.hasArticulation("staccato")
とすればスタッカートがあるかどうか調べることができます.現状のバージョンでは,staccato要素の属性などの情報を取り出すメソッドは用意されていませんので,もしもそういうのが必要な場合には自分でNotationsクラスを拡張する必要があります.もしもそのような声が多数あったら,次のバージョンアップでそういうメソッドを追加していく予定です.

ここまで駆け足でMusicXMLドキュメントからどのようにして情報を取得するかを説明してきました.MusicXMLドキュメントの各種要素(タグ)から情報を取り出すためのクラスがMusicXMLWrapperクラスの内部クラスとして定義されているので,そのオブジェクトを取得し,適切なメソッドを呼び出す,というのが基本的な考え方になります.上で紹介した以外にも様々なメソッドが用意されていますので,必要に応じてAPIリファレンスドキュメントで調べてください.MusicXMLドキュメントから音楽要素を取得する方法が分かったところで,上のサンプルプログラムの「System.out.println(...)」の部分を書き換えて,様々な音楽要素の取得を試してみましょう.

processNotePartwiseメソッドを用いてみよう

MusicXMLWrapperクラスのメソッド一覧を眺めているとprocessNotePartwiseというのがあるのが分かると思います.このメソッドはNoteHandlerPartwiseというインターフェイスを引数にとっています.NoteHandlerPartwiseインターフェイスを調べてみると,beginPart, endPart, beginMeasure, endMeasure, processMusicDataという5つのメソッドが宣言されています.これはいったい何をするものなのでしょうか.

これは,イベント駆動型プログラミングの考え方を利用して,MusicXMLドキュメントをたどっていく処理の実装を共通化するためのものです.つまり,たとえば各note要素に対してどんな処理を行うかはコマンドによって異なりますが,「まず,MusicXMLWrapper.Part配列オブジェクトを取得して,そのそれぞれの要素に対してMusicXMLWrapper.Measure配列オブジェクトを取得して,さらにそのそれぞれの要素に対してMusicXMLWrapper.MusicData配列オブジェクトを取得し,各々のオブジェクトに対して何らかの処理をする」という「処理の流れ」は,MusicXMLがこういう構造になっている以上,ほとんどの場合共通なはずです.そこで,「処理の流れ」と「処理の内容」を分離し,前者はあらかじめMusicXMLWrapperクラスに実装しておいたのがprocessNotePartwiseメソッドです.

この考え方では,処理の流れはすでに実装されているメソッドが制御しますが,流れのところで行うべき処理の内容は,こちらから指定する必要があります.これを実現するのがNoteHandlerPartwiseインターフェイスです.さきほど,NoteHandlerPartwiseインターフェイスにはbeginPart, endPart, beginMeasure, endMeasure, processMusicDataの5つのメソッドが宣言されていると書きましたが,これらがprocessNotePartwiseメソッドで決まった順序で呼び出されます. つまり,NoteHandlerPartwiseを実装したクラスを作って,そのインスタンスを引数としてprocessNotePartwiseメソッドを実行すれば,処理の流れを記述せずに,規定の流れの中で自分の定義した処理を行うことができます.たとえば,

class MyNoteHandler implements NoteHandlerPartwise {
  public void beginPart(MusicXMLWrapper.Part part, MusicXMLWrapper w) {
      // part要素の開始タグに出会ったときの処理
  }
  public void endPart(MusicXMLWrapper.Part part, MusicXMLWrapper w) {
      // part要素の終了タグに出会ったときの処理
  }
  public void beginMeasure(MusicXMLWrapper.Measure measure, MusicXMLWrapper w) {
      // measure要素の開始タグに出会ったときの処理
  }
  public void endMeasure(MusicXMLWrapper.Measure measure, MusicXMLWrapper w) {
      // measure要素の終了タグに出会ったときの処理
  }
  public void processMusicData(MusicXMLWrapper.MusicData md, MusicXMLWrapper w) {
     // 音楽データを表す要素(measure要素の子要素)に出会ったときの処理
  }
}
というクラスを用意して,
MyNoteHandler h = new MyNoteHandler();
musicxml.processNotePartwise(h);
とすれば,

  MusicXMLWrapper.Part[] partlist = musicxml.getPartList();
  for (MusicXMLWrapper.Part part : partlist) {
    // part要素の開始タグに出会ったときの処理
    MusicXMLWrapper.Measure[] measurelist = part.getMeasureList();
    for (MusicXMLWrapper.Measure measure : measurelist) {
      // measure要素の開始タグに出会ったときの処理
      MusicXMLWrapper.MusicData[] mdlist = measure.getMusicDataList();
      for (MusicData md : mdlist) {
        // 音楽データを表す要素(measure要素の子要素)に出会ったときの処理
      }
      // measure要素の終了タグに出会ったときの処理  
    }
    // part要素の終了タグに出会ったときの処理
  }
と等価な処理を実現できます.これは,Javaの「匿名クラス」という方法を使うと

musicxml.processNotePartwise(new NoteHandlerPartwise() {
  public void beginPart(MusicXMLWrapper.Part part, MusicXMLWrapper w) {
      // part要素の開始タグに出会ったときの処理
  }
  public void endPart(MusicXMLWrapper.Part part, MusicXMLWrapper w) {
      // part要素の終了タグに出会ったときの処理
  }
  public void beginMeasure(MusicXMLWrapper.Measure measure, MusicXMLWrapper w) {
      // measure要素の開始タグに出会ったときの処理
  }
  public void endMeasure(MusicXMLWrapper.Measure measure, MusicXMLWrapper w) {
      // measure要素の終了タグに出会ったときの処理
  }
  public void processMusicData(MusicXMLWrapper.MusicData md, MusicXMLWrapper w) {
     // 音楽データを表す要素(measure要素の子要素)に出会ったときの処理
  }
});
とより簡潔に書くこともできます.

Java 5.0から拡張for文が導入されたので,直接for文を3つネストして書いてもそれほどごちゃごちゃしなくなりましたが,こういう方法もあるというのは知っておいて損はないかと思います.

SAXをご存知の方はSAXに似ていると思われたかもしれません.実際,この仕組みはSAXを参考にしています.CMX APIではDOMを用いて要素の取得や追加を行いますが,将来的にリアルタイム性を要求されるタスクにおいてDOMが利用できず,SAXを利用する必要性が生じたときに,このような枠組みにしておけば,枠組みを変えないまま,SAXベースのXMLプロセッシングも導入できるかもしれないと思い,このような設計になっています.

おわりに

今回は,MusicXMLの基本的な構造と,CMX APIを用いてMusicXMLドキュメントから情報を取得する方法を学びました.次回は,出力すべきXMLドキュメントを生成し,そこに要素を追加していく方法を学びます.