Skip to content

[Bug]: The pitch worklet runs twelve idle chains at zero transpose, heard as pops on laptops (fix on a branch) #576

Description

@pywkt

Before reporting

  • I'm on the latest release and the issue still happens.
  • I searched existing issues and didn't find a duplicate.

What happened?

On a laptop client, both Web Audio engines pop and crackle continuously and the fan runs for the whole track, even with nothing transposed. The legacy per-stem <audio> path is clean. I expected the default engine to play a 7-stem track without audible glitches.

I tracked it down, and I have a fix on a branch. This repo currently limits pull requests to collaborators, so I cannot open one: https://github.com/pywkt/stemdeck/tree/fix/idle-pitch-chains (single commit, 32bd0ed, off main at 37a2c5f). Happy to open it as a PR if you allow it, or please pull it directly.

Cause. Both engines wire all thirteen semitone buses into the SoundTouch worklet at construction:

for (let k = 0; k < INPUT_COUNT; k++) buses[k].connect(stNode, 0, k);

The processor decides whether a semitone is in use by whether its input has channels, and takes its bypass path only when none do:

const connected = !!(inputs[k] && inputs[k].length);

The comment there says an unconnected input arrives as an empty array, and tests/js/pitch-shift.test.mjs hands unlisted inputs [] by construction. Chrome does not do that for a bus that is wired but has nothing playing into it: it hands the processor one channel of silence. So all twelve pitch inputs read as live, twelve PitchChains are built on the first render quantum, and each runs WSOLA, the anti-alias filter and the resampler on silence for the whole track. The bypass never engages, and a chain is only dropped after 256 blocks of an empty input, which never arrives. A side effect: the playhead runs ahead of the sound by the worklet's priming latency, because the engines assume none when no lane is transposed.

Measured in headless Chromium 151, offline render of 60 s, seven stems through the real soundtouch-processor.js, no transpose, tempo 1:

Graph Share of real time
No worklet in the graph 0.5%
Only the unpitched bus wired 0.6%
All 13 buses wired (current main) 13.0%

Channel counts the processor is handed on main, with audio on bus 6 only: [1,1,1,1,1,1,2,1,1,1,1,1,1].

Fix. Wire a pitch bus into the worklet only while a lane is routed to it (wired before the lane connects, unwired once the last lane leaves). The unpitched bus stays wired for the click. This is the handover the processor's own design describes; in Chrome it never happened because every chain was already warm. The routing test gains 12 checks that walk the graph for which inputs are wired as lanes arrive, share a key and leave.

Tested: all tests/js suites and the full Playwright suite (127) pass locally. Loading the real engine modules in headless Chromium with AudioNode.prototype.connect wrapped: main wires all 13 inputs from load; the branch wires [0] at load, [0, 2] after a +2 lane, [-5, 0] after moving it to -5, [0] when it returns, identically on both engines. On the laptop below, playback is clean afterwards and stepping individual lanes through keys during playback hands over without a gap or click.

Steps to reproduce

  1. Run StemDeck 0.17.0 from source on one machine (uvicorn, HTTPS so transpose is available) and open it in Chromium on a laptop on the same network.
  2. Import any track with all six stems and open it in the studio. Leave transpose at 0.
  3. Press play on the default (chunked) engine, or on the full-decode engine.
  4. Continuous pops and crackle; fan spins up. Switch to the legacy <audio> path (localStorage.setItem("stemdeck.audioEngine", "0") and reload): clean.
  5. To see the cause without a laptop: in any Chrome, wrap AudioNode.prototype.connect before the studio loads and log connections into the soundtouch-processor node. All 13 inputs are wired at load. Or render offline through the processor with all 13 inputs wired vs one, and compare render time.

Operating system

Linux

StemDeck version

v0.17.0 (37a2c5f)

How did you install it?

From source

Logs / screenshots

Client: ThinkPad T490, Intel i7-8565U, Arch Linux, Chromium, Plasma 6.
Server: Ubuntu 22.04, RTX 3060, uv run uvicorn app.main:app over HTTPS with a self-signed certificate. Separation queue idle during every test.

Fix branch: https://github.com/pywkt/stemdeck/tree/fix/idle-pitch-chains
Commit: pywkt@32bd0ed

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions