8.12.8. AUDIO-08 — MIDI Files

This tutorial covers MIDI file parsing, playback with the built-in sample banks, and cross-fading between two simultaneously playing MIDI tracks. It is split into three parts that mirror the structure of the source file.

8.12.8.1. Part A: MIDI Parsing

load_midi reads a .mid file and returns a MidiFile struct:

require strudel/strudel_midi

let midi = load_midi("fur_elise.mid")
print("Format: {midi.format}\n")         // 0 = single track, 1 = multi-track
print("Ticks/QN: {midi.ticks_per_qn}\n") // timing resolution
print("Tracks: {length(midi.tracks)}\n")

Each track is a MidiTrack containing an array of MidiEvent. Events carry a tick timestamp, a MidiEventKind (note-on, note-off, control change, tempo, etc.), a channel number, and two data bytes.

merge_tracks combines all tracks into a single sorted event list, which is easier to iterate for analysis or playback:

var merged <- merge_tracks(midi)
for (evt in merged) {
    if (evt.kind == MidiEventKind.midi_tempo) {
        let bpm = 60000000.0 / float(evt.tempo)
        print("Tempo change at tick {evt.tick}: {bpm:.1f} BPM\n")
    }
}

To compute the total duration in seconds, walk the merged events and accumulate time using the current tempo and ticks_per_qn:

var tempo_us = 500000   // default 120 BPM
var total_time = 0.0
var prev_tick = 0
for (evt in merged) {
    if (evt.tick > prev_tick) {
        let sec_per_tick = float(tempo_us) / 1000000.0 / float(midi.ticks_per_qn)
        total_time += float(evt.tick - prev_tick) * sec_per_tick
    }
    prev_tick = evt.tick
    if (evt.kind == MidiEventKind.midi_tempo && evt.tempo > 0) {
        tempo_us = evt.tempo
    }
}

8.12.8.2. Part B: MIDI Playback

The MIDI player uses the audio system’s streaming infrastructure and built-in piano and drum sample banks. Three functions manage its lifecycle:

  • midi_load_samples(media_path) — loads the sample banks from the media directory

  • midi_init() — starts the MIDI playback thread. The audio system has to be up first, so all of this runs inside with_audio_system(); calling it without one panics

  • midi_shutdown() — stops the thread and releases resources

midi_play starts a named MIDI track. The name is an arbitrary string that identifies the track for later control. Multiple tracks can play simultaneously:

require strudel/strudel_midi_player

with_audio_system() {
    midi_load_samples(media_path)
    midi_init()
    midi_play("music", "fur_elise.mid", [gain = 0.8, looping = true])
    sleep(5000u)
    midi_stop("music")
    midi_shutdown()
}

8.12.8.3. Part C: Cross-fading

Because tracks are named and independent, you can run two MIDI files at once and cross-fade between them. midi_set_volume smoothly transitions a track’s volume over a specified fade time in seconds:

// Start both — track_a at full volume, track_b silent
midi_play("track_a", fur_elise, [gain = 1.0, looping = true])
midi_play("track_b", bach_air, [gain = 0.0, looping = true])

// Cross-fade A -> B over 3 seconds
midi_set_volume("track_a", 0.0, 3.0)
midi_set_volume("track_b", 1.0, 3.0)
sleep(5000u)

// Cross-fade back B -> A
midi_set_volume("track_a", 1.0, 3.0)
midi_set_volume("track_b", 0.0, 3.0)
sleep(5000u)

midi_stop()   // no name: stops every track and shuts the player thread down

This pattern is common in games for transitioning between exploration and combat music without an audible cut.

8.12.8.4. Running the Tutorial

Run from the project root:

daslang.exe tutorials/dasAudio/08_midi.das

Part A prints the MIDI file structure, the first 20 merged events, and the computed duration. Part B plays Fur Elise for 5 seconds. Part C cross-fades between Fur Elise and Bach’s Air on the G String, fading each direction over 3 seconds.

See also

Full source: tutorials/dasAudio/08_midi.das

Audio module reference: Low-level audio bindings (miniaudio)

Next tutorial: AUDIO-09 — Playback Status