feat: high-quality resampler#843
Conversation
Oops, I've had this for a while in rodio experiments https://github.com/RustAudio/rodio-experiments/blob/main/src/conversions/resampler/variable_input.rs but hey that's nice we can compare :) and its probably not as extensive (for example I said hell no to seeking :)) |
|
Yes the hard work started when I thought I was done and started merging the tests from the existing Beyond padding blocks to satisfy Rubato’s chunks there is also output delay skipping, cutting off of mirror images after the last sample (too many samples) and flushing the last signal energy from Rubato’s filter (not enough samples). This complexity required some degree of complexity to address it - if you see opportunities to simplify then do let me know. The test suite quickly reveals what you cannot take out… |
|
Split into different files now. |
yara-blue
left a comment
There was a problem hiding this comment.
Thank you I see why this was sooo much work there are a lot of subtleties!
I've found a few rough edges that are worth sanding down. It's mostly code quality enhancements.
Due to it's size it took me a bit to find the time to review it. By now the PR needs a rebase, I propose we first address all the comments and only then rebase. Otherwise we'd lose where we are.
Proper resampling is the only way to get out of the span mess making this essential to the future of Rodio. Therefore let me thank you again for figuring out how this works and getting a solid implementation done.
Replace manual linear-interpolation converter with a Resample-backed implementation and a SourceAdapter for iterator inputs.
684ede4 to
a814bd6
Compare
|
Addressed review points in 97f5701 and rebased - sorry, I already force-pushed without thinking about it. This required backporting a fix from #786. I'll investigate a bit more why the Hydrogen SRC test shows that we're missing samples. This PR took great care to be sample-exact, so I don't yet understand. That should not hold up merging. |
|
What about the open questions from the PR body? |
|
Upon further investigation, it turns out that the sample delay depends on Rubato's sinc settings. With something as extreme as this, the sample delay is 0 as it should be: ResampleConfig::Sinc {
sinc_len: 2048,
oversampling_factor: 1024,
interpolation: Sinc::Cubic,
window: WindowFunction::BlackmanHarris2,
f_cutoff: 0.9945994235, // 0.5^(16/sinc_len)
chunk_size: 2048,
sub_chunks: 1,
}Note that that Finally, I can confirm that the spectrogram is clean when compiled with the In all, I think this is good to go. |
| } | ||
|
|
||
| fn fill_input_buffer(&mut self, needed: usize, num_channels: usize) { | ||
| while self.input_frame_count < needed { |
There was a problem hiding this comment.
I think this can read past span edges. There's no checking in this while loop of span boundaries, so we can read past into a new inner span with different channel counts and sample-rates.
|
|
||
| self.resampler | ||
| .process_into_buffer(&input_adapter, &mut output_adapter, indexing_ref) | ||
| .ok()? |
There was a problem hiding this comment.
If tracing feature is enabled, should this logged as an error before returing None to end the stream?
| num_channels, | ||
| num_frames, | ||
| ) | ||
| .ok()?; |
There was a problem hiding this comment.
Same thing here, perhaps log this error if tracing is enabled?
| resampler.input.sample_rate(), | ||
| resampler.input.is_exhausted(), | ||
| resampler.output_has_samples(), | ||
| resampler.output_len(), |
There was a problem hiding this comment.
I think perhaps this should be resampler.output_remaining(), not resampler.output_len().
While they might often be the same, when we're zooming through initial delay samples we use Buffer::skip() which advances the read marker but doesn't change the buffer length.
Since we're never emitting those initial dummy samples, I think using resampler.output_len() will overreport in this situation.
There was a problem hiding this comment.
Thinking more about this. This could be a problem even outside of initial sample delay skipping.
We use this outout_len value here in current_span_len():
// When the ratio contains a fraction, we cannot choose the floor or ceiling
// arbitrarily, because the resampler may produce either based on its internal state
if output_has_samples {
// Running state: we are iterating over our buffer with resampled samples
Some(output_len)
} If someone queries our current_span_len() mid-chunk, I think we want the amount remaining in the current buffer, not the total buffer size.
|
I have two more general comments on this: Amortization Resampling can be very bursty. A single outer next() does both of these things all at once in a single
It might make sense to try to amortize this over more calls to It's quite possible that I'm way over thinking this, and buffer sizes in CPAL make careful amortization of inner Resampler Pool I notice that we're allocating a new resampler on span changes. It might make sense to introduce the idea of a "resampler pool" where we can park used resamplers, reset them with Check out https://github.com/phayes/ardftsrc-rs/blob/master/ardftsrc/src/realtime.rs#L615, where I've implemented this for ardfcsrc. It's not quite the same idea since my architecture around handling span boundaries and buffers is pretty different, but it could be worth thinking about. Both of these ideas could be "future extensions" and are definitely niche optimizations, but I thought it worth sharing my thoughts. |
Q: How hard could it be to add a high-quality
Resamplesource using Rubato?A: Almost four weeks of evening work, that's how hard!
It's been surprisingly hard to get the details down. Way more than "just" creating a Rubato instance and processing some buffer. Very grateful for the test cases that were already there - without them, I'd never have gotten this down.
Features
Polynomial and sinc interpolation: including support for FFT-based resampling for fixed ratios.
Fixed-ratio optimization: auto-detects and switches sinc resamplers to FFT-based or nearest-neighbor processing when the ratio allows. This lowers CPU usage while providing the highest quality.
Span boundary handing: specialized implementation of fix: correct Source trait semantics and span tracking bugs #831 to recreate the resampler on the fly when parameters change at span boundaries, even after seeking.
Performance
Through refactoring of
UniformSourceIteratorand friends, the performance hit oncargo benchis now 7-8% - which is about the same as the span boundary detection from #831. Considering this PR also adds span boundary detection for the resampler, that means the improved resampling is virtually free.Initially I had made
SampleRateConvertera simple wrapper around the newResamplesource. This caused a 20-25% performance hit, and led to my refactoring described next.Changes
The
UniformSourceIteratornow is optimized for passthrough, channel count conversion, sample rate conversion, or both. It also inlines the formerTakestruct (private; not to be confused withTakeDuration) and works withResampledirectly.For
UniformSourceIteratorto work withResampledirectly, it needs to convert anIteratorinto aSource. That'sfrom_iterright? Wrong in v0.21: thereFromIteris actually something that chains sources together. That seemed counter-intuitive to me. So to bring it in line with Rust idioms:FromIterwas renamed toChain(chains sources from an iterator)FromFactorywas renamed toFromFn(chains sources from a function)FromIternow wraps anIterator<Item = Sample>into aSourceSourcesQueueOutputwas copied from fix: correct Source trait semantics and span tracking bugs #831 because it contains peeking fixes necessary to makeUniformSourceIteratordetect the sample rate correctly when transitioning out of silence.SampleRateConverteris tagged as deprecated.ChannelCountConverternow implementsSourcewhile keeping a minimum surface for justI: Iterator.Minor opportunistic refactoring.
Questions
Resamplefromsrc/source/resample.rstoSampleRateConverterinsrc/conversions/sample_rate.rsand basically replace it?I: Sourceinstead ofI: Iterator.Resample::newcould live besidesnew_poly,new_sincand the builder.Related
Supersedes #670