Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Notable changes to `samplerdisc`. Format-level findings live in [docs/formats/](

### Added

- **Every partition of an AKAI disc is read, not just the first.** An AKAI disc is a disk image — several partitions laid end to end — and the walk stopped at the one the origin resolved to. Across the 44 AKAI discs on the shelf that is the difference between **448 volumes and 14 670 files** and **2 154 volumes and 68 997 files**; the collection goes from 872 volumes and 56 662 files to **2 578 and 110 989**. `Loop Soup` alone goes from 7 volumes to 60. ([docs/formats/akai-fs.md](docs/formats/akai-fs.md), [ADR-0023](docs/adr/0023-partitions-come-from-the-table-the-disc-declares.md), [#22](https://github.com/bmxcode/samplerdisc/issues/22))

The partitions are not guessed at. The disk declares them, in a table at `0x4500` of the first partition: a count, that many sizes in blocks, then the disk's total. All 44 discs carry one and on all 44 the sizes sum to the total. A partition is read only where a header sits at the position the table gives and restates the size the table gave it.

Of the 44 174 samples this adds, **44 101 (99.83 %)** carry a payload header whose name matches the directory entry that placed them — the same rate as the partitions already being read. Partition 1's numbers do not move on any disc, and are pinned per disc so they cannot.

- **`list` says how many partitions the disc declares and how many the image holds.** `Kickin' Lunatic Beats 2 CD1` declares eleven and holds one: that image is short of the disc it was made from, and the ten missing partitions were previously an absence with nothing to see. ([ADR-0023](docs/adr/0023-partitions-come-from-the-table-the-disc-declares.md))

- **Emulator IV discs extract their samples.** All three E-IV discs in the reference collection previously listed their banks with correct names and yielded nothing; they now give **449, 2 822 and 828 samples**. E-IV banks carry no `EMULATOR` header — not one occurrence across 1.2 GB — and are reached through a chained `E3S1` sample directory instead, whose big-endian length is what sizes each sample. ([docs/formats/emu3.md](docs/formats/emu3.md), [ADR-0020](docs/adr/0020-read-e-iv-through-its-sample-directory.md))

[ADR-0015](docs/adr/0015-locate-banks-by-signature.md) held this back deliberately and conditionally, on the grounds that one specimen cannot distinguish a format from that disc's quirks. Three discs from two publishers met the condition, and the third earned its place: two constants that hold perfectly on the two Producer Series discs fail outright on the Miroslav Vitous one, and a two-disc study would have written one of them down as fact.
Expand All @@ -16,6 +24,10 @@ Notable changes to `samplerdisc`. Format-level findings live in [docs/formats/](

- **The AKAI partition's block allocation map is read.** At `0x70A`, one u16 per block, as many as the partition declares at `0x00`. It is the disc's own record of what every block holds, and it is verified rather than merely plausible: a file's chain length and the size its directory entry declares come from two different structures, and across all 44 AKAI discs they agree for **14 607 of 14 607 files**, exactly. `tests/test_discs.py` asserts that per disc, so an AKAI image that starts decoding wrongly now has something to fail against instead of presenting as a disc with less on it. ([docs/formats/akai-fs.md](docs/formats/akai-fs.md), [ADR-0022](docs/adr/0022-a-volume-is-explained-by-the-allocation-map.md))

### Changed

- **AKAI samples extract under their partition**: `out/partition-1/VOLUME 001/…` where it was `out/VOLUME 001/…`, on every AKAI disc including single-partition ones. Volume names repeat across a disc's partitions — nearly every one has a `VOLUME 001` — so a flat layout put two libraries' audio in one directory under `_2` suffixes with nothing saying which was which. The batch manifest keys volumes by partition and name for the same reason. ([ADR-0023](docs/adr/0023-partitions-come-from-the-table-the-disc-declares.md))

### Fixed

- **An AKAI volume that lists nothing now says why, in the disc's own words.** Ten volumes across three discs listed empty with no explanation, which is exactly the [ADR-0012](docs/adr/0012-a-probe-must-confirm-a-file.md) signature — it reads as an empty volume rather than as a wrong answer. Each now carries a note naming the block and what the partition's allocation map says is in it: file data on `Advance Orchestra` ×4 and the OMI disc, a free block on `Kickin' Lunatic Beats 2 CD1`, and — for four more on that disc — a block the disc says *is* a volume directory and the image has none at. ([#16](https://github.com/bmxcode/samplerdisc/issues/16), [#17](https://github.com/bmxcode/samplerdisc/issues/17), [ADR-0022](docs/adr/0022-a-volume-is-explained-by-the-allocation-map.md))
Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ All of that vanishes into a working parser. Six months on, the code says *what*
| D12 | E-mu `EMU3` backend ([ADR-0014](adr/0014-one-backend-per-on-disc-format.md), [ADR-0015](adr/0015-locate-banks-by-signature.md)) | done |
| D13 | Roland `S770 MR25A` backend ([ADR-0016](adr/0016-the-s7xx-hierarchy-is-located-not-walked.md), [ADR-0017](adr/0017-the-stereo-side-marker-is-a-character-class.md), [ADR-0018](adr/0018-the-s7xx-sample-rate-is-measured.md)) | done |
| D14 | E-mu Emulator IV bank extraction ([ADR-0020](adr/0020-read-e-iv-through-its-sample-directory.md)) | done |
| D15 | Every partition of an AKAI disc, from the table it declares ([ADR-0023](adr/0023-partitions-come-from-the-table-the-disc-declares.md)) | done |

Across the local collection: 39 of 49 discs, 28 712 samples, 2 864 stereo pairs, 161 audio tracks, 20 skipped entries, 30 s.
Across the local collection, by listing: 71 of 79 images claimed, 2 578 volumes, 110 989 files, 77 620 of them samples. The AKAI discs are 44 of those images and 68 997 of those files, read across 276 partitions — before D15 they were 14 670 files, because only the partition at the origin was read.

## What is not done

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0022 · An AKAI volume's emptiness is explained by the partition's allocation map

**Status:** accepted · 2026-08-21
**Status:** accepted · 2026-08-21 · *amended by [ADR-0023](0023-partitions-come-from-the-table-the-disc-declares.md): "no map, no note" now turns on the header restating its block count, rather than on the count fitting the image, so a partition the image ends inside keeps the map for the blocks it holds.*

## Context

Expand Down
65 changes: 65 additions & 0 deletions docs/adr/0023-partitions-come-from-the-table-the-disc-declares.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ADR-0023 · An AKAI disc's partitions come from the table it declares

**Status:** accepted · 2026-08-21

## Context

An AKAI disc is a **disk image**. The sampler wrote a hard disk of several partitions and the CD is a copy of it, so a disc of 66 000 blocks carries nine partitions of 7680 rather than one. `AkaiBackend.volumes()` walked the partition at the resolved origin and stopped, which is [issue #22](https://github.com/bmxcode/samplerdisc/issues/22): across the 44 AKAI discs on the shelf that listed **448 volumes and 14 670 files** of the **2 154 volumes and 68 997 files** they hold.

The issue recorded partitions as tiling **at multiples of the size declared at `0x00`** — an observation across 44 discs, not a field anyone had read — and named three discs it did not fit. It also named the obvious unread candidate: the 100 u16 at `0x02` that carry the same ramp on every disc.

Both questions have answers, and neither is the one the issue expected.

**The field at `0x02` declares nothing about other partitions.** Its 196 bytes are byte-identical on every disc *and every partition* — `3333 × i`, i = 0…97 — so nothing in it varies with anything. What does vary is the pair after it: the u16 at `0xC6` is the block count at `0x00` plus 47573, and the u16 at `0xC8` is 47, on all 276 partitions measured. That is the header restating its own size, not a table of anyone else's.

**The table is at `0x4500`**, in the first partition, past the largest allocation map that fits in front of it: a u8 count, a u8 flag, that many u16 partition sizes, then the disk's total in blocks. **All 44 discs carry one, and on all 44 the sizes sum to the total.** `Loop Soup` declares nine partitions — eight of 7680 blocks and a last of 4095, totalling 65 535, which is the ceiling u16 block numbers imply.

That last entry is what tiling could not have got right. The sizes are **not all equal**: the final partition is a remainder. Tiling reproduces the same partitions everywhere both can be applied, and then invents a fourteenth on `AKAI.S3000.Sound.Library.1` at block 65 535 that the table does not declare.

The three discs the issue could not explain are explained too, and not by the filesystem. Where a declared partition has no header at its declared position, a header turns up *earlier*, and the slippage accumulates down the disc: `Library.6` by 60 blocks and then 68, `Elektra Vox` from 424 to 2888 over nine partitions. **Every displacement is a whole number of 32 KB MDX blocks**, and every one of those images is `.mdx`. They are incomplete rips, the same finding [#17](https://github.com/bmxcode/samplerdisc/issues/17) reached from inside partition 1; the full table is in [the format doc](../formats/akai-fs.md).

## Decision

**Partitions come from the table the disk declares, and the header at each declared position confirms it.**

Four parts:

**The table places them.** Each partition begins where the sizes before it end. The table is refused unless its sizes sum to its total, which is the check that tells a table from whatever else could land at a fixed offset — the two are written separately and agree on all 44 discs.

**The header confirms them.** A partition is read only where the constant field, the size echo at `0xC6` and the tail at `0xC8` all hold *and* the size the header declares is the size the table gave it. Placement and confirmation come from different structures, which is the shape [ADR-0020](0020-read-e-iv-through-its-sample-directory.md) and [ADR-0021](0021-a-bank-owns-the-run-its-header-declares.md) already use: locate by one thing, confirm by another, and never place anything by arithmetic no field agrees with.

**A declared position with no header is skipped, never searched for.** Because the table gives absolute positions, one missing header costs its own partition and nothing after it — `Best Service Brass Super Section CD1` has an unwritten partition 6 and reads 7, 8 and 9 regardless.

**Block numbers stay the numbers the directory declares.** A `Volume` and a `File` carry the byte offset their blocks count from, and `read_file` adds it. Rewriting block numbers to be disc-relative would have been less plumbing and would have made every note, every allocation-map lookup and every future check speak in numbers no structure on the disc states.

Volume names repeat across partitions — nearly every one has a `VOLUME 001` — so extraction writes each volume **under its partition**: `out/partition-2/SOUP 120/`. `list` prints how many partitions the disk declares against how many the image holds.

## Alternatives rejected

**Tile at multiples of the first partition's size**, as the issue observed. Rejected on the register [ADR-0021](0021-a-bank-owns-the-run-its-header-declares.md) settled: a field the disc states beats a bound inferred between structures, and here the field exists. The measurement agrees — tiling and the table place the same partitions on every disc — but the two disagree exactly where a rule is worth having: the last partition is a remainder rather than a full size, and tiling puts a fourteenth partition on `Library.1` where the disk declares thirteen.

**Chain each header's own declared size**, taking the next partition to begin where this one's `0x00` says it ends. Genuinely attractive: it is also a declared field, and measured across the collection it yields **exactly the same 2 154 volumes and 68 997 files**. Rejected because it is a chain — a missing header takes everything after it with it, since the next position is unknown — and because it is one field where the table is a statement about the disk as a whole. It has the same fourteenth-partition fault as tiling.

**Locate every partition by scanning for the header signature.** This is [ADR-0015](0015-locate-banks-by-signature.md)'s instrument and it recovers 15 006 more files, so it was measured rather than dismissed. Rejected on three counts, each sufficient. The signature is a **sawtooth waveform** — `3333 × i` as PCM — and audio reproduces it: 374 blocks of `Global Trance Mission 2` and 153 of `ProSamples vol.14` match, every one in a block the allocation map calls free. Filtering those with the file-yielding test then *loses* real partitions elsewhere, 94 volumes down to 72 on `Advanced Media Trax 3`. And what it gains comes almost entirely from short images, where the audio is displaced: 7 723 of those files are on `Kickin' Lunatic Beats 2 CD1`, the disc whose partition 1 already extracts nine samples that are not their own ([#23](https://github.com/bmxcode/samplerdisc/issues/23)). Recovering a short image's partitions is deferred to [#25](https://github.com/bmxcode/samplerdisc/issues/25), with the displacements written down.

**Walk partitions in `probe()`/`find_origin`.** Rejected: a partition is an AKAI notion and origin resolution is shared with every backend, so this would put brand knowledge in the layer [ADR-0003](0003-brand-neutral-pluggable-backends.md) keeps clear of it. `Backend.volumes(image, offset)` already means "walk the filesystem rooted here", and for AKAI the filesystem is all of its partitions. The probe still resolves exactly one origin and is not touched.

**Rewrite block numbers as disc-relative when the walk yields them.** No plumbing at all: `read_file` would keep working untouched. Rejected because it discards the only numbers the disc actually states. The allocation map is per partition and indexed by the partition's own block numbers, so the ADR-0022 notes and the chain check would both have to undo the rewrite, and a `list` line would name a block no structure on that disc mentions.

**Nest extraction under the partition only where a disc has more than one.** Backwards compatible, and rejected for making the output layout depend on the disc: a script that finds `VOLUME 001` in one place on one disc and another on the next is worse than one that moved once.

## Consequences

**Good.** The 44 AKAI discs go from 448 volumes and 14 670 files to **2 154 and 68 997**; the collection from 872 and 56 662 to **2 578 and 110 989**. Partition 1 does not move on any disc — 448 volumes and 14 670 files before and after, pinned per disc in `tests/test_discs.py`, which is the control on the origin arithmetic.

**Good.** The new files verify against a structure that did not place them. Of 44 174 samples past the first partition, **44 101 (99.83 %)** carry a payload header whose id, valid byte and name match the directory entry — the same rate as partition 1, whose 19 mismatches include #23's nine. On `Loop Soup` all 3 200 agree exactly, and that is a test.

**Good.** A short image is now a stated fact: `list` prints eleven partitions declared against one present, where before the other ten were an absence nobody could see.

**Bad.** Extraction paths change for every AKAI disc, single-partition ones included. Anyone with a script pointing at `out/VOLUME 001` needs `out/partition-1/VOLUME 001`.

**Bad, and stated plainly.** Roughly fifteen thousand files on short images stay unread, and they are readable — a search would find their partitions ([#25](https://github.com/bmxcode/samplerdisc/issues/25)). That is deliberate: the audio on those images is displaced by the missing blocks, and [#23](https://github.com/bmxcode/samplerdisc/issues/23) is the standing evidence of what displaced audio extracts as.

**Watch for.** A disc whose first partition is damaged. The table lives there, and nowhere else that has been found, so losing it drops the disc back to reading one partition — quietly, since that is also the honest fallback for a disc that declares no table.

**Watch for.** `allocation_map` now takes its block count from a header that restates it rather than from what the image can hold, so a partition the image *ends inside* gets the map for the blocks present. That is what stopped two volumes on `ProSamples vol.17` being empty and silent, and it narrows [ADR-0022](0022-a-volume-is-explained-by-the-allocation-map.md)'s "no map, no note" to a count that is absent or unvouched for.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ If you find yourself writing an ADR with no rejected alternative, you are writin
| [0020](0020-read-e-iv-through-its-sample-directory.md) | Read E-IV through its `E3S1` sample directory | Waiting for a fourth disc; arithmetic on `start`; a signature walk; an `EMU4` backend; a stereo channel count |
| [0021](0021-a-bank-owns-the-run-its-header-declares.md) | An EIII/ESI bank owns the record run its own header declares | Bounding by the directory's `length`; deduplicating headers by address; dropping repeated names; listing the `4k` banks with a note; a looser header match |
| [0022](0022-a-volume-is-explained-by-the-allocation-map.md) | An AKAI volume's emptiness is explained by the partition's allocation map | Rejecting type 0; requiring the directory to parse; using the map as an allocation flag; recovering the displaced directories; calling the damage in the note |
| [0023](0023-partitions-come-from-the-table-the-disc-declares.md) | An AKAI disc's partitions come from the table it declares | Tiling at multiples of the first size; chaining each header's own size; locating headers by signature; walking partitions in the probe; rewriting block numbers as disc-relative; nesting extraction only where a disc has several |
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Backend:

`probe()` runs at every candidate offset during origin detection, so it must be cheap, and specific enough not to match zeros or audio.

`fs/akai.py` walks `Partition → Volume → File` ([akai-fs.md](formats/akai-fs.md)). AKAI caps a partition at 512 MB, so a large disc carries several — walk the table rather than assuming one. `fs/iso9660.py` covers discs whose payload is already WAV or AIFF, which is a meaningful share of the archives; it walks the Joliet supplementary descriptor where a disc carries one, because the primary descriptor's 8.3 names are lossy and on a real disc are not even unique — 61 of Vintage Pro's files share one ([iso9660.md](formats/iso9660.md), [ADR-0019](adr/0019-prefer-joliet-names.md)).
`fs/akai.py` walks `Partition → Volume → File` ([akai-fs.md](formats/akai-fs.md)). A disc is a *disk* of several partitions of 31–63 MB, and the disk lists them in a table it carries at `0x4500` of the first one — the walk reads that table and confirms a header at each position it gives, rather than assuming one partition or multiplying the first one's size out ([ADR-0023](adr/0023-partitions-come-from-the-table-the-disc-declares.md)). Block numbers count from the partition they are in, so a volume and a file carry the offset theirs count from. `fs/iso9660.py` covers discs whose payload is already WAV or AIFF, which is a meaningful share of the archives; it walks the Joliet supplementary descriptor where a disc carries one, because the primary descriptor's 8.3 names are lossy and on a real disc are not even unique — 61 of Vintage Pro's files share one ([iso9660.md](formats/iso9660.md), [ADR-0019](adr/0019-prefer-joliet-names.md)).

Damaged input degrades rather than crashing: entries whose start block or size fall outside the image are skipped and logged. Several of these rips have tail damage, and a disc yielding 400 of 420 samples is a good outcome.

Expand Down
Loading
Loading