majik
majik

Reputation: 138

How to write Midi File from scratch using python

I'm studying the Midi file specification, right now I'm testing this, which works fine if played by Timidity but it's corrupted for either Garage Band, OS X(The output doesn't play) and Synthesia.

head = '4d 54 68 64' 
chunklen = '00 00 00 06'
mformat = '00 01' 
ntracks = '00 02' 
tickdiv = '00 60'
trackid = '4d 54 72 6b'
eot = '00 ff 2f 00'

makeheader = lambda : " ".join([head,chunklen,mformat,ntracks,tickdiv])

def chunklencalc(notes):
    chlen = format(len(notes)*4, 'x')
    return " ".join([x for x in re.compile('(.{2})').split("00000000"[len(chlen):] + chlen) if x != ''])

maketrack = lambda notes : " ".join([trackid, chunklencalc(notes)] + notes + [eot])

makestandardquarter = lambda root : f"00 90 {root} 64 60 80 {root} 64"

def createMidi(filename,bytelist):
    with open(filename, 'wb') as f:
        for e in bytelist.split(" "):
            f.write(bytes.fromhex(e))


filename = 'firsttest.mid'
head = makeheader()
notes1 =[
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
]
notes2 =[
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
]
track1 = maketrack(notes1)
track2 = maketrack(notes2)

createMidi(filename, " ".join([head, track1,track2]))

I Expected a series of quarters in two tracks, got only the first four on only one track.

Upvotes: 2

Views: 516

Answers (1)

Joac
Joac

Reputation: 490

After taking a deep look with hexdump, and looking at the defined chunk lengths: first chunk declares to be 0x20(32) bytes long, starting at position 0x17 (23) and ends at 0x5b (91) that means that your chunk lenght calculations are off by 34 bytes.

00000000  4d 54 68 64 00 00 00 06  00 01 00 02 00 60 4d 54  |MThd.........`MT|
00000010  72 6b 00 00 00 20 00 90  3c 64 60 80 3c 64 00 90  |rk... ..<d`.<d..|
00000020  3c 64 60 80 3c 64 00 90  3c 64 60 80 3c 64 00 90  |<d`.<d..<d`.<d..|
*
00000050  3c 64 60 80 3c 64 00 ff  2f 00 4d 54 72 6b 00 00  |<d`.<d../.MTrk..|
00000060  00 20 00 90 40 64 60 80  40 64 00 90 40 64 60 80  |. ..@d`.@d..@d`.|
00000070  40 64 00 90 40 64 60 80  40 64 00 90 40 64 60 80  |@d..@d`.@d..@d`.|
*
000000a0  40 64 00 ff 2f 00                                 |@d../.|
000000a6

I wrote my own version using struct:

import struct

HEAD_ID = b"\x4d\x54\x68\x64"
TRACK_ID = b"\x4d\x54\x72\x6b"

class HeaderChunk:
    def __init__(self, format, ntrack, tickdiv):
        self.format = format
        self.ntrack = ntrack
        self.tickdiv = tickdiv

    def dump(self):
        payload = struct.pack(">HH2s", self.format, self.ntrack, self.tickdiv)
        header = HEAD_ID + struct.pack(">I", len(payload))
        return header + payload


class TrackChunk:
    """Represents a track"""
    def __init__(self):
        self.data = b""
    def quarter(self, note):
        self.data += b"\x00\x90" + note + b"\x64\x60\x80" + note + b"\x64"

    def dump(self):
        header = TRACK_ID + struct.pack(">I", len(self.data))
        return header + self.data


header = HeaderChunk(1, 2, b"\x00\x60")

first_track = TrackChunk()
for _ in range(8):
    first_track.quarter(b"\x3c")

second_track = TrackChunk()
for _ in range(8):
    second_track.quarter(b"\x40")


with open("joac-example.mid", "wb") as output:
    output.write(header.dump())
    output.write(first_track.dump())
    output.write(second_track.dump())

It is correctly loaded on garage band

Upvotes: 3

Related Questions