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
@@ -124,7 +124,7 @@ import {
|
||||
} from '../music/song';
|
||||
import { DEFAULT_SIGN, DEFAULT_TEMPO, MAX_TEMPO, MIN_TEMPO, SIGN_OPTIONS } from '../music/time';
|
||||
import { ASSET_FX_KEY } from './assetFx';
|
||||
import { BUS_FX_KEY, BUS_INTO_KEY, BUS_KEY, BUS_VOLUME_KEY } from './buses';
|
||||
import { BUS_FX_KEY, BUS_VOLUME_KEY } from './buses';
|
||||
import { Theme } from '../../ui';
|
||||
|
||||
/**
|
||||
@@ -133,9 +133,7 @@ import { Theme } from '../../ui';
|
||||
* produces fractions, and the field rounds the value away rather than hiding it. Without
|
||||
* `assetTypes` an asset picker offers every placeable asset; `none` is what it calls an empty pick,
|
||||
* `master` offers the master bus as a choice of its own, `noFx` "None" as one where empty means
|
||||
* "from the asset" (#352), and `intoFor` names the bus the row is
|
||||
* the Into of, which leaves out that bus and every one going into it when the picker opens (#325);
|
||||
* `uploadedOnly` keeps drawn pictures out
|
||||
* "from the asset" (#352); `uploadedOnly` keeps drawn pictures out
|
||||
* of a texture picker (#186). `renderLayers` makes a layer row pick from the project's render layers
|
||||
* instead of its collision layers (#302), with "Inherit" as the first choice of a single layer.
|
||||
*/
|
||||
@@ -149,7 +147,6 @@ type Extras<K extends FlatKind> = K extends 'number'
|
||||
none?: string;
|
||||
master?: boolean;
|
||||
noFx?: boolean;
|
||||
intoFor?: string;
|
||||
}
|
||||
: K extends 'texture'
|
||||
? { uploadedOnly?: boolean }
|
||||
@@ -485,16 +482,6 @@ const SLICE_SCALE_ROW: PropertyDef = {
|
||||
step: 0.1,
|
||||
};
|
||||
|
||||
/** The bus a sound, an instrument, a pattern or a song is heard on (#325). */
|
||||
const BUS_ROW: PropertyDef = {
|
||||
key: BUS_KEY,
|
||||
label: 'Bus',
|
||||
kind: 'asset',
|
||||
default: '',
|
||||
assetTypes: ['bus'],
|
||||
none: 'Master',
|
||||
};
|
||||
|
||||
/** The sound FX a sound, an instrument, a pattern or a song goes through, before its bus (#352). */
|
||||
const FX_ROW: PropertyDef = {
|
||||
key: ASSET_FX_KEY,
|
||||
@@ -706,12 +693,11 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
{ key: SOUND_VOLUME_KEY, label: 'Volume', kind: 'number', default: 1, min: 0, max: 1, step: 0.05 },
|
||||
{ key: SOUND_LOOP_KEY, label: 'Loop', kind: 'boolean', default: false },
|
||||
FX_ROW,
|
||||
BUS_ROW,
|
||||
],
|
||||
// The effects are the asset's graph (see SoundFxAsset), not a property.
|
||||
soundfx: [],
|
||||
// What a note sounds like is the asset's graph (see InstrumentAsset), not a property.
|
||||
instrument: [FX_ROW, BUS_ROW],
|
||||
instrument: [FX_ROW],
|
||||
// The notes and the loop are intrinsic data on the asset (see PatternAsset). The meter and the
|
||||
// tempo are for drawing and hearing it on its own; a song plays it at its own tempo (#334).
|
||||
pattern: [
|
||||
@@ -744,7 +730,6 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
step: 1,
|
||||
},
|
||||
FX_ROW,
|
||||
BUS_ROW,
|
||||
],
|
||||
// The tracks, items and loop are intrinsic data on the asset (see SongAsset).
|
||||
song: [
|
||||
@@ -766,13 +751,12 @@ const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
options: SONG_GRID_OPTIONS,
|
||||
},
|
||||
FX_ROW,
|
||||
BUS_ROW,
|
||||
],
|
||||
// Where sounds are mixed (#325): how loud, through which sound FX, and on into which bus.
|
||||
// Where sounds are mixed (#325): how loud and through which sound FX. Where it goes on is the
|
||||
// master routing's (#351).
|
||||
bus: [
|
||||
{ key: BUS_VOLUME_KEY, label: 'Volume', kind: 'number', default: 1, min: 0, max: 1, step: 0.05 },
|
||||
{ key: BUS_FX_KEY, label: 'Sound FX', kind: 'asset', default: '', assetTypes: ['soundfx'] },
|
||||
{ ...BUS_ROW, key: BUS_INTO_KEY, label: 'Into' },
|
||||
],
|
||||
// How many particles and for how long; what one of them does is the asset's graph, whose Property
|
||||
// nodes add their rows after these – see `schemaForBag`.
|
||||
@@ -1121,11 +1105,6 @@ function schemaForBag(project: Project, asset: Asset, bag: PropertyBag): Propert
|
||||
// Nothing but declarations: the starting values live in `props` under the very same keys.
|
||||
if (asset.type === 'variables' || asset.type === 'struct') return asset.variables;
|
||||
if (asset.type === 'sprite') return [...FULL_SCHEMAS.sprite, ...shaderParamRows(project, shaderOf(bag))];
|
||||
// A bus never goes into itself, nor into one that already goes into it.
|
||||
if (asset.type === 'bus')
|
||||
return FULL_SCHEMAS.bus.map((def) =>
|
||||
def.key === BUS_INTO_KEY && def.kind === 'asset' ? { ...def, intoFor: asset.id } : def,
|
||||
);
|
||||
// Unlike a sprite's shader, a source's graph is its own asset's, so no id has to be looked up.
|
||||
if (asset.type === 'particles')
|
||||
return sourceRows(
|
||||
|
||||
@@ -1,59 +1,18 @@
|
||||
import { findAssetOf, type Project } from '../model/Project';
|
||||
import type { PropertyBag } from '../model/Properties';
|
||||
import { coerceAs } from './kinds';
|
||||
|
||||
/** The bus a sound, an instrument, a pattern or a song is heard on (#325); empty is the master. */
|
||||
export const BUS_KEY = 'bus';
|
||||
/** How loud a bus is, 0–1. */
|
||||
export const BUS_VOLUME_KEY = 'volume';
|
||||
/** The sound FX all of a bus goes through, before its volume. */
|
||||
export const BUS_FX_KEY = 'fx';
|
||||
/** The bus a bus goes into; empty is the master. */
|
||||
export const BUS_INTO_KEY = 'into';
|
||||
|
||||
/**
|
||||
* What a node picks to name the master where empty means something else: "from the asset" on a
|
||||
* Play node, "no bus" on Set sound FX parameters. No asset id has a `#`.
|
||||
* Play node, "no bus" on Set sound FX parameters. No asset id has a `#`. It is also the id of the
|
||||
* master in the master routing (#351).
|
||||
*/
|
||||
export const MASTER_BUS = '#master';
|
||||
|
||||
export function busOf(bag: PropertyBag): string {
|
||||
return coerceAs(bag[BUS_KEY], 'asset', '');
|
||||
}
|
||||
|
||||
export function busVolumeOf(bag: PropertyBag): number {
|
||||
return Math.max(0, Math.min(1, coerceAs(bag[BUS_VOLUME_KEY], 'number', 1)));
|
||||
}
|
||||
|
||||
/** The bus a bus says it goes into, where that is a bus; '' for the master. */
|
||||
function namedInto(project: Project, busId: string): string {
|
||||
const bus = findAssetOf(project, busId, 'bus');
|
||||
const into = bus ? coerceAs(bus.props[BUS_INTO_KEY], 'asset', '') : '';
|
||||
return into !== '' && findAssetOf(project, into, 'bus') ? into : '';
|
||||
}
|
||||
|
||||
/** Whether following the buses on from `from` comes to `to`. */
|
||||
function reaches(project: Project, from: string, to: string): boolean {
|
||||
const seen = new Set<string>();
|
||||
for (let at = from; at !== '' && !seen.has(at); at = namedInto(project, at)) {
|
||||
if (at === to) return true;
|
||||
seen.add(at);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bus a bus goes into, '' for the master. One that would come round to itself goes into the
|
||||
* master instead, so however a project was written the buses never run in a loop.
|
||||
*/
|
||||
export function busInto(project: Project, busId: string): string {
|
||||
const into = namedInto(project, busId);
|
||||
return into !== '' && reaches(project, into, busId) ? '' : into;
|
||||
}
|
||||
|
||||
/** The buses a bus cannot go into: itself and every one that already goes into it. */
|
||||
export function busesInto(project: Project, busId: string): string[] {
|
||||
return project.assets
|
||||
.filter((asset) => asset.type === 'bus' && reaches(project, asset.id, busId))
|
||||
.map((asset) => asset.id);
|
||||
}
|
||||
Reference in new issue
Block a user