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
+27
-12
@@ -1,6 +1,6 @@
|
||||
import type { Application, Ticker } from 'pixi.js';
|
||||
import type { Asset } from '../core/model/Asset';
|
||||
import { findAsset, findObjectHolder } from '../core/model/Project';
|
||||
import { findAsset, findObjectHolder, ROUTING_ID } from '../core/model/Project';
|
||||
import { assetTitle } from '../core/music/embedded';
|
||||
import { mixOf } from '../core/music/mixer';
|
||||
import type { Role } from '../core/store/AuthStore';
|
||||
@@ -94,6 +94,7 @@ import type { PluginManager } from './plugins/loader';
|
||||
import { PluginsPane } from './plugins/PluginsPane';
|
||||
import { ManagementPane } from './users/ManagementPane';
|
||||
import { UsersPane } from './users/UsersPane';
|
||||
import { RoutingEditor } from './routing/RoutingEditor';
|
||||
|
||||
export type CentreTab =
|
||||
| 'scene'
|
||||
@@ -115,6 +116,7 @@ export type CentreTab =
|
||||
| 'tile'
|
||||
| 'container'
|
||||
| 'variables'
|
||||
| 'routing'
|
||||
| 'management';
|
||||
|
||||
/** A tab a plugin added; the prefix keeps it apart from every built-in one. */
|
||||
@@ -123,10 +125,10 @@ export type PluginTabId = `plugin:${string}`;
|
||||
export type TabId = CentreTab | PluginTabId;
|
||||
/**
|
||||
* An open document (#171): an asset by its id, or what a Variables tab shows (#252) by its
|
||||
* `valuesKey` – as many of those as were opened. The user management (#179) is no document, but it
|
||||
* opens and closes like one.
|
||||
* `valuesKey` – as many of those as were opened. The user management (#179) and the master routing
|
||||
* (#351) are no documents, but they open and close like one.
|
||||
*/
|
||||
export type DocumentId = `asset:${string}` | `values:${string}` | 'management';
|
||||
export type DocumentId = `asset:${string}` | `values:${string}` | 'routing' | 'management';
|
||||
/** The account behind this session; the menu for it sits at the far right of the toolbar. */
|
||||
export interface EditorAccount {
|
||||
/** Who is signed in – the menu says so, since the toolbar has no room for a second label. */
|
||||
@@ -144,7 +146,7 @@ export type StripTab = FixedTab | PluginTabId | DocumentId;
|
||||
|
||||
type FixedTab = (typeof FIXED_TABS)[number];
|
||||
/** The kinds whose documents are assets, each shown by one editor that is pointed at them in turn. */
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'variables' | 'management'>;
|
||||
type AssetKind = Exclude<CentreTab, FixedTab | 'variables' | 'routing' | 'management'>;
|
||||
|
||||
const FIXED_TABS = ['scene', 'game'] as const;
|
||||
|
||||
@@ -168,6 +170,7 @@ const PANE_ORDER = [
|
||||
'tile',
|
||||
'container',
|
||||
'variables',
|
||||
'routing',
|
||||
'management',
|
||||
] as const;
|
||||
|
||||
@@ -192,6 +195,7 @@ const TAB_LABELS: Record<CentreTab, string> = {
|
||||
tile: 'Tile',
|
||||
container: 'Container',
|
||||
variables: 'Variables',
|
||||
routing: 'Master routing',
|
||||
management: 'User management',
|
||||
};
|
||||
|
||||
@@ -202,7 +206,7 @@ const FIXED_TAB_ICONS: Partial<Record<FixedTab, string>> = {
|
||||
};
|
||||
|
||||
function isDocument(tab: StripTab): tab is DocumentId {
|
||||
return tab === 'management' || tab.startsWith('asset:') || tab.startsWith('values:');
|
||||
return tab === 'management' || tab === 'routing' || tab.startsWith('asset:') || tab.startsWith('values:');
|
||||
}
|
||||
|
||||
const MIN_LEFT_WIDTH = 160;
|
||||
@@ -245,6 +249,7 @@ export class Editor implements PlayHost {
|
||||
readonly tileEditor: TileEditor;
|
||||
readonly containerEditor: ContainerEditor;
|
||||
readonly variablesEditor: VariablesEditor;
|
||||
readonly routingEditor: RoutingEditor;
|
||||
/** What each open Variables tab shows, by its document. */
|
||||
private readonly valueTargets = new Map<DocumentId, ValuesTarget>();
|
||||
/** The switch per file in `data/plugins/`, in the user management tab (#179). */
|
||||
@@ -335,6 +340,7 @@ export class Editor implements PlayHost {
|
||||
this.tileEditor = new TileEditor(state);
|
||||
this.containerEditor = new ContainerEditor(state);
|
||||
this.variablesEditor = new VariablesEditor(state);
|
||||
this.routingEditor = new RoutingEditor(state);
|
||||
this.hierarchy = new HierarchyPanel(state);
|
||||
this.inspector = new Inspector(state);
|
||||
this.tilemapPanel = new TilemapPanel(state);
|
||||
@@ -383,7 +389,10 @@ export class Editor implements PlayHost {
|
||||
this.managementPane = new ManagementPane(this.usersPane, this.pluginsPane);
|
||||
|
||||
this.toolbar = new Toolbar(state, {
|
||||
openMainMenu: (anchor) => this.projects.openMainMenu(anchor, this.managementEntries()),
|
||||
openMainMenu: (anchor) =>
|
||||
this.projects.openMainMenu(anchor, this.managementEntries(), [
|
||||
{ label: 'Master routing', onSelect: () => this.openDocument('routing') },
|
||||
]),
|
||||
togglePlay: () => (state.mode === 'edit' ? this.play.play() : this.play.stop()),
|
||||
togglePause: () => this.play.togglePause(),
|
||||
openAccountMenu: (anchor) => this.openAccountMenu(anchor, account),
|
||||
@@ -453,6 +462,7 @@ export class Editor implements PlayHost {
|
||||
game: this.gamePane,
|
||||
...this.assetEditors,
|
||||
variables: this.variablesEditor,
|
||||
routing: this.routingEditor,
|
||||
management: this.managementPane,
|
||||
};
|
||||
// Following a call goes on in the same tab: the tab changes its document, not its place.
|
||||
@@ -532,7 +542,8 @@ export class Editor implements PlayHost {
|
||||
// creating it) is for – see the `open` event below.
|
||||
state.on('selection', () => {
|
||||
const s = state.selection;
|
||||
if (s?.kind === 'node' || s?.kind === 'frame' || s?.kind === 'clip' || s?.kind === 'song')
|
||||
if (s?.kind === 'node' && s.assetId === ROUTING_ID) this.openDocument('routing');
|
||||
else if (s?.kind === 'node' || s?.kind === 'frame' || s?.kind === 'clip' || s?.kind === 'song')
|
||||
this.openDocument(`asset:${s.assetId}`);
|
||||
if (s?.kind === 'object' && findAsset(state.project, s.holderId)?.type === 'container')
|
||||
this.openDocument(`asset:${s.holderId}`);
|
||||
@@ -791,6 +802,7 @@ export class Editor implements PlayHost {
|
||||
if (kind && kind in this.assetEditors)
|
||||
this.assetEditors[kind as AssetKind].forgetView(doc.slice('asset:'.length));
|
||||
this.valueTargets.delete(doc);
|
||||
if (doc === 'routing') this.routingEditor.close();
|
||||
if (!wasShowing) return;
|
||||
if (this.documents.active) this.showDocument(this.documents.active);
|
||||
else this.showTab('scene');
|
||||
@@ -807,8 +819,8 @@ export class Editor implements PlayHost {
|
||||
if (doc) this.openDocument(doc);
|
||||
return;
|
||||
}
|
||||
if (tab === 'management') {
|
||||
this.openDocument('management');
|
||||
if (tab === 'management' || tab === 'routing') {
|
||||
this.openDocument(tab);
|
||||
return;
|
||||
}
|
||||
if (tab in this.assetEditors) {
|
||||
@@ -856,7 +868,7 @@ export class Editor implements PlayHost {
|
||||
const target = this.valueTargets.get(doc);
|
||||
return target && valuesTitle(this.state, target) !== null ? 'variables' : null;
|
||||
}
|
||||
if (doc === 'management') return 'management';
|
||||
if (doc === 'management' || doc === 'routing') return doc;
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
const kind = type ? TAB_OF_ASSET[type] : undefined;
|
||||
return kind && kind !== 'scene' ? kind : null;
|
||||
@@ -870,12 +882,14 @@ export class Editor implements PlayHost {
|
||||
if (kind === 'variables' && target) this.variablesEditor.show(target);
|
||||
// Read on every showing: an account may have been changed from another tab in the meantime.
|
||||
if (kind === 'management') void this.usersPane?.reload();
|
||||
if (kind === 'routing') this.routingEditor.open();
|
||||
this.display(kind, doc);
|
||||
}
|
||||
|
||||
/** What a document's tab says: the asset's name, or what a Variables tab shows, as its header says. */
|
||||
private labelOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? TAB_LABELS.management : 'Plugins';
|
||||
if (doc === 'routing') return TAB_LABELS.routing;
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return valuesTitle(this.state, target, this.inspector.liveVariables) ?? '';
|
||||
const asset = findAsset(this.state.project, doc.slice('asset:'.length));
|
||||
@@ -885,6 +899,7 @@ export class Editor implements PlayHost {
|
||||
/** The icon in front of a document's name: its asset type's, as in the asset panel. */
|
||||
private iconOf(doc: DocumentId): string {
|
||||
if (doc === 'management') return this.usersPane ? USERS_ICON_SVG : PLUGINS_ICON_SVG;
|
||||
if (doc === 'routing') return ASSET_ICON_SVG.bus;
|
||||
const target = this.valueTargets.get(doc);
|
||||
if (target) return target.kind === 'list' ? LIST_ICON_SVG : ASSET_ICON_SVG.variables;
|
||||
const type = findAsset(this.state.project, doc.slice('asset:'.length))?.type;
|
||||
@@ -900,7 +915,7 @@ export class Editor implements PlayHost {
|
||||
const kind = this.kindOf(doc);
|
||||
if (!this.tabs.has(doc) && kind)
|
||||
this.tabs.addTab(doc, this.labelOf(doc), base + index, {
|
||||
kind: doc === 'management' ? this.labelOf(doc) : TAB_LABELS[kind],
|
||||
kind: doc === 'management' || doc === 'routing' ? this.labelOf(doc) : TAB_LABELS[kind],
|
||||
icon: this.iconOf(doc),
|
||||
onClose: () => this.closeDocument(doc),
|
||||
});
|
||||
|
||||
@@ -105,9 +105,10 @@ export class ProjectSession {
|
||||
|
||||
/**
|
||||
* The one menu of the top bar (#183): everything that is about the project rather than a scene.
|
||||
* `more` comes last – what the editor adds that is not about the project (#179).
|
||||
* `project` is what the editor adds about it, after its settings (#351); `more` comes last – what
|
||||
* the editor adds that is not about the project (#179).
|
||||
*/
|
||||
openMainMenu(anchor: Widget, more: MenuEntry[] = []): void {
|
||||
openMainMenu(anchor: Widget, more: MenuEntry[] = [], project: MenuEntry[] = []): void {
|
||||
const items: MenuEntry[] = [
|
||||
{ label: 'New project', onSelect: () => void this.run(this.createNext()) },
|
||||
{ label: 'Open project…', onSelect: () => this.openProjectPicker() },
|
||||
@@ -115,6 +116,7 @@ export class ProjectSession {
|
||||
{ label: 'Snapshots…', onSelect: () => this.openSnapshots() },
|
||||
{ label: 'Release…', onSelect: () => this.openRelease() },
|
||||
{ label: 'Project settings…', onSelect: () => this.openProjectSettings() },
|
||||
...project,
|
||||
{ label: 'Clear saved data', onSelect: () => this.clearSavedData() },
|
||||
...more,
|
||||
];
|
||||
|
||||
@@ -13,12 +13,17 @@ import {
|
||||
namesBehavior,
|
||||
SHADER_CALL,
|
||||
} from '../core/graph';
|
||||
import type { GraphAsset } from '../core/model/Asset';
|
||||
import { type Clip, copyGraph, copyObjects, type GraphClip, type ObjectClip } from '../core/model/clipboard';
|
||||
import { findObject } from '../core/model/hierarchy';
|
||||
import { isLiveInstance } from '../core/model/instance';
|
||||
import type { Vec2 } from '../core/model/Properties';
|
||||
import { findGraphAsset, findObjectHolder, type ObjectHolder, type Project } from '../core/model/Project';
|
||||
import {
|
||||
findGraphAsset,
|
||||
findObjectHolder,
|
||||
type GraphHolder,
|
||||
type ObjectHolder,
|
||||
type Project,
|
||||
} from '../core/model/Project';
|
||||
import { GRID_STEP } from './graph/graphView';
|
||||
import { paletteEntries } from './graph/paletteEntries';
|
||||
import { type EditorState, graphSelection, objectSelection } from './state';
|
||||
@@ -158,7 +163,7 @@ export function pasteObjects(
|
||||
}
|
||||
|
||||
/** Whether `asset`'s graph may hold `node`: the palette would offer its kind there. */
|
||||
function fitsGraph(project: Project, asset: GraphAsset, node: GraphNode): boolean {
|
||||
function fitsGraph(project: Project, asset: GraphHolder, node: GraphNode): boolean {
|
||||
return paletteEntries(project, asset).some(
|
||||
(entry) =>
|
||||
entry.def.type === node.type &&
|
||||
@@ -170,7 +175,7 @@ function fitsGraph(project: Project, asset: GraphAsset, node: GraphNode): boolea
|
||||
/** The part of a graph copy `asset` can take, and the nodes it cannot. */
|
||||
function fittingPart(
|
||||
project: Project,
|
||||
asset: GraphAsset,
|
||||
asset: GraphHolder,
|
||||
clip: GraphClip,
|
||||
): { clip: GraphClip; skipped: GraphNode[] } {
|
||||
const nodes = clip.nodes.filter((n) => fitsGraph(project, asset, n));
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||
import { centreOf, type HookContext } from './hookHelpers';
|
||||
|
||||
/** The Master routing tab (#351): its graph, where its nodes, ports and links are, and the buses it makes. */
|
||||
export function routingHooks({ state, editor }: HookContext) {
|
||||
const { routingEditor } = editor;
|
||||
const canvas = routingEditor.canvas;
|
||||
return {
|
||||
routing: {
|
||||
shown: () => editor.tab === 'routing' && canvas.currentAssetId !== '',
|
||||
info: () => routingEditor.shownInfo,
|
||||
graph: () => JSON.parse(JSON.stringify(state.project.routing)) as unknown,
|
||||
portAt: (nodeId: string, port: string, isOutput: boolean) => canvas.portGlobal(nodeId, port, isOutput),
|
||||
nodeAt: (nodeId: string) => canvas.nodeGlobal(nodeId),
|
||||
edgeAt: (edgeId: string) => canvas.edgeGlobal(edgeId),
|
||||
label: (nodeId: string) => canvas.nodeLabel(nodeId),
|
||||
problem: (nodeId: string) => canvas.nodeProblem(nodeId),
|
||||
fitAt: () => centreOf(routingEditor.fitButton),
|
||||
/** The loudest sample on each side of these buses of the mix, tapped on first asking (#359). */
|
||||
peaks: (names: string[]) => Object.fromEntries(soundLibrary.busPeaks(names)),
|
||||
buses: () => soundLibrary.busLevels(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { saturationAutoGain } from '../core/soundfx/saturation';
|
||||
import { curveHooks } from './debug/curveHooks';
|
||||
import { soundHooks } from './debug/soundHooks';
|
||||
import { patternHooks } from './debug/patternHooks';
|
||||
import { routingHooks } from './debug/routingHooks';
|
||||
import { songHooks } from './debug/songHooks';
|
||||
import { documentHooks } from './debug/documentHooks';
|
||||
import { graphHooks } from './debug/graphHooks';
|
||||
@@ -50,6 +51,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
...curveHooks(context),
|
||||
...patternHooks(context),
|
||||
...songHooks(context),
|
||||
...routingHooks(context),
|
||||
...soundHooks(context),
|
||||
...renderHooks(context),
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
objectSignature,
|
||||
refreshObjectSection,
|
||||
} from './objectSection';
|
||||
import { buildRoutingNodeSection, refreshRoutingNodeSection, routingNodeSignature } from './routingSection';
|
||||
import { buildSongSection, refreshSongSection, songSignature } from './songSection';
|
||||
import { ROUTING_ID } from '../../core/model/Project';
|
||||
import { buildTrackSection, refreshTrackSection, trackSignature } from './trackSection';
|
||||
import { INLINE_LIMIT } from '../variables/target';
|
||||
import { inlineListsIn } from '../variables/VariablesEditor';
|
||||
@@ -130,6 +132,7 @@ export class Inspector extends Panel {
|
||||
new Label('Select an asset, a scene object or a node.', { style: 'muted', wrap: true }),
|
||||
);
|
||||
else if (s.kind === 'asset') buildAssetSection(this.host, s.assetId);
|
||||
else if (s.kind === 'node' && s.assetId === ROUTING_ID) buildRoutingNodeSection(this.host, s.nodeId);
|
||||
else if (s.kind === 'node') buildNodeSection(this.host, s.assetId, s.nodeId);
|
||||
else if (s.kind === 'frame') buildFrameSection(this.host, s.assetId, s.frameId);
|
||||
else if (s.kind === 'clip') buildClipSection(this.host, s.assetId, s.clipId);
|
||||
@@ -147,6 +150,8 @@ export class Inspector extends Panel {
|
||||
const s = this.state.selection;
|
||||
if (!s) return '';
|
||||
if (s.kind === 'asset') return assetSignature(this.state.project, s.assetId);
|
||||
if (s.kind === 'node' && s.assetId === ROUTING_ID)
|
||||
return routingNodeSignature(this.state.project, s.nodeId);
|
||||
if (s.kind === 'node') return nodeSignature(this.state.project, s.assetId, s.nodeId);
|
||||
if (s.kind === 'frame') return frameSignature(this.state.project, s.assetId, s.frameId);
|
||||
if (s.kind === 'clip') return clipSignature(this.state.project, s.assetId, s.clipId);
|
||||
@@ -176,23 +181,25 @@ export class Inspector extends Panel {
|
||||
const ok =
|
||||
s.kind === 'asset'
|
||||
? refreshAssetSection(this.host, s.assetId)
|
||||
: s.kind === 'node'
|
||||
? refreshNodeSection(this.host, s.assetId, s.nodeId)
|
||||
: s.kind === 'frame'
|
||||
? refreshFrameSection(this.host, s.assetId, s.frameId)
|
||||
: s.kind === 'clip'
|
||||
? refreshClipSection(this.host, s.assetId, s.clipId)
|
||||
: s.kind === 'shape'
|
||||
? refreshShapeSection(this.host, s.assetId, s.shapeId)
|
||||
: s.kind === 'annotation'
|
||||
? refreshAnnotationSection(this.host, s.assetId, s.annotationId)
|
||||
: s.kind === 'track'
|
||||
? refreshTrackSection(this.host, s.assetId, s.trackId, s.keys)
|
||||
: s.kind === 'edge'
|
||||
? refreshEdgeSection(this.host, s.assetId, s.edgeId)
|
||||
: s.kind === 'song'
|
||||
? refreshSongSection(this.host, s.assetId, s.trackId, s.itemIds)
|
||||
: s.objectIds.length > 1 || refreshObjectSection(this.host, s.holderId, s.objectId);
|
||||
: s.kind === 'node' && s.assetId === ROUTING_ID
|
||||
? refreshRoutingNodeSection(this.host, s.nodeId)
|
||||
: s.kind === 'node'
|
||||
? refreshNodeSection(this.host, s.assetId, s.nodeId)
|
||||
: s.kind === 'frame'
|
||||
? refreshFrameSection(this.host, s.assetId, s.frameId)
|
||||
: s.kind === 'clip'
|
||||
? refreshClipSection(this.host, s.assetId, s.clipId)
|
||||
: s.kind === 'shape'
|
||||
? refreshShapeSection(this.host, s.assetId, s.shapeId)
|
||||
: s.kind === 'annotation'
|
||||
? refreshAnnotationSection(this.host, s.assetId, s.annotationId)
|
||||
: s.kind === 'track'
|
||||
? refreshTrackSection(this.host, s.assetId, s.trackId, s.keys)
|
||||
: s.kind === 'edge'
|
||||
? refreshEdgeSection(this.host, s.assetId, s.edgeId)
|
||||
: s.kind === 'song'
|
||||
? refreshSongSection(this.host, s.assetId, s.trackId, s.itemIds)
|
||||
: s.objectIds.length > 1 || refreshObjectSection(this.host, s.holderId, s.objectId);
|
||||
if (!ok) this.rebuild();
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import { SetSendCommand } from '../../core/commands/songCommands';
|
||||
import {
|
||||
type EdgeSend,
|
||||
findNode,
|
||||
type Graph,
|
||||
type GraphEdge,
|
||||
isStateNode,
|
||||
resolveDef,
|
||||
stateName,
|
||||
} from '../../core/graph';
|
||||
import { routedKind, sendsTapped } from '../../core/music/projectRouting';
|
||||
import { SEND_TAPS, sendOf } from '../../core/music/routing';
|
||||
import { TRACK_MAX_DB, TRACK_MIN_DB } from '../../core/music/song';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
@@ -47,15 +49,18 @@ export interface EdgeDescription {
|
||||
/** What flows through it, as the output names it. */
|
||||
type: string;
|
||||
name: string;
|
||||
/** On a link of a song's routing (#350): what it sends; null on any other link. */
|
||||
/** On a link of a song's routing (#350) or of the master routing (#351): what it sends; null on any other link. */
|
||||
send: EdgeSend | null;
|
||||
/** Whether the link may take the sound anywhere but after the level: not out of a sound, an instrument or a pattern. */
|
||||
tapped: boolean;
|
||||
}
|
||||
|
||||
/** Null when the link is gone. An end whose node or port is gone reads `?`. */
|
||||
export function describeEdge(project: Project, assetId: string, edgeId: string): EdgeDescription | null {
|
||||
const graph = findGraphAsset(project, assetId)?.graph;
|
||||
const holder = findGraphAsset(project, assetId);
|
||||
const graph = holder?.graph;
|
||||
const edge = graph?.edges.find((e) => e.id === edgeId);
|
||||
if (!graph || !edge) return null;
|
||||
if (!holder || !graph || !edge) return null;
|
||||
const lookup = graphLookup(project);
|
||||
const end = (side: GraphEdge['from'], ports: 'inputs' | 'outputs') => {
|
||||
const node = findNode(graph, side.node);
|
||||
@@ -78,10 +83,17 @@ export function describeEdge(project: Project, assetId: string, edgeId: string):
|
||||
input: to.port,
|
||||
type: from.type,
|
||||
name: edge.label ?? '',
|
||||
send: findGraphAsset(project, assetId)?.type === 'song' ? sendOf(edge) : null,
|
||||
send: holder.type === 'song' || holder.type === 'routing' ? sendOf(edge) : null,
|
||||
tapped: holder.type === 'song' || sendsTapped(fromKind(graph, edge)),
|
||||
};
|
||||
}
|
||||
|
||||
/** The asset type a link of the master routing (#351) comes out of; `''` for anything else. */
|
||||
function fromKind(graph: Graph, edge: GraphEdge): string {
|
||||
const node = findNode(graph, edge.from.node);
|
||||
return node ? routedKind(node) : '';
|
||||
}
|
||||
|
||||
/** Structure of a link selection: how many, and for one of them what it joins. */
|
||||
export function edgeSignature(
|
||||
project: Project,
|
||||
@@ -140,15 +152,19 @@ export function buildEdgeSection(
|
||||
onInput: () => {},
|
||||
onChange: (next) => set({ volume: Number(next) }),
|
||||
});
|
||||
host.fields.set(SEND_VOLUME.key, volume);
|
||||
host.body.add(volume.row);
|
||||
if (!d.tapped) {
|
||||
addNote(host, 'In decibels, after the volume and the sound FX of the asset.', 'small');
|
||||
return;
|
||||
}
|
||||
const tap = createFieldRow(host, SEND_TAP, send.tap, {
|
||||
onInput: () => {},
|
||||
onChange: (next) => set({ tap: SEND_TAPS.find((t) => t.value === textOf(next))?.value ?? 'post' }),
|
||||
});
|
||||
host.fields.set(SEND_VOLUME.key, volume);
|
||||
host.fields.set(SEND_TAP.key, tap);
|
||||
host.body.add(volume.row);
|
||||
host.body.add(tap.row);
|
||||
addNote(host, 'In decibels. Mode says where on its track the link takes the sound.', 'small');
|
||||
addNote(host, 'In decibels. Mode says where on its track or bus the link takes the sound.', 'small');
|
||||
} else {
|
||||
fact('Input', d.input);
|
||||
fact('Type', d.type);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
soundEntries,
|
||||
} from '../../core/properties/assetRef';
|
||||
import { NO_FX } from '../../core/properties/assetFx';
|
||||
import { busesInto, MASTER_BUS } from '../../core/properties/buses';
|
||||
import { MASTER_BUS } from '../../core/properties/buses';
|
||||
import { keyName } from '../../core/keys';
|
||||
import { FIRST_TILE_LAYER } from '../../core/model/liveTilemap';
|
||||
import { type ObjectChoice, objectReference, referencedId } from '../../core/model/objectRefs';
|
||||
@@ -403,19 +403,15 @@ const REFERENCES: Record<ReferenceKind, Reference> = {
|
||||
label: assetRefLabel,
|
||||
choices: (project, def) => {
|
||||
const extras = def.kind === 'asset' ? def : undefined;
|
||||
// Asked as the menu opens: another bus may have been sent into this one meanwhile.
|
||||
const left = new Set(extras?.intoFor ? busesInto(project, extras.intoFor) : []);
|
||||
return [
|
||||
...(extras?.master ? [{ id: MASTER_BUS, label: MASTER_LABEL }] : []),
|
||||
...(extras?.noFx ? [{ id: NO_FX, label: NO_ASSET_LABEL }] : []),
|
||||
...assetRefChoices(project, extras?.assetTypes)
|
||||
.filter((asset) => !left.has(asset.id))
|
||||
.flatMap((asset) =>
|
||||
// A sound with its clips (#357), each as "Kick / Clip 1 (sound)".
|
||||
asset.type === 'sound'
|
||||
? soundEntries(asset).map((entry) => ({ id: entry.id, label: `${entry.label} (sound)` }))
|
||||
: [{ id: asset.id, label: assetChoiceLabel(asset) }],
|
||||
),
|
||||
...assetRefChoices(project, extras?.assetTypes).flatMap((asset) =>
|
||||
// A sound with its clips (#357), each as "Kick / Clip 1 (sound)".
|
||||
asset.type === 'sound'
|
||||
? soundEntries(asset).map((entry) => ({ id: entry.id, label: `${entry.label} (sound)` }))
|
||||
: [{ id: asset.id, label: assetChoiceLabel(asset) }],
|
||||
),
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,9 +26,8 @@ import {
|
||||
TRANSITION_PARAM,
|
||||
variablesSourceOf,
|
||||
} from '../../core/graph';
|
||||
import type { GraphAsset } from '../../core/model/Asset';
|
||||
import { graphLookup } from '../../core/model/graphLookup';
|
||||
import { findAssetOf, findGraphAsset, type Project } from '../../core/model/Project';
|
||||
import { findAssetOf, findGraphAsset, type GraphHolder, type Project } from '../../core/model/Project';
|
||||
import { type PropertyValue, textOf } from '../../core/model/Properties';
|
||||
import {
|
||||
isListNode,
|
||||
@@ -106,7 +105,7 @@ function farStateName(graph: Graph, edge: GraphEdge, nodeId: string): string {
|
||||
}
|
||||
|
||||
/** A state machine declares no variables; its one node type has none to name. */
|
||||
const variablesOf = (asset: GraphAsset): PropertyDef[] => (asset.type === 'behavior' ? asset.variables : []);
|
||||
const variablesOf = (asset: GraphHolder): PropertyDef[] => (asset.type === 'behavior' ? asset.variables : []);
|
||||
|
||||
/** Description, settings (params) and the inline values of unconnected data inputs. */
|
||||
export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: string): void {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SetMasterFxCommand, SetMasterVolumeCommand } from '../../core/commands/projectCommands';
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import { textOf } from '../../core/model/Properties';
|
||||
import { MASTER_BUS } from '../../core/properties/buses';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { assetSignature, buildAssetSection, refreshAssetSection } from './assetSection';
|
||||
import { createFieldRow } from './fieldFactory';
|
||||
import { addNote, addTitle, type InspectorHost } from './host';
|
||||
|
||||
const MASTER_VOLUME: PropertyDef = {
|
||||
key: 'master-volume',
|
||||
label: 'Volume',
|
||||
kind: 'number',
|
||||
default: 1,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
};
|
||||
const MASTER_FX: PropertyDef = {
|
||||
key: 'master-fx',
|
||||
label: 'Sound FX',
|
||||
kind: 'asset',
|
||||
default: '',
|
||||
assetTypes: ['soundfx'],
|
||||
};
|
||||
|
||||
/**
|
||||
* A node of the master routing (#351) stands for its asset, and the inspector shows that asset as
|
||||
* the asset panel would; the master shows its volume and sound FX, those of the project settings.
|
||||
*/
|
||||
export function buildRoutingNodeSection(host: InspectorHost, nodeId: string): void {
|
||||
if (nodeId !== MASTER_BUS) return buildAssetSection(host, nodeId);
|
||||
const project = host.state.project;
|
||||
addTitle(host, 'MASTER');
|
||||
const volume = createFieldRow(host, MASTER_VOLUME, project.masterVolume, {
|
||||
onInput: () => {},
|
||||
onChange: (next) => void host.state.execute(new SetMasterVolumeCommand(Number(next))),
|
||||
});
|
||||
const fx = createFieldRow(host, MASTER_FX, project.masterFx, {
|
||||
onInput: () => {},
|
||||
onChange: (next) => void host.state.execute(new SetMasterFxCommand(textOf(next))),
|
||||
});
|
||||
host.fields.set(MASTER_VOLUME.key, volume);
|
||||
host.fields.set(MASTER_FX.key, fx);
|
||||
host.body.add(volume.row);
|
||||
host.body.add(fx.row);
|
||||
addNote(host, 'Where the game is heard. The same as in Project settings.', 'small');
|
||||
}
|
||||
|
||||
export function routingNodeSignature(project: Project, nodeId: string): string {
|
||||
if (nodeId === MASTER_BUS) return 'master';
|
||||
return findAsset(project, nodeId) ? assetSignature(project, nodeId) : 'missing';
|
||||
}
|
||||
|
||||
/** False when the node's asset is gone (rebuild). */
|
||||
export function refreshRoutingNodeSection(host: InspectorHost, nodeId: string): boolean {
|
||||
if (nodeId !== MASTER_BUS) return refreshAssetSection(host, nodeId);
|
||||
const project = host.state.project;
|
||||
host.fields.get(MASTER_VOLUME.key)?.setValue(project.masterVolume);
|
||||
host.fields.get(MASTER_FX.key)?.setValue(project.masterFx);
|
||||
return true;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import { DEFAULT_TEMPO } from '../../core/music/time';
|
||||
import { instrumentFx, playNote } from '../../runtime/systems/behaviorContext';
|
||||
import { Button, Dropdown, Label, Menu, type Widget } from '../../ui';
|
||||
import { Music } from '../../viewport/music';
|
||||
import { playedBus } from '../../core/music/mixer';
|
||||
import { playedRoute } from '../../core/music/mixer';
|
||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||
import { AssetTabEditor } from '../AssetTabEditor';
|
||||
import { claimTransport, releaseTransport } from '../music/transport';
|
||||
@@ -54,10 +54,10 @@ export class PatternEditor extends AssetTabEditor<PatternAsset, PianoRoll> {
|
||||
readonly ghostsButton: Button;
|
||||
/** Where the ghost notes come from: song, track and bar. */
|
||||
readonly ghostSource = new Label('', { style: 'muted' });
|
||||
/** Heard through the sound FX (#352) and on the bus of the asset and the master, as in the game (#325). */
|
||||
/** Heard through the sound FX (#352) and along the routing of the asset (#351), as in the game. */
|
||||
readonly music = new Music(
|
||||
() => this.state.project,
|
||||
() => playedBus(this.state.project, '', this.asset()?.props ?? {}),
|
||||
() => playedRoute(this.state.project, '', this.asset()?.id ?? ''),
|
||||
() => {
|
||||
const asset = this.asset();
|
||||
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), patternTempo(asset.props));
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { masterRoutingProblems } from '../../core/music/projectRouting';
|
||||
import { ROUTING_ID } from '../../core/model/Project';
|
||||
import { Button, Label, Spacer, VStack } from '../../ui';
|
||||
import { GraphCanvas } from '../graph/GraphCanvas';
|
||||
import { SnapToggle } from '../graph/SnapToggle';
|
||||
import type { EditorState } from '../state';
|
||||
import { TabHeader } from '../TabHeader';
|
||||
|
||||
const INFO_WIDTH = 220;
|
||||
|
||||
/**
|
||||
* The Master routing tab (#351), one per project: a node per sound, instrument, pattern, song and bus
|
||||
* and the master, the links between them where each one goes. Edited as the routing of a song is
|
||||
* (#350); a click on a node shows its asset in the inspector, a double click opens it.
|
||||
*/
|
||||
export class RoutingEditor extends VStack {
|
||||
readonly canvas: GraphCanvas;
|
||||
readonly fitButton: Button;
|
||||
readonly snap: SnapToggle;
|
||||
private readonly info = new Label('', { style: 'small', align: 'right' });
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super();
|
||||
this.canvas = new GraphCanvas(state, 'routing');
|
||||
this.snap = new SnapToggle(() => this.canvas);
|
||||
this.fitButton = new Button('Fit', { onClick: () => this.canvas.fit() });
|
||||
this.info.minWidth = INFO_WIDTH;
|
||||
const header = new TabHeader();
|
||||
for (const widget of this.snap.widgets) header.add(widget);
|
||||
header.add(this.fitButton);
|
||||
header.add(new Spacer(), 1);
|
||||
header.add(this.info);
|
||||
this.add(header);
|
||||
this.add(this.canvas, 1);
|
||||
state.on('project', () => this.refresh());
|
||||
state.on('load', () => this.close());
|
||||
}
|
||||
|
||||
/** Shows the routing of the project; framed the first time, as it was left after that. */
|
||||
open(): void {
|
||||
this.canvas.show(ROUTING_ID);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** The tab is closed: opened again, the routing starts from a fitted view. */
|
||||
close(): void {
|
||||
this.canvas.show('');
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/** The readout: how many links, and how many nodes go nowhere and are never heard. */
|
||||
get shownInfo(): string {
|
||||
return this.info.text;
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
if (this.canvas.currentAssetId !== ROUTING_ID) return this.info.setText('');
|
||||
const graph = this.state.project.routing;
|
||||
const silent = Object.keys(masterRoutingProblems(graph)).length;
|
||||
const links = `${graph.edges.length} link${graph.edges.length === 1 ? '' : 's'}`;
|
||||
this.info.setText(silent > 0 ? `${links} · ${silent} going nowhere` : links);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import { assetFxOf } from '../../core/properties/assetFx';
|
||||
import type { SoundStore } from '../../core/store/SoundStore';
|
||||
import { Button, Dialog, Dropdown, Label, Menu, ui, VStack, type Widget } from '../../ui';
|
||||
import { Music } from '../../viewport/music';
|
||||
import { playedBus } from '../../core/music/mixer';
|
||||
import { playedRoute } from '../../core/music/mixer';
|
||||
import { soundLibrary } from '../../viewport/SoundLibrary';
|
||||
import {
|
||||
type InputDevice,
|
||||
@@ -92,10 +92,10 @@ export class SongEditor extends AssetTabEditor<SongAsset, SongTimeline> {
|
||||
readonly snap = new SnapToggle(() => this.routing);
|
||||
private shown: SongView = 'timeline';
|
||||
private readonly pane = new VStack();
|
||||
/** Heard through the sound FX (#352) and on the bus of the asset and the master, as in the game (#325). */
|
||||
/** Heard through the sound FX (#352) and along the routing of the asset (#351), as in the game. */
|
||||
readonly music = new Music(
|
||||
() => this.state.project,
|
||||
() => playedBus(this.state.project, '', this.asset()?.props ?? {}),
|
||||
() => playedRoute(this.state.project, '', this.asset()?.id ?? ''),
|
||||
() => {
|
||||
const asset = this.asset();
|
||||
return asset && fxVoiceOf(this.state.project, assetFxOf(asset.props), songTempo(asset.props));
|
||||
|
||||
@@ -152,8 +152,8 @@ export class SoundEditor extends AssetTabEditor<SoundAsset, WaveCanvas> {
|
||||
this.stop();
|
||||
claimTransport(this.halt);
|
||||
const props = resolveAssetProps(this.state.project, asset);
|
||||
// Through its sound FX (#352), its bus and the master, as the game hears it (#325).
|
||||
const bus = playedBus(this.state.project, '', props);
|
||||
// Through its sound FX (#352) and along its routing (#351), as the game hears it.
|
||||
const bus = playedBus(this.state.project, '', asset.id);
|
||||
const fx = fxVoiceOf(this.state.project, assetFxOf(props), DEFAULT_TEMPO) ?? DRY_VOICE;
|
||||
const file = playedSoundOf({ asset, clip });
|
||||
this.voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, bus);
|
||||
|
||||
@@ -106,8 +106,8 @@ export class SoundFxEditor extends AssetTabEditor<SoundFxAsset, GraphCanvas> {
|
||||
if (!sound?.asset.file || !plan) return;
|
||||
const props = resolveAssetProps(this.state.project, sound.asset);
|
||||
const fx = { plan, params: { ...this.values } };
|
||||
// On the bus of the sound, as the game would play it there (#325).
|
||||
const bus = playedBus(this.state.project, '', props);
|
||||
// Along the routing of the sound, as the game would play it there (#351).
|
||||
const bus = playedBus(this.state.project, '', sound.asset.id);
|
||||
const file = playedSoundOf(sound);
|
||||
const voice = soundLibrary.play(file, volumeOf(props), false, PREVIEW_OWNER, fx, bus);
|
||||
if (voice) this.voices.push(voice);
|
||||
|
||||
+5
-1
@@ -28,6 +28,8 @@ import type { GameScale, PlayFrom } from '../core/model/playSettings';
|
||||
import type { PlaceableAssetType } from '../core/model/AssetTypes';
|
||||
import type { VisibilityPart } from '../core/model/sceneVisibility';
|
||||
import type { ValuePath } from '../core/properties/valuePath';
|
||||
import { KeepRoutingCommand } from '../core/commands/routingCommands';
|
||||
import { settleRouting } from '../core/music/projectRouting';
|
||||
|
||||
export type Selection =
|
||||
| { kind: 'asset'; assetId: string }
|
||||
@@ -310,6 +312,8 @@ export class EditorState {
|
||||
this.paused = false;
|
||||
this.project = project;
|
||||
this.revision = revision;
|
||||
// Every sound, instrument, pattern, song and bus has its node in the Master routing (#351).
|
||||
settleRouting(project);
|
||||
this.commands = new CommandStack(project);
|
||||
this.selection = null;
|
||||
this.openedValues = null;
|
||||
@@ -409,7 +413,7 @@ export class EditorState {
|
||||
*/
|
||||
execute(command: Command): boolean {
|
||||
return this.runEdit(() => {
|
||||
this.commands.execute(command);
|
||||
this.commands.execute(new KeepRoutingCommand(command));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user