Midoを使ってMIDIトラック内の音数を取得する

PythonのライブラリMidoを使って
MIDIトラック内の音数を取得するのが今回の目的です。
(音数が取得できると、歌詞を考えるツールとか作るのに役立ちそうかなと)

必要ライブラリのインポート

まずは、PythonMIDIファイルを扱うために、
Midoをインポートします。

import mido

任意のトラック内に含まれる音数を取得

忙しい人向け

「理屈は良いから、音数を取得するコードが欲しい」という人向けに
今回作成した関数を載せます。(Midoはきちんとインポートしてね)

※動作保証とかはしないので、自己責任で使ってください。

def count_note(midi_file, track_num=0):
    """
    指定トラックの音数を取得する。\n
    なお、指定トラックが存在しない場合はNoneを返す。\n
    Args:
        midi_file : MIDIファイル
        track_num : トラック番号(デフォルト値 0)
    Returns:
        count : 音数
    """
    # MIDIの読み込み
    midi = mido.MidiFile(midi_file)

    # カウンターの初期化
    count = 0

    # 引数の確認
    if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):

        # 指定トラック内の音を数える
        for msg in midi.tracks[track_num]:
            if msg.type == "note_on" and msg.dict()["velocity"] != 0:
                count += 1
    else:
        return None
        
    return count

以下の様に使うことができます。

# MIDIファイル
midi_file = "MIDI_test.mid"

# トラック番号
track_No = 1

# 音数の取得
notes = count_note(midi_file, track_No)
print(notes)

処理内容を知りたい人向け

処理内容を理解してから使いたい人向けに
count_note関数の処理内容を説明します。

MIDI内での音の定義

MIDIファイルは"Messageの集合体"のようなもので、
楽曲情報はMessageとして管理されています。

f:id:tory_0601:20220224230927p:plain
MIDIの構造イメージ

MIDIファイル内で「音を発する」というのは以下のようなMessageになります。

Message('note_on', channel=0, note=74, velocity=102, time=0)

Messageの意味は以下(らしい)です。

  • 'note_on' => このMessageは「音を発する」指令ですよ、という宣言
  • channel => どのチャンネルで鳴らすかを指定(0~15のチャンネルに音色を登録できるらしい)
  • note => 音の高さ(国際表記の場合、note=60 => C4)
  • velocity => 音の強弱(DAWでもvelocityって言うよね)
  • time => 直前のMessageから何tick後に鳴らし始めるか

正直、自分は理解しきってないのですが(音数を数えるだけなら不要だし)
以下の記事で詳しく解説されています。

hjp.hatenablog.com

まぁ、ここでは「'note_on'というMessageがあったら、音を発しているんだな」ってことが
分かっていれば、大体OKです。

音数を数える

ここで勘の良い人は、「じゃあ、音数を知りたい時は'note_on'のMessageを数えれば良いんだな」と気づくと思います。

実際その通りなのですが、一点だけ注意すべきことがあります。

それは、消音(ミュート)を'note_on'のvelocity=0としている場合があることです。

音を発する
Message('note_on', channel=0, note=74, velocity=102, time=0)

消音(普通は'note_off'で宣言する)
Message('note_off', channel=0, note=74, velocity=102, time=480)

まさかの消音
Message('note_on', channel=0, note=74, velocity=0, time=0)

そのため、音数を数える場合はvelocityが0でない'note_on'を数える必要があります。

では、velocity=0の'note_on'に注意しつつ、トラック番号0の音数を
Pythonで取得してみます。

# カウンターの初期化
count = 0

# トラック番号0内のMessage毎にループ
for msg in midi.tracks[0]:

    # 'note_on'でかつvelocity=0でないMessageを判定
    if msg.type == "note_on" and msg.dict()["velocity"] != 0:

        # 音を発するMessageの場合、カウンターを+1する
        count += 1

上記でしれっと使っているけど、Messageに.typeを付けるとメッセージのタイプ、
.dict()をつけると、Messageを辞書型に変換したものが取得できますよ。

トラックが存在しない場合の対処

上記までで、「音数を取得する」機能は完成なんですが、
関数化するにあたって、「存在しないトラック番号を指定された場合」の対応を入れておきます。

# track_numが、INT型で、かつ0 <= track_num < MIDIファイル内のトラック数となっているか判定
if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):
    # 指定トラック番号が問題ない場合の処理
else:
    # 指定トラック番号が不適な場合の処理

ということで、これまでの処理を合わせると以下のようになります。
(忙しい人向けで書いたやつと一緒です)

def count_note(midi_file, track_num=0):
    """
    指定トラックの音数を取得する。\n
    なお、指定トラックが存在しない場合はNoneを返す。\n
    Args:
        midi_file : MIDIファイル
        track_num : トラック番号(デフォルト値 0)
    Returns:
        count : 音数
    """
    # MIDIの読み込み
    midi = mido.MidiFile(midi_file)

    # カウンターの初期化
    count = 0

    # 引数の確認
    if type(track_num) is int and track_num >= 0 and track_num < len(midi.tracks):

        # 指定トラック内の音を数える
        for msg in midi.tracks[track_num]:
            if msg.type == "note_on" and msg.dict()["velocity"] != 0:
                count += 1
    else:
        return None
        
    return count

重ねて書いておきますが、上記関数はご自由にお使い頂いて問題ないですが、
動作保証はしないので、自己責任でお願いします。

今回はこの辺で。