How to Load a parangonada Note Alignment Across Several Performances
ParangonadaLoader, multimodal AlignmentBundle, divs/quarters and samples/seconds C-Maps, synchronous and NOMATCH MatchClaims
How to Load a parangonada Note Alignment Across Several Performances
parangonar aligns a symbolic score to a performance and exports the result as a parangonada triple of CSV files — the score notes, the performed notes, and the note-level correspondences between them. This guide loads such an export for one work performed five different times into a single multimodal .
We load an existing alignment; nothing here runs an aligner. The correspondences were computed once, offline, and written to disk; the loader’s job is to read them faithfully and arrange them as timelines and s.
The work is Beethoven’s Eroica Variations, Op. 35 — Var. XIV — in five recordings spanning six decades: Szegedi (1966), Gould (1970), Curzon (1971), Brendel (1985), and Hewitt (2023). One score, five performances, and the note-by-note alignment that ties each performance back to the score.
The arc:
Load the whole export in one call.
Read the shared logical score, in two units, linked by a .
Read one performer’s physical timelines, in two units, linked by a .
Inspect the cross-group s — both the synchronous matches and the sentinels — and read one shared score position across all five performances.
Read each performance’s measured per-beat tempo and dynamics, carried as events on its own timeline.
/home/laser/miniconda3/envs/timetoalign/lib/python3.11/site-packages/partitura/__init__.py:9: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
import pkg_resources
1. Load the export in one call
ParangonadaLoader discovers every performance under the export’s directory, parses the shared score once and each performance’s notes and alignment, and binds each recording’s audio for its sample rate. from_file() is the one-line form of the standard two-phase loader pattern.
Twelve timelines arranged in six groups: one shared "score" group, and one "perf:<key>" group per recording. Everything below reads from this single bundle.
The score is identical across all five performances, so it is parsed once and placed in its own group. It appears in two logical units — the same notes, measured two ways:
Both hold the same 251 notes, each carrying its MIDI pitch and voice. A look at the first few notes shows the pitch and voice the export recorded (the first note sits at quarter −0.5, the score’s anacrusis):
The division grid and the quarter grid are one conversion apart
The two logical timelines are not independent: a single on score:dlt1 carries the division grid to quarters. It is a LinearMap — 32 divisions to the quarter, shifted by the half-quarter anacrusis — and it reproduces the quarter onsets exactly, because divisions are the discrete grid the quarters were notated on:
The same conversion is what a exposes. Querying score:dlt1 at division 48 and asking for the quarter reading reports 1.0, the continuous↔︎discrete link of the logical domain made visible at a single coordinate:
The seconds grid and the samples grid are, again, one apart — here a SamplesToSeconds map carrying the recording’s 44.1 kHz sample rate. It is the physical-domain counterpart of the logical divs→quarters map: 44 100 samples is one second.
The score group and the five performance groups are tied together by cross-group s — one per row of each performance’s alignment file. A matched note becomes a synchronous claim relating a score quarter to a performed second; an unmatched note becomes a claim, which records the dangling note rather than discarding it.
claims = bundle.cross_group_claims{"total claims": len(claims),"synchronous (matched)": sum(1for c in claims if c.is_synchronous),"NOMATCH (score-only or performance-only)": sum(1for c in claims ifnot c.is_synchronous ),}
A synchronous claim carries an anchor: a score quarter on one side, the performed second on the other. This is Szegedi’s note at score quarter 40, played 38.18 seconds into the recording:
szegedi_synchronous = [ cfor c in claimsif c.is_synchronousand c.connects("perf:1966_Szegedi:cpt1")and c.start_anchor isnotNoneandfloat(c.start_anchor.coordinate_a) ==40.0]szegedi_synchronous[0]
MatchClaimANCHOR
Timeline A
score:clt1
@40
Timeline B
perf:1966_Szegedi:cpt1
@38.183334
Metadata
agent=parangonada
Try: claim.get_matchstamp()
A claim carries no anchor — there is no second coordinate to record — but it keeps the unmatched side’s coordinate so the dangling note stays legible. Its text repr prints that coordinate inline (...@<coord>) with a [NOMATCH] flag:
szegedi_nomatch = [ c for c in claims ifnot c.is_synchronous and c.connects("perf:1966_Szegedi:cpt1")]szegedi_nomatch[0]
MatchClaimCONCEPTUAL
Timeline A
score:clt1
Timeline B
perf:1966_Szegedi:cpt1
Metadata
agent=parangonada
Try: claim.get_matchstamp()
One score position across all five performances
Because every performance is anchored back to the same score, a single score coordinate resolves across the whole bundle. get_matchstamp_at takes a coordinate on score:clt1 and returns the corresponding coordinate on every timeline connected to it — here, score quarter 40 mapped to the second at which each of the five pianists played it:
Read across that : the same notated moment falls at 37.8 s for Brendel and 38.2 s for Szegedi, but at 63.1 s for Gould — the raw material of a tempo comparison, drawn straight from the loaded alignment with no aligner ever run.
5. Measured tempo and dynamics, as events on the performance
The alignment ties each performance to the score note by note, but parangonar also measured, for every recording, a per-beat tempo and dynamics profile. These are not a model or a synthesised constant-tempo grid: they are the variable tempo and loudness actually measured beat by beat in each recording. The loader places them on the performance’s own seconds timeline — perf:<key>:cpt1 — as Beat and Dynamics s, sitting alongside the Note events at their measured onsets. A performance timeline therefore carries three kinds of event: the notes that were played, and the measured tempo and dynamics that describe how.
We read them with the same event query used everywhere else — filter by event_type. The coordinate (start) comes back as a number; the remaining measured columns currently round-trip as strings, so we cast them as we read.
Tempo: the Beat events
Each Beat event carries the measured beat-per-minute reading at its onset, together with the measure_number and beat it falls on. There are 63 of them — one per measured beat of the variation. Take Szegedi’s:
{'beat events': 63,
'first onset (s)': np.float64(0.796354),
'measured BPM: min / median / max': (np.float64(20.7),
np.float64(67.5),
np.float64(309.9))}
The tempo is anything but constant. Reading the first measures of the variation shows the measured BPM moving from beat to beat — the local rubato of the playing, recorded as data:
szegedi_tempo.head(8)
measure_number
beat
bpm
onset_sec
0
1
1
83.660126
0.796354
1
1
2
79.558022
1.513542
2
2
1
80.559433
2.267708
3
2
2
82.324928
3.012500
4
3
1
87.894203
3.741319
5
3
2
84.705917
4.423958
6
4
1
86.356804
5.132291
7
4
2
67.466995
5.827083
Dynamics: the Dynamics events
At the same measured onsets sit 63 Dynamics events, each carrying the mean and peak MIDI velocity measured over its beat. The .dyn source rows have no onset of their own; the loader joined each to the Beat row sharing its (measure_number, beat) key, so the dynamics land at exactly the same seconds as the tempo readings:
Because every performance carries its own measured Beat events, the “five interpretations of one work” claim becomes concrete: the same 63 beats of the variation, measured in each recording. Summarising each performer’s measured tempo shows how differently they take it — Gould stretches the variation across nearly twice the span of Brendel, at roughly half the median tempo:
These profiles are measured features carried on the performance timelines, not anything TimeToAlign! inferred. Loaded next to the notes and the cross-group s, they let one bundle hold both what was played (the aligned notes) and how it was played (the measured tempo and dynamics), for every one of the five recordings.
Recap
What the bundle expresses
How
Score, two logical units
score:clt1 (quarters) + score:dlt1 (divs), one divs→quarters LinearMap
Performance, two physical units
perf:<key>:cpt1 (s) + perf:<key>:dpt1 (samples), one SamplesToSeconds
Score ↔︎ performance
a synchronous per match; a per gap
A shared position read everywhere
bundle.get_matchstamp_at(coord, "score:clt1")
Measured tempo and dynamics
Beat / Dynamics events on perf:<key>:cpt1, read via filter(event_type=...)
Two logical units for the score, two physical units per performance, each pair linked by a , the whole tied together by cross-group s, and each performance additionally carrying its measured per-beat tempo and dynamics as events — one carrying an existing note alignment of one work across five performances, and a faithful record of how each was played.