Skip to content

DelayNode: ring buffer wrap-around produces periodic audio artifacts on Android #997

@OhByron

Description

@OhByron

Bug

DelayNode.cpp has an inverted calculation in delayBufferOperation when handling ring-buffer wraparound. framesToEnd is computed as the overflow count (frames past the buffer end) rather than the pre-wrap count (frames remaining before the buffer end). This causes audio data to be written/read at incorrect positions in the ring buffer on every buffer boundary crossing.

File: packages/react-native-audio-api/common/cpp/audioapi/core/effects/DelayNode.cpp — line 49

// Current (buggy)
if (operationStartingIndex + framesToProcess > delayBuffer_->getSize()) {
    int framesToEnd = operationStartingIndex + framesToProcess - delayBuffer_->getSize();

framesToEnd is the overflow count. The code then processes framesToEnd frames at operationStartingIndex (near the buffer end), skipping the frames between operationStartingIndex + framesToEnd and bufferSize - 1.

Proposed fix:

    int framesToEnd = static_cast<int>(delayBuffer_->getSize()) - static_cast<int>(operationStartingIndex);

This gives the correct pre-wrap count: frames remaining before the buffer end. The subsequent second chunk then handles the overflow frames correctly at position 0.

Symptoms on Android (Nokia XR20, Android 14, sampleRate 48000)

  1. Periodic metronome-like clicking whose rate scales with delayTime.value higher values produce faster ticking. This is consistent with buffer boundary artifacts repeating at intervals tied to the delay time.
  2. The delay echo effect is barely audible or not audible despite correct parameter values.
  3. When DelayNode is wired into the audio graph (even with wet gain = 0), the dry audio path is disrupted, and audio only plays when the delay is actively producing output.

Applying the one-line fix above did not fully eliminate the artifacts on our device, suggesting there may be at least one additional bug elsewhere in the implementation (possibly in AudioBus::sum or the overall processNode scheduling), but the ring buffer calculation is clearly wrong and worth fixing independently.

Reproduction

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const delay = ctx.createDelay(1.0);
delay.delayTime.value = 0.3;
const wet = ctx.createGain();
wet.gain.value = 0.5;

osc.connect(ctx.destination);      // dry
osc.connect(delay);
delay.connect(wet);
wet.connect(ctx.destination);      // wet

osc.start();
ctx.resume();
// Expected: clean sine + echo at 300ms
// Actual: periodic clicking, echo inaudible or corrupted

Versions affected

  • 0.11.6 ✗
  • 0.11.7 ✗ (same code)

Platform: Android only (not tested on iOS)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions