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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed sources to detect parameter updates after mid-span seeks.
- Fixed `Stoppable` and `Skippable` not signaling exhaustion.
- Fixed `SpatialAudio` left and write channel swapping
- Fixed resampled output depending on how the input is split into spans.

## Version [0.22.2] (2026-03-05)

Expand Down
167 changes: 70 additions & 97 deletions src/conversions/sample_rate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ where
}
}

/// Rebuild for the format the input has moved on to.
fn recreate(&mut self) {
let source = self.inner.take().expect("always set").into_inner();
self.inner = Some(Self::create_resampler(
source,
self.target_rate,
&self.config,
));
self.pending_recreate = false;
self.cached_input_span_len = self.resampler().input().current_span_len();
}

fn next_resampled(&mut self) -> Option<Sample> {
match self.resampler_mut() {
ResampleInner::Passthrough { source, .. } => source.next(),
ResampleInner::Poly(resampler) => resampler.next_sample(),
ResampleInner::Sinc(resampler) => resampler.next_sample(),
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(resampler) => resampler.next_sample(),
}
}

/// Helper method to create a resampler from a source using the stored config and target rate.
fn create_resampler(
source: I,
Expand Down Expand Up @@ -330,35 +352,17 @@ where
self.pending_recreate = false;
let input_span_len = self.resampler().input().current_span_len();

match self.inner.as_mut().unwrap() {
ResampleInner::Passthrough {
input_span_pos: input_samples_consumed,
..
} => {
reset_seek_span_tracking(
input_samples_consumed.raw_mut(),
&mut self.cached_input_span_len,
position,
input_span_len,
);
}
ResampleInner::Poly(r) | ResampleInner::Sinc(r) => {
reset_seek_span_tracking(
r.pos_in_current_span.raw_mut(),
&mut self.cached_input_span_len,
position,
input_span_len,
);
}
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(r) => {
reset_seek_span_tracking(
r.pos_in_current_span.raw_mut(),
&mut self.cached_input_span_len,
position,
input_span_len,
);
}
// Only the passthrough counts samples against the span length;
// `reset` already cleared the resamplers' own tracking.
if let ResampleInner::Passthrough { input_span_pos, .. } =
self.inner.as_mut().expect("always set")
{
reset_seek_span_tracking(
input_span_pos.raw_mut(),
&mut self.cached_input_span_len,
position,
input_span_len,
);
}

Ok(())
Expand All @@ -376,60 +380,48 @@ where
// If a format change was detected at the previous span boundary, wait until the
// output buffer is fully drained before recreating the resampler. This guarantees
// that fill_input_buffer only ever reads from the current span.
if self.pending_recreate {
let output_empty = match self.resampler() {
ResampleInner::Passthrough { .. } => true,
ResampleInner::Poly(r) | ResampleInner::Sinc(r) => !r.output_has_samples(),
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(r) => !r.output_has_samples(),
};
if output_empty {
let source = self.inner.take().unwrap().into_inner();
self.inner = Some(Self::create_resampler(
source,
self.target_rate,
&self.config,
));
self.pending_recreate = false;
let sample = loop {
if self.pending_recreate {
let output_empty = match self.resampler() {
ResampleInner::Passthrough { .. } => true,
ResampleInner::Poly(r) | ResampleInner::Sinc(r) => !r.output_has_samples(),
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(r) => !r.output_has_samples(),
};
if output_empty {
self.recreate();
}
}
}

let sample = match self.resampler_mut() {
ResampleInner::Passthrough { source, .. } => source.next()?,
ResampleInner::Poly(resampler) => resampler.next_sample()?,
ResampleInner::Sinc(resampler) => resampler.next_sample()?,
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(resampler) => resampler.next_sample()?,
match self.next_resampled() {
Some(sample) => break sample,
// Built for one format, a resampler stops where its input
// changes rate or channels. Rebuilding matches the format
// it stopped at, so this retries at most once.
None if self.resampler().built_for_input() => return None,
None => self.recreate(),
}
};

// If input reports no span length, parameters are stable by contract
let input_span_len = self.resampler().input().current_span_len();
// Only the passthrough tracks spans here; a resampler stops at a
// format change itself while filling, and the loop above rebuilds it.
let ResampleInner::Passthrough {
input_span_pos,
channels,
source_rate,
source,
} = self.resampler_mut()
else {
return Some(sample);
};
let input_span_len = source.current_span_len();
// No span length means the format is stable by contract.
if input_span_len.is_none() {
return Some(sample);
}

let (expected_channels, expected_rate, samples_consumed) = match self.resampler_mut() {
ResampleInner::Passthrough {
input_span_pos: input_samples_consumed,
channels,
source_rate,
..
} => {
*input_samples_consumed += 1usize;
(*channels, *source_rate, *input_samples_consumed)
}
ResampleInner::Poly(r) | ResampleInner::Sinc(r) => (
r.output.channels,
r.input.sample_rate(),
r.pos_in_current_span,
),
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(r) => (
r.output.channels,
r.input.sample_rate(),
r.pos_in_current_span,
),
};
*input_span_pos += 1usize;
let (expected_channels, expected_rate, samples_consumed) =
(*channels, *source_rate, *input_span_pos);

let input = self.resampler().input();
let (at_boundary, parameters_changed) = Self::detect_boundary(
Expand All @@ -442,30 +434,11 @@ where
);

if at_boundary {
// Update cached span length (exits detection mode if we were in it)
self.cached_input_span_len = input_span_len;

if parameters_changed {
// Defer recreation until the output buffer is drained (handled above at the
// top of the next next() call) so no cross-span reads occur.
self.pending_recreate = true;
} else {
// Just crossed boundary without parameter change, reset counter
match self.resampler_mut() {
ResampleInner::Passthrough {
input_span_pos: input_samples_consumed,
..
} => {
*input_samples_consumed = InSamples::ZERO;
}
ResampleInner::Poly(r) | ResampleInner::Sinc(r) => {
r.pos_in_current_span = InSamples::ZERO;
}
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(r) => {
r.pos_in_current_span = InSamples::ZERO;
}
}
} else if let ResampleInner::Passthrough { input_span_pos, .. } = self.resampler_mut() {
*input_span_pos = InSamples::ZERO;
}
}

Expand Down
76 changes: 61 additions & 15 deletions src/conversions/sample_rate/rubato.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ impl<I: Source> ResampleInner<I> {
}
}

/// Whether the input still has the format this was built for.
#[inline]
pub fn built_for_input(&self) -> bool {
let (channels, source_rate) = match self {
ResampleInner::Passthrough {
channels,
source_rate,
..
} => (*channels, *source_rate),
ResampleInner::Poly(resampler) | ResampleInner::Sinc(resampler) => {
(resampler.output.channels, resampler.output.source_rate)
}
#[cfg(feature = "rubato-fft")]
ResampleInner::Fft(resampler) => {
(resampler.output.channels, resampler.output.source_rate)
}
};
self.input().channels() == channels && self.input().sample_rate() == source_rate
}

/// Extract the inner input source, consuming the resampler
#[inline]
pub fn into_inner(self) -> I {
Expand All @@ -83,7 +103,11 @@ pub struct RubatoResample<I: Source, R: rubato::Resampler<Sample>> {
pub resample_ratio: Float,

pub output_delay_remaining: OutFrameCount,
pub pos_in_current_span: InSamples,

/// How much of the span being read is left. Tracked because
/// `Source::current_span_len` reports the span's total length,
/// not its remainder.
span_remaining: InSamples,

pub frames_being_resampled: OutFrameCount,
}
Expand Down Expand Up @@ -118,7 +142,7 @@ impl<I: Source, R: rubato::Resampler<Sample>> RubatoResample<I, R> {
pub fn reset(&mut self) {
self.resampler.reset();
self.output.reset();
self.pos_in_current_span = InSamples::ZERO;
self.span_remaining = InSamples::ZERO;
self.output_delay_remaining = Self::output_delay(&self.resampler);
}

Expand Down Expand Up @@ -147,7 +171,6 @@ impl<I: Source, R: rubato::Resampler<Sample>> RubatoResample<I, R> {
}

self.frames_being_resampled += frames_in.resampled_by(self.resample_ratio);
self.pos_in_current_span += frames_in.samples(self.output.channels);

let indexing = Some(&rubato::Indexing {
input_offset: 0,
Expand Down Expand Up @@ -192,23 +215,46 @@ impl<I: Source, R: rubato::Resampler<Sample>> RubatoResample<I, R> {
Some(())
}

/// Reads across spans, since the resampler is built for one format
/// and only a span that changes it has to end the chunk. A short
/// read then means end of stream or a format change, which is when
/// rubato should treat the chunk as partial.
fn fill_input_buffer(&mut self, needed_by_resampler: InFrameCount) -> InFrameCount {
let current_span_length = self.input.current_span_len().map(InSamples);
let frames_to_take = needed_by_resampler
.samples(self.output.channels)
.min(current_span_length.unwrap_or(InSamples::MAX));

let wanted = needed_by_resampler.samples(self.output.channels);
self.input_buffer.clear();
for _ in 0..frames_to_take.raw() {
if let Some(sample) = self.input.next() {
self.input_buffer.push(sample);
} else {

while self.input_buffer.len() < wanted {
if self.span_remaining == InSamples::ZERO && !self.start_of_span() {
break;
}
let Some(sample) = self.input.next() else {
break;
};
self.input_buffer.push(sample);
self.span_remaining -= InSamples(1);
}

self.input_buffer.len().frames(self.output.channels)
}

/// Whether reading may continue into the next span, recording its
/// length. `false` once the stream ends or the format changes.
fn start_of_span(&mut self) -> bool {
let Some(span_len) = self.input.current_span_len() else {
// Without spans the format never changes.
self.span_remaining = InSamples::MAX;
return true;
};
if span_len == 0
|| self.input.channels() != self.output.channels
|| self.input.sample_rate() != self.output.source_rate
{
return false;
}
self.span_remaining = InSamples(span_len);
true
}

fn resampler_empty(&self) -> bool {
self.frames_being_resampled == OutFrameCount::ZERO
}
Expand Down Expand Up @@ -247,7 +293,7 @@ impl<I: Source> RubatoAsyncResample<I> {
resampler,
input_buffer: Input::new(input_buf_size.samples(channels)),
output: Output::new(source_rate, channels, output_buf_size),
pos_in_current_span: InSamples::ZERO,
span_remaining: InSamples::ZERO,
output_delay_remaining: initial_output_delay,
resample_ratio,
frames_being_resampled: OutFrameCount::ZERO,
Expand Down Expand Up @@ -298,7 +344,7 @@ impl<I: Source> RubatoAsyncResample<I> {
resampler,
input_buffer: Input::new(input_buf_size.samples(channels)),
output: Output::new(source_rate, channels, output_buf_size),
pos_in_current_span: InSamples::ZERO,
span_remaining: InSamples::ZERO,
output_delay_remaining: initial_output_delay,
resample_ratio,
frames_being_resampled: OutFrameCount::ZERO,
Expand Down Expand Up @@ -348,7 +394,7 @@ impl<I: Source> RubatoFftResample<I> {
resampler,
input_buffer: Input::new(input_buf_size.samples(channels)),
output: Output::new(source_rate, channels, output_buf_size),
pos_in_current_span: InSamples::ZERO,
span_remaining: InSamples::ZERO,
output_delay_remaining,
resample_ratio,
frames_being_resampled: OutFrameCount::ZERO,
Expand Down
Loading
Loading