smf2mml.py - SMF を MML に変換

に25日の日付で公開した*1 MML を作るのに使った Python スクリプト置いておきますね。(Python 2.6 用)

from __future__ import division
'''smf2mml.py
SMF are changed into MML file.
'''
# Copyright: This module has been placed in the public domain.
# パブリックドメイン。好きに流用してください。

__author__ = 'kadotanimitsuru'
__credits__ = ''
__date__ = '2008-12-31'
__version__ = '1.0.0'

import sys
import struct

QUANTIZE = 128  # 音長の最小単位
BASE_OCTAVE = 0  # ノートナンバー0 (C音)のオクターブ
OCTAVE_UP = '<'  # オクターブを上げる記号
OCTAVE_DOUN = '>'  # オクターブを下げる記号

PC = {  #GM
    1: ('Acoustic Grand Piano', 21, 108),
    2: ('Bright Acoustic Piano', 21, 108),
    3: ('Electric Grand Piano', 21, 108),
    4: ('Honky-tonk Piano', 21, 108),
    5: ('Electric Piano 1', 28, 103),
    6: ('Electric Piano 2', 28, 103),
    7: ('Harpsichord', 41, 89),
    8: ('Clavi', 36, 96),
    9: ('Celesta', 60, 108),
    10: ('Glockenspiel', 72, 108),
    11: ('Music Box', 60, 84),
    12: ('Vibraphone', 53, 89),
    13: ('Marimba', 48, 84),
    14: ('Xylophone', 65, 96),
    15: ('Tublar Bells', 60, 77),
    16: ('Dulcimer', 60, 84),
    17: ('Drawbar Organ', 36, 96),
    18: ('Percussive Organ', 36, 96),
    19: ('Rock Organ', 36, 96),
    20: ('Church Organ', 21, 108),
    21: ('Reed Organ', 36, 96),
    22: ('Accordion', 53, 89),
    23: ('Harmonica', 60, 84),
    24: ('Tango Accordion', 53, 89),
    25: ('Acoustic Guitar(nylon)', 40, 84),
    26: ('Acoustic Guitar(steel)', 40, 84),
    27: ('Electric Guitar(jazz)', 40, 86),
    28: ('Electric Guitar(clean)', 40, 86),
    29: ('Electric Guitar(muted)', 40, 86),
    30: ('Overdriven Guitar', 40, 86),
    31: ('Distortion Guitar', 40, 86),
    32: ('Guitar Harmonics', 40, 86),
    33: ('Acoustic Bass', 28, 55),
    34: ('Electric Bass(finger)', 28, 55),
    35: ('Electric Bass(pick)', 28, 55),
    36: ('Fretless Bass', 28, 55),
    37: ('Slap Bass 1', 28, 55),
    38: ('Slap Bass 2', 28, 55),
    39: ('Synth Bass 1', 28, 55),
    40: ('Synth Bass 2', 28, 55),
    41: ('Violin', 55, 96),
    42: ('Viola', 48, 84),
    43: ('Cello', 36, 72),
    44: ('Contrabass', 28, 55),
    45: ('Tremolo Strings', 28, 96),
    46: ('Pizzicato Strings', 28, 96),
    47: ('Orchestral Harp', 23, 103),
    48: ('Timpani', 36, 57),
    49: ('String Ensemble 1', 28, 96),
    50: ('String Ensemble 2', 28, 96),
    51: ('Synth Strings 1', 36, 96),
    52: ('Synth Strings 2', 36, 96),
    53: ('Choir Aahs', 48, 79),
    54: ('Voice Oohs', 48, 79),
    55: ('Synth Voice', 48, 84),
    56: ('Orchestra Hit', 48, 72),
    57: ('Trumpet', 58, 94),
    58: ('Trombone', 34, 75),
    59: ('Tuba', 29, 55),
    60: ('Muted Trumpet', 58, 82),
    61: ('French Horn', 41, 77),
    62: ('Brass Section', 36, 96),
    63: ('Synth Brass 1', 36, 96),
    64: ('Synth Brass 2', 36, 96),
    65: ('Soprano Sax', 54, 87),
    66: ('Alto Sax', 49, 80),
    67: ('Tenor Sax', 42, 75),
    68: ('Baritone Sax', 37, 68),
    69: ('Oboe', 58, 91),
    70: ('English Horn', 52, 81),
    71: ('Bassoon', 34, 72),
    72: ('Clarinet', 50, 91),
    73: ('Piccolo', 74, 108),
    74: ('Flute', 60, 96),
    75: ('Recorder', 60, 96),
    76: ('Pan Flute', 60, 96),
    77: ('Blown Bottle', 60, 96),
    78: ('Shakuhachi', 55, 84),
    79: ('Whistle', 60, 96),
    80: ('Ocarina', 60, 84),
    81: ('Lead 1(square)', 21, 108),
    82: ('Lead 2(sawtooth)', 21, 108),
    83: ('Lead 3(calliope)', 36, 96),
    84: ('Lead 4(chiff)', 36, 96),
    85: ('Lead 5(charang)', 36, 96),
    86: ('Lead 6(voice)', 36, 96),
    87: ('Lead 7(fifths)', 36, 96),
    88: ('Lead 8(bass+lead)', 21, 108),
    89: ('Pad 1(new age)', 36, 96),
    90: ('Pad 2(warm)', 36, 96),
    91: ('Pad 3(polysynth)', 36, 96),
    92: ('Pad 4(choir)', 36, 96),
    93: ('Pad 5(bowed)', 36, 96),
    94: ('Pad 6(metallic)', 36, 96),
    95: ('Pad 7(halo)', 36, 96),
    96: ('Pad 8(sweep)', 36, 96),
    97: ('FX 1(ice rain)', 36, 96),
    98: ('FX 2(soundtrack)', 36, 96),
    99: ('FX 3(crystal)', 36, 96),
    100: ('FX 4(atmosphere)', 36, 96),
    101: ('FX 5(brightness)', 36, 96),
    102: ('FX 6(goblins)', 36, 96),
    103: ('FX 7(echoes)', 36, 96),
    104: ('FX 8(sci-fi)', 36, 96),
    105: ('Sitar', 48, 77),
    106: ('Banjo', 48, 84),
    107: ('Shamisen', 50, 79),
    108: ('Koto', 55, 84),
    109: ('Kalimba', 48, 79),
    110: ('Bag Pipe', 36, 77),
    111: ('Fiddle', 55, 96),
    112: ('Shanai', 48, 72),
    113: ('Tinkle Bell', 72, 84),
    114: ('Agogo', 60, 72),
    115: ('Steel Drums', 52, 76),
    116: ('Woodblock', 60, 72),
    117: ('Taiko Drum', 60, 72),
    118: ('Melodic Tom', 60, 72),
    119: ('Synth Drum', 60, 72),
    120: ('Reverse Cymbal', 60, 72),
    121: ('Guitar Fret Noise', 60, 72),
    122: ('Breath Noise', 60, 72),
    123: ('Seashore', 60, 72),
    124: ('Bird Tweet', 60, 72),
    125: ('Telephone Ring', 60, 72),
    126: ('Helicopter', 60, 72),
    127: ('Applause', 60, 72),
    128: ('Gunshot', 60, 72)}

CC = {
    0: 'Bank Select (coarse)',
    1: 'Modulation Wheel (coarse)',
    2: 'Breath controller (coarse)',
    4: 'Foot Pedal (coarse)',
    5: 'Portamento Time (coarse)',
    6: 'Data Entry (coarse)',
    7: 'Volume (coarse)',
    8: 'Balance (coarse)',
    10: 'Pan position (coarse)',
    11: 'Expression (coarse)',
    12: 'Effect Control 1 (coarse)',
    13: 'Effect Control 2 (coarse)',
    16: 'General Purpose Slider 1',
    17: 'General Purpose Slider 2',
    18: 'General Purpose Slider 3',
    19: 'General Purpose Slider 4',
    32: 'Bank Select (fine)',
    33: 'Modulation Wheel (fine)',
    34: 'Breath controller (fine)',
    36: 'Foot Pedal (fine)',
    37: 'Portamento Time (fine)',
    38: 'Data Entry (fine)',
    39: 'Volume (fine)',
    40: 'Balance (fine)',
    42: 'Pan position (fine)',
    43: 'Expression (fine)',
    44: 'Effect Control 1 (fine)',
    45: 'Effect Control 2 (fine)',
    64: 'Hold Pedal (on/off)',
    65: 'Portamento (on/off)',
    66: 'Sustenuto Pedal (on/off)',
    67: 'Soft Pedal (on/off)',
    68: 'Legato Pedal (on/off)',
    69: 'Hold 2 Pedal (on/off)',
    70: 'Sound Variation',
    71: 'Sound Timbre',
    72: 'Sound Release Time',
    73: 'Sound Attack Time',
    74: 'Sound Brightness',
    75: 'Sound Control 6',
    76: 'Sound Control 7',
    77: 'Sound Control 8',
    78: 'Sound Control 9',
    79: 'Sound Control 10',
    80: 'General Purpose Button 1 (on/off)',
    81: 'General Purpose Button 2 (on/off)',
    82: 'General Purpose Button 3 (on/off)',
    83: 'General Purpose Button 4 (on/off)',
    91: 'Effects Level',
    92: 'Tremulo Level',
    93: 'Chorus Level',
    94: 'Celeste Level',
    95: 'Phaser Level',
    96: 'Data Button increment',
    97: 'Data Button decrement',
    98: 'Non-registered Parameter (fine)',
    99: 'Non-registered Parameter (coarse)',
    100: 'Registered Parameter (fine)',
    101: 'Registered Parameter (coarse)',
    120: 'All Sound Off',
    121: 'All Controllers Off',
    122: 'Local Keyboard (on/off)',
    123: 'All Notes Off',
    124: 'Omni Mode Off',
    125: 'Omni Mode On',
    126: 'Mono Operation',
    127: 'Poly Operation'}


def _h(s):
    return ' '.join(['%02X' % ord(x) for x in s])

def i2(data):
    if len(data) != 2:
        raise ValueError(data)
    return struct.unpack('>H', data)[0]
    
def i4(data):
    if len(data) != 4:
        raise ValueError(data)
    return struct.unpack('>I', data)[0]

def delta(data, p):
    i = 0
    while True:
        i <<= 7
        b = ord(data[p])
        p += 1
        if b & 0x80:
            i += b & 0x7f
        else:
            i += b
            break
    return i, p


class Smf(object):

    def __init__(self, data):
        if not data.startswith('MThd'):
            raise ValueError('not smf')
        self.track = []
        self.notes = []
        self.mml = ''
        for chunk_type, chunk in self._get_chunks(data):
            if chunk_type == 'MThd':
                self.Format = i2(chunk[0:2])
                self.NumTracks = i2(chunk[2:4])
                self.Division = struct.unpack('>h', chunk[4:6])[0]
            elif chunk_type == 'MTrk':
                events = self._make_events(chunk)
                self.track.append(events)
                notes = []
                for t, e, note in events:
                    if note is None:
                        continue
                    name, ch, nn, vel = note
                    if name == 'Note ON' and vel == 0:
                        name == 'Note OFF'
                    notes.append((t, name, ch, nn, vel))
                if notes:
                    self.mml += self._make_mml(notes)
                        
    def __str__(self):
        h = 'format: %d\ntrack: %d\ndivision: %d\n' % (
            self.Format, self.NumTracks, self.Division)
        t = '\n'.join(['\nTrk:%d\n%s' % (x[0],'\n'.join(
            ['%05d %s' % (y[0], y[1]) for y in x[1]]
            )) for x in enumerate(self.track)])
        return h + t

    def _make_mml(self, notes):
        n = []
        notenum = None
        for t, name, ch, nn, vel in notes:
            time = int(round(t * QUANTIZE / (4 * self.Division)))
            if name == 'Note ON':
                if notenum == None:
                    n.append((time, 'Note OFF', None, vel))
                n.append((time, 'Note ON', nn, vel))
                notenum = nn
            elif name == 'Note OFF':
                n.append((time, 'Note OFF', nn, vel))
                notenum = None
            else:
                raise ValueError(name)
        now = 0
        music = ['o4']
        mml = []
        o = 4
        note = None
        for time, name, nn, vel in n:
            t = time - now
            if name == 'Note ON':
                if t:
                    mml.append('&'.join(self._mml_note('r', t)))
                note = nn
            elif name == 'Note OFF':
                if t:
                    if note is None:
                        mml.append('&'.join(self._mml_note('r', t)))
                    else:
                        octave = nn // 12 + BASE_OCTAVE
                        if octave > o:
                            mml.append(OCTAVE_UP * (octave - o))
                        elif octave < o:
                            mml.append(OCTAVE_DOUN * (o - octave))
                        o = octave
                        notename = [
                            'c', 'c+', 'd', 'd+', 'e',
                            'f', 'f+', 'g', 'g+', 'a', 'a+', 'b'
                            ][nn % 12]
                        mml.append('&'.join(self._mml_note(notename, t)))
                note = None
            else:
                raise ValueError(name)
            if time // QUANTIZE > now // QUANTIZE:
                music.append(' '.join(mml))
                mml = []
            now = time
        if mml:
            music.append(' '.join(mml))
        return '\n'.join(music) + '\n;\n\n'

    def _mml_note(self, val, length, base=QUANTIZE):
        if base == 1:
            if length != 1:
                raise ValueError((length, base))
            return ['%s%i' % (val, QUANTIZE)]
        elif length == base:
            return ['%s%i' % (val, QUANTIZE // base)]
        elif length > base:
            return ['%s%i' % (val, QUANTIZE // base)] + self._mml_note(
                val, length - base, base)
        elif length < base:
            return self._mml_note(
                val, length, base // 2)            
        else:
            raise ValueError    

    def _make_events(self, data):
        p = t = 0
        events = []
        while p < len(data):
            e, p, t, note = self._get_event(data, p, t)
            events.append((t, e, note))
        return events

    def _get_chunks(self, data):
        p = 0
        chunks = []
        while p < len(data):
            chunk_type = data[p:p+4]
            length = i4(data[p+4:p+8])
            chunks.append((chunk_type, data[p+8:p+8+length]))
            p += (8 + length)
        return chunks

    def _get_event(self, mtrk, pointer, time):
        t, p = delta(mtrk, pointer)
        d = mtrk[p:]
        s = ord(d[0])
        note = None
        if s < 0x80:
            if self._running_status is None:
                raise ValueError('Abnomal running status %x' % s)
            s = self._running_status
            d = chr(s) + d
            p -= 1
        if s == 0xff:  #          メタイベント
            self._running_status = None
            e = ord(d[1])
            l = ord(d[2])
            r = 'Meta Event %02x len:%d' % (ord(d[1]), l)
            p += (3 + l)
            if e == 0x00:  # シーケンス番号
                r += ' Sequence Number %d' % i2(d[3:5])
            elif 0x01 <= e <= 0x09:  # テキスト
                r += ' %s "%s"' % ({
                    1:'Text',
                    2:'Copyright',
                    3:'Sequence/Track Name',
                    4:'Instrument',
                    5:'Lyric',
                    6:'Marker',
                    7:'Cue Point',
                    8:'Program (Patch) Name',
                    9:'Device (Port) Name'
                    }[e], d[3:3+l])
            elif e == 0x2f:  # トラックチャンク終了
                r += ' End of Track'
            elif e == 0x58:  # 拍子Time Signature
                r += ' Time Signature %d/%d' % (ord(d[3]), 2 ** ord(d[4]))
            elif e == 0x51:  # テンポ
                r += ' Tempo %f' % (1000000*60/i4('\x00' + d[3:6]))
            elif e == 0x59:  # 調性Key Signature
                sf = struct.unpack('b', d[3])[0]
                sf = '#' * sf if sf >= 0 else 'b' * -sf
                ml = 'minor' if ord(d[4]) else 'major'
                r += ' Key Signature %s %s' % (sf, ml)
        elif 0x80 <= s <= 0xef:  #  MIDI イベント
            self._running_status = s
            e = s & 0xf0
            ch = s & 0x0f
            if e == 0x80:  # ノートオフ
                nn, vel = ord(d[1]), ord(d[2])
                r = 'Note OFF ch:%2d, nn:%3d, vel:%3d' % (ch, nn, vel)
                p += 3
                note = ('Note OFF', ch, nn, vel)
            elif e == 0x90:  # ノートオン
                nn, vel = ord(d[1]), ord(d[2])
                r = 'Note ON  ch:%2d, nn:%3d, vel:%3d' % (ch, nn, vel)
                p += 3
                note = ('Note ON', ch, nn, vel)
            elif e == 0xa0:  # ポリフォニックキープレッシャー
                r = 'Aftertouch ch:%2d, nn:%3d, vel:%3d' % (
                    ch, ord(d[1]), ord(d[2]))
                p += 3
            elif e == 0xb0:  # コントロールチェンジ
                no = ord(d[1])
                r = 'Control Change Ch:%2d, no:%3d, vv:%3d %s' % (
                    ch, no, ord(d[2]), CC.get(no, 'Undefined'))
                p += 3
            elif e == 0xc0:  # プログラムチェンジ
                prog = ord(d[1])
                r = 'Program Change Ch:%2d, pp:%3d = %s' % (
                    ch, prog, PC[prog + 1][0])
                p += 2
            elif e == 0xd0:  # チャンネルプレッシャー
                r = 'Channel Pressure Ch:%2d, vv:%3d' % (ch, ord(d[1]))
                p += 2
            elif e == 0xe0:  # ピッチベンド
                r = 'Pitch Wheel Ch:%2d, mm:%3d, ll:%3d' % (ch, ord(d[1]), ord(d[2]))
                p += 3
            else:
                raise ValueError('%x %x %s' % (e, s, repr(d[:3])))
        elif s == 0xf0 or s == 0xf7:          # システムエクスクルーシブ
            self._running_status = None
            p += 1
            i, p = delta(mtrk, p)
            ex = _h(mtrk[p:p+i])
            if ex == '7E 7F 09 01 F7':
                ex = 'GM System On'
            elif ex == '7E 7F 09 03 F7':
                ex = 'GM2 System ON'
            elif ex == '41 10 42 12 40 00 7F 00 41 F7':
                ex = 'GS Reset'
            elif ex == '43 10 4C 00 00 7E 00 F7':
                ex = 'XG System ON'
            r = 'SysEx len:%d "%s"' % (i, ex)
            p += i
        else:
            raise ValueError('%x %s' % (s, repr(d[:3])))
        return r, p, time + t, note


def smf2mml(smfname, txtname):
    f = open(smfname, 'rb')
    m = f.read()
    f.close()
    s = Smf(m)
    print 'format: %d, track: %d, division: %d' % (s.Format, s.NumTracks, s.Division)
    f = open(txtname, 'w')
    f.write('%s/* MML end */\n\n%s' % (s.mml, str(s)))
    f.close()


##f = 'siaosiao20081225.mid'
##savename = f + '.txt'
##smf2mml(f, savename)
##print u'MML file "%s" was created.' % savename


if __name__ == '__main__':
    if len(sys.argv) > 1:
        for f in sys.argv[1:]:
            f = unicode(f, sys.getfilesystemencoding())
            savename = f + '.txt'
            smf2mml(f, savename)
            print u'MML file "%s" was created.' % savename
    else:
        import Tkinter
        import tkFileDialog
        import tkMessageBox
        root = Tkinter.Tk()
        root.withdraw() 
        f = tkFileDialog.askopenfilename(
            title=u'MML にする MIDI ファイルを選んでください',
            filetypes=[(u'MIDIファイル', '*.mid;*.smf')])
        if f:
            print f
            savename = f + '.txt'
            smf2mml(f, savename)
            tkMessageBox.showinfo(
                'smf2mml',
                u'"%s" を作りました。' % savename)

使い方は、とりあえず実行してみれば分かるはず。

1トラックにつき 1ch のモノフォニックな(発音が重なっていない)ものにしか対応していません。そして出てくる MML は音階とその長さのみで、テンポやら音色, 音量やらには一切関知しないのであしからず。あと(多分)ちゃんとノート・オンしてノート・オフするやつでないと誤動作しそう。リズムパートにも未対応。

要するに出来合いの MIDI ファイルを変換するのには使い物にならず、これ用に(Domino*2 なりで)自作したものを変換するのになら使える、程度の代物です。

公式

からもリンクのある mid2flmml*3が Domino 使って作ったあの MIDI ファイルではうまく動いてくれないので、むかし

作った MIDI ファイル内容表示プログラムを改造して自作してしまいました。うーん車輪の再発明

出自がそんなプログラムなので MML を作る用途としては無駄にオーバースペック。せっかくなので MML の後にその元のプログラムで出力していた内容表示を続けて出力するようにしてありますから、MIDI の中身を見るのにも使えますね。(ただの手抜きの副産物だけど)

ところで

このはてなメロディ再生記法ですが、最新版の FlMML でテストしていたのと比べると鳴るのがえらく違うんですが。とりあえずテンポ(T)と音量(V)の動作はおかしい感じ。いつのバージョンのままメンテナンスされてないのやら。

関連
MML も貼れる - つちのこ、のこのこ。(はてな番外地)