17.11. MIDI file playback with GM preset mapping

Module strudel_midi_player

17.11.1. Structures

GmPreset
Fields:
  • osc : OscType = strudel_synth::OscType.osc_sine - Oscillator waveform used to synthesise the instrument.

  • att : float = 0.005f - Envelope attack time in seconds.

  • dec : float = 0.1f - Envelope decay time in seconds.

  • sus : float = 0.5f - Envelope sustain level in [0, 1].

  • rel : float = 0.15f - Envelope release time in seconds.

MidiVoice

struct MidiVoice

MidiChannelState

struct MidiChannelState

MidiPlaybackState
Fields:
  • name : string - Track name supplied by the caller (also used by midi_stop / midi_set_volume).

  • events : array< MidiEvent> - All MIDI events across tracks, merged and sorted by absolute tick.

  • channels : MidiChannelState[16] - Per-channel state (program, CCs, pitch bend, sustain pedal).

  • voices : array< MidiVoice?> - Active oscillator/sample voices (legacy path; unused when SF2 is active).

  • csf2_voices : array< ma_sf2_voice> - Active SF2 voices rendered by the C runtime.

  • bank : SampleBank? - Optional sample bank pointer (owned by the main context).

  • sf2 : SF2File? - Optional SF2 soundfont pointer (owned by the main context).

  • ticks_per_qn : int = 480 - Ticks per quarter note, taken from the MIDI file header.

  • event_index : int = 0 - Index of the next event in events to process.

  • current_tick : double = 0lf - Current playback position in ticks.

  • ticks_per_sec : double = 0lf - Conversion factor from seconds to ticks (depends on current tempo).

  • tempo_usec : int = 500000 - Current tempo in microseconds per quarter note (500000 = 120 BPM).

  • playback_time : double = 0lf - Elapsed playback time in seconds.

  • looping : bool = false - If true, playback restarts from the beginning when the track ends.

  • gain : float = 1f - Current linear gain applied to the track mix.

  • target_gain : float = 1f - Fade target for gain; used by midi_set_volume to implement fades.

  • fade_speed : float = 0f - Fade rate per second; 0 means instantaneous.

  • remove_when_silent : bool = false - If true, the track is removed once it fades to zero (used by midi_stop).

  • done : bool = false - Set to true when playback has finished and no voices remain.

  • mixer : ma_volume_mixer - Volume/pan mixer used for drum and sample voices.

  • mixer_ready : bool = false - True after ma_volume_mixer_init has been called on mixer.

17.11.2. Lifecycle

midi_init()

Start the MIDI playback thread and attach it to the running audio stream. No-op if already running; panics if the audio system has not been initialised first.

midi_init_reverb()

Initialise the module-global reverb and chorus effects for offline rendering (calling midi_tick without running the thread).

midi_shutdown()

Stop the playback thread and release its channels; blocks until the audio thread confirms shutdown.

17.11.3. Playback control

midi_is_playing(): bool

Return true while the MIDI playback thread is running.

midi_play(name: string; path: string; gain: float = 1f; looping: bool = false; bank: SampleBank const? = null; use_sf2: bool = true): bool

Parse a .mid file from disk and submit it for playback under name. Auto-starts the playback thread if needed; falls back to the oscillator/sample path when no SF2 is loaded or use_sf2 is false.

Arguments:
  • name : string

  • path : string

  • gain : float

  • looping : bool

  • bank : SampleBank?

  • use_sf2 : bool

midi_play_data(name: string; data: array<uint8>; gain: float = 1f; looping: bool = false; bank: SampleBank const? = null; use_sf2: bool = true): bool

Parse a .mid file from an in-memory byte array and submit it for playback under name. Same semantics as midi_play but sourced from memory instead of disk.

Arguments:
  • name : string

  • data : array<uint8>

  • gain : float

  • looping : bool

  • bank : SampleBank?

  • use_sf2 : bool

midi_set_pause(paused: bool)

Pause or resume all MIDI playback (the underlying audio stream continues with silence while paused).

Arguments:
  • paused : bool

midi_stop(name: string = "")

Stop a single track by name, or shut down the whole playback thread when name is empty.

Arguments:
  • name : string

midi_tick(state: MidiPlaybackState; chunk_seconds: float): array<float>

Advance playback by chunk_seconds, process pending events, render all active voices and return an interleaved stereo PCM chunk. Handles oscillator voices, drum/sample playback, SF2 voices, and the shared reverb/chorus send buses.

Arguments:

17.11.4. Audio setup

midi_set_audio_vis_channel(ch: Channel?)

Register a channel that will receive rendered audio chunks for visualisation; call before midi_init.

Arguments:
midi_set_volume(name: string; volume: float; fade: float = 0f)

Set the linear gain of a named track, optionally fading over fade seconds (0 = instantaneous).

Arguments:
  • name : string

  • volume : float

  • fade : float

17.11.5. Asset loading

midi_load_samples(media_path: string)

Populate the module-global sample bank with piano samples (note_{n}) and GM drum samples under media_path.

Arguments:
  • media_path : string

midi_load_sf2(path: string): bool

Load an SF2 soundfont from disk into the module-global slot; returns true on success.

Arguments:
  • path : string

17.11.6. Event processing

init_playback(state: MidiPlaybackState; midi: MidiFile const?; name: string; gain: float; looping: bool; bank: SampleBank const? = null; sf2: SF2File const? = null)

Initialise a MidiPlaybackState from a parsed MidiFile, merging its tracks and resetting per-channel state to GM defaults.

Arguments:
process_events(state: MidiPlaybackState)

Dispatch all MIDI events up to the current playback tick: note-on/off, program changes, CCs, pitch bend, tempo. Routes note events either to the SF2 voice allocator or to the legacy oscillator/sample path depending on state.sf2.

Arguments:

17.11.7. Drum and GM preset rendering

gm_preset(program: int): GmPreset

Map a General MIDI program number (0-127) to a GmPreset used by the oscillator path. Groups programs by GM family (piano, strings, brass, synth lead/pad, …) and returns a reasonable default envelope per family.

Arguments:
  • program : int

midi_adsr(elapsed: float; note_off_elapsed: float; attack: float; decay: float; sustain: float; release: float): float

Evaluate the MIDI-style ADSR at elapsed seconds since note-on and note_off_elapsed seconds since note-off. note_off_elapsed < 0 means the note is still held. The sustain phase also decays exponentially so long-held notes fade.

Arguments:
  • elapsed : float

  • note_off_elapsed : float

  • attack : float

  • decay : float

  • sustain : float

  • release : float

render_drum_hit(note: int; velocity: float): array<float>

Render a GM channel-10 drum hit for note at the given velocity as a mono PCM buffer. Dispatches to the appropriate render_* drum voice from strudel_synth; falls back to a short hi-hat burst for unmapped notes.

Arguments:
  • note : int

  • velocity : float