import { BAKE_RATE, type BakedAsset, bakePlayback, bakeSourceOf, validBake } from '../../core/music/bake'; import { findAsset } from '../../core/model/Project'; import { renderRun } from '../../viewport/bakeRender'; import { centreOf, type HookContext } from './hookHelpers'; /** Bakes of patterns and songs (#340). */ export function bakeHooks({ state, editor }: HookContext) { const asset = (id: string): BakedAsset | null => { const found = findAsset(state.project, id); return found?.type === 'pattern' || found?.type === 'song' ? found : null; }; const button = (id: string) => asset(id)?.type === 'song' ? editor.songEditor.bakeButton : editor.patternEditor.bakeButton; return { bake: { /** Bakes it as Bake does in its tab; done once it is baked, failed or cancelled. */ run: (id: string) => button(id).bake(asset(id)), cancel: (id: string) => button(id).cancel(), /** What it keeps of its bake, whether that still holds, and the button of the tab showing it. */ info: (id: string) => { const found = asset(id); const shown = button(id); return { bake: found?.bake ?? null, valid: found !== null && validBake(state.project, found) !== null, status: shown.status(found), button: { ...centreOf(shown), toggled: shown.toggled, enabled: shown.enabled, tooltip: shown.tooltipText(), }, }; }, /** * Plays `seconds` of it offline from its notes and from its bake, looped or once, and holds the * two together: the level of every `window` seconds of each in dBFS, -Infinity for silence. */ compare: async (id: string, loop: boolean, seconds: number, window: number) => { const found = asset(id); const bake = found && validBake(state.project, found); const source = found && bakeSourceOf(state.project, found); if (!bake || !source) return null; const frames = Math.round(seconds * BAKE_RATE); const watch = { progress: () => {}, cancelled: () => false }; const played = { file: bake.file, gain: bake.peak, playback: bakePlayback(bake, loop) }; const live = await renderRun(state.project, source, loop, frames, null, watch); const baked = await renderRun(state.project, source, loop, frames, played, watch); return { live: levels(live.channels, window), baked: levels(baked.channels, window) }; }, }, }; } function levels(channels: readonly Float32Array[], window: number): number[] { const span = Math.round(window * BAKE_RATE); const length = channels[0]?.length ?? 0; const out: number[] = []; for (let from = 0; from + span <= length; from += span) { let sum = 0; for (const side of channels) for (let i = from; i < from + span; i++) sum += side[i]! * side[i]!; out.push(10 * Math.log10(sum / (span * channels.length))); } return out; }