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:
open_track_streamto resolve a track from aTocusingTrackBounds::SessionGap.open_track_stream_with_boundsto resolve a track using an explicitTrackBoundspolicy.open_track_stream_atto stream an explicit absolute sector range without consulting aToc.
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>
impl<'a, R: AudioSectorReader> AudioTrackStream<'a, R>
Sourcepub fn with_sectors_per_chunk(self, sectors: u32) -> Self
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.
Sourcepub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError>
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?
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
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}Sourcepub fn total_sectors(&self) -> u32
pub fn total_sectors(&self) -> u32
Total number of sectors in this track.
Examples found in repository?
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}Sourcepub fn current_sector(&self) -> u32
pub fn current_sector(&self) -> u32
Current position as a track-relative sector index.
Sourcepub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError>
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.
Sourcepub fn current_seconds(&self) -> f32
pub fn current_seconds(&self) -> f32
Current position in seconds (75 sectors = 1 second).
Sourcepub fn total_seconds(&self) -> f32
pub fn total_seconds(&self) -> f32
Total track duration in seconds (75 sectors = 1 second).
Examples found in repository?
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
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}Sourcepub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError>
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.