Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <henrik.enquist@gmail.com>"]
description = "Asynchronous resampling library intended for audio data"
license = "MIT OR Apache-2.0"
Expand All @@ -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"

Expand All @@ -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"
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions benches/resamplers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod bench_asyncro {
1.1,
interpolation_type,
interpolator,
f_cutoff,
chunksize,
channels,
FixedAsync::Input,
Expand Down
102 changes: 67 additions & 35 deletions examples/adjust_ratio_f64.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -18,23 +18,71 @@ 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,
// a negative offset slightly fewer.
//
// 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<R: Read + Seek>(inbuffer: &mut R) -> Vec<f64> {
let mut buffer = vec![0u8; BYTE_PER_SAMPLE];
Expand Down Expand Up @@ -66,31 +114,19 @@ 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::<usize>().unwrap();

let offset_str = env::args()
.nth(5)
.expect("Please specify the rate offset in ppm");
let offset_ppm = offset_str.parse::<f64>().unwrap();
let rel_ratio = 1.0 + offset_ppm / 1_000_000.0;
println!(
"Applying a rate offset of {} ppm (relative ratio {})",
offset_ppm, rel_ratio
);

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;
Expand All @@ -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<dyn Resampler<f64>> = match resampler_type.as_str() {
"SincFixedInput" => {
let mut resampler: Box<dyn Resampler<f64>> = match opts.resampler {
ResamplerType::SincFixedInput => {
let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2)
.oversampling_factor(256)
.interpolation(SincInterpolationType::Quadratic);
Expand All @@ -113,7 +149,7 @@ fn main() {
.unwrap(),
)
}
"SincFixedOutput" => {
ResamplerType::SincFixedOutput => {
let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2)
.oversampling_factor(256)
.interpolation(SincInterpolationType::Quadratic);
Expand All @@ -122,7 +158,7 @@ fn main() {
.unwrap(),
)
}
"PolyFixedInput" => Box::new(
ResamplerType::PolyFixedInput => Box::new(
Async::<f64>::new_poly(
1.0,
1.1,
Expand All @@ -133,7 +169,7 @@ fn main() {
)
.unwrap(),
),
"PolyFixedOutput" => Box::new(
ResamplerType::PolyFixedOutput => Box::new(
Async::<f64>::new_poly(
1.0,
1.1,
Expand All @@ -144,16 +180,12 @@ fn main() {
)
.unwrap(),
),
"SlipFixedInput" => {
ResamplerType::SlipFixedInput => {
Box::new(Slip::<f64>::new(chunk_size, channels, FixedAsync::Input).unwrap())
}
"SlipFixedOutput" => {
ResamplerType::SlipFixedOutput => {
Box::new(Slip::<f64>::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
Expand Down Expand Up @@ -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);
}
Loading