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 directorymidi_init()— starts the MIDI playback threadmidi_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.