概要
その3までで取得した歌詞、音符情報に基づいてNEUTRINOに入力可能なMusicXMLを生成します。今回で一区切りです。
シリーズ一覧
歌詞付きMIDIをMusicXMLに変換 リンクまとめ
方針
雛形のMusicXMLを用意して、要素を書き換えることで、作成します。
NEUTRINOが受け入れてくれる最低限のMusicXMLを参考に以下のtemplate.musicxmlを作成しました。
ほぼそのままで、<sound tempo="130"/>
をattributeに追加するという変更だけしています。
<?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>
準備
ライブラリのインポート
(その3以前で使ったもので不要なものもあります。)
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
歌詞・音符情報の読み込み
その3で出力した音符と歌詞の対応情報を読み込みます。
with open("measures_with_notes.json") as f:
measures = json.load(f)
templateの読み込み
template.musicxmlから雛形となる要素を抽出します。
以降、前回同様、MIDI2MusicXMLのメソッドとして関数を定義します。
attribute(テンポなどの情報)、note(音符)、rest(休符)、measure(小節)に対応する雛形を用意します。measureはaを順次追加する箱なので、clear()で中身を空にしています。一方、追加される側は、属性等を書き換えて使うので、中身は空にしません。
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の中身を確認します。
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の中身を確認します。
ET.dump(template["rest"])
<note>
<rest />
<duration>1</duration>
</note>
attributeの中身を確認します。
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の中身を確認します。
ET.dump(template["measure"])
<measure />
attribute要素の生成
MIDIから取得したtime_informationの情報に基づいてattribute_xmlを生成する関数を作ります。
@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
試してみます。
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仕様の音階情報に変換
- #の場合はalter要素を追加
- タイの場合はtie要素を追加。tie_typeの要素数だけ追加する
- 歌詞を追加
- 持続時間を追加
# 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]) # シャープの可能性があるので最初の1文字だけ
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を書き換えるだけです。
@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
音符、休符の書き換えを試してみます。
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を追加
@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
実行してみます。
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に存在した冒頭2行(ヘッダー)が消失しています。xmlではコメント扱いの記法のため無視されてしまうのでしょうか。
一応、musicxmlでは存在したほうが良い気がしたので、文字列としてtemplateを読み込んで、冒頭2行だけ取得する関数を作ります。
@staticmethod
def _get_musicxml_header_string(template_path: str
) -> str:
# 最初の2行を取り出す
with open(template_path) as f:
header = "\n".join(f.read().splitlines()[:2])
return header
出力を確認します。
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を指定する必要があることに注意してください。
@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
出力を確認します。
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>
ファイルに書き出します。
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で試した範囲では、その1で紹介した既存Webアプリと比べても、NEUTRINOにわたす前の手動修正がほぼ不要になったので、よかったです(別のMIDIではどうなるかわかりませんし、ケースバイケースと思います)。
本シリーズはここで区切りとしますが、使っていく中でバグ修正の必要等生じたら、適宜更新できればと思います。