Repository navigation
fix(audio): keep the pitch shifter's tail; no silence after a bitcrush seek - #5478
Conversation
…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.
Edit accuracy: accurate 2061 (base branch 2061), smooth 1512 of thoseThe gate passes. Quarantined, measured but not gated (0) |
jrusso1020
left a comment
There was a problem hiding this comment.
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.
processthen returns early and outputs nothing. So whatever was still in the grain ring never came out, even thoughpitchshiftTailalready 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 overwritesheldstraight 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/audiopasses (17 files, 367 tests).- Putting
heldback to 0 makes the new worklet test fail withexpected 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
Summary
Two follow-ups from the review of #5474.
OfflineAudioContext.samples - 1frames 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 withexpected 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 withexpected 0 to be greater than 0.1.packages/core/src/audio(17 files, 367 tests) and the fullaudioFxRender.test.ts(41 tests, browser cases included) pass. oxfmt, oxlint, the comment ratchet and the comment-citation check are clean.Not exercised
— Jerrai