Route sounds, songs and buses in one Master routing graph (#351)
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:
1 parent
8891f027db
commit
0e73fd85b9
46 files changed
+1253
-311
No files matched your search
@@ -26,14 +26,16 @@ import {
|
||||
soundFxParamsMismatch,
|
||||
startStateOf,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import { particleLibrary } from '../../viewport/ParticleLibrary';
|
||||
import { shaderLibrary } from '../../viewport/ShaderLibrary';
|
||||
import { soundFxBuildFor } from '../../core/soundfx/compile';
|
||||
import { instrumentCheck, instrumentSourcesOf } from '../../core/instrument/compile';
|
||||
import { masterRoutingProblems } from '../../core/music/projectRouting';
|
||||
import { routingProblems } from '../../core/music/routing';
|
||||
import { findAsset, findGraphAsset } from '../../core/model/Project';
|
||||
import { ROUTED_ASSET } from '../../core/music/routingNodes';
|
||||
import { hasAssetEditor } from '../assetTabs';
|
||||
import { findAsset, findGraphAsset, type GraphHolder } from '../../core/model/Project';
|
||||
import { type Trace, TRACE_FADE } from '../../runtime/systems/BehaviorSystem';
|
||||
import { ViewTween } from '../viewTween';
|
||||
import { BeadClock } from './beads';
|
||||
@@ -281,7 +283,7 @@ export class GraphCanvas extends Widget {
|
||||
return { w: Math.max(this.minWidth, 200), h: Math.max(this.minHeight, 200) };
|
||||
}
|
||||
|
||||
private asset(): GraphAsset | null {
|
||||
private asset(): GraphHolder | null {
|
||||
return (this.assetId && findGraphAsset(this.state.project, this.assetId)) || null;
|
||||
}
|
||||
|
||||
@@ -316,7 +318,9 @@ export class GraphCanvas extends Widget {
|
||||
? instrumentCheck(asset, instrumentSourcesOf(this.state.project)).problems
|
||||
: asset.type === 'song'
|
||||
? routingProblems(asset.graph)
|
||||
: null;
|
||||
: asset.type === 'routing'
|
||||
? masterRoutingProblems(asset.graph)
|
||||
: null;
|
||||
for (const node of asset.graph.nodes) {
|
||||
const resolved = resolveDef(node, asset.graph, lookup);
|
||||
if (!resolved) continue;
|
||||
@@ -429,7 +433,7 @@ export class GraphCanvas extends Widget {
|
||||
* one held by the selected object's instance. Without a selection nothing is strong – that keeps
|
||||
* "many instances, many states" readable instead of picking one of them at random.
|
||||
*/
|
||||
private stateMarks(asset: GraphAsset): Map<string, 'held' | 'current'> {
|
||||
private stateMarks(asset: GraphHolder): Map<string, 'held' | 'current'> {
|
||||
const marks = new Map<string, 'held' | 'current'>();
|
||||
if (asset.type !== 'statemachine') return marks;
|
||||
const selection = this.state.selection;
|
||||
@@ -665,6 +669,13 @@ export class GraphCanvas extends Widget {
|
||||
}
|
||||
|
||||
private openCalledBehavior(node: GraphNode): boolean {
|
||||
// A node of the master routing (#351) stands for its asset; a bus has no tab, its node shows it already.
|
||||
if (node.type === ROUTED_ASSET) {
|
||||
const asset = findAsset(this.state.project, node.id);
|
||||
if (!asset || !hasAssetEditor(asset.type)) return false;
|
||||
this.state.openAsset(node.id);
|
||||
return true;
|
||||
}
|
||||
if (node.type === SHADER_CALL || node.type === INSTRUMENT_SOUND_FX) {
|
||||
const shader = node.type === SHADER_CALL;
|
||||
const id = textOf(node.params[shader ? FUNCTION_PARAM : 'fx'] ?? '');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AddNodeCommand } from '../../core/commands/graphCommands';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import type { GraphHolder } from '../../core/model/Project';
|
||||
import type { Box } from '../framing';
|
||||
import type { EditorState } from '../state';
|
||||
import type { NodeView } from './NodeView';
|
||||
@@ -11,7 +11,7 @@ export interface GraphCanvasHost {
|
||||
readonly state: EditorState;
|
||||
/** The asset on the canvas; empty when there is none. */
|
||||
assetId(): string;
|
||||
asset(): GraphAsset | null;
|
||||
asset(): GraphHolder | null;
|
||||
views(): ReadonlyMap<string, NodeView>;
|
||||
/** Views in stacking order, topmost first – a node drawn over another one wins the pointer. */
|
||||
topDownViews(): Iterable<NodeView>;
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
STATE_NAME_PARAM,
|
||||
terminalNode,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset, ShaderAsset, ShaderFunctionAsset } from '../../core/model/Asset';
|
||||
import type { ShaderAsset, ShaderFunctionAsset } from '../../core/model/Asset';
|
||||
import { FIND, FIND_MODE } from '../../core/graph/findBlock';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import type { PropertyBag } from '../../core/model/Properties';
|
||||
import type { Project } from '../../core/model/Project';
|
||||
import type { GraphHolder, Project } from '../../core/model/Project';
|
||||
import {
|
||||
isParticlePropertyNode,
|
||||
PARTICLE_PROPERTY,
|
||||
@@ -132,14 +132,14 @@ function shaderEntries(project: Project, asset: ShaderAsset | ShaderFunctionAsse
|
||||
* generic "Run behavior" – and once more as the event "On <name>" if it has an End to be heard
|
||||
* through (#294) –, and the two terminals drop out once the graph has them.
|
||||
*/
|
||||
export function paletteEntries(project: Project, asset: GraphAsset): PaletteEntry[] {
|
||||
export function paletteEntries(project: Project, asset: GraphHolder): PaletteEntry[] {
|
||||
if (asset.type === 'statemachine') {
|
||||
const params = { [STATE_NAME_PARAM]: nextStateName(asset.graph) };
|
||||
return [{ id: State.type, hint: State.category, def: State, params }];
|
||||
}
|
||||
if (asset.type === 'shader' || asset.type === 'shaderfunction') return shaderEntries(project, asset);
|
||||
// A song's routing holds the nodes of its tracks and its output, which come and go on their own (#350).
|
||||
if (asset.type === 'song') return [];
|
||||
// A routing holds nodes that come and go on their own: a song's tracks (#350), the assets (#351).
|
||||
if (asset.type === 'song' || asset.type === 'routing') return [];
|
||||
if (asset.type === 'soundfx') {
|
||||
const placed = new Set(asset.graph.nodes.map((n) => n.type));
|
||||
return SOUND_FX_NODES.filter(
|
||||
|
||||
Reference in new issue
Block a user