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 directorymidi_init()— starts the MIDI playback thread. The audio system has to be up first, so all of this runs insidewith_audio_system(); calling it without one panicsmidi_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