Skip to main content

AudioTrackStream

Struct AudioTrackStream 

Source
pub struct AudioTrackStream<'a, R: AudioSectorReader> { /* private fields */ }
Expand description

A pull-based, sector-aligned stream of raw CD-DA PCM from an AudioSectorReader.

An AudioTrackStream borrows its source and represents a fixed sector range. Each call to next_chunk synchronously reads and returns the next portion of that range. Once all sectors have been consumed, it returns Ok(None).

Unlike read_track, the stream does not allocate or retain the entire track. Callers can process and discard each returned chunk before requesting the next one. Chunks contain complete CD-DA sectors in the format specified by AudioSectorReader; the final chunk may contain fewer sectors than the configured chunk size.

The chunk size can be changed with with_sectors_per_chunk. Stream position is relative to the beginning of its sector range and can be inspected or changed with current_sector, seek_to_sector, and seek_to_seconds.

Create a stream with:

This is the source-independent audio counterpart to TrackStream, which is tied to CdReader and supports drive-specific read options and data-sector formats.

Implementations§

Source§

impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R>

Source

pub fn with_sectors_per_chunk(self, sectors: u32) -> Self

Set the target chunk size in sectors (default 27; a full chunk is sectors_per_chunk * 2352 bytes). Zero is normalized to one.

Source

pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError>

Read the next chunk of PCM, or Ok(None) at end-of-track.

Each chunk is sectors_per_chunk * 2352 bytes except possibly the last.

§Errors

Returns CdReaderError::Backend if the backing read fails. The stream position does not advance on error, so a retry re-reads the same chunk.

Examples found in repository?
examples/bin_cue_backend.rs (line 147)
105fn main() -> Result<(), Box<dyn Error>> {
106    let output_dir = common::fresh_output_dir("bin_cue_backend")?;
107
108    let cue_path = match std::env::args().nth(1) {
109        Some(path) => PathBuf::from(path),
110        None => {
111            println!("No .cue argument given — writing a demo image to work against.\n");
112            write_demo_image(&output_dir)?
113        }
114    };
115
116    let (bin_path, toc) = parse_cue(&cue_path)?;
117    println!("Cue:   {}", cue_path.display());
118    println!("Bin:   {}", bin_path.display());
119    println!(
120        "Toc:   {} tracks, leadout at LBA {}\n",
121        toc.tracks.len(),
122        toc.leadout_lba
123    );
124
125    let image = BinImage {
126        bin: File::open(&bin_path)?,
127    };
128
129    // A single-FILE cue is a contiguous run of sectors, so the CD-Extra
130    // inter-session gap is not part of the addressing. See `explain_bounds`.
131    let bounds = TrackBounds::Gapless;
132    explain_bounds(&image, &toc)?;
133
134    for track in toc.tracks.iter().filter(|track| track.is_audio) {
135        let pcm = read_track_with_bounds(&image, &toc, track.number, bounds)?;
136        let wav_path = output_dir.join(format!("track{:02}.wav", track.number));
137        std::fs::write(&wav_path, create_wav(pcm))?;
138
139        println!("track {:02}: wrote {}", track.number, wav_path.display());
140    }
141
142    // The same backing streams instead of buffering — for a 74-minute image
143    // that is the difference between one chunk and ~650 MB resident.
144    if let Some(first_audio) = toc.tracks.iter().find(|track| track.is_audio) {
145        let mut stream = open_track_stream_with_bounds(&image, &toc, first_audio.number, bounds)?;
146        let (mut chunks, mut bytes) = (0u32, 0usize);
147        while let Some(chunk) = stream.next_chunk()? {
148            chunks += 1;
149            bytes += chunk.len();
150        }
151        println!(
152            "\nstreamed track {:02}: {bytes} bytes in {chunks} chunks ({:.1}s)",
153            first_audio.number,
154            stream.total_seconds()
155        );
156    }
157
158    Ok(())
159}
More examples
Hide additional examples
examples/file_backend.rs (line 94)
37fn main() -> Result<(), Box<dyn std::error::Error>> {
38    let output_dir = common::fresh_output_dir("file_backend")?;
39
40    // A real backing derives this TOC from the image's own track metadata.
41    // Here we fabricate a 2-track disc: 2 seconds + 3 seconds of audio.
42    let track1_sectors = 75 * 2;
43    let track2_sectors = 75 * 3;
44    let total_sectors = track1_sectors + track2_sectors;
45
46    let toc = Toc {
47        first_track: 1,
48        last_track: 2,
49        tracks: vec![
50            Track {
51                number: 1,
52                start_lba: 0,
53                start_msf: lba_to_msf(0),
54                is_audio: true,
55            },
56            Track {
57                number: 2,
58                start_lba: track1_sectors,
59                start_msf: lba_to_msf(track1_sectors),
60                is_audio: true,
61            },
62        ],
63        leadout_lba: total_sectors,
64    };
65
66    // Silence, just for the demo — a real backing decodes actual audio here.
67    let disc = InMemoryDisc {
68        pcm: vec![0u8; total_sectors as usize * 2352],
69    };
70
71    for track in &toc.tracks {
72        let pcm = read_track(&disc, &toc, track.number)?;
73        println!(
74            "track {}: {} bytes ({} sectors)",
75            track.number,
76            pcm.len(),
77            pcm.len() / 2352
78        );
79
80        let wav = create_wav(pcm);
81        let output_path = output_dir.join(format!("track{:02}.wav", track.number));
82        std::fs::write(&output_path, wav)?;
83        println!("  wrote {}", output_path.display());
84    }
85
86    // The same backing can be streamed instead of buffered: pull sector-aligned
87    // chunks so a player never holds a whole track in memory at once. (A backing
88    // whose tracks are addressed contiguously — a gap-stripped extract — would
89    // open with `TrackBounds::Gapless`, or supply its own bounds via
90    // `open_track_stream_at`; this demo TOC has no trailing data track, so plain
91    // `open_track_stream` is equivalent.)
92    let mut stream = open_track_stream(&disc, &toc, 1)?;
93    let (mut chunks, mut bytes) = (0u32, 0usize);
94    while let Some(chunk) = stream.next_chunk()? {
95        chunks += 1;
96        bytes += chunk.len();
97    }
98    println!(
99        "streamed track 1: {bytes} bytes in {chunks} chunks ({:.1}s of audio)",
100        stream.total_seconds()
101    );
102
103    Ok(())
104}
Source

pub fn total_sectors(&self) -> u32

Total number of sectors in this track.

Examples found in repository?
examples/bin_cue_backend.rs (line 171)
165fn explain_bounds(image: &BinImage, toc: &Toc) -> Result<(), Box<dyn Error>> {
166    let Some(track) = last_audio_before_data(toc) else {
167        return Ok(());
168    };
169
170    let gapless =
171        open_track_stream_with_bounds(image, toc, track, TrackBounds::Gapless)?.total_sectors();
172    let physical = match open_track_stream_with_bounds(image, toc, track, TrackBounds::SessionGap) {
173        Ok(stream) => format!("{} sectors", stream.total_sectors()),
174        // The gap is larger than the track, so subtracting it underflows.
175        Err(e) => format!("fails ({e})"),
176    };
177
178    println!(
179        "Track {track:02} is the last audio track before a data track, the one track \
180         the two bounds policies disagree on:\n  \
181         Gapless (used here): {gapless} sectors\n  \
182         SessionGap:          {physical}\n"
183    );
184    Ok(())
185}
Source

pub fn current_sector(&self) -> u32

Current position as a track-relative sector index.

Source

pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError>

Seek to a track-relative sector position (valid range 0..=total_sectors()).

§Errors

Returns CdReaderError::Io containing std::io::ErrorKind::InvalidInput if sector exceeds the track length.

Source

pub fn current_seconds(&self) -> f32

Current position in seconds (75 sectors = 1 second).

Source

pub fn total_seconds(&self) -> f32

Total track duration in seconds (75 sectors = 1 second).

Examples found in repository?
examples/bin_cue_backend.rs (line 154)
105fn main() -> Result<(), Box<dyn Error>> {
106    let output_dir = common::fresh_output_dir("bin_cue_backend")?;
107
108    let cue_path = match std::env::args().nth(1) {
109        Some(path) => PathBuf::from(path),
110        None => {
111            println!("No .cue argument given — writing a demo image to work against.\n");
112            write_demo_image(&output_dir)?
113        }
114    };
115
116    let (bin_path, toc) = parse_cue(&cue_path)?;
117    println!("Cue:   {}", cue_path.display());
118    println!("Bin:   {}", bin_path.display());
119    println!(
120        "Toc:   {} tracks, leadout at LBA {}\n",
121        toc.tracks.len(),
122        toc.leadout_lba
123    );
124
125    let image = BinImage {
126        bin: File::open(&bin_path)?,
127    };
128
129    // A single-FILE cue is a contiguous run of sectors, so the CD-Extra
130    // inter-session gap is not part of the addressing. See `explain_bounds`.
131    let bounds = TrackBounds::Gapless;
132    explain_bounds(&image, &toc)?;
133
134    for track in toc.tracks.iter().filter(|track| track.is_audio) {
135        let pcm = read_track_with_bounds(&image, &toc, track.number, bounds)?;
136        let wav_path = output_dir.join(format!("track{:02}.wav", track.number));
137        std::fs::write(&wav_path, create_wav(pcm))?;
138
139        println!("track {:02}: wrote {}", track.number, wav_path.display());
140    }
141
142    // The same backing streams instead of buffering — for a 74-minute image
143    // that is the difference between one chunk and ~650 MB resident.
144    if let Some(first_audio) = toc.tracks.iter().find(|track| track.is_audio) {
145        let mut stream = open_track_stream_with_bounds(&image, &toc, first_audio.number, bounds)?;
146        let (mut chunks, mut bytes) = (0u32, 0usize);
147        while let Some(chunk) = stream.next_chunk()? {
148            chunks += 1;
149            bytes += chunk.len();
150        }
151        println!(
152            "\nstreamed track {:02}: {bytes} bytes in {chunks} chunks ({:.1}s)",
153            first_audio.number,
154            stream.total_seconds()
155        );
156    }
157
158    Ok(())
159}
More examples
Hide additional examples
examples/file_backend.rs (line 100)
37fn main() -> Result<(), Box<dyn std::error::Error>> {
38    let output_dir = common::fresh_output_dir("file_backend")?;
39
40    // A real backing derives this TOC from the image's own track metadata.
41    // Here we fabricate a 2-track disc: 2 seconds + 3 seconds of audio.
42    let track1_sectors = 75 * 2;
43    let track2_sectors = 75 * 3;
44    let total_sectors = track1_sectors + track2_sectors;
45
46    let toc = Toc {
47        first_track: 1,
48        last_track: 2,
49        tracks: vec![
50            Track {
51                number: 1,
52                start_lba: 0,
53                start_msf: lba_to_msf(0),
54                is_audio: true,
55            },
56            Track {
57                number: 2,
58                start_lba: track1_sectors,
59                start_msf: lba_to_msf(track1_sectors),
60                is_audio: true,
61            },
62        ],
63        leadout_lba: total_sectors,
64    };
65
66    // Silence, just for the demo — a real backing decodes actual audio here.
67    let disc = InMemoryDisc {
68        pcm: vec![0u8; total_sectors as usize * 2352],
69    };
70
71    for track in &toc.tracks {
72        let pcm = read_track(&disc, &toc, track.number)?;
73        println!(
74            "track {}: {} bytes ({} sectors)",
75            track.number,
76            pcm.len(),
77            pcm.len() / 2352
78        );
79
80        let wav = create_wav(pcm);
81        let output_path = output_dir.join(format!("track{:02}.wav", track.number));
82        std::fs::write(&output_path, wav)?;
83        println!("  wrote {}", output_path.display());
84    }
85
86    // The same backing can be streamed instead of buffered: pull sector-aligned
87    // chunks so a player never holds a whole track in memory at once. (A backing
88    // whose tracks are addressed contiguously — a gap-stripped extract — would
89    // open with `TrackBounds::Gapless`, or supply its own bounds via
90    // `open_track_stream_at`; this demo TOC has no trailing data track, so plain
91    // `open_track_stream` is equivalent.)
92    let mut stream = open_track_stream(&disc, &toc, 1)?;
93    let (mut chunks, mut bytes) = (0u32, 0usize);
94    while let Some(chunk) = stream.next_chunk()? {
95        chunks += 1;
96        bytes += chunk.len();
97    }
98    println!(
99        "streamed track 1: {bytes} bytes in {chunks} chunks ({:.1}s of audio)",
100        stream.total_seconds()
101    );
102
103    Ok(())
104}
Source

pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError>

Seek to a track-relative time in seconds, clamped to the track length.

§Errors

Returns CdReaderError::Io containing std::io::ErrorKind::InvalidInput if seconds is negative or not finite.

Auto Trait Implementations§

§

impl<'a, R> Freeze for AudioTrackStream<'a, R>
where &'a R: Freeze,

§

impl<'a, R> RefUnwindSafe for AudioTrackStream<'a, R>

§

impl<'a, R> Send for AudioTrackStream<'a, R>
where &'a R: Send,

§

impl<'a, R> Sync for AudioTrackStream<'a, R>
where &'a R: Sync,

§

impl<'a, R> Unpin for AudioTrackStream<'a, R>
where &'a R: Unpin,

§

impl<'a, R> UnsafeUnpin for AudioTrackStream<'a, R>

§

impl<'a, R> UnwindSafe for AudioTrackStream<'a, R>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.