from pathlib import Path
import numpy as np
import pandas as pd
from PIL import Image
from timetoalign import (
ContinuousPhysicalTimeline,
DiscreteGraphicalTimeline,
NumberType,
RepoVizzLoader,
TableMap,
TimeUnit,
)
from timetoalign.alignment import (
AlignmentAnchor,
AlignmentBundle,
MatchClaim,
MatchLine,
MatchMetadata,
TimelineGroup,
WarpMap,
)
from timetoalign.alignment.matching import (
match_notes_by_attributes,
prepare_abc_notes_for_matching,
prepare_eep_notes_for_matching,
)
from timetoalign.loader.score import TSVLoader
from timetoalign.timelines.flow import FlowMode, create_unfolded_timeline
from timetoalign.timelines.types import SegmentLine
_notebook_dir = Path(".").resolve()
DATA_DIR = (
_notebook_dir.parent.parent
/ "tests"
/ "data"
/ "score"
/ "beethoven_op18-4iv_multimodal"
)
# XML manifest paths — the loader reads metadata from these files
NORMAL_XML = DATA_DIR / "StringQuartetEEP_I_Normal" / "StringQuartetEEP_I_Normal.xml"
MECHANICAL_XML = (
DATA_DIR / "StringQuartetEEP_I_Mechanical" / "StringQuartetEEP_I_Mechanical.xml"
)
EXAGGERATED_XML = (
DATA_DIR / "StringQuartetEEP_I_Exaggerated" / "StringQuartetEEP_I_Exaggerated.xml"
)
# Audio sources and instruments
AUDIO_SOURCES = [
"mono",
"binaural",
"pickup_vln1",
"pickup_vln2",
"pickup_vla",
"pickup_cello",
]
INSTRUMENTS = ["vln1", "vln2", "vla", "cello"]How to Align Multimodal Data (Beethoven)
How to Align Multimodal Data (Beethoven)
Figure 3 acid test for TimeToAlign! — 16+ timelines across all 3 domains (Physical, Logical, Graphical) in 5 TimelineGroups within one AlignmentBundle.
Structure: 1. Part I: Build 3 recording groups (Groups 1-3) — 15 DPTs 2. Part II: Build Score group (Group 4) + align with recordings 3. Part III: Build Emerson group (Group 5) + cross-group coordinate transfer
0. Gold Standard Reference Values
| ID | Description | Samples | Rate | Grp |
|---|---|---|---|---|
| DPT1-5 | Normal | 11,753,638 / 11,195 / 22,389 / 45,844 / 63,965 | 44.1k / 42 / 84 / 172 / 240 | 1 |
| DPT6-10 | Mechanical | 12,426,696 / 11,836 / 23,671 / 48,469 / 67,628 | same rates | 2 |
| DPT11-15 | Exaggerated | 8,197,748 / 7,808 / 15,616 / 31,975 / 44,614 | same rates | 3 |
| Recording | Notes | Matched | Unmatched EEP | Unmatched ABC |
|---|---|---|---|---|
| Normal | 4,026 | 3,740 | 16 | 23 |
| Mechanical | 4,026 | 3,743 | 13 | 20 |
| Exaggerated | 2,820 | 2,650 | 4 | 1,113 |
1. Setup
Each EEP recording directory contains 5 modalities (audio, 3 feature types, MoCap) plus .notes files with annotated note events. The function below builds a TimelineGroup from one such directory via the XML manifest.
Structure (per manuscript): - 5 parent physical timelines, each with a SamplesToSeconds c-map - Audio, Tonal, LowLevel, Rhythm parents: 6 children each (mono, binaural, 4 pickups) - MoCap parent: 4 children (one per instrument: vln1, vln2, vla, cello)
def build_recording_group(xml_path, group_id, group_name, dpt_base):
"""Build a TimelineGroup from one EEP recording directory via XML manifest.
Args:
xml_path: Path to the recording's XML manifest file.
group_id: ID for the TimelineGroup.
group_name: Human-readable name for the group.
dpt_base: Starting DPT number (e.g. 1 for dpt1-dpt5).
Returns:
TimelineGroup with 5 hierarchical DPTs (parent + children).
"""
rv = RepoVizzLoader.from_file(xml_path)
n = dpt_base
# 1. Audio (mono as parent, 6 sources as children)
audio = rv.create_timeline("mono", tl_uid=f"dpt{n}", name="Audio")
for src in AUDIO_SOURCES:
audio.add_child(rv.create_timeline(src, tl_uid=src), offset=0)
# 2-4. Essentia descriptors (tonal, lowlevel, rhythm)
desc_cfgs = [
("tonal", "ChordsStrength", 1),
("lowlevel", "Dissonance", 2),
("rhythm", "BeatsLoudness", 3),
]
descriptors = []
for desc_type, desc_name, offset in desc_cfgs:
parent = rv.create_timeline(
f"{desc_type}.{desc_name}.mono",
tl_uid=f"dpt{n + offset}",
name=desc_type.title(),
)
for src in AUDIO_SOURCES:
parent.add_child(
rv.create_timeline(
f"{desc_type}.{desc_name}.{src}", tl_uid=f"{src}_{desc_type}"
),
offset=0,
)
descriptors.append(parent)
# 5. MoCap bb_angle (from the DescriptorGroup section of the XML)
mocap = rv.create_timeline(
rv.find_descriptor("bb_angle", "vln1"),
tl_uid=f"dpt{n + 4}",
name="MoCap",
)
for inst in INSTRUMENTS:
child = rv.create_timeline(
rv.find_descriptor("bb_angle", inst),
tl_uid=f"{inst}_mocap",
)
mocap.add_child(child, offset=0)
# Add notes to pickup children
for inst in INSTRUMENTS:
notes = rv.store.notes_for_instrument(inst)
if notes and (pickup := audio.get_child(f"pickup_{inst}")):
pickup.add_events(notes.to_pandas().to_dict("records"))
return TimelineGroup(
id=group_id,
name=group_name,
timelines=[audio, *descriptors, mocap],
)Part I: Three Recording Groups (Groups 1-3)
Each EEP recording = 5 DPTs (audio + 3 feature types + MoCap) at different sampling rates, all sharing the same physical duration. Note events live as a child of the audio DPT.
2. Group 1: Normal Recording (DPT1-DPT5)
normal_group = build_recording_group(
NORMAL_XML, "normal", "Normal Recording", dpt_base=1
)
normal_groupTimelineGroup[normal] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt1] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ │ │ DiscretePhysicalTimeline[dpt2] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ │ │ DiscretePhysicalTimeline[dpt3] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ │ │ DiscretePhysicalTimeline[dpt4] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ │ │ DiscretePhysicalTimeline[dpt5] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ └────────────────────────────────────────────────────────────────────┘ Timestamps: 2
The audio timeline now carries the note annotations as a child:
normal_group.get_timeline("dpt1")DiscretePhysicalTimeline[dpt1] (6 children, 1 cmaps)
0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 samples
├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
└─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638
3. Group 2: Mechanical Recording (DPT6-DPT10)
mechanical_group = build_recording_group(
MECHANICAL_XML, "mechanical", "Mechanical Recording", dpt_base=6
)
mechanical_groupTimelineGroup[mechanical] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt6] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ │ │ DiscretePhysicalTimeline[dpt7] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ │ │ DiscretePhysicalTimeline[dpt8] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ │ │ DiscretePhysicalTimeline[dpt9] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ │ │ DiscretePhysicalTimeline[dpt10] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ └────────────────────────────────────────────────────────────────────┘ Timestamps: 2
4. Group 3: Exaggerated Recording (DPT11-DPT15)
Shorter recording (~186s) — stops after measure 131.
exaggerated_group = build_recording_group(
EXAGGERATED_XML,
"exaggerated",
"Exaggerated Recording",
dpt_base=11,
)
exaggerated_groupTimelineGroup[exaggerated] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt11] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ │ │ DiscretePhysicalTimeline[dpt12] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ │ │ DiscretePhysicalTimeline[dpt13] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ │ │ DiscretePhysicalTimeline[dpt14] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ │ │ DiscretePhysicalTimeline[dpt15] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ └────────────────────────────────────────────────────────────────────┘ Timestamps: 2
5. Part I Summary
3 groups, 15 timelines. Each audio DPT carries note events as a child timeline, making them accessible for matching in Part II.
Next: Part II builds the Score group and aligns each recording via note matching.
Part II: Score Group + Alignment to Recordings (Group 4)
The score group brings together three representations of the same music:
- CLT1: ABC v2.6 score (notes, measures, harmonies) —
ContinuousLogicalTimeline - DGT1: OMR ground truth (3,190 note heads across 22 pages) —
DiscreteGraphicalTimeline - OpenScore: OpenScore String Quartet edition (4th movement) —
ContinuousLogicalTimeline
All three go into one TimelineGroup. Cross-domain coordinate transfer (pixels ↔︎ quarters ↔︎ seconds) works automatically via linear interpolation.
6. CLT1: ABC v2.6 Score
ABC_DIR = DATA_DIR / "ABC"
abc_loader = TSVLoader.from_file(
ABC_DIR / "n04op18-4_04.notes.tsv",
ABC_DIR / "n04op18-4_04.measures.tsv",
ABC_DIR / "n04op18-4_04.harmonies.tsv",
)
clt1 = abc_loader.create_timeline(uid="clt1")
clt1ContinuousLogicalTimeline[clt1] (3768 events, 3 children, 2 cmaps)
0 _______________________________ 878.5 quarters
├─ notes 0 ______________________________ 872.5 (3156 events)
├─ measures 0 _______________________________ 878.5 (226 events)
└─ annotations 0 _______________________________ 878.5 (386 events)
6.1 ABC Flow Control: Repeat Structure
The ABC score has repeats and volta brackets. The loader’s create_flow_controller() derives the repeat structure from the measure data and computes the default flow (all repeats taken). This is the same flow control machinery used later for CLT2 (the recordings edition) in Part III.
abc_controller = abc_loader.create_flow_controller()
abc_flow = abc_controller.compute_flow(FlowMode.DEFAULT)
abc_flowFlow(default): 226 folded → 291 unfolded (×1.29), 11 sections # MCs Sections Reason ── ─────────── ──────── ────────────── 1 [1, 10) A start 2 [1, 19) A;B repeat → 1 3 [10, 28) B;C repeat → 10 4 [19, 45) C;D;E repeat → 19 5 [28, 44) D repeat → 28 6 [45, 85) F;G skip → 45 7 [78, 94) G;H;I repeat → 78 8 [85, 93) H repeat → 85 9 [94, 103) J;K;L skip → 94 10 [95, 102) K repeat → 95 11 [103, 227) M skip → 103 Sequence: A A B B C C D E D F G G H I H J K L K M
The flow controller and flow will be used in §9.2 to unfold the entire score group at once — not just CLT1, but all timelines.
7. DGT1: OMR Ground Truth
The OMR data contains 3,190 note head bounding boxes across 22 score pages. Each page has 2 systems (except the last which has 1), giving 43 system segments in reading order. Note events use Left (start) and Width (duration) as pixel coordinates. Each system’s onset_beats values provide a c-map from pixels to quarters.
Architecture: SegmentLine[SegmentLine[DiscreteGraphicalTimeline]] → 22 page SegmentLine[DiscreteGraphicalTimeline] segments → 2 system sub-segments each.
OMR_CSV = DATA_DIR / "OMR_groundtruth" / "OMR_xml_by_score" / "omr_note_heads.csv"
OMR_IMAGES = DATA_DIR / "OMR_groundtruth" / "Images"
omr_df = pd.read_csv(OMR_CSV)
IMAGE_WIDTH = Image.open(next(OMR_IMAGES.glob("*.png"))).size[0]Build the DGT1 bottom-up: system segments → page SegmentLine[DiscreteGraphicalTimeline] → top-level SegmentLine[SegmentLine[DiscreteGraphicalTimeline]]. Events and c-maps must be added before a timeline is locked as a child.
noteheads = pd.DataFrame(
{
"start": omr_df["Nodes.Node.Left"].astype(int),
"end": (omr_df["Nodes.Node.Left"] + omr_df["Nodes.Node.Width"]).astype(int),
"onset_beats": omr_df["onset_beats"].astype(float),
"pitch": omr_df["pitch"],
"staff_id": omr_df["staff_id"].astype(int),
"midi_pitch": omr_df["midi_pitch_code"].astype(int),
"top": omr_df["Nodes.Node.Top"].astype(int),
"page": omr_df["@pageIndex"],
"spacing_run_id": omr_df["spacing_run_id"],
}
)
dgt1 = SegmentLine(
length=0,
unit=TimeUnit.pixels,
number_type=NumberType.int,
segment_type=SegmentLine,
inner_segment_type=DiscreteGraphicalTimeline,
uid="dgt1",
)
for page_idx, page_data in noteheads.groupby("page", sort=True):
# Systems ordered by vertical position (top first = reading order)
sys_top = page_data.groupby("spacing_run_id")["top"].min()
sys_order = sys_top.sort_values().index
page = SegmentLine(
length=0,
unit=TimeUnit.pixels,
number_type=NumberType.int,
segment_type=DiscreteGraphicalTimeline,
)
for sys_rank, sys_id in enumerate(sys_order):
sys_data = page_data[page_data["spacing_run_id"] == sys_id]
system = DiscreteGraphicalTimeline(
length=IMAGE_WIDTH,
uid=f"p{page_idx}_s{sys_rank}",
name=f"Page {page_idx + 1}, System {sys_rank + 1}",
)
events = sys_data.drop(columns=["page", "spacing_run_id"])
system.add_events(events.assign(event_type="Notehead").to_dict("records"))
# C-map: pixels → quarters (deduplicated for chords at the same x)
pairs = (
events[["start", "onset_beats"]]
.drop_duplicates("start")
.sort_values("start")
)
if len(pairs) >= 2:
system.add_conversion_map(
TableMap(
x_values=pairs["start"].tolist(),
y_values=pairs["onset_beats"].tolist(),
source_unit="pixels",
target_unit="quarters",
uid=f"p{page_idx}_s{sys_rank}_px_to_qb",
)
)
page.append_segment(system)
dgt1.append_segment(page, name=f"page_{page_idx}")
dgt1SegmentLine[SegmentLine[DiscreteGraphicalTimeline]][dgt1] (22 children)
0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 106425 pixels
├─ page_0 0 ∶ 4950
├─ page_1 4950 ∶ 9900
├─ page_2 9900 ∶∶ 14850
│ ... (16 more children)
├─ page_19 94050 ∶ 99000
├─ page_20 99000 ∶∶ 103950
└─ page_21 103950 ∶ 106425
8. OpenScore (4th Movement Only)
The OpenScore edition covers all 4 movements. We use the flow controller to identify section breaks (movement boundaries) and extract the 4th movement as a child timeline.
OPENSCORE_DIR = DATA_DIR / "OpenScoreSQ"
os_loader = TSVLoader.from_file(
OPENSCORE_DIR / "sq8913219.notes.tsv",
OPENSCORE_DIR / "sq8913219.measures.tsv",
)
os_full = os_loader.create_timeline(uid="openscore_full")
os_fullContinuousLogicalTimeline[openscore_full] (12707 events, 2 children, 2 cmaps)
0 ________________________________ 2447 quarters
├─ notes 0 _______________________________ 2441 (11898 events)
└─ measures 0 ________________________________ 2447 (809 events)
The loader’s create_flow_controller() derives section boundaries from the score’s flow control markup. Splitting at those coordinates creates one region per movement.
os_flow_controller = os_loader.create_flow_controller()
boundaries = os_flow_controller.get_section_boundary_coordinates()
os_full.create_regions_from_boundaries(
[0, *[float(b) for b in boundaries], float(os_full.length.value)], prefix="movement"
)
openscore = os_full.create_child_from_region("movement_4", uid="openscore")
openscoreContinuousLogicalTimeline[openscore] (3382 events)
0 _______________________________ 878.5 quarters
The four movement regions and the extracted child timeline:
os_full.diagram(show={"regions", "children"})ContinuousLogicalTimeline[openscore_full] (16089 events, 3 children, 4 regions, 2 cmaps)
0 ________________________________ 2447 quarters
├─ notes 0 _______________________________ 2441 (11898 events)
├─ measures 0 ________________________________ 2447 (809 events)
└─ movement_4 1568.5 ____________ 2447 (3382 events)
┄ movement_1 0 ▐═════════▌ 880
┄ movement_2 880 ▐═══▌ 1271.5
┄ movement_3 1271.5 ▐══▌ 1568.5
┄ movement_4 1568.5 ▐══════════▌ 2447
9. Score Group (Group 4)
All three score representations in one TimelineGroup. Cross-domain coordinate transfer (pixels ↔︎ quarters) works via linear interpolation.
score_group = TimelineGroup(
id="score",
name="Score (ABC + OMR + OpenScore)",
timelines=[clt1, dgt1, openscore],
)
score_groupTimelineGroup[score] (3 timelines, 2 timestamps) ┌─────────────────────────────────────────────────────────────────────────┐ │ ContinuousLogicalTimeline[clt1] (3768 events, 3 children, 2 cmaps) │ │ 0 ___________________________ 878.5 quarters │ │ ├─ notes 0 __________________________ 872.5 (3156 events) │ │ ├─ measures 0 ___________________________ 878.5 (226 events) │ │ └─ annotations 0 ___________________________ 878.5 (386 events) │ │ │ │ SegmentLine[SegmentLine[DiscreteGraphicalTimeline]][dgt1] (22 children) │ │ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 106425 pixels │ │ ├─ page_0 0 ∶ 4950 │ │ ├─ page_1 4950 ∶ 9900 │ │ ├─ page_2 9900 ∶ 14850 │ │ │ ... (16 more children) │ │ ├─ page_19 94050 ∶∶ 99000 │ │ ├─ page_20 99000 ∶ 103950 │ │ └─ page_21 103950 ∶ 106425 │ │ │ │ ContinuousLogicalTimeline[openscore] (3382 events) │ │ 0 ___________________________ 878.5 quarters │ └─────────────────────────────────────────────────────────────────────────┘ Timestamps: 2
9.1 Cross-Domain Section Boundaries (Quarters → Pixels → Pages)
The playthrough section boundaries (from §6.1) can now be mapped through the score group to DGT1 pixel coordinates. This demonstrates cross-domain coordinate transfer within a TimelineGroup: the InterpolationMap between CLT1 (quarters) and DGT1 (pixels) uses each system’s pixel-to-quarter TableMap as its C-map anchor.
# Build a page-boundary lookup from DGT1's segment structure
_page_bounds = []
for _seg_id in dgt1.list_segments():
_off = dgt1.get_child_offset(_seg_id)
_seg = dgt1.get_child(_seg_id)
_page_bounds.append(
(float(_off.value), float(_off.value) + float(_seg.length.value))
)
_section_rows = []
for _sid, _qb in abc_controller.get_atomic_section_coordinates(flow=abc_flow).items():
_ts = score_group.get_timestamp_at(float(_qb), "clt1")
_px = _ts.to_dict().get("dgt1")
_page = next(
(i + 1 for i, (s, e) in enumerate(_page_bounds) if s <= _px < e),
"-",
)
_section_rows.append(
{"section": _sid, "quarters": float(_qb), "dgt1_pixels": _px, "page": _page}
)
section_boundary_table = pd.DataFrame(_section_rows).set_index("section")
section_boundary_table| quarters | dgt1_pixels | page | |
|---|---|---|---|
| section | |||
| A | 0.0 | 0 | 1 |
| B | 64.0 | 7753 | 2 |
| C | 128.0 | 15506 | 4 |
| D | 192.0 | 23260 | 5 |
| E | 253.0 | 30649 | 7 |
| F | 317.0 | 38403 | 8 |
| G | 448.5 | 54333 | 11 |
| H | 496.5 | 60148 | 13 |
| I | 525.0 | 63601 | 13 |
| J | 557.0 | 67477 | 14 |
| K | 561.0 | 67962 | 14 |
| L | 589.0 | 71354 | 15 |
| M | 621.0 | 75230 | 16 |
Each atomic section’s start coordinate is located precisely on a specific page of the OMR score image. The pixel column gives the linearised x-coordinate across all 22 pages; the page column tells which score image to open.
9.2 Unfolding the Entire Score Group
The score has repeats and volta brackets. Rather than unfolding each timeline individually, TimelineGroup.unfold() does it in one call: the flow controller’s section boundaries are resolved via the group’s interpolation maps, so every timeline — regardless of domain — is sliced and reassembled in playthrough order.
score_group_unfolded = score_group.unfold(
abc_flow, abc_controller, reference_timeline_id="clt1"
)
score_group_unfoldedInterval inconsistency: start=21.0, end=53.0, duration=0.0 (expected 32.0). Recomputing duration from end.
Interval inconsistency: start=21.0, end=53.0, duration=0.0 (expected 32.0). Recomputing duration from end.
Interval inconsistency: start=21.0, end=53.0, duration=0.0 (expected 32.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=168.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=184.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=216.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=88.0, end=248.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=102.0, end=262.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=106.0, end=266.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=280.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=168.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=184.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=216.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=88.0, end=248.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=102.0, end=262.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=106.0, end=266.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=280.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=168.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=184.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=216.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=88.0, end=248.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=102.0, end=262.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=106.0, end=266.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=280.0, duration=0.0 (expected 160.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=355.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=371.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=8.0, end=355.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=371.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=8.0, end=355.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=371.5, duration=0.0 (expected 347.5). Recomputing duration from end.
Interval inconsistency: start=4.0, end=355.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=20.0, end=371.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=4.0, end=355.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=20.0, end=371.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=4.0, end=355.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=20.0, end=371.5, duration=0.0 (expected 351.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=407.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=56.0, end=439.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=152.0, end=535.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=280.0, end=663.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=296.0, end=679.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=328.0, end=711.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=407.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=56.0, end=439.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=152.0, end=535.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=280.0, end=663.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=296.0, end=679.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=328.0, end=711.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=24.0, end=407.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=56.0, end=439.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=120.0, end=503.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=152.0, end=535.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=280.0, end=663.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=296.0, end=679.5, duration=0.0 (expected 383.5). Recomputing duration from end.
Interval inconsistency: start=328.0, end=711.5, duration=0.0 (expected 383.5). Recomputing duration from end.
TimelineGroup[score_unfolded] (3 timelines, 2 timestamps) ┌─────────────────────────────────────────────────────────────────────────┐ │ ContinuousLogicalTimeline[clt1] (9338 events (4669 own), 33 children) │ │ 0 _________________________ 1332.5 quarters │ │ ├─ tl:27 0 _ 32 (116 events) │ │ ├─ tl:28 0 _ 32 (9 events) │ │ ├─ tl:29 0 _ 32 (14 events) │ │ │ ... (27 more children) │ │ ├─ tl:149 621 ______________ 1332.5 (1674 events) │ │ ├─ tl:150 621 _________ 1116 (124 events) │ │ └─ tl:151 621 _________ 1116 (196 events) │ │ │ │ SegmentLine[SegmentLine][dgt1] (4090 events (3991 own), 1 children) │ │ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 135198 pixels │ │ └─ tl:31 0 ∶ 3877 (99 events) │ │ │ │ ContinuousLogicalTimeline[openscore] (4159 events) │ │ 0 ____________________________ 1116 quarters │ └─────────────────────────────────────────────────────────────────────────┘ Timestamps: 2
The unfolded CLT1 carries all note events in playthrough order. Extract them for note matching:
clt1_unfolded = score_group_unfolded.get_timeline("clt1")
abc_notes_df = clt1_unfolded.get_events(
event_type="Note", include_children=False
).to_pandas()
# Cast types restored from string (EventData stores extra columns as strings)
abc_notes_df["staff"] = pd.to_numeric(abc_notes_df["staff"], errors="coerce").astype(
"Int64"
)
abc_notes_df["tied"] = pd.to_numeric(abc_notes_df["tied"], errors="coerce")
abc_notes_df.loc[abc_notes_df["tied"] == 0, "tied"] = np.nan
abc_notes_df["quarterbeats_playthrough"] = abc_notes_df["start"]
abc_prepared = prepare_abc_notes_for_matching(abc_notes_df)
len(abc_prepared) # note onsets after dropping tied notes3763
10. Aligning Recordings with the Score via Note Matching
Each EEP recording’s note events (seconds, pitch, staff) are matched against the ABC unfolded score notes (quarterbeats, pitch, staff) prepared in §9.2 using greedy sequential matching. The result: MatchClaim objects that connect recording coordinates to score coordinates. No pre-computed TSV is needed — the unfolded CLT1 carries all the notes.
Match each recording against the score. The source_timeline_id and target_timeline_id are the audio DPT and CLT1 respectively — these appear in the resulting MatchClaim anchors.
We use rv.store.notes to access the EEP notes from the XML manifest’s score section — no direct EepNotesLoader import needed.
match_results = {}
for xml_path, dpt_id in [
(NORMAL_XML, "dpt1"),
(MECHANICAL_XML, "dpt6"),
(EXAGGERATED_XML, "dpt11"),
]:
rv = RepoVizzLoader.from_file(xml_path)
eep_events = rv.store.notes.to_pandas()
eep_prepared = prepare_eep_notes_for_matching(eep_events)
match_results[dpt_id] = match_notes_by_attributes(
eep_prepared,
abc_prepared,
match_columns=["pitch", "staff"],
source_coord_column="start",
target_coord_column="quarterbeats_playthrough",
source_timeline_id=dpt_id,
target_timeline_id="clt1",
)
normal_match = match_results["dpt1"]
mechanical_match = match_results["dpt6"]
exaggerated_match = match_results["dpt11"]{
"Normal": normal_match.summary(),
"Mechanical": mechanical_match.summary(),
"Exaggerated": exaggerated_match.summary(),
}{'Normal': {'matched': 3740,
'unmatched_source': 16,
'unmatched_target': 23,
'match_claims': 3740},
'Mechanical': {'matched': 3743,
'unmatched_source': 13,
'unmatched_target': 20,
'match_claims': 3743},
'Exaggerated': {'matched': 2650,
'unmatched_source': 4,
'unmatched_target': 1113,
'match_claims': 2650}}
Part II Summary
The score group unites 3 score representations across 2 domains (Logical + Graphical). Note matching produced MatchClaims connecting each recording group’s audio timeline to CLT1:
| Recording | Matched | Unmatched EEP | Unmatched ABC |
|---|---|---|---|
| Normal | 3,740 | 16 | 23 |
| Mechanical | 3,743 | 13 | 20 |
| Exaggerated | 2,650 | 4 | 1,113 |
Next: Part III adds the Emerson group and demonstrates cross-group coordinate transfer using an AlignmentBundle.
Part III: Emerson Recording + Cascading Alignment (Group 5)
The Emerson group connects a commercial recording to a second score edition via segment-level alignment. Unlike the EEP groups (per-note alignment), the Emerson recording is aligned at the level of 10 structural sections (alpha through kappa), derived from the score’s repeat structure.
The central payoff of this notebook is cascading alignment: by adding the recordings edition’s unfolded score (CLT2) to the same group as CLT1, coordinate transfer chains automatically from the EEP recordings through both score editions to the Emerson recording.
- CLT2: ABC v1.0 (“recordings edition”) score —
ContinuousLogicalTimeline - DPT16: Emerson String Quartet recording (DG 1997) —
ContinuousPhysicalTimeline
11. Building the Emerson Recording Components
11.1 CLT2: Recordings Edition Score
The recordings edition uses the same measure/repeat structure as CLT1 but was encoded independently (ABC v1.0). We load it via TSVLoader and use its flow controller to compute the traversal map.
REC_DIR = DATA_DIR / "recordings"
rec_loader = TSVLoader.from_file(
REC_DIR / "Beethoven_Op018No4-04.notes.tsv",
REC_DIR / "Beethoven_Op018No4-04.measures.tsv",
REC_DIR / "Beethoven_Op018No4-04.harmonies.tsv",
)
clt2 = rec_loader.create_timeline(uid="clt2")
clt2ContinuousLogicalTimeline[clt2] (3765 events, 3 children, 2 cmaps)
0 _________________________________ 876 quarters
├─ notes 0 ________________________________ 870 (3153 events)
├─ measures 0 _________________________________ 876 (226 events)
└─ annotations 0 _________________________________ 876 (386 events)
11.2 Flow Control: Inspect the Score’s Repeat Structure
The loader’s create_flow_controller() identifies atomic sections and flow control events (repeats, voltas) from the measure data.
rec_controller = rec_loader.create_flow_controller()
rec_controllerScoreFlowController (226 MCs, 13 atomic sections, 18 flow events)
├─A──┤├─B──┤├─C──┤ D ├─E──┤ F ├─G──┤├─H──┤├─I──┤ J ├─K──┤ L ├─M──┤
1-9 10-18 19-27 28 29-43 44 45-77 78-84 85-93 94 95-10 102 103-2
║: :║║: :║║: :║ ║: :║ ║: :║║: :║ ║: :║
┌1─ ┌2─ ┌1─ ┌2─
Flow control:
MC 1: repeat_start (section A)
MC 9: repeat_end → MC 1
MC 10: repeat_start (section B)
MC 18: repeat_end → MC 10
MC 19: repeat_start (section C)
MC 27: repeat_end → MC 19
MC 29: repeat_start (section E)
MC 44: repeat_end → MC 29
MC 44: volta 1 (section F)
MC 45: volta 2 (section G)
MC 78: repeat_start (section H)
MC 84: repeat_end → MC 78
MC 85: repeat_start (section I)
MC 93: repeat_end → MC 85
MC 95: repeat_start (section K)
MC 102: repeat_end → MC 95
MC 102: volta 1 (section L)
MC 103: volta 2 (section M)
Section transitions:
A → [A, B] B → [B, C] C → [C, D] D → [E]
E → [F, G] F → [E] G → [H] H → [H, I]
I → [I, J] J → [K] K → [L, M] L → [K]
M → []
Compute the default flow (all repeats taken) and a single-pass flow (no repeats, last volta only) for comparison:
default_flow = rec_controller.compute_flow(FlowMode.DEFAULT)
default_flowFlow(default): 226 folded → 291 unfolded (×1.29), 10 sections # MCs Sections Reason ── ─────────── ──────── ────────────── 1 [1, 10) A start 2 [1, 19) A;B repeat → 1 3 [10, 28) B;C repeat → 10 4 [19, 45) C;D;E;F repeat → 19 5 [29, 44) E repeat → 29 6 [45, 85) G;H skip → 45 7 [78, 94) H;I repeat → 78 8 [85, 103) I;J;K;L repeat → 85 9 [95, 102) K repeat → 95 10 [103, 227) M skip → 103 Sequence: A A B B C C D E F E G H H I I J K L K M
single_flow = rec_controller.compute_flow(FlowMode.SINGLE_PASS)
single_flowFlow(single): 226 folded → 224 unfolded (×0.99), 3 sections # MCs Sections Reason ── ─────────── ──────── ────────────── 1 [1, 44) A;B;C;D;E start 2 [45, 102) G;H;I;J;K skip → 45 3 [103, 227) M skip → 103 Sequence: A B C D E G H I J K M
11.3 Unfolding CLT2
The recordings edition has the same repeat structure as CLT1. We unfold it via the standalone create_unfolded_timeline() function, passing the default flow (all repeats taken). The result is a flat timeline with all sections concatenated in playthrough order — coordinates in quarter-beats, suitable for matching against the Emerson CSV’s unfolded floating-measure boundaries.
clt2_unfolded = create_unfolded_timeline(
clt2, default_flow, flow_controller=rec_controller
)
clt2_unfolded._id = "clt2_unfolded"
clt2_unfoldedInterval inconsistency: start=21.0, end=53.0, duration=0.0 (expected 32.0). Recomputing duration from end.
Interval inconsistency: start=21.0, end=53.0, duration=0.0 (expected 32.0). Recomputing duration from end.
Interval inconsistency: start=41.0, end=105.0, duration=0.0 (expected 64.0). Recomputing duration from end.
Interval inconsistency: start=41.0, end=105.0, duration=0.0 (expected 64.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=105.0, duration=0.0 (expected 97.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=105.0, duration=0.0 (expected 97.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=169.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=185.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=217.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=88.0, end=249.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=102.0, end=263.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=106.0, end=267.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=281.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=8.0, end=169.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=185.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=217.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=88.0, end=249.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=102.0, end=263.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=106.0, end=267.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=281.0, duration=0.0 (expected 161.0). Recomputing duration from end.
Interval inconsistency: start=37.0, end=353.0, duration=0.0 (expected 316.0). Recomputing duration from end.
Interval inconsistency: start=53.0, end=369.0, duration=0.0 (expected 316.0). Recomputing duration from end.
Interval inconsistency: start=37.0, end=353.0, duration=0.0 (expected 316.0). Recomputing duration from end.
Interval inconsistency: start=53.0, end=369.0, duration=0.0 (expected 316.0). Recomputing duration from end.
Interval inconsistency: start=4.0, end=353.0, duration=0.0 (expected 349.0). Recomputing duration from end.
Interval inconsistency: start=20.0, end=369.0, duration=0.0 (expected 349.0). Recomputing duration from end.
Interval inconsistency: start=4.0, end=353.0, duration=0.0 (expected 349.0). Recomputing duration from end.
Interval inconsistency: start=20.0, end=369.0, duration=0.0 (expected 349.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=405.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=437.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=501.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=501.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=152.0, end=533.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=280.0, end=661.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=296.0, end=677.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=328.0, end=709.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=108.0, end=489.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=112.0, end=493.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=376.0, end=757.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=24.0, end=405.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=56.0, end=437.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=501.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=120.0, end=501.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=152.0, end=533.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=280.0, end=661.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=296.0, end=677.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=328.0, end=709.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=108.0, end=489.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=112.0, end=493.0, duration=0.0 (expected 381.0). Recomputing duration from end.
Interval inconsistency: start=376.0, end=757.0, duration=0.0 (expected 381.0). Recomputing duration from end.
ContinuousLogicalTimeline[clt2_unfolded] (9350 events (4675 own), 30 children)
0 ________________________________ 1378 quarters
├─ tl:264 0 _ 32 (116 events)
├─ tl:265 0 _ 32 (9 events)
├─ tl:266 0 _ 32 (14 events)
│ ... (24 more children)
├─ tl:300 621 ________________ 1330 (1679 events)
├─ tl:301 621 ___________ 1116 (124 events)
└─ tl:302 621 __________________ 1378 (196 events)
11.4 DPT16: Emerson Recording
The measureMapAudio.csv provides a 10-segment alignment between the unfolded score (floating measures) and the Emerson recording (seconds). Each segment is labelled with a Greek letter (alpha through kappa).
ema_df = pd.read_csv(
REC_DIR / "Beethoven_Op018No4-04_EmersonStringQuartet_DG_measureMapAudio.csv",
sep="\t",
index_col=0,
)
ema_df| measure_score_start | measure_score_end | measure_unfold_start | measure_unfold_end | seconds_start | seconds_end | |
|---|---|---|---|---|---|---|
| α | 0.75 | 8.750 | 0.75 | 8.750 | 0.567007 | 7.381043 |
| β | 0.75 | 16.750 | 8.75 | 24.750 | 7.381043 | 21.823855 |
| γ | 8.75 | 24.750 | 24.75 | 40.750 | 21.823855 | 37.495283 |
| δ | 16.75 | 40.999 | 40.75 | 64.999 | 37.495283 | 59.309161 |
| ε | 25.00 | 39.999 | 65.00 | 79.999 | 59.309161 | 72.699388 |
| ζ | 41.00 | 79.750 | 80.00 | 118.750 | 72.699388 | 106.863129 |
| η | 73.75 | 87.750 | 118.75 | 132.750 | 106.863129 | 118.964393 |
| θ | 79.75 | 95.999 | 132.75 | 148.999 | 118.964393 | 132.918685 |
| ι | 88.00 | 94.999 | 149.00 | 155.999 | 132.918685 | 138.739229 |
| κ | 96.00 | 218.250 | 156.00 | 278.250 | 138.739229 | 241.823152 |
Create DPT16 as a ContinuousPhysicalTimeline in seconds. Unlike the EEP recordings (per-note alignment), the Emerson alignment operates at the level of section boundaries — the coordinates in ema_df will become MatchClaims in §11.5 rather than a C-map.
dpt16_duration = float(ema_df["seconds_end"].iloc[-1])
dpt16 = ContinuousPhysicalTimeline(length=dpt16_duration, uid="dpt16")
dpt16ContinuousPhysicalTimeline[dpt16]
0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241.8 seconds
11.5 Emerson MatchClaims (alpha through kappa)
Each row in the measure-map CSV defines a section boundary: a correspondence between an unfolded floating-measure coordinate on CLT2 and a seconds coordinate on DPT16. We create one MatchClaim per boundary, plus the final end boundary.
These cross-group claims are the key connection between the Emerson recording and the score group.
emerson_claims = []
for _, row in ema_df.iterrows():
anchor = AlignmentAnchor(
timeline_a_id="clt2_unfolded",
coordinate_a=float(row["measure_unfold_start"]),
timeline_b_id="dpt16",
coordinate_b=float(row["seconds_start"]),
)
emerson_claims.append(
MatchClaim(
timeline_a_id="clt2_unfolded",
timeline_b_id="dpt16",
start_anchor=anchor,
metadata=MatchMetadata(
agent="dataset",
decision_criteria="measure_map_audio",
),
)
)
# Final end boundary
final_anchor = AlignmentAnchor(
timeline_a_id="clt2_unfolded",
coordinate_a=float(ema_df["measure_unfold_end"].iloc[-1]),
timeline_b_id="dpt16",
coordinate_b=float(ema_df["seconds_end"].iloc[-1]),
)
emerson_claims.append(
MatchClaim(
timeline_a_id="clt2_unfolded",
timeline_b_id="dpt16",
start_anchor=final_anchor,
metadata=MatchMetadata(
agent="dataset",
decision_criteria="measure_map_audio",
),
)
)
len(emerson_claims)11
12. Bridging the Two AlignmentBundles
12.1 The Key Move: Adding CLT2_unfolded to the Unfolded Score Group
The Unfolded Score Group and the Emerson Group are currently independent: neither shares a timeline with the other, and no MatchClaims connect them. The Emerson MatchClaims (§11.5) link CLT2_unfolded to DPT16 — but CLT2_unfolded is not yet in any group that the bundle’s existing WarpMaps can reach.
The insight: CLT1_unfolded and CLT2_unfolded encode the same music from different editions. By adding CLT2_unfolded to the Unfolded Score Group, any coordinate on CLT1_unfolded can be transferred to CLT2_unfolded via within-group interpolation, and from there to DPT16 via the Emerson MatchLine’s WarpMap. The cascading path:
DPT1 -> (WarpMap) -> CLT1 -> (interpolation) -> CLT2_unfolded -> (WarpMap) -> DPT16
A single additional group membership retroactively enriches every timeline in both groups.
clt1_unfolded = score_group_unfolded.get_timeline("clt1")
score_group_unfolded.add_timeline(
clt2_unfolded,
start=(0.0, "clt1"),
end=(float(clt1_unfolded.length.value), "clt1"),
)
score_group_unfoldedTimelineGroup[score_unfolded] (4 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────────────────┐ │ ContinuousLogicalTimeline[clt1] (9338 events (4669 own), 33 children) │ │ 0 _________________________ 1332.5 quarters │ │ ├─ tl:27 0 _ 32 (116 events) │ │ ├─ tl:28 0 _ 32 (9 events) │ │ ├─ tl:29 0 _ 32 (14 events) │ │ │ ... (27 more children) │ │ ├─ tl:149 621 ______________ 1332.5 (1674 events) │ │ ├─ tl:150 621 _________ 1116 (124 events) │ │ └─ tl:151 621 _________ 1116 (196 events) │ │ │ │ SegmentLine[SegmentLine][dgt1] (4090 events (3991 own), 1 children) │ │ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 135198 pixels │ │ └─ tl:31 0 ∶ 3877 (99 events) │ │ │ │ ContinuousLogicalTimeline[openscore] (4159 events) │ │ 0 ____________________________ 1116 quarters │ │ │ │ ContinuousLogicalTimeline[clt2_unfolded] (9350 events (4675 own), 30 children) │ │ 0 ____________________________ 1378 quarters │ │ ├─ tl:264 0 _ 32 (116 events) │ │ ├─ tl:265 0 _ 32 (9 events) │ │ ├─ tl:266 0 _ 32 (14 events) │ │ │ ... (24 more children) │ │ ├─ tl:300 621 _______________ 1330 (1679 events) │ │ ├─ tl:301 621 __________ 1116 (124 events) │ │ └─ tl:302 621 ________________ 1378 (196 events) │ └────────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2
CLT2_unfolded now appears alongside CLT1, DGT1, and OpenScore in the unfolded score group. The group’s interpolation maps link all four timelines pairwise, bridging quarter-beats and floating measures.
12.2 The Emerson Group
The Emerson group contains only DPT16 — the recording timeline. CLT2_unfolded lives in the score group, and the Emerson MatchClaims connect the two groups via cross-group claims.
emerson_group = TimelineGroup(
id="emerson",
name="Emerson Recording (DG 1997)",
timelines=[dpt16],
)
emerson_groupTimelineGroup[emerson] (1 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────┐ │ ContinuousPhysicalTimeline[dpt16] │ │ 0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241.8 seconds │ └────────────────────────────────────────────────────────────────────┘ Timestamps: 2
13. The AlignmentBundle
The bundle collects all 5 groups and connects them via MatchClaims. Within each group, coordinate transfer uses linear interpolation. Between groups, WarpMaps (built from MatchClaims) enable cross-domain transfer.
bundle = AlignmentBundle(name="Beethoven Op.18/4 — Multimodal Alignment")
bundle.add_group(score_group_unfolded)
bundle.add_group(normal_group)
bundle.add_group(mechanical_group)
bundle.add_group(exaggerated_group)
bundle.add_group(emerson_group)
# Add EEP recording <-> CLT1 match claims
for dpt_id in ["dpt1", "dpt6", "dpt11"]:
bundle.add_match_claims(match_results[dpt_id].match_claims)
# Add Emerson section boundary claims (CLT2_unfolded <-> DPT16)
bundle.add_match_claims(emerson_claims)
bundleAlignmentBundle[bundle:AlignmentBundle_1] TimelineGroup[score_unfolded] (4 timelines, 2 timestamps) ┌─────────────────────────────────────────────────────────────────────────────────┐ │ ContinuousLogicalTimeline[clt1] (9338 events (4669 own), 33 children) │ │ 0 _________________________________ 1332.5 quarters │ │ ├─ tl:27 0 _ 32 (116 events) │ │ ├─ tl:28 0 _ 32 (9 events) │ │ ├─ tl:29 0 _ 32 (14 events) │ │ │ ... (27 more children) │ │ ├─ tl:149 621 __________________ 1332.5 (1674 events) │ │ ├─ tl:150 621 ____________ 1116 (124 events) │ │ └─ tl:151 621 ____________ 1116 (196 events) │ │ │ │ SegmentLine[SegmentLine][dgt1] (4090 events (3991 own), 1 children) │ │ 0 ∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶∶ 135198 pixels │ │ └─ tl:31 0 ∶ 3877 (99 events) │ │ │ │ ContinuousLogicalTimeline[openscore] (4159 events) │ │ 0 ____________________________________ 1116 quarters │ │ │ │ ContinuousLogicalTimeline[clt2_unfolded] (9350 events (4675 own), 30 children) │ │ 0 ____________________________________ 1378 quarters │ │ ├─ tl:264 0 _ 32 (116 events) │ │ ├─ tl:265 0 _ 32 (9 events) │ │ ├─ tl:266 0 _ 32 (14 events) │ │ │ ... (24 more children) │ │ ├─ tl:300 621 __________________ 1330 (1679 events) │ │ ├─ tl:301 621 _____________ 1116 (124 events) │ │ └─ tl:302 621 ____________________ 1378 (196 events) │ └─────────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2 TimelineGroup[normal] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt1] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11753638 │ │ │ │ DiscretePhysicalTimeline[dpt2] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11195 │ │ │ │ DiscretePhysicalTimeline[dpt3] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 22389 │ │ │ │ DiscretePhysicalTimeline[dpt4] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 45844 │ │ │ │ DiscretePhysicalTimeline[dpt5] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 63965 │ └────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2 TimelineGroup[mechanical] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt6] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 12426696 │ │ │ │ DiscretePhysicalTimeline[dpt7] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 11836 │ │ │ │ DiscretePhysicalTimeline[dpt8] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 23671 │ │ │ │ DiscretePhysicalTimeline[dpt9] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 48469 │ │ │ │ DiscretePhysicalTimeline[dpt10] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 67628 │ └────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2 TimelineGroup[exaggerated] (5 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────────────┐ │ DiscretePhysicalTimeline[dpt11] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 samples │ │ ├─ Cardioid ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Binaural ... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ ├─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ └─ Piezo pic... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 8197748 │ │ │ │ DiscretePhysicalTimeline[dpt12] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 7809 │ │ │ │ DiscretePhysicalTimeline[dpt13] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 15616 │ │ │ │ DiscretePhysicalTimeline[dpt14] (6 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 samples │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ ├─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ └─ essentia.... 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 31975 │ │ │ │ DiscretePhysicalTimeline[dpt15] (4 children, 1 cmaps) │ │ 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 samples │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ ├─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ │ └─ Bow Angle 0 ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅ 44614 │ └────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2 TimelineGroup[emerson] (1 timelines, 2 timestamps) ┌────────────────────────────────────────────────────────────────────────────┐ │ ContinuousPhysicalTimeline[dpt16] │ │ 0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241.8 seconds │ └────────────────────────────────────────────────────────────────────────────┘ Timestamps: 2 MatchClaims: 10144
Match claims per connection:
pd.DataFrame(
[
{
"recording": name,
"source": dpt_id,
"target": "clt1",
"matched": match_results[dpt_id].n_matched,
"unmatched_source": match_results[dpt_id].n_unmatched_source,
"unmatched_target": match_results[dpt_id].n_unmatched_target,
}
for name, dpt_id in [
("Normal", "dpt1"),
("Mechanical", "dpt6"),
("Exaggerated", "dpt11"),
]
]
+ [
{
"recording": "Emerson",
"source": "clt2_unfolded",
"target": "dpt16",
"matched": len(emerson_claims),
"unmatched_source": 0,
"unmatched_target": 0,
}
]
).set_index("recording")| source | target | matched | unmatched_source | unmatched_target | |
|---|---|---|---|---|---|
| recording | |||||
| Normal | dpt1 | clt1 | 3740 | 16 | 23 |
| Mechanical | dpt6 | clt1 | 3743 | 13 | 20 |
| Exaggerated | dpt11 | clt1 | 2650 | 4 | 1113 |
| Emerson | clt2_unfolded | dpt16 | 11 | 0 | 0 |
13.1 Explicit MatchLine and WarpMap
Before demonstrating bundle-level coordinate transfer, it is instructive to see the intermediate MatchLine and WarpMap that the bundle constructs internally. The MatchLine orders the 11 Emerson anchors by source coordinate; the WarpMap interpolates between them.
emerson_matchline = MatchLine.from_claims(
emerson_claims, source_timeline_id="clt2_unfolded"
)
emerson_matchlineMatchLine(source='clt2_unfolded', stamps=11, targets=[dpt16])
emerson_warpmap = WarpMap.from_match_line(emerson_matchline, target_timeline_id="dpt16")
emerson_warpmapWarpMap(source='clt2_unfolded', target='dpt16', n_anchors=11)
Verify the WarpMap manually: transfer a coordinate from CLT2_unfolded to DPT16 and compare with a known section boundary:
# The first section boundary from ema_df
first_fm = float(ema_df["measure_unfold_start"].iloc[0])
first_sec = float(ema_df["seconds_start"].iloc[0])
transferred = emerson_warpmap.forward(first_fm)
{
"CLT2_unfolded (floating measures)": first_fm,
"DPT16 expected (seconds)": first_sec,
"DPT16 via WarpMap (seconds)": float(transferred),
}{'CLT2_unfolded (floating measures)': 0.75,
'DPT16 expected (seconds)': 0.567006803,
'DPT16 via WarpMap (seconds)': 0.567006803}
14. Cross-Group Coordinate Transfer
The bundle’s get_timestamp_at() method is the primary interface for cross-domain coordinate transfer. Given a coordinate on any timeline, it returns corresponding coordinates on all connected timelines — regardless of domain. With CLT2_unfolded bridging the score group and the Emerson MatchClaims, the bundle now reaches all 5 groups.
14.1 Inspecting CLT1’s Harmony Annotations
Before transferring coordinates, let us see what harmonic events live on CLT1. The annotations child carries all harmony labels from the ABC score:
annotations_df = clt1.get_child("annotations").get_events().to_pandas()
annotations_df[["start", "name"]].head(15)| start | name | |
|---|---|---|
| 0 | 0 | c.i |
| 1 | 9 | V65 |
| 2 | 10 | i |
| 3 | 11 | V |
| 4 | 12 | i |
| 5 | 13 | V |
| 6 | 17 | i |
| 7 | 21 | v.iv |
| 8 | 24 | viio7/V |
| 9 | 25 | V(64) |
| 10 | 26 | It6 |
| 11 | 27 | V(64) |
| 12 | 28 | V |
| 13 | 29 | i\\ |
| 14 | 33 | i.V7/iv |
14.2 USE CASE A — Transfer a Harmony Across All Groups
The V7 at quarterbeat 79 (m. 20) is a dominant seventh — one of the most recognisable sonorities. Where does this moment land across all 5 groups, in every domain? The nested format groups results by TimelineGroup:
bundle.get_timestamp_at(79.0, "clt1", format="nested")MatchLine: dropped 11 stamp(s) that do not contain source timeline 'clt1'
MatchLine: dropped 1430 stamp(s) that do not contain source timeline 'dgt1'
MatchLine: dropped 1430 stamp(s) that do not contain source timeline 'openscore'
MatchLine: dropped 1419 stamp(s) that do not contain source timeline 'clt2_unfolded'
{'score_unfolded': {'clt1 (quarters)': 79.0,
'dgt1 (pixels)': 8015,
'openscore (quarters)': 66.16435272045028,
'clt2_unfolded (quarters)': 81.69756097560976},
'normal': {'dpt1 (samples)': 837936,
'dpt2 (samples)': 798,
'dpt3 (samples)': 1596,
'dpt4 (samples)': 3268,
'dpt5 (samples)': 4560},
'mechanical': {'dpt6 (samples)': 911828,
'dpt7 (samples)': 868,
'dpt8 (samples)': 1737,
'dpt9 (samples)': 3556,
'dpt10 (samples)': 4962},
'exaggerated': {'dpt11 (samples)': 848192,
'dpt12 (samples)': 808,
'dpt13 (samples)': 1616,
'dpt14 (samples)': 3308,
'dpt15 (samples)': 4616},
'emerson': {'dpt16 (seconds)': 74.19603380264199}}
Note that the emerson group now appears in the output: the cascading path CLT1 -> CLT2_unfolded -> DPT16 connects the Emerson recording to the rest of the bundle.
The flat format is useful for programmatic access:
bundle.get_timestamp_at(79.0, "clt1", format="flat"){'clt1 (quarters)': 79.0,
'dgt1 (pixels)': 8015,
'openscore (quarters)': 66.16435272045028,
'clt2_unfolded (quarters)': 81.69756097560976,
'dpt1 (samples)': 837936,
'dpt2 (samples)': 798,
'dpt3 (samples)': 1596,
'dpt4 (samples)': 3268,
'dpt5 (samples)': 4560,
'dpt6 (samples)': 911828,
'dpt7 (samples)': 868,
'dpt8 (samples)': 1737,
'dpt9 (samples)': 3556,
'dpt10 (samples)': 4962,
'dpt11 (samples)': 848192,
'dpt12 (samples)': 808,
'dpt13 (samples)': 1616,
'dpt14 (samples)': 3308,
'dpt15 (samples)': 4616,
'dpt16 (seconds)': 74.19603380264199}
14.3 USE CASE B — Reverse Transfer: Emerson to All Groups
The cascading alignment is bidirectional. Starting from a seconds coordinate on DPT16 (the Emerson recording), we can reach every connected timeline — including the three EEP recording groups:
bundle.get_timestamp_at(120.0, "dpt16", format="nested")MatchLine: dropped 1419 stamp(s) that do not contain source timeline 'dpt16'
{'emerson': {'dpt16 (seconds)': 120.0},
'score_unfolded': {'clt1 (quarters)': 129.53290591917968,
'dgt1 (pixels)': 13143,
'openscore (quarters)': 108.48684653343679,
'clt2_unfolded (quarters)': 133.95598075544436}}
A coordinate at 120 seconds into the Emerson recording is mapped through the WarpMap to CLT2_unfolded, then via interpolation to CLT1, and from there via the per-note WarpMaps to DPT1, DPT6, and DPT11 — all in a single call.
14.4 USE CASE C — Section Boundaries Across All Groups
The score’s repeat structure defines atomic sections (A through M). The flow controller (from §6.1) computes each section’s unfolded quarterbeat start coordinate. With the Emerson group now connected, the boundary table includes DPT16:
section_coords = abc_controller.get_atomic_section_coordinates(flow=abc_flow)
section_coords{'A': Fraction(0, 1),
'B': Fraction(64, 1),
'C': Fraction(128, 1),
'D': Fraction(192, 1),
'E': Fraction(253, 1),
'F': Fraction(317, 1),
'G': Fraction(897, 2),
'H': Fraction(993, 2),
'I': Fraction(525, 1),
'J': Fraction(557, 1),
'K': Fraction(561, 1),
'L': Fraction(589, 1),
'M': Fraction(621, 1)}
boundary_df = pd.DataFrame(
[
bundle.get_timestamp_at(float(qb), "clt1", format="flat")
for qb in section_coords.values()
],
index=list(section_coords.keys()),
)
boundary_df.index.name = "section"
boundary_df| clt1 (quarters) | dgt1 (pixels) | openscore (quarters) | clt2_unfolded (quarters) | dpt1 (samples) | dpt2 (samples) | dpt3 (samples) | dpt4 (samples) | dpt5 (samples) | dpt6 (samples) | dpt7 (samples) | dpt8 (samples) | dpt9 (samples) | dpt10 (samples) | dpt11 (samples) | dpt12 (samples) | dpt13 (samples) | dpt14 (samples) | dpt15 (samples) | dpt16 (seconds) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| section | ||||||||||||||||||||
| A | 0.0 | 0 | 0.000000 | 0.000000 | 44100 | 42 | 84 | 172 | 240 | 44100 | 42 | 84 | 172 | 240 | 44100 | 42 | 84 | 172 | 240 | -0.071809 |
| B | 64.0 | 6494 | 53.601501 | 66.185366 | 659662 | 628 | 1257 | 2573 | 3590 | 725796 | 691 | 1383 | 2831 | 3950 | 677580 | 645 | 1291 | 2643 | 3688 | 60.367316 |
| C | 128.0 | 12987 | 107.203002 | 132.370732 | 1388046 | 1322 | 2644 | 5414 | 7554 | 1509834 | 1438 | 2876 | 5889 | 8217 | 1406333 | 1340 | 2679 | 5485 | 7654 | 118.636563 |
| D | 192.0 | 19481 | 160.804503 | 198.556098 | 2055059 | 1957 | 3915 | 8016 | 11184 | 2232624 | 2126 | 4253 | 8708 | 12150 | 2063790 | 1966 | 3931 | 8050 | 11232 | 174.623478 |
| E | 253.0 | 25670 | 211.893433 | 261.639024 | 2487974 | 2370 | 4739 | 9704 | 13540 | 2905988 | 2768 | 5535 | 11334 | 15815 | 2684332 | 2557 | 5113 | 10470 | 14609 | 227.816407 |
| F | 317.0 | 32163 | 265.494934 | 327.824390 | 3370708 | 3211 | 6421 | 13147 | 18344 | 3625860 | 3454 | 6907 | 14142 | 19732 | 3334448 | 3176 | 6352 | 13006 | 18147 | 283.625382 |
| G | 448.5 | 45506 | 375.629268 | 463.814634 | 4998366 | 4761 | 9521 | 19496 | 27202 | 5393668 | 5137 | 10274 | 21037 | 29353 | 4963792 | 4728 | 9456 | 19361 | 27014 | 398.295385 |
| H | 496.5 | 50376 | 415.830394 | 513.453659 | 5252309 | 5003 | 10005 | 20486 | 28584 | 5669764 | 5400 | 10800 | 22114 | 30856 | 5208528 | 4962 | 9922 | 20316 | 28346 | 440.152116 |
| I | 525.0 | 53268 | 439.699812 | 542.926829 | 5552728 | 5289 | 10577 | 21658 | 30219 | 6400772 | 6097 | 12193 | 24966 | 34834 | 5870768 | 5592 | 11183 | 22899 | 31950 | 465.004550 |
| J | 557.0 | 56514 | 466.500563 | 576.019512 | 5861765 | 5583 | 11166 | 22863 | 31901 | 6423992 | 6119 | 12237 | 25056 | 34960 | 5827762 | 5551 | 11101 | 22731 | 31716 | 492.909037 |
| K | 561.0 | 56920 | 469.850657 | 580.156098 | 5930346 | 5648 | 11296 | 23131 | 32274 | 6401064 | 6097 | 12193 | 24967 | 34836 | 5868885 | 5591 | 11180 | 22891 | 31940 | 496.397098 |
| L | 589.0 | 59761 | 493.301313 | 609.112195 | 6210052 | 5915 | 11829 | 24222 | 33796 | 6729385 | 6410 | 12818 | 26247 | 36622 | 6144432 | 5853 | 11705 | 23966 | 33439 | 520.813524 |
| M | 621.0 | 63008 | 520.102064 | 642.204878 | 6550318 | 6239 | 12477 | 25549 | 35648 | 7058514 | 6723 | 13445 | 27531 | 38414 | 6482352 | 6175 | 12348 | 25284 | 35278 | 548.718012 |
Each row gives the exact coordinate of a section boundary in every timeline and domain — including the Emerson recording’s dpt16 column. The sample counts are integers; the seconds and quarterbeats are floats — matching each timeline’s native type.
15. Summary & Key Takeaways
“Any two events in the bundle can be related with each other — regardless of whether they live on the same timeline, in the same group, or even in the same domain — as long as a path of MatchClaims or ConversionMaps connects them.”
The Cascading Alignment Pattern
The central demonstration of this notebook is that a single additional group membership retroactively enriches every timeline already present in the bundle. Adding CLT2_unfolded to the Unfolded Score Group bridges two independent alignment networks:
- EEP recordings (per-note MatchClaims) connect DPT1-DPT15 to CLT1
- Emerson recording (section-boundary MatchClaims) connects DPT16 to CLT2_unfolded
- CLT2_unfolded in the score group bridges the two via within-group interpolation
Patterns Demonstrated
| Pattern | Example | Section |
|---|---|---|
build_recording_group() |
Reusable factory for EEP recordings | 2-4 |
TSVLoader.from_file() |
Load ABC score with notes, measures, annotations | 6 |
create_flow_controller() |
Repeat structure + default flow | 6.1 |
SegmentLine nesting |
OMR pages -> systems -> noteheads | 7 |
| Region extraction | OpenScore 4-movement -> movement 4 child | 8 |
| Cross-domain timestamps | Quarters -> pixels -> page number | 9.1 |
TimelineGroup.unfold() |
Unfold entire group via one flow | 9.2 |
match_notes_by_attributes() |
EEP <-> ABC note matching (from unfolded TL) | 10 |
create_unfolded_timeline() |
Unfold a single timeline | 11.3 |
MatchClaim + AlignmentAnchor |
Section-boundary alignment (alpha-kappa) | 11.5 |
add_timeline() on a group |
Bridge independent alignment networks | 12.1 |
MatchLine + WarpMap |
Explicit construction from MatchClaims | 13.1 |
AlignmentBundle |
Multi-group cross-domain transfer | 13 |
get_timestamp_at() |
Universal coordinate transfer | 14 |
| Reverse transfer | DPT16 -> all groups | 14.3 |
| Cascading alignment | EEP <-> Score <-> Emerson via shared group | 12-14 |
5 groups, 18+ timelines, 3 domains, 1 bundle.