244 lines
6.7 KiB
JavaScript
244 lines
6.7 KiB
JavaScript
|
|
/**
|
||
|
|
* Session Preset Manager — bundles control surface state + synth preset into
|
||
|
|
* a single loadable configuration.
|
||
|
|
*
|
||
|
|
* Presets are stored in localStorage under a dedicated key, separate from the
|
||
|
|
* main app state.
|
||
|
|
*
|
||
|
|
* @module session-presets
|
||
|
|
*/
|
||
|
|
|
||
|
|
const STORAGE_KEY = 'nisps-session-presets';
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// URL encoding helpers
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Encode a compact session state into URL search params.
|
||
|
|
* Only encodes the most important state for sharing.
|
||
|
|
*/
|
||
|
|
function encodeToURL(preset) {
|
||
|
|
const params = new URLSearchParams();
|
||
|
|
|
||
|
|
// Synth preset
|
||
|
|
if (preset.synthPresetId) {
|
||
|
|
params.set('sp', preset.synthPresetId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compound axes (3 values, comma-separated)
|
||
|
|
if (preset.controlSurface?.axes) {
|
||
|
|
const a = preset.controlSurface.axes;
|
||
|
|
params.set('cs', [
|
||
|
|
(a.boldness ?? 0.5).toFixed(2),
|
||
|
|
(a.memory ?? 0.5).toFixed(2),
|
||
|
|
(a.precision ?? 0.3).toFixed(2),
|
||
|
|
].join(','));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Output pipeline (only non-default values)
|
||
|
|
if (preset.outputPipeline) {
|
||
|
|
const op = preset.outputPipeline;
|
||
|
|
const parts = [];
|
||
|
|
if (op.globalCurve != null && op.globalCurve !== 1.0) parts.push(`gc=${op.globalCurve.toFixed(2)}`);
|
||
|
|
if (op.smoothing != null && op.smoothing !== 0) parts.push(`sm=${op.smoothing.toFixed(2)}`);
|
||
|
|
if (op.slewRate != null && op.slewRate !== 1.0) parts.push(`sl=${op.slewRate.toFixed(3)}`);
|
||
|
|
if (parts.length > 0) params.set('op', parts.join(','));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Key control surface overrides (compact: name=value pairs)
|
||
|
|
if (preset.controlSurface?.offsets) {
|
||
|
|
const offsets = preset.controlSurface.offsets;
|
||
|
|
const keys = Object.keys(offsets);
|
||
|
|
if (keys.length > 0 && keys.length <= 10) {
|
||
|
|
// Only encode up to 10 overrides to keep URL reasonable
|
||
|
|
const pairs = keys.slice(0, 10).map(k => {
|
||
|
|
const v = offsets[k];
|
||
|
|
return `${k}:${typeof v === 'number' ? v.toFixed(3) : v}`;
|
||
|
|
});
|
||
|
|
params.set('co', pairs.join(','));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return params.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Decode session state from URL search params.
|
||
|
|
* Returns a partial preset object.
|
||
|
|
*/
|
||
|
|
function decodeFromURL(urlParams) {
|
||
|
|
const preset = {};
|
||
|
|
|
||
|
|
// Synth preset
|
||
|
|
const sp = urlParams.get('sp');
|
||
|
|
if (sp) preset.synthPresetId = sp;
|
||
|
|
|
||
|
|
// Compound axes
|
||
|
|
const cs = urlParams.get('cs');
|
||
|
|
if (cs) {
|
||
|
|
const [b, m, p] = cs.split(',').map(Number);
|
||
|
|
preset.controlSurface = {
|
||
|
|
axes: {
|
||
|
|
boldness: isNaN(b) ? 0.5 : b,
|
||
|
|
memory: isNaN(m) ? 0.5 : m,
|
||
|
|
precision: isNaN(p) ? 0.3 : p,
|
||
|
|
},
|
||
|
|
offsets: {},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Control surface overrides
|
||
|
|
const co = urlParams.get('co');
|
||
|
|
if (co && preset.controlSurface) {
|
||
|
|
for (const pair of co.split(',')) {
|
||
|
|
const colonIdx = pair.indexOf(':');
|
||
|
|
if (colonIdx > 0) {
|
||
|
|
const key = pair.substring(0, colonIdx);
|
||
|
|
const valStr = pair.substring(colonIdx + 1);
|
||
|
|
const num = Number(valStr);
|
||
|
|
preset.controlSurface.offsets[key] = isNaN(num) ? valStr : num;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Output pipeline
|
||
|
|
const op = urlParams.get('op');
|
||
|
|
if (op) {
|
||
|
|
const pipeline = {};
|
||
|
|
for (const part of op.split(',')) {
|
||
|
|
const [k, v] = part.split('=');
|
||
|
|
const num = Number(v);
|
||
|
|
if (!isNaN(num)) {
|
||
|
|
if (k === 'gc') pipeline.globalCurve = num;
|
||
|
|
else if (k === 'sm') pipeline.smoothing = num;
|
||
|
|
else if (k === 'sl') pipeline.slewRate = num;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
preset.outputPipeline = pipeline;
|
||
|
|
}
|
||
|
|
|
||
|
|
return preset;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// SessionPresetManager
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
export class SessionPresetManager {
|
||
|
|
constructor() {
|
||
|
|
this._presets = this._loadFromStorage();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Capture a snapshot of the current full session state.
|
||
|
|
*
|
||
|
|
* @param {string} name — user-facing name for this preset
|
||
|
|
* @param {object} state — current state from various subsystems
|
||
|
|
* @param {object} state.controlSurface — result of ControlSurface.getState()
|
||
|
|
* @param {string} state.synthPresetId — active synth preset ID
|
||
|
|
* @param {Array} state.groupOverrides — current group overrides
|
||
|
|
* @param {object} state.inputPipeline — result of InputPipeline.getConfig()
|
||
|
|
* @param {object} state.outputPipeline — result of OutputPipeline.getConfig()
|
||
|
|
* @returns {object} the captured preset
|
||
|
|
*/
|
||
|
|
capture(name, state) {
|
||
|
|
return {
|
||
|
|
name,
|
||
|
|
controlSurface: state.controlSurface || null,
|
||
|
|
synthPresetId: state.synthPresetId || null,
|
||
|
|
groupOverrides: state.groupOverrides || null,
|
||
|
|
inputPipeline: state.inputPipeline || null,
|
||
|
|
outputPipeline: state.outputPipeline || null,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save current state as a named session preset to localStorage.
|
||
|
|
*
|
||
|
|
* @param {string} name
|
||
|
|
* @param {object} state — same as capture() state param
|
||
|
|
*/
|
||
|
|
save(name, state) {
|
||
|
|
const preset = this.capture(name, state);
|
||
|
|
this._presets[name] = preset;
|
||
|
|
this._saveToStorage();
|
||
|
|
return preset;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load a named session preset.
|
||
|
|
* @param {string} name
|
||
|
|
* @returns {object|null}
|
||
|
|
*/
|
||
|
|
load(name) {
|
||
|
|
return this._presets[name] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* List all saved session presets.
|
||
|
|
* @returns {Array<{name: string, timestamp: number}>}
|
||
|
|
*/
|
||
|
|
list() {
|
||
|
|
return Object.values(this._presets)
|
||
|
|
.map(p => ({ name: p.name, timestamp: p.timestamp }))
|
||
|
|
.sort((a, b) => b.timestamp - a.timestamp);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete a named session preset.
|
||
|
|
* @param {string} name
|
||
|
|
* @returns {boolean} true if deleted
|
||
|
|
*/
|
||
|
|
delete(name) {
|
||
|
|
if (name in this._presets) {
|
||
|
|
delete this._presets[name];
|
||
|
|
this._saveToStorage();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Encode a preset into URL search params for sharing.
|
||
|
|
* @param {object} preset
|
||
|
|
* @returns {string} URL search string (without leading ?)
|
||
|
|
*/
|
||
|
|
toURL(preset) {
|
||
|
|
return encodeToURL(preset);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Decode a preset from URL search params.
|
||
|
|
* @param {URLSearchParams} urlParams
|
||
|
|
* @returns {object} partial preset
|
||
|
|
*/
|
||
|
|
fromURL(urlParams) {
|
||
|
|
return decodeFromURL(urlParams);
|
||
|
|
}
|
||
|
|
|
||
|
|
// -----------------------------------------------------------------------
|
||
|
|
// Internal
|
||
|
|
// -----------------------------------------------------------------------
|
||
|
|
|
||
|
|
_loadFromStorage() {
|
||
|
|
try {
|
||
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||
|
|
if (!raw) return {};
|
||
|
|
return JSON.parse(raw);
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('[SessionPresets] Failed to load:', e);
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_saveToStorage() {
|
||
|
|
try {
|
||
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._presets));
|
||
|
|
} catch (e) {
|
||
|
|
console.warn('[SessionPresets] Failed to save:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|