Reputation: 138
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
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