Skip to content

Score Reading

Rodrigo Agurto edited this page May 30, 2026 · 2 revisions

Score Reading

Everything you can pull out of a loaded score short of editing it.

Metadata

let md = tk.metadata()?;
println!("{:?}", md.title);
println!("{:?}", md.composer);
println!("{:?}", md.lyricist);
println!("{:?}", md.arranger);
println!("{:?}", md.copyright);
for instr in &md.instruments {
    println!("{instr}");
}

Source format detection:

  • MEI: <title>, <persName role="composer|lyricist|arranger">, <availability>/<useRestrict>, <staffDef>/<label>
  • MusicXML: <work-title>, <creator type="composer|lyricist">, <rights>, <part-name> (DTDs allowed)
  • PAE / ABC / plaintext: best-effort T: / C: / Z: scrape

Verovio's C++ Toolkit doesn't surface these — they're parsed from the verbatim input cached on load_data. One extra String per loaded score.

Timemap (playback sync source of truth)

let timemap = tk.timemap()?;
for ev in &timemap {
    println!("{}ms — on: {:?}, off: {:?}, tempo: {:?}",
             ev.tstamp, ev.on, ev.off, ev.tempo);
}

Exact (rational) timestamps for tuplet-heavy or long scores where f64 drift matters:

let exact = tk.timemap_exact()?;
for ev in &exact {
    let (num, den) = ev.quarter_beats();
    let ms_at_60 = ev.tstamp_ms_at_tempo(60.0);
    // …
}

Cache-aware lookup helpers (verovio::lookup)

These walk a cached Timemap without re-entering Verovio. Benchmarked at ~100× faster than Toolkit::elements_at_time per call.

Function What it does
sounding_at(tm, ms) IDs currently sounding (sorted)
sounding_at_into(tm, ms, &mut Vec) Same, buffer reuse
sounding_count_at(tm, ms) Count only (UI badges)
chord_at(tm, ms) IDs sharing the latest onset
note_duration(tm, id) (on_ms, off_ms) for one ID
events_in_range(tm, start, end) Slice of events by ms window
next_event_after(tm, ms) / prev_event_before Step navigation
duration_ms(tm) Total wall-clock duration
distinct_element_count(tm) Unique-ID count over the score
measure_at_in(events, ms) Enclosing measure ID
measures_from_events(events) Build measure timeline
measure_by_id(measures, id) MeasureInfo by MEI ID

Cursors

For monotonic playback (audio / animation loops):

use verovio::lookup::PlaybackCursor;

let mut cursor = PlaybackCursor::new(&timemap);
for tick_ms in clock_ticks() {
    let active = cursor.advance_to(tick_ms);   // amortized O(1)
    // …
}

For loop-region practice ([start..end) with auto-wrap):

use verovio::lookup::LoopCursor;

let mut cursor = LoopCursor::new(&timemap, 4000.0, 8000.0);
for tick_ms in monotonic_clock() {
    let active = cursor.advance_to(tick_ms);   // wraps inside loop region
    // …
}

Tempo

let tm = tk.tempo_map()?.unwrap();
tm.qstamp_to_ms(4.0);                          // beats → ms (handles changes)
tm.ms_to_qstamp(2000.0);                       // ms → beats
tm.bpm_at_qstamp(4.0);                         // tempo at quarter position
tm.bpm_at_ms(2000.0);                          // tempo at wall position
tm.scaled(0.5);                                // practice mode (50% speed)

Measures + classification

for m in tk.measures()? {
    println!("measure {} from {}ms to {}ms (q {:?}–{:?})",
             m.id, m.start_ms, m.end_ms, m.start_qfrac, m.end_qfrac);
}

let kinds = tk.classified_elements()?;
for (id, kind) in &kinds {
    use verovio::ElementKind::*;
    match kind {
        Note    => println!("{id} is a note"),
        Chord   => println!("{id} is a chord"),
        Rest    => println!("{id} is a rest"),
        Measure => println!("{id} is a measure"),
    }
}

Visual ↔ audio correlation

Two side tables, both keyed by MEI ID.

Staff map (id → 1-indexed staff number):

let staff = tk.staff_map()?;
for (id, n) in staff.iter().take(5) {
    println!("{id} on staff {n}");
}

BBox map (id → SVG-unit pixel rect) — for click-to-seek hit testing and "highlight box around playing note" overlays:

use verovio::BBox;

let bboxes = tk.bbox_map()?;
if let Some(bbox) = bboxes.get("n1") {
    println!("note n1 on page {} at ({}, {}) size {}×{}",
             bbox.page, bbox.x, bbox.y, bbox.width, bbox.height);
}

// Hit-test a click point
let click_x = 1500.0;
let click_y = 800.0;
let hit = bboxes.iter().find(|(_, bb)| bb.contains(click_x, click_y));

Accuracy contract: bbox covers <use> anchor points and <path> M/L coordinates, with a 200×300 notehead approximation. Good enough for hit testing and visible highlights; not pixel-perfect for layout debugging (use Verovio's svgBoundingBoxes option for that).

Expansion map (repeats / voltas)

let exp = tk.expansion_map()?;
for (original, expanded) in &exp {
    println!("{original} → {expanded:?}");   // expanded order in playback
}

Clone this wiki locally