MIDIファイルを生成する

Python3は文字列とかバイト列とかの扱いがすっきりした感じがするので練習がてらStandardMIDIFileを出力させてみた.

:::python
#!/usr/bin/env python3
#-*- coding:utf-8 -*-
import copy
import heapq

class SMF(object):
    '''Standard MIDI File
    '''
    def __init__(self, fmt=1, division=480):
        self.division = division
        self.format = fmt
        self.tracks = []
    def measuretick(self, division):
        return divmod(abs_time, division * 4)
    def abs_time(self, measure, tick):
        return measure * 4 + tick
    def __bytes__(self):
        # header signature
        result = bytearray(b"MThd")
        # header size
        result += (6).to_bytes(4, byteorder='big')
        # format
        result += self.format.to_bytes(2, byteorder='big')
        # number of tracks
        if self.format == 0:
            result += (1).to_bytes(2, byteorder='big')
        elif self.format == 1:
            result += len(self.tracks).to_bytes(2, byteorder='big')
        # time division
        result += self.division.to_bytes(2, byteorder='big')
        if self.format == 0:
            sumtrack = Track()
            for t in self.tracks:
                sumtrack.events += t.events
            result += bytes(sumtrack)
        elif self.format == 1:
            for t in self.tracks:
                result += bytes(t)
        else:
            raise ValueError("format is invalid: %d".format(self.format))
        return bytes(result)

class Track(object):
    '''Track data
    '''
    def __init__(self):
        self.events = []
    def to_vbytes(self, dt):
        '''convert to the variable length format.
        '''
        result = [dt % 0x80]
        dt //= 0x80
        while dt > 0:
            result += [dt % 0x80 | 0x80]
            dt //= 0x80
        return bytes(result[::-1])
    def _block_bytes(self):
        result = bytearray()
        eventheap = copy.deepcopy(self.events)
        heapq.heapify(eventheap)
        t = 0
        runninng = None
        while len(eventheap) > 0:
            e = heapq.heappop(eventheap)
            delta = e.abs_time - t
            t = e.abs_time
            result += self.to_vbytes(delta)
            eventbytes = bytes(e)
            if runninng == eventbytes[0] and runninng != b"\xF0" and runninng != b"\xFF":
                result += eventbytes[1:]
            else:
                result += eventbytes
                runninng = eventbytes[0]
            if e.has_after():
                heapq.heappush(eventheap, e.after())
        # track end
        result += b"\x00\xFF\x2F\x00"
        return result
    def __bytes__(self):
        # track signature
        result = bytearray(b"MTrk")
        blocks = self._block_bytes()
        # block size
        result += len(blocks).to_bytes(4, byteorder="big")
        result += blocks
        return bytes(result)

class Event(object):
    def __init__(self, t=0):
        self.abs_time = t
    def __lt__(self, o):
        return self.abs_time < o.abs_time
    def __le__(self, o):
        return self.abs_time <= o.abs_time
    def __gt__(self, o):
        return self.abs_time > o.abs_time
    def __ge__(self, o):
        return self.abs_time >= o.abs_time
    def has_after(self):
        return hasattr(self, "after")

# SysEx Events

class GMSystemOn(Event):
    def __bytes__(self):
        return b"\xF0\x05\x7E\x7F\x09\x01\xF7"

# Meta Events

class Tempo(Event):
    def __init__(self, tempo=120, t=0):
        Event.__init__(self, t)
        if tempo <= 0:
            tempo = 120
        self.microsec = 60 * 1000000 // tempo
    def __bytes__(self):
        return b"\xFF\x51\x03" + self.microsec.to_bytes(3, "big")

# MIDI Events

class ControllChange(Event):
    def __init__(self, t, ch, d):
        Event.__init__(self, t)
        self.ch = ch
        self.d = d
    def __bytes__(self):
        return bytes([0xB0 | self.ch, self.ccnum, self.d])
class BankSelectMSB(ControllChange):
    ccnum = 0x00
class Modulation(ControllChange):
    ccnum = 0x01
class BreathController(ControllChange):
    ccnum = 0x02
class FootController(ControllChange):
    ccnum = 0x04
class Portamento(ControllChange):
    ccnum = 0x05
class DataEntryMSB(ControllChange):
    ccnum = 0x06
class Volume(ControllChange):
    ccnum = 0x07
class Balance(ControllChange):
    ccnum = 0x08
class Pan(ControllChange):
    ccnum = 0x0a
class Expression(ControllChange):
    ccnum = 0x0b
class Hold(ControllChange):
    ccnum = 0x40
class Portamento(ControllChange):
    ccnum = 0x41
class Sostenuto(ControllChange):
    ccnum = 0x42
class Soft(ControllChange):
    ccnum = 0x43
class Resonance(ControllChange):
    ccnum = 0x47
class Release(ControllChange):
    ccnum = 0x48
class Atack(ControllChange):
    ccnum = 0x49
class CutOff(ControllChange):
    ccnum = 0x4a

class NoteOff(Event):
    def __init__(self, t, ch, n):
        Event.__init__(self, t)
        self.ch = ch
        self.n = n
    def __bytes__(self):
        # use 0x90 for runninng status
        return bytes([0x90 | self.ch, self.n, 0x00])

class NoteOn(Event):
    def __init__(self, t, ch, n, vel, gate):
        Event.__init__(self, t)
        self.ch = ch
        self.n = n
        self.vel = vel
        self.gate = gate
    def __bytes__(self):
        return bytes([0x90 | self.ch, self.n, self.vel])
    def after(self):
        return NoteOff(self.abs_time + self.gate, self.ch, self.n)

class ProgramChange(Event):
    def __init__(self, t, ch, p):
        Event.__init__(self, t)
        self.ch = ch
        self.p = p
    def __bytes__(self):
        return bytes([0xC0 | self.ch, self.p])

import unittest

class SMFTest(unittest.TestCase):
    def test_smf_bytes(self):
        f = SMF()
        result = bytes(f)
        print(result)
        self.assertIsInstance(result, bytes)
        self.assertEqual(result[:4], b"MThd")
        print(int.from_bytes(result[4:8], "big"))
class TrackTest(unittest.TestCase):
    def test_to_vbytes(self):
        t = Track()
        self.assertEqual(t.to_vbytes(0x00000000), b"\x00")
        self.assertEqual(t.to_vbytes(0x00000040), b"\x40")
        self.assertEqual(t.to_vbytes(0x0000007F), b"\x7F")
        self.assertEqual(t.to_vbytes(0x00000080), b"\x81\x00")
        self.assertEqual(t.to_vbytes(0x00002000), b"\xC0\x00")
        self.assertEqual(t.to_vbytes(0x00003FFF), b"\xFF\x7F")
        self.assertEqual(t.to_vbytes(0x00004000), b"\x81\x80\x00")
        self.assertEqual(t.to_vbytes(0x00100000), b"\xC0\x80\x00")
        self.assertEqual(t.to_vbytes(0x001FFFFF), b"\xFF\xFF\x7F")
        self.assertEqual(t.to_vbytes(0x00200000), b"\x81\x80\x80\x00")
        self.assertEqual(t.to_vbytes(0x08000000), b"\xC0\x80\x80\x00")
        self.assertEqual(t.to_vbytes(0x0FFFFFFF), b"\xFF\xFF\xFF\x7F")

if __name__ == '__main__':
#   unittest.main()
    smf = SMF(fmt=1)
    smf.tracks.append(Track())
    smf.tracks[0].events.append(GMSystemOn(0))
    smf.tracks[0].events.append(Tempo(tempo=120, t=40))
    smf.tracks.append(Track())
    ch = 1
    smf.tracks[1].events.append(Volume(40, ch, 96))
    smf.tracks[1].events.append(ProgramChange(60, ch, 0x39))
    smf.tracks[1].events.append(NoteOn(0 + 480 * 4, ch, 0x3C, 127, 480))
    smf.tracks[1].events.append(NoteOn(0 + 480 * 4, ch, 0x44-12, 127, 480))
    smf.tracks[1].events.append(NoteOn(480 + 480 * 4, ch, 0x3E, 127, 480))
    smf.tracks[1].events.append(NoteOn(480 + 480 * 4, ch, 0x45-12, 127, 480))
    print("write to test.mid...")
    with open("test.mid", "wb") as f:
        f.write(bytes(smf))
    print("OK!")

参考