Python3で演奏する電子オルゴール

#はじめに
先日、[『PowerShellで演奏する電子オルゴール』](https://qiita.com/gx3n-inue/items/8ca1d987d58235e7f434)の記事を書きましたが、同様の処理をpython3に移植してみました。

関連記事）
[Golangで演奏する電子オルゴール](https://qiita.com/gx3n-inue/items/4de07dc9e1d90a1cfaa1)

GitHubにて、ソースコードおよびサンプルの楽譜データも公開しています。
[https://github.com/gx3n-inue/py_PlayBox](https://github.com/gx3n-inue/py_PlayBox)

MIDI関連のWin32APIを利用し、python3で楽譜データを演奏させてみます。
音階とノートナンバーの変換表や、楽譜データのフォーマットは、PowerShell版と同じです。

#メインプログラム
###
```python:py_PlayBox.py
#!python.exe

import ctypes
from ctypes import *
import os
import sys
from time import sleep

class myMIDI:
    """MIDI関連関数コール処理用クラス"""
    def __init__(self, initData):
        if sys.maxsize == 2 ** 63 - 1:
            self.initData = c_int64(initData)
            self.MIDI_MAPPER = c_int64(-1)
            self.h = c_uint64(0)
        else:
            self.initData = c_int32(initData)
            self.MIDI_MAPPER = c_int32(-1)
            self.h = c_uint32(0)
        
    def Init(self):
        ctypes.windll.Winmm.midiOutOpen(byref(self.h), self.MIDI_MAPPER, 0, 0, 0)
        ctypes.windll.winmm.midiOutShortMsg(self.h, self.initData)

    def Out(self, outData, length):
        ctypes.windll.winmm.midiOutShortMsg(self.h, outData)
        sleep(length/1000.0)

    def OutOnly(self, outData):
        #print('%x = %x' %(id(self.h), self.h))
        ctypes.windll.winmm.midiOutShortMsg(self.h, outData)

    def Close(self):
        ctypes.windll.winmm.midiOutReset(self.h)


class ScaleDefs:
    """音階定義を格納するクラス"""
    def __init__(self, scale, note):
        self.scale = scale
        self.note = note


class PlayData:
    """楽譜データを格納するクラス"""
    def __init__(self, scale, note, length):
        self.scale = scale
        self.note = note
        self.length = length


def loadDefFile(filename):
    """音階定義ファイルを読み込む"""

    # ファイルをオープンする
    defFile = open(filename, "r")

    # 行ごとにすべて読み込んでリストデータにする
    lines = defFile.readlines()

    # ファイルをクローズする
    defFile.close()

    defs = []
    for temp in lines:
        pos = temp.find("//")

        # コメント開始文字"//"より前を取り出す
        if pos >= 0:
            temp = temp[:pos]

        temp = temp.replace(" ", "").replace("\t", "").rstrip()
        flds = temp.split("=")

        if temp != "":
            defs.append(ScaleDefs(flds[0],flds[1]))

    return defs


def loadPlayFile(filename):
    """楽譜ファイルを読み込む"""

    # ファイルをオープンする
    playFile = open(filename, "r")

    # 行ごとにすべて読み込んでリストデータにする
    lines = playFile.readlines()

    # ファイルをクローズする
    playFile.close()

    pData = []
    for temp in lines:
        pos = temp.find("//")

        # コメント開始文字"//"より前を取り出す
        if pos >= 0:
            temp = temp[:pos]

        temp = temp.replace(" ", "").replace("\t", "").rstrip()
        flds = temp.split("=")

        if temp != "":
            pData.append(PlayData(flds[0], "", flds[1]))

    return pData


def replaceScalt_to_Freq(defs, pData):
    """音階文字列を検索し、ノートナンバーをセットする"""
    for currentData in pData:
        scale = currentData.scale.split(",")
        for temp in scale:
            for currentLen in defs:
                if temp == currentLen.scale:
                    if currentData.note == "":
                        currentData.note = currentLen.note
                    else:
                        currentData.note += "," + currentLen.note
                    break


def main():
    argv = sys.argv
    argc = len(argv)

    if argc < 2:
        #引数の個数チェック
        print('Usage: python %s musicDataFile <timbre>' %argv[0] )
        quit()

    note_number_file = "note-number.dat"
    if not os.path.exists(note_number_file):
        print('%s not found.' %note_number_file)
        quit()

    if argc >= 3:
            timbre = int(argv[2])
    else:
            timbre = 1

    # 音階定義ファイルの読み込み
    defs = loadDefFile(note_number_file)

    # 楽譜ファイルの読み込み
    pData = loadPlayFile(argv[1])

    # ノート番号のセット
    replaceScalt_to_Freq(defs, pData)

    initData = timbre*256 + 0xc0
    pm = myMIDI(initData)
    pm.Init()

    print("\nLoad Done. Play Start!!")

    i = 0
    for currentpData in pData:
        if currentpData.note != "":
            print('[%d] = %s( %s ), %s [ms]' %(i, currentpData.scale, currentpData.note, currentpData.length))
            cnote = currentpData.note.split(",")

            for data in cnote:
                # 鍵盤を押す
                play_on = "0x7f" + data + "90"
                pm.OutOnly(int(play_on, 16))

            sleep(int(currentpData.length) / 1000.0)

            for data in cnote:
                # 鍵盤を離す
                play_off = "0x7f" + data + "80"
                pm.OutOnly(int(play_off, 16))

        else:
            print('[%d] = rest, %s [ms]' %(i, currentpData.length))
            
            # 休符
            sleep(int(currentpData.length) / 1000.0)
        i += 1

    pm.Close()
    print()

if __name__ == "__main__":
    main()
```


#音階データとノート番号の変換表
楽譜データを入力する際に、ドレミ...もしくはdo,re,mi,...というように
音階で入力できるように、音階データとノート番号の変換表を作成しておきます。
###
```config:note-number.dat
// 全角カタカナ表記
ド4=3c
ド#4=3d
レ4=3e
レ#4=3f
ミ4=40
フ4=41
フ#4=42
ソ4=43
ソ#4=44
ラ4=45
ラ#4=46
...
(長いので途中省略）
...
...
re#8=6f
mi8=70
fa8=71
fa#8=72
so8=73
so#8=74
ra8=75
ra#8=76
si8=77

```

#楽譜データの作成
次に、演奏させたい曲の楽譜データを下記の書式で並べていきます。

```config
音階およびオクターブ番号=演奏時間(ms)
```

『こぎつねこんこん』だとこんな感じになります。
ちなみに、同じ長さの音符であれば、和音も鳴らすことができます。
###
```config:PB_kitune.txt
ド4,ド5 = 250
レ4 = 250
ミ4 = 250
フ4 = 250
ソ4,ソ5 = 500
ソ4,ソ5 = 500

ラ4 = 250
フ4 = 250
ド5 = 250
ラ4 = 250
ソ4,ソ5 = 1000

ラ4 = 250
フ4 = 250
ド5 = 250
ラ4 = 250
ソ4,ソ5 = 1000

ソ4 = 250
フ4 = 250
フ4 = 250
フ4 = 250

フ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250
レ4 = 250
ミ4 = 250
レ4 = 250
ド4 = 250
ミ4 = 250
ソ4,ソ5 = 500

ソ4 = 250
フ4 = 250
フ4 = 250
フ4 = 250

フ4 = 250
ミ4 = 250
ミ4 = 250
ミ4 = 250

ミ4 = 250
レ4 = 250
レ4 = 250
ミ4 = 250

ド4,ド5 = 1000
```

#演奏
では早速、演奏してみましょう。
下記の書式でpy_PlayBox.pyを実行します。

```console
D:\py_PlayBox> python py_PlayBox.py
Usage: python py_PlayBox.py musicDataFile <timbre>
```

timbreはMIDIの音色（楽器）の指定です。1～127の数値で指定してください。
以下は、さきほどの『ごぎつねこんこん』(PB_kitune.txt)をハーモニカ(23)で演奏する例です。

```console
D:\py_PlayBox> python py_PlayBox.py PB_kitune.txt 23

Load Done. Play Start!!
[0] =  ド4,ド5 ( 3c,48 ),  250 [ms]
[1] =  レ4 ( 3e ),  250 [ms]
[2] =  ミ4 ( 40 ),  250 [ms]
[3] =  フ4 ( 41 ),  250 [ms]
[4] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[5] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[6] =  ラ4 ( 45 ),  250 [ms]
[7] =  フ4 ( 41 ),  250 [ms]
[8] =  ド5 ( 48 ),  250 [ms]
[9] =  ラ4 ( 45 ),  250 [ms]
[10] =  ソ4,ソ5 ( 43,4f ),  1000 [ms]
[11] =  ラ4 ( 45 ),  250 [ms]
[12] =  フ4 ( 41 ),  250 [ms]
[13] =  ド5 ( 48 ),  250 [ms]
[14] =  ラ4 ( 45 ),  250 [ms]
[15] =  ソ4,ソ5 ( 43,4f ),  1000 [ms]
[16] =  ソ4 ( 43 ),  250 [ms]
[17] =  フ4 ( 41 ),  250 [ms]
[18] =  フ4 ( 41 ),  250 [ms]
[19] =  フ4 ( 41 ),  250 [ms]
[20] =  フ4 ( 41 ),  250 [ms]
[21] =  ミ4 ( 40 ),  250 [ms]
[22] =  ミ4 ( 40 ),  250 [ms]
[23] =  ミ4 ( 40 ),  250 [ms]
[24] =  ミ4 ( 40 ),  250 [ms]
[25] =  レ4 ( 3e ),  250 [ms]
[26] =  ミ4 ( 40 ),  250 [ms]
[27] =  レ4 ( 3e ),  250 [ms]
[28] =  ド4 ( 3c ),  250 [ms]
[29] =  ミ4 ( 40 ),  250 [ms]
[30] =  ソ4,ソ5 ( 43,4f ),  500 [ms]
[31] =  ソ4 ( 43 ),  250 [ms]
[32] =  フ4 ( 41 ),  250 [ms]
[33] =  フ4 ( 41 ),  250 [ms]
[34] =  フ4 ( 41 ),  250 [ms]
[35] =  フ4 ( 41 ),  250 [ms]
[36] =  ミ4 ( 40 ),  250 [ms]
[37] =  ミ4 ( 40 ),  250 [ms]
[38] =  ミ4 ( 40 ),  250 [ms]
[39] =  ミ4 ( 40 ),  250 [ms]
[40] =  レ4 ( 3e ),  250 [ms]
[41] =  レ4 ( 3e ),  250 [ms]
[42] =  ミ4 ( 40 ),  250 [ms]
[43] =  ド4,ド5 ( 3c,48 ),  1000 [ms]


D:\py_PlayBox>
```