Skip to main content

audiobox/stream/
file.rs

1//! Native disk reader. Only the worker owns the file and decoder.
2use super::DecoderOptions;
3use crate::decoder::file::FileDecoder;
4use crate::{AudioFormat, Error, ErrorCode, Result};
5use std::path::Path;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
8use std::thread::{self, JoinHandle};
9use std::time::Duration;
10
11/// Resident and input admission limits for a native disk stream.
12#[derive(Clone, Debug)]
13pub struct FileStreamOptions {
14    pub decoder: DecoderOptions,
15    /// PCM ring capacity, 1..=1048576 frames. Default 16384.
16    pub buffer_frames: usize,
17    /// Encoded sliding window, 4096..=16777216 bytes. Default 1 MiB.
18    /// FLAC frames must fit; STREAMINFO's nonzero maximum is checked on opening.
19    pub encoded_buffer_bytes: usize,
20    /// Conservative codec scratch budget, independent of duration. Default 16 MiB.
21    /// Admission includes a fixed 128 KiB allowance plus 20 bytes per block sample.
22    pub max_decoder_bytes: usize,
23    /// Ring samples, generation tags and worker output scratch. Default 32 MiB.
24    pub max_pcm_bytes: usize,
25    /// Maximum RIFF chunks / FLAC metadata blocks, default 4096.
26    pub max_header_chunks: usize,
27    /// Encoded file admission limit, default 8 GiB. Files must fit usize too.
28    pub max_file_bytes: u64,
29}
30impl Default for FileStreamOptions {
31    fn default() -> Self {
32        Self {
33            decoder: DecoderOptions::default(),
34            buffer_frames: 16384,
35            encoded_buffer_bytes: 1024 * 1024,
36            max_decoder_bytes: 16 * 1024 * 1024,
37            max_pcm_bytes: 32 * 1024 * 1024,
38            max_header_chunks: 4096,
39            max_file_bytes: 8 * 1024 * 1024 * 1024,
40        }
41    }
42}
43impl FileStreamOptions {
44    fn validate(&self) -> Result<()> {
45        if !(1..=1_048_576).contains(&self.buffer_frames)
46            || !(4096..=16 * 1024 * 1024).contains(&self.encoded_buffer_bytes)
47            || !(1..=65536).contains(&self.decoder.chunk_frames)
48            || self.max_header_chunks == 0
49            || self.max_header_chunks > 65536
50            || self.max_decoder_bytes == 0
51            || self.max_decoder_bytes > 256 * 1024 * 1024
52            || self.max_pcm_bytes == 0
53            || self.max_pcm_bytes > 256 * 1024 * 1024
54        {
55            return Err(Error::invalid(
56                "Invalid file-stream buffer or header limits",
57            ));
58        }
59        Ok(())
60    }
61}
62
63#[derive(Clone, Copy, Debug)]
64struct Info {
65    channels: usize,
66    rate: u32,
67    frames: Option<u64>,
68    format: AudioFormat,
69}
70/// Snapshot of the reader. Frame positions refer to source audio, not device time.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct FileStreamStatus {
73    /// Requested seek generation; zero is the initial read.
74    pub generation: u64,
75    /// Frames published in the PCM ring (possibly from an older seek generation).
76    pub buffered_frames: usize,
77    /// Read attempts that could not return even one frame. Not a count of silent samples.
78    pub underruns: u64,
79    pub cancelled: bool,
80}
81struct Ring {
82    samples: Box<[AtomicU32]>,
83    generations: Box<[AtomicU64]>,
84    head: AtomicUsize,
85    tail: AtomicUsize,
86    generation: AtomicU64,
87    target: AtomicU64,
88    /// (generation+1)<<8 | status, where 1=EOF and 2..=9 encode errors.
89    terminal: AtomicU64,
90    cancelled: AtomicBool,
91    worker_alive: AtomicBool,
92    underruns: AtomicU64,
93    info: Info,
94}
95impl Ring {
96    fn new(info: Info, capacity: usize) -> Self {
97        Self {
98            samples: (0..capacity * info.channels)
99                .map(|_| AtomicU32::new(0))
100                .collect(),
101            generations: (0..capacity).map(|_| AtomicU64::new(0)).collect(),
102            head: AtomicUsize::new(0),
103            tail: AtomicUsize::new(0),
104            generation: AtomicU64::new(0),
105            target: AtomicU64::new(0),
106            terminal: AtomicU64::new(0),
107            cancelled: AtomicBool::new(false),
108            worker_alive: AtomicBool::new(true),
109            underruns: AtomicU64::new(0),
110            info,
111        }
112    }
113    fn capacity(&self) -> usize {
114        self.generations.len()
115    }
116    fn next(&self, at: usize) -> usize {
117        (at + 1) % (2 * self.capacity())
118    }
119    fn len(&self) -> usize {
120        // A control-thread snapshot may race both counters; clamp for diagnostics.
121        let head = self.head.load(Ordering::Acquire);
122        let tail = self.tail.load(Ordering::Acquire);
123        (tail + 2 * self.capacity() - head) % (2 * self.capacity())
124    }
125    fn push(&self, frame: &[f32], generation: u64) -> bool {
126        let tail = self.tail.load(Ordering::Relaxed);
127        let head = self.head.load(Ordering::Acquire);
128        if (tail + 2 * self.capacity() - head) % (2 * self.capacity()) >= self.capacity() {
129            return false;
130        }
131        let at = tail % self.capacity();
132        for (c, &sample) in frame.iter().enumerate() {
133            self.samples[at * self.info.channels + c].store(sample.to_bits(), Ordering::Relaxed);
134        }
135        self.generations[at].store(generation, Ordering::Relaxed);
136        self.tail.store(self.next(tail), Ordering::Release);
137        true
138    }
139    fn finish(&self, generation: u64, error: Option<ErrorCode>) {
140        self.terminal.store(
141            ((generation + 1) << 8) | error.map_or(1, |e| error_number(e) as u64 + 1),
142            Ordering::Release,
143        );
144    }
145}
146
147/// A nonblocking, allocation-free consumer of a fixed PCM ring. Requires
148/// `file-stream` on native targets; mixer integration additionally needs `mixer`.
149///
150/// Opening waits for header scanning and one prefilled ring, on a loading thread.
151/// WAV/FLAC/MP3 files are read through a bounded encoded window, never loaded whole.
152/// MP3 opening scans frame headers once; compressed seeks replay from the start on
153/// the worker, retaining exact decoder state. They may take time for long files.
154/// Metadata is skipped; this API exposes audio shape, not tags. Files must remain
155/// unchanged while streaming. Loop restarts are asynchronous and can underrun.
156///
157/// Keep the returned [`FileStreamHandle`] on a control thread. Dropping the stream
158/// only requests cancellation; dropping the handle cancels and joins the worker.
159/// Construction, handle destruction and final stream destruction belong off the
160/// callback (the mixer's returned-sound queue provides this for played sources).
161pub struct FileStream {
162    ring: Arc<Ring>,
163    position: u64,
164    generation: u64,
165}
166/// Owns reader lifetime and memory until destruction on a control/loading thread.
167/// Dropping this cancels playback and waits for the reader; never drop in a callback.
168pub struct FileStreamHandle {
169    ring: Arc<Ring>,
170    worker: Option<JoinHandle<()>>,
171}
172impl FileStream {
173    pub fn open(path: impl AsRef<Path>) -> Result<(Self, FileStreamHandle)> {
174        Self::with_options(path, &FileStreamOptions::default())
175    }
176    pub fn with_options(
177        path: impl AsRef<Path>,
178        options: &FileStreamOptions,
179    ) -> Result<(Self, FileStreamHandle)> {
180        options.validate()?;
181        let path = path.as_ref().to_owned();
182        let options = options.clone();
183        let (tx, rx) = std::sync::mpsc::sync_channel(1);
184        let worker = thread::Builder::new()
185            .name("audiobox-file-reader".into())
186            .spawn(move || {
187                let result = FileDecoder::open(&path, &options);
188                let mut decoder = match result {
189                    Ok(d) => d,
190                    Err(e) => {
191                        let _ = tx.send(Err(e));
192                        return;
193                    }
194                };
195                let info = Info {
196                    channels: decoder.channels,
197                    rate: decoder.rate,
198                    frames: decoder.frames,
199                    format: decoder.format,
200                };
201                let pcm_bytes = options
202                    .buffer_frames
203                    .checked_mul(info.channels * 4 + 8)
204                    .and_then(|n| n.checked_add(options.decoder.chunk_frames * info.channels * 4));
205                if pcm_bytes.is_none_or(|n| n > options.max_pcm_bytes) {
206                    let _ = tx.send(Err(Error::limit("PCM ring/scratch exceeds budget")));
207                    return;
208                }
209                let ring = Arc::new(Ring::new(info, options.buffer_frames));
210                let _alive = Alive(ring.clone());
211                let mut scratch = vec![0.0; options.decoder.chunk_frames * info.channels];
212                let initial = fill_initial(&mut decoder, &ring, &mut scratch);
213                if let Err(e) = initial {
214                    let _ = tx.send(Err(e));
215                    return;
216                }
217                if tx.send(Ok(ring.clone())).is_err() {
218                    return;
219                }
220                run(&mut decoder, &ring, &mut scratch);
221            })?;
222        match rx.recv() {
223            Ok(Ok(ring)) => Ok((
224                Self {
225                    ring: ring.clone(),
226                    position: 0,
227                    generation: 0,
228                },
229                FileStreamHandle {
230                    ring,
231                    worker: Some(worker),
232                },
233            )),
234            result => {
235                let _ = worker.join();
236                Err(match result {
237                    Ok(Err(e)) => e,
238                    _ => Error::new(ErrorCode::Io, "Reader exited during opening"),
239                })
240            }
241        }
242    }
243    pub fn sample_rate(&self) -> u32 {
244        self.ring.info.rate
245    }
246    pub fn channels(&self) -> usize {
247        self.ring.info.channels
248    }
249    pub fn frames(&self) -> Option<u64> {
250        self.ring.info.frames
251    }
252    pub fn format(&self) -> AudioFormat {
253        self.ring.info.format
254    }
255    /// Index of the next delivered source frame. A seek updates it immediately.
256    pub fn position(&self) -> u64 {
257        self.position
258    }
259    /// Drain available complete frames. Returns 0 only for actual EOF (or a buffer
260    /// shorter than one frame). A partial read is returned before any pending/error
261    /// state. `Underrun` means retry later; the destination is untouched on that error.
262    /// Error messages are empty so all read paths, including errors, allocate nothing.
263    pub fn read(&mut self, out: &mut [f32]) -> Result<usize> {
264        let ch = self.channels();
265        let wanted = out.len() / ch;
266        if wanted == 0 {
267            return Ok(0);
268        }
269        let ring = &self.ring;
270        let mut written = 0;
271        // Bound stale-generation draining to one ring even if seeks race the reader.
272        for _ in 0..ring.capacity() {
273            if written == wanted {
274                break;
275            }
276            let head = ring.head.load(Ordering::Relaxed);
277            let tail = ring.tail.load(Ordering::Acquire);
278            if head == tail {
279                break;
280            }
281            let at = head % ring.capacity();
282            if ring.generations[at].load(Ordering::Relaxed) == self.generation {
283                for c in 0..ch {
284                    out[written * ch + c] =
285                        f32::from_bits(ring.samples[at * ch + c].load(Ordering::Relaxed));
286                }
287                written += 1;
288            }
289            ring.head.store(ring.next(head), Ordering::Release);
290        }
291        self.position += written as u64;
292        if written > 0 {
293            return Ok(written);
294        }
295        if ring.cancelled.load(Ordering::Acquire) {
296            return Err(empty_error(ErrorCode::Io));
297        }
298        let terminal = ring.terminal.load(Ordering::Acquire);
299        // This acquire observes every frame preceding the terminal publication.
300        let empty = ring.head.load(Ordering::Relaxed) == ring.tail.load(Ordering::Acquire);
301        if empty && terminal >> 8 == self.generation + 1 {
302            if terminal & 255 == 1 {
303                return Ok(0);
304            }
305            if terminal & 255 > 1 {
306                return Err(empty_error(number_error((terminal & 255) as u8 - 1)));
307            }
308        }
309        if !ring.worker_alive.load(Ordering::Acquire) {
310            return Err(empty_error(ErrorCode::Io));
311        }
312        ring.underruns.fetch_add(1, Ordering::Relaxed);
313        Err(empty_error(ErrorCode::Underrun))
314    }
315    /// Queue a seek without waiting, allocating or touching the file. Old queued
316    /// frames are discarded by generation. Next read can underrun while seeking.
317    pub fn seek(&mut self, frame: u64) -> Result<()> {
318        if self.generation >= (u64::MAX >> 8) - 1 {
319            return Err(empty_error(ErrorCode::LimitExceeded));
320        }
321        let frame = self.frames().map_or(frame, |n| frame.min(n));
322        self.position = frame;
323        self.generation += 1;
324        self.ring.target.store(frame, Ordering::Relaxed);
325        self.ring
326            .generation
327            .store(self.generation, Ordering::Release);
328        Ok(())
329    }
330    pub fn rewind(&mut self) -> Result<()> {
331        self.seek(0)
332    }
333}
334impl Drop for FileStream {
335    fn drop(&mut self) {
336        self.ring.cancelled.store(true, Ordering::Release);
337    }
338}
339impl FileStreamHandle {
340    pub fn status(&self) -> FileStreamStatus {
341        FileStreamStatus {
342            generation: self.ring.generation.load(Ordering::Acquire),
343            buffered_frames: self.ring.len().min(self.ring.capacity()),
344            underruns: self.ring.underruns.load(Ordering::Relaxed),
345            cancelled: self.ring.cancelled.load(Ordering::Acquire),
346        }
347    }
348    /// Cancel the reader; joining happens when the handle is dropped off-thread.
349    pub fn cancel(&self) {
350        self.ring.cancelled.store(true, Ordering::Release);
351    }
352}
353impl Drop for FileStreamHandle {
354    fn drop(&mut self) {
355        self.cancel();
356        if let Some(worker) = self.worker.take() {
357            let _ = worker.join();
358        }
359    }
360}
361struct Alive(Arc<Ring>);
362impl Drop for Alive {
363    fn drop(&mut self) {
364        self.0.worker_alive.store(false, Ordering::Release);
365    }
366}
367fn empty_error(code: ErrorCode) -> Error {
368    Error {
369        code,
370        message: String::new(),
371    }
372}
373fn error_number(code: ErrorCode) -> u8 {
374    match code {
375        ErrorCode::UnsupportedFormat => 1,
376        ErrorCode::DecodeError => 2,
377        ErrorCode::EncodeError => 3,
378        ErrorCode::InvalidArgument => 4,
379        ErrorCode::LimitExceeded => 5,
380        ErrorCode::Io => 6,
381        ErrorCode::Syntax => 7,
382        ErrorCode::Underrun => 8,
383    }
384}
385fn number_error(n: u8) -> ErrorCode {
386    match n {
387        1 => ErrorCode::UnsupportedFormat,
388        2 => ErrorCode::DecodeError,
389        3 => ErrorCode::EncodeError,
390        4 => ErrorCode::InvalidArgument,
391        5 => ErrorCode::LimitExceeded,
392        7 => ErrorCode::Syntax,
393        8 => ErrorCode::Underrun,
394        _ => ErrorCode::Io,
395    }
396}
397fn fill_initial(decoder: &mut FileDecoder, ring: &Ring, scratch: &mut [f32]) -> Result<()> {
398    while ring.len() < ring.capacity() {
399        let want = (ring.capacity() - ring.len()).min(scratch.len() / decoder.channels);
400        let n = decoder.read(&mut scratch[..want * decoder.channels])?;
401        if n == 0 {
402            ring.finish(0, None);
403            break;
404        }
405        for frame in scratch[..n * decoder.channels].chunks_exact(decoder.channels) {
406            assert!(ring.push(frame, 0));
407        }
408    }
409    Ok(())
410}
411fn run(decoder: &mut FileDecoder, ring: &Ring, scratch: &mut [f32]) {
412    let (mut generation, mut offset, mut ready) = (0, 0, 0);
413    loop {
414        if ring.cancelled.load(Ordering::Acquire) {
415            return;
416        }
417        let requested = ring.generation.load(Ordering::Acquire);
418        if requested != generation {
419            generation = requested;
420            offset = 0;
421            ready = 0;
422            ring.terminal.store(0, Ordering::Release);
423            let target = ring.target.load(Ordering::Relaxed);
424            let result = decoder.seek(target, || {
425                ring.cancelled.load(Ordering::Acquire)
426                    || ring.generation.load(Ordering::Acquire) != generation
427            });
428            if let Err(e) = result {
429                ring.finish(generation, Some(e.code));
430            }
431            continue;
432        }
433        if offset < ready {
434            let ch = decoder.channels;
435            if ring.push(&scratch[offset * ch..(offset + 1) * ch], generation) {
436                offset += 1;
437                continue;
438            }
439        } else if ring.terminal.load(Ordering::Acquire) == 0 {
440            match decoder.read(scratch) {
441                Ok(0) => ring.finish(generation, None),
442                Ok(n) => {
443                    offset = 0;
444                    ready = n;
445                    continue;
446                }
447                Err(e) => ring.finish(generation, Some(e.code)),
448            }
449        }
450        thread::sleep(Duration::from_millis(1));
451    }
452}
453
454#[cfg(feature = "mixer")]
455impl crate::mixer::Stream for FileStream {
456    fn sample_rate(&self) -> u32 {
457        self.sample_rate()
458    }
459    fn channels(&self) -> usize {
460        self.channels()
461    }
462    fn frames(&self) -> Option<u64> {
463        self.frames()
464    }
465    fn read(&mut self, out: &mut [f32]) -> Result<usize> {
466        self.read(out)
467    }
468    fn seek(&mut self, frame: u64) -> Result<()> {
469        self.seek(frame)
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    fn source(capacity: usize) -> FileStream {
477        FileStream {
478            ring: Arc::new(Ring::new(
479                Info {
480                    channels: 1,
481                    rate: 8000,
482                    frames: None,
483                    format: AudioFormat::Wav,
484                },
485                capacity,
486            )),
487            position: 0,
488            generation: 0,
489        }
490    }
491    #[test]
492    fn underrun_does_not_touch_output_or_turn_into_eof() {
493        let mut stream = source(3);
494        let mut out = [7.0; 4];
495        let e = stream.read(&mut out).unwrap_err();
496        assert_eq!(e.code, ErrorCode::Underrun);
497        assert!(e.message.is_empty());
498        assert_eq!(out, [7.0; 4]);
499        assert_eq!(stream.position(), 0);
500        assert!(stream.ring.push(&[0.5], 0));
501        stream.ring.finish(0, None);
502        assert_eq!(stream.read(&mut out).unwrap(), 1);
503        assert_eq!(out, [0.5, 7.0, 7.0, 7.0]);
504        assert_eq!(stream.read(&mut out).unwrap(), 0);
505    }
506    #[test]
507    fn stale_audio_and_terminal_are_ignored_after_seek() {
508        let mut stream = source(3);
509        for _ in 0..3 {
510            assert!(stream.ring.push(&[-1.0], 0));
511        }
512        stream.ring.finish(0, Some(ErrorCode::DecodeError));
513        stream.seek(100).unwrap();
514        assert_eq!(
515            stream.read(&mut [0.0; 10]).unwrap_err().code,
516            ErrorCode::Underrun
517        );
518        assert!(stream.ring.push(&[0.75], 1));
519        stream.ring.finish(1, None);
520        let mut out = [0.0; 2];
521        assert_eq!(stream.read(&mut out).unwrap(), 1);
522        assert_eq!(out[0], 0.75);
523        assert_eq!(stream.position(), 101);
524        assert_eq!(stream.read(&mut out).unwrap(), 0);
525    }
526    #[test]
527    fn worker_disconnection_and_terminal_errors_are_stable() {
528        let mut stream = source(1);
529        stream.ring.finish(0, Some(ErrorCode::DecodeError));
530        for _ in 0..3 {
531            assert_eq!(
532                stream.read(&mut [0.0]).unwrap_err().code,
533                ErrorCode::DecodeError
534            );
535        }
536        stream.seek(0).unwrap();
537        stream.ring.worker_alive.store(false, Ordering::Release);
538        assert_eq!(stream.read(&mut [0.0]).unwrap_err().code, ErrorCode::Io);
539    }
540    #[test]
541    fn odd_ring_wraps_and_eof_never_overtakes_the_last_frame() {
542        let mut stream = source(7);
543        let ring = stream.ring.clone();
544        let worker = thread::spawn(move || {
545            for i in 0..50000 {
546                while !ring.push(&[i as f32], 0) {
547                    thread::yield_now();
548                }
549            }
550            ring.finish(0, None);
551        });
552        let mut expected = 0;
553        let mut buffer = [0.0; 11];
554        loop {
555            match stream.read(&mut buffer) {
556                Ok(0) => break,
557                Ok(n) => {
558                    for &v in &buffer[..n] {
559                        assert_eq!(v, expected as f32);
560                        expected += 1;
561                    }
562                }
563                Err(e) if e.code == ErrorCode::Underrun => thread::yield_now(),
564                Err(e) => panic!("{e:?}"),
565            }
566        }
567        worker.join().unwrap();
568        assert_eq!(expected, 50000);
569    }
570}