歌詞付きMIDIをMusicXMLに変換 その４：MusicXMLへの変換

# 概要
その３までで取得した歌詞、音符情報に基づいてNEUTRINOに入力可能なMusicXMLを生成します。今回で一区切りです。

シリーズ一覧
[歌詞付きMIDIをMusicXMLに変換 リンクまとめ](https://qiita.com/shimajiroxyz/items/a675075b44cf4c0a7487)
# 方針
雛形のMusicXMLを用意して、要素を書き換えることで、作成します。
[NEUTRINOが受け入れてくれる最低限のMusicXML](https://neutrino.tnantoka.com/entry/2020/03/05/230611)を参考に以下のtemplate.musicxmlを作成しました。
ほぼそのままで、`<sound tempo="130"/>`をattributeに追加するという変更だけしています。

```xml:template.musicxml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
  <part>
    <measure>
      <attributes>
        <divisions>4</divisions>
        <key>
          <fifths>0</fifths>
          </key>
        <time>
          <beats>4</beats>
          <beat-type>4</beat-type>
          </time>
        <clef>
          <sign>G</sign>
          <line>2</line>
          </clef>
        <sound tempo="130"/>
        </attributes>
      <note>
        <pitch>
          <step>C</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>ど</text>
          </lyric>
        </note>
      <note>
        <pitch>
          <step>D</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>れ</text>
          </lyric>
        </note>
      <note>
        <pitch>
          <step>E</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>み</text>
          </lyric>
        </note>
      <note>
        <pitch>
          <step>F</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>ふぁ</text>
          </lyric>
        </note>
      </measure>
    <measure>
      <note>
        <pitch>
          <step>G</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>ソ</text>
          </lyric>
        </note>
      <note>
        <pitch>
          <step>A</step>
          <octave>4</octave>
          </pitch>
        <duration>2</duration>
        <lyric>
          <text>ラ</text>
          </lyric>
        </note>
      <note>
        <pitch>
          <step>B</step>
          <octave>4</octave>
          </pitch>
        <duration>1</duration>
        <lyric>
          <text>シ</text>
          </lyric>
        </note>
      <note>
        <rest/>
        <duration>1</duration>
        </note>
      <note>
        <pitch>
          <step>B</step>
          <octave>4</octave>
          </pitch>
        <duration>8</duration>
        <lyric>
          <text>ド</text>
          </lyric>
        </note>
      </measure>
    <measure>
      <note>
        <rest/>
        <duration>16</duration>
        </note>
      </measure>
    <measure>
      <note>
        <rest/>
        <duration>16</duration>
        </note>
      </measure>
    </part>
  </score-partwise>
```

# 準備
## ライブラリのインポート

（その３以前で使ったもので不要なものもあります。）

```Python
import pandas as pd
import re
import jaconv
import copy
import xml.etree.ElementTree as ET
import itertools
from sudachipy import tokenizer
from sudachipy import dictionary
from typing import TypedDict, Union, List, Tuple, Optional, Dict
import json
from pathlib import Path

# XF仕様MIDIを読み込むための独自クラス
from xfmidifile import XFMidiFile
```

## 歌詞・音符情報の読み込み
その３で出力した音符と歌詞の対応情報を読み込みます。

```Python
with open("measures_with_notes.json") as f:
  measures = json.load(f)
```

# templateの読み込み
template.musicxmlから雛形となる要素を抽出します。
以降、前回同様、MIDI2MusicXMLのメソッドとして関数を定義します。
attribute（テンポなどの情報）、note（音符）、rest（休符）、measure（小節）に対応する雛形を用意します。measureはaを順次追加する箱なので、clear()で中身を空にしています。一方、追加される側は、属性等を書き換えて使うので、中身は空にしません。

```Python
class MIDI2MusicXML:
  @staticmethod
  def _get_template_xml(path):
    tree = ET.parse(path)
    root = tree.getroot()
    template = {
      "note": copy.deepcopy(root.find("./part//note"))
      , "rest": copy.deepcopy(root.find(".//note/rest/.."))
      , "attribute": copy.deepcopy(root.find("./part//attributes"))
      , "measure": copy.deepcopy(root.find("./part//measure"))
      , "tree": tree
    }
    # measureの中身を空にする
    template["measure"].clear()

    return template
```

noteの中身を確認します。

```Python
template = MIDI2MusicXML._get_template("template.musicxml")
ET.dump(template["note"])
```

```
<note>
        <pitch>
          <step>C</step>
          <octave>4</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>ど</text>
          </lyric>
        </note>
```

restの中身を確認します。
```Python
ET.dump(template["rest"])
```

```
<note>
        <rest />
        <duration>1</duration>
        </note>
```

attributeの中身を確認します。

```Python
ET.dump(template["attribute"])
```

```      
<attributes>
        <divisions>4</divisions>
        <key>
          <fifths>0</fifths>
          </key>
        <time>
          <beats>4</beats>
          <beat-type>4</beat-type>
          </time>
        <clef>
          <sign>G</sign>
          <line>2</line>
          </clef>
        <sound tempo="130" />
        </attributes>
```

measureの中身を確認します。

```Python
ET.dump(template["measure"])
```

```
<measure />
```

# attribute要素の生成

MIDIから取得したtime_informationの情報に基づいてattribute_xmlを生成する関数を作ります。

```Python  
  @staticmethod
  def _get_attribute_xml(attribute_xml_template, time_information: TimeInformation):
    t = time_information
    attribute = copy.deepcopy(attribute_xml_template)
    # beats_per_minuteを計算
        
    attribute.find("./divisions").text = str(t["notated_32nd_notes_per_beat"])
    """    
    # beat情報があれば更新する
    beats = measure.find(".//attributes//time/beats")
    beattype = measure.find(".//attributes//time/beat-type")
    """
    attribute.find(".//time/beats").text = str(t["numerator"])
    attribute.find(".//time/beat-type").text = str(t["denominator"])
    # beats_per_minuteを計算
    beats_per_minute = int(60/t["tempo"]*1e6)
    attribute.find(".//sound").attrib["tempo"] = str(beats_per_minute)

    return attribute
```

試してみます。

```Python
time_information = measures[0]["time_information"]
attribute = MIDI2MusicXML._get_attribute_xml(template["attribute"], time_information)

ET.dump(attribute)

print("\n\ntime_information")
print(time_information)
```

```
<attributes>
        <divisions>8</divisions>
        <key>
          <fifths>0</fifths>
          </key>
        <time>
          <beats>4</beats>
          <beat-type>4</beat-type>
          </time>
        <clef>
          <sign>G</sign>
          <line>2</line>
          </clef>
        <sound tempo="100" />
        </attributes>
      


time_information
{'numerator': 4, 'denominator': 4, 'ticks_per_beat': 480, 'start_time': 0, 'tempo': 600000, 'notated_32nd_notes_per_beat': 8, 'ticks_per_measure': 1920, 'start_measure_id': 0, 'division_note': 32, 'divisions_per_measure': 32, 'ticks_per_division': 60, 'duration_time': 1920, 'measure_num': 1}
```

ぱっとみで比較がしにくいですが、templateと比べてdivisionが4から8になっていたり、tempoが130から100になっているなど、time_informationの情報にあうようにattributeを書き換えられていることがわかります。

# 音符、休符要素の作成
MIDIから抽出した音符（note_on）に基づいてxmlを生成する関数を作ります。
以下の処理を順次行います。

* pitchをMusicXML仕様の音階情報に変換
    * 参考：[MIDIノート番号と音名、周波数の対応表 - asahi-net.or.jp](https://www.asahi-net.or.jp/~hb9t-ktd/music/Japan/Research/DTM/freq_map.html)
* #の場合はalter要素を追加
* タイの場合はtie要素を追加。tie_typeの要素数だけ追加する
* 歌詞を追加
* 持続時間を追加

```Python
  # MIDIのnote番号をmusicxmlの音階に変換
  @staticmethod
  def _note_to_pitch(note_number: int) -> Tuple[str, int]:
    # 60がC4
    octave = note_number // 12 - 1
    step_num = note_number % 12
    steps = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
    return steps[step_num], octave

  @classmethod
  def _get_note_xml(cls, note_xml_template, note: Note):
    note_xml = copy.deepcopy(note_xml_template)
    step, octave = cls._note_to_pitch(int(note["note"]))
    # octaveは基本0以上のはずだが念の為チェック
    if octave < 0:
      print("warning: octave is too small. reset to 0")
      octave = 0
    if octave > 9:
      print("warning: octave is too large. reset to 9")
      octave = 9
    note_xml.find(".//step").text = str(step[0]) # シャープの可能性があるので最初の１文字だけ
    note_xml.find(".//octave").text = str(octave)
    # シャープの判定
    is_sharp = (len(step) == 2)
    if is_sharp:
      ET.SubElement(note_xml.find(".//pitch"), "alter")
      note_xml.find(".//alter").text = "1"
    # タイの処理
    if "tie_type" in note:
      ties = note["tie_type"]
      for tie in ties:
        ET.SubElement(note_xml, "tie")
        note_xml.find(".//tie").attrib["type"] = tie
    # 歌詞の追加
    #print(note)
    note_xml.find(".//lyric/text").text = note["lyric_pronunciation"]
    # durationの追加
    note_xml.find(".//duration").text = str(note["duration_division"])
    # 追加    
    return note_xml
```

休符を作成するメソッドも定義します。
休符の場合はtemplateのdurationを書き換えるだけです。


```Python
  @staticmethod
  def _get_rest_xml(rest_xml_template, note):
    note_xml = copy.deepcopy(rest_xml_template)
    note_xml.find("./duration").text = str(note["duration_division"])
    return note_xml
```

音符、休符の書き換えを試してみます。

```Python
note = measures[2]["notes"][1] #音符（歌詞は「し」）
rest = measures[2]["notes"][0] #休符

note_xml = MIDI2MusicXML._get_note_xml(template["note"], note)
rest_xml = MIDI2MusicXML._get_rest_xml(template["rest"], rest)

print("note")
ET.dump(note_xml)
print(note) #確認用

print("rest")
ET.dump(rest_xml)
print(rest) #確認用
```

```
note
<note>
        <pitch>
          <step>G</step>
          <octave>5</octave>
          </pitch>
        <duration>4</duration>
        <lyric>
          <text>し</text>
          </lyric>
        </note>
      
{'type': 'note_on', 'start_time': 5280, 'duration_time': 238, 'note': 79, 'start_division': 24, 'duration_division': 4, 'lyric_raw': '沈[し', 'lyric_surface': '沈', 'lyric_pronunciation': 'し'}
rest
<note>
        <rest />
        <duration>24</duration>
        </note>
      
{'type': 'rest', 'duration_division': 24, 'start_division': 0}
```

正しくxmlが生成できていそうです。

# tree全体の生成
measuresの情報に基づきtree全体のxmlを作成する関数を定義します。
以下の流れで処理をしています。

* template_pathからxmlのテンプレートを取得
* template_pathからtree全体を取得。measureを追加する箱であるpart要素を空にする
* measuresの要素ごとにループで処理をする
    * time_informationが直前と異なっていればattribute要素を生成し、measureに追加
    * notesの各要素に処理を適用。typeがnote_onであれば音符、restであれば休符を生成し、measure_xmlに追加
    * ループを終えたらpartのxmlにmeasure_xmlを追加


```Python
  @classmethod
  def _get_musicxml_tree(cls, measures: List[Measure], template_path: str
                        ) -> ET.ElementTree:
    template = cls._get_template_xml(template_path)

    tree = ET.parse(template_path)
    root = tree.getroot()
    # partの中身を空にする
    part = root.find("./part")
    part.clear()

    time_information = None
    for measure in measures:
      measure_xml = copy.deepcopy(template["measure"])
      if measure["time_information"] != time_information:
        time_information = measure["time_information"]
        attribute = cls._get_attribute_xml(template["attribute"], time_information)
        measure_xml.append(attribute)
      
      for note in measure["notes"]:
        if note["type"] == "note_on":
          note_xml = cls._get_note_xml(template["note"], note)
          # 追加
          measure_xml.append(note_xml)
        elif note["type"] == "rest":
          rest_xml = cls._get_rest_xml(template["rest"], note)
          # 追加
          measure_xml.append(rest_xml)
        else:
          # このケースは存在しないはず
          pass
      # partに追加
      part.append(measure_xml)
    return tree
```

実行してみます。

```Python
tree = MIDI2MusicXML._get_musicxml_tree(measures, "template.musicxml")
ET.dump(tree)
```

```
<score-partwise version="3.1">
  <part><measure><attributes>
        <divisions>8</divisions>
        <key>
          <fifths>0</fifths>
          </key>
        <time>
          <beats>4</beats>
          <beat-type>4</beat-type>
          </time>
        <clef>
          <sign>G</sign>
          <line>2</line>
          </clef>
        <sound tempo="100" />
        </attributes>
      <note>
        <rest />
        <duration>32</duration>
        </note>
      </measure><measure><attributes>
        <divisions>8</divisions>
        <key>
          <fifths>0</fifths>
          </key>
...
        <rest />
        <duration>32</duration>
        </note>
      </measure></part></score-partwise>
```

うまくいっているきがします。

# ヘッダーの生成（取得）
treeをdumpするとわかるのですが、template.musicxmlに存在した冒頭２行（ヘッダー）が消失しています。xmlではコメント扱いの記法のため無視されてしまうのでしょうか。
一応、musicxmlでは存在したほうが良い気がしたので、文字列としてtemplateを読み込んで、冒頭２行だけ取得する関数を作ります。

```Python
  @staticmethod
  def _get_musicxml_header_string(template_path: str
                                ) -> str:
    # 最初の２行を取り出す
    with open(template_path) as f:
      header = "\n".join(f.read().splitlines()[:2])
    return header
```

出力を確認します。

```Python
header = MIDI2MusicXML._get_musicxml_header_string("template.musicxml")
print(header)
```

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
```

# musicxml文字列の作成
tree本体とheaderをあわせてmusicxmlの文字列を作成する関数を作ります。
treeはET.tostringで文字列に変更可能です。日本語の場合、encodingを指定する必要があることに注意してください。

```Python
  @classmethod
  def _get_musicxml_string(cls, measures: List[Measure], template_path: str
                          ) -> str:
    tree = cls._get_musicxml_tree(measures, template_path)
    tree_string = ET.tostring(tree.getroot(), encoding="unicode")
    header_string = cls._get_musicxml_header_string(template_path)
    return header_string + "\n" + tree_string

```

出力を確認します。

```Python
musicxml_string = MIDI2MusicXML._get_musicxml_string(measures, "template.musicxml")
print(musicxml_string)
```

```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
  <part><measure><attributes>
        <divisions>8</divisions>
        <key>
          <fifths>0</fifths>
          </key>
        <time>
          <beats>4</beats>
          <beat-type>4</beat-type>
          </time>
        <clef>
          <sign>G</sign>
          <line>2</line>
          </clef>
        <sound tempo="100" />
        </attributes>
      <note>
        <rest />
        <duration>32</duration>
        </note>
      </measure><measure><attributes>
        <divisions>8</divisions>
        <key>
...
        <rest />
        <duration>32</duration>
        </note>
      </measure></part></score-partwise>
```

ファイルに書き出します。

```Python 
with open("yorunikakeru.musicxml", "w") as f:
  f.write(musicxml_string)
```

# NEUTRINOで合成できるかのテスト
出力したmusicxmlがNEUTRINOで生成できるかテストします。
NEUTRINOディレクトリを現在地として、`./score/musicxml`に生成したmusicxmlをおき、`./Run.sh`の対応するパスを書き換え実行します。詳細はNEUTRINO公式を参照してください。

エラーなく完了し、`./output`に出力されたwavファイルを開いて正しく歌えていれば成功です。
可能ならMIDIと同時に再生してみて、テンポが揃っていることも確認してみてください。例えばMacだとGrageBandがこの用途に使えます。

# おわりに
今回まででNEUTRINOが読み込み可能なMusicXMLを作成することができました。
手元のMIDIで試した範囲では、その１で紹介した既存Webアプリと比べても、NEUTRINOにわたす前の手動修正がほぼ不要になったので、よかったです（別のMIDIではどうなるかわかりませんし、ケースバイケースと思います）。
本シリーズはここで区切りとしますが、使っていく中でバグ修正の必要等生じたら、適宜更新できればと思います。