Commit Graph
25 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 f46b2316f2 Make 8-bit sounds without a file with a Chip node in instruments (#329)
game-maker / suite (push) Failing after 36s
game-maker / gate (push) Successful in 3m33s
An instrument could only play sound files so far. The Chip makes the
sound of a note itself, the way old game consoles and sfxr did: a raw
wave and what its frequency does over the note. A laser, a jump or a
pickup is now an instrument and ends by itself.

- Chip node with wave (square, triangle, saw, sine, noise and the
  periodic noise of the NES), follow note, frequency, duty, duty
  sweep, slide, delta slide, min frequency, vibrato, arpeggio and
  repeat. It counts in Hz, semitones and seconds, not sfxr's 0 to 1.
- It runs as a third processor in the FX worklet. The math is a plain
  class, ChipPlayer, tested in Node. Every sample is worked out 8
  times and averaged, as sfxr does.
- Frequency and duty are AudioParams, so an Envelope can move them
  and a parameter can tune them while the note plays. A Pitch behind
  a Chip moves its frequency directly, without the phase vocoder.
- Below its min frequency a Chip ends and tells the page. Without the
  worklet it is silent and its note ends at once.
- An Envelope with sustain 0 now ends its note once the decay is over,
  for every source under it, Samples too. Before, a note with
  duration 0 ran on silently until a Stop sound.
- A note made of Chips alone needs no file. playNote and the wait for
  the bytes no longer drop it for having none.
- Inputs that do nothing as things stand are grey in the inspector.
  idleParams is told which inputs are connected, so a connected one
  always counts.

The credit names sfxr (Tomas Pettersson, MIT) and jsfxr, which turned
out to be public domain, not MIT as the plan in #329 said.

Tests:
- Unit chip (new) 12, sound-library +4. All of tests/core,
  tests/viewport, tests/editor/graph and tests/editor/inspector:
  178 files, 2953 green.
- Counter-checks: semitones read as /24 turn 4 chip tests red. Without
  the sustain rule and the file check 4 library tests fail.
- e2e 34 (melektaus-atx): ALL PASSED, 60 checks (2 new). Offline in
  Chrome a square at 220 Hz plus 12 semitones and duty 0.25 gave 329
  periods in 0.75 s, up 0.249. 64 noise voices for 2 s rendered in
  2.0 s, about 63 at once. Live a 0.2 s note with sustain 0 was gone
  after 0.26 s.
- Gate (melektaus-atx): CHECK EXIT 0, 255 files, 3962 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:54:44 +02:00
adminandClaude Opus 5.5 165f022fc0 Crush a sound to fewer bits and a lower rate with a Bitcrusher (#322)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Successful in 18m49s
A new effect for sound FX and instruments makes a sound rough like an
old console. It rounds the sound to fewer steps and holds it at a lower
sample rate, and all three of its numbers move while the sound plays.

- Bitcrusher node with bits (1 to 16, not only whole), rate (100 Hz to
  48 kHz, sample-and-hold) and mix. A mix typed in at 0 leaves it out.
- It runs on the audio thread as a second processor next to the pitch
  player. The module is now fxWorklet.ts and the test hook is called
  fxWorkletUrl.
- A play that needs the worklet waits for it, for a Pitch or a
  Bitcrusher alike. Without the worklet the sound goes through as is.
- Bits and mix move in what they hold, the rate in octaves. An
  Envelope can move all three in an instrument.
- A stage that runs on the worklet is stopped with its voice, like the
  drivers of an Envelope or a Mix.
- The math is a plain class, Crusher, so it is tested in Node.

The cleanup check of e2e 64 from af9b202 turned suite 553 red. It
asked that no editor run was left at all. In CI a step before 64 still
leaves one of its own running. The check now asks only about its own
behavior and prints what is still running, so the next suite names it.

Tests:
- Unit crusher (new) 4, soundfx +2, sound-library +3. All of tests/core,
  tests/viewport and tests/editor/graph: 176 files, 2934 green.
- Counter-check: without the rounding 2 of 4 crusher tests fail.
- e2e 34 (melektaus-atx): ALL PASSED, 58 checks (1 new). Offline in
  Chrome 2 bits at 4 kHz leave five levels held for 12 samples each.
  64 stereo voices for 2 s render in 1.9 s, about 68 at once.
- e2e 64 (melektaus-atx): ALL PASSED, 30 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 254 files, 3946 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:22:54 +02:00
adminandClaude Opus 5.5 f1f4a57589 Cut a sound to a stretch of its file in its own Sound tab (#343)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 18m20s
A sound asset now opens in a tab that shows its file as a waveform.
Two handles set where the sound starts and ends in the file, and that
stretch is the sample: Play sound, a song item and an instrument's
Sample all play it and nothing else. The file itself is never changed.

- The asset has three new rows: Start (s), End (s) and Edge fade (ms).
  End 0 is the end of the file, the fade is 10 ms by default. Older
  assets have no values and play the whole file as before.
- Every way of playing asks playedSoundOf() for a name instead of the
  file: the file, or <file>#<start>~<end>~<fade> for a cut.
- SoundLibrary makes the cut from the decoded file the first time that
  name plays and ramps each edge inside the file over the fade. Length
  and peaks of a cut are read from the file without a copy.
- Cut buffers can be made again at any time, so the library lets the
  oldest go past 64 Mi samples. A voice holds its buffers itself.
- The tab draws min/max per pixel from 256-sample block peaks, and the
  samples as a line when zoomed in far. Wheel zooms, the middle button
  pans, as in the music tabs. Wave, cut and ruler sit under a mask,
  so a handle panned out of view does not draw over the panels beside.
- Dragging a handle moves that edge, dragging elsewhere spans a new
  stretch. One drag is one undo step for both edges. Whole file puts
  the handles back, Play or Space plays the stretch with a playhead.
- The inspector preview and the Sound FX preview play the cut too, and
  the song lanes draw and repeat it.

Tests:
- Unit: sounds 20, wave-view 8 (new), sound-library 68, plus 258 in
  the touched areas (instrument, song, pattern, inspector, tabs)
- e2e 34 on melektaus-atx: ALL PASSED, 57 checks (7 new)
- Gate on melektaus-atx: CHECK EXIT 0, 253 files, 3936 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:42:21 +02:00
adminandClaude Opus 5.5 2fdf97f7de Record a take from the microphone in the Song tab (#342)
game-maker / gate (push) Successful in 3m32s
game-maker / suite (push) Failing after 15m51s
A song could only be filled with sounds that were made somewhere else.
Record plays the song from the cursor and takes the input meanwhile, so
a part played along lands on the bar it was played to.

- An audio worklet takes the input's samples on the clock the song
  plays on. Each run carries the frame it was taken at, so the take is
  cut where the song was heard to start. The output's and the input's
  latency are taken off, as far as the browser reports them.
- The take goes up as mono 16-bit WAV. Sound, a new track "Recording"
  under the track clicked last and the item at the cursor are one undo
  step. The sound is named "Song – Take 1", 2, 3 and so on.
- While recording the take grows red with its waveform on its track,
  and a meter beside the input dropdown shows the level. The lanes take
  no edits then, only a middle-drag pans.
- The loop is not used while recording. The song plays once through
  and on past its end until Stop, Play or Space keep the take. Esc
  throws it away and nothing goes up.
- Echo cancelling, noise suppression and auto gain are off, nothing is
  played back. The input picked is kept in localStorage.
- A take stops by itself at the size a sound may have. A failed upload
  offers Retry, Download WAV and Discard.
- The API takes sounds up to 256 MB and bodies of the same size. nginx
  and PHP were raised to match outside the repo (body 256m, upload and
  post 256M, memory_limit 512M).
- The pure rules (cut, cap, WAV header, name, track) live in
  core/music/take.ts. The scheduler tells where it started on the
  audio clock.
- The e2e browser gets chromium's fake microphone and skips the
  permission prompt. Each run leaves its take in the sound store.

Found on the way:
- The header was too full with the new widgets and cut the track and
  item count on the right. Input dropdown and meter are narrower now.

Tests:
- Unit take.test.ts: 7 new, svg-icons adjusted to 8 toolbar icons
- e2e 65 (melektaus-atx): ALL PASSED – 31 checks (+4)
- Gate (melektaus-atx): CHECK EXIT 0, 252 files, 3923 tests
- Probe on stein2: a 3 s take of the test tone, peak 32767,
  285100 bytes, waveform seen in the screenshot

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:58:17 +02:00
adminandClaude Opus 5.5 aa097bd076 Zoom with the wheel and set the loop on its own strip in music tabs
The ruler of the Pattern and Song tabs did two things with one drag,
and the wheel did three things with Alt and Shift. Both now do one
thing each, and the Song tab gets a few edits it was missing.

- The ruler has a loop strip along its top. A drag there sets the
  loop, a press or drag below it moves only where playing starts.
- The wheel only zooms: the time over the grid, lanes and ruler, the
  rows over the keys, the tracks over their heads. Alt and Shift no
  longer change it. A middle-drag still pans.
- Tracks can be zoomed from 28 to 160 px. Below 46 px a head shows
  only its name, M and S. An old saved view gets the old height.
- A click on nothing in the lanes picks the song itself.
- A right-click on an item opens a menu with Delete instead of
  deleting it. The inspector has no delete buttons any more.
- A sound item goes up to +12 dB, a track up to +12 dB. A pattern
  item stays at 0 dB, since a note is held to unity.
- An end of an item under the pointer lights up and shows a resize
  cursor. A long item marks every point where it starts over.
- The Pattern tab has an Audition switch in its header, off at first.
  Only with it on does a note sound when drawn, grabbed or moved.
  The keys down the left sound either way.
- A looped sound item draws every round alike. Each round is drawn
  from its own start, and each column shows the loudest part of the
  sound it covers instead of one sampled bucket.
- A song is at least one bar long instead of four. An empty song
  fits that bar across, and Play song ends after the last item.
- The ghost notes held back in #334 got their own issue, #339.

Finding: suite 542 for f97961c failed 5 game checks in e2e 64 and 65
while my runs on the same Windows machine overlapped it. Modules
60-65 alone without load passed, so this push's suite has to show it.

Tests:
- Unit: song-edit 11, piano-roll 17, song 14, sound-library 67.
  Four of them are new, one in song changed.
- e2e 64+65 on melektaus-atx: ALL PASSED - 46 checks (+3).
- e2e 60-65 on melektaus-atx: ALL PASSED - 86 checks.
- Gate on melektaus-atx: 251 files, 3907 tests, CHECK EXIT 0.
- Audition, after the gate: e2e 64 alone ALL PASSED - 25 checks
  (+1), transitions and piano-roll 30 tests, typecheck and ESLint
  clean.
- Waveform and one bar, after the gate: song, song-edit, song-play
  and the node catalog 438 tests. e2e 65 alone ALL PASSED - 25
  checks (+1). A probe shot of a looped pop sound: all six rounds
  pixel for pixel alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:25:49 +02:00
adminandClaude Opus 5.5 f97961c8ff Play and stop a song from a behavior in the game #336
game-maker / gate (push) Successful in 3m35s
game-maker / suite (push) Failing after 16m36s
A song could only be heard in its tab. Now a game plays it through
Play song and ends it through Stop song, with its tracks as they are
set. A playing pattern or song also no longer undoes the pause of the
toolbar.

- Play song plays from the start of the song and then round the loop
  of its ruler, so an intro is heard once. Without loop it plays once
  to its end. Inputs are song, volume, fade in, loop (on by default)
  and detached (off by default). It gives out playback.
- Stop song stops one song, every song for none, or one playback by
  its number, over its fade out. Stop sound with no sound named stops
  songs too. Stop pattern with no pattern named stops only patterns.
- Songs play side by side, as patterns do. They are owned like a
  sound, so leaving the scene ends one unless it is detached. Pause
  game and the time scale leave the music alone.
- Each playing is heard through a master of its own, silent until its
  first note. The fades move it, and a song's tracks sit inside it on
  buses of their own. They go once the playing and its voices have.
- Notes and clips are struck the same way in the editor and the game
  (strikeNote, playClip). What a sequence needs comes from one place
  (core/music/needs).

Findings:
- Reading the audio clock resumed a suspended context. A playing
  pattern read it every 25 ms and so undid the toolbar's pause. The
  library now leaves a context alone while the toolbar holds it.
- A pattern played once to its end asked the list of playings about
  its voices, and that list asked the pattern again. Once its last
  voice was gone this ran until the stack overflowed. The scheduler of
  a playing now asks only after voices.
- e2e 64 reads the playings through __gm.sound.playbacks() now.

Tests:
- song-play.test.ts (new): 8 tests, node catalog +8 cases
- sound-library.test.ts: 66 tests, 4 new
- e2e 64 (melektaus-atx): 23 of 23 checks passed
- e2e 65 (melektaus-atx): ALL PASSED, 22 checks (+4)
- Gate (melektaus-atx): CHECK EXIT 0, 251 files, 3903 tests
- Counter-checks: the old way of asking after voices, a clock read
  that wakes a held context, and a start after a fade out began each
  fail 1 test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:38:44 +02:00
adminandClaude Opus 5.5 8f9c250eb6 Lay patterns and sounds out on tracks and play them as a song #334
game-maker / gate (push) Successful in 3m29s
game-maker / suite (push) Successful in 16m29s
Patterns could only be heard one at a time, in their own tab or from
a behavior. A song lays patterns and sounds out on tracks along one
time axis and plays them through the scheduler of #332, the third
piece of the music tools of #330.

- New asset type song under the heading Songs, with four empty tracks,
  120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows
  in the inspector. Tracks, items and the loop are the asset's data,
  held to their rules on load and in every command.
- A track has a name, volume, pan, mute and solo. Each is heard on a
  bus of its own, so a change reaches what is sounding at once.
- A pattern item repeats its pattern for as long as it is, with a
  transpose and another instrument if wanted. A sound item plays its
  sound as a clip at its own speed, round and round, through a sound
  FX if wanted. Both have a volume on top of their track's.
- Cut back at the left end or split, an item starts as far into what
  it plays as the cut is: ticks into a pattern, seconds into a sound.
- The tab shows the heads with M, S, volume and pan, the ruler of the
  Pattern tab and the items, a pattern with its notes, a sound with
  its waveform, a missing asset red. Patterns and sounds are dragged
  in from the Assets panel. Body, right and left end move, lengthen
  and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits
  where playing starts, Delete, arrows. A double-click opens a
  pattern item in its tab.
- What is picked is a new selection of its own, so the inspector
  shows the item or the track, with a button to delete it.
- The scheduler plans clips beside notes. Where the playhead starts
  or jumps back inside a clip, it is taken up that far into its
  sound. A clip fades out at the end of its item or of the loop.
- The editor plays one piece at a time: a pattern started stops the
  song and the other way round.
- The rules are pure functions in core/music/song, editor/song/
  songView and songEdit.

Findings:
- The suites of 4ea752f and 2a3b90b were red at e2e 54: the Pattern
  pushed Struct below the fold of the + menu. The module now scrolls
  to it, as e2e 47 does for Vector art. It also left the menu open,
  which made e2e 55 fail behind it.
- The Assets panel only dragged assets a scene can place. Patterns
  and sounds are dragged now as well, and a scene says why it does
  not take one instead of calling every asset a scene.

Tests:
- song.test.ts (new, 14) and song-edit.test.ts (new, 9): model,
  round trip, sequence of patterns and clips, mute and solo, cuts,
  clips on the scheduler, moving, sizing, trimming, splitting,
  duplicating, hits, head parts. sound-library.test.ts +5: clip
  offset, end fade, pitched clip, track bus, peaks.
- e2e 65 (new) on melektaus-atx: ALL PASSED, 18 checks. e2e 54 and
  55 with the fix: ALL PASSED, 33 checks. e2e 4 with 30 asset
  types: ALL PASSED, 62 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 250 test files, 3884 tests.
- Counter-checks: no taking up of a clip, no pattern offset, no clip
  end each fail 3, 1 and 1 tests.
- Probe on stein2: the tab with four tracks, before and while
  playing, looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:28:25 +02:00
adminandClaude Opus 5.5 2a3b90b017 Play and stop a pattern from a behavior in the game #336
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Failing after 13m22s
A pattern could only be heard in its tab. Songs come with #334, and
only then can #336 play them in a game. Until then a game plays a
single pattern, looped or once, through two new nodes.

- Play pattern plays the whole pattern through its instrument at its
  own tempo. The loop on the ruler is only for editing. Inputs are
  pattern, volume, loop (on by default) and detached. It gives out
  playback, 0 where there is no instrument or no note.
- Stop pattern stops one pattern, every pattern for none, or one
  playback by its number. Let ring lets notes with an Envelope fall
  away in their release.
- A playing is numbered and owned like a voice. Destroying the object,
  Stop sound with nothing named and the end of the run stop it. Stop
  sound with a voice takes a playback too.
- Each playing runs on a scheduler of its own (core/music/playbacks).
  It waits for its files first, and a stop that came earlier wins.
- The scheduler has an end for a sequence that does not loop. It stops
  waking there and is done once its last notes are.
- The worker clock moved to viewport/wake.ts, so SoundLibrary can use
  it without importing music.ts.

Tests:
- pattern-play.test.ts (new): 8 tests, node catalog +7 cases
- e2e 64 (melektaus-atx): ALL PASSED, 23 checks (+3)
- Gate (melektaus-atx): CHECK EXIT 0, 248 files, 3854 tests
- Counter-checks: no end in the scheduler, and no owner for the
  playing, each fail 1 test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:29:45 +02:00
adminandClaude Opus 5.5 9c71eb1a0d Shape notes with an Envelope and move numbers with its curve #335
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 15m58s
Without an envelope a note set in hard and broke off 10 ms after its
end. An instrument now has an Envelope node with attack, decay,
sustain and release. On the way of the sound it is the volume. Its
output "value" moves a number of the note with the same curve, from
"From" to "To", for a filter sweep among others. A new Filter node
gives that sweep something to move.

- Envelope (instrument only): attack a straight line, decay and
  release ease out and are 60 dB of the way at their time. A release
  takes 10 ms at least. Defaults 0.005 / 0.1 / 1 / 0.1.
- The end of a note goes to the Envelope instead of the 10 ms fade. A
  Gate or Loop Sample under it plays on through the release, "Loop,
  then release" leaves its loop too, and the voice ends with the
  release. A One-shot under an Envelope of its own plays through.
- A note ending before attack and decay releases from the level it
  has reached. Chrome holds it with cancelAndHoldAtTime, Firefox gets
  the level worked out from the shape.
- New port type "envelope" (coral). It fits only a number input that
  says it can follow one (PortDef.followsEnvelope): Filter cutoff and
  resonance, Pitch, Gain, the EQ bands and Mix. Not the Delay, not the
  Envelope's own numbers. portsMeet() decides it for ConnectCommand,
  the drag and the drawn edges.
- On the audio thread every Envelope of a note is one
  ConstantSourceNode. A number it moves gets the curve through a
  WaveShaper, in equal ratios for a cutoff and a gain. A number an
  Envelope moves is no longer reached by a parameter.
- Filter (sound FX and instrument): low-, high- or band-pass, cutoff
  in Hz, resonance 0 to 1 as a Q. A low- or high-pass is flat at 0
  and rises 20 dB at 1, a band-pass goes from Q 0.7 to 20.
- Stop sound with "let ring" lets a note with an Envelope fall away
  in its release. Without it, and with Stop in the tab, it is cut.
- The same note again during a release is a voice of its own.
- The Envelope's numbers are set when the note starts. They can come
  from velocity or a parameter, later changes do not bend the note.

Tests:
- envelope.test.ts (new, 12): curve, ratios, Filter Q, compiling an
  Envelope, its value into each number, a Delay refused
- sound-library +7: attack and decay on the clock, release from the
  level reached, One-shot held, Stop sound with and without let ring,
  a cutoff and a pitch moved by the curve
- e2e 34: ALL PASSED, 50 checks (+2). Offline in Chrome the curve
  rendered 0.5 / 1 / 0.5005 at 0.05 / 0.1 / 0.3 s, released at 0.5 s
  it was 0.0158 at 0.65 s and 0.0005 at 0.8 s, released mid-attack it
  fell from 0.5 to 0.05 in 0.1 s. A Gain moved 0.25 to 1 was 0.5
  halfway. A looped note of 0.2 s with a release of 1 s was gone
  after 1.32 s.
- Five counter-checks (release end, One-shot hold, the follows check,
  ratios, pitch sum) failed 3, 1, 1, 2 and 2 tests.
- Gate on melektaus-atx: CHECK EXIT 0, 245 files, 3812 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:54:46 +02:00
adminandClaude Opus 5.5 fb4d65a2ab Keep both sounds full at the middle of a Mix with a curve #331
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Successful in 16m2s
A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds
in an instrument now goes through a Mix (076c564), so the user asked
for a second curve where each side stays full up to the middle and
only fades on the other half. At 0.25 a is full and b half, at 0.5
both are full, at 0.75 a is half and b full.

- The Mix has a setting Curve with Crossfade (default) and Both at
  center. A plan and a chain only say `curve` where it is `both`, so
  old projects and their compiled plans stay as they were.
- A share is not a straight line of `mix` with that curve. Two ramps
  of their own would dip to half each when `mix` glides across the
  middle. So a ConstantSourceNode carries `mix` on the audio thread,
  and two WaveShaperNodes make the shares from it. The knob of such a
  Mix is that one source.
- The source is started with the voice and stopped when it is cut or
  ends by itself, so it does not run on after the voice.
- A sound FX node in an instrument keeps the curve of a Mix inside it.
- At 0.5 both sounds are added in full, up to +6 dB where both are
  loud. That is what several edges into the Output did before. The
  wiki says so.

Found on the way: the suite of 23bdc3f (task 529) failed the Step
check in e2e 34. It timed the jump from the start of the voice, but a
timed change runs from when it arrives. In CI that was 80 ms later,
and the pitch rightly held to 2.08 s.

- stats() says `tunedAt`, the second of the audio clock the last
  change moved a voice. A change that moves nothing, as On update
  sends every frame, leaves it.
- The Step check times its readings from there.

Tests:
- sound-library.test.ts: both shares shaped from one source, a glide
  moves only the source, the source stops with a cut and with an end
  by itself (50 tests, 2 new)
- soundfx.test.ts: curve only where set, one sound at 0 (41 tests)
- instrument.test.ts: the curve through a sound FX node (34 tests)
- Counter-checks: dropping the curve in throughFx and not stopping
  the source in done() each turn their new test red
- e2e 34 on melektaus-atx: ALL PASSED, 48 checks (+1). Rendered
  offline in Chrome, a and b come to 1/0, 1/0.5, 1/1, 0.5/1 and 0/1
  at mix 0 to 1, and a glide from 0 to 1 reads the same at 0.25, 0.5
  and 0.75 s. Step jumped 2.04 s after the change.
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3793 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:12:56 +02:00
adminandClaude Opus 5.5 23bdc3f775 Play notes in time on the audio clock with a scheduler #332
game-maker / gate (push) Successful in 3m34s
game-maker / suite (push) Failing after 16m7s
Patterns and songs (#333, #334) have to play sample-accurate in time,
whatever the frame rate does. What is counted in game time runs away
from the sound. This lays the ground: musical time, a scheduler that
plans notes ahead on the audio clock, and a player that starts and ends
a note at a given second. Nothing in the editor plays a sequence yet,
the check scripts drive it through `__gm.music`.

- Ticks, 480 to the quarter as in MIDI files (decided in #332).
  Triplets and quintuplets land on whole ticks. Any meter, a beat being
  the note value under the line. The tempo always counts quarters.
- `core/music/sequence.ts` is a pure rule: the notes starting in a
  window of playing time. Across the end of a loop it looks into the
  next round, so the seam has no gap. A note past the loop end ends
  there. Windows are whole ticks, and the seam is stepped exactly, so
  no rounding can drop the first note of a round.
- `core/music/Scheduler.ts` plans the next 100 ms every 25 ms through a
  host, so it is tested with a clock of its own. The first note sounds
  50 ms after the start. Changes to the sequence are heard from the
  next window on. Stop and seek fade what it started. A new tempo or
  loop counts from what is planned, and the playhead keeps its old
  pace until the clock gets there.
- The position is the audio clock less `outputLatency`, as bar, beat
  and tick.
- The beat comes from a Web Worker (`viewport/musicTimer.ts`), which
  keeps its pace in a hidden tab.
- The sound library starts a note at `timing.at`: a buffer source by
  `start(when)`, the pitch worklet by counting down inside the render
  block it falls in. A note's end is set on the clock at once, its
  fade to the sample. Leaving a loop still waits for the page.
- Delay synced to beats (decided in #332, noted from #327): a setting
  `Sync` (Seconds / Beats), `time` then in quarter notes, held to the
  2 s of the line. Play sound and Play note have an input `tempo`,
  120 BPM where nothing is connected. A scheduled note carries the
  tempo of its sequence.
- Swing is left for #333/#334, as decided.

Also fixes the red suite of 6f1f315 (task 526). Three checks in e2e 34
waited on the page's clock for things that run on the audio clock. In
CI the voice was already 0.7 s old when first seen, and the 0.3 s note
of Play note was over. `stats().last` now says when a voice started on
the audio clock, and the Quad, Step and Play note checks hold every
reading against it.

Tests:
- music.test.ts (new): 17 tests, time, windows, loop seam, scheduler
  with a jittery timer, stop, seek, tempo change
- sound-library +4, soundfx +1, node-catalog +2 (tempo inputs)
- Counter-checks: an unclamped note length, a window that never moves
  on, a buffer source ignoring its start time and a Delay ignoring
  Beats each turn 1, 6, 1 and 2 tests red
- e2e 34 on melektaus-atx: ALL PASSED, 47 checks (+2). Nine notes of a
  looped bar at 150 BPM exactly 0.4 s apart, each planned 89 to 91 ms
  ahead. The position matched the clock less 32 ms latency to the
  tick. Offline the worklet started on samples 590, 12000 and 15200.
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3786 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:29:31 +02:00
adminandClaude Opus 5.5 144dee0a97 Let a looped sample play out past its loop when the note ends #331
With "Mode" and "Loop" as two fields, One-shot plus Loop ran until
Stop was pressed: letting go of the key did nothing. The two fields
are now one, "Play", with four entries that each say what happens
while the note is held and when it is let go.

- Gate and One-shot play the sound once, as before. Loop repeats it
  and fades out with the note.
- "Loop, then release" is new: the sampler's sustain loop. Let go,
  it finishes the round it is in and plays what comes after "Loop
  end" to the end of the sound.
- A buffer source stops looping and plays on by itself. The pitch
  worklet gets a "leave" message. The phase vocoder then lets the
  round finish that its newest frame has read into, so nothing read
  already changes under the frames still sounding.
- Samples saved with "Mode" and "Loop" are turned into "Play" on
  load. Gate and One-shot stay, either with Loop becomes Loop, since
  playing until stopped is gone.
- The keys of the Instrument tab stay half lit while a note let go
  is still heard. Esc on the keys stops everything, like Stop.
- Striking a ringing key again plays a new note beside it rather
  than stopping the old one, so a drum can be hit quickly. This
  differs from what was proposed in the issue discussion.

Tests:
- instrument + pitch-player + sound-library + node-catalog:
  477 tests, green (12 new).
- e2e 34 (melektaus-atx): ALL PASSED, 45 checks (+1). Let go after
  a loop of 0.05 s, the note played out for 0.31 s on the audio
  clock, the key half lit meanwhile.
- Counter-check: a release that does not leave the loop fails the
  2 new player tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:34:30 +02:00
adminandClaude Opus 5.5 8a4e4a6762 Play sounds as notes of an instrument asset #331
game-maker / suite (push) Failing after 2m40s
game-maker / gate (push) Successful in 3m14s
A sound could only be played as it was, one at a time. An instrument
is a node graph that makes a note a sound: Notes nodes bring the note
in, Samples play sound assets for it, the effects of a sound FX shape
it and the Output sums the layers. Its tab plays it from a keyboard,
a behavior with Play note. It is what patterns and songs (#330) will
play.

- New asset type instrument, under the heading Music, not placeable.
  A new one starts as Notes into a Sample into the Output.
- Notes are counted as MIDI does: 60 is C4, names like C#4, sharps
  only, the lowest C-1. Notes has From and To, outputs any, note,
  frequency and velocity, and with Single notes a trigger per note,
  each with an optional name for a drum kit.
- New port type trigger, only into the trigger of a source. Inputs
  can take any number of edges now (PortDef.many): a trigger, and
  the Output, which plays everything into it side by side.
- Sample plays a sound asset. Follow note shifts it by note - root
  through the pitch worklet of #317, so it keeps its length. Gate
  fades it out at the end of the note, One-shot plays through. Loop
  runs between Loop start and Loop end, also in the worklet.
- Parameters declares the instrument's own parameters with a
  default. Sound FX sends the sound through a sound FX asset, its
  parameters as inputs, the sound set in where its Input was.
- compileInstrument walks the graph once per note. A trigger is on
  only for the notes it stands for, a silent source silences what it
  alone feeds, a Mix with a silent side is the other side. velocity
  and the parameters stay open for the voice. Builds are cached per
  note until the graph or a sound or FX it names changes.
- The player takes a note as one voice of several layers, each way
  with its own file and volume. release fades the gated ways out,
  one-shots play on. playNote can release after a duration.
- Play note plays one note with velocity, volume, duration and
  detached, and hands out voice for Set sound FX parameters and
  Stop sound. Velocity is always loudness. Instrument parameters
  picks values for the instrument's parameters like Sound FX
  parameters does for an FX, renames follow.
- The tab has the graph, a header field per parameter, Stop, and
  128 keys below. Keys without sound are grey. A key plays while it
  is held. Focused, the computer keyboard plays as in a tracker by
  KeyboardEvent.code, the arrows move the octave.

Findings:
- A label of the keys set fill on the shared small text style and
  darkened every small label of the editor. Caught in a screenshot
  before the commit, the keys have a style of their own now.
- Suite task 523 on cf6799a failed two checks of e2e 34 that timed
  audio by the page's clock: a 2.95 s echo tail was gone after 1 s,
  and Quad in was further along than 0.7 s allow. Both time on the
  audio clock now through a new hook __gm.sound.clock().

Tests:
- instrument.test.ts (new, 22): notes, asset, palette, round trip,
  compile per note, drum kit, zones, Mix, velocity and parameters,
  Sound FX node with rename, problems, cache, Play note, the keys
- sound-library +7 (layers, release, duration, early release, loop
  region in buffer and worklet), pitch-player +2 (loop region),
  node-catalog +4, graph adjusted. 582 tests in the touched files.
- e2e 4 and 34 on melektaus-atx: ALL PASSED, 34 with 44 checks (+7)
- Gate on melektaus-atx: CHECK EXIT 0, 243 test files, 3750 tests
- Counter-checks: any always on, release ending one-shots, Sound FX
  inputs ignored, loop region ignored. 3, 2, 1 and 1 tests failed.
- Browser probe on stein2: keys, held key, Single notes names and
  the header looked at in screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:07:40 +02:00
adminandClaude Opus 5.5 cf6799a784 Let echoes ring on with a Delay node in sound FX #324 #327
game-maker / gate (push) Successful in 3m20s
game-maker / suite (push) Failing after 15m4s
A voice used to end the moment its sources did, and Stop sound cut it
dead. An echo has to outlive its sound, and a stop must not click.

- Delay node: a delay line with a damped feedback loop, added at mix
  next to the untouched sound. Time 0.01 to 2 s, feedback up to 0.95,
  damping 20 kHz to 500 Hz evenly in octaves, all fed by parameters
  and moved by Set sound FX parameters like any other number.
- Ping-pong is a setting of the node. It takes the sound into the left
  of the line and swaps the sides every round.
- A voice with an echo rings on for its tail: until the last echo is
  60 dB below the sound, from time, feedback and mix (fxTailOf). It
  stays reachable meanwhile, and a change counts the tail again.
- The end of a tail is timed on the audio clock with a silent
  ConstantSourceNode, so a paused run holds it. Then the voice is
  disconnected, since an echo is a loop of nodes.
- Stop sound fades a voice out in 10 ms instead of cutting it. Let
  ring fades only its sources and keeps the voice for its echoes.
  Where the context is suspended it cuts at once.
- Stop sound takes a voice. Connected, it stops only that play, and a
  0 from a Play sound that played nothing stops nothing.
- Play sound hands out tail: length plus the echoes at its start
  values.
- Destroying a behavior and ending a run still cut, echoes included.
- No limit on voices. The emit of a sound FX node now also gets the
  node's settings.

Finding: in Chrome every round of a feedback loop comes one render
quantum late (128 samples, 2.7 ms at 48 kHz), measured as 0, 128, 256,
384 for the first four echoes. Left so, since a browser without it
would then hear the echoes early. Written in the wiki.

Tests:
- sound-library, soundfx, node-catalog, sounds, keep-on-scene-load:
  489 tests green. New: fade on stop, cut while suspended, echo and
  ping-pong wiring, ringing until the tail, tail counted again, let
  ring, stop by voice, echo knobs, delay compile, tail sums.
- Counter-checks: no tail, a stop without fade, a connected voice 0
  read as unconnected. Each fails its tests, files back by cmp.
- e2e 34 (melektaus-atx): ALL PASSED, 37 checks. A blip through a
  ping-pong Delay (0.3 s, 0.5, 0.5) rang 2.97 s, 2.95 s worked out.
  Let ring kept the voice, a stop after it cut it. Rendered offline
  with the player's own stageNodes the echoes are 0.25 left, 0.125
  right, 0.063 left. 64 stereo voices of it took about 0.3 s for 2 s,
  so about 400 play at once.
- Gate (melektaus-atx): CHECK EXIT 0, 242 test files, 3713 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:05:05 +02:00
adminandClaude Opus 5.5 6e70421b92 Keep the length of a sound whatever its pitch does #317
game-maker / gate (push) Successful in 3m13s
game-maker / suite (push) Successful in 15m36s
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
adminandClaude Opus 5.5 804a97b0a9 Hand out the length of a played sound from Play sound
game-maker / gate (push) Successful in 3m12s
game-maker / suite (push) Failing after 26m24s
Wired into the time of Set sound FX parameters, a change then spans
exactly one run of the clip, so a fade or a filter sweep ends with its
last sample.

- Play sound has a second output, length, in seconds. It is 0 where
  nothing was played or the sound has not loaded yet.
- The AudioSink gives the length of a file, the SoundLibrary takes it
  from the decoded buffer. A pitch the voice starts on keeps it (#312).
- A pitch changed while the voice plays still changes its length, so a
  pitch sweep over length does not end with the sound. A pitch that
  keeps the length in real time is #317.

Tests:
- soundfx, sounds, keep-on-scene-load, node-catalog, sound-library:
  462 tests green.
- Counter-check: with length always 0, the catalog case and the new
  soundfx case fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:27:44 +02:00
adminandClaude Opus 5.5 e7a360db73 Offer the easings of an animation key on Lerp and sound FX ramps
game-maker / gate (push) Successful in 3m15s
game-maker / suite (push) Successful in 15m36s
A timed Set sound FX parameters ran only linearly, and a Lerp had four
quadratic eases of its own. An animation key already had the full set.
Now all three offer the same choice: Linear, Step, the ten curves from
Sine to Bounce with In, Out or In-out, and a drawn Curve asset.

- easingParams in src/core/graph/nodes/easing.ts gives a node the
  fields Easing, Direction and Curve from the key's own lists, and
  easingOfParams reads them back as a key interpolation.
- There is no migration. A Lerp saved with the old in, out or inOut
  runs linear until its easing is picked again.
- Set sound FX parameters hands the easing to the player. Linear and
  Step stay ramps of the audio clock, every other easing goes to
  setValueCurveAtTime as 200 samples a second, in semitones and dB.
- Step now reaches its end at t 1. A key never asked for that, it
  reads the next key itself, so animations play as before.
- The curve-in-use count and the warning for a gone curve cover the
  sound node too.

Tests:
- node-catalog: Lerp with cubic in, quad out and in-out, a curve
  without a direction, step, an unknown easing as linear. The sound
  node hands on a curve, step and a gone curve as linear.
- sound-library: an eased change becomes a curve in semitones and
  decibels, a step a jump at the end.
- node-catalog and curve after dropping the old values: 399, green.
- e2e 6, 34, 53 on melektaus-atx: ALL PASSED, 198 checks. In Chrome a
  2 s Quad in from depth 7 to -5 read speed 0.864 early and 0.5 at the
  end, a 1 s Step held 1 and then jumped to 0.5.
- Counter-check: with curves sent as linear ramps, the curve test of
  the sound library fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:05:00 +02:00
adminandClaude Opus 5.5 501a1524ed Ramp sound FX parameters over a time on the audio clock
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Successful in 15m26s
A pitch sweep counted down in On update or On fixed update runs in
game time, while the sound runs on the clock of the audio device. A
short sound magnifies any offset between the two, so the sweep came
out different on every play and most of all on the first.

- Set sound FX parameters has a new input time in seconds. The values
  run evenly to their target in that time, scheduled on the audio
  clock with linearRampToValueAtTime or exponentialRampToValueAtTime.
- Evenly in their own unit: a pitch in semitones and a gain in
  decibels, an EQ band and a Mix share linearly in their value.
- A time of 0 or below keeps the quick glide of about 0.1 s.
- Each change starts from where the voice is now. Whatever was still
  planned is dropped, so a change never queues behind an older one.
  Firefox lacks cancelAndHoldAtTime, there the value is held by hand.
- A voice still waiting on its bytes starts a timed change with the
  sound, so the whole sweep is heard.

Tests:
- node-catalog: time is handed on, a time below 0 is a quick glide.
- sound-library: semitones and decibels ramp in their own steps, a
  timed change on a waiting voice starts with the sound.
- sound-library + node-catalog + soundfx: 419 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 31 checks. A 2 s ramp from
  depth 7 to -5 read speed 0.979, 0.926 and 0.778 half a second on.
- Counter-check: folding a timed change on a waiting voice into its
  start values fails the new test.
- The gate did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:13 +02:00
adminandClaude Opus 5.5 ed2b58b3c0 Load the sounds of a project ahead of their first play
A sound was fetched and decoded on its first Play sound. That play
started late, its beginning was not heard, and a graph that tuned the
voice in the meantime had moved on by the time it started. Images and
fonts were already loaded when a project opens. Sounds now are too.

- projectFiles lists the sounds of a project, and preloadProjectFiles
  hands them to the new soundLibrary.preload. That covers the editor
  and the published game.
- Preloading opens no audio device. It decodes with an
  OfflineAudioContext at 48 kHz. The real AudioContext is still made
  on the first play, and a source resamples where the device runs at
  another rate.
- A run started right after opening a project can still find a sound
  loading. It then starts late as before.

Tests:
- sound-library: a preload makes only the offline context, and a
  play after it starts at once, opening the device only then.
- sound-library + project-files: 18 tests, green.
- e2e 34 on melektaus-atx: ALL PASSED, 30 checks.
- Counter-check: decoding with the real AudioContext again fails the
  new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:00 +02:00
adminandClaude Opus 5.5 c7e9769f90 Change the sound FX parameters of a sound while it plays #315
game-maker / gate (push) Successful in 3m14s
game-maker / suite (push) Successful in 15m39s
Until now a value handed to a sound FX counted from the start of the
voice only. An engine whose pitch follows the speed, or a loop that
gets quieter, could not be built. Every play of Play sound now gives
out the number of its voice, and a new node lets the knobs of that one
voice glide to new values while it plays.

- Play sound has a new output voice: the number of this one play, or 0
  where nothing was played. Kept in a Number variable it reaches the
  voice from another event. Two plays of one sound are two voices.
- New action node Set sound FX parameters (exec, voice, fx params).
  It reaches exactly the voice it is given, a detached one too. A
  voice that has ended, or voice 0, is not reached.
- AudioSink.play returns the voice number and takes the plan and the
  values (FxVoice) instead of a finished chain. AudioSink.tune takes a
  voice number and new values. A name the voice's FX does not have is
  left out, one not given keeps its value.
- SoundLibrary hands out the number at once, before the bytes are
  there, and never twice. A voice still waiting for its bytes starts
  with the values handed in meanwhile.
- Every knob of a voice moves with setTargetAtTime (30 ms time
  constant). A knob whose value did not change is not sent again, so a
  tune every frame costs next to nothing.
- A pitch on a playing voice glides by playbackRate. A new WSOLA copy
  in the middle of a sound is not worked out, so the voice gets shorter
  or longer while it is moved. On a loop that is not heard.
- fxChainOf now gives a chain whose shape depends on the plan alone.
  A stage fed by a parameter stays at 0 dB, and a Mix fed by a
  parameter keeps both ways. A typed-in 0 or Mix at 0/1 is still left
  out by the compiler.
- Every way of a voice has a source of its own, also at the same
  pitch, so each can glide on its own. The pitched copy is shared.
- The tab keeps the numbers of what it plays, so moving a parameter
  field is heard at once.
- Test hook __gm.sound.levels(file) reads the knobs of every voice of
  a file, oldest first, and stats() counts the voices tuned.

Tests:
- soundfx.test.ts: 29 tests (+2 for the node in a running world: the
  voice of the first of two plays of one sound, and a play that played
  nothing)
- sound-library.test.ts: 15 tests (+3: glide, reach by number,
  pending voice)
- node-catalog.test.ts: 370 tests (+2, Play sound checks its voice)
- sounds + keep-on-scene-load: 31 tests, unchanged
- Counter-test: a tune that reaches every voice of the same file makes
  the reach test fail.
- e2e 34 (melektaus-atx): ALL PASSED - 30 checks (+1). In Chrome two
  looping voices of one sound start at depth 7. The one whose number
  sits in a variable glides to 0.7938 (2^(-4/12) = 0.7937), the other
  stays at 1. Both gains stay at 0.5012.
- The gate ran before voices got a number (CHECK EXIT 0, 241 files,
  3658 tests), not after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:53:26 +02:00
adminandClaude Opus 5.5 072537b2e8 Blend two ways of a sound FX with a Mix node #316
game-maker / gate (push) Successful in 3m10s
game-maker / suite (push) Successful in 15m30s
A sound FX was a single line from Input to Output, so two versions
of one sound could not be heard together. The new Mix node joins two
ways again, and each way can carry its own effects, its own Pitch
included.

- Mix has the audio inputs a and b and a number input mix (0 to 1,
  default 0.5). At 0 only a is heard, at 1 only b. Like every number
  it can come from a parameter of the Input and is held to its range.
- The compiled plan and the chain a voice gets are now a tree. A
  source carries a pitch and stages, a mix carries a, b and the
  stages after it. A Mix that ends at exactly 0 or 1 collapses to the
  one way heard, so the silent way is never worked out.
- A Pitch is still worked out on the sound as it starts (#312). Behind
  a Mix it goes to both of its sounds. An effect before a Pitch
  therefore acts as if it came after it, the node now says so.
- The player starts one source per pitch, all at once and all of the
  same length, and two ways at the same pitch share one. A mix becomes
  two gain nodes with 1 - mix and mix into a sum. A voice leaves the
  count when all of its sources have ended.
- stats().last reports pitches (one per source) instead of pitch.

Tests:
- soundfx.test.ts: 27 tests (+5), sound-library.test.ts: 12 (+2),
  node-catalog.test.ts: 368, all green. Typecheck, ESLint, Prettier
  clean.
- Counter-check: a Pitch behind the Mix given to a only, and b mixed
  at 1 - mix, each fails its new test.
- e2e 34 on melektaus-atx: ALL PASSED, 29 checks (+1). The new check
  plays the pitched way mixed with the dry Input in the browser: two
  sources at 5 and 0 semitones, as long as the sound itself.
- The full gate did not run before this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 20:09:30 +02:00
adminandClaude Opus 5.5 790e483198 Pitch a sound FX without changing its length #312 #313 #314
game-maker / gate (push) Successful in 3m9s
game-maker / suite (push) Successful in 15m19s
The Pitch node changed the playback speed, so a sound pitched up was
also shorter and one pitched down longer. A pitch now keeps the
length, takes fractions and reaches further. Gain and the EQ bands
reach further down, and the EQ is renamed so a parametric one can
come later without a mix-up.

- Pitch shifting is the pure function pitchShift in
  core/soundfx/pitchShift.ts. It reads the sound faster or slower and
  stretches it back by WSOLA: 40 ms frames under a Hann window, each
  moved within 12.5 ms to where it continues the frame before it.
  A looping sound reads on across its end.
- SoundLibrary plays a pitched copy of the decoded buffer instead of
  setting playbackRate. Copies are kept per file, pitch and loop, the
  last 32 of them.
- stats().last tells the duration of what was played, so a check can
  see that the length is kept.
- Pitch: -64 to +64 semitones, scrubbed in steps of 0.1 (#312).
- Gain: -120 to +24 dB (#313).
- EQ is now labelled "5-Band-EQ", its bands go -60 to +24 dB (#314).
  The node type stays SoundFxEq, so saved graphs load unchanged.

Findings:
- A plain granular shifter was tried first and missed the pitch: at
  +0.5 semitones it measured 438 Hz instead of 452.9 Hz, because every
  grain restarts at the phase of the original. WSOLA measures 452 Hz.
- Far ends: +64 semitones from 440 Hz lands above half the sample rate
  and folds back, -64 is below what a 40 ms frame can follow. Nobody
  has listened to either.
- Speed: 3 s of a 48 kHz channel take 47 ms at +3 and +64, 171 ms
  at -64 (measured in Node).

Tests:
- pitch-shift (new): 7 tests. A 440 Hz sine at +12, -12, +7, +0.5 and
  -30.5 semitones is within 2 % of the target and keeps its length.
  Level stays at most 1, a loop does not fade at its seam.
- sound-library: 10 tests (+1), soundfx 22, node-catalog 368.
- Counter-check: with the pitched copy switched off the new
  sound-library test fails.
- e2e 34 (melektaus-atx): ALL PASSED - 28 checks. Its octave-down
  voice is now also checked for the length of the sound.
- Typecheck, ESLint and Prettier clean. The gate did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:32:00 +02:00
adminandClaude Opus 5.5 de14f0f9e2 Play a sound through a graph of gain, pitch and EQ effects #311
A sound could only be played as it was uploaded. A Sound FX asset is
a node graph like a shader: Input, effects, Output. Play sound has a
new input fx that sends the voice through it.

- New asset type soundfx ("Sound FX"), not placeable, with a graph
  and its own tab. The heading in the asset panel is "Sound effects",
  so it does not take the click meant for the + menu entry.
- Nodes in core/soundfx/nodes.ts, category sound, port type audio
  that only fits audio: Input, Output, Gain (dB), Pitch (semitones),
  EQ (80 Hz and 12 kHz shelves, peaks at 250 Hz, 1 kHz and 4 kHz).
- compileSoundFx walks back from the Output with the shared graph
  emitter. An input takes one edge, so the result is one line: a
  pitch and a list of gain and filter stages. A node without input
  or a graph without Output reports a problem on the node.
- Pitch is the playback rate of the source. It applies to the whole
  sound and also changes its length, the node description says so.
- The BehaviorSystem resolves fx and hands the line to the AudioSink.
  An fx that is missing or does not compile plays the sound dry.
- SoundLibrary wires source, stages, volume, destination in that
  order. stats().last now also names pitch and stage count.
- The tab header picks one of the project's sounds and plays it
  through the graph as it stands. Stop there ends only its own voice.

Tests:
- soundfx.test.ts (new, 11), sound-library (9, +2 for the wiring),
  node-catalog (+1 for fx), graph and asset-groups adjusted
- e2e 34: ALL PASSED - 26 checks (+5: + menu opens the tab, palette,
  2 effects, tab Play and game Play through the FX at -12 st)
- Gate on melektaus-atx: typecheck, lint, prettier clean, 239 of 240
  test files green. The red one was sound-library (its fake
  AudioContext had no playbackRate), fixed afterwards, gate not
  re-run in full.
- Counter-check: without the audio rule in portsCompatible the port
  test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:45:42 +02:00
admin 1e9d21cd5d A game that is more than one scene needs a way from one to the next, so the graph can load one (#39)
game-maker / suite (push) Failing after 11m6s
Load scene sets a flag on the World like Stop and Restart do; the swap
happens after the tick, and the last wish of a tick wins. Exactly one
thing crosses over: the SharedState, so global variables and global
machines keep what they know while everything local is built anew.
Loading the scene that is already running rebuilds it, which is what
"reset the level" means when the score must survive.

A run starts either from the start scene (a new project field, set from
the asset panel and marked with an arrow in the list) or from whatever
scene is open. The toggle next to Play says which question is asked and
remembers the answer in localStorage: it is a way of working, not part
of the game.

Sounds no longer end because a scene ends. A voice belongs to the
behavior that started it and stops right after On destroy, so the scene
swap is no longer a special case at all. For the enemy that plays its
explosion and destroys itself in the same breath there is now a
detached input on Play sound.

The scene selector stays usable while a run plays, because a double
click in the asset panel already did the same thing unguarded. It moves
what the editor looks at, not what the run plays.
2026-09-15 13:24:26 +02:00
admin 5671a38044 A game that stays silent is only half a game, so a sound is an asset like any other (#45)
game-maker / suite (push) Successful in 12m1s
Sounds follow the road fonts already paved: the file goes to the server
content-addressed under its sha256, the project keeps a named asset that
points at it, and nothing is ever re-encoded. The format is read from the
file's own signature, not from what the browser claims - the same .m4a
arrives as audio/mp4, audio/x-m4a or as nothing at all.

Playing is two nodes. "Play sound" takes the asset and may raise or lower
its volume; every call is its own voice, so two jumps in a row are two
jumps heard. Its loop input is a choice of three - from the asset, on,
off - because a checkbox has to spend one of its two positions on "not
set" and would lose a case: an asset that loops could then never be
played once.

Below the graph core stays browser-free: NodeContext hands out an asset
id, BehaviorSystem resolves it against the project and folds the asset's
own volume and loop into the call, and only viewport/SoundLibrary knows
about Web Audio. A run without a sink plays nothing instead of throwing.

The CI browser is muted, so the ear is no witness: the debug hook counts
started and stopped voices, and step 34 reads that counter.
2026-09-15 07:54:28 +02:00