Commit Graph
555 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 852dce5142 Shrink a clip with the wave and drop it with a click beside (#357)
Zoomed out, the wave left room above and below it, but the bright file,
the picked clip and its handles still took the whole height. And a click
in the wave beside the picked clip kept it picked, so only the strip
could take it back to the sound.

- fileReach in waveView.ts is how far up and down the file reaches:
  full scale and its inset, never past the area. At level 1 it is the
  whole height, zoomed in too.
- The bright file, the shade outside the clip and the two handles are
  drawn within that reach.
- A click over the wave beside the picked clip picks the sound. Inside
  the clip or on a handle nothing changes, and a drag still spans the
  clip anew.

Tests:
- Unit wave-view: 11 green (1 new), a counter-check red
- e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 new)
- Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4195 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:13:09 +02:00
adminandClaude Opus 5.5 42a3759d9d Cut a sound into named clips, as a texture into frames (#357)
A sound could only be cut to one stretch, set on the asset itself. To
play two parts of one file, the file had to be imported twice. Now a
sound is always the whole file, and its clips are the stretches of it,
each with a name and a cut of its own.

- A clip is { id, name, props } on SoundAsset.clips. Its id shares the
  generator with asset ids, so a sound field holds a sound or a clip,
  as a texture field holds a texture or a frame.
- props are the cut: start, end, the fades and their curves, reverse.
  Volume, loop, sound FX and bus stay on the sound for all its clips.
  The cut rows left the sound's schema for CLIP_ROWS.
- findSound resolves either id. Play sound, the Sample of an
  instrument, a Reverb impulse, song items, the scheduler and the test
  sound of the Sound FX tab all go through it.
- Every sound field offers each sound followed by its clips, "Kick",
  "Kick / Clip 1". A clip that is gone reads as "<id>?".
- The Sound tab has a strip under the ruler with the clips as bands.
  A drag there makes a clip, a click picks one, beside the bands the
  sound. Only the picked clip has handles. Reverse, fades and curves
  in the header work on it and are greyed out without one.
- Whole file is gone from the header, as the user asked.
- Picking a clip is a selection of its own, like a frame. The
  inspector shows its name, its cut rows and Delete clip. Delete
  takes it away without asking, as with a frame. Ctrl+Z brings it
  back.
- Copies get fresh clip ids: Duplicate, Embed a copy, a song
  duplicated with its takes, and the copy a split or paste makes of
  an embedded take. An item on a clip plays the same clip of its copy.
- A double-click on a song item playing a clip opens its sound with
  the clip picked. The Sound row of an item on an embedded take
  offers the take and its clips, which no list shows.
- A new take stays the whole sound. Clips are made by hand, as the
  user decided.
- No migration in the code, as the user decided. The one project
  with cut sounds was changed over by hand.

Deviation from the issue: deleting a used clip does not warn. A frame
does not warn either, and the issue had promised "like a frame".

Tests:
- Unit sound-clips (new): 11 green, 7 counter-checks each red
- Unit sounds, embedded: adjusted to clips, green
- e2e 34 (melektaus-atx): ALL PASSED, 90 checks (3 new, 12 rewritten)
- Counter-check in the browser: clips left out of the sound fields,
  the picker check fails
- Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4194 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:18:47 +02:00
adminandClaude Opus 5.5 c7fa1d15b0 Zoom the height of a sound's wave over its level scale (#357)
The Sound tab always drew the wave at the full height of the tab. Quiet
stretches were hard to see, and there was no room above a loud wave.

- A level scale in dB stands left of the wave, a mark every 6 dB down
  from 0, as long as a mark lies inside the area and at least 14 px
  from the middle.
- The wheel over the scale zooms the height, from 0.25x to 16x. Over
  the wave it zooms the time as before.
- What is louder than the area stops at its edge. The fade ramps grow
  with the wave.
- Fit puts the height back to 1x. The tab keeps the height with its
  view when another tab is shown.
- The rules live in waveView.ts (zoomLevel, levelMarks), the hooks
  give the height, the scale and the top of a full-scale wave.

Tests:
- Unit wave-view: 10 green (2 new), 2 counter-checks each red
- e2e 34 (melektaus-atx): ALL PASSED, 87 checks (1 new)
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4183 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:21:02 +02:00
adminandClaude Opus 5.5 25c1d02b06 Keep the audio thread free of ended voices and too many vocoders (#356)
game-maker / gate (push) Successful in 3m45s
game-maker / suite (push) Successful in 14m44s
A song that had been played a few times in a long session stuttered
at regular intervals until the page was reloaded. A quick run up the
keys of a sampled instrument made every sound drop out for a second
or two. Both came from work the audio thread should not have had.

- Every worklet node of a voice listened on its port. Chrome never
  collects a port that is listened to, and through its listener not
  the voice either. Every node of the voice stayed on its bus and was
  worked on in every block. A way or stage on the worklet now closes
  its port once it ends or is stopped, and the processor closes its
  own side when it is told to stop.
- A voice without echoes now leaves its bus as soon as its sound and
  what its stages hold back are through. Before, only a voice with
  echoes did.
- At most 12 ways play through the pitch worklet at once. One more
  cuts the oldest in 10 ms, a note already let go before one still
  held. Each runs a phase vocoder, whose first block alone took
  2.7 ms of the 2.67 ms a block has on stein2.
- Play for the game ends everything the tabs play: the piece holding
  the transport and every key or preview (`stopPreviews`).
- Closing a Sound, Sound FX, Instrument, Pattern or Song tab stops
  what it plays of that asset (`AssetTabEditor.closed`).
- A preset in the Instrument tab plays nothing any more. With
  Audition on it is heard like any other change.
- Test hooks: `__gm.instrument.play`, `__gm.sound.playing`, and
  `vocoders` in the stats of the sound library.

Measured:
- A copy of the project, its Song 2 played for 12 s and stopped, six
  times (Chrome on stein2, CDP WebAudio): before, 746 nodes were left
  after the first time and 4469 after the sixth, the load at rest
  grew from 0.01 to 0.06. After, 7 nodes are left every time (the
  master and one bus) and the load at rest stays at 0.01 to 0.02.
- Pitch-player voices live in Chrome on melektaus-atx: 12 take 0.50
  of the audio thread, 16 take 0.71, 24 take 0.99. 30 struck 40 ms
  apart held it at 0.99 from the first second on.

Tests:
- Unit sound-library: 110 green (3 new), 4 counter-probes each red
- e2e 34 (melektaus-atx): ALL PASSED, 86 checks (3 new, 2 changed)
- e2e counter-probe with game start and preset undone: their 3
  checks red, and the old check of Play note in the game as well,
  which now counts on the preview being gone
- e2e counter-probe with tab close undone: exactly its check red. A
  first form of it held a key and stayed green, since the keys let go
  of a held key by themselves once the tab shows no asset
- Gate (melektaus-atx): 270 files, 4181 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:57:18 +02:00
adminandClaude Opus 5.5 8c0d1cfd3f Snap the routing graph of a song with the header's magnet (#350)
The routing of a song is the same graph editor as a sound FX, but its
tab had no magnet, so its nodes could not be put on the grid.

- The Song tab carries the magnet next to Routing. It is off at first
  and acts on the routing graph.
- It is greyed out on the timeline, as Grid is greyed out in the
  routing. Switching the view only updates these two buttons, so a
  playing song is not handed its sequence again.
- The check hooks give the magnet and the snapping of the routing.

Tests:
- e2e 65 (melektaus-atx): ALL PASSED, 52 checks (1 new)
- Counter-check (magnet enabled on the timeline): exactly this check red
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4178 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:13:23 +02:00
adminandClaude Opus 5.5 668682c4ff Give a song, a track or an item a sound FX of its own (#355)
A sound FX for one place in a song had to be a project asset, which
then stood in every picker. Now every pick of a sound FX in a song
can make one the song keeps for that place alone, on top of the
embedded assets of the first two parts.

- Create sound FX sits at the end of the Sound FX row of the song,
  a track and an item, and of the menu of a track's FX button. It
  makes "Sound FX 1", 2, … per song with the Input wired to the
  Output, picks it and opens it. That is one undo step.
- Embed a copy, while the place names a project FX, gives the place
  a copy under the same name.
- While the place names its own, the menu shows it with the mark and
  offers Edit sound FX and Make project asset.
- Picking anything else lets it go in the same undo step. For a
  track or an item `settleEmbedded` did that already. For the song's
  own row `AssetPropertyCommand` now settles the song after `fx`.
- The mark `•` moved to the core. `assetRefLabel` puts it in front of
  an embedded asset, so the inspector rows show it like the timeline.
- `FieldHandlers.moreChoices` lets a caller end a reference menu
  with entries of its own. `embeddedPatternName` is `embeddedName`
  with a base now.

Tests:
- Unit own-fx (new): 6 green, 6 counter-checks each red
- Unit embedded and 80 neighbouring files: 1434 green
- e2e 65 (melektaus-atx): ALL PASSED, 51 checks (2 new)
- Gate (melektaus-atx): CHECK EXIT 0, 270 files, 4178 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:32:41 +02:00
adminandClaude Opus 5.5 49669017b1 Make a pattern right in a song with a right-click (#355)
A pattern for one song had to be made in the Assets panel first and
dragged in, and then stood in every picker of the project. Now a
right-click on an empty spot of a track makes one the song keeps to
itself, on top of the embedded assets of the first part.

- Create pattern lays an item from the grid line before the spot,
  a bar long, and opens the pattern in its tab. Pattern, item and
  name are one undo step. The name is "Pattern 1", 2, … per song.
- It plays the instrument of the track's pattern item that starts
  last before the spot, else of the first one after it. An item's
  own pick counts before its pattern's.
- An embedded pattern runs on the song's clock. It takes the tempo
  and the meter of the song, and a change on the song moves it along
  in the same undo step (`AssetPropertyCommand`).
- Its rows Tempo, Beats per bar, Beat and Bus are hidden, as the
  song decides them. Length, grid, instrument and sound FX stay.
- A copy that comes to a song (Embed a copy, a paste from another
  song) takes that song's clock. Made a project asset, it keeps the
  values it had.
- `CreateAssetCommand` takes an owner. `embeddedCopy` takes the song
  now, not its id.
- The cleanup of e2e 65 also takes what the song keeps.

Tests:
- Unit embedded: 18 green (5 new), 6 counter-checks each red
- Unit take, song, pattern and the asset commands: green
- e2e 65 (melektaus-atx): ALL PASSED, 49 checks (2 new)
- Gate (melektaus-atx): CHECK EXIT 0, 269 files, 4172 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:54:40 +02:00
adminandClaude Opus 5.5 23cb34f781 Keep a recorded take inside its song (#355)
A take used to land in the Assets panel as "Song – Take N", next to
every other sound of the project. Now a song can keep a sound, a
pattern or a sound FX to itself: an ordinary asset whose new field
`owner` names the song. Everything that finds an asset by its id
finds it, so playing, the tabs and the game need no change. Only
the places that list assets leave it out.

- `core/music/embedded.ts` holds the rules: which types can be
  embedded, `listedAssets`, the title "Song › Take 1", and the
  rows an embedded sound hides (Loop, Bus).
- One user per embedded asset. `SetSongCommand` settles the song
  after every edit: a place naming an asset an earlier place
  claimed, or one of another song, gets a copy, and what nothing
  names any more goes in the same undo step. The outcome is worked
  out once, so a redo brings back the same copies.
- A split, Ctrl+D and a paste get that for free. The clipboard
  keeps the embedded assets of the copied items, so a paste after a
  delete brings the take back.
- Deleting a song deletes what it keeps. Duplicating it copies
  that and points the copy at the copies. A duplicate of an
  embedded asset is a project asset.
- The item menu has Open, Make project asset and Embed a copy.
- An embedded item carries a bullet in front of its name. The
  first pick was a diamond, which the gate's font test refused:
  the UI font has no glyph for it.
- A take is "Take 1", 2, … numbered per song. With the song gone
  while it uploads, it becomes a project sound under a free name.
- Left out of: the Assets panel, every asset picker, the sound FX
  menu of a track, the test sound of the Sound FX tab and the
  naming of new assets. Delete on an embedded asset only says
  where it lives. A ping lights up its song.
- Loading drops an `owner` whose song is gone, the asset stays as
  a project asset. No migration: older projects have no owner.

Tests:
- Unit embedded (new): 13 green, 6 counter-checks each red
- Unit take, song, song-edit, asset-fx, asset-ref, serialize and
  neighbours: 116 green
- e2e 65 (melektaus-atx): ALL PASSED, 47 checks (1 new)
- Counter-check e2e 65 (asset panel unfiltered): exactly that
  check red
- Gate (melektaus-atx): CHECK EXIT 0, 269 files, 4167 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:25 +02:00
adminandClaude Opus 5.5 7a17030472 Give every sound FX effect a Bypass and a Wet (#354)
A node could only be taken out of a sound FX by unplugging it, and
only six of them could be blended with the sound they get. Every
effect now has both, in the sound FX graph and in the instrument
graph alike. The Mix, the Input and the Output have neither.

- Bypass is a checkbox setting. A bypassed node is left out when
  the graph is compiled, so it costs nothing and holds nothing back.
  The graph draws it faded, and its body says "Bypass: true".
- Wet is a number input like the others. It can be a parameter, be
  moved by Set sound FX parameters and, where the node allows it for
  its other numbers, follow an Envelope.
- A blend is a stage of its own (`wet`) around what the node adds.
  The sound as it came in is held back as far as the node is late,
  so a Compressor, a Limiter or a Vibrato do not comb it out.
- The six nodes that had a Mix keep their key `mix` and only call
  it Wet. Saved values, parameters and edges stay as they were.
- The Delay crossfades now: the sound alone at 0, the sound and its
  echoes in full at 0.5, the echoes alone at 1. Before, the sound
  was always in full and `mix` was the echoes.
- The Pitch takes its semitones by Wet instead of blending, through
  a share `times` on the amount that an instrument binds as well.

Findings:

- A Delay saved before this sounds different. It sounds the same at
  half its old value. The two chip presets with a Delay (Echo Lead,
  Bell) are halved to keep their sound.
- Project cbbf160bb7e8 has two Delays (mix 1 and 0.5). They are
  left as they are.

Tests:

- Unit: soundfx 65 (8 new), sound-library 107 (3 new), instrument
  35 (1 new), envelope 13. Two delay tests and the filter ports test
  follow the new numbers.
- Counter-checks: 8, each turns its test red.
- e2e 34 (melektaus-atx): ALL PASSED, 83 checks (2 new). The
  ping-pong check at wet 0.25 measures what it did at mix 0.5.
- Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4154 tests.
- After the gate the derived ports of a blend are asked for once
  instead of twice: typecheck, lint and the three unit files green
  again (113 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:20:52 +02:00
adminandClaude Opus 5.5 c95fbc167e Ping or open an object's asset from its hierarchy row (#353)
The asset link in the inspector already pings an asset on a click and
opens it on a double-click. The rows of the Scene objects panel only
selected and folded. Now they lead to the asset the same way.

- A click on the row that is selected already pings the object's
  asset in the asset panel. The selection stays. The ping comes on
  release, so picking a row up to drag it pings nothing.
- A double-click or Enter on an object's row opens its asset,
  selected or not.
- ListView has a new onReselect for a plain click on the row that is
  selected alone. onSelect stays quiet then, as nothing changes.

Decided without asking:
- A double-click on an object's row no longer folds it. The chevron
  and the arrow keys still do. A group's row has no asset and keeps
  folding on a double-click.
- A click with Ctrl or Shift, and one that narrows a multi-selection
  to its row, does not ping. Both change the selection.
- A row whose asset is missing pings and opens nothing.

Findings:
- The double-click window goes by time (400 ms), not by the event's
  detail. Two quick click() calls from the harness are a double-click.
  The comment in e2e 01 said otherwise and is gone.

Tests:
- e2e 01 (melektaus-atx): ALL PASSED, 13 checks. The two checks for
  folding on a double-click became one for the ping and two for
  opening. Counter-check without the ping: that check red.
- Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4142 tests.
- Not checked: Enter on a row, a group's row on a double-click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:25:05 +02:00
adminandClaude Opus 5.5 1d53b9b3d0 Count the Bus asset type in e2e 04
The suite job has been red since 3a52f22 (#325), in the runs for
3a52f22, daeffe0 and d349251. Each time e2e 04 failed the same two
checks: every asset type offered once in the + menu, and every type
there with its icon. #325 added the asset type Bus, and the module
still expected 30 types.

- ASSET_TYPE_COUNT is now 31, one per entry of ASSET_TYPES. The menu
  itself was right: each type once, each category sorted, every icon
  there.

Tests:
- e2e 04 (melektaus-atx): ALL PASSED, 63 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:05:01 +02:00
adminandClaude Opus 5.5 a295d233ed Give sounds, instruments, patterns and songs a sound FX (#352)
A sound FX could only be picked where something was played: on a Play
node, a track or an item of a song. A sound that should always sound
through the same effect needed that pick everywhere it was used. Now
the asset says it once, and every place that plays it follows.

- New row Sound FX on a sound, an instrument, a pattern and a song,
  right before Bus. A sound goes through it on every voice, an
  instrument on every note after its graph, a pattern on all its
  notes together and a song on its output.
- The fx input of Play sound, Play note, Play pattern and Play song
  and the Sound FX of a song item now read "From the asset" while
  empty. None is a pick of its own, stored as `#none` like `#master`.
  A pick replaces the FX of the asset, it does not come on top.
- A pattern item's Instrument reads "From the asset" instead of None.
  The meaning is unchanged.
- A double-click on a sound item opens the sound, as one on a pattern
  item opens the pattern.
- The tabs play through the FX of their asset. The Pattern and Song
  tab put it on a bus of their own between the output and the bus of
  the asset. The keys of the Pattern tab play there too.
- Notes of a pattern or song go through the FX of their instrument.
  Its chain is loaded with the other needs of the sequence.

Decided without asking:
- Old items and Play nodes with an empty fx now mean "From the
  asset". No asset has an FX yet, so nothing sounds different.
- The label is "From the asset" as on Loop and Bus, not "From asset".
- A sound used as a Sample, as the impulse of a Reverb or as the test
  sound of the Sound FX tab plays without its own FX.

Findings:
- In e2e, `__gm.inspectorField('Sound FX')` and `('Instrument')` find
  the group of the Assets panel first. The check asks for the last
  match.

Tests:
- Unit asset-fx (new): 8 green. Six counter-checks each turned red.
- Field lists of sound, pattern and song: Sound FX added.
- e2e 65 (melektaus-atx): ALL PASSED, 46 checks (3 new). Counter-check
  without the FX bus of the tab: that check red.
- Gate (melektaus-atx): CHECK EXIT 0, 268 files, 4142 tests.
- Not checked: how it sounds, the Sound, Instrument and Pattern tab
  in the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:02:26 +02:00
adminandClaude Opus 5.5 d349251a01 Put a sound in a room, a hall or a cave with a Reverb node (#328)
game-maker / gate (push) Successful in 4m0s
game-maker / suite (push) Failing after 13m51s
A sound FX had no space around it. The Reverb node folds the sound
with an impulse response: one it makes itself, or a sound asset as the
answer of a real space. It works on a voice and on a bus.

- Type: Room, Hall, Cave and Plate have fixed numbers. Custom shows
  Decay (up to 10 s), Damping and Pre-delay. Sound shows the pick of a
  sound asset and Pre-delay. The rows are derived ports, the sound pick
  an idle param, so they only appear for their type.
- The made impulse response is seeded noise per side, 60 dB down at
  Decay, through a low-pass closing from 20 kHz toward 500 Hz as far
  as Damping says. It is made once per rate, decay and damping, the
  last 16 are kept.
- A sound as impulse response: the sound FX compilers now get the
  project, so a node can turn a sound asset into its file. The build
  key holds that file, so a cut or swapped sound builds again. The
  instrument compiler does the same for a Reverb inside an instrument.
- The library loads that file with the voice, a sequence or before a
  bus builds its chain. A missing file leaves the reverb silent.
- Mix blends dry and reverb and is the only number that moves while
  the sound plays, as the issue said. The voice rings for pre-delay
  plus decay, or as long as its sound.
- The open point from #325 about sends on project buses is #351, not
  part of this.

Tests:
- Unit reverb (new) 4, soundfx 57 (4 new), sound-library 104 (2 new),
  green.
- Counter-checks, each turns one test red: the impulse file not loaded
  with the voice, the tail without the length of the sound.
- e2e 34 (melektaus-atx): ALL PASSED, 81 checks (1 new). Chrome
  offline: silent during the pre-delay, 35.9 dB fall over 0.6 s of a
  1 s decay (36 expected), the dry half at 0.25 for mix 0.5. A sound
  with one sample at 0.2 s gives the impulse back 0.25 s later and
  nowhere else.
- Gate (melektaus-atx) over the end of this series #318 to #328:
  CHECK EXIT 0, 267 files, 4134 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:27:22 +02:00
adminandClaude Opus 5.5 071f194326 Swing the volume or the pitch with Tremolo and Vibrato (#320)
Alarms, sirens and wobbly retro sounds need a volume or a pitch that
swings. Two new nodes do that, on a voice and on a bus alike.

- Tremolo: Rate (0 to 20 Hz), Depth (0 to 100 %), Shape (Sine,
  Triangle, Square, Saw). The volume swings between full and
  1 - depth.
- Vibrato: Rate (0.5 to 20 Hz), Depth (0 to 4 semitones), Shape. The
  shape is the curve of the pitch, a Square jumps between two pitches.
- As the issue decided: two nodes, phase 0 on the first sample of the
  voice, no fade-in time. Both run on the FX worklet, the classes are
  in lfo.ts and tested in Node.
- Deviation from the issue text, as in the plan: the Vibrato is a delay
  that swings, not playbackRate. A bus has no playbackRate, and the
  sound keeps its length.
- The delay swings around a center taken from the start values. The
  center counts as the latency of the node, so a voice starts that
  much earlier and a Mix and a bus make up for it.
- Known limit, in the node text and the wiki: a depth turned up or a
  rate turned down after the start asks for a delay below 0. The lower
  half of the swing then flattens.

Finding: the first e2e run was red at the edge of a window. The phase
is summed up, so the square flips one sample later than 2400 steps of
10/48000 suggest. The check keeps 10 samples away from the edges.

Tests:
- Unit lfo (new) 5, soundfx 53 (2 new), sound-library 102 (1 new),
  green.
- Counter-checks, each turns one test red: the swing turned over (up
  and down swapped), the start time not passed to the worklet.
- e2e 34 (melektaus-atx): ALL PASSED, 80 checks (1 new). Chrome
  offline: Tremolo square silent before its voice, then 0.5 and 0 for
  50 ms each. Vibrato square at 5 Hz, 1 semitone: 1059.5 Hz and
  940.5 Hz.
- The gate runs once on the last commit of this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:13:18 +02:00
adminandClaude Opus 5.5 d3a4cb7d0e Warm up or distort a sound with a Saturation node (#323)
A sound FX had no way to bend a sound. The Saturation node pushes it
into a curve, from warmth to a hard distortion, on a voice or a bus.

- Eight types, each a fixed curve made once: Soft (tanh), Cubic, Hard
  clip, Tube, Fuzz, Foldback, Sine fold and Rectify. The issue asked
  for as many as useful.
- Drive (0 to 48 dB) is a gain in front of the curve. The curve spans
  256 times full scale, so the drive glides like any gain.
- Output and Auto-gain blend as on the Compressor. Auto-gain brings a
  sine at -12 dBFS back to the RMS it came in with.
- Tone is a low-pass after the curve, 1 kHz at 0 to 20 kHz at 1, as
  recommended in the plan. A 10 Hz high-pass takes out the DC the
  lopsided types make.
- Mix blends with the dry sound. The 4x oversampling is 192 samples
  late in Chrome, so the dry way is held back as far. The node counts
  as late by that much for voices, Mixes and buses.
- A test hook gives e2e the auto-gain at a drive.

Tests:
- Unit saturation (new) 5, soundfx 51 (2 new), sound-library 101
  (1 new), green.
- Counter-checks, each turns tests red: no hold-back of the dry way
  (1), the level without auto-gain (2).
- e2e 34 (melektaus-atx): ALL PASSED, 79 checks (1 new). Chrome
  offline: 192 samples late at 48 and at 44.1 kHz. Half dry at 1125 Hz
  0.00 dB. A sine at -12 dBFS driven 24 dB into Soft comes out at
  0.00 dB with full auto-gain.
- The gate runs once on the last commit of this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:04:16 +02:00
adminandClaude Opus 5.5 a645026568 Tame the loudness with a Compressor and a Limiter node (#326)
A sound FX could not control dynamics. A Compressor squeezes the
loudness together, and a Limiter keeps every peak below a line. Both
work on a voice and on a bus, where they tame everything at once.

- Compressor: DynamicsCompressorNode with Threshold, Ratio, Knee,
  Attack and Release, then Makeup and Auto-gain from 0 to 1. After the
  node the sound gets (1 - w) * makeup + w * A dB. A is what the curve
  takes off at 0 dBFS.
- The node adds 0.6 of A by itself, as the spec says. That gain is
  taken back out, so 0 really means no makeup.
- The knee shape is left to the browser by the spec. dynamics.ts ports
  Chromium's curve. It gives the gain Chrome was measured to add at
  four settings to three places.
- Limiter: a class on the FX worklet with Threshold, Ceiling, Release
  and Auto-gain. It looks 5 ms ahead: the smallest gain of the window
  ahead, averaged over the window, then a release. No sample passes the
  lower of threshold and ceiling. Auto-gain raises it toward the
  ceiling.
- Deviation from the plan in the issue: the Limiter has a Threshold as
  well. Without it Auto-gain has nothing to measure against.
- Latency is made up on voices now too. A voice starts as much earlier
  as its stages are late (Compressor 6 ms, Limiter 5 ms). The other
  way of a Mix and the other layers of a note are held back to the
  latest, and a bus counts it into its latency.
- A voice with a Limiter counts as ringing, so the worklet is stopped
  only after what it still holds is out.

Tests:
- Unit dynamics (new) 8, soundfx 49 (3 new), sound-library 100 (4 new),
  green.
- Counter-checks, each turns tests red: no look-ahead in the Limiter
  (3), no taking back of the node's gain (1), a voice not started
  earlier (1), no stage latency on a bus (1), no hold-back in a Mix (1).
- e2e 34 (melektaus-atx): ALL PASSED, 78 checks (1 new). Chrome
  offline: Compressor 288 samples late, Limiter 240. Full scale through
  -12 dB 4:1 hard knee: +0.19 dB with full auto-gain, -8.81 dB without.
  A sine at 2 through the Limiter at -6 dB peaks at -6.00 dB, with
  auto-gain 1 at -1.00 dB.
- The gate runs once on the last commit of this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:57:22 +02:00
adminandClaude Opus 5.5 c99bda6489 Move a sound left or right with a Pan node (#319)
A sound FX had no way to place a sound in the stereo field. The new
Pan node does that on a voice, a bus and a track alike.

- One input, Pan from -1 (left) to 1 (right), 0 by default. Behind it
  is a StereoPannerNode. A typed-in 0 leaves the node out, a pan fed
  from a parameter stays at any value.
- It moves with Set sound FX parameters, and an Envelope can move it
  in an instrument.
- As the issue decided, no pan from the position in Play sound and no
  width control for stereo sounds.

Finding: the issue and the plan said a stereo sound gets a balance.
Web Audio pans stereo differently: the side it goes to stays and the
other side moves over into it. The node description and the wiki say
so, the e2e check measures it.

Tests:
- Unit soundfx 46 (1 new), sound-library 97 (1 new), green.
- Counter-check: a pan from a parameter at 0 left out, 1 test red.
- e2e 34 (melektaus-atx): ALL PASSED, 77 checks (1 new). Chrome
  offline, a mono sine at 0.5 gives 0.5 / 0 at -1 and 0.354 / 0.354
  at 0. A stereo one at 0.5 panned 0.5 gives 0.354 / 0.854.
- The gate runs once on the last commit of this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:47:53 +02:00
adminandClaude Opus 5.5 e1ca6a5f5a Shape a sound with every filter type, a Q and a slope (#318)
The Filter node from #335 only knew low-pass, high-pass and band-pass
and had a Resonance from 0 to 1. It now has every type Web Audio
offers, a Q as a plain factor and a slope up to 48 dB per octave.

- Types: low-pass, high-pass, band-pass, notch, low shelf, high shelf,
  peaking and all-pass. The cutoff row is now labelled Frequency, its
  key stays `cutoff`.
- Q from 0.1 to 30, 0.707 by default, replaces Resonance. Web Audio
  takes the Q of a low- or high-pass in dB, so it is converted when the
  voice is wired. No project used the node.
- Gain in dB is an input only for the shelves and the peak. It is a
  derived port, so the sound FX compiler now resolves derived ports.
- Slope 12, 24 or 48 dB/oct runs 1, 2 or 4 filters in a row. They
  share one cutoff carried by a ConstantSource, only the last has the
  Q. Shelves, peak and all-pass hide the row.
- The Chip Presets get the Q their resonance gave before. Mutate moves
  Q in octaves and gain by up to a dB.
- bindStage in the instrument compiler binds every amount of a stage
  generically, so new stages need no case of their own there.

Finding: FLAT_Q was in dB and the delay damping used it. It is a
factor now, the damping takes it through passQ and is unchanged.

Tests:
- Unit envelope 13, sound-library 96 (2 new), chip-presets 14,
  soundfx 45, all green.
- Counter-checks: the Q on every filter in a row, the gain row for
  every type. Each turns one test red.
- e2e 34 (melektaus-atx): ALL PASSED, 76 checks (1 new). Chrome
  offline, 2 kHz through a low-pass at 1 kHz: -12.37 dB at 12 dB/oct,
  -49.5 dB at 48. A +6 dB peak gives 6.00 dB, a notch -124 dB.
- The gate runs once on the last commit of this series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:43:49 +02:00
adminandClaude Opus 5.5 daeffe0f9d Route the tracks of a song through a graph (#350)
game-maker / gate (push) Successful in 3m47s
game-maker / suite (push) Failing after 14m20s
Every track of a song went straight into its output. Now the Song tab
has a second view, Routing: a graph with a node per track and one
Output, the links between them sends. A track can go into other
tracks, into several at once, and a link has its own volume and the
point of the track it taps.

- The song asset has a graph like an instrument (SongTrack, SongOutput).
  heldSongGraph keeps it in step with the tracks on load and in
  SetSongCommand: a node per track named after it, one output, a new
  track and every track of an old song linked to the output. Links of
  gone tracks, doubled links and links closing a loop are dropped.
- A link carries an optional send (volume in dB, tap). The inspector
  shows Volume and Mode for a picked link of a song: Post volume (Post
  pan), Pre volume (Post FX), Pre volume (Pre FX). SetSendCommand.
- NodeDef has two new flags. `fixed` nodes are never added or deleted
  by hand. Links between `acyclic` nodes may not close a loop, checked
  in canConnect, dropTargetPort and ConnectCommand.
- songBuses gives each track its sends instead of one `into`, and a
  bus `song:<song>:<track>:#items` for its own items. That bus is what
  solo closes on a track that only passes a soloed one on.
- Mute and solo now act on the sends (trackGates). A muted track passes
  nothing on, on any tap. A soloed track is heard with what goes into
  it, through the tracks it goes through, their own items silent.
  Only sends on a way through a soloed track stay open, so a track
  going into it and into the output is heard through it only.
- SoundLibrary: a bus has either `into` or sends. Each send is tap,
  delay, level, target. busLatency takes the latest way (arrivalOf),
  and each send is held back so all ways of a track arrive together
  (sendDelays). Taps are wired again whenever a chain comes or goes.
- The Song tab has Timeline and Routing in its header. The routing
  uses GraphCanvas with an empty palette. A track linked nowhere shows
  "Goes nowhere". Delete takes links, never a track node.
- Game playings route the same way, the output being the master of
  the playing.

Decided without asking: a send ranges -60 to +12 dB like a track,
default 0 dB Post. Mute closes the sends, the track's own level stays.

Found on the way: the inspector built the Mode row with the send as it
was when the rows were made. Setting Volume and then Mode wrote the old
volume back. Both rows now read the current send when they change.

Tests:
- Unit song-routing (new): 13 green. Sound library: 4 new, 94 green.
  11 counter-probes (loop on load, canConnect, solo, solo past the
  soloed track, mute, preFx latency, graph sync, delete guard, delays,
  taps, glide), each red.
- Existing tests adjusted to the new bus names and send shape: song,
  bus-fx, song-play, graph registry, edge description.
- e2e 65 on melektaus-atx: ALL PASSED, 43 checks (5 new). The first
  run caught the inspector bug. Counter-probe with the old closure:
  2 FAILED.
- Gate on melektaus-atx: typecheck, lint, format clean, 2 tests red
  (registry rule, edge shape), fixed and rerun locally: 56 green.
- Not rerun after the per-send solo fix: e2e 65 and the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:18:09 +02:00
adminandClaude Opus 5.5 3a52f2290f Mix sounds through buses and a master (#325)
game-maker / gate (push) Successful in 3m36s
game-maker / suite (push) Failing after 14m30s
Until now every sound went straight to the speakers. There was no
point where all of them came together, so nothing could turn the
music down against the effects, and a compressor or a shared echo
over everything had nowhere to sit. The bus technique of #346 was
there already, only the end of the chain was missing.

- Bus asset (Audio): Volume, Sound FX and Into. Into is empty for
  the master and never offers the bus itself nor a bus that already
  goes into it. The list is worked out when the menu opens, so a
  change made meanwhile counts. A loop that is in a project anyway
  sends the buses on it into the master.
- The master is no asset. Its volume and sound FX are two new rows
  under Master in Project settings, one undo step each. A project
  written before has 1 and none, which is how it sounded.
- Sound, instrument, pattern and song have a row Bus, empty for the
  master. Play sound, Play note, Play pattern and Play song have an
  input bus: From the asset, Master or a bus. Inside a song or a
  pattern only the bus of the song or pattern counts.
- Set bus volume (bus, volume, time, easing) and Bus volume. An
  empty bus is the master. The volume set holds for the whole run,
  across scenes, over that of the asset. GameRun resets it at start
  and end. Set sound FX parameters without a voice now reaches the
  sound FX of the bus picked in its new bus input.
- SoundLibrary: the master is the bus "mix", each bus "mix:<id>".
  Every bus, the playing masters of #346 included, ends there, and
  only the master is heard. setMix sets them up once a context
  exists, so an editor nobody listened to opens no audio device. The
  same mix again changes nothing, which lets the editor hand it over
  on every change of the project.
- The tabs play as the game does: Sound, Sound FX, Instrument,
  Pattern and Song tab through the bus of their asset and the master.
- Pause game does nothing to the buses (the user's answer 4b). A
  game that wants quiet in its pause menu sets a bus volume itself.
- Streaming long music files is #349, split off as asked.

Finding on the way: the Into menu first worked from a list made when
the inspector row was built. After another bus was sent into the
open one, e2e 34 saw it still offered. It is now asked on open.

The SoundLibrary tests expected voices to end at the destination.
They now end in the master, which adds its input, level and pan to
every wiring and two gains to every list. Those expectations are
updated, nothing else in them changed.

Tests:
- Unit buses (new): 15 green. Ten counter-checks, each red on its
  own: loop check, bus picked, reset at run start, bus of Set sound
  FX parameters, master volume in the settings, and in SoundLibrary
  default into the master, voices into the master, run volume over
  the mix, no context before a play, a playing on its bus.
- Unit sound-library: 90 green (5 new). Node catalog: 416 green
  (9 new cases).
- e2e 34 (melektaus-atx): ALL PASSED, 75 checks (3 new). The voice
  of the Sound tab was heard on mix:<bus>, that bus into the other,
  that one at 0.5 into the master. Master volume 0.6 via the dialog,
  undo back to 1.
- Gate (melektaus-atx): CHECK EXIT 0, 262 files, 4072 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:13:32 +02:00
adminandClaude Opus 5.5 a848cf8469 Pick a new asset from category submenus in the + menu
game-maker / gate (push) Successful in 3m48s
game-maker / suite (push) Successful in 15m0s
The + menu was one long list in two groups, placeable types and
library ones. That split is no longer wanted. The menu now lists the
categories of the asset panel, and each opens its types beside it, so
a new asset is filed where the panel shows it.

- Menu learns submenus: an entry with submenu instead of onSelect
  opens a second Menu right of the first, or left of it where the
  canvas ends. Its first entry lines up with the row that opened it.
- A submenu opens on hover or click. Resting on another entry swaps
  or closes it after 150 ms, so a pointer cutting across the rows on
  its way into a submenu does not lose it.
- The row of an open submenu stays lit, and a triangle pointing right
  marks every such row.
- A submenu has no backdrop and no Escape of its own. Popup takes a
  backdrop option for that. The first menu's backdrop and Escape
  close the whole stack, and picking an entry does the same.
- assetMenuCategories replaces assetMenuGroups and takes categories
  and their order from ASSET_GROUPS. The types are sorted by label.
- New test hooks __gm.menuSubmenus() and __gm.addMenuCategory().
  pickAssetType opens the category first, so the modules that pick a
  type through it need no change of their own. Modules 04, 10, 47,
  48, 53 and 54 open the category themselves where they read the menu.

Tests:
- Unit asset-menu: 4 green
- e2e 04: ALL PASSED, 63 checks. New: categories in panel order, each
  type once and sorted, submenu placement, hover swaps the submenu,
  picking closes menu and submenu
- Gate: CHECK EXIT 0, 261 files, 4040 tests
- The other modules that use the + menu did not run here

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:17:21 +02:00
adminandClaude Opus 5.5 5e09ba9a50 Group the asset panel by category, then by type
The asset panel listed one heading per type, 30 of them side by side.
Now the types sit under five categories, and each type heading reads
like its entry in the + menu.

- ASSET_GROUPS replaces ASSET_GROUP_LABELS and gives every type a name
  and a category: Basics, Visuals, Logics, Physics or Audio.
- The categories keep that fixed order. The groups in a category and
  the assets in a group stay sorted by name.
- A category heading folds like a group heading. A project opens with
  every category open and every group folded, as before.
- Selecting an asset unfolds its category and its group. Folding the
  category of the selected asset lets go of the selection.
- The headings are singular now (Scene, Sprite, Sound FX). The old
  plural names only kept them apart from the + menu entries.
- locate looks in the popup layer first. A click by text meant for a
  menu entry hit the heading of the same name, since the panel is drawn
  first. Below an open menu a click only closes the menu anyway.
- New test hook __gm.assetCategories().

Tests:
- Unit asset-groups: 6 green, counter-check (empty categories kept)
  1 red
- e2e 04: ALL PASSED, 63 checks (1 new). Counter-check with locate
  taking the first hit again: 2 red, the click on Behavior created
  no asset
- Gate: CHECK EXIT 0, 261 files, 4040 tests
- The category names got their final plural form after these runs.
  The unit test ran again on them (6 green), e2e 04 did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:59:08 +02:00
adminandClaude Opus 5.5 8154569405 Fix the two checks the full run 304 found red
game-maker / gate (push) Successful in 3m42s
game-maker / suite (push) Successful in 14m23s
The full CI run on 4c7861b had two red checks. Neither showed when
only the touched modules ran, because each needs a module before it.

- e2e 40c-iii (#347): a stroke on a tilemap drew the captions
  "Comment" and "Held" again. They are a note and a group that e2e 36
  leaves in a graph. AnnotationView gave its caption a new TextStyle
  on every redraw, and Pixi draws a text again for a new style. The
  graph redraws on every edit of the project. The caption now keeps
  its style while colour and width stay the same.
- e2e 49a: a plain user deleting a shared file got 404 instead of
  403. Since #344 the DELETE route asked whether the file exists
  before whose it is. It now refuses a stranger first, so another
  folder tells them nothing. The wiki's API page says so.

Tests:
- Unit annotation-view (new): 2 green, counter-check 1 red
- e2e 36,39,40,49 (melektaus-atx): both checks green, 49 then hit a
  harness timeout (CDP Network.enable) once
- e2e 49 alone (melektaus-atx): ALL PASSED - 22 checks
- Gate (melektaus-atx): CHECK EXIT 0, 261 files, 4040 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:26:35 +02:00
adminandClaude Opus 5.5 4c7861b5b8 Keep the frame rate up while painting a big tilemap (#347)
Painting on the 60x60 map of a project with fifty objects dropped the
editor to 20 to 39 fps. A profile of a brush stroke showed four causes,
none of them the brush itself.

- Each frame of a stroke previews the model. The asset and hierarchy
  panels then built every row anew, so every label became a new text
  and a new upload to the GPU. One run spent 46 % of the main thread
  in those uploads. ListView.setItems now keeps a row whose item draws
  the same (sameListItem) and builds only the rows that changed.
- The brush drew the whole scene on every pointer event, on top of the
  preview. Now the preview draws what the stroke changed, once a frame.
  The rectangle tool asks for one drawing a frame for its box.
- An animated tile made the view draw in the tick and the preview drew
  it again in the same frame. animate now asks just before the frame
  is drawn and stays out when the view was drawn since the tick began.
- layoutSymbols ran on every drawing and costs up to the cube of the
  symbols. SymbolLayoutMemo keeps the spots while no anchor moves.
- A painted cell made the map read every "x,y" key again, and once
  more for each of the eight neighbours of every cell. layerCells reads
  a layer once per content into an index by place. layerTiles and the
  blocking cells take their neighbours from there (neighboursIn).
  parseCellKey reads a key without split, with the same results.

The test hook sceneRedraws counts how often the scene view was drawn.

Not done: a painted cell still works out the tiles and blocking cells
of the whole map again, so that part grows with the map.

Tests:
- Brush stroke on a copy of the project (stein2, Chrome): 20 to 39 fps
  before, 47.5 after, 55 to 57 without painting. Labels drawn again
  during the stroke 2012 before, none after. Scene drawings per frame
  1.32 before, 1 after.
- One painted cell, headless: 28 to 16 ms on 60x60, 109 to 49 ms on
  the same map doubled to 120x120.
- frame-preview: 2 new, red with the old pointer redraw.
- tilemap: 3 new (key reading, index, refresh), list-ping 1 new,
  symbol-layout 1 new. 5 single reversals turned each red.
- e2e 39,40 (melektaus-atx): ALL PASSED, 107 checks (1 new, a fast
  stroke: 6 drawings in 6 frames, no label drawn again). With the row
  reuse and the pointer redraw reverted it fails: 24 drawings in
  3 frames, and the panels draw their labels again.
- Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4038 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:54:59 +02:00
adminandClaude Opus 5.5 101b12504e Play a sound backwards with a Reverse switch on the asset (#321)
A sound asset can now play its stretch backwards. The switch sits on
the asset, so every way the sound is heard plays it reversed: Play
sound, a song item, an instrument's Sample and a Pitch in a sound FX.
There is no Reverse node in the sound FX graph. The user dropped it,
because playing backwards needs the whole sound in advance, which a
bus or a Chip does not have.

- The cut gets a `reverse` flag, stored as the asset property
  `reverse` (default off). The cut name ends in `~reverse` when it is
  on, so a reversed sound over the whole file is a cut of its own.
- The handles keep marking the same part of the file. The reversed
  copy is made once, like any cut, and the fades follow what is heard:
  Fade in ramps at the end handle, Fade out at the start handle.
- The Sound tab has a toggle with a left-pointing play triangle. Its
  playhead runs from right to left, and the wave draws the ramps at
  the edges they now belong to. The inspector has a Reverse row.
- The icon is a tab header icon (`HEADER_ICON_SVG`, next to the snap
  magnet), not a top bar icon. The first gate run had it in the top
  bar set, and the test that fixes that set went red.
- A song item draws the waveform of a reversed sound backwards.
  Lengths stay as they are.
- The header label reads "Fade in" instead of "Fade in (ms)" and the
  fade fields are 8 px narrower. With the new button the readout was
  cut to "0.03 s of 0..." at 1280 px, now it shows in full. The
  inspector keeps the unit.

Tests:
- Unit sounds 22, sound-library 85 (1 new each). Counter-checks:
  without turning the samples round, without moving the fade in to
  the end, and without mirroring the peaks, all red.
- e2e 34: ALL PASSED, 72 checks (2 new). The reversed stretch starts
  with the last samples of the forward one, and the playhead ran
  0.75, 0.60, 0.45, 0.30, 0.15 over the whole file.
- Gate: first run red, 1 of 4031 (the top bar icon list). After
  moving the icon, CHECK EXIT 0, 260 files, 4031 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:08:37 +02:00
adminandClaude Opus 5.5 d505f6ed27 Show what the song plays around a pattern as ghost notes (#339)
A bass line is written against the chords it goes with. Until now the
piano roll showed only the pattern itself, so writing a part to fit the
rest of a song meant switching between the Pattern and the Song tab.

- A double-click on a pattern item remembers song, item and the round
  clicked into for that pattern. The place lives in the session, not in
  the project, and goes by the item's id, so moving the item moves the
  ghost notes along.
- Opened from the Assets panel, the pattern shows the item of the last
  double-click. Without one, a pattern that lies in exactly one item of
  all the songs shows that item's first round.
- Ghost notes are the pattern items of the other tracks all along the
  song, and on the own track the items before and after and the other
  rounds of the item. The round edited itself does not count. Sound
  items have no notes.
- A ghost note sits on the row it sounds with: its own transpose, less
  the transpose of the item the pattern was opened from.
- The roll draws them grey, without an edge, under the notes. Resting
  on one names its track and its pattern.
- The header has a Ghosts switch, on by default, and names where they
  come from, e.g. "Tune · Bass · bar 5".
- The time axis pans back to the start of the song, with bar and grid
  lines before the pattern. Fit still frames only the pattern.
- A click or a paste outside the pattern no longer lands. Before, only
  the time left of bar 1 was closed, notes could still be drawn past
  the end, where they are kept but never played.
- The rounds of a pattern item are now worked out in one place,
  itemNotes() in song.ts, which songSequence() and the ghost notes
  share.

Tests:
- Unit ghosts (new): 5 green. Counter-check with the round edited
  not left out and the own transpose not taken off: 2 red.
- Unit piano-roll: 1 new (pan back to the song, lines before bar 1).
  Counter-check without the song's start: red.
- e2e 65 (melektaus-atx): ALL PASSED - 38 checks (3 new). The first
  run was red on the tooltip check: the ghost note it rested on lay a
  fifth up, outside the rows Fit frames. The test now uses a tone.
- e2e 64 (melektaus-atx): ALL PASSED - 30 checks.
- Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4027 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:06:32 +02:00
adminandClaude Opus 5.5 c33d6d98e5 Keep a song item's left edge from snapping on to its own end
Dragging the left edge of a song item to the right snapped to the grid
and to the points where the item's content starts again. One of those
points lies exactly on the item's end. When the edge caught it, the
item shrank to a single tick and showed as a thin white line. The
right edge already refused to snap back on to its own start, the left
edge had no such rule.

- trimDelta: a snapped start on or past the item's end now takes the
  last grid line before the end instead.
- If that grid line lies before the current start, the start stays
  where it is.
- Free trimming with Alt or Shift is unchanged and may still shrink
  the item to one tick.
- Dragging the left edge far past the end now leaves one grid step,
  as the right edge already did, instead of one tick.

Tests:
- song-edit unit tests: 14 green (2 new expectations, 1 changed)
- Counter-check with the rule removed: 2 red (1399 instead of 960,
  1919 instead of 1800)
- Prettier, ESLint, tsc clean

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:00:58 +02:00
adminandClaude Opus 5.5 b519cd192f Send a track, a pattern item or a whole song through a sound FX (#346)
A sound FX could only be put on a single sound. Now a track, a pattern
item, a Play pattern and a Play song send everything they play through
one chain of it, as an insert in a DAW. A Play note gets one of its own.

- Track: an FX box in the head next to M and S opens a menu of the
  project's sound FX, and the track's inspector has the row too.
  Order: items, track FX, volume, pan.
- Pattern item: Sound FX in its inspector. Its notes play on a bus of
  their own into the track's bus.
- Play pattern and Play song take fx and fx params. The chain sits on
  the master of the playing, after the tracks and before the level and
  the fades. Set sound FX parameters reaches it by the playback.
- Play note takes fx and fx params next to the instrument's params.
  The FX parameters go by "fx:" so the two sets stay apart. Set sound
  FX parameters on a note changes a name both have in both.
- A Pitch on a bus is a new PitchShifter: the phase vocoder of the
  PitchPlayer, fed while it plays. It cannot look ahead and is exactly
  one frame late at every pitch, 2048 samples.
- That latency is made up. The other way of a Mix is held back by a
  delay of the same length. Every note and clip starts as much earlier
  as the buses it passes are late. The scheduler plans that much
  further ahead, and the first note waits that much after Play.
- The Song tab waits for the chains before it plays and lets its buses
  go a moment after Stop. A playing in a game lets them go after its
  echoes and what the shifters still hold.
- songBuses moved to its own file. Kept in song.ts it pulled the FX
  compiler into the node registry through an import cycle.

Decisions of the user: one chain for the whole (1a), FX everywhere
asked for (2a), Set sound FX parameters on playbacks (3a), and a
pitch shifter of its own with exact latency compensation for a Pitch
on a bus.

Tests:
- Unit pitch-shifter (new): 11 green. At 0 semitones the output is
  the input 2048 samples later, off by at most 1.2e-7.
- Unit sound-library: 84 green, 8 new. Counter-checks (no early
  start, no Mix delay, chain made every time, tune misses the master)
  each turned their test red.
- Unit bus-fx (new): 6 green, 4 counter-checks red. Scheduler ahead: 1
  new, its counter-check red.
- e2e 65 (melektaus-atx): ALL PASSED, 35 checks, 4 new, first run.
  Chrome at 44.1 kHz: the bus was 0.046440 s late, the note started
  exactly that much early. Offline the shifter moved 1 kHz to 2000 Hz
  and passed a click 2048 samples later.
- Gate (melektaus-atx): CHECK EXIT 0, 259 files, 4021 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:15:52 +02:00
adminandClaude Opus 5.5 01fd6e6464 Swirl and comb a sound with a Phaser and a Flanger node (#341)
sfxr's "phaser" is a comb filter, the sound plus a copy delayed by up
to 2.9 ms. That is a flanger, not the phaser a guitarist knows. Both
effects come in as nodes of their own, so each name fits its sound and
sfxr is covered in full. Both run on the FX worklet like the
Bitcrusher, because a feedback loop in Web Audio costs a render quantum
per round.

- Phaser: first-order allpasses (Stages 2, 4, 6, 8, 12) around
  Frequency, moved by a sine LFO Depth octaves wide at Rate Hz, with
  Feedback and Mix. An Envelope can move Frequency and Mix.
- Flanger: a delay line of Offset ms. Sweep moves it on in ms/s from
  the start of the voice, an LFO adds up to Depth ms. As in sfxr the
  delay is mirrored at 0, and it never goes past 20 ms. Feedback comes
  back exactly one delay later. An Envelope can move Offset and Mix.
- Mix 0 leaves the sound as it is, 1 is half sound and half copy. The
  notches are deepest there and the peaks stay at the sound's level.
  sfxr adds both at full, so its sound comes out 6 dB louder.
- The Flanger's processor gets the start of its voice and begins the
  sweep on that sample. stageNodes takes it as a new argument `at`.
- Explosion gets a Flanger in one draw out of two, with sfxr's offset
  and sweep and no LFO. Offset is offset^2 * 2.89 ms, Sweep is
  ramp^2 * 125 ms/s, signed so the direction holds.
- Randomize draws a Flanger's offset and sweep like sfxr. Its LFO and
  feedback and all of a Phaser are drawn within bounds of their own.
  Mix and Stages stay, as a Bitcrusher's Mix does. Mutate moves them
  too, and a 0 stays 0.
- New instrument preset Phased Pad: saw, Envelope, low-pass, Phaser of
  six stages. The other 14 are unchanged.
- The Bitcrusher's worklet setup is now shared with both nodes
  (workletNodes), its behaviour is unchanged.

Tests:
- Unit phaser (new): 8 green. Counter-check with clamping at 0 instead
  of mirroring and a flipped allpass coefficient: 3 red.
- Unit soundfx, sound-library, chip-presets: 45 / 76 / 14 green.
  Counter-checks: start time not passed on, phaser frequency eased
  linearly, sweep sign dropped, Phaser left out of Mutate, each red.
- Unit core, viewport, graph: 179 files, 2979 tests green.
- e2e 34 (melektaus-atx): ALL PASSED, 70 checks (1 new), first run.
  Chrome offline: 1 kHz notch RMS 0.0000 with two stages, 0.5657
  with four. Flanger echo after 48 samples, swept echo at 533/534.
  About 47 Phasers (12 stages) or 63 Flangers fill the audio thread.
- Gate (melektaus-atx): CHECK EXIT 0, 257 files, 3991 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:11:54 +02:00
adminandClaude Opus 5.5 29b604dc07 Fade a sound's start and end apart, each along its own curve (#345)
A cut sound had one edge fade for both ends, always a straight ramp.
Now its start and its end each have a fade of their own, and each
fade has a curve of its own.

- Fade in (ms) and Fade out (ms) replace Edge fade (ms), 5 ms each by
  default. Like before they apply only to an edge inside the file.
- Fade in curve and Fade out curve each offer Linear (the default),
  Equal power, S-curve, Exponential and Logarithmic. A fade out runs
  its curve backwards.
- The Sound tab's header has a field and a curve dropdown per edge.
  Each dropdown shows its curve as a small picture, rising or falling,
  and names it in its tooltip. With two named dropdowns the header was
  too wide at 1280 px and cut off the readout on the right.
- The inspector shows the same four rows, the curves by name.
- The wave draws each ramp in its curve rather than as a line.
- A cut plays by
  <file>#<start>~<end>~<fadeIn>~<fadeOut>~<curveIn>~<curveOut>.
- No migration, as asked in the issue: the old fade key is no longer
  read, so saved sounds start with 5 ms each and Linear on both.

Tests:
- Unit sounds, sound-library: 96 tests
- Counter-check (fade out along the fade in's curve): 1 red
- e2e 34 (melektaus-atx): ALL PASSED - 69 checks
- Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3978 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:48:59 +02:00
adminandClaude Opus 5.5 6b49a0bea7 Give each account its own folder of uploaded files to delete (#344)
Uploaded images, fonts and sounds belonged to nobody. Every account
saw every file in the pickers, and only an administrator could
delete one, through the API alone. Now every account uploads into a
folder of its own, sees its own files and nobody's, and can delete a
file of its own that no project uses.

- Files are stored as data/<kind>/<folder>/<sha256>.<ext>, and the
  folder is part of asset.file. A name without a folder belongs to
  nobody.
- The folder is a random 12-character name kept in users.json and
  given with the first upload. Renaming an account moves no file, and
  a released game's URLs carry no account name.
- The list shows the caller's own files and nobody's. An
  administrator sees all, each with owner and deletable.
- DELETE takes the name with its folder. It answers 403 unless the
  file is the caller's or the caller is an administrator, and 409
  while a project, snapshot or release of any account names it
  (FileUsage). The 409 names the caller's own projects only.
- The pickers have a Delete button. The first press asks, the second
  deletes, and a refusal shows the server's reason.
- A deleted account's folder stays and only administrators see it.
- tools/migrate-file-owners.php moved every used file into the folder
  of the account using it (a copy each for two accounts) and rewrote
  projects, snapshots and releases. A project's revision goes up, so
  an open editor gets a conflict instead of saving old names back.
- Font families are named after the hash alone, so a folder never
  ends up in a CSS family name.

Run on stein2 on 2026-10-07, backup in
/root/config-backup-20261007/data-before-344: 25 files used,
27 copies, 8 projects, 53 snapshots and 1 release rewritten, 30
unused files stay nobody's. Afterwards all 600 references resolve to
a file in their owner's folder.

Found while building: the Delete button cut its new label to "Del…"
because a button does not measure itself again on a new text. The
picker now lays its button row out again.

Tests:
- PHP files.test.php (new): 46 checks, counter-check 3 red
- Unit core + editor: 220 files, 3403 tests
- e2e 34 (melektaus-atx): ALL PASSED - 67 checks (2 new)
- Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3977 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:25:13 +02:00
adminandClaude Opus 5.5 a2d0a02645 Play tunes on 14 instrument presets next to the sfxr sounds (#329)
game-maker / gate (push) Canceled after 9s
game-maker / suite (push) Canceled after 13s
The Preset menu only had sfxr's effects: each a fixed pitch on every
key that dies away by itself. Now its top half holds instruments to
play melodies, basses and drums on.

- Instruments: Square Lead, Pulse Lead, Echo Lead, Octave Arp, Synth
  Brass, Detuned Saws, String Pad, Organ, Flute, Bell, Pluck, Triangle
  Bass, Pluck Bass and Drum Kit. A line in the menu parts them from the
  seven effects below.
- Their Chips follow the note, and all but Bell, Pluck and the drums
  hold it while the key is held.
- They come out the same every time they are picked. Randomize and
  Mutate make a variant, as before.
- They use what the graph already has: two Chips through a Mix for
  Detuned Saws, String Pad, Organ, Flute and Bell, an Envelope on a
  Filter's cutoff for Brass, Pluck and Pluck Bass, a Pitch for the
  detune and the octave, a Delay for Echo Lead and Bell.
- Drum Kit uses single notes 36 to 46: kick, snare, closed and open
  hat on their General MIDI keys, each ending by itself.
- A small Sketch helper lays the nodes on a grid. The sfxr presets use
  it too and come out as before.
- Numbers keep to the steps of their rows. Pulse Lead's duty is 0.13,
  not 0.125, because the field rounds to 0.01.

Tests:
- Unit chip-presets: 12 green (5 new). Every instrument compiles
  without a problem, doubles its frequency an octave up, holds unless
  struck and is the same on every pick. The drum kit sounds on exactly
  36, 38, 42 and 46. Countercheck with Follow note forced off: red.
- e2e 34 (CI image on stein2, melektaus-atx was off): ALL PASSED,
  65 checks (1 new). A first run had the scheduler's looped-bar check
  red by 10 ticks between two clock reads, which the presets do not
  touch; the second run was green.
- Gate (stein2): CHECK EXIT 0, 256 files, 3974 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:09:37 +02:00
adminandClaude Opus 5.5 f3ae19431a Make an 8-bit sound with a Preset, Randomize or Mutate (#329)
game-maker / gate (push) Successful in 3m25s
game-maker / suite (push) Failing after 14m27s
A Chip could play a sound, but finding a good one meant typing a dozen
numbers. sfxr's buttons do that in one click: a kind of sound drawn
anew every time, a wild draw, or a small nudge. The Instrument tab now
has the same, and plays the result at once.

- Preset menu with Pickup, Laser, Explosion, Powerup, Hit, Jump and
  Blip. It puts a new graph in place of the instrument's, as one undo
  step: Notes for all 128 notes, Chip, Envelope, Filter, Bitcrusher,
  Output. The Chip keeps its own frequency with follow note off, so
  every key plays the same effect, and sustain 0 ends it by itself.
- The presets are sfxr's generators, worked out into Hz, semitones and
  seconds with sfxr's own formulas, in chipPresets.ts. sfxr's envelope
  holds and falls in a line, ours falls quick at first. So the decay
  is three times sfxr's hold and fall.
- Randomize draws every number of the Chips, Envelopes, Filters and
  Bitcrushers anew, as sfxr's randomizer does. Waves, filter types,
  the Bitcrusher's mix and an Envelope's from and to stay.
- Mutate moves each of those numbers a little, one in two. A 0 stays
  0, so nothing that was off comes on.
- An input fed by an edge is left alone by both. Both are grey while
  the graph has none of those nodes.
- Audition, off by default as in the Pattern tab, plays the note last
  played again 150 ms after a change to the graph rests.
- Preset, Randomize and Mutate always play that note, for up to 2 s.
- ReplaceGraphCommand swaps nodes and edges and keeps groups and notes.
- The info on the right of the header kept 220 px and was cut off
  with the four new buttons. 140 px still fit "No sound picked".

Not done: punch, filter sweeps and the phaser of sfxr (#341).

Tests:
- Unit chip-presets (new) 7. Counter-check: a wrong Hz formula and
  fed inputs not left alone turn 3 of them red. Unit tests of the
  instrument tab, graph commands and sound FX nodes: 49 files, 851
  green.
- e2e 34 (melektaus-atx): ALL PASSED, 64 checks (4 new): Preset,
  Randomize and Mutate, Audition, three undos back to the Sample.
- Screenshot with a Laser preset: the header fits at 1280 px, the
  chain is fitted, the Chip is a saw with its duty grey.
- Gate (melektaus-atx): CHECK EXIT 0, 256 files, 3969 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:29:39 +02:00
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 af9b202100 Keep the game's music when Play ends a run in the editor
game-maker / gate (push) Successful in 3m39s
game-maker / suite (push) Failing after 19m26s
A behavior with "Run in editor" kept its world until the frame after
Play. By then the game's On start had already started its pattern or
song, and ending the editor run called stopAll() on the sound library.
The game came up silent. This is what turned the game blocks of e2e
64 and 65 red in suites 542 and 550.

- EditorRun.modeChanged() ends the run the moment the mode leaves
  edit. enterPlay() announces the mode before the game is built, so
  the editor run's sounds stop first and the game's start after.
- The Editor wires the mode event to it instead of invalidate().

Tests:
- e2e 64 starts its game with an idle "Run in editor" behavior in
  the scene. Without the fix it fails like CI did, "game": [] and
  2 FAILED. With it: ALL PASSED, 30 checks (stein2).
- Unit editor-run: 21 green, 1 new.
- Typecheck, ESLint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:52:24 +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 32051b5097 Wrap node descriptions and notes in the inspector
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 16m24s
A node's description and every note in the inspector were one line
cut with an ellipsis. Stop pattern says what it does with no pattern
picked, but only past the cut, so nobody could read it.

- Label takes a wrap option. It breaks its text at the width it gets
  and keeps the margin a single line has, above and below all lines.
- Widget.heightFor(width) gives the height at a width. It is
  measure().h everywhere except where text wraps.
- VStack and SectionStack lay a column out with it, at the width
  each row will get.
- ScrollContainer measures again at the narrower width when its
  scrollbar shows, because the text then takes more lines.
- addNote and the empty inspector's hint wrap.
- The file list's own heightFor(count) is now heightOfRows, so the
  layout does not hand it a width as a row count.

Tests:
- Unit scroll-container: 7 green, 2 new (height for width, again
  beside the scrollbar)
- Unit ui + inspector: 17 files, 336 green (before the 2 new ones)
- e2e 02 (stein2, atx busy with suite 545): ALL PASSED, 29 checks
- Screenshot: Stop pattern shows its description in six lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:54:43 +02:00
adminandClaude Opus 5.5 5fde3e975e Reset a track slider with a double-click in the Song tab
game-maker / gate (push) Successful in 3m30s
game-maker / suite (push) Successful in 16m37s
The heads of the Song tab had four rough spots. Panned down, the
first track had no line above it. The M and S letters sat high in
their buttons. A volume or pan slider could only be dragged back to
the middle by eye. Muting a track paled its volume slider.

- The first track draws a border along its top, in the lanes and in
  its head, like the one below every track.
- M and S sit in the middle of their buttons. A label of the heads
  can now carry a height and is centred in its box.
- A double-click on a head's volume or pan sets it back to 0: the
  volume to 0 dB, the pan to the middle. It is one undo step of its
  own after the step the first click set.
- Mute only lights its own button. The volume slider keeps its
  colour, so a muted head no longer looks out of step.

Tests:
- e2e 65 on melektaus-atx: ALL PASSED, 27 checks (+1)
- typecheck, ESLint, Prettier: clean (also after the mute change)
- screenshot on stein2: border above track 1, M and S with 4 px free
  above and below the letter in a 16 px button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:36:07 +02:00
adminandClaude Opus 5.5 5b96d0b329 Paste under the pointer and pull song items out left in music tabs
game-maker / gate (push) Successful in 3m25s
game-maker / suite (push) Successful in 16m33s
A round of fixes to the Pattern and Song tabs: where a paste lands,
what Shift does on a drag, how far the rows zoom, and how an item of
a song is sized at its ends.

- Shift does what Alt does on a drag: off the grid, on the tick. A
  Shift click on a note or an item still adds or unpicks it. A Shift
  press only decides on letting go: no drag toggles, a drag moves.
- Ctrl+V lands under the pointer. The earliest copy goes on the grid
  line before it, the highest note on its row or the topmost item on
  its track, held to the keyboard or the tracks there are. Off the
  grid or left of bar 1 it lands at the start of playing as before.
- The rows of the piano roll zoom to 64 px instead of 28, the tracks
  of a song to 320 px instead of 160.
- Fit in the Pattern tab zooms the rows as well, so the notes fill
  the height, no longer held to the default of 14 px.
- An item of a song can be pulled out by its left end past the start
  of what it plays, which then repeats. The offset goes round to the
  end of a round. A sound not loaded yet still stops at its start.
- Both ends of an item snap to the start of a round of what it plays
  as well as to the grid, whichever is nearer. The end of a round
  between two ticks is cut to the tick before. A start put within
  half a tick of a round goes right on it, so it plays from the very
  beginning.
- An offset past a whole round is not folded back to 0, so a pattern
  that grows later still plays from where the item said.
- A middle-drag pans past the first and the last row, as it pans
  left of bar 1: the highest note or the first track up to half the
  height down, the other end up to half the height up. The room past
  them is dark in the piano roll and empty in the song.

Tests:
- Unit: song-edit 14, piano-roll 19, song 14 tests. New cases for
  the paste spot, pulling out left, the round snap and Fit.
- e2e 64+65 on melektaus-atx: ALL PASSED - 54 checks (+4). New:
  Shift drag and Shift click, Ctrl+V under the pointer in both tabs,
  Fit zooming the rows, pulling a pattern out left and snapping a
  sound to the end of its second round. e2e 64 zooms the rows out
  again after Fit, so the keys it plays later are on screen.
- Gate on melektaus-atx: 251 files, 3912 tests, CHECK EXIT 0.
- Then the pan down the rows: e2e 64 on melektaus-atx ALL PASSED -
  30 checks. Its pan check now also pans the top row down, half the
  grid's height, and a click above it draws nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:14:28 +02:00
adminandClaude Opus 5.5 a3101c590c Size notes by either end and pan before bar one in music tabs
A round of small fixes to the Pattern and Song tabs: what the ruler
and a pan do in both, and a few things the tabs carried that were in
the way.

- A middle-drag can take bar 1 up to half the grid or lanes in. The
  time before it is drawn dark on the ruler, the grid and the lanes.
  Nothing lands there: a click draws no note, a drop shows no frame.
- A press or drag on the ruler puts the start on the nearest grid
  line, no longer on the one before the pointer.
- A new song has one track instead of four. The "+ Track" row under
  the heads is gone, a head's menu adds tracks above or below. A song
  still keeps at least one, so that menu is always there.
- A note can be sized by its left end too: the start goes to the
  nearest grid line and the end stays put, never before tick 0.
- An end of a note under the pointer lights up and shows a resize
  cursor, as on an item of the Song tab.

Tests:
- Unit: piano-roll, song-edit, song, song-play 52 tests. Two new
  cases for the left end and the pan, the song fixtures add their
  tracks themselves now.
- e2e 64+65 on melektaus-atx: ALL PASSED - 50 checks (+2).
  e2e 65 adds its tracks through the head's menu and clicks the
  ruler three quarters into a bar, which only the nearest line
  passes.
- Gate on melektaus-atx: 251 files, 3909 tests, CHECK EXIT 0.
- A probe shot of the piano roll panned in: the time before bar 1
  dark, the left end of a note lit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:40:13 +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 4ea752feef Draw notes for an instrument in a piano roll and loop them #333
game-maker / gate (push) Successful in 3m22s
game-maker / suite (push) Failing after 13m18s
Notes could only be played one at a time, by a key or a behavior. A
pattern is a stretch of notes for one instrument, drawn in a piano
roll and played in time through the scheduler of #332. Songs (#334)
will lay patterns out on tracks.

- New asset type pattern under the heading Patterns. Instruments are
  listed under Instruments now, since a heading is one per type.
- Notes are a start and a length in ticks, a note number and a
  velocity, kept to their rules on load and in every command. The
  instrument, the length in bars, the grid, the meter and the tempo
  are rows in the inspector. The loop is the asset's own data, the
  whole pattern while none is set.
- Grids from 1/1 to 1/32 and the triplets 1/4T to 1/32T, all whole
  ticks. The meter only moves the bar lines, the notes stay put.
- The tab shows the keys down the left, a ruler with bars, beats,
  loop and start along the top, the notes between them and a
  velocity lane below. Keys the instrument plays nothing for are
  grey, the names of a drum kit stand on its keys.
- A click draws a note on the grid line before the pointer, as long
  as the last one drawn or lengthened and as hard as the last
  velocity set. The body moves a note, the right end lengthens it,
  Alt leaves the grid. A drag on empty grid picks with a band, Shift
  adds, a right-click deletes.
- Keys: Space plays, Delete, Ctrl+C/V/D/A, the arrows by a semitone
  or a grid step and with Shift by an octave or a bar. A paste lands
  where playing starts. Every gesture is one undo step.
- A click on the ruler sets where playing starts, a drag the loop or
  one of its ends, its menu loops a bar or the whole pattern.
- The wheel scrolls, Shift across, on the ruler or with Alt it zooms.
  Ctrl+wheel stays with the browser, Pixi listens passively.
- Play runs the loop through the scheduler. An edit goes to it on
  the next window without a restart, a new loop or tempo counts from
  what is planned. Another pattern shown or a project opened stops it.
- The time axis and the ruler are their own pieces in editor/music
  for the song tab. The rules of the roll are pure functions in
  rollView and rollEdit.

Findings:
- Music.play could start after a stop that came while it was still
  loading its files. A run counter makes the late start give up.
  Music.edit now loads what a new note needs as well.
- A click on an end of the loop would have set the loop again. A
  press there only moves the end once it is dragged.

Tests:
- pattern.test.ts (new, 9) and piano-roll.test.ts (new, 16): model,
  grids, meter, held notes, sequence, commands, loop, round trip,
  drum names, time lines, rows, hits, bands, lane, edit rules.
- e2e 64 (new) on melektaus-atx: ALL PASSED, 20 checks. e2e 4 with
  29 asset types: ALL PASSED, 62 checks.
- Gate on melektaus-atx: CHECK EXIT 0, 247 test files, 3839 tests.
- Counter-checks: the move without its clamp, the sequence without
  the end of the pattern, the duplicate offset without whole grid
  steps each fail 1 test. Without passing edits to the scheduler the
  e2e check for a note drawn while playing fails, the rest passes.
- Probe on stein2: the tab with a drum kit, before and while playing,
  looked at in two screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:06:51 +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 076c564415 Take one sound at the Output of an instrument, mixed by a Mix #331
The Output summed any number of edges as layers. That made it the only
sound input of its kind, and it read as if a note could be several
voices. A note was always one voice, but the graph did not say so.
Now the Output takes one sound like every other sound input, and
several are put together with a Mix in front of it (decided by the
user after #331).

- The Output's input is no longer `many`. A second edge replaces the
  first, as at any data input.
- With only one drum sounding for a note, a Mix is its other side. A
  drum kit keeps the volume of each drum.
- An instrument saved with several edges into the Output keeps them
  but does not compile. The Output says "Takes one sound: put a Mix in
  front of it to play several". No saved project has one: the only
  instrument there (cbbf160bb7e8) has a single edge.
- An Output with nothing in it is a problem now, not silence.
- The compiler gathers only triggers from several edges. The player
  still takes a list of layers, which an instrument fills with one.

Tests:
- instrument.test.ts: drum kit through a Mix, a second edge replacing
  the first, an old Output with two edges, an empty Output (33 tests)
- Counter-check: `many` back on the Output turns 15 tests red, the
  new one about the second edge among them
- e2e 34 on melektaus-atx: ALL PASSED, 47 checks
- Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3789 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:59:52 +02:00