diff --git a/README.md b/README.md index ccc96fea..8586483f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ [![Crates.io Downloads](https://img.shields.io/crates/d/rodio.svg)](https://crates.io/crates/rodio) [![Build Status](https://github.com/RustAudio/rodio/workflows/CI/badge.svg)](https://github.com/RustAudio/rodio/actions) +> [!WARNING] +> We are currently rewriting Rodio's core audio engine. The current state should +> be working but under documented. This is expected to take at least a month. +> For the latest unreleased work pre engine rewrite see [this +> checkout](https://github.com/RustAudio/rodio/tree/c5f1e94290922fe4ee963ce6f85754ea6ba36243). +> To follow our progress see the [tracking +> issue](https://github.com/RustAudio/rodio/issues/901) + Rust playback library. Playback is handled by [cpal](https://github.com/RustAudio/cpal). Format decoding is handled by [Symphonia](https://github.com/pdeljanov/Symphonia) by default, or by optional format-specific decoders: diff --git a/src/buffer.rs b/src/buffer.rs index 187a6b79..f9c03a41 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -12,9 +12,8 @@ //! use crate::common::{ChannelCount, SampleRate}; -use crate::math::{duration_to_float, NANOS_PER_SEC}; use crate::source::{SeekError, UniformSourceIterator}; -use crate::{Float, Sample, Source}; +use crate::{Sample, Source}; use std::sync::Arc; use std::time::Duration; @@ -25,37 +24,19 @@ pub struct SamplesBuffer { pos: usize, channels: ChannelCount, sample_rate: SampleRate, - duration: Duration, } impl SamplesBuffer { /// Builds a new `SamplesBuffer`. - /// - /// # Panics - /// - /// - 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. - /// pub fn new(channels: ChannelCount, sample_rate: SampleRate, data: D) -> Self where D: Into>, { - let data: Arc<[Sample]> = data.into().into(); - let duration_ns = NANOS_PER_SEC.checked_mul(data.len() as u64).unwrap() - / sample_rate.get() as u64 - / channels.get() as u64; - let duration = Duration::new( - duration_ns / NANOS_PER_SEC, - (duration_ns % NANOS_PER_SEC) as u32, - ); - Self { - data, + data: data.into().into(), pos: 0, channels, sample_rate, - duration, } } @@ -91,50 +72,11 @@ impl Source for SamplesBuffer { self.sample_rate } - #[inline] - fn total_duration(&self) -> Option { - Some(self.duration) - } - - /// This jumps in memory till the sample for `pos`. - #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - // This is fast because all the samples are in memory already - // and due to the constant sample_rate we can jump to the right - // sample directly. - - let curr_channel = self.pos % self.channels().get() as usize; - let new_pos = duration_to_float(pos) - * self.sample_rate().get() as Float - * self.channels().get() as Float; - // 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().get() as usize); - let new_pos = new_pos - curr_channel; - - self.pos = new_pos; - Ok(()) - } + crate::common::source::buffer::source_impl! {} } impl Iterator for SamplesBuffer { - type Item = Sample; - - #[inline] - fn next(&mut self) -> Option { - let sample = self.data.get(self.pos)?; - self.pos += 1; - Some(*sample) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - let remaining = self.data.len() - self.pos; - (remaining, Some(remaining)) - } + crate::common::source::buffer::iter_impl! {} } impl ExactSizeIterator for SamplesBuffer {} diff --git a/src/common.rs b/src/common.rs index d1ed0136..edc44b7f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,6 +3,8 @@ use std::num::NonZero; use crate::math::nz; +pub(crate) mod source; + /// Sample rate (a frame rate or samples per second per channel). pub type SampleRate = NonZero; diff --git a/src/common/source.rs b/src/common/source.rs new file mode 100644 index 00000000..6798c355 --- /dev/null +++ b/src/common/source.rs @@ -0,0 +1,20 @@ +//! Code shared between source types. +//! +//! Since we have three source types there is a lot of code duplication. We +//! combat that by placing whatever can be shared here. +//! +//! This can be: +//! - shared types like error enums +//! - shared free functions +//! - shared members / impl blocks through macro_rules +//! +//! # Note +//! Effects are defined through a macro and do not need this kind of +//! deduplication +//! +//! This modules structure mirrors that of what it deduplicates. For example +//! the code shared between [fixed_source::chain] and [const_source::chain] is in +//! common/source/chain.rs + +pub(crate) mod buffer; +pub(crate) mod chain; diff --git a/src/common/source/buffer.rs b/src/common/source/buffer.rs new file mode 100644 index 00000000..49a625b8 --- /dev/null +++ b/src/common/source/buffer.rs @@ -0,0 +1,66 @@ +macro_rules! source_impl { + () => { + /// # Panics + /// If the length of the buffer is larger than approximately 16 billion elements. + /// This is because the calculation of the duration would overflow. + #[inline] + fn total_duration(&self) -> Option { + use crate::math::NANOS_PER_SEC; + + let duration_ns = NANOS_PER_SEC + .checked_mul(self.data.len() as u64) + .expect("slices longer then 16 billion elements are not supported") + / self.sample_rate().get() as u64 + / self.channels().get() as u64; + let duration = Duration::new( + duration_ns / NANOS_PER_SEC, + (duration_ns % NANOS_PER_SEC) as u32, + ); + + Some(duration) + } + + /// This jumps in memory to the sample corresponding to `pos`. + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + // This is fast because all the samples are in memory already + // and due to the constant sample_rate we can jump to the right + // sample directly. + + let curr_channel = self.pos % self.channels().get() as usize; + let new_pos = crate::math::duration_to_float(pos) + * self.sample_rate().get() as crate::Float + * self.channels().get() as crate::Float; + // 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().get() as usize); + let new_pos = new_pos - curr_channel; + + self.pos = new_pos; + Ok(()) + } + }; +} + +macro_rules! iter_impl { + () => { + type Item = Sample; + #[inline] + fn next(&mut self) -> Option { + let sample = self.data.get(self.pos)?; + self.pos += 1; + Some(*sample) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining = self.data.len() - self.pos; + (remaining, Some(remaining)) + } + }; +} + +pub(crate) use iter_impl; +pub(crate) use source_impl; diff --git a/src/common/source/chain.rs b/src/common/source/chain.rs new file mode 100644 index 00000000..65f370b3 --- /dev/null +++ b/src/common/source/chain.rs @@ -0,0 +1,94 @@ +use crate::source::SeekError; + +#[derive(Debug, thiserror::Error)] +pub enum ChainSeekError { + #[error("Could not get duration of first source ({ty}")] + NoTotalDurationForFirst { ty: &'static str }, + #[error("Could not seek in first source")] + FailedToSeekInFirst { + ty: &'static str, + #[source] + error: SeekError, + }, + #[error("Could not seek in second source")] + FailedToSeekInSecond { + ty: &'static str, + #[source] + error: SeekError, + }, +} + +macro_rules! source_impl { + () => { + fn channels(&self) -> crate::ChannelCount { + self.first.channels() + } + + fn sample_rate(&self) -> crate::SampleRate { + self.first.sample_rate() + } + + fn total_duration(&self) -> Option { + self.first + .total_duration() + .and_then(|d| self.second.total_duration().map(|d2| d2 + d)) + } + + fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), crate::source::SeekError> { + use crate::source::SeekError; + use std::any::type_name_of_val; + use std::sync::Arc; + + let Some(first) = self.first.total_duration() else { + return Err(ChainSeekError::NoTotalDurationForFirst { + ty: type_name_of_val(&self.first), + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)); + }; + + if pos < first { + self.first + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInFirst { + ty: type_name_of_val(&self.first), + error, + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)) + } else { + self.second + .try_seek(pos) + .map_err(|error| ChainSeekError::FailedToSeekInSecond { + ty: type_name_of_val(&self.second), + error, + }) + .map_err(Arc::new) + .map_err(|e| SeekError::Other(e)) + } + } + }; +} + +macro_rules! iter_impl { + () => { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.playing_inner { + match self.first.next() { + Some(sample) => Some(sample), + None => { + self.playing_inner = false; + self.second.next() + } + } + } else { + self.second.next() + } + } + }; +} + +pub(crate) use iter_impl; +pub(crate) use source_impl; diff --git a/src/const_source.rs b/src/const_source.rs new file mode 100644 index 00000000..969786d3 --- /dev/null +++ b/src/const_source.rs @@ -0,0 +1,149 @@ +//! A sound source that has it's sample rate and number of channels fixed at compile time. +//! Some practical examples: +//! - An effect performed by a neural network trained on a specific channel +//! count and sample rate +//! - A custom audio source streaming in at a pre-determined fixed sample rate. +//! We use this in our VoIP example. +//! +//! A const pipeline _may_ also be optimized more by the compiler as many +//! branches are known at compile time, like when converting from one channel +//! count to another. +use std::marker::PhantomData; +use std::num::NonZeroU16; +use std::num::NonZeroU32; +use std::time::Duration; + +use crate::source::SeekError; +use crate::ChannelCount; +use crate::Sample; +use crate::SampleRate; +use crate::Source as DynamicSource; // Source will (probably) be renamed to this later + +mod buffer; +mod chain; + +pub use buffer::SamplesBuffer; +pub use chain::SourceChain; + +/// A source which sample rate and channel count are fixed at compile time. +pub trait ConstSource: Iterator { + /// Returns the sample rate which is the first generic (SR) of a ConstSource + fn sample_rate(&self) -> SampleRate { + const { NonZeroU32::new(SR).expect("SampleRate must be > 0") } + } + /// Returns the channel count which is the second generic (CH) of a ConstSource + fn channels(&self) -> ChannelCount { + const { NonZeroU16::new(CH).expect("Channel count must be > 0") } + } + + #[doc = include_str!("docs/total_duration.md")] + fn total_duration(&self) -> Option; + + #[allow(unused_variables)] + #[doc = include_str!("docs/try_seek.md")] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) + } + + /// Use this const source as if it's a dynamic source. You generally do not + /// want to do this since there are less effects for dynamic sources and + /// those that are available can not be implemented as efficient. This is + /// therefore purely provided for backwards compatibility. + fn into_dynamic_source(self) -> IntoDynamicSource + where + Self: Sized, + { + IntoDynamicSource { inner: self } + } + + #[doc = include_str!("docs/collect_into_buffer.md")] + fn collect_into_buffer(self) -> SamplesBuffer + where + Self: Sized, + { + SamplesBuffer::new(self.collect::>()) + } + + /// Add another source to play directly after this one. + /// + /// # Example + /// + /// ```rust + /// # use rodio::const_source::ConstSource; + /// # use rodio::const_source::SamplesBuffer; + /// let preamble = SamplesBuffer::<44100, 1>::new([1.0, 1.0]); + /// let signal = SamplesBuffer::<44100, 1>::new([2.0, 2.0]); + /// + /// let mixed = preamble.chain_source(signal); + /// assert_eq!(mixed.collect::>(), vec![1.0,1.0,2.0,2.0]) + /// ``` + fn chain_source(self, next: S) -> SourceChain + where + Self: Sized, + S: ConstSource, + { + SourceChain::new(self, next) + } + + // placeholder until effects land (need this for some examples) + #[allow(missing_docs)] + fn take_duration(self, _duration: Duration) -> Placeholder + where + Self: Sized, + { + todo!() + } +} + +// placeholder until effects land (need this for some examples) +#[allow(missing_docs)] +pub struct Placeholder +where + S: ConstSource, +{ + inner: PhantomData, +} + +/// A `DynamicSource` converted from a `ConstSource`. Useful for passing to old +/// APIs that do not accept a `ConstSource` nor `FixedSource`. +#[derive(Clone)] +pub struct IntoDynamicSource +where + S: ConstSource, +{ + inner: S, +} + +impl Iterator for IntoDynamicSource +where + S: ConstSource, +{ + type Item = Sample; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl DynamicSource for IntoDynamicSource +where + S: ConstSource, +{ + fn current_span_len(&self) -> Option { + None + } + + fn channels(&self) -> ChannelCount { + const { NonZeroU16::new(CH).expect("channel count must be larger then zero") } + } + + fn sample_rate(&self) -> SampleRate { + const { NonZeroU32::new(SR).expect("sample rate must be larger then zero") } + } + + fn total_duration(&self) -> Option { + self.inner.total_duration() + } +} diff --git a/src/const_source/buffer.rs b/src/const_source/buffer.rs new file mode 100644 index 00000000..55aaaa6b --- /dev/null +++ b/src/const_source/buffer.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; +use std::time::Duration; + +use crate::source::SeekError; +use crate::ConstSource; +use crate::Sample; + +/// A buffer of samples treated as a source. +#[derive(Debug, Clone)] +pub struct SamplesBuffer { + data: Arc<[Sample]>, + pos: usize, +} + +impl SamplesBuffer { + /// Builds a new `SamplesBuffer`. + /// + /// Note any call to total_duration will panic if the buffer is larger then + /// 16 billion elements. + pub fn new(data: D) -> SamplesBuffer + where + D: Into>, + { + const { assert!(SR > 0) }; + const { assert!(CH > 0) }; + + SamplesBuffer { + data: data.into().into(), + pos: 0, + } + } +} + +impl ConstSource for SamplesBuffer { + crate::common::source::buffer::source_impl! {} +} + +impl Iterator for SamplesBuffer { + crate::common::source::buffer::iter_impl! {} +} diff --git a/src/const_source/chain.rs b/src/const_source/chain.rs new file mode 100644 index 00000000..eb26c64b --- /dev/null +++ b/src/const_source/chain.rs @@ -0,0 +1,35 @@ +use crate::ConstSource; +use crate::Sample; + +/// Two chained sources, the one played after the other. +#[derive(Clone)] +pub struct SourceChain { + first: S1, + second: S2, + playing_inner: bool, +} + +impl, S2: ConstSource> + SourceChain +{ + pub(crate) fn new(s1: S1, s2: S2) -> Self { + SourceChain { + first: s1, + second: s2, + playing_inner: true, + } + } +} + +pub use crate::common::source::chain::ChainSeekError; +impl, S2: ConstSource> + ConstSource for SourceChain +{ + crate::common::source::chain::source_impl! {} +} + +impl, S2: ConstSource> Iterator + for SourceChain +{ + crate::common::source::chain::iter_impl! {} +} diff --git a/src/docs/collect_into_buffer.md b/src/docs/collect_into_buffer.md new file mode 100644 index 00000000..d97b115b --- /dev/null +++ b/src/docs/collect_into_buffer.md @@ -0,0 +1,5 @@ +Builds a new `SamplesBuffer`. + +# Panics +- Panics if the length of the buffer is larger than approximately 16 billion elements. + This is because the calculation of the duration would overflow. diff --git a/src/docs/total_duration.md b/src/docs/total_duration.md new file mode 100644 index 00000000..e05826cd --- /dev/null +++ b/src/docs/total_duration.md @@ -0,0 +1,6 @@ +Returns the total duration of this source, if known. + +`None` indicates at the same time "infinite" or "unknown". + +Note this value is free to change. For example if you removed an item from a +queue the total_duration lowers. diff --git a/src/docs/try_seek.md b/src/docs/try_seek.md new file mode 100644 index 00000000..cc614292 --- /dev/null +++ b/src/docs/try_seek.md @@ -0,0 +1,16 @@ +Attempts to seek to a given position in the current source. + +As long as the duration of the source is known, seek is guaranteed to saturate +at the end of the source. For example given a source that reports a total duration +of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to +42 seconds. + +# Errors +This function will return [`SeekError::NotSupported`] if one of the underlying +sources does not support seeking. + +It will return an error if an implementation ran +into one during the seek. + +Seeking beyond the end of a source might return an error if the total duration of +the source is not known. diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 00000000..bb69047f --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,10 @@ +//! Sources that produce sound without consuming anything + +// Note we make everything be it effects or generators available under / + +mod silence; + +/// Generators with both the sample rate and channel count known at compile time. +pub mod const_source { + pub use super::silence::const_source::Silence; +} diff --git a/src/generators/silence.rs b/src/generators/silence.rs new file mode 100644 index 00000000..fa0ed7d8 --- /dev/null +++ b/src/generators/silence.rs @@ -0,0 +1,62 @@ +pub mod const_source { + use std::time::Duration; + + use crate::source::SeekError; + use crate::ConstSource; + + /// A source producing an infinite amount of Silence. Like all generators you + /// probably want to limit the duration of this source. + /// + /// # Example + /// Padding a [`TakeDuration`](crate::const_source::Placeholder) to + /// guarantee an exact playtime: + /// + /// ```rust,no_run + /// # use std::time::Duration; + /// # use rodio::nz; + /// # use rodio::generators::const_source::Silence; + /// # use rodio::ConstSource; + /// # let unknown_length = Silence::new(); + /// let silence: Silence<44100> = Silence::new(); + /// let two_seconds = unknown_length + /// .chain_source(silence) + /// .take_duration(Duration::from_secs(2)); + /// ``` + pub struct Silence; + + impl Silence { + /// Create an infinite silence + pub fn new() -> Self { + Self + } + } + + impl Default for Silence { + fn default() -> Self { + Self + } + } + + impl ConstSource for Silence { + fn total_duration(&self) -> Option { + None + } + + /// This does nothing since all silence is equal :3 + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Ok(()) + } + } + + impl Iterator for Silence { + type Item = crate::Sample; + + fn next(&mut self) -> Option { + Some(0.0) + } + + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dbd6174c..b1553d60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,10 +224,13 @@ pub mod stream; mod wav_output; pub mod buffer; +pub mod const_source; pub mod conversions; pub mod decoder; #[cfg(feature = "experimental")] pub mod fixed_source; + +pub mod generators; pub mod math; #[cfg(feature = "recording")] /// Microphone input support for audio recording. @@ -238,6 +241,7 @@ pub mod source; pub mod static_buffer; pub use crate::common::{BitDepth, ChannelCount, Float, Sample, SampleRate, DEFAULT_SAMPLE_RATE}; +pub use crate::const_source::ConstSource; pub use crate::decoder::Decoder; #[cfg(feature = "experimental")] pub use crate::fixed_source::FixedSource; @@ -250,3 +254,5 @@ pub use crate::stream::{play, DeviceSinkBuilder, DeviceSinkError, MixerDeviceSin pub use crate::wav_output::wav_to_file; #[cfg(feature = "wav_output")] pub use crate::wav_output::wav_to_writer; + +pub use Source as DynamicSource; diff --git a/src/source/mod.rs b/src/source/mod.rs index b8ea8553..237735be 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -217,9 +217,7 @@ pub trait Source: Iterator { /// Returns the rate at which the source should be played. In number of samples per second. fn sample_rate(&self) -> SampleRate; - /// Returns the total duration of this source, if known. - /// - /// `None` indicates at the same time "infinite" or "unknown". + #[doc = include_str!("../docs/total_duration.md")] fn total_duration(&self) -> Option; /// Stores the source in a buffer in addition to returning it. This iterator can be cloned. @@ -697,23 +695,8 @@ pub trait Source: Iterator { SampleRateConverter::new(self, target_rate, config) } - /// Attempts to seek to a given position in the current source. - /// - /// As long as the duration of the source is known, seek is guaranteed to saturate - /// at the end of the source. For example given a source that reports a total duration - /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to - /// 42 seconds. - /// - /// # Errors - /// This function will return [`SeekError::NotSupported`] if one of the underlying - /// sources does not support seeking. - /// - /// It will return an error if an implementation ran - /// into one during the seek. - /// - /// Seeking beyond the end of a source might return an error if the total duration of - /// the source is not known. #[allow(unused_variables)] + #[doc = include_str!("../docs/try_seek.md")] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { Err(SeekError::NotSupported { underlying_source: std::any::type_name::(),