import { $, storeGet, storeSet } from "./utils.js"; import { STEM_NAMES } from "./constants.js"; // ─── DOM refs ─── export const form = $("job-form"); export const urlInput = $("url"); export const submitBtn = $("submit"); export const playBtn = $("t-play"); export const playMiniBtn = $("t-play-mini"); export const stopBtn = $("t-stop"); export const loopBtn = $("t-loop"); export const titleEl = $("title"); export const bpmChip = $("t-bpm"); export const keyChip = $("t-key"); export const stemsChip = $("t-stems-chip"); export const timeEl = $("t-time"); export const masterFader = $("t-master"); export const speedEl = $("t-speed"); export const speedLabelEl = $("t-speed-label"); export const npArt = $("np-art"); export const npThumb = $("np-thumb"); export const jobBox = $("job"); export const jobTitleEl = $("job-title"); export const jobStageEl = $("job-stage"); export const jobDetailEl = $("job-detail"); export const jobCancelBtn = $("job-cancel"); export const progressEl = $("progress"); export const errorEl = $("error"); export const lanesEl = $("lanes"); export const mixerEl = $("mixer"); export const multitrackContainer = $("multitrack-container"); export const wavesGrid = $("waves-grid"); export const rulerTime = $("ruler-time"); export const loopRegionEl = $("loop-region"); export const playheadMarker = document.querySelector(".playhead-marker"); export const waveScroll = $("wave-scroll"); export const waveCanvas = $("wave-canvas"); export const presenceRulerEl = $("presence-ruler"); export const presencePlayheadEl = $("presence-playhead"); export const footerTimeElapsed = $("footer-time-elapsed"); export const footerTimeTotal = $("footer-time-total"); export const loopStartInput = $("t-loop-start"); export const loopEndInput = $("t-loop-end"); export const stemListEl = document.querySelector(".stem-list"); export const npScrubEl = document.querySelector(".np-scrub"); export const npScrubFill = $("footer-scrub-fill"); export const footerTitle = $("footer-title"); export const footerMeta = $("footer-meta"); export const footerThumb = $("footer-thumb"); // ─── Mutable state ─── export let eventSource = null; export let multitrack = null; // Web Audio decode-and-mix engine (Safari-safe playback). Null = legacy streaming path. export let audioEngine = null; export let currentJobId = null; // `mixerState` is mutated in place (never reassigned). renderMixerRow's // closures capture each entry by reference, so on a new job we merge // localStorage values into the existing objects rather than replacing them. export const mixerState = {}; export let trackIndex = {}; export let totalDuration = 0; export let loopEnabled = false; export let loopStart = 0; export let loopEnd = 0; // Selected stems for extraction. The set determines (a) which stem // rows render in the studio dashboard after a job completes and (b) // which stem audio gets loaded into the multitrack. Backend always // runs Demucs on all 6 stems regardless -- filtering happens entirely // client-side at render time. Persisted across reloads in localStorage // so a user who turns off "Vocals" stays set up that way for the // next song. const _STEM_SEL_KEY = "stemdeck:selected-stems"; // Start with all stems selected (safe default). The async store load below // updates this binding once the store is available; ES module live bindings // ensure all importers see the updated value on next read. export let selectedStems = new Set(STEM_NAMES); // Resolves when the persisted stem selection has been loaded from the store. // Consumers that need the exact stored selection (e.g. the stem-choice UI) // should await this before reading selectedStems. export const stemSelectionReady = (async () => { try { const arr = await storeGet(_STEM_SEL_KEY, null); if (Array.isArray(arr) && arr.length > 0) { const valid = arr.filter((n) => STEM_NAMES.includes(n)); if (valid.length > 0) { selectedStems = new Set(valid); return; } } } catch (e) { console.warn("[state] failed to load stem selection:", e); } // Keep the all-stems default. })(); export function saveSelectedStems() { storeSet(_STEM_SEL_KEY, [...selectedStems]).catch((e) => console.warn("[state] failed to save stem selection:", e) ); } export function setStemSelected(name, selected) { if (selected) selectedStems.add(name); else selectedStems.delete(name); saveSelectedStems(); } // Web Audio analysers for live VU meters. export let audioContext = null; export let masterVolume = 0.5; // mirrored from masterFader.value export const trackAnalysers = []; // index → { analyser, data, vuEl } export let vuRafId = null; // Master bus nodes — created once in audio.js, shared across mixer.js. // masterBusGain is driven by the master fader; masterLimiter is a // transparent brickwall limiter that prevents inter-stem summing clipping. export let masterBusGain = null; export let masterLimiter = null; // ─── Setter helpers for mutable state (so other modules can update) ─── export function setEventSource(v) { eventSource = v; } export function setMultitrack(v) { multitrack = v; } export function setAudioEngine(v) { audioEngine = v; } export function setCurrentJobId(v) { currentJobId = v; } export function setTrackIndex(v) { trackIndex = v; } export function setTotalDuration(v) { totalDuration = v; } export function setLoopEnabled(v) { loopEnabled = v; } export function setLoopStart(v) { loopStart = v; } export function setLoopEnd(v) { loopEnd = v; } export function setAudioContext(v) { audioContext = v; } export function setMasterVolume(v) { masterVolume = v; } export let playbackSpeed = 1.0; export function setPlaybackSpeed(v) { playbackSpeed = v; } export function setVuRafId(v) { vuRafId = v; } export function setMasterBusGain(v) { masterBusGain = v; } export function setMasterLimiter(v) { masterLimiter = v; } // Footer waveform draw callback — set by player.js, called by transport.js export let footerWaveDrawFn = null; export function setFooterWaveDrawFn(fn) { footerWaveDrawFn = fn; }