How to Load an Audio-to-Audio Alignment (Listen Here!)

ListenHereLoader, dense audio-to-audio alignment matrix, columnar MatchClaimField, complete pairwise topology, get_matchstamp_at across all recordings

How to Load an Audio-to-Audio Alignment (Listen Here!)

Listen Here! aligns several recordings of one work to each other and exports the result as a single JSON file. Each recording is warped onto a shared, equidistant reference grid: per recording, the file stores a times array whose i-th entry is that recording’s clock-time (in seconds) at reference-grid column i. Because every recording is sampled against the same grid, the arrays are parallel and equal length — together they form a dense alignment matrix of shape (recordings × grid columns). This guide loads such an export into a single audio-to-audio with ListenHereLoader.

This is a different shape of data from the score↔︎performance loaders. There, each file relates one score to one performance, and a only emerges once several such bundles are combined. A Listen Here! file instead encodes the alignment of all recordings against one another directly — one file already carries the whole graph. The natural reading is a complete pairwise topology: at every grid column, every unordered pair of recordings is related by a synchronous instant .

We load an existing alignment; nothing here runs an aligner. The warp was computed once, offline, and written to disk; the loader reads it faithfully.

The arc:

  1. Load the export in one call and read the reference recording.
  2. Reach the columnar through the uniform get_field API, and inspect a single claim.
  3. Build the .
  4. The headline: place a point on one recording and read it on all the others.
  5. A note on scale — why the claims live in a column, not a million objects.

Setup

The example data is a real Listen Here! alignment export of six recordings of the whole of Beethoven’s Eroica Variations, Op. 35 — six readings of the entire work warped onto one reference grid. The file is large (about 14 MB of parallel onset arrays), so it is read directly from its place on disk rather than fetched through the test-data helper. The cell below walks up from the notebook to the directory that holds the export.

from __future__ import annotations

import os
from pathlib import Path

from timetoalign.alignment.claims import MatchClaim
from timetoalign.loader.alignment import ListenHereLoader

# Locate the local Listen Here! alignment export by walking up the directory
# tree until an ancestor contains it. This reads a local export file directly;
# it is not a packaged test corpus.
_EXPORT_REL = Path("beethoven_eroica_variations_op35/variation_14/mdw/alignment.json")
_search_roots = [Path.cwd(), *Path.cwd().parents]
if "__file__" in globals():
    _search_roots += list(Path(__file__).resolve().parents)
_alignment_json = next(
    candidate for base in _search_roots if (candidate := base / _EXPORT_REL).exists()
)
/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

ListenHereLoader parses the single alignment JSON file: it reads each recording’s times array, checks that they all index the same reference grid (equal length), and assembles the complete pairwise claim set. from_file() is the one-line form of the standard two-phase loader pattern.

loader = ListenHereLoader.from_file(_alignment_json)
loader

ListenHereLoader

Sources 1 file(s): alignment.json
Claims 1145640
File alignment
Recordings 6
Reference mdw-Wataru-MASHIMO.mp3
Timelines 6 in 6 group(s)
Recording keys mdw-Chang-Gyu-LEE, mdw-Hyo-Eun-PARK, mdw-Martin-NÖBAUER, mdw-Seika-ISHIDA, mdw-Wataru-MASHIMO, mdw-Yau-Nam-NG
Try create_timeline(), create_timelines(), create_bundle()

The loader names the reference recording — the recording whose clock defines the grid origin. Reading the reference matters because it is the anchor against which every other recording was warped; it is, however, just another recording in the bundle, not a privileged hub. Every recording is related to every other directly, so the bundle can be read from any of them.

loader.reference
'mdw-Wataru-MASHIMO.mp3'

Six recordings, reported as sorted stems:

loader.recording_keys
['mdw-Chang-Gyu-LEE',
 'mdw-Hyo-Eun-PARK',
 'mdw-Martin-NÖBAUER',
 'mdw-Seika-ISHIDA',
 'mdw-Wataru-MASHIMO',
 'mdw-Yau-Nam-NG']

2. The whole pairwise claim set, in a column

The alignment is reached through the uniform field API: loader.get_field(MatchClaim) returns a — a columnar, PyArrow-backed store that holds the entire set of pairwise s as Arrow columns and materialises individual MatchClaim objects only on demand. Its length is the complete topology: for R recordings and N grid columns, every unordered pair at every column, i.e. C(R, 2) × N claims. Six recordings give C(6, 2) = 15 pairs, and this 25-minute work is sampled at 76 376 grid columns — so the field holds well over a million claims:

field = loader.get_field(MatchClaim)

{
    "field type": type(field).__name__,
    "claims": len(field),
    "C(6,2) pairs": 15,
    "grid columns": len(field) // 15,
}
{'field type': 'MatchClaimField',
 'claims': 1145640,
 'C(6,2) pairs': 15,
 'grid columns': 76376}

The field is held columnar precisely so that this many claims need not be a million Python objects. Indexing it materialises one on demand — an ordinary pairwise claim relating two recordings at one grid column, carrying the shared provenance the export recorded (Listen Here!’s chroma-feature DTW). We look at a single claim to see the shape; we do not iterate or materialise the whole field.

field[0]
MatchClaim synchronous, instant
Timeline A mdw-Chang-Gyu-LEE:cpt1 @-0.125019
Timeline B mdw-Hyo-Eun-PARK:cpt1 @0
Metadata agent=Listen Here! v0.20.0
Try: claim.get_matchstamp()

3. Build the bundle

create_bundle() assembles the : one seconds per recording, each in its own group, and the complete pairwise claim set tying every recording to every other. The recordings carry no symbolic events — each timeline holds only a length (the recording’s stored duration) and a unit; all the alignment lives in the cross-group claim field, which the bundle keeps columnar rather than exploding into a million claim objects.

bundle = loader.create_bundle()

{
    "timelines": bundle.n_timelines,
    "groups": bundle.n_groups,
}
{'timelines': 6, 'groups': 6}

The bundle’s diagram confirms the shape: six single-timeline groups and the full claim count, read straight from the columnar field (no claim is materialised to count it):

print(bundle.diagram())
AlignmentBundle[bundle:AlignmentBundle_1]

  TimelineGroup[mdw-Chang-Gyu-LEE] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Chang-Gyu-LEE:cpt1]                         │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1442.4 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[mdw-Hyo-Eun-PARK] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Hyo-Eun-PARK:cpt1]                          │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1486.8 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[mdw-Martin-NÖBAUER] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Martin-NÖBAUER:cpt1]                       │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1376.6 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[mdw-Seika-ISHIDA] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Seika-ISHIDA:cpt1]                          │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1457.5 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[mdw-Wataru-MASHIMO] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Wataru-MASHIMO:cpt1]                        │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1527.5 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  TimelineGroup[mdw-Yau-Nam-NG] (1 timelines, 2 timestamps)
  ┌────────────────────────────────────────────────────────────────────────────┐
  │ ContinuousPhysicalTimeline[mdw-Yau-Nam-NG:cpt1]                            │
  │                        0 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1355.1 seconds │
  └────────────────────────────────────────────────────────────────────────────┘
  Timestamps: 2

  MatchClaims: 1145640

4. Place a point on one recording, read it on all the others

Here is the promise that makes audio-to-audio alignment compelling: place a point on one recording, and it is instantly placed on all the others. get_matchstamp_at takes a coordinate on any one recording’s timeline and returns the corresponding coordinate on every recording connected to it.

The query coordinate must land on an exact grid column carried by the field, so we take a real one from the data rather than inventing a value: a claim from roughly the middle of the work, read off the reference recording’s clock.

reference_uid = f"{os.path.splitext(loader.reference)[0]}:cpt1"

reference_claims = field.connecting(reference_uid)
mid_claim = reference_claims[len(reference_claims) // 2]
query_coord = mid_claim.start_anchor.get_coordinate_for(reference_uid)

query_coord
Coordinate(763.76, seconds)

That instant — about twelve and a half minutes into the reference reading — resolves to a spanning all six recordings at once:

stamp = bundle.get_matchstamp_at(query_coord, reference_uid)
stamp
MatchStamp 6 timelines, 5 edges
ID Coordinate Type
mdw-Wataru-MASHIMO:cpt1 763.76 anchor
mdw-Chang-Gyu-LEE:cpt1 762.07395 anchor
mdw-Seika-ISHIDA:cpt1 764.996912 anchor
mdw-Hyo-Eun-PARK:cpt1 742.486554 anchor
mdw-Martin-NÖBAUER:cpt1 704.984267 anchor
mdw-Yau-Nam-NG:cpt1 722.071576 anchor
Try: stamp.get(<tl_id>), stamp.get_coordinate(<tl_id>)

Read across that : the same musical instant the reference reaches at this second falls at a slightly different second in each of the other five recordings — a quicker reading reaches it earlier, a steadier one later. One coordinate, placed once, located in all six recordings in a single query:

{
    "timelines in the stamp": stamp.n_timelines,
    "seconds per recording": {
        tl_id: round(stamp.get_coordinate(tl_id).value, 2)
        for tl_id in sorted(stamp.coordinates)
    },
}
{'timelines in the stamp': 6,
 'seconds per recording': {'mdw-Chang-Gyu-LEE:cpt1': 762.07,
  'mdw-Hyo-Eun-PARK:cpt1': 742.49,
  'mdw-Martin-NÖBAUER:cpt1': 704.98,
  'mdw-Seika-ISHIDA:cpt1': 765.0,
  'mdw-Wataru-MASHIMO:cpt1': 763.76,
  'mdw-Yau-Nam-NG:cpt1': 722.07}}

Because the topology is complete, the same query works from any recording as the anchor, not only the reference — every pair is directly related, so no recording is a required hub.


5. A note on scale

This single file holds more than a million pairwise claims: six recordings give C(6, 2) = 15 pairs, and a 25-minute work sampled at 50 Hz runs to tens of thousands of grid columns. A whole-work export with more recordings would be larger still.

This is exactly why the claims live in a rather than as a million individual MatchClaim objects. The field stores the whole set as Arrow columns (the two timeline ids dictionary-encoded, the two coordinates as float64) and the loader builds it vectorised — never constructing a Python claim per row. A MatchClaim is materialised only when a single row is indexed. get_matchstamp_at likewise filters the column vectorised and materialises only the handful of claims at the queried coordinate. The columnar store is what lets one AlignmentBundle hold a whole-work, every-pair audio-to-audio graph without strain.

Recap

What the bundle expresses How
One recording per group <stem>:cpt1 seconds timeline, no events, length = the recording’s duration
The whole pairwise alignment a columnar via get_field(MatchClaim)
One claim, materialised field[i] → a synchronous instant
The reference recording loader.reference — the grid origin, but just another recording
A point placed everywhere bundle.get_matchstamp_at(coord, "<stem>:cpt1") → every recording at once

A single file encodes an entire audio-to-audio — every recording of one work warped onto a shared reference grid, every pair directly related. ListenHereLoader reads it into one in which a coordinate placed on any one recording resolves, in a single query, across all the others; and the dense claim set that makes that possible is held in a column, not a million objects.