Repository navigation
Slowed playback: no stutter, no doubled kicks, no echo - #727
Merged
Merged
Conversation
added 2 commits
September 30, 2026 07:23
The lookahead gate compared _scheduledTo, in source seconds, against the output playhead. With SoundTouch stretching, sources play at 1x and the worklet buffers the surplus, so below 1x the output playhead lags what the sources have consumed. The gate overstated its margin by (1 - rate) seconds per second until the sources ran dry and the next chunk was scheduled late and skipped. At 0.75x that happened about fifty seconds in. The gate now measures against ctxTimeToSourceTime, which already follows the source rate, so it also holds in the tape-effect fallback. Reported and diagnosed by @goermezer in #701. Closes #722
Below 1x each WSOLA sequence plays 70 ms of input but advances only 70 * tempo, so consecutive sequences overlap and an attack in the overlap is heard twice. At 0.75x kicks came out as a ~50 ms flam. The shared tempo stage now marks attacks on the sample-to-sample difference, and when the next sequence would replay one it carries straight on from where the last sequence ended instead. That splices identical samples, so it is seamless. The extra input is borrowed, at most 100 ms, and paid back by later sequences stretching slightly more. Only the shared tempo stage does this. The pitch stages are aligned sample for sample with the unpitched drums, and borrowing there would pull the band off the kit. Measured on a full mix at 0.75x: 93 attacks in, 104 out before, 93 out after. Closes #728
WSOLA slows audio by repeating overlapping 82 ms fragments, heard as an echo on everything sustained. The shared tempo stage now uses the Signalsmith Stretch WASM core (MIT, vendored from signalsmith-stretch 1.3.2) at 40 ms blocks, chosen by ear against 30, 60 and 120 ms. - The core is its own worklet module, added before the processor. The processor instantiates it asynchronously and swaps it in at the next flush, never under audio already playing. If it does not load, WSOLA stays the tempo stage. - The processor reports which stage runs and its latency in two parts. Latency is the priming plus the core's input side, divided by the tempo, plus its output side; measured end to end it lands within 1 ms. Both engines take it from one shared module, tempoStage.js, instead of each hard-coding WSOLA's. - script-src gains 'wasm-unsafe-eval', in the server CSP and the Tauri CSP. It allows WebAssembly compilation only, not JS eval. The CSP test now pins script-src to exactly 'self' and 'wasm-unsafe-eval'. On 20 s of a full mix at 0.75x (55 attacks in): WSOLA 60 attacks out with 8 close pairs, Signalsmith 61 with 2. Closes #729
Merged
thcp
added a commit
that referenced
this pull request
Oct 2, 2026
Release 0.19.1: everything on `next-release` since v0.19.0. - #727: slowed playback without stutter, doubled kicks or echo (Signalsmith Stretch as the tempo stage). - #737: the bugs reported against 0.19.0 (CUDA setup, torch that cannot load, favourites in a narrow window, minor keys, the key label). - #740: the remaining open issues (top bar and Extract stems, the card at every width, favourites on the phone, the lyrics lookup on the server, an adjustable slow speed, Rust advisories). - #741: trivy ignores the two urllib3 CVEs until the lock next moves. - #743: a 0.5x button beside the slow speed. Verified on local Windows NVIDIA builds: 0.19.1.dev0 passed 12 of 12 (#727, #737), and 0.19.1.dev1 passed 18 of 18 automated checks including #740, in an isolated profile. `uv.lock` is unchanged since v0.19.0, so the in-app update works. Closes #722 Closes #728 Closes #729 Closes #723 Closes #724 Closes #726 Closes #730 Closes #731 Closes #732 Closes #733 Closes #736 Closes #701 Closes #738 Closes #725 Closes #735 Closes #734 Closes #719 Closes #742
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #722
Closes #728
Closes #729
Reported and diagnosed by @goermezer in #701. This takes the engine half of their patch, with two adjustments. The slider half stays open in #701 as a separate design question.
What changes
The chunk scheduler's lookahead gate now measures against
ctxTimeToSourceTime(ctx.currentTime)instead of the output playhead.ctxTimeToSourceTimefollows_srcRate(), so the gate still counts at the slowed rate there, the same as before.Differences from the patch in #701:
ctxTimeToSourceTimeinstead of adding a second clock that assumed 1x, which was wrong for the tape-effect fallback.Tests
tests/js/slowed-scheduling.test.mjsplays a two minute synthetic track against a fake clock and checks, every 100 ms, that the scheduled sources still reach past "now". It covers SoundTouch at 1x, SoundTouch at 0.75x and tape effect at 0.75x.The e2e fixture is 6 seconds long, so no Playwright test can reach this.
Doubled kicks at slowed speeds (#728)
Found while testing the fix above. Below 1x the stretcher's grains overlap, and an attack in the overlap is played twice, which turns a kick into a ~50 ms flam.
Measured at 0.75x on 30 s of a full mix: 93 attacks in, 104 out before, 93 after.
tests/js/tempo-attacks.test.mjsstretches 40 synthetic kicks over a held chord. The old stretcher gives 47 attacks, the new one exactly 40, and no kick drifts past the borrow limit.Signalsmith Stretch as the tempo stage (#729)
Below 1x, WSOLA repeats overlapping 82 ms fragments, which is heard as an echo on everything sustained, not only drums. The shared tempo stage now uses the Signalsmith Stretch WASM core (MIT), at 40 ms blocks. That size was chosen by ear against 30, 60 and the library's 120 ms default. The 120 ms default softened about a quarter of the drum attacks.
static/vendor/signalsmith-stretch.js: the loader fromsignalsmith-stretch@1.3.2, unchanged, with its MIT license. The package's own AudioWorkletNode is not used, because it ignores the rate on live input.static/js/tempoStage.js. Measured end to end, it matches within 1 ms at 44.1 and 48 kHz.script-srcgains'wasm-unsafe-eval', in both the server and Tauri CSPs. It allows WebAssembly compilation only; JSevalstays blocked.test_csp.pynow pinsscript-srcto exactly those two sources.On 20 s of a full mix at 0.75x (55 attacks in): WSOLA 60 attacks out with 8 close pairs, Signalsmith 61 with 2.
Tests:
tests/js/signalsmith-stage.test.mjs: the swap, the fallback, latency within 1 ms, and 40 kicks out as 40 with no drift.tests/e2e/tempo-stage.spec.mjs: in real Chromium, slowed playback reports the Signalsmith stage. With'wasm-unsafe-eval'removed it fails and reportswsola, which also shows the fallback working in a browser.Found along the way, not changed here: the WSOLA path's latency as the engines count it (122 ms) is well under what I measure end to end (about 300 ms at 0.75x). Only the fallback still uses that path.
Gate
node --checkon all ofstatic/js, everytests/jstest, i18n coverage clean,uv.lockuntouched.retention-settingandlyrics-aligneach failed once in earlier runs and passed on rerun with and without these changes.pitch-shift.test.mjsstill passes, so the pitch stages are unaffected.cargo fmt --check,clippy -D warningsand 116 tests pass (tauri.conf.jsonchanged).