8f10353f0c
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
150 lines
6.0 KiB
JavaScript
150 lines
6.0 KiB
JavaScript
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; }
|