Commit Graph
6 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 25084f8576 Refactoring round 10: split SoundLibrary, fold sound duplicates (#85)
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Successful in 14m52s
Refactoring round 10 covers everything since round 9 (808e1ee): 110
commits with about 47 000 new lines, nearly all of them sound, sound
FX, instruments, patterns and songs. Behaviour is unchanged.

Duplicate code (jscpd over src, 50 tokens, TypeScript only, measured
on 808e1ee and here alike: 47 clones / 426 lines before, 33 / 245
after; round 9 itself ended at 22 / 160 by this count):

- PreviewParams (new, editor/soundfx) is the header's parameter bar
  of the Sound FX and the Instrument tab: values, defaults, fields
  and the voices a moved field reaches. Both tabs had it line for
  line.
- fxWorklet: an Effect base runs the bit crusher, phaser, flanger,
  limiter, LFO and bus shifter, a Source base the pitch player and
  the Chip. Start is the one wait for a voice's first sample, which
  four processors wrote out each.
- nodes/shared has checkPort, detachedPort, fxParamsPort and
  letRingPort. The Play and Stop nodes of sounds, notes, patterns and
  songs spelled each port out, up to five times.
- parameterValues builds both Sound FX parameters and Instrument
  parameters, which differed in name and text only.
- SetLoopCommand sets the ruler loop of a pattern or a song. It
  replaces SetPatternLoopCommand and SetSongLoopCommand.
- addLink in music/routing is the one rule for a link that both the
  song routing and the Master routing keep.
- eqCurves works out each band and their sum for the EQ tab, which
  did that in its drawing code. It replaces eqCurveDb, which only a
  test called.
- Smaller ones: the eight neighbour directions of a tile cell, and
  drawFloatingBox for the shadowed box of Popup and FloatWindow.
- Left on purpose: Play pattern and Play song stay two parallel node
  definitions, and the smaller clones under ten lines stay.

Architecture:

- SoundLibrary.ts goes from 2897 to 2188 lines. What a stage of a
  sound FX becomes as Web Audio nodes, and how the knobs of a playing
  voice move, is now viewport/fxNodes.ts. None of it keeps state of
  the library.

Dead code (TS language service, references per export and member):

- soundRefIds, LFO_SHAPE_INDEX, TREMOLO_ROWS, VIBRATO_ROWS and
  REVERB_ROWS had no reader. The four routing node defs are
  registered without an unused exported name.
- soundFileOf and portTypeOf were called by tests only. The tests
  now ask findSound and portOf, which the editor uses. DRY moves
  from src into the one test that uses it.
- 51 values used only in their own file lose their `export`. Types
  and the node catalogues stay exported.
- Kept on purpose: neighboursOf is the cell by cell reading the
  indexed neighboursIn is checked against, and PitchShifter.latency
  is the test's one view of how late the shifter is.

Comments and docs:

- Seven comments told history ("no longer", "any more", "as
  before", "of before #354"). Nine comment blocks ran far past the
  line width. Proof that this changed no code: the bundle was md5
  identical, and the touched tests and e2e modules were identical
  after esbuild.
- README: sound FX, instrument, pattern, song and bus in the asset
  list, the pages Instruments, Patterns and Songs, 1919 checks.
- Wiki: see its own commit.

Tests:

  Gate (melektaus-atx)    274 files, 4236 tests, CHECK EXIT 0
  e2e 34, 64, 65          ALL PASSED, 180 checks
  Unit, touched areas     16 files, 804 tests

No check was added, the e2e sum stays at 1919. Counter-checks, each
put back by copy and compared with cmp:

- addLink without the loop rule: 2 tests fail, one per routing.
- eqCurves with the sum of the last band only: 1 test fails.
- Start in the worklet never waiting: e2e 34 fails in exactly the
  four checks that hang on a voice's first sample (Flanger, Tremolo,
  Chip, a pitched note planned ahead).

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

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

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

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

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

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

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

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

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