Files
feuerwerk/src/editor/debug/songHooks.ts
T
adminandClaude Opus 5.5 f840ec3c44
game-maker / gate (push) Successful in 4m3s
game-maker / suite (push) Failing after 14m40s
Snap a new song to 1/16, and route new assets as the others (#365)
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>
2026-10-09 09:37:44 +02:00

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 }),
},
},
};
}