-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Bufferscaler implementation with the Libsamplerate callback API #15005
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
base: main
Are you sure you want to change the base?
Conversation
…oose scratching and keylock libraries.
…ling. TODO: GUI scratching incomplete, formal audio quality testing
…layback, tempo ramping.
…ase_rate conversion, scratching. Comments welcome.
@@ -31,14 +31,11 @@ | |||
<SizePolicy>min,me</SizePolicy> | |||
<MinimumSize>100,</MinimumSize> | |||
<Children> | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you revert this change. You may use a "--fixup" commit.
case EngineBuffer::ScratchingEngine::SampleRateSincFinest: | ||
src_quality = SRC_SINC_BEST_QUALITY; | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEBUG_ASSERT will abbort Mixxx during debug and will do nothing in release builds:
} | |
case EngineBuffer::ScratchingEngine::NaiveLinear: | |
case default: | |
DEBUG_ASSERT(!"unsupported quality setting"); | |
} |
|
||
if (error || !m_pResampler) { | ||
qWarning() << "libsamplerate initialization error:" << src_strerror(error); | ||
m_pResampler = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_pResampler = nullptr; | |
if (m_pResampler) { | |
free(m_pResampler); | |
m_pResampler = nullptr; | |
} |
<< "Effective rate:" << m_effectiveRate; | ||
} | ||
|
||
// Callback API requires two callbacks: THis one is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Callback API requires two callbacks: THis one is | |
// Callback API requires two callbacks: This one is |
// data: SRC_CB_DATA | ||
// --- | ||
// Needs to be a static function, since it is called by libsamplerate core. | ||
static long raman_get_input_frames_cb(void* cb_data, float** audio) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move it to the anonymous namespace above. This is the same than the static keyword.
return pThis->getInputFrames(audio); // public | ||
} | ||
|
||
long EngineBufferScaleSR::getInputFrames(float** audio) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a output parameter and requires a pointer to an array of float samples. "ppAudio".
libsamplerate does not maintain its own buffer.
|
||
long EngineBufferScaleSR::getInputFrames(float** audio) { | ||
SINT required_input_frames = static_cast<SINT>( | ||
std::ceil(m_outFrames / m_srcRatio)); // can be larger than backbuffersize. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getInputFrames call is repeated until all required frames are available. This is likely more than this calculation because sinc needs some settling samples. So we probably can instead pass a whole chunk without much worrying.
static_cast<SINT>(m_bufferBack.size())); | ||
|
||
SINT samples_read = m_pReadAheadManager->getNextSamples( | ||
m_effectiveRate, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we inform the RAMAN that we may want backward?
// are going forward or backward. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RAMAN getnextsamples() method uses the sign of m_effectiverate to infer direction.
m_effectiverate is always signed here, so an explicit check is not necessary.
The rubberband line linked is simply calculating effective_rate
*audio = nullptr; | ||
return 0; | ||
} | ||
m_inputFramesRead = getOutputSignal().samples2frames(samples_read); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_inputFramesRead returned by scaleBuffer() shall only return the actual read samples in the engine, not the buffered samples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, my mistake.
The problem is that libsamplerate callback api does not explicitly return #input samples consumed to caller.
We need to track how many frames we have passed via the callback and how many
state->saved_frames we had at the beginning of scaleBuffer and after. That's straight forward and works probably reasonable good.
Will implement this ^ to get an accurate m_inputFramesRead count.
const int framesProduced = do_scale(pOutputBuffer, m_outFrames); | ||
assert(framesProduced == m_outFrames); | ||
|
||
return m_inputFramesRead; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This here starts to get complicated. We need to track how many frames we have passed via the callback and how many
state->saved_frames we had at the beginning of scaleBuffer and after. That's straight forward and works probably reasonable good.
But to make it work perfectly we need double precision. We need create the fractional part, because the user may stop at an output sample that is just in between two input sample. (If you consider playing with half speed)
The libsamplerate interface cant do this ... Unfortunately ... so we may consider to fork libsamperate for adding the double precision interface.
Maybe this rounding down to long nature is the cause for the remaining clicks? .. Unsure ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But to make it work perfectly we need double precision. We need create the fractional part, because the user may stop at an output sample that is just in between two input sample. (If you consider playing with half speed)
The libsamplerate interface cant do this ... Unfortunately ... so we may consider to fork libsamperate for adding the double precision interface.
Maybe this rounding down to long nature is the cause for the remaining clicks? .. Unsure ..
I will correct the m_inputFramesRead value first and observe. Just to confirm, are the clicks you hear reproduceable? Or random? On my machine, most times playback occurs without audible clicks.
Maybe this rounding down to long nature is the cause for the remaining clicks? .. Unsure ..
So we'd have to submit a PR to libsamplerate with our proposed double interface? Or is it more common to distribute as a submodule?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see tree options:
- run a second model that keeps track of the position
- what you do here right now.
- copy the relevant code from libsamplerate to our lib folder and make the required variables double
- We may later contribute it upstream
- rip off the sinc algorithm of libsamplerate and use it here. Make it our own.
- More easy to maintain, but we are cut from upstream fixes
- Access private interface of libsamplerate
- This works on Linux, unsure if it works on Windows.
The private interface is here:
https://github.com/libsndfile/libsamplerate/blob/15c392d47e71b9395a759544b3818a1235fe1a1d/src/common.h#L155
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow :-) "last_position" is the double precision value that we need.
Let's copy the "struct SRC_STATE_tag" from common.h to our code and file a small wrapper src_get_last_position().
But first we need to check src_get_version() and we may also do a sanity check in addition and verify for example the address in "user_callback_data".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, will get this done asap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay how about this:
- I implement @daschuer "hack" immediately to test audio quality during resample. I still need to write scratching unit tests.
- Submit a PR to libsamplerate with a get_last_frame() and double-precision interface.
- Do not merge this pr into mixxx upstream until above libsamplerate changes are merged to libsamplerate upstream.
- While waiting for libsamplerate to be merged, I can implement bufferscalers using different resample libraries that may be merged to mixxx upstream.
@Swiftb0y @daschuer Does this seem like a good plan? Also if possible, could you please test this latest commit for audio quality issues? The automated enginebufferE2E tests fail, but values are very close to the "golden samples".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, so your solution is already dead on arrival...
You probably misread the comment. The braking comit has been introduced with the mayor version jump from 1.9 to 2.0. this was 5 years ago. Since then it is stable up to HEAD.
There is no other ready to use solution in sight, that is provided a as a general purpose library. Other solution likely require to distribute it outsrlf anyway.
Now we need here a POC that sinc works for scratching first. So we are in a good route.
But what do you think about the static link idea to pin a known working version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we need to make it "defined". This can be done by statically link to a specific version.
No, locking us into a specific version will be a maintenance nightmare for us and our packager maintainers. This is not a reasonable solution.
Again, do what you gotta do for a PoC, but don't introduce that stuff into main
. The only proper solution I see is to make that API stable upstream and discuss with the upstream devs/maintainers if that is reasonable for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Swiftb0y Please use "Request Changes" is such cases, otherwise such code can slip into the tree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow :-) "last_position" is the double precision value that we need.
Let's copy the "struct SRC_STATE_tag" from common.h to our code and file a small wrapper src_get_last_position().
But first we need to check src_get_version() and we may also do a sanity check in addition and verify for example the address in "user_callback_data".
The actual attribute is state->priv->in_used
. state->last_position
always returns a double < 1.
Example: src_linear.c:52::linear_vari_process(): data->input_frames_used = priv->in_used / state->channels ;
(similar for sinc, with some modifications.)
- Just to confirm, we should return
state->priv->in_used
+state->last_position
?
…Comments on audio quality would be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #15005 (comment)
It is getting better. Playback at different ratios works nice. Also rate transitions are smooth at any algorithms. I have added this hack:
And get this output.
The approach with the upstream patch is a good idea. Hopefully we get it merged soon, to not block our project on that. |
the linear resampler keeps a "last_value"
So we have in the pass through case a
This means we are up to 1 frame delayed. Unfortunately with the callback interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_inputFramesRead = 0.0 in scaleBuffer()
- The comment is misleading, it is from a older version where i set m_inputFframesRead = m_pResampler->saved_state.
Well done. The Linear rescaler works now with libsamplerate. Sinc has still issues. It is good notable if you want to hold the waveforms with the mouse. Than the sound is cycling around the hold position. I think we hear the settling samples, the sinc algorithms requires. I have confirmed by recording the input buffer of a 4410 Hz rectangular signal, that the output of libsamplerate is one frame delayed. I have recorded the original signal left and the scaled signal right and then watched the samples in audacity. We may correct that by doing a Dummy call consuming this one sample. Fastest sinc is exactly 512 frames behind. Instead of silently duplicating one sample, it requests this 512 buffer explicit during the first call. This way the it the samples are played at the right moment, but our transport calculation is tricked. I think we need report 512 consumed frames less during the first call after reset. I did a brief measurement of resampling a 48 kHz Track to 44.1 and a 5,8 ms buffer and got this is result: 448 µs Best This means we may either completely fix the linear version of libsamplerate and retire our internal linear resampler or apply the same trick libsamplerate used to speed it up the internal. |
Thank you!
|
Samplerate "linear" and "fastest sinc" now both work for scratching without artefacts. "Best quality" has underflows during scratch, but may be usable for moderate sample-rate conversions such as recording/broadcast wrt issue #10611