diff --git a/CHANGELOG.md b/CHANGELOG.md index 312a2b66b..e592ff51d 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 `Resample` source for high-quality sample rate conversion. ### Changed @@ -90,6 +91,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 @@ -103,6 +109,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 @@ -124,6 +135,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 dd1793809..ad194ff73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,56 @@ dependencies = [ "pkg-config", ] +[[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 0.61.2", +] + +[[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 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -66,6 +110,43 @@ 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 = "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.0" @@ -119,9 +200,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -159,6 +240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -167,11 +249,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" @@ -193,6 +289,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" @@ -220,9 +322,9 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ "bitflags 2.11.1", "libc", @@ -235,7 +337,7 @@ dependencies = [ [[package]] name = "cpal" version = "0.18.0" -source = "git+https://github.com/RustAudio/cpal#f938e338c9811fbe4d428517acf1d15cc6d694d4" +source = "git+https://github.com/RustAudio/cpal#2c7acf8ed42b6523f319145a8be256c446df5939" dependencies = [ "alsa", "block2", @@ -260,6 +362,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "windows", + "windows-core", ] [[package]] @@ -577,9 +680,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -606,7 +709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -625,6 +728,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 = "itoa" version = "1.0.18" @@ -687,9 +796,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ "cfg-if", "futures-util", @@ -722,9 +831,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libm" @@ -1028,11 +1137,17 @@ 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.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3280fe5b6f97ac1a35a0ac003e2fb0b92f8e4bdf2b2057e1bf9b87acca5696" +checksum = "2804e694ef0de3b4cbb254de565053b7cb48d3398df7fd60c6c62bed40c5372a" dependencies = [ "cmake", ] @@ -1207,6 +1322,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" @@ -1263,6 +1387,7 @@ version = "0.22.2" dependencies = [ "approx", "atomic_float", + "clap", "claxon", "cpal", "crossbeam-channel", @@ -1279,6 +1404,7 @@ dependencies = [ "rstest", "rstest_reuse", "rtrb", + "rubato", "symphonia", "symphonia-adapter-fdk-aac", "symphonia-adapter-libopus", @@ -1328,9 +1454,25 @@ dependencies = [ [[package]] name = "rtrb" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7204ed6420f698836b76d4d5c2ec5dec7585fd5c3a788fd1cde855d1de598239" +checksum = "4ade083ccbb4bf536df69d1f6432cc23deb7acccff86b183f3923a6fd56a1153" + +[[package]] +name = "rubato" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce96ead1a91f7895704a9f08ea5947dfc8bd7c1f2936a22295b655ec67e5c6ef" +dependencies = [ + "audioadapter", + "audioadapter-buffers", + "num-complex", + "num-integer", + "num-traits", + "realfft", + "visibility", + "windowfunctions", +] [[package]] name = "rustc_version" @@ -1503,6 +1645,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" @@ -1539,9 +1687,9 @@ dependencies = [ [[package]] name = "symphonia-adapter-libopus" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d17450685dda0e87467eddf3e0f9c0b2a1707fc5c3234c111f70d46c6e4494" +checksum = "2bfc8e95f95c23ed1b5328eb66920ad28d9968c797f9c7aa755d4b45a5f47a41" dependencies = [ "log", "opusic-sys", @@ -1901,6 +2049,23 @@ 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 = "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" @@ -1937,9 +2102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -1950,9 +2115,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ "js-sys", "wasm-bindgen", @@ -1960,9 +2125,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1970,9 +2135,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -1983,9 +2148,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -2026,9 +2191,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -2065,6 +2230,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" @@ -2252,9 +2426,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 7a1d5d254..eced67bf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,16 +11,17 @@ edition = "2021" rust-version = "1.89" [features] -# Default feature set provides audio playback and common format support default = [ "playback", "recording", + "dither", + "rubato-fft", + "simd", "flac", "mp3", "mp4", "vorbis", "wav", - "dither", ] # Core functionality features @@ -35,8 +36,10 @@ wav_output = ["dep:hound"] tracing = ["dep:tracing"] # Experimental features using atomic floating-point operations experimental = ["dep:atomic_float"] -# Perform all calculations with 64-bit floats (instead of 32) +# Perform calculations with 64-bit floats (instead of 32) 64bit = [] +# Enable SIMD optimizations +simd = ["symphonia/opt-simd"] # Audio generation features # @@ -62,14 +65,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"] @@ -104,11 +109,19 @@ symphonia-simd = ["symphonia/opt-simd"] # 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 @@ -116,7 +129,7 @@ rustdoc-args = ["--cfg", "docsrs"] cargo-args = ["-Zunstable-options"] [dependencies] -cpal = { git = "https://github.com/RustAudio/cpal", optional = true } +cpal = { git = "https://github.com/RustAudio/cpal", optional = true, default-features = false } dasp_sample = "0.11" claxon = { version = "0.4", optional = true } hound = { version = "3.5", optional = true } @@ -132,6 +145,9 @@ tracing = { version = "0.1.40", optional = true } atomic_float = { version = "1.1.0", optional = true } rtrb = { version = "0.3.2", optional = true } + +# Rubato resampling +rubato = { version = "2.0", default-features = false } num-rational = "0.4.2" symphonia-adapter-libopus = { version = "0.2", optional = true } @@ -144,6 +160,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" @@ -249,6 +266,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..e01db818c 100644 --- a/src/conversions/channels.rs +++ b/src/conversions/channels.rs @@ -1,5 +1,6 @@ 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. #[derive(Clone, Debug)] @@ -19,11 +20,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 +37,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 +66,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 +106,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..3e972ecce 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,11 +1,8 @@ -/*! -This module contains functions that convert from one PCM format to another. - -This includes conversion between sample formats, channels or sample rates. -*/ +//! This module contains functions that convert from one PCM format to another. pub use self::channels::ChannelCountConverter; pub use self::sample::SampleTypeConverter; +#[allow(deprecated)] pub use self::sample_rate::SampleRateConverter; mod channels; diff --git a/src/conversions/sample.rs b/src/conversions/sample.rs index f231f33be..cddb1b34a 100644 --- a/src/conversions/sample.rs +++ b/src/conversions/sample.rs @@ -24,7 +24,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 index 2120a7c8b..35fc991b0 100644 --- a/src/conversions/sample_rate.rs +++ b/src/conversions/sample_rate.rs @@ -1,388 +1,119 @@ 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)] +use crate::source::{resample::Poly, FromIter, Resample, ResampleConfig}; +use crate::{Sample, Source}; + +/// Iterator that converts from one sample rate to another. +#[deprecated( + since = "0.22.0", + note = "Use `Resample` with `FromIter` (or `from_iter` function) directly" +)] +#[derive(Debug)] +#[allow(deprecated)] pub struct SampleRateConverter where - I: Iterator, + 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, + inner: Resample>, } +#[allow(deprecated)] impl SampleRateConverter where - I: Iterator, + 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) - }; + pub fn new(input: I, from: SampleRate, to: SampleRate, channels: ChannelCount) -> Self { + let adapter = FromIter::new(input, channels, from); + let config = ResampleConfig::poly().degree(Poly::Linear).build(); + let inner = Resample::new(adapter, to, config); - // 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, - ), - } + Self { inner } } /// Destroys this iterator and returns the underlying iterator. #[inline] pub fn into_inner(self) -> I { - self.input + self.inner.into_inner().into_inner() } - /// Get mutable access to the iterator + /// Get mutable access to the underlying iterator. #[inline] pub fn inner_mut(&mut self) -> &mut I { - &mut self.input + self.inner.inner_mut().inner_mut() } - /// Get a reference to the underlying iterator + /// Get access to the underlying iterator. #[inline] pub fn inner(&self) -> &I { - &self.input + self.inner.inner().inner() } +} - 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; - } - } +#[allow(deprecated)] +impl Clone for SampleRateConverter +where + I: Iterator + Clone, +{ + fn clone(&self) -> Self { + let from_iter = self.inner.inner(); + Self::new( + from_iter.inner().clone(), + from_iter.sample_rate(), + self.inner.sample_rate(), + from_iter.channels(), + ) } } +#[allow(deprecated)] 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; + type Item = Sample; - 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 next(&mut self) -> Option { + self.inner.next() } #[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)) - } + self.inner.size_hint() } } -impl ExactSizeIterator for SampleRateConverter where I: ExactSizeIterator {} +#[allow(deprecated)] +impl ExactSizeIterator for SampleRateConverter where + I: Iterator + ExactSizeIterator +{ +} #[cfg(test)] +#[allow(deprecated)] 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)) - } - } + /// Minimal smoke test to ensure the deprecated SampleRateConverter wrapper still works. + /// Core resampling tests have been moved to src/source/resample.rs. #[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] + fn deprecated_wrapper_works() { + // Test basic upsampling + let input: Vec = vec![0.0, 0.5, 1.0, 0.5, 0.0]; + let from = nz!(1000); + let to = nz!(2000); + let channels = nz!(1); + + let converter = SampleRateConverter::new(input.into_iter(), from, to, channels); + let output: Vec<_> = converter.collect(); + + // Should produce approximately 2x samples (upsampling) + assert!( + output.len() >= 8 && output.len() <= 12, + "Expected approximately 10 samples, got {}", + output.len() ); } - - #[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/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..13cc7fa96 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,9 @@ macro_rules! nz { pub use nz; -use crate::{common::Float, Sample}; - #[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..ad03cdae6 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -15,6 +15,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 +26,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; @@ -34,6 +36,7 @@ pub use self::pausable::Pausable; pub use self::periodic::PeriodicAccess; pub use self::position::TrackPosition; pub use self::repeat::Repeat; +pub use self::resample::{Resample, ResampleConfig}; pub use self::sawtooth::SawtoothWave; pub use self::signal_generator::{Function, GeneratorFunction, SignalGenerator}; pub use self::sine::SineWave; @@ -52,6 +55,7 @@ mod agc; mod amplify; mod blt; mod buffered; +mod chain; mod channel_volume; mod chirp; mod crossfade; @@ -62,7 +66,7 @@ mod empty; mod empty_callback; mod fadein; mod fadeout; -mod from_factory; +mod from_fn; mod from_iter; mod limit; mod linear_ramp; @@ -71,6 +75,7 @@ mod pausable; mod periodic; mod position; mod repeat; +pub mod resample; mod sawtooth; mod signal_generator; mod sine; @@ -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 [`resample`] 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) -> Resample + where + Self: Sized, + { + Resample::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/resample/buffer.rs b/src/source/resample/buffer.rs new file mode 100644 index 000000000..7e39be47c --- /dev/null +++ b/src/source/resample/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/source/resample/builder.rs b/src/source/resample/builder.rs new file mode 100644 index 000000000..1c830ed8f --- /dev/null +++ b/src/source/resample/builder.rs @@ -0,0 +1,482 @@ +//! 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::source::{resample::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, + /// The number of intermediate points to use for interpolation + 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 oversampling factor (typical range: 64-4096). + /// + /// Higher values improve interpolation accuracy but increase memory usage. + 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/source/resample/mod.rs b/src/source/resample/mod.rs new file mode 100644 index 000000000..d0c4269d9 --- /dev/null +++ b/src/source/resample/mod.rs @@ -0,0 +1,784 @@ +//! 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 the [`ResampleConfig`] builder: +//! +//! ```rust +//! use rodio::math::nz; +//! use rodio::source::{SineWave, Source, Resample, ResampleConfig}; +//! use rodio::source::resample::{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 = Resample::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 ::rubato::Resampler as _; +use num_rational::Ratio; + +use super::{reset_seek_span_tracking, SeekError}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + 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 Resample +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, +} + +impl Clone for Resample +where + I: Source + Clone, +{ + fn clone(&self) -> Self { + // Shallow clone: this resets filter state + let source = self.inner().clone(); + Resample::new(source, self.target_rate, self.config.clone()) + } +} + +impl Resample +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, + } + } + + /// 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 { + let ratio = Ratio::new(target_rate.get(), source_rate.get()); + 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, + } => { + if *ratio.numer() <= MAX_FIXED_RATIO && *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, + } => { + if *ratio.numer() <= MAX_FIXED_RATIO && *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 = *ratio.numer().max(ratio.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 { + match self.inner.as_ref().unwrap() { + ResampleInner::Passthrough { source, .. } => source, + ResampleInner::Poly(resampler) => &resampler.input, + ResampleInner::Sinc(resampler) => &resampler.input, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => &resampler.input, + } + } + + /// Returns a mutable reference to the inner source. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + match self.inner.as_mut().unwrap() { + 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 Resample +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + let ( + input_span_len, + input_sample_rate, + input_exhausted, + output_has_samples, + output_len, + output_frames_next, + ) = match self.inner.as_ref().unwrap() { + 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_len(), + resampler.resampler.output_frames_next(), + ), + #[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_len(), + resampler.resampler.output_frames_next(), + ), + }; + + let ratio = Ratio::new(self.sample_rate().get(), input_sample_rate.get()); + if ratio.is_integer() { + // Integer upsampling (2x, 3x, etc.) - always exact and frame-aligned + input_span_len.map(|len| *ratio.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: our buffer is empty until the first call to next() loads it with + // resampled samples. Return the size of the next buffer. + Some(output_frames_next * 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(); + } + } + + 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 Resample +where + I: Source, +{ + type Item = Sample; + + #[inline] + fn next(&mut self) -> Option { + let sample = match self.inner.as_mut().unwrap() { + ResampleInner::Passthrough { source, .. } => source.next()?, + ResampleInner::Poly(resampler) => resampler.next_sample()?, + ResampleInner::Sinc(resampler) => resampler.next_sample()?, + #[cfg(feature = "rubato-fft")] + ResampleInner::Fft(resampler) => resampler.next_sample()?, + }; + + // If input reports no span length, parameters are stable by contract + let input_span_len = self.inner.as_ref().unwrap().input().current_span_len(); + if input_span_len.is_none() { + return Some(sample); + } + + let (expected_channels, expected_rate, samples_consumed) = + match self.inner.as_mut().unwrap() { + 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.inner.as_ref().unwrap().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 { + // Recreate resampler - new resampler will have counters reset to 0 + let source = self.inner.take().unwrap().into_inner(); + self.inner = Some(Self::create_resampler( + source, + self.target_rate, + &self.config, + )); + } else { + // Just crossed boundary without parameter change, reset counter + match self.inner.as_mut().unwrap() { + 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.inner.as_ref().unwrap() { + 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 TestSource { + samples: Vec, + index: usize, + sample_rate: SampleRate, + channels: ChannelCount, + } + + impl TestSource { + fn new(samples: Vec, sample_rate: SampleRate, channels: ChannelCount) -> Self { + Self { + samples, + index: 0, + sample_rate, + channels, + } + } + } + + impl Iterator for TestSource { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.index < self.samples.len() { + let sample = self.samples[self.index]; + self.index += 1; + Some(sample) + } else { + None + } + } + } + + impl Source for TestSource { + fn current_span_len(&self) -> Option { + Some(self.samples.len()) + } + + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + fn channels(&self) -> ChannelCount { + self.channels + } + + fn total_duration(&self) -> Option { + let samples = self.samples.len() / self.channels.get() as usize; + Some(Duration::from_secs_f64( + samples as f64 / self.sample_rate.get() as f64, + )) + } + + fn try_seek(&mut self, _position: Duration) -> Result<(), SeekError> { + Ok(()) + } + } + + /// 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 = Resample::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 = Resample::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 = Resample::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 = Resample::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}", + ); + } + } + } +} diff --git a/src/source/resample/rubato.rs b/src/source/resample/rubato.rs new file mode 100644 index 000000000..a5096df7a --- /dev/null +++ b/src/source/resample/rubato.rs @@ -0,0 +1,448 @@ +//! Rubato resampler wrapper and implementations. + +use dasp_sample::Sample as _; +use num_rational::Ratio; +use rubato::{audioadapter_buffers::direct::InterleavedSlice, Resampler}; + +use crate::source::{ChannelCount, SampleRate, Source}; +use crate::{Float, Sample}; + +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, +} + +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() + } + + /// Number of valid samples in the current output chunk. + pub fn output_len(&self) -> usize { + self.output_buffer.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); + } + + 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) -> 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 + let needed_input = self.resampler.input_frames_next(); + let frames_before = self.input_frame_count; + self.fill_input_buffer(needed_input, num_channels); + + // We can process with fewer frames than needed using partial_len when the input is + // exhausted. If we don't have enough input and more is coming, wait. + let made_progress = self.input_frame_count > frames_before; + if self.input_frame_count < needed_input && !self.input_exhausted && made_progress { + continue; + } + + let actual_frames = self.input_frame_count; + + let indexing; + let indexing_ref = if actual_frames < needed_input { + 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) = { + // InterleavedSlice is a zero-cost abstraction - no heap allocation occurs here + let input_adapter = + InterleavedSlice::new(&self.input_buffer, num_channels, actual_frames).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, + ) + .ok()?; + + self.resampler + .process_into_buffer(&input_adapter, &mut output_adapter, indexing_ref) + .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; + + // Shift remaining input samples to beginning of buffer + if actual_consumed < self.input_frame_count { + let src_start = actual_consumed * num_channels; + let src_end = self.input_frame_count * num_channels; + self.input_buffer.copy_within(src_start..src_end, 0); + } + 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 to cut off filter artifacts once input is exhausted + if self.input_exhausted && self.expected_output_samples > 0 { + let remaining = self + .expected_output_samples + .saturating_sub(self.total_output_samples); + self.output_buffer.cap_to_remaining(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, + 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, + 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(); + + // Calculate the GCD-reduced ratio + let ratio = Ratio::new(target_rate.get(), source_rate.get()); + let (_num, den) = ratio.into_raw(); + + // Determine input chunk size - must be multiple of denominator + let input_chunk_size = ((chunk_size / den as usize) + 1) * den as usize; + + 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, + resample_ratio, + }) + } +} diff --git a/src/source/uniform.rs b/src/source/uniform.rs index cacf5e326..3efccbd9b 100644 --- a/src/source/uniform.rs +++ b/src/source/uniform.rs @@ -1,11 +1,108 @@ -use std::cmp; use std::time::Duration; +use super::resample::{Poly, Resample, ResampleConfig}; use super::SeekError; use crate::common::{ChannelCount, SampleRate}; -use crate::conversions::{ChannelCountConverter, SampleRateConverter}; +use crate::conversions::ChannelCountConverter; use crate::Source; +#[derive(Clone)] +enum UniformInner { + Passthrough(I), + SampleRate(Resample), + ChannelCount(ChannelCountConverter), + BothUpmix(ChannelCountConverter>), + BothDownmix(Resample>), +} + +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,67 @@ 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 = Resample::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 = Resample::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 = + Resample::new(channel_converted, target_sample_rate, config); + UniformInner::BothDownmix(rate_converted) + } + } + } } } @@ -76,35 +200,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 +274,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(), + ); +}