Skip to content

Commit 3249382

Browse files
committed
Address review comments
Drop a duplicated sentence in the docs for Fft::cutoff, and pin the crossfade values that the docs for Slip::new quote with an assert, so they cannot drift unnoticed. Replace the float rounding used to size the FFT blocks and the sinc filter with integer arithmetic. Dividing as f32 loses precision for large values, and div_ceil says what is meant. Rounding down to zero blocks now falls back to a single block instead of panicking further down.
1 parent 1116c1a commit 3249382

3 files changed

Lines changed: 40 additions & 19 deletions

File tree

src/asynchro_sinc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ pub enum SincInterpolationType {
212212

213213
/// Round the sinc length up to the multiple of 8 that the interpolators use.
214214
pub(crate) fn round_sinc_len(sinc_len: usize) -> usize {
215-
8 * (((sinc_len as f32) / 8.0).ceil() as usize)
215+
sinc_len.next_multiple_of(8)
216216
}
217217

218218
/// Resolve the relative cutoff frequency of the sinc filter.

src/slip.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ mod tests {
601601
/// leaving room for a correction (`chunk >= 2 * len + 2`).
602602
#[test]
603603
fn crossfade_len_scales_with_chunk() {
604+
// The docs on Slip::new quote these two values, since they are what a caller
605+
// needs to pick a chunk size. Keep them in sync if the target ever changes.
606+
assert_eq!(MAX_CROSSFADE_LEN, 128);
607+
assert_eq!(2 * MAX_CROSSFADE_LEN + 2, 258);
604608
// Capped at the target once the chunk is big enough to hold it.
605609
assert_eq!(crossfade_len_for(4096), MAX_CROSSFADE_LEN);
606610
assert_eq!(

src/synchro.rs

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,21 @@ where
301301
FixedSync::Input => {
302302
let min_chunk_in = sample_rate_input / gcd;
303303
let wanted_subsize = chunk_size / sub_chunks;
304-
(wanted_subsize as f32 / min_chunk_in as f32).ceil() as usize
304+
wanted_subsize.div_ceil(min_chunk_in)
305305
}
306306
FixedSync::Output => {
307307
let min_chunk_out = sample_rate_output / gcd;
308308
let wanted_subsize = chunk_size / sub_chunks;
309-
(wanted_subsize as f32 / min_chunk_out as f32).ceil() as usize
309+
wanted_subsize.div_ceil(min_chunk_out)
310310
}
311311
FixedSync::Both => {
312312
let min_chunk_in = sample_rate_input / gcd;
313-
(chunk_size as f32 / min_chunk_in as f32).ceil() as usize
313+
chunk_size.div_ceil(min_chunk_in)
314314
}
315-
};
315+
}
316+
// Asking for more sub chunks than there are frames rounds down to zero blocks,
317+
// which is not a usable resampler. Fall back to a single minimum sized block.
318+
.max(1);
316319
let fft_size_out = fft_chunks * sample_rate_output / gcd;
317320
let fft_size_in = fft_chunks * sample_rate_input / gcd;
318321

@@ -397,8 +400,6 @@ where
397400
/// A larger block moves it closer to Nyquist.
398401
/// When downsampling it is scaled down to keep it below the lower Nyquist
399402
/// frequency of the two sample rates.
400-
///
401-
/// Multiply by `sample_rate_input / 2` to get the cutoff in Hz.
402403
pub fn cutoff(&self) -> f32 {
403404
self.resampler.cutoff
404405
}
@@ -412,21 +413,20 @@ where
412413
) -> (usize, usize) {
413414
match fixed {
414415
FixedSync::Input => {
415-
let subchunks_available: f32 =
416-
((chunk_size + saved_frames) as f32 / fft_size_in as f32).floor();
417-
let frames_available = (subchunks_available as usize) * fft_size_out;
416+
let subchunks_available = (chunk_size + saved_frames) / fft_size_in;
417+
let frames_available = subchunks_available * fft_size_out;
418418
(chunk_size, frames_available)
419419
}
420420
FixedSync::Output => {
421-
let subchunks_needed = ((chunk_size as f32 - saved_frames as f32)
422-
/ fft_size_out as f32)
423-
.ceil()
424-
.max(0.0);
425-
let frames_needed = (subchunks_needed as usize) * fft_size_in;
421+
// Saturating, since more frames may be saved than the chunk needs.
422+
let subchunks_needed = chunk_size
423+
.saturating_sub(saved_frames)
424+
.div_ceil(fft_size_out);
425+
let frames_needed = subchunks_needed * fft_size_in;
426426
(frames_needed, chunk_size)
427427
}
428428
FixedSync::Both => {
429-
let subchunks_needed = (chunk_size as f32 / fft_size_in as f32).ceil() as usize;
429+
let subchunks_needed = chunk_size.div_ceil(fft_size_in);
430430
let frames_needed_in = subchunks_needed * fft_size_in;
431431
let frames_needed_out = subchunks_needed * fft_size_out;
432432
(frames_needed_in, frames_needed_out)
@@ -467,9 +467,7 @@ where
467467
) -> usize {
468468
match fixed {
469469
FixedSync::Both | FixedSync::Input => chunk_size_in,
470-
FixedSync::Output => {
471-
(chunk_size_out as f32 / fft_size_out as f32).ceil() as usize * fft_size_in
472-
}
470+
FixedSync::Output => chunk_size_out.div_ceil(fft_size_out) * fft_size_in,
473471
}
474472
}
475473

@@ -759,6 +757,25 @@ mod tests {
759757
assert!((maxval - 1.0).abs() < 0.1);
760758
}
761759

760+
#[test_log::test(test_matrix([FixedSync::Input, FixedSync::Output, FixedSync::Both]))]
761+
fn fft_more_sub_chunks_than_frames(fixed: FixedSync) {
762+
// Asking for more sub chunks than the chunk has frames rounds the sub chunk
763+
// size down to zero. That used to give zero FFT blocks, and a panic on the
764+
// first division by the block size.
765+
let resampler = Fft::<f64>::new_custom(
766+
44100,
767+
48000,
768+
100,
769+
1000,
770+
2,
771+
WindowFunction::BlackmanHarris2,
772+
fixed,
773+
)
774+
.unwrap();
775+
assert_eq!(resampler.fft_size_in(), 147);
776+
assert_eq!(resampler.fft_size_out(), 160);
777+
}
778+
762779
#[test_log::test(test_matrix(
763780
[512, 1024, 4096],
764781
[(44100, 48000), (48000, 44100), (44100, 88200), (88200, 44100), (44100, 192000), (192000, 44100)],

0 commit comments

Comments
 (0)