From eae2b30474f665ae03d507e724daa3189f490308 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 5 Aug 2026 23:35:54 +0200 Subject: [PATCH 01/12] Update to audioadapter 5.0 and release 5.0.0 Bump audioadapter to 5.0 and audioadapter-buffers/audioadapter-sample to 5.1, raise the MSRV to 1.87 to match, and replace the commented-out path dependencies with a commented-out [patch.crates-io] block. --- Cargo.toml | 19 +++++++++++-------- README.md | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e120628..e18107b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rubato" -version = "4.0.0" -rust-version = "1.85" +version = "5.0.0" +rust-version = "1.87" authors = ["HEnquist "] description = "Asynchronous resampling library intended for audio data" license = "MIT OR Apache-2.0" @@ -25,10 +25,8 @@ realfft = { version = "3.5.0", optional = true } num-complex = { version = "0.4", optional = true } num-integer = "0.1.45" num-traits = "0.2" -#audioadapter = {version = "4.0.0", path = "../audioadapter-rs/audioadapter"} -audioadapter = "4.0" -#audioadapter-buffers = {version = "4.0.0", path = "../audioadapter-rs/audioadapter-buffers"} -audioadapter-buffers = "4.0" +audioadapter = "5.0" +audioadapter-buffers = "5.1" visibility = "0.1.1" windowfunctions = "0.1.1" @@ -41,8 +39,13 @@ log = "0.4.18" approx = "0.5.1" test-log = "0.2.16" test-case = "3" -#audioadapter-sample = {version = "4.0.0", path = "../audioadapter-rs/audioadapter-sample"} -audioadapter-sample = "4.0" +audioadapter-sample = "5.1" + +# Uncomment to build against a local audioadapter checkout instead of the published crates. +#[patch.crates-io] +#audioadapter = { path = "../audioadapter-rs/audioadapter" } +#audioadapter-buffers = { path = "../audioadapter-rs/audioadapter-buffers" } +#audioadapter-sample = { path = "../audioadapter-rs/audioadapter-sample" } [[bench]] name = "resamplers" diff --git a/README.md b/README.md index 4090e1a..e642b3f 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ See [Real-time considerations](#real-time-considerations) for more details. ## Input and output data format Input and output data is handled via -[`Adapter`](https://docs.rs/audioadapter/4.0.0/audioadapter/trait.Adapter.html) -and [`AdapterMut`](https://docs.rs/audioadapter/4.0.0/audioadapter/trait.AdapterMut.html) +[`Adapter`](https://docs.rs/audioadapter/5.0.0/audioadapter/trait.Adapter.html) +and [`AdapterMut`](https://docs.rs/audioadapter/5.0.0/audioadapter/trait.AdapterMut.html) objects from the [audioadapter](https://crates.io/crates/audioadapter) crate. By using a suitable adapter, any sample layout and format can be used. @@ -29,7 +29,7 @@ and the `audioadapter` traits are kept simple in order to make it easy to implem for new structures if needed. For projects migrating from a previous version of `rubato`, the -[`SequentialSliceOfVecs`](https://docs.rs/audioadapter-buffers/4.0.0/audioadapter_buffers/direct/struct.SequentialSliceOfVecs.html) +[`SequentialSliceOfVecs`](https://docs.rs/audioadapter-buffers/5.1.0/audioadapter_buffers/direct/struct.SequentialSliceOfVecs.html) adapter is a good starting point, since it wraps the vector of vectors commonly used with `rubato` v0.16 and earlier. @@ -405,7 +405,7 @@ Many audio editors, for example Audacity, are also able to directly import and e ## Compatibility -The `rubato` crate requires rustc version 1.85 or newer. +The `rubato` crate requires rustc version 1.87 or newer. ## Migrating from 3.x to 4.0 @@ -497,6 +497,16 @@ async_resampler.set_resample_ratio_relative(0.95, true)?; `ResamplerConstructionError`, add a `_ => ...` arm. ## Changelog +- v5.0.0 + - Fix an out-of-bounds panic in the asynchronous resamplers when the resampling ratio is + changed by a large factor with `ramp = true`. The input and output size estimates now + average the step sizes rather than the ratios, and correct for the ramp overshoot, so the + estimate is never lower than the number of frames the processing loop actually consumes. + - Speed up the polynomial interpolation in `Async::new_poly` by evaluating the polynomials + in Horner form. + - Update to `audioadapter` 5.0 and `audioadapter-buffers` 5.1. Bump your own `audioadapter` + dependencies to match the versions `rubato` re-exports. + - Raise the minimum supported rustc version to 1.87. - v4.0.0 - Update to `audioadapter` 4.0, which removes the lifetime parameter from the `Adapter` and `AdapterMut` traits. From 864dbc3b73c383a499ff06ce11a7dcccdf87dfc1 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 00:47:34 +0200 Subject: [PATCH 02/12] Add accessors for FFT sizes and filter cutoff Expose fft_size_in, fft_size_out and cutoff on Fft, and cutoff on Async. The cutoff is relative to the input Nyquist frequency, and is None for the polynomial variant of Async since it uses no anti-aliasing filter. Extract the cutoff resolution into resolve_cutoff so make_interpolator and new_sinc share one definition, and pass the resolved value to new_with_sinc_interpolator. Also fix the Debug impl of Fft, which named itself Fast. --- benches/resamplers.rs | 1 + src/asynchro.rs | 78 ++++++++++++++++++++++++++++++++++++++++++- src/asynchro_sinc.rs | 35 ++++++++++++++----- src/synchro.rs | 65 +++++++++++++++++++++++++++++++++++- 4 files changed, 168 insertions(+), 11 deletions(-) diff --git a/benches/resamplers.rs b/benches/resamplers.rs index 2078e59..a0a2fe7 100644 --- a/benches/resamplers.rs +++ b/benches/resamplers.rs @@ -50,6 +50,7 @@ mod bench_asyncro { 1.1, interpolation_type, interpolator, + f_cutoff, chunksize, channels, FixedAsync::Input, diff --git a/src/asynchro.rs b/src/asynchro.rs index cb683e9..a5915f8 100644 --- a/src/asynchro.rs +++ b/src/asynchro.rs @@ -4,7 +4,8 @@ use std::marker::PhantomData; use crate::asynchro_fast::{InnerPoly, PolynomialDegree}; use crate::asynchro_sinc::{ - make_interpolator, InnerSinc, SincInterpolationParameters, SincInterpolationType, + make_interpolator, resolve_cutoff, InnerSinc, SincInterpolationParameters, + SincInterpolationType, }; use crate::error::{ResampleError, ResampleResult, ResamplerConstructionError}; use crate::sinc_interpolator::{ @@ -112,6 +113,7 @@ pub struct Async { inner_resampler: Box>, channel_mask: Vec, fixed: FixedAsync, + sinc_cutoff: Option, } impl fmt::Debug for Async { @@ -251,6 +253,8 @@ where inner_resampler: Box::new(inner_resampler), channel_mask, fixed, + // Polynomial interpolation uses no anti-aliasing filter. + sinc_cutoff: None, }) } @@ -288,12 +292,38 @@ where max_resample_ratio_relative, parameters.interpolation, interpolator, + resolve_cutoff( + parameters.sinc_len, + resample_ratio, + parameters.f_cutoff, + parameters.window, + ), chunk_size, nbr_channels, fixed, ) } + /// The relative cutoff frequency of the sinc anti-aliasing filter, + /// or `None` if this resampler uses polynomial interpolation. + /// + /// The value is relative to the Nyquist frequency of the *input* rate, + /// where `1.0` means the cutoff sits right at Nyquist. + /// Multiply by `sample_rate_input / 2` to get the cutoff in Hz. + /// + /// The cutoff is either the one given in the + /// [SincInterpolationParameters], or, when that is left unset, one derived + /// from the sinc length and the window function. A longer sinc moves it + /// closer to Nyquist. When the resampler was created for downsampling, it + /// is scaled down to keep it below the output Nyquist frequency. + /// + /// The filter is built once, so the value reflects the resample ratio the + /// resampler was created with, and does not change with + /// [Adjustable::set_resample_ratio](crate::Adjustable::set_resample_ratio). + pub fn cutoff(&self) -> Option { + self.sinc_cutoff + } + /// Create a new Sinc using an existing Interpolator. /// /// Parameters are: @@ -301,6 +331,7 @@ where /// - `max_resample_ratio_relative`: Maximum ratio that can be set with [Adjustable::set_resample_ratio](crate::Adjustable::set_resample_ratio) relative to `resample_ratio`, must be >= 1.0. The minimum relative ratio is the reciprocal of the maximum. For example, with `max_resample_ratio_relative` of 10.0, the ratio can be set between `resample_ratio` * 10.0 and `resample_ratio` / 10.0. /// - `interpolation_type`: Parameters for interpolation, see `SincInterpolationParameters`. /// - `interpolator`: The interpolator to use. + /// - `f_cutoff`: The relative cutoff frequency the interpolator was built with, reported by [cutoff](Async::cutoff). /// - `chunk_size`: Size of output data in frames. /// - `nbr_channels`: Number of channels in input/output. #[cfg_attr(feature = "bench_asyncro", visibility::make(pub))] @@ -309,6 +340,7 @@ where max_resample_ratio_relative: f64, interpolation_type: SincInterpolationType, interpolator: AnyInterpolator, + f_cutoff: f32, chunk_size: usize, nbr_channels: usize, fixed: FixedAsync, @@ -372,6 +404,7 @@ where buffer, channel_mask, fixed, + sinc_cutoff: Some(f_cutoff), }) } @@ -790,6 +823,7 @@ mod tests { }; use crate::{Adjustable, Resampler, Resizable}; use crate::{Async, FixedAsync}; + use approx::assert_abs_diff_eq; use audioadapter_buffers::direct::SequentialSliceOfVecs; use test_case::test_matrix; @@ -803,6 +837,48 @@ mod tests { } } + #[test_log::test(test_matrix([0.8, 1.2, 0.125, 8.0]))] + fn poly_cutoff_is_none(ratio: f64) { + let resampler = Async::::new_poly( + ratio, + 1.0, + PolynomialDegree::Cubic, + 1024, + 2, + FixedAsync::Input, + ) + .unwrap(); + assert_eq!(resampler.cutoff(), None); + } + + #[test_log::test(test_matrix([0.8, 1.2, 0.125, 8.0]))] + fn sinc_cutoff_given(ratio: f64) { + // The given cutoff is used as is when upsampling, and scaled by the + // ratio when downsampling, to stay below the output Nyquist frequency. + let resampler = + Async::::new_sinc(ratio, 1.0, &basic_params(), 1024, 2, FixedAsync::Input) + .unwrap(); + let expected = 0.95 * ratio.min(1.0) as f32; + assert_abs_diff_eq!(resampler.cutoff().unwrap(), expected, epsilon = 1e-6); + } + + #[test_log::test(test_matrix([0.8, 1.2, 0.125, 8.0], [32, 256]))] + fn sinc_cutoff_automatic(ratio: f64, sinc_len: usize) { + // Without a given cutoff it is derived from the sinc length and window, + // and must stay below the lower Nyquist frequency of the two rates. + let params = SincInterpolationParameters { + sinc_len, + f_cutoff: None, + ..basic_params() + }; + let resampler = + Async::::new_sinc(ratio, 1.0, ¶ms, 1024, 2, FixedAsync::Input).unwrap(); + let limit = ratio.min(1.0) as f32; + let cutoff = resampler.cutoff().unwrap(); + assert!(cutoff > 0.5 * limit); + assert!(cutoff < limit); + } + #[test_log::test(test_matrix( [1, 100, 1024], [0.8, 1.2, 0.125, 8.0], diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index 898aee0..b84e1ec 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -210,6 +210,30 @@ pub enum SincInterpolationType { Nearest, } +/// Round the sinc length up to the multiple of 8 that the interpolators use. +pub(crate) fn round_sinc_len(sinc_len: usize) -> usize { + 8 * (((sinc_len as f32) / 8.0).ceil() as usize) +} + +/// Resolve the relative cutoff frequency of the sinc filter. +/// +/// An automatic cutoff is resolved against the rounded filter length, so that it +/// matches the filter that is actually built. When downsampling, the cutoff is +/// scaled by the resample ratio to keep it below the output Nyquist frequency. +pub(crate) fn resolve_cutoff( + sinc_len: usize, + resample_ratio: f64, + f_cutoff: Option, + window: WindowFunction, +) -> f32 { + let f_cutoff = f_cutoff.unwrap_or_else(|| calculate_cutoff(round_sinc_len(sinc_len), window)); + if resample_ratio >= 1.0 { + f_cutoff + } else { + f_cutoff * resample_ratio as f32 + } +} + pub fn make_interpolator( sinc_len: usize, resample_ratio: f64, @@ -220,15 +244,8 @@ pub fn make_interpolator( where T: AvxSample + SseSample + NeonSample + Sample, { - let sinc_len = 8 * (((sinc_len as f32) / 8.0).ceil() as usize); - // Resolve an automatic cutoff against the rounded filter length, so it matches the - // filter that is actually built. - let f_cutoff = f_cutoff.unwrap_or_else(|| calculate_cutoff(sinc_len, window)); - let f_cutoff = if resample_ratio >= 1.0 { - f_cutoff - } else { - f_cutoff * resample_ratio as f32 - }; + let f_cutoff = resolve_cutoff(sinc_len, resample_ratio, f_cutoff, window); + let sinc_len = round_sinc_len(sinc_len); #[cfg(target_arch = "x86_64")] if let Ok(interpolator) = diff --git a/src/synchro.rs b/src/synchro.rs index c4da06e..901f3eb 100644 --- a/src/synchro.rs +++ b/src/synchro.rs @@ -19,6 +19,7 @@ use realfft::{ComplexToReal, RealFftPlanner, RealToComplex}; struct FftResampler { fft_size_in: usize, fft_size_out: usize, + cutoff: f32, filter_f: Vec>, fft: Arc>, ifft: Arc>, @@ -66,7 +67,7 @@ pub struct Fft { impl fmt::Debug for Fft { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("Fast") + fmt.debug_struct("Fft") .field("nbr_channels", &self.nbr_channels) .field("chunk_size_in,", &self.chunk_size_in) .field("chunk_size_out,", &self.chunk_size_out) @@ -125,6 +126,7 @@ where FftResampler { fft_size_in, fft_size_out, + cutoff, filter_f, fft, ifft, @@ -365,6 +367,42 @@ where }) } + /// The FFT block size on the input side, in frames. + /// + /// This is the number of frames the resampler transforms at a time, + /// determined by the sample rates and the requested `chunk_size`. + /// It is not the same as the chunk size, unless the resampler was created + /// with [FixedSync::Both] and processes a single block per chunk. + /// The resampler delay is half of [fft_size_out](Fft::fft_size_out), + /// see [Resampler::output_delay]. + pub fn fft_size_in(&self) -> usize { + self.fft_size_in + } + + /// The FFT block size on the output side, in frames. + /// + /// The counterpart of [fft_size_in](Fft::fft_size_in), in the same ratio to + /// it as the output sample rate is to the input sample rate. + pub fn fft_size_out(&self) -> usize { + self.fft_size_out + } + + /// The relative cutoff frequency of the anti-aliasing filter. + /// + /// The value is relative to the Nyquist frequency of the *input* rate, + /// where `1.0` means the cutoff sits right at Nyquist. + /// Multiply by `sample_rate_input / 2` to get the cutoff in Hz. + /// + /// The cutoff is determined by the FFT block size and the window function. + /// A larger block moves it closer to Nyquist. + /// When downsampling it is scaled down to keep it below the lower Nyquist + /// frequency of the two sample rates. + /// + /// Multiply by `sample_rate_input / 2` to get the cutoff in Hz. + pub fn cutoff(&self) -> f32 { + self.resampler.cutoff + } + fn calc_chunk_sizes( fft_size_in: usize, fft_size_out: usize, @@ -721,6 +759,31 @@ mod tests { assert!((maxval - 1.0).abs() < 0.1); } + #[test_log::test(test_matrix( + [512, 1024, 4096], + [(44100, 48000), (48000, 44100), (44100, 88200), (88200, 44100), (44100, 192000), (192000, 44100)], + [FixedSync::Input, FixedSync::Output, FixedSync::Both] + ))] + fn fft_sizes_and_cutoff(chunksize: usize, rates: (usize, usize), fixed: FixedSync) { + let (input_rate, output_rate) = rates; + let resampler = Fft::::new(input_rate, output_rate, chunksize, 2, fixed).unwrap(); + + // The two block sizes are in the same ratio as the sample rates, + // and the delay is half the output block. + assert_abs_diff_eq!( + resampler.fft_size_out() as f64 / resampler.fft_size_in() as f64, + output_rate as f64 / input_rate as f64, + epsilon = 1e-9 + ); + assert_eq!(resampler.output_delay(), resampler.fft_size_out() / 2); + + // The cutoff must stay below the lower of the two Nyquist frequencies, + // expressed relative to the input Nyquist frequency. + let limit = (output_rate as f32 / input_rate as f32).min(1.0); + assert!(resampler.cutoff() > 0.5 * limit); + assert!(resampler.cutoff() < limit); + } + #[test_log::test(test_matrix( [512, 1024, 4096], [(44100, 48000), (48000, 44100), (44100, 88200), (88200, 44100), (44100, 192000), (192000, 44100), (44100, 44110)], From 76da317bd1c131f6c4a993394f6674e3ebeae405 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 00:47:42 +0200 Subject: [PATCH 03/12] Add a wav resampling example A small CLI that reads a wav file with waveadapter, resamples it with the FFT resampler, and writes it back in any sample format waveadapter supports. Options for output format, gain, chunk size and window, parsed with clap. --- Cargo.toml | 6 + examples/resample_wav.rs | 241 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 examples/resample_wav.rs diff --git a/Cargo.toml b/Cargo.toml index e18107b..bf75dd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ approx = "0.5.1" test-log = "0.2.16" test-case = "3" audioadapter-sample = "5.1" +waveadapter = "0.2" +clap = { version = "4.6", features = ["derive"] } # Uncomment to build against a local audioadapter checkout instead of the published crates. #[patch.crates-io] @@ -51,6 +53,10 @@ audioadapter-sample = "5.1" name = "resamplers" harness = false +[[example]] +name = "resample_wav" +required-features = ["fft_resampler"] + [lib] bench = false path = "src/lib.rs" diff --git a/examples/resample_wav.rs b/examples/resample_wav.rs new file mode 100644 index 0000000..aa7fced --- /dev/null +++ b/examples/resample_wav.rs @@ -0,0 +1,241 @@ +//! A minimal wav resampling command line tool. +//! +//! Reads a wav file, resamples it to a new sample rate with the FFT resampler, +//! and writes the result to a new wav file. The sample format of the output is +//! chosen freely, independent of the format of the input file. +//! +//! Compared to the `process_*` examples this one is deliberately small. The +//! [waveadapter](https://crates.io/crates/waveadapter) crate handles the wav +//! files, and [Resampler::process_all] resamples the whole clip in a single +//! call, taking care of the chunk loop and trimming the resampler delay. +//! +//! Run it with: +//! ```sh +//! cargo run --release --example resample_wav -- input.wav output.wav 48000 +//! cargo run --release --example resample_wav -- input.wav output.wav 96000 --format I24_3 --chunk 2048 +//! cargo run --release --example resample_wav -- --help +//! ``` + +use std::fs::File; +use std::io::{BufReader, BufWriter}; +use std::time::Instant; + +use audioadapter::stats::AdapterStats; +use audioadapter::{Adapter, AdapterMut}; +use clap::{Parser, ValueEnum}; +use rubato::{Fft, FixedSync, Resampler, WindowFunction}; +use waveadapter::{SampleFormat, WavReader, WavSpec, WavWriter}; + +/// Resample a wav file with the rubato FFT resampler. +#[derive(Parser)] +#[command(version)] +struct Options { + /// Wav file to read. + input: String, + + /// Wav file to write. + output: String, + + /// Sample rate of the output file, in Hz. + output_rate: usize, + + /// Sample format of the output file [default: same as the input file] + #[arg(short, long, value_enum, ignore_case = true)] + format: Option, + + /// Gain in dB to apply to the resampled audio. Use a small negative value + /// to add headroom, since resampling can overshoot the peak level of the + /// input and clip in the integer output formats. + #[arg(short, long, default_value_t = 0.0, allow_negative_numbers = true)] + gain: f64, + + /// Resampler chunk size in frames. A smaller value gives a lower delay, at + /// the cost of a lower cutoff frequency of the anti-aliasing filter. + #[arg(short, long, default_value_t = 1024)] + chunk: usize, + + /// Anti-aliasing window function. + #[arg(short, long, value_enum, ignore_case = true, default_value_t = Window::BlackmanHarris2)] + window: Window, +} + +/// The sample formats that waveadapter can write. +/// +/// The names are spelled like the [SampleFormat] variants they map to, so that +/// the values this tool accepts match the ones it prints. +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] +enum Format { + /// Unsigned 8 bit integer. + #[value(name = "U8")] + U8, + /// Signed 16 bit integer. + #[value(name = "I16")] + I16, + /// Signed 24 bit integer, packed in 3 bytes. + #[value(name = "I24_3")] + I24_3, + /// Signed 24 bit integer, left justified in 4 bytes. + #[value(name = "I24_4")] + I24_4, + /// Signed 32 bit integer. + #[value(name = "I32")] + I32, + /// 32 bit float. + #[value(name = "F32")] + F32, + /// 64 bit float. + #[value(name = "F64")] + F64, +} + +impl From for SampleFormat { + fn from(format: Format) -> Self { + match format { + Format::U8 => SampleFormat::U8, + Format::I16 => SampleFormat::I16, + Format::I24_3 => SampleFormat::I24_3, + Format::I24_4 => SampleFormat::I24_4, + Format::I32 => SampleFormat::I32, + Format::F32 => SampleFormat::F32, + Format::F64 => SampleFormat::F64, + } + } +} + +/// The anti-aliasing window functions the FFT resampler accepts. +/// +/// Spelled like the [WindowFunction] variants they map to, for the same reason +/// as [Format]. +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] +enum Window { + #[value(name = "Blackman")] + Blackman, + #[value(name = "Blackman2")] + Blackman2, + #[value(name = "BlackmanHarris")] + BlackmanHarris, + #[value(name = "BlackmanHarris2")] + BlackmanHarris2, + #[value(name = "Hann")] + Hann, + #[value(name = "Hann2")] + Hann2, +} + +impl From for WindowFunction { + fn from(window: Window) -> Self { + match window { + Window::Blackman => WindowFunction::Blackman, + Window::Blackman2 => WindowFunction::Blackman2, + Window::BlackmanHarris => WindowFunction::BlackmanHarris, + Window::BlackmanHarris2 => WindowFunction::BlackmanHarris2, + Window::Hann => WindowFunction::Hann, + Window::Hann2 => WindowFunction::Hann2, + } + } +} + +fn run(opts: Options) -> Result<(), Box> { + // Read the whole input file into an interleaved buffer of f64 samples. + // The reader converts from whatever format the file stores. + let mut reader = WavReader::new(BufReader::new(File::open(&opts.input)?))?; + let channels = reader.channels(); + let rate_in = reader.sample_rate(); + // A format the float path cannot decode, A-law for example, cannot be + // resampled here. Say so up front instead of failing inside the read. + let format_in = reader.sample_format().ok_or_else(|| { + format!( + "the input file cannot be decoded, format code 0x{:04X} with {} bits per sample", + reader.params().format_code, + reader.params().bits_per_sample + ) + })?; + println!( + "Input: {}, {} ch, {} Hz, {:?}, {} frames", + opts.input, + channels, + rate_in, + format_in, + reader.frames() + ); + let input = reader.read_all_to_float::()?; + + // Write the same sample format as the input file unless told otherwise. + let format_out = opts.format.map(SampleFormat::from).unwrap_or(format_in); + + // One sub chunk per chunk, so the chunk size is also the FFT block size. + let window = WindowFunction::from(opts.window); + let mut resampler = Fft::::new_custom( + rate_in, + opts.output_rate, + opts.chunk, + 1, + channels, + window, + FixedSync::Both, + )?; + + // With a single sub chunk the chunk sizes are the FFT block sizes. The + // cutoff is relative to the input Nyquist frequency, so scale it by half + // the input rate to report it in Hz. + println!( + "Config: chunks of {} -> {} frames, {:?} window, cutoff {:.0} Hz", + resampler.fft_size_in(), + resampler.fft_size_out(), + window, + resampler.cutoff() as f64 * rate_in as f64 / 2.0 + ); + + // Resample the entire clip in one call. This runs the chunk loop, trims the + // startup delay, and returns a buffer holding exactly the resampled frames. + let start = Instant::now(); + let mut output = resampler.process_all(&input, input.frames(), None)?; + println!( + "Resampled {} frames to {} frames in {:?}", + input.frames(), + output.frames(), + start.elapsed() + ); + + // Scale the resampled audio. Doing this after resampling is what matters + // for clipping, since the resampled peak can sit above the input peak. + if opts.gain != 0.0 { + let scale = 10.0f64.powf(opts.gain / 20.0); + for chan in 0..output.channels() { + for frame in 0..output.frames() { + let value = output.read_sample(chan, frame).unwrap() * scale; + output.write_sample(chan, frame, &value); + } + } + } + + // Report the peak, to make it easy to pick a gain that avoids clipping. + let peak = + (0..output.channels()).fold(0.0f64, |peak, chan| peak.max(output.channel_peak(chan))); + println!( + "Peak level after gain: {:.2} dBFS", + 20.0 * peak.max(1e-12).log10() + ); + + let spec = WavSpec::new(channels, opts.output_rate, format_out); + let mut writer = WavWriter::new(BufWriter::new(File::create(&opts.output)?), spec)?; + let clipped = writer.write_float_buffer(&output)?; + writer.finalize()?; + println!( + "Output: {}, {} ch, {} Hz, {:?}, {} frames, {} clipped samples", + opts.output, + channels, + opts.output_rate, + format_out, + output.frames(), + clipped + ); + Ok(()) +} + +fn main() { + if let Err(err) = run(Options::parse()) { + eprintln!("Error: {err}"); + std::process::exit(1); + } +} From b0f75654f6d4c05f3f5aee1f5c4bd4599e0f8094 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 00:48:16 +0200 Subject: [PATCH 04/12] Fix doc link to private MAX_CROSSFADE_LEN The public docs for Slip::new linked to a private constant, which rustdoc warns about and readers cannot follow. State the values instead. --- src/slip.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slip.rs b/src/slip.rs index a98acef..6a0fc94 100644 --- a/src/slip.rs +++ b/src/slip.rs @@ -295,8 +295,8 @@ where /// /// Parameters are: /// - `chunk_size`: Size of the fixed side (input or output, see `fixed`) in frames. Must be at - /// least 4. The internal crossfade grows with the chunk up to [MAX_CROSSFADE_LEN] frames - /// (reached at `2 * MAX_CROSSFADE_LEN + 2` = 258 and above) and shrinks for smaller chunks. + /// least 4. The internal crossfade grows with the chunk up to 128 frames (reached at a chunk + /// size of 258 and above) and shrinks for smaller chunks. /// - `nbr_channels`: Number of channels in input/output. /// - `fixed`: Whether the input or the output chunk size is fixed. pub fn new( From c4488b7da36ae001763a354be88a71cea2611942 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 00:51:56 +0200 Subject: [PATCH 05/12] Fix stale names in the Debug impls Async and Fft both printed themselves as Fast, left over from when the separate Fast and FFT resamplers were united, and both had trailing commas inside the field name strings. --- src/asynchro.rs | 10 +++++----- src/synchro.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/asynchro.rs b/src/asynchro.rs index a5915f8..19f422a 100644 --- a/src/asynchro.rs +++ b/src/asynchro.rs @@ -118,12 +118,12 @@ pub struct Async { impl fmt::Debug for Async { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("Fast") + fmt.debug_struct("Async") .field("nbr_channels", &self.nbr_channels) - .field("chunk_size,", &self.chunk_size) - .field("max_chunk_size,", &self.max_chunk_size) - .field("needed_input_size,", &self.needed_input_size) - .field("needed_output_size,", &self.needed_output_size) + .field("chunk_size", &self.chunk_size) + .field("max_chunk_size", &self.max_chunk_size) + .field("needed_input_size", &self.needed_input_size) + .field("needed_output_size", &self.needed_output_size) .field("last_index", &self.last_index) .field("current_buffer_fill", &self.current_buffer_fill) .field("resample_ratio", &self.resample_ratio) diff --git a/src/synchro.rs b/src/synchro.rs index 901f3eb..0b841c0 100644 --- a/src/synchro.rs +++ b/src/synchro.rs @@ -69,10 +69,10 @@ impl fmt::Debug for Fft { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("Fft") .field("nbr_channels", &self.nbr_channels) - .field("chunk_size_in,", &self.chunk_size_in) - .field("chunk_size_out,", &self.chunk_size_out) - .field("fft_size_in,", &self.fft_size_in) - .field("fft_size_out,", &self.fft_size_out) + .field("chunk_size_in", &self.chunk_size_in) + .field("chunk_size_out", &self.chunk_size_out) + .field("fft_size_in", &self.fft_size_in) + .field("fft_size_out", &self.fft_size_out) .field("overlaps[0].len()", &self.overlaps[0].len()) .field("input_scratch[0].len()", &self.input_scratch[0].len()) .field("output_scratch[0].len()", &self.output_scratch[0].len()) From 03950dcfce5e244b7761c089064409b132929f85 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 20:40:30 +0200 Subject: [PATCH 06/12] Fix clippy warnings Replace the manual modulo checks with is_multiple_of, which recent clippy versions flag, and allow the argument count of new_with_sinc_interpolator now that it also takes the filter cutoff. CI runs clippy with -D warnings, so both were fatal. --- src/asynchro.rs | 1 + src/sinc_interpolator/mod.rs | 5 ++++- src/sinc_interpolator/sinc_interpolator_avx.rs | 2 +- src/sinc_interpolator/sinc_interpolator_neon.rs | 2 +- src/sinc_interpolator/sinc_interpolator_sse.rs | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/asynchro.rs b/src/asynchro.rs index 19f422a..b1c622e 100644 --- a/src/asynchro.rs +++ b/src/asynchro.rs @@ -335,6 +335,7 @@ where /// - `chunk_size`: Size of output data in frames. /// - `nbr_channels`: Number of channels in input/output. #[cfg_attr(feature = "bench_asyncro", visibility::make(pub))] + #[allow(clippy::too_many_arguments)] fn new_with_sinc_interpolator( resample_ratio: f64, max_resample_ratio_relative: f64, diff --git a/src/sinc_interpolator/mod.rs b/src/sinc_interpolator/mod.rs index a573683..3a36a04 100644 --- a/src/sinc_interpolator/mod.rs +++ b/src/sinc_interpolator/mod.rs @@ -277,7 +277,10 @@ where f_cutoff: f32, window: WindowFunction, ) -> Self { - assert!(sinc_len % 8 == 0, "Sinc length must be a multiple of 8"); + assert!( + sinc_len.is_multiple_of(8), + "Sinc length must be a multiple of 8" + ); let raw_sincs: Vec> = make_sincs(sinc_len, oversampling_factor, f_cutoff, window); let sincs = raw_sincs .into_iter() diff --git a/src/sinc_interpolator/sinc_interpolator_avx.rs b/src/sinc_interpolator/sinc_interpolator_avx.rs index 7a3a929..d9f5d04 100644 --- a/src/sinc_interpolator/sinc_interpolator_avx.rs +++ b/src/sinc_interpolator/sinc_interpolator_avx.rs @@ -264,7 +264,7 @@ where return Err(MissingCpuFeature(*feature)); } - assert!(sinc_len % 8 == 0, "Sinc length must be a multiple of 8."); + assert!(sinc_len.is_multiple_of(8), "Sinc length must be a multiple of 8."); let raw_sincs: Vec> = make_sincs(sinc_len, oversampling_factor, f_cutoff, window); let sincs = raw_sincs .into_iter() diff --git a/src/sinc_interpolator/sinc_interpolator_neon.rs b/src/sinc_interpolator/sinc_interpolator_neon.rs index 8404d2e..34e0614 100644 --- a/src/sinc_interpolator/sinc_interpolator_neon.rs +++ b/src/sinc_interpolator/sinc_interpolator_neon.rs @@ -267,7 +267,7 @@ where return Err(MissingCpuFeature(*feature)); } - assert!(sinc_len % 8 == 0, "Sinc length must be a multiple of 8."); + assert!(sinc_len.is_multiple_of(8), "Sinc length must be a multiple of 8."); let raw_sincs: Vec> = make_sincs(sinc_len, oversampling_factor, f_cutoff, window); let sincs = raw_sincs .into_iter() diff --git a/src/sinc_interpolator/sinc_interpolator_sse.rs b/src/sinc_interpolator/sinc_interpolator_sse.rs index cade8b7..909d60c 100644 --- a/src/sinc_interpolator/sinc_interpolator_sse.rs +++ b/src/sinc_interpolator/sinc_interpolator_sse.rs @@ -264,7 +264,7 @@ where return Err(MissingCpuFeature(*feature)); } - assert!(sinc_len % 8 == 0, "Sinc length must be a multiple of 8."); + assert!(sinc_len.is_multiple_of(8), "Sinc length must be a multiple of 8."); let raw_sincs: Vec> = make_sincs(sinc_len, oversampling_factor, f_cutoff, window); let sincs = raw_sincs .into_iter() From 4406cf08645d65e9cf22944374a61f0f7aa71cc2 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 20:40:44 +0200 Subject: [PATCH 07/12] Drop two redundant examples process_all_f64 only differed from process_f64 by calling process_all_into_buffer, which is now shown by a doc example on the method itself. process_i16 duplicated process_f64 apart from wrapping the input in an integer byte adapter, which audioadapter documents. Removing it also drops the audioadapter-sample dev dependency. --- Cargo.toml | 1 - examples/process_all_f64.rs | 158 ------------------------------------ examples/process_i16.rs | 150 ---------------------------------- src/lib.rs | 33 ++++++++ 4 files changed, 33 insertions(+), 309 deletions(-) delete mode 100644 examples/process_all_f64.rs delete mode 100644 examples/process_i16.rs diff --git a/Cargo.toml b/Cargo.toml index bf75dd5..d6b8783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ log = "0.4.18" approx = "0.5.1" test-log = "0.2.16" test-case = "3" -audioadapter-sample = "5.1" waveadapter = "0.2" clap = { version = "4.6", features = ["derive"] } diff --git a/examples/process_all_f64.rs b/examples/process_all_f64.rs deleted file mode 100644 index bf975ff..0000000 --- a/examples/process_all_f64.rs +++ /dev/null @@ -1,158 +0,0 @@ -extern crate rubato; -use audioadapter_buffers::direct::InterleavedSlice; -use rubato::{ - Async, FixedAsync, PolynomialDegree, Resampler, SincInterpolationParameters, - SincInterpolationType, WindowFunction, -}; -#[cfg(feature = "fft_resampler")] -use rubato::{Fft, FixedSync}; -use std::convert::TryInto; -use std::env; -use std::fs::File; -use std::io::prelude::{Read, Seek, Write}; -use std::io::{BufReader, BufWriter}; -use std::time::Instant; - -extern crate env_logger; -extern crate log; -use env_logger::Builder; -use log::LevelFilter; -const BYTE_PER_SAMPLE: usize = 8; - -// A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. -// This example is a variation of the `process_f64`, example that uses the `process_all_into_buffer` -// convenience method to process the entire file with a single call. - -/// Helper to read an entire file to memory as f64 values -fn read_file(inbuffer: &mut R) -> Vec { - let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; - let mut data = Vec::new(); - loop { - let bytes_read = inbuffer.read(&mut buffer).unwrap(); - if bytes_read == 0 { - break; - } - let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); - data.push(value); - } - data -} - -/// Helper to write all frames to a file -fn write_file(data: &[f64], output: &mut W, values_to_write: usize) { - for value in data.iter().take(values_to_write) { - let bytes = value.to_le_bytes(); - output.write_all(&bytes).unwrap(); - } -} - -fn main() { - // init logger - let mut builder = Builder::from_default_env(); - builder.filter(None, LevelFilter::Debug).init(); - - let resampler_type = env::args() - .nth(1) - .expect("Please specify a resampler type, one of:\nSincFixedIn\nSincFixedOut\nFastFixedIn\nFastFixedOut\nFftFixedIn\nFftFixedOut\nFftFixedInOut"); - - let file_in = env::args().nth(2).expect("Please specify an input file."); - let file_out = env::args().nth(3).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); - - let fs_in_str = env::args() - .nth(4) - .expect("Please specify an input sample rate"); - let fs_out_str = env::args() - .nth(5) - .expect("Please specify an output sample rate"); - let fs_in = fs_in_str.parse::().unwrap(); - let fs_out = fs_out_str.parse::().unwrap(); - println!("Resampling from {} to {}", fs_in, fs_out); - - let channels_str = env::args() - .nth(6) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - - println!("Copy input file to buffer"); - let file_in_disk = File::open(file_in).expect("Can't open file"); - let mut file_in_reader = BufReader::new(file_in_disk); - let indata = read_file(&mut file_in_reader); - let nbr_input_frames = indata.len() / channels; - - let f_ratio = fs_out as f64 / fs_in as f64; - - // Create buffer for storing output - let mut outdata = vec![0.0; 2 * channels * (nbr_input_frames as f64 * f_ratio) as usize]; - - println!("Creating resampler"); - // Create resampler - let mut resampler: Box> = match resampler_type.as_str() { - "SincFixedInput" => { - let sinc_len = 128; - let oversampling_factor = 256; - let interpolation = SincInterpolationType::Quadratic; - let window = WindowFunction::Blackman2; - - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - Box::new(Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Input).unwrap()) - } - "SincFixedOutput" => { - let sinc_len = 128; - let oversampling_factor = 512; - let interpolation = SincInterpolationType::Cubic; - let window = WindowFunction::Blackman2; - - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - Box::new(Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Output).unwrap()) - } - "PolyFixedInput" => { - Box::new(Async::::new_poly(f_ratio, 1.1, PolynomialDegree::Septic, 1024, channels, FixedAsync::Input).unwrap()) - } - "PolyFixedOutput" => { - Box::new(Async::::new_poly(f_ratio, 1.1, PolynomialDegree::Septic, 1024, channels, FixedAsync::Output).unwrap()) - } - #[cfg(feature = "fft_resampler")] - "FftFixedInput" => { - Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Input).unwrap()) - } - #[cfg(feature = "fft_resampler")] - "FftFixedOutput" => { - Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Output).unwrap()) - } - #[cfg(feature = "fft_resampler")] - "FftFixedBoth" => { - Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Both).unwrap()) - } - _ => panic!("Unknown resampler type {}\nMust be one of SincFixedInput, SincFixedOutput, PolyFixedInput, PolyFixedOutput, FftFixedInput, FftFixedOutput, FftFixedBoth", resampler_type), - }; - - // Prepare - let input_adapter = InterleavedSlice::new(&indata, channels, nbr_input_frames).unwrap(); - let outdata_capacity = outdata.len() / channels; - let mut output_adapter = - InterleavedSlice::new_mut(&mut outdata, channels, outdata_capacity).unwrap(); - - println!("Processing..."); - let start = Instant::now(); - - let (nbr_in, nbr_out) = resampler - .process_all_into_buffer(&input_adapter, &mut output_adapter, nbr_input_frames, None) - .unwrap(); - - let duration = start.elapsed(); - println!("Resampling took: {:?}", duration); - - println!( - "Processed {} input frames into {} output frames", - nbr_in, nbr_out - ); - - println!("Write output to file, trimming off the silent frames from both ends."); - let mut file_out_disk = BufWriter::new(File::create(file_out).unwrap()); - write_file(&outdata, &mut file_out_disk, nbr_out * channels); -} diff --git a/examples/process_i16.rs b/examples/process_i16.rs deleted file mode 100644 index 6a03f95..0000000 --- a/examples/process_i16.rs +++ /dev/null @@ -1,150 +0,0 @@ -extern crate rubato; -use audioadapter_buffers::number_to_float::InterleavedNumbers; -use audioadapter_sample::sample::I16_LE; - -use rubato::{ - Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType, - WindowFunction, -}; -use std::env; -use std::fs::File; -use std::io::prelude::{Read, Write}; -use std::time::Instant; - -extern crate env_logger; -extern crate log; -use env_logger::Builder; -use log::LevelFilter; -const BYTE_PER_SAMPLE: usize = std::mem::size_of::(); - -// A resampler app that reads a raw file of little-endian 16 bit integers, and writes the output in the same format. -// The command line arguments are resampler type, input filename, output filename, input samplerate, output samplerate, number of channels -// To use a sinc resampler with fixed input size to resample the file `sine_i16_2ch.raw` from 44.1kHz to 192kHz, and assuming the file has two channels, the command is: -// ``` -// cargo run --release --example process_f64 SincFixedInput sine_i16_2ch.raw test.raw 44100 192000 2 -// ``` -// There are two helper python scripts for testing. -// - `makesineraw.py` to generate test files in raw format. -// Run it with the `-h` flag for instructions. -// - `analyze_result.py` to analyze the result. -// This takes three arguments: number of channels, samplerate, and sample format. -// Example, to analyze the file created above: -// ``` -// python examples/analyze_result.py test.raw 2 192000 i16 -// ``` - -/// Helper to read an entire file to memory as f64 values -fn read_file(filename: &str) -> Vec { - let mut f = File::open(filename).expect("Can't open file"); - let mut data = vec![]; - f.read_to_end(&mut data).unwrap(); - data -} - -/// Helper to write all frames to a file -fn write_file(filename: &str, data: &[u8], bytes_to_skip: usize, bytes_to_write: usize) { - let mut f = File::create(filename).expect("Can't open file"); - f.write_all(&data[bytes_to_skip..bytes_to_skip + bytes_to_write]) - .expect("Failed to write data to file"); -} - -fn main() { - // init logger - let mut builder = Builder::from_default_env(); - builder.filter(None, LevelFilter::Debug).init(); - - let file_in = env::args().nth(1).expect("Please specify an input file."); - let file_out = env::args().nth(2).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); - - let fs_in_str = env::args() - .nth(3) - .expect("Please specify an input sample rate"); - let fs_out_str = env::args() - .nth(4) - .expect("Please specify an output sample rate"); - let fs_in = fs_in_str.parse::().unwrap(); - let fs_out = fs_out_str.parse::().unwrap(); - println!("Resampling from {} to {}", fs_in, fs_out); - - let channels_str = env::args() - .nth(5) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - - println!("Copy input file to buffer"); - - let indata = read_file(&file_in); - let nbr_input_frames = indata.len() / (channels * BYTE_PER_SAMPLE); - - let f_ratio = fs_out as f64 / fs_in as f64; - - // Create buffer for storing output - let mut outdata: Vec = - vec![0; 2 * channels * BYTE_PER_SAMPLE * (nbr_input_frames as f64 * f_ratio) as usize]; - - println!("Creating resampler"); - let sinc_len = 128; - let oversampling_factor = 256; - let interpolation = SincInterpolationType::Quadratic; - let window = WindowFunction::Blackman2; - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - let mut resampler = - Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Input).unwrap(); - - // Prepare - let mut input_frames_next = resampler.input_frames_next(); - let resampler_delay = resampler.output_delay(); - - let input_adapter = - InterleavedNumbers::<&[I16_LE], f32>::new_from_bytes(&indata, channels, nbr_input_frames) - .unwrap(); - let outdata_capacity = outdata.len() / (channels * BYTE_PER_SAMPLE); - let mut output_adapter = InterleavedNumbers::<&mut [I16_LE], f32>::new_from_bytes_mut( - &mut outdata, - channels, - outdata_capacity, - ) - .unwrap(); - - println!("Process all full chunks"); - let start = Instant::now(); - let mut indexing = Indexing::new(); - let mut input_frames_left = nbr_input_frames; - - while input_frames_left >= input_frames_next { - let (nbr_in, nbr_out) = resampler - .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) - .unwrap(); - - indexing.input_offset += nbr_in; - indexing.output_offset += nbr_out; - input_frames_left -= nbr_in; - input_frames_next = resampler.input_frames_next(); - } - - println!("Process a partial chunk with the last frames."); - indexing.partial_len = Some(input_frames_left); - let (_nbr_in, _nbr_out) = resampler - .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) - .unwrap(); - - let duration = start.elapsed(); - println!("Resampling took: {:?}", duration); - - let nbr_output_frames = (nbr_input_frames as f32 * fs_out as f32 / fs_in as f32) as usize; - println!( - "Processed {} input frames into {} output frames", - nbr_input_frames, nbr_output_frames - ); - - println!("Write output to file, trimming off the silent frames from both ends."); - write_file( - &file_out, - &outdata, - resampler_delay * channels * BYTE_PER_SAMPLE, - nbr_output_frames * channels * BYTE_PER_SAMPLE, - ); -} diff --git a/src/lib.rs b/src/lib.rs index ac4224c..23cbe97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,6 +282,39 @@ where /// [process_into_buffer](Resampler::process_into_buffer). /// /// Returns the lengths of the original input and the resampled output. + /// + /// # Example + /// + /// Resample one second of 44.1 kHz audio to 48 kHz, into a buffer allocated + /// up front. This is the variant to use when allocating during processing is + /// not acceptable, such as in a realtime thread. See + /// [process_all](Resampler::process_all) for the allocating counterpart. + /// + /// ``` + /// use audioadapter_buffers::owned::InterleavedOwned; + /// use rubato::{Fft, FixedSync, Resampler}; + /// + /// let channels = 2; + /// let input_len = 44100; + /// let input = InterleavedOwned::::new(0.0, channels, input_len); + /// + /// let mut resampler = + /// Fft::::new(44100, 48000, 1024, channels, FixedSync::Both).unwrap(); + /// + /// // Allocate an output buffer that is guaranteed to be big enough. + /// let needed_len = resampler.process_all_needed_output_len(input_len); + /// let mut output = InterleavedOwned::::new(0.0, channels, needed_len); + /// + /// let (consumed, produced) = resampler + /// .process_all_into_buffer(&input, &mut output, input_len, None) + /// .unwrap(); + /// + /// // The resampled audio is the first `produced` frames of the buffer. + /// // The rest is padding, since the buffer is sized for the worst case. + /// assert_eq!(consumed, input_len); + /// assert!(produced >= 48000); + /// assert!(produced <= needed_len); + /// ``` fn process_all_into_buffer( &mut self, buffer_in: &dyn Adapter, From ed77619be54f669e87145e5f9be918714b34a91c Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 20:40:54 +0200 Subject: [PATCH 08/12] Parse example arguments with clap The examples took a long list of positional arguments with no help text. Give them all named options with defaults instead, matching the new resample_wav example. Also merge fixedout_ramp64 and polyfixedin_ramp64 into ramp_ratio_f64. They only differed by resampler type and which side is fixed, both of which are now options, so the merged example also covers the two combinations neither of them had. --- examples/adjust_ratio_f64.rs | 102 ++++++++++----- examples/fixedout_ramp64.rs | 170 ------------------------- examples/polyfixedin_ramp64.rs | 159 ----------------------- examples/process_f64.rs | 167 +++++++++++++++--------- examples/ramp_ratio_f64.rs | 225 +++++++++++++++++++++++++++++++++ 5 files changed, 401 insertions(+), 422 deletions(-) delete mode 100644 examples/fixedout_ramp64.rs delete mode 100644 examples/polyfixedin_ramp64.rs create mode 100644 examples/ramp_ratio_f64.rs diff --git a/examples/adjust_ratio_f64.rs b/examples/adjust_ratio_f64.rs index fcbaa9e..9d6db28 100644 --- a/examples/adjust_ratio_f64.rs +++ b/examples/adjust_ratio_f64.rs @@ -1,11 +1,11 @@ extern crate rubato; use audioadapter_buffers::direct::InterleavedSlice; +use clap::{Parser, ValueEnum}; use rubato::{ Async, FixedAsync, PolynomialDegree, Resampler, SincInterpolationParameters, SincInterpolationType, Slip, WindowFunction, }; use std::convert::TryInto; -use std::env; use std::fs::File; use std::io::prelude::{Read, Seek, Write}; use std::io::{BufReader, BufWriter}; @@ -18,7 +18,7 @@ use log::LevelFilter; const BYTE_PER_SAMPLE: usize = 8; // A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. -// Unlike the `process_all_f64` example, which converts between two fixed rates, this one uses one of the +// Unlike the `process_f64` example, which converts between two fixed rates, this one uses one of the // *adjustable* resamplers to apply a small, constant rate offset. This is the clock-drift / rate-matching case: // the nominal input and output rates are equal (ratio 1:1), and the resampler is nudged by a user-selected // offset given in parts per million (ppm). A positive offset produces slightly more output frames than input, @@ -26,15 +26,63 @@ const BYTE_PER_SAMPLE: usize = 8; // // The offset is applied through the `Resampler::as_adjustable` capability accessor, so the same code drives every // adjustable resampler type without knowing the concrete type. The synchronous FFT resamplers cannot change -// ratio and are therefore not offered here; use the `process_all_f64` example for fixed-ratio conversion. +// ratio and are therefore not offered here; use the `process_f64` example for fixed-ratio conversion. +// For a ratio that changes while processing, see the `ramp_ratio_f64` example. // -// The command line arguments are resampler type, input filename, output filename, number of channels, -// and the rate offset in ppm. The adjustable resamplers support offsets up to roughly +/- 10%. +// The adjustable resamplers support offsets up to roughly +/- 10%. // To apply a +50 ppm offset to the two-channel file `sine_f64_2ch.raw` using the Slip resampler: // ``` -// cargo run --release --example adjust_ratio_f64 SlipFixedOutput sine_f64_2ch.raw test.raw 2 50 +// cargo run --release --example adjust_ratio_f64 sine_f64_2ch.raw test.raw -r SlipFixedOutput -o 50 // ``` +/// Apply a small constant rate offset to a raw file of 64 bit floats. +#[derive(Parser)] +#[command(version)] +struct Options { + /// Raw file of little-endian 64 bit floats to read. + input: String, + + /// Raw file to write, in the same format. + output: String, + + /// Resampler to use. Only the adjustable types are offered, since the + /// synchronous FFT resamplers cannot change ratio. + #[arg(short, long, value_enum, ignore_case = true, default_value_t = ResamplerType::SlipFixedOutput)] + resampler: ResamplerType, + + /// Number of channels in the file. + #[arg(short, long, default_value_t = 2)] + channels: usize, + + /// Rate offset in parts per million. Positive gives slightly more output + /// frames than input, negative slightly fewer. + #[arg(short, long, default_value_t = 50.0, allow_negative_numbers = true)] + offset: f64, +} + +/// The adjustable resampler types this example can build. +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] +enum ResamplerType { + /// Sinc interpolation, fixed input size. + #[value(name = "SincFixedInput")] + SincFixedInput, + /// Sinc interpolation, fixed output size. + #[value(name = "SincFixedOutput")] + SincFixedOutput, + /// Polynomial interpolation, fixed input size. + #[value(name = "PolyFixedInput")] + PolyFixedInput, + /// Polynomial interpolation, fixed output size. + #[value(name = "PolyFixedOutput")] + PolyFixedOutput, + /// Slip resampler, fixed input size. + #[value(name = "SlipFixedInput")] + SlipFixedInput, + /// Slip resampler, fixed output size. + #[value(name = "SlipFixedOutput")] + SlipFixedOutput, +} + /// Helper to read an entire file to memory as f64 values fn read_file(inbuffer: &mut R) -> Vec { let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; @@ -66,23 +114,11 @@ fn main() { let mut builder = Builder::from_default_env(); builder.filter(None, LevelFilter::Debug).init(); - let resampler_type = env::args().nth(1).expect( - "Please specify a resampler type, one of:\nSincFixedInput\nSincFixedOutput\nPolyFixedInput\nPolyFixedOutput\nSlipFixedInput\nSlipFixedOutput", - ); - - let file_in = env::args().nth(2).expect("Please specify an input file."); - let file_out = env::args().nth(3).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); + let opts = Options::parse(); + let channels = opts.channels; + let offset_ppm = opts.offset; + println!("Opening files: {}, {}", opts.input, opts.output); - let channels_str = env::args() - .nth(4) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - - let offset_str = env::args() - .nth(5) - .expect("Please specify the rate offset in ppm"); - let offset_ppm = offset_str.parse::().unwrap(); let rel_ratio = 1.0 + offset_ppm / 1_000_000.0; println!( "Applying a rate offset of {} ppm (relative ratio {})", @@ -90,7 +126,7 @@ fn main() { ); println!("Copy input file to buffer"); - let file_in_disk = File::open(file_in).expect("Can't open file"); + let file_in_disk = File::open(&opts.input).expect("Can't open file"); let mut file_in_reader = BufReader::new(file_in_disk); let indata = read_file(&mut file_in_reader); let nbr_input_frames = indata.len() / channels; @@ -103,8 +139,8 @@ fn main() { // Every branch is built at the nominal ratio of 1.0. The asynchronous resamplers get a maximum // relative ratio of 1.1, matching the Slip resampler's built-in +/- 10% range. let chunk_size = 1024; - let mut resampler: Box> = match resampler_type.as_str() { - "SincFixedInput" => { + let mut resampler: Box> = match opts.resampler { + ResamplerType::SincFixedInput => { let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2) .oversampling_factor(256) .interpolation(SincInterpolationType::Quadratic); @@ -113,7 +149,7 @@ fn main() { .unwrap(), ) } - "SincFixedOutput" => { + ResamplerType::SincFixedOutput => { let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2) .oversampling_factor(256) .interpolation(SincInterpolationType::Quadratic); @@ -122,7 +158,7 @@ fn main() { .unwrap(), ) } - "PolyFixedInput" => Box::new( + ResamplerType::PolyFixedInput => Box::new( Async::::new_poly( 1.0, 1.1, @@ -133,7 +169,7 @@ fn main() { ) .unwrap(), ), - "PolyFixedOutput" => Box::new( + ResamplerType::PolyFixedOutput => Box::new( Async::::new_poly( 1.0, 1.1, @@ -144,16 +180,12 @@ fn main() { ) .unwrap(), ), - "SlipFixedInput" => { + ResamplerType::SlipFixedInput => { Box::new(Slip::::new(chunk_size, channels, FixedAsync::Input).unwrap()) } - "SlipFixedOutput" => { + ResamplerType::SlipFixedOutput => { Box::new(Slip::::new(chunk_size, channels, FixedAsync::Output).unwrap()) } - _ => panic!( - "Unknown or non-adjustable resampler type {}\nMust be one of SincFixedInput, SincFixedOutput, PolyFixedInput, PolyFixedOutput, SlipFixedInput, SlipFixedOutput", - resampler_type - ), }; // Recover the adjust-ratio capability from the trait object and apply the offset once. Since the @@ -193,6 +225,6 @@ fn main() { ); println!("Write output to file, trimming off the silent frames from both ends."); - let mut file_out_disk = BufWriter::new(File::create(file_out).unwrap()); + let mut file_out_disk = BufWriter::new(File::create(&opts.output).unwrap()); write_file(&outdata, &mut file_out_disk, nbr_out * channels); } diff --git a/examples/fixedout_ramp64.rs b/examples/fixedout_ramp64.rs deleted file mode 100644 index ac95072..0000000 --- a/examples/fixedout_ramp64.rs +++ /dev/null @@ -1,170 +0,0 @@ -extern crate rubato; -use audioadapter_buffers::direct::InterleavedSlice; -use rubato::{ - Adjustable, Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, - SincInterpolationType, WindowFunction, -}; -use std::convert::TryInto; -use std::env; -use std::fs::File; -use std::io::prelude::{Read, Seek, Write}; -use std::io::{BufReader, BufWriter}; -use std::time::Instant; - -extern crate env_logger; -extern crate log; -use env_logger::Builder; -use log::LevelFilter; - -const BYTE_PER_SAMPLE: usize = std::mem::size_of::(); - -// A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. -// While resampling, it ramps the resampling ratio from 100% to a user-provided value, during a given time duration (measured in output time). -// This version takes a varying number of input samples per chunk, and outputs a fixed number of samples. -// The command line arguments are input filename, output filename, input samplerate, output samplerate, -// number of channels, final relative ratio in percent, and ramp duration in seconds. -// To resample the file `sine_f64_2ch.raw` from 44.1kHz to 192kHz, and assuming the file has two channels, -// and that the resampling ratio should be ramped to 150% during 3 seconds, the command is: -// ``` -// cargo run --release --example fixedout_ramp64 sine_f64_2ch.raw test.raw 44100 192000 2 150 3 -// ``` -// There are two helper python scripts for testing. -// - `makesineraw.py` to generate test files in raw format. -// Run it with the `-h` flag for instructions. -// - `analyze_result.py` to analyze the result. -// This takes three arguments: number of channels, samplerate, and sample format. -// Example, to analyze the file created above: -// ``` -// python examples/analyze_result.py test.raw 2 192000 f64 -// ``` - -/// Helper to read an entire file to memory as f64 values -fn read_file(inbuffer: &mut R) -> Vec { - let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; - let mut data = Vec::new(); - loop { - let bytes_read = inbuffer.read(&mut buffer).unwrap(); - if bytes_read == 0 { - break; - } - let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); - data.push(value); - } - data -} - -/// Helper to write all frames to a file -fn write_file(data: &[f64], output: &mut W) { - for value in data.iter() { - let bytes = value.to_le_bytes(); - output.write_all(&bytes).unwrap(); - } -} - -fn main() { - // init logger - let mut builder = Builder::from_default_env(); - builder.filter(None, LevelFilter::Trace).init(); - - let file_in = env::args().nth(1).expect("Please specify an input file."); - let file_out = env::args().nth(2).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); - - let fs_in_str = env::args() - .nth(3) - .expect("Please specify an input sample rate"); - let fs_out_str = env::args() - .nth(4) - .expect("Please specify an output sample rate"); - let fs_in = fs_in_str.parse::().unwrap(); - let fs_out = fs_out_str.parse::().unwrap(); - println!("Resampling from {} to {}", fs_in, fs_out); - - let channels_str = env::args() - .nth(5) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - - let ratio_str = env::args() - .nth(6) - .expect("Please specify final resampling ratio in percent"); - let final_ratio = ratio_str.parse::().unwrap(); - - let duration_str = env::args() - .nth(7) - .expect("Please specify ramp time in seconds"); - let duration = duration_str.parse::().unwrap(); - - println!("Copy input file to buffer"); - let file_in_disk = File::open(file_in).expect("Can't open file"); - let mut file_in_reader = BufReader::new(file_in_disk); - let indata = read_file(&mut file_in_reader); - let nbr_input_frames = indata.len() / channels; - - let f_ratio = fs_out as f64 / fs_in as f64; - - // Create buffer for storing output, size is preliminary and may grow - let mut outdata = - Vec::with_capacity(2 * channels * (nbr_input_frames as f64 * f_ratio) as usize); - - // Balanced for async, see the fixedin64 example for more config examples - let sinc_len = 128; - let oversampling_factor = 2048; - let interpolation = SincInterpolationType::Linear; - let window = WindowFunction::Blackman2; - - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - - let chunksize = 1024; - let target_ratio = final_ratio / 100.0; - let mut resampler = Async::::new_sinc( - f_ratio, - target_ratio, - ¶ms, - chunksize, - channels, - FixedAsync::Output, - ) - .unwrap(); - - let input_adapter = InterleavedSlice::new(&indata, channels, nbr_input_frames).unwrap(); - let mut indexing = Indexing::new(); - - let start = Instant::now(); - let mut output_time = 0.0; - let mut frames_left = nbr_input_frames; - let next_nbr_input_frames = resampler.input_frames_next(); - while frames_left > next_nbr_input_frames { - let mut output_scratch = vec![0.0; channels * resampler.output_frames_next()]; - let mut output_adapter = InterleavedSlice::new_mut( - &mut output_scratch, - channels, - resampler.output_frames_next(), - ) - .unwrap(); - let (nbr_in, nbr_out) = resampler - .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) - .unwrap(); - outdata.append(&mut output_scratch); - frames_left -= nbr_in; - output_time += nbr_out as f64 / fs_out as f64; - if output_time < duration { - let rel_time = output_time / duration; - let rel_ratio = 1.0 + (target_ratio - 1.0) * rel_time; - println!("time {}, rel ratio {}", output_time, rel_ratio); - resampler - .set_resample_ratio_relative(rel_ratio, true) - .unwrap(); - } - indexing.input_offset += nbr_in; - } - - let duration = start.elapsed(); - - println!("Resampling took: {:?}", duration); - - let mut f_out_disk = BufWriter::new(File::create(file_out).unwrap()); - write_file(&outdata, &mut f_out_disk); -} diff --git a/examples/polyfixedin_ramp64.rs b/examples/polyfixedin_ramp64.rs deleted file mode 100644 index dc64ad3..0000000 --- a/examples/polyfixedin_ramp64.rs +++ /dev/null @@ -1,159 +0,0 @@ -extern crate rubato; -use audioadapter_buffers::direct::InterleavedSlice; -use rubato::{Adjustable, Async, FixedAsync, Indexing, PolynomialDegree, Resampler}; -use std::convert::TryInto; -use std::env; -use std::fs::File; -use std::io::prelude::{Read, Seek, Write}; -use std::io::{BufReader, BufWriter}; -use std::time::Instant; - -extern crate env_logger; -extern crate log; -use env_logger::Builder; -use log::LevelFilter; - -const BYTE_PER_SAMPLE: usize = std::mem::size_of::(); - -// A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. -// The command line arguments are input filename, output filename, input samplerate, output samplerate, -// number of channels, final relative ratio in percent, and ramp duration in seconds. -// To resample the file `sine_f64_2ch.raw` from 44.1kHz to 192kHz, and assuming the file has two channels, -// and that the resampling ratio should be ramped to 150% during 3 seconds, the command is: -// ``` -// cargo run --release --example polyfixedin_ramp64 sine_f64_2ch.raw test.raw 44100 192000 2 150 3 -// ``` -// There are two helper python scripts for testing. -// - `makesineraw.py` to generate test files in raw format. -// Run it with the `-h` flag for instructions. -// - `analyze_result.py` to analyze the result. -// This takes three arguments: number of channels, samplerate, and sample format. -// Example, to analyze the file created above: -// ``` -// python examples/analyze_result.py test.raw 2 192000 f64 -// ``` - -/// Helper to read an entire file to memory as f64 values -fn read_file(inbuffer: &mut R) -> Vec { - let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; - let mut data = Vec::new(); - loop { - let bytes_read = inbuffer.read(&mut buffer).unwrap(); - if bytes_read == 0 { - break; - } - let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); - data.push(value); - } - data -} - -/// Helper to write all frames to a file -fn write_file(data: &[f64], output: &mut W) { - for value in data.iter() { - let bytes = value.to_le_bytes(); - output.write_all(&bytes).unwrap(); - } -} - -fn main() { - // init logger - let mut builder = Builder::from_default_env(); - builder.filter(None, LevelFilter::Debug).init(); - - let file_in = env::args().nth(1).expect("Please specify an input file."); - let file_out = env::args().nth(2).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); - - let fs_in_str = env::args() - .nth(3) - .expect("Please specify an input sample rate"); - let fs_out_str = env::args() - .nth(4) - .expect("Please specify an output sample rate"); - let fs_in = fs_in_str.parse::().unwrap(); - let fs_out = fs_out_str.parse::().unwrap(); - println!("Resampling from {} to {}", fs_in, fs_out); - - let channels_str = env::args() - .nth(5) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - - let ratio_str = env::args() - .nth(6) - .expect("Please specify final resampling ratio in percent"); - let final_ratio = ratio_str.parse::().unwrap(); - - let duration_str = env::args() - .nth(7) - .expect("Please specify ramp time in seconds"); - let duration = duration_str.parse::().unwrap(); - - println!("Copy input file to buffer"); - let file_in_disk = File::open(file_in).expect("Can't open file"); - let mut file_in_reader = BufReader::new(file_in_disk); - let indata = read_file(&mut file_in_reader); - let nbr_input_frames = indata.len() / channels; - - let f_ratio = fs_out as f64 / fs_in as f64; - - // Create buffer for storing output, size is preliminary and may grow - let mut outdata = - Vec::with_capacity(2 * channels * (nbr_input_frames as f64 * f_ratio) as usize); - - // parameters - - let chunksize = 1024; - let target_ratio = final_ratio / 100.0; - let mut resampler = Async::::new_poly( - f_ratio, - target_ratio, - PolynomialDegree::Cubic, - chunksize, - channels, - FixedAsync::Input, - ) - .unwrap(); - - let num_chunks = nbr_input_frames / chunksize; - let mut output_time = 0.0; - - let input_adapter = InterleavedSlice::new(&indata, channels, nbr_input_frames).unwrap(); - let mut indexing = Indexing::new(); - - let start = Instant::now(); - - for chunk in 0..num_chunks { - let input_offset = chunksize * chunk; - indexing.input_offset = input_offset; - let mut output_scratch = vec![0.0; channels * resampler.output_frames_next()]; - let mut output_adapter = InterleavedSlice::new_mut( - &mut output_scratch, - channels, - resampler.output_frames_next(), - ) - .unwrap(); - let (_nbr_in, nbr_out) = resampler - .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) - .unwrap(); - - outdata.append(&mut output_scratch); - output_time += nbr_out as f64 / fs_out as f64; - if output_time < duration { - let rel_time = output_time / duration; - let rel_ratio = 1.0 + (target_ratio - 1.0) * rel_time; - println!("time {}, rel ratio {}", output_time, rel_ratio); - resampler - .set_resample_ratio_relative(rel_ratio, true) - .unwrap(); - } - } - - let duration = start.elapsed(); - - println!("Resampling took: {:?}", duration); - - let mut f_out_disk = BufWriter::new(File::create(file_out).unwrap()); - write_file(&outdata, &mut f_out_disk); -} diff --git a/examples/process_f64.rs b/examples/process_f64.rs index d2b5f90..ca8cf1e 100644 --- a/examples/process_f64.rs +++ b/examples/process_f64.rs @@ -1,5 +1,6 @@ extern crate rubato; use audioadapter_buffers::direct::InterleavedSlice; +use clap::{Parser, ValueEnum}; use rubato::{ Async, FixedAsync, Indexing, PolynomialDegree, Resampler, SincInterpolationParameters, SincInterpolationType, WindowFunction, @@ -7,7 +8,6 @@ use rubato::{ #[cfg(feature = "fft_resampler")] use rubato::{Fft, FixedSync}; use std::convert::TryInto; -use std::env; use std::fs::File; use std::io::prelude::{Read, Seek, Write}; use std::io::{BufReader, BufWriter}; @@ -20,10 +20,13 @@ use log::LevelFilter; const BYTE_PER_SAMPLE: usize = 8; // A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. -// The command line arguments are resampler type, input filename, output filename, input samplerate, output samplerate, number of channels -// To use a sinc resampler with fixed input size to resample the file `sine_f64_2ch.raw` from 44.1kHz to 192kHz, and assuming the file has two channels, the command is: +// This is the fixed ratio case. See the `adjust_ratio_f64` example for applying a constant rate offset, +// and `ramp_ratio_f64` for a ratio that changes while processing. +// +// To use a sinc resampler with fixed input size to resample the file `sine_f64_2ch.raw` from 44.1kHz +// to 192kHz, and assuming the file has two channels, the command is: // ``` -// cargo run --release --example process_f64 SincFixedInput sine_f64_2ch.raw test.raw 44100 192000 2 +// cargo run --release --example process_f64 sine_f64_2ch.raw test.raw 44100 192000 -r SincFixedInput // ``` // There are two helper python scripts for testing. // - `makesineraw.py` to generate test files in raw format. @@ -35,6 +38,60 @@ const BYTE_PER_SAMPLE: usize = 8; // python examples/analyze_result.py test.raw 2 192000 f64 // ``` +/// Resample a raw file of 64 bit floats between two fixed sample rates. +#[derive(Parser)] +#[command(version)] +struct Options { + /// Raw file of little-endian 64 bit floats to read. + input: String, + + /// Raw file to write, in the same format. + output: String, + + /// Sample rate of the input file, in Hz. + input_rate: usize, + + /// Sample rate of the output file, in Hz. + output_rate: usize, + + /// Resampler to use. + #[arg(short, long, value_enum, ignore_case = true, default_value_t = ResamplerType::SincFixedInput)] + resampler: ResamplerType, + + /// Number of channels in the file. + #[arg(short, long, default_value_t = 2)] + channels: usize, +} + +/// The resampler types this example can build. +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] +enum ResamplerType { + /// Sinc interpolation, fixed input size. + #[value(name = "SincFixedInput")] + SincFixedInput, + /// Sinc interpolation, fixed output size. + #[value(name = "SincFixedOutput")] + SincFixedOutput, + /// Polynomial interpolation, fixed input size. + #[value(name = "PolyFixedInput")] + PolyFixedInput, + /// Polynomial interpolation, fixed output size. + #[value(name = "PolyFixedOutput")] + PolyFixedOutput, + /// Synchronous FFT, fixed input size. + #[cfg(feature = "fft_resampler")] + #[value(name = "FftFixedInput")] + FftFixedInput, + /// Synchronous FFT, fixed output size. + #[cfg(feature = "fft_resampler")] + #[value(name = "FftFixedOutput")] + FftFixedOutput, + /// Synchronous FFT, both sizes fixed. + #[cfg(feature = "fft_resampler")] + #[value(name = "FftFixedBoth")] + FftFixedBoth, +} + /// Helper to read an entire file to memory as f64 values fn read_file(inbuffer: &mut R) -> Vec { let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; @@ -68,31 +125,14 @@ fn main() { let mut builder = Builder::from_default_env(); builder.filter(None, LevelFilter::Debug).init(); - let resampler_type = env::args() - .nth(1) - .expect("Please specify a resampler type, one of:\nSincFixedIn\nSincFixedOut\nFastFixedIn\nFastFixedOut\nFftFixedIn\nFftFixedOut\nFftFixedInOut"); - - let file_in = env::args().nth(2).expect("Please specify an input file."); - let file_out = env::args().nth(3).expect("Please specify an output file."); - println!("Opening files: {}, {}", file_in, file_out); - - let fs_in_str = env::args() - .nth(4) - .expect("Please specify an input sample rate"); - let fs_out_str = env::args() - .nth(5) - .expect("Please specify an output sample rate"); - let fs_in = fs_in_str.parse::().unwrap(); - let fs_out = fs_out_str.parse::().unwrap(); + let opts = Options::parse(); + let channels = opts.channels; + let (fs_in, fs_out) = (opts.input_rate, opts.output_rate); + println!("Opening files: {}, {}", opts.input, opts.output); println!("Resampling from {} to {}", fs_in, fs_out); - let channels_str = env::args() - .nth(6) - .expect("Please specify number of channels"); - let channels = channels_str.parse::().unwrap(); - println!("Copy input file to buffer"); - let file_in_disk = File::open(file_in).expect("Can't open file"); + let file_in_disk = File::open(&opts.input).expect("Can't open file"); let mut file_in_reader = BufReader::new(file_in_disk); let indata = read_file(&mut file_in_reader); let nbr_input_frames = indata.len() / channels; @@ -104,48 +144,59 @@ fn main() { println!("Creating resampler"); // Create resampler - let mut resampler: Box> = match resampler_type.as_str() { - "SincFixedInput" => { - let sinc_len = 128; - let oversampling_factor = 256; - let interpolation = SincInterpolationType::Quadratic; - let window = WindowFunction::Blackman2; - - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - Box::new(Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Input).unwrap()) - } - "SincFixedOutput" => { - let sinc_len = 128; - let oversampling_factor = 512; - let interpolation = SincInterpolationType::Cubic; - let window = WindowFunction::Blackman2; - - let params = SincInterpolationParameters::new(sinc_len, window) - .oversampling_factor(oversampling_factor) - .interpolation(interpolation); - Box::new(Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Output).unwrap()) - } - "PolyFixedInput" => { - Box::new(Async::::new_poly(f_ratio, 1.1, PolynomialDegree::Septic, 1024, channels, FixedAsync::Input).unwrap()) + let mut resampler: Box> = match opts.resampler { + ResamplerType::SincFixedInput => { + let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2) + .oversampling_factor(256) + .interpolation(SincInterpolationType::Quadratic); + Box::new( + Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Input) + .unwrap(), + ) } - "PolyFixedOutput" => { - Box::new(Async::::new_poly(f_ratio, 1.1, PolynomialDegree::Septic, 1024, channels, FixedAsync::Output).unwrap()) + ResamplerType::SincFixedOutput => { + let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2) + .oversampling_factor(512) + .interpolation(SincInterpolationType::Cubic); + Box::new( + Async::::new_sinc(f_ratio, 1.1, ¶ms, 1024, channels, FixedAsync::Output) + .unwrap(), + ) } + ResamplerType::PolyFixedInput => Box::new( + Async::::new_poly( + f_ratio, + 1.1, + PolynomialDegree::Septic, + 1024, + channels, + FixedAsync::Input, + ) + .unwrap(), + ), + ResamplerType::PolyFixedOutput => Box::new( + Async::::new_poly( + f_ratio, + 1.1, + PolynomialDegree::Septic, + 1024, + channels, + FixedAsync::Output, + ) + .unwrap(), + ), #[cfg(feature = "fft_resampler")] - "FftFixedInput" => { + ResamplerType::FftFixedInput => { Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Input).unwrap()) } #[cfg(feature = "fft_resampler")] - "FftFixedOutput" => { + ResamplerType::FftFixedOutput => { Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Output).unwrap()) } #[cfg(feature = "fft_resampler")] - "FftFixedBoth" => { + ResamplerType::FftFixedBoth => { Box::new(Fft::::new(fs_in, fs_out, 1024, channels, FixedSync::Both).unwrap()) } - _ => panic!("Unknown resampler type {}\nMust be one of SincFixedInput, SincFixedOutput, PolyFixedInput, PolyFixedOutput, FftFixedInput, FftFixedOutput, FftFixedBoth", resampler_type), }; // Prepare @@ -189,7 +240,7 @@ fn main() { ); println!("Write output to file, trimming off the silent frames from both ends."); - let mut file_out_disk = BufWriter::new(File::create(file_out).unwrap()); + let mut file_out_disk = BufWriter::new(File::create(&opts.output).unwrap()); write_file( &outdata, &mut file_out_disk, diff --git a/examples/ramp_ratio_f64.rs b/examples/ramp_ratio_f64.rs new file mode 100644 index 0000000..7906857 --- /dev/null +++ b/examples/ramp_ratio_f64.rs @@ -0,0 +1,225 @@ +extern crate rubato; +use audioadapter_buffers::direct::InterleavedSlice; +use clap::{Parser, ValueEnum}; +use rubato::{ + Async, FixedAsync, Indexing, PolynomialDegree, Resampler, SincInterpolationParameters, + SincInterpolationType, WindowFunction, +}; +use std::convert::TryInto; +use std::fs::File; +use std::io::prelude::{Read, Seek, Write}; +use std::io::{BufReader, BufWriter}; +use std::time::Instant; + +extern crate env_logger; +extern crate log; +use env_logger::Builder; +use log::LevelFilter; + +const BYTE_PER_SAMPLE: usize = std::mem::size_of::(); + +// A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format. +// While resampling, it ramps the resampling ratio from 100% to a user-provided value, during a given time +// duration (measured in output time). Unlike the `adjust_ratio_f64` example, which applies one constant offset, +// this one changes the ratio continuously while processing. +// +// To resample the file `sine_f64_2ch.raw` from 44.1kHz to 192kHz, and assuming the file has two channels, +// and that the resampling ratio should be ramped to 150% during 3 seconds, the command is: +// ``` +// cargo run --release --example ramp_ratio_f64 sine_f64_2ch.raw test.raw 44100 192000 -r SincFixedOutput -t 150 -d 3 +// ``` +// There are two helper python scripts for testing. +// - `makesineraw.py` to generate test files in raw format. +// Run it with the `-h` flag for instructions. +// - `analyze_result.py` to analyze the result. +// This takes three arguments: number of channels, samplerate, and sample format. +// Example, to analyze the file created above: +// ``` +// python examples/analyze_result.py test.raw 2 192000 f64 +// ``` + +/// Resample a raw file of 64 bit floats while ramping the ratio. +#[derive(Parser)] +#[command(version)] +struct Options { + /// Raw file of little-endian 64 bit floats to read. + input: String, + + /// Raw file to write, in the same format. + output: String, + + /// Sample rate of the input file, in Hz. + input_rate: usize, + + /// Nominal sample rate of the output file, in Hz. The ramp is applied on top of this. + output_rate: usize, + + /// Resampler to use. The synchronous FFT resamplers cannot change ratio and are not offered. + #[arg(short, long, value_enum, ignore_case = true, default_value_t = ResamplerType::SincFixedOutput)] + resampler: ResamplerType, + + /// Number of channels in the file. + #[arg(short, long, default_value_t = 2)] + channels: usize, + + /// Ratio to ramp to, in percent of the nominal ratio. + #[arg(short, long, default_value_t = 150.0)] + target: f64, + + /// Ramp duration in seconds, measured in output time. + #[arg(short, long, default_value_t = 3.0)] + duration: f64, +} + +/// The adjustable resampler types this example can build. +#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] +enum ResamplerType { + /// Sinc interpolation, fixed input size. + #[value(name = "SincFixedInput")] + SincFixedInput, + /// Sinc interpolation, fixed output size. + #[value(name = "SincFixedOutput")] + SincFixedOutput, + /// Polynomial interpolation, fixed input size. + #[value(name = "PolyFixedInput")] + PolyFixedInput, + /// Polynomial interpolation, fixed output size. + #[value(name = "PolyFixedOutput")] + PolyFixedOutput, +} + +/// Helper to read an entire file to memory as f64 values +fn read_file(inbuffer: &mut R) -> Vec { + let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; + let mut data = Vec::new(); + loop { + let bytes_read = inbuffer.read(&mut buffer).unwrap(); + if bytes_read == 0 { + break; + } + let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); + data.push(value); + } + data +} + +/// Helper to write all frames to a file +fn write_file(data: &[f64], output: &mut W) { + for value in data.iter() { + let bytes = value.to_le_bytes(); + output.write_all(&bytes).unwrap(); + } +} + +fn main() { + // init logger + let mut builder = Builder::from_default_env(); + builder.filter(None, LevelFilter::Debug).init(); + + let opts = Options::parse(); + let channels = opts.channels; + let (fs_in, fs_out) = (opts.input_rate, opts.output_rate); + let ramp_duration = opts.duration; + println!("Opening files: {}, {}", opts.input, opts.output); + println!("Resampling from {} to {}", fs_in, fs_out); + + println!("Copy input file to buffer"); + let file_in_disk = File::open(&opts.input).expect("Can't open file"); + let mut file_in_reader = BufReader::new(file_in_disk); + let indata = read_file(&mut file_in_reader); + let nbr_input_frames = indata.len() / channels; + + let f_ratio = fs_out as f64 / fs_in as f64; + + // Create buffer for storing output, size is preliminary and may grow + let mut outdata = + Vec::with_capacity(2 * channels * (nbr_input_frames as f64 * f_ratio) as usize); + + println!("Creating resampler"); + let chunksize = 1024; + let target_ratio = opts.target / 100.0; + // The maximum relative ratio must cover the ramp, so it is set to the target. + let mut resampler: Box> = match opts.resampler { + ResamplerType::SincFixedInput | ResamplerType::SincFixedOutput => { + // Balanced for ratio changes: a high oversampling factor keeps the + // interpolation between the sinc tables cheap and accurate. + let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2) + .oversampling_factor(2048) + .interpolation(SincInterpolationType::Linear); + let fixed = if opts.resampler == ResamplerType::SincFixedInput { + FixedAsync::Input + } else { + FixedAsync::Output + }; + Box::new( + Async::::new_sinc(f_ratio, target_ratio, ¶ms, chunksize, channels, fixed) + .unwrap(), + ) + } + ResamplerType::PolyFixedInput | ResamplerType::PolyFixedOutput => { + let fixed = if opts.resampler == ResamplerType::PolyFixedInput { + FixedAsync::Input + } else { + FixedAsync::Output + }; + Box::new( + Async::::new_poly( + f_ratio, + target_ratio, + PolynomialDegree::Cubic, + chunksize, + channels, + fixed, + ) + .unwrap(), + ) + } + }; + + let input_adapter = InterleavedSlice::new(&indata, channels, nbr_input_frames).unwrap(); + let mut indexing = Indexing::new(); + + println!("Processing..."); + let start = Instant::now(); + let mut output_time = 0.0; + let mut frames_left = nbr_input_frames; + + // The same loop drives both the fixed input and the fixed output resamplers. + // Ask how many input frames the next call needs, and advance by the number it consumed. + while frames_left > resampler.input_frames_next() { + let frames_out = resampler.output_frames_next(); + let mut output_scratch = vec![0.0; channels * frames_out]; + let mut output_adapter = + InterleavedSlice::new_mut(&mut output_scratch, channels, frames_out).unwrap(); + let (nbr_in, nbr_out) = resampler + .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) + .unwrap(); + + // Keep only the frames that were actually written. With a fixed input size, + // the output size varies and can be shorter than the scratch buffer. + output_scratch.truncate(channels * nbr_out); + outdata.append(&mut output_scratch); + + frames_left -= nbr_in; + indexing.input_offset += nbr_in; + + // Ramp the ratio linearly towards the target, as a function of output time. + output_time += nbr_out as f64 / fs_out as f64; + if output_time < ramp_duration { + let rel_time = output_time / ramp_duration; + let rel_ratio = 1.0 + (target_ratio - 1.0) * rel_time; + println!("time {}, rel ratio {}", output_time, rel_ratio); + resampler + .as_adjustable() + .expect("the selected resampler type is adjustable") + .set_resample_ratio_relative(rel_ratio, true) + .unwrap(); + } + } + + let duration = start.elapsed(); + println!("Resampling took: {:?}", duration); + + let mut f_out_disk = BufWriter::new(File::create(&opts.output).unwrap()); + write_file(&outdata, &mut f_out_disk); +} From 1116c1a44688671b3b0f52c09c7fcb3369173455 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 6 Aug 2026 20:59:30 +0200 Subject: [PATCH 09/12] Update the examples section of the README The section still described a 16-bit integer example that is gone, and did not mention that resample_wav reads and writes wav files directly. List the four examples and what each one is for. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e642b3f..bf146a6 100644 --- a/README.md +++ b/README.md @@ -386,8 +386,15 @@ loop { The `examples` directory contains a few sample applications for testing the resamplers. There are also Python scripts for generating simple test signals as well as analyzing the resampled results. +Run any of them with `--help` for the full list of options. -The examples read and write raw audio data in either 64-bit float or 16-bit integer format. +- `resample_wav` reads and writes .wav files directly, and converts to any sample format + supported by the [waveadapter](https://crates.io/crates/waveadapter) crate. +- `process_f64` converts between two fixed sample rates, using any of the resampler types. +- `adjust_ratio_f64` applies a small constant rate offset, the clock drift case. +- `ramp_ratio_f64` ramps the ratio while processing. + +Apart from `resample_wav`, the examples read and write raw audio data as 64-bit floats. They can be used to process .wav files if the files are first converted to the right format. Example, use `sox` to convert a .wav to 64-bit float raw samples: ```sh From 32493827f1e79a19e80a905c80ecc15824d37559 Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 7 Aug 2026 08:52:16 +0200 Subject: [PATCH 10/12] Address review comments Drop a duplicated sentence in the docs for Fft::cutoff, and pin the crossfade values that the docs for Slip::new quote with an assert, so they cannot drift unnoticed. Replace the float rounding used to size the FFT blocks and the sinc filter with integer arithmetic. Dividing as f32 loses precision for large values, and div_ceil says what is meant. Rounding down to zero blocks now falls back to a single block instead of panicking further down. --- src/asynchro_sinc.rs | 2 +- src/slip.rs | 4 ++++ src/synchro.rs | 53 +++++++++++++++++++++++++++++--------------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index b84e1ec..f6f5bf3 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -212,7 +212,7 @@ pub enum SincInterpolationType { /// Round the sinc length up to the multiple of 8 that the interpolators use. pub(crate) fn round_sinc_len(sinc_len: usize) -> usize { - 8 * (((sinc_len as f32) / 8.0).ceil() as usize) + sinc_len.next_multiple_of(8) } /// Resolve the relative cutoff frequency of the sinc filter. diff --git a/src/slip.rs b/src/slip.rs index 6a0fc94..5526b79 100644 --- a/src/slip.rs +++ b/src/slip.rs @@ -601,6 +601,10 @@ mod tests { /// leaving room for a correction (`chunk >= 2 * len + 2`). #[test] fn crossfade_len_scales_with_chunk() { + // The docs on Slip::new quote these two values, since they are what a caller + // needs to pick a chunk size. Keep them in sync if the target ever changes. + assert_eq!(MAX_CROSSFADE_LEN, 128); + assert_eq!(2 * MAX_CROSSFADE_LEN + 2, 258); // Capped at the target once the chunk is big enough to hold it. assert_eq!(crossfade_len_for(4096), MAX_CROSSFADE_LEN); assert_eq!( diff --git a/src/synchro.rs b/src/synchro.rs index 0b841c0..69ec50d 100644 --- a/src/synchro.rs +++ b/src/synchro.rs @@ -301,18 +301,21 @@ where FixedSync::Input => { let min_chunk_in = sample_rate_input / gcd; let wanted_subsize = chunk_size / sub_chunks; - (wanted_subsize as f32 / min_chunk_in as f32).ceil() as usize + wanted_subsize.div_ceil(min_chunk_in) } FixedSync::Output => { let min_chunk_out = sample_rate_output / gcd; let wanted_subsize = chunk_size / sub_chunks; - (wanted_subsize as f32 / min_chunk_out as f32).ceil() as usize + wanted_subsize.div_ceil(min_chunk_out) } FixedSync::Both => { let min_chunk_in = sample_rate_input / gcd; - (chunk_size as f32 / min_chunk_in as f32).ceil() as usize + chunk_size.div_ceil(min_chunk_in) } - }; + } + // Asking for more sub chunks than there are frames rounds down to zero blocks, + // which is not a usable resampler. Fall back to a single minimum sized block. + .max(1); let fft_size_out = fft_chunks * sample_rate_output / gcd; let fft_size_in = fft_chunks * sample_rate_input / gcd; @@ -397,8 +400,6 @@ where /// A larger block moves it closer to Nyquist. /// When downsampling it is scaled down to keep it below the lower Nyquist /// frequency of the two sample rates. - /// - /// Multiply by `sample_rate_input / 2` to get the cutoff in Hz. pub fn cutoff(&self) -> f32 { self.resampler.cutoff } @@ -412,21 +413,20 @@ where ) -> (usize, usize) { match fixed { FixedSync::Input => { - let subchunks_available: f32 = - ((chunk_size + saved_frames) as f32 / fft_size_in as f32).floor(); - let frames_available = (subchunks_available as usize) * fft_size_out; + let subchunks_available = (chunk_size + saved_frames) / fft_size_in; + let frames_available = subchunks_available * fft_size_out; (chunk_size, frames_available) } FixedSync::Output => { - let subchunks_needed = ((chunk_size as f32 - saved_frames as f32) - / fft_size_out as f32) - .ceil() - .max(0.0); - let frames_needed = (subchunks_needed as usize) * fft_size_in; + // Saturating, since more frames may be saved than the chunk needs. + let subchunks_needed = chunk_size + .saturating_sub(saved_frames) + .div_ceil(fft_size_out); + let frames_needed = subchunks_needed * fft_size_in; (frames_needed, chunk_size) } FixedSync::Both => { - let subchunks_needed = (chunk_size as f32 / fft_size_in as f32).ceil() as usize; + let subchunks_needed = chunk_size.div_ceil(fft_size_in); let frames_needed_in = subchunks_needed * fft_size_in; let frames_needed_out = subchunks_needed * fft_size_out; (frames_needed_in, frames_needed_out) @@ -467,9 +467,7 @@ where ) -> usize { match fixed { FixedSync::Both | FixedSync::Input => chunk_size_in, - FixedSync::Output => { - (chunk_size_out as f32 / fft_size_out as f32).ceil() as usize * fft_size_in - } + FixedSync::Output => chunk_size_out.div_ceil(fft_size_out) * fft_size_in, } } @@ -759,6 +757,25 @@ mod tests { assert!((maxval - 1.0).abs() < 0.1); } + #[test_log::test(test_matrix([FixedSync::Input, FixedSync::Output, FixedSync::Both]))] + fn fft_more_sub_chunks_than_frames(fixed: FixedSync) { + // Asking for more sub chunks than the chunk has frames rounds the sub chunk + // size down to zero. That used to give zero FFT blocks, and a panic on the + // first division by the block size. + let resampler = Fft::::new_custom( + 44100, + 48000, + 100, + 1000, + 2, + WindowFunction::BlackmanHarris2, + fixed, + ) + .unwrap(); + assert_eq!(resampler.fft_size_in(), 147); + assert_eq!(resampler.fft_size_out(), 160); + } + #[test_log::test(test_matrix( [512, 1024, 4096], [(44100, 48000), (48000, 44100), (44100, 88200), (88200, 44100), (44100, 192000), (192000, 44100)], From 8f85c2d7bf15ccea354bee10c601ccf9183fa0cf Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 7 Aug 2026 11:31:35 +0200 Subject: [PATCH 11/12] Read whole samples in the examples The read helper decoded the buffer whatever the read returned, so a short read or a truncated trailing sample decoded stale bytes from the previous iteration as audio. Use read_exact and stop at a clean end of file, like adjust_ratio_f64 already did. Also correct the comment in resample_wav that claimed the chunk size is the FFT block size. It is the requested size, rounded up to a block size valid for the sample rate pair. --- examples/process_f64.rs | 13 ++++++++----- examples/ramp_ratio_f64.rs | 13 ++++++++----- examples/resample_wav.rs | 8 +++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/examples/process_f64.rs b/examples/process_f64.rs index ca8cf1e..424367f 100644 --- a/examples/process_f64.rs +++ b/examples/process_f64.rs @@ -97,12 +97,15 @@ fn read_file(inbuffer: &mut R) -> Vec { let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; let mut data = Vec::new(); loop { - let bytes_read = inbuffer.read(&mut buffer).unwrap(); - if bytes_read == 0 { - break; + match inbuffer.read_exact(&mut buffer) { + Ok(()) => { + let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); + data.push(value); + } + // A clean end of file stops the loop; a partial trailing read means a malformed file. + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, + Err(e) => panic!("Error reading input file: {}", e), } - let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); - data.push(value); } data } diff --git a/examples/ramp_ratio_f64.rs b/examples/ramp_ratio_f64.rs index 7906857..258bdd7 100644 --- a/examples/ramp_ratio_f64.rs +++ b/examples/ramp_ratio_f64.rs @@ -93,12 +93,15 @@ fn read_file(inbuffer: &mut R) -> Vec { let mut buffer = vec![0u8; BYTE_PER_SAMPLE]; let mut data = Vec::new(); loop { - let bytes_read = inbuffer.read(&mut buffer).unwrap(); - if bytes_read == 0 { - break; + match inbuffer.read_exact(&mut buffer) { + Ok(()) => { + let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); + data.push(value); + } + // A clean end of file stops the loop; a partial trailing read means a malformed file. + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break, + Err(e) => panic!("Error reading input file: {}", e), } - let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap()); - data.push(value); } data } diff --git a/examples/resample_wav.rs b/examples/resample_wav.rs index aa7fced..9870130 100644 --- a/examples/resample_wav.rs +++ b/examples/resample_wav.rs @@ -163,7 +163,9 @@ fn run(opts: Options) -> Result<(), Box> { // Write the same sample format as the input file unless told otherwise. let format_out = opts.format.map(SampleFormat::from).unwrap_or(format_in); - // One sub chunk per chunk, so the chunk size is also the FFT block size. + // One sub chunk per chunk, so each chunk is a single FFT block. The requested + // chunk size is only a starting point: it is rounded up to a block size that is + // valid for the sample rate pair, so 1024 frames becomes 1029 for 44.1k to 48k. let window = WindowFunction::from(opts.window); let mut resampler = Fft::::new_custom( rate_in, @@ -175,8 +177,8 @@ fn run(opts: Options) -> Result<(), Box> { FixedSync::Both, )?; - // With a single sub chunk the chunk sizes are the FFT block sizes. The - // cutoff is relative to the input Nyquist frequency, so scale it by half + // Report the block sizes the resampler settled on, not the requested chunk size. + // The cutoff is relative to the input Nyquist frequency, so scale it by half // the input rate to report it in Hz. println!( "Config: chunks of {} -> {} frames, {:?} window, cutoff {:.0} Hz", From 3c3c254dbd7928ee06bf401d1a223823db3ffc30 Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 7 Aug 2026 11:49:17 +0200 Subject: [PATCH 12/12] Process the last frames in the ramp example The loop stopped as soon as fewer frames than a full chunk were left, so the tail of the clip was silently dropped. Add the partial call after the loop, like process_f64 does. --- examples/ramp_ratio_f64.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/ramp_ratio_f64.rs b/examples/ramp_ratio_f64.rs index 258bdd7..2fe0ae5 100644 --- a/examples/ramp_ratio_f64.rs +++ b/examples/ramp_ratio_f64.rs @@ -220,6 +220,22 @@ fn main() { } } + // Process the frames that are left over, fewer than the resampler asks for. + // Setting `partial_len` tells it how many of the frames are real, and it inserts + // silence in place of the rest. Without this the tail of the clip is dropped. + if frames_left > 0 { + let frames_out = resampler.output_frames_next(); + let mut output_scratch = vec![0.0; channels * frames_out]; + let mut output_adapter = + InterleavedSlice::new_mut(&mut output_scratch, channels, frames_out).unwrap(); + indexing.partial_len = Some(frames_left); + let (_nbr_in, nbr_out) = resampler + .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) + .unwrap(); + output_scratch.truncate(channels * nbr_out); + outdata.append(&mut output_scratch); + } + let duration = start.elapsed(); println!("Resampling took: {:?}", duration);