From a6664d0cf55afdf0b1ec22d32039ae7cdf16594a Mon Sep 17 00:00:00 2001 From: dvdsk Date: Fri, 28 Feb 2025 12:22:26 +0100 Subject: [PATCH 1/4] This makes ChannelCount NonZero and channels not zero asserts I ran into a lot of bugs while adding tests that had to do with channel being set to zero somewhere. While this change makes the API slightly less easy to use it prevents very hard to debug crashes/underflows etc. Performance might drop in decoders, the current implementation makes the bound check every time `channels` is called which is once per span. This could be cached to alleviate that. --- benches/pipeline.rs | 4 +- benches/shared.rs | 2 +- examples/mix_multiple_sources.rs | 3 +- src/buffer.rs | 35 ++-- src/common.rs | 6 +- src/conversions/channels.rs | 47 +++-- src/conversions/sample_rate.rs | 56 ++--- src/decoder/flac.rs | 11 +- src/decoder/mod.rs | 1 + src/decoder/symphonia.rs | 21 +- src/decoder/vorbis.rs | 3 +- src/decoder/wav.rs | 7 +- src/math.rs | 10 + src/mixer.rs | 51 +++-- src/queue.rs | 28 ++- src/sink.rs | 17 +- src/source/buffered.rs | 3 +- src/source/channel_volume.rs | 15 +- src/source/chirp.rs | 3 +- src/source/crossfade.rs | 5 +- src/source/delay.rs | 2 +- src/source/empty.rs | 3 +- src/source/empty_callback.rs | 3 +- src/source/from_iter.rs | 10 +- src/source/linear_ramp.rs | 9 +- src/source/mod.rs | 1 + src/source/pausable.rs | 4 +- src/source/periodic.rs | 8 +- src/source/position.rs | 12 +- src/source/sawtooth.rs | 3 +- src/source/signal_generator.rs | 3 +- src/source/sine.rs | 3 +- src/source/skip.rs | 11 +- src/source/square.rs | 3 +- src/source/take.rs | 2 +- src/source/triangle.rs | 3 +- src/static_buffer.rs | 21 +- src/stream.rs | 338 +++++++++---------------------- src/wav_output.rs | 7 +- tests/seek.rs | 178 +++------------- 40 files changed, 389 insertions(+), 563 deletions(-) diff --git a/benches/pipeline.rs b/benches/pipeline.rs index 8be8541e..11363215 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -1,6 +1,7 @@ use std::time::Duration; use divan::Bencher; +use rodio::ChannelCount; use rodio::{source::UniformSourceIterator, Source}; mod shared; @@ -31,7 +32,8 @@ fn long(bencher: Bencher) { .buffered() .reverb(Duration::from_secs_f32(0.05), 0.3) .skippable(); - let resampled = UniformSourceIterator::new(effects_applied, 2, 40_000); + let resampled = + UniformSourceIterator::new(effects_applied, ChannelCount::new(2).unwrap(), 40_000); resampled.for_each(divan::black_box_drop) }) } diff --git a/benches/shared.rs b/benches/shared.rs index 442621f4..e3ea7835 100644 --- a/benches/shared.rs +++ b/benches/shared.rs @@ -6,7 +6,7 @@ use rodio::{ChannelCount, Sample, SampleRate, Source}; pub struct TestSource { samples: vec::IntoIter, - channels: u16, + channels: ChannelCount, sample_rate: u32, total_duration: Duration, } diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index 75109c8b..b312a01d 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -1,11 +1,12 @@ use rodio::mixer; use rodio::source::{SineWave, Source}; use std::error::Error; +use std::num::NonZero; use std::time::Duration; fn main() -> Result<(), Box> { // Construct a dynamic controller and mixer, stream_handle, and sink. - let (controller, mixer) = mixer::mixer(2, 44_100); + let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), 44_100); let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); diff --git a/src/buffer.rs b/src/buffer.rs index 8f5dc111..1744e042 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -6,7 +6,8 @@ //! //! ``` //! use rodio::buffer::SamplesBuffer; -//! let _ = SamplesBuffer::new(1, 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! use rodio::ChannelCount; +//! let _ = SamplesBuffer::new(ChannelCount::new(1).unwrap(), 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! @@ -31,7 +32,6 @@ impl SamplesBuffer { /// /// # Panics /// - /// - Panics if the number of channels is zero. /// - Panics if the samples rate is zero. /// - Panics if the length of the buffer is larger than approximately 16 billion elements. /// This is because the calculation of the duration would overflow. @@ -40,13 +40,12 @@ impl SamplesBuffer { where D: Into>, { - assert!(channels >= 1); assert!(sample_rate >= 1); let data: Arc<[f32]> = data.into().into(); let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() / sample_rate as u64 - / channels as u64; + / channels.get() as u64; let duration = Duration::new( duration_ns / 1_000_000_000, (duration_ns % 1_000_000_000) as u32, @@ -90,14 +89,14 @@ impl Source for SamplesBuffer { // and due to the constant sample_rate we can jump to the right // sample directly. - let curr_channel = self.pos % self.channels() as usize; - let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32; + let curr_channel = self.pos % self.channels().get() as usize; + let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels().get() as f32; // saturate pos at the end of the source let new_pos = new_pos as usize; let new_pos = new_pos.min(self.data.len()); // make sure the next sample is for the right channel - let new_pos = new_pos.next_multiple_of(self.channels() as usize); + let new_pos = new_pos.next_multiple_of(self.channels().get() as usize); let new_pos = new_pos - curr_channel; self.pos = new_pos; @@ -124,28 +123,23 @@ impl Iterator for SamplesBuffer { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::source::Source; #[test] fn basic() { - let _ = SamplesBuffer::new(1, 44100, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); - } - - #[test] - #[should_panic] - fn panic_if_zero_channels() { - SamplesBuffer::new(0, 44100, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let _ = SamplesBuffer::new(ch!(1), 44100, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] #[should_panic] fn panic_if_zero_sample_rate() { - SamplesBuffer::new(1, 0, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + SamplesBuffer::new(ch!(1), 0, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] fn duration_basic() { - let buf = SamplesBuffer::new(2, 2, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let buf = SamplesBuffer::new(ch!(2), 2, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); let dur = buf.total_duration().unwrap(); assert_eq!(dur.as_secs(), 1); assert_eq!(dur.subsec_nanos(), 500_000_000); @@ -153,7 +147,7 @@ mod tests { #[test] fn iteration() { - let mut buf = SamplesBuffer::new(1, 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + let mut buf = SamplesBuffer::new(ch!(1), 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); assert_eq!(buf.next(), Some(1.0)); assert_eq!(buf.next(), Some(2.0)); assert_eq!(buf.next(), Some(3.0)); @@ -173,14 +167,17 @@ mod tests { #[test] fn channel_order_stays_correct() { const SAMPLE_RATE: SampleRate = 100; - const CHANNELS: ChannelCount = 2; + const CHANNELS: ChannelCount = ch!(2); let mut buf = SamplesBuffer::new( CHANNELS, SAMPLE_RATE, (0..2000i16).map(|s| s as Sample).collect::>(), ); buf.try_seek(Duration::from_secs(5)).unwrap(); - assert_eq!(buf.next(), Some(5.0 * SAMPLE_RATE as f32 * CHANNELS as f32)); + assert_eq!( + buf.next(), + Some(5.0 * SAMPLE_RATE as f32 * CHANNELS.get() as f32) + ); assert!(buf.next().is_some_and(|s| s.trunc() as i32 % 2 == 1)); assert!(buf.next().is_some_and(|s| s.trunc() as i32 % 2 == 0)); diff --git a/src/common.rs b/src/common.rs index 12b3a94d..bf3f7acb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,8 +1,10 @@ +use std::num::NonZero; + /// Stream sample rate (a frame rate or samples per second per channel). pub type SampleRate = u32; -/// Number of channels in a stream. -pub type ChannelCount = u16; +/// Number of channels in a stream. Can never be Zero +pub type ChannelCount = NonZero; /// Represents value of a single sample. /// Silence corresponds to the value `0.0`. The expected amplitude range is -1.0...1.0. diff --git a/src/conversions/channels.rs b/src/conversions/channels.rs index 17887673..980ebf98 100644 --- a/src/conversions/channels.rs +++ b/src/conversions/channels.rs @@ -11,7 +11,7 @@ where from: ChannelCount, to: ChannelCount, sample_repeat: Option, - next_output_sample_pos: ChannelCount, + next_output_sample_pos: u16, } impl ChannelCountConverter @@ -26,9 +26,6 @@ where /// #[inline] pub fn new(input: I, from: ChannelCount, to: ChannelCount) -> ChannelCountConverter { - assert!(from >= 1); - assert!(to >= 1); - ChannelCountConverter { input, from, @@ -65,7 +62,7 @@ where self.sample_repeat = value; value } - x if x < self.from => self.input.next(), + x if x < self.from.get() => self.input.next(), 1 => self.sample_repeat, _ => Some(0.0), }; @@ -74,11 +71,11 @@ where self.next_output_sample_pos += 1; } - if self.next_output_sample_pos == self.to { + if self.next_output_sample_pos == self.to.get() { self.next_output_sample_pos = 0; if self.from > self.to { - for _ in self.to..self.from { + for _ in self.to.get()..self.from.get() { self.input.next(); // discarding extra input } } @@ -91,13 +88,13 @@ where fn size_hint(&self) -> (usize, Option) { let (min, max) = self.input.size_hint(); - let consumed = std::cmp::min(self.from, self.next_output_sample_pos) as usize; + let consumed = std::cmp::min(self.from.get(), self.next_output_sample_pos) as usize; - let min = ((min + consumed) / self.from as usize * self.to as usize) + let min = ((min + consumed) / self.from.get() as usize * self.to.get() as usize) .saturating_sub(self.next_output_sample_pos as usize); let max = max.map(|max| { - ((max + consumed) / self.from as usize * self.to as usize) + ((max + consumed) / self.from.get() as usize * self.to.get() as usize) .saturating_sub(self.next_output_sample_pos as usize) }); @@ -111,31 +108,37 @@ impl ExactSizeIterator for ChannelCountConverter where I: ExactSizeIterato mod test { use super::ChannelCountConverter; use crate::common::ChannelCount; + use crate::math::ch; use crate::Sample; #[test] fn remove_channels() { let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; - let output = ChannelCountConverter::new(input.into_iter(), 3, 2).collect::>(); + let output = + ChannelCountConverter::new(input.into_iter(), ch!(3), ch!(2)).collect::>(); assert_eq!(output, [1.0, 2.0, 4.0, 5.0]); let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; - let output = ChannelCountConverter::new(input.into_iter(), 4, 1).collect::>(); + let output = + ChannelCountConverter::new(input.into_iter(), ch!(4), ch!(1)).collect::>(); assert_eq!(output, [1.0, 5.0]); } #[test] fn add_channels() { let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), 1, 2).collect::>(); + let output = + ChannelCountConverter::new(input.into_iter(), ch!(1), ch!(2)).collect::>(); assert_eq!(output, [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0]); let input = vec![1.0, 2.0]; - let output = ChannelCountConverter::new(input.into_iter(), 1, 4).collect::>(); + let output = + ChannelCountConverter::new(input.into_iter(), ch!(1), ch!(4)).collect::>(); assert_eq!(output, [1.0, 1.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0]); let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), 2, 4).collect::>(); + let output = + ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(4)).collect::>(); assert_eq!(output, [1.0, 2.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0]); } @@ -152,24 +155,24 @@ mod test { assert_eq!(converter.size_hint(), (0, Some(0))); } - test(&[1.0, 2.0, 3.0], 1, 2); - test(&[1.0, 2.0, 3.0, 4.0], 2, 4); - test(&[1.0, 2.0, 3.0, 4.0], 4, 2); - test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 8); - test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], 4, 1); + test(&[1.0, 2.0, 3.0], ch!(1), ch!(2)); + test(&[1.0, 2.0, 3.0, 4.0], ch!(2), ch!(4)); + test(&[1.0, 2.0, 3.0, 4.0], ch!(4), ch!(2)); + test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], ch!(3), ch!(8)); + test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], ch!(4), ch!(1)); } #[test] fn len_more() { let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), 2, 3); + let output = ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(3)); assert_eq!(output.len(), 6); } #[test] fn len_less() { let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), 2, 1); + let output = ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(1)); assert_eq!(output.len(), 2); } } diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index f6264978..95888c57 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -54,7 +54,6 @@ where to: SampleRate, num_channels: ChannelCount, ) -> SampleRateConverter { - assert!(num_channels >= 1); assert!(from >= 1); assert!(to >= 1); @@ -64,11 +63,11 @@ where } else { let first = input .by_ref() - .take(num_channels as usize) + .take(num_channels.get() as usize) .collect::>(); let next = input .by_ref() - .take(num_channels as usize) + .take(num_channels.get() as usize) .collect::>(); (first, next) }; @@ -85,7 +84,7 @@ where next_output_span_pos_in_chunk: 0, current_span: first_samples, next_frame: next_samples, - output_buffer: Vec::with_capacity(num_channels as usize - 1), + output_buffer: Vec::with_capacity(num_channels.get() as usize - 1), } } @@ -106,7 +105,7 @@ where mem::swap(&mut self.current_span, &mut self.next_frame); self.next_frame.clear(); - for _ in 0..self.channels { + for _ in 0..self.channels.get() { if let Some(i) = self.input.next() { self.next_frame.push(i); } else { @@ -213,7 +212,7 @@ where // removing the samples of the current chunk that have not yet been read let samples_after_chunk = samples_after_chunk.saturating_sub( self.from.saturating_sub(self.current_span_pos_in_chunk + 2) as usize - * usize::from(self.channels), + * usize::from(self.channels.get()), ); // calculating the number of samples after the transformation // TODO: this is wrong here \|/ @@ -222,7 +221,7 @@ where // `samples_current_chunk` will contain the number of samples remaining to be output // for the chunk currently being processed let samples_current_chunk = (self.to - self.next_output_span_pos_in_chunk) as usize - * usize::from(self.channels); + * usize::from(self.channels.get()); samples_current_chunk + samples_after_chunk + self.output_buffer.len() }; @@ -242,14 +241,15 @@ impl ExactSizeIterator for SampleRateConverter where I: ExactSizeIterator< mod test { use super::SampleRateConverter; use crate::common::{ChannelCount, SampleRate}; + use crate::math::ch; use crate::Sample; use core::time::Duration; use quickcheck::{quickcheck, TestResult}; quickcheck! { /// Check that resampling an empty input produces no output. - fn empty(from: u16, to: u16, channels: u8) -> TestResult { - if channels == 0 || channels > 128 + fn empty(from: u16, to: u16, channels: ChannelCount) -> TestResult { + if channels.get() > 128 || from == 0 || to == 0 { @@ -260,7 +260,7 @@ mod test { let input: Vec = Vec::new(); let output = - SampleRateConverter::new(input.into_iter(), from, to, channels as ChannelCount) + SampleRateConverter::new(input.into_iter(), from, to, channels) .collect::>(); assert_eq!(output, []); @@ -268,13 +268,13 @@ mod test { } /// Check that resampling to the same rate does not change the signal. - fn identity(from: u16, channels: u8, input: Vec) -> TestResult { - if channels == 0 || channels > 128 || from == 0 { return TestResult::discard(); } + fn identity(from: u16, channels: ChannelCount, input: Vec) -> TestResult { + if channels.get() > 128 || from == 0 { return TestResult::discard(); } let from = from as SampleRate; let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); let output = - SampleRateConverter::new(input.clone().into_iter(), from, from, channels as ChannelCount) + SampleRateConverter::new(input.clone().into_iter(), from, from, channels) .collect::>(); TestResult::from_bool(input == output) @@ -282,8 +282,8 @@ mod test { /// Check that dividing the sample rate by k (integer) is the same as /// dropping a sample from each channel. - fn divide_sample_rate(to: u16, k: u16, input: Vec, channels: u8) -> TestResult { - if k == 0 || channels == 0 || channels > 128 || to == 0 || to > 48000 { + fn divide_sample_rate(to: u16, k: u16, input: Vec, channels: ChannelCount) -> TestResult { + if k == 0 || channels.get() > 128 || to == 0 || to > 48000 { return TestResult::discard(); } let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); @@ -293,24 +293,24 @@ mod test { // Truncate the input, so it contains an integer number of spans. let input = { - let ns = channels as usize; + let ns = channels.get() as usize; let mut i = input; i.truncate(ns * (i.len() / ns)); i }; let output = - SampleRateConverter::new(input.clone().into_iter(), from, to, channels as ChannelCount) + SampleRateConverter::new(input.clone().into_iter(), from, to, channels) .collect::>(); - TestResult::from_bool(input.chunks_exact(channels.into()) + TestResult::from_bool(input.chunks_exact(channels.get().into()) .step_by(k as usize).collect::>().concat() == output) } /// Check that, after multiplying the sample rate by k, every k-th /// sample in the output matches exactly with the input. - fn multiply_sample_rate(from: u16, k: u8, input: Vec, channels: u8) -> TestResult { - if k == 0 || channels == 0 || channels > 128 || from == 0 { + fn multiply_sample_rate(from: u16, k: u8, input: Vec, channels: ChannelCount) -> TestResult { + if k == 0 || channels.get() > 128 || from == 0 { return TestResult::discard(); } let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); @@ -320,24 +320,24 @@ mod test { // Truncate the input, so it contains an integer number of spans. let input = { - let ns = channels as usize; + let ns = channels.get() as usize; let mut i = input; i.truncate(ns * (i.len() / ns)); i }; let output = - SampleRateConverter::new(input.clone().into_iter(), from, to, channels as ChannelCount) + SampleRateConverter::new(input.clone().into_iter(), from, to, channels) .collect::>(); TestResult::from_bool(input == - output.chunks_exact(channels.into()) + output.chunks_exact(channels.get().into()) .step_by(k as usize).collect::>().concat()) } #[ignore] /// Check that resampling does not change the audio duration, - /// except by a negligible amount (± 1ms). Reproduces #316. + /// except by a negligible amount (± 1ms). Reproduces #316. /// Ignored, pending a bug fix. fn preserve_durations(d: Duration, freq: f32, to: SampleRate) -> TestResult { if to == 0 { return TestResult::discard(); } @@ -348,7 +348,7 @@ mod test { let from = source.sample_rate(); let resampled = - SampleRateConverter::new(source, from, to, 1); + SampleRateConverter::new(source, from, to, ch!(1)); let duration = Duration::from_secs_f32(resampled.count() as f32 / to as f32); @@ -360,7 +360,7 @@ mod test { #[test] fn upsample() { let input = vec![2.0, 16.0, 4.0, 18.0, 6.0, 20.0, 8.0, 22.0]; - let output = SampleRateConverter::new(input.into_iter(), 2000, 3000, 2); + let output = SampleRateConverter::new(input.into_iter(), 2000, 3000, ch!(2)); assert_eq!(output.len(), 12); // Test the source's Iterator::size_hint() let output = output.map(|x| x.trunc()).collect::>(); @@ -373,7 +373,7 @@ mod test { #[test] fn upsample2() { let input = vec![1.0, 14.0]; - let output = SampleRateConverter::new(input.into_iter(), 1000, 7000, 1); + let output = SampleRateConverter::new(input.into_iter(), 1000, 7000, ch!(1)); let size_estimation = output.len(); let output = output.map(|x| x.trunc()).collect::>(); assert_eq!(output, [1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0]); @@ -383,7 +383,7 @@ mod test { #[test] fn downsample() { let input = Vec::from_iter((0..17).map(|x| x as Sample)); - let output = SampleRateConverter::new(input.into_iter(), 12000, 2400, 1); + let output = SampleRateConverter::new(input.into_iter(), 12000, 2400, ch!(1)); let size_estimation = output.len(); let output = output.collect::>(); assert_eq!(output, [0.0, 5.0, 10.0, 15.0]); diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index dd21d871..f60dbd0e 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -61,7 +61,12 @@ where current_block_off: 0, bits_per_sample: spec.bits_per_sample, sample_rate, - channels: spec.channels as ChannelCount, + channels: ChannelCount::new( + spec.channels + .try_into() + .expect("rodio supports only up to u16::MAX (65_535) channels"), + ) + .expect("flac should never have zero channels"), total_duration, }) } @@ -115,9 +120,9 @@ where loop { if self.current_block_off < self.current_block.len() { // Read from current block. - let real_offset = (self.current_block_off % self.channels as usize) + let real_offset = (self.current_block_off % self.channels.get() as usize) * self.current_block_channel_len - + self.current_block_off / self.channels as usize; + + self.current_block_off / self.channels.get() as usize; let raw_val = self.current_block[real_offset]; self.current_block_off += 1; let bits = self.bits_per_sample; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index d2f792d1..277017fc 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -56,6 +56,7 @@ use std::{ #[allow(unused_imports)] use std::io::SeekFrom; +use crate::math::ch; use crate::{ common::{ChannelCount, SampleRate}, source::{SeekError, Source}, diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index cd3a6050..b16878b0 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -165,7 +165,14 @@ impl Source for SymphoniaDecoder { #[inline] fn channels(&self) -> ChannelCount { - self.spec.channels.count() as ChannelCount + ChannelCount::new( + self.spec + .channels + .count() + .try_into() + .expect("rodio only support up to u16::MAX channels (65_535)"), + ) + .expect("audio should always have at least one channel") } #[inline] @@ -307,6 +314,18 @@ impl SymphoniaDecoder { self.next(); } +<<<<<<< HEAD +||||||| parent of 3fb371d (This makes ChannelCount NonZero and channels not zero asserts) + let decoded = decoded.map_err(SeekError::Decoding)?; + decoded.spec().clone_into(&mut self.spec); + self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec); + self.current_span_offset = samples_to_pass as usize * self.channels() as usize; +======= + let decoded = decoded.map_err(SeekError::Decoding)?; + decoded.spec().clone_into(&mut self.spec); + self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec); + self.current_span_offset = samples_to_pass as usize * self.channels().get() as usize; +>>>>>>> 3fb371d (This makes ChannelCount NonZero and channels not zero asserts) Ok(()) } } diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 656d96ca..1cf76a12 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -69,7 +69,8 @@ where #[inline] fn channels(&self) -> ChannelCount { - self.stream_reader.ident_hdr.audio_channels as ChannelCount + ChannelCount::new(self.stream_reader.ident_hdr.audio_channels.into()) + .expect("audio should have at least one channel") } #[inline] diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 421a75e0..0259f22b 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -41,6 +41,7 @@ where let sample_rate = spec.sample_rate; let channels = spec.channels; + assert!(channels > 0); let total_duration = { let data_rate = sample_rate as u64 * channels as u64; @@ -53,7 +54,7 @@ where reader, total_duration, sample_rate: sample_rate as SampleRate, - channels: channels as ChannelCount, + channels: ChannelCount::new(channels).expect("wav should have a least one channel"), }) } @@ -175,13 +176,13 @@ where let new_pos = new_pos.min(file_len); // saturate pos at the end of the source // make sure the next sample is for the right channel - let to_skip = self.reader.samples_read % self.channels() as u32; + let to_skip = self.reader.samples_read % self.channels().get() as u32; self.reader .reader .seek(new_pos) .map_err(SeekError::HoundDecoder)?; - self.reader.samples_read = new_pos * self.channels() as u32; + self.reader.samples_read = new_pos * self.channels().get() as u32; for _ in 0..to_skip { self.next(); diff --git a/src/math.rs b/src/math.rs index 2d23869e..c47209c8 100644 --- a/src/math.rs +++ b/src/math.rs @@ -76,6 +76,16 @@ pub fn linear_to_db(linear: f32) -> f32 { linear.log2() * std::f32::consts::LOG10_2 * 20.0 } +/// short macro to generate a `ChannelCount` for tests +/// this panics during compile if the passed in literal is zero +macro_rules! ch { + ($n:literal) => { + const { core::num::NonZeroU16::new($n).unwrap() } + }; +} + +pub(crate) use ch; + #[cfg(test)] mod test { use super::*; diff --git a/src/mixer.rs b/src/mixer.rs index 03cb67fe..dd9cdba2 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -177,7 +177,7 @@ impl MixerSource { let mut pending = self.input.0.pending_sources.lock().unwrap(); // TODO: relax ordering? for source in pending.drain(..) { - let in_step = self.sample_count % source.channels() as usize == 0; + let in_step = self.sample_count % source.channels().get() as usize == 0; if in_step { self.current_sources.push(source); @@ -211,17 +211,22 @@ impl MixerSource { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::mixer; use crate::source::Source; #[test] fn basic() { - let (tx, mut rx) = mixer::mixer(1, 48000); + let (tx, mut rx) = mixer::mixer(ch!(1), 48000); - tx.add(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); - tx.add(SamplesBuffer::new(1, 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); + tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); - assert_eq!(rx.channels(), 1); + assert_eq!(rx.channels(), ch!(1)); assert_eq!(rx.sample_rate(), 48000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(-5.0)); @@ -232,12 +237,16 @@ mod tests { #[test] fn channels_conv() { - let (tx, mut rx) = mixer::mixer(2, 48000); + let (tx, mut rx) = mixer::mixer(ch!(2), 48000); - tx.add(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); - tx.add(SamplesBuffer::new(1, 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); + tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); - assert_eq!(rx.channels(), 2); + assert_eq!(rx.channels(), ch!(2)); assert_eq!(rx.sample_rate(), 48000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(15.0)); @@ -252,12 +261,16 @@ mod tests { #[test] fn rate_conv() { - let (tx, mut rx) = mixer::mixer(1, 96000); + let (tx, mut rx) = mixer::mixer(ch!(1), 96000); - tx.add(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); - tx.add(SamplesBuffer::new(1, 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); + tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); - assert_eq!(rx.channels(), 1); + assert_eq!(rx.channels(), ch!(1)); assert_eq!(rx.sample_rate(), 96000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(5.0)); @@ -271,15 +284,19 @@ mod tests { #[test] fn start_afterwards() { - let (tx, mut rx) = mixer::mixer(1, 48000); + let (tx, mut rx) = mixer::mixer(ch!(1), 48000); - tx.add(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); + tx.add(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); tx.add(SamplesBuffer::new( - 1, + ch!(1), 48000, vec![5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 7.0], )); @@ -290,7 +307,7 @@ mod tests { assert_eq!(rx.next(), Some(6.0)); assert_eq!(rx.next(), Some(6.0)); - tx.add(SamplesBuffer::new(1, 48000, vec![2.0])); + tx.add(SamplesBuffer::new(ch!(1), 48000, vec![2.0])); assert_eq!(rx.next(), Some(9.0)); assert_eq!(rx.next(), Some(7.0)); diff --git a/src/queue.rs b/src/queue.rs index 2abfc47b..b57a0ab7 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -4,6 +4,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +use crate::math::ch; use crate::source::{Empty, SeekError, Source, Zero}; use crate::Sample; @@ -220,7 +221,7 @@ impl SourcesQueueOutput { let mut next = self.input.next_sounds.lock().unwrap(); if next.is_empty() { - let silence = Box::new(Zero::new_samples(1, 44100, THRESHOLD)) as Box<_>; + let silence = Box::new(Zero::new_samples(ch!(1), 44100, THRESHOLD)) as Box<_>; if self.input.keep_alive_if_empty.load(Ordering::Acquire) { // Play a short silence in order to avoid spinlocking. (silence, None) @@ -241,6 +242,7 @@ impl SourcesQueueOutput { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::queue; use crate::source::Source; @@ -249,16 +251,20 @@ mod tests { fn basic() { let (tx, mut rx) = queue::queue(false); - tx.append(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); - tx.append(SamplesBuffer::new(2, 96000, vec![5.0, 5.0, 5.0, 5.0])); + tx.append(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); + tx.append(SamplesBuffer::new(ch!(2), 96000, vec![5.0, 5.0, 5.0, 5.0])); - assert_eq!(rx.channels(), 1); + assert_eq!(rx.channels(), ch!(1)); assert_eq!(rx.sample_rate(), 48000); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); - assert_eq!(rx.channels(), 2); + assert_eq!(rx.channels(), ch!(2)); assert_eq!(rx.sample_rate(), 96000); assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(5.0)); @@ -276,7 +282,11 @@ mod tests { #[test] fn keep_alive() { let (tx, mut rx) = queue::queue(true); - tx.append(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); + tx.append(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); @@ -297,7 +307,11 @@ mod tests { assert_eq!(rx.next(), Some(0.0)); } - tx.append(SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0])); + tx.append(SamplesBuffer::new( + ch!(1), + 48000, + vec![10.0, -10.0, 10.0, -10.0], + )); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); assert_eq!(rx.next(), Some(10.0)); diff --git a/src/sink.rs b/src/sink.rs index dd864a19..5bb699a6 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -356,6 +356,7 @@ mod tests { use std::sync::atomic::Ordering; use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::{Sink, Source}; #[test] @@ -372,8 +373,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // Low rate to ensure immediate control. - sink.append(SamplesBuffer::new(1, 1, v.clone())); - let mut reference_src = SamplesBuffer::new(1, 1, v); + sink.append(SamplesBuffer::new(ch!(1), 1, v.clone())); + let mut reference_src = SamplesBuffer::new(ch!(1), 1, v); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); @@ -400,8 +401,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; - sink.append(SamplesBuffer::new(1, 1, v.clone())); - let mut src = SamplesBuffer::new(1, 1, v.clone()); + sink.append(SamplesBuffer::new(ch!(1), 1, v.clone())); + let mut src = SamplesBuffer::new(ch!(1), 1, v.clone()); assert_eq!(queue_rx.next(), src.next()); assert_eq!(queue_rx.next(), src.next()); @@ -411,8 +412,8 @@ mod tests { assert!(sink.controls.stopped.load(Ordering::SeqCst)); assert_eq!(queue_rx.next(), Some(0.0)); - src = SamplesBuffer::new(1, 1, v.clone()); - sink.append(SamplesBuffer::new(1, 1, v)); + src = SamplesBuffer::new(ch!(1), 1, v.clone()); + sink.append(SamplesBuffer::new(ch!(1), 1, v)); assert!(!sink.controls.stopped.load(Ordering::SeqCst)); // Flush silence @@ -429,8 +430,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // High rate to avoid immediate control. - sink.append(SamplesBuffer::new(2, 44100, v.clone())); - let src = SamplesBuffer::new(2, 44100, v.clone()); + sink.append(SamplesBuffer::new(ch!(2), 44100, v.clone())); + let src = SamplesBuffer::new(ch!(2), 44100, v.clone()); let mut src = src.amplify(0.5); sink.set_volume(0.5); diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 402e63bb..8d354e2a 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -5,6 +5,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::Source; /// Internal function that builds a `Buffered` object. @@ -214,7 +215,7 @@ where fn channels(&self) -> ChannelCount { match *self.current_span { Span::Data(SpanData { channels, .. }) => channels, - Span::End => 1, + Span::End => ch!(1), Span::Input(_) => unreachable!(), } } diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index 89b2280d..6dbe7cce 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -24,10 +24,10 @@ where /// Wrap the input source and make it mono. Play that mono sound to each /// channel at the volume set by the user. The volume can be changed using /// [`ChannelVolume::set_volume`]. - pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume - where - I: Source, - { + /// + /// # Panics if channel_volumes is empty + pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume { + assert!(!channel_volumes.is_empty()); let channel_count = channel_volumes.len(); // See next() implementation. ChannelVolume { input, @@ -75,12 +75,12 @@ where self.current_channel = 0; self.current_sample = None; let num_channels = self.input.channels(); - for _ in 0..num_channels { + for _ in 0..num_channels.get() { if let Some(s) = self.input.next() { self.current_sample = Some(self.current_sample.unwrap_or(0.0) + s); } } - self.current_sample.map(|s| s / num_channels as f32); + self.current_sample.map(|s| s / num_channels.get() as f32); } let result = self .current_sample @@ -108,7 +108,8 @@ where #[inline] fn channels(&self) -> ChannelCount { - self.channel_volumes.len() as ChannelCount + ChannelCount::new(self.channel_volumes.len() as u16) + .expect("checked to be non-empty in new implementation") } #[inline] diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 4b99eb85..9a719e33 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -1,6 +1,7 @@ //! Chirp/sweep source. use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::Source; use std::{f32::consts::TAU, time::Duration}; @@ -62,7 +63,7 @@ impl Source for Chirp { } fn channels(&self) -> ChannelCount { - 1 + ch!(1) } fn sample_rate(&self) -> SampleRate { diff --git a/src/source/crossfade.rs b/src/source/crossfade.rs index 0109c5e4..0390753d 100644 --- a/src/source/crossfade.rs +++ b/src/source/crossfade.rs @@ -33,11 +33,12 @@ pub type Crossfade = Mix, FadeIn>>; mod tests { use super::*; use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::source::Zero; fn dummy_source(length: u8) -> SamplesBuffer { let data: Vec = (1..=length).map(f32::from).collect(); - SamplesBuffer::new(1, 1, data) + SamplesBuffer::new(ch!(1), 1, data) } #[test] @@ -60,7 +61,7 @@ mod tests { #[test] fn test_crossfade() { let source1 = dummy_source(10); - let source2 = Zero::new(1, 1); + let source2 = Zero::new(ch!(1), 1); let mixed = crossfade( source1, source2, diff --git a/src/source/delay.rs b/src/source/delay.rs index f2476ddd..68cf270a 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -10,7 +10,7 @@ fn remaining_samples( channels: ChannelCount, ) -> usize { let ns = until_playback.as_secs() * 1_000_000_000 + until_playback.subsec_nanos() as u64; - let samples = ns * channels as u64 * sample_rate as u64 / 1_000_000_000; + let samples = ns * channels.get() as u64 * sample_rate as u64 / 1_000_000_000; samples as usize } diff --git a/src/source/empty.rs b/src/source/empty.rs index 8c8b4853..7fdd3108 100644 --- a/src/source/empty.rs +++ b/src/source/empty.rs @@ -2,6 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::{Sample, Source}; /// An empty source. @@ -41,7 +42,7 @@ impl Source for Empty { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index 08d8b0fb..88fa67aa 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -2,6 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::{Sample, Source}; /// An empty source that executes a callback function @@ -38,7 +39,7 @@ impl Source for EmptyCallback { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index 48251763..95cf3d7b 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -2,6 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::Source; /// Builds a source that chains sources provided by an iterator. @@ -117,7 +118,7 @@ where src.channels() } else { // Dummy value that only happens if the iterator was empty. - 2 + ch!(2) } } @@ -149,21 +150,22 @@ where #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::source::{from_iter, Source}; #[test] fn basic() { let mut rx = from_iter((0..2).map(|n| { if n == 0 { - SamplesBuffer::new(1, 48000, vec![10.0, -10.0, 10.0, -10.0]) + SamplesBuffer::new(ch!(1), 48000, vec![10.0, -10.0, 10.0, -10.0]) } else if n == 1 { - SamplesBuffer::new(2, 96000, vec![5.0, 5.0, 5.0, 5.0]) + SamplesBuffer::new(ch!(2), 96000, vec![5.0, 5.0, 5.0, 5.0]) } else { unreachable!() } })); - assert_eq!(rx.channels(), 1); + assert_eq!(rx.channels(), ch!(1)); assert_eq!(rx.sample_rate(), 48000); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index af830a5e..18c19486 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -45,7 +45,7 @@ impl LinearGainRamp where I: Source, { - /// Returns a reference to the innner source. + /// Returns a reference to the inner source. #[inline] pub fn inner(&self) -> &I { &self.input @@ -88,7 +88,7 @@ where factor = self.start_gain * (1.0f32 - p) + self.end_gain * p; } - if self.sample_idx % (self.channels() as u64) == 0 { + if self.sample_idx % (self.channels().get() as u64) == 0 { self.elapsed_ns += 1000000000.0 / (self.input.sample_rate() as f32); } @@ -140,13 +140,14 @@ mod tests { use super::*; use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::Sample; /// Create a SamplesBuffer of identical samples with value `value`. /// Returned buffer is one channel and has a sample rate of 1 hz. fn const_source(length: u8, value: Sample) -> SamplesBuffer { let data: Vec = (1..=length).map(|_| value).collect(); - SamplesBuffer::new(1, 1, data) + SamplesBuffer::new(ch!(1), 1, data) } /// Create a SamplesBuffer of repeating sample values from `values`. @@ -156,7 +157,7 @@ mod tests { .map(|(i, _)| values[i % values.len()]) .collect(); - SamplesBuffer::new(1, 1, data) + SamplesBuffer::new(ch!(1), 1, data) } #[test] diff --git a/src/source/mod.rs b/src/source/mod.rs index 7a7a5752..c81b0bd2 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -169,6 +169,7 @@ pub trait Source: Iterator { fn current_span_len(&self) -> Option; /// Returns the number of channels. Channels are always interleaved. + /// Should never be Zero fn channels(&self) -> ChannelCount; /// Returns the rate at which the source should be played. In number of samples per second. diff --git a/src/source/pausable.rs b/src/source/pausable.rs index c06e9b9d..5143b622 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -31,7 +31,7 @@ where pub struct Pausable { input: I, paused_channels: Option, - remaining_paused_samples: ChannelCount, + remaining_paused_samples: u16, } impl Pausable @@ -89,7 +89,7 @@ where } if let Some(paused_channels) = self.paused_channels { - self.remaining_paused_samples = paused_channels - 1; + self.remaining_paused_samples = paused_channels.get() - 1; return Some(0.0); } diff --git a/src/source/periodic.rs b/src/source/periodic.rs index 31260fd1..b89e7dc9 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -12,7 +12,8 @@ where // TODO: handle the fact that the samples rate can change // TODO: generally, just wrong let update_ms = period.as_secs() as u32 * 1_000 + period.subsec_millis(); - let update_frequency = (update_ms * source.sample_rate()) / 1000 * source.channels() as u32; + let update_frequency = + (update_ms * source.sample_rate()) / 1000 * source.channels().get() as u32; PeriodicAccess { input: source, @@ -131,12 +132,13 @@ mod tests { use std::time::Duration; use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::source::Source; #[test] fn stereo_access() { // Stereo, 1Hz audio buffer - let inner = SamplesBuffer::new(2, 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(ch!(2), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let cnt = RefCell::new(0); @@ -164,7 +166,7 @@ mod tests { #[test] fn fast_access_overflow() { // 1hz is lower than 0.5 samples per 5ms - let inner = SamplesBuffer::new(1, 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.periodic_access(Duration::from_millis(5), |_src| {}); source.next(); diff --git a/src/source/position.rs b/src/source/position.rs index ef0b3b32..3aa8192b 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -2,6 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::Source; /// Internal function that builds a `TrackPosition` object. See trait docs for @@ -12,7 +13,7 @@ pub fn track_position(source: I) -> TrackPosition { samples_counted: 0, offset_duration: 0.0, current_span_sample_rate: 0, - current_span_channels: 0, + current_span_channels: ch!(1), current_span_len: None, } } @@ -66,7 +67,7 @@ where pub fn get_pos(&self) -> Duration { let seconds = self.samples_counted as f64 / self.input.sample_rate() as f64 - / self.input.channels() as f64 + / self.input.channels().get() as f64 + self.offset_duration; Duration::from_secs_f64(seconds) } @@ -101,7 +102,7 @@ where if Some(self.samples_counted) == self.current_span_len() { self.offset_duration += self.samples_counted as f64 / self.current_span_sample_rate as f64 - / self.current_span_channels as f64; + / self.current_span_channels.get() as f64; // Reset. self.samples_counted = 0; @@ -160,11 +161,12 @@ mod tests { use std::time::Duration; use crate::buffer::SamplesBuffer; + use crate::math::ch; use crate::source::Source; #[test] fn test_position() { - let inner = SamplesBuffer::new(1, 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.track_position(); assert_eq!(source.get_pos().as_secs_f32(), 0.0); @@ -180,7 +182,7 @@ mod tests { #[test] fn test_position_in_presence_of_speedup() { - let inner = SamplesBuffer::new(1, 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.speed(2.0).track_position(); assert_eq!(source.get_pos().as_secs_f32(), 0.0); diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index c6ae01f3..bb6785ff 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -1,4 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -45,7 +46,7 @@ impl Source for SawtoothWave { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index a59118cf..5eeeb067 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -13,6 +13,7 @@ //! ``` use super::SeekError; use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::Source; use std::f32::consts::TAU; use std::time::Duration; @@ -143,7 +144,7 @@ impl Source for SignalGenerator { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/sine.rs b/src/source/sine.rs index e3814435..c2158f60 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,4 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -45,7 +46,7 @@ impl Source for SineWave { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/skip.rs b/src/source/skip.rs index ee3c7365..6d2be58a 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -40,7 +40,7 @@ where } let ns_per_sample: u128 = - NS_PER_SECOND / input.sample_rate() as u128 / input.channels() as u128; + NS_PER_SECOND / input.sample_rate() as u128 / input.channels().get() as u128; // Check if we need to skip only part of the current span. if span_len as u128 * ns_per_sample > duration.as_nanos() { @@ -62,7 +62,7 @@ where { let samples_per_channel: u128 = duration.as_nanos() * input.sample_rate() as u128 / NS_PER_SECOND; - let samples_to_skip: u128 = samples_per_channel * input.channels() as u128; + let samples_to_skip: u128 = samples_per_channel * input.channels().get() as u128; skip_samples(input, samples_to_skip as usize); } @@ -165,6 +165,7 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::common::{ChannelCount, SampleRate}; + use crate::math::ch; use crate::source::Source; fn test_skip_duration_samples_left( @@ -173,13 +174,13 @@ mod tests { seconds: u32, seconds_to_skip: u32, ) { - let buf_len = (sample_rate * channels as u32 * seconds) as usize; + let buf_len = (sample_rate * channels.get() as u32 * seconds) as usize; assert!(buf_len < 10 * 1024 * 1024); let data: Vec = vec![0f32; buf_len]; let test_buffer = SamplesBuffer::new(channels, sample_rate, data); let seconds_left = seconds.saturating_sub(seconds_to_skip); - let samples_left_expected = (sample_rate * channels as u32 * seconds_left) as usize; + let samples_left_expected = (sample_rate * channels.get() as u32 * seconds_left) as usize; let samples_left = test_buffer .skip_duration(Duration::from_secs(seconds_to_skip as u64)) .count(); @@ -190,7 +191,7 @@ mod tests { macro_rules! skip_duration_test_block { ($(channels: $ch:expr, sample rate: $sr:expr, seconds: $sec:expr, seconds to skip: $sec_to_skip:expr;)+) => { $( - test_skip_duration_samples_left($ch, $sr, $sec, $sec_to_skip); + test_skip_duration_samples_left(ch!($ch), $sr, $sec, $sec_to_skip); )+ } } diff --git a/src/source/square.rs b/src/source/square.rs index ac6bd678..9389be2b 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -1,4 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -45,7 +46,7 @@ impl Source for SquareWave { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/source/take.rs b/src/source/take.rs index 9abcf7f6..b075bdd9 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -58,7 +58,7 @@ where /// Returns the duration elapsed for each sample extracted. #[inline] fn get_duration_per_sample(input: &I) -> Duration { - let ns = NANOS_PER_SEC / (input.sample_rate() as u64 * input.channels() as u64); + let ns = NANOS_PER_SEC / (input.sample_rate() as u64 * input.channels().get() as u64); // \|/ the maximum value of `ns` is one billion, so this can't fail Duration::new(0, ns as u32) } diff --git a/src/source/triangle.rs b/src/source/triangle.rs index eb73801d..2ca9b830 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -1,4 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; +use crate::math::ch; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -45,7 +46,7 @@ impl Source for TriangleWave { #[inline] fn channels(&self) -> ChannelCount { - 1 + ch!(1) } #[inline] diff --git a/src/static_buffer.rs b/src/static_buffer.rs index 014d824c..49c18fa5 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -6,7 +6,8 @@ //! //! ``` //! use rodio::static_buffer::StaticSamplesBuffer; -//! let _ = StaticSamplesBuffer::new(1, 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! use rodio::ChannelCount; +//! let _ = StaticSamplesBuffer::new(ChannelCount::new(1).unwrap(), 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! @@ -41,12 +42,11 @@ impl StaticSamplesBuffer { sample_rate: SampleRate, data: &'static [Sample], ) -> StaticSamplesBuffer { - assert!(channels != 0); assert!(sample_rate != 0); let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() / sample_rate as u64 - / channels as u64; + / channels.get() as u64; let duration = Duration::new( duration_ns / 1_000_000_000, (duration_ns % 1_000_000_000) as u32, @@ -106,29 +106,24 @@ impl Iterator for StaticSamplesBuffer { #[cfg(test)] mod tests { + use crate::math::ch; use crate::source::Source; use crate::static_buffer::StaticSamplesBuffer; #[test] fn basic() { - let _ = StaticSamplesBuffer::new(1, 44100, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); - } - - #[test] - #[should_panic] - fn panic_if_zero_channels() { - StaticSamplesBuffer::new(0, 44100, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let _ = StaticSamplesBuffer::new(ch!(1), 44100, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] #[should_panic] fn panic_if_zero_sample_rate() { - StaticSamplesBuffer::new(1, 0, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + StaticSamplesBuffer::new(ch!(1), 0, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] fn duration_basic() { - let buf = StaticSamplesBuffer::new(2, 2, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let buf = StaticSamplesBuffer::new(ch!(2), 2, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); let dur = buf.total_duration().unwrap(); assert_eq!(dur.as_secs(), 1); assert_eq!(dur.subsec_nanos(), 500_000_000); @@ -136,7 +131,7 @@ mod tests { #[test] fn iteration() { - let mut buf = StaticSamplesBuffer::new(1, 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + let mut buf = StaticSamplesBuffer::new(ch!(1), 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); assert_eq!(buf.next(), Some(1.0)); assert_eq!(buf.next(), Some(2.0)); assert_eq!(buf.next(), Some(3.0)); diff --git a/src/stream.rs b/src/stream.rs index d69e1420..7294f947 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,84 +1,34 @@ -//! Output audio via the OS via mixers or play directly -//! -//! This module provides a builder that's used to configure and open audio output. Once -//! opened sources can be mixed into the output via `OutputStream::mixer`. -//! -//! There is also a convenience function `play` for using that output mixer to -//! play a single sound. use crate::common::{ChannelCount, SampleRate}; use crate::decoder; +use crate::math::ch; use crate::mixer::{mixer, Mixer, MixerSource}; use crate::sink::Sink; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{BufferSize, Sample, SampleFormat, StreamConfig}; +use cpal::{BufferSize, FrameCount, Sample, SampleFormat, StreamConfig, SupportedBufferSize}; use std::io::{Read, Seek}; use std::marker::Sync; +use std::sync::Arc; use std::{error, fmt}; const HZ_44100: SampleRate = 44_100; -/// `cpal::Stream` container. Use `mixer()` method to control output. -/// -///
When dropped playback will end, and the associated -/// output stream will be disposed
-/// -/// # Note -/// On drop this will print a message to stderr or emit a log msg when tracing is -/// enabled. Though we recommend you do not you can disable that print/log with: -/// [`OutputStream::log_on_drop(false)`](OutputStream::log_on_drop). -/// If the `OutputStream` is dropped because the program is panicking we do not print -/// or log anything. -/// -/// # Example -/// ```no_run -/// # use rodio::OutputStreamBuilder; -/// # fn main() -> Result<(), Box> { -/// let mut stream_handle = OutputStreamBuilder::open_default_stream()?; -/// stream_handle.log_on_drop(false); // Not recommended during development -/// println!("Output config: {:?}", stream_handle.config()); -/// let mixer = stream_handle.mixer(); -/// # Ok(()) -/// # } -/// ``` +/// `cpal::Stream` container. +/// Use `mixer()` method to control output. +/// If this is dropped, playback will end, and the associated output stream will be disposed. pub struct OutputStream { - config: OutputStreamConfig, - mixer: Mixer, - log_on_drop: bool, + mixer: Arc, _stream: cpal::Stream, } impl OutputStream { /// Access the output stream's mixer. - pub fn mixer(&self) -> &Mixer { - &self.mixer - } - - /// Access the output stream's config. - pub fn config(&self) -> &OutputStreamConfig { - &self.config - } - - /// When [`OutputStream`] is dropped a message is logged to stderr or - /// emitted through tracing if the tracing feature is enabled. - pub fn log_on_drop(&mut self, enabled: bool) { - self.log_on_drop = enabled; + pub fn mixer(&self) -> Arc { + self.mixer.clone() } } -impl Drop for OutputStream { - fn drop(&mut self) { - if self.log_on_drop && !std::thread::panicking() { - #[cfg(feature = "tracing")] - tracing::debug!("Dropping OutputStream, audio playing through this stream will stop"); - #[cfg(not(feature = "tracing"))] - eprintln!("Dropping OutputStream, audio playing through this stream will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this OutputStream") - } - } -} - -/// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] -pub struct OutputStreamConfig { +struct OutputStreamConfig { channel_count: ChannelCount, sample_rate: SampleRate, buffer_size: BufferSize, @@ -88,7 +38,7 @@ pub struct OutputStreamConfig { impl Default for OutputStreamConfig { fn default() -> Self { Self { - channel_count: 2, + channel_count: ch!(2), sample_rate: HZ_44100, buffer_size: BufferSize::Default, sample_format: SampleFormat::F32, @@ -96,26 +46,13 @@ impl Default for OutputStreamConfig { } } -impl OutputStreamConfig { - /// Access the output stream config's channel count. - pub fn channel_count(&self) -> ChannelCount { - self.channel_count - } - - /// Access the output stream config's sample rate. - pub fn sample_rate(&self) -> SampleRate { - self.sample_rate - } - - /// Access the output stream config's buffer size. - pub fn buffer_size(&self) -> &BufferSize { - &self.buffer_size - } - - /// Access the output stream config's sample format. - pub fn sample_format(&self) -> SampleFormat { - self.sample_format - } +/// Convenience builder for audio output stream. +/// It provides methods to configure several parameters of the audio output and opening default +/// device. See examples for use-cases. +#[derive(Default)] +pub struct OutputStreamBuilder { + device: Option, + config: OutputStreamConfig, } impl core::fmt::Debug for OutputStreamBuilder { @@ -133,45 +70,12 @@ impl core::fmt::Debug for OutputStreamBuilder { } } -fn default_error_callback(err: cpal::StreamError) { - #[cfg(feature = "tracing")] - tracing::error!("audio stream error: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("audio stream error: {err}"); -} - -/// Convenience builder for audio output stream. -/// It provides methods to configure several parameters of the audio output and opening default -/// device. See examples for use-cases. -/// -///
When the OutputStream is dropped playback will end, and the associated -/// output stream will be disposed
-pub struct OutputStreamBuilder -where - E: FnMut(cpal::StreamError) + Send + 'static, -{ - device: Option, - config: OutputStreamConfig, - error_callback: E, -} - -impl Default for OutputStreamBuilder { - fn default() -> Self { - Self { - device: None, - config: OutputStreamConfig::default(), - error_callback: default_error_callback, - } - } -} - impl OutputStreamBuilder { /// Sets output device and its default parameters. pub fn from_device(device: cpal::Device) -> Result { let default_config = device .default_output_config() .map_err(StreamError::DefaultStreamConfigError)?; - Ok(Self::default() .with_device(device) .with_supported_config(&default_config)) @@ -185,128 +89,62 @@ impl OutputStreamBuilder { Self::from_device(default_device) } - /// Try to open a new output stream for the default output device with its default configuration. - /// Failing that attempt to open output stream with alternative configuration and/or non default - /// output devices. Returns stream for first of the tried configurations that succeeds. - /// If all attempts fail return the initial error. - pub fn open_default_stream() -> Result { - Self::from_default_device() - .and_then(|x| x.open_stream()) - .or_else(|original_err| { - let mut devices = match cpal::default_host().output_devices() { - Ok(devices) => devices, - Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("error getting list of output devices: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("error getting list of output devices: {err}"); - return Err(original_err); - } - }; - devices - .find_map(|d| { - Self::from_device(d) - .and_then(|x| x.open_stream_or_fallback()) - .ok() - }) - .ok_or(original_err) - }) - } -} - -impl OutputStreamBuilder -where - E: FnMut(cpal::StreamError) + Send + 'static, -{ /// Sets output audio device keeping all existing stream parameters intact. /// This method is useful if you want to set other parameters yourself. /// To also set parameters that are appropriate for the device use [Self::from_device()] instead. - pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { + pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { self.device = Some(device); self } /// Sets number of output stream's channels. - pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { - assert!(channel_count > 0); + pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { self.config.channel_count = channel_count; self } /// Sets output stream's sample rate. - pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { self.config.sample_rate = sample_rate; self } /// Sets preferred output buffer size. - /// - /// To play sound without any glitches the audio card may never receive a - /// sample to late. Some samples might take longer to generate then - /// others. For example because: - /// - The OS preempts the thread creating the samples. This happens more - /// often if the computer is under high load. - /// - The decoder needs to read more data from disk. - /// - Rodio code takes longer to run for some samples then others - /// - The OS can only send audio samples in groups to the DAC. - /// - /// The OS solves this by buffering samples. The larger that buffer the - /// smaller the impact of variable sample generation time. On the other - /// hand Rodio controls audio by changing the value of samples. We can not - /// change a sample already in the OS buffer. That means there is a - /// minimum delay (latency) of `/` - /// seconds before a change made through rodio takes effect. - /// - /// # Large vs Small buffer - /// - A larger buffer size results in high latency. Changes made trough - /// Rodio (volume/skip/effects etc) takes longer before they can be heard. - /// - A small buffer might cause: - /// - Higher CPU usage - /// - Playback interruptions such as buffer underruns. - /// - Rodio to log errors like: `alsa::poll() returned POLLERR` - /// - /// # Recommendation - /// If low latency is important to you consider offering the user a method - /// to find the minimum buffer size that works well on their system under - /// expected conditions. A good example of this approach can be seen in - /// [mumble](https://www.mumble.info/documentation/user/audio-settings/) - /// (specifically the *Output Delay* & *Jitter buffer*. - /// - /// These are some typical values that are a good starting point. They may also - /// break audio completely, it depends on the system. - /// - Low-latency (audio production, live monitoring): 512-1024 - /// - General use (games, media playback): 1024-2048 - /// - Stability-focused (background music, non-interactive): 2048-4096 - pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { + /// Larger buffer size causes longer playback delays. Buffer sizes that are too small + /// may cause higher CPU usage or playback interruptions. + pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { self.config.buffer_size = buffer_size; self } /// Select scalar type that will carry a sample. - pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { + pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { self.config.sample_format = sample_format; self } - /// Set available parameters from a CPAL supported config. You can get a list of + /// Set available parameters from a CPAL supported config. You can get list of /// such configurations for an output device using [crate::stream::supported_output_configs()] pub fn with_supported_config( mut self, config: &cpal::SupportedStreamConfig, - ) -> OutputStreamBuilder { + ) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: config.channels() as ChannelCount, + channel_count: ChannelCount::new(config.channels()) + .expect("cpal should never return a zero channel output"), sample_rate: config.sample_rate().0 as SampleRate, + // In case of supported range limit buffer size to avoid unexpectedly long playback delays. + buffer_size: clamp_supported_buffer_size(config.buffer_size(), 1024), sample_format: config.sample_format(), - ..Default::default() }; self } /// Set all output stream parameters at once from CPAL stream config. - pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { + pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: config.channels as ChannelCount, + channel_count: ChannelCount::new(config.channels) + .expect("cpal should never return a zero channel output"), sample_rate: config.sample_rate.0 as SampleRate, buffer_size: config.buffer_size, ..self.config @@ -314,42 +152,23 @@ where self } - /// Set a callback that will be called when an error occurs with the stream - pub fn with_error_callback(self, callback: F) -> OutputStreamBuilder - where - F: FnMut(cpal::StreamError) + Send + 'static, - { - OutputStreamBuilder { - device: self.device, - config: self.config, - error_callback: callback, - } - } - /// Open output stream using parameters configured so far. - pub fn open_stream(self) -> Result { + pub fn open_stream(&self) -> Result { let device = self.device.as_ref().expect("output device specified"); - - OutputStream::open(device, &self.config, self.error_callback) + OutputStream::open(device, &self.config) } /// Try opening a new output stream with the builder's current stream configuration. /// Failing that attempt to open stream with other available configurations /// supported by the device. /// If all attempts fail returns initial error. - pub fn open_stream_or_fallback(&self) -> Result - where - E: Clone, - { + pub fn open_stream_or_fallback(&self) -> Result { let device = self.device.as_ref().expect("output device specified"); - let error_callback = &self.error_callback; - - OutputStream::open(device, &self.config, error_callback.clone()).or_else(|err| { + OutputStream::open(device, &self.config).or_else(|err| { for supported_config in supported_output_configs(device)? { - if let Ok(handle) = OutputStreamBuilder::default() + if let Ok(handle) = Self::default() .with_device(device.clone()) .with_supported_config(&supported_config) - .with_error_callback(error_callback.clone()) .open_stream() { return Ok(handle); @@ -358,6 +177,48 @@ where Err(err) }) } + + /// Try to open a new output stream for the default output device with its default configuration. + /// Failing that attempt to open output stream with alternative configuration and/or non default + /// output devices. Returns stream for first of the tried configurations that succeeds. + /// If all attempts fail return the initial error. + pub fn open_default_stream() -> Result { + Self::from_default_device() + .and_then(|x| x.open_stream()) + .or_else(|original_err| { + let mut devices = match cpal::default_host().output_devices() { + Ok(devices) => devices, + Err(err) => { + #[cfg(feature = "tracing")] + tracing::error!("error getting list of output devices: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("error getting list of output devices: {err}"); + return Err(original_err); + } + }; + devices + .find_map(|d| { + Self::from_device(d) + .and_then(|x| x.open_stream_or_fallback()) + .ok() + }) + .ok_or(original_err) + }) + } +} + +fn clamp_supported_buffer_size( + buffer_size: &SupportedBufferSize, + preferred_size: FrameCount, +) -> BufferSize { + match buffer_size { + SupportedBufferSize::Range { min, max } => { + let size = preferred_size.clamp(*min, *max); + assert!(size > 0, "selected buffer size is greater than zero"); + BufferSize::Fixed(size) + } + SupportedBufferSize::Unknown => BufferSize::Default, + } } /// A convenience function. Plays a sound once. @@ -375,7 +236,7 @@ where impl From<&OutputStreamConfig> for StreamConfig { fn from(config: &OutputStreamConfig) -> Self { cpal::StreamConfig { - channels: config.channel_count as cpal::ChannelCount, + channels: config.channel_count.get() as cpal::ChannelCount, sample_rate: cpal::SampleRate(config.sample_rate), buffer_size: config.buffer_size, } @@ -468,45 +329,36 @@ impl OutputStream { assert!(sz > 0, "fixed buffer size is greater than zero"); } assert!(config.sample_rate > 0, "sample rate is greater than zero"); - assert!( - config.channel_count > 0, - "channel number is greater than zero" - ); } - fn open( + fn open( device: &cpal::Device, config: &OutputStreamConfig, - error_callback: E, - ) -> Result - where - E: FnMut(cpal::StreamError) + Send + 'static, - { + ) -> Result { Self::validate_config(config); let (controller, source) = mixer(config.channel_count, config.sample_rate); - Self::init_stream(device, config, source, error_callback).and_then(|stream| { + Self::init_stream(device, config, source).and_then(|stream| { stream.play().map_err(StreamError::PlayStreamError)?; Ok(Self { _stream: stream, mixer: controller, - config: *config, - log_on_drop: true, }) }) } - fn init_stream( + fn init_stream( device: &cpal::Device, config: &OutputStreamConfig, mut samples: MixerSource, - error_callback: E, - ) -> Result - where - E: FnMut(cpal::StreamError) + Send + 'static, - { + ) -> Result { + let error_callback = |err| { + #[cfg(feature = "tracing")] + tracing::error!("Playback error: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("Playback error: {err}"); + }; let sample_format = config.sample_format; - let config = config.into(); - + let config: cpal::StreamConfig = config.into(); match sample_format { cpal::SampleFormat::F32 => device.build_output_stream::( &config, @@ -621,7 +473,7 @@ impl OutputStream { } /// Return all formats supported by the device. -pub fn supported_output_configs( +fn supported_output_configs( device: &cpal::Device, ) -> Result, StreamError> { let mut supported: Vec<_> = device diff --git a/src/wav_output.rs b/src/wav_output.rs index 3b91d8a4..fabcc1b9 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,4 +1,4 @@ -use crate::{ChannelCount, Source}; +use crate::Source; use hound::{SampleFormat, WavSpec}; use std::path; @@ -10,7 +10,7 @@ pub fn output_to_wav( wav_file: impl AsRef, ) -> Result<(), Box> { let format = WavSpec { - channels: source.channels() as ChannelCount, + channels: source.channels().get(), sample_rate: source.sample_rate(), bits_per_sample: 32, sample_format: SampleFormat::Float, @@ -26,7 +26,6 @@ pub fn output_to_wav( #[cfg(test)] mod test { use super::output_to_wav; - use crate::common::ChannelCount; use crate::Source; use std::io::BufReader; use std::time::Duration; @@ -47,7 +46,7 @@ mod test { hound::WavReader::new(BufReader::new(file)).expect("wav file can be read back"); let reference = make_source(); assert_eq!(reference.sample_rate(), reader.spec().sample_rate); - assert_eq!(reference.channels(), reader.spec().channels as ChannelCount); + assert_eq!(reference.channels().get(), reader.spec().channels); let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); let expected_samples: Vec = reference.collect(); diff --git a/tests/seek.rs b/tests/seek.rs index 6743865e..9f9f8d96 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -1,49 +1,29 @@ -#![allow(dead_code)] -#![allow(unused_imports)] - -#[cfg(feature = "symphonia-mp3")] -use rodio::{decoder::symphonia, source::SeekError}; -use rodio::{ChannelCount, Decoder, Source}; +use rodio::{Decoder, Source}; use rstest::rstest; use rstest_reuse::{self, *}; -use std::io::{Read, Seek}; +use std::io::{BufReader, Read, Seek}; use std::path::Path; use std::time::Duration; -#[cfg(any( - feature = "claxon", - feature = "minimp3", - feature = "symphonia-aac", - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[template] #[rstest] -#[cfg_attr( - all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), - case("ogg", true, "symphonia") -)] +// note: disabled, broken decoder see issue: #516 and #539 +// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")], #[cfg_attr( all(feature = "minimp3", not(feature = "symphonia-mp3")), case("mp3", false, "minimp3") )] #[cfg_attr( - all(feature = "hound", not(feature = "symphonia-wav")), + all(feature = "wav", not(feature = "symphonia-wav")), case("wav", true, "hound") )] #[cfg_attr( - all(feature = "claxon", not(feature = "symphonia-flac")), + all(feature = "flac", not(feature = "symphonia-flac")), case("flac", false, "claxon") )] #[cfg_attr(feature = "symphonia-mp3", case("mp3", true, "symphonia"))] -#[cfg_attr( - all(feature = "symphonia-isomp4", feature = "symphonia-aac"), - case("m4a", true, "symphonia") -)] +// note: disabled, broken decoder see issue: #577 +#[cfg_attr(feature = "symphonia-isomp4", case("m4a", true, "symphonia"))] #[cfg_attr(feature = "symphonia-wav", case("wav", true, "symphonia"))] #[cfg_attr(feature = "symphonia-flac", case("flac", true, "symphonia"))] fn all_decoders( @@ -53,43 +33,21 @@ fn all_decoders( ) { } -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[template] #[rstest] +// note: disabled, broken decoder see issue: #516 and #539 +// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")], #[cfg_attr( - all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), - case("ogg", "symphonia") -)] -#[cfg_attr( - all(feature = "hound", not(feature = "symphonia-wav")), + all(feature = "wav", not(feature = "symphonia-wav")), case("wav", "hound") )] #[cfg_attr(feature = "symphonia-mp3", case("mp3", "symphonia"))] -#[cfg_attr( - all(feature = "symphonia-isomp4", feature = "symphonia-aac"), - case("m4a", "symphonia") -)] +// note: disabled, broken decoder see issue: #577 +// #[cfg_attr(feature = "symphonia-isomp4", case("m4a", "symphonia"))] #[cfg_attr(feature = "symphonia-wav", case("wav", "symphonia"))] #[cfg_attr(feature = "symphonia-flac", case("flac", "symphonia"))] fn supported_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {} -#[cfg(any( - feature = "claxon", - feature = "minimp3", - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[apply(all_decoders)] #[trace] fn seek_returns_err_if_unsupported( @@ -102,14 +60,6 @@ fn seek_returns_err_if_unsupported( assert_eq!(res.is_ok(), supports_seek, "decoder: {decoder_name}"); } -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[apply(supported_decoders)] #[trace] fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: &'static str) { @@ -121,14 +71,6 @@ fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: assert!(time_remaining(decoder) < Duration::from_secs(1)); } -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[apply(supported_decoders)] #[trace] fn seek_results_in_correct_remaining_playtime( @@ -157,14 +99,6 @@ fn seek_results_in_correct_remaining_playtime( } } -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[apply(supported_decoders)] #[trace] fn seek_possible_after_exausting_source( @@ -175,37 +109,23 @@ fn seek_possible_after_exausting_source( while source.next().is_some() {} assert!(source.next().is_none()); - source.try_seek(Duration::ZERO).unwrap(); + source.try_seek(Duration::from_secs(0)).unwrap(); assert!(source.next().is_some()); } -#[cfg(any( - feature = "symphonia-flac", - feature = "symphonia-mp3", - feature = "symphonia-isomp4", - feature = "symphonia-ogg", - feature = "symphonia-wav", - feature = "hound", -))] #[apply(supported_decoders)] #[trace] fn seek_does_not_break_channel_order( #[case] format: &'static str, #[case] _decoder_name: &'static str, ) { - if format == "m4a" { - // skip this test for m4a while the symphonia decoder has issues with aac timing. - // re-investigate when symphonia 0.5.5 or greater is released. - return; - } - let mut source = get_rl(format); let channels = source.channels(); - assert_eq!(channels, 2, "test needs a stereo beep file"); + assert_eq!(channels.get(), 2, "test needs a stereo beep file"); let beep_range = second_channel_beep_range(&mut source); let beep_start = Duration::from_secs_f32( - beep_range.start as f32 / source.channels() as f32 / source.sample_rate() as f32, + beep_range.start as f32 / source.channels().get() as f32 / source.sample_rate() as f32, ); let mut source = get_rl(format); @@ -222,9 +142,9 @@ fn seek_does_not_break_channel_order( source.try_seek(beep_start + offset).unwrap(); let samples: Vec<_> = source.by_ref().take(100).collect(); - let channel0 = channel_offset; + let channel0 = 0 + channel_offset; assert!( - is_silent(&samples, source.channels(), channel0), + is_silent(&samples, source.channels().get() as usize, channel0), "channel0 should be silent, channel0 starts at idx: {channel0} seek: {beep_start:?} + {offset:?} @@ -232,7 +152,7 @@ fn seek_does_not_break_channel_order( ); let channel1 = (1 + channel_offset) % 2; assert!( - !is_silent(&samples, source.channels(), channel1), + !is_silent(&samples, source.channels().get() as usize, channel1), "channel1 should not be silent, channel1; starts at idx: {channel1} seek: {beep_start:?} + {offset:?} @@ -241,40 +161,11 @@ fn seek_does_not_break_channel_order( } } -#[cfg(feature = "symphonia-mp3")] -#[test] -fn random_access_seeks() { - // Decoder::new:: does *not* set byte_len and is_seekable - let mp3_file = std::fs::File::open("assets/music.mp3").unwrap(); - let mut decoder = Decoder::new(mp3_file).unwrap(); - assert!( - decoder.try_seek(Duration::from_secs(2)).is_ok(), - "forward seek should work without byte_len" - ); - assert!( - matches!( - decoder.try_seek(Duration::from_secs(1)), - Err(SeekError::SymphoniaDecoder( - symphonia::SeekError::RandomAccessNotSupported, - )) - ), - "backward seek should fail without byte_len" - ); - - // Decoder::try_from:: sets byte_len and is_seekable - let mut decoder = get_music("mp3"); - assert!( - decoder.try_seek(Duration::from_secs(2)).is_ok(), - "forward seek should work with byte_len" - ); - assert!( - decoder.try_seek(Duration::from_secs(1)).is_ok(), - "backward seek should work with byte_len" - ); -} - -fn second_channel_beep_range(source: &mut R) -> std::ops::Range { - let channels = source.channels() as usize; +fn second_channel_beep_range(source: &mut R) -> std::ops::Range +where + R: Iterator, +{ + let channels = source.channels().get() as usize; let samples: Vec = source.by_ref().collect(); const WINDOW: usize = 50; @@ -311,21 +202,15 @@ fn second_channel_beep_range(source: &mut R) -> std::ops::Rang .next_multiple_of(channels); let samples = &samples[beep_starts..beep_starts + 100]; - assert!( - is_silent(samples, channels as ChannelCount, 0), - "{samples:?}" - ); - assert!( - !is_silent(samples, channels as ChannelCount, 1), - "{samples:?}" - ); + assert!(is_silent(samples, channels, 0), "{samples:?}"); + assert!(!is_silent(samples, channels, 1), "{samples:?}"); beep_starts..beep_ends } -fn is_silent(samples: &[f32], channels: ChannelCount, channel: usize) -> bool { +fn is_silent(samples: &[f32], channels: usize, channel: usize) -> bool { assert_eq!(samples.len(), 100); - let channel = samples.iter().skip(channel).step_by(channels as usize); + let channel = samples.iter().skip(channel).step_by(channels); let volume = channel.map(|s| s.abs()).sum::() / samples.len() as f32 * channels as f32; const BASICALLY_ZERO: f32 = 0.0001; @@ -334,7 +219,7 @@ fn is_silent(samples: &[f32], channels: ChannelCount, channel: usize) -> bool { fn time_remaining(decoder: Decoder) -> Duration { let rate = decoder.sample_rate() as f64; - let n_channels = decoder.channels() as f64; + let n_channels = decoder.channels().get() as f64; let n_samples = decoder.into_iter().count() as f64; Duration::from_secs_f64(n_samples / rate / n_channels) } @@ -342,11 +227,12 @@ fn time_remaining(decoder: Decoder) -> Duration { fn get_music(format: &str) -> Decoder { let asset = Path::new("assets/music").with_extension(format); let file = std::fs::File::open(asset).unwrap(); - Decoder::try_from(file).unwrap() + Decoder::new(BufReader::new(file)).unwrap() } fn get_rl(format: &str) -> Decoder { let asset = Path::new("assets/RL").with_extension(format); + println!("opening: {}", asset.display()); let file = std::fs::File::open(asset).unwrap(); - Decoder::try_from(file).unwrap() + Decoder::new(BufReader::new(file)).unwrap() } From 9cdf0fe14368c4e838552eb7e46fa7b4d0504e04 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Fri, 28 Feb 2025 18:03:36 +0100 Subject: [PATCH 2/4] rename ch! macro to nz and make SampleRate NonZero Similar to the channels situation a lot of bugs seem to be caused by zero sample_rate. Since audio can literally not play at zero speed it makes no sense to have zero sample_rate's. --- benches/pipeline.rs | 8 +- benches/resampler.rs | 3 +- benches/shared.rs | 2 +- examples/custom_config.rs | 3 +- examples/mix_multiple_sources.rs | 2 +- examples/signal_generator.rs | 3 +- src/buffer.rs | 31 ++- src/common.rs | 2 +- src/conversions/channels.rs | 26 +-- src/conversions/sample_rate.rs | 49 ++--- src/decoder/flac.rs | 3 +- src/decoder/mod.rs | 8 +- src/decoder/mp3.rs | 4 +- src/decoder/symphonia.rs | 22 +- src/decoder/vorbis.rs | 3 +- src/decoder/wav.rs | 5 +- src/math.rs | 11 +- src/mixer.rs | 62 +++--- src/queue.rs | 32 +-- src/sink.rs | 18 +- src/source/agc.rs | 2 +- src/source/blt.rs | 2 +- src/source/buffered.rs | 6 +- src/source/chirp.rs | 10 +- src/source/crossfade.rs | 6 +- src/source/delay.rs | 2 +- src/source/empty.rs | 6 +- src/source/empty_callback.rs | 6 +- src/source/from_iter.rs | 18 +- src/source/limit.rs | 29 ++- src/source/linear_ramp.rs | 8 +- src/source/noise.rs | 37 ++-- src/source/periodic.rs | 8 +- src/source/position.rs | 16 +- src/source/sawtooth.rs | 6 +- src/source/signal_generator.rs | 20 +- src/source/sine.rs | 6 +- src/source/skip.rs | 13 +- src/source/speed.rs | 3 +- src/source/square.rs | 6 +- src/source/take.rs | 2 +- src/source/triangle.rs | 6 +- src/static_buffer.rs | 22 +- src/stream.rs | 351 ++++++++++++++++++++++--------- src/wav_output.rs | 4 +- tests/limit.rs | 7 +- tests/seek.rs | 184 +++++++++++++--- 47 files changed, 684 insertions(+), 399 deletions(-) diff --git a/benches/pipeline.rs b/benches/pipeline.rs index 11363215..3164ca40 100644 --- a/benches/pipeline.rs +++ b/benches/pipeline.rs @@ -1,3 +1,4 @@ +use std::num::NonZero; use std::time::Duration; use divan::Bencher; @@ -32,8 +33,11 @@ fn long(bencher: Bencher) { .buffered() .reverb(Duration::from_secs_f32(0.05), 0.3) .skippable(); - let resampled = - UniformSourceIterator::new(effects_applied, ChannelCount::new(2).unwrap(), 40_000); + let resampled = UniformSourceIterator::new( + effects_applied, + ChannelCount::new(2).unwrap(), + NonZero::new(40_000).unwrap(), + ); resampled.for_each(divan::black_box_drop) }) } diff --git a/benches/resampler.rs b/benches/resampler.rs index 6c5c0683..deb10f08 100644 --- a/benches/resampler.rs +++ b/benches/resampler.rs @@ -4,7 +4,7 @@ use rodio::source::UniformSourceIterator; mod shared; use shared::music_wav; -use rodio::Source; +use rodio::{SampleRate, Source}; fn main() { divan::main(); @@ -31,6 +31,7 @@ const COMMON_SAMPLE_RATES: [u32; 12] = [ #[divan::bench(args = COMMON_SAMPLE_RATES)] fn resample_to(bencher: Bencher, target_sample_rate: u32) { + let target_sample_rate = SampleRate::new(target_sample_rate).unwrap(); bencher .with_inputs(|| { let source = music_wav(); diff --git a/benches/shared.rs b/benches/shared.rs index e3ea7835..dbe23395 100644 --- a/benches/shared.rs +++ b/benches/shared.rs @@ -7,7 +7,7 @@ use rodio::{ChannelCount, Sample, SampleRate, Source}; pub struct TestSource { samples: vec::IntoIter, channels: ChannelCount, - sample_rate: u32, + sample_rate: SampleRate, total_duration: Duration, } diff --git a/examples/custom_config.rs b/examples/custom_config.rs index 4e1ff76a..aa83a7ee 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -3,6 +3,7 @@ use cpal::{BufferSize, SampleFormat}; use rodio::source::SineWave; use rodio::Source; use std::error::Error; +use std::num::NonZero; use std::thread; use std::time::Duration; @@ -15,7 +16,7 @@ fn main() -> Result<(), Box> { // No need to set all parameters explicitly here, // the defaults were set from the device's description. .with_buffer_size(BufferSize::Fixed(256)) - .with_sample_rate(48_000) + .with_sample_rate(NonZero::new(48_000).unwrap()) .with_sample_format(SampleFormat::F32) // Note that the function below still tries alternative configs if the specified one fails. // If you need to only use the exact specified configuration, diff --git a/examples/mix_multiple_sources.rs b/examples/mix_multiple_sources.rs index b312a01d..8c6be920 100644 --- a/examples/mix_multiple_sources.rs +++ b/examples/mix_multiple_sources.rs @@ -6,7 +6,7 @@ use std::time::Duration; fn main() -> Result<(), Box> { // Construct a dynamic controller and mixer, stream_handle, and sink. - let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), 44_100); + let (controller, mixer) = mixer::mixer(NonZero::new(2).unwrap(), NonZero::new(44_100).unwrap()); let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(stream_handle.mixer()); diff --git a/examples/signal_generator.rs b/examples/signal_generator.rs index 1ae4048e..2257f30d 100644 --- a/examples/signal_generator.rs +++ b/examples/signal_generator.rs @@ -1,6 +1,7 @@ //! Test signal generator example. use std::error::Error; +use std::num::NonZero; fn main() -> Result<(), Box> { use rodio::source::{chirp, Function, SignalGenerator, Source}; @@ -11,7 +12,7 @@ fn main() -> Result<(), Box> { let test_signal_duration = Duration::from_millis(1000); let interval_duration = Duration::from_millis(1500); - let sample_rate = 48000; + let sample_rate = NonZero::new(48000).unwrap(); println!("Playing 1000 Hz tone"); stream_handle.mixer().add( diff --git a/src/buffer.rs b/src/buffer.rs index 1744e042..abca44d4 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -6,8 +6,8 @@ //! //! ``` //! use rodio::buffer::SamplesBuffer; -//! use rodio::ChannelCount; -//! let _ = SamplesBuffer::new(ChannelCount::new(1).unwrap(), 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! use core::num::NonZero; +//! let _ = SamplesBuffer::new(NonZero::new(1).unwrap(), NonZero::new(44100).unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! @@ -40,11 +40,9 @@ impl SamplesBuffer { where D: Into>, { - assert!(sample_rate >= 1); - let data: Arc<[f32]> = data.into().into(); let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() - / sample_rate as u64 + / sample_rate.get() as u64 / channels.get() as u64; let duration = Duration::new( duration_ns / 1_000_000_000, @@ -90,7 +88,8 @@ impl Source for SamplesBuffer { // sample directly. let curr_channel = self.pos % self.channels().get() as usize; - let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels().get() as f32; + let new_pos = + pos.as_secs_f32() * self.sample_rate().get() as f32 * self.channels().get() as f32; // saturate pos at the end of the source let new_pos = new_pos as usize; let new_pos = new_pos.min(self.data.len()); @@ -123,23 +122,17 @@ impl Iterator for SamplesBuffer { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::source::Source; #[test] fn basic() { - let _ = SamplesBuffer::new(ch!(1), 44100, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); - } - - #[test] - #[should_panic] - fn panic_if_zero_sample_rate() { - SamplesBuffer::new(ch!(1), 0, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let _ = SamplesBuffer::new(nz!(1), nz!(44100), vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] fn duration_basic() { - let buf = SamplesBuffer::new(ch!(2), 2, vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let buf = SamplesBuffer::new(nz!(2), nz!(2), vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); let dur = buf.total_duration().unwrap(); assert_eq!(dur.as_secs(), 1); assert_eq!(dur.subsec_nanos(), 500_000_000); @@ -147,7 +140,7 @@ mod tests { #[test] fn iteration() { - let mut buf = SamplesBuffer::new(ch!(1), 44100, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + let mut buf = SamplesBuffer::new(nz!(1), nz!(44100), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); assert_eq!(buf.next(), Some(1.0)); assert_eq!(buf.next(), Some(2.0)); assert_eq!(buf.next(), Some(3.0)); @@ -166,8 +159,8 @@ mod tests { #[test] fn channel_order_stays_correct() { - const SAMPLE_RATE: SampleRate = 100; - const CHANNELS: ChannelCount = ch!(2); + const SAMPLE_RATE: SampleRate = nz!(100); + const CHANNELS: ChannelCount = nz!(2); let mut buf = SamplesBuffer::new( CHANNELS, SAMPLE_RATE, @@ -176,7 +169,7 @@ mod tests { buf.try_seek(Duration::from_secs(5)).unwrap(); assert_eq!( buf.next(), - Some(5.0 * SAMPLE_RATE as f32 * CHANNELS.get() as f32) + Some(5.0 * SAMPLE_RATE.get() as f32 * CHANNELS.get() as f32) ); assert!(buf.next().is_some_and(|s| s.trunc() as i32 % 2 == 1)); diff --git a/src/common.rs b/src/common.rs index bf3f7acb..17ff3b98 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ use std::num::NonZero; /// Stream sample rate (a frame rate or samples per second per channel). -pub type SampleRate = u32; +pub type SampleRate = NonZero; /// Number of channels in a stream. Can never be Zero pub type ChannelCount = NonZero; diff --git a/src/conversions/channels.rs b/src/conversions/channels.rs index 980ebf98..c1401357 100644 --- a/src/conversions/channels.rs +++ b/src/conversions/channels.rs @@ -108,19 +108,19 @@ impl ExactSizeIterator for ChannelCountConverter where I: ExactSizeIterato mod test { use super::ChannelCountConverter; use crate::common::ChannelCount; - use crate::math::ch; + use crate::math::nz; use crate::Sample; #[test] fn remove_channels() { let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let output = - ChannelCountConverter::new(input.into_iter(), ch!(3), ch!(2)).collect::>(); + ChannelCountConverter::new(input.into_iter(), nz!(3), nz!(2)).collect::>(); assert_eq!(output, [1.0, 2.0, 4.0, 5.0]); let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let output = - ChannelCountConverter::new(input.into_iter(), ch!(4), ch!(1)).collect::>(); + ChannelCountConverter::new(input.into_iter(), nz!(4), nz!(1)).collect::>(); assert_eq!(output, [1.0, 5.0]); } @@ -128,17 +128,17 @@ mod test { fn add_channels() { let input = vec![1.0, 2.0, 3.0, 4.0]; let output = - ChannelCountConverter::new(input.into_iter(), ch!(1), ch!(2)).collect::>(); + ChannelCountConverter::new(input.into_iter(), nz!(1), nz!(2)).collect::>(); assert_eq!(output, [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0]); let input = vec![1.0, 2.0]; let output = - ChannelCountConverter::new(input.into_iter(), ch!(1), ch!(4)).collect::>(); + ChannelCountConverter::new(input.into_iter(), nz!(1), nz!(4)).collect::>(); assert_eq!(output, [1.0, 1.0, 0.0, 0.0, 2.0, 2.0, 0.0, 0.0]); let input = vec![1.0, 2.0, 3.0, 4.0]; let output = - ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(4)).collect::>(); + ChannelCountConverter::new(input.into_iter(), nz!(2), nz!(4)).collect::>(); assert_eq!(output, [1.0, 2.0, 0.0, 0.0, 3.0, 4.0, 0.0, 0.0]); } @@ -155,24 +155,24 @@ mod test { assert_eq!(converter.size_hint(), (0, Some(0))); } - test(&[1.0, 2.0, 3.0], ch!(1), ch!(2)); - test(&[1.0, 2.0, 3.0, 4.0], ch!(2), ch!(4)); - test(&[1.0, 2.0, 3.0, 4.0], ch!(4), ch!(2)); - test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], ch!(3), ch!(8)); - test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], ch!(4), ch!(1)); + test(&[1.0, 2.0, 3.0], nz!(1), nz!(2)); + test(&[1.0, 2.0, 3.0, 4.0], nz!(2), nz!(4)); + test(&[1.0, 2.0, 3.0, 4.0], nz!(4), nz!(2)); + test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], nz!(3), nz!(8)); + test(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], nz!(4), nz!(1)); } #[test] fn len_more() { let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(3)); + let output = ChannelCountConverter::new(input.into_iter(), nz!(2), nz!(3)); assert_eq!(output.len(), 6); } #[test] fn len_less() { let input = vec![1.0, 2.0, 3.0, 4.0]; - let output = ChannelCountConverter::new(input.into_iter(), ch!(2), ch!(1)); + let output = ChannelCountConverter::new(input.into_iter(), nz!(2), nz!(1)); assert_eq!(output.len(), 2); } } diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 95888c57..2c7db4ac 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -54,9 +54,6 @@ where to: SampleRate, num_channels: ChannelCount, ) -> SampleRateConverter { - assert!(from >= 1); - assert!(to >= 1); - let (first_samples, next_samples) = if from == to { // if `from` == `to` == 1, then we just pass through (Vec::new(), Vec::new()) @@ -73,7 +70,7 @@ where }; // Reducing numerator to avoid numeric overflows during interpolation. - let (to, from) = Ratio::new(to, from).into_raw(); + let (to, from) = Ratio::new(to.get(), from.get()).into_raw(); SampleRateConverter { input, @@ -241,22 +238,18 @@ impl ExactSizeIterator for SampleRateConverter where I: ExactSizeIterator< mod test { use super::SampleRateConverter; use crate::common::{ChannelCount, SampleRate}; - use crate::math::ch; + use crate::math::nz; use crate::Sample; use core::time::Duration; use quickcheck::{quickcheck, TestResult}; quickcheck! { /// Check that resampling an empty input produces no output. - fn empty(from: u16, to: u16, channels: ChannelCount) -> TestResult { + fn empty(from: SampleRate, to: SampleRate, channels: ChannelCount) -> TestResult { if channels.get() > 128 - || from == 0 - || to == 0 { return TestResult::discard(); } - let from = from as SampleRate; - let to = to as SampleRate; let input: Vec = Vec::new(); let output = @@ -268,9 +261,8 @@ mod test { } /// Check that resampling to the same rate does not change the signal. - fn identity(from: u16, channels: ChannelCount, input: Vec) -> TestResult { - if channels.get() > 128 || from == 0 { return TestResult::discard(); } - let from = from as SampleRate; + fn identity(from: SampleRate, channels: ChannelCount, input: Vec) -> TestResult { + if channels.get() > 128 { return TestResult::discard(); } let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); let output = @@ -282,14 +274,14 @@ mod test { /// Check that dividing the sample rate by k (integer) is the same as /// dropping a sample from each channel. - fn divide_sample_rate(to: u16, k: u16, input: Vec, channels: ChannelCount) -> TestResult { - if k == 0 || channels.get() > 128 || to == 0 || to > 48000 { + fn divide_sample_rate(to: SampleRate, k: u16, input: Vec, channels: ChannelCount) -> TestResult { + if k == 0 || channels.get() > 128 || to.get() > 48000 { return TestResult::discard(); } let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); let to = to as SampleRate; - let from = to * k as u32; + let from = to.get() * k as u32; // Truncate the input, so it contains an integer number of spans. let input = { @@ -300,7 +292,7 @@ mod test { }; let output = - SampleRateConverter::new(input.clone().into_iter(), from, to, channels) + SampleRateConverter::new(input.clone().into_iter(), SampleRate::new(from).unwrap(), to, channels) .collect::>(); TestResult::from_bool(input.chunks_exact(channels.get().into()) @@ -308,15 +300,16 @@ mod test { } /// Check that, after multiplying the sample rate by k, every k-th - /// sample in the output matches exactly with the input. - fn multiply_sample_rate(from: u16, k: u8, input: Vec, channels: ChannelCount) -> TestResult { - if k == 0 || channels.get() > 128 || from == 0 { + /// sample in the output matches exactly with the input. + fn multiply_sample_rate(from: SampleRate, k: u8, input: Vec, channels: ChannelCount) -> TestResult { + if k == 0 || from.get() > u16::MAX as u32 || channels.get() > 128 { return TestResult::discard(); } let input = Vec::from_iter(input.iter().map(|x| *x as Sample)); let from = from as SampleRate; - let to = from * k as u32; + dbg!(from, k); + let to = from.get() * k as u32; // Truncate the input, so it contains an integer number of spans. let input = { @@ -327,7 +320,7 @@ mod test { }; let output = - SampleRateConverter::new(input.clone().into_iter(), from, to, channels) + SampleRateConverter::new(input.clone().into_iter(), from, SampleRate::new(to).unwrap(), channels) .collect::>(); TestResult::from_bool(input == @@ -340,17 +333,15 @@ mod test { /// except by a negligible amount (± 1ms). Reproduces #316. /// Ignored, pending a bug fix. fn preserve_durations(d: Duration, freq: f32, to: SampleRate) -> TestResult { - if to == 0 { return TestResult::discard(); } - use crate::source::{SineWave, Source}; let source = SineWave::new(freq).take_duration(d); let from = source.sample_rate(); let resampled = - SampleRateConverter::new(source, from, to, ch!(1)); + SampleRateConverter::new(source, from, to, nz!(1)); let duration = - Duration::from_secs_f32(resampled.count() as f32 / to as f32); + Duration::from_secs_f32(resampled.count() as f32 / to.get() as f32); let delta = duration.abs_diff(d); TestResult::from_bool(delta < Duration::from_millis(1)) @@ -360,7 +351,7 @@ mod test { #[test] fn upsample() { let input = vec![2.0, 16.0, 4.0, 18.0, 6.0, 20.0, 8.0, 22.0]; - let output = SampleRateConverter::new(input.into_iter(), 2000, 3000, ch!(2)); + let output = SampleRateConverter::new(input.into_iter(), nz!(2000), nz!(3000), nz!(2)); assert_eq!(output.len(), 12); // Test the source's Iterator::size_hint() let output = output.map(|x| x.trunc()).collect::>(); @@ -373,7 +364,7 @@ mod test { #[test] fn upsample2() { let input = vec![1.0, 14.0]; - let output = SampleRateConverter::new(input.into_iter(), 1000, 7000, ch!(1)); + let output = SampleRateConverter::new(input.into_iter(), nz!(1000), nz!(7000), nz!(1)); let size_estimation = output.len(); let output = output.map(|x| x.trunc()).collect::>(); assert_eq!(output, [1.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0]); @@ -383,7 +374,7 @@ mod test { #[test] fn downsample() { let input = Vec::from_iter((0..17).map(|x| x as Sample)); - let output = SampleRateConverter::new(input.into_iter(), 12000, 2400, ch!(1)); + let output = SampleRateConverter::new(input.into_iter(), nz!(12000), nz!(2400), nz!(1)); let size_estimation = output.len(); let output = output.collect::>(); assert_eq!(output, [0.0, 5.0, 10.0, 15.0]); diff --git a/src/decoder/flac.rs b/src/decoder/flac.rs index f60dbd0e..be93d6bc 100644 --- a/src/decoder/flac.rs +++ b/src/decoder/flac.rs @@ -60,7 +60,8 @@ where current_block_channel_len: 1, current_block_off: 0, bits_per_sample: spec.bits_per_sample, - sample_rate, + sample_rate: SampleRate::new(sample_rate) + .expect("flac data should never have a zero sample rate"), channels: ChannelCount::new( spec.channels .try_into() diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 277017fc..b4e03f20 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -56,9 +56,9 @@ use std::{ #[allow(unused_imports)] use std::io::SeekFrom; -use crate::math::ch; use crate::{ common::{ChannelCount, SampleRate}, + math::nz, source::{SeekError, Source}, Sample, }; @@ -713,9 +713,7 @@ where /// Returns the default channel count if there is no active decoder. #[inline] fn channels(&self) -> ChannelCount { - self.inner - .as_ref() - .map_or(ChannelCount::default(), |inner| inner.channels()) + self.inner.as_ref().map_or(nz!(1), |inner| inner.channels()) } /// Returns the sample rate of the audio stream. @@ -725,7 +723,7 @@ where fn sample_rate(&self) -> SampleRate { self.inner .as_ref() - .map_or(SampleRate::default(), |inner| inner.sample_rate()) + .map_or(nz!(44100), |inner| inner.sample_rate()) } /// Returns the total duration of this audio source. diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index 8e6c6680..e14b3579 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -67,7 +67,7 @@ where #[inline] fn sample_rate(&self) -> SampleRate { - self.current_span.sample_rate as _ + self.current_span.sample_rate } #[inline] @@ -78,7 +78,7 @@ where fn try_seek(&mut self, _pos: Duration) -> Result<(), SeekError> { // TODO waiting for PR in minimp3_fixed or minimp3 - // let pos = (pos.as_secs_f32() * self.sample_rate() as f32) as u64; + // let pos = (pos.as_secs_f32() * self.sample_rate().get() as f32) as u64; // // do not trigger a sample_rate, channels and frame/span len update // // as the seek only takes effect after the current frame/span is done // self.decoder.seek_samples(pos)?; diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index b16878b0..6c81d672 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -177,7 +177,7 @@ impl Source for SymphoniaDecoder { #[inline] fn sample_rate(&self) -> SampleRate { - self.spec.rate + SampleRate::new(self.spec.rate).expect("audio should always have a non zero SampleRate") } #[inline] @@ -204,7 +204,7 @@ impl Source for SymphoniaDecoder { } // Remember the current channel, so we can restore it after seeking. - let active_channel = self.current_span_offset % self.channels() as usize; + let active_channel = self.current_span_offset % self.channels().get() as usize; let seek_res = match self.format.seek( self.seek_mode, @@ -302,30 +302,18 @@ impl SymphoniaDecoder { .calc_time(seek_res.required_ts.saturating_sub(seek_res.actual_ts)), ) .as_secs_f32() - * self.sample_rate() as f32 - * self.channels() as f32) + * self.sample_rate().get() as f32 + * self.channels().get() as f32) .ceil() as usize; // Re-align the seek position to the first channel. - samples_to_skip -= samples_to_skip % self.channels() as usize; + samples_to_skip -= samples_to_skip % self.channels().get() as usize; // Skip ahead to the precise position. for _ in 0..samples_to_skip { self.next(); } -<<<<<<< HEAD -||||||| parent of 3fb371d (This makes ChannelCount NonZero and channels not zero asserts) - let decoded = decoded.map_err(SeekError::Decoding)?; - decoded.spec().clone_into(&mut self.spec); - self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec); - self.current_span_offset = samples_to_pass as usize * self.channels() as usize; -======= - let decoded = decoded.map_err(SeekError::Decoding)?; - decoded.spec().clone_into(&mut self.spec); - self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec); - self.current_span_offset = samples_to_pass as usize * self.channels().get() as usize; ->>>>>>> 3fb371d (This makes ChannelCount NonZero and channels not zero asserts) Ok(()) } } diff --git a/src/decoder/vorbis.rs b/src/decoder/vorbis.rs index 1cf76a12..8d81fbf5 100644 --- a/src/decoder/vorbis.rs +++ b/src/decoder/vorbis.rs @@ -75,7 +75,8 @@ where #[inline] fn sample_rate(&self) -> SampleRate { - self.stream_reader.ident_hdr.audio_sample_rate + SampleRate::new(self.stream_reader.ident_hdr.audio_sample_rate) + .expect("audio should always have a non zero SampleRate") } #[inline] diff --git a/src/decoder/wav.rs b/src/decoder/wav.rs index 0259f22b..db1a39d0 100644 --- a/src/decoder/wav.rs +++ b/src/decoder/wav.rs @@ -53,7 +53,8 @@ where Ok(WavDecoder { reader, total_duration, - sample_rate: sample_rate as SampleRate, + sample_rate: SampleRate::new(sample_rate) + .expect("wav should have a sample rate higher then zero"), channels: ChannelCount::new(channels).expect("wav should have a least one channel"), }) } @@ -171,7 +172,7 @@ where fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let file_len = self.reader.reader.duration(); - let new_pos = pos.as_secs_f32() * self.sample_rate() as f32; + let new_pos = pos.as_secs_f32() * self.sample_rate().get() as f32; let new_pos = new_pos as u32; let new_pos = new_pos.min(file_len); // saturate pos at the end of the source diff --git a/src/math.rs b/src/math.rs index c47209c8..551f2e28 100644 --- a/src/math.rs +++ b/src/math.rs @@ -76,15 +76,16 @@ pub fn linear_to_db(linear: f32) -> f32 { linear.log2() * std::f32::consts::LOG10_2 * 20.0 } -/// short macro to generate a `ChannelCount` for tests -/// this panics during compile if the passed in literal is zero -macro_rules! ch { +/// short macro to generate a `NonZero`. It panics during compile if the +/// passed in literal is zero. Used for `ChannelCount` and `Samplerate` +/// constants +macro_rules! nz { ($n:literal) => { - const { core::num::NonZeroU16::new($n).unwrap() } + const { core::num::NonZero::new($n).unwrap() } }; } -pub(crate) use ch; +pub(crate) use nz; #[cfg(test)] mod test { diff --git a/src/mixer.rs b/src/mixer.rs index dd9cdba2..b434dba9 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -211,23 +211,27 @@ impl MixerSource { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::mixer; use crate::source::Source; #[test] fn basic() { - let (tx, mut rx) = mixer::mixer(ch!(1), 48000); + let (tx, mut rx) = mixer::mixer(nz!(1), nz!(48000)); tx.add(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); - tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + nz!(1), + nz!(48000), + vec![5.0, 5.0, 5.0, 5.0], + )); - assert_eq!(rx.channels(), ch!(1)); - assert_eq!(rx.sample_rate(), 48000); + assert_eq!(rx.channels(), nz!(1)); + assert_eq!(rx.sample_rate().get(), 48000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(-5.0)); assert_eq!(rx.next(), Some(15.0)); @@ -237,17 +241,21 @@ mod tests { #[test] fn channels_conv() { - let (tx, mut rx) = mixer::mixer(ch!(2), 48000); + let (tx, mut rx) = mixer::mixer(nz!(2), nz!(48000)); tx.add(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); - tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + nz!(1), + nz!(48000), + vec![5.0, 5.0, 5.0, 5.0], + )); - assert_eq!(rx.channels(), ch!(2)); - assert_eq!(rx.sample_rate(), 48000); + assert_eq!(rx.channels(), nz!(2)); + assert_eq!(rx.sample_rate().get(), 48000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(-5.0)); @@ -261,17 +269,21 @@ mod tests { #[test] fn rate_conv() { - let (tx, mut rx) = mixer::mixer(ch!(1), 96000); + let (tx, mut rx) = mixer::mixer(nz!(1), nz!(96000)); tx.add(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); - tx.add(SamplesBuffer::new(ch!(1), 48000, vec![5.0, 5.0, 5.0, 5.0])); + tx.add(SamplesBuffer::new( + nz!(1), + nz!(48000), + vec![5.0, 5.0, 5.0, 5.0], + )); - assert_eq!(rx.channels(), ch!(1)); - assert_eq!(rx.sample_rate(), 96000); + assert_eq!(rx.channels(), nz!(1)); + assert_eq!(rx.sample_rate().get(), 96000); assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(-5.0)); @@ -284,11 +296,11 @@ mod tests { #[test] fn start_afterwards() { - let (tx, mut rx) = mixer::mixer(ch!(1), 48000); + let (tx, mut rx) = mixer::mixer(nz!(1), nz!(48000)); tx.add(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); @@ -296,8 +308,8 @@ mod tests { assert_eq!(rx.next(), Some(-10.0)); tx.add(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![5.0, 5.0, 6.0, 6.0, 7.0, 7.0, 7.0], )); @@ -307,7 +319,7 @@ mod tests { assert_eq!(rx.next(), Some(6.0)); assert_eq!(rx.next(), Some(6.0)); - tx.add(SamplesBuffer::new(ch!(1), 48000, vec![2.0])); + tx.add(SamplesBuffer::new(nz!(1), nz!(48000), vec![2.0])); assert_eq!(rx.next(), Some(9.0)); assert_eq!(rx.next(), Some(7.0)); diff --git a/src/queue.rs b/src/queue.rs index b57a0ab7..495606b4 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use crate::math::ch; +use crate::math::nz; use crate::source::{Empty, SeekError, Source, Zero}; use crate::Sample; @@ -221,7 +221,7 @@ impl SourcesQueueOutput { let mut next = self.input.next_sounds.lock().unwrap(); if next.is_empty() { - let silence = Box::new(Zero::new_samples(ch!(1), 44100, THRESHOLD)) as Box<_>; + let silence = Box::new(Zero::new_samples(nz!(1), nz!(44100), THRESHOLD)) as Box<_>; if self.input.keep_alive_if_empty.load(Ordering::Acquire) { // Play a short silence in order to avoid spinlocking. (silence, None) @@ -242,7 +242,7 @@ impl SourcesQueueOutput { #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::queue; use crate::source::Source; @@ -252,20 +252,24 @@ mod tests { let (tx, mut rx) = queue::queue(false); tx.append(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); - tx.append(SamplesBuffer::new(ch!(2), 96000, vec![5.0, 5.0, 5.0, 5.0])); + tx.append(SamplesBuffer::new( + nz!(2), + nz!(96000), + vec![5.0, 5.0, 5.0, 5.0], + )); - assert_eq!(rx.channels(), ch!(1)); - assert_eq!(rx.sample_rate(), 48000); + assert_eq!(rx.channels(), nz!(1)); + assert_eq!(rx.sample_rate().get(), 48000); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); - assert_eq!(rx.channels(), ch!(2)); - assert_eq!(rx.sample_rate(), 96000); + assert_eq!(rx.channels(), nz!(2)); + assert_eq!(rx.sample_rate().get(), 96000); assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(5.0)); @@ -283,8 +287,8 @@ mod tests { fn keep_alive() { let (tx, mut rx) = queue::queue(true); tx.append(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); @@ -308,8 +312,8 @@ mod tests { } tx.append(SamplesBuffer::new( - ch!(1), - 48000, + nz!(1), + nz!(48000), vec![10.0, -10.0, 10.0, -10.0], )); assert_eq!(rx.next(), Some(10.0)); diff --git a/src/sink.rs b/src/sink.rs index 5bb699a6..d72fbeb6 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -356,7 +356,7 @@ mod tests { use std::sync::atomic::Ordering; use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::{Sink, Source}; #[test] @@ -373,8 +373,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // Low rate to ensure immediate control. - sink.append(SamplesBuffer::new(ch!(1), 1, v.clone())); - let mut reference_src = SamplesBuffer::new(ch!(1), 1, v); + sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + let mut reference_src = SamplesBuffer::new(nz!(1), nz!(1), v); assert_eq!(source.next(), reference_src.next()); assert_eq!(source.next(), reference_src.next()); @@ -401,8 +401,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; - sink.append(SamplesBuffer::new(ch!(1), 1, v.clone())); - let mut src = SamplesBuffer::new(ch!(1), 1, v.clone()); + sink.append(SamplesBuffer::new(nz!(1), nz!(1), v.clone())); + let mut src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); assert_eq!(queue_rx.next(), src.next()); assert_eq!(queue_rx.next(), src.next()); @@ -412,8 +412,8 @@ mod tests { assert!(sink.controls.stopped.load(Ordering::SeqCst)); assert_eq!(queue_rx.next(), Some(0.0)); - src = SamplesBuffer::new(ch!(1), 1, v.clone()); - sink.append(SamplesBuffer::new(ch!(1), 1, v)); + src = SamplesBuffer::new(nz!(1), nz!(1), v.clone()); + sink.append(SamplesBuffer::new(nz!(1), nz!(1), v)); assert!(!sink.controls.stopped.load(Ordering::SeqCst)); // Flush silence @@ -430,8 +430,8 @@ mod tests { let v = vec![10.0, -10.0, 20.0, -20.0, 30.0, -30.0]; // High rate to avoid immediate control. - sink.append(SamplesBuffer::new(ch!(2), 44100, v.clone())); - let src = SamplesBuffer::new(ch!(2), 44100, v.clone()); + sink.append(SamplesBuffer::new(nz!(2), nz!(44100), v.clone())); + let src = SamplesBuffer::new(nz!(2), nz!(44100), v.clone()); let mut src = src.amplify(0.5); sink.set_volume(0.5); diff --git a/src/source/agc.rs b/src/source/agc.rs index 8dc5ded2..4ac2f538 100644 --- a/src/source/agc.rs +++ b/src/source/agc.rs @@ -143,7 +143,7 @@ pub(crate) fn automatic_gain_control( where I: Source, { - let sample_rate = input.sample_rate(); + let sample_rate = input.sample_rate().get(); let attack_coeff = (-1.0 / (attack_time * sample_rate as f32)).exp(); let release_coeff = (-1.0 / (release_time * sample_rate as f32)).exp(); diff --git a/src/source/blt.rs b/src/source/blt.rs index 2aad5b76..151b0bee 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -119,7 +119,7 @@ where let last_in_span = self.input.current_span_len() == Some(1); if self.applier.is_none() { - self.applier = Some(self.formula.to_applier(self.input.sample_rate())); + self.applier = Some(self.formula.to_applier(self.input.sample_rate().get())); } let sample = self.input.next()?; diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 8d354e2a..974389dc 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -5,7 +5,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::Source; /// Internal function that builds a `Buffered` object. @@ -215,7 +215,7 @@ where fn channels(&self) -> ChannelCount { match *self.current_span { Span::Data(SpanData { channels, .. }) => channels, - Span::End => ch!(1), + Span::End => nz!(1), Span::Input(_) => unreachable!(), } } @@ -224,7 +224,7 @@ where fn sample_rate(&self) -> SampleRate { match *self.current_span { Span::Data(SpanData { rate, .. }) => rate, - Span::End => 44100, + Span::End => nz!(44100), Span::Input(_) => unreachable!(), } } diff --git a/src/source/chirp.rs b/src/source/chirp.rs index 9a719e33..a17b3ad2 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -1,7 +1,7 @@ //! Chirp/sweep source. use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::Source; use std::{f32::consts::TAU, time::Duration}; @@ -38,7 +38,7 @@ impl Chirp { sample_rate, start_frequency, end_frequency, - total_samples: (duration.as_secs_f64() * (sample_rate as f64)) as u64, + total_samples: (duration.as_secs_f64() * (sample_rate.get() as f64)) as u64, elapsed_samples: 0, } } @@ -52,7 +52,7 @@ impl Iterator for Chirp { let ratio = self.elapsed_samples as f32 / self.total_samples as f32; self.elapsed_samples += 1; let freq = self.start_frequency * (1.0 - ratio) + self.end_frequency * ratio; - let t = (i as f32 / self.sample_rate() as f32) * TAU * freq; + let t = (i as f32 / self.sample_rate().get() as f32) * TAU * freq; Some(t.sin()) } } @@ -63,7 +63,7 @@ impl Source for Chirp { } fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } fn sample_rate(&self) -> SampleRate { @@ -71,7 +71,7 @@ impl Source for Chirp { } fn total_duration(&self) -> Option { - let secs: f64 = self.total_samples as f64 / self.sample_rate as f64; + let secs: f64 = self.total_samples as f64 / self.sample_rate.get() as f64; Some(Duration::new(1, 0).mul_f64(secs)) } } diff --git a/src/source/crossfade.rs b/src/source/crossfade.rs index 0390753d..2e6efc8b 100644 --- a/src/source/crossfade.rs +++ b/src/source/crossfade.rs @@ -33,12 +33,12 @@ pub type Crossfade = Mix, FadeIn>>; mod tests { use super::*; use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::source::Zero; fn dummy_source(length: u8) -> SamplesBuffer { let data: Vec = (1..=length).map(f32::from).collect(); - SamplesBuffer::new(ch!(1), 1, data) + SamplesBuffer::new(nz!(1), nz!(1), data) } #[test] @@ -61,7 +61,7 @@ mod tests { #[test] fn test_crossfade() { let source1 = dummy_source(10); - let source2 = Zero::new(ch!(1), 1); + let source2 = Zero::new(nz!(1), nz!(1)); let mixed = crossfade( source1, source2, diff --git a/src/source/delay.rs b/src/source/delay.rs index 68cf270a..1f2d3fb4 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -10,7 +10,7 @@ fn remaining_samples( channels: ChannelCount, ) -> usize { let ns = until_playback.as_secs() * 1_000_000_000 + until_playback.subsec_nanos() as u64; - let samples = ns * channels.get() as u64 * sample_rate as u64 / 1_000_000_000; + let samples = ns * channels.get() as u64 * sample_rate.get() as u64 / 1_000_000_000; samples as usize } diff --git a/src/source/empty.rs b/src/source/empty.rs index 7fdd3108..bfdf422e 100644 --- a/src/source/empty.rs +++ b/src/source/empty.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::{Sample, Source}; /// An empty source. @@ -42,12 +42,12 @@ impl Source for Empty { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] fn sample_rate(&self) -> SampleRate { - 48000 + nz!(48000) } #[inline] diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index 88fa67aa..4ae62437 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::{Sample, Source}; /// An empty source that executes a callback function @@ -39,12 +39,12 @@ impl Source for EmptyCallback { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] fn sample_rate(&self) -> SampleRate { - 48000 + nz!(48000) } #[inline] diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index 95cf3d7b..59845758 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::Source; /// Builds a source that chains sources provided by an iterator. @@ -118,7 +118,7 @@ where src.channels() } else { // Dummy value that only happens if the iterator was empty. - ch!(2) + nz!(2) } } @@ -128,7 +128,7 @@ where src.sample_rate() } else { // Dummy value that only happens if the iterator was empty. - 44100 + nz!(44100) } } @@ -150,29 +150,29 @@ where #[cfg(test)] mod tests { use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::source::{from_iter, Source}; #[test] fn basic() { let mut rx = from_iter((0..2).map(|n| { if n == 0 { - SamplesBuffer::new(ch!(1), 48000, vec![10.0, -10.0, 10.0, -10.0]) + SamplesBuffer::new(nz!(1), nz!(48000), vec![10.0, -10.0, 10.0, -10.0]) } else if n == 1 { - SamplesBuffer::new(ch!(2), 96000, vec![5.0, 5.0, 5.0, 5.0]) + SamplesBuffer::new(nz!(2), nz!(96000), vec![5.0, 5.0, 5.0, 5.0]) } else { unreachable!() } })); - assert_eq!(rx.channels(), ch!(1)); - assert_eq!(rx.sample_rate(), 48000); + assert_eq!(rx.channels(), nz!(1)); + assert_eq!(rx.sample_rate().get(), 48000); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); assert_eq!(rx.next(), Some(10.0)); assert_eq!(rx.next(), Some(-10.0)); /*assert_eq!(rx.channels(), 2); - assert_eq!(rx.sample_rate(), 96000);*/ + assert_eq!(rx.sample_rate().get(), 96000);*/ // FIXME: not working assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(5.0)); diff --git a/src/source/limit.rs b/src/source/limit.rs index 55882667..37ca282a 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -481,7 +481,7 @@ pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { let sample_rate = input.sample_rate(); let attack = duration_to_coefficient(settings.attack, sample_rate); let release = duration_to_coefficient(settings.release, sample_rate); - let channels = input.channels() as usize; + let channels = input.channels().get() as usize; let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); @@ -1136,39 +1136,48 @@ where /// Smoothing coefficient in the range [0.0, 1.0] for use in exponential filters #[must_use] fn duration_to_coefficient(duration: Duration, sample_rate: SampleRate) -> f32 { - f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate as f32)) + f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.get() as f32)) } #[cfg(test)] mod tests { use super::*; use crate::buffer::SamplesBuffer; + use crate::math::nz; use crate::source::{SineWave, Source}; use std::time::Duration; - fn create_test_buffer(samples: Vec, channels: u16, sample_rate: u32) -> SamplesBuffer { + fn create_test_buffer( + samples: Vec, + channels: ChannelCount, + sample_rate: SampleRate, + ) -> SamplesBuffer { SamplesBuffer::new(channels, sample_rate, samples) } #[test] fn test_limiter_creation() { // Test mono - let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3], 1, 44100); + let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3], nz!(1), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); - assert_eq!(limiter.channels(), 1); - assert_eq!(limiter.sample_rate(), 44100); + assert_eq!(limiter.channels(), nz!(1)); + assert_eq!(limiter.sample_rate(), nz!(44100)); matches!(limiter.0, LimitInner::Mono(_)); // Test stereo - let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], 2, 44100); + let buffer = create_test_buffer( + vec![0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], + nz!(2), + nz!(44100), + ); let limiter = limit(buffer, LimitSettings::default()); - assert_eq!(limiter.channels(), 2); + assert_eq!(limiter.channels(), nz!(2)); matches!(limiter.0, LimitInner::Stereo(_)); // Test multichannel - let buffer = create_test_buffer(vec![0.5; 12], 3, 44100); + let buffer = create_test_buffer(vec![0.5; 12], nz!(3), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); - assert_eq!(limiter.channels(), 3); + assert_eq!(limiter.channels(), nz!(3)); matches!(limiter.0, LimitInner::MultiChannel(_)); } } diff --git a/src/source/linear_ramp.rs b/src/source/linear_ramp.rs index 18c19486..eb0aad8a 100644 --- a/src/source/linear_ramp.rs +++ b/src/source/linear_ramp.rs @@ -89,7 +89,7 @@ where } if self.sample_idx % (self.channels().get() as u64) == 0 { - self.elapsed_ns += 1000000000.0 / (self.input.sample_rate() as f32); + self.elapsed_ns += 1000000000.0 / (self.input.sample_rate().get() as f32); } self.input.next().map(|value| value * factor) @@ -140,14 +140,14 @@ mod tests { use super::*; use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::Sample; /// Create a SamplesBuffer of identical samples with value `value`. /// Returned buffer is one channel and has a sample rate of 1 hz. fn const_source(length: u8, value: Sample) -> SamplesBuffer { let data: Vec = (1..=length).map(|_| value).collect(); - SamplesBuffer::new(ch!(1), 1, data) + SamplesBuffer::new(nz!(1), nz!(1), data) } /// Create a SamplesBuffer of repeating sample values from `values`. @@ -157,7 +157,7 @@ mod tests { .map(|(i, _)| values[i % values.len()]) .collect(); - SamplesBuffer::new(ch!(1), 1, data) + SamplesBuffer::new(nz!(1), nz!(1), data) } #[test] diff --git a/src/source/noise.rs b/src/source/noise.rs index ec812f43..ef003a53 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -16,17 +16,25 @@ //! ## Basic Usage //! //! ```rust +//! use std::num::NonZero; //! use rodio::source::noise::{WhiteUniform, Pink, WhiteTriangular, Blue}; //! +//! let sample_rate = NonZero::new(44100).unwrap(); +//! //! // Simple usage - creates generators with `SmallRng` -//! let white = WhiteUniform::new(44100); // For testing equipment linearly -//! let pink = Pink::new(44100); // For pleasant background sound -//! let triangular = WhiteTriangular::new(44100); // For TPDF dithering -//! let blue = Blue::new(44100); // For high-passed dithering applications +//! +//! // For testing equipment linearly +//! let white = WhiteUniform::new(sample_rate); +//! // For pleasant background sound +//! let pink = Pink::new(sample_rate); +//! // For TPDF dithering +//! let triangular = WhiteTriangular::new(sample_rate); +//! // For high-passed dithering applications +//! let blue = Blue::new(sample_rate); //! //! // Advanced usage - specify your own RNG type //! use rand::{rngs::StdRng, SeedableRng}; -//! let white_custom = WhiteUniform::::new_with_rng(44100, StdRng::seed_from_u64(12345)); +//! let white_custom = WhiteUniform::::new_with_rng(sample_rate, StdRng::seed_from_u64(12345)); //! ``` use std::time::Duration; @@ -38,6 +46,7 @@ use rand::{ }; use rand_distr::{Normal, Triangular}; +use crate::math::nz; use crate::{ChannelCount, Sample, SampleRate, Source}; /// Convenience function to create a new `WhiteUniform` noise source. @@ -62,7 +71,7 @@ macro_rules! impl_noise_source { } fn channels(&self) -> ChannelCount { - 1 + nz!(1) } fn sample_rate(&self) -> SampleRate { @@ -227,7 +236,7 @@ impl Velvet { /// Create a new velvet noise generator with a custom RNG. pub fn new_with_rng(sample_rate: SampleRate, mut rng: R) -> Self { let density = VELVET_DEFAULT_DENSITY; - let grid_size = sample_rate as f32 / density; + let grid_size = sample_rate.get() as f32 / density; let impulse_pos = rng.random::() * grid_size; Self { @@ -251,7 +260,7 @@ impl Velvet { pub fn new_with_density(sample_rate: SampleRate, density: f32) -> Self { let mut rng = R::from_os_rng(); let density = density.max(f32::MIN_POSITIVE); - let grid_size = sample_rate as f32 / density; + let grid_size = sample_rate.get() as f32 / density; let impulse_pos = rng.random::() * grid_size; Self { @@ -603,7 +612,7 @@ impl Brownian { // while preventing excessive low-frequency buildup across common sample rates. let center_freq_hz = 5.0; let leak_factor = - 1.0 - ((2.0 * std::f32::consts::PI * center_freq_hz) / sample_rate as f32); + 1.0 - ((2.0 * std::f32::consts::PI * center_freq_hz) / sample_rate.get() as f32); // Calculate the scaling factor to normalize output based on leak factor. // This ensures consistent output level regardless of the leak factor value. @@ -648,7 +657,7 @@ mod tests { use rstest_reuse::{self, *}; // Test constants - const TEST_SAMPLE_RATE: u32 = 44100; + const TEST_SAMPLE_RATE: SampleRate = nz!(44100); const TEST_SAMPLES_SMALL: usize = 100; const TEST_SAMPLES_MEDIUM: usize = 1000; @@ -760,7 +769,7 @@ mod tests { let source = create_generator_source(generator_name); // All noise generators should be mono (1 channel) - assert_eq!(source.channels(), 1, "{generator_name} should be mono"); + assert_eq!(source.channels(), nz!(1), "{generator_name} should be mono"); // All should have the expected sample rate assert_eq!( @@ -920,7 +929,7 @@ mod tests { fn test_brownian_noise_properties() { // Test that brownian noise doesn't accumulate DC indefinitely let mut generator = Brownian::new(TEST_SAMPLE_RATE); - let samples: Vec = (0..TEST_SAMPLE_RATE * 10) + let samples: Vec = (0..TEST_SAMPLE_RATE.get() * 10) .map(|_| generator.next().unwrap()) .collect(); // 10 seconds @@ -949,7 +958,7 @@ mod tests { let mut generator = Velvet::new(TEST_SAMPLE_RATE); let mut impulse_count = 0; - for _ in 0..TEST_SAMPLE_RATE { + for _ in 0..TEST_SAMPLE_RATE.get() { let sample = generator.next().unwrap(); if sample != 0.0 { impulse_count += 1; @@ -971,7 +980,7 @@ mod tests { let mut generator = Velvet::::new_with_density(TEST_SAMPLE_RATE, density); let mut impulse_count = 0; - for _ in 0..TEST_SAMPLE_RATE { + for _ in 0..TEST_SAMPLE_RATE.get() { if generator.next().unwrap() != 0.0 { impulse_count += 1; } diff --git a/src/source/periodic.rs b/src/source/periodic.rs index b89e7dc9..fb55e673 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -13,7 +13,7 @@ where // TODO: generally, just wrong let update_ms = period.as_secs() as u32 * 1_000 + period.subsec_millis(); let update_frequency = - (update_ms * source.sample_rate()) / 1000 * source.channels().get() as u32; + (update_ms * source.sample_rate().get()) / 1000 * source.channels().get() as u32; PeriodicAccess { input: source, @@ -132,13 +132,13 @@ mod tests { use std::time::Duration; use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::source::Source; #[test] fn stereo_access() { // Stereo, 1Hz audio buffer - let inner = SamplesBuffer::new(ch!(2), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(nz!(2), nz!(1), vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let cnt = RefCell::new(0); @@ -166,7 +166,7 @@ mod tests { #[test] fn fast_access_overflow() { // 1hz is lower than 0.5 samples per 5ms - let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(nz!(1), nz!(1), vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.periodic_access(Duration::from_millis(5), |_src| {}); source.next(); diff --git a/src/source/position.rs b/src/source/position.rs index 3aa8192b..1788625b 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -2,7 +2,7 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::Source; /// Internal function that builds a `TrackPosition` object. See trait docs for @@ -12,8 +12,8 @@ pub fn track_position(source: I) -> TrackPosition { input: source, samples_counted: 0, offset_duration: 0.0, - current_span_sample_rate: 0, - current_span_channels: ch!(1), + current_span_sample_rate: nz!(1), + current_span_channels: nz!(1), current_span_len: None, } } @@ -66,7 +66,7 @@ where #[inline] pub fn get_pos(&self) -> Duration { let seconds = self.samples_counted as f64 - / self.input.sample_rate() as f64 + / self.input.sample_rate().get() as f64 / self.input.channels().get() as f64 + self.offset_duration; Duration::from_secs_f64(seconds) @@ -101,7 +101,7 @@ where // offset_duration and start collecting samples again. if Some(self.samples_counted) == self.current_span_len() { self.offset_duration += self.samples_counted as f64 - / self.current_span_sample_rate as f64 + / self.current_span_sample_rate.get() as f64 / self.current_span_channels.get() as f64; // Reset. @@ -161,12 +161,12 @@ mod tests { use std::time::Duration; use crate::buffer::SamplesBuffer; - use crate::math::ch; + use crate::math::nz; use crate::source::Source; #[test] fn test_position() { - let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(nz!(1), nz!(1), vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.track_position(); assert_eq!(source.get_pos().as_secs_f32(), 0.0); @@ -182,7 +182,7 @@ mod tests { #[test] fn test_position_in_presence_of_speedup() { - let inner = SamplesBuffer::new(ch!(1), 1, vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); + let inner = SamplesBuffer::new(nz!(1), nz!(1), vec![10.0, -10.0, 10.0, -10.0, 20.0, -20.0]); let mut source = inner.speed(2.0).track_position(); assert_eq!(source.get_pos().as_secs_f32(), 0.0); diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index bb6785ff..a0e812e4 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -1,5 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct SawtoothWave { } impl SawtoothWave { - const SAMPLE_RATE: SampleRate = 48000; + const SAMPLE_RATE: SampleRate = nz!(48000); /// The frequency of the sine. #[inline] @@ -46,7 +46,7 @@ impl Source for SawtoothWave { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 5eeeb067..6f448d11 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -8,12 +8,13 @@ //! //! ``` //! use rodio::source::{SignalGenerator,Function}; +//! use core::num::NonZero; //! -//! let tone = SignalGenerator::new(48000, 440.0, Function::Sine); +//! let tone = SignalGenerator::new(NonZero::new(48000).unwrap(), 440.0, Function::Sine); //! ``` use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::Source; use std::f32::consts::TAU; use std::time::Duration; @@ -111,7 +112,7 @@ impl SignalGenerator { generator_function: GeneratorFunction, ) -> Self { assert!(frequency != 0.0, "frequency must be greater than zero"); - let period = sample_rate as f32 / frequency; + let period = sample_rate.get() as f32 / frequency; let phase_step = 1.0f32 / period; SignalGenerator { @@ -144,7 +145,7 @@ impl Source for SignalGenerator { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] @@ -159,7 +160,7 @@ impl Source for SignalGenerator { #[inline] fn try_seek(&mut self, duration: Duration) -> Result<(), SeekError> { - let seek = duration.as_secs_f32() * (self.sample_rate as f32) / self.period; + let seek = duration.as_secs_f32() * (self.sample_rate.get() as f32) / self.period; self.phase = seek.rem_euclid(1.0f32); Ok(()) } @@ -167,12 +168,13 @@ impl Source for SignalGenerator { #[cfg(test)] mod tests { + use crate::math::nz; use crate::source::{Function, SignalGenerator}; use approx::assert_abs_diff_eq; #[test] fn square() { - let mut wf = SignalGenerator::new(2000, 500.0f32, Function::Square); + let mut wf = SignalGenerator::new(nz!(2000), 500.0f32, Function::Square); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(1.0f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -185,7 +187,7 @@ mod tests { #[test] fn triangle() { - let mut wf = SignalGenerator::new(8000, 1000.0f32, Function::Triangle); + let mut wf = SignalGenerator::new(nz!(8000), 1000.0f32, Function::Triangle); assert_eq!(wf.next(), Some(-1.0f32)); assert_eq!(wf.next(), Some(-0.5f32)); assert_eq!(wf.next(), Some(0.0f32)); @@ -206,7 +208,7 @@ mod tests { #[test] fn saw() { - let mut wf = SignalGenerator::new(200, 50.0f32, Function::Sawtooth); + let mut wf = SignalGenerator::new(nz!(200), 50.0f32, Function::Sawtooth); assert_eq!(wf.next(), Some(0.0f32)); assert_eq!(wf.next(), Some(0.5f32)); assert_eq!(wf.next(), Some(-1.0f32)); @@ -218,7 +220,7 @@ mod tests { #[test] fn sine() { - let mut wf = SignalGenerator::new(1000, 100f32, Function::Sine); + let mut wf = SignalGenerator::new(nz!(1000), 100f32, Function::Sine); assert_abs_diff_eq!(wf.next().unwrap(), 0.0f32); assert_abs_diff_eq!(wf.next().unwrap(), 0.58778525f32); diff --git a/src/source/sine.rs b/src/source/sine.rs index c2158f60..a85fcb63 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -1,5 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct SineWave { } impl SineWave { - const SAMPLE_RATE: u32 = 48000; + const SAMPLE_RATE: SampleRate = nz!(48000); /// The frequency of the sine. #[inline] @@ -46,7 +46,7 @@ impl Source for SineWave { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] diff --git a/src/source/skip.rs b/src/source/skip.rs index 6d2be58a..36f8d210 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -40,7 +40,7 @@ where } let ns_per_sample: u128 = - NS_PER_SECOND / input.sample_rate() as u128 / input.channels().get() as u128; + NS_PER_SECOND / input.sample_rate().get() as u128 / input.channels().get() as u128; // Check if we need to skip only part of the current span. if span_len as u128 * ns_per_sample > duration.as_nanos() { @@ -61,7 +61,7 @@ where I: Source, { let samples_per_channel: u128 = - duration.as_nanos() * input.sample_rate() as u128 / NS_PER_SECOND; + duration.as_nanos() * input.sample_rate().get() as u128 / NS_PER_SECOND; let samples_to_skip: u128 = samples_per_channel * input.channels().get() as u128; skip_samples(input, samples_to_skip as usize); @@ -165,7 +165,7 @@ mod tests { use crate::buffer::SamplesBuffer; use crate::common::{ChannelCount, SampleRate}; - use crate::math::ch; + use crate::math::nz; use crate::source::Source; fn test_skip_duration_samples_left( @@ -174,13 +174,14 @@ mod tests { seconds: u32, seconds_to_skip: u32, ) { - let buf_len = (sample_rate * channels.get() as u32 * seconds) as usize; + let buf_len = (sample_rate.get() * channels.get() as u32 * seconds) as usize; assert!(buf_len < 10 * 1024 * 1024); let data: Vec = vec![0f32; buf_len]; let test_buffer = SamplesBuffer::new(channels, sample_rate, data); let seconds_left = seconds.saturating_sub(seconds_to_skip); - let samples_left_expected = (sample_rate * channels.get() as u32 * seconds_left) as usize; + let samples_left_expected = + (sample_rate.get() * channels.get() as u32 * seconds_left) as usize; let samples_left = test_buffer .skip_duration(Duration::from_secs(seconds_to_skip as u64)) .count(); @@ -191,7 +192,7 @@ mod tests { macro_rules! skip_duration_test_block { ($(channels: $ch:expr, sample rate: $sr:expr, seconds: $sec:expr, seconds to skip: $sec_to_skip:expr;)+) => { $( - test_skip_duration_samples_left(ch!($ch), $sr, $sec, $sec_to_skip); + test_skip_duration_samples_left(nz!($ch), nz!($sr), $sec, $sec_to_skip); )+ } } diff --git a/src/source/speed.rs b/src/source/speed.rs index 9454ed98..e38c2b99 100644 --- a/src/source/speed.rs +++ b/src/source/speed.rs @@ -128,7 +128,8 @@ where #[inline] fn sample_rate(&self) -> SampleRate { - (self.input.sample_rate() as f32 * self.factor) as u32 + SampleRate::new((self.input.sample_rate().get() as f32 * self.factor).max(1.0) as u32) + .expect("minimum is 1.0 > 0") } #[inline] diff --git a/src/source/square.rs b/src/source/square.rs index 9389be2b..4beaf33d 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -1,5 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct SquareWave { } impl SquareWave { - const SAMPLE_RATE: u32 = 48000; + const SAMPLE_RATE: SampleRate = nz!(48000); /// The frequency of the sine. #[inline] @@ -46,7 +46,7 @@ impl Source for SquareWave { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] diff --git a/src/source/take.rs b/src/source/take.rs index b075bdd9..60ee9c6d 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -58,7 +58,7 @@ where /// Returns the duration elapsed for each sample extracted. #[inline] fn get_duration_per_sample(input: &I) -> Duration { - let ns = NANOS_PER_SEC / (input.sample_rate() as u64 * input.channels().get() as u64); + let ns = NANOS_PER_SEC / (input.sample_rate().get() as u64 * input.channels().get() as u64); // \|/ the maximum value of `ns` is one billion, so this can't fail Duration::new(0, ns as u32) } diff --git a/src/source/triangle.rs b/src/source/triangle.rs index 2ca9b830..a35df3af 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -1,5 +1,5 @@ use crate::common::{ChannelCount, SampleRate}; -use crate::math::ch; +use crate::math::nz; use crate::source::{Function, SignalGenerator}; use crate::Source; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct TriangleWave { } impl TriangleWave { - const SAMPLE_RATE: SampleRate = 48000; + const SAMPLE_RATE: SampleRate = nz!(48000); /// The frequency of the sine. #[inline] @@ -46,7 +46,7 @@ impl Source for TriangleWave { #[inline] fn channels(&self) -> ChannelCount { - ch!(1) + nz!(1) } #[inline] diff --git a/src/static_buffer.rs b/src/static_buffer.rs index 49c18fa5..2a3c54a2 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -6,8 +6,8 @@ //! //! ``` //! use rodio::static_buffer::StaticSamplesBuffer; -//! use rodio::ChannelCount; -//! let _ = StaticSamplesBuffer::new(ChannelCount::new(1).unwrap(), 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); +//! use core::num::NonZero; +//! let _ = StaticSamplesBuffer::new(NonZero::new(1).unwrap(), NonZero::new(44100).unwrap(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); //! ``` //! @@ -42,10 +42,8 @@ impl StaticSamplesBuffer { sample_rate: SampleRate, data: &'static [Sample], ) -> StaticSamplesBuffer { - assert!(sample_rate != 0); - let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap() - / sample_rate as u64 + / sample_rate.get() as u64 / channels.get() as u64; let duration = Duration::new( duration_ns / 1_000_000_000, @@ -106,24 +104,18 @@ impl Iterator for StaticSamplesBuffer { #[cfg(test)] mod tests { - use crate::math::ch; + use crate::math::nz; use crate::source::Source; use crate::static_buffer::StaticSamplesBuffer; #[test] fn basic() { - let _ = StaticSamplesBuffer::new(ch!(1), 44100, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); - } - - #[test] - #[should_panic] - fn panic_if_zero_sample_rate() { - StaticSamplesBuffer::new(ch!(1), 0, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let _ = StaticSamplesBuffer::new(nz!(1), nz!(44100), &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); } #[test] fn duration_basic() { - let buf = StaticSamplesBuffer::new(ch!(2), 2, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); + let buf = StaticSamplesBuffer::new(nz!(2), nz!(2), &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); let dur = buf.total_duration().unwrap(); assert_eq!(dur.as_secs(), 1); assert_eq!(dur.subsec_nanos(), 500_000_000); @@ -131,7 +123,7 @@ mod tests { #[test] fn iteration() { - let mut buf = StaticSamplesBuffer::new(ch!(1), 44100, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); + let mut buf = StaticSamplesBuffer::new(nz!(1), nz!(44100), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]); assert_eq!(buf.next(), Some(1.0)); assert_eq!(buf.next(), Some(2.0)); assert_eq!(buf.next(), Some(3.0)); diff --git a/src/stream.rs b/src/stream.rs index 7294f947..36b6cf0a 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,34 +1,86 @@ +//! Output audio via the OS via mixers or play directly +//! +//! This module provides a builder that's used to configure and open audio output. Once +//! opened sources can be mixed into the output via `OutputStream::mixer`. +//! +//! There is also a convenience function `play` for using that output mixer to +//! play a single sound. use crate::common::{ChannelCount, SampleRate}; use crate::decoder; -use crate::math::ch; +use crate::math::nz; use crate::mixer::{mixer, Mixer, MixerSource}; use crate::sink::Sink; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{BufferSize, FrameCount, Sample, SampleFormat, StreamConfig, SupportedBufferSize}; +use cpal::{BufferSize, Sample, SampleFormat, StreamConfig}; use std::io::{Read, Seek}; use std::marker::Sync; -use std::sync::Arc; +use std::num::NonZero; use std::{error, fmt}; -const HZ_44100: SampleRate = 44_100; - -/// `cpal::Stream` container. -/// Use `mixer()` method to control output. -/// If this is dropped, playback will end, and the associated output stream will be disposed. +const HZ_44100: SampleRate = nz!(44_100); + +/// `cpal::Stream` container. Use `mixer()` method to control output. +/// +///
When dropped playback will end, and the associated +/// output stream will be disposed
+/// +/// # Note +/// On drop this will print a message to stderr or emit a log msg when tracing is +/// enabled. Though we recommend you do not you can disable that print/log with: +/// [`OutputStream::log_on_drop(false)`](OutputStream::log_on_drop). +/// If the `OutputStream` is dropped because the program is panicking we do not print +/// or log anything. +/// +/// # Example +/// ```no_run +/// # use rodio::OutputStreamBuilder; +/// # fn main() -> Result<(), Box> { +/// let mut stream_handle = OutputStreamBuilder::open_default_stream()?; +/// stream_handle.log_on_drop(false); // Not recommended during development +/// println!("Output config: {:?}", stream_handle.config()); +/// let mixer = stream_handle.mixer(); +/// # Ok(()) +/// # } +/// ``` pub struct OutputStream { - mixer: Arc, + config: OutputStreamConfig, + mixer: Mixer, + log_on_drop: bool, _stream: cpal::Stream, } impl OutputStream { /// Access the output stream's mixer. - pub fn mixer(&self) -> Arc { - self.mixer.clone() + pub fn mixer(&self) -> &Mixer { + &self.mixer + } + + /// Access the output stream's config. + pub fn config(&self) -> &OutputStreamConfig { + &self.config + } + + /// When [`OutputStream`] is dropped a message is logged to stderr or + /// emitted through tracing if the tracing feature is enabled. + pub fn log_on_drop(&mut self, enabled: bool) { + self.log_on_drop = enabled; } } +impl Drop for OutputStream { + fn drop(&mut self) { + if self.log_on_drop && !std::thread::panicking() { + #[cfg(feature = "tracing")] + tracing::debug!("Dropping OutputStream, audio playing through this stream will stop"); + #[cfg(not(feature = "tracing"))] + eprintln!("Dropping OutputStream, audio playing through this stream will stop, to prevent this message from appearing use tracing or call `.log_on_drop(false)` on this OutputStream") + } + } +} + +/// Describes the output stream's configuration #[derive(Copy, Clone, Debug)] -struct OutputStreamConfig { +pub struct OutputStreamConfig { channel_count: ChannelCount, sample_rate: SampleRate, buffer_size: BufferSize, @@ -38,7 +90,7 @@ struct OutputStreamConfig { impl Default for OutputStreamConfig { fn default() -> Self { Self { - channel_count: ch!(2), + channel_count: nz!(2), sample_rate: HZ_44100, buffer_size: BufferSize::Default, sample_format: SampleFormat::F32, @@ -46,13 +98,26 @@ impl Default for OutputStreamConfig { } } -/// Convenience builder for audio output stream. -/// It provides methods to configure several parameters of the audio output and opening default -/// device. See examples for use-cases. -#[derive(Default)] -pub struct OutputStreamBuilder { - device: Option, - config: OutputStreamConfig, +impl OutputStreamConfig { + /// Access the output stream config's channel count. + pub fn channel_count(&self) -> ChannelCount { + self.channel_count + } + + /// Access the output stream config's sample rate. + pub fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + /// Access the output stream config's buffer size. + pub fn buffer_size(&self) -> &BufferSize { + &self.buffer_size + } + + /// Access the output stream config's sample format. + pub fn sample_format(&self) -> SampleFormat { + self.sample_format + } } impl core::fmt::Debug for OutputStreamBuilder { @@ -70,12 +135,45 @@ impl core::fmt::Debug for OutputStreamBuilder { } } +fn default_error_callback(err: cpal::StreamError) { + #[cfg(feature = "tracing")] + tracing::error!("audio stream error: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("audio stream error: {err}"); +} + +/// Convenience builder for audio output stream. +/// It provides methods to configure several parameters of the audio output and opening default +/// device. See examples for use-cases. +/// +///
When the OutputStream is dropped playback will end, and the associated +/// output stream will be disposed
+pub struct OutputStreamBuilder +where + E: FnMut(cpal::StreamError) + Send + 'static, +{ + device: Option, + config: OutputStreamConfig, + error_callback: E, +} + +impl Default for OutputStreamBuilder { + fn default() -> Self { + Self { + device: None, + config: OutputStreamConfig::default(), + error_callback: default_error_callback, + } + } +} + impl OutputStreamBuilder { /// Sets output device and its default parameters. pub fn from_device(device: cpal::Device) -> Result { let default_config = device .default_output_config() .map_err(StreamError::DefaultStreamConfigError)?; + Ok(Self::default() .with_device(device) .with_supported_config(&default_config)) @@ -89,86 +187,175 @@ impl OutputStreamBuilder { Self::from_device(default_device) } + /// Try to open a new output stream for the default output device with its default configuration. + /// Failing that attempt to open output stream with alternative configuration and/or non default + /// output devices. Returns stream for first of the tried configurations that succeeds. + /// If all attempts fail return the initial error. + pub fn open_default_stream() -> Result { + Self::from_default_device() + .and_then(|x| x.open_stream()) + .or_else(|original_err| { + let mut devices = match cpal::default_host().output_devices() { + Ok(devices) => devices, + Err(err) => { + #[cfg(feature = "tracing")] + tracing::error!("error getting list of output devices: {err}"); + #[cfg(not(feature = "tracing"))] + eprintln!("error getting list of output devices: {err}"); + return Err(original_err); + } + }; + devices + .find_map(|d| { + Self::from_device(d) + .and_then(|x| x.open_stream_or_fallback()) + .ok() + }) + .ok_or(original_err) + }) + } +} + +impl OutputStreamBuilder +where + E: FnMut(cpal::StreamError) + Send + 'static, +{ /// Sets output audio device keeping all existing stream parameters intact. /// This method is useful if you want to set other parameters yourself. /// To also set parameters that are appropriate for the device use [Self::from_device()] instead. - pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { + pub fn with_device(mut self, device: cpal::Device) -> OutputStreamBuilder { self.device = Some(device); self } /// Sets number of output stream's channels. - pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { + pub fn with_channels(mut self, channel_count: ChannelCount) -> OutputStreamBuilder { + assert!(channel_count.get() > 0); self.config.channel_count = channel_count; self } /// Sets output stream's sample rate. - pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { + pub fn with_sample_rate(mut self, sample_rate: SampleRate) -> OutputStreamBuilder { self.config.sample_rate = sample_rate; self } /// Sets preferred output buffer size. - /// Larger buffer size causes longer playback delays. Buffer sizes that are too small - /// may cause higher CPU usage or playback interruptions. - pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { + /// + /// To play sound without any glitches the audio card may never receive a + /// sample to late. Some samples might take longer to generate then + /// others. For example because: + /// - The OS preempts the thread creating the samples. This happens more + /// often if the computer is under high load. + /// - The decoder needs to read more data from disk. + /// - Rodio code takes longer to run for some samples then others + /// - The OS can only send audio samples in groups to the DAC. + /// + /// The OS solves this by buffering samples. The larger that buffer the + /// smaller the impact of variable sample generation time. On the other + /// hand Rodio controls audio by changing the value of samples. We can not + /// change a sample already in the OS buffer. That means there is a + /// minimum delay (latency) of `/` + /// seconds before a change made through rodio takes effect. + /// + /// # Large vs Small buffer + /// - A larger buffer size results in high latency. Changes made trough + /// Rodio (volume/skip/effects etc) takes longer before they can be heard. + /// - A small buffer might cause: + /// - Higher CPU usage + /// - Playback interruptions such as buffer underruns. + /// - Rodio to log errors like: `alsa::poll() returned POLLERR` + /// + /// # Recommendation + /// If low latency is important to you consider offering the user a method + /// to find the minimum buffer size that works well on their system under + /// expected conditions. A good example of this approach can be seen in + /// [mumble](https://www.mumble.info/documentation/user/audio-settings/) + /// (specifically the *Output Delay* & *Jitter buffer*. + /// + /// These are some typical values that are a good starting point. They may also + /// break audio completely, it depends on the system. + /// - Low-latency (audio production, live monitoring): 512-1024 + /// - General use (games, media playback): 1024-2048 + /// - Stability-focused (background music, non-interactive): 2048-4096 + pub fn with_buffer_size(mut self, buffer_size: cpal::BufferSize) -> OutputStreamBuilder { self.config.buffer_size = buffer_size; self } /// Select scalar type that will carry a sample. - pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { + pub fn with_sample_format(mut self, sample_format: SampleFormat) -> OutputStreamBuilder { self.config.sample_format = sample_format; self } - /// Set available parameters from a CPAL supported config. You can get list of + /// Set available parameters from a CPAL supported config. You can get a list of /// such configurations for an output device using [crate::stream::supported_output_configs()] pub fn with_supported_config( mut self, config: &cpal::SupportedStreamConfig, - ) -> OutputStreamBuilder { + ) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: ChannelCount::new(config.channels()) - .expect("cpal should never return a zero channel output"), - sample_rate: config.sample_rate().0 as SampleRate, - // In case of supported range limit buffer size to avoid unexpectedly long playback delays. - buffer_size: clamp_supported_buffer_size(config.buffer_size(), 1024), + channel_count: NonZero::new(config.channels()) + .expect("no valid cpal config has zero channels"), + sample_rate: NonZero::new(config.sample_rate().0) + .expect("no valid cpal config has zero sample rate"), sample_format: config.sample_format(), + ..Default::default() }; self } /// Set all output stream parameters at once from CPAL stream config. - pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { + pub fn with_config(mut self, config: &cpal::StreamConfig) -> OutputStreamBuilder { self.config = OutputStreamConfig { - channel_count: ChannelCount::new(config.channels) - .expect("cpal should never return a zero channel output"), - sample_rate: config.sample_rate.0 as SampleRate, + channel_count: NonZero::new(config.channels) + .expect("no valid cpal config has zero channels"), + sample_rate: NonZero::new(config.sample_rate.0) + .expect("no valid cpal config has zero sample rate"), buffer_size: config.buffer_size, ..self.config }; self } + /// Set a callback that will be called when an error occurs with the stream + pub fn with_error_callback(self, callback: F) -> OutputStreamBuilder + where + F: FnMut(cpal::StreamError) + Send + 'static, + { + OutputStreamBuilder { + device: self.device, + config: self.config, + error_callback: callback, + } + } + /// Open output stream using parameters configured so far. - pub fn open_stream(&self) -> Result { + pub fn open_stream(self) -> Result { let device = self.device.as_ref().expect("output device specified"); - OutputStream::open(device, &self.config) + + OutputStream::open(device, &self.config, self.error_callback) } /// Try opening a new output stream with the builder's current stream configuration. /// Failing that attempt to open stream with other available configurations /// supported by the device. /// If all attempts fail returns initial error. - pub fn open_stream_or_fallback(&self) -> Result { + pub fn open_stream_or_fallback(&self) -> Result + where + E: Clone, + { let device = self.device.as_ref().expect("output device specified"); - OutputStream::open(device, &self.config).or_else(|err| { + let error_callback = &self.error_callback; + + OutputStream::open(device, &self.config, error_callback.clone()).or_else(|err| { for supported_config in supported_output_configs(device)? { - if let Ok(handle) = Self::default() + if let Ok(handle) = OutputStreamBuilder::default() .with_device(device.clone()) .with_supported_config(&supported_config) + .with_error_callback(error_callback.clone()) .open_stream() { return Ok(handle); @@ -177,48 +364,6 @@ impl OutputStreamBuilder { Err(err) }) } - - /// Try to open a new output stream for the default output device with its default configuration. - /// Failing that attempt to open output stream with alternative configuration and/or non default - /// output devices. Returns stream for first of the tried configurations that succeeds. - /// If all attempts fail return the initial error. - pub fn open_default_stream() -> Result { - Self::from_default_device() - .and_then(|x| x.open_stream()) - .or_else(|original_err| { - let mut devices = match cpal::default_host().output_devices() { - Ok(devices) => devices, - Err(err) => { - #[cfg(feature = "tracing")] - tracing::error!("error getting list of output devices: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("error getting list of output devices: {err}"); - return Err(original_err); - } - }; - devices - .find_map(|d| { - Self::from_device(d) - .and_then(|x| x.open_stream_or_fallback()) - .ok() - }) - .ok_or(original_err) - }) - } -} - -fn clamp_supported_buffer_size( - buffer_size: &SupportedBufferSize, - preferred_size: FrameCount, -) -> BufferSize { - match buffer_size { - SupportedBufferSize::Range { min, max } => { - let size = preferred_size.clamp(*min, *max); - assert!(size > 0, "selected buffer size is greater than zero"); - BufferSize::Fixed(size) - } - SupportedBufferSize::Unknown => BufferSize::Default, - } } /// A convenience function. Plays a sound once. @@ -237,7 +382,7 @@ impl From<&OutputStreamConfig> for StreamConfig { fn from(config: &OutputStreamConfig) -> Self { cpal::StreamConfig { channels: config.channel_count.get() as cpal::ChannelCount, - sample_rate: cpal::SampleRate(config.sample_rate), + sample_rate: cpal::SampleRate(config.sample_rate.get()), buffer_size: config.buffer_size, } } @@ -328,37 +473,41 @@ impl OutputStream { if let BufferSize::Fixed(sz) = config.buffer_size { assert!(sz > 0, "fixed buffer size is greater than zero"); } - assert!(config.sample_rate > 0, "sample rate is greater than zero"); } - fn open( + fn open( device: &cpal::Device, config: &OutputStreamConfig, - ) -> Result { + error_callback: E, + ) -> Result + where + E: FnMut(cpal::StreamError) + Send + 'static, + { Self::validate_config(config); let (controller, source) = mixer(config.channel_count, config.sample_rate); - Self::init_stream(device, config, source).and_then(|stream| { + Self::init_stream(device, config, source, error_callback).and_then(|stream| { stream.play().map_err(StreamError::PlayStreamError)?; Ok(Self { _stream: stream, mixer: controller, + config: *config, + log_on_drop: true, }) }) } - fn init_stream( + fn init_stream( device: &cpal::Device, config: &OutputStreamConfig, mut samples: MixerSource, - ) -> Result { - let error_callback = |err| { - #[cfg(feature = "tracing")] - tracing::error!("Playback error: {err}"); - #[cfg(not(feature = "tracing"))] - eprintln!("Playback error: {err}"); - }; + error_callback: E, + ) -> Result + where + E: FnMut(cpal::StreamError) + Send + 'static, + { let sample_format = config.sample_format; - let config: cpal::StreamConfig = config.into(); + let config = config.into(); + match sample_format { cpal::SampleFormat::F32 => device.build_output_stream::( &config, @@ -473,7 +622,7 @@ impl OutputStream { } /// Return all formats supported by the device. -fn supported_output_configs( +pub fn supported_output_configs( device: &cpal::Device, ) -> Result, StreamError> { let mut supported: Vec<_> = device @@ -486,7 +635,7 @@ fn supported_output_configs( let max_rate = sf.max_sample_rate(); let min_rate = sf.min_sample_rate(); let mut formats = vec![sf.with_max_sample_rate()]; - let preferred_rate = cpal::SampleRate(HZ_44100); + let preferred_rate = cpal::SampleRate(HZ_44100.get()); if preferred_rate < max_rate && preferred_rate > min_rate { formats.push(sf.with_sample_rate(preferred_rate)) } diff --git a/src/wav_output.rs b/src/wav_output.rs index fabcc1b9..43221fa6 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -11,7 +11,7 @@ pub fn output_to_wav( ) -> Result<(), Box> { let format = WavSpec { channels: source.channels().get(), - sample_rate: source.sample_rate(), + sample_rate: source.sample_rate().get(), bits_per_sample: 32, sample_format: SampleFormat::Float, }; @@ -45,7 +45,7 @@ mod test { let mut reader = hound::WavReader::new(BufReader::new(file)).expect("wav file can be read back"); let reference = make_source(); - assert_eq!(reference.sample_rate(), reader.spec().sample_rate); + assert_eq!(reference.sample_rate().get(), reader.spec().sample_rate); assert_eq!(reference.channels().get(), reader.spec().channels); let actual_samples: Vec = reader.samples::().map(|x| x.unwrap()).collect(); diff --git a/tests/limit.rs b/tests/limit.rs index 267bad6e..62434f97 100644 --- a/tests/limit.rs +++ b/tests/limit.rs @@ -1,4 +1,5 @@ use rodio::source::Source; +use std::num::NonZero; use std::time::Duration; #[test] @@ -129,7 +130,11 @@ fn test_limiter_stereo_processing() { stereo_samples.push(right_samples[i]); } - let buffer = SamplesBuffer::new(2, 44100, stereo_samples); + let buffer = SamplesBuffer::new( + NonZero::new(2).unwrap(), + NonZero::new(44100).unwrap(), + stereo_samples, + ); let settings = rodio::source::LimitSettings::default().with_threshold(-3.0); let limiter = buffer.limit(settings); diff --git a/tests/seek.rs b/tests/seek.rs index 9f9f8d96..7a96eeb2 100644 --- a/tests/seek.rs +++ b/tests/seek.rs @@ -1,29 +1,49 @@ -use rodio::{Decoder, Source}; +#![allow(dead_code)] +#![allow(unused_imports)] + +#[cfg(feature = "symphonia-mp3")] +use rodio::{decoder::symphonia, source::SeekError}; +use rodio::{ChannelCount, Decoder, Source}; use rstest::rstest; use rstest_reuse::{self, *}; -use std::io::{BufReader, Read, Seek}; +use std::io::{Read, Seek}; use std::path::Path; use std::time::Duration; +#[cfg(any( + feature = "claxon", + feature = "minimp3", + feature = "symphonia-aac", + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[template] #[rstest] -// note: disabled, broken decoder see issue: #516 and #539 -// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")], +#[cfg_attr( + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + case("ogg", true, "symphonia") +)] #[cfg_attr( all(feature = "minimp3", not(feature = "symphonia-mp3")), case("mp3", false, "minimp3") )] #[cfg_attr( - all(feature = "wav", not(feature = "symphonia-wav")), + all(feature = "hound", not(feature = "symphonia-wav")), case("wav", true, "hound") )] #[cfg_attr( - all(feature = "flac", not(feature = "symphonia-flac")), + all(feature = "claxon", not(feature = "symphonia-flac")), case("flac", false, "claxon") )] #[cfg_attr(feature = "symphonia-mp3", case("mp3", true, "symphonia"))] -// note: disabled, broken decoder see issue: #577 -#[cfg_attr(feature = "symphonia-isomp4", case("m4a", true, "symphonia"))] +#[cfg_attr( + all(feature = "symphonia-isomp4", feature = "symphonia-aac"), + case("m4a", true, "symphonia") +)] #[cfg_attr(feature = "symphonia-wav", case("wav", true, "symphonia"))] #[cfg_attr(feature = "symphonia-flac", case("flac", true, "symphonia"))] fn all_decoders( @@ -33,21 +53,43 @@ fn all_decoders( ) { } +#[cfg(any( + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[template] #[rstest] -// note: disabled, broken decoder see issue: #516 and #539 -// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")], #[cfg_attr( - all(feature = "wav", not(feature = "symphonia-wav")), + all(feature = "symphonia-ogg", feature = "symphonia-vorbis"), + case("ogg", "symphonia") +)] +#[cfg_attr( + all(feature = "hound", not(feature = "symphonia-wav")), case("wav", "hound") )] #[cfg_attr(feature = "symphonia-mp3", case("mp3", "symphonia"))] -// note: disabled, broken decoder see issue: #577 -// #[cfg_attr(feature = "symphonia-isomp4", case("m4a", "symphonia"))] +#[cfg_attr( + all(feature = "symphonia-isomp4", feature = "symphonia-aac"), + case("m4a", "symphonia") +)] #[cfg_attr(feature = "symphonia-wav", case("wav", "symphonia"))] #[cfg_attr(feature = "symphonia-flac", case("flac", "symphonia"))] fn supported_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {} +#[cfg(any( + feature = "claxon", + feature = "minimp3", + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[apply(all_decoders)] #[trace] fn seek_returns_err_if_unsupported( @@ -60,6 +102,14 @@ fn seek_returns_err_if_unsupported( assert_eq!(res.is_ok(), supports_seek, "decoder: {decoder_name}"); } +#[cfg(any( + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[apply(supported_decoders)] #[trace] fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: &'static str) { @@ -71,6 +121,14 @@ fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: assert!(time_remaining(decoder) < Duration::from_secs(1)); } +#[cfg(any( + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[apply(supported_decoders)] #[trace] fn seek_results_in_correct_remaining_playtime( @@ -99,6 +157,14 @@ fn seek_results_in_correct_remaining_playtime( } } +#[cfg(any( + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[apply(supported_decoders)] #[trace] fn seek_possible_after_exausting_source( @@ -109,30 +175,46 @@ fn seek_possible_after_exausting_source( while source.next().is_some() {} assert!(source.next().is_none()); - source.try_seek(Duration::from_secs(0)).unwrap(); + source.try_seek(Duration::ZERO).unwrap(); assert!(source.next().is_some()); } +#[cfg(any( + feature = "symphonia-flac", + feature = "symphonia-mp3", + feature = "symphonia-isomp4", + feature = "symphonia-ogg", + feature = "symphonia-wav", + feature = "hound", +))] #[apply(supported_decoders)] #[trace] fn seek_does_not_break_channel_order( #[case] format: &'static str, #[case] _decoder_name: &'static str, ) { + if format == "m4a" { + // skip this test for m4a while the symphonia decoder has issues with aac timing. + // re-investigate when symphonia 0.5.5 or greater is released. + return; + } + let mut source = get_rl(format); let channels = source.channels(); assert_eq!(channels.get(), 2, "test needs a stereo beep file"); let beep_range = second_channel_beep_range(&mut source); let beep_start = Duration::from_secs_f32( - beep_range.start as f32 / source.channels().get() as f32 / source.sample_rate() as f32, + beep_range.start as f32 + / source.channels().get() as f32 + / source.sample_rate().get() as f32, ); let mut source = get_rl(format); let mut channel_offset = 0; for offset in [1, 4, 7, 40, 41, 120, 179] - .map(|offset| offset as f32 / (source.sample_rate() as f32)) + .map(|offset| offset as f32 / (source.sample_rate().get() as f32)) .map(Duration::from_secs_f32) { source.next(); // WINDOW is even, make the amount of calls to next @@ -142,9 +224,9 @@ fn seek_does_not_break_channel_order( source.try_seek(beep_start + offset).unwrap(); let samples: Vec<_> = source.by_ref().take(100).collect(); - let channel0 = 0 + channel_offset; + let channel0 = channel_offset; assert!( - is_silent(&samples, source.channels().get() as usize, channel0), + is_silent(&samples, source.channels(), channel0), "channel0 should be silent, channel0 starts at idx: {channel0} seek: {beep_start:?} + {offset:?} @@ -152,7 +234,7 @@ fn seek_does_not_break_channel_order( ); let channel1 = (1 + channel_offset) % 2; assert!( - !is_silent(&samples, source.channels().get() as usize, channel1), + !is_silent(&samples, source.channels(), channel1), "channel1 should not be silent, channel1; starts at idx: {channel1} seek: {beep_start:?} + {offset:?} @@ -161,10 +243,39 @@ fn seek_does_not_break_channel_order( } } -fn second_channel_beep_range(source: &mut R) -> std::ops::Range -where - R: Iterator, -{ +#[cfg(feature = "symphonia-mp3")] +#[test] +fn random_access_seeks() { + // Decoder::new:: does *not* set byte_len and is_seekable + let mp3_file = std::fs::File::open("assets/music.mp3").unwrap(); + let mut decoder = Decoder::new(mp3_file).unwrap(); + assert!( + decoder.try_seek(Duration::from_secs(2)).is_ok(), + "forward seek should work without byte_len" + ); + assert!( + matches!( + decoder.try_seek(Duration::from_secs(1)), + Err(SeekError::SymphoniaDecoder( + symphonia::SeekError::RandomAccessNotSupported, + )) + ), + "backward seek should fail without byte_len" + ); + + // Decoder::try_from:: sets byte_len and is_seekable + let mut decoder = get_music("mp3"); + assert!( + decoder.try_seek(Duration::from_secs(2)).is_ok(), + "forward seek should work with byte_len" + ); + assert!( + decoder.try_seek(Duration::from_secs(1)).is_ok(), + "backward seek should work with byte_len" + ); +} + +fn second_channel_beep_range(source: &mut R) -> std::ops::Range { let channels = source.channels().get() as usize; let samples: Vec = source.by_ref().collect(); @@ -202,23 +313,33 @@ where .next_multiple_of(channels); let samples = &samples[beep_starts..beep_starts + 100]; - assert!(is_silent(samples, channels, 0), "{samples:?}"); - assert!(!is_silent(samples, channels, 1), "{samples:?}"); + assert!( + is_silent(samples, ChannelCount::new(channels as u16).unwrap(), 0), + "{samples:?}" + ); + assert!( + !is_silent(samples, ChannelCount::new(channels as u16).unwrap(), 1), + "{samples:?}" + ); beep_starts..beep_ends } -fn is_silent(samples: &[f32], channels: usize, channel: usize) -> bool { +fn is_silent(samples: &[f32], channels: ChannelCount, channel: usize) -> bool { assert_eq!(samples.len(), 100); - let channel = samples.iter().skip(channel).step_by(channels); - let volume = channel.map(|s| s.abs()).sum::() / samples.len() as f32 * channels as f32; + let channel = samples + .iter() + .skip(channel) + .step_by(channels.get() as usize); + let volume = + channel.map(|s| s.abs()).sum::() / samples.len() as f32 * channels.get() as f32; const BASICALLY_ZERO: f32 = 0.0001; volume < BASICALLY_ZERO } fn time_remaining(decoder: Decoder) -> Duration { - let rate = decoder.sample_rate() as f64; + let rate = decoder.sample_rate().get() as f64; let n_channels = decoder.channels().get() as f64; let n_samples = decoder.into_iter().count() as f64; Duration::from_secs_f64(n_samples / rate / n_channels) @@ -227,12 +348,11 @@ fn time_remaining(decoder: Decoder) -> Duration { fn get_music(format: &str) -> Decoder { let asset = Path::new("assets/music").with_extension(format); let file = std::fs::File::open(asset).unwrap(); - Decoder::new(BufReader::new(file)).unwrap() + Decoder::try_from(file).unwrap() } fn get_rl(format: &str) -> Decoder { let asset = Path::new("assets/RL").with_extension(format); - println!("opening: {}", asset.display()); let file = std::fs::File::open(asset).unwrap(); - Decoder::new(BufReader::new(file)).unwrap() + Decoder::try_from(file).unwrap() } From c8d7dabaa3f6e0bffe06a7d8518bf4a923d73c67 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Sat, 9 Aug 2025 21:55:45 +0200 Subject: [PATCH 3/4] fix mp3 decoder --- src/decoder/mp3.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index e14b3579..a84026db 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -1,4 +1,5 @@ use std::io::{Read, Seek, SeekFrom}; +use std::num::NonZero; use std::time::Duration; use crate::common::{ChannelCount, Sample, SampleRate}; @@ -62,12 +63,12 @@ where #[inline] fn channels(&self) -> ChannelCount { - self.current_span.channels as _ + NonZero::new(self.current_span.channels as _).expect("mp3's have at least one channel") } #[inline] fn sample_rate(&self) -> SampleRate { - self.current_span.sample_rate + NonZero::new(self.current_span.sample_rate as _).expect("mp3's have a non zero sample rate") } #[inline] From cf7a3efca6e5226942a5de13c6202c8adc9e1f04 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Mon, 11 Aug 2025 01:07:59 +0200 Subject: [PATCH 4/4] Feedback from review co-author: roderickvd --- benches/resampler.rs | 2 +- src/conversions/sample_rate.rs | 2 +- src/source/channel_volume.rs | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/benches/resampler.rs b/benches/resampler.rs index deb10f08..b4f66955 100644 --- a/benches/resampler.rs +++ b/benches/resampler.rs @@ -31,7 +31,7 @@ const COMMON_SAMPLE_RATES: [u32; 12] = [ #[divan::bench(args = COMMON_SAMPLE_RATES)] fn resample_to(bencher: Bencher, target_sample_rate: u32) { - let target_sample_rate = SampleRate::new(target_sample_rate).unwrap(); + let target_sample_rate = SampleRate::new(target_sample_rate).expect("Is not zero"); bencher .with_inputs(|| { let source = music_wav(); diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs index 2c7db4ac..556566f1 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -292,7 +292,7 @@ mod test { }; let output = - SampleRateConverter::new(input.clone().into_iter(), SampleRate::new(from).unwrap(), to, channels) + SampleRateConverter::new(input.clone().into_iter(), SampleRate::new(from).expect("to is nonzero and k is nonzero"), to, channels) .collect::>(); TestResult::from_bool(input.chunks_exact(channels.get().into()) diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index 6dbe7cce..c80c4d73 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -24,10 +24,7 @@ where /// Wrap the input source and make it mono. Play that mono sound to each /// channel at the volume set by the user. The volume can be changed using /// [`ChannelVolume::set_volume`]. - /// - /// # Panics if channel_volumes is empty pub fn new(input: I, channel_volumes: Vec) -> ChannelVolume { - assert!(!channel_volumes.is_empty()); let channel_count = channel_volumes.len(); // See next() implementation. ChannelVolume { input,