Skip to content

fix: two ring-buffer bugs in DelayNode::delayBufferOperation#998

Open
OhByron wants to merge 2 commits intosoftware-mansion:mainfrom
OhByron:fix/delay-node-ring-buffer-wraparound
Open

fix: two ring-buffer bugs in DelayNode::delayBufferOperation#998
OhByron wants to merge 2 commits intosoftware-mansion:mainfrom
OhByron:fix/delay-node-ring-buffer-wraparound

Conversation

@OhByron
Copy link
Copy Markdown

@OhByron OhByron commented Mar 27, 2026

Summary

Two bugs in DelayNode::delayBufferOperation that together cause periodic clicking artifacts and inaudible delay echo on Android.

Bug 1 — Inverted wraparound index (framesToEnd)

framesToEnd was computed as the overflow count (frames past the buffer end) but used as the size of the pre-wrap chunk (frames remaining before the buffer end):

// Before (buggy): overflow count, not pre-wrap count
int framesToEnd = operationStartingIndex + framesToProcess - delayBuffer_->getSize();

// After: correct pre-wrap count
int framesToEnd = static_cast<int>(delayBuffer_->getSize()) - static_cast<int>(operationStartingIndex);

This caused audio to be written/read at wrong positions on every buffer boundary crossing.

Bug 2 — Pre-wrap chunk not zeroed after READ

The READ path only zeroed the second (post-wrap) chunk. The first chunk — frames from operationStartingIndex to the buffer end — was consumed but never cleared. On the next full buffer rotation (~1 s for a 1 s max-delay buffer) that stale data was re-read as new audio:

} else { // READ
  processingBuffer->sum(
      *delayBuffer_, operationStartingIndex, processingBufferStartIndex, framesToEnd);
  delayBuffer_->zero(operationStartingIndex, framesToEnd);  // added
}

Symptoms (Android, Nokia XR20, sampleRate 48000)

  • Periodic metronome-like clicking whose rate scales with delayTime.value — fixed by Bug 1
  • ~1 s periodic tick independent of delay time — fixed by Bug 2
  • Delay echo inaudible — resolved once both fixes are applied

Test plan

  • Create AudioContext, wire OscillatorNode → DelayNode → GainNode(wet) → destination with a separate dry path
  • Set delayTime to a value < maxDelayTime (e.g. 0.3 s, 0.95 s)
  • Start oscillator, play for several seconds — confirm no periodic clicking
  • Stop oscillator — confirm echo tail is audible for ~delayTime seconds after stop
  • Verify no regression on iOS

Tested on Android (Nokia XR20, Android 14) via a local dev build with both fixes applied.

🤖 Generated with Claude Code

OhByron and others added 2 commits March 27, 2026 08:42
framesToEnd was computed as the overflow count (frames past the buffer
end) instead of the pre-wrap count (frames remaining before the buffer
end). This caused audio data to be written/read at wrong positions on
every buffer boundary crossing, producing periodic clicking artifacts
and inaudible delay echo on Android.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The READ path only zeroed the post-wrap chunk (second half of a
split operation), leaving the pre-wrap chunk (frames read from
operationStartingIndex to buffer end) unconsumed in the delay buffer.
On the next full buffer rotation (~1 second for a 1s max-delay buffer)
that stale data was re-read as new audio, producing a periodic ~1s
metronome tick independent of delayTime.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@mdydek mdydek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nitpick and I think we're ready to go

Comment on lines 46 to +47
int framesToEnd =
static_cast<int>(operationStartingIndex + framesToProcess - delayBuffer_->getSize());
static_cast<int>(delayBuffer_->getSize()) - static_cast<int>(operationStartingIndex);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
int framesToEnd =
static_cast<int>(operationStartingIndex + framesToProcess - delayBuffer_->getSize());
static_cast<int>(delayBuffer_->getSize()) - static_cast<int>(operationStartingIndex);
auto framesToEnd =
static_cast<int>(delayBuffer_->getSize() - operationStartingIndex);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants