tags: python midi jdamidi 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: 1. Reaper style: press a note or chord and when released, notes are entered and the cursor advances; 2. Ableton style: hold notes and press left/right arrows; 3. Clicking on cells to toggle on/off like a step sequencer; 4. 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_length` turns an integer into the variable-length byte sequence required by SMF * `make_mthd` makes the `MThd` header chunk * `make_trk` takes raw MIDI data and wraps it in a `MTrk` chunk. * `event_list_to_midi` takes a list of events with absolute tick times and turns into raw MIDI data with delta times * `fevent_list_to_event_list` takes a list of events with float times (number of quarter notes) and returns an event list suitable for `event_list_to_midi` ```py 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 ```