Skip to content

Conversation

@rosostolato
Copy link

Problem

When reusing SoundTouch instances after seeking (calling clear()), audio artifacts (buzz, pops, crackling) would persist for 1-2 seconds. This made it impractical to reuse instances, forcing developers to recreate SoundTouch objects on every seek, which causes GC pressure in real-time audio applications.

Root Cause

The clear() method had three issues:

  1. FifoSampleBuffer.clear() only reset pointers, not the actual Float32Array data. Stale audio samples remained in memory and bled into subsequent processing.

  2. RateTransposer had no clear() method. The interpolation state (prevSampleL, prevSampleR, slopeCount) persisted, causing discontinuities when processing resumed.

  3. Stretch.clearMidBuffer() had two problems:

    • Only cleared midBuffer when midBufferDirty was true
    • Never cleared refMidBuffer (correlation reference buffer), causing the WSOLA algorithm to find incorrect overlap positions

Solution

FifoSampleBuffer.js

clear() {
  this._vector.fill(0);  // Zero actual data, not just pointers
  this._position = 0;
  this._frameCount = 0;
}

RateTransposer.js

clear() {
  super.clear();
  this.reset();  // Reset interpolation state
}

Stretch.js

clearMidBuffer() {
  this.midBufferDirty = false;
  this.midBuffer = null;  // Trigger proper reinitialization in process()
  if (this.refMidBuffer) {
    this.refMidBuffer.fill(0);  // Clear correlation buffer
  }
  this.skipFract = 0;  // Reset skip fraction
}

Why midBuffer = null instead of fill(0)?

Setting midBuffer = null triggers the initialization block in process():

if (this.midBuffer === null) {
  this.midBuffer = new Float32Array(this.overlapLength * 2);
  this._inputBuffer.receiveSamples(this.midBuffer, this.overlapLength);
}

This ensures midBuffer is populated with actual audio samples from the new position, rather than zeros. When midBuffer contains zeros, the cross-correlation calculations produce arbitrary results, leading to incorrect overlap positions and audio artifacts.

Testing

Tested in a multi-track audio player with pitch shifting:

  • Play from start with pitch shift enabled ✓
  • Seek to random positions ✓
  • Rapid seeking back and forth ✓
  • Persistent buzz/artifacts eliminated ✓
  • Brief warmup period (~1 second) still occurs due to WSOLA algorithm needing time to find optimal overlap positions - this is expected behavior

Breaking Changes

None. This is a bug fix that makes clear() work as expected.

rosostolato and others added 3 commits January 30, 2026 14:55
Previously, calling clear() on SoundTouch instances would not fully reset
internal state, causing buzz/crackling sounds when reusing instances after
seeking. This was because:

1. FifoSampleBuffer.clear() only reset pointers, not the actual data
2. RateTransposer had no clear() method at all
3. Stretch.clearMidBuffer() only nullified midBuffer when dirty flag was set,
   and never cleared refMidBuffer (correlation reference buffer)

Changes:
- FifoSampleBuffer: Zero the actual Float32Array data
- RateTransposer: Add clear() method that resets interpolation state
- Stretch: Always zero midBuffer and refMidBuffer, reset skipFract

This eliminates the need to recreate SoundTouch instances on every seek,
reducing GC pressure in real-time audio applications.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When midBuffer is zeroed but not null, the initialization block in process()
is skipped because the check `this.midBuffer === null` returns false.
This causes the algorithm to use zeroed samples for overlap calculations,
resulting in audio pops.

By setting midBuffer to null, we trigger proper reinitialization where
process() creates a fresh Float32Array and fills it with actual audio
samples from the input buffer. This ensures overlap calculations use
real audio data, eliminating the pops when seeking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@cutterbl
Copy link
Owner

@rosostolato Thanks for your PR. And thank you for digging in to work it out. Great explanation of actual vs expected behavior, and cause and solution. I will review everything over the weekend, and we'll see if we can't get this in play. (no pun intended ;) )

@rosostolato
Copy link
Author

This was all claude's help but worked to me

@rosostolato
Copy link
Author

There's another bug still going on that makes the audio pop when seeking and playing but I can't find what it is. Idk if it's in the lib or my implementation

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