Skip to content

MIDI Playback

Rodrigo Agurto edited this page May 30, 2026 · 1 revision

MIDI Playback

Verovio renders MIDI as a Format-1 SMF with one track per staff, all on channel 0 with no program-change or controller events. That's not useful for genuine multi-track playback — every voice goes through the same synth instrument.

verovio::midi rewrites the SMF post-render to fix this. The same Toolkit::render_to_midi_bytes() produces SMF bytes; passing them through a MidiTrackPolicy reassigns channels, inserts instrument changes, sets volumes, and so on.

Quick recipe

use std::collections::BTreeMap;
use verovio::midi::{MidiTrackPolicy, TrackOverride};

let policy = MidiTrackPolicy {
    auto_distribute_channels: true,   // staff 1 → ch 0, staff 2 → ch 1, …
    overrides: BTreeMap::from([
        (1, TrackOverride { program: Some(0),  ..Default::default() }), // Piano
        (2, TrackOverride { program: Some(42), volume: Some(96), ..Default::default() }), // Cello
    ]),
    ..MidiTrackPolicy::default()
};
let bytes = tk.render_to_midi_bytes_with_policy(&policy)?;
# Ok::<(), verovio::Error>(())

TrackOverride — the complete controller set

TrackOverride {
    // Routing
    channel:          Option<u8>,        // 0-15 (override auto-distribute)
    program:          Option<u8>,        // GM program 0-127
    bank_select:      Option<(u8, u8)>,  // CC#0 + CC#32 (GS/XG bank pages)
    midi_port:        Option<u8>,        // Meta::MidiPort (multi-port routing)

    // Levels
    volume:           Option<u8>,        // CC#7
    pan:              Option<u8>,        // CC#10 (64 = center)
    expression:       Option<u8>,        // CC#11
    mute:             bool,              // zero every NoteOn velocity

    // Pitch
    transpose:        Option<i8>,        // semitones (clamped 0..=127)

    // Modulation / effects
    modulation:       Option<u8>,        // CC#1 (vibrato)
    sustain:          Option<bool>,      // CC#64 (pedal down/up)
    reverb:           Option<u8>,        // CC#91
    chorus:           Option<u8>,        // CC#93

    // DAW labels (Meta events)
    name:             Option<String>,    // MetaMessage::TrackName
    instrument_name:  Option<String>,    // MetaMessage::InstrumentName
}

MidiTrackPolicy — score-wide settings

MidiTrackPolicy {
    overrides:                 BTreeMap<u32, TrackOverride>,
    auto_distribute_channels:  bool,                        // staff N → ch (N-1) % 16
    tempo_override:            Option<TempoMap>,            // replace Verovio's tempo
    time_signature:            Option<(u8, u8)>,            // Meta::TimeSignature
    key_signature:             Option<i8>,                  // -7..=7, sharps positive
    key_signature_minor:       bool,
    measure_markers:           Option<Vec<MeasureInfo>>,    // Meta::Marker per measure
    lyrics:                    Option<Vec<(f64, String)>>,  // Meta::Lyric at q-stamps
    cue_points:                Option<Vec<(f64, String)>>,  // Meta::CuePoint
}

Builder shortcuts

// Practice mode — solo just track 1, mute everything else
let p = MidiTrackPolicy::with_solo(&[1]);

// Mute specific tracks
let p = MidiTrackPolicy::with_mute(&[3, 5]);

// Assign programs without building the BTreeMap by hand
let p = MidiTrackPolicy::with_programs(&[(1, 0), (2, 42)]);

// Chain
let p = MidiTrackPolicy::with_programs(&[(1, 0)])
    .with_auto_distribute_channels();

General MIDI lookup tables

use verovio::midi::gm;

gm::program_name(40);        // Some("Violin")
gm::all_programs();          // &[&str; 128]
gm::drum_key_name(36);       // Some("Bass Drum 1")
gm::note_name(60);           // Some("C4")
gm::midi_key_from_name("Bb3"); // Some(58)

Inspect the SMF

use verovio::midi::{summarize, TrackInfo};

let info: Vec<TrackInfo> = summarize(&bytes).unwrap();
for t in info {
    println!("track {} channels={:?} notes={} program={:?}",
             t.track_index, t.channels, t.audible_note_count, t.program);
}

Streaming events (for software synths)

iter_smf_events walks the SMF and emits chronologically sorted TimedEvents with wall-clock millisecond onsets:

use verovio::midi::{iter_smf_events, TimedMessage};

for ev in iter_smf_events(&bytes).unwrap() {
    match ev.message {
        TimedMessage::NoteOn { key, vel } if vel > 0 => {
            scheduler.note_on(ev.at_ms, ev.channel, key, vel);
        }
        TimedMessage::Tempo { usec_per_quarter } => {
            scheduler.set_tempo(ev.at_ms, usec_per_quarter);
        }
        _ => {}
    }
}

TimedMessage covers note-on/off, aftertouch (key + channel), controllers, program changes, pitch bend, tempo, and opaque meta + sysex projections.

Panic SMF

Emergency-stop bytes — sends CC#120 (All Sound Off) and CC#123 (All Notes Off) on every one of the 16 channels:

let panic = verovio::midi::build_panic_smf();
synth.play(&panic);  // flushes stuck voices without restarting

Clone this wiki locally