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:
1 parent
49669017b1
commit
668682c4ff
11 files changed
+339
-37
No files matched your search
@@ -42,6 +42,7 @@ import { type Command, CommandError } from './Command';
|
||||
import { nextAssetName } from './naming';
|
||||
import { requireAsset, requirePropDef, requireStateMachine, requireVariablesAsset } from './require';
|
||||
import { SetBagValueCommand } from './SetBagValueCommand';
|
||||
import { SetSongCommand } from './songCommands';
|
||||
|
||||
export function buildAsset(type: AssetType, name: string, id = newId()): Asset {
|
||||
const base = { id, name, props: initialPropsFor(type) };
|
||||
@@ -539,9 +540,13 @@ export class SetMachineStartCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** An embedded pattern follows the tempo and the meter of its song (#355), in the same undo step. */
|
||||
/**
|
||||
* An embedded pattern follows the tempo and the meter of its song (#355), in the same undo step. A
|
||||
* song that picks another sound FX lets go of the one it kept to itself, in that step too.
|
||||
*/
|
||||
abstract class AssetPropertyCommand extends SetBagValueCommand<Asset> {
|
||||
private followed: { id: string; previous: PropertyValue | undefined }[] = [];
|
||||
private settled: SetSongCommand | null = null;
|
||||
|
||||
protected constructor(
|
||||
private readonly assetId: string,
|
||||
@@ -559,9 +564,13 @@ abstract class AssetPropertyCommand extends SetBagValueCommand<Asset> {
|
||||
setBagValue(pattern.props, this.key, asset.props[this.key]);
|
||||
return { id: pattern.id, previous };
|
||||
});
|
||||
this.settled =
|
||||
asset.type === 'song' && this.key === ASSET_FX_KEY ? new SetSongCommand(asset.id, {}) : null;
|
||||
this.settled?.execute(project);
|
||||
}
|
||||
|
||||
override undo(project: Project): void {
|
||||
this.settled?.undo(project);
|
||||
for (const { id, previous } of this.followed)
|
||||
setBagValue(requireAsset(project, id).props, this.key, previous);
|
||||
super.undo(project);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* track or an item. `settleEmbedded` holds a song to that after every edit, so a split, a duplicate
|
||||
* or a paste gets a copy of its own and what nothing uses any more goes.
|
||||
*/
|
||||
import type { Graph } from '../graph/Graph';
|
||||
import { SOUND_FX_INPUT, SOUND_FX_OUTPUT } from '../graph/nested';
|
||||
import { newId } from '../ids';
|
||||
import type { Asset, PatternAsset, SongAsset } from '../model/Asset';
|
||||
import type { AssetType } from '../model/AssetTypes';
|
||||
@@ -40,6 +42,13 @@ export function isEmbedded(asset: Asset): boolean {
|
||||
return (asset.owner ?? '') !== '';
|
||||
}
|
||||
|
||||
/** In front of the name of what the song keeps to itself, wherever it is named. */
|
||||
export const EMBEDDED_MARK = '•';
|
||||
|
||||
export function markedName(asset: Asset): string {
|
||||
return isEmbedded(asset) ? `${EMBEDDED_MARK} ${asset.name}` : asset.name;
|
||||
}
|
||||
|
||||
/** The assets a list offers: all but those a song keeps to itself. */
|
||||
export function listedAssets(project: Project): Asset[] {
|
||||
return project.assets.filter((a) => !isEmbedded(a));
|
||||
@@ -93,14 +102,27 @@ export function clockFollowers(project: Project, asset: Asset, key: string): Pat
|
||||
return embeddedIn(project, asset.id).filter((a): a is PatternAsset => a.type === 'pattern');
|
||||
}
|
||||
|
||||
/** "Pattern 1", 2, 3 …: the first number none of what the song keeps to itself has. */
|
||||
export function embeddedPatternName(project: Project, songId: string): string {
|
||||
/** "Pattern 1", 2, 3 … ("Sound FX 1" …): the first number none of what the song keeps to itself has. */
|
||||
export function embeddedName(project: Project, songId: string, base: string): string {
|
||||
return nextNumberedName(
|
||||
embeddedIn(project, songId).map((a) => a.name),
|
||||
'Pattern',
|
||||
base,
|
||||
);
|
||||
}
|
||||
|
||||
/** What a sound FX made in a song starts as (#355): the Input wired to the Output, so it lets the sound through. */
|
||||
export function passThroughFxGraph(): Graph {
|
||||
const input = newId();
|
||||
const output = newId();
|
||||
return {
|
||||
nodes: [
|
||||
{ id: input, type: SOUND_FX_INPUT, x: 0, y: 0, params: {} },
|
||||
{ id: output, type: SOUND_FX_OUTPUT, x: 260, y: 0, params: {} },
|
||||
],
|
||||
edges: [{ id: newId(), from: { node: input, port: 'out' }, to: { node: output, port: 'in' } }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The instrument a pattern made at `tick` on a track is played with: that of the pattern item there
|
||||
* that starts last before it, else of the first one after; '' on a track without one.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Asset } from '../model/Asset';
|
||||
import { type AssetType, isPlaceableType } from '../model/AssetTypes';
|
||||
import { findAsset, type Project } from '../model/Project';
|
||||
import { listedAssets } from '../music/embedded';
|
||||
import { listedAssets, markedName } from '../music/embedded';
|
||||
import { NO_FX } from './assetFx';
|
||||
import { MASTER_BUS } from './buses';
|
||||
|
||||
@@ -9,13 +9,15 @@ export const NO_ASSET_LABEL = 'None';
|
||||
export const MASTER_LABEL = 'Master';
|
||||
|
||||
/**
|
||||
* The asset's name, "None" for the empty id, `<id>?` when it no longer resolves – a dangling
|
||||
* reference shows its id rather than passing for an empty field.
|
||||
* The asset's name, marked for one a song keeps to itself (#355), "None" for the empty id, `<id>?`
|
||||
* when it no longer resolves – a dangling reference shows its id rather than passing for an empty
|
||||
* field.
|
||||
*/
|
||||
export function assetRefLabel(project: Project | null, id: string): string {
|
||||
if (id === '' || id === NO_FX) return NO_ASSET_LABEL;
|
||||
if (id === MASTER_BUS) return MASTER_LABEL;
|
||||
return (project ? findAsset(project, id) : undefined)?.name ?? `${id}?`;
|
||||
const asset = project ? findAsset(project, id) : undefined;
|
||||
return asset ? markedName(asset) : `${id}?`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user