Files
feuerwerk/tools
adminandClaude Opus 5.5 6e70421b92
game-maker / gate (push) Successful in 3m13s
game-maker / suite (push) Successful in 15m36s
Keep the length of a sound whatever its pitch does #317
A Pitch used to keep the length only for the pitch a voice started at.
Set sound FX parameters changed it by speed, so a sweep made the sound
longer or shorter, and length into time no longer spanned the sound.
Now a way with a Pitch is played by an AudioWorklet, a phase vocoder
whose pitch is an AudioParam in semitones. The length never changes,
and a pitch glides, ramps and eases like a Gain.

- PitchPlayer (core/soundfx) is the vocoder as a plain class, so it
  runs in a unit test. Frames of about 43 ms, four overlapping, a real
  FFT done as a complex one of half the size.
- Peaks are moved with the bins around them (Laroche and Dolson's
  phase locking), and frames are turned by half their size so a phase
  is the one at the middle. Without both, a tone beat against itself
  and dipped to 0.54 of its loudness while the pitch moved.
- It reads the sound itself rather than being fed it. It looks ahead
  as far as a frame reaches, so a voice starts with its first sample
  and every way of a Mix stays in step. A change of pitch is heard
  about half a frame (20 ms) late.
- At exactly 0 a frame is the sound itself and costs next to nothing.
  Coming back to 0, the phases are pulled round to the sound's for
  about half a second before it switches, so the switch does not dip.
- pitchWorklet (viewport) is the processor, bundled by Vite through
  ?worker&url. It is loaded with the first play that has a Pitch, which
  waits for it like for its bytes. A file's samples are sent with its
  first voice only and kept in the worklet's scope.
- A way has the worklet when its plan has a Pitch, a typed-in 0
  excepted (FxChain.shifted). A way without one stays a plain buffer
  source. Without AudioWorklet a Pitch is not heard, and the console
  says so.
- The pitched copies (WSOLA, pitchShift.ts) and their cache are gone.
- Decisions of the user: the player at the source rather than an
  effect in the chain, a phase vocoder rather than WSOLA, no limit on
  voices, no range tighter than the node's -64 to +64.
- stats() says whether the worklet is there, levels() gives a pitch in
  semitones, and __gm.sound.pitchWorkletUrl() lets a check run it.

Tests:
- pitch-player.test.ts (new, replaces pitch-shift.test.ts): 11 tests.
  Pitches from -30.5 to +12 within 2 %, the length to the sample, 0
  exact to 1e-5, a sweep half a frame behind, 8 kHz sounds, loops,
  ramps there and back without a dip.
- sound-library.test.ts: 23 tests (+4), soundfx.test.ts: 31 (+1).
- Counter-checks: no worklet in the player, shifted taken from the
  value, no pull back to 0. 15 tests fail, files restored by cmp.
- e2e 34 on melektaus-atx: ALL PASSED, 34 checks (+1). In real Chrome
  the worklet is there and the pitch param glides to 3.0007.
  Offline: 880.0 Hz for 440 an octave up, 48000 of 48000 samples, a
  sweep 0 to -12 at 392 then 248 Hz, still 48000 samples.
- Gate on melektaus-atx: 242 test files, 3690 tests, CHECK EXIT 0.
- Cost: 53 ms per second of a stereo voice at +5 in Node on stein2,
  5 ms at 0. In headless Chrome 16 voices for 2 s took 1.3 s, about 24
  fill the audio thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:00:13 +02:00
..
Plugins belong to the project, and they load before it: the project file gained the field plugins: ["demo-plugin.js"] (default empty, no formatVersion bump), the Plugins tab's switch writes into it as an ordinary undo step (SetPluginActiveCommand) instead of into a server-wide plugins.json, and the start order is turned around - startPlugins only fetches the list of files, then editor.boot() runs and ProjectStore.load(id, prepare) hands the raw JSON through before it is deserialized, where pluginsOf(raw) reads the list and PluginManager.syncTo() activates exactly those; that is the point of the issue, because what a plugin registers has to stand there before normalizeGraph would throw it away as unknown (#126, #127), a project switch first stops what the new project does not name and then starts what it does, a file the server does not have is passed over silently, and after signing out the manager is dead so a late event cannot start anything in a torn-down session; on the server plugins.json, PluginStore::setActive, isActive, the route PUT ?r=plugins/{file} and the 403 for inactive modules are gone and GET ?r=plugins only answers {file,bytes,updatedAt}, which makes every plugin file fetchable - the consequence accepted in #117, written down in the wiki for #115 and #116; publishPlugins(root) only copies files now, the seeder switches the demo plugin on in the fixture project, and the Plugins tab carries a puzzle piece as its icon while only a plugin's own tab still goes without one (the knob and the socket are cubic curves, not arcs, which Pixi's parser draws as a spike after an L); 3 new e2e checks in module 38 and 5 new unit tests cover the project as the owner of the switch, its undo, the order, pluginsOf against crude input and an unknown file name (#119)
2026-09-20 14:24:27 +02:00