From 1f834f19c44743b2c1ef2a640c4d6ab99434cbd9 Mon Sep 17 00:00:00 2001 From: Henrik Date: Thu, 30 May 2024 20:55:40 +0200 Subject: [PATCH 1/9] Support reducing the chunk size --- src/asynchro_sinc.rs | 52 ++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 4 ++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index 1a56092..60d760f 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -86,6 +86,7 @@ pub enum SincInterpolationType { pub struct SincFixedIn { nbr_channels: usize, chunk_size: usize, + max_chunk_size: usize, last_index: f64, resample_ratio: f64, resample_ratio_original: f64, @@ -114,6 +115,7 @@ pub struct SincFixedIn { pub struct SincFixedOut { nbr_channels: usize, chunk_size: usize, + max_chunk_size: usize, needed_input_size: usize, last_index: f64, current_buffer_fill: usize, @@ -293,6 +295,7 @@ where Ok(SincFixedIn { nbr_channels, chunk_size, + max_chunk_size: chunk_size, last_index: -((interpolator.len() / 2) as f64), resample_ratio, resample_ratio_original: resample_ratio, @@ -479,7 +482,7 @@ where fn output_frames_max(&self) -> usize { // Set length to chunksize*ratio plus a safety margin of 10 elements. - (self.chunk_size as f64 * self.resample_ratio_original * self.max_relative_ratio + 10.0) + (self.max_chunk_size as f64 * self.resample_ratio_original * self.max_relative_ratio + 10.0) as usize } @@ -497,7 +500,7 @@ where } fn input_frames_max(&self) -> usize { - self.chunk_size + self.max_chunk_size } fn input_frames_next(&self) -> usize { @@ -537,6 +540,14 @@ where self.resample_ratio = self.resample_ratio_original; self.target_ratio = self.resample_ratio_original; } + + fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { + if chunksize > self.max_chunk_size || chunksize == 0 { + return Err(ResampleError::SyncNotAdjustable); + } + self.chunk_size = chunksize; + Ok(()) + } } impl SincFixedOut @@ -610,6 +621,7 @@ where Ok(SincFixedOut { nbr_channels, chunk_size, + max_chunk_size: chunk_size, needed_input_size, last_index: -((interpolator.len() / 2) as f64), current_buffer_fill: needed_input_size, @@ -791,7 +803,7 @@ where } fn input_frames_max(&self) -> usize { - (self.chunk_size as f64 / self.resample_ratio_original * self.max_relative_ratio).ceil() + (self.max_chunk_size as f64 / self.resample_ratio_original * self.max_relative_ratio).ceil() as usize + 2 + self.interpolator.len() / 2 @@ -806,7 +818,7 @@ where } fn output_frames_max(&self) -> usize { - self.chunk_size + self.max_chunk_size } fn output_frames_next(&self) -> usize { @@ -860,6 +872,18 @@ where self.resample_ratio = self.resample_ratio_original; self.target_ratio = self.resample_ratio_original; } + + fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { + if chunksize > self.max_chunk_size || chunksize == 0 { + return Err(ResampleError::SyncNotAdjustable); + } + self.chunk_size = chunksize; + self.needed_input_size = (self.last_index as f32 + + self.chunk_size as f32 / self.resample_ratio as f32 + + self.interpolator.len() as f32) + .ceil() as usize; + Ok(()) + } } #[cfg(test)] @@ -1343,4 +1367,24 @@ mod tests { let mut resampler = SincFixedIn::::new(ratio, 1.0, params, 1024, 2).unwrap(); check_ratio!(resampler, ratio, 100); } + + #[test] + fn check_fo_output_resize() { + let params = basic_params(); + let mut resampler = SincFixedOut::::new(1.2, 1.0, params, 1024, 2).unwrap(); + assert_eq!(resampler.output_frames_next(), 1024); + resampler.set_chunksize(256).unwrap(); + assert_eq!(resampler.output_frames_next(), 256); + check_output!(resampler); + } + + #[test] + fn check_fi_output_resize() { + let params = basic_params(); + let mut resampler = SincFixedIn::::new(1.2, 1.0, params, 1024, 2).unwrap(); + assert_eq!(resampler.input_frames_next(), 1024); + resampler.set_chunksize(256).unwrap(); + assert_eq!(resampler.input_frames_next(), 256); + check_output!(resampler); + } } diff --git a/src/lib.rs b/src/lib.rs index a4ee343..22928ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -426,6 +426,10 @@ where /// Reset the resampler state and clear all internal buffers. fn reset(&mut self); + + fn set_chunksize(&mut self, _chunksize: usize) -> ResampleResult<()> { + Err(ResampleError::SyncNotAdjustable) + } } use crate as rubato; From de2b99fd5c9f68c23b4c61609204ea8185ed0664 Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 31 May 2024 22:45:53 +0200 Subject: [PATCH 2/9] Reduce duplication --- src/asynchro_sinc.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index 60d760f..60d2243 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -635,6 +635,14 @@ where channel_mask, }) } + + fn update_needed_len(&mut self) { + self.needed_input_size = (self.last_index as f32 + + self.chunk_size as f32 + / (0.5 * self.resample_ratio as f32 + 0.5 * self.target_ratio as f32) + + self.interpolator.len() as f32) + .ceil() as usize; + } } impl Resampler for SincFixedOut @@ -787,10 +795,7 @@ where let input_frames_used = self.needed_input_size; self.last_index = idx - self.current_buffer_fill as f64; self.resample_ratio = self.target_ratio; - self.needed_input_size = (self.last_index as f32 - + self.chunk_size as f32 / self.resample_ratio as f32 - + sinc_len as f32) - .ceil() as usize; + self.update_needed_len(); trace!( "Resampling channels {:?}, {} frames in, {} frames out. Next needed length: {} frames, last index {}", active_channels_mask, @@ -839,11 +844,7 @@ where } self.target_ratio = new_ratio; - self.needed_input_size = (self.last_index as f32 - + self.chunk_size as f32 - / (0.5 * self.resample_ratio as f32 + 0.5 * self.target_ratio as f32) - + self.interpolator.len() as f32) - .ceil() as usize; + self.update_needed_len(); Ok(()) } else { Err(ResampleError::RatioOutOfBounds { @@ -863,14 +864,13 @@ where self.buffer .iter_mut() .for_each(|ch| ch.iter_mut().for_each(|s| *s = T::zero())); - self.needed_input_size = (self.chunk_size as f64 / self.resample_ratio_original).ceil() - as usize - + self.interpolator.len() / 2; - self.current_buffer_fill = self.needed_input_size; - self.last_index = -((self.interpolator.len() / 2) as f64); - self.channel_mask.iter_mut().for_each(|val| *val = true); + self.resample_ratio = self.resample_ratio_original; self.target_ratio = self.resample_ratio_original; + self.last_index = -((self.interpolator.len() / 2) as f64); + self.update_needed_len(); + self.current_buffer_fill = self.needed_input_size; + self.channel_mask.iter_mut().for_each(|val| *val = true); } fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { @@ -878,10 +878,7 @@ where return Err(ResampleError::SyncNotAdjustable); } self.chunk_size = chunksize; - self.needed_input_size = (self.last_index as f32 - + self.chunk_size as f32 / self.resample_ratio as f32 - + self.interpolator.len() as f32) - .ceil() as usize; + self.update_needed_len(); Ok(()) } } From 89101dd28c026bc0a0a16c7e1d94df74ad12c619 Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 31 May 2024 22:52:10 +0200 Subject: [PATCH 3/9] Remove some more duplication, fix reset --- src/asynchro_sinc.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index 60d2243..6ae2976 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -307,6 +307,11 @@ where channel_mask, }) } + + fn calc_needed_len(&self) -> usize { + (self.chunk_size as f64 * (0.5 * self.resample_ratio + 0.5 * self.target_ratio) + 10.0) + as usize + } } impl Resampler for SincFixedIn @@ -326,9 +331,7 @@ where }; // Set length to chunksize*ratio plus a safety margin of 10 elements. - let needed_len = (self.chunk_size as f64 - * (0.5 * self.resample_ratio + 0.5 * self.target_ratio) - + 10.0) as usize; + let needed_len = self.calc_needed_len(); validate_buffers( wave_in, @@ -487,8 +490,7 @@ where } fn output_frames_next(&self) -> usize { - (self.chunk_size as f64 * (0.5 * self.resample_ratio + 0.5 * self.target_ratio) + 10.0) - as usize + self.calc_needed_len() } fn output_delay(&self) -> usize { @@ -539,6 +541,7 @@ where self.last_index = -((self.interpolator.len() / 2) as f64); self.resample_ratio = self.resample_ratio_original; self.target_ratio = self.resample_ratio_original; + self.chunk_size = self.max_chunk_size; } fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { @@ -868,6 +871,7 @@ where self.resample_ratio = self.resample_ratio_original; self.target_ratio = self.resample_ratio_original; self.last_index = -((self.interpolator.len() / 2) as f64); + self.chunk_size = self.max_chunk_size; self.update_needed_len(); self.current_buffer_fill = self.needed_input_size; self.channel_mask.iter_mut().for_each(|val| *val = true); From 0bd3ae115dad6719fcefd6d751089467962c292c Mon Sep 17 00:00:00 2001 From: Henrik Date: Fri, 31 May 2024 23:21:28 +0200 Subject: [PATCH 4/9] Add specific error types for changing chunk size --- src/asynchro_sinc.rs | 18 ++++++++++++------ src/error.rs | 30 +++++++++++++++++++++++++++--- src/lib.rs | 16 ++++++++++++++-- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index 6ae2976..b72200c 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -544,9 +544,12 @@ where self.chunk_size = self.max_chunk_size; } - fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { + fn set_chunk_size(&mut self, chunksize: usize) -> ResampleResult<()> { if chunksize > self.max_chunk_size || chunksize == 0 { - return Err(ResampleError::SyncNotAdjustable); + return Err(ResampleError::InvalidChunkSize { + max: self.max_chunk_size, + requested: chunksize, + }); } self.chunk_size = chunksize; Ok(()) @@ -877,9 +880,12 @@ where self.channel_mask.iter_mut().for_each(|val| *val = true); } - fn set_chunksize(&mut self, chunksize: usize) -> ResampleResult<()> { + fn set_chunk_size(&mut self, chunksize: usize) -> ResampleResult<()> { if chunksize > self.max_chunk_size || chunksize == 0 { - return Err(ResampleError::SyncNotAdjustable); + return Err(ResampleError::InvalidChunkSize { + max: self.max_chunk_size, + requested: chunksize, + }); } self.chunk_size = chunksize; self.update_needed_len(); @@ -1374,7 +1380,7 @@ mod tests { let params = basic_params(); let mut resampler = SincFixedOut::::new(1.2, 1.0, params, 1024, 2).unwrap(); assert_eq!(resampler.output_frames_next(), 1024); - resampler.set_chunksize(256).unwrap(); + resampler.set_chunk_size(256).unwrap(); assert_eq!(resampler.output_frames_next(), 256); check_output!(resampler); } @@ -1384,7 +1390,7 @@ mod tests { let params = basic_params(); let mut resampler = SincFixedIn::::new(1.2, 1.0, params, 1024, 2).unwrap(); assert_eq!(resampler.input_frames_next(), 1024); - resampler.set_chunksize(256).unwrap(); + resampler.set_chunk_size(256).unwrap(); assert_eq!(resampler.input_frames_next(), 256); check_output!(resampler); } diff --git a/src/error.rs b/src/error.rs index fec70aa..8ffc41f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -123,11 +123,20 @@ pub enum ResampleError { /// on a synchronous resampler. SyncNotAdjustable, /// Error raised when the number of channels in the input buffer doesn't match the value expected. - WrongNumberOfInputChannels { expected: usize, actual: usize }, + WrongNumberOfInputChannels { + expected: usize, + actual: usize, + }, /// Error raised when the number of channels in the output buffer doesn't match the value expected. - WrongNumberOfOutputChannels { expected: usize, actual: usize }, + WrongNumberOfOutputChannels { + expected: usize, + actual: usize, + }, /// Error raised when the number of channels in the mask doesn't match the value expected. - WrongNumberOfMaskChannels { expected: usize, actual: usize }, + WrongNumberOfMaskChannels { + expected: usize, + actual: usize, + }, /// Error raised when the number of frames in an input channel is less /// than the minimum expected. InsufficientInputBufferSize { @@ -142,6 +151,11 @@ pub enum ResampleError { expected: usize, actual: usize, }, + InvalidChunkSize { + max: usize, + requested: usize, + }, + ChunkSizeNotAdjustable, } impl fmt::Display for ResampleError { @@ -201,6 +215,16 @@ impl fmt::Display for ResampleError { actual, channel, expected ) } + Self::InvalidChunkSize { max, requested } => { + write!( + f, + "Invalid chunk size {}, value must be non-zero and cannot exceed {}", + requested, max + ) + } + Self::ChunkSizeNotAdjustable { .. } => { + write!(f, "This resampler does not support changing the chunk size") + } } } } diff --git a/src/lib.rs b/src/lib.rs index 22928ac..dba8bf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -427,8 +427,20 @@ where /// Reset the resampler state and clear all internal buffers. fn reset(&mut self); - fn set_chunksize(&mut self, _chunksize: usize) -> ResampleResult<()> { - Err(ResampleError::SyncNotAdjustable) + /// Change the chunk size for the resampler. + /// This is not supported by all resampler types. + /// The value must be equal to or smaller than the chunk size the value + /// that the resampler was created with. + /// [ResampleError::InvalidChunkSize] is returned if the value is zero or too large. + /// + /// The meaning of chunk size depends on the resampler, + /// it refers to the input size for FixedIn, + /// and output size for FixedOut types. + /// + /// Types that do not support changing the chunk size + /// return [ResampleError::ChunkSizeNotAdjustable]. + fn set_chunk_size(&mut self, _chunksize: usize) -> ResampleResult<()> { + Err(ResampleError::ChunkSizeNotAdjustable) } } From 58c8d0737149d2af52c828887a03a8c61e04dcdc Mon Sep 17 00:00:00 2001 From: Henrik Date: Sun, 2 Jun 2024 22:29:33 +0200 Subject: [PATCH 5/9] Readme and documentation updates --- Cargo.toml | 2 +- README.md | 13 +++++++------ src/asynchro_sinc.rs | 13 +++++++++++++ src/lib.rs | 2 ++ 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9021207..9f00773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rubato" -version = "0.15.0" +version = "0.16.0" rust-version = "1.61" authors = ["HEnquist "] description = "Asynchronous resampling library intended for audio data" diff --git a/README.md b/README.md index 470c710..c9bb6ee 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ for realtime use (it is disabled by default). ## Input and output data format -Input and output data is stored non-interleaved. +Input and output data are stored in a non-interleaved format. Input and output data are stored as slices of references, `&[AsRef<[f32]>]` or `&[AsRef<[f64]>]`. -The inner vectors (`AsRef<[f32]>` or `AsRef<[f64]>`) hold the sample values for one channel each. +The inner references (`AsRef<[f32]>` or `AsRef<[f64]>`) hold the sample values for one channel each. Since normal vectors implement the `AsRef` trait, `Vec>` and `Vec>` can be used for both input and output. @@ -51,9 +51,9 @@ The asynchronous resampler supports SIMD on x86_64 and on aarch64. The SIMD capabilities of the CPU are determined at runtime. If no supported SIMD instruction set is available, it falls back to a scalar implementation. -On x86_64 it will try to use AVX. If AVX isn't available, it will instead try SSE3. +On x86_64, it will try to use AVX. If AVX isn't available, it will instead try SSE3. -On aarch64 (64-bit Arm) it will use Neon if available. +On aarch64 (64-bit Arm), it will use Neon if available. ### Synchronous resampling @@ -101,8 +101,7 @@ let waves_out = resampler.process(&waves_in, None).unwrap(); ## Included examples 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. +There are also Python scripts for generating simple test signals as well as analyzing the resampled results. The examples read and write raw audio data in 64-bit float format. They can be used to process .wav files if the files are first converted to the right format. @@ -123,6 +122,8 @@ The `rubato` crate requires rustc version 1.61 or newer. ## Changelog +- v0.16.0 + - Add support for changing the fixed input or output size of the asynchronous resamplers. - v0.15.0 - Make FFT resamplers optional via `fft_resampler` feature. - Fix calculation of input and output sizes when creating FftFixedInOut resampler. diff --git a/src/asynchro_sinc.rs b/src/asynchro_sinc.rs index b72200c..ba68595 100644 --- a/src/asynchro_sinc.rs +++ b/src/asynchro_sinc.rs @@ -73,6 +73,12 @@ pub enum SincInterpolationType { /// An asynchronous resampler that accepts a fixed number of audio frames for input /// and returns a variable number of frames. +/// The number of input frames is determined by the chunk size argument to the constructor. +/// This value can be changed by the `set_chunk_size()` method, +/// to let the resampler process smaller chunks of audio data. +/// Note that the chunk size cannot exceed the value given at creation time. +/// The maximum value can be retrieved using the `input_size_max()` method, +/// and `input_frames_next()` gives the current value. /// /// The resampling is done by creating a number of intermediate points (defined by oversampling_factor) /// by sinc interpolation. The new samples are then calculated by interpolating between these points. @@ -102,6 +108,13 @@ pub struct SincFixedIn { /// The number of input frames required is given by the /// [input_frames_next](Resampler::input_frames_next) function. /// +/// The number of output frames is determined by the chunk size argument to the constructor. +/// This value can be changed by the `set_chunk_size()` method, +/// to let the resampler process smaller chunks of audio data. +/// Note that the chunk size cannot exceed the value given at creation time. +/// The maximum value can be retrieved using the `output_size_max()` method, +/// and `output_frames_next()` gives the current value. +/// /// The resampling is done by creating a number of intermediate points (defined by oversampling_factor) /// by sinc interpolation. The new samples are then calculated by interpolating between these points. /// diff --git a/src/lib.rs b/src/lib.rs index dba8bf8..75e6ecb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,8 @@ //! //! # Changelog //! +//! - v0.16.0 +//! - Add support for changing the fixed input or output size of the asynchronous resamplers. //! - v0.15.0 //! - Make FFT resamplers optional via `fft_resampler` feature. //! - Fix calculation of input and output sizes when creating FftFixedInOut resampler. From a154e07adedfdbbb6175f6feecf14a48cb3cbfd7 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Mon, 16 Sep 2024 20:27:34 +0200 Subject: [PATCH 6/9] Add a section to readme on intended workflow --- README.md | 94 +++++++++++++++++++++++++++++++ src/lib.rs | 159 +---------------------------------------------------- 2 files changed, 95 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index c9bb6ee..164dea5 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,100 @@ Synchronous resampling is implemented via FFT. The data is FFT:ed, the spectrum and then inverse FFT:ed to get the resampled data. This type of resampler is considerably faster but doesn't support changing the resampling ratio. +## Usage +The resamplers provided by this library are intended to process audio in chunks. +The optimal chunk size is determined by the application, +but will likely end up somwhere between a few hundred to a few thousand frames. +This gives a good compromize between efficiency and memory usage. + +### Real time considerations +Rubato is suitable for real-time applications when using the `Resampler::process_into_buffer()` method. +This stores the output in a pre-allocated output buffer, and performs no allocations or other +operations that may block the thread. + +### Resampling a given audio clip +A suggested simple process for resampling an audio clip of known length to a new sample rate is as follows. +Here it is assumed that the source data is stored in a vec, +or some other structure that supports reading arbitrary number of frames at a time. +For simplicity, the output is stored in a temporary buffer during resampling, +and copied to the destination afterwards. + +Preparations: +1. Create a resampler of suitable type, for example FFTFixedIn which is quite fast and gives good quality. + Since neither input or output has any restrictions for the number of frames that can be read or written at a time, + the chunk size can be chosen arbitrarily. Start with a chunk size of for example 1024. +2. Create an input buffer. +3. Create a temporary buffer for collecting the resampled output data. +4. Call `Resampler::output_delay()` to know how many frames of delay the resampler gives. + Store the number as `delay`. +5. Calculate the new clip length as `new_length = original_length * new_rate / original_rate`. + +Now it's time to process the bulk of the clip by repeated procesing calls. Loop: +1. Call `Resampler::input_frames_next()` to learn how many frames the resampler needs. +2. Check the number of available frames in the source. If it is less than the needed input size, break the loop. +3. Read the required number of frames from the source, convert the sample values to float, and copy them to the input buffer. +4. Call `Resampler::process()` or `Resampler::process_into_buffer()`. +5. Append the output frames to the temporary output buffer. + +The next step is to process the last remaining frames. +1. Read the available frames fom the source, convert the sample values to float, and copy them to the input buffer. +2. Call `Resampler::process_partial()` or `Resampler::process_partial_into_buffer()`. +3. Append the output frames to the temporary buffer. + +At this point, all frames have been sent to the resampler, +but because of the delay through the resampler, +it may still have some frames in its internal buffers. +When all wanted frames have been generated, the length of the temporary +output buffer should be at least `new_length + delay`. +If this is not the case, call `Resampler::process_partial()` +or `Resampler::process_partial_into_buffer()` with `None` as input, +and append the output to the temporary output buffer. +If needed, repeat until the length is sufficient. + +Finally, copy the data from the temporary output buffer to the desired destination. +Skip the first `delay` frames, and copy `new_length` frames. + +If there is more than one clip to resample from and to the same sample rates, +the same resampler should be reused. +Creating a new resampler is an expensive task and should be avoided if possible. +Start the procedire from the start, but instead of creating a new resampler, +call `Resampler::reset()` on the existing one to prepare it for a new job. + +### Resampling a stream +When resamping a stream, the process is normally performed in real time, +and either the input of output is some API that provides or consumes frames at a given rate. + +#### Example, record to file from an audio API +Audio APIs such as [CoreAudio](https://crates.io/crates/coreaudio-rs) on MacOS, +or the cross platform [cpal](https://crates.io/crates/cpal) crate, +often use callback functions for data exchange. + +When capturing audio from these, the application passes a function to the audio API. +The API then calls this function periodically, with a pointer to a data buffer containing new audio frames. +Typically the data buffer size is the same on every call. +It is important that the function does not block, +since this would block some internal loop of the API and cause loss of some audio data. +It is recommended to keep the callback function light. +Ideally it should read the provided audio data from the buffer provided by the API, +optionally perform some light processing such as sample format conversion, +and then store the audio data to a larger shared buffer. +The buffer may be a `Arc>>`, +or something more advanced such as [ringbuf](https://crates.io/crates/ringbuf). + +A separate thread should then read from that buffer, resample, and store to file. + +In this case, the Audio API provides a fixed input size. This value is then a good choice for the chunk size. + +The process is then similar to [resampling a clip](#resampling-a-given-audio-clip), +but the input is now the shared buffer. +The loop needs to wait for the needed number of frames to become available in the buffer, +before reading and passing them to the resampler. + +It would also be appropriate to omit the temporary output buffer, +and write the output directly to the destination. +The [hound](https://crates.io/crates/hound) crate is a popular choice +for reading and writing uncompressed audio formats. + ## SIMD acceleration ### Asynchronous resampling with anti-aliasing diff --git a/src/lib.rs b/src/lib.rs index 75e6ecb..f44f490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,161 +1,4 @@ -//! An audio sample rate conversion library for Rust. -//! -//! This library provides resamplers to process audio in chunks. -//! -//! The ratio between input and output sample rates is completely free. -//! Implementations are available that accept a fixed length input -//! while returning a variable length output, and vice versa. -//! -//! Rubato can be used in realtime applications without any allocation during -//! processing by preallocating a [Resampler] and using its -//! [input_buffer_allocate](Resampler::input_buffer_allocate) and -//! [output_buffer_allocate](Resampler::output_buffer_allocate) methods before -//! beginning processing. The [log feature](#log-enable-logging) feature should be disabled -//! for realtime use (it is disabled by default). -//! -//! # Input and output data format -//! -//! Input and output data are stored in a non-interleaved format. -//! -//! Input and output data are stored as slices of references, `&[AsRef<[f32]>]` or `&[AsRef<[f64]>]`. -//! The inner references (`AsRef<[f32]>` or `AsRef<[f64]>`) hold the sample values for one channel each. -//! -//! Since normal vectors implement the `AsRef` trait, -//! `Vec>` and `Vec>` can be used for both input and output. -//! -//! # Asynchronous resampling -//! -//! The asynchronous resamplers are available with and without anti-aliasing filters. -//! -//! Resampling with anti-aliasing is based on band-limited interpolation using sinc -//! interpolation filters. The sinc interpolation upsamples by an adjustable factor, -//! and then the new sample points are calculated by interpolating between these points. -//! The resampling ratio can be updated at any time. -//! -//! Resampling without anti-aliasing omits the cpu-heavy sinc interpolation. -//! This runs much faster but produces a lower quality result. -//! -//! # Synchronous resampling -//! -//! Synchronous resampling is implemented via FFT. The data is FFT:ed, the spectrum modified, -//! and then inverse FFT:ed to get the resampled data. -//! This type of resampler is considerably faster but doesn't support changing the resampling ratio. -//! -//! # SIMD acceleration -//! -//! ## Asynchronous resampling with anti-aliasing -//! -//! The asynchronous resampler supports SIMD on x86_64 and on aarch64. -//! The SIMD capabilities of the CPU are determined at runtime. -//! If no supported SIMD instruction set is available, it falls back to a scalar implementation. -//! -//! On x86_64, it will try to use AVX. If AVX isn't available, it will instead try SSE3. -//! -//! On aarch64 (64-bit Arm), it will use Neon if available. -//! -//! ## Synchronous resampling -//! -//! The synchronous resamplers benefit from the SIMD support of the RustFFT library. -//! -//! # Cargo features -//! -//! ## `fft_resampler`: Enable the FFT based synchronous resamplers -//! -//! This feature is enabled by default. Disable it if the FFT resamplers are not needed, -//! to save compile time and reduce the resulting binary size. -//! -//! ## `log`: Enable logging -//! -//! This feature enables logging via the `log` crate. This is intended for debugging purposes. -//! Note that outputting logs allocates a [std::string::String] and most logging implementations involve various other system calls. -//! These calls may take some (unpredictable) time to return, during which the application is blocked. -//! This means that logging should be avoided if using this library in a realtime application. -//! -//! # Example -//! -//! Resample a single chunk of a dummy audio file from 44100 to 48000 Hz. -//! See also the "process_f64" example that can be used to process a file from disk. -//! ``` -//! use rubato::{Resampler, SincFixedIn, SincInterpolationType, SincInterpolationParameters, WindowFunction}; -//! let params = SincInterpolationParameters { -//! sinc_len: 256, -//! f_cutoff: 0.95, -//! interpolation: SincInterpolationType::Linear, -//! oversampling_factor: 256, -//! window: WindowFunction::BlackmanHarris2, -//! }; -//! let mut resampler = SincFixedIn::::new( -//! 48000 as f64 / 44100 as f64, -//! 2.0, -//! params, -//! 1024, -//! 2, -//! ).unwrap(); -//! -//! let waves_in = vec![vec![0.0f64; 1024];2]; -//! let waves_out = resampler.process(&waves_in, None).unwrap(); -//! ``` -//! -//! # Included examples -//! -//! 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. -//! -//! The examples read and write raw audio data in 64-bit float format. -//! They can be used to process .wav files if the files are first converted to the right format. -//! Use `sox` to convert a .wav to raw samples: -//! ```sh -//! sox some_file.wav -e floating-point -b 64 some_file_f64.raw -//! ``` -//! After processing, the result can be converted back to new .wav. This examples converts to 16-bits at 44.1 kHz: -//! ```sh -//! sox -e floating-point -b 64 -r 44100 -c 2 resampler_output.raw -e signed-integer -b 16 some_file_resampled.wav -//! ``` -//! -//! Many audio editors, for example Audacity, are also able to directly import and export the raw samples. -//! -//! # Compatibility -//! -//! The `rubato` crate requires rustc version 1.61 or newer. -//! -//! # Changelog -//! -//! - v0.16.0 -//! - Add support for changing the fixed input or output size of the asynchronous resamplers. -//! - v0.15.0 -//! - Make FFT resamplers optional via `fft_resampler` feature. -//! - Fix calculation of input and output sizes when creating FftFixedInOut resampler. -//! - Fix panic when using very small chunksizes (less than 5). -//! - v0.14.1 -//! - More bugfixes for buffer allocation and max output length calculation. -//! - Fix building with `log` feature. -//! - v0.14.0 -//! - Add argument to let `input/output_buffer_allocate()` optionally pre-fill buffers with zeros. -//! - Add convenience methods for managing buffers. -//! - Bugfixes for buffer allocation and max output length calculation. -//! - v0.13.0 -//! - Switch to slices of references for input and output data. -//! - Add faster (lower quality) asynchronous resamplers. -//! - Add a macro to help implement custom object safe resamplers. -//! - Optional smooth ramping of ratio changes to avoid audible steps. -//! - Add convenience methods for handling last frames in a stream. -//! - Add resampler reset method. -//! - Refactoring for a more logical structure. -//! - Add helper function for calculating cutoff frequency. -//! - Add quadratic interpolation for sinc resampler. -//! - Add method to get the delay through a resampler as a number of output frames. -//! - v0.12.0 -//! - Always enable all simd acceleration (and remove the simd Cargo features). -//! - v0.11.0 -//! - New api to allow use in realtime applications. -//! - Configurable adjust range of asynchronous resamplers. -//! - v0.10.1 -//! - Fix compiling with neon feature after changes in latest nightly. -//! - v0.10.0 -//! - Add an object-safe wrapper trait for Resampler. -//! - v0.9.0 -//! - Accept any AsRef<\[T\]> as input. -//! +#![doc = include_str!("../README.md")] #[cfg(feature = "log")] extern crate log; From d4e3c50170c565b981bb9ff823461d9b08fcc93c Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 17 Sep 2024 21:17:32 +0200 Subject: [PATCH 7/9] More details on chunk size --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 164dea5..8f56e70 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rubato +# Rubato An audio sample rate conversion library for Rust. @@ -111,23 +111,32 @@ Audio APIs such as [CoreAudio](https://crates.io/crates/coreaudio-rs) on MacOS, or the cross platform [cpal](https://crates.io/crates/cpal) crate, often use callback functions for data exchange. +A complete + When capturing audio from these, the application passes a function to the audio API. The API then calls this function periodically, with a pointer to a data buffer containing new audio frames. -Typically the data buffer size is the same on every call. +The data buffer size is usually the same on every call, but that varies between APIs. It is important that the function does not block, since this would block some internal loop of the API and cause loss of some audio data. It is recommended to keep the callback function light. Ideally it should read the provided audio data from the buffer provided by the API, -optionally perform some light processing such as sample format conversion, -and then store the audio data to a larger shared buffer. +and optionally perform some light processing such as sample format conversion. +No heavy processing such as resampling should be performed here. +It should then store the audio data to a shared buffer. The buffer may be a `Arc>>`, or something more advanced such as [ringbuf](https://crates.io/crates/ringbuf). -A separate thread should then read from that buffer, resample, and store to file. - -In this case, the Audio API provides a fixed input size. This value is then a good choice for the chunk size. - -The process is then similar to [resampling a clip](#resampling-a-given-audio-clip), +A separate loop, running either in the main or a separate thread, +should then read from that buffer, resample, and save to file. +If the Audio API provides a fixed buffer size, +then this number of frames is a good choice for the resampler chunk size. +If the size varies, the shared buffer can be used to adapt the chunk sizes of the audio API and the resampler. +A good starting point for the resampler chunk size is to use an "easy" value +near the average chunk size of the audio API. +Make sure that the shared buffer is large enough to not get full +in case for the loop gets blocked waiting for example for disk access. + +The loop should follow a process similar to [resampling a clip](#resampling-a-given-audio-clip), but the input is now the shared buffer. The loop needs to wait for the needed number of frames to become available in the buffer, before reading and passing them to the resampler. @@ -137,6 +146,7 @@ and write the output directly to the destination. The [hound](https://crates.io/crates/hound) crate is a popular choice for reading and writing uncompressed audio formats. + ## SIMD acceleration ### Asynchronous resampling with anti-aliasing From 3c871ead58a476ceb82cb4da9a3b32e5062c4dd2 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 28 Sep 2024 10:26:44 +0200 Subject: [PATCH 8/9] Documentation updates --- README.md | 8 ++++++++ src/lib.rs | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 8f56e70..02f1d5f 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,14 @@ Note that outputting logs allocates a [std::string::String] and most logging imp These calls may take some (unpredictable) time to return, during which the application is blocked. This means that logging should be avoided if using this library in a realtime application. +The `log` feature can be enabled when running tests, which can be very useful when debugging. +The logging level can be set via the `RUST_LOG` environment variable. + +Example: +```sh +RUST_LOG=trace cargo test --features log +``` + ## Example Resample a single chunk of a dummy audio file from 44100 to 48000 Hz. diff --git a/src/lib.rs b/src/lib.rs index b694c4e..0ec4c57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,6 +237,11 @@ where /// Get the number of frames per channel that will be output from the next call to /// [process_into_buffer](Resampler::process_into_buffer) or [process](Resampler::process). + /// For the resamplers with a fixed output size, sush as [FastFixedOut], + /// this gives the exact number. + /// For the resamplers with a varying output size, like [FastFixedIn], + /// the number is an estimation that may be a few frames larger than + /// (and never smaller than) the actual number of output frames. fn output_frames_next(&self) -> usize; /// Get the delay for the resampler, reported as a number of output frames. From 8f88e88683fbb22b0adee5a70789ed4880d9f920 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sat, 28 Sep 2024 10:51:21 +0200 Subject: [PATCH 9/9] Update checkout action --- .github/workflows/ci_test.yml | 8 ++++---- .github/workflows/publish.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_test.yml b/.github/workflows/ci_test.yml index 0203afd..676cbbe 100644 --- a/.github/workflows/ci_test.yml +++ b/.github/workflows/ci_test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install toolchain uses: dtolnay/rust-toolchain@stable @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install toolchain uses: dtolnay/rust-toolchain@stable @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install toolchain uses: dtolnay/rust-toolchain@stable @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d242925..ac64ed3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install toolchain uses: dtolnay/rust-toolchain@stable