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