Dup Ver Goto 📝

JdaMidi_001

To
108 lines, 502 words, 3358 chars Page 'JdaMidi_001' does not exist.

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).

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