fragmenta / app /frontend /src /components /usePerformanceSession.js
MazCodes's picture
Upload folder using huggingface_hub
c7986a9 verified
raw
history blame
4.68 kB
import { useCallback, useEffect, useRef, useState } from 'react';
export const PERFORMANCE_SESSION_STORAGE_KEY = 'fragmenta.performance.session.v1';
const STORAGE_KEY = PERFORMANCE_SESSION_STORAGE_KEY;
export function clearPerformanceSession() {
try { localStorage.removeItem(STORAGE_KEY); }
catch { /* non-fatal */ }
}
const PRESETS_STORAGE_KEY = 'fragmenta.performance.presets.v1';
function readPresetBag() {
try {
const raw = localStorage.getItem(PRESETS_STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function writePresetBag(bag) {
try { localStorage.setItem(PRESETS_STORAGE_KEY, JSON.stringify(bag)); }
catch { /* quota — non-fatal */ }
}
export function listPresetNames() {
return Object.keys(readPresetBag()).sort((a, b) => a.localeCompare(b));
}
export function savePreset(name, sessionData) {
const trimmed = (name || '').trim();
if (!trimmed) return false;
const bag = readPresetBag();
bag[trimmed] = sessionData;
writePresetBag(bag);
return true;
}
export function deletePreset(name) {
const bag = readPresetBag();
if (!(name in bag)) return false;
delete bag[name];
writePresetBag(bag);
return true;
}
// Replace the live session storage with a preset's snapshot. Caller is
// expected to force-remount the panel afterward so its useState mirrors
// pick up the new shape; localStorage alone won't reset mounted state.
export function loadPresetIntoSession(name) {
const bag = readPresetBag();
const preset = bag[name];
if (!preset) return false;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(preset));
return true;
} catch {
return false;
}
}
const CHANNEL_DEFAULT = {
prompt: '',
duration: 8,
durationMode: 'seconds',
bars: 4,
looping: true,
muted: false,
soloed: false,
batchSize: 1,
knobs: { gain: -6, pan: 0, filter: 18000, delay: 0, reverb: 0 },
};
function defaultSession(channelCount) {
return {
bpm: 120,
launchQuantum: 4,
masterDb: 0,
injectBpm: true,
linkEnabled: false,
selectedModel: '',
selectedUnwrappedModel: '',
steps: 250,
randomSeed: true,
seedValue: '',
cueDeviceId: '',
channels: Array.from({ length: channelCount }, () => ({
...CHANNEL_DEFAULT,
knobs: { ...CHANNEL_DEFAULT.knobs },
})),
};
}
function loadSession(channelCount) {
const fallback = defaultSession(channelCount);
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return fallback;
const parsed = JSON.parse(raw);
// Merge against defaults so older saves don't crash on missing fields.
// Length shifts (channel count change between releases) are absorbed
// by always producing exactly `channelCount` channels.
const channels = Array.from({ length: channelCount }, (_, i) => ({
...CHANNEL_DEFAULT,
...(parsed.channels?.[i] || {}),
knobs: { ...CHANNEL_DEFAULT.knobs, ...(parsed.channels?.[i]?.knobs || {}) },
}));
return { ...fallback, ...parsed, channels };
} catch {
return fallback;
}
}
export function usePerformanceSession(channelCount = 4) {
const [session, setSession] = useState(() => loadSession(channelCount));
const persistTimerRef = useRef(null);
// Knobs and sliders fire many times per second; debounce writes so we don't
// hammer localStorage. Last-write-wins is fine for session continuity.
useEffect(() => {
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
persistTimerRef.current = setTimeout(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
} catch { /* quota or serialization — non-fatal */ }
}, 250);
return () => {
if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
};
}, [session]);
const updateGlobal = useCallback((key, value) => {
setSession(prev => (prev[key] === value ? prev : { ...prev, [key]: value }));
}, []);
const updateChannel = useCallback((index, partial) => {
setSession(prev => {
const channels = prev.channels.slice();
channels[index] = { ...channels[index], ...partial };
return { ...prev, channels };
});
}, []);
return { session, updateGlobal, updateChannel };
}