-
-
Notifications
You must be signed in to change notification settings - Fork 65
fix: use Hermite interpolation at loop boundaries to eliminate clicks #1055
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -220,18 +220,32 @@ void AudioBufferSourceNode::processWithInterpolation( | |||||
|
|
||||||
| while (framesLeft > 0) { | ||||||
| auto readIndex = static_cast<size_t>(vReadIndex_); | ||||||
| size_t nextReadIndex = readIndex + 1; | ||||||
| auto factor = static_cast<float>(vReadIndex_ - static_cast<double>(readIndex)); | ||||||
|
|
||||||
| if (nextReadIndex >= frameEnd) { | ||||||
| nextReadIndex = loop_ ? frameStart : readIndex; | ||||||
| } | ||||||
|
|
||||||
| for (size_t i = 0; i < processingBuffer->getNumberOfChannels(); i++) { | ||||||
| auto destination = processingBuffer->getChannel(i)->span(); | ||||||
| const auto source = buffer_->getChannel(i)->span(); | ||||||
|
|
||||||
| destination[writeIndex] = dsp::linearInterpolate(source, readIndex, nextReadIndex, factor); | ||||||
| if (loop_) { | ||||||
| // Use Hermite 4-point interpolation with proper loop wrapping. | ||||||
| // Linear interpolation creates audible clicks at loop boundaries | ||||||
| // because it only ensures C0 (value) continuity. Hermite ensures | ||||||
| // C1 (slope) continuity, eliminating the click. | ||||||
| auto wrap = [&](int64_t idx) -> size_t { | ||||||
| int64_t len = static_cast<int64_t>(frameEnd - frameStart); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| int64_t rel = static_cast<int64_t>(idx) - static_cast<int64_t>(frameStart); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cast is not necessary
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implicates forward only playback. Please handle reverse playback case too (when |
||||||
| return static_cast<size_t>(frameStart + ((rel % len) + len) % len); | ||||||
| }; | ||||||
| size_t idx0 = wrap(static_cast<int64_t>(readIndex) - 1); | ||||||
| size_t idx1 = wrap(static_cast<int64_t>(readIndex)); | ||||||
| size_t idx2 = wrap(static_cast<int64_t>(readIndex) + 1); | ||||||
| size_t idx3 = wrap(static_cast<int64_t>(readIndex) + 2); | ||||||
| destination[writeIndex] = dsp::hermiteInterpolate(source, idx0, idx1, idx2, idx3, factor); | ||||||
| } else { | ||||||
| size_t nextReadIndex = readIndex + 1; | ||||||
| if (nextReadIndex >= frameEnd) nextReadIndex = readIndex; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use curly braces for readability. |
||||||
| destination[writeIndex] = dsp::linearInterpolate(source, readIndex, nextReadIndex, factor); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| writeIndex += 1; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,25 @@ namespace audioapi::dsp { | |
| return std::lerp(source[firstIndex], source[secondIndex], factor); | ||
| } | ||
|
|
||
| // Hermite 4-point interpolation for smooth looping. | ||
| // Unlike linear interpolation, Hermite matches both value AND slope at | ||
| // the interpolation point, eliminating audible clicks at loop boundaries | ||
| // when playbackRate != 1.0. | ||
|
Comment on lines
+31
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| [[nodiscard]] inline float hermiteInterpolate( | ||
| std::span<const float> source, | ||
| size_t idx0, | ||
| size_t idx1, | ||
| size_t idx2, | ||
| size_t idx3, | ||
| float t) { | ||
| float y0 = source[idx0], y1 = source[idx1], y2 = source[idx2], y3 = source[idx3]; | ||
| float c0 = y1; | ||
| float c1 = 0.5f * (y2 - y0); | ||
| float c2 = y0 - 2.5f * y1 + 2.0f * y2 - 0.5f * y3; | ||
| float c3 = 0.5f * (y3 - y0) + 1.5f * (y1 - y2); | ||
| return ((c3 * t + c2) * t + c1) * t + c0; | ||
| } | ||
|
|
||
| [[nodiscard]] inline float linearToDecibels(float value) { | ||
| constexpr float kDecibelsLinearFactor = 20.0f; | ||
| return kDecibelsLinearFactor * log10f(value); | ||
|
|
||
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.
It is not recommended to capture everything by ref
[&]. Just use[frameStart, frameEnd].