Skip to content

Report a two-lefts stereo collision instead of welding it silently (#11) - #45

Merged
bmxcode merged 1 commit into
mainfrom
d22-two-lefts-guard
Aug 24, 2026
Merged

Report a two-lefts stereo collision instead of welding it silently (#11)#45
bmxcode merged 1 commit into
mainfrom
d22-two-lefts-guard

Conversation

@bmxcode

@bmxcode bmxcode commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Closes #11.

What was wrong

extract_volume collected each mono sample into parsed[entry.name] = sample and then paired stereo halves with find_pairs(list(parsed)). That made the filesystem name both the pairing input and the join lookup key — and a name is not unique on these filesystems.

Where a backend yielded two distinct entries under one name, the second silently overwrote the first in that dict before find_pairs ran. find_pairs is deliberately conservative and refuses to pair a base that has two lefts — that guard exists precisely to catch this — but the dict collapse happened one step upstream of it, so the guard never saw two lefts. The pairing then welded whichever same-named -L was walked last to the -R and reported it as an ordinary Joined, with nothing saying a choice had been made.

That is the exact failure ADR-0007 keeps the mono originals as insurance against: a wrong pairing is silent, produces a stereo file with two unrelated sounds in it, and — for a user who trusts the joined file — is found only in a DAW, weeks later.

How likely it is

Not reachable on any disc in the collection today. ISO 9660 now reads its unique Joliet names (ADR-0019), which closes the one path that previously produced colliding names, and its .EBL short names carried no -L/-R marker in any case. But File.name has no uniqueness guarantee on the AKAI, E-mu or Roland backends, and a non-Joliet disc with colliding short names and a side marker would hit it. Latent, the same way #10's collision was before .EBL support was on the horizon.

The fix

Carry each sample through find_pairs as an opaque payload beside its name, rather than looking it up afterward by a name a second entry may share.

  • extract_volume accumulates mono samples into an ordered list[(name, sample)], not a name-keyed dict. Nothing can be dropped before the pairing sees it. This removes the collapse at its source.
  • find_pairs takes list[(name, payload)] and returns a Pairing: the clean 1-1 matches (each Pair now carrying the two sample objects, not names) and the bases it refused. Only the name drives the match, so it stays a pure name heuristic; the payload just rides along. interleave is unchanged.
  • A base with both sides present in any count other than exactly one-and-one — two lefts and a right, three files — is now reported as Ambiguous, and extract turns each into a Skipped with a reason. That matches how extract already surfaces every other ambiguity it will not resolve on its own. A lone half with no opposite is still silently left as a mono sample, exactly as before.

The name-based join lookup (parsed[pair.left]) is gone entirely, which is what made this more than a one-liner.

What this does not claim

  • It recovers no audio and changes no output on any disc in the collection — the ambiguous path is unreached today. The value is that a real disc which does reach it degrades to a reported Skipped (both mono halves still on disk) instead of a silent wrong join.
  • It does not touch how names are read, deduplicated, or displayed. Uniqueness of names remains the backends' concern (or non-concern, per ADR-0019); this only stops a duplicate from defeating the stereo guard.

Tests

Adds the first test anywhere of what extraction does with two entries sharing a name — the issue's KICK -L / KICK -L / KICK -R repro, end to end against a synthetic AKAI disc: no Joined is produced, a Skipped for base KICK names the ambiguity, both lefts and the right are on disk (KICK -L.wav, KICK -L_2.wav, KICK -R.wav), and no stereo/ directory is created. The find_pairs unit tests move to the new (name, payload) contract, and the two "ambiguous names are left alone" cases now assert the refusal is reported, not merely dropped.

Verify

uv run ruff check .
uv run ruff format --check .
uv run pytest -q
uv tool install --editable . && samplerdisc --version

All four pass.

🤖 Generated with Claude Code

Closes #11.

extract_volume collected each mono sample into a dict keyed by the
filesystem name and paired stereo halves from that dict. A name is not
unique on these filesystems, so where two distinct entries shared one
name the second overwrote the first *before* find_pairs ran -- and its
conservative "two lefts, no pair" guard, which exists precisely to
refuse this, never saw the collision. The result was a stereo WAV welded
from whichever same-named -L was walked last, reported as an ordinary
Joined: the silent, unrecoverable wrong join ADR-0007 keeps the mono
originals as insurance against.

Carry each sample through find_pairs as an opaque payload beside its
name, so no sample is ever collapsed and the guard sees the true
multiset. find_pairs now returns a Pairing of clean matches plus the
bases it refused, and extract yields a Skipped with a reason for each --
the same way it surfaces every other ambiguity it will not guess. The
name-based join lookup is gone; a matched pair hands back the two
sample objects directly.

Not reachable on any disc in the collection today (ISO 9660 reads unique
Joliet names per ADR-0019), but File.name has no uniqueness guarantee on
the AKAI, E-mu or Roland backends, so it was latent. Adds the first test
of what extraction does with two entries sharing a name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bmxcode
bmxcode merged commit ee4a6b8 into main Aug 24, 2026
1 check passed
@bmxcode
bmxcode deleted the d22-two-lefts-guard branch August 24, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate File.name collapses the stereo-pairing dict, defeating find_pairs' two-lefts guard

1 participant