概要
前回まで作成した関数をブラウザから実行できるようにします。
下図のようなUIで、MIDIファイル(XF仕様)をアップロードしたら、MusicXMLをダウンロードできるようにすることを目指します。
シリーズ一覧
歌詞付きMIDIをMusicXMLに変換 リンクまとめ
方針
Flaskで作っていきます。
参考にさせていただいた記事は末尾にまとめます。
フォルダ構成
以下のようにします。
server.pyとtepmlatesフォルダが今回新規に作成したものです。XFMidiFile.pyは「その1」、MIDI2MusicXML.py、template.musicxmlは「その4」を作成したものです。
./
├── MIDI2MusicXML.py
├── XFMidiFile.py
├── template.musicxml
├── server.py
└── templates
├── flask_api_index.html
└── layout.html
インストール
pip install flask
server.py
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, make_response
from midi2musicxml import MIDI2MusicXML
from pathlib import Path
from werkzeug.utils import secure_filename
MUSICXML_MIMETYPE = "application/vnd.recordare.musicxml"
app = Flask(__name__)
@app.route('/')
def index():
return render_template('./flask_api_index.html')
@app.route('/result', methods=['POST'])
def result():
# submitした画像が存在したら処理する
if request.files['midi']:
stream = request.files["midi"].stream
filename = request.files["midi"].filename
filename = secure_filename(filename)
# 画像の読み込み
musicxml_string = MIDI2MusicXML.exec(stream
, template_musicxml_path="template.musicxml"
, save=False)
if musicxml_string:
response = make_response()
response.data = musicxml_string
downloadFileName = '{}_from_midi.musicxml'.format(Path(filename).stem)
response.headers['Content-Disposition'] = 'attachment; filename=' + downloadFileName
response.mimetype = MUSICXML_MIMETYPE
return response
if __name__ == '__main__':
app.debug = True
app.run(host='localhost', port=5000)
stream = request.files["midi"].stream
で取得したファイルストリームをMIDI2MusicXML.exec
に渡しています。MIDI2MusicXML側はファイルストリームを受け取れるように、「その4」から一部変更しています(後述)
またmake_response
で作成したmusicxmlをダウンロードさせるようにしています。
MIDI2MusicXML.pyとXFMidiFile.pyの改修
MIDI2MusicXML.execはもともとファイルパス(str)を受け取る関数でしたが、Webアプリ化するにあたってファイルストリームを直接受け取れたほうがよいので、少し変更しています。
変更後のexec全体は以下です。
@classmethod
def exec(self, filepath, *
, template_musicxml_path = "template.musicxml"
, outdir = None
, save=True):
has_xfkm_chunk = XFMidiFile.has_xfkm_chunk(filepath)
# もしxfkm_chunkがなければ解析不可
if not has_xfkm_chunk:
print("{} is not xf format".format(filepath))
return
# midiのタイプが0じゃなかったら解析不可とする処理を書きたい
"""todo"""
# 楽曲情報を取得
_, _, _, lang = XFMidiFile.get_xflyricinfo(filepath)
# JPの場合、文字コードを指定
if lang == "JP":
if type(filepath) is str:
xfmidi = XFMidiFile(filepath, charset="cp932")
else:
xfmidi = XFMidiFile(None, filepath, charset="cp932")
else:
if type(filepath) is str:
xfmidi = XFMidiFile(filepath)
else:
xfmidi = XFMidiFile(None,filepath)
self.xfmidi = xfmidi
# trackをdictlistに変換
lyrics = [v.dict() for v in xfmidi.xfkm]
messages = [v.dict() for v in xfmidi.tracks[0]]
# 開始時間を付与
messages = self._add_start_time(messages)
# 開始時間を付与
lyrics = self._add_start_time(lyrics)
# 楽曲情報を取得
_, melody_channel, _, _ = XFMidiFile.get_xflyricinfo(filepath)
melody_channel = int(melody_channel) - 1 # melody_channelのindexが1ずれている。ヤマハだけ?
# melody_channelのnote_on, note_offだけを抽出
note_ons = [(v["note"], v["start_time"]) for v in messages if v["type"] == "note_on" and v.get("channel", -1) == melody_channel]
note_offs = [(v["note"], v["start_time"]) for v in messages if v["type"] == "note_off" and v.get("channel", -1) == melody_channel]
duration_times = self._get_duration_times(note_ons, note_offs)
notes: List[Note] = []
for (pitch, start_time), duration_time in zip(note_ons, duration_times):
note: Note = {
"type": "note_on"
, "start_time": start_time
, "duration_time": duration_time
, "note": pitch
}
notes.append(note)
# time_informationを取得
time_informations = self._get_time_informations(messages)
# measureのリストを取得
measures = self._get_measures(time_informations)
# noteをmeasure単位にsplit
measure_end_times = [m["start_time"]+m["time_information"]["ticks_per_measure"] for m in measures]
note_measures = self._split_notes_by_measure(notes, measure_end_times)
#print(note_measures)
# タイの情報を追加
measure_end_times = [(m["start_time"]+m["time_information"]["ticks_per_measure"]) for m in measures]
note_measures = self._add_tie(note_measures, measure_end_times)
# noteにmeasure起点のdivisionと持続divisionを追加
measure_start_times = [m["start_time"] for m in measures]
measure_ticks_per_divisions = [m["time_information"]["ticks_per_division"] for m in measures]
note_measures = self._add_division(note_measures, measure_start_times, measure_ticks_per_divisions)
# 休符の追加
divisions_per_measures = [m["time_information"]["divisions_per_measure"] for m in measures]
note_measures = [self._add_rest(notes, divisions_per_measure) for notes, divisions_per_measure in zip(note_measures, divisions_per_measures)]
# start_time、歌詞をkey, valueとする辞書を作成
"""todo: start_timeのかぶりは本当にない?"""
lyric_dict = {v["start_time"]: v["text"] for v in lyrics if v["type"] == "lyrics"}
# noteと歌詞を対応付け
"""todo
・noteとlyricのタイムスタンプがずれているときの処理(今はnoteとlyricのタイムスタンプが厳密に一致している前提)
"""
note_measures = [[self._add_lyric(note, lyric_dict) for note in notes] for notes in note_measures]
# 「は」、「へ」を「わ」「え」になおす
fixed_pronunciations = self._get_fixed_pronunciations(note_measures)
for pronunciation, measure_id, note_id in fixed_pronunciations:
note_measures[measure_id][note_id]["lyric_pronunciation"] = pronunciation
# lyricが対応しないnoteの処理。直前にnoteがあれば長音、なければ「あ」とする
"""todo
・直前の文字が「ん」のときや、ないときも同じ処理で良い?
"""
note_measures = self._add_rule_base_lyric(note_measures)
#outputjson(note_measures, "note_measures_with_rule_base_lyric.json")
# lyricが2文字の要素の処理。
"""todo
・durationの分割の最小単位を判定(たぶん32分音符まで?)
"""
# measuresのnoteをセット
temp = []
for notes, measure in zip(note_measures, measures):
measure["notes"] = notes
temp.append(measure)
measures = temp
self.note_measures = note_measures
self.measures = measures
musicxml_string = self._get_musicxml_string(measures, template_musicxml_path)
if save:
outdir = outdir or self._get_outdir(filepath)
outfile = Path(outdir).joinpath("from_midi.musicxml")
with open(outfile, "w") as f:
f.write(musicxml_string)
return musicxml_string
主な変更点は以下です。
- 引数にsaveを追加し、Falseのときはファイルを保存しないようにする。(WebアプリではMusicXMLをサーバ側で保存する必要がないため)
- filepathのタイプがstrとそれ以外のときで処理を分岐。strでないとき=ファイルストリームのときはXFMidiFileの第1引数をNone、第2引数にfilepathを渡すことで、ファイルストリームとして処理できる。XFMidiFileの継承元のmido.MidiFileがファイルストリームを受け取る機能をそのまま使っているだけ。
またXFMidiFileも以下を改修します。
@staticmethod
def has_xfkm_chunk(filepath):
if type(filepath) is str:
with open(filepath, "rb") as f:
buf = f.read()
else:
start_pos = filepath.tell()
buf = filepath.read()
filepath.seek(start_pos)
# XF Karaoke Message チャンクの取得
chunk_type_bytes = b'XFKM'
return chunk_type_bytes in buf
...
@staticmethod
def get_xflyricinfo(filepath):
if type(filepath) is str:
with open(filepath, "rb") as f:
buf = f.read()
else:
start_pos = filepath.tell()
buf = filepath.read()
filepath.seek(start_pos)
# XF Karaoke Message チャンクの取得
chunk_type_bytes = b'XFKM'
if chunk_type_bytes not in buf:
return (None, None, None, None)
index = buf.index(chunk_type_bytes)
xfkm_bytes = buf[index:]
information_header_index = xfkm_bytes.index(b'\xff\x07')
information_header_data_length = xfkm_bytes[information_header_index+2] # FF 07 len textという構成
information_header = xfkm_bytes[information_header_index: information_header_index+3+information_header_data_length]
# 情報を取得(日本語はないはずなので文字コードは気にせずdecodeしてsplit)
id, melody_channel, offset, lang = information_header[3:].decode().split(":")
return id, melody_channel, offset, lang
どちらも、MIDI2MusicXML.execで使われる関数であるため、filepathがストリームの場合も処理できるように変更しています。
flask_api_index.html
受け取るファイルのタイプをmidiにしています。
{% extends "layout.html" %}
{% block content %}
<form action="/result" method="post" enctype="multipart/form-data">
<input type="file" name="midi" accept="audio/midi">
<button type="submit">Submit</button>
</form>
{% endblock %}
layout.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API Sample</title>
</head>
<body>
{% block content %}
<!-- ここに内容 -->
{% endblock %}
</body>
</html>
実行
python server.py
でサーバを起動後、127.0.0.1:5000
にアクセスします(自環境(Mac)ではなぜかlocalhost:5000ではだめで、127.0.0.1だといけました)
適当なXF仕様のMIDIをアップロードして「Submit」を押下するとファイルがダウンロードされます。中身が正しそうなMusicXMLであれば成功です。
おわりに
無事、ブラウザからMIDIファイルをMusicXMLに変換できるようになりました。
Flask、簡単でいいですね。
参考記事
-
Flaskで画像分類Webアプリを作成する(Mobile Net)
- フォルダ構成とserver.py、flask_api_index.html、layout.htmlの中身はほぼそのまま使わせていただきました。
-
Flaskでファイルダウンロードを実現する3つの方法
- make_responseを使ったダウンロード機能の実装の参考にさせていただきました。
-
ファイルのアップロードUploading Files | Flask docs
- secure_filenameの利用方法を参考にしました
-
MIMEタイプ一覧
- midiのMIMEタイプを調べるために見ました。