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
+54
-12
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* What a song keeps to itself (#355): an asset with the song as its owner, left out of every list,
|
||||
* with one user in the song. Split, duplicate and paste give a place a copy of its own; what nothing
|
||||
* names goes in the same step, and comes back with an undo.
|
||||
* shared by the places of the song (#357). A paste from another song gives it a copy of its own;
|
||||
* what nothing names goes in the same step, and comes back with an undo.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
@@ -81,24 +81,50 @@ describe('an embedded asset (#355)', () => {
|
||||
expect(isRowHiddenWhenEmbedded(findAsset(f.project, blipId)!, 'loop')).toBe(false);
|
||||
});
|
||||
|
||||
it('gives the right half of a split a copy of its own, the same one again on redo', () => {
|
||||
it('is shared by both halves of a split (#357), and stays until the last place lets go', () => {
|
||||
const { f, song, takeId, edit } = setup();
|
||||
const before = f.project.assets.length;
|
||||
const left = song().items[0]!;
|
||||
edit([
|
||||
{ ...left, length: BAR },
|
||||
{ ...left, id: 'right', tick: BAR, length: BAR },
|
||||
]);
|
||||
const [a, b] = song().items;
|
||||
expect(a!.asset).toBe(takeId);
|
||||
expect(b!.asset).not.toBe(takeId);
|
||||
const copy = findAsset(f.project, b!.asset)!;
|
||||
expect([copy.owner, copy.name, copy.type]).toEqual([song().id, 'Take 1', 'sound']);
|
||||
expect(song().items.map((i) => i.asset)).toEqual([takeId, takeId]);
|
||||
expect(f.project.assets.length).toBe(before);
|
||||
edit([song().items[1]!]);
|
||||
expect(findAsset(f.project, takeId)?.owner).toBe(song().id);
|
||||
edit([]);
|
||||
expect(findAsset(f.project, takeId)).toBeUndefined();
|
||||
f.stack.undo();
|
||||
expect(findAsset(f.project, copy.id)).toBeUndefined();
|
||||
expect(embeddedIn(f.project, song().id).map((x) => x.id)).toEqual([takeId]);
|
||||
f.stack.redo();
|
||||
expect(song().items[1]!.asset).toBe(copy.id);
|
||||
expect(findAsset(f.project, copy.id)).toBe(copy);
|
||||
});
|
||||
|
||||
it('is shared by the song, its tracks and its items, whichever kind (#357)', () => {
|
||||
const { f, song, songId, track, edit } = setup();
|
||||
const fx = new CreateAssetCommand('soundfx', 'Sound FX 1', {}, songId);
|
||||
const pattern = new CreateAssetCommand('pattern', 'Pattern 1', {}, songId);
|
||||
f.stack.execute(fx);
|
||||
f.stack.execute(pattern);
|
||||
const tracks = song().tracks.map((t) => ({ ...t, fx: fx.asset.id }));
|
||||
const played = { track, kind: 'pattern' as const, asset: pattern.asset.id, length: BAR };
|
||||
const items = [0, BAR].map((tick) => ({ ...newItem({ ...played, tick }), fx: fx.asset.id }));
|
||||
f.stack.execute(new SetSongCommand(songId, { tracks, items }));
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'fx', fx.asset.id));
|
||||
expect(song().items.map((i) => [i.asset, i.fx])).toEqual([
|
||||
[pattern.asset.id, fx.asset.id],
|
||||
[pattern.asset.id, fx.asset.id],
|
||||
]);
|
||||
expect(song().tracks[0]!.fx).toBe(fx.asset.id);
|
||||
expect(
|
||||
embeddedIn(f.project, songId)
|
||||
.map((a) => a.type)
|
||||
.sort(),
|
||||
).toEqual(['pattern', 'soundfx']);
|
||||
f.stack.execute(new SetAssetPropertyCommand(songId, 'fx', ''));
|
||||
edit(song().items.map((i) => ({ ...i, fx: '' })));
|
||||
expect(findAsset(f.project, fx.asset.id)?.owner).toBe(songId);
|
||||
f.stack.execute(new SetSongCommand(songId, { tracks: song().tracks.map((t) => ({ ...t, fx: '' })) }));
|
||||
expect(findAsset(f.project, fx.asset.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('goes with the item that names it, and comes back on undo where it was', () => {
|
||||
@@ -144,6 +170,22 @@ describe('an embedded asset (#355)', () => {
|
||||
expect(findAsset(f.project, takeId)?.owner).toBe(song().id);
|
||||
});
|
||||
|
||||
it('is copied once for all the items of another song that name it (#357)', () => {
|
||||
const { f, takeId } = setup();
|
||||
const other = new CreateAssetCommand('song', 'Other');
|
||||
f.stack.execute(other);
|
||||
const otherSong = other.asset as SongAsset;
|
||||
const track = otherSong.tracks[0]!.id;
|
||||
const pasted = [0, BAR].map((tick) =>
|
||||
newItem({ track, kind: 'sound', asset: takeId, tick, length: BAR }),
|
||||
);
|
||||
f.stack.execute(new SetSongCommand(otherSong.id, { items: pasted }));
|
||||
const named = findAssetOf(f.project, otherSong.id, 'song')!.items.map((i) => i.asset);
|
||||
expect(named[0]).not.toBe(takeId);
|
||||
expect(named[1]).toBe(named[0]);
|
||||
expect(embeddedIn(f.project, otherSong.id).map((a) => a.id)).toEqual([named[0]]);
|
||||
});
|
||||
|
||||
it('leaves a project asset named by several items alone', () => {
|
||||
const { f, song, blipId, edit, track } = setup();
|
||||
const before = f.project.assets.length;
|
||||
|
||||
@@ -172,20 +172,17 @@ describe('a clip in a song (#357)', () => {
|
||||
expect(itemFx(f.project, song().items[0]!)).toBe('some-fx');
|
||||
});
|
||||
|
||||
it('keeps its take, and a second item on a clip of it gets a copy with its own clip ids', () => {
|
||||
it('keeps its take, which a second item shares, on the same clip or the whole take', () => {
|
||||
const { f, song, songId, track, takeId, takeClipId } = setup();
|
||||
expect(embeddedUsedBy(f.project, song().items).map((a) => a.id)).toEqual([takeId]);
|
||||
const before = f.project.assets.length;
|
||||
const [item] = song().items;
|
||||
f.stack.execute(new SetSongCommand(songId, { items: [item!, { ...item!, id: 'second', tick: BAR }] }));
|
||||
const named = song().items[1]!.asset;
|
||||
const copy = findSound(f.project, named)!;
|
||||
expect(copy.asset.id).not.toBe(takeId);
|
||||
expect(copy.asset.owner).toBe(songId);
|
||||
expect(copy.clip?.name).toBe('Clip 1');
|
||||
expect(named).not.toBe(takeClipId);
|
||||
expect(findSound(f.project, takeClipId)?.asset.id).toBe(takeId);
|
||||
// The take nothing else names stays.
|
||||
expect(song().items[0]!.asset).toBe(takeClipId);
|
||||
const second = { ...item!, id: 'second', tick: BAR };
|
||||
f.stack.execute(new SetSongCommand(songId, { items: [item!, second] }));
|
||||
expect(song().items.map((i) => i.asset)).toEqual([takeClipId, takeClipId]);
|
||||
f.stack.execute(new SetSongCommand(songId, { items: [item!, { ...second, asset: takeId }] }));
|
||||
expect(song().items.map((i) => i.asset)).toEqual([takeClipId, takeId]);
|
||||
expect(f.project.assets.length).toBe(before);
|
||||
expect(song().items.map((i) => i.track)).toEqual([track, track]);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,18 @@ describe('a sound FX of its own in a song (#355)', () => {
|
||||
expect(embeddedIn(state.project, songId).map((a) => a.name)).toEqual(['Sound FX 2']);
|
||||
});
|
||||
|
||||
it('is offered to every place of its song, which share it (#357)', () => {
|
||||
const { state, songId, theme, track, onTrack, onItem } = setup();
|
||||
const id = createOwnFx(state, songId, onTrack)!;
|
||||
const entries = ownFxEntries(state, songId, '', onItem);
|
||||
expect(labels(entries)).toEqual(['• Sound FX 1', 'Create sound FX']);
|
||||
const own = entries.find((e) => 'label' in e && e.label === '• Sound FX 1');
|
||||
if (own && 'onSelect' in own) own.onSelect();
|
||||
expect([track().fx, theme().items[0]!.fx]).toEqual([id, id]);
|
||||
state.execute(onTrack(''));
|
||||
expect(findAsset(state.project, id)?.owner).toBe(songId);
|
||||
});
|
||||
|
||||
it('goes when its track picks another one, and comes back on undo', () => {
|
||||
const { state, songId, track, echoId, onTrack } = setup();
|
||||
const id = createOwnFx(state, songId, onTrack)!;
|
||||
|
||||
Reference in new issue
Block a user