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

7.9.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
    }
}

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

  • 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

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

7.9.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()   // stop all tracks
midi_shutdown()

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

7.9.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: stdlib_audio