Spaces:
Running
Running
File size: 4,676 Bytes
9571865 c7986a9 9571865 c7986a9 9571865 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 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 };
}
|