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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 17:38:44 +02:00
1 parent 8f9c250eb6
commit f97961c8ff
23 files changed
+1134 -194

No files matched your search

+51
View File
@@ -2133,6 +2133,57 @@ const CASES: Record<string, Case[]> = {
after: (ctx) => expect(ctx.silenced).toEqual([]),
},
],
PlaySong: [
{
name: 'no song is a no-op that still continues the flow',
inputs: { song: '' },
next: 'exec',
outputs: { playback: 0 },
after: (ctx) => expect(ctx.songs).toEqual([]),
},
{
name: 'hands the song on with its volume and fade in, looping and owned unless asked otherwise',
inputs: { song: 'song-1', volume: 0.5, fadeIn: 2, loop: true },
next: 'exec',
outputs: { playback: 1 },
after: (ctx) =>
expect(ctx.songs).toEqual([
{ songId: 'song-1', volume: 0.5, fadeIn: 2, loop: true, detached: false },
]),
},
{
name: 'plays once and detached when asked to',
inputs: { song: 'song-1', volume: 1, fadeIn: 0, loop: false, detached: true },
next: 'exec',
after: (ctx) => expect(ctx.songs[0]).toMatchObject({ loop: false, detached: true }),
},
],
StopSong: [
{
name: 'stops the song it names over its fade out',
inputs: { song: 'song-1', fadeOut: 1.5 },
next: 'exec',
after: (ctx) => expect(ctx.silenced).toEqual(['song song-1 (fade 1.5)']),
},
{
name: 'naming none stops every song',
inputs: { song: '', fadeOut: 0 },
next: 'exec',
after: (ctx) => expect(ctx.silenced).toEqual(['song (fade 0)']),
},
{
name: 'with a playback connected stops only that one, whatever song is named',
inputs: { song: 'song-1', playback: 4, fadeOut: 2 },
next: 'exec',
after: (ctx) => expect(ctx.silenced).toEqual(['playback 4 (fade 2)']),
},
{
name: 'a connected playback of 0 – a Play song that played nothing – stops nothing',
inputs: { song: 'song-1', playback: 0 },
next: 'exec',
after: (ctx) => expect(ctx.silenced).toEqual([]),
},
],
SoundFxParameters: [
{
name: 'bundles its picked parameters as numbers',