Route sounds, songs and buses in one Master routing graph (#351)
game-maker / gate (push) Successful in 4m20s
game-maker / suite (push) Failing after 15m17s

A project bus could only go into one other bus, and a sound, an
instrument, a pattern or a song into exactly one bus. A shared reverb
needs a sound to go there in part and on dry at the same time. Instead
of a list of sends on every asset, the project gets one routing graph
like the routing of a song (#350), one level up.

- Menu -> Master routing opens a tab with one graph per project. It has
  a node per sound, instrument, pattern, song and bus, named after its
  asset, and one Master. Embedded assets have none, their song routes
  them.
- The nodes follow the assets. Every edit settles the routing in the
  same undo step (KeepRoutingCommand), so an undone delete brings the
  links of its asset back. A new asset gets a node and a link to the
  master. What went only into a deleted bus goes to the master.
- New nodes start in three columns: what plays, the buses, and the
  master a row above, so no link into it runs behind a bus.
- Links are sends: -60 to +12 dB, and a Mode on links out of a bus or a
  song. Out of a sound, an instrument or a pattern a link always takes
  the sound after its volume and FX. Loops are refused.
- A node without a link is silent and says "Goes nowhere".
- The rows Bus and Into are gone. Play nodes with a picked bus still
  overrule the routing.
- A click on a node shows its asset in the inspector, the Master shows
  the master volume and sound FX. A double click opens the asset's tab.
- The graph lives in project.routing and is held to the assets when a
  project is read, but nothing is added there. The editor adds the
  nodes when it opens a project. An asset without a node plays on the
  master, so old projects sound as before without a migration.
- In the mix an asset with one link at 0 dB goes straight into that bus.
  A sound or instrument with more gets a bus of its own with the sends.
  A pattern or song sends from the master of its playing, which is
  where its Mode taps. Buses of the mix can have sends now too.

Found:

- The check "the scheduler plans a looped bar" in e2e 34 wobbles. It
  failed in 2 of 5 runs here and in CI task 580 at 490fcba, before
  this change. HEAD passed it 2 of 2. Not fixed here.

Tests:

- Unit master-routing (new, 10), sound-library (2 new), buses
  rewritten onto the routing, 5 lists without the Bus row. 5 counter
  checks red.
- e2e 34 (melektaus-atx): ALL PASSED, 93 checks (3 new, 2 rewritten).
  Counter check with the Sound tab ignoring the routing: the routing
  check red.
- Gate (melektaus-atx): CHECK EXIT 0, 273 files, 4225 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-09 09:18:46 +02:00
1 parent 8891f027db
commit 0e73fd85b9
46 files changed
+1253 -311

No files matched your search

+11 -5
View File
@@ -31,8 +31,9 @@ import {
songTempo,
SOUND_ITEM_MAX_DB,
} from '../../core/music/song';
import type { SequencePlay } from '../../core/music/playbacks';
import { songBuses } from '../../core/music/songBuses';
import { playedBus, reachedBus } from '../../core/music/mixer';
import { type MixRoute, playedBus, playedRoute, reachedBus } from '../../core/music/mixer';
import { DRY_VOICE, type FxParams, type FxVoice } from '../../core/soundfx/nodes';
import type { Entity } from '../Entity';
import { PhysicsSystem } from '../physics/PhysicsSystem';
@@ -295,7 +296,7 @@ function playSound(
loop ?? loopsOf(props),
owner,
fx ?? DRY_VOICE,
playedBus(project, bus, props),
playedBus(project, bus, ref.asset.id),
);
}
@@ -306,7 +307,7 @@ export function instrumentFx(project: Project, instrumentId: string, picked: str
/** The bus of the mix a note of that instrument is heard on, by the pick of a node (#325). */
export function instrumentBus(project: Project, instrumentId: string, picked: string): string {
return playedBus(project, picked, findAssetOf(project, instrumentId, 'instrument')?.props ?? {});
return playedBus(project, picked, instrumentId);
}
/**
@@ -357,6 +358,11 @@ export function instrumentVoiceName(instrumentId: string): string {
return `instrument:${instrumentId}`;
}
/** Where a playing of a pattern or a song goes: into one bus, or along its links (#351). */
function heardOn(route: MixRoute): Pick<SequencePlay, 'bus' | 'sends'> {
return typeof route === 'string' ? { bus: route } : { sends: route };
}
/**
* A pattern played by a game: its notes through its instrument at its tempo, the whole of it once or
* round and round – the loop set on its ruler is for editing. Its notes take the volume and the owner
@@ -390,7 +396,7 @@ function playPattern(
loop: loop ? { start: 0, end: length } : null,
needs: sequenceNeeds(project, sequence),
...(fx && { fx }),
bus: playedBus(project, bus, asset.props),
...heardOn(playedRoute(project, bus, patternId)),
strike: (note) => strikeNote({ project, audio }, { ...note, volume: level }, owner),
},
owner,
@@ -431,7 +437,7 @@ function playSong(
fadeIn: heldFade(fadeIn),
buses: songBuses(project, song),
...(fx && { fx }),
bus: playedBus(project, bus, song.props),
...heardOn(playedRoute(project, bus, songId)),
strike: (note) => strikeNote({ project, audio }, note, owner),
strikeClip: (clip) => playClip({ project, audio }, clip),
},