Skip to content

Commit e912257

Browse files
HEnquistclaude
andcommitted
Add adjust_ratio_f64 example for the adjustable resamplers
Companion to process_all_f64: instead of converting between two fixed rates, this drives any of the adjustable resamplers (Async sinc/poly and Slip, fixed input or output) at a nominal 1:1 ratio and applies a small user-selected rate offset given in ppm. This is the clock-drift / rate-matching case. The offset is applied through `Resampler::as_adjustable`, so one code path handles every adjustable type without knowing the concrete resampler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 162920d commit e912257

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

examples/adjust_ratio_f64.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
extern crate rubato;
2+
use audioadapter_buffers::direct::InterleavedSlice;
3+
use rubato::{
4+
Async, FixedAsync, PolynomialDegree, Resampler, SincInterpolationParameters,
5+
SincInterpolationType, Slip, WindowFunction,
6+
};
7+
use std::convert::TryInto;
8+
use std::env;
9+
use std::fs::File;
10+
use std::io::prelude::{Read, Seek, Write};
11+
use std::io::{BufReader, BufWriter};
12+
use std::time::Instant;
13+
14+
extern crate env_logger;
15+
extern crate log;
16+
use env_logger::Builder;
17+
use log::LevelFilter;
18+
const BYTE_PER_SAMPLE: usize = 8;
19+
20+
// A resampler app that reads a raw file of little-endian 64 bit floats, and writes the output in the same format.
21+
// Unlike the `process_all_f64` example, which converts between two fixed rates, this one uses one of the
22+
// *adjustable* resamplers to apply a small, constant rate offset. This is the clock-drift / rate-matching case:
23+
// the nominal input and output rates are equal (ratio 1:1), and the resampler is nudged by a user-selected
24+
// offset given in parts per million (ppm). A positive offset produces slightly more output frames than input,
25+
// a negative offset slightly fewer.
26+
//
27+
// The offset is applied through the `Resampler::as_adjustable` capability accessor, so the same code drives every
28+
// adjustable resampler type without knowing the concrete type. The synchronous FFT resamplers cannot change
29+
// ratio and are therefore not offered here; use the `process_all_f64` example for fixed-ratio conversion.
30+
//
31+
// The command line arguments are resampler type, input filename, output filename, number of channels,
32+
// and the rate offset in ppm. The adjustable resamplers support offsets up to roughly +/- 10%.
33+
// To apply a +50 ppm offset to the two-channel file `sine_f64_2ch.raw` using the Slip resampler:
34+
// ```
35+
// cargo run --release --example adjust_ratio_f64 SlipFixedOutput sine_f64_2ch.raw test.raw 2 50
36+
// ```
37+
38+
/// Helper to read an entire file to memory as f64 values
39+
fn read_file<R: Read + Seek>(inbuffer: &mut R) -> Vec<f64> {
40+
let mut buffer = vec![0u8; BYTE_PER_SAMPLE];
41+
let mut data = Vec::new();
42+
loop {
43+
let bytes_read = inbuffer.read(&mut buffer).unwrap();
44+
if bytes_read == 0 {
45+
break;
46+
}
47+
let value = f64::from_le_bytes(buffer.as_slice().try_into().unwrap());
48+
data.push(value);
49+
}
50+
data
51+
}
52+
53+
/// Helper to write all frames to a file
54+
fn write_file<W: Write + Seek>(data: &[f64], output: &mut W, values_to_write: usize) {
55+
for value in data.iter().take(values_to_write) {
56+
let bytes = value.to_le_bytes();
57+
output.write_all(&bytes).unwrap();
58+
}
59+
}
60+
61+
fn main() {
62+
// init logger
63+
let mut builder = Builder::from_default_env();
64+
builder.filter(None, LevelFilter::Debug).init();
65+
66+
let resampler_type = env::args().nth(1).expect(
67+
"Please specify a resampler type, one of:\nSincFixedInput\nSincFixedOutput\nPolyFixedInput\nPolyFixedOutput\nSlipFixedInput\nSlipFixedOutput",
68+
);
69+
70+
let file_in = env::args().nth(2).expect("Please specify an input file.");
71+
let file_out = env::args().nth(3).expect("Please specify an output file.");
72+
println!("Opening files: {}, {}", file_in, file_out);
73+
74+
let channels_str = env::args()
75+
.nth(4)
76+
.expect("Please specify number of channels");
77+
let channels = channels_str.parse::<usize>().unwrap();
78+
79+
let offset_str = env::args()
80+
.nth(5)
81+
.expect("Please specify the rate offset in ppm");
82+
let offset_ppm = offset_str.parse::<f64>().unwrap();
83+
let rel_ratio = 1.0 + offset_ppm / 1_000_000.0;
84+
println!(
85+
"Applying a rate offset of {} ppm (relative ratio {})",
86+
offset_ppm, rel_ratio
87+
);
88+
89+
println!("Copy input file to buffer");
90+
let file_in_disk = File::open(file_in).expect("Can't open file");
91+
let mut file_in_reader = BufReader::new(file_in_disk);
92+
let indata = read_file(&mut file_in_reader);
93+
let nbr_input_frames = indata.len() / channels;
94+
95+
// The nominal ratio is 1:1, so the output has at most `rel_ratio` frames per input frame.
96+
// The factor of two leaves generous headroom for the resampler's internal buffering.
97+
let mut outdata = vec![0.0; 2 * channels * (nbr_input_frames as f64 * rel_ratio) as usize];
98+
99+
println!("Creating resampler");
100+
// Every branch is built at the nominal ratio of 1.0. The asynchronous resamplers get a maximum
101+
// relative ratio of 1.1, matching the Slip resampler's built-in +/- 10% range.
102+
let chunk_size = 1024;
103+
let mut resampler: Box<dyn Resampler<f64>> = match resampler_type.as_str() {
104+
"SincFixedInput" => {
105+
let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2)
106+
.oversampling_factor(256)
107+
.interpolation(SincInterpolationType::Quadratic);
108+
Box::new(
109+
Async::<f64>::new_sinc(1.0, 1.1, &params, chunk_size, channels, FixedAsync::Input)
110+
.unwrap(),
111+
)
112+
}
113+
"SincFixedOutput" => {
114+
let params = SincInterpolationParameters::new(128, WindowFunction::Blackman2)
115+
.oversampling_factor(256)
116+
.interpolation(SincInterpolationType::Quadratic);
117+
Box::new(
118+
Async::<f64>::new_sinc(1.0, 1.1, &params, chunk_size, channels, FixedAsync::Output)
119+
.unwrap(),
120+
)
121+
}
122+
"PolyFixedInput" => Box::new(
123+
Async::<f64>::new_poly(
124+
1.0,
125+
1.1,
126+
PolynomialDegree::Septic,
127+
chunk_size,
128+
channels,
129+
FixedAsync::Input,
130+
)
131+
.unwrap(),
132+
),
133+
"PolyFixedOutput" => Box::new(
134+
Async::<f64>::new_poly(
135+
1.0,
136+
1.1,
137+
PolynomialDegree::Septic,
138+
chunk_size,
139+
channels,
140+
FixedAsync::Output,
141+
)
142+
.unwrap(),
143+
),
144+
"SlipFixedInput" => {
145+
Box::new(Slip::<f64>::new(chunk_size, channels, FixedAsync::Input).unwrap())
146+
}
147+
"SlipFixedOutput" => {
148+
Box::new(Slip::<f64>::new(chunk_size, channels, FixedAsync::Output).unwrap())
149+
}
150+
_ => panic!(
151+
"Unknown or non-adjustable resampler type {}\nMust be one of SincFixedInput, SincFixedOutput, PolyFixedInput, PolyFixedOutput, SlipFixedInput, SlipFixedOutput",
152+
resampler_type
153+
),
154+
};
155+
156+
// Recover the adjust-ratio capability from the trait object and apply the offset once. Since the
157+
// nominal ratio is 1.0, the relative ratio is also the absolute ratio. `ramp` is false so the new
158+
// ratio takes effect from the first chunk.
159+
let adjustable = resampler
160+
.as_adjustable()
161+
.expect("the selected resampler type is adjustable");
162+
if let Err(e) = adjustable.set_resample_ratio_relative(rel_ratio, false) {
163+
panic!(
164+
"Could not apply an offset of {} ppm: {}. The adjustable resamplers support roughly +/- 10%.",
165+
offset_ppm, e
166+
);
167+
}
168+
169+
// Prepare
170+
let input_adapter = InterleavedSlice::new(&indata, channels, nbr_input_frames).unwrap();
171+
let outdata_capacity = outdata.len() / channels;
172+
let mut output_adapter =
173+
InterleavedSlice::new_mut(&mut outdata, channels, outdata_capacity).unwrap();
174+
175+
println!("Processing...");
176+
let start = Instant::now();
177+
178+
let (nbr_in, nbr_out) = resampler
179+
.process_all_into_buffer(&input_adapter, &mut output_adapter, nbr_input_frames, None)
180+
.unwrap();
181+
182+
let duration = start.elapsed();
183+
println!("Resampling took: {:?}", duration);
184+
185+
println!(
186+
"Processed {} input frames into {} output frames (realized ratio {:.6})",
187+
nbr_in,
188+
nbr_out,
189+
nbr_out as f64 / nbr_in as f64
190+
);
191+
192+
println!("Write output to file, trimming off the silent frames from both ends.");
193+
let mut file_out_disk = BufWriter::new(File::create(file_out).unwrap());
194+
write_file(&outdata, &mut file_out_disk, nbr_out * channels);
195+
}

0 commit comments

Comments
 (0)