Skip to content

PitchShift: Fix streaming silence when reset=False#486

Open
codyjhsieh wants to merge 1 commit into
spotify:masterfrom
codyjhsieh:fix/pitch-shift-streaming-silence
Open

PitchShift: Fix streaming silence when reset=False#486
codyjhsieh wants to merge 1 commit into
spotify:masterfrom
codyjhsieh:fix/pitch-shift-streaming-silence

Conversation

@codyjhsieh
Copy link
Copy Markdown

Fixes #415.

Three interacting bugs cause PitchShift to produce all-zero output when processing audio in streaming mode (reset=False). A single-shot call with reset=True works correctly, but any streaming use — voice chat, DJ apps, chunked file processing — gets permanent silence.

Problem

  1. PrimeWithSilence::prepare() clears the delay line buffer on every call. It calls JUCE's setMaximumDelayInSamples() unconditionally, which internally calls reset()bufferData.clear(). Since processFloat32 calls prepare() before every process(), the 44100-sample delay line loses all stored audio between streaming calls. The pitch shifter only ever receives silence as input.

  2. RubberbandPlugin::getLatencyHint() ratchets upward via std::max. The initialSamplesRequired member variable can only grow, inflating the latency estimate on each call. The pipeline uses this to decide how many output samples to trim.

  3. processFloat32 trims output by latency even in streaming mode. When reset=False, the pipeline passes isProbablyLastProcessCall=false, so it never extends the buffer to accommodate latency. The latency trimming then reduces the output to a zero-length array.

Solution

  • PrimeWithSilence.h: Guard setMaximumDelayInSamples() with a check — only call it when the value actually changes. This preserves the delay buffer across streaming calls.
  • RubberbandPlugin.h: Replace the ratcheting getLatencyHint() with a constant: rbPtr->getLatency() + lastSpec.maximumBlockSize. Remove the initialSamplesRequired member.
  • process.h: In streaming mode (reset=False), set totalOutputLatencySamples = 0 so the full buffer is returned, including any leading silence from plugin priming.

Result

  • Streaming produces audio after the expected ~46500-sample priming period at all chunk sizes (512, 2048, 4096, 8192, 44100, 88200).
  • Single-shot reset=True latency compensation is unchanged (PitchShift(0) identity error < 1e-6).
  • All existing tests pass (pitch shift, priming, latency compensation, time stretch).
  • Added streaming tests covering multiple chunk sizes, sample rates, and reset-then-stream sessions.

🤖 Generated with Claude Code

Problem

PitchShift produces all-zero output when processing audio in streaming
mode (reset=False). Three interacting bugs cause this:

1. PrimeWithSilence::prepare() calls setMaximumDelayInSamples() on every
   call, which unconditionally resets the JUCE delay buffer. Stored audio
   is wiped between streaming calls, so the pitch shifter only receives
   silence as input.

2. RubberbandPlugin::getLatencyHint() ratchets upward via std::max on a
   member variable that never decreases, inflating the latency estimate
   on every call.

3. processFloat32() trims output by the latency estimate even in
   streaming mode, reducing the output to a zero-length array when the
   plugin's startup latency exceeds the chunk size.

Solution

- Guard setMaximumDelayInSamples() with a value-changed check so the
  delay buffer is preserved across streaming calls.
- Replace the ratcheting getLatencyHint() with a constant structural
  latency: getLatency() + maximumBlockSize.
- In streaming mode (reset=False), return the full buffer instead of
  trimming by latency.

Result

Streaming produces audio after the expected priming period at all chunk
sizes. Single-shot reset=True latency compensation is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Pitchshift doesn't work when reset is False

1 participant