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

Audio

Two paths: offline (render to PCM/WAV bytes; the default audio story) and live (drive a system output device through cpal; shipped as an example).

Offline rendering (audio feature)

Pure Rust via rustysynth. The caller supplies a SoundFont (SF2) — this crate does not bundle one, because usable SoundFonts are 6+ MB and ship best as separate downloads.

verovio = { version = "0.1", features = ["audio"] }
let sf2 = std::fs::read("TimGM6mb.sf2")?;
let wav = tk.render_to_wav(&sf2, 44_100)?;
std::fs::write("out.wav", wav)?;
# Ok::<(), Box<dyn std::error::Error>>(())

Free GM-compatible SoundFonts

Name Size License
TimGM6mb ~6 MB GPL-2
GeneralUser GS ~30 MB custom (free)
FluidR3 GM2-2 ~140 MB MIT
ChoriumRevA ~26 MB custom (free)

Surface

verovio::audio::render_pcm(midi_bytes, sf2_bytes, sample_rate) -> Result<Pcm>
verovio::audio::render_wav(midi_bytes, sf2_bytes, sample_rate) -> Result<Vec<u8>>
verovio::audio::pcm_to_wav(&Pcm) -> Vec<u8>

// Toolkit convenience:
tk.render_to_pcm(&sf2, sample_rate)?;
tk.render_to_wav(&sf2, sample_rate)?;
tk.render_to_wav_with_policy(&sf2, sample_rate, &midi_policy)?;

Pcm carries sample_rate, left, right and offers duration_secs and interleaved for hand-off to APIs that expect interleaved PCM.

WAV output is 16-bit signed PCM stereo RIFF — universal player compatibility. Samples outside [-1.0, 1.0] are clipped.

Threading

Audio rendering touches no Verovio C++ state. The synth runs entirely in Rust on the SMF bytes Verovio already produced. Safe to spawn N parallel renders.

Live playback (live-audio feature)

Working end-to-end demo using cpal for the OS audio device:

cargo run --release --features live-audio \
    --example live_playback -- path/to/font.sf2

The example renders a built-in PAE demo, opens the default output device, and drives a MidiFileSequencer from inside cpal's audio callback. ~120 LoC; copy-paste starting point for full players.

Why gated separately

cpal pulls in alsa-sys on Linux, which needs alsa-lib from the system. To keep cargo test portable on bare environments (CI, NixOS), cpal is gated behind the live-audio feature (which also turns on audio).

NixOS

nix-shell -p alsa-lib pkg-config
cargo run --release --features live-audio \
    --example live_playback -- soundfont.sf2

Design notes for building a real player

The shipped example is intentionally minimal — no transport, no seeking, no UI sync. For production:

  1. Lock-free command channelcrossbeam-channel SPSC for play / pause / seek instructions from UI thread to audio thread.
  2. Atomic playheadAtomicU64 of milliseconds; the audio thread writes, the UI thread reads to sync highlight overlays.
  3. Avoid Mutex in the audio callback — the example uses one for Send-passing only. Production should keep the sequencer owned exclusively by the audio thread and never lock.
  4. Error recovery on device disconnect — cpal surfaces this via the error callback; reopen the stream on a known good device.

The expectation is that consumers (e.g., xpart) build their own player; this crate provides the synth, the SMF, and the timemap.

Clone this wiki locally