Patterns could only be heard one at a time, in their own tab or from a behavior. A song lays patterns and sounds out on tracks along one time axis and plays them through the scheduler of #332, the third piece of the music tools of #330. - New asset type song under the heading Songs, with four empty tracks, 120 BPM in 4/4 and a grid of a bar. Tempo, meter and grid are rows in the inspector. Tracks, items and the loop are the asset's data, held to their rules on load and in every command. - A track has a name, volume, pan, mute and solo. Each is heard on a bus of its own, so a change reaches what is sounding at once. - A pattern item repeats its pattern for as long as it is, with a transpose and another instrument if wanted. A sound item plays its sound as a clip at its own speed, round and round, through a sound FX if wanted. Both have a volume on top of their track's. - Cut back at the left end or split, an item starts as far into what it plays as the cut is: ticks into a pattern, seconds into a sound. - The tab shows the heads with M, S, volume and pan, the ruler of the Pattern tab and the items, a pattern with its notes, a sound with its waveform, a missing asset red. Patterns and sounds are dragged in from the Assets panel. Body, right and left end move, lengthen and cut back, Alt leaves the grid. Ctrl+C/V/D/A, Ctrl+E splits where playing starts, Delete, arrows. A double-click opens a pattern item in its tab. - What is picked is a new selection of its own, so the inspector shows the item or the track, with a button to delete it. - The scheduler plans clips beside notes. Where the playhead starts or jumps back inside a clip, it is taken up that far into its sound. A clip fades out at the end of its item or of the loop. - The editor plays one piece at a time: a pattern started stops the song and the other way round. - The rules are pure functions in core/music/song, editor/song/ songView and songEdit. Findings: - The suites of4ea752fand2a3b90bwere red at e2e 54: the Pattern pushed Struct below the fold of the + menu. The module now scrolls to it, as e2e 47 does for Vector art. It also left the menu open, which made e2e 55 fail behind it. - The Assets panel only dragged assets a scene can place. Patterns and sounds are dragged now as well, and a scene says why it does not take one instead of calling every asset a scene. Tests: - song.test.ts (new, 14) and song-edit.test.ts (new, 9): model, round trip, sequence of patterns and clips, mute and solo, cuts, clips on the scheduler, moving, sizing, trimming, splitting, duplicating, hits, head parts. sound-library.test.ts +5: clip offset, end fade, pitched clip, track bus, peaks. - e2e 65 (new) on melektaus-atx: ALL PASSED, 18 checks. e2e 54 and 55 with the fix: ALL PASSED, 33 checks. e2e 4 with 30 asset types: ALL PASSED, 62 checks. - Gate on melektaus-atx: CHECK EXIT 0, 250 test files, 3884 tests. - Counter-checks: no taking up of a clip, no pattern offset, no clip end each fail 3, 1 and 1 tests. - Probe on stein2: the tab with four tracks, before and while playing, looked at in two screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
/**
|
||
* Editing a song (#334). As with the notes of a pattern, one gesture is one command: the tracks or
|
||
* items are written whole once the pointer lets go, so an undo takes the drag back. What is written
|
||
* is held to the rules of a track and an item first; an item whose track goes goes with it.
|
||
*/
|
||
import type { SongAsset } from '../model/Asset';
|
||
import type { Project } from '../model/Project';
|
||
import { heldSongItems, heldSongTracks, type SongItem, type SongTrack } from '../music/song';
|
||
import { heldLoop, type LoopRange } from '../music/sequence';
|
||
import type { Command } from './Command';
|
||
import { requireSong } from './require';
|
||
|
||
export interface SongParts {
|
||
tracks?: readonly SongTrack[];
|
||
items?: readonly SongItem[];
|
||
}
|
||
|
||
export class SetSongCommand implements Command {
|
||
private previous: { tracks: SongTrack[]; items: SongItem[] } | null = null;
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly parts: SongParts,
|
||
readonly label = 'Edit song',
|
||
) {}
|
||
|
||
/** Nothing to do when the song already reads exactly like this – no empty undo step. */
|
||
static changes(asset: SongAsset, parts: SongParts): boolean {
|
||
const tracks = heldSongTracks(parts.tracks ?? asset.tracks);
|
||
const items = heldSongItems(parts.items ?? asset.items, tracks);
|
||
return JSON.stringify([asset.tracks, asset.items]) !== JSON.stringify([tracks, items]);
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireSong(project, this.assetId);
|
||
this.previous = { tracks: asset.tracks, items: asset.items };
|
||
asset.tracks = heldSongTracks(this.parts.tracks ?? asset.tracks);
|
||
asset.items = heldSongItems(this.parts.items ?? asset.items, asset.tracks);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
if (!this.previous) return;
|
||
const asset = requireSong(project, this.assetId);
|
||
asset.tracks = this.previous.tracks;
|
||
asset.items = this.previous.items;
|
||
}
|
||
}
|
||
|
||
/** Sets the loop of the ruler, or takes it away (`null`): then the whole song is the loop. */
|
||
export class SetSongLoopCommand implements Command {
|
||
readonly label: string;
|
||
private previous: LoopRange | undefined;
|
||
|
||
constructor(
|
||
private readonly assetId: string,
|
||
private readonly loop: LoopRange | null,
|
||
) {
|
||
this.label = loop ? 'Set loop' : 'Loop the whole song';
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const asset = requireSong(project, this.assetId);
|
||
this.previous = asset.loop;
|
||
const held = heldLoop(this.loop);
|
||
if (held) asset.loop = { start: Math.round(held.start), end: Math.round(held.end) };
|
||
else delete asset.loop;
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const asset = requireSong(project, this.assetId);
|
||
if (this.previous) asset.loop = this.previous;
|
||
else delete asset.loop;
|
||
}
|
||
}
|