My aim is to understand the StandardMidiFile format, and be able to generate simple MIDI files from patterns. I intend to write a simple step entry app, allowing step entry either:
- Reaper style: press a note or chord and when released, notes are entered and the cursor advances;
- Ableton style: hold notes and press left/right arrows;
- Clicking on cells to toggle on/off like a step sequencer;
- Drag up and down to change velocity (like in Geist)
Then I intend to write processors that e.g. turn chords into arpeggiators, or function as a chord memory device, turning individual notes into chords, or otherwise generating patterns based on input data. That input data needn't be midi.
At least, those are the initial aims.
jdamidi
To begin with, I've started writing a simple Python module full of utility functions (and perhaps classes).
encode_lengthturns an integer into the variable-length byte sequence required by SMFmake_mthdmakes theMThdheader chunkmake_trktakes raw MIDI data and wraps it in aMTrkchunk.event_list_to_miditakes a list of events with absolute tick times and turns into raw MIDI data with delta timesfevent_list_to_event_listtakes a list of events with float times (number of quarter notes) and returns an event list suitable forevent_list_to_midi
import struct
def encode_length(x):
xs = []
y = x
while x >= 0x80:
xs.append(x&0x7f)
x >>= 7
xs.append(x)
for i in range(1,len(xs)):
xs[i] += 0x80
xs = list(reversed(xs))
return bytes(xs)
def make_mthd(smf_format=1,num_tracks=1,ppqn=960):
if smf_format < 0 or smf_format > 2:
raise ValueError("smf_format must be 0..2")
if num_tracks < 1:
raise ValueError("number of tracks must be positive")
return b"MThd"+struct.pack(">IHHH",6,smf_format,num_tracks,ppqn)
def make_trk(midi_data):
# takes raw midi data and wraps in a MTrk
o = b"MTrk"
o += struct.pack(">I",len(midi_data))
o += midi_data
return o
def make_smf(tracks,smf_format=None, ppqn=960):
if smf_format is None:
if len(tracks) == 1:
smf_format = 0
else:
smf_format = 1
mthd = make_mthd(smf_format=smf_format,num_tracks=len(tracks),ppqn=ppqn)
mtracks = [ make_trk(track) for track in tracks ]
mid = mthd + b"".join(mtracks)
return mid
def fevent_list_to_event_list(fevents,multiplier=1.0,ppqn=960):
# t, data
# [ 1.0, [ 0x90, 0x60, 0x61 ] ]
# or
# [ 2.0, b"\x90\x60\x61 ]
# where 1.0, 2.0 is position in quater notes
# times are multiplied by multiplier (e.g. multiplier=0.25 will turn quarters into sixteenths)
events = []
for event in fevents:
t,d = event
t *= multiplier
t = int(t*ppqn)
events.append([t,d])
return events
def event_list_to_midi(events,length):
# t,data
# [ 45, [ 0x90, 0x60, 0x61 ] ]
# or
# [ 45, b"\x90\x60\x61 ]
# sort by time
events = list(sorted(events,key = lambda t: t[0]))
eventsd = []
data = b""
t0 = 0
for event in events:
t,d = event
dt = t - t0 # delta time from previous event
t0 = t
data += encode_length(dt)
data += bytes(d)
if length < t0:
raise ValueError(f"Events exceed given length {length}<{t0}")
# append all notes off and end track meta event
dt = length - t0
data += encode_length(dt)
data += b"\xb0\x7b\x00\x00\xff\x2f\x00"
return data