diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d434dc7..9ed597ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All sources now implement `Iterator::size_hint()`. - `Chirp` now implements `try_seek`. - Added `DEFAULT_SAMPLE_RATE` set to match `cpal::SAMPLE_RATE_48K`. +- Added `Source::resample` and `ResampleConfig` for high-quality sample rate conversion. ### Changed @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Explicitly document the requirement for sources to return complete frames. - Ensured decoders to always return complete frames, as well as `TakeDuration` when expired. - Breaking: `Zero::new_samples()` now returns `Result` requiring a frame-aligned number of samples. +- Breaking: `SampleRateConverter` now wraps a `Source` instead of an `Iterator` and takes a `ResampleConfig`. - Improved queue, buffer, mixer and sample rate conversion performance. - Default sample rate changed from 44.1 kHz to 48 kHz consistently. - `open_sink_or_fallback` now tries 48 kHz and 44.1 kHz before the device's maximum sample rate. @@ -91,6 +93,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Source::dither()` function for applying dithering - Added `64bit` feature to opt-in to 64-bit sample precision (`f64`). - Added `SampleRateConverter::inner` to get underlying iterator by ref. +- Added `Resample` source for high-quality sample rate conversion. +- Added `FromIter` source that wraps a sample iterator. +- Added `ChannelCountConverter::inner()` for immutable access to the underlying iterator. +- `ChannelCountConverter` now implements `Source`. +- Added `FromIter::{inner, inner_mut, into_inner}` accessor methods. ### Fixed @@ -104,6 +111,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed `Empty` source to properly report exhaustion. - Fixed `Zero::current_span_len` returning remaining samples instead of span length. +### Deprecated +- `SampleRateConverter` is deprecated in favor of using `Resample` with `FromIter`. +- `FromFactoryIter` type is deprecated, renamed to `FromFn`. +- `from_factory()` function is deprecated, renamed to `from_fn()`. + ### Changed - Breaking: _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced @@ -125,6 +137,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `cpal` to v0.17. - Clarified `Source::current_span_len()` contract documentation. - Improved queue, mixer and sample rate conversion performance. +- `SampleRateConverter` uses the new `Resample` source for better quality. +- Renamed `FromIter` for sequencing multiple sources to `Chain`. +- Renamed `FromFactoryIter` for generating sources from a function to `FromFn`. ## Version [0.21.1] (2025-07-14) diff --git a/Cargo.lock b/Cargo.lock index f2ac235d8..bf4f0a113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,12 +43,56 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -89,6 +133,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" +[[package]] +name = "audio-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ebbf82d06013f4c41fe71303feb980cddd78496d904d06be627972de51a24" + [[package]] name = "audio_thread_priority" version = "0.35.1" @@ -103,6 +153,37 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "audioadapter" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f87b70b051c5866680ad79f6743a42ccab264c009d1a71f4d33a3872ae60c8" +dependencies = [ + "audio-core", + "num-traits", +] + +[[package]] +name = "audioadapter-buffers" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9097d67933fb083d382ce980430afdb758aada60846010aee6be068c06cef0ca" +dependencies = [ + "audioadapter", + "audioadapter-sample", + "num-traits", +] + +[[package]] +name = "audioadapter-sample" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ab94f2bc04a14e1f49ee5f222f66460e8a1b51627bdfedf34eed394d747938" +dependencies = [ + "audio-core", + "num-traits", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -241,6 +322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -249,11 +331,25 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", "terminal_size", ] +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "1.1.0" @@ -275,6 +371,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -801,6 +903,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1148,16 +1256,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -1187,17 +1285,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1335,6 +1422,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opusic-sys" version = "0.7.3" @@ -1555,6 +1648,15 @@ dependencies = [ "rand 0.10.1", ] +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1611,6 +1713,7 @@ version = "0.22.2" dependencies = [ "approx", "atomic_float", + "clap", "claxon", "cpal", "crossbeam-channel", @@ -1620,13 +1723,13 @@ dependencies = [ "inquire", "lewton", "minimp3_fixed", - "num-rational", "quickcheck", "rand 0.10.1", "rand_distr", "rstest", "rstest_reuse", "rtrb", + "rubato", "symphonia", "symphonia-adapter-fdk-aac", "symphonia-adapter-libopus", @@ -1680,6 +1783,22 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" +[[package]] +name = "rubato" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4be9c88e3d722d3d36939e41941f6b6f52810c1235bf19998a7d4535b402892" +dependencies = [ + "audioadapter", + "audioadapter-buffers", + "num-complex", + "num-integer", + "num-traits", + "realfft", + "visibility", + "windowfunctions", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1888,6 +2007,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "symphonia" version = "0.5.5" @@ -2326,12 +2451,29 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version-compare" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -2496,6 +2638,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windowfunctions" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90628d739333b7c5d2ee0b70210b97b8cddc38440c682c96fd9e2c24c2db5f3a" +dependencies = [ + "num-traits", +] + [[package]] name = "windows" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 38f286335..586f8b0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ edition = "2021" rust-version = "1.89" [features] -# Default feature set provides audio playback and common format support default = [ # Core functionality "playback", @@ -88,14 +87,16 @@ symphonia = ["dep:symphonia"] # - .mp3 is an MPEG-1 Audio Layer III file, which is a container format that uses the MP3 codec # - .mp4 is an MPEG-4 container, typically (but not always) with an AAC-encoded audio stream # - .ogg is an Ogg container with a Vorbis-encoded audio stream -# -# A reasonable set of audio demuxers and decoders for most applications. flac = ["symphonia-flac"] mp3 = ["symphonia-mp3"] mp4 = ["symphonia-isomp4", "symphonia-aac"] vorbis = ["symphonia-ogg", "symphonia-vorbis"] wav = ["symphonia-wav", "symphonia-pcm"] +# Aliases +aac = ["mp4"] +ogg = ["vorbis"] + # The following features are combinations of demuxers and decoders provided by Symphonia. # Unless you are developing a generic audio player, this is probably overkill. symphonia-all = ["symphonia/all-formats", "symphonia/all-codecs"] @@ -127,11 +128,19 @@ symphonia-wav = ["symphonia/wav"] # libopus adapter for Symphonia symphonia-libopus = ["symphonia", "dep:symphonia-adapter-libopus"] +# Resampling features +# +# Enable FFT-based synchronous resampling as an optimization for fixed-ratio conversions. When +# enabled, conversions between standard sample rates (e.g., 48 kHz and 96 kHz, as well as 44.1 kHz +# and 48 kHz) that simplify to small integer ratios automatically use FFT processing for faster +# processing at the cost of a larger binary. +rubato-fft = ["rubato/fft_resampler"] + # Alternative decoders and demuxers claxon = ["dep:claxon"] # FLAC hound = ["dep:hound"] # WAV -minimp3 = ["dep:minimp3_fixed"] # MP3 lewton = ["dep:lewton"] # Ogg Vorbis +minimp3 = ["dep:minimp3_fixed"] # MP3 [package.metadata.docs.rs] all-features = true @@ -155,7 +164,9 @@ tracing = { version = "0.1.40", optional = true } atomic_float = { version = "1.1.0", optional = true } rtrb = { version = "0.3.2", optional = true } -num-rational = "0.4.2" + +# Rubato resampling +rubato = { version = "3.0", default-features = false } symphonia-adapter-libopus = { version = "0.2", optional = true } @@ -167,6 +178,7 @@ approx = "0.5.1" divan = "0.1.14" inquire = "0.9.3" symphonia-adapter-fdk-aac = "0.1" +clap = { version = "4", features = ["derive"] } [[bench]] name = "effects" @@ -272,6 +284,10 @@ required-features = ["playback", "symphonia-libopus"] name = "noise_generator" required-features = ["playback", "noise"] +[[example]] +name = "resample" +required-features = ["playback"] + [[example]] name = "reverb" required-features = ["playback", "vorbis"] diff --git a/examples/resample.rs b/examples/resample.rs new file mode 100644 index 000000000..b93de8bda --- /dev/null +++ b/examples/resample.rs @@ -0,0 +1,121 @@ +//! Example demonstrating audio resampling with different quality presets. + +use clap::Parser; +use rodio::source::{ResampleConfig, Source}; +#[cfg(feature = "wav_output")] +use rodio::wav_to_file; +use rodio::{Decoder, DeviceSinkBuilder, Player}; +use std::error::Error; +use std::num::NonZero; +use std::path::PathBuf; +use std::time::Instant; + +#[derive(Parser)] +#[command(about = "Resample audio using different quality presets")] +struct Args { + /// Target sample rate in Hz (default: device native rate when playing, source rate when + /// writing) + #[arg(long = "rate")] + target_rate: Option>, + + /// Path to input audio file + #[arg(long = "input", default_value = "assets/music.ogg")] + input: PathBuf, + + /// Path to output WAV file; if omitted, audio plays to the default device + #[cfg(feature = "wav_output")] + #[arg(long = "output")] + output: Option, + + /// Resampling method + #[arg(long = "method", value_enum, default_value_t = Method::Balanced)] + method: Method, +} + +#[derive(clap::ValueEnum, Clone)] +enum Method { + /// Nearest-neighbor (zero-order hold) polynomial resampling. Fastest, no anti-aliasing. + Nearest, + /// Linear polynomial resampling. Fast, no anti-aliasing. + Linear, + /// Cubic polynomial resampling. Smoother than linear, no anti-aliasing. + Cubic, + /// Quintic polynomial resampling. Smoother than cubic, no anti-aliasing. + Quintic, + /// Septic polynomial resampling. Highest polynomial quality, no anti-aliasing. + Septic, + /// 64-tap sinc, linear interpolation, Hann2 window. + VeryFast, + /// 128-tap sinc, linear interpolation, Blackman2 window. + Fast, + /// 192-tap sinc, quadratic interpolation, BlackmanHarris2 window (default). + Balanced, + /// 256-tap sinc, cubic interpolation, BlackmanHarris2 window. + Accurate, +} + +impl From for ResampleConfig { + fn from(method: Method) -> Self { + match method { + Method::Nearest => ResampleConfig::nearest(), + Method::Linear => ResampleConfig::linear(), + Method::Cubic => ResampleConfig::cubic(), + Method::Quintic => ResampleConfig::quintic(), + Method::Septic => ResampleConfig::septic(), + Method::VeryFast => ResampleConfig::very_fast(), + Method::Fast => ResampleConfig::fast(), + Method::Balanced => ResampleConfig::balanced(), + Method::Accurate => ResampleConfig::accurate(), + } + } +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let config = ResampleConfig::from(args.method); + + let file = std::fs::File::open(&args.input) + .map_err(|e| format!("Failed to open '{}': {e}", args.input.display()))?; + let source = Decoder::try_from(file)?; + + let source_rate = source.sample_rate().get(); + let channels = source.channels().get(); + + if let Some(dur) = source.total_duration() { + println!("Duration: {dur:?}"); + } + + #[cfg(feature = "wav_output")] + if let Some(output_path) = args.output { + let target_rate = args.target_rate.unwrap_or_else(|| source.sample_rate()); + println!("Resampling {channels}ch {source_rate} Hz → {target_rate} Hz"); + println!("Configuration: {config:#?}"); + let resampled = source.resample(target_rate, config); + println!("Writing to '{}'...", output_path.display()); + let start = Instant::now(); + wav_to_file(resampled, &output_path)?; + println!("Finished in {:?}", start.elapsed()); + return Ok(()); + } + + let builder = DeviceSinkBuilder::from_default_device()?; + let stream_handle = match args.target_rate { + Some(rate) => builder.with_sample_rate(rate).open_stream()?, + None => builder.open_stream()?, + }; + let target_rate = stream_handle.config().sample_rate(); + + println!("Resampling {channels}ch {source_rate} Hz → {target_rate} Hz"); + println!("Configuration: {config:#?}"); + + let resampled = source.resample(target_rate, config); + let player = Player::connect_new(stream_handle.mixer()); + + println!("Playing... (Ctrl+C to stop)"); + let start = Instant::now(); + player.append(resampled); + player.sleep_until_end(); + + println!("Finished in {:?}", start.elapsed()); + Ok(()) +} diff --git a/src/conversions/channels.rs b/src/conversions/channels.rs index c1401357f..94bcf4428 100644 --- a/src/conversions/channels.rs +++ b/src/conversions/channels.rs @@ -1,7 +1,11 @@ use crate::common::ChannelCount; -use crate::Sample; +use crate::{Sample, Source}; +use dasp_sample::Sample as _; -/// Iterator that converts from a certain channel count to another. +/// Converts a source from one channel count to another. +/// +/// When adding channels, a mono input is duplicated across the first two channels and the rest are +/// filled with silence. When removing channels, the surplus channels are dropped. #[derive(Clone, Debug)] pub struct ChannelCountConverter where @@ -19,11 +23,6 @@ where I: Iterator, { /// Initializes the iterator. - /// - /// # Panic - /// - /// Panics if `from` or `to` are equal to 0. - /// #[inline] pub fn new(input: I, from: ChannelCount, to: ChannelCount) -> ChannelCountConverter { ChannelCountConverter { @@ -41,7 +40,13 @@ where self.input } - /// Get mutable access to the iterator + /// Get immutable access to the underlying iterator. + #[inline] + pub fn inner(&self) -> &I { + &self.input + } + + /// Get mutable access to the underlying iterator. #[inline] pub fn inner_mut(&mut self) -> &mut I { &mut self.input @@ -64,7 +69,7 @@ where } x if x < self.from.get() => self.input.next(), 1 => self.sample_repeat, - _ => Some(0.0), + _ => Some(Sample::EQUILIBRIUM), }; if result.is_some() { @@ -104,6 +109,38 @@ where impl ExactSizeIterator for ChannelCountConverter where I: ExactSizeIterator {} +impl crate::Source for ChannelCountConverter +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + self.input + .current_span_len() + .map(|input_len| input_len / self.from.get() as usize * self.to.get() as usize) + } + + #[inline] + fn channels(&self) -> crate::common::ChannelCount { + self.to + } + + #[inline] + fn sample_rate(&self) -> crate::common::SampleRate { + self.input.sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.input.total_duration() + } + + #[inline] + fn try_seek(&mut self, pos: std::time::Duration) -> Result<(), crate::source::SeekError> { + self.input.try_seek(pos) + } +} + #[cfg(test)] mod test { use super::ChannelCountConverter; diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 0ec9d94f2..6adaa15ff 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,13 +1,29 @@ -/*! -This module contains functions that convert from one PCM format to another. - -This includes conversion between sample formats, channels or sample rates. -*/ +//! Convert audio between PCM formats. +//! +//! A PCM stream is described by three properties, each with a converter that changes it while +//! leaving the others intact: +//! +//! - **Sample type:** the numeric representation of each sample (e.g. `i16`, `f32`). +//! Changed with [`SampleTypeConverter`]. +//! - **Channel count:** the number of channels in the audio stream (e.g. mono, stereo, 5.1). +//! Changed with [`ChannelCountConverter`], which duplicates or drops channels as needed. +//! - **Sample rate:** frames per second (e.g. 44.1 kHz to 48 kHz). Changed with +//! [`SampleRateConverter`], which resamples the signal. See the [`sample_rate`] module for the +//! available algorithms and quality presets. +//! +//! Each converter is a [`Source`](crate::Source) (and [`Iterator`]) adapter that wraps another +//! source, so they can be composed by nesting. To retarget a source to a fixed output format in +//! one step, prefer the higher-level [`UniformSourceIterator`](crate::source::UniformSourceIterator), +//! which applies channel-count and sample-rate conversion together in the correct order, or +//! [`Source::resample`](crate::Source::resample) for the sample rate alone. pub use self::channels::ChannelCountConverter; pub use self::sample::SampleTypeConverter; -pub use self::sample_rate::SampleRateConverter; +pub use self::sample_rate::{ + Poly, PolyConfigBuilder, ResampleConfig, SampleRateConverter, Sinc, SincConfigBuilder, + WindowFunction, +}; mod channels; mod sample; -mod sample_rate; +pub mod sample_rate; diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index f231f33be..cf7094308 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -1,7 +1,10 @@ use dasp_sample::{FromSample, ToSample}; use std::marker::PhantomData; -/// Converts the samples data type to `O`. +/// Converts each sample's numeric type to `O`. +/// +/// Rescales the values to the target type's range (for example `i16` to `f32`), leaving the +/// channel count and sample rate unchanged. #[derive(Clone, Debug)] pub struct SampleTypeConverter { input: I, @@ -24,7 +27,13 @@ impl SampleTypeConverter { self.input } - /// get mutable access to the iterator + /// Get immutable access to the underlying iterator. + #[inline] + pub fn inner(&self) -> &I { + &self.input + } + + /// Get mutable access to the underlying iterator. #[inline] pub fn inner_mut(&mut self) -> &mut I { &mut self.input diff --git a/src/conversions/sample_rate.rs b/src/conversions/sample_rate.rs deleted file mode 100644 index 2120a7c8b..000000000 --- a/src/conversions/sample_rate.rs +++ /dev/null @@ -1,388 +0,0 @@ -use crate::common::{ChannelCount, SampleRate}; -use crate::{math, Sample}; -use num_rational::Ratio; -use std::collections::VecDeque; -use std::mem; - -/// Iterator that converts from a certain sample rate to another. -#[derive(Clone, Debug)] -pub struct SampleRateConverter -where - I: Iterator, -{ - /// The iterator that gives us samples. - input: I, - /// We convert chunks of `from` samples into chunks of `to` samples. - from: u32, - /// We convert chunks of `from` samples into chunks of `to` samples. - to: u32, - /// Number of channels in the stream - channels: ChannelCount, - /// One sample per channel, extracted from `input`. - current_span: Vec, - /// Position of `current_sample` modulo `from`. - current_span_pos_in_chunk: u32, - /// The samples right after `current_sample` (one per channel), extracted from `input`. - next_frame: Vec, - /// The position of the next sample that the iterator should return, modulo `to`. - /// This counter is incremented (modulo `to`) every time the iterator is called. - next_output_span_pos_in_chunk: u32, - /// The buffer containing the samples waiting to be output. - output_buffer: VecDeque, -} - -impl SampleRateConverter -where - I: Iterator, -{ - /// Create new sample rate converter. - /// - /// The converter uses simple linear interpolation for up-sampling - /// and discards samples for down-sampling. This may introduce audible - /// distortions in some cases (see [#584](https://github.com/RustAudio/rodio/issues/584)). - /// - /// # Limitations - /// Some rate conversions where target rate is high and rates are mutual primes the sample - /// interpolation may cause numeric overflows. Conversion between usual sample rates - /// 2400, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, ... is expected to work. - /// - /// # Panic - /// Panics if `from`, `to` or `num_channels` are 0. - #[inline] - pub fn new( - mut input: I, - from: SampleRate, - to: SampleRate, - num_channels: ChannelCount, - ) -> SampleRateConverter { - let (first_samples, next_samples) = if from == to { - // if `from` == `to` == 1, then we just pass through - (Vec::new(), Vec::new()) - } else { - let first = input - .by_ref() - .take(num_channels.get() as usize) - .collect::>(); - let next = input - .by_ref() - .take(num_channels.get() as usize) - .collect::>(); - (first, next) - }; - - // Reducing numerator to avoid numeric overflows during interpolation. - let (to, from) = Ratio::new(to.get(), from.get()).into_raw(); - - SampleRateConverter { - input, - from, - to, - channels: num_channels, - current_span_pos_in_chunk: 0, - next_output_span_pos_in_chunk: 0, - current_span: first_samples, - next_frame: next_samples, - // Capacity: worst case is upsampling where we buffer multiple frames worth of samples. - output_buffer: VecDeque::with_capacity( - (to as f32 / from as f32).ceil() as usize * num_channels.get() as usize, - ), - } - } - - /// Destroys this iterator and returns the underlying iterator. - #[inline] - pub fn into_inner(self) -> I { - self.input - } - - /// Get mutable access to the iterator - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.input - } - - /// Get a reference to the underlying iterator - #[inline] - pub fn inner(&self) -> &I { - &self.input - } - - fn next_input_span(&mut self) { - self.current_span_pos_in_chunk += 1; - - mem::swap(&mut self.current_span, &mut self.next_frame); - self.next_frame.clear(); - for _ in 0..self.channels.get() { - if let Some(i) = self.input.next() { - self.next_frame.push(i); - } else { - break; - } - } - } -} - -impl Iterator for SampleRateConverter -where - I: Iterator, -{ - type Item = I::Item; - - fn next(&mut self) -> Option { - // the algorithm below doesn't work if `self.from == self.to` - if self.from == self.to { - debug_assert_eq!(self.from, 1); - return self.input.next(); - } - - // Short circuit if there are some samples waiting. - if let Some(sample) = self.output_buffer.pop_front() { - return Some(sample); - } - - // The span we are going to return from this function will be a linear interpolation - // between `self.current_span` and `self.next_span`. - - if self.next_output_span_pos_in_chunk == self.to { - // If we jump to the next span, we reset the whole state. - self.next_output_span_pos_in_chunk = 0; - - self.next_input_span(); - while self.current_span_pos_in_chunk != self.from { - self.next_input_span(); - } - self.current_span_pos_in_chunk = 0; - } else { - // Finding the position of the first sample of the linear interpolation. - let req_left_sample = - (self.from * self.next_output_span_pos_in_chunk / self.to) % self.from; - - // Advancing `self.current_span`, `self.next_span` and - // `self.current_span_pos_in_chunk` until the latter variable - // matches `req_left_sample`. - while self.current_span_pos_in_chunk != req_left_sample { - self.next_input_span(); - debug_assert!(self.current_span_pos_in_chunk < self.from); - } - } - - // Merging `self.current_span` and `self.next_span` into `self.output_buffer`. - // Note that `self.output_buffer` can be truncated if there is not enough data in - // `self.next_span`. - let mut result = None; - let numerator = (self.from * self.next_output_span_pos_in_chunk) % self.to; - for (off, (cur, next)) in self - .current_span - .iter() - .zip(self.next_frame.iter()) - .enumerate() - { - let sample = math::lerp(*cur, *next, numerator, self.to); - - if off == 0 { - result = Some(sample); - } else { - self.output_buffer.push_back(sample); - } - } - - // Incrementing the counter for the next iteration. - self.next_output_span_pos_in_chunk += 1; - - if result.is_some() { - result - } else { - // draining `self.current_span` - let mut current_span = self.current_span.drain(..); - let r = current_span.next()?; - self.output_buffer.extend(current_span); - Some(r) - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - let apply = |samples: usize| { - // `samples_after_chunk` will contain the number of samples remaining after the chunk - // currently being processed - let samples_after_chunk = samples; - // adding the samples of the next chunk that may have already been read - let samples_after_chunk = if self.current_span_pos_in_chunk == self.from - 1 { - samples_after_chunk + self.next_frame.len() - } else { - samples_after_chunk - }; - // 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.get()), - ); - // calculating the number of samples after the transformation - // TODO: this is wrong here \|/ - let samples_after_chunk = samples_after_chunk * self.to as usize / self.from as usize; - - // `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.get()); - - samples_current_chunk + samples_after_chunk + self.output_buffer.len() - }; - - if self.from == self.to { - self.input.size_hint() - } else { - let (min, max) = self.input.size_hint(); - (apply(min), max.map(apply)) - } - } -} - -impl ExactSizeIterator for SampleRateConverter where I: ExactSizeIterator {} - -#[cfg(test)] -mod test { - use super::SampleRateConverter; - use crate::common::{ChannelCount, SampleRate}; - 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: SampleRate, to: SampleRate, channels: ChannelCount) -> TestResult { - if from.get() > 384_000*2 || to.get() > 384_000*2 || channels.get() > 128 - { - return TestResult::discard(); - } - - let input: Vec = Vec::new(); - let output = - SampleRateConverter::new(input.into_iter(), from, to, channels) - .collect::>(); - - assert_eq!(output, []); - TestResult::passed() - } - - /// Check that resampling to the same rate does not change the signal. - 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 = - SampleRateConverter::new(input.clone().into_iter(), from, from, channels) - .collect::>(); - - TestResult::from_bool(input == output) - } - - /// Check that dividing the sample rate by k (integer) is the same as - /// dropping a sample from each channel. - 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.get() * k as u32; - - // Truncate the input, so it contains an integer number of spans. - let input = { - 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(), SampleRate::new(from).expect("to is nonzero and k is nonzero"), to, channels) - .collect::>(); - - 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: 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.get() * k as u32; - - // Truncate the input, so it contains an integer number of spans. - let input = { - 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, SampleRate::new(to).unwrap(), channels) - .collect::>(); - - TestResult::from_bool(input == - 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. - /// Ignored, pending a bug fix. - fn preserve_durations(d: Duration, freq: f32, to: SampleRate) -> TestResult { - 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, nz!(1)); - let duration = - 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)) - } - } - - #[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(), 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::>(); - assert_eq!( - output, - [2.0, 16.0, 3.0, 17.0, 4.0, 18.0, 6.0, 20.0, 7.0, 21.0, 8.0, 22.0] - ); - } - - #[test] - fn upsample2() { - let input = vec![1.0, 14.0]; - 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]); - assert!((size_estimation as f32 / output.len() as f32).abs() < 2.0); - } - - #[test] - fn downsample() { - let input = Vec::from_iter((0..17).map(|x| x as Sample)); - 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]); - assert!((size_estimation as f32 / output.len() as f32).abs() < 2.0); - } -} diff --git a/src/conversions/sample_rate/buffer.rs b/src/conversions/sample_rate/buffer.rs new file mode 100644 index 000000000..7e39be47c --- /dev/null +++ b/src/conversions/sample_rate/buffer.rs @@ -0,0 +1,90 @@ +//! Fixed-capacity sample buffer with a read cursor. +//! +//! Holds one chunk of resampled output. Callers reset it with the number of freshly written +//! samples, optionally skip delay samples at the head, optionally cap it to trim filter +//! artifacts at the tail, and then drain it sample-by-sample. + +use std::fmt; + +use crate::Sample; +use dasp_sample::Sample as _; + +/// Fixed-capacity sample buffer with a read cursor. +pub struct Buffer { + data: Box<[Sample]>, + pos: usize, + len: usize, +} + +impl fmt::Debug for Buffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Buffer") + .field("capacity", &self.data.len()) + .field("pos", &self.pos) + .field("len", &self.len) + .finish() + } +} + +impl Buffer { + /// Create a new buffer with the given capacity, initialized to equilibrium samples. + pub fn new(capacity: usize) -> Self { + Self { + data: vec![Sample::EQUILIBRIUM; capacity].into_boxed_slice(), + pos: 0, + len: 0, + } + } + + /// Reset for a new fill: rewind cursor to 0 and record the number of valid samples. + pub fn reset(&mut self, filled: usize) { + self.pos = 0; + self.len = filled; + } + + /// Advance the cursor by `n` samples (capped at `len`). + pub fn skip(&mut self, n: usize) { + self.pos = (self.pos + n).min(self.len); + } + + /// Shrink `len` so at most `remaining` more samples will be returned from the cursor. + pub fn cap_to_remaining(&mut self, remaining: usize) { + self.len = self.len.min(self.pos + remaining); + } + + /// True when the cursor has reached the end of the valid data. + #[inline] + pub fn is_empty(&self) -> bool { + self.pos >= self.len + } + + /// Read the next sample and advance the cursor. Panics in debug if the buffer is empty. + #[inline] + pub fn read(&mut self) -> Sample { + debug_assert!(!self.is_empty(), "read from empty Buffer"); + let s = self.data[self.pos]; + self.pos += 1; + s + } + + /// Total capacity of the backing allocation. + pub fn capacity(&self) -> usize { + self.data.len() + } + + /// Number of valid samples set by the last `reset` call. + pub fn len(&self) -> usize { + self.len + } + + /// Number of samples remaining before the cursor reaches the end. + #[inline] + pub fn remaining(&self) -> usize { + self.len - self.pos + } + + /// Full backing slice for writing via an audio adapter. + pub fn as_mut_slice(&mut self) -> &mut [Sample] { + &mut self.data + } +} diff --git a/src/conversions/sample_rate/builder.rs b/src/conversions/sample_rate/builder.rs new file mode 100644 index 000000000..ae3613424 --- /dev/null +++ b/src/conversions/sample_rate/builder.rs @@ -0,0 +1,488 @@ +//! Configuration types and builders for resampling. + +use std::num::NonZero; + +use crate::Float; + +const DEFAULT_CHUNK_SIZE: usize = 1024; +#[cfg(feature = "rubato-fft")] +const DEFAULT_SUB_CHUNKS: usize = 1; + +/// Polynomial interpolation degree, no anti-aliasing. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Poly { + /// Zero-order hold - nearest neighbor sampling. + /// + /// Simply picks the nearest input sample without interpolation. + /// Creates a "stepped" waveform. + Nearest, + + /// Linear interpolation between 2 samples. + #[default] + Linear, + + /// Cubic interpolation using 4 samples. + Cubic, + + /// Quintic interpolation using 6 samples. + Quintic, + + /// Septic interpolation using 8 samples. + Septic, +} + +impl From for rubato::PolynomialDegree { + fn from(poly: Poly) -> Self { + match poly { + Poly::Nearest => rubato::PolynomialDegree::Nearest, + Poly::Linear => rubato::PolynomialDegree::Linear, + Poly::Cubic => rubato::PolynomialDegree::Cubic, + Poly::Quintic => rubato::PolynomialDegree::Quintic, + Poly::Septic => rubato::PolynomialDegree::Septic, + } + } +} + +/// Sinc interpolation type. +/// +/// Controls how intermediate values are calculated between precomputed sinc points +/// in the windowed sinc filter. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Sinc { + /// No interpolation - picks nearest intermediate point. + /// + /// Optimal when upsampling by exact ratios (e.g., 48kHz and 96kHz) and the oversampling factor + /// is equal to the ratio. In these cases, no unnecessary computations are performed and the + /// result is equivalent to that of synchronous resampling. + Nearest, + + /// Linear interpolation between two adjacent sinc filter coefficients. + /// + /// To resample to a fractional position, Rubato looks up the nearest two entries in the + /// precomputed sinc coefficient table and draws a straight line between them. Because the sinc + /// function is curved, this straight-line segment requires more intermediate points to push + /// the resampling artefacts below the noise floor. This is achieved using a higher + /// [`oversampling_factor`](SincConfigBuilder::oversampling_factor). [`Cubic`](Sinc::Cubic) + /// fits a polynomial that follows the curvature, achieving the same accuracy with a smaller + /// table. + #[default] + Linear, + + /// Quadratic interpolation using three nearest points. + /// + /// The computation time lies approximately halfway between that of linear and cubic + /// interpolation. + Quadratic, + + /// Cubic interpolation using four nearest points. + /// + /// The computation time is approximately twice as long as that of linear interpolation, but it + /// requires much fewer intermediate points for a good result. + Cubic, +} + +impl From for rubato::SincInterpolationType { + fn from(sinc: Sinc) -> Self { + match sinc { + Sinc::Nearest => rubato::SincInterpolationType::Nearest, + Sinc::Linear => rubato::SincInterpolationType::Linear, + Sinc::Quadratic => rubato::SincInterpolationType::Quadratic, + Sinc::Cubic => rubato::SincInterpolationType::Cubic, + } + } +} + +/// Window functions for sinc filter. +/// +/// The window function is applied to the sinc filter to reduce ripple artifacts and control the +/// trade-off between transition bandwidth and stopband attenuation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum WindowFunction { + /// Hann window: ~44 dB stopband attenuation, fast -18 dB/octave rolloff. + /// + /// Good transition band but moderate rejection. Suitable for less critical applications. + Hann, + + /// Squared Hann: ~50 dB stopband attenuation, medium -12 dB/octave rolloff. + /// + /// Better rejection than Hann with slightly wider transition band. + Hann2, + + /// Blackman window: ~75 dB stopband attenuation, fast -18 dB/octave rolloff. + /// + /// Excellent rejection with sharp cutoff. + Blackman, + + /// Squared Blackman: ~81 dB stopband attenuation, medium -12 dB/octave rolloff. + /// + /// Very good rejection with moderate transition band. + Blackman2, + + /// Blackman-Harris window: ~92 dB stopband attenuation, slow -6 dB/octave rolloff. + /// + /// Extremely high rejection but wider transition band. + BlackmanHarris, + + /// Squared Blackman-Harris: ~98 dB stopband attenuation, very slow -3 dB/octave rolloff. + /// + /// Maximum stopband rejection, widest transition band. + #[default] + BlackmanHarris2, +} + +impl From for rubato::WindowFunction { + fn from(window: WindowFunction) -> Self { + match window { + WindowFunction::Hann => rubato::WindowFunction::Hann, + WindowFunction::Hann2 => rubato::WindowFunction::Hann2, + WindowFunction::Blackman => rubato::WindowFunction::Blackman, + WindowFunction::Blackman2 => rubato::WindowFunction::Blackman2, + WindowFunction::BlackmanHarris => rubato::WindowFunction::BlackmanHarris, + WindowFunction::BlackmanHarris2 => rubato::WindowFunction::BlackmanHarris2, + } + } +} + +/// Builder for polynomial resampling configuration without anti-aliasing. +#[derive(Debug, Clone)] +pub struct PolyConfigBuilder { + degree: Poly, + chunk_size: usize, +} + +impl Default for PolyConfigBuilder { + fn default() -> Self { + Self { + degree: Poly::default(), + chunk_size: DEFAULT_CHUNK_SIZE, + } + } +} + +/// Builder for sinc resampling configuration with anti-aliasing. +#[derive(Debug, Clone)] +pub struct SincConfigBuilder { + sinc_len: usize, + oversampling_factor: usize, + interpolation: Sinc, + window: WindowFunction, + f_cutoff: Float, + chunk_size: usize, + #[cfg(feature = "rubato-fft")] + #[cfg_attr(docsrs, doc(cfg(feature = "rubato-fft")))] + sub_chunks: usize, +} + +impl Default for SincConfigBuilder { + fn default() -> Self { + Self { + sinc_len: 256, + window: WindowFunction::default(), + oversampling_factor: 128, + interpolation: Sinc::default(), + f_cutoff: 0.95, + chunk_size: DEFAULT_CHUNK_SIZE, + #[cfg(feature = "rubato-fft")] + sub_chunks: DEFAULT_SUB_CHUNKS, + } + } +} + +/// Resampling configuration. +/// +/// Specifies the algorithm and parameters for sample rate conversion. +/// +/// # Examples +/// +/// ```rust +/// use rodio::math::nz; +/// use rodio::conversions::{Poly, ResampleConfig}; +/// +/// // Use presets +/// let config = ResampleConfig::balanced(); +/// let config = ResampleConfig::fast(); +/// let config = ResampleConfig::accurate(); +/// +/// // Customize from builder +/// let config = ResampleConfig::sinc().chunk_size(nz!(512)); +/// let config = ResampleConfig::poly().degree(Poly::Cubic); +/// ``` +#[derive(Debug, Clone)] +pub enum ResampleConfig { + /// Polynomial resampling (fast, no anti-aliasing) + Poly { + /// Polynomial degree + degree: Poly, + /// Desired chunk size in frames + chunk_size: usize, + }, + /// Sinc resampling (high quality, anti-aliasing) + Sinc { + /// Length of the windowed sinc interpolation filter + sinc_len: usize, + /// Number of entries per tap in the precomputed sinc filter lookup table. + /// + /// See [`SincConfigBuilder::oversampling_factor`] for details. + oversampling_factor: usize, + /// Interpolation type for filter table lookup + interpolation: Sinc, + /// Window function to use + window: WindowFunction, + /// Cutoff frequency of the sinc interpolation filter relative to Nyquist (0.0-1.0) + f_cutoff: Float, + /// Desired chunk size in frames + chunk_size: usize, + /// Desired number of sub chunks to use for processing + #[cfg(feature = "rubato-fft")] + #[cfg_attr(docsrs, doc(cfg(feature = "rubato-fft")))] + sub_chunks: usize, + }, +} + +impl ResampleConfig { + /// Create a very fast sinc resampling configuration. + pub fn very_fast() -> Self { + let sinc_len = 64; + let window = WindowFunction::Hann2; + Self::Sinc { + sinc_len, + window, + oversampling_factor: 1024, + interpolation: Sinc::Linear, + f_cutoff: rubato::calculate_cutoff(sinc_len, window.into()), + chunk_size: DEFAULT_CHUNK_SIZE, + #[cfg(feature = "rubato-fft")] + sub_chunks: DEFAULT_SUB_CHUNKS, + } + } + + /// Create a fast sinc resampling configuration. + pub fn fast() -> Self { + let sinc_len = 128; + let window = WindowFunction::Blackman2; + Self::Sinc { + sinc_len, + window, + oversampling_factor: 1024, + interpolation: Sinc::Linear, + f_cutoff: rubato::calculate_cutoff(sinc_len, window.into()), + chunk_size: DEFAULT_CHUNK_SIZE, + #[cfg(feature = "rubato-fft")] + sub_chunks: DEFAULT_SUB_CHUNKS, + } + } + + /// Create a balanced sinc resampling configuration. + pub fn balanced() -> Self { + let sinc_len = 192; + let window = WindowFunction::BlackmanHarris2; + Self::Sinc { + sinc_len, + window, + oversampling_factor: 512, + interpolation: Sinc::Quadratic, + f_cutoff: rubato::calculate_cutoff(sinc_len, window.into()), + chunk_size: DEFAULT_CHUNK_SIZE, + #[cfg(feature = "rubato-fft")] + sub_chunks: DEFAULT_SUB_CHUNKS, + } + } + + /// Create an accurate sinc resampling configuration. + pub fn accurate() -> Self { + let sinc_len = 256; + let window = WindowFunction::BlackmanHarris2; + Self::Sinc { + sinc_len, + window, + oversampling_factor: 256, + interpolation: Sinc::Cubic, + f_cutoff: rubato::calculate_cutoff(sinc_len, window.into()), + chunk_size: DEFAULT_CHUNK_SIZE, + #[cfg(feature = "rubato-fft")] + sub_chunks: DEFAULT_SUB_CHUNKS, + } + } + + /// Nearest-neighbor (zero-order hold) polynomial resampling. Fastest, no anti-aliasing. + pub fn nearest() -> Self { + Self::poly().degree(Poly::Nearest).build() + } + + /// Linear polynomial resampling. Fast, no anti-aliasing. + pub fn linear() -> Self { + Self::poly().degree(Poly::Linear).build() + } + + /// Cubic polynomial resampling. Smoother than linear, no anti-aliasing. + pub fn cubic() -> Self { + Self::poly().degree(Poly::Cubic).build() + } + + /// Quintic polynomial resampling. Smoother than cubic, no anti-aliasing. + pub fn quintic() -> Self { + Self::poly().degree(Poly::Quintic).build() + } + + /// Septic polynomial resampling. Highest polynomial quality, no anti-aliasing. + pub fn septic() -> Self { + Self::poly().degree(Poly::Septic).build() + } + + /// Create a polynomial resampling configuration builder. + pub fn poly() -> PolyConfigBuilder { + PolyConfigBuilder::default() + } + + /// Create a sinc resampling configuration builder. + pub fn sinc() -> SincConfigBuilder { + SincConfigBuilder::default() + } +} + +impl Default for ResampleConfig { + fn default() -> Self { + Self::balanced() + } +} + +impl PolyConfigBuilder { + /// Set the polynomial degree for interpolation. + pub fn degree(mut self, degree: Poly) -> Self { + self.degree = degree; + self + } + + /// Set number of audio frames processed at once (typical range: 32-2048). + /// + /// Smaller chunks reduce latency (time delay through the resampler) but increase per-sample + /// overhead. One frame contains one sample per channel. Default is 1024 frames, which at 48 + /// kHz is ~10.7ms latency. + pub fn chunk_size(mut self, size: NonZero) -> Self { + self.chunk_size = size.get(); + self + } + + /// Build the final [`ResampleConfig`]. + pub fn build(self) -> ResampleConfig { + ResampleConfig::Poly { + degree: self.degree, + chunk_size: self.chunk_size, + } + } +} + +impl From for ResampleConfig { + fn from(builder: PolyConfigBuilder) -> Self { + builder.build() + } +} + +impl SincConfigBuilder { + /// Set the length of the sinc filter in taps (typical range: 32-2048). + /// + /// Longer filters provide better quality but use more CPU. + pub fn sinc_len(mut self, len: NonZero) -> Self { + self.sinc_len = len.get(); + self + } + + /// Set the number of entries per tap in the precomputed sinc filter lookup table + /// (typical range: 64-4096). + /// + /// A higher value packs more entries into the table, so adjacent entries are closer together + /// and interpolation between them incurs less error. This matters most with + /// [`Sinc::Linear`] and [`Sinc::Quadratic`]; [`Sinc::Cubic`] achieves comparable accuracy + /// with a smaller table. Increases memory usage proportionally. + pub fn oversampling_factor(mut self, factor: NonZero) -> Self { + self.oversampling_factor = factor.get(); + self + } + + /// Set interpolation type. + pub fn interpolation(mut self, interpolator: Sinc) -> Self { + self.interpolation = interpolator; + self + } + + /// Set window function. + pub fn window(mut self, window: WindowFunction) -> Self { + self.window = window; + self + } + + /// Set the cutoff frequency as fraction of the Nyquist frequency. + /// + /// Value should be between 0.0 and 1.0, where 1.0 represents the Nyquist frequency (half the + /// sample rate) of the input sampling rate or output sampling rate, whichever is lower. The + /// cutoff determines where the anti-aliasing filter begins to attenuate frequencies. + /// + /// Lower values provide more anti-aliasing protection but reduce high frequency response. + /// + /// # Panics + /// + /// Panics if cutoff is not in range 0.0-1.0. + pub fn f_cutoff(mut self, cutoff: Float) -> Self { + assert!( + (0.0..=1.0).contains(&cutoff), + "f_cutoff must be between 0.0 and 1.0" + ); + self.f_cutoff = cutoff; + self + } + + /// Set the length of the sinc filter, the window function, automatically calculating + /// the cutoff frequency for the combination of the two. + pub fn with_sinc_and_window( + mut self, + sinc_len: NonZero, + window: WindowFunction, + ) -> Self { + self.sinc_len = sinc_len.get(); + self.window = window; + self.f_cutoff = rubato::calculate_cutoff(sinc_len.get(), window.into()); + self + } + + /// Set chunk size for processing (typical range: 512-4096). + /// + /// This balances between efficiency and memory usage. If the device sink uses a fixed buffer + /// size, then this number of frames is a good choice for the resampler chunk size. + pub fn chunk_size(mut self, size: NonZero) -> Self { + self.chunk_size = size.get(); + self + } + + /// Set number of sub-chunks for FFT resampling. + /// + /// The delay of the resampler can be reduced by increasing the number of sub-chunks. A large + /// number of sub-chunks reduces the cutoff frequency of the anti-aliasing filter. It is + /// recommended to set keep this at 1 unless this leads to an unacceptably large delay. + #[cfg(feature = "rubato-fft")] + #[cfg_attr(docsrs, doc(cfg(feature = "rubato-fft")))] + pub fn sub_chunks(mut self, count: NonZero) -> Self { + self.sub_chunks = count.get(); + self + } + + /// Build the final [`ResampleConfig`]. + pub fn build(self) -> ResampleConfig { + ResampleConfig::Sinc { + sinc_len: self.sinc_len, + oversampling_factor: self.oversampling_factor, + interpolation: self.interpolation, + window: self.window, + f_cutoff: self.f_cutoff, + chunk_size: self.chunk_size, + #[cfg(feature = "rubato-fft")] + #[cfg_attr(docsrs, doc(cfg(feature = "rubato-fft")))] + sub_chunks: self.sub_chunks, + } + } +} + +impl From for ResampleConfig { + fn from(builder: SincConfigBuilder) -> Self { + builder.build() + } +} diff --git a/src/conversions/sample_rate/mod.rs b/src/conversions/sample_rate/mod.rs new file mode 100644 index 000000000..12247f19a --- /dev/null +++ b/src/conversions/sample_rate/mod.rs @@ -0,0 +1,915 @@ +//! Audio resampling from one sample rate to another. +//! +//! # Quick Start +//! +//! Use the [`Source::resample`] method with a quality preset: +//! +//! ```rust +//! use rodio::SampleRate; +//! use rodio::source::{SineWave, Source, ResampleConfig}; +//! +//! let source = SineWave::new(440.0); +//! let config = ResampleConfig::balanced(); +//! let resampled = source.resample(SampleRate::new(96000).unwrap(), config); +//! ``` +//! +//! For advanced control, use [`SampleRateConverter`] directly: +//! +//! ```rust +//! use rodio::math::nz; +//! use rodio::source::{SineWave, Source, ResampleConfig}; +//! use rodio::conversions::{SampleRateConverter, Sinc, WindowFunction}; +//! +//! let source = SineWave::new(440.0); +//! let config = ResampleConfig::sinc() // Sinc resampling +//! .sinc_len(nz!(256)) // 256-tap filter +//! .interpolation(Sinc::Cubic) // Cubic interpolation +//! .window(WindowFunction::BlackmanHarris2) // Squared Blackman-Harris window +//! .chunk_size(nz!(512)) // Low latency (5.3 ms @ 1-channel 96 kHz) +//! .build(); +//! let resampled = SampleRateConverter::new(source, nz!(96000), config); +//! ``` +//! +//! # Understanding Resampling +//! +//! ## Polynomial vs. Sinc Interpolation +//! +//! When converting between sample rates, sample values at positions that don't exist in the +//! original signal need to be calculated. There are two main approaches: +//! +//! **Polynomial interpolation** is fast but does not include anti-aliasing. This can cause +//! artifacts in the output audio. Higher degrees provide smoother interpolation but cannot +//! prevent these artifacts. +//! +//! **Sinc interpolation** uses a windowed sinc function for mathematically correct reconstruction. +//! It is of higher quality and includes anti-aliasing to reduce artifacts, but is more +//! computationally expensive. +//! +//! ## Fixed vs Arbitrary Ratios +//! +//! A **fixed ratio** is when the sample rate conversion can be expressed as a simple fraction, +//! like 1:2 (e.g., 48 kHz and 96 kHz) or 147:160 (e.g., 44.1 kHz and 48 kHz). +//! +//! When the resampler is configured for sinc interpolation, it automatically detects these ratios +//! and optimizes resampling by switching to: +//! 1. optimized FFT-based processing when the `rubato-fft` feature is enabled +//! 2. sinc interpolation with nearest-neighbor lookup when FFT is not available +//! +//! This reduces CPU usage while providing highest quality. +//! +//! **Arbitrary ratios** (non-reducible or large fractions) use the async sinc resampler, which +//! can handle any conversion. This is CPU intensive and should be compiled with release profile to +//! prevent choppy audio. +//! +//! # Quality Presets +//! +//! As per [`CamillaDSP`](https://henquist.github.io/3.0.x/): +//! +//! | Parameter | [`VeryFast`](ResampleConfig::very_fast) | [`Fast`](ResampleConfig::fast) | [`Balanced`](ResampleConfig::balanced) | [`Accurate`](ResampleConfig::accurate) | +//! | sinc_len | 64 | 128 | 192 | 256 | +//! | oversampling_factor | 1024 | 1024 | 512 | 256 | +//! | interpolation | Linear | Linear | Quadratic | Cubic | +//! | window | Hann2 | Blackman2 | BlackmanHarris2 | BlackmanHarris2 | +//! | f_cutoff (#) | 0.91 | 0.92 | 0.93 | 0.95 | +//! (#) These cutoff values are approximate. The actual values used are calculated automatically at runtime for the combination of sinc length and window. + +#![cfg_attr(docsrs, feature(doc_cfg))] + +use std::time::Duration; + +use crate::source::{reset_seek_span_tracking, SeekError}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + math::gcd, + Float, Source, +}; + +mod buffer; +mod builder; +mod rubato; + +#[cfg(feature = "rubato-fft")] +use rubato::RubatoFftResample; +use rubato::{ResampleInner, RubatoAsyncResample}; + +pub use builder::{ + Poly, PolyConfigBuilder, ResampleConfig, Sinc, SincConfigBuilder, WindowFunction, +}; + +/// Maximum for optimized fixed-ratio resampling: 44.1 and 384 kHz (147:1280). +const MAX_FIXED_RATIO: u32 = 1280; + +/// Resamples an audio source to a target sample rate using Rubato. +#[derive(Debug)] +pub struct SampleRateConverter +where + I: Source, +{ + // Kept in Option so we can take ownership for in-place recreation on parameter change + inner: Option>, + target_rate: SampleRate, + config: ResampleConfig, + cached_input_span_len: Option, + // True when a format change was detected at a span boundary but the output buffer still + // has samples from the old format. Recreation is deferred until the buffer is drained so + // fill_input_buffer never reads the next span's samples with the wrong channel count. + pending_recreate: bool, +} + +impl Clone for SampleRateConverter +where + I: Source + Clone, +{ + fn clone(&self) -> Self { + // Shallow clone: this resets filter state + let source = self.inner().clone(); + SampleRateConverter::new(source, self.target_rate, self.config.clone()) + } +} + +impl SampleRateConverter +where + I: Source, +{ + /// Create a new resampler with the given configuration. + pub fn new(source: I, target_rate: SampleRate, config: ResampleConfig) -> Self { + let inner = Self::create_resampler(source, target_rate, &config); + + #[cfg(debug_assertions)] + if matches!(inner, ResampleInner::Sinc(_)) { + eprintln!( + "Warning: async sinc resampling is active. This is CPU-intensive and may \ + produce choppy audio in a debug build. Either use an integer-multiple ratio \ + or compile with --release." + ); + } + + let cached_input_span_len = match &inner { + ResampleInner::Passthrough { .. } => inner.input().current_span_len(), + ResampleInner::Poly(resampler) => resampler.input.current_span_len(), + ResampleInner::Sinc(resampler) => resampler.input.current_span_len(), + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.input.current_span_len(), + }; + + Self { + inner: Some(inner), + target_rate, + config, + cached_input_span_len, + pending_recreate: false, + } + } + + /// Helper method to create a resampler from a source using the stored config and target rate. + fn create_resampler( + source: I, + target_rate: SampleRate, + config: &ResampleConfig, + ) -> ResampleInner { + let source_rate = source.sample_rate(); + + if source.is_exhausted() || source_rate == target_rate { + let channels = source.channels(); + ResampleInner::Passthrough { + source, + input_span_pos: 0, + channels, + source_rate, + } + } else { + match config { + ResampleConfig::Poly { degree, chunk_size } => { + let resampler = + RubatoAsyncResample::new_poly(source, target_rate, *chunk_size, *degree) + .expect("Failed to create polynomial resampler"); + ResampleInner::Poly(resampler) + } + #[cfg(feature = "rubato-fft")] + ResampleConfig::Sinc { + sinc_len, + oversampling_factor, + interpolation, + window, + f_cutoff, + chunk_size, + sub_chunks, + } => { + let g = gcd(target_rate.get(), source_rate.get()); + let numer = target_rate.get() / g; + let denom = source_rate.get() / g; + if numer <= MAX_FIXED_RATIO && denom <= MAX_FIXED_RATIO { + // Use FFT resampler for optimal performance + let resampler = + RubatoFftResample::new(source, target_rate, *chunk_size, *sub_chunks) + .expect("Failed to create FFT resampler"); + ResampleInner::Fft(resampler) + } else { + let resampler = RubatoAsyncResample::new_sinc( + source, + target_rate, + *chunk_size, + *sinc_len, + *f_cutoff, + *oversampling_factor, + *interpolation, + *window, + ) + .expect("Failed to create sinc resampler"); + ResampleInner::Sinc(resampler) + } + } + #[cfg(not(feature = "rubato-fft"))] + ResampleConfig::Sinc { + sinc_len, + oversampling_factor, + interpolation, + window, + f_cutoff, + chunk_size, + } => { + let g = gcd(target_rate.get(), source_rate.get()); + let numer = target_rate.get() / g; + let denom = source_rate.get() / g; + if numer <= MAX_FIXED_RATIO && denom <= MAX_FIXED_RATIO { + // Fixed ratio without FFT - use Sinc::Nearest optimization + // Set oversampling_factor to match the ratio for optimal performance + let ratio = numer.max(denom) as usize; + let resampler = RubatoAsyncResample::new_sinc( + source, + target_rate, + *chunk_size, + *sinc_len, + *f_cutoff, + ratio, + Sinc::Nearest, + *window, + ) + .expect("Failed to create optimized sinc resampler"); + ResampleInner::Sinc(resampler) + } else { + let resampler = RubatoAsyncResample::new_sinc( + source, + target_rate, + *chunk_size, + *sinc_len, + *f_cutoff, + *oversampling_factor, + *interpolation, + *window, + ) + .expect("Failed to create sinc resampler"); + ResampleInner::Sinc(resampler) + } + } + } + } + } + + #[inline] + fn resampler(&self) -> &ResampleInner { + self.inner.as_ref().unwrap() + } + + #[inline] + fn resampler_mut(&mut self) -> &mut ResampleInner { + self.inner.as_mut().unwrap() + } + + /// Returns a reference to the inner source. + #[inline] + pub fn inner(&self) -> &I { + self.resampler().input() + } + + /// Returns a mutable reference to the inner source. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + match self.resampler_mut() { + ResampleInner::Passthrough { source, .. } => source, + ResampleInner::Poly(resampler) => &mut resampler.input, + ResampleInner::Sinc(resampler) => &mut resampler.input, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => &mut resampler.input, + } + } + + /// Returns the inner source. + #[inline] + pub fn into_inner(self) -> I { + self.inner.unwrap().into_inner() + } + + /// Returns `(at_boundary, parameters_changed)` given span tracking state. + /// + /// Two modes: + /// - Counting (`cached_span_len` is `Some`): boundary when `samples_consumed >= span_len` + /// - Detection (`cached_span_len` is `None`): boundary when parameters change (post-seek) + fn detect_boundary( + cached_span_len: Option, + samples_consumed: usize, + current_channels: ChannelCount, + expected_channels: ChannelCount, + current_rate: SampleRate, + expected_rate: SampleRate, + ) -> (bool, bool) { + let known_boundary = cached_span_len.map(|len| samples_consumed >= len); + // In counting mode: only check parameters at boundary + // In detection mode: check parameters at every sample until a boundary is detected + let parameters_changed = if known_boundary.is_none_or(|at| at) { + current_channels != expected_channels || current_rate != expected_rate + } else { + false + }; + ( + known_boundary.unwrap_or(parameters_changed), + parameters_changed, + ) + } +} + +impl Source for SampleRateConverter +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + let (input_span_len, input_sample_rate, input_exhausted, output_has_samples, output_len) = + match self.resampler() { + ResampleInner::Passthrough { source, .. } => return source.current_span_len(), + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => ( + resampler.input.current_span_len(), + resampler.input.sample_rate(), + resampler.input.is_exhausted(), + resampler.output_has_samples(), + resampler.output_span_len(), + ), + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => ( + resampler.input.current_span_len(), + resampler.input.sample_rate(), + resampler.input.is_exhausted(), + resampler.output_has_samples(), + resampler.output_span_len(), + ), + }; + + let g = gcd(self.sample_rate().get(), input_sample_rate.get()); + let numer = self.sample_rate().get() / g; + let denom = input_sample_rate.get() / g; + if denom == 1 { + // Integer upsampling (2x, 3x, etc.) - always exact and frame-aligned + input_span_len.map(|len| numer as usize * len) + } else { + // When the ratio contains a fraction, we cannot choose the floor or ceiling + // arbitrarily, because the resampler may produce either based on its internal state + if output_has_samples { + // Running state: we are iterating over our buffer with resampled samples + Some(output_len) + } else if input_exhausted { + // End state: we are at the end of our buffer and the source is exhausted + Some(0) + } else { + // Initial state: buffer is empty, actual output count is unknown until the first + // process_into_buffer call. Return one frame so consumers recheck promptly. + Some(self.channels().get() as usize) + } + } + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.target_rate + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.resampler().input().channels() + } + + #[inline] + fn total_duration(&self) -> Option { + self.resampler().input().total_duration() + } + + #[inline] + fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { + match self.resampler_mut() { + ResampleInner::Passthrough { source, .. } => source.try_seek(position)?, + ResampleInner::Poly(r) | ResampleInner::Sinc(r) => { + r.input.try_seek(position)?; + r.reset(); + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(r) => { + r.input.try_seek(position)?; + r.reset(); + } + } + + self.pending_recreate = false; + let input_span_len = self.resampler().input().current_span_len(); + + match self.inner.as_mut().unwrap() { + ResampleInner::Passthrough { + input_span_pos: input_samples_consumed, + .. + } => { + reset_seek_span_tracking( + input_samples_consumed, + &mut self.cached_input_span_len, + position, + input_span_len, + ); + } + ResampleInner::Poly(r) | ResampleInner::Sinc(r) => { + reset_seek_span_tracking( + &mut r.input_samples_consumed, + &mut self.cached_input_span_len, + position, + input_span_len, + ); + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(r) => { + reset_seek_span_tracking( + &mut r.input_samples_consumed, + &mut self.cached_input_span_len, + position, + input_span_len, + ); + } + } + + Ok(()) + } +} + +impl Iterator for SampleRateConverter +where + I: Source, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + // If a format change was detected at the previous span boundary, wait until the + // output buffer is fully drained before recreating the resampler. This guarantees + // that fill_input_buffer only ever reads from the current span. + if self.pending_recreate { + let output_empty = match self.resampler() { + ResampleInner::Passthrough { .. } => true, + ResampleInner::Poly(r) | ResampleInner::Sinc(r) => !r.output_has_samples(), + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(r) => !r.output_has_samples(), + }; + if output_empty { + let source = self.inner.take().unwrap().into_inner(); + self.inner = Some(Self::create_resampler( + source, + self.target_rate, + &self.config, + )); + self.pending_recreate = false; + } + } + + let cached = self.cached_input_span_len; + let sample = match self.resampler_mut() { + ResampleInner::Passthrough { source, .. } => source.next()?, + ResampleInner::Poly(resampler) => resampler.next_sample(cached)?, + ResampleInner::Sinc(resampler) => resampler.next_sample(cached)?, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.next_sample(cached)?, + }; + + // If input reports no span length, parameters are stable by contract + let input_span_len = self.resampler().input().current_span_len(); + if input_span_len.is_none() { + return Some(sample); + } + + let (expected_channels, expected_rate, samples_consumed) = match self.resampler_mut() { + ResampleInner::Passthrough { + input_span_pos: input_samples_consumed, + channels, + source_rate, + .. + } => { + *input_samples_consumed += 1; + (*channels, *source_rate, *input_samples_consumed) + } + ResampleInner::Poly(r) | ResampleInner::Sinc(r) => { + (r.channels, r.source_rate, r.input_samples_consumed) + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(r) => (r.channels, r.source_rate, r.input_samples_consumed), + }; + + let input = self.resampler().input(); + let (at_boundary, parameters_changed) = Self::detect_boundary( + self.cached_input_span_len, + samples_consumed, + input.channels(), + expected_channels, + input.sample_rate(), + expected_rate, + ); + + if at_boundary { + // Update cached span length (exits detection mode if we were in it) + self.cached_input_span_len = input_span_len; + + if parameters_changed { + // Defer recreation until the output buffer is drained (handled above at the + // top of the next next() call) so no cross-span reads occur. + self.pending_recreate = true; + } else { + // Just crossed boundary without parameter change, reset counter + match self.resampler_mut() { + ResampleInner::Passthrough { + input_span_pos: input_samples_consumed, + .. + } => { + *input_samples_consumed = 0; + } + ResampleInner::Poly(r) | ResampleInner::Sinc(r) => { + r.input_samples_consumed = 0; + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(r) => { + r.input_samples_consumed = 0; + } + } + } + } + + Some(sample) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (input_hint, source_rate, buffered_remaining) = match self.resampler() { + ResampleInner::Passthrough { source, .. } => return source.size_hint(), + ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => { + let input_hint = resampler.input.size_hint(); + let buffered_remaining = resampler.output_remaining(); + (input_hint, resampler.source_rate, buffered_remaining) + } + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => { + let input_hint = resampler.input.size_hint(); + let buffered_remaining = resampler.output_remaining(); + (input_hint, resampler.source_rate, buffered_remaining) + } + }; + + let (input_lower, input_upper) = input_hint; + let ratio = self.target_rate.get() as Float / source_rate.get() as Float; + + let lower = buffered_remaining + (input_lower as Float * ratio).ceil() as usize; + let upper = + input_upper.map(|upper| buffered_remaining + (upper as Float * ratio).ceil() as usize); + + (lower, upper) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::{from_iter, SineWave}; + use crate::Source; + use dasp_sample::ToSample; + use quickcheck::{quickcheck, Arbitrary, Gen, TestResult}; + use std::num::NonZero; + + #[derive(Debug, Clone, Copy)] + struct TestSampleRate(SampleRate); + + impl Arbitrary for TestSampleRate { + fn arbitrary(g: &mut Gen) -> Self { + // Generate realistic sample rates: 8 kHz to 384 kHz + let rate = u32::arbitrary(g) % 376_001 + 8_000; + TestSampleRate(SampleRate::new(rate).unwrap()) + } + } + + impl std::ops::Deref for TestSampleRate { + type Target = SampleRate; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + #[derive(Debug, Clone, Copy)] + struct TestChannelCount(ChannelCount); + + impl Arbitrary for TestChannelCount { + fn arbitrary(g: &mut Gen) -> Self { + // Generate realistic channel counts: 1 to 8 + let channels = (u16::arbitrary(g) % 7) + 1; + TestChannelCount(ChannelCount::new(channels).unwrap()) + } + } + + impl std::ops::Deref for TestChannelCount { + type Target = ChannelCount; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + struct TestSpan { + samples: Vec, + rate: SampleRate, + channels: ChannelCount, + } + + /// Multi-span test source. + /// + /// Build with [`TestSource::new`] and extend with [`.chain()`](TestSource::chain). + struct TestSource { + spans: Vec, + span: usize, + offset: usize, + } + + impl TestSource { + fn new(samples: Vec, rate: SampleRate, channels: ChannelCount) -> Self { + Self { + spans: vec![TestSpan { + samples, + rate, + channels, + }], + span: 0, + offset: 0, + } + } + + fn chain(mut self, samples: Vec, rate: SampleRate, channels: ChannelCount) -> Self { + self.spans.push(TestSpan { + samples, + rate, + channels, + }); + self + } + + /// Returns the active span for metadata queries. + fn current_span(&self) -> &TestSpan { + self.spans + .get(self.span) + .unwrap_or_else(|| self.spans.last().unwrap()) + } + } + + impl Iterator for TestSource { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.span >= self.spans.len() { + return None; + } + let s = self.spans[self.span].samples[self.offset]; + self.offset += 1; + if self.offset >= self.spans[self.span].samples.len() { + self.span += 1; + self.offset = 0; + } + Some(s) + } + } + + impl Source for TestSource { + fn current_span_len(&self) -> Option { + Some(self.spans.get(self.span).map_or(0, |s| s.samples.len())) + } + + fn sample_rate(&self) -> SampleRate { + self.current_span().rate + } + + fn channels(&self) -> ChannelCount { + self.current_span().channels + } + + fn total_duration(&self) -> Option { + Some( + self.spans + .iter() + .map(|s| { + let frames = s.samples.len() / s.channels.get() as usize; + Duration::from_secs_f64(frames as f64 / s.rate.get() as f64) + }) + .sum(), + ) + } + + fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { + let position = self.total_duration().map_or(position, |d| position.min(d)); + let mut remaining = position.as_secs_f64(); + for (i, span) in self.spans.iter().enumerate() { + let frames = span.samples.len() / span.channels.get() as usize; + let span_dur = frames as f64 / span.rate.get() as f64; + let is_last = i + 1 == self.spans.len(); + if remaining < span_dur || is_last { + let frame_offset = (remaining * span.rate.get() as f64) as usize; + let sample_offset = + (frame_offset * span.channels.get() as usize).min(span.samples.len()); + self.span = i; + self.offset = sample_offset; + if self.offset >= span.samples.len() { + self.span += 1; + self.offset = 0; + } + return Ok(()); + } + remaining -= span_dur; + } + unreachable!("TestSource always has at least one span") + } + } + + /// Convert and truncate input to contain a frame-aligned number of samples. + fn convert_to_frames>( + input: Vec, + channels: ChannelCount, + ) -> Vec { + let mut input: Vec = input.iter().map(|x| x.to_sample()).collect(); + let frame_size = channels.get() as usize; + input.truncate(frame_size * (input.len() / frame_size)); + input + } + + quickcheck! { + /// Check that resampling an empty input produces no output. + fn empty(from: TestSampleRate, to: TestSampleRate, channels: TestChannelCount) -> bool { + let input = vec![]; + let config = ResampleConfig::default(); + let source = from_iter(input.clone().into_iter(), *channels, *from); + let output = SampleRateConverter::new(source, *to, config).collect::>(); + input == output + } + + /// Check that resampling to the same rate does not change the signal. + fn identity(from: TestSampleRate, channels: TestChannelCount, input: Vec) -> bool { + let input = convert_to_frames(input, *channels); + let config = ResampleConfig::default(); + let source = from_iter(input.clone().into_iter(), *channels, *from); + let output = SampleRateConverter::new(source, *from, config).collect::>(); + input == output + } + + /// Check that resampling does not change the audio duration, except by a negligible + /// amount (± 1ms). Reproduces #316. + fn preserve_durations(d: Duration, freq: f32, to: TestSampleRate) -> TestResult { + use crate::source::{SineWave, Source}; + if !freq.is_normal() || freq <= 0.0 || d > Duration::from_secs(1) { + return TestResult::discard(); + } + + let source = SineWave::new(freq).take_duration(d); + let from = source.sample_rate(); + + let config = ResampleConfig::poly().degree(Poly::Linear).build(); + let resampled = SampleRateConverter::new(source, *to, config); + let duration = 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)) + } + } + + /// Helper to create interleaved multi-channel test data using SineWave sources. + fn create_test_input(frames: usize, channels: u16) -> Vec { + let frequencies = [440.0, 1000.0]; + let total_samples = frames * channels as usize; + let mut input = Vec::with_capacity(total_samples); + + // Create a SineWave for each channel + let mut waves: Vec<_> = (0..channels) + .map(|ch| SineWave::new(frequencies[ch as usize % frequencies.len()])) + .collect(); + + // Interleave samples from each channel + for _ in 0..frames { + for wave in waves.iter_mut() { + input.push(wave.next().unwrap()); + } + } + input + } + + /// Test various ratio types: integer, fractional, and reciprocal. + #[test] + fn test_sample_rate_conversions() { + let test_cases = [ + // (from_rate, to_rate, channels, description) + (1000, 7000, 1, "integer upsample 7x"), + (2000, 3000, 2, "fractional upsample 1.5x"), + (12000, 2400, 1, "integer downsample 1/5x"), + (48000, 44100, 2, "fractional downsample (DVD to CD)"), + (8000, 48001, 1, "async sinc"), + ]; + + let configs: &[(&str, ResampleConfig)] = &[ + ("poly", ResampleConfig::poly().build()), + ("sinc", ResampleConfig::sinc().build()), + ]; + + for (config_name, config) in configs { + for (from_rate, to_rate, channels, desc) in test_cases { + let from = SampleRate::new(from_rate).unwrap(); + let to = SampleRate::new(to_rate).unwrap(); + let ch = ChannelCount::new(channels).unwrap(); + + let input_frames = 100; + let input = create_test_input(input_frames, channels); + let input_samples = input.len(); + + let source = from_iter(input.into_iter(), ch, from); + let resampler = SampleRateConverter::new(source, to, config.clone()); + + let size_hint_lower = resampler.size_hint().0; + let output_count = resampler.count(); + + assert_eq!( + output_count, size_hint_lower, + "[{config_name}] {desc}: size_hint {size_hint_lower} should equal actual output {output_count}", + ); + + let ratio = to.get() as f64 / from.get() as f64; + let expected_samples = (input_samples as f64 * ratio).ceil() as usize; + + assert_eq!( + output_count.abs_diff(expected_samples), 0, + "[{config_name}] {desc}: expected {expected_samples} samples, got {output_count}", + ); + } + } + } + + #[test] + fn test_current_span_len_excludes_delay() { + let channels = ChannelCount::new(1).unwrap(); + let from = SampleRate::new(44100).unwrap(); + let to = SampleRate::new(48000).unwrap(); + + let input = create_test_input(2048, 1); + let source = from_iter(input.into_iter(), channels, from); + // sinc_len=16 gives a non-zero output delay without being slow in debug builds + let config = ResampleConfig::sinc() + .sinc_len(NonZero::new(16).unwrap()) + .build(); + let mut resampler = SampleRateConverter::new(source, to, config); + + let _ = resampler.next().expect("should have samples"); + let reported = resampler + .current_span_len() + .expect("should report span len"); + + let mut count = 1; + while resampler.current_span_len() == Some(reported) { + assert!( + resampler.next().is_some(), + "source exhausted before first chunk drained" + ); + count += 1; + } + + assert_eq!( + count, reported, + "current_span_len() = {reported} but first chunk emitted {count} samples" + ); + } + + #[test] + fn test_span_boundary_same_format() { + let span_frames = 100usize; + let channels = ChannelCount::new(1).unwrap(); + let rate = SampleRate::new(44100).unwrap(); + let target = SampleRate::new(48000).unwrap(); + + let source = TestSource::new(vec![0.1; span_frames], rate, channels).chain( + vec![0.9; span_frames], + rate, + channels, + ); + + let output: Vec = + SampleRateConverter::new(source, target, ResampleConfig::poly().build()).collect(); + + let ratio = target.get() as f64 / rate.get() as f64; + let expected = ((2 * span_frames) as f64 * ratio).ceil() as usize; + + assert_eq!( + output.len(), + expected, + "expected {expected} samples from both spans, got {} \ + (second span likely not processed)", + output.len() + ); + } +} diff --git a/src/conversions/sample_rate/rubato.rs b/src/conversions/sample_rate/rubato.rs new file mode 100644 index 000000000..64fa51353 --- /dev/null +++ b/src/conversions/sample_rate/rubato.rs @@ -0,0 +1,490 @@ +//! Rubato resampler wrapper and implementations. + +use dasp_sample::Sample as _; +use rubato::{audioadapter_buffers::direct::InterleavedSlice, Resampler}; + +use crate::common::{ChannelCount, SampleRate}; +use crate::{Float, Sample, Source}; + +use super::buffer::Buffer; +use super::builder::{Poly, Sinc, WindowFunction}; + +#[derive(thiserror::Error, Debug)] +#[error("Failed to create resampler")] +pub(super) struct ResamplerCreationError(#[from] rubato::ResamplerConstructionError); + +/// Type alias for Async (polynomial/sinc) resampler. +pub type RubatoAsyncResample = RubatoResample>; + +/// Type alias for FFT resampler (synchronous, fixed-ratio). +#[cfg(feature = "rubato-fft")] +pub type RubatoFftResample = RubatoResample>; + +/// The inner resampler implementation chosen based on configuration and sample rates. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum ResampleInner { + /// Passthrough when source rate is equal to the target rate + Passthrough { + source: I, + input_span_pos: usize, + channels: ChannelCount, + source_rate: SampleRate, + }, + + /// Polynomial resampling (fast, no anti-aliasing) + Poly(RubatoAsyncResample), + + /// Sinc resampling (with anti-aliasing) + Sinc(RubatoAsyncResample), + + /// FFT resampling for fixed ratios (synchronous resampling) + #[cfg(feature = "rubato-fft")] + #[cfg_attr(docsrs, doc(cfg(feature = "rubato-fft")))] + Fft(RubatoFftResample), +} + +impl ResampleInner { + /// Get a reference to the inner input source + #[inline] + pub fn input(&self) -> &I { + match self { + ResampleInner::Passthrough { source, .. } => source, + ResampleInner::Poly(resampler) => &resampler.input, + ResampleInner::Sinc(resampler) => &resampler.input, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => &resampler.input, + } + } + + /// Extract the inner input source, consuming the resampler + #[inline] + pub fn into_inner(self) -> I { + match self { + ResampleInner::Passthrough { source, .. } => source, + ResampleInner::Poly(resampler) => resampler.input, + ResampleInner::Sinc(resampler) => resampler.input, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.input, + } + } +} + +/// Generic wrapper around Rubato resamplers for sample-by-sample iteration. +#[derive(Debug)] +pub struct RubatoResample> { + pub input: I, + pub resampler: R, + + pub input_buffer: Box<[Sample]>, + pub input_frame_count: usize, + + output_buffer: Buffer, + + /// The following are cached at construction for parameter-change detection. + pub channels: ChannelCount, + pub source_rate: SampleRate, + + pub input_samples_consumed: usize, + pub input_exhausted: bool, + + pub total_input_frames: usize, + pub total_output_samples: usize, + pub expected_output_samples: usize, + + /// The number of real (non-flush) frames currently in the input buffer. + pub real_frames_in_buffer: usize, + + pub output_delay_remaining: usize, + pub resample_ratio: Float, + + /// Effective length of the current output chunk as seen by callers of `current_span_len`. + /// Differs from `output_buffer.len()` when delay-compensation skip consumed leading samples. + pub output_span_len: usize, +} + +impl> RubatoResample { + /// Calculate the number of output samples to skip for delay compensation. + pub fn calculate_delay_compensation(resampler: &R, channels: ChannelCount) -> usize { + // Skip delay-1 frames to align the first output frame with input position 0. + let delay_frames = resampler.output_delay(); + let delay_to_skip = delay_frames.saturating_sub(1); + delay_to_skip * channels.get() as usize + } + + /// Whether the output buffer has unconsumed samples. + pub fn output_has_samples(&self) -> bool { + !self.output_buffer.is_empty() + } + + /// Effective span length of the current output chunk for `current_span_len` reporting. + pub fn output_span_len(&self) -> usize { + self.output_span_len + } + + /// Number of output samples remaining to be read. + pub fn output_remaining(&self) -> usize { + self.output_buffer.remaining() + } + + pub fn reset(&mut self) { + self.resampler.reset(); + self.output_buffer.reset(0); + self.input_frame_count = 0; + self.input_samples_consumed = 0; + self.input_exhausted = false; + self.total_input_frames = 0; + self.total_output_samples = 0; + self.expected_output_samples = 0; + self.real_frames_in_buffer = 0; + self.output_delay_remaining = + Self::calculate_delay_compensation(&self.resampler, self.channels); + self.output_span_len = 0; + } + + fn fill_input_buffer(&mut self, needed: usize, num_channels: usize) { + while self.input_frame_count < needed { + if self.input_exhausted { + break; + } + let sample_pos = self.input_frame_count * num_channels; + for ch in 0..num_channels { + if let Some(sample) = self.input.next() { + self.input_buffer[sample_pos + ch] = sample; + } else { + self.input_exhausted = true; + break; + } + } + if !self.input_exhausted { + self.input_frame_count += 1; + self.real_frames_in_buffer += 1; + } + } + + // Zero-pad if we ran out of input to flush the filter tail + if self.input_frame_count == 0 { + self.input_buffer[..needed * num_channels].fill(Sample::EQUILIBRIUM); + self.input_frame_count = needed; + // real_frames_in_buffer stays at 0 - these are flush frames + } + } + + pub fn next_sample(&mut self, cached_input_span_len: Option) -> Option { + let num_channels = self.channels.get() as usize; + loop { + // If we have buffered output, return it + if !self.output_buffer.is_empty() { + let sample = self.output_buffer.read(); + self.total_output_samples += 1; + return Some(sample); + } + + // Need more input - first check if we're completely done + if self.input_exhausted + && self.input_frame_count == 0 + && self.total_output_samples >= self.expected_output_samples + { + return None; + } + + // Fill input buffer, flushing with zeros if input is exhausted. + // Cap to the span boundary so we never read into the next span's samples, + // which may have a different channel count or sample rate. + let original_needed = self.resampler.input_frames_next(); + let needed_input = if let Some(span_len) = cached_input_span_len { + let span_frames = span_len / num_channels; + // input_samples_consumed resets to 0 at each span boundary, so this + // stays span-relative even when the resampler processes multiple spans. + let already_read = + self.input_samples_consumed / num_channels + self.real_frames_in_buffer; + let remaining = span_frames.saturating_sub(already_read); + original_needed.min(self.input_frame_count + remaining) + } else { + original_needed + }; + + // When the span cap brings needed_input to zero and the buffer is empty, + // check whether the source is truly exhausted (single-span done) or just + // at a same-format span boundary. For the latter, SampleRateConverter::next() + // will have refreshed cached_input_span_len before the next call. + if needed_input == 0 && !self.input_exhausted { + self.input_exhausted = self.input.is_exhausted(); + } + + // When exhausted, use a full chunk so zero-padding flushes the filter tail. + let fill_target = if self.input_exhausted { + original_needed + } else { + needed_input + }; + self.fill_input_buffer(fill_target, num_channels); + + let actual_frames = self.input_frame_count; + + // Use original_needed (pre-cap) so that partial_len is signalled to Rubato + // whenever we have fewer frames than a full chunk, regardless of whether the + // shortfall is due to source exhaustion or a span boundary cap. + let indexing; + let indexing_ref = if actual_frames < original_needed { + indexing = rubato::Indexing { + input_offset: 0, + output_offset: 0, + partial_len: Some(actual_frames), + active_channels_mask: None, + }; + Some(&indexing) + } else { + None + }; + + let (frames_in, frames_out) = { + let input_adapter = + InterleavedSlice::new(&self.input_buffer, num_channels, actual_frames) + .inspect_err(|_e| { + #[cfg(feature = "tracing")] + tracing::error!("resampler: failed to create input adapter: {_e}"); + }) + .ok()?; + + let num_frames = self.output_buffer.capacity() / num_channels; + let mut output_adapter = InterleavedSlice::new_mut( + self.output_buffer.as_mut_slice(), + num_channels, + num_frames, + ) + .inspect_err(|_e| { + #[cfg(feature = "tracing")] + tracing::error!("resampler: failed to create output adapter: {_e}"); + }) + .ok()?; + + self.resampler + .process_into_buffer(&input_adapter, &mut output_adapter, indexing_ref) + .inspect_err(|_e| { + #[cfg(feature = "tracing")] + tracing::error!("resampler: processing failed: {_e}"); + }) + .ok()? + }; + + // If no output was produced and input is exhausted, we're done + if frames_out == 0 && self.input_exhausted { + return None; + } + + // When using partial_len, Rubato may report consuming more frames than we + // actually provided (it counts the zero-padded frames). Clamp to actual. + let actual_consumed = frames_in.min(actual_frames); + self.input_samples_consumed += actual_consumed * num_channels; + + // Only count real (non-flush) frames toward expected output + let real_consumed = actual_consumed.min(self.real_frames_in_buffer); + self.real_frames_in_buffer -= real_consumed; + self.total_input_frames += real_consumed; + self.expected_output_samples = (self.total_input_frames as Float * self.resample_ratio) + .ceil() as usize + * num_channels; + + self.input_frame_count -= actual_consumed; + + self.output_buffer.reset(frames_out * num_channels); + + // Skip warmup delay samples + if self.output_delay_remaining > 0 { + let samples_to_skip = self.output_delay_remaining.min(self.output_buffer.len()); + self.output_buffer.skip(samples_to_skip); + self.output_delay_remaining -= samples_to_skip; + } + + // Cap output whenever a partial chunk was processed (span boundary or + // exhaustion) or once all input is gone. Rubato internally zero-pads a + // partial chunk to a full output chunk, so without the cap the output + // would exceed expected_output_samples. + if (self.input_exhausted || indexing_ref.is_some()) && self.expected_output_samples > 0 + { + let remaining = self + .expected_output_samples + .saturating_sub(self.total_output_samples); + self.output_buffer.cap_to_remaining(remaining); + } + + // Snapshot remaining after skip and cap. Stays constant while the chunk drains, + // giving current_span_len a stable total that excludes delay-skipped leading samples. + self.output_span_len = self.output_buffer.remaining(); + } + } +} + +// Async resampler (polynomial and sinc) implementations +impl RubatoAsyncResample { + pub fn new_poly( + input: I, + target_rate: SampleRate, + chunk_size: usize, + degree: Poly, + ) -> Result { + let source_rate = input.sample_rate(); + let channels = input.channels(); + + let resample_ratio = target_rate.get() as Float / source_rate.get() as Float; + + let resampler = rubato::Async::new_poly( + resample_ratio as _, + 1.0, + degree.into(), + chunk_size, + channels.get() as usize, + rubato::FixedAsync::Output, + )?; + + let input_buf_size = resampler.input_frames_max(); + let output_buf_size = resampler.output_frames_max(); + + let output_delay_remaining = + RubatoResample::>::calculate_delay_compensation( + &resampler, channels, + ); + + Ok(Self { + input, + resampler, + input_buffer: vec![Sample::EQUILIBRIUM; input_buf_size * channels.get() as usize] + .into_boxed_slice(), + input_frame_count: 0, + output_buffer: Buffer::new(output_buf_size * channels.get() as usize), + channels, + source_rate, + input_samples_consumed: 0, + input_exhausted: false, + output_delay_remaining, + output_span_len: 0, + total_input_frames: 0, + total_output_samples: 0, + expected_output_samples: 0, + real_frames_in_buffer: 0, + resample_ratio, + }) + } + + #[allow(clippy::too_many_arguments)] + pub fn new_sinc( + input: I, + target_rate: SampleRate, + chunk_size: usize, + sinc_len: usize, + f_cutoff: Float, + oversampling_factor: usize, + interpolation: Sinc, + window: WindowFunction, + ) -> Result { + let source_rate = input.sample_rate(); + let channels = input.channels(); + + let parameters = rubato::SincInterpolationParameters { + sinc_len, + f_cutoff: f_cutoff as _, + oversampling_factor, + interpolation: interpolation.into(), + window: window.into(), + }; + + let resample_ratio = target_rate.get() as Float / source_rate.get() as Float; + + let resampler = rubato::Async::new_sinc( + resample_ratio as _, + 1.0, + ¶meters, + chunk_size, + channels.get() as usize, + rubato::FixedAsync::Output, + )?; + + let input_buf_size = resampler.input_frames_max(); + let output_buf_size = resampler.output_frames_max(); + + let output_delay_remaining = + RubatoResample::>::calculate_delay_compensation( + &resampler, channels, + ); + + Ok(Self { + input, + resampler, + input_buffer: vec![Sample::EQUILIBRIUM; input_buf_size * channels.get() as usize] + .into_boxed_slice(), + input_frame_count: 0, + output_buffer: Buffer::new(output_buf_size * channels.get() as usize), + channels, + source_rate, + input_samples_consumed: 0, + input_exhausted: false, + output_delay_remaining, + output_span_len: 0, + total_input_frames: 0, + total_output_samples: 0, + expected_output_samples: 0, + real_frames_in_buffer: 0, + resample_ratio, + }) + } +} + +// FFT resampler implementation +#[cfg(feature = "rubato-fft")] +impl RubatoFftResample { + /// Create a new FFT resampler for fixed-ratio sample rate conversion. + /// + /// The FFT resampler requires that: + /// - Input chunk size must be a multiple of the GCD-reduced denominator + /// - Output chunk size must be a multiple of the GCD-reduced numerator + pub fn new( + input: I, + target_rate: SampleRate, + chunk_size: usize, + sub_chunks: usize, + ) -> Result { + let source_rate = input.sample_rate(); + let channels = input.channels(); + + // Determine input chunk size - must be multiple of the GCD-reduced denominator + let g = crate::math::gcd(target_rate.get(), source_rate.get()); + let den = (source_rate.get() / g) as usize; + let input_chunk_size = ((chunk_size / den) + 1) * den; + + let resampler = rubato::Fft::new( + source_rate.get() as usize, + target_rate.get() as usize, + input_chunk_size, + sub_chunks, + channels.get() as usize, + rubato::FixedSync::Output, + )?; + + let input_buf_size = resampler.input_frames_max(); + let output_buf_size = resampler.output_frames_max(); + let resample_ratio = target_rate.get() as Float / source_rate.get() as Float; + + let output_delay_remaining = Self::calculate_delay_compensation(&resampler, channels); + + Ok(Self { + input, + resampler, + input_buffer: vec![Sample::EQUILIBRIUM; input_buf_size * channels.get() as usize] + .into_boxed_slice(), + input_frame_count: 0, + output_buffer: Buffer::new(output_buf_size * channels.get() as usize), + channels, + source_rate, + input_samples_consumed: 0, + input_exhausted: false, + total_input_frames: 0, + total_output_samples: 0, + expected_output_samples: 0, + real_frames_in_buffer: 0, + output_delay_remaining, + output_span_len: 0, + resample_ratio, + }) + } +} diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index fd3383ac6..5e1c68849 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -147,8 +147,9 @@ impl SymphoniaDecoder { continue; } - let decoded = match decoder.decode(¤t_span) { - Ok(decoded) => decoded, + match decoder.decode(¤t_span) { + Ok(decoded) if decoded.frames() > 0 => break decoded, + Ok(_) => continue, // skip setup/header packets with no audio frames (e.g. Vorbis) Err(e) => match e { Error::DecodeError(_) => { // Decode errors are intentionally ignored with no retry limit. @@ -158,15 +159,6 @@ impl SymphoniaDecoder { } _ => return Err(e), }, - }; - - // Loop until we get a packet with audio frames. This is necessary because some - // formats can have packets with only metadata, particularly when rewinding, in - // which case the iterator would otherwise end with `None`. - // Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which - // can resturn non-zero durations for packets without audio frames. - if decoded.frames() > 0 { - break decoded; } }; let spec = decoded.spec().to_owned(); diff --git a/src/math.rs b/src/math.rs index 5a1fa2794..9fe31722c 100644 --- a/src/math.rs +++ b/src/math.rs @@ -1,6 +1,6 @@ //! Math utilities for audio processing. -use crate::common::SampleRate; +use crate::{Float, SampleRate}; use std::time::Duration; /// Nanoseconds per second, used for Duration calculations. @@ -13,18 +13,6 @@ pub use std::f32::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI #[cfg(feature = "64bit")] pub use std::f64::consts::{E, LN_10, LN_2, LOG10_2, LOG10_E, LOG2_10, LOG2_E, PI, TAU}; -/// Linear interpolation between two samples. -/// -/// The result should be equivalent to -/// `first * (1 - numerator / denominator) + second * numerator / denominator`. -/// -/// To avoid numeric overflows pick smaller numerator. -// TODO (refactoring) Streamline this using coefficient instead of numerator and denominator. -#[inline] -pub(crate) fn lerp(first: Sample, second: Sample, numerator: u32, denominator: u32) -> Sample { - first + (second - first) * numerator as Float / denominator as Float -} - /// Converts decibels to linear amplitude scale. /// /// This function converts a decibel value to its corresponding linear amplitude value @@ -176,44 +164,19 @@ macro_rules! nz { pub use nz; -use crate::{common::Float, Sample}; +/// Greatest common divisor (Euclidean algorithm). +pub(crate) fn gcd(mut a: u32, mut b: u32) -> u32 { + while b != 0 { + let r = a % b; + a = b; + b = r; + } + a +} #[cfg(test)] mod test { use super::*; - use num_rational::Ratio; - use quickcheck::{quickcheck, TestResult}; - - quickcheck! { - fn lerp_random(first: Sample, second: Sample, numerator: u32, denominator: u32) -> TestResult { - if denominator == 0 { return TestResult::discard(); } - - // Constrain to realistic audio sample range [-1.0, 1.0] - // Audio samples rarely exceed this range, and large values cause floating-point error accumulation - if first.abs() > 1.0 || second.abs() > 1.0 { return TestResult::discard(); } - - // Discard infinite or NaN samples (can occur in quickcheck) - if !first.is_finite() || !second.is_finite() { return TestResult::discard(); } - - let (numerator, denominator) = Ratio::new(numerator, denominator).into_raw(); - // Reduce max numerator to avoid floating-point error accumulation with large ratios - if numerator > 1000 { return TestResult::discard(); } - - let a = first as f64; - let b = second as f64; - let c = numerator as f64 / denominator as f64; - if !(0.0..=1.0).contains(&c) { return TestResult::discard(); }; - - let reference = a * (1.0 - c) + b * c; - let x = lerp(first, second, numerator, denominator); - - // With realistic audio-range inputs, lerp should be very precise - // f32 has ~7 decimal digits, so 1e-6 tolerance is reasonable - // This is well below 16-bit audio precision (~1.5e-5) - let tolerance = 1e-6; - TestResult::from_bool((x as f64 - reference).abs() < tolerance) - } - } /// Tolerance values for precision tests, derived from empirical measurement /// of actual implementation errors across the full ±100dB range. diff --git a/src/mixer.rs b/src/mixer.rs index 3e9d40de9..39f8b8a9b 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -281,6 +281,7 @@ mod tests { assert_eq!(rx.next(), Some(15.0)); assert_eq!(rx.next(), Some(5.0)); assert_eq!(rx.next(), Some(-5.0)); + assert_eq!(rx.next(), Some(-2.5)); assert_eq!(rx.next(), None); } diff --git a/src/queue.rs b/src/queue.rs index 542a05c88..840e4f50c 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -5,8 +5,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use dasp_sample::Sample as _; - use crate::source::{Empty, SeekError, Source}; use crate::Sample; @@ -37,6 +35,7 @@ pub fn queue(keep_alive_if_empty: bool) -> (Arc, SourcesQueue current: Box::new(Empty::new()) as Box<_>, signal_after_end: None, input: input.clone(), + samples_consumed_in_span: 0, silence_samples_remaining: 0, }; @@ -121,56 +120,44 @@ pub struct SourcesQueueOutput { // The next sounds. input: Arc, - // This counts how many silence samples to inject for keep-alive behavior. + // Track samples consumed in the current span to detect mid-span endings. + samples_consumed_in_span: usize, + + // When a source ends mid-frame, this counts how many silence samples to inject + // to complete the frame before transitioning to the next source. silence_samples_remaining: usize, } impl Source for SourcesQueueOutput { #[inline] fn current_span_len(&self) -> Option { - let len = match self.current.current_span_len() { - Some(len) if len == 0 && self.silence_samples_remaining > 0 => { - // - Current source ended mid-frame, and we're injecting silence to frame-align it. - self.silence_samples_remaining - } - Some(len) if len > 0 || !self.input.keep_alive_if_empty() => { - // - Current source is not exhausted, and is reporting some span length, or - // - Current source is exhausted, and won't output silence after it: end of queue. - len - } - _ => { - // - Current source is not exhausted, and is reporting no span length, or - // - Current source is exhausted, and will output silence after it. - self.channels().get() as usize - } - }; - - // Special case: if the current source is `Empty` and there are queued sounds after it. - if len == 0 - && self - .current - .total_duration() - .is_some_and(|duration| duration.is_zero()) - { - if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { - return next - .current_span_len() - .or_else(|| Some(next.channels().get() as usize)); - } + if !self.current.is_exhausted() { + return self.current.current_span_len(); } - - // A queue must never return None: that could cause downstream sources to assume sample - // rate or channel count would never change from one queue item to the next. - Some(len) + // A queue must never return None: that would cause downstream sources to miss format + // changes between queue items. Return a small value so boundaries are checked often. + Some(self.channels().get() as usize) } #[inline] fn channels(&self) -> ChannelCount { if self.current.is_exhausted() && self.silence_samples_remaining == 0 { - if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { - // Current source exhausted, peek at next queued source - // This is critical: UniformSourceIterator queries metadata during append, - // before any samples are pulled. We must report the next source's metadata. + // Skip exhausted sources at the head of the queue (e.g. an empty chain) and + // return the first non-exhausted source's metadata. This is critical: + // UniformSourceIterator queries metadata before pulling any samples, so we + // must report the upcoming source's format, not a preceding exhausted stub. + // + // If the queue is genuinely empty there is nothing to peek at. The stale value + // is returned below. This is corrected at the first span boundary after the + // new source begins playing. + if let Some((next, _)) = self + .input + .next_sounds + .lock() + .unwrap() + .iter() + .find(|(s, _)| !s.is_exhausted()) + { return next.channels(); } } @@ -181,9 +168,14 @@ impl Source for SourcesQueueOutput { #[inline] fn sample_rate(&self) -> SampleRate { if self.current.is_exhausted() && self.silence_samples_remaining == 0 { - if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { - // Current source exhausted, peek at next queued source - // This prevents wrong resampling setup in UniformSourceIterator + if let Some((next, _)) = self + .input + .next_sounds + .lock() + .unwrap() + .iter() + .find(|(s, _)| !s.is_exhausted()) + { return next.sample_rate(); } } @@ -217,10 +209,10 @@ impl Iterator for SourcesQueueOutput { #[inline] fn next(&mut self) -> Option { loop { - // If we're playing silence for keep-alive, return silence. + // If we're padding to complete a frame, return silence. if self.silence_samples_remaining > 0 { self.silence_samples_remaining -= 1; - return Some(Sample::EQUILIBRIUM); + return Some(0.0); } // Basic situation that will happen most of the time. @@ -228,8 +220,21 @@ impl Iterator for SourcesQueueOutput { return Some(sample); } - // Current source is exhausted. Move to next sound, play silence, or end. - // In order to avoid inlining that expensive operation, the code is in another function. + // Source ended - check if we ended mid-frame and need padding. + let channels = self.current.channels().get() as usize; + let incomplete_frame_samples = self.samples_consumed_in_span % channels; + if incomplete_frame_samples > 0 { + // We're mid-frame - need to pad with silence to complete it. + self.silence_samples_remaining = channels - incomplete_frame_samples; + // Reset counter now since we're transitioning to a new span. + self.samples_consumed_in_span = 0; + // Continue loop - next iteration will inject silence. + continue; + } + + // Reset counter and move to next sound. + // In order to avoid inlining this expensive operation, the code is in another function. + self.samples_consumed_in_span = 0; if self.go_next().is_err() { if self.input.keep_alive_if_empty() { self.silence_samples_remaining = self.current.channels().get() as usize; @@ -272,9 +277,47 @@ impl SourcesQueueOutput { mod tests { use crate::buffer::SamplesBuffer; use crate::math::nz; - use crate::queue; - use crate::source::test_utils::TestSource; - use crate::source::Source; + use crate::source::{chain, SeekError, Source}; + use crate::{queue, ChannelCount, Sample, SampleRate}; + use std::time::Duration; + + #[test] + #[ignore = "known limitation: metadata gap when queue is briefly empty after exhaustion"] + fn metadata_gap_when_queue_briefly_empty() { + let new_rate = nz!(48000); + let (tx, mut rx) = queue::queue(false); + tx.append(SamplesBuffer::new(nz!(1), nz!(44100), vec![1.0])); + assert_eq!(rx.next(), Some(1.0)); + + // Source is exhausted, nothing queued yet. A real consumer reads metadata here + // to set up its converter — it gets the stale value. + let rate_seen_by_consumer = rx.sample_rate(); + + // The replacement source arrives only after the metadata was already queried. + tx.append(SamplesBuffer::new(nz!(1), new_rate, vec![2.0])); + + // Ideally the consumer would have seen 48000. In practice it saw 44100. + assert_eq!(rate_seen_by_consumer, new_rate); + } + + #[test] + fn exhausted_source_in_queue_is_skipped_for_metadata() { + let source_rate = nz!(48000); + // The empty chain's dummy rate must differ from source_rate, otherwise the test + // would not catch the bug (both values would satisfy the assertion below). + let empty_chain_dummy_rate = chain(std::iter::empty::()).sample_rate(); + assert_ne!(empty_chain_dummy_rate, source_rate); + + let (tx, mut rx) = queue::queue(false); + tx.append(chain(std::iter::empty::())); + tx.append(SamplesBuffer::new(nz!(1), source_rate, vec![1.0, 2.0])); + + assert_eq!(rx.channels(), nz!(1)); + assert_eq!(rx.sample_rate(), source_rate); + assert_eq!(rx.next(), Some(1.0)); + assert_eq!(rx.next(), Some(2.0)); + assert_eq!(rx.next(), None); + } #[test] fn basic() { diff --git a/src/source/chain.rs b/src/source/chain.rs new file mode 100644 index 000000000..2440dca43 --- /dev/null +++ b/src/source/chain.rs @@ -0,0 +1,178 @@ +use std::time::Duration; + +use super::SeekError; +use crate::common::{ChannelCount, SampleRate}; +use crate::math::nz; +use crate::Source; + +/// Builds a source that chains sources provided by an iterator. +/// +/// The `iterator` parameter is an iterator that produces a source. The source is then played. +/// Whenever the source ends, the `iterator` is used again in order to produce the source that is +/// played next. +/// +/// If the `iterator` produces `None`, then the sound ends. +pub fn chain(iterator: I) -> Chain +where + I: IntoIterator, +{ + let mut iterator = iterator.into_iter(); + let first_source = iterator.next(); + + Chain { + iterator, + current_source: first_source, + } +} + +/// A source that chains sources provided by an iterator. +#[derive(Clone)] +pub struct Chain +where + I: Iterator, +{ + // The iterator that provides sources. + iterator: I, + // Is only ever `None` if the first element of the iterator is `None`. + current_source: Option, +} + +impl Iterator for Chain +where + I: Iterator, + I::Item: Iterator + Source, +{ + type Item = ::Item; + + #[inline] + fn next(&mut self) -> Option { + loop { + if let Some(src) = &mut self.current_source { + if let Some(value) = src.next() { + return Some(value); + } + } + + if let Some(src) = self.iterator.next() { + self.current_source = Some(src); + } else { + return None; + } + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + if let Some(cur) = &self.current_source { + (cur.size_hint().0, None) + } else { + (0, Some(0)) + } + } +} + +impl Source for Chain +where + I: Iterator, + I::Item: Iterator + Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + // The transition between sources must be a span boundary. We propagate the current + // source's span length directly. When the source is exhausted it already returns Some(0), + // which correctly signals end-of-span. The None case (empty iterator) is likewise + // signalled as Some(0). + match &self.current_source { + None => Some(0), + Some(src) => src.current_span_len(), + } + } + + #[inline] + fn channels(&self) -> ChannelCount { + if let Some(src) = &self.current_source { + src.channels() + } else { + // Dummy value that only happens if the iterator was empty. + nz!(2) + } + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + if let Some(src) = &self.current_source { + src.sample_rate() + } else { + // Dummy value that only happens if the iterator was empty. + nz!(44100) + } + } + + #[inline] + fn total_duration(&self) -> Option { + None + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + if let Some(source) = self.current_source.as_mut() { + source.try_seek(pos) + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use crate::buffer::SamplesBuffer; + use crate::math::nz; + use crate::source::{chain, Source}; + + #[test] + fn empty_chain_reports_end_of_span() { + let c = chain(std::iter::empty::()); + assert_eq!(c.current_span_len(), Some(0)); + } + + #[test] + fn exhausted_chain_reports_end_of_span() { + let mut c = chain(std::iter::once(SamplesBuffer::new( + nz!(1), + nz!(48000), + vec![1.0, 2.0], + ))); + assert_eq!(c.next(), Some(1.0)); + assert_eq!(c.next(), Some(2.0)); + assert_eq!(c.next(), None); + assert_eq!(c.current_span_len(), Some(0)); + } + + #[test] + fn basic() { + let mut rx = chain((0..2).map(|n| { + if n == 0 { + SamplesBuffer::new(nz!(1), nz!(48000), vec![10.0, -10.0, 10.0, -10.0]) + } else if n == 1 { + SamplesBuffer::new(nz!(2), nz!(96000), vec![5.0, 5.0, 5.0, 5.0]) + } else { + unreachable!() + } + })); + + 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().get(), 96000);*/ + // FIXME: not working + assert_eq!(rx.next(), Some(5.0)); + assert_eq!(rx.next(), Some(5.0)); + assert_eq!(rx.next(), Some(5.0)); + assert_eq!(rx.next(), Some(5.0)); + assert_eq!(rx.next(), None); + } +} diff --git a/src/source/from_factory.rs b/src/source/from_factory.rs deleted file mode 100644 index e1219c6cb..000000000 --- a/src/source/from_factory.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::source::{from_iter, FromIter}; - -/// Builds a source that chains sources built from a factory. -/// -/// The `factory` parameter is a function that produces a source. The source is then played. -/// Whenever the source ends, `factory` is called again in order to produce the source that is -/// played next. -/// -/// If the `factory` closure returns `None`, then the sound ends. -pub fn from_factory(factory: F) -> FromIter> -where - F: FnMut() -> Option, -{ - from_iter(FromFactoryIter { factory }) -} - -/// Internal type used by `from_factory`. -pub struct FromFactoryIter { - factory: F, -} - -impl Iterator for FromFactoryIter -where - F: FnMut() -> Option, -{ - type Item = S; - - #[inline] - fn next(&mut self) -> Option { - (self.factory)() - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - (0, None) - } -} diff --git a/src/source/from_fn.rs b/src/source/from_fn.rs new file mode 100644 index 000000000..54f859590 --- /dev/null +++ b/src/source/from_fn.rs @@ -0,0 +1,52 @@ +use crate::source::{chain, Chain}; + +/// Builds a source that chains sources built from a factory function. +/// +/// The `factory` parameter is a function that produces a source. The source is then played. +/// Whenever the source ends, `factory` is called again in order to produce the source that is +/// played next. +/// +/// If the `factory` closure returns `None`, then the sound ends. +pub fn from_fn(factory: F) -> Chain> +where + F: FnMut() -> Option, +{ + chain(FromFn { factory }) +} + +/// Deprecated: Use `from_fn()` instead. +#[deprecated(since = "0.22.0", note = "Use `from_fn()` instead")] +pub fn from_factory(factory: F) -> Chain> +where + F: FnMut() -> Option, +{ + from_fn(factory) +} + +/// Iterator that generates sources from a factory function. +/// +/// Created by the `from_fn()` function. +pub struct FromFn { + factory: F, +} + +/// Deprecated: Use `FromFn` instead. +#[deprecated(since = "0.22.0", note = "Use `FromFn` instead")] +pub type FromFactoryIter = FromFn; + +impl Iterator for FromFn +where + F: FnMut() -> Option, +{ + type Item = S; + + #[inline] + fn next(&mut self) -> Option { + (self.factory)() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (0, None) + } +} diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index 2b4544ec2..0ade85bc6 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -2,109 +2,108 @@ use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::math::nz; -use crate::Source; +use crate::{Sample, Source}; -/// Builds a source that chains sources provided by an iterator. +/// Creates a `Source` from a sample iterator with specified audio parameters. /// -/// The `iterator` parameter is an iterator that produces a source. The source is then played. -/// Whenever the source ends, the `iterator` is used again in order to produce the source that is -/// played next. +/// This adapter wraps any iterator that produces `Sample` values and provides +/// the `Source` trait implementation by storing the channel count and sample rate. /// -/// If the `iterator` produces `None`, then the sound ends. -pub fn from_iter(iterator: I) -> FromIter +/// # Example +/// +/// ``` +/// use rodio::source::from_iter; +/// use rodio::math::nz; +/// +/// let samples = vec![0.1, 0.2, 0.3, 0.4]; +/// let source = from_iter(samples.into_iter(), nz!(2), nz!(44100)); +/// ``` +#[inline] +pub fn from_iter(iter: I, channels: ChannelCount, sample_rate: SampleRate) -> FromIter where - I: IntoIterator, + I: Iterator, { - let mut iterator = iterator.into_iter(); - let first_source = iterator.next(); - FromIter { - iterator, - current_source: first_source, + iter, + channels, + sample_rate, } } -/// A source that chains sources provided by an iterator. -#[derive(Clone)] -pub struct FromIter -where - I: Iterator, -{ - // The iterator that provides sources. - iterator: I, - // Is only ever `None` if the first element of the iterator is `None`. - current_source: Option, +/// A `Source` that wraps a sample iterator with audio metadata. +/// +/// Created by the `from_iter()` function. +#[derive(Clone, Debug)] +pub struct FromIter { + iter: I, + channels: ChannelCount, + sample_rate: SampleRate, +} + +impl FromIter { + /// Creates a new `FromIter` from an iterator and audio parameters. + #[inline] + pub fn new(iter: I, channels: ChannelCount, sample_rate: SampleRate) -> Self { + Self { + iter, + channels, + sample_rate, + } + } + + /// Destroys this source and returns the underlying iterator. + #[inline] + pub fn into_inner(self) -> I { + self.iter + } + + /// Get immutable access to the underlying iterator. + #[inline] + pub fn inner(&self) -> &I { + &self.iter + } + + /// Get mutable access to the underlying iterator. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + &mut self.iter + } } impl Iterator for FromIter where - I: Iterator, - I::Item: Iterator + Source, + I: Iterator, { - type Item = ::Item; + type Item = Sample; #[inline] fn next(&mut self) -> Option { - loop { - if let Some(src) = &mut self.current_source { - if let Some(value) = src.next() { - return Some(value); - } - } - - if let Some(src) = self.iterator.next() { - self.current_source = Some(src); - } else { - return None; - } - } + self.iter.next() } #[inline] fn size_hint(&self) -> (usize, Option) { - if let Some(cur) = &self.current_source { - (cur.size_hint().0, None) - } else { - (0, Some(0)) - } + self.iter.size_hint() } } impl Source for FromIter where - I: Iterator, - I::Item: Iterator + Source, + I: Iterator, { #[inline] fn current_span_len(&self) -> Option { - if let Some(src) = &self.current_source { - if !src.is_exhausted() { - return src.current_span_len(); - } - } - - None + self.iter.size_hint().1 } #[inline] fn channels(&self) -> ChannelCount { - if let Some(src) = &self.current_source { - src.channels() - } else { - // Dummy value that only happens if the iterator was empty. - nz!(2) - } + self.channels } #[inline] fn sample_rate(&self) -> SampleRate { - if let Some(src) = &self.current_source { - src.sample_rate() - } else { - // Dummy value that only happens if the iterator was empty. - crate::DEFAULT_SAMPLE_RATE - } + self.sample_rate } #[inline] @@ -113,46 +112,11 @@ where } #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - if let Some(source) = self.current_source.as_mut() { - source.try_seek(pos) - } else { - Ok(()) - } + fn try_seek(&mut self, _pos: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) } } -#[cfg(test)] -mod tests { - use crate::buffer::SamplesBuffer; - 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(nz!(1), nz!(48000), vec![10.0, -10.0, 10.0, -10.0]) - } else if n == 1 { - SamplesBuffer::new(nz!(2), nz!(96000), vec![5.0, 5.0, 5.0, 5.0]) - } else { - unreachable!() - } - })); - - 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().get(), 96000);*/ - // FIXME: not working - assert_eq!(rx.next(), Some(5.0)); - assert_eq!(rx.next(), Some(5.0)); - assert_eq!(rx.next(), Some(5.0)); - assert_eq!(rx.next(), Some(5.0)); - assert_eq!(rx.next(), None); - } -} +impl ExactSizeIterator for FromIter where I: ExactSizeIterator {} diff --git a/src/source/mod.rs b/src/source/mod.rs index 63d5233e9..7e4452808 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use crate::{ buffer::SamplesBuffer, common::{assert_error_traits, ChannelCount, SampleRate}, + conversions::SampleRateConverter, math, Float, Sample, }; @@ -15,6 +16,7 @@ pub use self::agc::{AutomaticGainControl, AutomaticGainControlSettings}; pub use self::amplify::Amplify; pub use self::blt::BltFilter; pub use self::buffered::Buffered; +pub use self::chain::{chain, Chain}; pub use self::channel_volume::ChannelVolume; pub use self::chirp::{chirp, Chirp}; pub use self::crossfade::Crossfade; @@ -25,7 +27,8 @@ pub use self::empty::Empty; pub use self::empty_callback::EmptyCallback; pub use self::fadein::FadeIn; pub use self::fadeout::FadeOut; -pub use self::from_factory::{from_factory, FromFactoryIter}; +#[allow(deprecated)] +pub use self::from_fn::{from_factory, from_fn, FromFactoryIter, FromFn}; pub use self::from_iter::{from_iter, FromIter}; pub use self::limit::{Limit, LimitSettings}; pub use self::linear_ramp::LinearGainRamp; @@ -47,11 +50,13 @@ pub use self::take::TakeDuration; pub use self::triangle::TriangleWave; pub use self::uniform::UniformSourceIterator; pub use self::zero::{Zero, ZeroError}; +pub use crate::conversions::ResampleConfig; mod agc; mod amplify; mod blt; mod buffered; +mod chain; mod channel_volume; mod chirp; mod crossfade; @@ -62,7 +67,7 @@ mod empty; mod empty_callback; mod fadein; mod fadeout; -mod from_factory; +mod from_fn; mod from_iter; mod limit; mod linear_ramp; @@ -730,9 +735,33 @@ pub trait Source: Iterator { distortion::distortion(self, gain, threshold) } - // There is no `can_seek()` method as it is impossible to use correctly. Between - // checking if a source supports seeking and actually seeking the sink can - // switch to a new source. + /// Resamples this source to a different sample rate. + /// + /// See the [`sample_rate`](crate::conversions::sample_rate) module documentation for detailed + /// information about resampling algorithms and quality presets. + /// + /// # Quality Presets + /// + /// - **Fast**: Lower quality, lower CPU usage, lower latency + /// - **Balanced**: Good quality, moderate CPU usage (default) + /// - **Accurate**: Best quality, higher CPU usage + /// + /// # Examples + /// + /// ``` + /// use rodio::SampleRate; + /// use rodio::source::{SineWave, Source, ResampleConfig}; + /// + /// let source = SineWave::new(440.0); + /// let resampled = source.resample(SampleRate::new(96000).unwrap(), ResampleConfig::balanced()); + /// ``` + #[inline] + fn resample(self, target_rate: SampleRate, config: ResampleConfig) -> SampleRateConverter + where + Self: Sized, + { + SampleRateConverter::new(self, target_rate, config) + } /// Attempts to seek to a given position in the current source. /// @@ -861,6 +890,24 @@ pub(crate) fn padding_samples_needed( } } +/// Resets span tracking state after a seek operation. +#[inline] +pub(crate) fn reset_seek_span_tracking( + samples_counted: &mut usize, + cached_span_len: &mut Option, + pos: Duration, + input_span_len: Option, +) { + *samples_counted = 0; + if pos == Duration::ZERO { + // Set span-counting mode when seeking to start + *cached_span_len = input_span_len; + } else { + // Set detection mode for arbitrary positions + *cached_span_len = None; + } +} + #[cfg(test)] pub(crate) mod test_utils { use super::*; diff --git a/src/source/uniform.rs b/src/source/uniform.rs index cacf5e326..14b2421b3 100644 --- a/src/source/uniform.rs +++ b/src/source/uniform.rs @@ -1,11 +1,108 @@ -use std::cmp; use std::time::Duration; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::conversions::{ChannelCountConverter, SampleRateConverter}; +use crate::conversions::ChannelCountConverter; +use crate::conversions::{Poly, ResampleConfig, SampleRateConverter}; use crate::Source; +#[derive(Clone)] +enum UniformInner { + Passthrough(I), + SampleRate(SampleRateConverter), + ChannelCount(ChannelCountConverter), + BothUpmix(ChannelCountConverter>), + BothDownmix(SampleRateConverter>), +} + +impl Iterator for UniformInner { + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + match self { + UniformInner::Passthrough(take) => take.next(), + UniformInner::SampleRate(converter) => converter.next(), + UniformInner::ChannelCount(converter) => converter.next(), + UniformInner::BothUpmix(converter) => converter.next(), + UniformInner::BothDownmix(converter) => converter.next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + UniformInner::Passthrough(take) => take.size_hint(), + UniformInner::SampleRate(converter) => converter.size_hint(), + UniformInner::ChannelCount(converter) => converter.size_hint(), + UniformInner::BothUpmix(converter) => converter.size_hint(), + UniformInner::BothDownmix(converter) => converter.size_hint(), + } + } +} + +impl UniformInner { + #[inline] + fn into_inner(self) -> I { + match self { + UniformInner::Passthrough(source) => source, + UniformInner::SampleRate(converter) => converter.into_inner(), + UniformInner::ChannelCount(converter) => converter.into_inner(), + UniformInner::BothUpmix(converter) => converter.into_inner().into_inner(), + UniformInner::BothDownmix(converter) => converter.into_inner().into_inner(), + } + } + + #[inline] + fn inner(&self) -> &I { + match self { + UniformInner::Passthrough(source) => source, + UniformInner::SampleRate(converter) => converter.inner(), + UniformInner::ChannelCount(converter) => converter.inner(), + UniformInner::BothUpmix(converter) => converter.inner().inner(), + UniformInner::BothDownmix(converter) => converter.inner().inner(), + } + } + + #[inline] + fn inner_mut(&mut self) -> &mut I { + match self { + UniformInner::Passthrough(source) => source, + UniformInner::SampleRate(converter) => converter.inner_mut(), + UniformInner::ChannelCount(converter) => converter.inner_mut(), + UniformInner::BothUpmix(converter) => converter.inner_mut().inner_mut(), + UniformInner::BothDownmix(converter) => converter.inner_mut().inner_mut(), + } + } +} + +impl Source for UniformInner { + #[inline] + fn current_span_len(&self) -> Option { + self.inner().current_span_len() + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.inner().channels() + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.inner().sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.inner().total_duration() + } + + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.inner_mut().try_seek(pos) + } +} + /// An iterator that reads from a `Source` and converts the samples to a /// specific type, sample-rate and channels count. /// @@ -16,11 +113,13 @@ pub struct UniformSourceIterator where I: Source, { - inner: Option>>>, - pending: Option, + inner: Option>, target_channels: ChannelCount, target_sample_rate: SampleRate, - total_duration: Option, + current_channels: ChannelCount, + current_sample_rate: SampleRate, + current_span_pos: usize, + cached_span_len: Option, } impl UniformSourceIterator @@ -29,42 +128,68 @@ where { /// Wrap a `Source` and lazily convert its samples to a specific type, /// sample-rate and channels count. - #[inline] pub fn new( input: I, target_channels: ChannelCount, target_sample_rate: SampleRate, ) -> UniformSourceIterator { - let total_duration = input.total_duration(); + let current_channels = input.channels(); + let current_sample_rate = input.sample_rate(); + let inner = UniformSourceIterator::bootstrap(input, target_channels, target_sample_rate); + let cached_span_len = inner.current_span_len(); - UniformSourceIterator { - inner: None, - pending: Some(input), + Self { + inner: Some(inner), target_channels, target_sample_rate, - total_duration, + current_channels, + current_sample_rate, + current_span_pos: 0, + cached_span_len, } } - #[inline] fn bootstrap( input: I, target_channels: ChannelCount, target_sample_rate: SampleRate, - ) -> ChannelCountConverter>> { - // Limit the span length to something reasonable - let span_len = input.current_span_len().map(|x| x.min(32768)); - + ) -> UniformInner { let from_channels = input.channels(); let from_sample_rate = input.sample_rate(); - let input = Take { - iter: input, - n: span_len, - }; - let input = - SampleRateConverter::new(input, from_sample_rate, target_sample_rate, from_channels); - ChannelCountConverter::new(input, from_channels, target_channels) + let needs_rate_conversion = from_sample_rate != target_sample_rate; + let needs_channel_conversion = from_channels != target_channels; + + match (needs_rate_conversion, needs_channel_conversion) { + (false, false) => UniformInner::Passthrough(input), + (true, false) => { + let config = ResampleConfig::poly().degree(Poly::Linear).build(); + let rate_converted = SampleRateConverter::new(input, target_sample_rate, config); + UniformInner::SampleRate(rate_converted) + } + (false, true) => { + let channel_converted = + ChannelCountConverter::new(input, from_channels, target_channels); + UniformInner::ChannelCount(channel_converted) + } + (true, true) => { + let config = ResampleConfig::poly().degree(Poly::Linear).build(); + + if target_channels > from_channels { + let rate_converted = + SampleRateConverter::new(input, target_sample_rate, config); + let channel_converted = + ChannelCountConverter::new(rate_converted, from_channels, target_channels); + UniformInner::BothUpmix(channel_converted) + } else { + let channel_converted = + ChannelCountConverter::new(input, from_channels, target_channels); + let rate_converted = + SampleRateConverter::new(channel_converted, target_sample_rate, config); + UniformInner::BothDownmix(rate_converted) + } + } + } } } @@ -76,35 +201,56 @@ where #[inline] fn next(&mut self) -> Option { - if let Some(value) = self.inner.as_mut().and_then(|i| i.next()) { - return Some(value); - } + if let Some(span_len) = self.cached_span_len { + if self.current_span_pos >= span_len { + // At span boundary - check if parameters changed + let source = self.inner.as_mut().unwrap().inner_mut(); + let new_channels = source.channels(); + let new_sample_rate = source.sample_rate(); + + let parameters_changed = new_channels != self.current_channels + || new_sample_rate != self.current_sample_rate; + + if parameters_changed { + let source = self.inner.take().unwrap().into_inner(); + self.current_channels = new_channels; + self.current_sample_rate = new_sample_rate; + let new_inner = UniformSourceIterator::bootstrap( + source, + self.target_channels, + self.target_sample_rate, + ); + self.inner = Some(new_inner); + } - let input = match self.inner.take() { - Some(inner) => inner.into_inner().into_inner().iter, - None => self - .pending - .take() - .expect("pending is Some when inner is None"), - }; + // Calculate new output span length based on the conversion type + let new_span_len = match self.inner.as_ref().unwrap() { + UniformInner::Passthrough(source) => source.current_span_len(), + UniformInner::SampleRate(resample) => resample.current_span_len(), + UniformInner::ChannelCount(converter) => converter.current_span_len(), + UniformInner::BothUpmix(converter) => converter.current_span_len(), + UniformInner::BothDownmix(converter) => converter.current_span_len(), + }; + + self.current_span_pos = 0; + self.cached_span_len = new_span_len; + } + } - let mut input = - UniformSourceIterator::bootstrap(input, self.target_channels, self.target_sample_rate); + if let Some(sample) = self.inner.as_mut().unwrap().next() { + // Only increment counter when tracking spans + if self.cached_span_len.is_some() { + self.current_span_pos += 1; + } + return Some(sample); + } - let value = input.next(); - self.inner = Some(input); - value + None } #[inline] fn size_hint(&self) -> (usize, Option) { - let lower = self - .inner - .as_ref() - .map(|i| i.size_hint().0) - .or_else(|| self.pending.as_ref().map(|p| p.size_hint().0)) - .unwrap_or(0); - (lower, None) + (self.inner.as_ref().unwrap().size_hint().0, None) } } @@ -129,71 +275,15 @@ where #[inline] fn total_duration(&self) -> Option { - self.total_duration + self.inner.as_ref().unwrap().inner().total_duration() } #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { if let Some(input) = self.inner.as_mut() { - input.inner_mut().inner_mut().inner_mut().try_seek(pos) - } else if let Some(pending) = self.pending.as_mut() { - pending.try_seek(pos) + input.inner_mut().try_seek(pos) } else { Ok(()) } } } - -#[derive(Clone, Debug)] -struct Take { - iter: I, - n: Option, -} - -impl Take { - #[inline] - pub fn inner_mut(&mut self) -> &mut I { - &mut self.iter - } -} - -impl Iterator for Take -where - I: Iterator, -{ - type Item = ::Item; - - #[inline] - fn next(&mut self) -> Option<::Item> { - if let Some(n) = &mut self.n { - if *n != 0 { - *n -= 1; - self.iter.next() - } else { - None - } - } else { - self.iter.next() - } - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - if let Some(n) = self.n { - let (lower, upper) = self.iter.size_hint(); - - let lower = cmp::min(lower, n); - - let upper = match upper { - Some(x) if x < n => Some(x), - _ => Some(n), - }; - - (lower, upper) - } else { - self.iter.size_hint() - } - } -} - -impl ExactSizeIterator for Take where I: ExactSizeIterator {} diff --git a/tests/flac_test.rs b/tests/flac_test.rs index e17602a66..e9b000fb3 100644 --- a/tests/flac_test.rs +++ b/tests/flac_test.rs @@ -1,9 +1,8 @@ -#[cfg(any(feature = "claxon", feature = "symphonia-flac"))] +#![cfg(any(feature = "claxon", feature = "symphonia-flac"))] + use rodio::Source; -#[cfg(any(feature = "claxon", feature = "symphonia-flac"))] use std::time::Duration; -#[cfg(any(feature = "claxon", feature = "symphonia-flac"))] #[test] fn test_flac_encodings() { // 16 bit FLAC file exported from Audacity (2 channels, compression level 5) diff --git a/tests/vorbis_test.rs b/tests/vorbis_test.rs new file mode 100644 index 000000000..a0c0e28b6 --- /dev/null +++ b/tests/vorbis_test.rs @@ -0,0 +1,16 @@ +#![cfg(feature = "symphonia-vorbis")] + +use rodio::{Decoder, Source}; + +#[test] +fn vorbis_decoder_not_exhausted_at_construction() { + let file = std::fs::File::open("assets/music.ogg").unwrap(); + let decoder = Decoder::try_from(file).unwrap(); + + assert!( + !decoder.is_exhausted(), + "decoder should not be exhausted immediately after construction; \ + current_span_len={:?}", + decoder.current_span_len(), + ); +}