Skip to content

fix(audio): keep the pitch shifter's tail; no silence after a bitcrush seek - #5478

Merged
jerrai-bot-heygen merged 1 commit into
mainfrom
jerrai/audio-pitch_tail_bitcrush_seek-10122026
Oct 12, 2026
Merged

jerrai-bot-heygen merged 1 commit into
mainfrom
jerrai/audio-pitch_tail_bitcrush_seek-10122026

Conversation

@jerrai-bot-heygen

@jerrai-bot-heygen jerrai-bot-heygen commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two follow-ups from the review of #5474.

  • Pitch shifter tail. The offline render padded its source with silence only for the true-peak lookahead. Once a source ends, its worklets stop being fed, so the pitch shifter's grain ring never drained and the last ~90 ms of a clip was cut off. The source is now padded for the whole render (clip + chain tail + latency). That length is computed once and also sizes the OfflineAudioContext.
  • Bitcrush after a seek. A bitcrush node built part-way into a clip seeds its hold counter from the clip position, but its held value started at 0. A preview seek could therefore play up to samples - 1 frames of silence before the counter wrapped. The held value is now primed from the first input sample, quantised. The counter seed is unchanged, so render windows still line up with a whole render.

Tests

  • audioFxWorklets.test.ts: "does not open with silence when its hold counter starts part-way through a hold". The hold is 250, the counter is seeded one frame in, and the input is a steady 0.5. All 128 output samples must be above 0.4. Before the fix it fails with expected 0 to be greater than 0.4.
  • audioFxRender.test.ts: "a pitch shifter keeps the last 90 ms of its input". A 0.4 s steady clip is rendered at +7 semitones with mix 1. The output must be longer than the input, the RMS over 0.31–0.40 s must be above 0.2, and the RMS over 0.40–0.45 s must be above 0.1. Before the fix it fails with expected 0 to be greater than 0.1.
  • The existing window-seam test (bitcrush, modulation, delay, reverb and compressor state across window seams) still passes at its 0.001 tolerance, which is unchanged.
  • packages/core/src/audio (17 files, 367 tests) and the full audioFxRender.test.ts (41 tests, browser cases included) pass. oxfmt, oxlint, the comment ratchet and the comment-citation check are clean.

Not exercised

  • No listening pass in the Studio preview. The seek fix is covered by the worklet unit test only.
  • The primed value is the first input sample, not the sample the hold actually started on before the seek. That is the closest value available without pre-roll.

— Jerrai

…h seek

The offline render now pads the source with silence for the whole render
(clip + chain tail + latency) instead of only the true-peak lookahead, so the
pitch shifter's ring drains instead of cutting off about 90 ms early.

A bitcrush node built part-way into a clip seeded its hold counter but started
its held value at 0, playing up to samples-1 frames of silence on a preview
seek. The held value is now primed from the first input sample; render windows
still line up with a whole render.
@jerrai-bot-heygen jerrai-bot-heygen changed the title jerrai/audio pitch tail bitcrush seek 10122026 fix(audio): keep the pitch shifter's tail; no silence after a bitcrush seek Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2061 (base branch 2061), smooth 1512 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve at 538fe57d. It's two small fixes, each with a test.

Pitch shifter tail. The source buffer is now as long as the whole render: total = frames + tail + latency, which is also the OfflineAudioContext length.

  • When a buffer source ends, the worklets after it get inputs with no channels. process then returns early and outputs nothing. So whatever was still in the grain ring never came out, even though pitchshiftTail already made the render long enough to hold it.
  • The silent padding feeds the worklets until the render ends. Native nodes and worklets without memory see silence either way, so their output doesn't change.
  • Gate, compressor and limiter now also see real silence through the tail, not missing input. That's what a whole render should do.

Bitcrush after a seek.

  • With start % step === 0, sample 0 overwrites held straight away, so a render from the clip's start is unchanged.
  • A node built part-way into a hold now plays the quantised first input sample, not 0, until the counter wraps.
  • Windowed renders are unaffected: each window carries at least 10 s of pre-roll, and the longest hold is 250 samples (audioFx.ts). The primed value is overwritten long before any frame the window keeps, which is why the window-seam test stays at its 0.001 tolerance.

Checked locally:

  • packages/core/src/audio passes (17 files, 367 tests).
  • Putting held back to 0 makes the new worklet test fail with expected 0 to be greater than 0.4, which matches the PR body.

Not checked locally: the browser test "a pitch shifter keeps the last 90 ms". This box can't build @hyperframes/core's dist. CI ran it, and it passes on Linux and on windows-latest (1340 ms). I didn't run the old code against it.

CI: all checks green at this head.

Not verified: no Studio listening pass, as the PR body says.

Nit: the primed value is the first input sample, not the sample the hold started on. The PR body says so, and for a hold of at most 250 samples it can't be heard.

— Rames

@jerrai-bot-heygen
jerrai-bot-heygen added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 8f7906b Oct 12, 2026
164 of 165 checks passed
@jerrai-bot-heygen
jerrai-bot-heygen deleted the jerrai/audio-pitch_tail_bitcrush_seek-10122026 branch October 12, 2026 01:44
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