Let the places of a song share what it keeps to itself (#357)
With clips, one take serves many items: four items on the four clips of one take. The rule from #355 gave every further place its own copy of an embedded asset, so a cut made in the Sound tab reached one item only. Now the song, its tracks and its items share a sound, a pattern or a sound FX the song keeps. - settleEmbedded no longer copies within the song. An embedded asset stays as long as one place names it, and goes with the last one. - A paste from another song still gets a copy, but one copy for all the places naming the same asset, with its clip ids mapped. - The picks in a song offer what the song keeps, under a line after the project's assets: the Sound row of an item the song's sounds and their clips, the Pattern row its patterns, every sound FX pick its sound FX. - Make project asset and Embed a copy are unchanged. The project cbbf160bb7e8 was merged by hand afterwards: the items of Song 2 on three copies of Take 1 now play the same clips of the first one, and the copies are gone. Tests: - Unit embedded, sound-clips, own-fx and the song tests: 66 green, 3 counter-checks red as expected - e2e 65 on melektaus-atx: ALL PASSED, 52 checks - Gate on melektaus-atx: CHECK EXIT 0, 271 files, 4198 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
95edf9959b
commit
e721bba120
6 files changed
+131
-56
No files matched your search
@@ -2,8 +2,7 @@ import { SetSongCommand } from '../../core/commands/songCommands';
|
||||
import type { SongAsset } from '../../core/model/Asset';
|
||||
import { ASSET_TYPE_LABELS } from '../../core/model/AssetTypes';
|
||||
import { findAssetOf, type Project } from '../../core/model/Project';
|
||||
import { findSound } from '../../core/model/sound';
|
||||
import { isEmbedded } from '../../core/music/embedded';
|
||||
import { embeddedIn, markedName } from '../../core/music/embedded';
|
||||
import { assetRefLabel, soundEntries } from '../../core/properties/assetRef';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import {
|
||||
@@ -204,8 +203,9 @@ function buildItem(host: InspectorHost, asset: SongAsset, item: SongItem, picked
|
||||
});
|
||||
if (def.key === FX_ROW.key)
|
||||
handlers.moreChoices = (current) => ownFxEntries(state, asset.id, current, commit);
|
||||
if (def.key === 'asset' && item.kind === 'sound')
|
||||
handlers.moreChoices = (current) => ownSoundEntries(state, current, (id) => state.execute(commit(id)));
|
||||
if (def.key === 'asset')
|
||||
handlers.moreChoices = (current) =>
|
||||
keptEntries(state, asset.id, item.kind, current, (id) => state.execute(commit(id)));
|
||||
const row = createFieldRow(host, def, item[def.key as keyof SongItem], handlers);
|
||||
host.fields.set(def.key, row);
|
||||
host.body.add(row.row);
|
||||
@@ -220,15 +220,24 @@ function buildItem(host: InspectorHost, asset: SongAsset, item: SongItem, picked
|
||||
}
|
||||
|
||||
/**
|
||||
* The sound the song keeps for the item (#355) and its clips (#357), which no list offers: a take's
|
||||
* clips are picked here.
|
||||
* The sounds with their clips (#357) or the patterns the song keeps (#355), which no list offers:
|
||||
* its items share them, so a take's clips are picked here.
|
||||
*/
|
||||
function ownSoundEntries(state: EditorState, current: string, pick: (id: string) => void): MenuEntry[] {
|
||||
const sound = findSound(state.project, current)?.asset;
|
||||
if (!sound || !isEmbedded(sound)) return [];
|
||||
function keptEntries(
|
||||
state: EditorState,
|
||||
songId: string,
|
||||
kind: SongItem['kind'],
|
||||
current: string,
|
||||
pick: (id: string) => void,
|
||||
): MenuEntry[] {
|
||||
const entries = embeddedIn(state.project, songId)
|
||||
.filter((a) => a.type === kind)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.flatMap((a) => (a.type === 'sound' ? soundEntries(a) : [{ id: a.id, label: markedName(a) }]));
|
||||
if (entries.length === 0) return [];
|
||||
return [
|
||||
{ separator: true },
|
||||
...soundEntries(sound).map((entry) => ({
|
||||
...entries.map((entry) => ({
|
||||
label: entry.label,
|
||||
current: entry.id === current,
|
||||
onSelect: () => pick(entry.id),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* A sound FX of its own for a song, a track or an item (#355): the entries every pick of a sound FX
|
||||
* in a song ends with. The FX made here is kept by the song and named by that one place, so the
|
||||
* place letting go of it takes it away (`settleEmbedded`).
|
||||
* in a song ends with. The FX made here is kept by the song and shared by its places (#357), so the
|
||||
* last place letting go of it takes it away (`settleEmbedded`).
|
||||
*/
|
||||
import { CreateAssetCommand, MakeProjectAssetCommand } from '../../core/commands/assetCommands';
|
||||
import type { Command } from '../../core/commands/Command';
|
||||
import { CompoundCommand } from '../../core/commands/CompoundCommand';
|
||||
import type { SoundFxAsset } from '../../core/model/Asset';
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import { EMBEDDED_MARK, embeddedName, passThroughFxGraph } from '../../core/music/embedded';
|
||||
import { EMBEDDED_MARK, embeddedIn, embeddedName, passThroughFxGraph } from '../../core/music/embedded';
|
||||
import type { MenuEntry } from '../../ui';
|
||||
import type { EditorState } from '../state';
|
||||
|
||||
@@ -16,8 +16,8 @@ import type { EditorState } from '../state';
|
||||
export type FxPlace = (fxId: string) => Command;
|
||||
|
||||
/**
|
||||
* Its own FX with Edit and Make project asset while the place names one; else Create sound FX, and
|
||||
* Embed a copy while it names one of the project's.
|
||||
* The FX the song keeps, which its places share (#357); then Edit and Make project asset while the
|
||||
* place names one of them, else Create sound FX, and Embed a copy while it names one of the project's.
|
||||
*/
|
||||
export function ownFxEntries(
|
||||
state: EditorState,
|
||||
@@ -28,15 +28,24 @@ export function ownFxEntries(
|
||||
const project = state.project;
|
||||
if (!findAssetOf(project, songId, 'song')) return [];
|
||||
const picked = findAssetOf(project, current, 'soundfx');
|
||||
const kept = embeddedIn(project, songId)
|
||||
.filter((a) => a.type === 'soundfx')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((fx) => ({
|
||||
label: `${EMBEDDED_MARK} ${fx.name}`,
|
||||
current: fx.id === current,
|
||||
onSelect: () => (fx.id === current ? state.openAsset(fx.id) : state.execute(place(fx.id))),
|
||||
}));
|
||||
if (picked?.owner === songId)
|
||||
return [
|
||||
{ separator: true },
|
||||
{ label: `${EMBEDDED_MARK} ${picked.name}`, current: true, onSelect: () => state.openAsset(picked.id) },
|
||||
...kept,
|
||||
{ label: 'Edit sound FX', onSelect: () => state.openAsset(picked.id) },
|
||||
{ label: 'Make project asset', onSelect: () => state.execute(new MakeProjectAssetCommand(picked.id)) },
|
||||
];
|
||||
return [
|
||||
{ separator: true },
|
||||
...kept,
|
||||
{ label: 'Create sound FX', onSelect: () => createOwnFx(state, songId, place) },
|
||||
...(picked ? [{ label: 'Embed a copy', onSelect: () => createOwnFx(state, songId, place, picked) }] : []),
|
||||
];
|
||||
|
||||
Reference in new issue
Block a user