123 lines
7.8 KiB
React
123 lines
7.8 KiB
React
|
|
/* Console 2.0 — shared instrument model. Pure data + inference used by all
|
|||
|
|
three altitudes (Console / Perform / Zen) so they share one spine. */
|
|||
|
|
function mkParams(spec) {
|
|||
|
|
const out = [];
|
|||
|
|
for (const [group, names] of spec) {
|
|||
|
|
names.forEach((name) => out.push({ name, group, status: 'live', val: 0.5, min: 0, max: 1, curve: 0.5 }));
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
const MF_MODES = [
|
|||
|
|
{ id: 'paf_synth', label: 'PAF Synth', cls: 'Synth', glyph: '∿', input: 'xy',
|
|||
|
|
params: mkParams([['formant', ['F1', 'F2', 'F3', 'tilt', 'spread', 'skirt']], ['pitch', ['root', 'glide', 'detune']], ['amp', ['gain', 'attack', 'decay']], ['filter', ['cutoff', 'res', 'env']], ['fx', ['drive', 'air', 'width']]]) },
|
|||
|
|
{ id: 'channel_strip', label: 'Channel Strip', cls: 'Synth', glyph: '▤', input: 'joystick',
|
|||
|
|
params: mkParams([['filter', ['lo', 'loMid', 'hiMid', 'hi']], ['amp', ['comp', 'gate', 'makeup']], ['fx', ['sat', 'width', 'glue', 'tilt', 'air']]]) },
|
|||
|
|
{ id: 'verb_fx', label: 'Verb FX', cls: 'Synth', glyph: '◞', input: 'joystick',
|
|||
|
|
params: mkParams([['fx', ['size', 'decay', 'damp', 'diff']], ['mod', ['rate', 'depth']], ['filter', ['lo', 'hi']]]) },
|
|||
|
|
{ id: 'elysiamorf', label: 'Elysiamorf', cls: 'Synth', glyph: '❋', input: 'xy',
|
|||
|
|
params: mkParams([['formant', ['grain', 'size', 'pos', 'spray']], ['mod', ['rate', 'depth', 'jitter']], ['amp', ['gain', 'env']], ['filter', ['cutoff', 'res']], ['fx', ['blur', 'shimmer', 'freeze', 'width']]]) },
|
|||
|
|
{ id: 'memlcelium', label: 'MEML Celium', cls: 'Sequencer', glyph: '☷', input: 'xy',
|
|||
|
|
params: mkParams([['mod', ['cvA', 'cvB', 'gate', 'div']], ['pitch', ['root', 'scale', 'oct']], ['amp', ['vca', 'slew']]]) },
|
|||
|
|
{ id: 'breakor', label: 'Breakor', cls: 'Sequencer', glyph: '⊟', input: 'joystick',
|
|||
|
|
params: mkParams([['mod', ['density', 'swing', 'fill', 'stutter']], ['amp', ['punch', 'decay']], ['filter', ['tone', 'crush']], ['fx', ['glitch', 'rev']]]) },
|
|||
|
|
{ id: 'sound_analysis_midi', label: 'Sound Analysis → MIDI', cls: 'Controller', glyph: '⇉', input: 'audio_in', badge: '1-input',
|
|||
|
|
params: mkParams([['mod', ['cc1', 'cc2', 'cc3', 'cc4']], ['pitch', ['note', 'bend']], ['amp', ['vel', 'press']]]) },
|
|||
|
|
{ id: 'visualizer', label: 'Visualizer', cls: 'Visual', glyph: '◑', input: 'xy',
|
|||
|
|
params: mkParams([['mod', ['hue', 'sat', 'flow', 'warp']], ['amp', ['bloom', 'fade']], ['fx', ['grain', 'trail']]]) },
|
|||
|
|
{ id: 'c15', label: 'C15', cls: 'Synth', glyph: '◆', input: 'xy', placeholder: true, badge: 'soon', params: mkParams([['amp', ['a', 'b']]]) },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function applyCurve(v, c) { const e = 0.25 + c * 1.75; return Math.pow(Math.max(0, Math.min(1, v)), e); }
|
|||
|
|
function MF_infer(pos, seed, params) {
|
|||
|
|
const [x, y] = pos;
|
|||
|
|
return params.map((p, i) => {
|
|||
|
|
if (p.status === 'off') return 0; // deactivated → muted
|
|||
|
|
if (p.status === 'fixed') return p.val ?? 0.5; // deactivated → held static
|
|||
|
|
// live → driven by the model, shaped by min/max/curve
|
|||
|
|
const a = Math.sin(x * (1.2 + i * 0.19) * Math.PI + i * 0.7 + seed) * 0.5 + 0.5;
|
|||
|
|
const b = Math.cos(y * (0.9 + i * 0.13) * Math.PI - i * 0.4 - seed * 0.5) * 0.5 + 0.5;
|
|||
|
|
let v = a * 0.55 + b * 0.45;
|
|||
|
|
v = p.min + applyCurve(v, p.curve) * (p.max - p.min);
|
|||
|
|
return Math.max(0, Math.min(1, v));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function MF_seededGradient(rev) {
|
|||
|
|
const n = 4, norms = [], status = [];
|
|||
|
|
for (let i = 0; i < n; i++) {
|
|||
|
|
const r = Math.abs(Math.sin((rev + 1) * (i + 1) * 12.9898) * 43758.5453 % 1);
|
|||
|
|
norms.push(0.2 + r * 0.8);
|
|||
|
|
status.push(r > 0.85 ? 'exploding' : r < 0.18 ? 'vanishing' : r < 0.3 ? 'converged' : 'healthy');
|
|||
|
|
}
|
|||
|
|
return { norms, status };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* useInstrument — the shared reactive spine + verdict/training actions.
|
|||
|
|
Returns flat state + actions; each view renders as much of it as it needs. */
|
|||
|
|
let MF_SNAP_ID = 0;
|
|||
|
|
function useInstrument(initialMode) {
|
|||
|
|
const [modeId, setModeId] = React.useState(initialMode || 'paf_synth');
|
|||
|
|
const mode = MF_MODES.find((m) => m.id === modeId) || MF_MODES[0];
|
|||
|
|
const [params, setParams] = React.useState(() => mode.params.map((p) => ({ ...p })));
|
|||
|
|
const [pos, setPos] = React.useState([0.5, 0.5]);
|
|||
|
|
const [seed, setSeed] = React.useState(0.4);
|
|||
|
|
const [axes, setAxes] = React.useState({ boldness: 0.55, memory: 0.4, precision: 0.5 });
|
|||
|
|
const [noiseCap, setNoiseCap] = React.useState(0.12);
|
|||
|
|
const [examples, setExamples] = React.useState(0);
|
|||
|
|
const [loss, setLoss] = React.useState([]);
|
|||
|
|
const [busy, setBusy] = React.useState(false);
|
|||
|
|
const [snapshots, setSnapshots] = React.useState([]);
|
|||
|
|
const [ab, setAB] = React.useState('B');
|
|||
|
|
const [holdingA, setHoldingA] = React.useState(false);
|
|||
|
|
const aRef = React.useRef(null);
|
|||
|
|
const [health, setHealth] = React.useState(0.8);
|
|||
|
|
const [rev, setRev] = React.useState(1);
|
|||
|
|
const [follow, setFollow] = React.useState(false);
|
|||
|
|
const [pins, setPins] = React.useState([]);
|
|||
|
|
const [firstSession, setFirstSession] = React.useState(true);
|
|||
|
|
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
setParams(mode.params.map((p) => ({ ...p }))); setPos([0.5, 0.5]); setExamples(0);
|
|||
|
|
setLoss([]); setSnapshots([]); setSeed(0.4); setFollow(false); setPins([]); setHealth(0.8); setRev(1);
|
|||
|
|
}, [modeId]);
|
|||
|
|
|
|||
|
|
const effSeed = holdingA && aRef.current ? aRef.current.seed : seed;
|
|||
|
|
const values = React.useMemo(() => MF_infer(pos, effSeed, params), [pos, effSeed, params]);
|
|||
|
|
const gradient = React.useMemo(() => MF_seededGradient(rev), [rev]);
|
|||
|
|
|
|||
|
|
const pushSnap = (tag) => setSnapshots((s) => [...s, { id: ++MF_SNAP_ID, tag, noise: noiseCap, seed }].slice(-50));
|
|||
|
|
const commit = () => {
|
|||
|
|
setFirstSession(false); pushSnap('commit +'); setBusy(true);
|
|||
|
|
setNoiseCap((n) => Math.max(0.02, n * 0.7)); setHealth((h) => Math.min(1, h + 0.08)); setRev((r) => r + 1);
|
|||
|
|
setTimeout(() => { setLoss((prev) => { const b = prev.length ? prev[prev.length - 1] : 0.5; return [...prev, Math.max(0.004, b * (0.8 + Math.random() * 0.08))].slice(-120); }); setBusy(false); }, 260);
|
|||
|
|
};
|
|||
|
|
const perturb = () => {
|
|||
|
|
setFirstSession(false); pushSnap('perturb −');
|
|||
|
|
setSeed((s) => s + (Math.random() - 0.5) * (noiseCap * 4 + 0.3));
|
|||
|
|
setNoiseCap((n) => Math.min(0.5, n + 0.06)); setHealth((h) => Math.max(0.1, h - 0.06)); setRev((r) => r + 1);
|
|||
|
|
};
|
|||
|
|
const reroll = () => { setFirstSession(false); pushSnap('re-roll'); setSeed(Math.random() * 6); setNoiseCap(0.4); setHealth(0.5); setRev((r) => r + 1); };
|
|||
|
|
const undo = () => setSnapshots((s) => { if (!s.length) return s; const last = s[s.length - 1]; setSeed(last.seed); setNoiseCap(last.noise); setRev((r) => r + 1); return s.slice(0, -1); });
|
|||
|
|
const train = () => { setBusy(true); setTimeout(() => { setLoss((p) => { const b = p.length ? p[p.length - 1] : 0.5; return [...p, Math.max(0.004, b * 0.82)].slice(-120); }); setBusy(false); }, 260); };
|
|||
|
|
const setParam = (i, patch) => setParams((ps) => ps.map((p, j) => (j === i ? { ...p, ...patch } : p)));
|
|||
|
|
const cycleStatus = (i) => setParams((ps) => ps.map((p, j) => (j === i ? { ...p, status: ({ off: 'fixed', fixed: 'live', live: 'off' })[p.status] } : p)));
|
|||
|
|
const toggleAB = () => { if (ab === 'B') { aRef.current = { seed }; setAB('A'); } else { if (aRef.current) setSeed(aRef.current.seed); setAB('B'); } };
|
|||
|
|
const cycleMode = (dir = 1) => {
|
|||
|
|
const live = MF_MODES.filter((m) => !m.placeholder);
|
|||
|
|
const i = live.findIndex((m) => m.id === modeId);
|
|||
|
|
setModeId(live[(i + dir + live.length) % live.length].id);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
MODES: MF_MODES, modeId, setModeId, cycleMode, mode, params, setParam, cycleStatus,
|
|||
|
|
pos, setPos, seed, values, axes, setAxes, setAxis: (k, v) => setAxes((s) => ({ ...s, [k]: v })),
|
|||
|
|
noiseCap, setNoiseCap, examples, loss, busy, train,
|
|||
|
|
snapshots, commit, perturb, reroll, undo, ab, toggleAB, holdingA, setHoldingA,
|
|||
|
|
health, rev, gradient, follow, setFollow, pins, setPins, firstSession,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.MF_MODES = MF_MODES;
|
|||
|
|
window.MF_infer = MF_infer;
|
|||
|
|
window.MF_seededGradient = MF_seededGradient;
|
|||
|
|
window.useInstrument = useInstrument;
|