Skip to content

Add pluggable AudioSectorReader file backing (1.0) - #58

Open
danifunker wants to merge 9 commits into
Bloomca:mainfrom
danifunker:file-backend-on-1.0
Open

Add pluggable AudioSectorReader file backing (1.0)#58
danifunker wants to merge 9 commits into
Bloomca:mainfrom
danifunker:file-backend-on-1.0

Conversation

@danifunker

Copy link
Copy Markdown
Contributor

Summary

Adds a hardware-independent seam for reading CD-DA tracks from a file/image
(CHD, BIN/CUE, in-memory, network, …) reusing the crate's existing TOC/track
machinery, with no image-format dependencies pulled into the crate. Also bumps
to 1.0.0, the first release carrying the breaking 1.0 API already on main.

This is the file-based-backend work rebuilt on top of the 1.0 API rather than
git-rebased: main and that branch had independently built data-track support, so
a literal rebase would have collided and dragged back the older implementation.
Only the genuinely-unique file backing is ported here.

Public API additions

  • AudioSectorReader trait — implement read_audio_sectors(start_lba, count) for
    any backing that yields raw CD-DA sectors (2352 B, 16-bit signed LE, stereo).
  • read_track(&src, &toc, n) -> Result<Vec<u8>, TrackReadError<E>> — the
    file/image counterpart to CdReader::read_track; TrackReadError keeps a bad
    track request (Toc) separate from a backing failure (Backend(E)).
  • Free create_wav(Vec<u8>) and public lba_to_msf(u32) — the support surface a
    backing needs (WAV wrapping without naming CdReader; building a Toc from
    image metadata).
  • CdReader itself implements AudioSectorReadeked code share the generic read_track` path.

No changes to existing drive behavior.

Examples

  • examples/file_backend.rs — dependency-free ihe seam
    without a decoder).
  • examples/save_data_track.rs — detect a data it
    cooked to a mountable .iso (bounded memory),t command;
    Mode 2 falls back to saving complete raw secto

Docs

  • docs/consuming-cd-da-reader.md — downstream-del,
    sector formats, detect_track_format, the dats Mode 2,
    file backing, and a pre-1.0 → 1.0 migration ta
  • README: "Reading data tracks" and "Reading from a file image" sections.

Verification

cargo fmt --check, cargo clippy --all-targets -- -D warnings, and
cargo test (49 unit + 6 doctests) all pass. fnd (valid RIFF…WAVE, correct sizes). The drive-dependent examples (save_data_track, read_data_track`) are compit path
needs a physical mixed-mode disc.

Note for reviewers

The 1.0.0 bump rides along with this feature. If you'd rather tag the release
independently, land the version commit (`885335ed keep
this PR purely additive — or just squash-merge.

danifunker and others added 3 commits July 12, 2026 18:44
Reconstructs the pluggable file/image backing from the pre-1.0
`file-based-backend` branch on top of main's unified read API, since a
literal rebase would collide with main's independently-built data-track
support.

- backend.rs: AudioSectorReader trait, `read_track` free fn, and
  TrackReadError, with the CdReader impl now reading via
  `read_sector_range(.., &ReadOptions::default())`.
- Expose the support surface the seam needs: free `create_wav` and a
  public `lba_to_msf` (for building a Toc from image metadata).
- examples: file_backend (dependency-free backing) and save_data_track
  (detect -> read cooked -> save .iso -> mount).
- docs/consuming-cd-da-reader.md: downstream-consumer guide covering the
  options model, sector formats, detection, the data-track workflow,
  Mode 1 vs Mode 2, file backing, and a pre-1.0 -> 1.0 migration table.
- README: data-track and file-image sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The data-track example read the entire track into a Vec before writing —
a full data track can be hundreds of MB. Switch it to the streaming API
(open_track_stream_with_options) so cooked/raw chunks are written as they
arrive and peak memory is one ~64 KB chunk regardless of track size.

Both the ISO (Mode1Cooked) and Mode 2 branches now share one
stream_track_to_file helper with a progress line, which also showcases
that reading and streaming run over the same options/read path. Docs
updated to recommend streaming for large images.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First release carrying the breaking 1.0 API (SectorReadFormat, per-read
ReadOptions, detect_track_format) plus the AudioSectorReader file backing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Bloomca

Bloomca commented Jul 14, 2026

Copy link
Copy Markdown
Owner

@danifunker hey, thanks for the PR.

I looked over it but I am a bit confused on how is it supposed to be used. In the description you mentioned the BIN/CUE and network. So is it intended to turn CUE + PCM data into individual files? But if you have that, wouldn't you have tracks as actual tracks and not just PCM data? And if you have a CD image, wouldn't it be easier to mount it into a virtual drive?

@danifunker

Copy link
Copy Markdown
Contributor Author

Thanks for review it so quickly! I may read like a format-conversion feature, but it's really just an abstraction seam - it parses/converts nothing and pulls in no image-format deps. I am hoping for a native rust-only solution for my projects.

AudioSectorReader is a one-method trait: read_audio_sectors(start_lba, count) -> Vec of raw 2352-byte sectors. BIN/CUE, CHD, and "network" were just examples of backings a user could implement - not formats the crate reads. The value is reusing everything above the raw read (track-boundary math incl. the CD-Extra trailing-gap rule, plus create_wav), which is currently welded to the SCSI path. CdReader now implements the trait too, so drive- and file-backed code share one read_track.

To answer your questions:

  • "wouldn't you have tracks as actual tracks, not just PCM?" Answer: Usually no. A CHD, or .bin+.cue, or a raw dump is one contiguous run of sectors + a TOC of offsets (same shape a physical disc presents), not per-track files. Computing where a track really ends; especially the CD-Extra gap, is the non-trivial bit this crate already gets right; the seam lets image-backed code reuse it instead of re-deriving it wrong.
  • "wouldn't mounting a virtual drive be easier?" Answer: Mounting needs OS privileges, is platform-specific, and getting bin/cue audio tracks mounted can be quite the headache, especially in MacOS. This keeps it a pure bytes in → PCM out computation with no drive at all — see examples/file_backend.rs: an in-memory backing, no drive, no deps.

I've already implemented this in one of my cross-platform projects which helps users identify what discs they may be looking at, and provides a way to "listen to the track" in the case users are familiar with the music (think vintage video-game CDs, or older OSTs/Anime CDs).

@Bloomca

Bloomca commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Alright, I am going to review it properly tomorrow! We definitely don't want to bundle the 1.0 update with this PR, I update all releases as a separate PR/commit

@danifunker

Copy link
Copy Markdown
Contributor Author

Sounds fair! I thought you may have overlooked that a bit. Do you want me to revert that part of the PR?

@Bloomca Bloomca left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, apologies for the delay. I think it looks pretty good overall, I have 2 main questions:

  • is it worth making fn read_audio_sectors as &mut self? Or I guess the consumers can wrap the mutating part into their own function
  • do you think it is worth wrapping around the streaming API part? You said that in some project you provide "listen to", but that would require to read the entire track first.

I also think that creating a ToC is kinda awkward if you can't make it automatically. How do you create that in your project?

Comment thread src/lib.rs
Comment thread src/backend.rs Outdated
Comment thread docs/consuming-cd-da-reader.md Outdated
danifunker and others added 2 commits July 20, 2026 17:33
Address PR Bloomca#58 review feedback:

- Fold the backing error into CdReaderError::Backend(Box<dyn Error + Send +
  Sync>); read_track now returns CdReaderError and the generic TrackReadError<E>
  is gone, so file- and drive-backed reads share one error type. The backing's
  own error is preserved as the boxed source().
- Add TrackBounds { PhysicalDisc, GaplessImage }. The CD-Extra trailing-gap rule
  is correct for a physical disc but wrong for a gapless CHD/BIN extract, where
  it would drop ~2.5 min off the last audio track before a data session. Both
  the buffered path (read_track_with_bounds) and the stream take the geometry;
  PhysicalDisc stays the default, so existing behaviour is unchanged.
- Add AudioTrackStream, the file/image counterpart to TrackStream (next_chunk,
  seek_to_sector/seconds, current/total_sectors/seconds, with_sectors_per_chunk).
  Open it with open_track_stream (TOC + physical), open_track_stream_with_bounds
  (TOC + explicit geometry), or open_track_stream_at (raw absolute sector range,
  for backings that compute their own layout). "Listen to" can now stream
  instead of buffering a whole track.
- Drop docs/consuming-cd-da-reader.md; the file-backing story lives in the
  backend module docs.
- Demonstrate streaming in examples/file_backend.rs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danifunker

Copy link
Copy Markdown
Contributor Author

Thanks for working with me on this review! I've addressed all of it - summary below.

ToC creation
Agreed hand-authoring is awkward; in practice it isn't hand-authored. A real backing derives it from the container's own TOC: in my consumer, libchdman-rs's list_tracks gives per-track type + frame counts, and I fold cumulative frame offsets into Track/Toc (is_audio from TrackType::Audio, start_lba/start_msf from the running total, leadout_lbafrom the grand total). BIN/CUE parses the .cue the same way. The manual construction only appears in the dependency-free in-memory example.

&mut self vs &self
Kept it &self, and I can point at a concrete reason: my real CHD backing re-opens the extracted BIN per read (positioned seek+read), so read_audio_sectors is naturally &self&mut self would just force interior mutability onto the drive impl and turn read_track(&R) into &mut R for no gain. It also matches CdReader. If you'd rather optimise for a persistent-handle backing and take &mut self, say so and I'll flip it.

Streaming
This was a great idea, so I included this as part of he PR. read_audio_sectors(start, count) was already chunk-oriented, so there's now an AudioTrackStream mirroring TrackStream (next_chunk, seek_to_sector/ seek_to_seconds, current/total_seconds), so "listen to" streams into the sink instead of buffering the whole track. Buffered read_track` stays for the simple case; no trait change.

One thing worth flagging
A wrinkle in the track-bounds math for file backings. get_track_bounds subtracts the ~11,400-sector CD-Extra inter-session gap from the last audio track before a data session. That's correct whenever that gap is actually part of the track addressing — a physical disc, and equally an image whose TOC preserves the disc's real LBAs. It's only wrong when the tracks are addressed back-to-back with the gap stripped out (e.g. a chdman extractcd-style gapless extract), where subtracting a gap that isn't there lops ~2.5 min of real audio off that track.

Since only the backing knows how its own image is laid out, bounds resolution now takes an explicit TrackBounds:

  • TrackBounds::PhysicalDisc (default) — addressing includes the inter-session gap; applies the CD-Extra rule. Matches CdReader::read_track, so a physical disc or a gap-preserving image is unchanged.
  • TrackBounds::GaplessImage — tracks are contiguous (gap stripped); spans start_lba(n) .. start_lba(n+1)|leadout with no subtraction.

It's on both the buffered path (read_track_with_bounds) and the stream (open_track_stream_with_bounds), plus a raw open_track_stream_at(start_lba, sectors) for backings that compute their own layout. The variant names lean on
the typical case for each.

danifunker and others added 2 commits July 20, 2026 18:31
The CD-Extra gap subtraction applies whenever a TOC's addressing includes the
inter-session gap — a physical disc OR an image that preserves the disc's real
LBAs — and must be skipped only for a gap-stripped contiguous extract. The old
PhysicalDisc/GaplessImage names wrongly implied image == gapless. Rename the
variants to name that geometry instead of the medium:

  PhysicalDisc -> SessionGap   (addressing includes the gap; default)
  GaplessImage -> Gapless      (tracks contiguous, gap stripped)

Reword the module/item docs, the gapless-bounds helper doc, and the example
comment to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reading a raw optical device can require more rights than the calling
process has, and open_path() has no way to gain them — the descriptor
has to come from elsewhere. On macOS a plain open("/dev/rdiskN") returns
EPERM for an unprivileged GUI app, and the caller's only recourse today
is to fail.

Add CdReader::from_file(File) (unix only) so a caller that hits EPERM /
EACCES can escalate through /usr/libexec/authopen — which shows the
native macOS auth dialog and passes the fd back over SCM_RIGHTS — and
hand the resulting handle over. Drive::from_file adopts it on both unix
backends and closes it on drop, exactly as if it had opened the node.

Windows is excluded: its Drive wraps a HANDLE, not a fd.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants