diff --git a/Cargo.toml b/Cargo.toml index e120628..d6b8783 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,13 +39,23 @@ 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" +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] +#audioadapter = { path = "../audioadapter-rs/audioadapter" } +#audioadapter-buffers = { path = "../audioadapter-rs/audioadapter-buffers" } +#audioadapter-sample = { path = "../audioadapter-rs/audioadapter-sample" } [[bench]] name = "resamplers" harness = false +[[example]] +name = "resample_wav" +required-features = ["fft_resampler"] + [lib] bench = false path = "src/lib.rs" diff --git a/README.md b/README.md index 4090e1a..bf146a6 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. @@ -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 @@ -405,7 +412,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 +504,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. 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/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_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_f64.rs b/examples/process_f64.rs index d2b5f90..424367f 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,17 +38,74 @@ 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]; 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 } @@ -68,31 +128,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 +147,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 +243,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/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/examples/ramp_ratio_f64.rs b/examples/ramp_ratio_f64.rs new file mode 100644 index 0000000..2fe0ae5 --- /dev/null +++ b/examples/ramp_ratio_f64.rs @@ -0,0 +1,244 @@ +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 { + 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), + } + } + 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(); + } + } + + // 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); + + let mut f_out_disk = BufWriter::new(File::create(&opts.output).unwrap()); + write_file(&outdata, &mut f_out_disk); +} diff --git a/examples/resample_wav.rs b/examples/resample_wav.rs new file mode 100644 index 0000000..9870130 --- /dev/null +++ b/examples/resample_wav.rs @@ -0,0 +1,243 @@ +//! 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 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, + opts.output_rate, + opts.chunk, + 1, + channels, + window, + FixedSync::Both, + )?; + + // 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", + 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); + } +} diff --git a/src/asynchro.rs b/src/asynchro.rs index cb683e9..b1c622e 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,16 +113,17 @@ pub struct Async { inner_resampler: Box>, channel_mask: Vec, fixed: FixedAsync, + sinc_cutoff: Option, } 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) @@ -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,14 +331,17 @@ 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))] + #[allow(clippy::too_many_arguments)] fn new_with_sinc_interpolator( resample_ratio: f64, max_resample_ratio_relative: f64, interpolation_type: SincInterpolationType, interpolator: AnyInterpolator, + f_cutoff: f32, chunk_size: usize, nbr_channels: usize, fixed: FixedAsync, @@ -372,6 +405,7 @@ where buffer, channel_mask, fixed, + sinc_cutoff: Some(f_cutoff), }) } @@ -790,6 +824,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 +838,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..f6f5bf3 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 { + sinc_len.next_multiple_of(8) +} + +/// 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/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, 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() diff --git a/src/slip.rs b/src/slip.rs index a98acef..5526b79 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( @@ -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 c4da06e..69ec50d 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,12 +67,12 @@ 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) - .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()) @@ -125,6 +126,7 @@ where FftResampler { fft_size_in, fft_size_out, + cutoff, filter_f, fft, ifft, @@ -299,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; @@ -365,6 +370,40 @@ 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. + pub fn cutoff(&self) -> f32 { + self.resampler.cutoff + } + fn calc_chunk_sizes( fft_size_in: usize, fft_size_out: usize, @@ -374,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) @@ -429,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, } } @@ -721,6 +757,50 @@ 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)], + [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)],