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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-08 11:32:41 +02:00
1 parent 49669017b1
commit 668682c4ff
11 files changed
+339 -37

No files matched your search

+56
View File
@@ -1129,6 +1129,62 @@ export async function song(t) {
JSON.stringify((await model()).items.map((item) => item.id)),
);
// 65p. A sound FX of the track's own (#355), from the menu of its FX button: it lets the sound
// through, is opened in its tab and named there with a mark. Picking another lets it go, and
// a Ctrl+Z brings it back.
const fxTrackBefore = (await model()).tracks[0].fx;
p = await json("__gm.song.headAt(0, 'fx')");
await click(p.x, p.y);
await sleep(150);
const createRows = await json('__gm.menuRows()');
await clickText('Create sound FX');
await sleep(250);
const ownFxId = JSON.stringify((await model()).tracks[0].fx);
const ownFx = await json(
`(() => { const a = __gm.state.project.assets.find((x) => x.id === ${ownFxId}); return a ? { name: a.name, owner: a.owner, type: a.type, nodes: a.graph.nodes.map((n) => n.type), edges: a.graph.edges.length } : null; })()`,
);
const ownFxTab = await json(
`{ tab: __gm.tab(), title: __gm.tabRow().labels['asset:' + ${ownFxId}], row: __gm.assetRow(${ownFxId}) }`,
);
check(
"Create sound FX on a track's FX button: one of the song's own, Input to Output, opened in its tab",
createRows?.includes('Create sound FX') &&
ownFxId !== JSON.stringify(fxTrackBefore) &&
ownFx?.type === 'soundfx' &&
ownFx.owner === id &&
ownFx.name === 'Sound FX 1' &&
JSON.stringify(ownFx.nodes) === JSON.stringify(['SoundFxInput', 'SoundFxOutput']) &&
ownFx.edges === 1 &&
ownFxTab.title === `${songName} › Sound FX 1` &&
ownFxTab.row === null,
JSON.stringify({ createRows, ownFx, ownFxTab }),
);
await evaluate(`__gm.state.openAsset(${JSON.stringify(id)})`);
await sleep(200);
p = await json("__gm.song.headAt(0, 'fx')");
await click(p.x, p.y);
await sleep(150);
const ownRows = await json('__gm.menuRows()');
await clickText('No sound FX');
await sleep(150);
const letGo = await json(`__gm.state.project.assets.some((a) => a.id === ${ownFxId})`);
await press('z', { ctrl: true });
await sleep(150);
const backAgain = await json(
`{ fx: __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)}).tracks[0].fx, owner: __gm.state.project.assets.find((a) => a.id === ${ownFxId})?.owner }`,
);
check(
'its menu names it with Edit and Make project asset, No sound FX lets it go, Ctrl+Z brings it back',
ownRows?.includes('• Sound FX 1') &&
ownRows.includes('Edit sound FX') &&
ownRows.includes('Make project asset') &&
!ownRows.includes('Create sound FX') &&
letGo === false &&
JSON.stringify(backAgain.fx) === ownFxId &&
backAgain.owner === id,
JSON.stringify({ ownRows, letGo, backAgain }),
);
// 65j. The step takes back everything it made.
await evaluate('__gm.sound.stopAll()');
await mutate(`