LoginSignup
8
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2018-07-04

はじめに

先日、『PowerShellで演奏する電子オルゴール』の記事を書きましたが、同様の処理をpython3に移植してみました。

関連記事)
Golangで演奏する電子オルゴール

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

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

メインプログラム

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,...というように
音階で入力できるように、音階データとノート番号の変換表を作成しておきます。

note-number.dat
// 全角カタカナ表記
ド4=3c#4=3d
4=3e#4=3f
4=404=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

楽譜データの作成

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

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

『こぎつねこんこん』だとこんな感じになります。
ちなみに、同じ長さの音符であれば、和音も鳴らすことができます。

PB_kitune.txt
4,ド5 = 2504 = 2504 = 2504 = 2504,ソ5 = 5004,ソ5 = 5004 = 2504 = 2505 = 2504 = 2504,ソ5 = 10004 = 2504 = 2505 = 2504 = 2504,ソ5 = 10004 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504,ソ5 = 5004 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504 = 2504,ド5 = 1000

演奏

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

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

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

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>
8
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7