A new song snapped to whole bars, too coarse to lay out a beat. And a new or duplicated sound, instrument, pattern or song went to the master alone, so every one had to be linked into the buses by hand again. - A song without a grid of its own snaps to 1/16. The songs in the user's project have Bar written in and keep it. - A new or duplicated sound, instrument, pattern or song gets the links of the one whose targets most others of its type share, at their volume. On a tie the latest of them decides, so a single song decides for the next one. - Where nothing of its type is linked yet, it goes as most of all that plays, and where nothing is, to the master. A new bus still goes to the master alone. - Assets made at once are matched against what stood before, not against each other. A project from before the routing still opens with everything on the master. - The song tab has a gridAt hook. e2e 65 checks the new grid and sets Bar, which its checks go by. Tests: - Unit master-routing (1 new) and song (1 rewritten): 26 green. Counter checks (rule off, grid back to Bar) red. - e2e 65 (melektaus-atx): ALL PASSED, 58 checks (1 new). - Gate (melektaus-atx): CHECK EXIT 0, 273 files, 4226 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.3 KiB
TypeScript
83 lines
4.3 KiB
TypeScript
import { findAssetOf } from '../../core/model/Project';
|
|
import type { HeadPart } from '../song/songView';
|
|
import { soundLibrary } from '../../viewport/SoundLibrary';
|
|
import { centreOf, snapButton, type HookContext } from './hookHelpers';
|
|
|
|
/** The Song tab and its tracks (#334). */
|
|
export function songHooks({ state, editor }: HookContext) {
|
|
const { songEditor } = editor;
|
|
const timeline = songEditor.canvas;
|
|
return {
|
|
song: {
|
|
current: () => songEditor.currentAssetId,
|
|
info: () => songEditor.shownInfo,
|
|
rect: () => songEditor.canvasRect(),
|
|
/** The tracks, items and loop as the model holds them. */
|
|
model: (assetId: string) => {
|
|
const asset = findAssetOf(state.project, assetId, 'song');
|
|
return asset
|
|
? (JSON.parse(
|
|
JSON.stringify({ tracks: asset.tracks, items: asset.items, loop: asset.loop ?? null }),
|
|
) as unknown)
|
|
: null;
|
|
},
|
|
picked: () => timeline.pickedIds,
|
|
pickedTrack: () => timeline.pickedTrack,
|
|
/** Where a tick of a track's lane, a part of an item, a part of a head and a tick of the ruler are on screen. */
|
|
laneAt: (tick: number, track: number) => timeline.lanePoint(tick, track),
|
|
itemAt: (itemId: string, part?: 'start' | 'body' | 'end') => timeline.itemPoint(itemId, part),
|
|
headAt: (track: number, part: HeadPart, share?: number) => timeline.headPoint(track, part, share),
|
|
rulerAt: (tick: number) => timeline.rulerPoint(tick),
|
|
loopAt: (tick: number) => timeline.loopPoint(tick),
|
|
view: () => timeline.viewInfo(),
|
|
items: () => timeline.itemsDrawn(),
|
|
playAt: () => ({ ...centreOf(songEditor.playButton), text: songEditor.playButton.text }),
|
|
fitAt: () => centreOf(songEditor.fitButton),
|
|
gridAt: () => ({ ...centreOf(songEditor.gridButton), text: songEditor.gridButton.text }),
|
|
playing: () => songEditor.isPlaying,
|
|
/** Record (#342): its button, whether a take is being made, and the input's level. */
|
|
recordAt: () => ({ ...centreOf(songEditor.recordButton), text: songEditor.recordButton.text }),
|
|
recording: () => songEditor.isRecording,
|
|
/** The notes and clips the scheduler handed to the player since Play, and where it stands. */
|
|
planned: () => [...songEditor.music.planned],
|
|
clips: () => [...songEditor.music.clips],
|
|
position: () => songEditor.music.current?.position() ?? null,
|
|
clock: () => soundLibrary.clock(),
|
|
stats: () => soundLibrary.stats(),
|
|
buses: () => soundLibrary.busLevels(),
|
|
/** The level meters (#359): each track's by its id and Out, in dB, and how many buses are tapped. */
|
|
meters: () => {
|
|
const asset = findAssetOf(state.project, songEditor.currentAssetId, 'song');
|
|
return {
|
|
tracks: Object.fromEntries((asset?.tracks ?? []).map((t) => [t.id, timeline.meterOf(t.id)])),
|
|
out: timeline.outShown(),
|
|
metered: soundLibrary.metered(),
|
|
};
|
|
},
|
|
/** The loudest sample on each side behind the limiter at the output, null without it. */
|
|
guard: () => soundLibrary.guardPeaks(),
|
|
latency: () => soundLibrary.latency(),
|
|
meterAt: (track: number) => timeline.meterPoint(track),
|
|
outAt: () => timeline.outPoint(),
|
|
/** Timeline or Routing (#350), and where their switches are. */
|
|
shown: () => songEditor.view,
|
|
viewAt: (view: 'timeline' | 'routing') =>
|
|
centreOf(view === 'routing' ? songEditor.routingButton : songEditor.timelineButton),
|
|
routing: {
|
|
graph: (assetId: string) => {
|
|
const asset = findAssetOf(state.project, assetId, 'song');
|
|
return asset ? (JSON.parse(JSON.stringify(asset.graph)) as unknown) : null;
|
|
},
|
|
portAt: (nodeId: string, port: string, isOutput: boolean) =>
|
|
songEditor.routing.portGlobal(nodeId, port, isOutput),
|
|
nodeAt: (nodeId: string) => songEditor.routing.nodeGlobal(nodeId),
|
|
edgeAt: (edgeId: string) => songEditor.routing.edgeGlobal(edgeId),
|
|
label: (nodeId: string) => songEditor.routing.nodeLabel(nodeId),
|
|
problem: (nodeId: string) => songEditor.routing.nodeProblem(nodeId),
|
|
snapping: () => songEditor.routing.snapEnabled,
|
|
snapButton: () => ({ ...snapButton(songEditor.snap), enabled: songEditor.snap.button.enabled }),
|
|
},
|
|
},
|
|
};
|
|
}
|