Commit Graph
10 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 23922ee4a2 Show the zoom of the music tabs and the EQ as a transition (#377)
game-maker / gate (push) Successful in 3m58s
game-maker / suite (push) Failing after 14m51s
The zoom transition of #309 left out the Pattern, Song and Sound tabs
and the Parametric EQ's curve. There a wheel step or Fit still jumped,
whatever "Zoom (ms)" in the general settings said.

- axisTween wraps ViewTween for a single axis. The music tabs zoom
  their time across and their rows, tracks or height down apart, so
  each keeps one tween per axis and either goes its own way.
- Fit by its button is shown as a transition too, as in the other
  tabs. The fit on opening a document still jumps.
- A middle-button pan first lands a transition under way, and saving
  a document's view lands it on the step it was heading for.
- The EQ's range is saved on the node at once. While the zoom is on
  its way the curve draws a range of its own and writes nothing.
  Its dB lines are those of the new range, sliding to their place,
  so no line shows a crooked number.

Found on the way, not part of #377:
- CI task 611 for 82e96e6 failed the stream check of e2e 34. The
  renderer had grown by 60.1 MB with nothing decoded. The check
  summed the RSS of every Chrome renderer, and the test Chrome runs
  four of them. It now measures the largest one, the tab's own, and
  writes all of them into the check's detail.

Tests:
- view-tween.test.ts: 18 tests, 2 new. They fail when axisTween
  writes the wrong offset.
- e2e 34, 64, 65 on melektaus-atx: ALL PASSED, 202 checks, 4 new.
  At 1000 ms each zoom is between its two steps a frame after the
  wheel and lands on the step a jump reaches.
- Stream check: the tab's renderer grew by 8.6 MB streamed and by
  66.9 MB decoded.
- Gate on melektaus-atx: CHECK EXIT 0, 280 files, 4321 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:28:50 +02:00
adminandClaude Opus 5.5 82e96e680f Decode a clip from a file of its own, not its whole sound (#349)
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Failing after 14m58s
A clip was cut from its sound's file, so a game decoded the whole file
for it and held it for as long as the page was open. A long recording
played through a few short clips cost the recording. Every clip now has
a copy of its cut as Opus, with Stream on or off, and a game decodes
that instead.

- StreamKeeper makes the copy of each clip whether its sound streams
  or not. Stream only decides whether a sound gets a copy of its own.
- clipCopyTable says which cut decodes from which copy. The sound
  library loads the copy for a clip unless the whole file is decoded
  already. Then the clip is cut from that as before.
- projectFiles decodes the whole file ahead only where something may
  play the sound itself: no clips, a clip without a fitting copy, or
  the sound's id anywhere in an asset that can name one. That last
  test walks the data, not the fields, so no place is missed.
- A copy that cannot be had falls back to the whole file. A library
  that bakes keeps to the whole files, as the bake's fingerprint does.

Tests:
- Gate: CHECK EXIT 0, 280 files, 4319 tests (7 new: streams +4,
  sound-library +3).
- e2e 34: ALL PASSED, 108 checks (2 new). With Stream off a fresh
  library decodes the ten-second reversed clip's copy and not the
  five-minute file. It is the cut made from the original to within
  0.0091 a sample and 0.013 dB in any 20 ms. Once a node plays the
  sound itself, the whole file is decoded ahead again.
- e2e 65: all passed with this change.
- Counter-checks red as they should be: the library ignoring copies,
  the whole file always decoded ahead, clip copies only with Stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:53:49 +02:00
adminandClaude Opus 5.5 2764919bb8 Stream long sounds from an Opus copy instead of decoding them (#349)
A sound was decoded whole as soon as the project opened, in the editor
and in a released game: five minutes of stereo held some 115 MB for as
long as the page was open. A sound with Stream on now plays from a copy
as Opus in Ogg, fetched a piece at a time and decoded a few seconds
ahead by WebCodecs, so a game holds a few megabytes for it.

- The Stream switch is a row of the sound, off by default. The Sound
  tab suggests it from a minute on and says how far the copy has got.
- StreamKeeper makes the copies in the browser, a moment after the
  last change, one at a time: the sound's own and one per clip of its
  cut, fades and Reverse baked in. A copy that is no longer wanted is
  taken off and deleted once the project is saved without it.
- streamTable says which played names stream from which copy. A clip
  without a fitting copy, a Sample and a Reverb impulse stay decoded.
  A sound with Stream on is never decoded ahead.
- The page fetches by Range and takes the whole file from a server
  that ignores it, as the CI's PHP server does. It keeps the bytes
  and the first two seconds decoded, the rest goes four seconds ahead.
- A jump decodes 32 packets before the place first. Measured in
  Chrome, what follows is then the same to the bit as decoding from
  the start. With 4 packets it was 0.06 off.
- The stream-player on the FX worklet reads the samples at the rate
  of the context along a Catmull-Rom curve. What comes late is silent
  and skipped once it is there, so a clip keeps its beat.
- The opening stretch goes with the player as it is made. Sent by
  message it came a few milliseconds late, and 557 samples of the
  attack were lost.
- Pitch on a streamed sound is a shifter after the player, as on a
  bus: the length stays and it is heard 42.7 ms late.
- Bakes of patterns and songs (#340) always stream now.
- The song timeline draws a streamed sound from its copy decoded at
  8 kHz, keeping only the peaks.

Tests:
- Gate: CHECK EXIT 0, 280 files, 4312 tests (31 new: ogg +3,
  stream-queue 6, streams 11, streaming 6, sound-library +5).
- e2e 34: ALL PASSED, 106 checks (7 new). Five minutes of mono,
  4.8 MB as WAV, 3.6 MB as its copy: streamed the renderer grows by
  5.8 MB, decoded by 69.6 MB. Offline at 48 kHz, start, loop seam and
  a reversed clip are the copy decoded whole to the sample (0).
- Counter-checks red as they should be: no pre-roll, no catch-up of
  late samples at the end, no opening stretch, a streamed sound
  decoded ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:27:32 +02:00
adminandClaude Opus 5.5 cf96442835 Keep the Sound tab smooth while it pans (#360)
game-maker / gate (push) Successful in 3m51s
game-maker / suite (push) Successful in 15m55s
Panning the Sound tab drew the whole wave anew on every move, one
filled rectangle per pixel column, and gave the ruler's labels new
texts whenever a tick left the area. At 2560 px across that took the
tab down to 16 fps.

- The wave is drawn half the area's width past either side of it. A
  pan within that moves the drawing. Zoom, height, size or a pan
  past the margin draw it anew.
- The bars of the wave share one fill instead of one fill each.
- A ruler label keeps the second it already shows. Before, a pan
  shifted every label by one and each had to be rendered anew.
- A clip's name is given its style only when the style changes.
- The rule for what a drawing covers is pure in `waveView.ts`
  (`shownSeconds`, `drawnSeconds`, `coversShown`).

Measured with a copy of the user's project, "Take 1" with 4 clips,
240 pan moves with the middle button, headless on stein2:

- 2560x1440: main thread 712 -> 373 ms/s, texts rendered 694 -> 65
- 1280x800: main thread 429 -> 225 ms/s

What is left in the profile is Pixi's render pass and the pointer
events, which hovering pays as well.

Tests:
- Unit wave-view: 12 green (1 new), counter-probe red
- e2e 34 (stein2, melektaus-atx was down): ALL PASSED, 92 checks
  (1 new). Counter-probe with the labels given out by position: red,
  7 labels set anew instead of 1.
- Gate (stein2): 272 files, 4210 tests green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 06:19:05 +02:00
adminandClaude Opus 5.5 95edf9959b Show the clips not picked faint over a sound's wave (#357)
Only the picked clip showed in the wave, between its handles. Where the
other clips lay could only be read from the strip above, so cutting one
next to another meant looking up and down all the time.

- Every clip but the picked one is drawn over the wave as a pale tint
  with a thin edge, as high as the file reaches.
- With the sound picked, all its clips are faint. While a drag spans a
  clip, that one is left out.
- Test hook soundTab.faintClips() names the faint clips.

Tests:
- e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 extended)
- 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:17 +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 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 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 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