Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt a higher quality resampler #647

Open
PetrGlad opened this issue Dec 1, 2024 · 13 comments
Open

Adopt a higher quality resampler #647

PetrGlad opened this issue Dec 1, 2024 · 13 comments

Comments

@PetrGlad
Copy link
Collaborator

PetrGlad commented Dec 1, 2024

Currently implemented sample rate converter uses simple linear interpolation algorithm and is prone to producing high frequency noise in output. See for example #584 and #316 related to the resampler implementation. However the algorithm is fast and requires little resources, so we should keep it at least as an option.

We should integrate an existing sample rate converter and let users to pick a resampler implementation depending on their needs. A simple optional low-pass filter may be also implemented as post-processing step for existing Rodio's SampleRateConverter to improve its audio quality.

It seems that there are 2 implementations that we can use:

  1. Rubato
  2. rust-samplerate which uses C bindings to libsamplerate
  3. Maybe dasp-interpolate

Note that all of these libraries process only floating point samples.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 2, 2024

A simple optional low-pass filter may be also implemented as post-processing step for existing Rodio's SampleRateConverter to improve its audio quality.

I like this idea 👍 might give a good middle ground between low quality fast and high quality but slow. Lets first see what the perf difference the different resamplers.

I've already looked at rubato and it looks great. I'm not sure about rust-samplerate. I do not like adding a C-dependency. Those complicate cross-compiling a lot.....

Instead we could try the resampler in dasp: https://docs.rs/dasp/latest/dasp/interpolate/sinc/struct.Sinc.html.

@Losses
Copy link

Losses commented Dec 3, 2024

This is, indeed a issue.

In our investigation into the playback quality of Rune, our player, which used Rodio as the playback backend, the issue happened. To rule out software-related influences, we controlled all external parameters:

  • We generated an audio file with a sample rate of 44100Hz, consisting of a 1000Hz mono sine wave.
  • Using OBS for recording, we set the recording parameters to 44100Hz to avoid resampling issues.
  • During the recording process, both the audio player's volume and the system volume were set to 100%.

This setup resulted in a clean, smooth spectrum, confirming that there are no inherent development errors within Rune affecting audio quality.

Clean Spectrum

However, we observed that Rodio's resampling process has flaws, particularly when downsampling from 48000Hz to 44100Hz, leading to suboptimal signal quality.

Resampling Issue

By subtracting Rodio's decoded output from the Ground Truth signal, the flaws in the resampling parameters become evident.The period of the mixed signal is four seconds. That is to say, the output frequency is 0.25hz lower/higher.

Resampling Flaws

We hope these observations could help.

Best regards.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 3, 2024

I just looked at the investigation in the rune player and they went through quite some trouble before finding the issue is our resampler. Given rodio is a playback lib I think it can be expected it does not introduce artifacts.... So I am going to mark this a bug.

@PetrGlad do you want to pick this up or shall I?

@PetrGlad
Copy link
Collaborator Author

PetrGlad commented Dec 3, 2024

@dvdsk You can pick it if you want.
I wonder if resampler can be an option without changing API. Currently it is always a part of Sink and UniformSourceIterato.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 4, 2024

UniformSourceIterator

and through it mixer, yeah its gonna be a challenge. I have an idea, we could do the same as I propose here: #646 (comment), so make the sink/stream instruct its parents/children on what re-sampler to use. Though that could impact performance, but maybe not that much as most re-samplers operate on groups of samples. An alternative would be to make every source that does resampling (UniformSourceIterator and through it mixer) generic over the resampling implementation. With static dispatch (so not Box) the optimizer can really do its thing.

Do you have any suggestions? Right now I am thinking of doing both and comparing the perf.

@PetrGlad
Copy link
Collaborator Author

PetrGlad commented Dec 4, 2024

@dvdsk You have answered this question before, but now I am thinking again about introducing "uniform source" trait whose implementations do not change their stream format (sample format, sample frequency, or channels number) mid-flight. This way complicated logic can be contained only where it is really needed. I imagine having convering 'UniformSourceIterator' next to external inputs, and using simpler processing steps everywhere else, except maybe an additional conversion may be needed just before the output device. With such simplified sources, when an input is added, it is only asserted once that stream parameters match what is expected and then the code in down-stream steps can just rely on that.
For example regarding this issue, number of places where sample rate conversion is necessary will be limited so the rate converter can be configured explicitly somehow. I need some time to think how the API would look like, though. Ideally I would like to let users bring in their own resamplers, but just having some limited choice is probably enough.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 4, 2024

Ideally I would like to let users bring in their own resamplers

Could you explain why? I was thinking about having the choice of a fast lowfi resampler (what we have now) and a hifi resampler like rubato. I do support using a trait to decouple the implementation from rodio's codebase. That is usefull if the resampler ever gets abandoned and we want to swap it out for another. But I would keep that trait private to rodio.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 4, 2024

and using simpler processing steps everywhere else

The disadvantage is that you then have two processes (one simpel, one complex) instead of just one. It might have performance benefits, but no one has complained about rodio's current performance (one or two issues in the distant past but that had to do with clear bugs if I remember correctly).

@Losses
Copy link

Losses commented Dec 5, 2024

Maybe this one could be a reference?

librespot-org/librespot#1180

@PetrGlad
Copy link
Collaborator Author

PetrGlad commented Dec 5, 2024

@dvdsk I'd think that giving a resampler choice if implemented properly may result in code that is less coupled and easier to change afterwards. That is not a requirement, of course.

@dvdsk
Copy link
Collaborator

dvdsk commented Dec 6, 2024

giving a resampler choice if implemented properly may result in code that is less coupled and easier to change afterwards

oh I'm all for decoupling. I just like to keep the API as small and simple as reasonable. Internally this can be a trait implemented by different resamplers.

@PetrGlad
Copy link
Collaborator Author

#584 (comment)

@dvdsk
Copy link
Collaborator

dvdsk commented Jan 4, 2025

relevent work in progress PR #670

@dvdsk dvdsk changed the title Adapt a higher quality resampler Adopt a higher quality resampler Jan 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants