From d41e4c3e9fd7cbb62064f212dc82726a222f83d2 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Wed, 29 Apr 2026 15:35:58 +0300 Subject: [PATCH] chore(playground): remove legacy a-immersive playground Clears the old vanilla-JS / multi-app playground (a-immersive.html, b-workbench.html, c-journey.html, designs.html, index.html, plus the js/, css/, c15/, faust/, firmware/, osc-bridge/, wasm/ subtrees and the serve/test scripts). The fresh SolidJS + Vite scaffold lands in the next commits per the rewrite plan in .local/architecture.md. Reference docs (SPEC-controls.md, SPEC-shapeseq.md, PLAN-solidjs-migration.md, ARCHITECTURE.md, README.md, TODOS.md, devlog/) have been moved to .local/playground-archive/ (out of git) so future streams can still consult them. Stream 8 of the rewrite (meml-911). --- playground/ARCHITECTURE.md | 42 - playground/PLAN-solidjs-migration.md | 831 --- playground/README.md | 89 - playground/SPEC-controls.md | 802 --- playground/SPEC-shapeseq.md | 533 -- playground/TODOS.md | 9 - playground/a-immersive.html | 446 -- playground/b-workbench.html | 184 - playground/c-journey.html | 191 - playground/c15/c15_engine.wasm | Bin 269526 -> 0 bytes playground/c15/parameters.json | 5433 ----------------- playground/c15/worklet-processor.js | 592 -- playground/css/a-immersive.css | 2538 -------- playground/css/b-workbench.css | 836 --- playground/css/c-journey.css | 971 --- playground/css/style.css | 750 --- playground/designs.html | 211 - .../2026-03-21-ui-redesign-explorations.md | 103 - playground/faust/MODULAR_DESTINATIONS.md | 45 - playground/faust/README.md | 154 - playground/faust/additive-processor.js | 402 -- playground/faust/additive.dsp | 279 - playground/faust/additive.json | 1 - playground/faust/additive.wasm | Bin 94610 -> 0 bytes playground/faust/build.sh | 123 - playground/faust/eoc-compressor-processor.js | 173 - playground/faust/eoc-compressor.dsp | 34 - playground/faust/eoc-compressor.json | 1 - playground/faust/eoc-compressor.wasm | Bin 7859 -> 0 bytes playground/faust/eoc-delay-processor.js | 159 - playground/faust/eoc-delay.dsp | 103 - playground/faust/eoc-delay.json | 1 - playground/faust/eoc-delay.wasm | Bin 10699 -> 0 bytes playground/faust/eoc-eq-processor.js | 183 - playground/faust/eoc-eq.dsp | 38 - playground/faust/eoc-eq.json | 1 - playground/faust/eoc-eq.wasm | Bin 14053 -> 0 bytes playground/faust/eoc-master-processor.js | 149 - playground/faust/eoc-master.dsp | 85 - playground/faust/eoc-master.json | 1 - playground/faust/eoc-master.wasm | Bin 8739 -> 0 bytes playground/faust/eoc-reverb-processor.js | 178 - playground/faust/eoc-reverb.dsp | 52 - playground/faust/eoc-reverb.json | 1 - playground/faust/eoc-reverb.wasm | Bin 17599 -> 0 bytes playground/faust/eoc-saturation-processor.js | 149 - playground/faust/eoc-saturation.dsp | 71 - playground/faust/eoc-saturation.json | 1 - playground/faust/eoc-saturation.wasm | Bin 8239 -> 0 bytes playground/faust/faust-worklet-processor.js | 187 - playground/faust/fm-matrix-processor.js | 193 - playground/faust/fm-matrix.dsp | 243 - playground/faust/fm-matrix.json | 1 - playground/faust/fm-matrix.wasm | Bin 46317 -> 0 bytes playground/faust/gen-modular-dsp.py | 737 --- playground/faust/mod-pool.lib | 435 -- .../faust/modular-additive-processor.js | 257 - playground/faust/modular-additive.dsp | 1212 ---- playground/faust/modular-additive.json | 1 - playground/faust/modular-additive.wasm | Bin 351294 -> 0 bytes playground/faust/modular-fm-processor.js | 257 - playground/faust/modular-fm.dsp | 1198 ---- playground/faust/modular-fm.json | 1 - playground/faust/modular-fm.wasm | Bin 313094 -> 0 bytes .../faust/modular-subtractive-processor.js | 261 - .../faust/modular-subtractive-smoke.html | 255 - playground/faust/modular-subtractive.dsp | 1201 ---- playground/faust/modular-subtractive.json | 1 - playground/faust/modular-subtractive.wasm | Bin 317527 -> 0 bytes playground/index.html | 211 - playground/js/a-app.js | 4521 -------------- playground/js/app.js | 603 -- playground/js/audio/audio-canvas.js | 1590 ----- playground/js/b-app.js | 1290 ---- playground/js/c-app.js | 1244 ---- playground/js/eoc/eoc-chain.js | 347 -- playground/js/eoc/eoc-module.js | 294 - playground/js/eoc/index.js | 15 - .../js/eoc/modules/compressor-module.js | 129 - playground/js/eoc/modules/delay-module.js | 138 - playground/js/eoc/modules/eq-module.js | 143 - playground/js/eoc/modules/master-module.js | 123 - playground/js/eoc/modules/reverb-module.js | 147 - .../js/eoc/modules/saturation-module.js | 123 - playground/js/event-bus.js | 117 - playground/js/midi/midi-cc-map.js | 108 - playground/js/midi/midi-cc-presets.js | 60 - playground/js/midi/midi-output.js | 147 - playground/js/midi/presets/polybrute.json | 68 - playground/js/nisps/dataset.js | 121 - playground/js/nisps/iml.js | 324 - playground/js/nisps/layer.js | 133 - playground/js/nisps/mlp.js | 270 - playground/js/nisps/nisps-wasm-worker.js | 134 - playground/js/nisps/nisps-wasm.js | 612 -- playground/js/nisps/node.js | 153 - playground/js/nisps/osc-client.js | 241 - playground/js/shapeseq/chain-ui.js | 1250 ---- playground/js/shapeseq/chain.js | 397 -- playground/js/shapeseq/clock.js | 269 - playground/js/shapeseq/delta.js | 50 - playground/js/shapeseq/event-bus.js | 2 - playground/js/shapeseq/freeze-ui.js | 159 - playground/js/shapeseq/freeze.js | 246 - playground/js/shapeseq/mlp-mode.js | 54 - playground/js/shapeseq/param-map.js | 94 - playground/js/shapeseq/pattern.js | 354 -- playground/js/shapeseq/primitive.js | 254 - playground/js/shapeseq/primitives.js | 524 -- playground/js/shapeseq/prng.js | 137 - playground/js/shapeseq/projection.js | 124 - playground/js/shapeseq/seq-iml.js | 113 - playground/js/shapeseq/sequencer.js | 607 -- playground/js/shapeseq/session.js | 116 - playground/js/shapeseq/signal.js | 54 - playground/js/shapeseq/step-viz.js | 328 - playground/js/shapeseq/tests/delta.test.js | 193 - .../js/shapeseq/tests/freeze-pattern.test.js | 217 - .../js/shapeseq/tests/freeze-ui.test.js | 366 -- playground/js/shapeseq/tests/freeze.test.js | 293 - .../js/shapeseq/tests/midi-note.test.js | 243 - playground/js/shapeseq/tests/mlp-mode.test.js | 148 - .../js/shapeseq/tests/pattern-merge.test.js | 229 - .../js/shapeseq/tests/pitch-walker.test.js | 214 - .../shapeseq/tests/polyrhythm-chain.test.js | 210 - .../js/shapeseq/tests/polyrhythm.test.js | 183 - .../js/shapeseq/tests/projection.test.js | 208 - playground/js/shapeseq/tests/re-eval.test.js | 86 - playground/js/shapeseq/tests/run.sh | 9 - playground/js/shapeseq/tests/seq-iml.test.js | 87 - .../tests/sequencer-generation.test.js | 165 - playground/js/shapeseq/tests/session.test.js | 394 -- playground/js/shapeseq/tests/signal.test.js | 116 - playground/js/shapeseq/tests/smoke.test.js | 253 - .../js/shapeseq/tests/voice-mode.test.js | 232 - playground/js/synth/additive-engine.js | 14 - playground/js/synth/additive-presets.js | 204 - playground/js/synth/arpeggiator-worker.js | 166 - playground/js/synth/arpeggiator.js | 120 - playground/js/synth/c15-adapter.js | 175 - playground/js/synth/c15-bridge.js | 245 - playground/js/synth/engine-interface.js | 145 - playground/js/synth/faust-engine-base.js | 243 - playground/js/synth/faust-param-meta.js | 156 - playground/js/synth/fm-engine.js | 14 - playground/js/synth/fm-presets.js | 213 - playground/js/synth/midi-input.js | 160 - playground/js/synth/modular-engine.js | 946 --- playground/js/synth/modular-presets.js | 306 - playground/js/synth/osc-output.js | 118 - playground/js/synth/param-map.js | 304 - playground/js/synth/presets.js | 628 -- playground/js/ui/ab-compare.js | 153 - playground/js/ui/auto-explore.js | 160 - playground/js/ui/control-surface-ui.js | 884 --- playground/js/ui/control-surface.js | 405 -- playground/js/ui/controls.js | 157 - playground/js/ui/dev-panel.js | 222 - playground/js/ui/engine-switcher.js | 134 - playground/js/ui/eoc-chain-ui.js | 396 -- playground/js/ui/eoc-joystick.js | 73 - playground/js/ui/gamepad.js | 168 - playground/js/ui/gradient-flow.js | 261 - playground/js/ui/hand-tracker.js | 530 -- playground/js/ui/input-heatmap.js | 420 -- playground/js/ui/input-pipeline.js | 633 -- playground/js/ui/joy-map-enhanced.js | 745 --- playground/js/ui/joystick.js | 183 - playground/js/ui/modular-ui.js | 1531 ----- playground/js/ui/output-pipeline.js | 312 - playground/js/ui/param-display.js | 133 - playground/js/ui/param-pin.js | 151 - playground/js/ui/phase2-ui.js | 626 -- playground/js/ui/phase3-ui.js | 588 -- playground/js/ui/phase4-ui.js | 577 -- playground/js/ui/pressure-feedback.js | 194 - playground/js/ui/region-pin.js | 205 - playground/js/ui/session-presets.js | 291 - playground/js/ui/snapshot-stack.js | 169 - playground/js/ui/visualizer.js | 288 - playground/js/ui/weight-health.js | 218 - playground/osc-bridge/bridge.mjs | 275 - playground/osc-bridge/bridge.ts | 350 -- playground/osc-bridge/compile.sh | 62 - playground/osc-bridge/deno.lock | 18 - playground/osc-bridge/package-lock.json | 36 - playground/osc-bridge/test-receive.ts | 112 - playground/serve-coop.py | 18 - playground/serve.sh | 18 - playground/test-midi-receiver.py | 32 - playground/wasm/build.sh | 47 - playground/wasm/nisps.js | 2 - playground/wasm/nisps.wasm | Bin 36699 -> 0 bytes playground/wasm/nisps_bindings.cpp | 370 -- 194 files changed, 62073 deletions(-) delete mode 100644 playground/ARCHITECTURE.md delete mode 100644 playground/PLAN-solidjs-migration.md delete mode 100644 playground/README.md delete mode 100644 playground/SPEC-controls.md delete mode 100644 playground/SPEC-shapeseq.md delete mode 100644 playground/TODOS.md delete mode 100644 playground/a-immersive.html delete mode 100644 playground/b-workbench.html delete mode 100644 playground/c-journey.html delete mode 100755 playground/c15/c15_engine.wasm delete mode 100644 playground/c15/parameters.json delete mode 100644 playground/c15/worklet-processor.js delete mode 100644 playground/css/a-immersive.css delete mode 100644 playground/css/b-workbench.css delete mode 100644 playground/css/c-journey.css delete mode 100644 playground/css/style.css delete mode 100644 playground/designs.html delete mode 100644 playground/devlog/2026-03-21-ui-redesign-explorations.md delete mode 100644 playground/faust/MODULAR_DESTINATIONS.md delete mode 100644 playground/faust/README.md delete mode 100644 playground/faust/additive-processor.js delete mode 100644 playground/faust/additive.dsp delete mode 100644 playground/faust/additive.json delete mode 100644 playground/faust/additive.wasm delete mode 100755 playground/faust/build.sh delete mode 100644 playground/faust/eoc-compressor-processor.js delete mode 100644 playground/faust/eoc-compressor.dsp delete mode 100644 playground/faust/eoc-compressor.json delete mode 100644 playground/faust/eoc-compressor.wasm delete mode 100644 playground/faust/eoc-delay-processor.js delete mode 100644 playground/faust/eoc-delay.dsp delete mode 100644 playground/faust/eoc-delay.json delete mode 100644 playground/faust/eoc-delay.wasm delete mode 100644 playground/faust/eoc-eq-processor.js delete mode 100644 playground/faust/eoc-eq.dsp delete mode 100644 playground/faust/eoc-eq.json delete mode 100644 playground/faust/eoc-eq.wasm delete mode 100644 playground/faust/eoc-master-processor.js delete mode 100644 playground/faust/eoc-master.dsp delete mode 100644 playground/faust/eoc-master.json delete mode 100644 playground/faust/eoc-master.wasm delete mode 100644 playground/faust/eoc-reverb-processor.js delete mode 100644 playground/faust/eoc-reverb.dsp delete mode 100644 playground/faust/eoc-reverb.json delete mode 100644 playground/faust/eoc-reverb.wasm delete mode 100644 playground/faust/eoc-saturation-processor.js delete mode 100644 playground/faust/eoc-saturation.dsp delete mode 100644 playground/faust/eoc-saturation.json delete mode 100644 playground/faust/eoc-saturation.wasm delete mode 100644 playground/faust/faust-worklet-processor.js delete mode 100644 playground/faust/fm-matrix-processor.js delete mode 100644 playground/faust/fm-matrix.dsp delete mode 100644 playground/faust/fm-matrix.json delete mode 100644 playground/faust/fm-matrix.wasm delete mode 100644 playground/faust/gen-modular-dsp.py delete mode 100644 playground/faust/mod-pool.lib delete mode 100644 playground/faust/modular-additive-processor.js delete mode 100644 playground/faust/modular-additive.dsp delete mode 100644 playground/faust/modular-additive.json delete mode 100644 playground/faust/modular-additive.wasm delete mode 100644 playground/faust/modular-fm-processor.js delete mode 100644 playground/faust/modular-fm.dsp delete mode 100644 playground/faust/modular-fm.json delete mode 100644 playground/faust/modular-fm.wasm delete mode 100644 playground/faust/modular-subtractive-processor.js delete mode 100644 playground/faust/modular-subtractive-smoke.html delete mode 100644 playground/faust/modular-subtractive.dsp delete mode 100644 playground/faust/modular-subtractive.json delete mode 100644 playground/faust/modular-subtractive.wasm delete mode 100644 playground/index.html delete mode 100644 playground/js/a-app.js delete mode 100644 playground/js/app.js delete mode 100644 playground/js/audio/audio-canvas.js delete mode 100644 playground/js/b-app.js delete mode 100644 playground/js/c-app.js delete mode 100644 playground/js/eoc/eoc-chain.js delete mode 100644 playground/js/eoc/eoc-module.js delete mode 100644 playground/js/eoc/index.js delete mode 100644 playground/js/eoc/modules/compressor-module.js delete mode 100644 playground/js/eoc/modules/delay-module.js delete mode 100644 playground/js/eoc/modules/eq-module.js delete mode 100644 playground/js/eoc/modules/master-module.js delete mode 100644 playground/js/eoc/modules/reverb-module.js delete mode 100644 playground/js/eoc/modules/saturation-module.js delete mode 100644 playground/js/event-bus.js delete mode 100644 playground/js/midi/midi-cc-map.js delete mode 100644 playground/js/midi/midi-cc-presets.js delete mode 100644 playground/js/midi/midi-output.js delete mode 100644 playground/js/midi/presets/polybrute.json delete mode 100644 playground/js/nisps/dataset.js delete mode 100644 playground/js/nisps/iml.js delete mode 100644 playground/js/nisps/layer.js delete mode 100644 playground/js/nisps/mlp.js delete mode 100644 playground/js/nisps/nisps-wasm-worker.js delete mode 100644 playground/js/nisps/nisps-wasm.js delete mode 100644 playground/js/nisps/node.js delete mode 100644 playground/js/nisps/osc-client.js delete mode 100644 playground/js/shapeseq/chain-ui.js delete mode 100644 playground/js/shapeseq/chain.js delete mode 100644 playground/js/shapeseq/clock.js delete mode 100644 playground/js/shapeseq/delta.js delete mode 100644 playground/js/shapeseq/event-bus.js delete mode 100644 playground/js/shapeseq/freeze-ui.js delete mode 100644 playground/js/shapeseq/freeze.js delete mode 100644 playground/js/shapeseq/mlp-mode.js delete mode 100644 playground/js/shapeseq/param-map.js delete mode 100644 playground/js/shapeseq/pattern.js delete mode 100644 playground/js/shapeseq/primitive.js delete mode 100644 playground/js/shapeseq/primitives.js delete mode 100644 playground/js/shapeseq/prng.js delete mode 100644 playground/js/shapeseq/projection.js delete mode 100644 playground/js/shapeseq/seq-iml.js delete mode 100644 playground/js/shapeseq/sequencer.js delete mode 100644 playground/js/shapeseq/session.js delete mode 100644 playground/js/shapeseq/signal.js delete mode 100644 playground/js/shapeseq/step-viz.js delete mode 100644 playground/js/shapeseq/tests/delta.test.js delete mode 100644 playground/js/shapeseq/tests/freeze-pattern.test.js delete mode 100644 playground/js/shapeseq/tests/freeze-ui.test.js delete mode 100644 playground/js/shapeseq/tests/freeze.test.js delete mode 100644 playground/js/shapeseq/tests/midi-note.test.js delete mode 100644 playground/js/shapeseq/tests/mlp-mode.test.js delete mode 100644 playground/js/shapeseq/tests/pattern-merge.test.js delete mode 100644 playground/js/shapeseq/tests/pitch-walker.test.js delete mode 100644 playground/js/shapeseq/tests/polyrhythm-chain.test.js delete mode 100644 playground/js/shapeseq/tests/polyrhythm.test.js delete mode 100644 playground/js/shapeseq/tests/projection.test.js delete mode 100644 playground/js/shapeseq/tests/re-eval.test.js delete mode 100755 playground/js/shapeseq/tests/run.sh delete mode 100644 playground/js/shapeseq/tests/seq-iml.test.js delete mode 100644 playground/js/shapeseq/tests/sequencer-generation.test.js delete mode 100644 playground/js/shapeseq/tests/session.test.js delete mode 100644 playground/js/shapeseq/tests/signal.test.js delete mode 100644 playground/js/shapeseq/tests/smoke.test.js delete mode 100644 playground/js/shapeseq/tests/voice-mode.test.js delete mode 100644 playground/js/synth/additive-engine.js delete mode 100644 playground/js/synth/additive-presets.js delete mode 100644 playground/js/synth/arpeggiator-worker.js delete mode 100644 playground/js/synth/arpeggiator.js delete mode 100644 playground/js/synth/c15-adapter.js delete mode 100644 playground/js/synth/c15-bridge.js delete mode 100644 playground/js/synth/engine-interface.js delete mode 100644 playground/js/synth/faust-engine-base.js delete mode 100644 playground/js/synth/faust-param-meta.js delete mode 100644 playground/js/synth/fm-engine.js delete mode 100644 playground/js/synth/fm-presets.js delete mode 100644 playground/js/synth/midi-input.js delete mode 100644 playground/js/synth/modular-engine.js delete mode 100644 playground/js/synth/modular-presets.js delete mode 100644 playground/js/synth/osc-output.js delete mode 100644 playground/js/synth/param-map.js delete mode 100644 playground/js/synth/presets.js delete mode 100644 playground/js/ui/ab-compare.js delete mode 100644 playground/js/ui/auto-explore.js delete mode 100644 playground/js/ui/control-surface-ui.js delete mode 100644 playground/js/ui/control-surface.js delete mode 100644 playground/js/ui/controls.js delete mode 100644 playground/js/ui/dev-panel.js delete mode 100644 playground/js/ui/engine-switcher.js delete mode 100644 playground/js/ui/eoc-chain-ui.js delete mode 100644 playground/js/ui/eoc-joystick.js delete mode 100644 playground/js/ui/gamepad.js delete mode 100644 playground/js/ui/gradient-flow.js delete mode 100644 playground/js/ui/hand-tracker.js delete mode 100644 playground/js/ui/input-heatmap.js delete mode 100644 playground/js/ui/input-pipeline.js delete mode 100644 playground/js/ui/joy-map-enhanced.js delete mode 100644 playground/js/ui/joystick.js delete mode 100644 playground/js/ui/modular-ui.js delete mode 100644 playground/js/ui/output-pipeline.js delete mode 100644 playground/js/ui/param-display.js delete mode 100644 playground/js/ui/param-pin.js delete mode 100644 playground/js/ui/phase2-ui.js delete mode 100644 playground/js/ui/phase3-ui.js delete mode 100644 playground/js/ui/phase4-ui.js delete mode 100644 playground/js/ui/pressure-feedback.js delete mode 100644 playground/js/ui/region-pin.js delete mode 100644 playground/js/ui/session-presets.js delete mode 100644 playground/js/ui/snapshot-stack.js delete mode 100644 playground/js/ui/visualizer.js delete mode 100644 playground/js/ui/weight-health.js delete mode 100644 playground/osc-bridge/bridge.mjs delete mode 100644 playground/osc-bridge/bridge.ts delete mode 100755 playground/osc-bridge/compile.sh delete mode 100644 playground/osc-bridge/deno.lock delete mode 100644 playground/osc-bridge/package-lock.json delete mode 100644 playground/osc-bridge/test-receive.ts delete mode 100755 playground/serve-coop.py delete mode 100755 playground/serve.sh delete mode 100644 playground/test-midi-receiver.py delete mode 100755 playground/wasm/build.sh delete mode 100644 playground/wasm/nisps.js delete mode 100755 playground/wasm/nisps.wasm delete mode 100644 playground/wasm/nisps_bindings.cpp diff --git a/playground/ARCHITECTURE.md b/playground/ARCHITECTURE.md deleted file mode 100644 index c5e8175..0000000 --- a/playground/ARCHITECTURE.md +++ /dev/null @@ -1,42 +0,0 @@ -# Playground Architecture - -## Event Systems - -### Event Bus (preferred for new code) - -The shared event bus at `js/event-bus.js` is the preferred event system for all new playground code. It provides namespaced pub/sub with wildcard support and automatic timestamping. - -**Import:** - -```js -import { EventBus, getDefaultBus, SEQ, ML, UI } from './js/event-bus.js'; -``` - -**API:** - -| Method | Description | -|--------|-------------| -| `new EventBus(audioCtx?)` | Create a new bus. Optional AudioContext for `seq.*` timestamps. | -| `bus.on(event, cb)` | Subscribe. Exact (`'seq.step'`) or wildcard (`'seq.*'`). | -| `bus.off(event, cb)` | Unsubscribe. Same signature as `on()`. | -| `bus.emit(event, data?)` | Emit. A `timestamp` field is added automatically. | -| `bus.setAudioContext(ctx)` | Set or replace the AudioContext for `seq.*` timestamps. | -| `getDefaultBus(audioCtx?)` | Get (or lazily create) the shared singleton bus. | - -**Namespace conventions:** - -| Prefix | Constants | Purpose | -|--------|-----------|---------| -| `seq.*` | `SEQ.STEP`, `SEQ.NOTE_ON`, `SEQ.NOTE_OFF`, `SEQ.PARAM_CHANGE`, `SEQ.LOOP_START` | Sequencer transport and timing events. Timestamped via `AudioContext.currentTime`. | -| `ml.*` | `ML.TRAINED`, `ML.FROZEN`, `ML.UNFROZEN`, `ML.DELTA_UPDATE` | ML engine state changes. | -| `ui.*` | `UI.PARAM_SELECT`, `UI.CHAIN_EDIT`, `UI.PRESET_LOAD`, `UI.FREEZE_TOGGLE` | UI interaction events. | - -New features should define their own namespace constants following the same pattern. - -**Callbacks** receive `(data, eventName)` where `data` always includes a `timestamp` field (`AudioContext.currentTime` for `seq.*`, `performance.now()` for everything else). - -### DOM CustomEvents (existing code) - -Existing code uses DOM `CustomEvent` dispatched on DOM elements (e.g. `controlsurface:change`, `eoc:change`). These work fine and should **not** be migrated at this time. Both systems coexist without conflict. - -When the app is rewritten in the future, the goal is to unify on the event bus and retire DOM CustomEvents for inter-module communication. diff --git a/playground/PLAN-solidjs-migration.md b/playground/PLAN-solidjs-migration.md deleted file mode 100644 index 84c558b..0000000 --- a/playground/PLAN-solidjs-migration.md +++ /dev/null @@ -1,831 +0,0 @@ -# SolidJS Migration Plan - -## Decision Record - -| Decision | Choice | -|----------|--------| -| Framework | SolidJS (reactivity + components) | -| Build | Vite + vite-plugin-solid | -| Apps | Immersive only, extensible for future layouts | -| Core integration | Deep — ML/synth/audio modeled as SolidJS stores/signals | -| Output reactivity | Batch at frame rate (one signal update per rAF) | -| Event system | Unified signal bus with topics (replaces EventBus + CustomEvents) | -| Migration strategy | Big bang rewrite, old code as reference | -| Routing | None — signals only, URL params for config | -| Canvas | Components with refs, internal render loops | -| Mobile | Desktop-first, mobile later | -| Layout | Headless UI primitives (Drawer, Overlay, Panel, Dock) | - ---- - -## Architecture Overview - -``` -src/ -├── index.html # Single entry point -├── App.tsx # Root component, provider tree -├── vite.config.ts -│ -├── core/ # Framework-agnostic engines (transplanted) -│ ├── wasm/ # WASM binaries + Emscripten glue (unchanged) -│ │ ├── nisps.wasm -│ │ ├── nisps.js -│ │ └── nisps-wasm-worker.js -│ ├── iml.ts # WasmIML wrapper (thin adaptation) -│ ├── dataset.ts # Training dataset (FIFO ring buffer) -│ ├── synth/ # Synth engines (transplanted, minimal changes) -│ │ ├── engine-interface.ts -│ │ ├── c15-adapter.ts -│ │ ├── c15-bridge.ts -│ │ ├── additive-engine.ts -│ │ ├── fm-engine.ts -│ │ ├── faust-engine-base.ts -│ │ ├── param-map.ts # 126 C15 params (data, unchanged) -│ │ └── presets.ts # Synth presets (data, unchanged) -│ ├── audio/ -│ │ ├── arpeggiator.ts -│ │ ├── arpeggiator-worker.js -│ │ ├── audio-canvas.ts -│ │ └── midi-io.ts # MIDIInput + MIDIOutput merged -│ ├── eoc/ # Effects chain (transplanted) -│ │ ├── eoc-chain.ts -│ │ ├── eoc-module.ts -│ │ └── modules/ # Individual effect modules -│ └── shapeseq/ # Sequencer (transplanted, minimal changes) -│ ├── sequencer.ts -│ ├── chain.ts -│ ├── clock.ts -│ ├── pattern.ts -│ ├── primitives.ts -│ └── ... -│ -├── bus/ # Signal bus -│ └── signal-bus.ts # createSignalBus(), topic(), match(), emit() -│ -├── stores/ # SolidJS stores (the reactive state layer) -│ ├── ml-store.ts # IML instances, outputs, loss, training state -│ ├── input-store.ts # Joystick position, input mode, pipeline config -│ ├── output-store.ts # Output mode, overrides, routing config -│ ├── synth-store.ts # Active engine, arpeggiator, volume, presets -│ ├── eoc-store.ts # EOC chain state, nisps mode, modules -│ ├── midi-store.ts # MIDI CC map, devices, output state -│ ├── session-store.ts # Persistence (save/load localStorage) -│ └── ui-store.ts # Drawer state, active panels, help seen -│ -├── hooks/ # Reactive glue (SolidJS "hooks" / composables) -│ ├── useInference.ts # Per-frame inference loop -│ ├── useTraining.ts # Train/thumbs-up/thumbs-down actions -│ ├── useOutputRouting.ts # Route outputs → synth/visual/midi/audio-canvas -│ ├── useInputPipeline.ts # Deadzone, zoom, curve, smoothing, momentum -│ ├── useOutputPipeline.ts # Slew rate, smoothing, freeze gate -│ ├── usePersistence.ts # Auto-save/load, URL param parsing -│ ├── useAudioContext.ts # Lazy AudioContext creation, resume on gesture -│ ├── useGamepad.ts # Gamepad polling -│ └── useKeyboard.ts # Keyboard shortcuts -│ -├── primitives/ # Headless UI primitives -│ ├── Drawer.tsx # Slide-in panel (headless) -│ ├── Overlay.tsx # Floating positioned element -│ ├── Panel.tsx # Collapsible content section -│ ├── Dock.tsx # Icon bar with drawer triggers -│ ├── PillToggle.tsx # Segmented toggle (output mode, input mode) -│ ├── Slider.tsx # Range input with label/value display -│ └── Canvas.tsx # Canvas wrapper with ref + resize observer -│ -├── components/ # Feature components -│ ├── app/ -│ │ └── ImmersiveLayout.tsx # Main layout shell -│ ├── input/ -│ │ ├── Joystick.tsx # Virtual joystick (pointer events) -│ │ ├── JoyMap.tsx # Zoom minimap + trails (canvas) -│ │ └── InputModeToggle.tsx -│ ├── output/ -│ │ ├── FlowField.tsx # Particle visualizer (canvas) -│ │ ├── SynthVisualizer.tsx # Param bar chart (canvas) -│ │ ├── Heatmap.tsx # Parameter heatmap grid -│ │ └── OutputModeToggle.tsx -│ ├── training/ -│ │ ├── TrainingControls.tsx # Add/Train/Clear/Randomize buttons -│ │ ├── RLControls.tsx # Thumbs up/down, noise display -│ │ ├── LossPlot.tsx # Loss history (canvas) -│ │ └── StatusLine.tsx # Example count, loss, mode -│ ├── synth/ -│ │ ├── SynthControls.tsx # Start/stop, volume, arp controls -│ │ ├── PresetSelector.tsx # Synth preset chips -│ │ ├── EngineSwitcher.tsx # Engine dropdown -│ │ └── ParamEditor.tsx # Per-param override popup -│ ├── eoc/ -│ │ ├── EOCPanel.tsx # Effects chain UI -│ │ ├── EOCModule.tsx # Single effect module card -│ │ └── EOCJoystick.tsx # Independent mode joystick -│ ├── midi/ -│ │ ├── MIDIInputPanel.tsx # Device select, CC mapping -│ │ ├── MIDICCPanel.tsx # CC output config -│ │ └── MIDIPresets.tsx # CC preset management -│ ├── shapeseq/ -│ │ ├── ShapeSeqPanel.tsx # Sequencer controls -│ │ ├── StepVisualizer.tsx # Step display (canvas) -│ │ └── ChainBuilder.tsx # Primitive chain editor -│ ├── controls/ -│ │ ├── ControlSurface.tsx # Boldness/Memory/Precision axes -│ │ ├── InputHeatmap.tsx # 2D input space heatmap (canvas) -│ │ └── EngineParams.tsx # Spread, noise, decay sliders -│ └── debug/ -│ ├── DevPanel.tsx # Debug tools -│ ├── WeightHealth.tsx # Weight magnitude histogram (canvas) -│ └── GradientFlow.tsx # Per-layer gradient analysis (canvas) -│ -├── actions/ # Imperative operations (not reactive) -│ ├── resize-mlp.ts # Resize MLP, warm-start weights -│ ├── apply-overrides.ts # Override application logic -│ └── export-import.ts # Session export/import -│ -└── assets/ - ├── c15/ # C15 WASM synth binary - └── faust/ # Faust DSP files -``` - ---- - -## Store Design (Deep Integration) - -### ML Store (`stores/ml-store.ts`) - -```typescript -import { createStore, produce } from "solid-js/store"; -import { createSignal } from "solid-js"; - -// The hot path: batched per-frame, NOT per-parameter -const [outputs, setOutputs] = createSignal(new Float32Array(126)); -const [rawParams, setRawParams] = createSignal(new Float32Array(126)); - -const [mlState, setMlState] = createStore({ - // IML instances (not reactive themselves — opaque handles) - imlJoy: null as WasmIML | null, - imlHand: null as WasmIML | null, - activeIml: 'joy' as 'joy' | 'hand', - - // Reactive state derived from IML - outputCount: 126, - loss: null as number | null, - lossHistory: [] as number[], - exampleCount: 0, - isTraining: false, - layerStats: null, - - // RL config - spreadLevel: 0.6, - noiseLevel: 0.05, - rlDecay: 0.97, - learningRate: 0.1, - maxIterations: 50, - - // Undo - undoStack: [] as Float32Array[], -}); - -// The IML instances live OUTSIDE the store (mutable, non-proxy-safe). -// The store tracks their *state* reactively. -// After each inference: setOutputs(iml.getOutputs()) -// After each train: setMlState({ loss, exampleCount, lossHistory }) -``` - -**Key design choice**: `Float32Array` outputs are a signal, not a store property. Stores use proxies which don't play well with typed arrays. A signal holding the array reference, replaced each frame, gives us batch reactivity cheaply. - -### Input Store (`stores/input-store.ts`) - -```typescript -const [inputState, setInputState] = createStore({ - mode: 'joystick' as 'joystick' | 'hands', - joyX: 0.5, - joyY: 0.5, - isDragging: false, - followMode: false, - - // Input pipeline config - pipeline: { - deadzone: 0.02, - zoom: 1.0, - zoomAnchorX: 0.5, - zoomAnchorY: 0.5, - anchorMode: 'auto' as 'auto' | 'sticky' | 'center', - curve: 1.0, // 1.0 = linear - smoothing: 0.0, // 0.0 = none - momentumZoom: false, - }, -}); -``` - -### Output Store (`stores/output-store.ts`) - -```typescript -const [outputState, setOutputState] = createStore({ - mode: 'visual' as 'visual' | 'synth' | 'midi-cc' | 'audio-canvas', - - // Per-mode overrides (arrays of { min, max, curve, muted, fixedValue }) - visualOverrides: [] as ParamOverride[], - synthOverrides: { - type: 'grouped' as 'grouped' | 'flat', - groups: [] as GroupOverride[], // C15 grouped - flat: [] as ParamOverride[], // Faust flat - }, - midiCCOverrides: [] as ParamOverride[], - audioCanvasOverrides: [] as ParamOverride[], - - // Output pipeline config - pipeline: { - globalCurve: 1.0, - smoothing: 0.0, - slewRate: 1.0, - freezeGate: false, - }, -}); -``` - -### Synth Store (`stores/synth-store.ts`) - -```typescript -const [synthState, setSynthState] = createStore({ - engineId: 'shaper-feedback' as string, - engine: null as SynthEngine | null, // Opaque handle - isRunning: false, - volume: 0.7, - - // Arpeggiator - arp: { - enabled: false, - tempo: 120, - progression: 'major' as string, - octaves: 1, - offset: 0, - }, - - // Current preset - presetId: null as string | null, -}); -``` - ---- - -## Signal Bus Design (`bus/signal-bus.ts`) - -```typescript -import { createSignal, createMemo } from "solid-js"; - -type Topic = { - (): T | undefined; // Read (reactive) - fire: (data: T) => void; // Write (imperative) -}; - -export function createSignalBus() { - const topics = new Map>(); - - function topic(name: string): Topic { - if (!topics.has(name)) { - const [get, set] = createSignal(undefined, { equals: false }); - topics.set(name, [get, set]); - } - const [get, set] = topics.get(name)!; - const accessor = () => get() as T | undefined; - accessor.fire = (data: T) => set(() => data); - return accessor as Topic; - } - - function emit(name: string, data: T) { - topic(name).fire(data); - } - - function match(pattern: string) { - // 'seq.*' → derived signal merging all seq.* topics - const prefix = pattern.replace('*', ''); - return createMemo(() => { - let latest: { name: string; data: any } | undefined; - for (const [name, [get]] of topics) { - if (name.startsWith(prefix)) { - const val = get(); - if (val !== undefined) { - latest = { name, data: val }; - } - } - } - return latest; - }); - } - - return { topic, emit, match }; -} -``` - -**Usage equals: false** is critical — it means every `emit()` triggers subscribers even if the data is identical. This matches event semantics (every `seq.step` matters, even if the step number repeats). - ---- - -## Inference Loop (`hooks/useInference.ts`) - -```typescript -import { createEffect, onCleanup } from "solid-js"; - -export function useInference(mlStore, inputStore, outputStore, bus) { - let rafId: number; - - function tick() { - const iml = mlStore.activeIml === 'joy' ? mlStore.imlJoy : mlStore.imlHand; - if (!iml) { rafId = requestAnimationFrame(tick); return; } - - // Set inputs - iml.setInput(0, inputStore.joyX); - iml.setInput(1, inputStore.joyY); - - // Forward pass - iml.process(); - const raw = iml.getOutputs(); - - // Batch update — single signal write per frame - setOutputs(new Float32Array(raw)); - - // Route to active output - routeOutputs(raw, outputStore, synthStore, bus); - - // Fire bus event (for non-UI subscribers like sequencer) - bus.emit('ml.inference', { outputs: raw }); - - rafId = requestAnimationFrame(tick); - } - - rafId = requestAnimationFrame(tick); - onCleanup(() => cancelAnimationFrame(rafId)); -} -``` - ---- - -## Output Routing (`hooks/useOutputRouting.ts`) - -```typescript -function routeOutputs(raw: Float32Array, outputStore, synthStore, bus) { - const mode = outputStore.mode; - const overrides = getOverridesForMode(mode, outputStore); - - // Apply overrides (pure function, no side effects) - const processed = applyOverrides(raw, overrides); - - switch (mode) { - case 'visual': - bus.emit('output.visual', processed); - break; - case 'synth': - // Throttled: only send to engine at 20fps - bus.emit('output.synth', processed); - break; - case 'midi-cc': - bus.emit('output.midi', processed); - break; - case 'audio-canvas': - bus.emit('output.audiocanvas', processed); - break; - } - - // Always update heatmap (unthrottled — it's just DOM) - bus.emit('output.heatmap', raw); -} -``` - -Each output component subscribes to its own bus topic. The FlowField listens to `output.visual`, the SynthVisualizer to `output.synth`, etc. Throttling for synth param sends lives inside the synth subscriber, not in the routing. - ---- - -## Component Examples - -### Joystick Component - -```tsx -const Joystick = () => { - const { inputState, setInputState } = useInputStore(); - const bus = useBus(); - - const onPointerDown = (e: PointerEvent) => { - (e.target as Element).setPointerCapture(e.pointerId); - const rect = (e.target as Element).getBoundingClientRect(); - const x = (e.clientX - rect.left) / rect.width; - const y = 1 - (e.clientY - rect.top) / rect.height; - setInputState({ joyX: x, joyY: y, isDragging: true }); - }; - - const onPointerMove = (e: PointerEvent) => { - if (!inputState.isDragging) return; - const rect = (e.target as Element).getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const y = Math.max(0, Math.min(1, 1 - (e.clientY - rect.top) / rect.height)); - setInputState({ joyX: x, joyY: y }); - }; - - const onPointerUp = () => setInputState({ isDragging: false }); - - return ( -
-
-
- ); -}; -``` - -### FlowField (Canvas Component) - -```tsx -const FlowField = () => { - let canvasRef: HTMLCanvasElement; - const bus = useBus(); - const visualData = bus.topic('output.visual'); - - onMount(() => { - const ctx = canvasRef.getContext('2d')!; - const vis = new FlowFieldVisualizer(ctx); // Transplanted engine - - // Internal render loop — reads signal each frame - let raf: number; - const loop = () => { - const data = visualData(); - if (data) vis.setParams(data); - vis.draw(); - raf = requestAnimationFrame(loop); - }; - raf = requestAnimationFrame(loop); - onCleanup(() => cancelAnimationFrame(raf)); - }); - - return ; -}; -``` - -### Heatmap (Reactive DOM) - -```tsx -const Heatmap = () => { - const bus = useBus(); - const heatmapData = bus.topic('output.heatmap'); - const { outputState } = useOutputStore(); - const paramMeta = () => getParamMeta(outputState.mode); - - return ( -
- - {(param, i) => ( - heatmapData()?.[i()] ?? 0} - /> - )} - -
- ); -}; - -const HeatmapCell = (props) => { - const width = () => `${(props.value() * 100).toFixed(1)}%`; - return ( -
-
-
- ); -}; -``` - ---- - -## Provider Tree (`App.tsx`) - -```tsx -const App = () => { - return ( - - - - - - - - - - - - - - - - ); -}; -``` - -Each provider creates its store and exposes it via context. The `SessionProvider` handles persistence (auto-save on interval, load on mount, URL param parsing). - ---- - -## Migration Phases - -### Phase 1: Skeleton + Core Loop - -**Goal**: Joystick → MLP → FlowField working in SolidJS. **Also**: validate all risky integration points early. - -1. `npm create vite@latest playground-solid -- --template solid-ts` -2. Set up project structure (`core/`, `stores/`, `components/`, `bus/`) -3. **Configure Vite**: COOP/COEP headers, WASM asset handling (spike `public/` vs `locateFile`) -4. Transplant WASM files (`nisps.wasm`, `nisps.js`, worker) into `core/wasm/` -5. **Spike**: Verify WASM loads and runs inference in Vite dev server (before building any UI) -6. **Spike**: Verify C15 WASM + SharedArrayBuffer works with COOP/COEP headers -7. Implement `createSignalBus()` -8. Implement `ml-store.ts` and `input-store.ts` (minimal) -9. Implement reactive inference (`createEffect` on input signals, not rAF) -10. Build `Joystick` component -11. Build `FlowField` component (transplant `FlowFieldVisualizer` class, own rAF loop) -12. Wire it up in `App.tsx` with minimal `ImmersiveLayout` -13. Verify: drag joystick → see particles respond -14. Add worker `dispose()` to IML, verify cleanup in `onCleanup()` - -**Validates**: SolidJS + WASM integration, COOP/COEP, signal bus, reactive inference, canvas components, worker lifecycle. - -### Phase 2: Training + RL - -**Goal**: Full ML interaction loop. - -1. Implement `useTraining` hook (add example, train async, thumbs up/down) -2. Implement `output-store.ts` (visual overrides only for now) -3. Build `TrainingControls` (Add/Train/Clear/Randomize) -4. Build `RLControls` (thumbs up/down, noise display) -5. Build `LossPlot` (canvas) -6. Build `StatusLine` -7. Implement undo stack in `ml-store` -8. Implement `useOutputRouting` (visual mode only) - -**Validates**: Async training with UI updates, RL feedback loop, undo. - -### Phase 3: Synth Integration - -**Goal**: Synth output mode working. - -1. Transplant `c15-adapter.ts`, `c15-bridge.ts`, `param-map.ts`, `presets.ts` -2. Implement `synth-store.ts` -3. Implement `useAudioContext` (lazy creation, gesture resume) -4. Build `SynthControls` (start/stop, volume) -5. Build `SynthVisualizer` (canvas, param bars) -6. Build `PresetSelector` -7. Build `EngineSwitcher` (C15, Additive, FM) -8. Implement synth output routing with throttling -9. Implement `output-store` grouped overrides for C15 -10. Build `Heatmap` + `ParamEditor` popup - -**Validates**: Multi-engine support, override system, audio integration. - -### Phase 4: Layout + UI Primitives - -**Goal**: Full immersive UI. - -1. Build headless primitives: `Drawer`, `Overlay`, `Dock`, `PillToggle`, `Slider` -2. Build `ImmersiveLayout` (fullscreen canvas + floating controls + drawer stack) -3. Build `ControlSurface` (Boldness/Memory/Precision compound axes) -4. Implement `useInputPipeline` (deadzone, zoom, curve, smoothing) -5. Build `JoyMap` (zoom minimap with trails, canvas) -6. Build `InputHeatmap` (2D color field, canvas) -7. Implement `useKeyboard` (shortcuts: 1/2/Z etc.) -8. Wire drawer system (dock icons → drawer toggles) - -**Validates**: Layout primitives, compound axis system, input pipeline. - -### Phase 5: Remaining Features - -**Goal**: Feature parity with a-immersive. - -1. **MIDI**: `MIDIInputPanel`, `MIDICCPanel`, `MIDIPresets`, midi-cc output mode -2. **Audio Canvas**: Transplant, wire as output mode -3. **EOC Chain**: `EOCPanel`, `EOCModule`, EOC joystick, nisps modes (Shared/Linked/Independent) -4. **Arpeggiator**: Controls, worker integration -5. **Output Pipeline**: `useOutputPipeline` (slew, smoothing, freeze) -6. **Weight Health**: `WeightHealth`, `GradientFlow` (canvas) -7. **Session Presets**: Save/load full state, URL sharing -8. **Persistence**: Auto-save, localStorage round-trip -9. **Debug probe**: `window.__nisps` when `?debug=1` - -### Phase 6: ShapeSeq - -**Goal**: Sequencer as optional subsystem. - -1. Transplant sequencer core (already modular) -2. Build `ShapeSeqPanel`, `StepVisualizer`, `ChainBuilder` -3. Wire to signal bus (`seq.*` topics) -4. Lazy-load when enabled via URL param - -### Phase 7: Polish + Tests - -1. Port Playwright e2e tests (update selectors for new DOM) -2. Add component-level tests (vitest + solid-testing-library) -3. Responsive CSS pass -4. Performance profiling (ensure 60fps inference + rendering) -5. Accessibility pass on interactive elements - ---- - -## Fresh Eyes: What the Plan Was Missing - -### 1. COOP/COEP Headers & SharedArrayBuffer (Blocker) - -The C15 synth engine requires `SharedArrayBuffer` for its audio ring buffer. This means the server **must** send: -``` -Cross-Origin-Opener-Policy: same-origin -Cross-Origin-Embedder-Policy: require-corp -``` - -The current codebase uses `serve-coop.py` for this. **Vite dev server must be configured** with these headers or C15 synth mode will fail silently. This should be validated in Phase 1, not Phase 3. - -```typescript -// vite.config.ts -export default defineConfig({ - plugins: [solidPlugin()], - server: { - headers: { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, - }, -}); -``` - -### 2. Worker Lifecycle (Currently Leaked) - -Workers are **never terminated** in the current code: -- `nisps-wasm-worker.js` — created lazily on first `trainAsync()`, lives forever -- `arpeggiator-worker.js` — created on arpeggiator init, lives forever - -In a SolidJS app where components mount/unmount, workers must be cleaned up in `onCleanup()`. The current code leaks them because the page never changes — the SolidJS version will need explicit termination, especially if engine switching or mode changes recreate IML instances. - -**Add to architecture**: `core/iml.ts` must expose a `dispose()` method that terminates the worker. `synth-store` cleanup must terminate the arpeggiator worker. - -### 3. AudioContext Lifecycle Gotchas - -- **C15Bridge creates its own AudioContext internally**; Faust engines require one passed in. This API inconsistency means `useAudioContext()` can't own the single context — C15 creates its own. -- **No `dispose()` on C15Bridge** — relies on GC of the AudioContext. Engine switching disconnects audio nodes (line 1121 in a-app.js) but doesn't explicitly close the context. -- **AudioWorklet module loading** via `addModule()` — browsers deduplicate, but if the context is recreated, modules must be re-registered. - -**Add to architecture**: The `useAudioContext` hook needs to handle two patterns: "I own the context" (Faust) vs "the engine owns the context" (C15). Consider normalizing this in the transplant — make C15Bridge accept an external AudioContext. - -### 4. Debug Probe Synchrony Contract - -The Playwright tests depend on `window.__nisps` methods being **synchronous**: -- `setInputs(x, y)` → synchronously runs inference + routes outputs + updates heatmap -- `thumbsUp()` → synchronously calls `addExample()` before returning -- `saveState()` → synchronously writes to localStorage -- `train()` → synchronously trains and returns loss (not the async variant) - -In SolidJS, store updates are batched by default. If `setInputs()` writes to a signal but the effect that runs inference hasn't flushed yet, the probe will return stale data. **The probe must bypass SolidJS reactivity** and call imperative methods directly on the IML instance. - -**Add to Phase 5 (debug probe)**: Build the probe as a direct imperative bridge to the IML/store internals, not as a reactive consumer. Use `batch()` or `untrack()` where needed. - -### 5. MIDI CC Map — Engine-Scoped Dynamic Storage Keys - -MIDI CC maps are stored with engine-scoped localStorage keys: `nisps-midi-cc-map:${activeEngine.id}`. When the engine switches: -1. Current map is saved to the old engine's key -2. New map is loaded from the new engine's key -3. `midiCCMap` and `midiCCOverrides` arrays are mutated in-place - -In SolidJS, in-place array mutation (`arr.length = 0; arr.push(...)`) won't trigger reactivity. The `midi-store` must use `setStore(produce(...))` or replace arrays entirely. The engine-scoped key pattern needs to be replicated in `session-store.ts`. - -### 6. EOC Chain — Mutable Audio Graph + MLP Resize Cascade - -When EOC modules are added/removed in "Shared" mode: -1. MLP output count changes → MLP must be destroyed and recreated -2. Training examples are lost (different output dimensionality) -3. Audio graph nodes must be reconnected -4. Heatmap must be rebuilt - -This is a **cascade of side effects** triggered by a single user action. In the current code it's handled by a `window.addEventListener('eoc:change', ...)` handler that orchestrates everything imperatively. - -In SolidJS, this should be modeled as: EOC module list is a store → derived signal computes total output count → `createEffect` watches output count and triggers MLP resize when it changes. But the MLP resize is async (WASM allocation) and has a confirmation dialog ("this will clear examples"). **Effects can't show dialogs**. - -**Proposed pattern**: EOC store exposes a `pendingResize` signal. A component watches it and shows the confirmation UI. On confirm, an action triggers the actual resize. Don't try to make this fully reactive — keep it as an explicit action flow. - -### 7. WASM + Emscripten Glue Loading in Vite - -The WASM is loaded via Emscripten's `nisps.js` glue file, which does its own `fetch()` of `nisps.wasm` using a relative path. Vite's asset handling will hash filenames in production builds, breaking the hardcoded path. - -**Options**: -- Configure Vite to copy WASM files to `public/` (no hashing, always available at known path) -- Modify the Emscripten glue to accept a custom `locateFile` override -- Use Vite's `?url` import to get the resolved asset path and pass it to the WASM loader - -This must be spiked in Phase 1. Same issue applies to C15 WASM (`c15/c15_engine.wasm`), C15 parameters (`c15/parameters.json`), and Faust DSP files. - -### 8. Session Presets Are Shared Across Apps - -`nisps-session-presets` localStorage key is **shared across all three current apps**. Since we're consolidating to one app, this is fine — but the key should be documented, and the migration should handle importing presets saved by the old app. - -### 9. Lazy Loading Needs Suspense Boundaries - -Three features are lazily loaded: -- **ShapeSeq**: dynamic `import()` when `?shapeseq=1` -- **Hand tracking**: imports MediaPipe from CDN (`cdn.jsdelivr.net`) — external dependency that can fail -- **Audio Canvas**: created on first switch to audio-canvas mode - -SolidJS `lazy()` + `` handles this naturally, but: -- MediaPipe CDN fetch failure needs a fallback UI (not just a blank screen) -- ShapeSeq lazy loading should show a loading state, not block the whole app -- Audio Canvas creation involves AudioContext (requires gesture) — can't be wrapped in Suspense naively - -### 10. CSS Animations Are Stateful - -Several CSS classes trigger animations that encode UI state: -- `.follow-pulse` — 1.5s infinite pulse (follow mode active) -- `.btn-flash` / `.rl-flash` — 0.2s feedback flash -- `.drawerSlideIn` — drawer appearance - -If SolidJS re-renders a component (e.g., `` toggling), CSS animations restart from the beginning. For the pulse animation this is fine, but for the flash animations, a re-render mid-flash would cause visual glitches. - -**Mitigation**: Use `classList` toggling on stable DOM nodes rather than conditional rendering for animation-bearing elements. Or use the Web Animations API for imperative control. - -### 11. `window.__nispsEoc` Is Unconditional - -Unlike `window.__nisps` (gated by `?debug=1`), `window.__nispsEoc` is **always exposed** (line 1528 in a-app.js). It provides `trainingTarget` getter/setter and `imlEoc` reference. If external code depends on this, it needs to be preserved in the SolidJS version unconditionally. - -### 12. Inference Should NOT Be in rAF - -The plan puts inference in a `requestAnimationFrame` loop. But inference only needs to run **when inputs change** (joystick drag, gamepad poll, hand tracking frame). Running it every frame when the joystick is idle wastes CPU. - -**Better pattern**: Run inference reactively — `createEffect` watching `inputState.joyX` and `inputState.joyY`. When they change, run inference and update outputs. The canvas render loops (FlowField, SynthVisualizer) still run on rAF for smooth animation, but they just read the latest outputs signal — they don't trigger inference. - -Exception: gamepad polling needs a rAF loop to read the Gamepad API, but that loop should only set input signals, not run inference directly. - ---- - -## Key Risks & Mitigations - -| Risk | Mitigation | -|------|------------| -| WASM + SolidJS reactivity overhead | Typed arrays stay as signals (not store properties). Batch per frame. Profile early in Phase 1. | -| 126 params × 20fps = 2520 synth param sends/sec | Keep existing throttle (50ms interval, 0.002 dead zone) in synth subscriber, not in routing. | -| Store proxy overhead on large arrays | Use `createSignal` for `Float32Array` outputs, not `createStore`. Stores only for structured config. | -| Signal bus wildcard `match()` perf | Memoize prefix scans. Most topics are fixed at startup. Profile if >50 topics. | -| Canvas components fighting rAF | Each canvas owns its loop. No shared orchestrator unless profiling shows frame contention. | -| MLP resize destroys training data | Same as current: warn user, warm-start weights for joystick IML. Store handles the state transition. | -| `equals: false` on bus signals | Required for event semantics but means every emit triggers all subscribers. Keep topic count bounded. | -| SharedArrayBuffer / COOP+COEP | Configure Vite dev server headers in Phase 1. Validate C15 works before Phase 3. | -| WASM asset paths broken by Vite hashing | Spike in Phase 1: use `public/` dir or `locateFile` override for all WASM/JSON assets. | -| Worker leak on component unmount | Add `dispose()` to IML and arpeggiator. Call in `onCleanup()`. | -| EOC resize cascade needs confirmation dialog | Model as pending action, not reactive effect. Component shows dialog, action triggers resize. | -| Debug probe expects synchronous execution | Build probe as imperative bridge, bypass SolidJS batching with `batch()`/`untrack()`. | -| CSS animation restart on re-render | Use `classList` on stable nodes, not ``/`` for animated elements. | -| Idle inference wastes CPU | Make inference reactive to input changes, not rAF-driven. Canvas loops stay on rAF. | - ---- - -## What Gets Transplanted vs Rewritten - -### Transplanted (minimal changes, mostly just TS types) -- `nisps-wasm.js` + worker + WASM binary -- `dataset.ts` (FIFO ring buffer) -- `FlowFieldVisualizer` class (canvas rendering) -- `c15-bridge.ts`, `c15-adapter.ts` -- `param-map.ts`, `presets.ts` (pure data) -- `additive-engine.ts`, `fm-engine.ts`, `faust-engine-base.ts` -- `arpeggiator.ts` + worker -- `eoc-chain.ts`, `eoc-module.ts`, all effect modules -- ShapeSeq core (`sequencer.ts`, `chain.ts`, `clock.ts`, `pattern.ts`, `primitives.ts`) -- `audio-canvas.ts` -- `input-pipeline.ts` logic (becomes `useInputPipeline` hook wrapping same math) -- `output-pipeline.ts` logic (becomes `useOutputPipeline` hook) -- `control-surface.ts` compound axis logic (data tables + interpolation) - -### Rewritten from scratch -- All DOM manipulation (→ JSX components) -- Event wiring (→ signal bus + reactive effects) -- State management (scattered module-scope vars → stores) -- Override application (→ `apply-overrides.ts` pure function) -- Persistence (→ `SessionProvider` with `createEffect` auto-save) -- Layout/CSS (→ new CSS with headless primitives) -- Init/boot sequence (→ provider tree + `onMount` hooks) - -### Deleted (not ported) -- `b-app.js`, `c-app.js`, `app.js` (consolidated into one app) -- `b-workbench.html`, `c-journey.html`, `index.html` (single entry point) -- `iml.js`, `mlp.js`, `layer.js`, `node.js` (legacy JS ML engine, WASM only) -- `event-bus.js` (replaced by signal bus) -- All `wire*()` functions (replaced by component-local event handlers) -- DOM-string-building functions (`buildHeatmap`, `buildDrawerHTML`, etc.) - ---- - -## Open Questions - -1. **TypeScript strictness**: Full strict mode from day one, or gradual? (Transplanted JS modules will need type annotations.) -2. **CSS approach**: Plain CSS files per component? CSS modules? Vanilla Extract? (Plain CSS is simplest and matches the current approach.) -3. **Testing during migration**: Run old Playwright tests against old code in parallel, or wait for Phase 7? -4. **WASM loading**: Vite handles `.wasm` imports natively, but the Emscripten glue (`nisps.js`) may need special config. Spike this in Phase 1. -5. **Faust DSP loading**: Currently fetched at runtime. Keep as-is or bundle? diff --git a/playground/README.md b/playground/README.md deleted file mode 100644 index ee7e472..0000000 --- a/playground/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# NISPS Playground - -Browser-based interactive demo of the NISPS ML engine. - -## Run locally - -```bash -python3 -m http.server -# Open http://localhost:8000 -``` - -Run the command from the `playground/` directory. - -## What it does - -- Maps 2D joystick input (X/Y) through an MLP (`[3, 32, 48, 64, 126]`) to 126 outputs. -- **Visual mode**: first 20 outputs drive a flow-field particle visualization on Canvas2D. -- **Synth mode (C15)**: all 126 outputs control the C15 WASM synthesizer — envelopes, oscillators, shapers, filters, feedback/output mixers, cabinet, and effects. -- Supports two learning modes: - - **Examples**: add explicit input/output pairs and train. - - **RL Feedback**: give thumbs up/down while exploring outputs. - -## C15 synth parameters - -The 126 synth parameters (`js/synth/param-map.js`) cover all sonically meaningful continuous parameters of the C15 engine. Excluded from the C15's 287 total params: - -- Hardware routing (56) — no physical MIDI in browser -- Macro controls (12) — conflicts with direct ML control -- Scale/tuning (13) — would break pitch -- Key tracking / velocity (22) — depend on note context ML can't observe -- Envelope mod depths (19) — too many multiplicative interactions for 2-input ML -- Discrete/structural/dangerous (19) — pitch sweep, volume, switches, resets -- Secondary config (15) — curves, chirp, shaper blend, source selects - -## UI controls - -- **Output mode tabs** (floating bar): switch between Visual and Synth modes. -- **Expand / Collapse** chevron: expands bottom sheet with training controls, synth settings, advanced param sliders. -- **Synth presets**: tiered presets (Beginner → Expert) control which of the 126 params the ML engine can modify. -- **Help** (`?`) overlay: in-app usage guide. -- **Follow mode**: double-click joystick in RL mode to toggle no-hold interaction. -- **Keyboard in Follow mode (RL)**: `2` = thumbs up, `1` = thumbs down. -- **Gamepad in RL**: `RB` = thumbs up, `LB` = thumbs down, `A` = train, `X` = randomize, `B` = clear. - -## Control Surface - -The immersive app has a control surface system (Phase 1 of `SPEC-controls.md`) for tuning how exploration and learning feel. - -**Compound axes** on the floating bar (3 sliders: Bold, Mem, Prec): - -| Axis | Controls | Low end | High end | -|------|----------|---------|----------| -| **Boldness** | Input zoom, noise cap, noise growth, LR, weight decay | Cautious: small changes, heavy regularisation | Bold: full range, explosive exploration, fast learning | -| **Memory** | Max examples, example decay, noise decay, convergence | Amnesia: only last few interactions matter | Elephant: every example sacred, stable mapping | -| **Precision** | Input curve, deadzone, smoothing, slew rate, momentum | Raw: 1:1 with physical movement, twitchy | Precise: heavy shaping, deadzones, smooth | - -**Settings drawer** (gear icon, bottom-right): individual param overrides for Input, Training, Exploration, and Output sections. Manual overrides persist as offsets when compound axes move (trim-pot model). Double-tap an axis to re-link all params. - -**Control presets**: Default, First Touch, Jazz Hands, Sculptor, Improviser, Microscope. - -**Input pipeline**: joystick input is processed through deadzone → zoom → curve → smoothing → momentum-as-zoom before reaching the MLP. Zoom narrows the effective input window around an anchor. Zoom-at-zero freezes input. - -**Enhanced joy-map**: zoom minimap with adaptive grid (4×4 → 32×32), vanishing trail with tap-to-return, dual noise rings (zoom + noise level). - -## Files - -### Original app (index.html) -- `index.html` - page structure. -- `css/style.css` - layout and visual styling. -- `js/app.js` - app wiring and interaction logic. - -### Immersive app (a-immersive.html) -- `a-immersive.html` - fullscreen immersive UI. -- `css/a-immersive.css` - immersive layout and styling. -- `js/a-app.js` - immersive app wiring, state management, persistence. - -### Shared modules -- `js/nisps/` - JavaScript MLP + IML core (also WASM variant). -- `js/ui/visualizer.js` - flow-field particle system (Canvas2D). -- `js/ui/joystick.js` - virtual joystick component. -- `js/ui/gamepad.js` - gamepad input handling. -- `js/ui/hand-tracker.js` - MediaPipe hand tracking (14 features). -- `js/ui/input-pipeline.js` - input processing pipeline (zoom, deadzone, curve, smoothing, momentum). -- `js/ui/control-surface.js` - compound axes, override resolution, control presets. -- `js/ui/control-surface-ui.js` - settings drawer and floating bar axis sliders. -- `js/ui/joy-map-enhanced.js` - zoom minimap, vanishing trail, dual noise rings. -- `js/synth/` - C15 WASM bridge, parameter map, presets, arpeggiator. -- `c15/` - C15 engine WASM binary and parameter definitions. -- `SPEC-controls.md` - comprehensive control surface spec (4 phases). diff --git a/playground/SPEC-controls.md b/playground/SPEC-controls.md deleted file mode 100644 index 057b18d..0000000 --- a/playground/SPEC-controls.md +++ /dev/null @@ -1,802 +0,0 @@ -# NISPS Control Surface — Comprehensive Spec - -## Overview - -This spec defines the full control surface for the NISPS playground's immersive app (`a-immersive.html`). The goal: make NISPS work for creatives who need both broad exploration *and* millimeter-precision refinement. The difference between a good guitar player and a great one is within the millimeters and milliseconds — these controls give users that resolution when they want it. - -### Design Principles - -1. **Compound axes over independent knobs** — Users interact with 3-4 perceptually meaningful axes. Individual parameters exist underneath as overrides, but the default experience is high-level. -2. **Zoom is the core metaphor** — Like Google Maps: same physical movement, different scale of traversal. Zooming in reveals (and teaches) fine structure. -3. **Pinning preserves what works** — Users can protect regions, parameters, or training snapshots from being disturbed while they refine other areas. -4. **Everything is a preset** — Every control state can be saved, restored, and shared. The preset system tames complexity without removing capability. -5. **Embodied first** — Momentum-as-zoom, pressure-sensitive feedback, and vanishing trails make the system feel like an instrument, not a control panel. - -### System Diagram - -``` - Physical Input - (joystick / hand / gamepad) - │ - ▼ - ┌─────────────────────┐ - │ Input Pipeline │ - │ deadzone → zoom → │ - │ curve → smoothing → │ - │ momentum scaling │ - └─────────┬───────────┘ - │ scaled [0,1] - ▼ - ┌─────────────────────┐ ┌──────────────────┐ - │ NISPS MLP │◄────│ Training Engine │ - │ (WASM inference) │ │ LR, iterations, │ - │ │ │ convergence │ - └─────────┬───────────┘ └────────┬─────────┘ - │ raw [0,1] │ - ▼ │ - ┌─────────────────────┐ ┌────────▼─────────┐ - │ Output Pipeline │ │ RL Feedback │ - │ smoothing → slew → │ │ noise, decay, │ - │ tame → group curve │ │ pinning, zoom- │ - │ [freeze gate] │ │ aware scaling │ - └─────────┬───────────┘ └──────────────────┘ - │ - ┌─────┴─────┐ - ▼ ▼ - Visual Engine C15 Synth -``` - -### Pipeline Order Rationale - -The input pipeline order is deliberate: - -1. **Deadzone** first — kill physical jitter before any processing -2. **Zoom** second — narrow the input window around the anchor -3. **Curve** third — shape movement *within* the zoomed window (so zoomed-in precision gets the benefit of curve shaping, not just the physical input) -4. **Smoothing** fourth — temporal filter on the shaped signal -5. **Momentum scaling** last — velocity-based zoom modulation on the final signal - -Alternative orderings are worth experimenting with (curve before zoom means shaping the physical input, which might feel more "instrument-like"). The implementation should make the pipeline order configurable or at least easy to swap during development. - ---- - -## Part 1: Compound Control Axes - -These are the primary user-facing controls. Each axis moves multiple underlying parameters along a perceptually coherent dimension. - -### Axis 1: Caution ↔ Boldness - -*"How adventurous is my exploration?"* - -Controls the overall risk/reward balance of the RL loop and weight perturbation. - -| Boldness | Input Zoom | Noise Cap | Noise Growth | LR | Weight Decay | Noise Distribution | -|----------|-----------|-----------|-------------|----|--------------|--------------------| -| 0 (Cautious) | 0.1 | 0.02 | 1.1x | 0.1 | 0.15 | gaussian | -| 0.5 (Balanced) | 0.5 | 0.12 | 1.5x | 1.0 | 0.06 | gaussian | -| 1.0 (Bold) | 1.0 | 0.3 | 2.5x | 3.0 | 0.0 | cauchy | - -**Perceptual meaning**: At low boldness, everything is gentle — small input range, small perturbations, slow learning, heavy regularisation. At high boldness, full input range, explosive exploration, fast learning, no guardrails. - -### Axis 2: Memory ↔ Amnesia - -*"How much does the network remember vs adapt to what I'm doing right now?"* - -Controls the temporal horizon of learning. - -| Memory | Example Capacity | Example Decay | Weight Decay | Noise Decay (per +) | Convergence Threshold | -|--------|-----------------|---------------|--------------|---------------------|-----------------------| -| 0 (Amnesia) | 5 | 0.3 | 0.2 | 0.85 | 1e-3 | -| 0.5 (Balanced) | 50 | 0.7 | 0.06 | 0.97 | 1e-5 | -| 1.0 (Elephant) | 500 | 1.0 | 0.0 | 0.995 | 1e-8 | - -**Perceptual meaning**: At low memory, the network forgets quickly — only the last few interactions matter, the mapping is fluid and impermanent. At high memory, every example is sacred, the mapping is stable and hard to shift. - -### Axis 3: Precision ↔ Expression - -*"How raw and responsive vs filtered and controlled is the input?"* - -Controls the input pipeline's character. - -| Precision | Input Curve | Deadzone | Smoothing | Slew Rate | Momentum Zoom | -|-----------|-----------|----------|-----------|-----------|---------------| -| 0 (Raw) | linear | 0 | 0 | unlimited | off | -| 0.5 (Balanced) | mild expo (1.5) | 0.05 | 0.15 | 0.3/frame | gentle | -| 1.0 (Precise) | strong expo (3.0) | 0.15 | 0.4 | 0.1/frame | strong | - -**Perceptual meaning**: At low precision, input is 1:1 with physical movement — twitchy, expressive, immediate. At high precision, input is heavily shaped — deadzones eat jitter, curves give more resolution in the center, smoothing removes noise, slew rate prevents jumps. - -### Axis 4: Stability ↔ Fluidity (stretch goal) - -*"How locked-in vs free-flowing are the outputs?"* - -Controls post-network output behavior. - -| Stability | Output Smoothing | Tame | Global Curve | Pin Strength | -|-----------|-----------------|------|-------------|--------------| -| 0 (Fluid) | 0 | 0 | 1.0 (linear) | none | -| 0.5 (Balanced) | 0.3 | 0.5 | 1.0 | soft pins | -| 1.0 (Locked) | 0.8 | 1.0 | n/a | hard pins | - ---- - -## Part 2: Input Pipeline - -### 2.1 Input Zoom - -The core navigation control. Narrows the effective input window around an anchor point. - -**Metaphor**: Google Maps zoom. Same physical joystick movement, but at zoom=0.1, your full joystick travel covers only 10% of the input space. - -**Implementation**: -``` -effective_input[i] = anchor[i] + (raw_input[i] - 0.5) * zoom_level -clamped to [0, 1] -``` - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `zoom` | 0.01–1.0 (log) | 1.0 | Affects all axes uniformly | -| `zoomX` / `zoomY` | 0.01–1.0 | 1.0 | Per-axis override (joystick only) | -| `anchorMode` | auto / sticky / center | auto | See below | - -**Anchor modes**: -- **auto**: Anchor updates to current joystick position whenever zoom level changes. Natural "zoom in on what I'm looking at." -- **sticky**: Anchor stays where last set. Joystick pans within the zoomed window. Change anchor explicitly (e.g., double-tap). -- **center**: Always anchored at (0.5, 0.5). Zooming always narrows toward center of input space. - -**Zoom-at-zero = Freeze**: The zoom slider has a detent at the bottom that freezes input. No separate "freeze input" toggle needed — it's the natural limit of zooming in. - -**Critical coupling with training**: Zooming in and training teaches the network fine structure in that region. This is the primary refinement workflow: -1. Zoom out → broad explore → find interesting region -2. Zoom in → RL feedback to refine detail -3. Zoom out → verify the big picture wasn't destroyed - -**Risk**: Training while zoomed in can distort mappings outside the zoom window. Mitigated by pinning (see Part 4). - -### 2.2 Momentum-as-Zoom - -An alternative/complementary zoom mechanism tied to movement speed. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `momentumZoom` | off / gentle / strong | off | Toggle, not a slider | -| `velocityWindow` | 50–500ms | 150ms | Time window for velocity estimation | - -**When enabled**: Movement speed modulates effective zoom in real time. -- Slow, deliberate movement → high effective zoom (fine control) -- Fast sweeps → low effective zoom (broad traversal) -- Stationary → zoom level holds at last value - -**Interaction with explicit zoom**: Momentum zoom multiplies with the manual zoom slider. If manual zoom is 0.5 and you move slowly, effective zoom might be 0.15. If you move fast, effective zoom might be 0.8. - -### 2.3 Input Curve - -Response curve per input axis. Reshapes the relationship between physical movement and input value. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `inputCurve` | 0.2–5.0 | 1.0 (linear) | Power function exponent | -| `inputCurveX` / `inputCurveY` | 0.2–5.0 | 1.0 | Per-axis override | - -**Implementation** (centered power curve): -``` -// input in [0,1], centered at 0.5 -offset = input - 0.5 -shaped = sign(offset) * pow(abs(offset) * 2, exponent) / 2 -output = shaped + 0.5 -``` - -- Exponent < 1.0: logarithmic feel — fine at center, coarse at edges -- Exponent = 1.0: linear (no shaping) -- Exponent > 1.0: exponential feel — coarse at center, fine at edges. Good for "I want to stay near center but occasionally sweep to extremes." - -### 2.4 Input Deadzone - -Percentage of travel from center that produces no change. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `deadzone` | 0–0.4 | 0 (joystick), 0.1 (hands) | Percentage of half-travel | - -Remaps the live zone to still cover full [0,1] output range — deadzone doesn't shrink the output, it just eats jitter near center. - -### 2.5 Input Smoothing - -Exponential moving average on the input signal. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `inputSmoothing` | 0–0.95 | 0 (joystick), 0.3 (hands) | EMA factor (0 = no smoothing) | - -**Implementation**: -``` -smoothed = smoothing * previous + (1 - smoothing) * raw -``` - -Higher values = more latency, smoother signal. Essential for hand tracking where raw MediaPipe coordinates jitter. - -### 2.6 Axis Invert - -| Parameter | Range | Default | -|-----------|-------|---------| -| `invertX` / `invertY` | bool | false | - ---- - -## Part 3: Training Dynamics - -### 3.1 Learning Rate - -SGD step size passed to WASM `Train()`. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `learningRate` | 0.01–5.0 (log scale) | 1.0 | Affects both example training and RL thumbs-up | - -Lower LR = network changes more gently per training call. At 0.01, each thumbs-up barely nudges; at 5.0, each thumbs-up aggressively reshapes. Interacts with max iterations — low LR + low iterations = almost no change; low LR + high iterations = careful convergence. - -### 3.2 Max Iterations - -How many SGD passes per `train()` call. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `maxIterations` | 10–5000 | 1000 | Higher = more polished fit but longer async wait | - -### 3.3 Convergence Threshold - -Early-stop when loss drops below this. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `convergenceThreshold` | 1e-8–1e-2 (log) | 1e-5 | Lower = stricter fit | - -### 3.4 RL Train Intensity - -Number of training calls per thumbs-up event. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `rlTrainIntensity` | 1–10 | 1 | More = stronger reinforcement per positive signal | - -### 3.5 Example Memory - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `maxExamples` | 5–500 | 100 | FIFO eviction when full | - -### 3.6 Example Decay - -Weighted forgetting — older examples contribute less to training loss. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `exampleDecay` | 0.1–1.0 | 1.0 | 1.0 = all equal, 0.5 = half-life per example age | - -**Implementation**: During training, each example's loss contribution is weighted by `decay^age` where age is its position from most-recent (0) to oldest (N-1). Requires modifying the WASM training call to accept per-sample weights, or implementing weighted sampling on the JS side before sending to WASM. - ---- - -## Part 4: Pinning System - -The ability to say "I like this, don't touch it" — the core of precision refinement. - -### 4.1 Region Pinning - -Pin a rectangular region of the input space. Training and noise perturbation are suppressed for examples/weights that primarily affect the pinned region. - -**Interaction model**: -1. Navigate to the region you like -2. Long-press / dedicated gesture → "Pin this area" -3. A colored overlay appears on the joy-map showing the pinned region -4. Continue exploring elsewhere — pinned region's outputs remain stable - -**Implementation approaches** (in order of complexity): - -#### Approach A: Example Pinning (simplest) -- When pinning, snapshot all current examples whose inputs fall within the pinned region -- These examples are marked as "pinned" — they're always included in training with high weight and can't be evicted by FIFO -- New training still affects the whole network, but the pinned examples anchor the behavior in that region - -#### Approach B: Weight Masking (moderate) -- On pin, snapshot the current weights -- During `moveWeights()`, blend: for each weight, compute how much it contributes to the pinned region (approximated by which input neurons it connects to) and reduce noise proportionally -- During training, add a regularisation term that penalises divergence from the pinned-region snapshot - -#### Approach C: Dual Network (most powerful, most complex) -- Maintain a "frozen" copy of weights for pinned regions -- Inference blends: for inputs in/near the pinned region, use frozen weights; for inputs outside, use live weights; blend in the transition zone -- Requires spatial partitioning of the input space - -**Recommended starting point**: Approach A (example pinning) — simple, effective, leverages existing training. Approach B as a follow-up. - -### 4.2 Parameter Pinning - -Pin individual output parameters or parameter groups. Pinned parameters are excluded from `moveWeights()` perturbation and their training targets are held fixed. - -This partially exists already — the mute system in the synth visualizer drawer removes params from NISPS control. Pinning is different: the param *stays* NISPS-controlled but its current learned mapping is protected. - -**Implementation**: -- Per-param pin flag in the group overrides structure -- During `moveWeights()`, skip weights in the final layer that connect to pinned output nodes -- During training, fix pinned output targets to their current inferred values (so the network maintains its current mapping for those outputs regardless of new examples) - -### 4.3 Snapshot Stack (Undo/History) - -A stack of weight snapshots that supports multi-level undo and zoom-aware branching. - -| Action | Snapshot behavior | -|--------|-------------------| -| Zoom in | Auto-push snapshot ("before refinement") | -| Thumbs-down | Push snapshot (can undo the perturbation) | -| Train | Push snapshot (can undo the training) | -| Randomize | Push snapshot (can undo the randomization) | -| Pin region | Push snapshot + tag as "pinned baseline" | - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `maxSnapshots` | 5–50 | 20 | Ring buffer, oldest evicted | - -**UI**: Undo button (single step back). Long-press for snapshot list showing tagged entries. - ---- - -## Part 5: Exploration Noise (RL) - -### 5.1 Spread (Master Regime) - -Existing parameter, promoted from URL-only to a panel slider. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `spread` | 0–1 | 0.6 | Controls init scale, noise cap, per-layer scaling, weight decay simultaneously | - -Spread remains the "personality" of the exploration system. Individual noise parameters below can override spread's derived values. - -### 5.2 Noise Floor - -Minimum noise level that thumbs-up can't decay below. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `noiseFloor` | 0–0.1 | 0.005 | Higher = always some exploration | - -### 5.3 Noise Cap - -Maximum noise level reachable via thumbs-down. Overrides spread-derived cap when set explicitly. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `noiseCap` | 0.01–1.0 | derived from spread | `0.3*(1-spread) + 0.05*spread` when not overridden | - -### 5.4 Noise Growth Rate - -Multiplier per thumbs-down event. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `noiseGrowth` | 1.1–3.0 | 1.5 | Higher = faster escalation to exploration | - -### 5.5 Noise Decay Rate - -Multiplier per thumbs-up event. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `noiseDecay` | 0.8–0.99 | 0.97 | Lower = faster convergence after positive feedback | - -### 5.6 Weight Decay - -Per-`moveWeights` call shrinkage. Prevents unbounded weight drift from repeated exploration. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `weightDecay` | 0–0.3 | derived from spread | `0.1 * spread` when not overridden | - -### 5.7 Noise Distribution - -Shape of random perturbation in `moveWeights()`. - -| Parameter | Values | Default | Notes | -|-----------|--------|---------|-------| -| `noiseDistribution` | gaussian / uniform / cauchy | gaussian | Cauchy = rare big jumps, good for escaping local optima | - -### 5.8 Layer-Aware Noise - -Whether noise scales by 1/sqrt(fan_in) per layer. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `layerAwareNoise` | 0–1 | derived from spread | 0 = flat noise, 1 = Xavier-scaled. Currently tied to spread. | - -### 5.9 Zoom-Aware Feedback Scaling - -When enabled, feedback intensity scales with zoom level. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `zoomAwareFeedback` | off / on | on | When on: zoomed-in thumbs-down = gentle nudge, zoomed-out = big shake | - -**Implementation**: Multiply noise growth and RL train intensity by `zoom_level`. At zoom=0.1, a thumbs-down applies 1/10th the normal perturbation. This is the natural interaction: your feedback matches your exploration scale. - -### 5.10 Asymmetric / Pressure-Sensitive Feedback - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `pressureFeedback` | off / on | off | Touch force modulates feedback intensity (where supported) | -| `holdDurationFeedback` | off / on | on | Longer hold = stronger signal (already partially implemented for hand gestures) | - -### 5.11 Auto-Explore - -Automated thumbs-down at regular intervals. A "wander" mode for weight space — the system drifts continuously while the user only gives thumbs-up when it lands on something good. Useful for hands-free exploration, performance contexts, or when you want to sit back and listen/watch. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `autoExplore` | off / on | off | Toggle | -| `autoExploreInterval` | 0.5–10s | 2s | Time between automatic perturbations | -| `autoExploreIntensity` | 0.1–1.0 | 0.5 | Scales the noise applied per auto-step (relative to current noise level) | - -**Interaction with other controls**: -- Respects noise cap, spread, zoom-aware feedback scaling — all the same rules as manual thumbs-down -- Thumbs-up still works normally (trains + decays noise), creating a "selection pressure" against the auto-drift -- Auto-Explore + Follow Mode = fully autonomous exploration (joystick wanders + weights drift + user just watches and occasionally thumbs-up) -- Auto-Explore intensity could scale with zoom: zoomed in = gentler auto-steps, zoomed out = bigger leaps - ---- - -## Part 6: Output Pipeline - -### 6.1 Output Smoothing - -Temporal smoothing on network outputs. Prevents jarring jumps when the mapping changes. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `outputSmoothing` | 0–0.95 | 0 | EMA factor. Higher = more latency, smoother transitions. | - -### 6.2 Output Slew Rate - -Maximum change per frame per output. Hard limiter (vs the soft EMA above). - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `outputSlewRate` | 0.005–1.0 | 1.0 (unlimited) | In units per frame. 0.01 = very slow transitions. | - -### 6.3 Tame - -Existing parameter — constrains synth output ranges toward safe defaults. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `tame` | 0–1 | 1.0 | Already implemented, promote to panel | - -### 6.4 Global Output Curve - -Apply a single power curve to all outputs. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `globalCurve` | 0.2–5.0 | 1.0 (linear) | < 1 = push toward extremes, > 1 = push toward center | - -### 6.5 Freeze Output - -Lock current outputs — the network still runs inference as input moves, but the output pipeline doesn't update the synth/visualizer. Use case: "let me hear this sound while I adjust control surface settings" or "hold this visual while I tweak noise parameters." - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `freezeOutput` | bool | false | Gate at the end of the output pipeline | - -Distinct from Freeze Input (zoom-at-zero): with Freeze Input, inference stops because input doesn't change. With Freeze Output, inference keeps running (you can watch the heatmap shift) but the audible/visible result stays locked. This lets you preview what *would* change before committing. - -### 6.6 A/B Compare - -Rapid toggle between two weight states. Snapshot a "reference" state, keep exploring, then flip back and forth to hear/see the difference. - -**Interaction model**: -1. Press "A" to snapshot current state (weights + noise level + zoom) -2. Continue exploring (this is the "B" state, live) -3. Toggle A↔B to switch instantly between the two -4. "Accept B" to discard the A snapshot and continue -5. "Revert to A" to restore the snapshot and discard B - -| Parameter | Notes | -|-----------|-------| -| `abSnapshot` | Stored weight array + noise level + control state | -| `abActive` | bool — is A/B mode engaged? | - -**UI**: Could be a toggle button, or a press-and-hold (hold to hear A, release to return to B — like a preview pedal). - ---- - -## Part 7: Visualization & Feedback - -### 7.1 Zoom Minimap - -The joy-map canvas shows the zoom window boundary overlaid on the full input space. - -**Requirements**: -- Show the full [0,1]x[0,1] input space at all times -- Draw the current zoom window as a rectangle (gets smaller as you zoom in) -- Inside the zoom window, show a "graph paper" grid that subdivides as zoom increases — like zooming into actual graph paper and seeing finer grid lines appear -- Current joystick position shown as a dot within the zoom window -- Pinned regions shown as colored overlays - -### 7.2 Vanishing Trail - -A fading trail of recent joystick positions on the joy-map. - -**Requirements**: -- Trail persists for ~5 seconds, fading from opaque to transparent -- Trail is drawn in the full input space (not zoom-relative), so you can see where you've been even after zooming -- Trail points are clickable/tappable — tap a trail point to snap the joystick back to that position ("I liked what I heard 3 seconds ago, go back") -- Trail color matches current output mode (orange for visual, blue for synth, etc.) -- Optional: trail width encodes zoom level at the time — thicker = more zoomed out, thinner = more zoomed in - -The trail already partially exists (joyTrail in a-app.js). This extends it with persistence, interactivity, and zoom-awareness. - -### 7.3 Noise Ring - -Already exists — ring around joystick showing current noise level. - -**Enhancement**: Ring could also encode zoom level (ring radius = zoom, ring thickness = noise). Two concentric rings: inner = zoom window, outer = noise magnitude. - -### 7.4 Input Space Heatmap - -A 2D color field on the joy-map showing what the network produces across the entire input space. A bird's-eye view of the learned landscape. - -**Implementation**: -- Sample the network at a grid of input points (e.g., 8x8 or 16x16) -- For each point, run inference and reduce the output vector to a color (e.g., average output magnitude → brightness, output variance → saturation, dominant output cluster → hue) -- Render as a background layer on the joy-map canvas -- Update on weight change events (train, randomize, moveWeights), NOT every frame -- At higher zoom levels, the heatmap re-samples the zoomed window at the same grid resolution, revealing finer structure (if the network has learned any) - -**Use case**: "There's something interesting in that corner — the heatmap shows high variance there, let me navigate over." Also gives immediate visual feedback on whether training actually changed the landscape. - -**Cost**: At 16x16 = 256 inference calls per update. With the WASM engine this should be <5ms. Can be throttled to update at most once per 200ms. - -**Color reduction strategies** (which to try): -- Mean output luminance (simple, shows "loud" vs "quiet" regions) -- Output entropy / variance (shows "interesting" vs "flat" regions) -- Principal component to RGB (shows the dominant output dimensions as color channels) -- Difference-from-center (shows how much each region diverges from the center point's output) - -### 7.5 Weight Health Indicator - -A small ambient indicator showing network weight statistics. - -| Visual | Meaning | -|--------|---------| -| Calm, low-saturation glow | Weights are well-distributed, network is healthy | -| Hot, pulsing glow | Weights are saturating (high magnitude), sigmoid is clamping | -| Dim/dead | Weights are near-zero, network is underpowered | - -Implementation: Compute weight magnitude histogram from `_getFlatWeights()` periodically (not every frame — every 500ms or on weight change events). - -### 7.6 Gradient Flow Indicator - -During and after training, show per-layer gradient magnitudes — signals whether the network is actually learning or if gradients are vanishing/exploding. - -| Visual | Meaning | -|--------|---------| -| Even bars across layers | Healthy gradient flow | -| Bars shrinking left-to-right | Vanishing gradients (deeper layers aren't learning) | -| Bars growing left-to-right | Exploding gradients (unstable training) | -| All bars near zero | Network has converged or is stuck | - -**Implementation**: Requires exposing per-layer gradient norms from the WASM training path. Options: -- Add a `nisps_mlp_get_gradient_norms()` binding that returns per-layer L2 gradient norms after a training call -- Or compute approximately on the JS side by measuring weight deltas before/after training (less accurate but no WASM changes) - -**Audience**: Power users tuning LR, architecture, or diagnosing why training isn't working. Can be hidden behind an "Advanced" toggle. - ---- - -## Part 8: Engine Configuration (Separate from Runtime Controls) - -These require a full network reset and destroy the current mapping. They live in a separate "Engine" panel gated behind a confirmation dialog. - -| Parameter | Range | Default | Notes | -|-----------|-------|---------|-------| -| `hiddenLayers` | array of ints | [32, 48, 64] | MLP topology | -| `hiddenLayerCount` | 1–6 | 3 | Convenience — resizes the array | -| `outputActivation` | sigmoid / tanh | sigmoid | Final layer activation | -| `hiddenActivation` | relu / leaky_relu / tanh / gelu | relu | Hidden layer activation | - -Changing any of these triggers: "This will reset the network and all training. Continue?" - ---- - -## Part 9: Preset System - -### Control Presets - -Control presets define a complete control surface state (all parameters from Parts 1-6). They do NOT include network weights or training data — those are separate. - -**Built-in control presets** (initial set, to be expanded through experimentation): - -| Preset | Caution/Boldness | Memory/Amnesia | Precision/Expression | Character | -|--------|-----------------|----------------|---------------------|-----------| -| **Default** | 0.5 | 0.5 | 0.3 | Balanced starting point | -| **First Touch** | 0.2 | 0.7 | 0.6 | Gentle for newcomers. Small changes, stable memory, forgiving input. | -| **Jazz Hands** | 0.8 | 0.2 | 0.0 | Wild exploration. Big noise, short memory, raw input. | -| **Sculptor** | 0.3 | 0.9 | 0.8 | Precision refinement. Small careful changes, long memory, heavy smoothing. | -| **Improviser** | 0.6 | 0.3 | 0.2 | Responsive and forgetful. Medium exploration, recent-biased, low latency. | -| **Microscope** | 0.1 | 1.0 | 1.0 | Maximum zoom, maximum memory, maximum precision. For fine detail work. | - -### Preset Storage - -```javascript -{ - name: "Sculptor", - // Compound axis positions (for UI display) - axes: { boldness: 0.3, memory: 0.9, precision: 0.8, stability: 0.5 }, - // Resolved individual parameters (actual values used) - input: { zoom: 0.2, momentumZoom: 'gentle', inputCurve: 2.0, deadzone: 0.08, smoothing: 0.3, invertX: false, invertY: false }, - training: { learningRate: 0.3, maxIterations: 2000, convergenceThreshold: 1e-7, rlTrainIntensity: 1, maxExamples: 300, exampleDecay: 0.95 }, - noise: { spread: 0.7, noiseFloor: 0.003, noiseCap: 0.08, noiseGrowth: 1.2, noiseDecay: 0.99, weightDecay: 0.07, noiseDistribution: 'gaussian', layerAwareNoise: 0.8, zoomAwareFeedback: true }, - output: { outputSmoothing: 0.4, outputSlewRate: 0.1, tame: 0.8, globalCurve: 1.0 }, -} -``` - ---- - -## Part 10: Implementation Priority - -### Phase 1 — Core Zoom + Compound Axes ✅ IMPLEMENTED -1. ✅ Input zoom with anchor modes and minimap visualization — `js/ui/input-pipeline.js`, `js/ui/joy-map-enhanced.js` -2. ✅ Zoom-at-zero freeze behavior — `InputPipeline.isFrozen()`, frozen overlay in joy-map -3. ✅ Vanishing trail with tap-to-return — Catmull-Rom spline, ring buffer, 5s duration, zoom-width encoding -4. ✅ Compound axis sliders (Boldness, Memory, Precision) wired to underlying params — `js/ui/control-surface.js` -5. ✅ Spread, LR, noise cap promoted to panel sliders — settings drawer with all params from Parts 2-6 -6. ✅ Resolve UI location (Part 11.5) — Option E (hybrid): axes on floating bar, overrides in gear drawer -7. ✅ (bonus) Input curve, deadzone, smoothing, momentum-zoom — all implemented in pipeline, exposed in drawer -8. ✅ (bonus) Zoom-aware feedback scaling — thumbs-down noise scales by zoom level -9. ✅ (bonus) Control presets with offset-based override resolution — 6 built-in presets, trim-pot model -10. ✅ (bonus) Dual concentric noise rings (zoom + noise) replacing CSS-only ring - -### Phase 2 — Pinning + History ✅ IMPLEMENTED -7. ✅ Parameter pinning — per-output pin flags, pin mask passed to `moveWeights()`, double-tap to toggle in synth visualizer -8. ✅ Region pinning (Approach A: example pinning) — long-press joy-map pins current zoom window, pinned examples always included in training with high weight -9. ✅ Snapshot stack with undo — ring buffer (20 max), auto-snapshot on train/randomize/thumbs-down, long-press for history popup -10. ✅ A/B Compare toggle — capture A, toggle between states, accept B or revert to A -11. ✅ Modified `mlp.js` moveWeights to accept optional `outputPinMask` for pinned output nodes - -### Phase 3 — Input Refinement + Exploration ✅ IMPLEMENTED -12. ✅ Pressure/hold-duration feedback — touch force + hold duration modulate noise growth/decay strength -13. ✅ Auto-Explore mode — automated thumbs-down at configurable interval, zoom-scaled intensity, emerald toggle button with progress ring -14. ✅ Input space heatmap — 16×16 grid inference sampling, 3 color modes (luminance/variance/divergence), zoom-aware resampling, throttled updates - -### Phase 4 — Output, Persistence + Polish ✅ IMPLEMENTED -15. ✅ Output pipeline — global curve → smoothing → slew rate → freeze gate, wired into `routeOutputs()` -16. ✅ Weight health indicator — weight magnitude histogram, dead/saturating/healthy status, ambient glow visualization -17. ✅ Gradient flow indicator — per-layer weight-delta analysis, vanishing/exploding/converged detection, bar visualization -18. ✅ Session presets — save/load full state (control surface + synth preset + pipelines), URL sharing via compact params - -### Remaining -- Engine configuration panel (Part 8) — network architecture, loss function, optimizer selection - ---- - -## Part 11: Open Design Questions - -Issues that need resolution through experimentation. Leaving all possibilities open. - -### 11.1 Compound Axis Override Resolution - -When a user moves the Boldness axis, it sets ~6 individual parameters. If they then manually override Noise Cap, what happens when they move Boldness again? - -**Options**: -- **Axis always wins** — Simple but frustrating. Manual tweaks get blown away. -- **Override sticks** — Manual overrides detach the param from its axis. Moving the axis moves the other 5 params but leaves Noise Cap alone. Visual indicator shows detached params (e.g., dimmed link icon). -- **Override as offset** — The manual tweak becomes a delta on top of the axis-derived value. Moving the axis shifts the base, the offset persists. E.g., if axis says noiseCap=0.12 and user overrides to 0.15, the offset is +0.03. Moving boldness to a new position that says noiseCap=0.08 results in effective noiseCap=0.11. -- **Re-engage gesture** — Double-tap the axis slider to re-link all params. Single moves only affect still-linked params. - -All four are worth prototyping. The offset approach is most "musical" (like trim pots on a mixing desk), but the detach approach is most predictable. - -### 11.2 Gamepad Input Pipeline - -Gamepad sticks (Steam Deck, Xbox) often have OS-level deadzone and response curves applied. The input pipeline would double-process these. - -**Options**: -- **Bypass pipeline for gamepad** — Gamepad feeds directly into the zoom stage, skipping deadzone and curve (since the OS already applied them). Smoothing and momentum still apply. -- **Full pipeline, user manages** — Let users set deadzone to 0 for gamepad. Simpler implementation, slight risk of feeling "mushy." -- **Per-input-type pipeline presets** — Different default pipeline settings for joystick vs gamepad vs hand tracking. The "Precision ↔ Expression" axis resolves to different underlying values depending on input type. -- **Raw mode toggle** — Gamepad can request raw stick values (bypassing OS processing) on some platforms. Offer this as an option. - -Currently `GamepadInput` in `gamepad.js` maps stick values to joystick position. The pipeline applies on top of that. - -### 11.3 Persistence & Sharing - -Where are user control presets saved, and how are they shared? - -**Storage options**: -- **localStorage** — Current approach for app state (`nisps-a-immersive` key). Natural extension for control presets. -- **URL parameters** — Like existing `?spread=0.6&tame=1`. Enables sharing via link. Gets unwieldy with 30+ params but works well for compound axes (e.g., `?boldness=0.3&memory=0.9&precision=0.8`). -- **Export/Import JSON** — Copy-paste or file download/upload. Full fidelity, shareable, but more friction. -- **Named presets in localStorage** — User can save multiple named presets, select from a dropdown. - -**Sharing scenarios**: -- "Try this control setup" → URL with compound axis values -- "Here's my complete session" → JSON export (control preset + synth preset + training data + weights) -- "Starting point for a workshop" → URL with preset name that maps to a built-in - -All of these should be possible. URL params for quick sharing, localStorage for persistence, JSON for full export. - -### 11.4 Control Presets vs Synth Presets Composition - -Control presets (how you explore) and synth presets (what parameters exist and their ranges) are orthogonal. But users will want combined "session" presets. - -**Options**: -- **Independent** — Two separate dropdowns. User picks a synth preset AND a control preset. Simple, composable, but requires two decisions. -- **Bundled sessions** — A "session" preset bundles both. "Beginner Sculptor" = beginner-1 synth + Sculptor controls. More opinionated, fewer choices. -- **Synth preset suggests controls** — Loading a beginner synth preset auto-suggests "First Touch" controls. User can override. A soft coupling. -- **All of the above** — Independent selection as the base, with bundled suggestions as convenience shortcuts. - -### 11.5 UI Location & Panel Architecture - -Where do these controls physically live in the immersive app? - -**Options under consideration**: - -#### Option A: Settings Drawer (gear icon) -- New gear icon next to the help button (top right) -- Opens a side drawer with compound axis sliders at top, expandable sections for individual params below -- Pro: Doesn't clutter the main performance surface. Con: Hidden, less discoverable. - -#### Option B: Extended Bottom Sheet -- Add a "Controls" tab to the existing bottom sheet (alongside the existing examples/training/synth tabs) -- Compound axes as prominent sliders, individual params in expandable sections -- Pro: Consistent with existing architecture. Con: Bottom sheet is already busy. - -#### Option C: Floating Control Strip -- A minimal floating strip (like the existing floating bar) with compound axis sliders -- Tap any axis to expand into a popover showing the individual override params -- Pro: Always visible, minimal footprint. Con: Screen real estate on mobile. - -#### Option D: Dedicated Mode -- A "Control Surface" mode alongside Visual and Synth modes -- Full-screen control panel when active, hidden during performance -- Pro: Maximum space for controls. Con: Can't adjust while playing. - -#### Option E: Hybrid -- Compound axes on the floating bar (always visible, 3 small sliders) -- Individual overrides in a settings drawer (gear icon) -- Pinning/zoom controls integrated into the joy-map (contextual, spatial) -- A/B compare as a floating toggle button near the RL buttons - -This is likely the right approach — distribute controls by frequency of use and spatial relevance. - ---- - -## Appendix A: Hand Tracking Zoom - -For hand tracking's 14 input dimensions, per-axis zoom is unmanageable. Instead, group inputs into semantic clusters: - -| Group | Inputs | Description | -|-------|--------|-------------| -| **Position** | palmX, palmY, depth | Spatial position of the hand | -| **Pose** | 5 finger curls, finger spread | Hand shape | -| **Orientation** | pitch, yaw, roll | Hand rotation | -| **Pinch** | pinch distance, pinch confidence | Fine gesture | - -Each group gets a single zoom slider. This is a backlog item for deeper exploration — see beads issue. - -## Appendix B: Zoom-Training Coupling - -When zoomed in, the training dynamics change fundamentally: - -1. **Examples are clustered** — all new examples fall within the zoom window. The network learns fine distinctions in this small region. -2. **Risk of catastrophic forgetting** — the network may distort its mapping for inputs outside the zoom window, since it's not seeing examples from those regions. -3. **Pinning mitigates this** — pinned examples from the broader region anchor the network's behavior outside the zoom window. - -**Recommended workflow for refinement**: -1. Explore broadly, find a mapping you mostly like -2. Pin the regions/parameters you want to keep -3. Zoom into the area that needs refinement -4. Use RL feedback at the zoomed-in scale -5. Zoom back out to verify the big picture -6. Unpin and iterate - -This workflow should be surfaced in the help modal and possibly guided by an onboarding tooltip sequence. diff --git a/playground/SPEC-shapeseq.md b/playground/SPEC-shapeseq.md deleted file mode 100644 index d84099c..0000000 --- a/playground/SPEC-shapeseq.md +++ /dev/null @@ -1,533 +0,0 @@ -# ShapeSeq — NISPS Generative Sequencing System - -## Overview - -ShapeSeq is a generative sequencing system for the NISPS playground where interactive ML (via the NISPS MLP engine) controls **parameters of algorithmic sequencing primitives** rather than raw note data. The user shapes sequences by navigating a learned parameter space with a joystick or hand tracking, can freeze sequences they like, then selectively re-expose specific parameters for further ML-driven exploration. - -ShapeSeq replaces the existing placeholder arpeggiator. In synth output mode, the ShapeSeq UI (circular step visualizer + chain builder + param sliders) replaces the flow-field particle system. - -## Core Architecture - -### Design Principles - -1. **MLP outputs are abstract [0,1] values** — musical meaning is applied downstream by the primitive chain and its symbolic processing -2. **Separate NISPS instances** for timbre control and sequence control, following the existing `imlJoy`/`imlHand` dual-instance pattern in `a-app.js`. Architecture supports future unification into a single instance -3. **Port-ready JS** — no closures in hot paths, explicit state, data structures that map cleanly to C++ for future RP2040 firmware porting. Note: the event bus and clock orchestration are JS-only concerns and not expected to port directly; the primitives themselves are the portable layer -4. **Modular primitives** — small, combinable algorithmic building blocks that generate musical patterns from continuous parameters -5. **Symbolic chain** — primitives compose as transforms over a pattern *description*, not concrete values. Each primitive takes the previous "pattern-generating machine" specification and produces a new one. The chain is evaluated once per loop (or on param change) to produce a complete pattern, which the clock then steps through - -### System Diagram - -``` - ┌──────────────────┐ - │ Clock Engine │ ← drives everything - │ (AudioContext) │ - └──────┬───────────┘ - │ tick - ▼ - ┌──────────────────┐ ┌─────────────────┐ - │ Sequencer Core │◄────│ Input Router │ - │ (orchestrator) │ │ (configurable) │ - └──┬───────────┬───┘ └──┬──────────┬───┘ - │ │ │ │ - │ query │ query ┌────▼───┐ ┌───▼────────┐ - │ pattern │ MLP │NISPS │ │ NISPS MLP │ - │ │ │(timbre)│ │ (sequence) │ - │ │ └───┬────┘ └───┬────────┘ - │ │ │ │ - │ ┌──────▼────────┐ │ ┌─────▼──────────┐ - │ │ Param Mapping │ │ │ Param Mapping │ - │ │ (16 MLP outs │ │ │ (16 MLP outs → │ - │ │ → N prim │ │ │ 126 synth │ - │ │ params) │ │ │ params) │ - │ └──────┬────────┘ │ └─────┬──────────┘ - │ │ │ │ - │ ┌──────▼────────┐ │ ┌─────▼──────────┐ - │ │Delta Controller│ │ │ Synth Param Map │ - │ │(frozen+deltas)│ │ └─────┬──────────┘ - │ └──────┬────────┘ │ │ - │ │ │ │ - │ ┌──────▼────────┐ │ │ - │ │Primitive Chain │ │ │ - │ │(symbolic eval) │ │ │ - │ └──────┬────────┘ │ │ - │ │ │ │ - ┌──────▼───────────▼───┐ │ │ - │ Namespaced Event Bus │ │ │ - │ seq.* ml.* ui.* │ │ │ - └──┬───────────────┬───┘ │ │ - │ │ │ │ - ┌──────▼───────┐ ┌────▼───────────▼────────▼──┐ - │ Circular Viz │ │ C15 Synth │ - │ + Chain UI │ │ (noteOn/Off + params) │ - └──────────────┘ └────────────────────────────┘ -``` - -### Fixed MLP + Param Mapping Layer - -The `WasmIML` creates an MLP with a **fixed output count** at construction time — it cannot be resized. Since the primitive chain is dynamic (users add/remove primitives, changing total param count), the MLP cannot output directly to primitive params. - -**Solution:** The sequence MLP always outputs a fixed number of values (e.g., 16). A **param mapping layer** fans these 16 outputs to however many primitive params the current chain requires. This is the same pattern used by the timbre MLP (which maps to 126 synth params via `param-map.js`). - -The mapping can be: -- **Automatic** (default): outputs are distributed across primitive params in chain order. If there are 30 primitive params and 16 MLP outputs, each output influences ~2 params via interpolation. -- Future: configurable user-defined mapping. - -> **Design note:** The fixed-16-output approach is the simplest starting point. If experimentation reveals that 16 is too few (or too many), the MLP can be reconstructed with a different size — this is a one-time setup cost, not a per-frame cost. The mapping layer insulates the rest of the system from this choice. Revisit if the mapping layer becomes a bottleneck for expressiveness. - -### Namespaced Event Bus - -A pub/sub event system with namespaced channels: - -| Namespace | Events | Purpose | -|-----------|--------|---------| -| `seq.*` | `seq.step`, `seq.noteOn`, `seq.noteOff`, `seq.paramChange`, `seq.loopStart` | Musical output from sequencer to synth and visualizer | -| `ml.*` | `ml.trained`, `ml.frozen`, `ml.unfrozen`, `ml.deltaUpdate` | ML state changes | -| `ui.*` | `ui.paramSelect`, `ui.chainEdit`, `ui.presetLoad`, `ui.freezeToggle` | User actions | - -All events carry a timestamp (AudioContext.currentTime for `seq.*`, performance.now() for others). - -Note: the event bus is a JS-only orchestration concern (string-namespaced pub/sub). It does not need to be port-ready — the portable layer is the primitives themselves. - -### Input Routing Matrix - -A configurable routing layer that maps any input source to either NISPS instance's inputs. Builds on the existing `imlJoy`/`imlHand` switching pattern in `a-app.js`. - -**Input sources:** -- Joystick X, Y (2 values) -- Hand tracking features (14 values) -- Gamepad axes (variable) - -**Routing targets:** -- Timbre NISPS input 0, input 1 -- Sequence NISPS input 0, input 1 - -Default: joystick → timbre NISPS, hand tracking features 0+1 → sequence NISPS. User-configurable via UI. - -## Sequencing Primitives - -### Primitive Categories - -Primitives are categorized by their role in the chain: - -| Category | Role | Examples | -|----------|------|----------| -| **Generator** | Produces data from params alone (no input required) | Euclidean, Density Morph, Pitch Walker | -| **Processor** | Transforms incoming data | Probability Gate, Velocity Shaper | -| **Timing Modifier** | Modulates the timing of events in the pattern description | Swing/Groove, Ratchet | -| **Converter** | Changes data type (e.g., continuous → discrete) | Interval Lock | - -**Generator combination rule:** When multiple generators appear in the same chain, their outputs combine according to the chain's **combination mode** (user-configurable in real time): -- **Additive** (OR) — triggers from any generator fire. Pitch/velocity values are averaged where multiple generators contribute. -- **Multiplicative** (AND) — only steps where ALL generators agree will fire. Creates sparser, more selective patterns. - -### Symbolic Chain Evaluation - -Primitives do NOT process concrete note data step-by-step. Instead, each primitive takes the previous **pattern description** (a symbolic representation of the entire sequence) and produces a new one. The complete chain is evaluated to produce a full pattern, which the clock then steps through. - -This means: -- **Timing modifiers** (Swing, Ratchet) work by annotating the pattern description with timing offsets and subdivisions *before* any concrete scheduling happens -- The clock reads the finalized pattern description and schedules all events (including ratchet subdivisions and swing offsets) using AudioContext.currentTime -- Re-evaluation happens when params change (MLP output updates, user edits), not on every tick - -**Pattern description structure:** -```javascript -// The symbolic output of the chain — a complete loop description -{ - steps: [ - { - trigger: true, // whether this step fires - pitch: 0.72, // [0,1] abstract pitch (pre-quantization) - velocity: 0.85, // [0,1] - accent: false, // accent flag - timeOffset: 0.0, // swing offset in fractions of a step (-0.5 to +0.5) - subdivisions: 1, // ratchet: 1 = normal, 2-4 = subdivided - }, - // ... one per step - ], - stepCount: 8, - metadata: { ... } // chain-specific info for visualization -} -``` - -### Primitive Definitions - -Each primitive is a pure function (or stateful generator with explicit state) that accepts a parameter object and produces typed output. All parameters are normalized [0,1]. - -#### 1. Euclidean Rhythm Generator - -**Category:** Generator -**Params:** `steps` (int, from continuous), `pulses` (int), `rotation` (int) -**Output:** trigger pattern (boolean array) -**Stateless:** yes - -Generates Bjorklund-distributed trigger patterns. The continuous [0,1] params are projected to integer ranges based on current step count. - -#### 2. Probability Gate - -**Category:** Processor -**Params:** `density` [0,1], `accentProbability` [0,1] -**Input:** trigger pattern -**Output:** filtered trigger pattern with accent flags -**Stateless:** yes (per-step coin flip using seeded PRNG) - -Each incoming trigger survives with probability `density`. Surviving triggers receive accent flag with probability `accentProbability`. - -#### 3. Pitch Walker - -**Category:** Generator -**Params:** `stepSize` [0,1], `directionBias` [0,1] (0.5=unbiased), `gravity` [0,1] (pull toward center), `range` [0,1] -**Output:** pitch values [0,1] per triggered step -**Stateful:** yes — maintains current position in pitch space - -Constrained random walk that generates melodic contour. `gravity` pulls the walk toward center (0.5), preventing it from getting stuck at extremes. State includes current position and PRNG state. - -#### 4. Ratchet - -**Category:** Timing Modifier -**Params:** `maxDivision` [0,1] (maps to 1-4 subdivisions), `probability` [0,1] -**Input:** pattern description with triggers -**Output:** pattern description with `subdivisions` field set per step -**Stateless:** yes (per-step coin flip) - -Annotates triggered steps with subdivision counts. The clock engine reads `subdivisions` and schedules rapid repeats within the step's time window. Division count determined by `maxDivision`, applied probabilistically. - -#### 5. Swing / Groove - -**Category:** Timing Modifier -**Params:** `swingAmount` [0,1] (0=straight, 1=full swing), `swingGrid` [0,1] (which subdivisions swing) -**Input:** pattern description -**Output:** pattern description with `timeOffset` field set per step -**Stateless:** yes - -Annotates alternating steps with timing offsets. At `swingAmount=0.67` this produces classic 2:1 shuffle. `swingGrid` controls whether swing applies to 8th notes, 16th notes, or triplets. The clock engine reads `timeOffset` and adjusts scheduling accordingly. - -#### 6. Density Morph - -**Category:** Generator -**Params:** `density` [0,1], `clustering` [0,1] (0=spread evenly, 1=clustered together) -**Output:** trigger pattern -**Stateless:** yes - -Alternative to Euclidean — generates trigger patterns with controllable density and spatial distribution. At high clustering, triggers group together creating bursts; at low clustering, triggers spread evenly. - -#### 7. Interval Lock (Scale Quantizer) - -**Category:** Converter -**Params:** `root` [0,1] (maps to 0-11 semitones), `mode` [0,1] (maps to scale index), `octaveRange` [0,1] (1-4 octaves) -**Input:** pitch values [0,1] -**Output:** MIDI note numbers -**Stateless:** yes - -The sole pitch quantization mechanism — the projection layer does NOT duplicate this. All pitch quantization goes through Interval Lock. - -Available scales: chromatic, major, natural minor, harmonic minor, pentatonic major, pentatonic minor, blues, dorian, mixolydian, whole tone, diminished. - -#### 8. Velocity Shaper - -**Category:** Processor -**Params:** `curveType` [0,1] (maps to: flat, accent-every-N, crescendo, decrescendo, random), `depth` [0,1], `phase` [0,1] -**Input:** trigger pattern with step indices -**Output:** velocity values [0,1] per step -**Stateless:** yes - -Applies cyclic velocity patterns. `phase` rotates the pattern, `depth` controls contrast between quiet and loud. - -### Primitive Interface - -```javascript -// Port-ready: explicit state, no closures -class Primitive { - constructor(name, paramSchema, category) { ... } - - // category: 'generator' | 'processor' | 'timing' | 'converter' - - // paramSchema: array of { name, min, max, default, boundary } - // boundary: 'clamp' | 'wrap' | 'scaled' - // For 'scaled': operates within ±scaledRange of frozen value - - // Symbolic processing: transforms a pattern description - process(params, patternDesc, state, rng) → { patternDesc, nextState } - - // State management for freeze - getState() → serializable object - setState(state) → void - getSeed() → number - setSeed(seed) → void -} -``` - -### Chain Connection Modes - -Three configurable modes for how primitives connect in a chain: - -**1. Sequential Pipeline** — each primitive transforms the pattern description in order. Generators create initial data, processors/timing modifiers transform it. If multiple generators appear, they combine according to the generator combination mode (additive/multiplicative, configurable in real time). - -**2. Parallel + Merge** — each primitive runs independently and produces a pattern description. Descriptions merge (OR for triggers in additive mode, AND in multiplicative mode; average for continuous values). Order doesn't matter. - -**3. Typed Routing** — primitives connect via typed ports. A primitive's output connects to the next primitive that accepts that type. Multiple primitives can feed the same type (merged). Most flexible, most complex. - -The chain connection mode is a global setting (per-chain), configurable via UI. Default: sequential pipeline. - -**Generator combination mode** (additive/multiplicative) is an independent setting, also configurable in real time via UI. - -## Delta Control System - -### Freeze Workflow - -1. User plays with NISPS, finds a sequence they like -2. User activates **freeze** — all current parameter values are captured -3. User selects specific parameters to **re-expose** (mark as "live"): - - Click/tap parameters in the UI - - Or use hand tracking: point with index finger, pinch gesture to toggle -4. Live parameters receive **deltas** from NISPS MLP output -5. Frozen parameters hold their captured values - -### Freeze Modes - -**Freeze as Algorithm** — captures parameter values + PRNG seed. Stateful primitives (pitch walker) will replay identically. Re-exposing params resumes algorithmic generation with delta-modified params. - -**Freeze as Pattern** — captures the realized note pattern (snapshot of all step events for one full loop). The primitive chain is bypassed; the sequencer loops the frozen pattern directly. Re-exposing params requires switching back to algorithm mode. - -User chooses freeze mode via UI toggle. - -### Delta Boundary Behavior - -Each parameter declares its boundary behavior: - -| Behavior | Description | Good for | -|----------|-------------|----------| -| `clamp` | Delta result clamped to [0,1] | Velocity, volume, most continuous params | -| `wrap` | Values wrap around (1.1 → 0.1) | Rotation, phase, cyclic params | -| `scaled` | Delta operates within ±`scaledRange` centered on frozen value | Precision control near a sweet spot | - -Parameters also declare a `scaledRange` (default 0.3) for the scaled boundary mode. Example: frozen value 0.8 with scaledRange 0.3 → effective range [0.5, 1.0], clamped at boundaries. - -## Clock Engine - -Replaces setTimeout-based arpeggiator with AudioContext-scheduled timing. - -```javascript -class ClockEngine { - constructor(audioContext) { ... } - - // Properties - bpm // beats per minute - stepCount // total steps in sequence - - // Lookahead scheduling: schedule events slightly ahead of time - // using AudioContext.currentTime for sample-accurate timing - start() → void - stop() → void - setTempo(bpm) → void - - // The clock reads the finalized pattern description and schedules - // all events, including: - // - timeOffset per step (swing) - // - subdivisions per step (ratchet) - // - accent flags (velocity scaling) - schedulePattern(patternDesc) → void - - // Callback: called with { stepIndex, time, velocity, pitch, isSubdivision } - onEvent(callback) → void -} -``` - -The clock uses the standard Web Audio lookahead pattern: -- A setInterval (~25ms) checks if any events need scheduling in the next ~100ms -- Events are scheduled using AudioContext.currentTime for sample-accurate timing -- This decouples visual updates (requestAnimationFrame) from audio timing -- The clock handles ratchet subdivisions and swing offsets natively by reading the pattern description's per-step `subdivisions` and `timeOffset` fields - -## Projection Layer - -A composable chain of transform functions that convert raw [0,1] primitive outputs into final musical values. Each transform is a small, independent module. - -Note: pitch quantization is handled by the **Interval Lock** primitive, not the projection layer. The projection layer handles non-pitch transforms only. - -### Available Transforms - -| Transform | Input | Output | Params | -|-----------|-------|--------|--------| -| Velocity Curve | [0,1] | [0,1] | curve shape (linear, exponential, S-curve) | -| Gate Threshold | [0,1] | boolean | threshold value | -| Range Map | [0,1] | [min,max] | min, max | -| Octave Folder | MIDI note | MIDI note | target octave range | -| Stutter Map | [0,1] | repeat count | max repeats | - -Transforms snap together: output type of one must match input type of next. The chain is validated on construction. - -### Projection Presets - -Pre-built chain configurations for common use cases: -- **Expressive** — velocity curve (exponential) → range map (48-84) -- **Percussive** — gate threshold (0.5) → velocity curve (accent) -- **Full Range** — range map (24-96) → velocity curve (linear) - -Users can edit any preset or build custom chains. - -## UI Design - -### Mode Integration - -ShapeSeq activates in **synth output mode**. When synth mode is active: -- The flow-field particle visualizer is replaced by the ShapeSeq UI (circular step viz + chain builder + param sliders) -- The timbre NISPS instance continues to control C15 synth parameters as before -- The sequence NISPS instance drives the ShapeSeq primitive chain - -In visual output mode, the particle system remains unchanged. - -### Circular Step Visualizer - -Steps arranged in a circle with even angular spacing (7 steps = heptagon, 13 steps = 13-gon, etc.). No grid overlay — the ear provides rhythmic context. - -**Visual elements:** -- Each step is a node on the circle -- Active/triggered steps glow or pulse -- Current playback position shown with a rotating indicator -- Pitch mapped to node distance from center (low=outer, high=inner) -- Velocity mapped to node size -- Accents shown with brighter color - -**Interaction:** -- Tap a step to solo/mute it -- Long-press for step detail (all params for that step) - -### Primitive Chain Builder - -Vertical stack layout (like a guitar pedalboard): -- Each primitive is a card with its name and key params visible -- Drag to reorder -- Swipe left to delete -- "+" button at bottom opens primitive palette -- Each card expandable to show all params as sliders -- Params marked as "live" (NISPS-controlled) get a distinct visual indicator (e.g., pulsing border) - -### Parameter Selection (for freeze/re-expose) - -Two input modes: -1. **Mouse/touch** — tap a parameter slider to toggle it between frozen (dimmed) and live (highlighted) -2. **Hand tracking** — point index finger at parameter, pinch to toggle. Visual cursor follows index finger tip. - -Live params show their current NISPS delta as a secondary indicator on the slider. - -### Layout - -``` -┌──────────────────────────────────┐ -│ Circular Step Viz │ -│ (upper half of screen) │ -│ │ -│ ○ ○ │ -│ ○ ○ │ -│ ○ ▶ ○ │ -│ ○ ○ │ -│ ○ ○ │ -│ │ -├──────────────────────────────────┤ -│ Chain Builder (scrollable stack) │ -│ ┌──────────────────────────────┐ │ -│ │ Euclidean [steps][pulses] │ │ -│ │ [rotation] │ │ -│ ├──────────────────────────────┤ │ -│ │ Prob Gate [density][accent] │ │ -│ ├──────────────────────────────┤ │ -│ │ Pitch Walk [step][bias] │ │ -│ ├──────────────────────────────┤ │ -│ │ [ + Add Primitive ] │ │ -│ └──────────────────────────────┘ │ -├──────────────────────────────────┤ -│ [▶ Play] [❄ Freeze] [Chain:Seq] │ -│ [+×] BPM:120 Steps:8 Gen:Add │ -└──────────────────────────────────┘ -``` - -## Phased Implementation Plan - -### Phase 1 — Foundation (MVP) - -**Goal:** All 8 primitives working, chain builder, basic UI, NISPS control. Full architecture with minimal polish. - -1. **Event bus** — namespaced pub/sub system -2. **Clock engine** — AudioContext-based precise timing with pattern description scheduling (handles swing offsets + ratchet subdivisions) -3. **Primitive framework** — base class, param schema, category system, state management, symbolic pattern description structure -4. **All 8 primitives** — implement each with their param schemas and categories -5. **Sequential chain** — primitives connected in sequence (pipeline mode only), with additive/multiplicative generator combination mode -6. **Param mapping layer** — fixed 16-output MLP → N primitive params, automatic distribution -7. **Projection layer** — velocity curve + gate threshold (2 transforms minimum, no scale quantizer — that's Interval Lock) -8. **Sequence NISPS instance** — second WasmIML (16 outputs), following existing `imlJoy`/`imlHand` pattern -9. **Basic circular viz** — step circle with playback indicator -10. **Basic chain UI** — vertical stack with sliders, add/remove primitives, generator combo mode toggle -11. **Bridge integration** — sequence events → C15 noteOn/noteOff via event bus -12. **Replace arpeggiator** — remove old arpeggiator, ShapeSeq takes over in synth mode - -### Phase 2 — Freeze & Delta Control - -1. **Freeze system** — capture params + seed, capture pattern snapshot -2. **Parameter selection UI** — click to toggle frozen/live -3. **Delta controller** — applies MLP deltas to live params with boundary config -4. **Hand tracking param select** — pinch gesture to toggle params -5. **Freeze mode toggle** — algorithm vs pattern freeze - -### Phase 3 — Advanced Chain & Routing - -1. **Parallel + merge chain mode** -2. **Typed routing chain mode** -3. **Input routing matrix** — configurable input → NISPS instance mapping -4. **Projection chain builder** — user-editable transform chains -5. **Projection presets** - -### Phase 4 — Polish & Expansion - -1. **Preset chains** — pre-built primitive combinations for common genres -2. **Save/load** — persist chain configs, frozen sequences, NISPS state -3. **Unified NISPS mode** — single MLP controlling both timbre + sequence -4. **Additional primitives** as discovered through experimentation -5. **Freeform lasso param selection** (see Future Work) -6. **Per-track variable step counts** (polyrhythm) - -## Open Design Questions - -These are deliberately deferred decisions to be revisited after experimentation: - -1. **MLP output count:** Is 16 the right number for the sequence MLP? Too few may limit expressiveness; too many may make learning harder. The param mapping layer insulates the system, so this can be changed without architectural impact. -2. **Param mapping strategy:** Automatic distribution is the starting point. Should users be able to manually wire MLP outputs to specific primitive params? This could enable more intentional control but adds UI complexity. -3. **Generator combination modes:** Additive and multiplicative are the starting pair. Other modes worth exploring: weighted average, priority (first generator wins), XOR (one or the other but not both). -4. **Chain evaluation frequency:** Currently re-evaluates when params change. Should there be an option for per-loop re-evaluation (stateful primitives produce different patterns each loop)? - -## Future Work - -- **Freeform lasso selection** — draw/lasso over the step visualization to select params spatially. Intuitive but complex to implement. (Backlog issue: meml-hud) -- **MIDI clock sync** — accept external MIDI clock for hardware sync -- **OSC output** — route sequencer events via OSC for external software/hardware -- **C++ port** — port primitive framework and chain system to nisps-core for RP2040 firmware -- **Multi-track** — multiple independent primitive chains running simultaneously with different step counts (polyrhythm) -- **Markov chain primitive** — transition-probability-based note selection -- **L-system primitive** — Lindenmayer system string rewriting for self-similar patterns -- **Cellular automata primitive** — 1D CA rules (e.g., Rule 30) generating trigger patterns - -## Technical Notes - -### Port-Ready JS Conventions - -To facilitate future C++ porting of the **primitive layer**: -- No closures in primitive process functions — all state is explicit -- Use typed arrays (Float32Array) for parameter vectors where possible -- Primitives are pure functions with explicit state in/out -- Seeded PRNG (not Math.random()) for deterministic replay -- All time values in seconds (AudioContext convention), not milliseconds - -The orchestration layer (event bus, clock, UI) is JS-only and not expected to port. - -### PRNG - -Use a seedable PRNG (e.g., mulberry32 or xoshiro128) so that: -- Freeze-as-algorithm can replay identical sequences from seed -- Different primitives in a chain get independent PRNG streams (derived from a master seed) -- Deterministic behavior aids debugging and reproducibility - -### Performance Budget - -The chain evaluates on param change, not per tick. The clock merely steps through the pre-computed pattern description. Per-tick cost is minimal: read the next step from the pattern, schedule the event. MLP inference (~<1ms) only runs when input changes. - -Chain re-evaluation (all 8 primitives) happens when the MLP output changes. At ~60fps input update rate, this means ~16ms budget per evaluation. Each primitive is simple math, so 8 primitives is well within budget even on mobile. diff --git a/playground/TODOS.md b/playground/TODOS.md deleted file mode 100644 index d545325..0000000 --- a/playground/TODOS.md +++ /dev/null @@ -1,9 +0,0 @@ - - -## synth view Immersive UI -- [x] add hover tooltip for each parameter slider (canvas tooltip follows mouse, shows name/value/range/curve) -- [x] hovering on the name of a module/group at the top should open a little drawer panel that allows us to set - - [x] minimum and maximum values for each parameter (dual-thumb range slider) - - [x] a curve parameter that's normalised and goes between logarithmic and exponential, with a little graph to visualise, to skew the distribution in either direction (per-param draggable canvas + group master curve with relative adjustment) - - [x] mute toggle per parameter (removes from NISPS, replaces with fixed value slider) -- [x] if the audio engine hasn't been initialised yet, the play button at the top left should be pulsing and have an orange highlight diff --git a/playground/a-immersive.html b/playground/a-immersive.html deleted file mode 100644 index 2a1cc11..0000000 --- a/playground/a-immersive.html +++ /dev/null @@ -1,446 +0,0 @@ - - - - - - - - NISPS Immersive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - -
-
- -
- -
-
- - - - - - - - -
- - - - -
- - - - - -
- 0 examples · untrained -
- - -
- - - - - - -
- - -
- - - - - - - - - - - - - - - - - -
- - - - - - - diff --git a/playground/b-workbench.html b/playground/b-workbench.html deleted file mode 100644 index 031013f..0000000 --- a/playground/b-workbench.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - NISPS Workbench - - - - - - -
- - -
-
- - - -

NISPS Workbench

-
-
- Visual - -
-
- - -
- - -
-
- Flow Field -
- - - -
-
-
- - -
-
- - -
-
- Input Space - -
-
-
- - -
Drag to explore · double-click or f to toggle follow
-
-
- -
- - 20 parameters -
-
- - -
-
- Parameters -
- - -
-
-
-
- - -
-
- Controls -
- - -
- - - - - - - -
- - - - - -
-
Loss
- -
- - -
-
Examples0
-
Loss
-
Noise0.050
-
Joy X,Y0.50, 0.50
-
-
-
- -
- - - - - - - -
- - - - diff --git a/playground/c-journey.html b/playground/c-journey.html deleted file mode 100644 index d429e05..0000000 --- a/playground/c-journey.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - NISPS Journey - - - - - - -
- - - - - - - - - - - - -
- - - -
- - -
-
- 20 -
-
- - -
- - -
- - -
-

Move the joystick to explore

-
- - - - -
- - -
- - - - - -
-
- - -
- - -
-
- -
- - -
- - -
- - - - - - -
- - -
- - -
- - - - - -
- Fine-tune parameters -
-
-
-
- - - - - -
- 0 examples - - - -
- - - -
- - -
- -
- - - - -
- - - - diff --git a/playground/c15/c15_engine.wasm b/playground/c15/c15_engine.wasm deleted file mode 100755 index 00ea237ec54718d60dd63235fd6dc1e87acebbf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 269526 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 A). At zero, the output signal of ", - "category": "Local_Unmodulateables", - "pid": "Osc_A_PM_B_Shp", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Shaper" - }, - { - "id": 68, - "valid": true, - "name": "Osc_A_PM_FB", - "displayName": "PM FB", - "groupName": "Oscillator A", - "groupShortName": "Oscillator A", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -0.5, - "max": 0.5, - "bipolar": true, - "description": "Amount of phase modulation by the Feedback signal.", - "category": "Local_Modulateables", - "pid": "Osc_A_PM_FB", - "scaleFactor": 0.5, - "scaleOffset": 0, - "shortName": "PM FB" - }, - { - "id": 70, - "valid": true, - "name": "Osc_A_PM_FB_Env_C", - "displayName": "PM FB - Env C", - "groupName": "Oscillator A", - "groupShortName": "Oscillator A", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (C) amount for the phase modulation by the Feedback signal. At zero, the modulation stays constant, ", - "category": "Local_Unmodulateables", - "pid": "Osc_A_PM_FB_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 71, - "valid": true, - "name": "Shp_A_Drive", - "displayName": "Drive", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0.2, - "scalingType": "Expon_Shaper_Drive", - "min": 0.18, - "max": 50.18, - "bipolar": false, - "description": "Input gain [in dB] of the sine shaper stage. Higher gains will create more distortion and harmonics.", - "category": "Local_Modulateables", - "pid": "Shp_A_Drive", - "scaleFactor": 50, - "scaleOffset": 0.18, - "shortName": "Drive" - }, - { - "id": 73, - "valid": true, - "name": "Shp_A_Drive_Env_A", - "displayName": "Drive - Env A", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (A) amount for the Drive factor. At zero, the gain stays constant, at higher values, the envelope ", - "category": "Local_Unmodulateables", - "pid": "Shp_A_Drive_Env_A", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env A" - }, - { - "id": 74, - "valid": true, - "name": "Shp_A_Fold", - "displayName": "Fold", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of folding back of the shaper curve for high input amplitudes.\\n0.0 %: flat saturation, no ", - "category": "Local_Unmodulateables", - "pid": "Shp_A_Fold", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Fold" - }, - { - "id": 75, - "valid": true, - "name": "Shp_A_Asym", - "displayName": "Asymmetry", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Asymmetry of the shaper curve, generating even (2nd, 4th, ...) harmonics. At higher values it becomes a ", - "category": "Local_Unmodulateables", - "pid": "Shp_A_Asym", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Asym" - }, - { - "id": 76, - "valid": true, - "name": "Shp_A_Mix", - "displayName": "Mix", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Mix amount of Shaper A in the signal sent to the Filters and to the Output Mixer. At zero, it is the input ", - "category": "Local_Modulateables", - "pid": "Shp_A_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 78, - "valid": true, - "name": "Shp_A_FB_Mix", - "displayName": "FB Mix", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Cubic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between Oscillator & Shaper A (at zero) and the Feedback signal for the signal A.", - "category": "Local_Modulateables", - "pid": "Shp_A_FB_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "FB Mix" - }, - { - "id": 80, - "valid": true, - "name": "Shp_A_FB_Env_C", - "displayName": "FB - Env C", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (C) amount for the Feedback Mix. At zero, only a Gate signal is applied, at higher values, Envelope ", - "category": "Local_Unmodulateables", - "pid": "Shp_A_FB_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 81, - "valid": true, - "name": "Shp_A_Ring_Mod", - "displayName": "Ring Mod", - "groupName": "Shaper A", - "groupShortName": "Shaper A", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Mix amount of the ring modulation between both Oscillators & Shapers.", - "category": "Local_Modulateables", - "pid": "Shp_A_Ring_Mod", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Ring Mod" - }, - { - "id": 83, - "valid": true, - "name": "Osc_B_Pitch", - "displayName": "Pitch", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0.5333333333333333, - "scalingType": "Linear", - "min": -20, - "max": 130, - "bipolar": false, - "description": "Pitch (logarithmic frequency) of Oscillator B at C3 (MIDI note 60) [in semitones, based on MIDI note ", - "category": "Local_Modulateables", - "pid": "Osc_B_Pitch", - "scaleFactor": 150, - "scaleOffset": -20, - "shortName": "Pitch" - }, - { - "id": 85, - "valid": true, - "name": "Osc_B_Pitch_KT", - "displayName": "Pitch - Key Trk", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0.9523809523809523, - "scalingType": "Linear", - "min": 0, - "max": 1.05, - "bipolar": false, - "description": "Key tracking of the oscillator pitch. It's the scaling factor between the key position of a received MIDI ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_Pitch_KT", - "scaleFactor": 1.05, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 86, - "valid": true, - "name": "Osc_B_Pitch_Env_C", - "displayName": "Pitch - Env C", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -80, - "max": 80, - "bipolar": true, - "description": "Amount of pitch modulation by Envelope C [in semitones].", - "category": "Local_Unmodulateables", - "pid": "Osc_B_Pitch_Env_C", - "scaleFactor": 80, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 87, - "valid": true, - "name": "Osc_B_Fluct", - "displayName": "Fluctuation", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": 0, - "max": 0.95, - "bipolar": false, - "description": "Fluctuation of the oscillator frequency. If the parameter is set larger than 0.0 %, the frequency is changed ", - "category": "Local_Modulateables", - "pid": "Osc_B_Fluct", - "scaleFactor": 0.95, - "scaleOffset": 0, - "shortName": "Fluctuation" - }, - { - "id": 89, - "valid": true, - "name": "Osc_B_Fluct_Env_C", - "displayName": "Fluct - Env C", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of modulation of the frequency fluctuation by Envelope C. At zero, the modulation stays constant, at ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_Fluct_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 90, - "valid": true, - "name": "Osc_B_PM_Self", - "displayName": "PM Self", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -0.5, - "max": 0.5, - "bipolar": true, - "description": "Amount of phase modulation by Oscillator & Shaper B (local feedback).", - "category": "Local_Modulateables", - "pid": "Osc_B_PM_Self", - "scaleFactor": 0.5, - "scaleOffset": 0, - "shortName": "PM Self" - }, - { - "id": 92, - "valid": true, - "name": "Osc_B_PM_Self_Env_B", - "displayName": "PM Self - Env B", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (B) amount for the phase modulation by Oscillator & Shaper B (local feedback). At zero, the ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_PM_Self_Env_B", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env B" - }, - { - "id": 93, - "valid": true, - "name": "Osc_B_PM_Self_Shp", - "displayName": "PM Self - Shaper", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Mix amount of Shaper B in the signal being used for phase modulation (self). At zero, the output signal of ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_PM_Self_Shp", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Shaper" - }, - { - "id": 94, - "valid": true, - "name": "Osc_B_PM_A", - "displayName": "PM A", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Amount of phase modulation by Oscillator & Shaper A (cross feedback).", - "category": "Local_Modulateables", - "pid": "Osc_B_PM_A", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "PM A" - }, - { - "id": 96, - "valid": true, - "name": "Osc_B_PM_A_Env_A", - "displayName": "PM A - Env A", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (A) amount for the pahse modulation by Oscillator & Shaper A (cross feedback). At zero, the ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_PM_A_Env_A", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env A" - }, - { - "id": 97, - "valid": true, - "name": "Osc_B_PM_A_Shp", - "displayName": "PM A - Shaper", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Mix amount of Shaper A in the signal being used for phase modulation (A -> B). At zero, the output signal of ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_PM_A_Shp", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Shaper" - }, - { - "id": 98, - "valid": true, - "name": "Osc_B_PM_FB", - "displayName": "PM FB", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -0.5, - "max": 0.5, - "bipolar": true, - "description": "Amount of phase modulation by the Feedback signal.", - "category": "Local_Modulateables", - "pid": "Osc_B_PM_FB", - "scaleFactor": 0.5, - "scaleOffset": 0, - "shortName": "PM FB" - }, - { - "id": 100, - "valid": true, - "name": "Osc_B_PM_FB_Env_C", - "displayName": "PM FB - Env C", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (C) amount for the phase modulation by the Feedback signal. At zero, the modulation stays constant, ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_PM_FB_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 101, - "valid": true, - "name": "Shp_B_Drive", - "displayName": "Drive", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0.2, - "scalingType": "Expon_Shaper_Drive", - "min": 0.18, - "max": 50.18, - "bipolar": false, - "description": "Input gain [in dB] of the sine shaper stage. Higher gains will create more distortion and harmonics.", - "category": "Local_Modulateables", - "pid": "Shp_B_Drive", - "scaleFactor": 50, - "scaleOffset": 0.18, - "shortName": "Drive" - }, - { - "id": 103, - "valid": true, - "name": "Shp_B_Drive_Env_B", - "displayName": "Drive - Env B", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (B) amount for the Drive factor. At zero, the gain stays constant, at higher values, the envelope ", - "category": "Local_Unmodulateables", - "pid": "Shp_B_Drive_Env_B", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env B" - }, - { - "id": 104, - "valid": true, - "name": "Shp_B_Fold", - "displayName": "Fold", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of folding back of the shaper curve for high input amplitudes.\\n0.0 %: flat saturation, no ", - "category": "Local_Unmodulateables", - "pid": "Shp_B_Fold", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Fold" - }, - { - "id": 105, - "valid": true, - "name": "Shp_B_Asym", - "displayName": "Asymmetry", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Asymmetry of the shaper curve, generating even (2nd, 4th, ...) harmonics. At higher values it becomes a ", - "category": "Local_Unmodulateables", - "pid": "Shp_B_Asym", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Asym" - }, - { - "id": 106, - "valid": true, - "name": "Shp_B_Mix", - "displayName": "Mix", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Mix amount of Shaper B in the signal sent to the Filters and to the Output Mixer. At zero, it is the input ", - "category": "Local_Modulateables", - "pid": "Shp_B_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 108, - "valid": true, - "name": "Shp_B_FB_Mix", - "displayName": "FB Mix", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Cubic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between Oscillator & Shaper B (at zero) and the Feedback signal for the signal B.", - "category": "Local_Modulateables", - "pid": "Shp_B_FB_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "FB Mix" - }, - { - "id": 110, - "valid": true, - "name": "Shp_B_FB_Env_C", - "displayName": "FB - Env C", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Envelope (C) amount for the Feedback Mix. At zero, only a Gate signal is applied, at higher values, Envelope ", - "category": "Local_Unmodulateables", - "pid": "Shp_B_FB_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 111, - "valid": true, - "name": "Shp_B_Ring_Mod", - "displayName": "Ring Mod", - "groupName": "Shaper B", - "groupShortName": "Shaper B", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Mix amount of the ring modulation between both Oscillators & Shapers.", - "category": "Local_Modulateables", - "pid": "Shp_B_Ring_Mod", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Ring Mod" - }, - { - "id": 113, - "valid": true, - "name": "Comb_Flt_In_A_B", - "displayName": "A - B", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "S_Curve", - "min": 0, - "max": 1, - "bipolar": false, - "description": "The signal for the Comb Filter as a crossfade between the outputs of Oscillator & Shaper A and Oscillator & ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_In_A_B", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "A - B" - }, - { - "id": 115, - "valid": true, - "name": "Comb_Flt_Pitch", - "displayName": "Pitch", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0.5, - "scalingType": "Expon_Lin_Pitch", - "min": 0, - "max": 120, - "bipolar": false, - "description": "Coarse pitch of the Comb Filter (delay) at C3 (MIDI note 60) [in semitones, based on MIDI note numbers].", - "category": "Local_Modulateables", - "pid": "Comb_Flt_Pitch", - "scaleFactor": 120, - "scaleOffset": 0, - "shortName": "Pitch" - }, - { - "id": 117, - "valid": true, - "name": "Comb_Flt_Pitch_KT", - "displayName": "Pitch - Key Trk", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0.9523809523809523, - "scalingType": "Linear", - "min": 0, - "max": 1.05, - "bipolar": false, - "description": "Key scaling of the tuning of the Comb Filter (delay).\\n0.0 %: same tuning for all keys\\n100.0 %: full ", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_Pitch_KT", - "scaleFactor": 1.05, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 118, - "valid": true, - "name": "Comb_Flt_Pitch_Env_C", - "displayName": "Pitch - Env C", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -80, - "max": 80, - "bipolar": true, - "description": "Amount of modulation of the Comb Filter pitch by Envelope C [in semitones].", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_Pitch_Env_C", - "scaleFactor": 80, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 119, - "valid": true, - "name": "Comb_Flt_Decay", - "displayName": "Decay", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -100, - "max": 100, - "bipolar": true, - "description": "Amount of the internal feedback noticeable as the decay time of the impules response [logarithmic scaling]. ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_Decay", - "scaleFactor": 100, - "scaleOffset": 0, - "shortName": "Decay" - }, - { - "id": 121, - "valid": true, - "name": "Comb_Flt_Decay_KT", - "displayName": "Decay - Key Trk", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0.33, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Key scaling of the decay time.\\n0.0 %: equal time for all keys\\n100.0 %: shortening to half time per octave, ", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_Decay_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 123, - "valid": true, - "name": "Comb_Flt_AP_Tune", - "displayName": "Allpass - Tune", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 1, - "scalingType": "Expon_Lin_Pitch", - "min": 0, - "max": 140, - "bipolar": false, - "description": "Center frequency of the 2-pole allpass filter [in semitones]. It is the frequency where the phase is shifted ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_AP_Tune", - "scaleFactor": 140, - "scaleOffset": 0, - "shortName": "AP Tune" - }, - { - "id": 125, - "valid": true, - "name": "Comb_Flt_AP_KT", - "displayName": "Allpass - Key Trk", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Key scaling of the allpass filter frequency.\\n0.0 %: same tuning for all keys\\n100.0 %: full tracking with ", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_AP_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 126, - "valid": true, - "name": "Comb_Flt_AP_Env_C", - "displayName": "Allpass - Env C", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -80, - "max": 80, - "bipolar": true, - "description": "Amount of modulation of the allpass center frequency by Envelope C [in semitones].", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_AP_Env_C", - "scaleFactor": 80, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 127, - "valid": true, - "name": "Comb_Flt_AP_Res", - "displayName": "Allpass - Reson", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Resonance of the 2-pole allpass. Determines how much the phase shift increases around the center ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_AP_Res", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "AP Reson" - }, - { - "id": 129, - "valid": true, - "name": "Comb_Flt_LP_Tune", - "displayName": "Hi Cut", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 1, - "scalingType": "Expon_Lin_Pitch", - "min": 40, - "max": 140, - "bipolar": false, - "description": "Cutoff frequency [in semitones] of the lowpass filter that damps the higher frequencies in the comb filter's ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_LP_Tune", - "scaleFactor": 100, - "scaleOffset": 40, - "shortName": "Hi Cut" - }, - { - "id": 131, - "valid": true, - "name": "Comb_Flt_LP_KT", - "displayName": "Hi Cut - Key Trk", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Key scaling of the lowpass cutoff frequency.\\n0.0 %: same cutoff for all keys\\n100.0 %: full tracking with ", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_LP_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 132, - "valid": true, - "name": "Comb_Flt_LP_Env_C", - "displayName": "Hi Cut - Env C ", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -100, - "max": 100, - "bipolar": true, - "description": "Amount of modulation of the cutoff frequency of the lowpass filter by Envelope C [in semitones].", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_LP_Env_C", - "scaleFactor": 100, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 133, - "valid": true, - "name": "Comb_Flt_PM", - "displayName": "PM", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -0.9, - "max": 0.9, - "bipolar": true, - "description": "Amount of (phase) modulation of the comb filter pitch (delay time) by the output signals of Oscillator & ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_PM", - "scaleFactor": 0.9, - "scaleOffset": 0, - "shortName": "PM" - }, - { - "id": 135, - "valid": true, - "name": "Comb_Flt_PM_A_B", - "displayName": "PM - A-B", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "The (phase) modulation signal for the Comb Filter as a crossfade between the outputs of Oscillator & Shaper A ", - "category": "Local_Unmodulateables", - "pid": "Comb_Flt_PM_A_B", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "A - B" - }, - { - "id": 136, - "valid": true, - "name": "SV_Flt_In_A_B", - "displayName": "A - B", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "The signal for the State Variable Filter as a crossfade between the outputs of Oscillator & Shaper A and ", - "category": "Local_Modulateables", - "pid": "SV_Flt_In_A_B", - "shortName": "A - B" - }, - { - "id": 138, - "valid": true, - "name": "SV_Flt_Comb_Mix", - "displayName": "Comb Mix", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "The input signal for the State Variable Filter as mix of the signals from the A-B mixer and from the Comb ", - "category": "Local_Modulateables", - "pid": "SV_Flt_Comb_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Comb Mix" - }, - { - "id": 140, - "valid": true, - "name": "SV_Flt_Cut", - "displayName": "Cutoff", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0.5, - "scalingType": "Expon_Lin_Pitch", - "min": 20, - "max": 140, - "bipolar": false, - "description": "Static value of the filter cutoff frequency at C3 [in semitones], applies to both stages of the filter. The ", - "category": "Local_Modulateables", - "pid": "SV_Flt_Cut", - "scaleFactor": 120, - "scaleOffset": 20, - "shortName": "Cutoff" - }, - { - "id": 142, - "valid": true, - "name": "SV_Flt_Cut_KT", - "displayName": "Cutoff - Key Trk", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Key scaling of the filter cutoffs.\\n0.0 %: no influence\\n100.0 %: full tracking with the keys, origin at C3 = ", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_Cut_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 143, - "valid": true, - "name": "SV_Flt_Cut_Env_C", - "displayName": "Cutoff - Env C", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -100, - "max": 100, - "bipolar": true, - "description": "Amount of cutoff modulation by Envelope C [in semitones].", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_Cut_Env_C", - "scaleFactor": 100, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 144, - "valid": true, - "name": "SV_Flt_Res", - "displayName": "Resonance", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of filter resonance, creating peaks at the cutoff frequencies.", - "category": "Local_Modulateables", - "pid": "SV_Flt_Res", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Reson" - }, - { - "id": 146, - "valid": true, - "name": "SV_Flt_Res_KT", - "displayName": "Reson - Key Trk", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Key scaling of the filter resonance.\\n0.0 %: no influence\\n100.0 %: full tracking with the keys, origin at C3 ", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_Res_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 147, - "valid": true, - "name": "SV_Flt_Res_Env_C", - "displayName": "Reson - Env C ", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Amount of resonance modulation by Envelope C.", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_Res_Env_C", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Env C" - }, - { - "id": 148, - "valid": true, - "name": "SV_Flt_Spread", - "displayName": "Spread", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0.2, - "scalingType": "Linear", - "min": -30, - "max": 30, - "bipolar": true, - "description": "Amount of splitting of the cutoffs of the two 2-pole filters. Half of the value is applied as a positive ", - "category": "Local_Modulateables", - "pid": "SV_Flt_Spread", - "scaleFactor": 30, - "scaleOffset": 0, - "shortName": "Spread" - }, - { - "id": 150, - "valid": true, - "name": "SV_Flt_LBH", - "displayName": "L - B - H", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between the lowpass, bandpass and highpass outputs of the two filter stages.\\n(first stage -> ", - "category": "Local_Modulateables", - "pid": "SV_Flt_LBH", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "L-B-H" - }, - { - "id": 152, - "valid": true, - "name": "SV_Flt_Par", - "displayName": "Parallel", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "At zero, the two 2-pole filter stages are in series forming a 4-pole filter. Positive values crossfade from ", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_Par", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Parallel " - }, - { - "id": 153, - "valid": true, - "name": "SV_Flt_FM", - "displayName": "FM", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -5, - "max": 5, - "bipolar": true, - "description": "Amount of modulation of the cutoff frequencies by the output signals of Oscillator & Shaper A and B, with the ", - "category": "Local_Modulateables", - "pid": "SV_Flt_FM", - "scaleFactor": 5, - "scaleOffset": 0, - "shortName": "FM" - }, - { - "id": 155, - "valid": true, - "name": "SV_Flt_FM_A_B", - "displayName": "FM - A-B", - "groupName": "State Variable Filter", - "groupShortName": "SV Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "The signal for the FM (cutoff frequency modulation) of the State Variable Filter as a crossfade between the ", - "category": "Local_Unmodulateables", - "pid": "SV_Flt_FM_A_B", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "A - B" - }, - { - "id": 156, - "valid": true, - "name": "FB_Mix_Comb", - "displayName": "Comb Filter", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Feedback mix factor for the output of the Comb Filter.", - "category": "Local_Modulateables", - "pid": "FB_Mix_Comb", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Comb" - }, - { - "id": 158, - "valid": true, - "name": "FB_Mix_SVF", - "displayName": "SV Filter", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Feedback mix factor for the output of the State Variable Filter.", - "category": "Local_Modulateables", - "pid": "FB_Mix_SVF", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "SV Filter" - }, - { - "id": 160, - "valid": true, - "name": "FB_Mix_FX", - "displayName": "Effects", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Feedback mix factor for the output of the Effects chain. The reverb amount in the feedback can be set by the ", - "category": "Local_Modulateables", - "pid": "FB_Mix_FX", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Effects" - }, - { - "id": 162, - "valid": true, - "name": "FB_Mix_Rvb", - "displayName": "Reverb Mix", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Controls the amount of reverb in the feedback independantly from the Mix in the Reverb section.\\n0.0 %: dry, ", - "category": "Local_Modulateables", - "pid": "FB_Mix_Rvb", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Rev Mix" - }, - { - "id": 164, - "valid": true, - "name": "FB_Mix_Drive", - "displayName": "Drive", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0.2857142857142857, - "scalingType": "Expon_Mix_Drive", - "min": 0.25, - "max": 70.25, - "bipolar": false, - "description": "Input gain [in dB] of the sine shaper stage. Higher gains will create more distortion and harmonics.", - "category": "Local_Modulateables", - "pid": "FB_Mix_Drive", - "scaleFactor": 70, - "scaleOffset": 0.25, - "shortName": "Drive" - }, - { - "id": 166, - "valid": true, - "name": "FB_Mix_Fold", - "displayName": "Fold", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of folding back of the shaper curve for high input amplitudes.\\n0.0 %: flat saturation, no ", - "category": "Local_Unmodulateables", - "pid": "FB_Mix_Fold", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Fold" - }, - { - "id": 167, - "valid": true, - "name": "FB_Mix_Asym", - "displayName": "Asymmetry", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Asymmetry of the shaper curve, generating even (2nd, 4th, ...) harmonics. At higher values, it becomes a ", - "category": "Local_Unmodulateables", - "pid": "FB_Mix_Asym", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Asym" - }, - { - "id": 168, - "valid": true, - "name": "FB_Mix_Lvl_KT", - "displayName": "Level - Key Trk", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Key scaling of the feedback level [in dB per semitone].\\npositive values: higher level for higher notes (+ ", - "category": "Local_Unmodulateables", - "pid": "FB_Mix_Lvl_KT", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Key Trk" - }, - { - "id": 169, - "valid": true, - "name": "Out_Mix_A_Lvl", - "displayName": "A - Level", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0.75, - "scalingType": "Linear", - "min": -2, - "max": 2, - "bipolar": true, - "description": "Output mix factor for the signal from Oscillator & Shaper A.", - "category": "Local_Modulateables", - "pid": "Out_Mix_A_Lvl", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "A" - }, - { - "id": 171, - "valid": true, - "name": "Out_Mix_A_Pan", - "displayName": "A - Pan", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": true, - "description": "Pan position of the signal from Oscillator & Shaper A in the output mix.", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_A_Pan", - "scaleFactor": 0.5, - "scaleOffset": 0.5, - "shortName": "Pan" - }, - { - "id": 172, - "valid": true, - "name": "Out_Mix_B_Lvl", - "displayName": "B - Level", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -2, - "max": 2, - "bipolar": true, - "description": "Output mix factor for the signal from Oscillator & Shaper B.", - "category": "Local_Modulateables", - "pid": "Out_Mix_B_Lvl", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "B" - }, - { - "id": 174, - "valid": true, - "name": "Out_Mix_B_Pan", - "displayName": "B - Pan", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": true, - "description": "Pan position of the signal from Oscillator & Shaper B in the output mix.", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_B_Pan", - "scaleFactor": 0.5, - "scaleOffset": 0.5, - "shortName": "Pan" - }, - { - "id": 175, - "valid": true, - "name": "Out_Mix_Comb_Lvl", - "displayName": "Comb - Level", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -2, - "max": 2, - "bipolar": true, - "description": "Output mix factor for the signal from the Comb Filter.", - "category": "Local_Modulateables", - "pid": "Out_Mix_Comb_Lvl", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "Comb" - }, - { - "id": 177, - "valid": true, - "name": "Out_Mix_Comb_Pan", - "displayName": "Comb - Pan", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": true, - "description": "Pan position of the signal from the Comb Filter in the output mix.", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_Comb_Pan", - "scaleFactor": 0.5, - "scaleOffset": 0.5, - "shortName": "Pan" - }, - { - "id": 178, - "valid": true, - "name": "Out_Mix_SVF_Lvl", - "displayName": "SV Filter - Level", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -2, - "max": 2, - "bipolar": true, - "description": "Output mix factor for the signal from the State Variable Filter.", - "category": "Local_Modulateables", - "pid": "Out_Mix_SVF_Lvl", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "SV Filter" - }, - { - "id": 180, - "valid": true, - "name": "Out_Mix_SVF_Pan", - "displayName": "SV Filter - Pan", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": true, - "description": "Pan position of the signal from the State Variable Filter in the output mix.", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_SVF_Pan", - "scaleFactor": 0.5, - "scaleOffset": 0.5, - "shortName": "Pan" - }, - { - "id": 181, - "valid": true, - "name": "Out_Mix_Drive", - "displayName": "Drive", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Expon_Mix_Drive", - "min": 0.25, - "max": 50.25, - "bipolar": false, - "description": "Input gain [in dB] of the sine shaper stage. Higher gains will create more distortion and harmonics.", - "category": "Local_Modulateables", - "pid": "Out_Mix_Drive", - "scaleFactor": 50, - "scaleOffset": 0.25, - "shortName": "Drive" - }, - { - "id": 183, - "valid": true, - "name": "Out_Mix_Fold", - "displayName": "Fold", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of folding back of the shaper curve for high input amplitudes.\\n0.0 %: flat saturation, no ", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_Fold", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Fold" - }, - { - "id": 184, - "valid": true, - "name": "Out_Mix_Asym", - "displayName": "Asymmetry", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Asymmetry of the shaper curve, generating even (2nd, 4th, ...) harmonics. At higher values, it becomes a ", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_Asym", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Asym" - }, - { - "id": 185, - "valid": true, - "name": "Out_Mix_Lvl", - "displayName": "Level", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0.38, - "scalingType": "Parabolic", - "min": 0, - "max": 2.56, - "bipolar": false, - "description": "Master output level [in dB] of the synth. The output signal will be processed by the (monophonic) chain of ", - "category": "Local_Modulateables", - "pid": "Out_Mix_Lvl", - "scaleFactor": 2.56, - "scaleOffset": 0, - "shortName": "Level" - }, - { - "id": 187, - "valid": true, - "name": "Out_Mix_Key_Pan", - "displayName": "Key Pan", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of key panning for each note (referring to the key position relative to the center at C3 = 60 ", - "category": "Local_Unmodulateables", - "pid": "Out_Mix_Key_Pan", - "shortName": "Key Pan" - }, - { - "id": 188, - "valid": true, - "name": "Cabinet_Drive", - "displayName": "Drive", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.4, - "scalingType": "Expon_Gain", - "min": 0, - "max": 50, - "bipolar": false, - "description": "Gain [in dB] for the input signal. Higher gains will increase the amount of distortion/saturation.", - "category": "Local_Modulateables", - "pid": "Cabinet_Drive", - "scaleFactor": 50, - "scaleOffset": 0, - "shortName": "Drive" - }, - { - "id": 190, - "valid": true, - "name": "Cabinet_Fold", - "displayName": "Fold", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.25, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of folding back of the shaper curve for high input amplitudes.\\n0.0 %: flat saturation, no ", - "category": "Local_Unmodulateables", - "pid": "Cabinet_Fold", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Fold" - }, - { - "id": 191, - "valid": true, - "name": "Cabinet_Asym", - "displayName": "Asymmetry", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.25, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Asymmetry of the shaper curve, generating even (2nd, 4th, ...) harmonics. At higher values, it becomes a ", - "category": "Local_Unmodulateables", - "pid": "Cabinet_Asym", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Asym" - }, - { - "id": 192, - "valid": true, - "name": "Cabinet_Tilt", - "displayName": "Tilt", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": -100, - "max": 100, - "bipolar": true, - "description": "Controls two inverted shelving EQs before and after the distortion stage.\\nNegative: more distortions at high ", - "category": "Local_Modulateables", - "pid": "Cabinet_Tilt", - "scaleFactor": 100, - "scaleOffset": 0, - "shortName": "Tilt" - }, - { - "id": 194, - "valid": true, - "name": "Cabinet_Hi_Cut", - "displayName": "Hi Cut", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.625, - "scalingType": "Expon_Lin_Pitch", - "min": 60, - "max": 140, - "bipolar": false, - "description": "Cutoff frequency [in semitones] of the lowpass filter at the output.", - "category": "Local_Modulateables", - "pid": "Cabinet_Hi_Cut", - "scaleFactor": 80, - "scaleOffset": 60, - "shortName": "Hi Cut" - }, - { - "id": 196, - "valid": true, - "name": "Cabinet_Lo_Cut", - "displayName": "Lo Cut", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.125, - "scalingType": "Expon_Lin_Pitch", - "min": 20, - "max": 100, - "bipolar": false, - "description": "Cutoff frequency [in semitones] of the highpass filter at the input.", - "category": "Local_Unmodulateables", - "pid": "Cabinet_Lo_Cut", - "scaleFactor": 80, - "scaleOffset": 20, - "shortName": "Lo Cut" - }, - { - "id": 197, - "valid": true, - "name": "Cabinet_Cab_Lvl", - "displayName": "Cab Level", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0.72, - "scalingType": "Expon_Gain", - "min": -50, - "max": 0, - "bipolar": false, - "description": "Output level [in dB] of the saturation effect before it is mixed with the dry signal.", - "category": "Local_Modulateables", - "pid": "Cabinet_Cab_Lvl", - "scaleFactor": 50, - "scaleOffset": -50, - "shortName": "Cab Level" - }, - { - "id": 199, - "valid": true, - "name": "Cabinet_Mix", - "displayName": "Mix", - "groupName": "Cabinet", - "groupShortName": "Cabinet", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between the dry signal and the saturated signal.", - "category": "Local_Modulateables", - "pid": "Cabinet_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 201, - "valid": true, - "name": "Gap_Flt_Center", - "displayName": "Center", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 24, - "max": 120, - "bipolar": false, - "description": "Shifts the mean cutoff frequency of both 4-pole filters on both channels up or down [in semitones].", - "category": "Local_Modulateables", - "pid": "Gap_Flt_Center", - "scaleFactor": 96, - "scaleOffset": 24, - "shortName": "Center" - }, - { - "id": 203, - "valid": true, - "name": "Gap_Flt_Stereo", - "displayName": "Stereo", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -36, - "max": 36, - "bipolar": true, - "description": "Sets the difference between the center frequencies of the left and of the right channel [in semitones].", - "category": "Local_Unmodulateables", - "pid": "Gap_Flt_Stereo", - "scaleFactor": 36, - "scaleOffset": 0, - "shortName": "Stereo" - }, - { - "id": 204, - "valid": true, - "name": "Gap_Flt_Gap", - "displayName": "Gap", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0.125, - "scalingType": "Linear", - "min": 0, - "max": 48, - "bipolar": false, - "description": "Offset between the cutoffs of the lowpass and the highpass [in semitones]. When the filters are running in ", - "category": "Local_Modulateables", - "pid": "Gap_Flt_Gap", - "scaleFactor": 48, - "scaleOffset": 0, - "shortName": "Gap" - }, - { - "id": 206, - "valid": true, - "name": "Gap_Flt_Res", - "displayName": "Resonance", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 0.9, - "bipolar": false, - "description": "Resonance of both filters. Higher values create two resonance peaks at the upper and lower end of the ", - "category": "Local_Unmodulateables", - "pid": "Gap_Flt_Res", - "scaleFactor": 0.9, - "scaleOffset": 0, - "shortName": "Reson" - }, - { - "id": 207, - "valid": true, - "name": "Gap_Flt_Bal", - "displayName": "Balance", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Balance between the levels of the ranges above and below the gap.\\nIn the center position both ranges are ", - "category": "Local_Modulateables", - "pid": "Gap_Flt_Bal", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Balance" - }, - { - "id": 209, - "valid": true, - "name": "Gap_Flt_Mix", - "displayName": "Mix", - "groupName": "Gap Filter", - "groupShortName": "Gap Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Crossfades between the dry signal and the filtered signal. At positive values, the filter runs in parallel ", - "category": "Local_Modulateables", - "pid": "Gap_Flt_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 211, - "valid": true, - "name": "Flanger_Time_Mod", - "displayName": "Time Mod", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Relative amount of the modulation of the delay times by the LFO and/or the envelope.", - "category": "Local_Modulateables", - "pid": "Flanger_Time_Mod", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Time Mod" - }, - { - "id": 213, - "valid": true, - "name": "Flanger_Phase", - "displayName": "Phase", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 180, - "bipolar": false, - "description": "Phase offset between the LFO signals modulating the delay times in the left and the right channel.", - "category": "Local_Unmodulateables", - "pid": "Flanger_Phase", - "scaleFactor": 180, - "scaleOffset": 0, - "shortName": "Phase" - }, - { - "id": 214, - "valid": true, - "name": "Flanger_Rate", - "displayName": "Rate", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0.317, - "scalingType": "Parabolic", - "min": 0, - "max": 10, - "bipolar": false, - "description": "Frequency of the LFO and rate of the envelope. Both can be modulation sources for the delay times and the ", - "category": "Local_Modulateables", - "pid": "Flanger_Rate", - "scaleFactor": 10, - "scaleOffset": 0, - "shortName": "Rate" - }, - { - "id": 216, - "valid": true, - "name": "Flanger_Time", - "displayName": "Time", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0.317, - "scalingType": "Parabolic", - "min": 0, - "max": 50, - "bipolar": false, - "description": "Mean value of the delay times in the left and right channel.\\nThe \\", - "category": "Local_Modulateables", - "pid": "Flanger_Time", - "scaleFactor": 50, - "scaleOffset": 0, - "shortName": "Time" - }, - { - "id": 218, - "valid": true, - "name": "Flanger_Stereo", - "displayName": "Stereo", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": -50, - "max": 50, - "bipolar": true, - "description": "Sets the ratio between the delay times of the left and of the right channel [the value shows the offset to ", - "category": "Local_Unmodulateables", - "pid": "Flanger_Stereo", - "scaleFactor": 50, - "scaleOffset": 0, - "shortName": "Stereo" - }, - { - "id": 219, - "valid": true, - "name": "Flanger_Feedback", - "displayName": "Feedback", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": true, - "description": "Amount of the internal feedback. At negative values, the feedback is inverted and will emphasize other ", - "category": "Local_Modulateables", - "pid": "Flanger_Feedback", - "scaleFactor": 0.5, - "scaleOffset": 0.5, - "shortName": "Feedback" - }, - { - "id": 221, - "valid": true, - "name": "Flanger_Cross_FB", - "displayName": "Cross FB", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Amount of the cross feedback between the left and the right channel, increasing the complexity of the ", - "category": "Local_Unmodulateables", - "pid": "Flanger_Cross_FB", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Cross FB" - }, - { - "id": 222, - "valid": true, - "name": "Flanger_Hi_Cut", - "displayName": "Hi Cut", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0.75, - "scalingType": "Expon_Lin_Pitch", - "min": 60, - "max": 140, - "bipolar": false, - "description": "Cutoff frequency [in semitones] of the filter that damps the higher frequencies of the delayed signal.", - "category": "Local_Unmodulateables", - "pid": "Flanger_Hi_Cut", - "scaleFactor": 80, - "scaleOffset": 60, - "shortName": "Hi Cut" - }, - { - "id": 223, - "valid": true, - "name": "Flanger_Mix", - "displayName": "Mix", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Crossfades between the dry signal and the delayed signal. At negative values, the delayed signal is ", - "category": "Local_Modulateables", - "pid": "Flanger_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 225, - "valid": true, - "name": "Echo_Time", - "displayName": "Time", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0.433, - "scalingType": "Parabolic", - "min": 0, - "max": 2, - "bipolar": false, - "description": "Mean delay time [in milliseconds]. (As there can be an offset between the left and right channel, this ", - "category": "Local_Modulateables", - "pid": "Echo_Time", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "Time" - }, - { - "id": 227, - "valid": true, - "name": "Echo_Stereo", - "displayName": "Stereo", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0, - "scalingType": "Linear", - "min": -33, - "max": 33, - "bipolar": true, - "description": "Sets the ratio between the delay times of the left and of the right channel [the value shows the offset to ", - "category": "Local_Modulateables", - "pid": "Echo_Stereo", - "scaleFactor": 33, - "scaleOffset": 0, - "shortName": "Stereo" - }, - { - "id": 229, - "valid": true, - "name": "Echo_Feedback", - "displayName": "Feedback", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of internal feedback.", - "category": "Local_Modulateables", - "pid": "Echo_Feedback", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Feedback" - }, - { - "id": 231, - "valid": true, - "name": "Echo_Cross_FB", - "displayName": "Cross FB", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of the cross feedback between the left and right channel.\\n0.0 %: no cross feedback\\n100.0 %: all ", - "category": "Local_Unmodulateables", - "pid": "Echo_Cross_FB", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Cross FB" - }, - { - "id": 232, - "valid": true, - "name": "Echo_Hi_Cut", - "displayName": "Hi Cut", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0.75, - "scalingType": "Expon_Lin_Pitch", - "min": 60, - "max": 140, - "bipolar": false, - "description": "Cutoff frequency [in semitones] of the filter that damps the higher frequencies of the delayed signal.", - "category": "Local_Unmodulateables", - "pid": "Echo_Hi_Cut", - "scaleFactor": 80, - "scaleOffset": 60, - "shortName": "Hi Cut" - }, - { - "id": 233, - "valid": true, - "name": "Echo_Mix", - "displayName": "Mix", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between the dry signal and the delayed signal.", - "category": "Local_Modulateables", - "pid": "Echo_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 235, - "valid": true, - "name": "Reverb_Size", - "displayName": "Size", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 0.33, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "The room size and reverb time are set here.", - "category": "Local_Modulateables", - "pid": "Reverb_Size", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Size" - }, - { - "id": 237, - "valid": true, - "name": "Reverb_Pre_Dly", - "displayName": "Pre Delay", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 0.33, - "scalingType": "Cubic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Pre delay time, shifting the late reflections. This has a profound effect on the perceived room size.", - "category": "Local_Unmodulateables", - "pid": "Reverb_Pre_Dly", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Pre Delay" - }, - { - "id": 238, - "valid": true, - "name": "Reverb_Color", - "displayName": "Color", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Cutoff of the filter that damps the lower frequencies of the reverberation signal.", - "category": "Local_Modulateables", - "pid": "Reverb_Color", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Color" - }, - { - "id": 240, - "valid": true, - "name": "Reverb_Chorus", - "displayName": "Chorus", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 0.25, - "scalingType": "Parabolic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This controls the internal modulation of the reverberation delays. At higher amounts, there is more movement ", - "category": "Local_Unmodulateables", - "pid": "Reverb_Chorus", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Chorus" - }, - { - "id": 241, - "valid": true, - "name": "Reverb_Mix", - "displayName": "Mix", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 0, - "scalingType": "Parabolic", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between the dry signal and the reverberation signal.", - "category": "Local_Modulateables", - "pid": "Reverb_Mix", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Mix" - }, - { - "id": 243, - "valid": true, - "name": "MC_A", - "displayName": "\ue000", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_A", - "shortName": "\ue000" - }, - { - "id": 244, - "valid": true, - "name": "MC_B", - "displayName": "\ue001", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_B", - "shortName": "\ue001" - }, - { - "id": 245, - "valid": true, - "name": "MC_C", - "displayName": "\ue002", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_C", - "shortName": "\ue002" - }, - { - "id": 246, - "valid": true, - "name": "MC_D", - "displayName": "\ue003", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_D", - "shortName": "\ue003" - }, - { - "id": 247, - "valid": true, - "name": "Master_Volume", - "displayName": "Master Volume", - "groupName": "Master", - "groupShortName": "Master", - "defaultValue": 0.5, - "scalingType": "Parabolic", - "min": 0, - "max": 4, - "bipolar": false, - "description": "Master volume [in dB], applied at the end of the effect chain, before the soft clipper.", - "category": "Global_Modulateables", - "pid": "Master_Volume", - "scaleFactor": 4, - "scaleOffset": 0, - "shortName": "Volume" - }, - { - "id": 248, - "valid": true, - "name": "Master_Tune", - "displayName": "Master Tune", - "groupName": "Master", - "groupShortName": "Master", - "defaultValue": 0, - "scalingType": "Linear", - "min": -48, - "max": 48, - "bipolar": true, - "description": "Global pitch transposition [in semitones].", - "category": "Global_Modulateables", - "pid": "Master_Tune", - "scaleFactor": 48, - "scaleOffset": 0, - "shortName": "Tune" - }, - { - "id": 249, - "valid": true, - "name": "Unison_Voices", - "displayName": "Voices", - "groupName": "Unison", - "groupShortName": "Unison", - "defaultValue": 0, - "scalingType": "Linear", - "min": 1, - "max": 24, - "bipolar": false, - "description": "Number of unison voices that will be assigned to a key. At 1, the unison effect is disabled.", - "category": "Local_Unmodulateables", - "pid": "Unison_Voices", - "scaleFactor": 23, - "scaleOffset": 1, - "shortName": "Voices" - }, - { - "id": 250, - "valid": true, - "name": "Unison_Detune", - "displayName": "Detune", - "groupName": "Unison", - "groupShortName": "Unison", - "defaultValue": 0.004166666666666667, - "scalingType": "Linear", - "min": 0, - "max": 12, - "bipolar": false, - "description": "Spreading of the pitches of the unison voices [in semitones]. The Fine mode allows for adjustments in steps ", - "category": "Local_Modulateables", - "pid": "Unison_Detune", - "scaleFactor": 12, - "scaleOffset": 0, - "shortName": "Detune" - }, - { - "id": 252, - "valid": true, - "name": "Unison_Phase", - "displayName": "Phase", - "groupName": "Unison", - "groupShortName": "Unison", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Spreading the (Oscillators') start phases of the unison voices to get different phase cancellations at the ", - "category": "Local_Unmodulateables", - "pid": "Unison_Phase", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Phase" - }, - { - "id": 253, - "valid": true, - "name": "Unison_Pan", - "displayName": "Pan", - "groupName": "Unison", - "groupShortName": "Unison", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Voice panning inside of a group of unison voices. At 100.0 %, the voices are spread over the full stereo ", - "category": "Local_Unmodulateables", - "pid": "Unison_Pan", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Pan" - }, - { - "id": 254, - "valid": true, - "name": "Pedal_1", - "displayName": "Pedal 1", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Pedal_1", - "shortName": "Pedal 1" - }, - { - "id": 255, - "valid": true, - "name": "Pedal_1_to_MC_A", - "displayName": "Pedal 1 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control A. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_A", - "shortName": "Ped 1 to \ue000" - }, - { - "id": 256, - "valid": true, - "name": "Pedal_1_to_MC_B", - "displayName": "Pedal 1 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control B. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_B", - "shortName": "Ped 1 to \ue001" - }, - { - "id": 257, - "valid": true, - "name": "Pedal_1_to_MC_C", - "displayName": "Pedal 1 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control C. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_C", - "shortName": "Ped 1 to \ue002" - }, - { - "id": 258, - "valid": true, - "name": "Pedal_1_to_MC_D", - "displayName": "Pedal 1 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control D. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_D", - "shortName": "Ped 1 to \ue003" - }, - { - "id": 259, - "valid": true, - "name": "Pedal_2", - "displayName": "Pedal 2", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Pedal_2", - "shortName": "Pedal 2" - }, - { - "id": 260, - "valid": true, - "name": "Pedal_2_to_MC_A", - "displayName": "Pedal 2 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control A. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_A", - "shortName": "Ped 2 to \ue000" - }, - { - "id": 261, - "valid": true, - "name": "Pedal_2_to_MC_B", - "displayName": "Pedal 2 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control B. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_B", - "shortName": "Ped 2 to \ue001" - }, - { - "id": 262, - "valid": true, - "name": "Pedal_2_to_MC_C", - "displayName": "Pedal 2 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control C. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_C", - "shortName": "Ped 2 to \ue002" - }, - { - "id": 263, - "valid": true, - "name": "Pedal_2_to_MC_D", - "displayName": "Pedal 2 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control D. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_D", - "shortName": "Ped 2 to \ue003" - }, - { - "id": 264, - "valid": true, - "name": "Pedal_3", - "displayName": "Pedal 3", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Pedal_3", - "shortName": "Pedal 3" - }, - { - "id": 265, - "valid": true, - "name": "Pedal_3_to_MC_A", - "displayName": "Pedal 3 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control A. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_A", - "shortName": "Ped 3 to \ue000" - }, - { - "id": 266, - "valid": true, - "name": "Pedal_3_to_MC_B", - "displayName": "Pedal 3 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control B. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_B", - "shortName": "Ped 3 to \ue001" - }, - { - "id": 267, - "valid": true, - "name": "Pedal_3_to_MC_C", - "displayName": "Pedal 3 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control C. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_C", - "shortName": "Ped 3 to \ue002" - }, - { - "id": 268, - "valid": true, - "name": "Pedal_3_to_MC_D", - "displayName": "Pedal 3 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control D. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_D", - "shortName": "Ped 3 to \ue003" - }, - { - "id": 269, - "valid": true, - "name": "Pedal_4", - "displayName": "Pedal 4", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Pedal_4", - "shortName": "Pedal 4" - }, - { - "id": 270, - "valid": true, - "name": "Pedal_4_to_MC_A", - "displayName": "Pedal 4 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control A. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_A", - "shortName": "Ped 4 to \ue000" - }, - { - "id": 271, - "valid": true, - "name": "Pedal_4_to_MC_B", - "displayName": "Pedal 4 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control B. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_B", - "shortName": "Ped 4 to \ue001" - }, - { - "id": 272, - "valid": true, - "name": "Pedal_4_to_MC_C", - "displayName": "Pedal 4 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control C. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_C", - "shortName": "Ped 4 to \ue002" - }, - { - "id": 273, - "valid": true, - "name": "Pedal_4_to_MC_D", - "displayName": "Pedal 4 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control D. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_D", - "shortName": "Ped 4 to \ue003" - }, - { - "id": 274, - "valid": true, - "name": "Bender", - "displayName": "Bender", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Bender", - "shortName": "Bender" - }, - { - "id": 275, - "valid": true, - "name": "Bender_to_MC_A", - "displayName": "Bender to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control A. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_A", - "shortName": "Bend to \ue000" - }, - { - "id": 276, - "valid": true, - "name": "Bender_to_MC_B", - "displayName": "Bender to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control B. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_B", - "shortName": "Bend to \ue001" - }, - { - "id": 277, - "valid": true, - "name": "Bender_to_MC_C", - "displayName": "Bender to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control C. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_C", - "shortName": "Bend to \ue002" - }, - { - "id": 278, - "valid": true, - "name": "Bender_to_MC_D", - "displayName": "Bender to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control D. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_D", - "shortName": "Bend to \ue003" - }, - { - "id": 279, - "valid": true, - "name": "Aftertouch", - "displayName": "Aftertouch", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Aftertouch", - "shortName": "Aftertouch" - }, - { - "id": 280, - "valid": true, - "name": "Aftertouch_to_MC_A", - "displayName": "Aftertouch to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control A. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_A", - "shortName": "AT to \ue000" - }, - { - "id": 281, - "valid": true, - "name": "Aftertouch_to_MC_B", - "displayName": "Aftertouch to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control B. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_B", - "shortName": "AT to \ue001" - }, - { - "id": 282, - "valid": true, - "name": "Aftertouch_to_MC_C", - "displayName": "Aftertouch to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control C. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_C", - "shortName": "AT to \ue002" - }, - { - "id": 283, - "valid": true, - "name": "Aftertouch_to_MC_D", - "displayName": "Aftertouch to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control D. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_D", - "shortName": "AT to \ue003" - }, - { - "id": 284, - "valid": true, - "name": "Ribbon_1", - "displayName": "Ribbon 1", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Ribbon_1", - "shortName": "Ribbon 1" - }, - { - "id": 285, - "valid": true, - "name": "Ribbon_1_to_MC_A", - "displayName": "Ribbon 1 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control A. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_A", - "shortName": "Rib 1 to \ue000" - }, - { - "id": 286, - "valid": true, - "name": "Ribbon_1_to_MC_B", - "displayName": "Ribbon 1 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control B. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_B", - "shortName": "Rib 1 to \ue001" - }, - { - "id": 287, - "valid": true, - "name": "Ribbon_1_to_MC_C", - "displayName": "Ribbon 1 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control C. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_C", - "shortName": "Rib 1 to \ue002" - }, - { - "id": 288, - "valid": true, - "name": "Ribbon_1_to_MC_D", - "displayName": "Ribbon 1 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control D. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_D", - "shortName": "Rib 1 to \ue003" - }, - { - "id": 289, - "valid": true, - "name": "Ribbon_2", - "displayName": "Ribbon 2", - "groupName": "Hardware Source", - "groupShortName": "HW Source", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "This control is the parameter representation of the \\", - "category": "Hardware_Sources", - "pid": "Ribbon_2", - "shortName": "Ribbon 2" - }, - { - "id": 290, - "valid": true, - "name": "Ribbon_2_to_MC_A", - "displayName": "Ribbon 2 to \ue000", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control A. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_A", - "shortName": "Rib 2 to \ue000" - }, - { - "id": 291, - "valid": true, - "name": "Ribbon_2_to_MC_B", - "displayName": "Ribbon 2 to \ue001", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control B. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_B", - "shortName": "Rib 2 to \ue001" - }, - { - "id": 292, - "valid": true, - "name": "Ribbon_2_to_MC_C", - "displayName": "Ribbon 2 to \ue002", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control C. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_C", - "shortName": "Rib 2 to \ue002" - }, - { - "id": 293, - "valid": true, - "name": "Ribbon_2_to_MC_D", - "displayName": "Ribbon 2 to \ue003", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control D. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_D", - "shortName": "Rib 2 to \ue003" - }, - { - "id": 294, - "valid": true, - "name": "Env_A_Att_Curve", - "displayName": "Attack Curve", - "groupName": "Envelope A", - "groupShortName": "Envelope A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Curvature of the (polynomial) Attack segment. (negative: decelerating slope, zero: linear, positive: ", - "category": "Local_Unmodulateables", - "pid": "Env_A_Att_Curve", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Curve" - }, - { - "id": 295, - "valid": true, - "name": "Env_B_Att_Curve", - "displayName": "Attack Curve", - "groupName": "Envelope B", - "groupShortName": "Envelope B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Curvature of the (polynomial) Attack segment. (negative: decelerating slope, zero: linear, positive: ", - "category": "Local_Unmodulateables", - "pid": "Env_B_Att_Curve", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Curve" - }, - { - "id": 296, - "valid": true, - "name": "Env_C_Att_Curve", - "displayName": "Attack Curve", - "groupName": "Envelope C", - "groupShortName": "Envelope C", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Curvature of the (polynomial) Attack segment. (negative: decelerating slope, zero: linear, positive: ", - "category": "Local_Unmodulateables", - "pid": "Env_C_Att_Curve", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Curve" - }, - { - "id": 297, - "valid": true, - "name": "Env_C_Sus", - "displayName": "Sustain Level", - "groupName": "Envelope C", - "groupShortName": "Envelope C", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Sustain level (bipolar, target of the second Decay segment).", - "category": "Local_Modulateables", - "pid": "Env_C_Sus", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Sustain" - }, - { - "id": 299, - "valid": true, - "name": "FB_Mix_Lvl", - "displayName": "Level", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0.38, - "scalingType": "Parabolic", - "min": 0, - "max": 4, - "bipolar": false, - "description": "Level of the (global) Feedback mix [in dB].", - "category": "Local_Modulateables", - "pid": "FB_Mix_Lvl", - "scaleFactor": 4, - "scaleOffset": 0, - "shortName": "Level" - }, - { - "id": 301, - "valid": true, - "name": "Osc_A_Phase", - "displayName": "Phase", - "groupName": "Oscillator A", - "groupShortName": "Oscillator A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -0.5, - "max": 0.5, - "bipolar": true, - "description": "Start phase [in degrees]. The Oscillator will be set to this phase position with each Note-On.", - "category": "Local_Modulateables", - "pid": "Osc_A_Phase", - "scaleFactor": 0.5, - "scaleOffset": 0, - "shortName": "Phase" - }, - { - "id": 302, - "valid": true, - "name": "Osc_B_Phase", - "displayName": "Phase", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -0.5, - "max": 0.5, - "bipolar": true, - "description": "Start phase [in degrees]. The Oscillator will be set to this phase position with each Note-On.", - "category": "Local_Modulateables", - "pid": "Osc_B_Phase", - "scaleFactor": 0.5, - "scaleOffset": 0, - "shortName": "Phase" - }, - { - "id": 303, - "valid": true, - "name": "Osc_A_Chirp", - "displayName": "Chirp", - "groupName": "Oscillator A", - "groupShortName": "Oscillator A", - "defaultValue": 0.6666666666666666, - "scalingType": "Expon_Lin_Pitch", - "min": 80, - "max": 140, - "bipolar": false, - "description": "Cutoff [in semitones] of the lowpass in the phase modulation signal path that can be applied to reduce the ", - "category": "Local_Unmodulateables", - "pid": "Osc_A_Chirp", - "scaleFactor": 60, - "scaleOffset": 80, - "shortName": "Chirp" - }, - { - "id": 304, - "valid": true, - "name": "Osc_B_Chirp", - "displayName": "Chirp", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 0.6666666666666666, - "scalingType": "Expon_Lin_Pitch", - "min": 80, - "max": 140, - "bipolar": false, - "description": "Cutoff [in semitones] of the lowpass in the phase modulation signal path that can be applied to reduce the ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_Chirp", - "scaleFactor": 60, - "scaleOffset": 80, - "shortName": "Chirp" - }, - { - "id": 305, - "valid": true, - "name": "Comb_Flt_Decay_Gate", - "displayName": "Decay - Gate", - "groupName": "Comb Filter", - "groupShortName": "Comb Filter", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Amount of gating applied to the decay time. It reduces the decay time when the key is released ", - "category": "Local_Modulateables", - "pid": "Comb_Flt_Decay_Gate", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Gate" - }, - { - "id": 307, - "valid": true, - "name": "Flanger_Envelope", - "displayName": "Envelope", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between the signals of the LFO and the envelope as modulation sources for the delay times and the ", - "category": "Local_Unmodulateables", - "pid": "Flanger_Envelope", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Envelope" - }, - { - "id": 308, - "valid": true, - "name": "Flanger_AP_Mod", - "displayName": "Allpass Mod", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": -70, - "max": 70, - "bipolar": true, - "description": "Relative amount of the modulation of the allpass center frequencies by the LFO and/or the envelope. The ", - "category": "Local_Modulateables", - "pid": "Flanger_AP_Mod", - "scaleFactor": 70, - "scaleOffset": 0, - "shortName": "AP Mod" - }, - { - "id": 310, - "valid": true, - "name": "Flanger_AP_Tune", - "displayName": "Allpass Tune", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 140, - "bipolar": false, - "description": "Mean center frequency of the 4-pole allpass filters which are in series with the delays. Their ", - "category": "Local_Modulateables", - "pid": "Flanger_AP_Tune", - "scaleFactor": 140, - "scaleOffset": 0, - "shortName": "AP Tune" - }, - { - "id": 312, - "valid": true, - "name": "Scale_Base_Key", - "displayName": "Base Key", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 11, - "bipolar": false, - "description": "Sets the base key for the custom scale. The scale is defined for the eleven keys above the base key and will ", - "category": "Global_Unmodulateables", - "pid": "Scale_Base_Key", - "scaleFactor": 11, - "scaleOffset": 0, - "shortName": "Base Key" - }, - { - "id": 313, - "valid": true, - "name": "Scale_Offset_1", - "displayName": "Offset +1", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the first key following the base key [in cents]. At zero, the interval to the base key would be the ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_1", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +1" - }, - { - "id": 314, - "valid": true, - "name": "Scale_Offset_2", - "displayName": "Offset +2", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the second key following the base key [in cents]. At zero, the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_2", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +2" - }, - { - "id": 315, - "valid": true, - "name": "Scale_Offset_3", - "displayName": "Offset +3", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the third key following the base key [in cents]. At zero, the interval to the base key would be the ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_3", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +3" - }, - { - "id": 316, - "valid": true, - "name": "Scale_Offset_4", - "displayName": "Offset +4", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the fourth key following the base key [in cents]. At zero, the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_4", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +4" - }, - { - "id": 317, - "valid": true, - "name": "Scale_Offset_5", - "displayName": "Offset +5", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the fifth key following the base key [in cents]. At zero, the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_5", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +5" - }, - { - "id": 318, - "valid": true, - "name": "Scale_Offset_6", - "displayName": "Offset +6", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the sixth key following the base key [in cents]. At zero, the interval to the base key would be the ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_6", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +6" - }, - { - "id": 319, - "valid": true, - "name": "Scale_Offset_7", - "displayName": "Offset +7", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the seventh key following the base key [in cents]. At zero,the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_7", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +7" - }, - { - "id": 320, - "valid": true, - "name": "Scale_Offset_8", - "displayName": "Offset +8", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the eighth key following the base key [in cents]. At zero, the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_8", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +8" - }, - { - "id": 321, - "valid": true, - "name": "Scale_Offset_9", - "displayName": "Offset +9", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the ninth key following the base key [in cents]. At zero, the interval to the base key would be the ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_9", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +9" - }, - { - "id": 322, - "valid": true, - "name": "Scale_Offset_10", - "displayName": "Offset +10", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the tenth key following the base key [in cents]. At zero, the interval to the base key would be the ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_10", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +10" - }, - { - "id": 323, - "valid": true, - "name": "Scale_Offset_11", - "displayName": "Offset +11", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the eleventh key following the base key [in cents]. At zero, the interval to the base key would be ", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_11", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +11" - }, - { - "id": 324, - "valid": true, - "name": "MC_Time_A", - "displayName": "\ue000 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control A. ", - "category": "Macro_Times", - "pid": "MC_Time_A", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 325, - "valid": true, - "name": "MC_Time_B", - "displayName": "\ue001 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control B. ", - "category": "Macro_Times", - "pid": "MC_Time_B", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 326, - "valid": true, - "name": "MC_Time_C", - "displayName": "\ue002 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control C. ", - "category": "Macro_Times", - "pid": "MC_Time_C", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 327, - "valid": true, - "name": "MC_Time_D", - "displayName": "\ue003 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control D. ", - "category": "Macro_Times", - "pid": "MC_Time_D", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 328, - "valid": true, - "name": "Env_A_Dec_1_Vel", - "displayName": "Decay 1 Velocity", - "groupName": "Envelope A", - "groupShortName": "Envelope A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -60, - "max": 60, - "bipolar": true, - "description": "Velocity influence on the Decay 1 time. The value represents the logarithmic amount of the reduction ", - "category": "Local_Unmodulateables", - "pid": "Env_A_Dec_1_Vel", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Velocity" - }, - { - "id": 330, - "valid": true, - "name": "Env_A_Dec_2_Vel", - "displayName": "Decay 2 Velocity", - "groupName": "Envelope A", - "groupShortName": "Envelope A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -60, - "max": 60, - "bipolar": true, - "description": "Velocity influence on the Decay 2 time. The value represents the logarithmic amount of the reduction ", - "category": "Local_Unmodulateables", - "pid": "Env_A_Dec_2_Vel", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Velocity" - }, - { - "id": 332, - "valid": true, - "name": "Env_A_Elevate", - "displayName": "Elevate T - A", - "groupName": "Envelope A", - "groupShortName": "Envelope A", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Transforms the breakpoints of the Envelope signal, becoming more and more a flat organ-like curve towards the ", - "category": "Local_Unmodulateables", - "pid": "Env_A_Elevate", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Elevate" - }, - { - "id": 334, - "valid": true, - "name": "Env_B_Dec_1_Vel", - "displayName": "Decay 1 Velocity", - "groupName": "Envelope B", - "groupShortName": "Envelope B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -60, - "max": 60, - "bipolar": true, - "description": "Velocity influence on the Decay 1 time. The value represents the logarithmic amount of the reduction ", - "category": "Local_Unmodulateables", - "pid": "Env_B_Dec_1_Vel", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Velocity" - }, - { - "id": 336, - "valid": true, - "name": "Env_B_Dec_2_Vel", - "displayName": "Decay 2 Velocity", - "groupName": "Envelope B", - "groupShortName": "Envelope B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -60, - "max": 60, - "bipolar": true, - "description": "Velocity influence on the Decay 2 time. The value represents the logarithmic amount of the reduction ", - "category": "Local_Unmodulateables", - "pid": "Env_B_Dec_2_Vel", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Velocity" - }, - { - "id": 338, - "valid": true, - "name": "Env_B_Elevate", - "displayName": "Elevate T - A", - "groupName": "Envelope B", - "groupShortName": "Envelope B", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Transforms the breakpoints of the Envelope signal, becoming more and more a flat organ-like curve towards the ", - "category": "Local_Unmodulateables", - "pid": "Env_B_Elevate", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Elevate" - }, - { - "id": 340, - "valid": true, - "name": "Env_C_Retr_H", - "displayName": "Retrig Hardness", - "groupName": "Envelope C", - "groupShortName": "Envelope C", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines how much the Envelope signal is forced to start at zero instead of the current level (usually from ", - "category": "Local_Unmodulateables", - "pid": "Env_C_Retr_H", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Retrig" - }, - { - "id": 342, - "valid": true, - "name": "Echo_Send", - "displayName": "Send", - "groupName": "Echo", - "groupShortName": "Echo", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Level control at the input of the effect signal path. In contrast to the Echo Mix parameter, the Delay ", - "category": "Local_Modulateables", - "pid": "Echo_Send", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Send" - }, - { - "id": 344, - "valid": true, - "name": "Reverb_Send", - "displayName": "Send", - "groupName": "Reverb", - "groupShortName": "Reverb", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Level control at the input of the effect signal path. In contrast to the Reverb Mix parameter, the Reverb ", - "category": "Local_Modulateables", - "pid": "Reverb_Send", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Send" - }, - { - "id": 346, - "valid": true, - "name": "FB_Mix_Osc", - "displayName": "A/B @VG", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": -1, - "max": 1, - "bipolar": true, - "description": "Level of the signal from branch A or B of the other Part. (Only available when using Layer mode.)", - "category": "Local_Modulateables", - "pid": "FB_Mix_Osc", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "A/B @VG" - }, - { - "id": 348, - "valid": true, - "name": "FB_Mix_Osc_Src", - "displayName": "A - B", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades between branch A and B of the other Part. (Only available when using Layer mode.)", - "category": "Local_Modulateables", - "pid": "FB_Mix_Osc_Src", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "A - B" - }, - { - "id": 350, - "valid": true, - "name": "FB_Mix_Comb_Src", - "displayName": "Comb - From @VG", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades to the Comb Filter signal of the other Part. (Only available when using Layer mode.)", - "category": "Local_Modulateables", - "pid": "FB_Mix_Comb_Src", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "From @VG" - }, - { - "id": 352, - "valid": true, - "name": "FB_Mix_SVF_Src", - "displayName": "SV Filter - From @VG", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades to the State Variable Filter signal of the other Part. (Only available when using Layer mode.)", - "category": "Local_Modulateables", - "pid": "FB_Mix_SVF_Src", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "From @VG" - }, - { - "id": 354, - "valid": true, - "name": "FB_Mix_FX_Src", - "displayName": "Effects - From @VG", - "groupName": "Feedback Mixer", - "groupShortName": "FB Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Crossfades to the Effects signal of the other Part. (Only available when using Layer mode.)", - "category": "Local_Modulateables", - "pid": "FB_Mix_FX_Src", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "From @VG" - }, - { - "id": 356, - "valid": true, - "name": "Split_Split_Point", - "displayName": "Split Point", - "groupName": "Split", - "groupShortName": "Split", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0.05, - "max": 60.05, - "bipolar": false, - "description": "Determines the split position of a sound as a key number. Keys from the lower end up to the Split Point will ", - "category": "Local_Modulateables", - "pid": "Split_Split_Point", - "scaleFactor": 60, - "scaleOffset": 0.05, - "shortName": "Split Pnt" - }, - { - "id": 358, - "valid": true, - "name": "Voice_Grp_Volume", - "displayName": "Part Volume", - "groupName": "Voices", - "groupShortName": "Voices", - "defaultValue": 0.5, - "scalingType": "Parabolic", - "min": 0, - "max": 4, - "bipolar": false, - "description": "Part volume [in dB], available when using Split or Layer mode. Applied at the end of the corresponding effect ", - "category": "Local_Modulateables", - "pid": "Voice_Grp_Volume", - "scaleFactor": 4, - "scaleOffset": 0, - "shortName": "Volume" - }, - { - "id": 360, - "valid": true, - "name": "Voice_Grp_Tune", - "displayName": "Part Tune", - "groupName": "Voices", - "groupShortName": "Voices", - "defaultValue": 0, - "scalingType": "Linear", - "min": -48, - "max": 48, - "bipolar": true, - "description": "Part pitch transposition [in semitones], available when using Split or Layer mode. Applied for the ", - "category": "Local_Modulateables", - "pid": "Voice_Grp_Tune", - "scaleFactor": 48, - "scaleOffset": 0, - "shortName": "Tune" - }, - { - "id": 362, - "valid": true, - "name": "Out_Mix_To_FX", - "displayName": "To FX @VG", - "groupName": "Output Mixer", - "groupShortName": "Output Mixer", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines how much of the Output Mixer signal is sent to the Effects section of the other Part. At 100.0 % ", - "category": "Local_Modulateables", - "pid": "Out_Mix_To_FX", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "To FX @VG" - }, - { - "id": 364, - "valid": true, - "name": "Mono_Grp_Enable", - "displayName": "Enable", - "groupName": "Mono", - "groupShortName": "Mono", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "When enabled, only one voice will be assigned, even when multiple keys are pressed.", - "category": "Local_Unmodulateables", - "pid": "Mono_Grp_Enable", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Enable" - }, - { - "id": 365, - "valid": true, - "name": "Mono_Grp_Prio", - "displayName": "Key Priority", - "groupName": "Mono", - "groupShortName": "Mono", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 2, - "bipolar": false, - "description": "When pressing multiple keys in Mono mode, they are sorted according to the priority set by this parameter. ", - "category": "Local_Unmodulateables", - "pid": "Mono_Grp_Prio", - "scaleFactor": 2, - "scaleOffset": 0, - "shortName": "Priority" - }, - { - "id": 366, - "valid": true, - "name": "Mono_Grp_Legato", - "displayName": "Legato", - "groupName": "Mono", - "groupShortName": "Mono", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 3, - "bipolar": false, - "description": "When a new key matches the Priority, it will take over. When the priorized key is released the second ", - "category": "Local_Unmodulateables", - "pid": "Mono_Grp_Legato", - "scaleFactor": 3, - "scaleOffset": 0, - "shortName": "Legato" - }, - { - "id": 367, - "valid": true, - "name": "Mono_Grp_Glide", - "displayName": "Glide Time", - "groupName": "Mono", - "groupShortName": "Mono", - "defaultValue": 0, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "In Legato mode this time can be used to smooth the transitions of the pitches.", - "category": "Local_Modulateables", - "pid": "Mono_Grp_Glide", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Glide" - }, - { - "id": 369, - "valid": true, - "name": "MC_E", - "displayName": "\ue200", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_E", - "shortName": "\ue200" - }, - { - "id": 370, - "valid": true, - "name": "MC_Time_E", - "displayName": "\ue200 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control E. ", - "category": "Macro_Times", - "pid": "MC_Time_E", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 371, - "valid": true, - "name": "MC_F", - "displayName": "\ue201", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.5, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "", - "category": "Macro_Controls", - "pid": "MC_F", - "shortName": "\ue201" - }, - { - "id": 372, - "valid": true, - "name": "MC_Time_F", - "displayName": "\ue201 Smoothing", - "groupName": "Macro Control", - "groupShortName": "Macro Control", - "defaultValue": 0.4, - "scalingType": "Expon_Env_Time", - "min": -20, - "max": -19, - "bipolar": false, - "description": "Sets the smoothing time [in milliseconds] for modulations on target parameters assigned to Macro Control F. ", - "category": "Macro_Times", - "pid": "MC_Time_F", - "scaleFactor": 1, - "scaleOffset": -20, - "shortName": "Smoothing" - }, - { - "id": 373, - "valid": true, - "name": "Pedal_1_to_MC_E", - "displayName": "Pedal 1 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control E. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_E", - "shortName": "Ped 1 to \ue200" - }, - { - "id": 374, - "valid": true, - "name": "Pedal_1_to_MC_F", - "displayName": "Pedal 1 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 1 on Macro Control F. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_1_to_MC_F", - "shortName": "Ped 1 to \ue201" - }, - { - "id": 375, - "valid": true, - "name": "Pedal_2_to_MC_E", - "displayName": "Pedal 2 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control E. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_E", - "shortName": "Ped 2 to \ue200" - }, - { - "id": 376, - "valid": true, - "name": "Pedal_2_to_MC_F", - "displayName": "Pedal 2 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 2 on Macro Control F. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_2_to_MC_F", - "shortName": "Ped 2 to \ue201" - }, - { - "id": 377, - "valid": true, - "name": "Pedal_3_to_MC_E", - "displayName": "Pedal 3 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control E. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_E", - "shortName": "Ped 3 to \ue200" - }, - { - "id": 378, - "valid": true, - "name": "Pedal_3_to_MC_F", - "displayName": "Pedal 3 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 3 on Macro Control F. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_3_to_MC_F", - "shortName": "Ped 3 to \ue201" - }, - { - "id": 379, - "valid": true, - "name": "Pedal_4_to_MC_E", - "displayName": "Pedal 4 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control E. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_E", - "shortName": "Ped 4 to \ue200" - }, - { - "id": 380, - "valid": true, - "name": "Pedal_4_to_MC_F", - "displayName": "Pedal 4 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Pedal 4 on Macro Control F. If the pedal is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Pedal_4_to_MC_F", - "shortName": "Ped 4 to \ue201" - }, - { - "id": 381, - "valid": true, - "name": "Bender_to_MC_E", - "displayName": "Bender to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control E. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_E", - "shortName": "Bend to \ue200" - }, - { - "id": 382, - "valid": true, - "name": "Bender_to_MC_F", - "displayName": "Bender to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Bender on Macro Control F. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Bender_to_MC_F", - "shortName": "Bend to \ue201" - }, - { - "id": 383, - "valid": true, - "name": "Aftertouch_to_MC_E", - "displayName": "Aftertouch to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control E. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_E", - "shortName": "AT to \ue200" - }, - { - "id": 384, - "valid": true, - "name": "Aftertouch_to_MC_F", - "displayName": "Aftertouch to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of the Aftertouch on Macro Control F. The amount can be adjusted continuously [in ", - "category": "Hardware_Amounts", - "pid": "Aftertouch_to_MC_F", - "shortName": "AT to \ue201" - }, - { - "id": 385, - "valid": true, - "name": "Ribbon_1_to_MC_E", - "displayName": "Ribbon 1 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control E. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_E", - "shortName": "Rib 1 to \ue200" - }, - { - "id": 386, - "valid": true, - "name": "Ribbon_1_to_MC_F", - "displayName": "Ribbon 1 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 1 on Macro Control F. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_1_to_MC_F", - "shortName": "Rib 1 to \ue201" - }, - { - "id": 387, - "valid": true, - "name": "Ribbon_2_to_MC_E", - "displayName": "Ribbon 2 to \ue200", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control E. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_E", - "shortName": "Rib 2 to \ue200" - }, - { - "id": 388, - "valid": true, - "name": "Ribbon_2_to_MC_F", - "displayName": "Ribbon 2 to \ue201", - "groupName": "Hardware Amount", - "groupShortName": "HW Amount", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the influence of Ribbon 2 on Macro Control F. If the ribbon is set to a returning mode, the amount ", - "category": "Hardware_Amounts", - "pid": "Ribbon_2_to_MC_F", - "shortName": "Rib 2 to \ue201" - }, - { - "id": 389, - "valid": true, - "name": "Flanger_Tremolo", - "displayName": "Tremolo", - "groupName": "Flanger", - "groupShortName": "Flanger", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Determines the amount of amplitude modulation by the Flanger's stereo LFO. It is applied after the Mix stage ", - "category": "Local_Modulateables", - "pid": "Flanger_Tremolo", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Tremolo" - }, - { - "id": 391, - "valid": true, - "name": "Scale_Offset_0", - "displayName": "Offset +0", - "groupName": "Scale", - "groupShortName": "Scale", - "defaultValue": 0, - "scalingType": "Linear", - "min": -8, - "max": 8, - "bipolar": true, - "description": "Offset of the base key [in cents]. At zero, the interval to the base key would be perfect identity.", - "category": "Global_Unmodulateables", - "pid": "Scale_Offset_0", - "scaleFactor": 8, - "scaleOffset": 0, - "shortName": "Offset +0" - }, - { - "id": 393, - "valid": true, - "name": "Osc_A_Reset", - "displayName": "Reset", - "groupName": "Oscillator A", - "groupShortName": "Oscillator A", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "If enabled, the Oscillator will reset its phase everytime the envelopes start (in the corresponding voice). ", - "category": "Local_Unmodulateables", - "pid": "Osc_A_Reset", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Reset" - }, - { - "id": 394, - "valid": true, - "name": "Osc_B_Reset", - "displayName": "Reset", - "groupName": "Oscillator B", - "groupShortName": "Oscillator B", - "defaultValue": 1, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "If enabled, every pressed key will cause the Oscillator to reset its phase (for the corresponding voice). If ", - "category": "Local_Unmodulateables", - "pid": "Osc_B_Reset", - "scaleFactor": 1, - "scaleOffset": 0, - "shortName": "Reset" - }, - { - "id": 395, - "valid": true, - "name": "Voice_Grp_Mute", - "displayName": "Part Mute", - "groupName": "Voices", - "groupShortName": "Voices", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 1, - "bipolar": false, - "description": "Part mute, available when usig Split or Layer mode. When enabled, the corresponding Part will be muted.", - "category": "Local_Unmodulateables", - "pid": "Voice_Grp_Mute", - "shortName": "Part Mute" - }, - { - "id": 396, - "valid": true, - "name": "Voice_Grp_Fade_From", - "displayName": "Fade From", - "groupName": "Voices", - "groupShortName": "Voices", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 60, - "bipolar": false, - "description": "Available when using Layer mode. For Part I the full level range spans from the lowest key (C1) to the key ", - "category": "Local_Unmodulateables", - "pid": "Voice_Grp_Fade_From", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Fade From" - }, - { - "id": 397, - "valid": true, - "name": "Voice_Grp_Fade_Range", - "displayName": "Fade Range", - "groupName": "Voices", - "groupShortName": "Voices", - "defaultValue": 0, - "scalingType": "Linear", - "min": 0, - "max": 60, - "bipolar": false, - "description": "Available when using Layer mode. This parameter adjusts the width of a key range [in semitones] where the ", - "category": "Local_Unmodulateables", - "pid": "Voice_Grp_Fade_Range", - "scaleFactor": 60, - "scaleOffset": 0, - "shortName": "Fade Range" - } - ], - "groups": [ - { - "name": "Cabinet", - "parameterCount": 8 - }, - { - "name": "Comb Filter", - "parameterCount": 16 - }, - { - "name": "Echo", - "parameterCount": 7 - }, - { - "name": "Envelope A", - "parameterCount": 16 - }, - { - "name": "Envelope B", - "parameterCount": 16 - }, - { - "name": "Envelope C", - "parameterCount": 13 - }, - { - "name": "Feedback Mixer", - "parameterCount": 14 - }, - { - "name": "Flanger", - "parameterCount": 13 - }, - { - "name": "Gap Filter", - "parameterCount": 6 - }, - { - "name": "Hardware Amount", - "parameterCount": 48 - }, - { - "name": "Hardware Source", - "parameterCount": 8 - }, - { - "name": "Macro Control", - "parameterCount": 12 - }, - { - "name": "Master", - "parameterCount": 2 - }, - { - "name": "Mono", - "parameterCount": 4 - }, - { - "name": "Oscillator A", - "parameterCount": 16 - }, - { - "name": "Oscillator B", - "parameterCount": 16 - }, - { - "name": "Output Mixer", - "parameterCount": 14 - }, - { - "name": "Reverb", - "parameterCount": 6 - }, - { - "name": "Scale", - "parameterCount": 13 - }, - { - "name": "Shaper A", - "parameterCount": 8 - }, - { - "name": "Shaper B", - "parameterCount": 8 - }, - { - "name": "Split", - "parameterCount": 1 - }, - { - "name": "State Variable Filter", - "parameterCount": 13 - }, - { - "name": "Unison", - "parameterCount": 4 - }, - { - "name": "Voices", - "parameterCount": 5 - } - ] -} \ No newline at end of file diff --git a/playground/c15/worklet-processor.js b/playground/c15/worklet-processor.js deleted file mode 100644 index 640f43b..0000000 --- a/playground/c15/worklet-processor.js +++ /dev/null @@ -1,592 +0,0 @@ -/** - * C15 Audio Engine - AudioWorklet Processor - * - * This processor loads the C15 WASM module and calls render() each audio frame. - * It runs in AudioWorkletGlobalScope and connects to the Web Audio graph with - * 0 inputs and 2 outputs (stereo). - * - * WASM API: - * - engineInit(sampleRate, polyphony) -> int - * - render(numFrames) -> float* (interleaved stereo) - * - noteOn(note, velocity) - * - noteOff(note, velocity) - * - setParameter(paramId, value) - * - reset() - * - * Ring Buffer Protocol (SharedArrayBuffer): - * - Lock-free SPSC ring buffer for main thread -> worklet communication - * - Message types: 0=parameter, 1=noteOn, 2=noteOff - * - Message format: [type, id/note, value/velocity, reserved] - * - * @file worklet-processor.js - */ - -// WASM module state (shared across all processor instances) -let wasmInstance = null; -let wasmMemory = null; -let wasmReady = false; - -// Ring buffer state -let ringBufferReader = null; - -// Message type constants (must match ring-buffer.js) -const MessageType = { - PARAMETER: 0, - NOTE_ON: 1, - NOTE_OFF: 2 -}; - -/** - * Ring buffer layout constants - */ -const HEADER_SIZE = 3; -const MESSAGE_SIZE = 4; -const RING_CAPACITY = 512; - -/** - * RingBufferReader - Reads messages from SharedArrayBuffer ring buffer - * - * This is the consumer side of the SPSC ring buffer, designed for use - * in the AudioWorklet's process() callback. - * - * @class RingBufferReader - */ -class RingBufferReader { - constructor(sharedBuffer) { - this._buffer = new Float32Array(sharedBuffer); - this._capacity = RING_CAPACITY; - this._messageSize = MESSAGE_SIZE; - this._headerSize = HEADER_SIZE; - } - - /** - * Get current write index (main thread updates this) - * @private - */ - _getWriteIndex() { - return Atomics.load(new Int32Array(this._buffer.buffer), 0); - } - - /** - * Get current read index - * @private - */ - _getReadIndex() { - return Atomics.load(new Int32Array(this._buffer.buffer), 1); - } - - /** - * Advance read index with atomic store - * @private - */ - _advanceReadIndex(currentIdx) { - const nextIdx = (currentIdx + 1) % this._capacity; - Atomics.store(new Int32Array(this._buffer.buffer), 1, nextIdx); - return nextIdx; - } - - /** - * Read and process messages with callbacks - * - * This is the preferred method for use in the AudioWorklet process() call. - * It avoids creating arrays and directly calls the appropriate callback. - * - * @param {Object} callbacks - Callback handlers - * @param {Function} callbacks.onParameter - Called for parameter updates (paramId, value) - * @param {Function} callbacks.onNoteOn - Called for note on events (note, velocity) - * @param {Function} callbacks.onNoteOff - Called for note off events (note, velocity) - * @param {number} maxMessages - Maximum messages to process per call - * @returns {number} Number of messages processed - */ - processMessages(callbacks, maxMessages = 32) { - const writeIdx = this._getWriteIndex(); - let readIdx = this._getReadIndex(); - - let count = 0; - - while (readIdx !== writeIdx && count < maxMessages) { - // Read message from buffer - const msgOffset = this._headerSize + (readIdx * this._messageSize); - - const type = this._buffer[msgOffset + 0]; - const id = this._buffer[msgOffset + 1]; - const value = this._buffer[msgOffset + 2]; - - // Dispatch to callback based on type - switch (type) { - case MessageType.PARAMETER: - if (callbacks.onParameter) { - callbacks.onParameter(id, value); - } - break; - - case MessageType.NOTE_ON: - if (callbacks.onNoteOn) { - callbacks.onNoteOn(id, value); - } - break; - - case MessageType.NOTE_OFF: - if (callbacks.onNoteOff) { - callbacks.onNoteOff(id, value); - } - break; - - default: - console.warn('[RingBufferReader] Unknown message type:', type); - } - - // Advance read index - readIdx = this._advanceReadIndex(readIdx); - count++; - } - - return count; - } - - /** - * Get number of available messages (for debugging/monitoring) - */ - getAvailableCount() { - const writeIdx = this._getWriteIndex(); - const readIdx = this._getReadIndex(); - - if (writeIdx >= readIdx) { - return writeIdx - readIdx; - } else { - return this._capacity - readIdx + writeIdx; - } - } -} - -/** - * Initialize WASM module from compiled WebAssembly.Module and memory - * This is called when the main thread sends the 'init-wasm' message - */ -function initWasmFromModule(wasmModule, memory) { - return WebAssembly.instantiate(wasmModule, { - // Import object - Emscripten typically uses 'a' for the main import namespace - a: { - // Memory import if needed - d: () => { throw new Error('abort'); }, - b: () => 1, // nowIsMonotonic - a: () => performance.now(), // _emscripten_get_now - c: (size) => { // _emscripten_resize_heap - not typically needed with fixed memory - return 0; - } - } - }).then(instance => { - wasmInstance = instance; - wasmMemory = memory; - wasmReady = true; - - // Call the constructors - if (instance.exports.f) { - instance.exports.f(); - } - - return true; - }); -} - -/** - * C15Processor - AudioWorklet processor for the C15 synth engine - * - * Fulfills validation assertions: - * - VAL-M2-001: WASM module loads in AudioWorkletGlobalScope - * - VAL-M2-002: registerProcessor() succeeds with 0 inputs, 2 outputs - * - VAL-M2-003: AudioWorkletNode connects to AudioContext destination - * - VAL-M2-004: WASM render produces valid stereo float32 output - * - VAL-M2-005: Ring buffer enables lock-free parameter updates without corruption - * - VAL-M2-006: Note on/off via ring buffer triggers audio start/release - * - * NOTE: WebAssembly.Module cannot be sent via MessagePort.postMessage() in Chrome. - * The WASM module must be passed via processorOptions in the AudioWorkletNode constructor. - * See: https://issues.chromium.org/issues/40855462 - */ -class C15Processor extends AudioWorkletProcessor { - constructor(options) { - super(options); - - // Processor configuration - this._initialized = false; - this._sampleRate = 48000; - this._polyphony = 24; - this._bufferSize = 128; - - // Default configuration - const processorOptions = options.processorOptions || {}; - this._sampleRate = processorOptions.sampleRate || 48000; - this._polyphony = processorOptions.polyphony || 24; - - // Listen for messages from main thread - this.port.onmessage = this._handleMessage.bind(this); - - // Debug: verify message handler is bound - this.port.postMessage({ type: 'status', status: 'handler-bound', test: true }); - - // Log that processor was created - this._postStatus('created', { - sampleRate: this._sampleRate, - polyphony: this._polyphony - }); - - // Initialize WASM from processorOptions if provided - // Chrome requires this approach - WebAssembly.Module cannot be sent via postMessage - // See: https://issues.chromium.org/issues/40855462 - if (processorOptions.wasmModule instanceof WebAssembly.Module) { - this._postStatus('wasm-module-received-via-options', { - hasModule: true, - moduleType: 'WebAssembly.Module' - }); - // Initialize WASM asynchronously - this._initFromWasmModule(processorOptions.wasmModule, null); - } else if (processorOptions.wasmModule) { - this._postError(new Error('wasmModule in processorOptions is not a WebAssembly.Module'), 'constructor'); - } else { - this._postStatus('no-wasm-in-options', { - hint: 'WASM module should be passed via processorOptions.wasmModule' - }); - } - - // Initialize ring buffer if provided - if (processorOptions.ringBuffer) { - ringBufferReader = new RingBufferReader(processorOptions.ringBuffer); - this._postStatus('ring-buffer-ready', { - hasRingBuffer: true, - capacity: RING_CAPACITY - }); - } - } - - /** - * Post status message to main thread - * @private - */ - _postStatus(status, data = {}) { - this.port.postMessage({ - type: 'status', - status: status, - ...data - }); - } - - /** - * Post error message to main thread - * @private - */ - _postError(error, context = '') { - this.port.postMessage({ - type: 'error', - error: error.toString(), - context: context - }); - } - - /** - * Handle messages from main thread - * @private - */ - _handleMessage(event) { - const data = event.data; // event.data contains the message - - // Debug log all messages (except high-frequency ones) - if (data.type !== 'setParameter' && data.type !== 'tick') { - this._postStatus('message-received', { msgType: data.type }); - } - - switch (data.type) { - case 'test': - this._postStatus('test-received', { value: data.value }); - break; - - case 'init-wasm': - // Initialize WASM from compiled module sent by main thread - // NOTE: This may not work in Chrome due to cross-origin issues - // See: https://issues.chromium.org/issues/40855462 - // The WASM module should be passed via processorOptions instead - if (this._initialized) { - this._postStatus('wasm-already-initialized', { - hint: 'WASM was already initialized via processorOptions' - }); - return; - } - - // Check if wasmModule is valid - if (!data.wasmModule) { - this._postError(new Error('No wasmModule in init-wasm message'), 'init-wasm'); - return; - } - - if (!(data.wasmModule instanceof WebAssembly.Module)) { - this._postError(new Error('wasmModule is not a WebAssembly.Module: ' + typeof data.wasmModule), 'init-wasm'); - return; - } - - this._postStatus('starting-wasm-init', { - hasModule: true, - moduleType: 'WebAssembly.Module', - note: 'Using postMessage (may fail in Chrome)' - }); - this._initFromWasmModule(data.wasmModule, data.memory); - break; - - case 'init-ring-buffer': - // Initialize ring buffer from SharedArrayBuffer sent by main thread - if (this._initialized && ringBufferReader) { - this._postStatus('ring-buffer-already-initialized', { - hint: 'Ring buffer was already initialized via processorOptions' - }); - return; - } - - if (data.ringBuffer) { - ringBufferReader = new RingBufferReader(data.ringBuffer); - this._postStatus('ring-buffer-ready', { - hasRingBuffer: true, - capacity: RING_CAPACITY - }); - } else { - this._postError(new Error('No SharedArrayBuffer provided'), 'ring buffer init'); - } - break; - - case 'noteOn': - if (this._initialized && wasmInstance) { - wasmInstance.exports.o(data.note, data.velocity); // _noteOn - } - break; - - case 'noteOff': - if (this._initialized && wasmInstance) { - wasmInstance.exports.p(data.note, data.velocity); // _noteOff - } - break; - - case 'setParameter': - if (this._initialized && wasmInstance) { - wasmInstance.exports.q(data.paramId, data.value); // _setParameter - } - break; - - case 'reset': - if (this._initialized && wasmInstance) { - wasmInstance.exports.t(); // _reset - } - break; - - case 'getConfig': - this._postStatus('config', { - sampleRate: this._sampleRate, - polyphony: this._polyphony, - initialized: this._initialized - }); - break; - - default: - console.warn('[C15Processor] Unknown message type:', data.type); - } - } - - /** - * Initialize from pre-compiled WASM module - * @private - */ - async _initFromWasmModule(wasmModule, memory) { - try { - this._postStatus('loading'); - - // Lazy reference to WASM memory — needed by _clock_time_get before we can - // assign wasmMemory (which only exists after WebAssembly.instantiate returns). - const memRef = [null]; - - // Define the WASM imports matching Emscripten's expected structure. - // Mapping (namespace 'a') as of the current build: - // a -> _emscripten_get_now - // b -> _proc_exit - // c -> __emscripten_runtime_keepalive_clear - // d -> __setitimer_js - // e -> _clock_time_get (WASI clock; writes i64 nanoseconds into WASM heap) - // f -> _emscripten_resize_heap - // g -> __abort_js - const imports = { - a: { - a: () => performance.now(), - b: (code) => { throw new Error('WASM exit: ' + code); }, - c: () => {}, - d: (_which, _timeout_ms) => 0, - e: (clk_id, _ignored_precision, ptime) => { - const now = clk_id === 0 ? Date.now() : performance.now(); - const nsec = BigInt(Math.round(now * 1e6)); - if (memRef[0]) { - new BigInt64Array(memRef[0].buffer)[ptime >> 3] = nsec; - } - return 0; - }, - f: (requestedSize) => { - console.warn('[C15Processor] Heap resize requested but not supported'); - return 0; - }, - g: () => { throw new Error('WASM abort called'); } - } - }; - - // Instantiate the compiled module - const instance = await WebAssembly.instantiate(wasmModule, imports); - - wasmInstance = instance; - - // Get memory from exports (export 'h' is memory) - wasmMemory = instance.exports.h; - memRef[0] = wasmMemory; - - // Call runtime init (export 'i' is initRuntime/__wasm_call_ctors) - if (instance.exports.i) { - instance.exports.i(); - } - - // Initialize the engine (export 'j' is _engineInit) - const initResult = instance.exports.j(this._sampleRate, this._polyphony); - - if (initResult !== 1) { - throw new Error('engineInit failed with result: ' + initResult); - } - - this._initialized = true; - wasmReady = true; - - // Get the default frames per render call (export 'v' is _getDefaultFrames) - this._bufferSize = instance.exports.v(); - - this._postStatus('ready', { - sampleRate: this._sampleRate, - polyphony: this._polyphony, - bufferSize: this._bufferSize - }); - - } catch (error) { - this._postError(error, 'WASM load/init'); - console.error('[C15Processor] Failed to init WASM:', error); - } - } - - /** - * Process audio frames - * - * Called by the browser for each audio render quantum (typically 128 frames). - * - * VAL-M2-005: Processes ring buffer messages lock-free from main thread - * VAL-M2-006: Note on/off via ring buffer triggers audio - * - * @param {Float32Array[][]} inputs - Input audio buffers (unused) - * @param {Float32Array[][]} outputs - Output audio buffers (stereo) - * @param {Object} parameters - Automatable parameters (unused) - * @returns {boolean} - True to keep processor alive - */ - process(inputs, outputs, parameters) { - const output = outputs[0]; - - if (!output || output.length < 2) { - return true; // Keep alive but no output - } - - const leftChannel = output[0]; - const rightChannel = output[1]; - const numFrames = leftChannel.length; - - // If not initialized, output silence - if (!this._initialized || !wasmInstance) { - for (let i = 0; i < numFrames; i++) { - leftChannel[i] = 0; - rightChannel[i] = 0; - } - return true; - } - - // Process ring buffer messages first (VAL-M2-005, VAL-M2-006) - if (ringBufferReader) { - ringBufferReader.processMessages({ - onParameter: (paramId, value) => { - wasmInstance.exports.q(paramId, value); // _setParameter - }, - onNoteOn: (note, velocity) => { - this._dbgNoteOns = (this._dbgNoteOns || 0) + 1; - wasmInstance.exports.o(note, velocity); // _noteOn - }, - onNoteOff: (note, velocity) => { - wasmInstance.exports.p(note, velocity); // _noteOff - } - }, 64); // Process up to 64 messages per audio frame - } - - // Cache HEAPF32 view (recreate if memory grew) - if (!this._heapF32 || this._heapF32.buffer !== wasmMemory.buffer) { - this._heapF32 = new Float32Array(wasmMemory.buffer); - } - - try { - // Render audio using WASM - // export 'm' is _render - returns pointer to interleaved stereo buffer - const bufferPtr = wasmInstance.exports.m(numFrames); - - if (!bufferPtr) { - // Render failed, output silence - for (let i = 0; i < numFrames; i++) { - leftChannel[i] = 0; - rightChannel[i] = 0; - } - return true; - } - - const heapF32 = this._heapF32; - const bufferOffset = bufferPtr >> 2; // Convert byte offset to float32 index - - // Deinterleave stereo data from WASM buffer to output channels - let frameMax = 0; - for (let i = 0; i < numFrames; i++) { - const sampleIndex = bufferOffset + (i * 2); - const l = heapF32[sampleIndex]; - const r = heapF32[sampleIndex + 1]; - leftChannel[i] = l; - rightChannel[i] = r; - const v = Math.abs(l) > Math.abs(r) ? Math.abs(l) : Math.abs(r); - if (v > frameMax) frameMax = v; - } - - // Periodic diagnostics (every ~2 seconds = 750 frames at 128 frames/quantum) - this._dbgFrameCount = (this._dbgFrameCount || 0) + 1; - this._dbgMaxSample = Math.max(this._dbgMaxSample || 0, frameMax); - if (this._dbgFrameCount % 750 === 0) { - this.port.postMessage({ - type: 'diag', - frames: this._dbgFrameCount, - maxSample: this._dbgMaxSample, - noteOns: this._dbgNoteOns || 0 - }); - this._dbgMaxSample = 0; - } - - } catch (error) { - // On error, output silence - console.error('[C15Processor] Render error:', error); - for (let i = 0; i < numFrames; i++) { - leftChannel[i] = 0; - rightChannel[i] = 0; - } - } - - return true; // Keep processor alive - } - - /** - * Static getter for parameter descriptors (for automatable parameters) - * Currently not used but required for proper AudioWorklet interface. - */ - static get parameterDescriptors() { - return []; - } -} - -// Register the processor with the AudioWorkletGlobalScope -// VAL-M2-002: This registration must succeed for the processor to be usable -registerProcessor('c15-processor', C15Processor); diff --git a/playground/css/a-immersive.css b/playground/css/a-immersive.css deleted file mode 100644 index 398a329..0000000 --- a/playground/css/a-immersive.css +++ /dev/null @@ -1,2538 +0,0 @@ -/* NISPS Immersive — Design A */ -/* Full-viewport canvas with floating overlays + dock/drawer UI */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} - -:root { - --glass-bg: rgba(13, 13, 13, 0.65); - --glass-border: rgba(255, 255, 255, 0.08); - --glass-blur: 16px; - --accent: #ff6a00; - --accent-dim: rgba(255, 106, 0, 0.25); - --danger: #ff4466; - --text: #e0e0e0; - --text-dim: #888; - --radius: 12px; - --radius-sm: 8px; - --safe-bottom: env(safe-area-inset-bottom, 0px); - --dock-width: 48px; - --dock-gap: 8px; -} - -html, body { - width: 100%; - height: 100%; - overflow: hidden; - background: #0d0d0d; - color: var(--text); - font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 13px; - touch-action: none; - user-select: none; - -webkit-user-select: none; -} - -/* ---- Back button ---- */ -.back-btn { - position: fixed; - top: 45px; - left: 8px; - z-index: 30; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border-radius: 6px; - background: rgba(13, 13, 13, 0.65); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border: 1px solid rgba(255, 255, 255, 0.08); - color: var(--text); - text-decoration: none; - line-height: 1; - transition: all 0.15s; -} - -.back-btn:hover, .back-btn:active { - border-color: var(--accent); - color: var(--accent); -} - -/* ---- Fullscreen canvas ---- */ -#vis-canvas, -#synth-vis-canvas { - position: fixed; - inset: 0; - width: 100%; - height: 100%; - display: block; - z-index: 0; -} - -#synth-vis-canvas { - display: none; -} - -#synth-vis-canvas.active { - display: block; -} - -#vis-canvas.hidden-canvas { - display: none; -} - -/* ---- Glass mixin ---- */ -.glass { - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); -} - -/* ---- Heatmap strip (top) — interactive bars ---- */ -.heatmap-strip { - position: fixed; - top: 0; - left: 0; - right: 0; - height: 22px; - z-index: 20; - background: rgba(13, 13, 13, 0.5); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - display: flex; - align-items: flex-end; - padding: 2px 4px; -} - -.heatmap-cells { - display: flex; - flex-direction: row; - flex: 1; - height: 16px; - gap: 1px; - border-radius: 3px; - overflow: hidden; - align-items: stretch; -} - -.heatmap-cell { - flex: 1; - min-width: 0; - position: relative; - cursor: pointer; - height: 16px; - background: rgba(255, 255, 255, 0.04); - overflow: hidden; -} - -.heatmap-cell.dragging { - cursor: ew-resize; -} - -.heatmap-cell-bar { - height: 100%; - border-radius: 0 1px 1px 0; - transition: width 0.15s; - pointer-events: none; -} - -.heatmap-cell:hover .heatmap-cell-bar { - filter: brightness(1.3); -} - -.heatmap-tooltip { - position: fixed; - top: 32px; - padding: 4px 10px; - background: rgba(0, 0, 0, 0.85); - border: 1px solid var(--glass-border); - border-radius: 6px; - font-size: 12px; - color: var(--text); - pointer-events: none; - opacity: 0; - transition: opacity 0.15s; - z-index: 25; - white-space: nowrap; -} - -.heatmap-tooltip.visible { - opacity: 1; -} - -/* ---- Floating joystick with integrated map ---- */ -.joystick-container { - position: fixed; - bottom: 100px; - left: 24px; - width: 160px; - height: 160px; - z-index: 30; - touch-action: none; - border-radius: 50%; -} - -#joy-map { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - border-radius: 50%; - cursor: grab; - touch-action: none; -} - -#joy-map:active { - cursor: grabbing; -} - -.joystick-glow { - position: absolute; - inset: -8px; - border-radius: 50%; - pointer-events: none; - background: radial-gradient(circle, rgba(255,106,0,0.06) 0%, transparent 70%); - z-index: -1; -} - -/* Noise ring (RL exploration indicator) */ -.noise-ring { - position: absolute; - inset: -4px; - border-radius: 50%; - border: 0px solid rgba(255, 100, 50, 0.0); - pointer-events: none; - transition: border-width 0.3s, border-color 0.3s; -} - -.noise-ring.active { - border-width: 3px; - border-color: rgba(255, 100, 50, 0.5); -} - -.noise-ring.high { - border-width: 6px; - border-color: rgba(255, 60, 40, 0.6); -} - -/* Follow badge near joystick */ -.follow-badge { - position: absolute; - top: -24px; - left: 50%; - transform: translateX(-50%); - padding: 2px 10px; - border-radius: 10px; - background: rgba(255, 106, 0, 0.15); - border: 1px solid rgba(255, 106, 0, 0.4); - color: var(--accent); - font-size: 10px; - font-weight: 700; - letter-spacing: 1px; - pointer-events: none; - white-space: nowrap; -} - -.follow-badge.hidden { - display: none; -} - -/* Joystick follow-mode pulsing border on canvas */ -.joystick-container.follow-active #joy-map { - box-shadow: 0 0 0 2px rgba(255, 106, 0, 0.5); - animation: follow-pulse 1.5s ease-in-out infinite; -} - -@keyframes follow-pulse { - 0%, 100% { box-shadow: 0 0 0 2px rgba(255, 106, 0, 0.3); } - 50% { box-shadow: 0 0 12px 3px rgba(255, 106, 0, 0.5); } -} - -/* ---- Hand tracking PIP ---- */ -.hand-pip { - position: fixed; - bottom: 100px; - left: 24px; - width: 180px; - height: 135px; - z-index: 30; - border-radius: var(--radius); - overflow: hidden; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); -} - -.hand-pip.hidden { - display: none; -} - -.hand-pip video { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; -} - -.hand-pip canvas { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - transform: scaleX(-1); - pointer-events: none; -} - -.hand-status { - position: absolute; - bottom: 4px; - left: 0; - right: 0; - text-align: center; - font-size: 9px; - color: var(--text-dim); - pointer-events: none; - text-shadow: 0 1px 3px rgba(0,0,0,0.8); -} - -.hand-status.tracking { - color: var(--accent); -} - -.gesture-indicator { - position: absolute; - top: 6px; - right: 6px; - width: 36px; - height: 36px; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s; -} - -.gesture-indicator.active { - opacity: 1; -} - -.gesture-ring { - width: 100%; - height: 100%; - transform: rotate(-90deg); -} - -.gesture-ring-bg { - fill: none; - stroke: rgba(255,255,255,0.1); - stroke-width: 3; -} - -.gesture-ring-progress { - fill: none; - stroke: var(--accent); - stroke-width: 3; - stroke-dasharray: 100.53; - stroke-dashoffset: 100.53; - stroke-linecap: round; - transition: stroke-dashoffset 0.05s linear; -} - -.gesture-label { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - color: white; - text-shadow: 0 1px 3px rgba(0,0,0,0.8); -} - -/* ---- Dev panel (hand tracking tuning) ---- */ -.dev-panel { - position: fixed; - top: 60px; - right: calc(var(--dock-width) + 24px); - width: 220px; - z-index: 90; - background: rgba(10, 10, 10, 0.92); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - font-size: 10px; - color: var(--text-dim); - user-select: none; - box-shadow: 0 4px 20px rgba(0,0,0,0.5); -} - -.dev-panel.collapsed { - width: auto; -} - -.dev-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 10px; - cursor: grab; - border-bottom: 1px solid rgba(255,255,255,0.06); -} - -.dev-panel-header:active { - cursor: grabbing; -} - -.dev-panel-title { - font-size: 10px; - font-weight: 500; - color: var(--accent); - letter-spacing: 0.03em; -} - -.dev-panel-collapse { - background: none; - border: 1px solid rgba(255,255,255,0.15); - color: var(--text-dim); - width: 18px; - height: 18px; - border-radius: 3px; - cursor: pointer; - font-size: 10px; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.dev-panel-body { - padding: 6px 10px 10px; -} - -.dev-row { - margin-bottom: 6px; -} - -.dev-label { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 9px; - color: rgba(255,255,255,0.5); - margin-bottom: 2px; - cursor: help; -} - -.dev-slider-row { - display: flex; - align-items: center; - gap: 6px; -} - -.dev-slider { - flex: 1; - height: 3px; - -webkit-appearance: none; - appearance: none; - background: rgba(255,255,255,0.1); - border-radius: 2px; - outline: none; -} - -.dev-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent); - cursor: pointer; -} - -.dev-slider::-moz-range-thumb { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent); - border: none; - cursor: pointer; -} - -.dev-value { - font-family: 'JetBrains Mono', monospace; - font-size: 9px; - color: rgba(255,255,255,0.6); - min-width: 28px; - text-align: right; -} - -.dev-toggle { - accent-color: var(--accent); - cursor: pointer; -} - -.dev-row-actions { - margin-top: 8px; - padding-top: 6px; - border-top: 1px solid rgba(255,255,255,0.06); -} - -.dev-reset-btn { - background: rgba(255,255,255,0.06); - border: 1px solid rgba(255,255,255,0.1); - color: var(--text-dim); - font-size: 9px; - padding: 3px 8px; - border-radius: 4px; - cursor: pointer; - width: 100%; -} - -.dev-reset-btn:hover { - background: rgba(255,255,255,0.1); - color: white; -} - -.dev-feature-bars { - width: 100%; - height: 56px; - border-radius: 4px; - background: rgba(0,0,0,0.3); - display: block; -} - -/* ---- RL floating buttons (with undo) ---- */ -.rl-buttons { - position: fixed; - bottom: 36px; - left: 50%; - transform: translateX(-50%); - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto auto; - gap: 12px; - justify-items: center; - z-index: 45; - transition: opacity 0.3s; -} - -.rl-buttons.hidden { - opacity: 0; - pointer-events: none; -} - -.rl-btn { - width: 64px; - height: 64px; - border-radius: 50%; - border: 2px solid var(--glass-border); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - color: var(--text); - cursor: pointer; - transition: all 0.15s; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding-top: 2px; - gap: 0; -} - -.rl-icon { - font-size: 28px; - font-weight: 700; - line-height: 1; -} - -.key-num { - font-size: 12px; - color: rgba(255, 255, 255, 0.5); - font-family: inherit; - line-height: 1; - margin-top: -1px; -} - -.rl-btn:active { - transform: scale(0.9); -} - -.rl-up { - border-color: rgba(0, 200, 160, 0.4); - color: #00c8a0; - background: rgba(0, 200, 160, 0.08); -} - -.rl-up:active { - background: rgba(0, 200, 160, 0.2); - box-shadow: 0 0 20px rgba(0, 200, 160, 0.3); -} - -.rl-down { - border-color: rgba(220, 60, 20, 0.4); - color: #dc3c14; - background: rgba(220, 60, 20, 0.08); -} - -.rl-down:active { - background: rgba(220, 60, 20, 0.2); - box-shadow: 0 0 20px rgba(220, 60, 20, 0.3); -} - -.rl-btn.flash { - animation: rl-flash 0.25s ease-out; -} - -@keyframes rl-flash { - 0% { transform: scale(1.15); } - 100% { transform: scale(1); } -} - -/* Undo button — small, between/below the +/- */ -.rl-undo-btn { - grid-column: 1 / -1; - width: 28px; - height: 28px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - color: var(--text-dim); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - transition: all 0.15s; - opacity: 0.4; -} - -.rl-undo-btn:hover { - opacity: 0.8; - border-color: rgba(255, 255, 255, 0.25); - color: var(--text); -} - -.rl-undo-btn:active { - transform: scale(0.9); - opacity: 1; -} - -.rl-undo-btn.has-undo { - opacity: 0.7; -} - -.rl-undo-btn.flash { - animation: rl-flash 0.25s ease-out; -} - -/* ---- RL label (shown in Linked mode to distinguish Synth vs FX) ---- */ -.rl-label { - grid-column: 1 / -1; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.05em; - opacity: 0.6; - text-align: center; - color: var(--text); -} -.rl-label.hidden { display: none; } - -/* ---- EOC RL buttons (FX feedback, Linked mode) ---- */ -.eoc-rl-buttons { - position: fixed; - bottom: 36px; - left: calc(50% + 100px); - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - z-index: 45; - transition: opacity 0.3s; -} -.eoc-rl-buttons.hidden { display: none; } - -.eoc-rl-label { - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.05em; - opacity: 0.6; - color: var(--text); -} - -.eoc-rl-btn { - width: 48px; - height: 48px; -} - -.eoc-rl-btn.rl-up { - border-color: rgba(78, 205, 196, 0.4); - color: #4ecdc4; - background: rgba(78, 205, 196, 0.08); -} - -.eoc-rl-btn.rl-up:active { - background: rgba(78, 205, 196, 0.2); - box-shadow: 0 0 20px rgba(78, 205, 196, 0.3); -} - -.eoc-rl-btn.rl-down { - border-color: rgba(78, 205, 196, 0.3); - color: #e07040; - background: rgba(224, 112, 64, 0.08); -} - -.eoc-rl-btn.rl-down:active { - background: rgba(224, 112, 64, 0.2); - box-shadow: 0 0 20px rgba(224, 112, 64, 0.3); -} - -/* ---- Status line (floating, minimal) ---- */ -.status-line { - position: fixed; - bottom: 8px; - left: 50%; - transform: translateX(-50%); - padding: 2px 12px; - border-radius: 10px; - background: rgba(0, 0, 0, 0.4); - font-size: 10px; - color: var(--text-dim); - white-space: nowrap; - pointer-events: none; - z-index: 40; -} - -/* ---- Right-side dock (macOS-style) ---- */ -.dock { - position: fixed; - right: 8px; - top: 50%; - transform: translateY(-50%); - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - z-index: 50; - padding: 8px 4px; - border-radius: 16px; - background: rgba(13, 13, 13, 0.5); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.dock-icon { - width: 36px; - height: 36px; - border-radius: 10px; - border: 1px solid transparent; - background: transparent; - color: var(--text-dim); - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1px; - padding: 0; - transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1); - position: relative; -} - -.dock-label { - font-size: 7px; - letter-spacing: 0.3px; - opacity: 0.5; - transition: opacity 0.2s; - line-height: 1; -} - -/* macOS magnification effect */ -.dock-icon:hover { - transform: scale(1.35); - color: var(--text); - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.08); - z-index: 2; -} - -.dock-icon:hover .dock-label { - opacity: 1; -} - -/* Adjacent icons scale slightly on hover (approximation) */ -.dock-icon:hover + .dock-icon { - transform: scale(1.12); -} - -.dock-icon:has(+ .dock-icon:hover) { - transform: scale(1.12); -} - -/* Active state (drawer is open) */ -.dock-icon.active { - color: var(--accent); - background: rgba(255, 106, 0, 0.08); - border-color: rgba(255, 106, 0, 0.2); -} - -.dock-icon.active .dock-label { - opacity: 0.8; - color: var(--accent); -} - -.dock-icon:active { - transform: scale(0.95); -} - -/* ---- Drawer stack (right side, left of dock) ---- */ -.drawer-stack { - position: fixed; - right: calc(var(--dock-width) + 16px); - top: 28px; - bottom: 28px; - width: 260px; - display: flex; - flex-direction: column; - gap: 8px; - z-index: 45; - pointer-events: none; - overflow-y: auto; - overflow-x: hidden; - scrollbar-width: none; -} - -.drawer-stack::-webkit-scrollbar { - display: none; -} - -/* Individual drawer panel */ -.drawer { - pointer-events: auto; - width: 100%; - max-height: 400px; - overflow-y: auto; - background: rgba(13, 13, 13, 0.88); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: var(--radius-sm); - animation: drawerSlideIn 0.2s cubic-bezier(0.22, 1, 0.36, 1); - flex-shrink: 0; -} - -.drawer.hidden { - display: none; -} - -@keyframes drawerSlideIn { - from { - opacity: 0; - transform: translateX(12px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.drawer::-webkit-scrollbar { - width: 3px; -} - -.drawer::-webkit-scrollbar-track { - background: transparent; -} - -.drawer::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; -} - -.drawer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 10px; - font-size: 11px; - font-weight: 700; - color: var(--accent); - letter-spacing: 0.5px; - text-transform: uppercase; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); -} - -.drawer-close { - width: 20px; - height: 20px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.1); - background: transparent; - color: var(--text-dim); - font-size: 14px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - line-height: 1; - transition: all 0.15s; -} - -.drawer-close:hover { - border-color: var(--accent); - color: var(--accent); -} - -.drawer-body { - padding: 8px 10px 10px; -} - -/* ---- Pill toggle (used in mode drawer) ---- */ -.pill-toggle { - display: inline-flex; - border-radius: 6px; - overflow: hidden; - border: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.04); -} - -.pill-toggle-sm { - border-radius: 4px; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.08); -} - -.pill-toggle-sm .pill-opt { - padding: 3px 10px; - font-size: 11px; -} - -.pill-opt { - padding: 6px 18px; - background: none; - border: none; - color: var(--text-dim); - font-size: 13px; - cursor: pointer; - transition: all 0.2s; -} - -.pill-opt.active { - background: var(--accent-dim); - color: var(--accent); - font-weight: 600; -} - -/* Mode group row */ -.mode-group { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 8px; -} - -.mode-label { - font-size: 10px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - min-width: 48px; -} - -/* Follow pill */ -.follow-pill { - padding: 4px 12px; - border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.04); - color: var(--text-dim); - font-size: 12px; - cursor: pointer; - transition: all 0.15s; -} - -.follow-pill.active { - background: rgba(255, 106, 0, 0.15); - border-color: rgba(255, 106, 0, 0.4); - color: var(--accent); -} - -/* ---- Action buttons (inside drawers) ---- */ -.action-row { - display: flex; - gap: 6px; - flex-wrap: wrap; - justify-content: flex-start; - margin-bottom: 8px; -} - -.action-btn { - padding: 5px 10px; - border-radius: 5px; - border: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.06); - color: var(--text); - font-size: 11px; - cursor: pointer; - transition: all 0.15s; - touch-action: manipulation; -} - -.action-btn:active { - transform: scale(0.95); -} - -.action-btn.accent { - border-color: rgba(255, 106, 0, 0.3); - color: var(--accent); - background: rgba(255, 106, 0, 0.08); -} - -.action-btn.accent:active { - background: rgba(255, 106, 0, 0.2); -} - -.action-btn.dim { - color: var(--text-dim); - border-color: rgba(255, 255, 255, 0.05); -} - -.action-btn.flash { - animation: btn-flash 0.2s ease-out; -} - -@keyframes btn-flash { - 0% { background: rgba(255, 106, 0, 0.3); } - 100% { background: rgba(255, 255, 255, 0.06); } -} - -/* Preset row */ -.preset-row { - display: flex; - gap: 4px; - align-items: center; - flex-wrap: wrap; - margin-bottom: 8px; -} - -.preset-label { - font-size: 9px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-right: 2px; -} - -.preset-chip { - padding: 3px 8px; - border-radius: 5px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.04); - color: var(--text-dim); - font-size: 10px; - cursor: pointer; - transition: all 0.15s; - touch-action: manipulation; -} - -.preset-chip:active { - background: rgba(255, 106, 0, 0.15); - color: var(--accent); -} - -/* Synth panel (inside synth drawer) */ -.synth-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} - -.synth-row label { - font-size: 10px; - color: var(--text-dim); - min-width: 60px; -} - -.synth-row input[type="range"] { - flex: 1; - accent-color: var(--accent); - height: 3px; -} - -.synth-row select { - flex: 1; - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--glass-border); - border-radius: 5px; - color: var(--text); - padding: 3px 6px; - font-size: 10px; -} - -.synth-status { - font-size: 10px; - color: var(--text-dim); -} - -/* Loss section */ -.loss-section { - margin-bottom: 4px; -} - -.loss-section label { - font-size: 9px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - display: block; - margin-bottom: 3px; -} - -#loss-canvas { - width: 100%; - height: 60px; - border-radius: var(--radius-sm); - background: rgba(0, 0, 0, 0.3); - border: 1px solid var(--glass-border); -} - -/* Raw parameters (inside params drawer) */ -.raw-params { - display: grid; - grid-template-columns: 1fr; - gap: 3px; - max-height: 300px; - overflow-y: auto; - padding: 4px 0; -} - -.raw-param { - display: flex; - align-items: center; - gap: 6px; - font-size: 10px; - position: relative; -} - -.raw-param-label { - width: 56px; - color: var(--text-dim); - text-align: right; - flex-shrink: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 9px; -} - -.raw-param input[type="range"] { - flex: 1; - accent-color: var(--accent); - height: 3px; -} - -.raw-param-val { - width: 32px; - color: var(--text-dim); - text-align: left; - flex-shrink: 0; - font-variant-numeric: tabular-nums; - font-size: 9px; -} - -/* Tooltip for raw param sliders */ -.raw-param[data-tooltip]:hover::after { - content: attr(data-tooltip); - position: absolute; - bottom: calc(100% + 6px); - left: 50%; - transform: translateX(-50%); - padding: 4px 10px; - background: rgba(0, 0, 0, 0.9); - border: 1px solid var(--glass-border); - border-radius: 6px; - font-size: 12px; - color: var(--text); - white-space: nowrap; - pointer-events: none; - z-index: 50; -} - -/* ---- Engine params ---- */ -.engine-params { - display: grid; - grid-template-columns: 1fr; - gap: 6px; - padding: 4px 0; -} - -.engine-param { - display: flex; - align-items: center; - gap: 6px; - font-size: 10px; -} - -.engine-param-label { - width: 64px; - color: var(--text-dim); - text-align: right; - flex-shrink: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 9px; - cursor: help; -} - -.engine-param input[type="range"] { - flex: 1; - accent-color: var(--accent); - height: 3px; -} - -.engine-param-val { - width: 42px; - color: var(--text-dim); - text-align: left; - flex-shrink: 0; - font-variant-numeric: tabular-nums; - font-size: 9px; -} - -/* ---- Synth quick controls (play button next to back) ---- */ -.synth-quick-controls { - position: fixed; - top: 45px; - left: 52px; - z-index: 30; - display: flex; - flex-direction: row; - align-items: center; - gap: 0; -} - -.play-btn { - width: 36px; - height: 36px; - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(13, 13, 13, 0.65); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - color: var(--text); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.15s, border-color 0.15s; - padding: 0; - flex-shrink: 0; - align-self: flex-start; -} - -.play-btn:active { - background: rgba(255, 255, 255, 0.12); -} - -.play-btn.playing { - border-color: rgba(255, 106, 0, 0.4); - color: var(--accent); - background: rgba(255, 106, 0, 0.12); -} - -.play-btn.audio-needs-init { - animation: audioInitPulse 2s ease-in-out infinite; -} - -@keyframes audioInitPulse { - 0%, 100% { - border-color: rgba(255, 106, 0, 0.3); - box-shadow: 0 0 6px rgba(255, 106, 0, 0.15); - color: var(--accent); - } - 50% { - border-color: rgba(255, 106, 0, 0.6); - box-shadow: 0 0 14px rgba(255, 106, 0, 0.3); - color: #ffaa55; - } -} - -.play-drawer { - display: none; - flex-direction: column; - gap: 6px; - padding: 8px 12px; - border-radius: 18px; - background: rgba(13, 13, 13, 0.65); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border: 1px solid rgba(255, 255, 255, 0.08); - margin-left: 6px; - height: auto; - white-space: nowrap; -} - -.play-drawer label { - font-size: 11px; - color: var(--text-dim); - display: flex; - align-items: center; - gap: 6px; -} - -.play-drawer input[type="range"] { - width: 80px; - accent-color: var(--accent); - height: 3px; -} - -.play-drawer span { - font-size: 10px; - font-variant-numeric: tabular-nums; - min-width: 24px; - color: var(--text-dim); -} - -.play-btn:hover ~ .play-drawer, -.play-drawer:hover, -.play-drawer.show { - display: flex; -} - -/* Synth preset dropdown */ -.preset-select { - margin-left: 8px; - height: 36px; - padding: 0 10px; - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(13, 13, 13, 0.65); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - color: var(--text); - font-family: inherit; - font-size: 11px; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - padding-right: 24px; - min-width: 0; - max-width: 160px; - text-overflow: ellipsis; - align-self: flex-start; -} - -.preset-select:hover, -.preset-select:focus { - border-color: rgba(255, 255, 255, 0.15); - background-color: rgba(255, 255, 255, 0.06); - outline: none; -} - -.preset-select option, -.preset-select optgroup { - background: #1a1a1a; - color: var(--text); -} - -/* ---- Utility ---- */ -.hidden { - display: none !important; -} - -/* ---- Group Override Drawer ---- */ -.group-drawer { - position: fixed; - z-index: 50; - width: 320px; - max-height: 60vh; - overflow-y: auto; - background: rgba(13, 13, 13, 0.92); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius-sm); - padding: 0; - opacity: 0; - pointer-events: none; - transform: translateY(-4px); - transition: opacity 0.15s, transform 0.15s; -} - -.group-drawer.visible { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -.group-drawer-header { - padding: 6px 10px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.group-drawer-body { - padding: 6px 8px 8px; -} - -/* Group curve row */ -.gd-curve-row { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; - padding-bottom: 6px; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); -} - -.gd-label { - font-size: 10px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - min-width: 34px; - flex-shrink: 0; -} - -.gd-curve-canvas { - width: 48px; - height: 48px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; -} - -.gd-val { - font-size: 10px; - color: var(--text-dim); - min-width: 30px; - text-align: right; - font-variant-numeric: tabular-nums; - flex-shrink: 0; -} - -/* Per-param row */ -.gd-param-row { - display: flex; - align-items: center; - gap: 4px; - margin-bottom: 2px; - min-height: 28px; -} - -.gd-param-name { - font-size: 9px; - color: var(--text-dim); - min-width: 52px; - max-width: 52px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 0; -} - -/* Per-param curve mini-canvas */ -.gd-param-curve-canvas { - width: 28px; - height: 28px; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; -} - -/* Dual-range slider wrapper */ -.gd-range-wrap { - position: relative; - flex: 1; - height: 16px; - min-width: 0; -} - -.gd-range-fill { - position: absolute; - top: 50%; - height: 3px; - transform: translateY(-50%); - background: var(--accent); - border-radius: 2px; - pointer-events: none; - opacity: 0.6; -} - -.gd-range-input { - position: absolute; - width: 100%; - top: 0; - left: 0; - height: 16px; - margin: 0; - padding: 0; - -webkit-appearance: none; - appearance: none; - background: transparent; - pointer-events: none; -} - -.gd-range-input::-webkit-slider-runnable-track { - height: 3px; - background: transparent; - border-radius: 2px; -} - -.gd-range-input::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - pointer-events: auto; - width: 10px; - height: 10px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.3); - cursor: pointer; - margin-top: -3.5px; -} - -.gd-range-min::-webkit-slider-thumb { - background: #4488ff; -} - -.gd-range-max::-webkit-slider-thumb { - background: #ff6a00; -} - -/* Firefox dual-range */ -.gd-range-input::-moz-range-track { - height: 3px; - background: transparent; - border: none; -} - -.gd-range-input::-moz-range-thumb { - pointer-events: auto; - width: 10px; - height: 10px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.3); - cursor: pointer; -} - -.gd-range-min::-moz-range-thumb { - background: #4488ff; -} - -.gd-range-max::-moz-range-thumb { - background: #ff6a00; -} - -/* Value slider (shown when muted) */ -.gd-val-slider { - flex: 1; - min-width: 0; - accent-color: #888; - height: 3px; - display: none; -} - -/* Mute button */ -.gd-mute-btn { - width: 20px; - height: 20px; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: transparent; - color: rgba(255, 255, 255, 0.25); - font-size: 9px; - font-weight: 700; - cursor: pointer; - flex-shrink: 0; - padding: 0; - line-height: 18px; - text-align: center; - transition: all 0.1s; -} - -.gd-mute-btn:hover { - border-color: rgba(255, 255, 255, 0.3); - color: rgba(255, 255, 255, 0.5); -} - -.gd-mute-btn.muted { - background: rgba(255, 60, 60, 0.2); - border-color: rgba(255, 60, 60, 0.4); - color: #ff5555; -} - -/* Muted param row: hide curve + range, show value slider */ -.gd-param-row.gd-muted .gd-param-curve-canvas, -.gd-param-row.gd-muted .gd-range-wrap { - display: none; -} - -.gd-param-row.gd-muted .gd-val-slider { - display: block; -} - -.gd-param-row.gd-muted .gd-param-name { - color: rgba(255, 255, 255, 0.2); - text-decoration: line-through; -} - -/* Drawer scrollbar */ -.group-drawer::-webkit-scrollbar { - width: 3px; -} - -.group-drawer::-webkit-scrollbar-track { - background: transparent; -} - -.group-drawer::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; -} - -/* ---- Heatmap Param Popup ---- */ -.param-popup { - position: fixed; - z-index: 50; - width: 260px; - background: rgba(13, 13, 13, 0.92); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius-sm); - padding: 0; - opacity: 0; - pointer-events: none; - transform: translateY(-4px); - transition: opacity 0.15s, transform 0.15s; -} - -.param-popup.visible { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -.pp-header { - padding: 6px 10px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.pp-body { - padding: 6px 10px 8px; -} - -.pp-row { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 6px; - min-height: 28px; -} - -.pp-label { - font-size: 10px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - min-width: 38px; - flex-shrink: 0; -} - -.pp-curve-canvas { - width: 36px; - height: 36px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; -} - -.pp-val { - font-size: 10px; - color: var(--text-dim); - min-width: 55px; - text-align: right; - font-variant-numeric: tabular-nums; - flex-shrink: 0; -} - -.pp-range-wrap { - position: relative; - flex: 1; - height: 16px; - min-width: 0; -} - -.pp-range-wrap .gd-range-fill { - position: absolute; - top: 50%; - height: 3px; - background: rgba(255, 255, 255, 0.15); - border-radius: 2px; - transform: translateY(-50%); - opacity: 0.6; -} - -.pp-range-wrap .gd-range-input { - position: absolute; - width: 100%; - top: 0; - height: 100%; - margin: 0; - padding: 0; - background: transparent; - cursor: pointer; - pointer-events: none; - -webkit-appearance: none; - appearance: none; -} - -.pp-range-wrap .gd-range-input::-webkit-slider-runnable-track { - height: 3px; - background: transparent; - border-radius: 2px; -} - -.pp-range-wrap .gd-range-input::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - pointer-events: auto; - width: 10px; - height: 10px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.2); - cursor: pointer; - margin-top: -3.5px; -} - -.pp-range-wrap .gd-range-min::-webkit-slider-thumb { - background: #4488ff; -} - -.pp-range-wrap .gd-range-max::-webkit-slider-thumb { - background: #ff6a00; -} - -.pp-freeze-row { - padding-top: 4px; - border-top: 1px solid rgba(255, 255, 255, 0.04); -} - -.pp-freeze-btn { - padding: 3px 10px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.15); - background: rgba(255, 255, 255, 0.06); - color: rgba(255, 255, 255, 0.4); - font-size: 10px; - font-weight: 600; - cursor: pointer; - flex-shrink: 0; - transition: all 0.1s; -} - -.pp-freeze-btn:hover { - border-color: rgba(255, 255, 255, 0.3); - color: rgba(255, 255, 255, 0.6); -} - -.pp-freeze-btn.frozen { - background: rgba(80, 160, 255, 0.2); - border-color: rgba(80, 160, 255, 0.5); - color: #50a0ff; -} - -.pp-val-slider { - flex: 1; - min-width: 0; - accent-color: #50a0ff; - height: 16px; -} - -/* Frozen heatmap cell indicator */ -.heatmap-cell.heatmap-cell-frozen .heatmap-cell-bar { - opacity: 0.4; -} - -.heatmap-cell.heatmap-cell-frozen::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: repeating-linear-gradient( - 45deg, - transparent, - transparent 2px, - rgba(80, 160, 255, 0.15) 2px, - rgba(80, 160, 255, 0.15) 4px - ); - pointer-events: none; -} - -/* ---- Heatmap shared-mode indicator (EOC NISPS Shared mode active) ---- */ -/* .heatmap-strip gets this class; ::after appends a small '+ FX' label */ -.heatmap-strip.shared-mode { - border-bottom: 1px solid rgba(120, 220, 180, 0.35); -} - -.heatmap-strip.shared-mode::after { - content: '+ FX'; - font-size: 9px; - font-weight: 600; - letter-spacing: 0.04em; - color: rgba(120, 220, 180, 0.8); - padding: 0 4px; - white-space: nowrap; - align-self: center; - pointer-events: none; -} - -/* ---- Help modal ---- */ -.help-overlay { - position: fixed; - inset: 0; - z-index: 100; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); - display: flex; - align-items: center; - justify-content: center; - padding: 16px; - animation: helpFadeIn 0.2s ease-out; -} - -.help-overlay.hidden { - display: none; -} - -@keyframes helpFadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.help-modal { - position: relative; - max-width: 520px; - width: 100%; - max-height: 85vh; - overflow-y: auto; - background: rgba(18, 18, 18, 0.95); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: var(--radius); - padding: 28px 24px 20px; - color: var(--text); -} - -.help-modal h2 { - font-size: 18px; - font-weight: 700; - margin: 0 0 2px; - color: #fff; -} - -.help-subtitle { - font-size: 11px; - color: var(--text-dim); - margin: 0 0 16px; - letter-spacing: 0.5px; -} - -.help-close { - position: absolute; - top: 12px; - right: 12px; - width: 28px; - height: 28px; - border-radius: 50%; - border: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.06); - color: var(--text-dim); - font-size: 18px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - line-height: 1; - transition: all 0.15s; -} - -.help-close:hover { - border-color: var(--accent); - color: var(--accent); -} - -.help-section { - margin-bottom: 16px; -} - -.help-section h3 { - font-size: 13px; - font-weight: 700; - color: var(--accent); - margin: 0 0 6px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.help-section p { - font-size: 12px; - line-height: 1.5; - margin: 0 0 6px; - color: var(--text); -} - -.help-section ul { - margin: 0; - padding-left: 18px; - font-size: 12px; - line-height: 1.6; - color: var(--text); -} - -.help-section li { - margin-bottom: 2px; -} - -.help-keys { - width: 100%; - border-collapse: collapse; - font-size: 12px; -} - -.help-keys th { - text-align: left; - padding: 8px 0 4px; - color: var(--text-dim); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.help-keys td { - padding: 3px 0; - color: var(--text); -} - -.help-keys td:first-child { - color: #fff; - font-weight: 500; - width: 45%; -} - -.help-keys kbd { - display: inline-block; - padding: 1px 6px; - border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.15); - background: rgba(255, 255, 255, 0.06); - font-family: inherit; - font-size: 11px; -} - -.help-got-it { - display: block; - width: 100%; - padding: 10px; - margin-top: 8px; - border-radius: 6px; - border: 1px solid rgba(255, 106, 0, 0.3); - background: rgba(255, 106, 0, 0.1); - color: var(--accent); - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.15s; -} - -.help-got-it:active { - background: rgba(255, 106, 0, 0.25); -} - -/* Help modal scrollbar */ -.help-modal::-webkit-scrollbar { - width: 4px; -} - -.help-modal::-webkit-scrollbar-track { - background: transparent; -} - -.help-modal::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; -} - -/* ---- Media queries ---- */ -@media (min-width: 768px) { - .joystick-container { - width: 180px; - height: 180px; - bottom: 120px; - left: 32px; - } - - #joy-map { - width: 180px; - height: 180px; - } - - .hand-pip { - width: 200px; - height: 150px; - bottom: 120px; - left: 32px; - } - - .rl-btn { - width: 72px; - height: 72px; - font-size: 32px; - } - - .eoc-rl-btn { - width: 54px; - height: 54px; - } - - .drawer-stack { - width: 280px; - } -} - -@media (max-width: 480px) { - .dock { - right: 4px; - padding: 6px 2px; - } - - .dock-icon { - width: 32px; - height: 32px; - } - - .drawer-stack { - right: calc(var(--dock-width) + 4px); - width: 220px; - } - - .rl-btn { - width: 56px; - height: 56px; - } - - .rl-icon { - font-size: 24px; - } - - .eoc-rl-btn { - width: 42px; - height: 42px; - } - - .eoc-rl-buttons { - left: calc(50% + 80px); - } -} - -/* ---- MIDI CC mode ---- */ -.midi-cc-quick-controls { - position: fixed; - top: 45px; - left: 52px; - z-index: 30; - display: flex; - flex-direction: row; - align-items: center; - gap: 6px; -} - -.midi-cc-quick-controls .preset-select { - max-width: 160px; -} - -.midi-cc-param-row { - padding: 4px 8px !important; - margin-bottom: 2px; -} - -/* Text/number inputs in param popup (for MIDI CC editing) */ -.pp-text-input, -.pp-num-input { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 4px; - color: var(--text); - font-family: 'JetBrains Mono', monospace; - font-size: 0.75rem; - padding: 3px 6px; - outline: none; - flex: 1; -} - -.pp-text-input:focus, -.pp-num-input:focus { - border-color: rgba(255, 106, 0, 0.4); -} - -.pp-num-input { - width: 50px; - flex: 0 0 auto; -} - -#midi-cc-count { - font-family: 'JetBrains Mono', monospace; - font-size: 0.8rem; - opacity: 0.8; - min-width: 20px; - text-align: center; -} - -/* ---- EOC Chain UI ---- */ - -.eoc-nisps-bar { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; - flex-wrap: wrap; -} - -.eoc-section-label { - font-size: 9px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - min-width: 36px; -} - -.eoc-nisps-toggle { - flex: 1; -} - -.eoc-module-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 10px; -} - -.eoc-empty-hint { - font-size: 10px; - color: var(--text-dim); - text-align: center; - padding: 10px 0; - opacity: 0.6; -} - -.eoc-module-row { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 6px; - padding: 5px 6px; - border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.06); - background: rgba(255, 255, 255, 0.03); - cursor: default; - transition: background 0.15s; -} - -.eoc-module-row:hover { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.1); -} - -.eoc-module-row.dragging { - opacity: 0.4; - cursor: grabbing; -} - -.eoc-drag-handle { - cursor: grab; - touch-action: none; - color: var(--text-dim); - font-size: 14px; - line-height: 1; - padding: 0 2px; - flex-shrink: 0; - user-select: none; -} - -.eoc-drag-handle:active { - cursor: grabbing; -} - -.eoc-module-name { - flex: 1; - font-size: 11px; - color: var(--text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.eoc-param-badge { - font-size: 9px; - color: var(--text-dim); - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 8px; - padding: 1px 6px; - white-space: nowrap; - flex-shrink: 0; -} - -/* Bypass pill toggle */ -.eoc-bypass-toggle { - position: relative; - display: inline-block; - width: 30px; - height: 16px; - flex-shrink: 0; - cursor: pointer; -} - -.eoc-bypass-toggle input { - opacity: 0; - width: 0; - height: 0; - position: absolute; -} - -.eoc-bypass-slider { - position: absolute; - inset: 0; - border-radius: 8px; - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.1); - transition: background 0.2s, border-color 0.2s; -} - -.eoc-bypass-slider::after { - content: ''; - position: absolute; - left: 2px; - top: 50%; - transform: translateY(-50%); - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--text-dim); - transition: transform 0.2s, background 0.2s; -} - -.eoc-bypass-toggle input:checked + .eoc-bypass-slider { - background: rgba(255, 106, 0, 0.25); - border-color: rgba(255, 106, 0, 0.4); -} - -.eoc-bypass-toggle input:checked + .eoc-bypass-slider::after { - transform: translateY(-50%) translateX(14px); - background: var(--accent); -} - -.eoc-remove-btn { - background: none; - border: 1px solid transparent; - color: var(--text-dim); - font-size: 15px; - line-height: 1; - width: 18px; - height: 18px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - cursor: pointer; - padding: 0; - flex-shrink: 0; - transition: all 0.15s; -} - -.eoc-remove-btn:hover { - color: var(--danger); - border-color: rgba(255, 68, 102, 0.3); - background: rgba(255, 68, 102, 0.08); -} - -/* Drop indicator line between rows during drag */ -.eoc-drop-indicator { - height: 2px; - border-radius: 1px; - background: var(--accent); - margin: 2px 0; - pointer-events: none; - opacity: 0.8; -} - -/* Add module bar */ -.eoc-add-bar { - display: flex; - flex-wrap: wrap; - gap: 5px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} - -.eoc-add-hint { - font-size: 10px; - color: var(--text-dim); - opacity: 0.5; - padding: 4px 0; -} - -.eoc-add-btn { - font-size: 10px; - padding: 3px 8px; -} - -/* Per-module param sliders */ -.eoc-params-section { - width: 100%; - margin-top: 4px; -} - -.eoc-params-toggle { - background: none; - border: none; - color: var(--text-dim, rgba(255, 255, 255, 0.4)); - font-size: 9px; - cursor: pointer; - padding: 2px 0; - text-align: left; - width: 100%; -} - -.eoc-params-toggle:hover { - color: var(--text-muted, rgba(255, 255, 255, 0.65)); -} - -.eoc-params-list { - display: flex; - flex-direction: column; - gap: 4px; - padding: 4px 0 2px 0; -} - -.eoc-params-list.eoc-params-collapsed { - display: none; -} - -.eoc-param-row { - display: flex; - align-items: center; - gap: 6px; -} - -.eoc-param-label { - font-size: 9px; - color: var(--text-dim, rgba(255, 255, 255, 0.4)); - min-width: 60px; - flex-shrink: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.eoc-param-slider { - flex: 1; - height: 3px; - accent-color: var(--accent, #ff6a00); - cursor: pointer; -} - -/* ---- Engine Switcher ---- */ -.engine-switcher-section { - margin-bottom: 4px; -} - -.engine-switcher-heading { - font-size: 9px; - font-weight: 700; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 6px; - padding: 0 2px; -} - -.engine-card-grid { - display: grid; - grid-template-columns: 1fr; - gap: 5px; -} - -.engine-card { - padding: 7px 9px; - border-radius: var(--radius-sm); - border: 1px solid var(--glass-border); - background: rgba(255, 255, 255, 0.03); - cursor: pointer; - transition: border-color 0.15s, background 0.15s, opacity 0.15s; - position: relative; - overflow: hidden; -} - -.engine-card:hover:not(.coming-soon):not(.loading) { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.14); -} - -.engine-card:active:not(.coming-soon) { - transform: scale(0.98); -} - -.engine-card.active { - border-color: rgba(255, 106, 0, 0.45); - background: rgba(255, 106, 0, 0.07); -} - -.engine-card.loading { - opacity: 0.5; - cursor: wait; - pointer-events: none; -} - -.engine-card.loading::after { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, var(--accent), transparent); - animation: engine-loading-bar 1.2s linear infinite; -} - -@keyframes engine-loading-bar { - from { transform: translateX(-100%); } - to { transform: translateX(300%); } -} - -.engine-card.coming-soon { - opacity: 0.38; - cursor: default; -} - -.engine-card-name-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 6px; - margin-bottom: 3px; -} - -.engine-card-name { - font-size: 10px; - font-weight: 700; - color: var(--text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.engine-card.active .engine-card-name { - color: var(--accent); -} - -.engine-card-params { - font-size: 8px; - padding: 1px 5px; - border-radius: 4px; - background: rgba(255, 255, 255, 0.07); - color: var(--text-dim); - white-space: nowrap; - flex-shrink: 0; -} - -.engine-card.active .engine-card-params { - background: rgba(255, 106, 0, 0.15); - color: var(--accent); -} - -.engine-card-desc { - font-size: 9px; - color: var(--text-dim); - line-height: 1.4; - display: block; -} - -.engine-switcher-divider { - height: 1px; - background: var(--glass-border); - margin: 8px 0 6px; -} - -/* ---- Toast notification ---- */ -.nisps-toast { - position: fixed; - bottom: 28px; - left: 50%; - transform: translateX(-50%) translateY(8px); - padding: 5px 14px; - border-radius: 10px; - background: rgba(20, 20, 20, 0.92); - border: 1px solid var(--glass-border); - color: var(--text); - font-size: 11px; - white-space: nowrap; - pointer-events: none; - z-index: 60; - opacity: 0; - transition: opacity 0.18s, transform 0.18s; -} - -.nisps-toast.visible { - opacity: 1; - transform: translateX(-50%) translateY(0); -} - -/* ---- EOC Independent mode joystick ---- */ -.eoc-joy-container { - position: fixed; - bottom: 80px; - right: 16px; - z-index: 100; -} -.eoc-joy-container.hidden { display: none; } -.eoc-joy-map { - border: 1px solid rgba(78, 205, 196, 0.3); - border-radius: 8px; - touch-action: none; - cursor: crosshair; -} -.eoc-joy-label { - text-align: center; - font-size: 10px; - text-transform: uppercase; - color: #4ecdc4; - opacity: 0.6; - margin-top: 4px; - letter-spacing: 1px; -} diff --git a/playground/css/b-workbench.css b/playground/css/b-workbench.css deleted file mode 100644 index db1b4ce..0000000 --- a/playground/css/b-workbench.css +++ /dev/null @@ -1,836 +0,0 @@ -/* NISPS Workbench — Design B CSS */ - -/* === Reset & Base === */ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -:root { - --bg: #0d0d0d; - --surface: #1a1a1a; - --elevated: #252525; - --border: #222; - --border-light: #333; - --text: #e0e0e0; - --text-dim: #888; - --text-muted: #555; - --accent-visual: #ff6a00; - --accent-synth: #ff8c00; - --accent: var(--accent-visual); - --danger: #ff4444; - --good: #ff8c00; - --bad: #ff4444; - --radius: 8px; - --radius-sm: 4px; - --transition: 0.2s ease; - /* Group accent colors */ - --group-motion: #00ccff; - --group-color: #ff00cc; - --group-particles: #ffcc00; - --group-forces: #ff3366; - --group-effects: #9b59b6; - /* Synth group colors */ - --group-osc: #ff6b35; - --group-shapers: #ff006e; - --group-filters: #3a86ff; - --group-comb: #06d6a0; - --group-fx: #8338ec; - --group-output: #ffd166; -} - -body { - background: var(--bg); - color: var(--text); - font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 13px; - line-height: 1.4; - overflow-x: hidden; - -webkit-font-smoothing: antialiased; -} - -body.synth-mode { - --accent: var(--accent-synth); -} - -/* === Layout === */ -.workbench { - display: flex; - flex-direction: column; - min-height: 100vh; - max-width: 1400px; - margin: 0 auto; - padding: 8px; - gap: 8px; -} - -.wb-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background: var(--surface); - border-radius: var(--radius); - border: 1px solid var(--border); -} - -.wb-header-left { - display: flex; - align-items: center; - gap: 10px; -} - -.wb-back-btn { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: var(--radius-sm); - border: 1px solid #333; - background: var(--elevated); - color: #666; - font-size: 16px; - text-decoration: none; - transition: all var(--transition); - flex-shrink: 0; -} -.wb-back-btn:hover { - border-color: var(--accent); - color: var(--accent); - background: var(--surface); -} - -.wb-title { - font-size: 16px; - font-weight: 600; - color: var(--text); - letter-spacing: 0.5px; -} - -.wb-title-accent { - color: var(--accent); - font-weight: 400; -} - -.wb-header-right { - display: flex; - align-items: center; - gap: 8px; -} - -.wb-mode-badge { - font-size: 11px; - padding: 2px 8px; - border-radius: 10px; - background: var(--accent); - color: var(--bg); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.wb-mode-badge.synth { - background: var(--accent-synth); -} - -.wb-help-btn { - width: 28px; - height: 28px; - border-radius: 50%; - border: 1px solid var(--border-light); - background: var(--elevated); - color: var(--text-dim); - font-size: 14px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} -.wb-help-btn:hover { - border-color: var(--accent); - color: var(--accent); -} - -/* === Grid === */ -.wb-grid { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto auto; - gap: 8px; - flex: 1; -} - -/* === Panel === */ -.wb-panel { - background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - display: flex; - flex-direction: column; -} - -.wb-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - border-bottom: 1px solid var(--border); - min-height: 36px; -} - -.wb-panel-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - color: var(--text-dim); -} - -/* === Canvas Panel === */ -.wb-canvas-panel { - grid-column: 1; - grid-row: 1; - min-height: 300px; -} - -.wb-canvas-wrap { - flex: 1; - position: relative; - min-height: 250px; -} - -.wb-canvas-wrap canvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: block; -} - -#synth-vis-canvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: block; - z-index: 1; -} -#synth-vis-canvas.hidden { - display: none !important; -} - -/* Presets */ -.presets { - display: flex; - gap: 4px; -} - -.preset-pill { - font-size: 10px; - padding: 2px 8px; - border-radius: 10px; - border: 1px solid var(--border-light); - background: var(--elevated); - color: var(--text-dim); - cursor: pointer; - transition: all var(--transition); - white-space: nowrap; -} -.preset-pill:hover { - border-color: var(--accent); - color: var(--accent); -} - -/* === Mapping Panel === */ -.wb-mapping-panel { - grid-column: 2; - grid-row: 1; -} - -.wb-mapping-panel.follow-active { - border-color: var(--accent); -} - -.wb-mapping-area { - display: flex; - gap: 12px; - padding: 12px; - align-items: flex-start; - justify-content: center; - flex-wrap: wrap; -} - -.wb-heatmap-wrap { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - position: relative; -} - -.wb-heatmap-wrap canvas { - border-radius: var(--radius-sm); - border: 1px solid var(--border-light); - cursor: crosshair; - touch-action: none; - width: 100%; - aspect-ratio: 1; - max-width: 280px; -} - -.wb-heatmap-label { - font-size: 10px; - color: var(--text-muted); - text-align: center; -} - -/* Follow toggle button */ -.wb-follow-toggle-btn { - font-size: 10px; - padding: 2px 10px; - border-radius: 10px; - border: 1px solid var(--border-light); - background: var(--bg); - color: var(--text-muted); - cursor: pointer; - transition: all var(--transition); - font-family: inherit; -} -.wb-follow-toggle-btn:hover { - border-color: var(--accent); - color: var(--accent); -} -.wb-follow-toggle-btn.active { - background: var(--accent); - color: var(--bg); - border-color: var(--accent); - font-weight: 600; -} - -/* Follow badge */ -.wb-follow-badge { - position: absolute; - top: 8px; - right: 8px; - font-size: 10px; - font-weight: 700; - padding: 2px 8px; - border-radius: 10px; - background: var(--accent); - color: var(--bg); - text-transform: uppercase; - letter-spacing: 1px; - pointer-events: none; - z-index: 10; -} - -/* Constellation */ -.wb-constellation-wrap { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 8px 12px; - border-top: 1px solid var(--border); -} - -.wb-constellation-wrap canvas { - flex-shrink: 0; -} - -.wb-constellation-label { - font-size: 11px; - color: var(--text-muted); - font-style: italic; -} - -/* === Params Panel === */ -.wb-params-panel { - grid-column: 1; - grid-row: 2; - max-height: 400px; - overflow-y: auto; -} - -.wb-output-toggle { - display: flex; - gap: 2px; -} - -.wb-toggle-btn { - font-size: 10px; - padding: 2px 10px; - border: 1px solid var(--border-light); - background: var(--bg); - color: var(--text-muted); - cursor: pointer; - transition: all var(--transition); -} -.wb-toggle-btn:first-child { border-radius: 10px 0 0 10px; } -.wb-toggle-btn:last-child { border-radius: 0 10px 10px 0; } -.wb-toggle-btn.active { - background: var(--accent); - color: var(--bg); - border-color: var(--accent); - font-weight: 600; -} - -.wb-param-groups { - padding: 4px; - display: flex; - flex-direction: column; - gap: 2px; -} - -/* Param group card */ -.wb-group { - border-radius: var(--radius-sm); - overflow: hidden; - border: 1px solid var(--border); - background: var(--bg); -} - -.wb-group-header { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 8px; - cursor: pointer; - user-select: none; - transition: background var(--transition); -} -.wb-group-header:hover { - background: var(--elevated); -} - -.wb-group-accent { - width: 3px; - height: 18px; - border-radius: 2px; - flex-shrink: 0; -} - -.wb-group-name { - font-size: 11px; - font-weight: 600; - color: var(--text); - flex: 1; -} - -.wb-group-sparkline { - width: 40px; - height: 12px; - flex-shrink: 0; -} - -.wb-group-chevron { - font-size: 10px; - color: var(--text-muted); - transition: transform var(--transition); -} -.wb-group.open .wb-group-chevron { - transform: rotate(90deg); -} - -.wb-group-body { - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease; -} -.wb-group.open .wb-group-body { - max-height: 500px; -} - -.wb-group-params { - padding: 4px 8px 8px; - display: flex; - flex-direction: column; - gap: 3px; -} - -/* Individual param bar */ -.wb-param { - display: flex; - align-items: center; - gap: 6px; - height: 20px; -} - -.wb-param-name { - font-size: 10px; - color: var(--text-dim); - width: 60px; - flex-shrink: 0; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.wb-param-track { - flex: 1; - height: 8px; - background: var(--surface); - border-radius: 4px; - position: relative; - cursor: pointer; - overflow: hidden; - touch-action: none; -} - -.wb-param-fill { - height: 100%; - border-radius: 4px; - transition: width 0.1s ease; - min-width: 1px; - pointer-events: none; -} - -.wb-param-value { - font-size: 10px; - color: var(--text-muted); - width: 32px; - text-align: right; - flex-shrink: 0; -} - -/* === Controls Panel === */ -.wb-controls-panel { - grid-column: 2; - grid-row: 2; - gap: 0; -} - -.wb-action-group { - display: flex; - flex-wrap: wrap; - gap: 4px; - padding: 4px 12px 8px; -} - -/* Buttons */ -.wb-btn { - padding: 6px 14px; - border: 1px solid var(--border-light); - background: var(--elevated); - color: var(--text); - font-size: 12px; - font-family: inherit; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all var(--transition); - white-space: nowrap; -} -.wb-btn:hover { border-color: var(--text-muted); } -.wb-btn:active { transform: scale(0.97); } - -.wb-btn-primary { - background: var(--accent); - color: var(--bg); - border-color: var(--accent); - font-weight: 600; -} -.wb-btn-primary:hover { filter: brightness(1.15); } - -.wb-btn-primary.audio-needs-init { - animation: audioInitPulse 2s ease-in-out infinite; -} - -@keyframes audioInitPulse { - 0%, 100% { - box-shadow: 0 0 6px rgba(255, 106, 0, 0.2); - filter: brightness(1); - } - 50% { - box-shadow: 0 0 14px rgba(255, 106, 0, 0.4); - filter: brightness(1.15); - } -} - -.wb-btn-accent { - border-color: var(--accent); - color: var(--accent); -} -.wb-btn-accent:hover { background: var(--accent); color: var(--bg); } - -.wb-btn-danger { - border-color: var(--danger); - color: var(--danger); -} -.wb-btn-danger:hover { background: var(--danger); color: var(--bg); } - -.wb-btn-good { - background: rgba(0, 200, 160, 0.12); - color: #00c8a0; - border-color: rgba(0, 200, 160, 0.4); - font-weight: 600; -} -.wb-btn-good:hover { - background: rgba(0, 200, 160, 0.25); -} -.wb-btn-bad { - background: rgba(220, 60, 20, 0.12); - color: #dc3c14; - border-color: rgba(220, 60, 20, 0.4); - font-weight: 600; -} -.wb-btn-bad:hover { - background: rgba(220, 60, 20, 0.25); -} - -.wb-btn.flash { - filter: brightness(1.8); -} - -.wb-btn-bad, -.wb-btn-good { - display: flex; - flex-direction: column; - align-items: center; - gap: 1px; - padding-top: 4px; - padding-bottom: 3px; -} - -.wb-btn-label { - font-weight: 600; -} - -.wb-key-hint { - font-size: 11px; - font-family: inherit; - color: rgba(255, 255, 255, 0.5); - text-align: center; - line-height: 1; -} - -/* Synth controls */ -.wb-synth-controls { - padding: 4px 12px 8px; - display: flex; - flex-direction: column; - gap: 6px; - border-top: 1px solid var(--border); -} - -.wb-synth-row { - display: flex; - align-items: center; - gap: 8px; -} -.wb-synth-row label { - font-size: 11px; - color: var(--text-dim); - min-width: 70px; -} -.wb-synth-row input[type="range"] { - flex: 1; - accent-color: var(--accent); - height: 4px; -} -.wb-synth-row select { - flex: 1; - background: var(--elevated); - border: 1px solid var(--border-light); - color: var(--text); - font-family: inherit; - font-size: 11px; - padding: 3px 6px; - border-radius: var(--radius-sm); -} - -.wb-synth-status { - font-size: 10px; - color: var(--text-muted); -} - -/* Loss plot */ -.wb-loss-wrap { - padding: 4px 12px; - border-top: 1px solid var(--border); -} -.wb-loss-label { - font-size: 10px; - color: var(--text-muted); - margin-bottom: 2px; - text-transform: uppercase; - letter-spacing: 0.5px; -} -.wb-loss-wrap canvas { - width: 100%; - height: 60px; - display: block; - border-radius: var(--radius-sm); -} - -/* Metrics */ -.wb-metrics { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px; - padding: 8px 12px; - border-top: 1px solid var(--border); -} - -.wb-metric { - display: flex; - justify-content: space-between; - align-items: center; -} - -.wb-metric-label { - font-size: 10px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.wb-metric-value { - font-size: 11px; - color: var(--accent); - font-weight: 600; -} - -/* === Toast === */ -.wb-toast { - position: fixed; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - padding: 8px 20px; - border-radius: 20px; - background: var(--accent); - color: var(--bg); - font-size: 12px; - font-weight: 600; - font-family: inherit; - letter-spacing: 0.5px; - z-index: 2000; - pointer-events: none; - opacity: 1; - transition: opacity 0.5s ease; -} -.wb-toast.fade-out { - opacity: 0; -} - -/* === Help Overlay === */ -.wb-help-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.75); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 20px; -} - -.wb-help-content { - background: var(--surface); - border: 1px solid var(--border-light); - border-radius: var(--radius); - max-width: 520px; - max-height: 80vh; - overflow-y: auto; - padding: 24px; - color: var(--text); -} -.wb-help-content h2 { - font-size: 18px; - margin-bottom: 12px; - color: var(--accent); -} -.wb-help-content h3 { - font-size: 14px; - margin: 16px 0 6px; - color: var(--text); -} -.wb-help-content p, .wb-help-content ol { - font-size: 13px; - color: var(--text-dim); - margin-bottom: 8px; -} -.wb-help-content ol { - padding-left: 20px; -} -.wb-help-content li { - margin-bottom: 4px; -} - -/* === Utility === */ -.hidden { - display: none !important; -} - -/* Scrollbar */ -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: var(--bg); } -::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } - -/* === Mobile: single column === */ -@media (max-width: 767px) { - .workbench { - padding: 4px; - gap: 4px; - } - - .wb-grid { - grid-template-columns: 1fr; - grid-template-rows: auto auto auto auto; - } - - .wb-canvas-panel { grid-column: 1; grid-row: 1; min-height: 220px; } - .wb-mapping-panel { grid-column: 1; grid-row: 2; } - .wb-params-panel { grid-column: 1; grid-row: 3; max-height: 300px; } - .wb-controls-panel { grid-column: 1; grid-row: 4; } - - .wb-mapping-area { - justify-content: center; - gap: 8px; - padding: 8px; - } - - #mapping-canvas { - max-width: 220px !important; - } - - .wb-title { font-size: 14px; } - .presets { display: none; } - - /* Collapsible sections on mobile */ - .wb-panel.collapsible > .wb-panel-header { - cursor: pointer; - } - .wb-panel.collapsible.collapsed > *:not(.wb-panel-header) { - max-height: 0; - overflow: hidden; - padding: 0; - border: none; - } -} - -/* === Tablet and up === */ -@media (min-width: 768px) { - .wb-canvas-panel { - min-height: 380px; - } - - .wb-mapping-area { - gap: 16px; - padding: 16px; - } -} - -/* Large screens */ -@media (min-width: 1100px) { - .wb-canvas-panel { - min-height: 450px; - } -} diff --git a/playground/css/c-journey.css b/playground/css/c-journey.css deleted file mode 100644 index 0917fea..0000000 --- a/playground/css/c-journey.css +++ /dev/null @@ -1,971 +0,0 @@ -/* NISPS Journey — Design C */ -/* Phases that dissolve: Explore -> Teach -> Perform */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} - -:root { - --bg: #0d0d0d; - --glass-bg: rgba(13, 13, 13, 0.6); - --glass-border: rgba(255, 255, 255, 0.08); - --glass-blur: 16px; - --accent: #ff6a00; - --accent-dim: rgba(255, 106, 0, 0.2); - --accent-glow: rgba(255, 106, 0, 0.4); - --danger: #ff4466; - --text: #e0e0e0; - --text-dim: rgba(255, 255, 255, 0.45); - --text-muted: rgba(255, 255, 255, 0.25); - --radius: 12px; - --radius-sm: 8px; - --radius-pill: 6px; - --transition: 400ms cubic-bezier(0.22, 1, 0.36, 1); - --transition-fast: 200ms ease-out; - --safe-bottom: env(safe-area-inset-bottom, 0px); -} - -html, body { - width: 100%; - height: 100%; - overflow: hidden; - background: var(--bg); - color: var(--text); - font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 13px; - touch-action: none; - user-select: none; - -webkit-user-select: none; -} - -/* ============================================================ - Canvas - ============================================================ */ -#vis-canvas { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: block; - z-index: 0; - transition: height var(--transition); -} - -.app.phase-teach #vis-canvas, -.app.phase-teach #synth-vis-canvas { - height: 40vh; -} - -/* Synth visualizer canvas */ -#synth-vis-canvas { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: block; - z-index: 0; - transition: height var(--transition); -} - -/* ============================================================ - Phase dots (top-center) - ============================================================ */ -.phase-dots { - position: fixed; - top: 12px; - left: 50%; - transform: translateX(-50%); - z-index: 100; - display: flex; - gap: 10px; - padding: 6px 14px; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - transition: opacity var(--transition); -} - -.phase-dot { - width: 10px; - height: 10px; - border-radius: 50%; - border: 1.5px solid var(--text-dim); - background: transparent; - cursor: pointer; - padding: 0; - transition: all var(--transition-fast); -} - -.phase-dot.active { - background: var(--accent); - border-color: var(--accent); - box-shadow: 0 0 8px var(--accent-glow); -} - -.phase-dot:hover { - border-color: var(--text); -} - -/* ============================================================ - Heatmap strip - ============================================================ */ -.heatmap-strip { - position: fixed; - top: 40px; - left: 50%; - transform: translateX(-50%); - z-index: 90; - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - transition: opacity var(--transition); - cursor: pointer; -} - -.heatmap-cells { - display: flex; - gap: 1px; - height: 16px; -} - -.heatmap-cell { - flex: 0 0 auto; - width: 14px; - height: 16px; - background: #1a1a1a; - border-radius: 2px; - overflow: hidden; - position: relative; -} - -.heatmap-bar { - position: absolute; - left: 0; - top: 0; - height: 100%; - width: 50%; - transition: width 0.1s ease-out; - border-radius: 2px; -} - -.heatmap-strip.wide .heatmap-cell { - width: 3px; - height: 16px; -} - -.heatmap-count { - font-size: 11px; - color: var(--text-dim); - white-space: nowrap; - min-width: 16px; - text-align: right; -} - -.heatmap-tooltip { - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - margin-top: 6px; - padding: 4px 10px; - background: rgba(0, 0, 0, 0.85); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - font-size: 11px; - color: var(--text); - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity var(--transition-fast); -} - -.heatmap-tooltip.show { - opacity: 1; -} - -/* ============================================================ - Mode pill (top-left) - ============================================================ */ -.mode-pill { - position: fixed; - top: 12px; - left: 56px; - z-index: 100; - display: flex; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - overflow: hidden; - transition: opacity var(--transition); -} - -.mode-opt { - padding: 5px 14px; - font-size: 12px; - color: var(--text-dim); - background: transparent; - border: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.mode-opt.active { - color: var(--accent); - background: var(--accent-dim); -} - -/* ============================================================ - Explore phase prompts - ============================================================ */ -.explore-prompt { - position: fixed; - top: 35%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 50; - text-align: center; - pointer-events: none; - transition: opacity var(--transition); -} - -.prompt-text { - font-size: 18px; - color: var(--text-dim); - animation: fadeSlideIn 800ms ease-out forwards; - opacity: 0; - animation-delay: 1s; -} - -@keyframes fadeSlideIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.explore-ctas { - position: fixed; - bottom: calc(180px + var(--safe-bottom)); - left: 50%; - transform: translateX(-50%); - z-index: 60; - display: flex; - gap: 12px; - transition: opacity var(--transition); -} - -.cta-pill { - padding: 10px 22px; - font-size: 14px; - color: var(--text); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.cta-pill:hover, .cta-pill:active { - border-color: var(--text-dim); -} - -.cta-pill.primary { - background: var(--accent-dim); - border-color: var(--accent); - color: var(--accent); -} - -.cta-pill.primary:hover, .cta-pill.primary:active { - background: rgba(255, 106, 0, 0.3); -} - -/* ============================================================ - Joystick - ============================================================ */ -.joystick-area { - position: fixed; - z-index: 85; - width: 150px; - height: 150px; - transition: all var(--transition); -} - -/* Phase 1: bottom-center */ -.app.phase-explore .joystick-area { - bottom: calc(24px + var(--safe-bottom)); - left: 50%; - transform: translateX(-50%); - width: 150px; - height: 150px; - opacity: 1; -} - -/* Phase 2: left side, smaller */ -.app.phase-teach .joystick-area { - bottom: auto; - top: calc(40vh + 16px); - left: 16px; - transform: none; - width: 120px; - height: 120px; - opacity: 1; -} - -/* Phase 3: ghost, bottom-center, smaller */ -.app.phase-perform .joystick-area { - bottom: calc(60px + var(--safe-bottom)); - left: 50%; - transform: translateX(-50%); - width: 120px; - height: 120px; - opacity: 0.55; -} - -.joystick-canvas { - width: 100%; - height: 100%; - border-radius: 50%; - touch-action: none; -} - -/* Noise ring for RL mode */ -.noise-ring { - position: absolute; - inset: -8px; - border-radius: 50%; - border: 2px solid var(--accent); - opacity: 0; - pointer-events: none; - transition: all var(--transition-fast); -} - -/* ============================================================ - Teach panel - ============================================================ */ -.teach-panel { - position: fixed; - top: 40vh; - left: 0; - right: 0; - bottom: 0; - z-index: 80; - background: var(--bg); - border-top: 1px solid var(--glass-border); - overflow-y: auto; - -webkit-overflow-scrolling: touch; - padding: 0 16px 24px; - opacity: 0; - pointer-events: none; - transform: translateY(20px); - transition: opacity var(--transition), transform var(--transition); -} - -.app.phase-teach .teach-panel { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -/* Teach content sections */ -.teach-content { - transition: opacity var(--transition-fast); -} - -.teach-layout { - display: flex; - flex-direction: column; - gap: 12px; - padding-left: 136px; /* room for joystick on left */ -} - -/* Preset grid */ -.preset-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; -} - -.preset-thumb { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 6px; - background: rgba(255, 255, 255, 0.03); - border: 1.5px solid var(--glass-border); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all var(--transition-fast); - color: var(--text-dim); - font-size: 11px; -} - -.preset-thumb.selected { - border-color: var(--accent); - background: var(--accent-dim); - color: var(--accent); -} - -.preset-thumb:hover { - border-color: var(--text-dim); -} - -.preset-canvas { - width: 56px; - height: 56px; - border-radius: 4px; - background: var(--bg); -} - -/* Teach actions */ -.teach-actions { - display: flex; - gap: 8px; -} - -/* Minimap */ -.minimap { - width: 120px; - height: 120px; - border-radius: var(--radius-sm); - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--glass-border); - align-self: center; -} - -/* Fine-tune */ -.fine-tune { - margin-top: 4px; -} - -.fine-tune summary { - font-size: 12px; - color: var(--text-dim); - cursor: pointer; - padding: 6px 0; -} - -.param-sliders { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4px 12px; - max-height: 200px; - overflow-y: auto; -} - -.param-slider-row { - display: flex; - align-items: center; - gap: 4px; -} - -.param-slider-row label { - font-size: 10px; - color: var(--text-dim); - width: 50px; - flex-shrink: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.param-slider-row input[type="range"] { - flex: 1; - height: 4px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; - outline: none; -} - -.param-slider-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--accent); - cursor: pointer; -} - -/* RL buttons (now part of unified teach panel) */ -.rl-buttons { - display: flex; - justify-content: center; - gap: 48px; - margin-top: 16px; -} - -.rl-btn { - width: 64px; - height: 64px; - border-radius: 50%; - border: 2px solid var(--glass-border); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - cursor: pointer; - transition: all var(--transition-fast); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding-top: 2px; - gap: 0; -} - -.rl-icon { - font-size: 28px; - line-height: 1; -} - -.rl-btn.up { - border-color: rgba(0, 200, 160, 0.4); - color: #00c8a0; - background: rgba(0, 200, 160, 0.08); -} - -.rl-btn.up:hover, .rl-btn.up:active { - background: rgba(0, 200, 160, 0.2); - box-shadow: 0 0 20px rgba(0, 200, 160, 0.3); -} - -.rl-btn.down { - border-color: rgba(220, 60, 20, 0.4); - color: #dc3c14; - background: rgba(220, 60, 20, 0.08); -} - -.rl-btn.down:hover, .rl-btn.down:active { - background: rgba(220, 60, 20, 0.2); - box-shadow: 0 0 20px rgba(220, 60, 20, 0.3); -} - -/* Synth section */ -.synth-section { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--glass-border); - display: flex; - flex-direction: column; - gap: 8px; -} - -.synth-row { - display: flex; - align-items: center; - gap: 10px; -} - -.synth-row label { - font-size: 12px; - color: var(--text-dim); - min-width: 80px; -} - -.synth-row input[type="range"] { - flex: 1; - height: 4px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.08); - border-radius: 2px; - outline: none; -} - -.synth-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 14px; - height: 14px; - border-radius: 50%; - background: var(--accent); - cursor: pointer; -} - -.synth-row select { - flex: 1; - padding: 4px 8px; - font-size: 12px; - color: var(--text); - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - outline: none; -} - -.synth-status { - font-size: 12px; - color: var(--text-dim); -} - -/* Teach status bar */ -.teach-status { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 0; - font-size: 12px; - color: var(--text-dim); - margin-top: 8px; - border-top: 1px solid var(--glass-border); -} - -/* Done button */ -.done-btn { - display: block; - width: 100%; - padding: 12px; - margin-top: 8px; - font-size: 14px; - color: var(--bg); - background: var(--accent); - border: none; - border-radius: var(--radius-pill); - cursor: pointer; - transition: all var(--transition-fast); -} - -.done-btn:hover, .done-btn:active { - background: #ff8833; -} - -/* ============================================================ - Buttons (shared) - ============================================================ */ -.action-btn { - padding: 8px 16px; - font-size: 13px; - color: var(--text); - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.action-btn:hover, .action-btn:active { - background: rgba(255, 255, 255, 0.1); - border-color: var(--text-dim); -} - -.action-btn.primary { - background: var(--accent-dim); - border-color: var(--accent); - color: var(--accent); -} - -.action-btn.primary:hover { - background: rgba(255, 106, 0, 0.3); -} - -.action-btn.accent { - background: rgba(255, 106, 0, 0.12); - border-color: rgba(255, 106, 0, 0.3); - color: var(--accent); -} - -.action-btn.accent:hover { - background: rgba(255, 106, 0, 0.2); -} - -.action-btn.accent.audio-needs-init, -.audio-btn.audio-needs-init { - animation: audioInitPulse 2s ease-in-out infinite; -} - -@keyframes audioInitPulse { - 0%, 100% { - border-color: rgba(255, 106, 0, 0.3); - box-shadow: 0 0 6px rgba(255, 106, 0, 0.15); - } - 50% { - border-color: rgba(255, 106, 0, 0.6); - box-shadow: 0 0 14px rgba(255, 106, 0, 0.3); - } -} - -.action-btn.dim { - color: var(--text-dim); - border-color: transparent; - background: transparent; -} - -.action-btn.small { - padding: 4px 10px; - font-size: 11px; -} - -/* Train button pulse animation */ -.action-btn.accent.pulse { - animation: trainPulse 1.5s ease-in-out infinite; -} - -@keyframes trainPulse { - 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); } - 50% { box-shadow: 0 0 12px 4px var(--accent-glow); } -} - -/* ============================================================ - Edit pill (Phase 3) - ============================================================ */ -.edit-pill-wrap { - position: fixed; - bottom: calc(16px + var(--safe-bottom)); - left: 50%; - transform: translateX(-50%); - z-index: 80; - opacity: 0; - pointer-events: none; - transition: opacity var(--transition); -} - -.app.phase-perform .edit-pill-wrap { - opacity: 1; - pointer-events: auto; -} - -.edit-pill { - padding: 10px 28px; - font-size: 13px; - color: var(--text); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - cursor: pointer; - transition: all var(--transition-fast); -} - -.edit-pill:hover { - border-color: var(--text-dim); -} - -/* Audio control (Phase 3, synth mode) */ -.audio-control { - position: fixed; - top: 12px; - right: 12px; - z-index: 100; - display: flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - transition: opacity var(--transition); -} - -.audio-btn { - width: 28px; - height: 28px; - border-radius: 50%; - border: 1px solid var(--glass-border); - background: transparent; - color: var(--text); - font-size: 12px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} - -.audio-vol { - width: 60px; - height: 3px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.12); - border-radius: 2px; - outline: none; -} - -.audio-vol::-webkit-slider-thumb { - -webkit-appearance: none; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent); - cursor: pointer; -} - -/* ============================================================ - Phase visibility rules - ============================================================ */ - -/* Explore: show prompts, CTAs; hide teach panel, edit pill */ -.app.phase-explore .teach-panel, -.app.phase-explore .edit-pill-wrap, -.app.phase-explore .audio-control { opacity: 0; pointer-events: none; } - -/* Teach: hide prompts, CTAs, edit pill */ -.app.phase-teach .explore-prompt, -.app.phase-teach .explore-ctas, -.app.phase-teach .edit-pill-wrap, -.app.phase-teach .audio-control { opacity: 0; pointer-events: none; } - -/* Perform: hide prompts, CTAs, teach panel */ -.app.phase-perform .explore-prompt, -.app.phase-perform .explore-ctas, -.app.phase-perform .teach-panel { opacity: 0; pointer-events: none; } - -/* Perform phase: auto-fade idle elements */ -.app.phase-perform.idle .edit-pill-wrap { - opacity: 0.1; -} -.app.phase-perform.idle .heatmap-strip { - opacity: 0.15; -} -.app.phase-perform.idle .phase-dots { - opacity: 0.15; -} -.app.phase-perform.idle .mode-pill { - opacity: 0.15; -} - -/* ============================================================ - Back button - ============================================================ */ -.back-btn { - position: fixed; - top: 12px; - left: 12px; - z-index: 110; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: 6px; - color: var(--text); - text-decoration: none; - font-size: 18px; - line-height: 1; - transition: all var(--transition-fast); -} - -.back-btn:hover { - border-color: var(--accent); - color: var(--accent); -} - -/* ============================================================ - RL key hints - ============================================================ */ -.key-hint { - font-family: inherit; - font-size: 12px; - color: rgba(255, 255, 255, 0.5); - text-align: center; - line-height: 1; - margin-top: -1px; -} - -/* ============================================================ - Follow mode - ============================================================ */ -.follow-indicator { - position: absolute; - top: -20px; - left: 50%; - transform: translateX(-50%); - font-size: 10px; - font-weight: 600; - letter-spacing: 1px; - color: var(--accent); - text-shadow: 0 0 8px var(--accent-glow); - pointer-events: none; -} - -.follow-pill { - position: absolute; - bottom: -24px; - left: 50%; - transform: translateX(-50%); - padding: 2px 10px; - font-size: 10px; - color: var(--text-dim); - background: var(--glass-bg); - backdrop-filter: blur(var(--glass-blur)); - -webkit-backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--glass-border); - border-radius: var(--radius-pill); - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.follow-pill:hover { - border-color: var(--text-dim); -} - -.follow-pill.active { - color: var(--accent); - border-color: var(--accent); - background: var(--accent-dim); -} - -/* ============================================================ - Hidden utility - ============================================================ */ -.hidden { - display: none !important; -} - -/* ============================================================ - Responsive - ============================================================ */ -@media (min-width: 600px) { - .teach-layout { - padding-left: 140px; - } - - .preset-grid { - grid-template-columns: repeat(3, 1fr); - max-width: 400px; - } - - .rl-buttons { - gap: 60px; - } -} - -@media (max-width: 380px) { - .preset-canvas { - width: 44px; - height: 44px; - } - - .teach-layout { - padding-left: 110px; - } - - .cta-pill { - padding: 8px 14px; - font-size: 13px; - } -} diff --git a/playground/css/style.css b/playground/css/style.css deleted file mode 100644 index a2e36f3..0000000 --- a/playground/css/style.css +++ /dev/null @@ -1,750 +0,0 @@ -/* NISPS Playground - Dark theme, mobile-first */ - -:root { - --bg: #0d0d0d; - --bg-surface: #1a1a1a; - --bg-elevated: #252525; - --text: #ccc; - --text-dim: #666; - --accent: #ff6a00; - --accent-dim: #cc5500; - --danger: #ff3366; - --good: #ff8c00; - --bad: #ff4444; - --synth-accent: #ff8c00; -} - -* { box-sizing: border-box; margin: 0; padding: 0; } - -html, body { - background: var(--bg); - color: var(--text); - font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 13px; - height: 100%; - overflow: hidden; - touch-action: manipulation; - -webkit-user-select: none; - user-select: none; -} - -/* --- Layout --- */ -.app { - display: grid; - grid-template-rows: auto 1fr auto auto auto; - height: 100vh; - height: 100dvh; - max-width: 600px; - margin: 0 auto; - position: relative; -} - -.header, -.visual-container, -.param-container, -.controls-area, -.presets, -.expand-visual-btn { - transition: all 0.2s ease; -} - -/* Header */ -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - border-bottom: 1px solid #222; -} - -.header h1 { - font-size: 15px; - font-weight: 600; - letter-spacing: 0.5px; - color: var(--accent); -} - -.header-right { - display: flex; - align-items: center; - gap: 8px; -} - -.header button { - background: none; - border: 1px solid #333; - color: var(--text-dim); - width: 28px; - height: 28px; - border-radius: 50%; - cursor: pointer; - font-size: 14px; -} - -.mode-badge { - font-size: 10px; - padding: 3px 8px; - border-radius: 10px; - background: rgba(0, 255, 136, 0.12); - border: 1px solid var(--accent-dim); - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 600; -} - -.mode-badge.synth { - background: rgba(255, 107, 53, 0.12); - border-color: var(--synth-accent); - color: var(--synth-accent); -} - -/* ==================== Side Panel ==================== */ -.side-panel-toggle { - position: fixed; - left: 0; - top: 50%; - transform: translateY(-50%); - z-index: 50; - background: var(--bg-surface); - border: 1px solid #333; - border-left: none; - border-radius: 0 8px 8px 0; - color: var(--text-dim); - font-size: 16px; - padding: 12px 6px; - cursor: pointer; - line-height: 1; - transition: background 0.15s; -} - -.side-panel-toggle:hover { - background: var(--bg-elevated); - color: var(--text); -} - -.side-panel { - position: fixed; - left: 0; - top: 0; - bottom: 0; - width: 260px; - background: var(--bg-surface); - border-right: 1px solid #333; - z-index: 60; - transform: translateX(-100%); - transition: transform 0.25s ease; - display: flex; - flex-direction: column; - overflow-y: auto; -} - -.side-panel.open { - transform: translateX(0); -} - -.side-panel-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 55; -} - -.side-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px 10px; - border-bottom: 1px solid #222; -} - -.side-panel-title { - font-size: 14px; - font-weight: 600; - color: var(--text); - letter-spacing: 0.5px; -} - -.side-panel-close { - background: none; - border: none; - color: var(--text-dim); - font-size: 22px; - cursor: pointer; - line-height: 1; - padding: 0 4px; -} - -.side-panel-tabs { - display: flex; - gap: 0; - padding: 12px 16px; - border-bottom: 1px solid #222; -} - -.sp-tab { - flex: 1; - padding: 8px 0; - background: var(--bg-elevated); - border: 1px solid #333; - color: var(--text-dim); - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all 0.15s; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.sp-tab:first-child { - border-radius: 6px 0 0 6px; -} - -.sp-tab:last-child { - border-radius: 0 6px 6px 0; - border-left: none; -} - -.sp-tab.active[data-mode="visual"] { - background: rgba(0, 255, 136, 0.12); - border-color: var(--accent-dim); - color: var(--accent); -} - -.sp-tab.active[data-mode="synth"] { - background: rgba(255, 107, 53, 0.12); - border-color: var(--synth-accent); - color: var(--synth-accent); -} - -.sp-hint { - padding: 16px; - font-size: 12px; - color: var(--text-dim); - line-height: 1.5; -} - -/* Synth controls */ -.synth-controls { - padding: 12px 16px; - display: flex; - flex-direction: column; - gap: 14px; -} - -.synth-section { - display: flex; - flex-direction: column; - gap: 6px; -} - -.synth-label { - font-size: 11px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 600; -} - -.synth-label span { - color: var(--synth-accent); - font-weight: 700; -} - -.synth-slider { - -webkit-appearance: none; - appearance: none; - width: 100%; - height: 6px; - background: #333; - border-radius: 3px; - outline: none; -} - -.synth-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - background: var(--synth-accent); - border-radius: 50%; - cursor: pointer; -} - -.synth-slider::-moz-range-thumb { - width: 16px; - height: 16px; - background: var(--synth-accent); - border-radius: 50%; - cursor: pointer; - border: none; -} - -.synth-select { - background: var(--bg-elevated); - border: 1px solid #333; - color: var(--text); - padding: 6px 8px; - border-radius: 6px; - font-size: 12px; - cursor: pointer; -} - -.synth-start-btn { - font-size: 12px; -} - -.synth-arp-btn { - font-size: 12px; -} - -.synth-arp-btn.playing { - background: rgba(255, 107, 53, 0.15); - border-color: var(--synth-accent); - color: var(--synth-accent); -} - -.synth-status { - font-size: 10px; - color: var(--text-dim); - font-family: monospace; -} - -/* Visual canvas */ -.visual-container { - position: relative; - min-height: 0; -} - -.expand-visual-btn { - position: absolute; - top: 8px; - right: 8px; - z-index: 2; - background: rgba(26, 26, 26, 0.82); - border: 1px solid #333; - color: #9f9f9f; - border-radius: 12px; - font-size: 11px; - line-height: 1; - padding: 6px 10px; - cursor: pointer; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} - -.expand-visual-btn:active, -.expand-visual-btn[aria-pressed="true"] { - border-color: var(--accent-dim); - color: var(--accent); - background: rgba(0, 255, 136, 0.15); -} - -.visual-container canvas { - width: 100%; - height: 100%; - display: block; -} - -/* Preset pills */ -.presets { - position: absolute; - top: 8px; - left: 8px; - display: flex; - gap: 6px; - z-index: 2; -} - -.preset-pill { - background: rgba(26, 26, 26, 0.8); - border: 1px solid #333; - color: var(--text-dim); - padding: 4px 10px; - border-radius: 12px; - font-size: 11px; - cursor: pointer; - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} - -.preset-pill:active { - background: rgba(0, 255, 136, 0.15); - border-color: var(--accent-dim); - color: var(--accent); -} - -/* Parameter display */ -.param-container { - padding: 6px 12px; - background: var(--bg-surface); - border-top: 1px solid #222; -} - -.param-row { - display: flex; - align-items: center; - gap: 8px; - height: 22px; -} - -.param-label { - width: 42px; - font-size: 10px; - color: var(--text-dim); - text-transform: uppercase; - letter-spacing: 0.5px; - flex-shrink: 0; -} - -.param-container.synth-mode { - max-height: 50vh; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #444 #1a1a1a; -} - -.param-container.synth-mode .param-row { - height: 16px; -} - -.param-container.synth-mode .param-label { - width: 82px; - font-size: 9px; - color: #886644; -} - -.param-container.synth-mode .param-track { - height: 6px; -} - -.param-container.synth-mode .param-value { - font-size: 8px; - min-width: 24px; -} - -.param-container.synth-mode.draggable .param-track { - height: 10px; -} - -.param-track { - flex: 1; - height: 8px; - background: #222; - border-radius: 4px; - overflow: hidden; - position: relative; -} - -.draggable .param-track { - cursor: ew-resize; - height: 14px; - border: 1px solid #333; -} - -.draggable .param-track:active { - border-color: var(--accent-dim); -} - -.param-fill { - height: 100%; - border-radius: 4px; - transition: width 0.05s ease-out; -} - -.param-value { - width: 32px; - font-size: 10px; - color: var(--text-dim); - text-align: right; - font-family: monospace; - flex-shrink: 0; -} - -/* Controls area */ -.controls-area { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 10px 12px; - background: var(--bg-surface); - border-top: 1px solid #222; -} - -.app.expanded { - grid-template-rows: auto minmax(0, 1fr) 58px 102px 0; -} - -.app.expanded .header { - padding-top: 6px; - padding-bottom: 6px; -} - -.app.expanded .header h1 { - font-size: 13px; - color: #9a9a9a; -} - -.app.expanded .presets { - top: 6px; - left: 6px; - gap: 4px; -} - -.app.expanded .preset-pill { - font-size: 10px; - padding: 3px 7px; - color: #868686; -} - -.app.expanded .param-container { - padding: 4px 10px; - overflow: hidden; - opacity: 0.78; -} - -.app.expanded .param-row { - height: 17px; -} - -.app.expanded .param-track { - height: 5px; -} - -.app.expanded .draggable .param-track { - height: 9px; -} - -.app.expanded .param-label, -.app.expanded .param-value { - font-size: 9px; -} - -.app.expanded .controls-area { - padding: 6px 10px; - gap: 8px; - overflow: hidden; - opacity: 0.82; -} - -.app.expanded #joystick-container { - width: 94px; - height: 94px; - overflow: hidden; -} - -.app.expanded #joystick-container canvas { - transform: scale(0.58); - transform-origin: top left; -} - -.app.expanded .controls-actions { - margin-bottom: 4px; -} - -.app.expanded .btn { - font-size: 10px; - padding: 5px 10px; - border-radius: 6px; -} - -.app.expanded .btn-good, -.app.expanded .btn-bad { - font-size: 14px; - padding: 5px 12px; -} - -.app.expanded .controls-status, -.app.expanded .metrics-grid, -.app.expanded .loss-plot-wrap { - display: none; -} - -#joystick-container { - flex-shrink: 0; -} - -#controls-container { - flex: 1; - min-width: 0; -} - -.controls-actions { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; -} - -.btn { - padding: 8px 14px; - background: var(--bg-elevated); - border: 1px solid #333; - color: var(--text); - font-size: 12px; - border-radius: 8px; - cursor: pointer; - transition: all 0.1s; - white-space: nowrap; -} - -.btn:active { - transform: scale(0.96); -} - -.btn-primary { - background: rgba(0, 255, 136, 0.12); - border-color: var(--accent-dim); - color: var(--accent); -} - -.btn-primary:active { - background: rgba(0, 255, 136, 0.25); -} - -.btn-danger { - color: var(--danger); - border-color: #552233; -} - -.btn-danger:active { - background: rgba(255, 51, 102, 0.15); -} - -.btn-good { - background: rgba(0, 255, 136, 0.12); - border-color: var(--accent-dim); - color: var(--good); - font-size: 18px; - padding: 8px 20px; -} - -.btn-good:active { - background: rgba(0, 255, 136, 0.3); -} - -.btn-bad { - background: rgba(255, 102, 68, 0.1); - border-color: #553322; - color: var(--bad); - font-size: 18px; - padding: 8px 20px; -} - -.btn-bad:active { - background: rgba(255, 102, 68, 0.25); -} - -.flash { - animation: flash-anim 0.2s ease-out; -} - -@keyframes flash-anim { - 0% { box-shadow: 0 0 12px var(--accent); } - 100% { box-shadow: none; } -} - -.controls-status { - display: flex; - gap: 12px; - font-size: 11px; - color: var(--text-dim); - font-family: monospace; - margin-bottom: 8px; -} - -.metrics-grid { - display: grid; - grid-template-columns: auto 1fr; - gap: 3px 10px; - font-size: 10px; - margin-bottom: 8px; - font-family: monospace; -} - -.metric-label { - color: #707070; -} - -.metric-value { - color: #b5b5b5; - text-align: right; -} - -.loss-plot-wrap { - width: 100%; - border: 1px solid #2a2a2a; - border-radius: 8px; - overflow: hidden; -} - -#loss-plot { - width: 100%; - height: 86px; - display: block; -} - -.hidden { display: none !important; } - -/* Help overlay */ -.help-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.85); - z-index: 100; - display: flex; - align-items: center; - justify-content: center; - padding: 20px; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); -} - -.help-content { - background: var(--bg-surface); - border: 1px solid #333; - border-radius: 12px; - padding: 20px; - max-width: 360px; - max-height: 80vh; - overflow-y: auto; - font-size: 13px; - line-height: 1.5; -} - -.help-content h2 { - color: var(--accent); - font-size: 16px; - margin-bottom: 12px; -} - -.help-content h3 { - color: var(--text); - font-size: 13px; - margin-top: 14px; - margin-bottom: 4px; -} - -.help-content p, .help-content li { - color: var(--text-dim); - margin-bottom: 6px; -} - -.help-content ol { - padding-left: 18px; -} - -/* Responsive: wider screens (foldable inner, tablet) */ -@media (min-width: 500px) { - .app { - max-width: 800px; - } - - .controls-area { - padding: 12px 16px; - } - - .param-container { - padding: 8px 16px; - } -} diff --git a/playground/designs.html b/playground/designs.html deleted file mode 100644 index 02a9d7f..0000000 --- a/playground/designs.html +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - NISPS Playground - - - - - - diff --git a/playground/devlog/2026-03-21-ui-redesign-explorations.md b/playground/devlog/2026-03-21-ui-redesign-explorations.md deleted file mode 100644 index 7db6f53..0000000 --- a/playground/devlog/2026-03-21-ui-redesign-explorations.md +++ /dev/null @@ -1,103 +0,0 @@ -# Playground UI Redesign Explorations - -**Date**: 2026-03-21 → 2026-03-22 -**Status**: Three designs built and iterated through 4 rounds of feedback. Several refinements still open. - -## Context - -The existing playground UI (`index.html`) is a single-column mobile-first layout: header, canvas, 20 parameter bars, joystick + controls. It works but has several tensions: - -- **Individual param bars dominate the screen** and are only interactive in Examples mode -- **Two orthogonal mode axes** (learning mode: examples/RL, output mode: visual/synth) live in different places with different UI patterns -- The synth side panel feels bolted on -- The "teach then perform" arc isn't surfaced — new users don't know what to do first -- Most importantly: **manually setting sliders contradicts the core thesis** — NISPS exists precisely because you *shouldn't* need to understand individual parameters - -With the C15 synth mode now at **126 parameters**, the slider approach is impossible, making the ML mapping the only viable interface. - -## Design Thesis - -The three designs explore different answers to: **if users shouldn't set params directly, what DO they interact with?** - -## Design A: "Immersive" - -**File**: `a-immersive.html` | **Philosophy**: The canvas IS the app - -The flow field fills the entire viewport. Everything else floats on top as translucent glassmorphism overlays: - -- **Floating circular joystick** with integrated training-example minimap (merged into one control) -- **Heatmap strip** at top — horizontal fill bars (like a tiny equalizer) showing output values at a glance. Hidden in synth mode since the full-screen synth visualizer serves this purpose. -- **Bottom sheet** with chevron toggle (collapsed/expanded), mode toggles always visible in a sticky floating bar -- **RL mode is default** — floating thumbs up/down buttons with key hints (1/2) built in -- **Follow mode** for trackpad-friendly exploration without holding the mouse -- **Synth mode**: full-screen parameter landscape (126 vertical bars grouped by synthesis section), interactive/draggable in examples mode, with a quick play button + hover drawer next to the back button - -**Strengths**: Feels like an instrument. Canvas gets maximum space. The synth visualizer conveys the full 126-param scale. -**Open issues**: See bd issue for remaining refinements (synth bar layout, slider interactivity, default mode). - -## Design B: "Workbench" - -**File**: `b-workbench.html` | **Philosophy**: Show the mapping, not the params - -A CSS grid dashboard (2-column on desktop, single-column with collapsible sections on mobile): - -- **Mapping heatmap** (the key innovation): a 2D canvas sampling a 20×20 grid through the network, showing the learned function as a color field. Training examples are visible dots. Click/drag anywhere to control position — the mapping IS the joystick (merged). -- **Parameter constellation**: dots in a circle, sized by value, with a count label ("20 parameters" / "126 synth parameters") -- **Semantic param groups**: collapsible cards with sparkline headers. 5 groups for visual mode, 17 groups for synth mode (covering all 126 params by synthesis section) -- **Synth mode**: replaces the flow field canvas with a parameter landscape visualization (126 grouped bars) -- **Softened heatmap colors** with grid overlay for readability - -**Strengths**: The mapping heatmap makes the learned function tangible. Param groups reduce cognitive load. -**Open issues**: Visual pill toggle reliability, heatmap color tuning. - -## Design C: "Journey" - -**File**: `c-journey.html` | **Philosophy**: The UI teaches you, then disappears - -Three phases that transition based on user actions: - -1. **Explore** (start): fullscreen canvas + floating joystick + gentle prompts. Minimal chrome. -2. **Teach** (active training): canvas shrinks to ~40%. Teaching interface with mode tabs (Examples/RL). Preset snapshot grid. Minimap of examples. Collapsible raw sliders. -3. **Perform** (post-training): canvas expands back to fullscreen. Controls fade to near-invisible after 5s inactivity. - -- **RL mode is default** teach mode with keyboard shortcuts (1/2) working in all phases -- **Heatmap strip** with horizontal fill bars, scales from 20 to 126 params -- **Synth mode**: parameter landscape visualization replacing the flow field -- **Follow mode** with double-click toggle and pill button - -**Strengths**: Maps perfectly to the real user arc. Phase 3 auto-dissolve is genuinely novel. -**Open issues**: RL mode joystick visibility, fine-tune slider style consistency. - -## Shared Architecture - -All three designs share unchanged modules: -- `js/nisps/` — MLP, IML, Dataset, Layer, Node (the ML engine) -- `js/ui/visualizer.js` — FlowFieldVisualizer (Canvas2D particle system) -- `js/synth/` — C15Bridge (WASM synth), Arpeggiator, param-map (126 params) - -Each design has its own HTML + CSS + app.js. The `designs.html` homepage links to all three with live iframe previews. - -### Common features across all designs: -- **126-output MLP** with `[32, 48, 64]` hidden layers -- **Visual mode**: first 20 outputs → flow field particle system -- **Synth mode**: all 126 outputs → SynthVisualizer (grouped bar chart) + C15 WASM engine -- **`?tame=0.7`** URL parameter for safe-range constraining of synth params -- **RL as default** learning mode with keyboard shortcuts (1=negative, 2=positive) -- **Follow mode** for trackpad-friendly exploration -- **localStorage persistence** (random on first boot, restores on refresh) -- **Presets** padded from 20 to 126 outputs with 0.5 defaults - -## Answered Questions - -- **RL mode is now the default** across all designs — it's the purer expression of the NISPS thesis -- **Network architecture scaled** from `[10, 10, 14]` (20 outputs) to `[32, 48, 64]` (126 outputs) -- **Synth mode gets its own visualization** — parameter landscape with 126 grouped bars, not the particle system -- **Snapshot teaching + fine-grained sliders coexist** — presets for quick setup, expandable raw params for power users - -## Iteration History - -1. **Initial build** (2026-03-21): Three designs from PRD, all functional -2. **Round 1**: Added back buttons, follow mode, merged joystick+minimap (A), merged joystick+mapping (B), fixed RL functions (C) -3. **Round 2**: Restored localStorage, added keyboard hints, fixed slider responsiveness (B), fixed Visual pill toggle (B), horizontal heatmap bars (A) -4. **Round 3**: Upgraded to 126 outputs + `[32, 48, 64]` MLP, added SynthVisualizer to all designs, added randomize buttons, `?tame` URL param, back button SVGs -5. **Round 4**: Homepage with live previews, synth quick-play button (A), interactive synth bars (A), heatmap hidden in synth mode (A), tame param to B+C diff --git a/playground/faust/MODULAR_DESTINATIONS.md b/playground/faust/MODULAR_DESTINATIONS.md deleted file mode 100644 index 0f7d2cf..0000000 --- a/playground/faust/MODULAR_DESTINATIONS.md +++ /dev/null @@ -1,45 +0,0 @@ -# Modular engines — modulation destination table - -Each of the three `modular-*` Faust engines exposes a 48-source × 10-destination -modulation matrix. All engines import `mod-pool.lib` (16 ADSRs + 32 LFOs = 48 -sources) and bind those sources to engine-specific destinations via the -`MM_Matrix/sNN_dNN_` slider labels. - -The destination index (`dNN`) is consistent per engine but NOT consistent across -engines — `d08` happens to be `amp` for all three because the generator places -the master amplitude destination in the same slot for every engine, which makes -it easy for Phase B to wire up a default ADSR1→amp patch regardless of the -active engine. - -| Engine | d00 | d01 | d02 | d03 | d04 | d05 | d06 | d07 | d08 | d09 | -|-------------|-------|-------------|-------------|----------------|------------|------------------|-----------------|----------------|-----|-----| -| subtractive | pitch | osc2_detune | osc3_detune | osc_mix_bal | noise_level| cutoff | resonance | filter_env_amt | amp | pan | -| additive | pitch | bright | tilt | inharmonicity | odd_even | formant_ctr | formant_depth | noise_mix | amp | pan | -| fm | pitch | op1_level | op2_level | op3_level | op4_level | cross_mod_global | feedback_global | global_ratio | amp | pan | - -## Conventions - -- **Source index** (`sNN`): 00..15 = ADSR slots 1..16, 16..47 = LFO slots 1..32. -- **Amount range**: each matrix slider is `[-1.0, +1.0]`, default 0 (no route). -- **`amp` destination** (d08): always the master amplitude destination for the - engine. Without any source routed here, the voice is either silent - (subtractive — the mod_amp signal multiplies into the VCA) or produces a - sustained tone at the engine's natural level (additive and fm — still gated - through mod_amp; without a route the amp signal is 0). **Route ADSR1 → d08 - for a standard VCA envelope in all three engines.** -- **`pitch` destination** (d00): always semitones, ±12 per mod unit. -- **`pan` destination** (d09): added to the master pan knob, clamped to [-1,1]. - -## Notes - -- `additive.bright` is a progressive high-partial boost/cut (replaces the old - brightness envelope in the non-modular `additive.dsp`). At mod=0, it is - unity; at mod=+1 it boosts top partials, at -1 it cuts them. -- `additive.formant_ctr` shifts BOTH formant centre frequencies by the same - amount (up to ±8 harmonics) so you can "move" the vocal shape with an LFO or - envelope. -- `fm.cross_mod_global` and `fm.feedback_global` are `(1 + mod)` multipliers - applied to the respective matrix cells BEFORE the feedback loop is closed, - so a mod of +1 doubles FM depth and -1 zeroes it out. -- `fm.global_ratio` is an ADDITIVE offset on all four operator ratios (±8), - clamped to ≥0.01. This can behave like a pitch-bend when animated slowly. diff --git a/playground/faust/README.md b/playground/faust/README.md deleted file mode 100644 index ae8753d..0000000 --- a/playground/faust/README.md +++ /dev/null @@ -1,154 +0,0 @@ -# playground/faust — Faust DSP Build Pipeline - -This directory contains Faust DSP source files and the toolchain for compiling -them to WebAssembly for use in the MEMLNaut playground. - -## Required Tools - -| Tool | Version | Purpose | -|---------|----------|-------------------------------------------------| -| `faust` | >= 2.60 | Compiles `.dsp` → `.wasm` + `.json` | -| `emcc` | >= 3.1.x | Optional — needed only if linking extra C++ code | - -`emcc` is already available at `/usr/lib/emscripten/emcc` on this system. -`faust` is not currently installed — use `nix-shell -p faust` for a one-off -build, or `nix profile install nixpkgs#faust` to install permanently. - -## How to Compile - -```bash -cd playground/faust -./build.sh -``` - -This compiles every `.dsp` file in the directory, producing alongside it: - -- `.wasm` — the compiled DSP binary (loaded as an AudioWorkletNode) -- `.json` — the Faust UI descriptor (consumed by `faustJsonToParamMeta`) -- `.js` — JS glue / AudioWorklet wrapper generated by faust - -## DSP Files - -| File | Status | Description | -|------------------|-------------|-----------------------------------------------------| -| `additive.dsp` | Placeholder | 4-harmonic sine bank (2 params). Full engine: meml-pj4 | -| `fm-matrix.dsp` | Placeholder | 2-op FM synth (4 params). Full engine: meml-wgg | - -## Output Format — `.json` Descriptor - -Faust's `-json` flag emits a UI descriptor tree. Example: - -```json -{ - "name": "additive", - "version": "2.75.7", - "options": "-vec", - "size": "0", - "inputs": "0", - "outputs": "2", - "meta": [...], - "ui": [ - { - "type": "vgroup", - "label": "additive", - "items": [ - { - "type": "hslider", - "label": "freq", - "address": "/additive/freq", - "meta": [{"unit": "Hz"}], - "init": 220, - "min": 20, - "max": 4000, - "step": 0.1 - }, - { - "type": "hslider", - "label": "amp", - "address": "/additive/amp", - "init": 0.5, - "min": 0, - "max": 1, - "step": 0.001 - } - ] - } - ] -} -``` - -## How `faustJsonToParamMeta` Consumes the JSON - -`playground/js/synth/faust-param-meta.js` exports: - -```js -import { faustJsonToParamMeta, loadFaustParamMeta } from './faust-param-meta.js'; - -// From a pre-parsed object: -const paramMeta = faustJsonToParamMeta(faustJson); - -// Or fetch + parse in one step: -const paramMeta = await loadFaustParamMeta('faust/additive.json'); -``` - -`faustJsonToParamMeta` recursively walks the `ui` tree, collects all -`hslider` / `vslider` / `nentry` items, and returns: - -```js -[ - { id: 'freq', name: 'freq', min: 20, max: 4000, init: 220, curve: 0.5, group: 'additive' }, - { id: 'amp', name: 'amp', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'additive' }, -] -``` - -This is the standard `paramMeta` format used throughout the playground -(`SynthEngine.paramMeta`, preset system, group drawer, etc.). - -## FaustEngineBase Loading Pattern - -`playground/js/synth/faust-engine-base.js` provides a base class for any -engine compiled with this pipeline: - -```js -import { FaustEngineBase } from './js/synth/faust-engine-base.js'; - -class AdditiveEngine extends FaustEngineBase { - constructor() { - super({ - id: 'additive', - displayName: 'Additive', - wasmUrl: 'faust/additive.wasm', - jsonUrl: 'faust/additive.json', - workletUrl: 'faust/additive-processor.js', // AudioWorklet file - processorName: 'additive-processor', // registerProcessor() name - }); - } -} - -const engine = new AdditiveEngine(); -await engine.init(audioCtx); // fetches JSON + WASM, loads worklet -engine.noteOn(69, 0.8); // A4, velocity 0.8 -engine.setParam(0, 0.6); // normalized [0,1] → maps to param range -engine.noteOff(69); -``` - -The `init()` call: -1. Fetches `jsonUrl` → calls `loadFaustParamMeta()` → populates `engine.paramMeta` -2. Fetches `wasmUrl` → transfers bytes to AudioWorklet -3. Registers `workletUrl` with `audioCtx.audioWorklet.addModule()` -4. Creates an `AudioWorkletNode` and connects it to `audioCtx.destination` -5. Sends `{ type: 'init', wasmBytes, sampleRate }` to the worklet -6. Waits for the worklet to reply `{ type: 'ready' }` (10 s timeout) - -## AudioWorklet Processor Base - -`faust-worklet-processor.js` (this directory) defines `FaustWorkletProcessor`, -a base class for concrete engine worklets. It handles: - -- `{ type: 'init', wasmBytes, sampleRate }` — calls `_initWasm()` -- `{ type: 'setParam', index, value }` — calls `_onSetParam()` -- `{ type: 'noteOn', freq, vel }` — calls `_onNoteOn()` -- `{ type: 'noteOff', freq }` — calls `_onNoteOff()` -- Replies `{ type: 'ready' }` or `{ type: 'error', message }` to main thread - -Each concrete engine processor overrides `_initWasm()` and `_renderBlock()`. diff --git a/playground/faust/additive-processor.js b/playground/faust/additive-processor.js deleted file mode 100644 index 24f13a5..0000000 --- a/playground/faust/additive-processor.js +++ /dev/null @@ -1,402 +0,0 @@ -/** - * additive-processor.js — AudioWorklet processor for the Faust additive synthesiser. - * - * Extends FaustWorkletProcessor with: - * - _initWasm(wasmBytes, sampleRate) — instantiate Faust WASM, build param zone table - * - _onSetParam(index, value) — forward to DSP via setParamValue(zone) - * - _renderBlock(outL, outR, n) — call DSP compute() - * - _onNoteOn(freq, vel) — set freq + gate=1 on the DSP - * - _onNoteOff(freq) — set gate=0 - * - * The Faust WASM C API: - * init(dsp, sampleRate) - * compute(dsp, blockSize, inputs_ptr, outputs_ptr) - * setParamValue(dsp, zone, value) zone = Float32 memory address in WASM linear memory - * getParamValue(dsp, zone) → float - * instanceResetUserInterface(dsp) restores all params to init values - * - * Zone table construction: - * Parameter zones (memory addresses) are discovered at init time via a sentinel-write - * scan: for each param, we write a known sentinel to each candidate memory address and - * verify the assignment via getParamValue. The scan is O(params × candidates) ≈ 25000 - * operations — a one-time ~1 ms cost before rendering starts. - * - * WASM import requirements (Faust math builtins): - * env._sinf, _cosf, _tanf, _expf, _logf, _powf, _tanhf, _sqrtf, _fabsf, _floorf - */ - -// --------------------------------------------------------------------------- -// Import base class — FaustWorkletProcessor is defined in faust-worklet-processor.js -// which must be loaded by addModule() before this file. -// --------------------------------------------------------------------------- - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -// Number of output channels (stereo) -const NUM_OUTPUTS = 2; - -// Block size for DSP rendering -const BLOCK_SIZE = 128; - -// DSP instance pointer — Faust single-instance WASM always uses 0 -const DSP = 0; - -// --------------------------------------------------------------------------- -// AdditiveProcessor -// --------------------------------------------------------------------------- - -class AdditiveProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - - // DSP state - this._dspInst = null; // WebAssembly instance - this._dspMemory = null; // Float32Array view over WASM memory - this._paramZones = []; // Array of zone addresses, one per param (incl. hidden) - this._freqZone = 0; // Zone address for freq param (hidden) - this._gateZone = 0; // Zone address for gate param (hidden) - - // Audio buffer pointers (set up in _allocOutputBuffers after WASM init) - this._outPtrsAddr = 0; // WASM address of [outL_addr, outR_addr] array - this._outLAddr = 0; // WASM address of left channel buffer - this._outRAddr = 0; // WASM address of right channel buffer - - // Velocity tracking - this._currentVel = 0.7; - } - - // ------------------------------------------------------------------------- - // _initWasm — called once with the binary WASM bytes from the main thread - // ------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sampleRate) { - // Build the WASM import object with the math functions Faust needs - const importObj = { - env: { - _sinf: Math.sin, - _cosf: Math.cos, - _tanf: Math.tan, - _expf: Math.exp, - _logf: Math.log, - _powf: Math.pow, - _tanhf: Math.tanh, - _sqrtf: Math.sqrt, - _fabsf: Math.abs, - _floorf: Math.floor, - _ceilf: Math.ceil, - _remainderf: (a, b) => a % b, - _fmodf: (a, b) => a % b, - _roundf: Math.round, - _truncf: Math.trunc, - _log10f: Math.log10, - }, - }; - - const result = await WebAssembly.instantiate(wasmBytes, importObj); - this._dspInst = result.instance; - - const exports = this._dspInst.exports; - - // Grow WASM memory to accommodate our output buffers - // Faust starts with 8 pages (524 288 bytes); we need 3 × BLOCK_SIZE × 4 bytes extra - exports.memory.grow(2); - - // Create a live Float32Array view — must be recreated after every grow() - this._dspMemory = new Float32Array(exports.memory.buffer); - - // Initialise the DSP - exports.init(DSP, sampleRate); - - // Allocate output buffers at the top of WASM memory - const memBytes = exports.memory.buffer.byteLength; - this._outLAddr = memBytes - BLOCK_SIZE * 4 * 3; - this._outRAddr = this._outLAddr + BLOCK_SIZE * 4; - this._outPtrsAddr = this._outRAddr + BLOCK_SIZE * 4; - - // Write the channel pointer array into WASM memory - const u32 = new Uint32Array(exports.memory.buffer); - u32[this._outPtrsAddr / 4] = this._outLAddr; - u32[this._outPtrsAddr / 4 + 1] = this._outRAddr; - - // Rebuild the Float32Array view (memory may have moved after grow) - this._dspMemory = new Float32Array(exports.memory.buffer); - - // Build the parameter zone table - await this._buildZoneTable(exports); - } - - // ------------------------------------------------------------------------- - // _buildZoneTable — discover which WASM memory address holds each parameter. - // - // Strategy: - // 1. Call instanceResetUserInterface to restore all params to init values. - // 2. Write a large sentinel to every address in the "param zone" region. - // 3. Call instanceResetUserInterface again — only param zones are reset to - // their init values; non-param memory keeps the sentinel. - // 4. Record all (addr → initValue) pairs where the sentinel was cleared. - // 5. For each JSON param in order, resolve its zone by process of elimination: - // - params with unique init values match directly - // - params with ambiguous init values: write unique sentinels one-by-one - // to the candidate list, calling instanceResetUserInterface each time to - // identify which candidate gets reset - // ------------------------------------------------------------------------- - - _buildZoneTable(exports) { - const f32 = this._dspMemory; - const SENTINEL = 99999.9; - - // Param zone region (empirically determined from this compiled WASM) - const SCAN_START = 262100; - const SCAN_END = 264200; - - // Step 1+2: write sentinel everywhere in range - for (let addr = SCAN_START; addr <= SCAN_END; addr += 4) { - f32[addr / 4] = SENTINEL; - } - - // Step 3: reset — param zones revert to init, others keep sentinel - exports.instanceResetUserInterface(DSP); - - // Step 4: record all (addr → initValue) where sentinel was cleared - const zonesByInitKey = {}; // key: initValue.toFixed(7) → [addr, ...] - for (let addr = SCAN_START; addr <= SCAN_END; addr += 4) { - const v = f32[addr / 4]; - if (Math.abs(v - SENTINEL) > 1.0) { - const key = v.toFixed(7); - if (!zonesByInitKey[key]) zonesByInitKey[key] = []; - zonesByInitKey[key].push(addr); - } - } - - // Step 5: match each JSON param to a zone. - // paramDefs is an ordered list of all params (incl. hidden) from the JSON. - // Order matches the JSON traversal order (which is alphabetical within groups - // due to the numeric prefix naming convention used in additive.dsp). - // - // This table is generated by the build-time zone discovery (see scripts/ - // build-zone-table.js) and is hard-coded here for performance. The zones - // are deterministic for a given WASM binary. - // - // Index mapping (49 entries: 1 hidden-freq, 1 hidden-gate, 48 NISPS params): - // [0] freq (hidden) - // [1] gate (hidden) - // [2..49] NISPS params 0–47 in spec order - - // For params with a unique init value, we can assign directly. - // For ambiguous ones, we use the sequential sentinel method. - - // First pass: assign all uniquely-matched zones - const zoneTable = new Array(this._paramDefsLength()).fill(0); - const assigned = new Array(this._paramDefsLength()).fill(false); - const usedZones = new Set(); - - const paramDefs = this._paramDefs(); - - for (let i = 0; i < paramDefs.length; i++) { - const p = paramDefs[i]; - const key = p.init.toFixed(7); - const candidates = (zonesByInitKey[key] || []).filter(a => !usedZones.has(a)); - - if (candidates.length === 1) { - zoneTable[i] = candidates[0]; - assigned[i] = true; - usedZones.add(candidates[0]); - } - } - - // Second pass: for unresolved params, use individual sentinel writes. - for (let i = 0; i < paramDefs.length; i++) { - if (assigned[i]) continue; - - const p = paramDefs[i]; - const key = p.init.toFixed(7); - const candidates = (zonesByInitKey[key] || []).filter(a => !usedZones.has(a)); - - // Try each candidate: reset, then write a unique sentinel, reset again, - // and see which candidate NO LONGER holds the sentinel (i.e. got reset). - // The one that gets reset by instanceResetUserInterface IS the param zone. - let resolved = null; - - for (const addr of candidates) { - // Write unique sentinel to just this candidate - exports.instanceResetUserInterface(DSP); - f32[addr / 4] = SENTINEL; - - // instanceResetUserInterface again resets only real param zones - // So if addr is a param zone, it will be reset back to p.init - exports.instanceResetUserInterface(DSP); - const v = exports.getParamValue(DSP, addr); - - if (Math.abs(v - p.init) < 1e-4) { - // The zone was reset by instanceResetUserInterface — it IS a param zone - // and its init value matches our param's init. Assign it. - resolved = addr; - break; - } - } - - if (resolved !== null) { - zoneTable[i] = resolved; - assigned[i] = true; - usedZones.add(resolved); - } else if (candidates.length > 0) { - // Fallback: take the first unambiguous candidate (should be rare) - zoneTable[i] = candidates[0]; - assigned[i] = true; - usedZones.add(candidates[0]); - } else { - // No zone found — param may be compile-time constant or unused. - // Write to address 0 (DSP instance pointer) is safe (read-only effectively). - zoneTable[i] = 0; - } - } - - // Expose freq and gate zones separately (first two entries in paramDefs) - this._freqZone = zoneTable[0]; - this._gateZone = zoneTable[1]; - // NISPS param zones start at index 2 - this._paramZones = zoneTable.slice(2); - - // Restore param defaults - exports.instanceResetUserInterface(DSP); - } - - // ------------------------------------------------------------------------- - // _paramDefs — ordered list matching the JSON traversal order. - // - // This list defines the zone-building traversal order. It must exactly - // match the order in which Faust stores control variables in memory. - // For additive.dsp, the JSON param order is: - // 0_Hidden (freq, gate) → 1_Spectral Shape (14) → 2_Temporal (10) → - // 3_Phase (8) → 4_Modulation (10) → 5_Master (6) - // ------------------------------------------------------------------------- - - _paramDefs() { - return [ - // Hidden - { init: 220 }, // [0] freq - { init: 0 }, // [1] gate (button) - - // 1_Spectral Shape (params 0–13) - { init: 0.8 }, // [2] h1_amp - { init: 0.5 }, // [3] h2_amp - { init: 0.35 }, // [4] h3_amp - { init: 0.25 }, // [5] h4_amp - { init: 0.18 }, // [6] h5_amp - { init: 0.12 }, // [7] h6_amp - { init: 0.08 }, // [8] h7_amp - { init: 0.06 }, // [9] h8_amp - { init: 0.05 }, // [10] h9_16_amp - { init: 0.025 }, // [11] h17_32_amp - { init: 0.01 }, // [12] h33_64_amp - { init: 0 }, // [13] spectral_tilt - { init: 0 }, // [14] inharmonicity - { init: 0.5 }, // [15] odd_even - - // 2_Temporal (params 14–23) - { init: 0.01 }, // [16] attack - { init: 0.3 }, // [17] decay - { init: 0.7 }, // [18] sustain - { init: 0.5 }, // [19] release - { init: 0.005 }, // [20] brightness_attack - { init: 0.15 }, // [21] brightness_decay - { init: 0.4 }, // [22] brightness_sustain - { init: 0.3 }, // [23] brightness_release - { init: 0.5 }, // [24] spectral_flux_rate - { init: 0.1 }, // [25] spectral_flux_depth - - // 3_Phase (params 24–31) - { init: 0 }, // [26] phase_random - { init: 0 }, // [27] phase_walk_rate - { init: 0 }, // [28] beating_depth - { init: 1 }, // [29] beating_rate - { init: 0.1 }, // [30] stereo_phase_spread - { init: 0 }, // [31] noise_floor - { init: 0.5 }, // [32] noise_color - { init: 0 }, // [33] sub_harmonic - - // 4_Modulation (params 32–41) - { init: 5 }, // [34] vibrato_rate - { init: 0 }, // [35] vibrato_depth - { init: 0.3 }, // [36] vibrato_delay - { init: 4 }, // [37] tremolo_rate - { init: 0 }, // [38] tremolo_depth - { init: 0 }, // [39] drift_rate - { init: 0 }, // [40] drift_depth - { init: 3 }, // [41] formant1_freq - { init: 6 }, // [42] formant2_freq - { init: 0 }, // [43] formant_depth - - // 5_Master (params 42–47) - { init: 0.7 }, // [44] level - { init: 0.5 }, // [45] vel_sens - { init: 0.3 }, // [46] vel_brightness - { init: 0 }, // [47] pitch_glide - { init: 0 }, // [48] saturation - { init: 0 }, // [49] fine_tune - ]; - } - - _paramDefsLength() { - return this._paramDefs().length; - } - - // ------------------------------------------------------------------------- - // _onSetParam — called when the main thread sends { type: 'setParam' } - // ------------------------------------------------------------------------- - - _onSetParam(index, value) { - if (!this._dspInst) return; - const zone = this._paramZones[index]; - if (!zone) return; - this._dspInst.exports.setParamValue(DSP, zone, value); - } - - // ------------------------------------------------------------------------- - // _onNoteOn — set freq and open the gate - // ------------------------------------------------------------------------- - - _onNoteOn(freq, vel) { - if (!this._dspInst) return; - this._currentVel = vel ?? 0.7; - const ex = this._dspInst.exports; - if (this._freqZone) ex.setParamValue(DSP, this._freqZone, freq); - if (this._gateZone) ex.setParamValue(DSP, this._gateZone, 1.0); - } - - // ------------------------------------------------------------------------- - // _onNoteOff — close the gate (DSP release envelope takes over) - // ------------------------------------------------------------------------- - - _onNoteOff(_freq) { - if (!this._dspInst) return; - if (this._gateZone) this._dspInst.exports.setParamValue(DSP, this._gateZone, 0.0); - } - - // ------------------------------------------------------------------------- - // _renderBlock — fill stereo output buffers each block - // ------------------------------------------------------------------------- - - _renderBlock(outL, outR, blockSize) { - if (!this._dspInst || !this._dspMemory) return; - - const ex = this._dspInst.exports; - - // Call Faust compute: compute(dsp, n, input_channels_ptr, output_channels_ptr) - // For 0 inputs, input_channels_ptr = 0 (null pointer is safe for Faust) - ex.compute(DSP, blockSize, 0, this._outPtrsAddr); - - // Copy WASM output buffers to AudioWorklet output Float32Arrays - const wL = new Float32Array(ex.memory.buffer, this._outLAddr, blockSize); - const wR = new Float32Array(ex.memory.buffer, this._outRAddr, blockSize); - outL.set(wL); - outR.set(wR); - } -} - -// --------------------------------------------------------------------------- -// Register the processor -// --------------------------------------------------------------------------- -registerProcessor('additive-processor', AdditiveProcessor); diff --git a/playground/faust/additive.dsp b/playground/faust/additive.dsp deleted file mode 100644 index 7447d04..0000000 --- a/playground/faust/additive.dsp +++ /dev/null @@ -1,279 +0,0 @@ -// additive.dsp — Full 48-parameter additive synthesiser for MEMLNaut NISPS playground -// -// 64 harmonic sine banks with spectral-descriptor parametrisation. -// All 48 params are designed for continuous ML exploration via NISPS. -// -// Parameter order matches NISPS output indices 0–47: -// Group 1 — Spectral Shape (0–13): harmonic bank + tilt + inharmonicity + odd/even -// Group 2 — Temporal (14–23): global ADSR + brightness ADSR + spectral flux -// Group 3 — Phase (24–31): phase randomisation, beating, stereo, noise, sub -// Group 4 — Modulation (32–41): vibrato, tremolo, drift, formants -// Group 5 — Master (42–47): level, velocity, glide, saturation, fine-tune -// -// Groups are prefixed "1_", "2_" etc., and each parameter is prefixed "00_", -// "01_" etc., so that Faust's alphabetical JSON ordering matches the spec. -// The faustJsonToParamMeta parser strips numeric prefixes and "[...]" metadata -// from labels to produce clean names. -// -// Hidden controls (not in paramMeta — worklet drives them directly): -// freq — fundamental frequency Hz (noteOn) -// gate — gate signal 0/1 (noteOn/noteOff) -// -// Build: -// faust -lang wasm -cn additive -e additive.dsp -o additive.wasm -// faust -json additive.dsp -o /dev/null (produces additive.dsp.json) -// -import("stdfaust.lib"); - -// --------------------------------------------------------------------------- -// Hidden controls -// --------------------------------------------------------------------------- -freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("0_Hidden/gate[hidden:1]"); - -// --------------------------------------------------------------------------- -// Group 1 — Spectral Shape (params 0–13) -// --------------------------------------------------------------------------- -h1_amp = hslider("1_Spectral Shape/00_h1_amp[tooltip:H1 amplitude]", 0.8, 0, 1, 0.001); -h2_amp = hslider("1_Spectral Shape/01_h2_amp[tooltip:H2 amplitude]", 0.5, 0, 1, 0.001); -h3_amp = hslider("1_Spectral Shape/02_h3_amp[tooltip:H3 amplitude]", 0.35, 0, 1, 0.001); -h4_amp = hslider("1_Spectral Shape/03_h4_amp[tooltip:H4 amplitude]", 0.25, 0, 1, 0.001); -h5_amp = hslider("1_Spectral Shape/04_h5_amp[tooltip:H5 amplitude]", 0.18, 0, 1, 0.001); -h6_amp = hslider("1_Spectral Shape/05_h6_amp[tooltip:H6 amplitude]", 0.12, 0, 1, 0.001); -h7_amp = hslider("1_Spectral Shape/06_h7_amp[tooltip:H7 amplitude]", 0.08, 0, 1, 0.001); -h8_amp = hslider("1_Spectral Shape/07_h8_amp[tooltip:H8 amplitude]", 0.06, 0, 1, 0.001); -h9_16_amp = hslider("1_Spectral Shape/08_h9_16_amp[tooltip:H9-16 group amp]", 0.05, 0, 1, 0.001); -h17_32_amp = hslider("1_Spectral Shape/09_h17_32_amp[tooltip:H17-32 group amp]",0.025, 0, 1, 0.001); -h33_64_amp = hslider("1_Spectral Shape/10_h33_64_amp[tooltip:H33-64 group amp]",0.01, 0, 1, 0.001); -spectral_tilt = hslider("1_Spectral Shape/11_spectral_tilt[tooltip:Global tilt]", 0, -1, 1, 0.001); -inharmonicity = hslider("1_Spectral Shape/12_inharmonicity[tooltip:Inharmonicity]", 0, 0, 0.15, 0.0001); -odd_even = hslider("1_Spectral Shape/13_odd_even[tooltip:Odd/even balance]", 0.5, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Temporal (params 14–23) -// --------------------------------------------------------------------------- -attack = hslider("2_Temporal/00_attack[scale:log][tooltip:Attack]", 0.01, 0.001, 5, 0.001); -decay = hslider("2_Temporal/01_decay[tooltip:Decay]", 0.3, 0.001, 10, 0.001); -sustain = hslider("2_Temporal/02_sustain[tooltip:Sustain]", 0.7, 0, 1, 0.001); -release = hslider("2_Temporal/03_release[tooltip:Release]", 0.5, 0.01, 10, 0.001); -brightness_attack = hslider("2_Temporal/04_brightness_attack[scale:log][tooltip:Bright A]", 0.005, 0.001, 5, 0.001); -brightness_decay = hslider("2_Temporal/05_brightness_decay[tooltip:Bright D]", 0.15, 0.001, 5, 0.001); -brightness_sustain = hslider("2_Temporal/06_brightness_sustain[tooltip:Bright S]", 0.4, 0, 1, 0.001); -brightness_release = hslider("2_Temporal/07_brightness_release[tooltip:Bright R]", 0.3, 0.01, 5, 0.001); -spectral_flux_rate = hslider("2_Temporal/08_spectral_flux_rate[tooltip:Flux rate]", 0.5, 0, 10, 0.01); -spectral_flux_depth= hslider("2_Temporal/09_spectral_flux_depth[tooltip:Flux depth]", 0.1, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Phase & Coherence (params 24–31) -// --------------------------------------------------------------------------- -phase_random = hslider("3_Phase/00_phase_random[tooltip:Phase randomisation]", 0, 0, 1, 0.001); -phase_walk_rate = hslider("3_Phase/01_phase_walk_rate[tooltip:Phase walk rate]", 0, 0, 5, 0.001); -beating_depth = hslider("3_Phase/02_beating_depth[tooltip:Beating depth]", 0, 0, 0.02, 0.0001); -beating_rate = hslider("3_Phase/03_beating_rate[tooltip:Beating rate]", 1, 0, 10, 0.01); -stereo_phase_spread= hslider("3_Phase/04_stereo_phase_spread[tooltip:Stereo spread]", 0.1, 0, 1, 0.001); -noise_floor = hslider("3_Phase/05_noise_floor[tooltip:Noise floor]", 0, 0, 0.2, 0.001); -noise_color = hslider("3_Phase/06_noise_color[tooltip:Noise colour]", 0.5, 0, 1, 0.001); -sub_harmonic = hslider("3_Phase/07_sub_harmonic[tooltip:Sub-harmonic]", 0, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Group 4 — Modulation (params 32–41) -// --------------------------------------------------------------------------- -vibrato_rate = hslider("4_Modulation/00_vibrato_rate[tooltip:Vibrato rate]", 5, 0, 10, 0.01); -vibrato_depth = hslider("4_Modulation/01_vibrato_depth[tooltip:Vibrato depth]", 0, 0, 0.05, 0.0001); -vibrato_delay = hslider("4_Modulation/02_vibrato_delay[tooltip:Vibrato delay]", 0.3, 0, 2, 0.001); -tremolo_rate = hslider("4_Modulation/03_tremolo_rate[tooltip:Tremolo rate]", 4, 0, 20, 0.01); -tremolo_depth = hslider("4_Modulation/04_tremolo_depth[tooltip:Tremolo depth]", 0, 0, 1, 0.001); -drift_rate = hslider("4_Modulation/05_drift_rate[tooltip:Drift rate]", 0, 0, 2, 0.001); -drift_depth = hslider("4_Modulation/06_drift_depth[tooltip:Drift depth]", 0, 0, 0.3, 0.001); -formant1_freq = hslider("4_Modulation/07_formant1_freq[tooltip:Formant 1 freq]",3, 1, 16, 0.01); -formant2_freq = hslider("4_Modulation/08_formant2_freq[tooltip:Formant 2 freq]",6, 1, 16, 0.01); -formant_depth = hslider("4_Modulation/09_formant_depth[tooltip:Formant depth]", 0, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Group 5 — Master (params 42–47) -// --------------------------------------------------------------------------- -level = hslider("5_Master/00_level[tooltip:Output level]", 0.7, 0, 1, 0.001); -vel_sens = hslider("5_Master/01_vel_sens[tooltip:Velocity sens]", 0.5, 0, 1, 0.001); -vel_bright = hslider("5_Master/02_vel_brightness[tooltip:Vel bright]",0.3, 0, 1, 0.001); -pitch_glide= hslider("5_Master/03_pitch_glide[tooltip:Portamento]", 0, 0, 10, 0.01); -saturation = hslider("5_Master/04_saturation[tooltip:Saturation]", 0, 0, 1, 0.001); -fine_tune = hslider("5_Master/05_fine_tune[unit:ct][tooltip:Cents]", 0, -50, 50, 0.1); - -// --------------------------------------------------------------------------- -// Internal constants -// --------------------------------------------------------------------------- -N = 64; -PI = ma.PI; - -// --------------------------------------------------------------------------- -// Pitch — portamento + fine tune -// --------------------------------------------------------------------------- -glide_tau = 0.0001 + pitch_glide * 0.2; -fine_ratio = pow(2.0, fine_tune / 1200.0); -freq_smooth = freq * fine_ratio : si.smooth(ba.tau2pole(glide_tau)); - -// --------------------------------------------------------------------------- -// Vibrato LFO — delayed onset via slow-attack envelope -// --------------------------------------------------------------------------- -vibrato_env = en.adsr(vibrato_delay, 0.01, 1.0, 0.5, gate); -vibrato_lfo = os.osc(vibrato_rate) * vibrato_depth * vibrato_env; - -// --------------------------------------------------------------------------- -// Tremolo LFO -// --------------------------------------------------------------------------- -tremolo_lfo = 1.0 - tremolo_depth * 0.5 * (1.0 + os.osc(tremolo_rate)); - -// --------------------------------------------------------------------------- -// Global amplitude ADSR -// --------------------------------------------------------------------------- -amp_env = en.adsr(attack, decay, sustain, release, gate); - -// --------------------------------------------------------------------------- -// Brightness envelope — controls high harmonic amplitude over time -// --------------------------------------------------------------------------- -bright_env = en.adsr(brightness_attack, brightness_decay, - brightness_sustain, brightness_release, gate); -bright_blend(k) = float(k - 1) / float(N - 1); -bright_factor(k) = 1.0 - bright_blend(k) + bright_blend(k) * bright_env; - -// --------------------------------------------------------------------------- -// Spectral flux — slow LFO on upper harmonic amplitudes -// --------------------------------------------------------------------------- -flux_lfo = os.osc(spectral_flux_rate); -flux_factor(k) = ba.if(k > 8, - 1.0 + spectral_flux_depth * flux_lfo * bright_blend(k), - 1.0); - -// --------------------------------------------------------------------------- -// Drift — slow random walk on per-partial amplitudes -// --------------------------------------------------------------------------- -drift_lfo(k) = no.noise * (float(k % 7 + 1) / 7.0) - : fi.lowpass(1, max(0.1, drift_rate * 2.0)) - : *(drift_depth); -drift_factor(k) = 1.0 + drift_lfo(k); - -// --------------------------------------------------------------------------- -// Per-harmonic base amplitude from group sliders -// --------------------------------------------------------------------------- -group_amp(k) = - ba.if(k == 1, h1_amp, - ba.if(k == 2, h2_amp, - ba.if(k == 3, h3_amp, - ba.if(k == 4, h4_amp, - ba.if(k == 5, h5_amp, - ba.if(k == 6, h6_amp, - ba.if(k == 7, h7_amp, - ba.if(k == 8, h8_amp, - ba.if(k <= 16, h9_16_amp, - ba.if(k <= 32, h17_32_amp, - h33_64_amp)))))))))); - -// Spectral tilt: amp *= k^tilt (k=1 is always unity) -tilt_factor(k) = pow(float(k), spectral_tilt); - -// Odd/even balance: ×2 so unity gain at odd_even=0.5 -odd_weight = (1.0 - odd_even) * 2.0; -even_weight = odd_even * 2.0; -odd_even_factor(k) = ba.if(k % 2 == 0, even_weight, odd_weight); - -// Combined raw amplitude -harm_amp_raw(k) = group_amp(k) * tilt_factor(k) * odd_even_factor(k); - -// --------------------------------------------------------------------------- -// Formant shaping — two Gaussian bumps in harmonic-index space -// --------------------------------------------------------------------------- -sigma_sq = 1.5 * 1.5; -formant_bump(k, ctr) = exp(-0.5 * (float(k) - ctr) * (float(k) - ctr) / sigma_sq); -formant_factor(k) = - 1.0 + formant_depth * (formant_bump(k, formant1_freq) + formant_bump(k, formant2_freq)); - -harm_amp(k) = harm_amp_raw(k) * formant_factor(k); - -// --------------------------------------------------------------------------- -// Inharmonic partial frequency: freq_k = k*f0*(1 + B*(k^2 - 1)) -// --------------------------------------------------------------------------- -harm_freq(k) = freq_smooth * float(k) - * (1.0 + inharmonicity * (float(k) * float(k) - 1.0)); - -// --------------------------------------------------------------------------- -// Phase randomisation — adds per-harmonic noise to the oscillator frequency, -// gradually dephasing partials (phase_random=0: phase-locked, =1: random) -// --------------------------------------------------------------------------- -phase_rand_lfo(k) = no.noise * (float((k * 17 + 3) % 31 + 1) / 31.0) - : si.smooth(ba.tau2pole(0.05)) - : *(phase_random * harm_freq(k) * 0.01); - -// --------------------------------------------------------------------------- -// Phase random walk — slower independent drift per partial -// --------------------------------------------------------------------------- -phase_walk(k) = no.noise * (float(k % 7 + 1) / 7.0) - : si.smooth(ba.tau2pole(0.1)) - : *(phase_walk_rate); - -// --------------------------------------------------------------------------- -// Inter-partial beating — sinusoidal detuning stagger -// --------------------------------------------------------------------------- -beating_offset(k) = beating_depth - * os.osc(beating_rate * float(k % 3 + 1) * 0.7) - * harm_freq(k); - -// --------------------------------------------------------------------------- -// Stereo spread — R channel gets a small per-harmonic pitch offset -// --------------------------------------------------------------------------- -stereo_spread_freq(k) = stereo_phase_spread * float(k) * 0.01; - -// --------------------------------------------------------------------------- -// Additive oscillator sums — L and R -// --------------------------------------------------------------------------- -additive_L = sum(k, N, - harm_amp(k+1) * bright_factor(k+1) * flux_factor(k+1) * drift_factor(k+1) * - os.osc( harm_freq(k+1) * (1.0 + vibrato_lfo) - + beating_offset(k+1) - + phase_rand_lfo(k+1) - + phase_walk(k+1) - ) -); - -additive_R = sum(k, N, - harm_amp(k+1) * bright_factor(k+1) * flux_factor(k+1) * drift_factor(k+1) * - os.osc( harm_freq(k+1) * (1.0 + vibrato_lfo + stereo_spread_freq(k+1)) - + beating_offset(k+1) - + phase_rand_lfo(k+1) - + phase_walk(k+1) - ) -); - -// --------------------------------------------------------------------------- -// Sub-harmonic (0.5× fundamental) -// --------------------------------------------------------------------------- -sub_osc = sub_harmonic * os.osc(freq_smooth * 0.5); - -// --------------------------------------------------------------------------- -// Noise floor — coloured via one-pole LP (noise_color=0→white, =1→dark) -// --------------------------------------------------------------------------- -noise_lp_cutoff = 200.0 + (1.0 - noise_color) * 19800.0; -noise_signal = no.noise : fi.lowpass(1, noise_lp_cutoff); -noise_out = noise_floor * noise_signal; - -// --------------------------------------------------------------------------- -// Soft-clip saturation — tanh waveshaper -// --------------------------------------------------------------------------- -drive = 1.0 + saturation * 9.0; -softclip(x) = ma.tanh(x * drive) / drive; - -// --------------------------------------------------------------------------- -// Output gain -// vel_bright modulates how much the brightness envelope boosts the overall -// level during note onset (couples velocity sensitivity to brightness). -// Here it acts as a subtle mid-term gain shaper via the bright_env signal. -// --------------------------------------------------------------------------- -vel_bright_boost = 1.0 + vel_bright * bright_env * 0.3; -out_gain = level * (1.0 - vel_sens * 0.3) * vel_bright_boost; - -// --------------------------------------------------------------------------- -// Final assembly -// --------------------------------------------------------------------------- -signal_L = (additive_L + sub_osc + noise_out) * amp_env * tremolo_lfo * out_gain; -signal_R = (additive_R + sub_osc + noise_out) * amp_env * tremolo_lfo * out_gain; - -process = softclip(signal_L), softclip(signal_R); diff --git a/playground/faust/additive.json b/playground/faust/additive.json deleted file mode 100644 index 78fe7b9..0000000 --- a/playground/faust/additive.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "additive","filename": "additive.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn additive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/envelopes.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/oscillators.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/noises.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 264068,"code": "Ly8gYWRkaXRpdmUuZHNwIOKAlCBGdWxsIDQ4LXBhcmFtZXRlciBhZGRpdGl2ZSBzeW50aGVzaXNlciBmb3IgTUVNTE5hdXQgTklTUFMgcGxheWdyb3VuZAovLwovLyA2NCBoYXJtb25pYyBzaW5lIGJhbmtzIHdpdGggc3BlY3RyYWwtZGVzY3JpcHRvciBwYXJhbWV0cmlzYXRpb24uCi8vIEFsbCA0OCBwYXJhbXMgYXJlIGRlc2lnbmVkIGZvciBjb250aW51b3VzIE1MIGV4cGxvcmF0aW9uIHZpYSBOSVNQUy4KLy8KLy8gUGFyYW1ldGVyIG9yZGVyIG1hdGNoZXMgTklTUFMgb3V0cHV0IGluZGljZXMgMOKAkzQ3OgovLyAgIEdyb3VwIDEg4oCUIFNwZWN0cmFsIFNoYXBlICgw4oCTMTMpOiAgaGFybW9uaWMgYmFuayArIHRpbHQgKyBpbmhhcm1vbmljaXR5ICsgb2RkL2V2ZW4KLy8gICBHcm91cCAyIOKAlCBUZW1wb3JhbCAgICAgICAoMTTigJMyMyk6IGdsb2JhbCBBRFNSICsgYnJpZ2h0bmVzcyBBRFNSICsgc3BlY3RyYWwgZmx1eAovLyAgIEdyb3VwIDMg4oCUIFBoYXNlICAgICAgICAgICgyNOKAkzMxKTogcGhhc2UgcmFuZG9taXNhdGlvbiwgYmVhdGluZywgc3RlcmVvLCBub2lzZSwgc3ViCi8vICAgR3JvdXAgNCDigJQgTW9kdWxhdGlvbiAgICAgKDMy4oCTNDEpOiB2aWJyYXRvLCB0cmVtb2xvLCBkcmlmdCwgZm9ybWFudHMKLy8gICBHcm91cCA1IOKAlCBNYXN0ZXIgICAgICAgICAoNDLigJM0Nyk6IGxldmVsLCB2ZWxvY2l0eSwgZ2xpZGUsIHNhdHVyYXRpb24sIGZpbmUtdHVuZQovLwovLyBHcm91cHMgYXJlIHByZWZpeGVkICIxXyIsICIyXyIgZXRjLiwgYW5kIGVhY2ggcGFyYW1ldGVyIGlzIHByZWZpeGVkICIwMF8iLAovLyAiMDFfIiBldGMuLCBzbyB0aGF0IEZhdXN0J3MgYWxwaGFiZXRpY2FsIEpTT04gb3JkZXJpbmcgbWF0Y2hlcyB0aGUgc3BlYy4KLy8gVGhlIGZhdXN0SnNvblRvUGFyYW1NZXRhIHBhcnNlciBzdHJpcHMgbnVtZXJpYyBwcmVmaXhlcyBhbmQgIlsuLi5dIiBtZXRhZGF0YQovLyBmcm9tIGxhYmVscyB0byBwcm9kdWNlIGNsZWFuIG5hbWVzLgovLwovLyBIaWRkZW4gY29udHJvbHMgKG5vdCBpbiBwYXJhbU1ldGEg4oCUIHdvcmtsZXQgZHJpdmVzIHRoZW0gZGlyZWN0bHkpOgovLyAgIGZyZXEgIOKAlCBmdW5kYW1lbnRhbCBmcmVxdWVuY3kgSHogKG5vdGVPbikKLy8gICBnYXRlICDigJQgZ2F0ZSBzaWduYWwgMC8xICAgICAgICAgIChub3RlT24vbm90ZU9mZikKLy8KLy8gQnVpbGQ6Ci8vICAgZmF1c3QgLWxhbmcgd2FzbSAtY24gYWRkaXRpdmUgLWUgYWRkaXRpdmUuZHNwIC1vIGFkZGl0aXZlLndhc20KLy8gICBmYXVzdCAtanNvbiBhZGRpdGl2ZS5kc3AgLW8gL2Rldi9udWxsICAocHJvZHVjZXMgYWRkaXRpdmUuZHNwLmpzb24pCi8vCmltcG9ydCgic3RkZmF1c3QubGliIik7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gSGlkZGVuIGNvbnRyb2xzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmcmVxID0gaHNsaWRlcigiMF9IaWRkZW4vZnJlcVtoaWRkZW46MV1bdW5pdDpIel0iLCAyMjAsIDIwLCA0MDAwLCAwLjAxKTsKZ2F0ZSA9IGJ1dHRvbigiMF9IaWRkZW4vZ2F0ZVtoaWRkZW46MV0iKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAxIOKAlCBTcGVjdHJhbCBTaGFwZSAocGFyYW1zIDDigJMxMykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmgxX2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzAwX2gxX2FtcFt0b29sdGlwOkgxIGFtcGxpdHVkZV0iLCAgICAgICAgMC44LCAgIDAsICAgIDEsICAgICAwLjAwMSk7CmgyX2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzAxX2gyX2FtcFt0b29sdGlwOkgyIGFtcGxpdHVkZV0iLCAgICAgICAgMC41LCAgIDAsICAgIDEsICAgICAwLjAwMSk7CmgzX2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzAyX2gzX2FtcFt0b29sdGlwOkgzIGFtcGxpdHVkZV0iLCAgICAgICAgMC4zNSwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg0X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzAzX2g0X2FtcFt0b29sdGlwOkg0IGFtcGxpdHVkZV0iLCAgICAgICAgMC4yNSwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg1X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA0X2g1X2FtcFt0b29sdGlwOkg1IGFtcGxpdHVkZV0iLCAgICAgICAgMC4xOCwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg2X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA1X2g2X2FtcFt0b29sdGlwOkg2IGFtcGxpdHVkZV0iLCAgICAgICAgMC4xMiwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg3X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA2X2g3X2FtcFt0b29sdGlwOkg3IGFtcGxpdHVkZV0iLCAgICAgICAgMC4wOCwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg4X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA3X2g4X2FtcFt0b29sdGlwOkg4IGFtcGxpdHVkZV0iLCAgICAgICAgMC4wNiwgIDAsICAgIDEsICAgICAwLjAwMSk7Cmg5XzE2X2FtcCAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA4X2g5XzE2X2FtcFt0b29sdGlwOkg5LTE2IGdyb3VwIGFtcF0iLCAgMC4wNSwgIDAsICAgIDEsICAgICAwLjAwMSk7CmgxN18zMl9hbXAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzA5X2gxN18zMl9hbXBbdG9vbHRpcDpIMTctMzIgZ3JvdXAgYW1wXSIsMC4wMjUsIDAsICAgIDEsICAgICAwLjAwMSk7CmgzM182NF9hbXAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzEwX2gzM182NF9hbXBbdG9vbHRpcDpIMzMtNjQgZ3JvdXAgYW1wXSIsMC4wMSwgIDAsICAgIDEsICAgICAwLjAwMSk7CnNwZWN0cmFsX3RpbHQgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzExX3NwZWN0cmFsX3RpbHRbdG9vbHRpcDpHbG9iYWwgdGlsdF0iLCAgMCwgICAgLTEsICAgIDEsICAgICAwLjAwMSk7CmluaGFybW9uaWNpdHkgPSBoc2xpZGVyKCIxX1NwZWN0cmFsIFNoYXBlLzEyX2luaGFybW9uaWNpdHlbdG9vbHRpcDpJbmhhcm1vbmljaXR5XSIsIDAsICAgIDAsICAgIDAuMTUsICAwLjAwMDEpOwpvZGRfZXZlbiAgICAgID0gaHNsaWRlcigiMV9TcGVjdHJhbCBTaGFwZS8xM19vZGRfZXZlblt0b29sdGlwOk9kZC9ldmVuIGJhbGFuY2VdIiwgIDAuNSwgICAwLCAgICAxLCAgICAgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDIg4oCUIFRlbXBvcmFsIChwYXJhbXMgMTTigJMyMykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmF0dGFjayAgICAgICAgICAgICA9IGhzbGlkZXIoIjJfVGVtcG9yYWwvMDBfYXR0YWNrW3NjYWxlOmxvZ11bdG9vbHRpcDpBdHRhY2tdIiwgICAgICAgICAgICAgMC4wMSwgIDAuMDAxLCA1LCAgIDAuMDAxKTsKZGVjYXkgICAgICAgICAgICAgID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wMV9kZWNheVt0b29sdGlwOkRlY2F5XSIsICAgICAgICAgICAgICAgICAgICAgICAgICAgMC4zLCAgIDAuMDAxLCAxMCwgIDAuMDAxKTsKc3VzdGFpbiAgICAgICAgICAgID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wMl9zdXN0YWluW3Rvb2x0aXA6U3VzdGFpbl0iLCAgICAgICAgICAgICAgICAgICAgICAgMC43LCAgIDAsICAgICAxLCAgIDAuMDAxKTsKcmVsZWFzZSAgICAgICAgICAgID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wM19yZWxlYXNlW3Rvb2x0aXA6UmVsZWFzZV0iLCAgICAgICAgICAgICAgICAgICAgICAgMC41LCAgIDAuMDEsICAxMCwgIDAuMDAxKTsKYnJpZ2h0bmVzc19hdHRhY2sgID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wNF9icmlnaHRuZXNzX2F0dGFja1tzY2FsZTpsb2ddW3Rvb2x0aXA6QnJpZ2h0IEFdIiwgMC4wMDUsIDAuMDAxLCA1LCAgIDAuMDAxKTsKYnJpZ2h0bmVzc19kZWNheSAgID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wNV9icmlnaHRuZXNzX2RlY2F5W3Rvb2x0aXA6QnJpZ2h0IERdIiwgICAgICAgICAgICAgMC4xNSwgIDAuMDAxLCA1LCAgIDAuMDAxKTsKYnJpZ2h0bmVzc19zdXN0YWluID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wNl9icmlnaHRuZXNzX3N1c3RhaW5bdG9vbHRpcDpCcmlnaHQgU10iLCAgICAgICAgICAgMC40LCAgIDAsICAgICAxLCAgIDAuMDAxKTsKYnJpZ2h0bmVzc19yZWxlYXNlID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wN19icmlnaHRuZXNzX3JlbGVhc2VbdG9vbHRpcDpCcmlnaHQgUl0iLCAgICAgICAgICAgMC4zLCAgIDAuMDEsICA1LCAgIDAuMDAxKTsKc3BlY3RyYWxfZmx1eF9yYXRlID0gaHNsaWRlcigiMl9UZW1wb3JhbC8wOF9zcGVjdHJhbF9mbHV4X3JhdGVbdG9vbHRpcDpGbHV4IHJhdGVdIiwgICAgICAgICAgMC41LCAgIDAsICAgICAxMCwgIDAuMDEpOwpzcGVjdHJhbF9mbHV4X2RlcHRoPSBoc2xpZGVyKCIyX1RlbXBvcmFsLzA5X3NwZWN0cmFsX2ZsdXhfZGVwdGhbdG9vbHRpcDpGbHV4IGRlcHRoXSIsICAgICAgICAwLjEsICAgMCwgICAgIDEsICAgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDMg4oCUIFBoYXNlICYgQ29oZXJlbmNlIChwYXJhbXMgMjTigJMzMSkKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnBoYXNlX3JhbmRvbSAgICAgICA9IGhzbGlkZXIoIjNfUGhhc2UvMDBfcGhhc2VfcmFuZG9tW3Rvb2x0aXA6UGhhc2UgcmFuZG9taXNhdGlvbl0iLCAgICAgIDAsICAgMCwgICAgMSwgICAgMC4wMDEpOwpwaGFzZV93YWxrX3JhdGUgICAgPSBoc2xpZGVyKCIzX1BoYXNlLzAxX3BoYXNlX3dhbGtfcmF0ZVt0b29sdGlwOlBoYXNlIHdhbGsgcmF0ZV0iLCAgICAgICAgMCwgICAwLCAgICA1LCAgICAwLjAwMSk7CmJlYXRpbmdfZGVwdGggICAgICA9IGhzbGlkZXIoIjNfUGhhc2UvMDJfYmVhdGluZ19kZXB0aFt0b29sdGlwOkJlYXRpbmcgZGVwdGhdIiwgICAgICAgICAgICAwLCAgIDAsICAgIDAuMDIsIDAuMDAwMSk7CmJlYXRpbmdfcmF0ZSAgICAgICA9IGhzbGlkZXIoIjNfUGhhc2UvMDNfYmVhdGluZ19yYXRlW3Rvb2x0aXA6QmVhdGluZyByYXRlXSIsICAgICAgICAgICAgICAxLCAgIDAsICAgIDEwLCAgIDAuMDEpOwpzdGVyZW9fcGhhc2Vfc3ByZWFkPSBoc2xpZGVyKCIzX1BoYXNlLzA0X3N0ZXJlb19waGFzZV9zcHJlYWRbdG9vbHRpcDpTdGVyZW8gc3ByZWFkXSIsICAgICAgMC4xLCAwLCAgICAxLCAgICAwLjAwMSk7Cm5vaXNlX2Zsb29yICAgICAgICA9IGhzbGlkZXIoIjNfUGhhc2UvMDVfbm9pc2VfZmxvb3JbdG9vbHRpcDpOb2lzZSBmbG9vcl0iLCAgICAgICAgICAgICAgICAwLCAgIDAsICAgIDAuMiwgIDAuMDAxKTsKbm9pc2VfY29sb3IgICAgICAgID0gaHNsaWRlcigiM19QaGFzZS8wNl9ub2lzZV9jb2xvclt0b29sdGlwOk5vaXNlIGNvbG91cl0iLCAgICAgICAgICAgICAgIDAuNSwgMCwgICAgMSwgICAgMC4wMDEpOwpzdWJfaGFybW9uaWMgICAgICAgPSBoc2xpZGVyKCIzX1BoYXNlLzA3X3N1Yl9oYXJtb25pY1t0b29sdGlwOlN1Yi1oYXJtb25pY10iLCAgICAgICAgICAgICAgMCwgICAwLCAgICAxLCAgICAwLjAwMSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gR3JvdXAgNCDigJQgTW9kdWxhdGlvbiAocGFyYW1zIDMy4oCTNDEpCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQp2aWJyYXRvX3JhdGUgID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzAwX3ZpYnJhdG9fcmF0ZVt0b29sdGlwOlZpYnJhdG8gcmF0ZV0iLCAgIDUsICAgMCwgIDEwLCAgIDAuMDEpOwp2aWJyYXRvX2RlcHRoID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzAxX3ZpYnJhdG9fZGVwdGhbdG9vbHRpcDpWaWJyYXRvIGRlcHRoXSIsIDAsICAgMCwgIDAuMDUsIDAuMDAwMSk7CnZpYnJhdG9fZGVsYXkgPSBoc2xpZGVyKCI0X01vZHVsYXRpb24vMDJfdmlicmF0b19kZWxheVt0b29sdGlwOlZpYnJhdG8gZGVsYXldIiwgMC4zLCAwLCAgMiwgICAgMC4wMDEpOwp0cmVtb2xvX3JhdGUgID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzAzX3RyZW1vbG9fcmF0ZVt0b29sdGlwOlRyZW1vbG8gcmF0ZV0iLCAgIDQsICAgMCwgIDIwLCAgIDAuMDEpOwp0cmVtb2xvX2RlcHRoID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzA0X3RyZW1vbG9fZGVwdGhbdG9vbHRpcDpUcmVtb2xvIGRlcHRoXSIsIDAsICAgMCwgIDEsICAgIDAuMDAxKTsKZHJpZnRfcmF0ZSAgICA9IGhzbGlkZXIoIjRfTW9kdWxhdGlvbi8wNV9kcmlmdF9yYXRlW3Rvb2x0aXA6RHJpZnQgcmF0ZV0iLCAgICAgICAwLCAgIDAsICAyLCAgICAwLjAwMSk7CmRyaWZ0X2RlcHRoICAgPSBoc2xpZGVyKCI0X01vZHVsYXRpb24vMDZfZHJpZnRfZGVwdGhbdG9vbHRpcDpEcmlmdCBkZXB0aF0iLCAgICAgMCwgICAwLCAgMC4zLCAgMC4wMDEpOwpmb3JtYW50MV9mcmVxID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzA3X2Zvcm1hbnQxX2ZyZXFbdG9vbHRpcDpGb3JtYW50IDEgZnJlcV0iLDMsICAgMSwgIDE2LCAgIDAuMDEpOwpmb3JtYW50Ml9mcmVxID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzA4X2Zvcm1hbnQyX2ZyZXFbdG9vbHRpcDpGb3JtYW50IDIgZnJlcV0iLDYsICAgMSwgIDE2LCAgIDAuMDEpOwpmb3JtYW50X2RlcHRoID0gaHNsaWRlcigiNF9Nb2R1bGF0aW9uLzA5X2Zvcm1hbnRfZGVwdGhbdG9vbHRpcDpGb3JtYW50IGRlcHRoXSIsIDAsICAgMCwgIDEsICAgIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCA1IOKAlCBNYXN0ZXIgKHBhcmFtcyA0MuKAkzQ3KQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KbGV2ZWwgICAgICA9IGhzbGlkZXIoIjVfTWFzdGVyLzAwX2xldmVsW3Rvb2x0aXA6T3V0cHV0IGxldmVsXSIsICAgICAgIDAuNywgIDAsICAgMSwgICAwLjAwMSk7CnZlbF9zZW5zICAgPSBoc2xpZGVyKCI1X01hc3Rlci8wMV92ZWxfc2Vuc1t0b29sdGlwOlZlbG9jaXR5IHNlbnNdIiwgICAwLjUsICAwLCAgIDEsICAgMC4wMDEpOwp2ZWxfYnJpZ2h0ID0gaHNsaWRlcigiNV9NYXN0ZXIvMDJfdmVsX2JyaWdodG5lc3NbdG9vbHRpcDpWZWwgYnJpZ2h0XSIsMC4zLCAgMCwgICAxLCAgIDAuMDAxKTsKcGl0Y2hfZ2xpZGU9IGhzbGlkZXIoIjVfTWFzdGVyLzAzX3BpdGNoX2dsaWRlW3Rvb2x0aXA6UG9ydGFtZW50b10iLCAgIDAsICAgIDAsICAgMTAsICAwLjAxKTsKc2F0dXJhdGlvbiA9IGhzbGlkZXIoIjVfTWFzdGVyLzA0X3NhdHVyYXRpb25bdG9vbHRpcDpTYXR1cmF0aW9uXSIsICAgIDAsICAgIDAsICAgMSwgICAwLjAwMSk7CmZpbmVfdHVuZSAgPSBoc2xpZGVyKCI1X01hc3Rlci8wNV9maW5lX3R1bmVbdW5pdDpjdF1bdG9vbHRpcDpDZW50c10iLCAwLCAgIC01MCwgIDUwLCAgMC4xKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBJbnRlcm5hbCBjb25zdGFudHMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCk4gID0gNjQ7ClBJID0gbWEuUEk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gUGl0Y2gg4oCUIHBvcnRhbWVudG8gKyBmaW5lIHR1bmUKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmdsaWRlX3RhdSAgID0gMC4wMDAxICsgcGl0Y2hfZ2xpZGUgKiAwLjI7CmZpbmVfcmF0aW8gID0gcG93KDIuMCwgZmluZV90dW5lIC8gMTIwMC4wKTsKZnJlcV9zbW9vdGggPSBmcmVxICogZmluZV9yYXRpbyA6IHNpLnNtb290aChiYS50YXUycG9sZShnbGlkZV90YXUpKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBWaWJyYXRvIExGTyDigJQgZGVsYXllZCBvbnNldCB2aWEgc2xvdy1hdHRhY2sgZW52ZWxvcGUKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnZpYnJhdG9fZW52ID0gZW4uYWRzcih2aWJyYXRvX2RlbGF5LCAwLjAxLCAxLjAsIDAuNSwgZ2F0ZSk7CnZpYnJhdG9fbGZvID0gb3Mub3NjKHZpYnJhdG9fcmF0ZSkgKiB2aWJyYXRvX2RlcHRoICogdmlicmF0b19lbnY7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gVHJlbW9sbyBMRk8KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnRyZW1vbG9fbGZvID0gMS4wIC0gdHJlbW9sb19kZXB0aCAqIDAuNSAqICgxLjAgKyBvcy5vc2ModHJlbW9sb19yYXRlKSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gR2xvYmFsIGFtcGxpdHVkZSBBRFNSCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQphbXBfZW52ID0gZW4uYWRzcihhdHRhY2ssIGRlY2F5LCBzdXN0YWluLCByZWxlYXNlLCBnYXRlKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBCcmlnaHRuZXNzIGVudmVsb3BlIOKAlCBjb250cm9scyBoaWdoIGhhcm1vbmljIGFtcGxpdHVkZSBvdmVyIHRpbWUKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmJyaWdodF9lbnYgICA9IGVuLmFkc3IoYnJpZ2h0bmVzc19hdHRhY2ssIGJyaWdodG5lc3NfZGVjYXksCiAgICAgICAgICAgICAgICAgICAgICAgIGJyaWdodG5lc3Nfc3VzdGFpbiwgYnJpZ2h0bmVzc19yZWxlYXNlLCBnYXRlKTsKYnJpZ2h0X2JsZW5kKGspID0gZmxvYXQoayAtIDEpIC8gZmxvYXQoTiAtIDEpOwpicmlnaHRfZmFjdG9yKGspID0gMS4wIC0gYnJpZ2h0X2JsZW5kKGspICsgYnJpZ2h0X2JsZW5kKGspICogYnJpZ2h0X2VudjsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTcGVjdHJhbCBmbHV4IOKAlCBzbG93IExGTyBvbiB1cHBlciBoYXJtb25pYyBhbXBsaXR1ZGVzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmbHV4X2xmbyA9IG9zLm9zYyhzcGVjdHJhbF9mbHV4X3JhdGUpOwpmbHV4X2ZhY3RvcihrKSA9IGJhLmlmKGsgPiA4LAogICAgICAgICAgICAgICAgICAgMS4wICsgc3BlY3RyYWxfZmx1eF9kZXB0aCAqIGZsdXhfbGZvICogYnJpZ2h0X2JsZW5kKGspLAogICAgICAgICAgICAgICAgICAgMS4wKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBEcmlmdCDigJQgc2xvdyByYW5kb20gd2FsayBvbiBwZXItcGFydGlhbCBhbXBsaXR1ZGVzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpkcmlmdF9sZm8oaykgPSBuby5ub2lzZSAqIChmbG9hdChrICUgNyArIDEpIC8gNy4wKQogICAgICAgICAgICAgICA6IGZpLmxvd3Bhc3MoMSwgbWF4KDAuMSwgZHJpZnRfcmF0ZSAqIDIuMCkpCiAgICAgICAgICAgICAgIDogKihkcmlmdF9kZXB0aCk7CmRyaWZ0X2ZhY3RvcihrKSA9IDEuMCArIGRyaWZ0X2xmbyhrKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBQZXItaGFybW9uaWMgYmFzZSBhbXBsaXR1ZGUgZnJvbSBncm91cCBzbGlkZXJzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpncm91cF9hbXAoaykgPQogIGJhLmlmKGsgPT0gMSwgaDFfYW1wLAogIGJhLmlmKGsgPT0gMiwgaDJfYW1wLAogIGJhLmlmKGsgPT0gMywgaDNfYW1wLAogIGJhLmlmKGsgPT0gNCwgaDRfYW1wLAogIGJhLmlmKGsgPT0gNSwgaDVfYW1wLAogIGJhLmlmKGsgPT0gNiwgaDZfYW1wLAogIGJhLmlmKGsgPT0gNywgaDdfYW1wLAogIGJhLmlmKGsgPT0gOCwgaDhfYW1wLAogIGJhLmlmKGsgPD0gMTYsIGg5XzE2X2FtcCwKICBiYS5pZihrIDw9IDMyLCBoMTdfMzJfYW1wLAogICAgICAgICAgICAgICAgIGgzM182NF9hbXApKSkpKSkpKSkpOwoKLy8gU3BlY3RyYWwgdGlsdDogYW1wICo9IGtedGlsdCAoaz0xIGlzIGFsd2F5cyB1bml0eSkKdGlsdF9mYWN0b3IoaykgPSBwb3coZmxvYXQoayksIHNwZWN0cmFsX3RpbHQpOwoKLy8gT2RkL2V2ZW4gYmFsYW5jZTogw5cyIHNvIHVuaXR5IGdhaW4gYXQgb2RkX2V2ZW49MC41Cm9kZF93ZWlnaHQgID0gKDEuMCAtIG9kZF9ldmVuKSAqIDIuMDsKZXZlbl93ZWlnaHQgPSBvZGRfZXZlbiAqIDIuMDsKb2RkX2V2ZW5fZmFjdG9yKGspID0gYmEuaWYoayAlIDIgPT0gMCwgZXZlbl93ZWlnaHQsIG9kZF93ZWlnaHQpOwoKLy8gQ29tYmluZWQgcmF3IGFtcGxpdHVkZQpoYXJtX2FtcF9yYXcoaykgPSBncm91cF9hbXAoaykgKiB0aWx0X2ZhY3RvcihrKSAqIG9kZF9ldmVuX2ZhY3RvcihrKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBGb3JtYW50IHNoYXBpbmcg4oCUIHR3byBHYXVzc2lhbiBidW1wcyBpbiBoYXJtb25pYy1pbmRleCBzcGFjZQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0Kc2lnbWFfc3EgPSAxLjUgKiAxLjU7CmZvcm1hbnRfYnVtcChrLCBjdHIpID0gZXhwKC0wLjUgKiAoZmxvYXQoaykgLSBjdHIpICogKGZsb2F0KGspIC0gY3RyKSAvIHNpZ21hX3NxKTsKZm9ybWFudF9mYWN0b3IoaykgPQogIDEuMCArIGZvcm1hbnRfZGVwdGggKiAoZm9ybWFudF9idW1wKGssIGZvcm1hbnQxX2ZyZXEpICsgZm9ybWFudF9idW1wKGssIGZvcm1hbnQyX2ZyZXEpKTsKCmhhcm1fYW1wKGspID0gaGFybV9hbXBfcmF3KGspICogZm9ybWFudF9mYWN0b3Ioayk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gSW5oYXJtb25pYyBwYXJ0aWFsIGZyZXF1ZW5jeTogZnJlcV9rID0gaypmMCooMSArIEIqKGteMiAtIDEpKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KaGFybV9mcmVxKGspID0gZnJlcV9zbW9vdGggKiBmbG9hdChrKQogICAgICAgICAgICAgICAqICgxLjAgKyBpbmhhcm1vbmljaXR5ICogKGZsb2F0KGspICogZmxvYXQoaykgLSAxLjApKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBQaGFzZSByYW5kb21pc2F0aW9uIOKAlCBhZGRzIHBlci1oYXJtb25pYyBub2lzZSB0byB0aGUgb3NjaWxsYXRvciBmcmVxdWVuY3ksCi8vIGdyYWR1YWxseSBkZXBoYXNpbmcgcGFydGlhbHMgKHBoYXNlX3JhbmRvbT0wOiBwaGFzZS1sb2NrZWQsID0xOiByYW5kb20pCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpwaGFzZV9yYW5kX2xmbyhrKSA9IG5vLm5vaXNlICogKGZsb2F0KChrICogMTcgKyAzKSAlIDMxICsgMSkgLyAzMS4wKQogICAgICAgICAgICAgICAgICAgIDogc2kuc21vb3RoKGJhLnRhdTJwb2xlKDAuMDUpKQogICAgICAgICAgICAgICAgICAgIDogKihwaGFzZV9yYW5kb20gKiBoYXJtX2ZyZXEoaykgKiAwLjAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBQaGFzZSByYW5kb20gd2FsayDigJQgc2xvd2VyIGluZGVwZW5kZW50IGRyaWZ0IHBlciBwYXJ0aWFsCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpwaGFzZV93YWxrKGspID0gbm8ubm9pc2UgKiAoZmxvYXQoayAlIDcgKyAxKSAvIDcuMCkKICAgICAgICAgICAgICAgIDogc2kuc21vb3RoKGJhLnRhdTJwb2xlKDAuMSkpCiAgICAgICAgICAgICAgICA6ICoocGhhc2Vfd2Fsa19yYXRlKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBJbnRlci1wYXJ0aWFsIGJlYXRpbmcg4oCUIHNpbnVzb2lkYWwgZGV0dW5pbmcgc3RhZ2dlcgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KYmVhdGluZ19vZmZzZXQoaykgPSBiZWF0aW5nX2RlcHRoCiAgICAgICAgICAgICAgICAgICAgKiBvcy5vc2MoYmVhdGluZ19yYXRlICogZmxvYXQoayAlIDMgKyAxKSAqIDAuNykKICAgICAgICAgICAgICAgICAgICAqIGhhcm1fZnJlcShrKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTdGVyZW8gc3ByZWFkIOKAlCBSIGNoYW5uZWwgZ2V0cyBhIHNtYWxsIHBlci1oYXJtb25pYyBwaXRjaCBvZmZzZXQKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnN0ZXJlb19zcHJlYWRfZnJlcShrKSA9IHN0ZXJlb19waGFzZV9zcHJlYWQgKiBmbG9hdChrKSAqIDAuMDE7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gQWRkaXRpdmUgb3NjaWxsYXRvciBzdW1zIOKAlCBMIGFuZCBSCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQphZGRpdGl2ZV9MID0gc3VtKGssIE4sCiAgaGFybV9hbXAoaysxKSAqIGJyaWdodF9mYWN0b3IoaysxKSAqIGZsdXhfZmFjdG9yKGsrMSkgKiBkcmlmdF9mYWN0b3IoaysxKSAqCiAgb3Mub3NjKCAgaGFybV9mcmVxKGsrMSkgKiAoMS4wICsgdmlicmF0b19sZm8pCiAgICAgICAgICsgYmVhdGluZ19vZmZzZXQoaysxKQogICAgICAgICArIHBoYXNlX3JhbmRfbGZvKGsrMSkKICAgICAgICAgKyBwaGFzZV93YWxrKGsrMSkKICApCik7CgphZGRpdGl2ZV9SID0gc3VtKGssIE4sCiAgaGFybV9hbXAoaysxKSAqIGJyaWdodF9mYWN0b3IoaysxKSAqIGZsdXhfZmFjdG9yKGsrMSkgKiBkcmlmdF9mYWN0b3IoaysxKSAqCiAgb3Mub3NjKCAgaGFybV9mcmVxKGsrMSkgKiAoMS4wICsgdmlicmF0b19sZm8gKyBzdGVyZW9fc3ByZWFkX2ZyZXEoaysxKSkKICAgICAgICAgKyBiZWF0aW5nX29mZnNldChrKzEpCiAgICAgICAgICsgcGhhc2VfcmFuZF9sZm8oaysxKQogICAgICAgICArIHBoYXNlX3dhbGsoaysxKQogICkKKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTdWItaGFybW9uaWMgKDAuNcOXIGZ1bmRhbWVudGFsKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0Kc3ViX29zYyA9IHN1Yl9oYXJtb25pYyAqIG9zLm9zYyhmcmVxX3Ntb290aCAqIDAuNSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gTm9pc2UgZmxvb3Ig4oCUIGNvbG91cmVkIHZpYSBvbmUtcG9sZSBMUCAobm9pc2VfY29sb3I9MOKGkndoaXRlLCA9MeKGkmRhcmspCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpub2lzZV9scF9jdXRvZmYgPSAyMDAuMCArICgxLjAgLSBub2lzZV9jb2xvcikgKiAxOTgwMC4wOwpub2lzZV9zaWduYWwgICAgPSBuby5ub2lzZSA6IGZpLmxvd3Bhc3MoMSwgbm9pc2VfbHBfY3V0b2ZmKTsKbm9pc2Vfb3V0ICAgICAgID0gbm9pc2VfZmxvb3IgKiBub2lzZV9zaWduYWw7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gU29mdC1jbGlwIHNhdHVyYXRpb24g4oCUIHRhbmggd2F2ZXNoYXBlcgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KZHJpdmUgICAgICAgPSAxLjAgKyBzYXR1cmF0aW9uICogOS4wOwpzb2Z0Y2xpcCh4KSA9IG1hLnRhbmgoeCAqIGRyaXZlKSAvIGRyaXZlOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIE91dHB1dCBnYWluCi8vIHZlbF9icmlnaHQgbW9kdWxhdGVzIGhvdyBtdWNoIHRoZSBicmlnaHRuZXNzIGVudmVsb3BlIGJvb3N0cyB0aGUgb3ZlcmFsbAovLyBsZXZlbCBkdXJpbmcgbm90ZSBvbnNldCAoY291cGxlcyB2ZWxvY2l0eSBzZW5zaXRpdml0eSB0byBicmlnaHRuZXNzKS4KLy8gSGVyZSBpdCBhY3RzIGFzIGEgc3VidGxlIG1pZC10ZXJtIGdhaW4gc2hhcGVyIHZpYSB0aGUgYnJpZ2h0X2VudiBzaWduYWwuCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQp2ZWxfYnJpZ2h0X2Jvb3N0ID0gMS4wICsgdmVsX2JyaWdodCAqIGJyaWdodF9lbnYgKiAwLjM7Cm91dF9nYWluID0gbGV2ZWwgKiAoMS4wIC0gdmVsX3NlbnMgKiAwLjMpICogdmVsX2JyaWdodF9ib29zdDsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBGaW5hbCBhc3NlbWJseQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0Kc2lnbmFsX0wgPSAoYWRkaXRpdmVfTCArIHN1Yl9vc2MgKyBub2lzZV9vdXQpICogYW1wX2VudiAqIHRyZW1vbG9fbGZvICogb3V0X2dhaW47CnNpZ25hbF9SID0gKGFkZGl0aXZlX1IgKyBzdWJfb3NjICsgbm9pc2Vfb3V0KSAqIGFtcF9lbnYgKiB0cmVtb2xvX2xmbyAqIG91dF9nYWluOwoKcHJvY2VzcyA9IHNvZnRjbGlwKHNpZ25hbF9MKSwgc29mdGNsaXAoc2lnbmFsX1IpOwo=","inputs": 0,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn additive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "envelopes.lib/adsr:author": "Yann Orlarey and Andrey Bundin" },{ "envelopes.lib/author": "GRAME" },{ "envelopes.lib/copyright": "GRAME" },{ "envelopes.lib/license": "LGPL with exception" },{ "envelopes.lib/name": "Faust Envelope Library" },{ "envelopes.lib/version": "1.3.0" },{ "filename": "additive.dsp" },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "additive" },{ "noises.lib/name": "Faust Noise Generator Library" },{ "noises.lib/version": "1.4.1" },{ "oscillators.lib/name": "Faust Oscillator Library" },{ "oscillators.lib/version": "1.6.0" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "additive","items": [ {"type": "hslider","label": "0_Hidden/freq","varname": "fHslider19","shortname": "0_Hidden_freq","address": "/additive/0_Hidden_freq","index": 262336,"meta": [{ "hidden": "1" },{ "unit": "Hz" }],"init": 220,"min": 20,"max": 4000,"step": 0.01},{"type": "button","label": "0_Hidden/gate","varname": "fButton0","shortname": "0_Hidden_gate","address": "/additive/0_Hidden_gate","index": 262152,"meta": [{ "hidden": "1" }]},{"type": "hslider","label": "1_Spectral Shape/00_h1_amp","varname": "fHslider41","shortname": "1_Spectral_Shape_00_h1_amp","address": "/additive/1_Spectral_Shape_00_h1_amp","index": 263488,"meta": [{ "tooltip": "H1 amplitude" }],"init": 0.8,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/01_h2_amp","varname": "fHslider40","shortname": "1_Spectral_Shape_01_h2_amp","address": "/additive/1_Spectral_Shape_01_h2_amp","index": 263476,"meta": [{ "tooltip": "H2 amplitude" }],"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/02_h3_amp","varname": "fHslider39","shortname": "1_Spectral_Shape_02_h3_amp","address": "/additive/1_Spectral_Shape_02_h3_amp","index": 263464,"meta": [{ "tooltip": "H3 amplitude" }],"init": 0.35,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/03_h4_amp","varname": "fHslider38","shortname": "1_Spectral_Shape_03_h4_amp","address": "/additive/1_Spectral_Shape_03_h4_amp","index": 263452,"meta": [{ "tooltip": "H4 amplitude" }],"init": 0.25,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/04_h5_amp","varname": "fHslider37","shortname": "1_Spectral_Shape_04_h5_amp","address": "/additive/1_Spectral_Shape_04_h5_amp","index": 263440,"meta": [{ "tooltip": "H5 amplitude" }],"init": 0.18,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/05_h6_amp","varname": "fHslider36","shortname": "1_Spectral_Shape_05_h6_amp","address": "/additive/1_Spectral_Shape_05_h6_amp","index": 263428,"meta": [{ "tooltip": "H6 amplitude" }],"init": 0.12,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/06_h7_amp","varname": "fHslider35","shortname": "1_Spectral_Shape_06_h7_amp","address": "/additive/1_Spectral_Shape_06_h7_amp","index": 263416,"meta": [{ "tooltip": "H7 amplitude" }],"init": 0.08,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/07_h8_amp","varname": "fHslider34","shortname": "1_Spectral_Shape_07_h8_amp","address": "/additive/1_Spectral_Shape_07_h8_amp","index": 263404,"meta": [{ "tooltip": "H8 amplitude" }],"init": 0.06,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/08_h9_16_amp","varname": "fHslider33","shortname": "1_Spectral_Shape_08_h9_16_amp","address": "/additive/1_Spectral_Shape_08_h9_16_amp","index": 263336,"meta": [{ "tooltip": "H9-16 group amp" }],"init": 0.05,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/09_h17_32_amp","varname": "fHslider32","shortname": "1_Spectral_Shape_09_h17_32_amp","address": "/additive/1_Spectral_Shape_09_h17_32_amp","index": 263204,"meta": [{ "tooltip": "H17-32 group amp" }],"init": 0.025,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/10_h33_64_amp","varname": "fHslider30","shortname": "1_Spectral_Shape_10_h33_64_amp","address": "/additive/1_Spectral_Shape_10_h33_64_amp","index": 262436,"meta": [{ "tooltip": "H33-64 group amp" }],"init": 0.01,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/11_spectral_tilt","varname": "fHslider29","shortname": "1_Spectral_Shape_11_spectral_tilt","address": "/additive/1_Spectral_Shape_11_spectral_tilt","index": 262432,"meta": [{ "tooltip": "Global tilt" }],"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral Shape/12_inharmonicity","varname": "fHslider20","shortname": "1_Spectral_Shape_12_inharmonicity","address": "/additive/1_Spectral_Shape_12_inharmonicity","index": 262348,"meta": [{ "tooltip": "Inharmonicity" }],"init": 0,"min": 0,"max": 0.15,"step": 0.0001},{"type": "hslider","label": "1_Spectral Shape/13_odd_even","varname": "fHslider31","shortname": "1_Spectral_Shape_13_odd_even","address": "/additive/1_Spectral_Shape_13_odd_even","index": 262440,"meta": [{ "tooltip": "Odd/even balance" }],"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Temporal/00_attack","varname": "fHslider8","shortname": "2_Temporal_00_attack","address": "/additive/2_Temporal_00_attack","index": 262232,"meta": [{ "scale": "log" },{ "tooltip": "Attack" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "2_Temporal/01_decay","varname": "fHslider9","shortname": "2_Temporal_01_decay","address": "/additive/2_Temporal_01_decay","index": 262236,"meta": [{ "tooltip": "Decay" }],"init": 0.3,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "2_Temporal/02_sustain","varname": "fHslider10","shortname": "2_Temporal_02_sustain","address": "/additive/2_Temporal_02_sustain","index": 262240,"meta": [{ "tooltip": "Sustain" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Temporal/03_release","varname": "fHslider7","shortname": "2_Temporal_03_release","address": "/additive/2_Temporal_03_release","index": 262228,"meta": [{ "tooltip": "Release" }],"init": 0.5,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "2_Temporal/04_brightness_attack","varname": "fHslider1","shortname": "2_Temporal_04_brightness_attack","address": "/additive/2_Temporal_04_brightness_attack","index": 262192,"meta": [{ "scale": "log" },{ "tooltip": "Bright A" }],"init": 0.005,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "2_Temporal/05_brightness_decay","varname": "fHslider2","shortname": "2_Temporal_05_brightness_decay","address": "/additive/2_Temporal_05_brightness_decay","index": 262196,"meta": [{ "tooltip": "Bright D" }],"init": 0.15,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "2_Temporal/06_brightness_sustain","varname": "fHslider3","shortname": "2_Temporal_06_brightness_sustain","address": "/additive/2_Temporal_06_brightness_sustain","index": 262200,"meta": [{ "tooltip": "Bright S" }],"init": 0.4,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Temporal/07_brightness_release","varname": "fHslider0","shortname": "2_Temporal_07_brightness_release","address": "/additive/2_Temporal_07_brightness_release","index": 262172,"meta": [{ "tooltip": "Bright R" }],"init": 0.3,"min": 0.01,"max": 5,"step": 0.001},{"type": "hslider","label": "2_Temporal/08_spectral_flux_rate","varname": "fHslider22","shortname": "2_Temporal_08_spectral_flux_rate","address": "/additive/2_Temporal_08_spectral_flux_rate","index": 262384,"meta": [{ "tooltip": "Flux rate" }],"init": 0.5,"min": 0,"max": 10,"step": 0.01},{"type": "hslider","label": "2_Temporal/09_spectral_flux_depth","varname": "fHslider23","shortname": "2_Temporal_09_spectral_flux_depth","address": "/additive/2_Temporal_09_spectral_flux_depth","index": 262396,"meta": [{ "tooltip": "Flux depth" }],"init": 0.1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Phase/00_phase_random","varname": "fHslider11","shortname": "3_Phase_00_phase_random","address": "/additive/3_Phase_00_phase_random","index": 262280,"meta": [{ "tooltip": "Phase randomisation" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Phase/01_phase_walk_rate","varname": "fHslider21","shortname": "3_Phase_01_phase_walk_rate","address": "/additive/3_Phase_01_phase_walk_rate","index": 262372,"meta": [{ "tooltip": "Phase walk rate" }],"init": 0,"min": 0,"max": 5,"step": 0.001},{"type": "hslider","label": "3_Phase/02_beating_depth","varname": "fHslider13","shortname": "3_Phase_02_beating_depth","address": "/additive/3_Phase_02_beating_depth","index": 262300,"meta": [{ "tooltip": "Beating depth" }],"init": 0,"min": 0,"max": 0.02,"step": 0.0001},{"type": "hslider","label": "3_Phase/03_beating_rate","varname": "fHslider12","shortname": "3_Phase_03_beating_rate","address": "/additive/3_Phase_03_beating_rate","index": 262284,"meta": [{ "tooltip": "Beating rate" }],"init": 1,"min": 0,"max": 10,"step": 0.01},{"type": "hslider","label": "3_Phase/04_stereo_phase_spread","varname": "fHslider48","shortname": "3_Phase_04_stereo_phase_spread","address": "/additive/3_Phase_04_stereo_phase_spread","index": 263536,"meta": [{ "tooltip": "Stereo spread" }],"init": 0.1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Phase/05_noise_floor","varname": "fHslider43","shortname": "3_Phase_05_noise_floor","address": "/additive/3_Phase_05_noise_floor","index": 263504,"meta": [{ "tooltip": "Noise floor" }],"init": 0,"min": 0,"max": 0.2,"step": 0.001},{"type": "hslider","label": "3_Phase/06_noise_color","varname": "fHslider42","shortname": "3_Phase_06_noise_color","address": "/additive/3_Phase_06_noise_color","index": 263492,"meta": [{ "tooltip": "Noise colour" }],"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Phase/07_sub_harmonic","varname": "fHslider44","shortname": "3_Phase_07_sub_harmonic","address": "/additive/3_Phase_07_sub_harmonic","index": 263520,"meta": [{ "tooltip": "Sub-harmonic" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Modulation/00_vibrato_rate","varname": "fHslider15","shortname": "4_Modulation_00_vibrato_rate","address": "/additive/4_Modulation_00_vibrato_rate","index": 262312,"meta": [{ "tooltip": "Vibrato rate" }],"init": 5,"min": 0,"max": 10,"step": 0.01},{"type": "hslider","label": "4_Modulation/01_vibrato_depth","varname": "fHslider16","shortname": "4_Modulation_01_vibrato_depth","address": "/additive/4_Modulation_01_vibrato_depth","index": 262324,"meta": [{ "tooltip": "Vibrato depth" }],"init": 0,"min": 0,"max": 0.05,"step": 0.0001},{"type": "hslider","label": "4_Modulation/02_vibrato_delay","varname": "fHslider14","shortname": "4_Modulation_02_vibrato_delay","address": "/additive/4_Modulation_02_vibrato_delay","index": 262308,"meta": [{ "tooltip": "Vibrato delay" }],"init": 0.3,"min": 0,"max": 2,"step": 0.001},{"type": "hslider","label": "4_Modulation/03_tremolo_rate","varname": "fHslider5","shortname": "4_Modulation_03_tremolo_rate","address": "/additive/4_Modulation_03_tremolo_rate","index": 262208,"meta": [{ "tooltip": "Tremolo rate" }],"init": 4,"min": 0,"max": 20,"step": 0.01},{"type": "hslider","label": "4_Modulation/04_tremolo_depth","varname": "fHslider6","shortname": "4_Modulation_04_tremolo_depth","address": "/additive/4_Modulation_04_tremolo_depth","index": 262224,"meta": [{ "tooltip": "Tremolo depth" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Modulation/05_drift_rate","varname": "fHslider24","shortname": "4_Modulation_05_drift_rate","address": "/additive/4_Modulation_05_drift_rate","index": 262400,"meta": [{ "tooltip": "Drift rate" }],"init": 0,"min": 0,"max": 2,"step": 0.001},{"type": "hslider","label": "4_Modulation/06_drift_depth","varname": "fHslider25","shortname": "4_Modulation_06_drift_depth","address": "/additive/4_Modulation_06_drift_depth","index": 262416,"meta": [{ "tooltip": "Drift depth" }],"init": 0,"min": 0,"max": 0.3,"step": 0.001},{"type": "hslider","label": "4_Modulation/07_formant1_freq","varname": "fHslider27","shortname": "4_Modulation_07_formant1_freq","address": "/additive/4_Modulation_07_formant1_freq","index": 262424,"meta": [{ "tooltip": "Formant 1 freq" }],"init": 3,"min": 1,"max": 16,"step": 0.01},{"type": "hslider","label": "4_Modulation/08_formant2_freq","varname": "fHslider26","shortname": "4_Modulation_08_formant2_freq","address": "/additive/4_Modulation_08_formant2_freq","index": 262420,"meta": [{ "tooltip": "Formant 2 freq" }],"init": 6,"min": 1,"max": 16,"step": 0.01},{"type": "hslider","label": "4_Modulation/09_formant_depth","varname": "fHslider28","shortname": "4_Modulation_09_formant_depth","address": "/additive/4_Modulation_09_formant_depth","index": 262428,"meta": [{ "tooltip": "Formant depth" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "5_Master/00_level","varname": "fHslider47","shortname": "5_Master_00_level","address": "/additive/5_Master_00_level","index": 263532,"meta": [{ "tooltip": "Output level" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "5_Master/01_vel_sens","varname": "fHslider46","shortname": "5_Master_01_vel_sens","address": "/additive/5_Master_01_vel_sens","index": 263528,"meta": [{ "tooltip": "Velocity sens" }],"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "5_Master/02_vel_brightness","varname": "fHslider4","shortname": "5_Master_02_vel_brightness","address": "/additive/5_Master_02_vel_brightness","index": 262204,"meta": [{ "tooltip": "Vel bright" }],"init": 0.3,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "5_Master/03_pitch_glide","varname": "fHslider17","shortname": "5_Master_03_pitch_glide","address": "/additive/5_Master_03_pitch_glide","index": 262328,"meta": [{ "tooltip": "Portamento" }],"init": 0,"min": 0,"max": 10,"step": 0.01},{"type": "hslider","label": "5_Master/04_saturation","varname": "fHslider45","shortname": "5_Master_04_saturation","address": "/additive/5_Master_04_saturation","index": 263524,"meta": [{ "tooltip": "Saturation" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "5_Master/05_fine_tune","varname": "fHslider18","shortname": "5_Master_05_fine_tune","address": "/additive/5_Master_05_fine_tune","index": 262332,"meta": [{ "tooltip": "Cents" },{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/additive.wasm b/playground/faust/additive.wasm deleted file mode 100644 index 84a582a431034938805b22a259af525d11b6d07f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94610 zcmce93wTw<)ppLw%?e6vZLO_s=h)h!^%AayR+JEKQG#-ZsFj332qX|mxLWH00t5)R zaJU*UNRV(xE*b?jDk>@}Dk>^!R8&;dsHmu@|GU<#nZ0M9|M}X#Pya8^p1t59Qeh}mL856EcB%JG z#lCs!u(8D@1gJg)QV8ztr-cC zB&0A>BFV(g;m#Q+=BKG}M!0NLMnd~=0uDo?B4H}&nbf&?xOu2qvu1>6>?vRqJ!~Rj z6RE_(CNVZ4Bh)?&;~5DV2`$4X>7Zpex*%;-Ml_losh!E0I586<%c;F7`glcH z%atN;6Im^CmB`g1Zx>l3a*fD4M6MNCD{`I4J4LP+StoLX$c-X5iQFu5i^#3ybE@`r zo0M%Ad6&o?B6o`1C33gOyG7n3vR>r9BJUHqN910S_ltZ$80tCGvZbKZyKMWHeEqk5ffX6FFUExyTtJXNsI9 zvO?r+k#j`O6Raa*@a?k&8vfL@p7zROB*|%SEmbxl-h9BCAEN61iIB z?ILSLt`T{M$h9JCMXnQhr^xjp>qKr4xl!aMk()(s5xG_5Hj&#!-X(H}$ekj0iQFyn zZjtwhtQUE&$ooX@5xH07{URR_`Jl)Kkq?P{SmYxj_lbN|zUxTSaaYxn1O4B6o<~DRP&{-6HQ6d5_3?k@t$cPvjnvdqv(a z@&S<#ifj=1kjRHcJ|c3T$VWv!Ch~ES`$aw>@=1|Ti98_kX_3!}d{*Q^kIUgQfR z4~cwHpVrl%o90Z1P_h zO3GJ@yj^6C$TcGG5V=-lt;ls`?`))>YkPM}`FfFcA~%TKC~}j?%_6sm+$wUL$n7HU z61hXw}T-xc|u$oEAa6ZwJ24@G_?^0>&4MSddkQ;|&~KNI=6$S*{m z5c#FZuS9+=@}$UbM1CvsJCUbEelPL|kw1!z%KzR|MNShrU1Yh)86szjoF%eC>Sd{pFPA|DsIU*r=apA`9&$O9su7Ws_G zXGI5oW%LTZV0A<{)ie@40(=@O(%kuF0@M{0%C8mSFZTcmbK?UDY1gb}?+ zN2I?Z{SE1Iq)tdzAYF;n87Tv)3sNRhSEO!8-I01A^+d`-x(exPq+UqbNWGD+LAn;H z4^m&Gen|b11|SVY8iX_$X$Vpd(sfAJBi(@XccdGUh9c!6?k@MzLkxe2$6ZyHwFGQXY`K8FOM1C#uq{weXek<}jk*7p{FY*VGKZ=aX zIOkN6(?m`eSuS#h$eAK%iL4MgTjU&(b46B)oF{U=$OR%7id-bJO5|dZF_B9|E)}^< zx=qTO!V-A%iDXjf0Wduewc?e@@aFYWH9-2=3Hkai8U zdx&-q)9w-4?W5hJw0n$pkJD~H?Vh0BleBw^b_ZzpH0_?D-LteiNW14~_dM-hpxq(b zy-2&4X!kPh4%6-x+PzA<*JyWycCXX!4cfg)yGGi*MZ33Y_YUoj((YZ_y+^zEX?Kiv zAJFbY+I>X3uyBV~bNxNCJtDxO%+RdTeT-sIAZXWIC({2In7Se7J z?W$DnlE`YVyH9gAP>G%dV#b4%=gFXW{|;$;JVoUAcbF6ro-%?bcfh~3 z%+Uz{c;X19wekKP(e~Ccwqj~p1d~dqzx?|v)t%v3WIdNwqO^{W7EVo5btvD!xoVX2 z6jQvHwNtqlH!)6HS%7zyX@!YPz+Xx+|>pRgoR6I||)SJ`%^9$njlVatb=0`O;lqJ}nK6IM9hn zi7z@0?T)1-E{X8G7)b99FOKls7$37ERZAlEY;EDRw8*`D6vNSde6$=#d-$jtM|=6G z21obvQ7w)h;G;SmJs4Kl9>53E5LP^K7VV<$52L{f(`+5*54(c+x~M(j#YlX-IEL=> zsqTY{r|6T)E<0O-{3wO+)?Un72Ijz*tRwNLpSJPmUr>UgDBNWywY5 zkz*RP)9L4Ea+(T_4)K#?QgqMO(VG0Qw7e*u97UGA$R&r$l8eeC#~kZhQ_j}pG|!se z*H4Za*D0$DH2D!}`N(*36j}0-E;&?|TvQ%8CU9qbbe1OPN!+Xb*L8$WXZ30$)U33qVmWwuYFUe(=|EGZKt>Q zlVg^9!VQBo`EhCa;CONrS@OXyIaHQhR314d&ClMNs>x~MJbIa*98>7+vioWBCTV&9 zcybh3^8PM4RF+&+9yw;@>wlZ9$!S);simJB^YbSzzebavkd|K?PmUr>eyvLml_eLI zM~>lcJIIZgRT&-asK@_*;WS8MW<((+#M=P--Y{p1L^*!Iz1HF<@!{PK8m6j}1iU2>=_xu`sH#Bcm=#z&f* zVmQ*1{N#w{81q_tOx&0^7MFe6j}0g zmmDffE-H^4VJ%;^Jfg`twB>z2If7g^ZM;a6FPD~I98ZoSOMbCS4wWSrl}C=4nNFv_ ztjQ^EX6O+=IU;CAt^A`Vua=g#j3-BtC2#4HLuJWD<&h&GXU0d*X>tzAdEQTs@SF?h zoUh4iq~(8zCr6Pb|AR{ol_eLIM~-M8{2!soDdMN`aX&d?e~wMYFJ1b1T`MhD@#HA7 z$qdqsWq<=aNHZ$wlRnBTni1 z1rKX-id7oA%TJD|r4d7Zp~*K(%YPY9jv`C`OP3rfOD-yp96?aECHHG`4usm|Cr3!s z1zA7UXRdQDDIR*h@?9eq_-JS1&DZ9#4)UOWxcihsu(R$|FZ? z*v)5e)#MZ(Hgut%9Fbyqzf95Od!^;4#gn7RlAq?1LuJWD<&h)s?2#|)G&u*KRrtvf zh88&uzjLVN4bt+Ycybh3@+6lWDoZXZj~r2Lr@y>blT&0{!>xXD#JD~9>33H!`96$d zVeF|QE%In0{g?k}Vl47lqSE1R@eU>U97=E=)XhCqb^=cq&nGG~LOJxWNV#*?u{h;S4rFW!(^!=LVwkXn2f&WKm>Z!%)8)h|$}# z6~6`OLwQ#EpeddlMV7qDC5OtAi^?NM$lqI2Vw#-8{`&gK5dhe6{yUod1!?)Qcybh3 z@?$PJRF+&+9y#I(FT8$%Ca0LfhD<*>q6_aSeod3VEG=)0Cr6PbZ*<9_vgD%j$Puh~ zQ>QtaoC6ly`^gcy*e?48P5zp+{BS%uiY)nImmDffE-H^4k&yMjouSDo8nU6KpB!4plfNk~KNwGrB1?YIC5OtAi^?PCFw3YW=TOV@{NxC_Y}Nb`P5zFwe1AMSiY)nl zmmDffE-H^4F`Nq?9>JUNOi zd7VoRl_eLIM~=8z8t&#h9L3H?ANP|ZigtbBN=^Q?w7e#s97UGA#wCZ!l8eeCM^Ntd z3%Y1>4$R%l@`dr_D6-@WU2>=_xu`sHgcIL<_NAJ9Y7&OP7y8K&TpT%VnkJu~q`%cF z;>l5D$tzrPs4TgtJaWV;V=6_HbG-7ce)5?~G%T#iXJIALMd7;HX_1N~`nIn~ibZB8 zDGp+e#yf-}>rm8n2rBE4s62-d42_{;?GOh<5A{2Q&}f=7;E!Tn5`Ei~8}pMC$4HmN zyMZF>Mv3bNRMrhqd2S%q8l#um4UV_Y^t*w`YYd}lH>#u$hQ^bl$dV6r$)U33qVmWQ z$c&AGFCSNKozc!v6MV9BM&djuda=_xu`sH#JAH-8MmBc+#mCkuSueRwKVx1(sC6~jv`C0Tym%^xu`sH1kdANElth= z^fi8Rgwo?Tzb3y^TAmh9jv`B*=8{8Y$wlRnBQhVq`87F5=l{b`zJacHy7-UJ#v~{v zFXM?(WQh}8VyG;!s61kX{^O^mCg$+}&VJ&}bcGY~Nlo0?IqugyiY#$sXXn>ERF+s& z9x>Jy;P;It=Jf^mZ_t}%-DX-`K8IV}5KoLEOWfcRLuH9Y1?0={H5F)v_f$nX*G zFfCrFiEHACQDlj0TwtI09+IbHtfk=*ayrI(8XmBwW2~v+@nbs1x*8tNrDLqE;n7t(#`+o_ z?4;wj$hU{DV1KaAhKB;F9BXZOWR8xp-i8Ot=oo8mc-)GPvF?V4k?0s}Z+KLPjjve({^?K0ShK^w#OWC8cKC-e9b@ed|4yZ2 ztl#0Eh;)oKJp4P2jTxE*7ESL7&^v!9{y25$5_+D-|=*ebv^u*OUGE-!{2~( z98K0=N_0FmncCHf+lX~OeCwuito7l$DIH_I58vSE7;Ap`oCULC@fe1uTtmf__%42>#kAC6I3OAxachllQnRdop0 zMd?aRNKiV`3)z@j6-MkA0g>aIK=jHg#Co zum}kuaV^8;(X`Q#pQxXh$R>3=n>!Fqt7;aZ)k$h@GPe1PJB0T$e-x3*7twL7E^@j$ zJ>$n86MH7zs!mU)qh`F033r4VSW(SL(F``4;gJUyg%dP&Y;iBTRxTgf>vIsg~`~cFJc_prTYTM=!@p6 zx#^4M(idl_jnoZiP&b_6bwez&$WOAFo4uV$Xc?EPO2$Scn|&lW2FaP~Oha;}khD-+ zh@=IPwD3sunW7`!sDruh^;toEP=i+MGb3ish{eeY+oI&ENQ}JL9!7*3LHFx^+Ba2t z@-V7OPadSQi^7c>aRdq-sfMFiWQnhlggUOl6fp;t4sL@{a;%GrCCAi)S#Du%vZ zN)9Yd#zx(640gTqQ%Am?7WLIxUSCC) zC!LKLX4 zImB~l8lM34nL@RX4dXLayc`h!SgcOQ2dY2QHPqlW$=ImD2dSd&Cf^5yV|+kotFz4qbhdmzKi417&*=mDIjYNTFBZ9zKcGAD0j(GJ zPI~T1hexOL2XqPw-QhTjMe2O+QHNJdw;c}EQ>Mdl@=WTY*5mZ6wHMI_YQTnMY}A1A z>HOu9PJJEONUHj<+L$co_Ck`TDNOC6wk1`IEqEK2)@R{OFz_Ud@U5ZA8-_lYz_3ocCR0xde+wuW%_2a#rDHC zYV5XTY}5~%sUn?tn8@&7-SuFh9@qt5ed6KB^uYFHg$1P?HAy{yAsOlc1W)>U;4W#= zUav)R;_cHdYJlQ-b`(dk$PU5p_wdpK2Q~fx6uJj+6pQQ(^uS@S2cUYv*8?Sd-Kib6 z2X;{p>`KN)J#fhMz?j$W_V+*|Sf~e%fLHebj!X~iPF7ed%b}Zc=}`|Lg45RncT0?n?6k-dUn?%|~eYBatY3f%)Zibd`Z^gx~015mx@>w%H_D%fj# z-~sA^2a>T-57bgceHC2i?}6=Lp>tp}c=b7eBhv#9CMzs&<{(ym6+A*cfLK;v4>U-N zc6lw5b6~G-Q9TrIu%kFaSa+aB`@9z6N5Y%F7Uk=+`XSq*hp9ymCu5@)HNfu5WF6&7 z-eHoIylZs4dqlk3ALrdc?cD(=-eyu9#UlFx-W~S5!`IzAKJSL=Gj5;t?osmY(PV6V z&A~3dx;PA0Uq9h1;2ol2eSPMkwB6QXCcDO&pUiKyzlewdVMxL zZoS)2-tAAuM&2ERUEJY0PL{l*u}^r1a9N*sPl$I-ao(NO-kpHrLng%${=*M=7oB0R zR@|{a@_9E{f3ip2F7~dv^+UarfmwTJnyDh2b6IX?@-u5bw%o1h2fx8GPkc zK=BEa;s|340qhrF@{$!uF-aSL!J(G-$`fefY;?BsCw&Wd+n8P~+-1@wG zR=ist=Ut8Vt{RHZnG{DD)d+Z3=Xr-a-4{OZuGOFHLF?UfR}gm6pqy;?`WO{-XUby=iQ6qT|=CA`?Yubp!imkl6Nl!ygTT5hdau5KJR+!PxdA2 z-OJ?N%lIl`?+(B&?kF70OWx6B6ud)Bug|-~;@zP*?~Z8i4ny$+li~SJT_nLTjEY7C;f^xb=UoT=$-Zs9dxyMx z2VW@cT{Z0Dj=}-O0$>be{P{2DxCi}emP`o=3=iMRg-9adp zGAWKQJ{s`ui02*dD9e1_U93OZkF0mc$-Cpp*vPxXu!}ni$1Ia~6kP)E5W4L1?ql(; zG0wYV+PkAdC9R=RN zJH$5oylWEgPQ-b4N_%$_iq%YtBa8_Lyepq&uUXttZufcj2mQ%@X1)8Iy!$*E8}(gu z7T-}g>Y2Qwm?U_I;AfwAUx;@V0K3=h!dZMrsf6MVCdCm(qXXV8_q@Yzl(jzZl>TH- zSns|h@4igNM&89>7k3m6ize?VTnpYII@;&mSK?iDoOiX_yBa9&WKtYq96aFNX3sm^ zQP%ssJ5PVIUt8}^l6NPQv5|Lmu!}ni$4!%W6v+nf5I*hm?i=xLdz^Rm+Phs)Y-Ca# zVFW(lU4!Qx?kJml-XZ3hMkcCEo3e z^X`E5Za)-TnG{DD`ww_`$ny?&lx;rm&eoso_tv`~$h#ksu~FY0gk9WGIP#jjqxeU7 zhX8D!cRz}EhvU3!)ZQI|Vh5Ar2(u0;-hd{3`5yDU!yRR(&$~4J$wpJmFxphi9;m4) z*vPx1u!}nihh~#^6dDTe5TWh!ZW^laSA3jzC$x7>P~6R=IKq5Kz`IkPcetb6Ga7?x89YLcjYPA$h(uUi#rO(Zj*NuH4E<$!tL{JhIkjP2wt-l6@1N>LvbII;s`S> z0q+)8`0prteBL4Mnue8UTJL6&ce7Hkk$07_i{B_5+)dt5XfeD)jJMCbiWKz#o8-xX z*#Yk59yjh64;pU8Pi;!#kIsBll#@&EJyNR2e0QWwJdwz=A&n9`0bU}c7 zv&W5V{RzX3QOJ`SdMMxa`9k7en1YSC>*%wjku*ec6E}4}xDm|lYxklQ^)#F0k;19~ z_b!hcSMoDH?xqa>O9hnlBDj6ru@v#9a?=T&EnlP26P0rmqhreh-1|Ik zT!kUPD1-K7*-1ydh z)yI8U_xWv>yPCMGQ?L>DesJR&|#ecUxE>Mb_O<2`Ev+($faTw`zhxcBQm zUt_uNAnrR-uo3rRaN`=|sBPk=9|qt?@V1Y8ZHjuAP4Z|?ZGiiz$Bk?3Js&rsi0FT> zTFbqTxYwm%Bko3U;~L|jZQ`b%OyEZBwvYSH6!ig{RB+W44K#eoTTJq1!(04JqnlHpwF~8w1=YJZ@ZLpZK`9>ptITxi=B_ zrW9<%-2`r2V;r(g-1IXW+=$%vac@phpRvhm`hRyzfcuokjce?4A9tPZ^DUNpD{*g4 z!A9IC!HsK-Besc~{s93u0=IqK+fvk*Y?4P&wg)v2T6c2&Ja)(4Cfh7jf@O!A9H_ z;Knt^@!G^q|FnV|VcS0L-6`sOHpwF(cL%s*9yhMBAAH;kb)VmDx$hzFds46w_d;;v z8sl(n;--J+!HuYGA9p=|Dd{jKYWKYX?rM)4*Vr@$>hTQ(QqwP+doA~U#C=~1HsW3m zZd_v=txeoC&;f1)ZTqc|D?A$DZt%GAJ+{QheXZ{EM=keb#Qj(bHsan3ZhVi?@F};O22{a~ zkZd3K<0)!6o8(c0{Q>U%9yhMB6+Z6Hy3hAp?k9-*i4<(ay${^D#yAw4+D${v;6_BY zkNZh{1XebD2^HMYvfov!=*Da(C;xDTXYBklv>#x=%~*u+hP@Zd&3wvYSi z6jj3}`9J<=0^El^Zd_w)eB4U+`7@ULS>k>+1sic60ynNPnvCE+r|AiBBOcqweK1AU zvPu4b{<#2mqsNVFY@LrgP51e8miu|)em(^oaUTIUt}*;H*4#9418#(4`?z1gw;r41 z|JDx$xQ}_H|`S$%XQ6WNugKG{G+u;;!)Y7KeY4kHiQ_Yi+5ww;>oEn<5MzaW_{1~%LvG#kYVRN>OWxG=u2*be{q`c8nY)?=&pv31rx`3|$y zYqq1rb~FVWwXKHR7S?POn^A>}1>?Nz=%vKQ{`%slw%faa(*Ru^s*o^@9N0Udu+K4-^&@!#FKS5dXzz;)lfXVG1_l zs3*93D2bv{8rbRqjx@%95p&X%4pzny#$y9KM>NkNp`&25W)XTotU8{8jcgx={|ORb zg=2*8{6hW0gzx-9!gtP5kAPO4L*YB;U_{4;?|htME5k`t{-ye*sr*Z+e6EF{uK@X# z`jvtFN+7?s5PW!moTtt+kn;rc8wU;w^Um(A?5ZnL&`Gfj{f&4)r7gz}HNPzrN{n0@FD3Cu{2yU2wv{Wq(q@_SEv=H3S z0l7$BWFQv_z{#R9p+Lhx+}$ffF11G!Wnms!YOhNP=>14$Q1D+|GI z30P^ZS{q1ffwZv@{L}%Yt!ir^Z3WWKLhwrrkoKy*fwULMUn~Sa6and=Iv7X?fpoNx zg$((t`m2HbRUm(}5d6%BmCM!T26DMTI$21RAy=p?4CD%d@FOKTun_;Gz)EM;*+4oA zB*R+4|4@K*QC$qAi$F3h1pndz(p7aekgfvhW+C_w6p-$!yMc5UNDm9aKgfXeR6Px( zr$DkS1pmJSa+SKuK&}$V)fR%`1weYKUIx-jAlVjz(GNg+tKJ6ETOik1NIgTYRo5EG zwF2p5A=??!SM@cJz5?lIA$1JtulgHEe}N3J5RAORmw{@afeaMLAPd33ARvR)U;`N} zkRcX=aZ5mQRE~k<2;@2o!N4mZ*Q@Iddr^D&Iiz1v1=1VhkCfMi|HlffQIslp!P4NCO!ukU|T= z{1x0CrA8UZD1nT&5KO=UQlyFuq(~se7J?Z>K*p#s1~NtvXCLskiv7mWmP~lB&ew& z3qw2)EKLh3JOo@|(+q672SZR2VC5lIZeisHHp7D*V%W@(nrUG(4Q!SNLwFdhRfH6t zEiR-L1~%J+AubNEIU$7yj|*&$fz9<`2p|NkGNkZSa)DJE*gOx0NK3%xhZG)JF0lCq zw!niSBo(lQA%*9e3v8i*E%IRH466#MDhsPJu*Dt>k=3vk3n@GaU980nY>5X$P(5Hv zLkf>c7uZq*TNYAD>a8I{@Ru3fMKNI{!I+uPTo$LFFglUr`b&P&; z{hCz2_N%JYy-Dg_(*1^Xzj1YUC#knd_dC-4&eiQqQg4zjLb`~n+n%J3knZ=S`@O5% zlB8ZC-36q(z}0O`QZJG2Po(>kt6QI>ULf6tq`T17txZw~N%v>c{n^#so}`{8-6f>E z#MRxFq@E<*Wu&{z)h$a>kCUzy=~}tE#Yt)(>DrL4jjLObq#hz&JJPjtb#s!`1El*4 z>HgyC%9GR{(sd+VM^_h3QuUP{u9-K6V8x=ya{WTM(Zx+_U{rK>xUsJ4+V zgLE0Lt|?J%CS4}!GF{#AM74o*-ALEX)g4Pzcap9L>3X=jqls!Q>9RH3kbpQ}5Ns1}iK z0O#G)on;r6iuLplWw@HTbHQ5B3%LL3S8Zq zL`4xCs*rSru5MMLqSzBPnslRG-HJp-Q8TKTbj7Z2Nur{7A2pVAV_n^%MD;%DZX(@H zu5MnUq8KYxO1e^4H#9wF^)*3J%SUk|GW(#~b=+<^9#u-Ze~d90lm(7qT} zcawGjYZnBx&xO@pq+P_?MFH*8VYQjGi&?ukpxqx-`ZBjt$r$C@iVgc8D{C&Vr9_ca?c`0 zvhAuDEOvp9C0!I+eIQ`*bF%n(3O4dF7PMI7S;Y9XT}7h!O>kI9f>u8ru=oX8{2~P# zS*#9Ptn(~l)Y-1eQ2fp~EX=@X{Y=2(39@)11shqc4O-mpS;SbfT@|4oqE=&J1X}%U zz~Yx=@k`7&vc=6oi}jvGoIQ3WgW5+Hv6um^J{Yk06I7vxu|Du0Bxw zMmsD(K&zh%Sp1qSew~7iEba|j-0xY$*<)7>D1H|n77d`)&j&1?B#S3gu#v@mL5l}H zi#U61l)vJ)=poP_t$rb3@f))E4Q5Z-;(?&W!=6Q)JvK659i)D|mRo%&VDVeB_-zU{ z^6^m6VxwmfXOE4hSN!Ha1k|I|F9s}rM;5Q@35eHnLb=8N5Gw7IF62C~U=VMMNMrTK!tU;#9IY zH5D6KtPEOQ?pegyV(zfUopTYV&8aT-~imWqum#)1}WJc~GcY_zlbh+2(+XSDkD zfW_%#ae68?vRECoSm#;9*<&M=6~Ds~LCR?L8v%>uWU)LI8(FLkTHNkg#Mxt`iWR^8 z5rM_{tltb+oIw_6q+%nBn}ZhXJ&QPdY$UMa_eLTZ7_Dv$Se!`~XQpB!i@Smr8$63R zdu;Ts;x|qrfETTPD`0UJS)7%MjV$gBTHNnh#MxsbZV_mUPXqx0WCaVj>l zSRJ%j=UK$rVG!+|J+!eIg;911kV`B~#zY!K;hxn{N z3s_u67MG=BBa3^37WaD=arW3qLdEZr-OjE4JYaD-SzMlqjV$g9T0H1k#Mxux0~No8 zwvJoOESe=TEEFKM7 zZ1ODP?6Glo2)Bce72N8t0~S}2#Z{@;$l~#!#gm>zoIN(0PVswsqulC~0gJ22;_6gv zWbs7MVsyT}KjQ4MF>;FE@LR&I{w84YcCvVTDmJosDrm97vxu|DM!qS2H}Fty^|t|w zHDs|S6&qPBpC7zGdKPi^*myR@Zxz0lTm4x zxB67T;vHo1j#O-9F&4B~<5|SnV`I-0zsWeATm5~&;##t}HWeFLtPWbN^DN@*u@Pm8 z-+`>S)jtF*){@2ARBU9iHfV9XXAx(QjT2M+_T)5f^^XCI>&W7|RBU8%bI@YFXAx(Q zjs8;lz02I{C_NO$H{Nq6S-dk98(G{HwAkQT#MxtmycEB&xv?`{*HZ%)*OSHdso2Qk z-k`<(o<*EJHjqp4yPg|5(>K?&fWPRTY-I6J&|;%!5oeDLx>EdR>dxHi z83Bu%$l|6{Y-I6B(Bd)ABF-KgSf%tkt+~}R0~R-v#m%YM$l}qU#U{_<5)3=);3ST(~ohZJ59EU?W6w#9?R7`8Q}@a|xNZ8flM z9;}jK+d~R378clc1G~$El{0KdNbRt&9R{}3gW>xL9kMH=@JeE_w#&eFdoX-40(N&u z;eEvdyW7C-@nHBy2CP1$@G@h8)f?Eo9t=MP0J|@w@TOyd-DhBXJQ#k10Jb-z@ET-+ z?KQCbJs5uc0QNvgJz!xE7}$dz48OVnYX~X4Kv}Fc7}!G|3_te(dpM-cMs~?6Ht~%)%Zsu*W^vW`^w#DZI2-smi_ zCk^Z=4_3{v10jXiJqzrBfj#ZPVhnpGr0_0ifjwhj&w8**h8+wkyeL{=2Mz2w4_3~w z=R@jw3wz$cUhrV}e-Yh#D5UU;X|Z<5z+UuV_(v76mqH5fpBC6l2KKTC!+*$t9S$kH zoLXRq4eS*UhJVcgdo`r+=4yexYGAK`f1bQ4qiyLkcg%7Hf?L_Lc|3@C#sXhZNqHEwHx@>>UrbmtjXk>ZpYsHL!O**e-^> z7gBi7wpe@5z~1*@n;CX2r0^1MfgLlj4?I{c!#)ftyrElQ9~#(49;}*S$3qIQ?-tl` z1N+#6#TfQUNa5Yy0{g_kKJ{Rg3~LH0y!cySO$PRv2PZE4Dh+I&2a7RmepumM?P6`dfi3W0l?+=L zR(O%Sz!n(3qr5Ay8rH4}XiXf}TGp-&XiX&6I@YcWXiY5Ede*KFXiYTM2G(u}XiYrUCf05WXiY@c z7S?VFXiZGkHr8$nXiZesU97z;pfzz>J6XFkpf!~+ZfY!u!eaG7G0$LN{^*w994`@w{*N?3IF`%_kUSW>%!gq=HwNRyOloxBK2edZI zi?uTXS{voX+F1dujq+mc?10urd9ikGKx?DCSUWGEwNYNIT@cXPC@ zv^L6%wMzn88|B5?Wv&)aF~D;RQZQgA0bcY_ot9xd+`oOe+&pC-5BSHzbe-d8`nTr? z{R7EQ=*K{zLGC9cp0cm4;Gy;H=~?!ayNDiIj{#|u!%993{hFt*<6-kyer|GjDm)%+ zVscnwhsAOxhljA^VGt&VcyA04nH(OEj)xYQ9G+v2pl_{e4zEoPq2?H1GdVn{8-rsehv&xNaS$elM`oi%CU=T*QF;h3@gc4qOEpcdl5%() zwbtPYFc<(fxmwEAnH<9A(RP#DMY(#D!_!+ZJZW+VD2JyiYnzD5$AUnU!!wT2OD2aW zub`Jq4o@9MFPR*I{^2z}W0k!wN3O!;@YECxeVbf0U_uTAb0<)ZW)NAem^+`uPoa#*kd zuT2h5#DLc(hxH!t+T`%W3Y-%rhvg#h+T`|8ZokQ4wF$g7IXv+J=Y+{&u?oC4IXrO^ z%a2VCYhB>A$>E6!IOpj}cI-9fDoqYgG{6;La#(2tuT2h59KbbZa#*MXuT5?*wMs~$zjDl&SaCrG9h?vaz`n5%;d1j2ws~UR=i`P$K$`C#n;ceH!E2MlYHVD8CWi%D@Y>{#P_EJBu)Ygkn;cd; zEDwX%CWm#&IFsqgC+s!l7MdKEqrq#F!}>M!waH<@8@x6-tdK)rn;e$Z;S4c3 ztii*1ZE{$&hc=iTR{5a~CWmE#=y{XFIzpT;CU=Z-{A3#Pvx#yijP4}mPMI9mLgI6v z74dxbP_EMCu-X#myvbpCChjjLhxMHJR7?&FKyjs-99E3tQ!zO#F~z51a#)j!bIIhe zh!t0W$zjziu3MACGFf~^CU=x_{G0%85#>%8-6_iPI(u%z9OQT%Jms)97@rD76jKh1 ziQ%=$Vf8V*HaRR;#^+#iSUruNH@U-<+q zXNbw+Uv2nea`^uoXNbw+pL*OwOb#Of@XX{ee1QHmIgB}=KTU2Q$p?xNYF+8--o^hnyQOfab zA?1!!4l{k~w2Y)tk<&=1P*KmM&dtNkL(T9vBN~n31&j$T%u%>Sv$#juUZ~Wf z*&M;N4A(@{S~Qz))oIBDN;|XJ8&AVZXm9A&jD!f@_!Pk-rXz_+c+^1z|0+Zf^BTb{ zV&pWWpCFx%)C{RP(iuo+BDFw5??&+QoXAg+eui{5($A58fpiYiFOkkg`W4czkW{T}HLNEaae5$R7zEs-unx(MmdNEahrf^;d;Wk~5rt&mzHwLxl& z)DEdV(qE7|Aaz9gE7ISPE=THwbOjP#lNafXl!4R*DHEwHQa7aTNIj5xB4r_6g>*Gi zFQjaw-bmLVU5nHQsV`DLr2a?)kOm?RLK=)T1StpUI;887Zb14w(v3(%k#dppkcJ`U zBMnCyfmDDr5~&br6w+vKK!Y)n?}3ov@56G4BBD!Y(iuf?XW;LAu^kGSOc37nM*q?b4`fMqa9YV zCPWs{4vSS2B8zB;^{5Gv#k9kc(}c(p+F?~`LSz~3F#nu@8E5P;#hehijdqwtPKd0c z9VUtsBDd47hIVUccL(j((yo?v>u85b+l0t^+F@QcA+mvXn0if!Y@!`zR}&&zXom^Z zgvd79VU9E*au@9|4Vnr1F&3}zQb~^KB!VYVp#x85IR3j-$qqc3;K@|zz*88Ge?L`<=Sd1-Q>etj zrZN`)oT^mEbSlA9snCI+#&G=iiaVRXPfjB^-R@lYPZ*AWZ`Ds6_)iFq7t{sdr!yS? z|Kb&Ne(R?boZd_4T5ra1dIk+W)y4aOni0Gi6*|5*XE?pkthobkPH=kDn+rdK;q;!e zGaUFC1V4ic9qVT@9RGvkZEk*N&LlX!;?0G(U^xCsS1lZP3xcIci=xK_|K`(vHlB&V=MyC{_*?%3xd-Ffn4}G z498%FI>&*ZL-2E`(6RnYhGR4X4+rvF|0Tici9s&>T!v$~L!Ilu&n5V|ROnd$6~i$e zg69hPt^bPP^spfp{%eL~z(oDpf&ZG|zotUR`gsh;hzcG_Kq zjRXG;!GA-Aj`iO%9Ah$gnvvi7ZwXG1IC9~?V>kwF)bAYl?+E@oDs-$XhGSGmDF?0y zuBgy~M;MM_9~E)n5rRjk(1D-NaEuG7^Bws41V5h&9r*7Vj)5cfdk6k|g8!Zh9rzy@ zj*%rie#zf6e;_zLlgWi&!0>Xs5a$91egVNRphCy`9~q9ZDfLGO{zrn-Z z_@5m3p9uaZDs-&3WH?5*@X#l}?=1;VPl9sc7cv~fU+O{!ej&jxq(aB~MGVLI7@im9 zw|)`9=^;`s{Lc)>0Gj%<1OGF@|4fCB^@|yf5jH$(%5VK*g46S-T=*pn#}J&l#DQNz z@Jp!Bv3@DTF-C`{RQau6N^p8~l?%U&;TXhImpSmu2!0tAI@Z$}j!{09?!eOtPEWmZ z;jI{c3h-7AycNM)QK4hKHN!DZh-YK@eQ!;0dSI3dZ^Lj5B&s$JybZzIP@!YJEyJq; zZ|lI@5}cm3<-*%B97B<+oda)2@OD(_SZ~j8j8)=^Tz=o%6PzB?<--5Ma13Uuzc}!} z5d1Gx=veQ-aEx~1VP1ag9SBZO_;TSL8IIvl)zN`>BzQ+EbgciC;TSK)bHM!8|4MLr zIG79n8^bZ6s{ZD{|3>h?QK4h~a)x8X6^{_}Tfdy(^jt9)-ihHDl2x4?cqf8)qC&^| z6%5CiEuKE+w|)h|>5*hE{7Q!7`A6zX2Yw~NucSi9dS`}X)E5sf^IPvs@Xl1|z%v+L z4S0qF&medP6*}-P49B=Jo_XfC-i6@wAT$@A$#4uTt4s%;N$^Z6bgXw}_+G%fI`FOp zr)Q|S@NNvp(6s92z`GH=8x=a%yE7bP*Lc#J-}mkWr^m0k@E#1u;I``FzgT}w5xgH2I@bF${0QLv9e96&)3fnh_yC4u zszMEL-~$LgfC?S!0~wCF3_L;4@B2W4(_{5q_#lR3vO^7W;DZQ0hzcF+gBgz55IlU( zZ+$Sq>4|(Ud0y2@{5pnX!bM%@z^^0t zbyVnBznwjlB=5+A1K)?0B6Pz9) z=)!MgI3|76jSl=qg5OAmj`g7o$E*+@H0ZZJl;A_D(1GVN9MeWB*Ma8}JeLX`cpk$s zuY_k5`mN^?oE}{0!iO;&6HjWG10P24VN~c?&u2Jhrto+}zx8~A(=!iU_;7|}>PiiF z;KK<%oC+Q5BN&dkEj$^~Z+!&8>2ZlJynx}D{89xDynx^ZROnbA$#6Vc6c1JOTOUdA zkyPlw3mJ~-Ggauo3khCGg${fa!!f^x=P~-Nk0Lldq|t?sW;iC`)My7jn&6|U(6L^` zaLmx*QI3A=MFgkkJG$^DqZAvir6(uI#@I3@wr zSO-3q;A5%Kv0lP(%o3^+2VO#Oda9%gzlq_PMpQRB@S6yJ6BRnv$1xo9jCl5>-}i9@ zrw3BH@KT0jB2tw)@KS=8QlVqLjNwgympSkk}A` z#~R~_mww+T5S$)^>B1*69Fw7Hq642u@QGCDSf9jj%$DL|nSSe&2u@GXbm5a3j_Fi2 z*?~_c_+%<{tWRM$=3DWcO~3Ui1gD2@y6~GBjtN*TF=!!i0#w4zj2-J;db>Pr0sx>1fYo^$FTi#qc{T<-A9*L4s| zK+kve^B{{l`nj(z&jMWs0S9=5ol^%{)L9VXa))Q3u7j`ydO)n72U*n74~}(t7U?<& zUckffoI1#&&Y}>PJ3Ljo4niChX7v0#$f8aaKY7;WS*+_I5CV_YbLt?AI*UVG?(oEP z9fV8JBWnFT$fAyZbgj#?MAt!31s>4n)IkB3zq#n$fC|2 zypY-DS*zmZ9dYeQV_@YL!$2*;rn&wd_cQKyzyLc2WcbR7iksC8}~WKm~b zh|3+GJ9Qm|_Rx}PKM%5~b0;sQc6rw8ItTz#n5_5LK^Aq^hq&C~snc~3CPZtl{dJH< z9laLY<=LR?AecxY6u@5xS=8AO;&O*)qppLHBU-fWuY)Y==*8SF&n8_5fk_I12L3w8 zqRyrempeS0bsdB+(W-BM9b{2QuLgH{w&*$ta#9Fm@Yg{Wb+&}K+~L`(>mU@0mW})C zAd5PBIl0TTP1ixdltPe%zYemfvn|Bs4$pR72VqsT?%ZDoS=7<%(OsUqbR7g=DTHA7 z>mZ9dcZImz;n|_@AB-{br2M$5I*9sgDmRo4sp4|bGNR8&@oy%@2`U_>gXl)F3&x>4g$;+ zf>Zo;kVTz)LR{|f)ayD3L!-6#{yNB_j$V`R^4zQIAXrTy^u=EXS=6~V#N`gpeYy@p z+Gz2J z2ysiuKy*%f3WLi?$V`a-ttWzTqF2%q4HWHIwiGe4TE(RoQEmyMl1sApPZk=dqA8^Z>!&0Kcf=q+4V z`&`ducyA-OL)+WOWv4~TeQj&xvJoRvd>qHQEFmEw-Sg!nmtD9n=HrOb$_aY!tk35f zE(6CUUjOaovN5mS?ZfTovQO`y=(Bf}%iyyQ*TiLS-u$T#S4nFhz(2I-9pSQne0smHtV!%W=Ig6+TD_2Y)rISPxaC|1ULS5Vmz_JT z(uZr{GSjE}Gdjd&&vY#C;f_h!`@XVMTsC@3J73vCT9c5us`MFOSuK~H7D@7z)pObC zExUbX2e=IF@cG=xWq*CY+J`$~?D=pNw5lKzpRW&B&1Ik7|Fy4dJC|+QJHcQnFVW&ig}i(wZ&w0%Ffl327~mnm2p3T;DGU zkL$dGuiup`R$PI#0DOG<%WMCJSYRnX7v;P7_~`=&a9r=h@8x6MX*wf5SdPi&L34a1 z{NUr$U;h2&xMulSa~gY*Z1Cidz5WsNFUMrFnvZqeS}CsqZ=Dab%XeJwJKihD z*Jr{{U-<#w@lkUOf13Ch?d^p7n<u+pBLqf{92qRpD^*g7qm5W;s^vyEHBH=jQyw|MyZe^uw_=I0ld z6;2p_{>A5yC@dNtEWR|qv;?XN!^f2tjvYf4T3`B?j+eGU*|4$2C8%-Z*pf1+N=bJ~ zQSO)mH8HoeSY0x#Otr#(j4}-BlHsLPR6HzSwQ3KU51HP&Egel(ZQ7_yN(;vn6b)CG zj3}F;(!o$vm^UtW+~gaJ3QNmi{<`xo8&f#xveL4#LI&ud+r-eJnb ziQ@`N^NVgOEo_@!+-~BSl9EeCbyF`0?S?S1J)Zrr3&*y_nVICy^6`RAyOc-7? zwq$tef757jZrRBHs=<#YH`fi!qWfl z=Y#8|4A;efvo}i%3&!LYX_Gfxe174WVMXKfhu>I&Gl8y5y2$=JR{@>)<4ea~Ry1~4 zZjt};FbdxlgQ82Rc;wjP;g?NpHKtuz`k2c~#|^tIyIXdz-nrw;F6o`s@7jKs;q;nZ zFmCMlG5PvRJ0C45Eu2EPnbz&wrnm1v_l$hL>-3u3p%J4z$(sO$Z zoRT}ZH|dJ=+GG{u(0_DM{{h{z3x@TWG;;W${K0oRZ=SCtIw*>?i?CZg`hyg>ughZPr18G<(CwQf5;v#`U2tZv;)^D;BiugMh5 z;OqimVH<5|T{I@Ic;M))9yw%V6u8sD+X}u-$?DcBJFCaQw(xfX+Klp!QOX4r$^B!(q*HMM^EX~=9&WThk~qb+&2@mx^^pQJ2rDbR`=c$a|ZQBySkU+7=6^G zC@ZsIB6R(;`)8M2QLwgCd=nOAZJjkiFs|%ubCOp za~hl7zkgxDRoRoXCy#EQ)g!+&uQlnrbm`l@cR#W_WXx41Iju*gqhF^Co9vbQKD&aP z;z_NB(`P>!Hf?*erM+|>^hO&kC%6kH_vU`eo^*Bpu{ej&p4I~j&?o4}j%7IK(APy1 z@(L%S{R8QEe9q*FIA8D?7Z04!E3?b^yw*iia{5u(z(PJRV83sht0v?R&M4@e*%fWV zIXbB0Xq@X}OY>jdHG4Akb?>g-(AQmB=MK7RGR`luiO*yp&eutFJR0X+aen4Rw5=mP zi|*s|duH>w;p^8{(yzVIzVw{IS*A}(j_uvG7xim;Z|>JgMft@8OJNtE&!h=MS{J1c zhwnM9Cza&%7*Lwk6YcFeko=(YycFjzKH~vpC>x*C$6QnLdG?g^y=RxnIfHUahWEhv zo|!di@Z_vg$jOiexSmJ#qP`o{4%foKkwaSJT650nB3y$`{fXJ#I~4ZPpJ8Ek*X|>- z3cKL^&mIq-r{qoUf_p*t^sH_8PXc(a<`KH{nOhP;QSh$ zGx#d_Kdb=#mOiBQ$ReCKsE^N=u6KQ%6%6b#64$J@*KbVk33)|*C&+cz8#YJ6zmdbn z@V#eBeowkDWEbR(8Hjt;Rdhd@h|j7xcQD(=J*7`UR#EBrUWM)Q;WypSa4!Ql_1)wy z#kda2vT&cmxl~HNpgpKh=LXKp9=%I*@cCx-Xh-*(DZS{r<$G9{tX$kbaXl3bY26W@ z|5X$6;KS8D+D*vM#5HK{Q3L2cA=h@#eB4{gN^@|3%z@AN%+b$%$L0)5FTni)pI7hn zyq=>=?7g8!&g7iI-3sJ9D$d8}MfaV&F@5P8gx>=vUYl1q z+MJ7HaL(Y`(&u|l>+b1v@6H{RKOXngg6zx=licfW*^8k3J}Jl*(RJY?{|DfyWhWx4d(JKsI+Mh|O~0h=8PdbRFblwXLx zr|&CVQ^W8X_3Bf?=TTPI^n%=;y-RUU z%d#ikWf9D15(|PIGPcNi;|9gGF_W|>LI7zOz{=<3< z9EIzeuC@NSmrcy+hjUEdC#Pg}rE4mC622GtGyGpTSIRPr3nmS2-MtLo+4j5S+J0Te z4&(2Mfs?PM`+TeT-w(Z}WK0~~TIzN$OV4ZFu@v8#6R#OvFbUTnu7yb@_%0cpGmt+^ zYdbr$ZCiYf1zBD3*|O}WrT%OuX7|IjQd}_kKlBN0k-Dx=_$~E6`qb**@M+3_=o8vf zpzFFmP5Bo-P3evA>wnQt>Hncm@TWl6b$!C`%Kx#SCjT2gwfYZzf0jNS#(3Ww zgWKr;CI7vC>NaU`n_%6({=Vv&*P~-0{ocfPPHDdV>~PKDv%qKjKYX_ojrac7?TzoK z(f{UW*A4gaVBI3WPge#1N2brt`^?PuUB>wA{`f4-_Z|IDII&~`#z98p;5Ta?{ukHZ zi-F%x13Ug-emiuSl-;VMSGUOjJ=}40PG(2?9}vF*@q433_whqo55)f}d|w*R=Q#O5 z-@%y~lSJb8JI*!sY3%>rZx9}1F=G~4@;`jv|5M$y|D>{P>3@anpJtM?mF@b&;Zr_6 zzJ_Uo{R#1qTT=2dC-03mc;ulu8njiuxpfg+7nyfMv@lA6jkj}OmY?@FJ~QJsrL^_q-AZZFyKEO8 zkqbtSTcxi`f3G1X#@{1*4EiKJzWfIIg>7f_9mwGzr#}n%8k7!gf<_zgYTWLt%t zdVbHGa?IOKyJW#$F9g5d^D`lS+CCa$4d5mCt8zUKe61DHNBO59FXDPNiA(x?Ni^bf zU0--KNuNycz4Q|~FLDK213!A=czpUSZC?szS3|#^VZh!$A}_ARLUPVN^2N;i5g7zN zf+j!yAlu2lE$Lh@XCvov#M*b-&!$Qn>lAfAZ~Qt;nLi83o?N@M=R!Z6Ur1U(o6rwb zQ|mmGhCc6DREMa%rtD`Px&ieS$cj;t{IEqX*hLOl)eU!I?h~pr@SG}*&|O5E+)qKL zwx7i}s&zg>_aJ|^QT&6H{cI1BM@N@Ln{@FTY9A^+*P=#++9~|YzRLHCPZYbKUR)r3 zBKZ!~A(m8!^=S*+bfa{`^3pnaZ@MK>BWssX2Sv?Oi$}y-IUij`jWCHe_+@0zQ{;2& zJX{Tf7_Q;dma?A~_S1NBOF@0uP-PRJjSb~9T`uL?L-HBj&pqe(Gir}YUOA{g?l1p6 z_v@U#4h``R{L?0C$Od9lu8ng}*+PAMO1=Q(HKIf1XOK5lf1YzZEqEp>{{bDIUl8_N zV(5TAm+vaY`2-KLhRZzw(l1bL3&$6}-z;?GRGV{2FP`5J>$0$US3_sw z!jG@yp^M;KqxV6xD{{^g(%%^8O23`rGS)qui_~qLUw8h*-fWS>fsR8DKaAN<@s)OJ{3Ax+9$$g!?A&86K*%%hYoV#!rvs_lg%TYA=Q3~{uuxD zf@ws5fw297Zd3J{j05nm^n=iUbG1S}KaV^?Y&|bVZjc8?AI!uU^`(ha}T^b$8w{k6#=y=+R4u5a5^%?TL zl8)HD3Gh(k3r&wid3Ap}T&rP5dQa-qFM(dJ{s#5gwdg^rbN|EX-(BOT`Rj;v(c2>Y zcgRUdUfYn@3i3|vji^%Qi9Am3XpiGHffCVDWM()Y%Dyc{v2cC> zHg~YE2H8~V$@BL;{B98&275lC9;x<_d!!KGg!Hnu=dcYE>OX|7GB8H=HoQ0$E%$p) zYwwG-dvQYE5VrDO*lxrSzoBp83Oys}dqaQH8TT|&{?_kD+e=@4*Qx0F55Cu&60BP?r!_!)a4>Tvj(`%`~|PVwqMqSl5U z=r#3U_t1ar_5ShM0Q3M;KQQ_pof&G!eouV}_cx5)aR2?o9(Byb^ARg0l4Z)P!MAI# ze@rUqkFCw5kH~%bK^+o%qWP8-gHkhGu_1+b`zmgvKwy570 zU+dN7ocRWM(Wl3o8S3>b_|9{(W0WhUzuk?+-_Ua*Yoo}cR$h>A+Q{)zj$`Ln&L?Q^ zg)R)9$KJo}z1LLj58+yy$9L#YC|NbObFgdUveCaR)A^bp-?=H1+%0jg!@%C{wNIF9 zg67Ahw;^6DZGTKk+o#PaTGQjY;XU=rh&rO#8Q~lc&fJU^^%jl28vVfe3GCtXpX1X4 z&J_fF7UDaxzwnX#vp{}bQ{+fUUspvvkafGZw*jwZz2i6NATQS={2Af5pmqY;=$Wuf zsQGtccjz1e`c=@UQbGLXl8^tp_Fv}DmYvB7)xj5owXr*Iigy|$T=G=YG?P) zO*)t4$#ioAzw{<{pK-3JGyegZcP!*6tj3Jucbp9=)G3a|&oW;|elYqzV4unVeMK*S zs0W1OZ=8b|k0h-gk5Heo%+-Z>@%#dFfzGP@ocJ4jy8r(38`@{JRstK3J{NVr?=dU{+!?6PX3(V-%0+Q-|r-TPW`#v^HKcg@b?>c$WK<4p4PMXI?jes zY&Gvd57y4#OEsJ|gAar=cFwS|Rj>%@K+0KRE~W4DbPm7M|Nr`3^#9%aI@0U4JcpeF zezmh1bna@v{g*f=)7|FL_m4AP)yfijrU)Mg@i3h^d##He&C&#YaCy`crJUnjV1Fj< zGGw35r1|F*5!ce$L+E=!$6avNkj^JAq0?SEILG&nH(}gc4d>SQTw>Khe-6$H;XD+b zPka>T6HAC;jWe9T>QqWr=Ye{naDEGz)`hI%tQLR9{gSXJLLZ#toC)=0y1^L4yg|R^ zICw62S25yGJWVceuJVce5%kQ|p8q6XrdV5_8+)tK=dT@o8>=UZ%OAL&U}?VM&7AEx z_ib|>qA&4{$P{8Y&u0Odp?Ny}Y#$<6Y7jE}Xd? z!G?_;@FDn&&c!LdjDaz??f~%t?Ar^UWrQENhnTR=XMrCo!@hNa+z7?p_mp>`e<|hhS)iKmSszF5^|L(XeQAs;7tcRh-gEFn>hMc?$bqu2idd7*GJ{{i z|4p2kHAcvJit|zO3@Q4qG`Sf5J)CQ&zf_TKHCKSlo_VT^ZyH|4Xku0wwHGB~Rm z|EyPgxez%XafX=noAkBWHnpi|y-c+?ORS6XNj?oK{dm6FQl$TDpl8NAgD>tRpt}d?Ofq62rT1ea z&w2BiT|PrhIWg!>s>#zCf5y{0Ba0Xp_XFS@3G#3T`o)%m7S~z9GfmcAzK>1kcrQf{ znBVhumw(53aM54XG9cH@1#~=eb@>ncUHa{Oz7%<-zh8&l_`PrZ?njQxWPDY8`!Vr) zcvL;ae)$Z(Ngp|_(=BunY+QqL(C8<8#`%2o6(;)WB1VHx(Klb$eG|O}*6plLejRe` z&fV2J`iNZi+0T(Dj`#KbIpCwqUM-0o^qH^?q6U1ywo&B5MJ;Q8xh<}xhImrOWJC6pd)cVNr7|Vfsczg zd9xI}FLUOae=a}s&LRYP=>^$R?1juhDL%0nL-N^^^*hK;$oKQcpTZ}l8r4#*+fnW< zq+f9dkL8jA{`DhbDCCG_FGXd!&)A6WaC$VU*1-Bt?+xF;HU+f^_V*E2d%X;v z-TDnR+B@$1N4^&E74_tEUWD#9bBJg56Klw`0?U4-^x`14>P(&}Z?eG|1d%`YYURj( z$Y;g5f-&jB(~s-BXYNfwO-O$3-$O(`1#)$2Eg`-p;GL!DUE-`R`T=`pXF@U}F{As| zeE$TxUH)C}0d#Ws-Fvr<_ht$Ah8Zu&+rPt>D?M)o&+mkcO`fU#W8xerVpGJtd-OBK zy;R~1mDm@~39znI`|!wDyoPIi*At6msVx=iWErtJA%OLJu+BT1|ZH-xDUV+K-;mdSy|wXI#m@2z()>KbCn* z?}HHef(CN9*zZb-_nOAb{$tA5in$Qaqvr#*0{s)n_i)c$19?*FlTzLR{y<;c)FnW}(mV-U^Ym2)wE6d0HJL!4EPV%l$^lDSR^pekLQBM@<2B}wZ zzv2yJNOdRFbJh;kT`oiYSHPHbHTsOH{)7HD%7&{TN24bEG@K)&Sc3dv zsn=-^j!pSa7|{m<9j}~I;a&yu4}4xAy+ilJ;eHL=SIPba?l-$-eO%|fJk!yrqkh;Y zU5)$yaE}4**rK~bfrn9}TF-yuI@a-YzFyIVu=zi}&DJaL7xRyAlfkM>_r&J@mHQT4 zqkAZCcCOI9$hmSC_v_{^+`(ipUFCj=i`}WkGPIK1_7Z*Eka^0zuJDoc8V;T3)j;9`-p&9(a)!Wm?MeH;E z*?hAYKac3b>2Jx8;h5E6x)gWWR{#8wdmpbxxxq(&z?Wgi&a7|1g?D3g2z#Ai4Svf< zKk=e&0h@9!G4A($pLdtbygkC2=qH2L)u`QlU#>Q|YrAz<|6Vw-a&kSp%%E(-)!H4e zm$~bMTx&uI)M_I!9c8?Hml8_5Y-hDY5t@Q2PE>#O2VT?Nw1(4UJDDLSy!X2EFPhk-*U9&n#`Z z$F(hgc5#|a9v>JUBl(xL9|kH)RUg6U+Wy5qqTv1`QM&{)>YUfWW#J*V3m#RdHxEvH z>g5dbu6}P(!JX+L^UFcpD*xy#0Iop&sx<{S9c){)V1}>>=e`jF%SAz-P1fTntKVYy}WW5*dx z4~L7vU-;myyO7&FycCZcCH#E}gSwKR$U(L@2(MiXmi*-oFu>xgA`Zh3ji=7w14Nof zrez%k9teDi5tBd*kb<(Fj@hR*CradPCCJ>pi5b*C;9;S7g2aROyY` zBExRY2mRHe>*iXc?tE~VhoLs??u5T{Bf4J&BTRoVgUvJ8>I|`qNgWZa%qpDVzJBRT zVr8q@%w3IXBfec4Igsd%SNLuMZ4>hcG=?|P-$O&)Kz#>yLd38~W(p#a9gVI}nPEpn zlo2W-$2xDTh?t3p6u%r1(;ivLh*%Msjv}JWup=VM2o;f|LL!e?iHKNV4~b=u3Mr7B zWQ&b5!;X+BBUDI23PEL(LLwyQ*F#dUN5vFKjs3k79v!7Xa+D2;GQ*CLC?ixz%w*+ylne>kny=NaNA~DA1(ISmB+3jsLZXaNAu*D* z>v19^`LE}aV|#Rx0!bko5@m)RAyG!CkmQrq>q#;sgyYw8$%#EG*+!b|&=-Pfj#bp? z=#%USj6O)gChTacdbac(-!H}ZQ zJ;w2O%S?vo>&=k?$JVm!qckg>?aGB`*$t!cr~o6g5DHgRF0!oeM}=gJ;Mabwlo^J- z^r8WKh3pYHMh#%Rkj>0??_ws}5vV8y8diF!qhM8aXFbF)Otl)+k=vV>rpm4kqN);aLR&)6zv~i7(&de2+VO_EK(&RGkV!HBLbe zM9LKMA^Z!AmCh|%o+Kv94k$g$8@4m(cM}9DqHKbZwlXpkj^HI48i^j?Ig_NX+=^Ye z0hv+cnW6)UaY#ABhixv9>lgbMfkn)Ws)AVE4GtH${Fkkn9*m z8PSs}6!EiUf)YPWfyXF)Mpcw~CArhEud6t_U!yIGDrhN+BjWF>n8i+#30dqY4Y5RZ z;=ywsf`@FZ=fiysI!7uxsmdi1FRAz>Op}RF!Z-yZluNPXSog%ks1?G9BG6w6CWCby z=8si)QngR2>P?s?6P|=|3OvTKE_8}9_Tae(&d@v=)_Ag>G7FKQhh4Z~Z`d)IwUlcr=q;$}$4ByNNPifmjHdTZscQ<7FQX z%ZDRb*%}dL>!T4Fo(@LjaRm!AS+$luEF#)9Ms0jbxB4&?Y@oLo%shEn&KHBO6R$lA zkto?mBWad&P?AOk*jUBnL|u#N&UqC7OW$s}U(8s&kOX5t<_wkc=?_DyC&5I`M5mY}B~MFvq9V zs7f8B)Z~iNENrCQtWHANweTc6;1V6A0%In+?h=?CZLRKkWZGJFVTXBBS(uxYcr+pc zq)uYTOtJ$y(KoB;a49)fm;!W&UQL<~f&fsemCq?xby<{@TP+3?sIL@x$`Gf>6gvPD z8KdB0CgXA^XpM?fG-*JZD$p{YQ!)h~loWBwkf!JyJHQehq~ek+O%j(~r0G|*Z_#!Z zJ@Fo~HVJK-sIz#}|F z1thtnMF6r3f88E$T!iQz2;+oIWfB`Dfxq6trSY4*qI%;_7bPv5>_4&5jmE9*kEkN z;^Ddm&#LQBt`oKg?u?p1a!j2LK#WJP>WWM~Di^{2DhTWOwZa4#@Mo<7PENqSYIuSS zEa0eyM+AuAq(rhGKX7w`(o8q?XT`v>=i^m>WIxk^lXxtQ6k!Mt&p;ylxB`Qf%ou;- zL`#=Ww$5}Iq?RNV8&f{cG9YqG5A(=abysTyzLdC*&jv+Y4LRBlkIsM~^sWE_tC(y? zTLF2RTtpH0NvSB>!|`-ruh!EfCWzRF004b(#`^V#gXQC_QKDu%!`?5k_jH_D9ysc+ V;+f%l(Pszx$@eFWfBzHz{U3@a@vr~@ diff --git a/playground/faust/build.sh b/playground/faust/build.sh deleted file mode 100755 index 8124ef3..0000000 --- a/playground/faust/build.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -# build.sh — compile all Faust DSP files in this directory to WASM + JSON -# -# Required tools: -# faust >= 2.60.0 (https://faust.grame.fr / nix: faust) -# emcc >= 3.1.x (Emscripten, already present at /usr/lib/emscripten/emcc) -# -# Usage: -# cd playground/faust && ./build.sh -# -# Outputs (alongside each .dsp): -# .wasm — compiled audio DSP binary -# .json — Faust UI descriptor (consumed by faustJsonToParamMeta) -# .js — JS glue / AudioWorklet wrapper generated by faust2wasm -# -# The JSON descriptor is the key artifact: it contains the full UI tree -# (hslider / vslider / nentry / groups) that faustJsonToParamMeta.js parses -# into the playground's standard paramMeta format. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# --------------------------------------------------------------------------- -# Dependency checks -# --------------------------------------------------------------------------- -# -# If faust isn't in PATH, automatically re-exec under `nix-shell -p faust` -# when nix-shell is available. NISPS_BUILD_IN_NIX_SHELL prevents infinite -# recursion if the nix-shell environment somehow still lacks faust. - -if ! command -v faust &>/dev/null; then - if [ "${NISPS_BUILD_IN_NIX_SHELL:-0}" = "1" ]; then - echo "ERROR: 'faust' still not found inside nix-shell. Check your nixpkgs channel." - exit 1 - fi - if command -v nix-shell &>/dev/null; then - echo "faust not in PATH — re-exec under nix-shell -p faust ..." - exec env NISPS_BUILD_IN_NIX_SHELL=1 nix-shell -p faust --run "\"$0\" $*" - fi - echo "" - echo "ERROR: 'faust' not found in PATH (and nix-shell is unavailable)." - echo "" - echo "Install options:" - echo " nix-shell -p faust # one-off" - echo " nix profile install nixpkgs#faust # permanent" - echo " Or download from: https://faust.grame.fr/downloads/" - echo "" - echo "Required version: >= 2.60.0" - echo "Check: faust --version" - exit 1 -fi - -FAUST_VER="$(faust --version 2>&1 | head -1)" -echo "faust: $FAUST_VER" - -# emcc is optional — faust -lang wasm doesn't require it. -# It is needed if you want to link extra C++ into the WASM module. -if command -v emcc &>/dev/null; then - EMCC_VER="$(emcc --version 2>&1 | head -1)" - echo "emcc : $EMCC_VER" -else - echo "emcc : not found (not required for basic faust -lang wasm builds)" -fi - -echo "" - -# --------------------------------------------------------------------------- -# Compile each .dsp -# --------------------------------------------------------------------------- - -DSP_FILES=("$SCRIPT_DIR"/*.dsp) - -if [ ${#DSP_FILES[@]} -eq 0 ]; then - echo "No .dsp files found in $SCRIPT_DIR" - exit 0 -fi - -for DSP in "${DSP_FILES[@]}"; do - NAME="$(basename "$DSP" .dsp)" - echo "Compiling $NAME.dsp ..." - - # Faust -o is interpreted relative to -O (so we must cd into SCRIPT_DIR - # and pass just the filename). Class names (-cn) must be valid C identifiers, - # so dash characters are replaced with underscores. - CLASSNAME="$(echo "$NAME" | tr '-' '_')" - - # Step 1: emit WASM binary + JS glue - # -lang wasm — target WebAssembly - # -cn — class name prefix in generated JS - # -O — output directory - # - # NB: do NOT pass -e here. In Faust 2.79, -e means "export expanded DSP - # (textual)" — it causes the .wasm output to be Faust source text, not a - # real WebAssembly binary. This script historically used -e by mistake. - ( cd "$SCRIPT_DIR" && \ - faust -lang wasm \ - -cn "$CLASSNAME" \ - -O "$SCRIPT_DIR" \ - "${NAME}.dsp" \ - -o "${NAME}.wasm" ) - - # Note: `faust -lang wasm -O . foo.dsp -o foo.wasm` already writes - # foo.json alongside foo.wasm with the wasm-native descriptor (including - # numeric `index` fields that workers use as setParamValue zone addresses). - # The old standalone `-json` pass produced a smaller JSON WITHOUT the - # `index` field, which is the wrong shape for workers — don't run it. - if [ ! -f "$SCRIPT_DIR/${NAME}.json" ]; then - echo " WARNING: ${NAME}.json not produced — check faust version" - fi - - if [ -f "$SCRIPT_DIR/${NAME}.wasm" ]; then - WASM_SIZE="$(du -h "$SCRIPT_DIR/${NAME}.wasm" | cut -f1)" - echo " -> ${NAME}.wasm (${WASM_SIZE})" - fi - if [ -f "$SCRIPT_DIR/${NAME}.json" ]; then - echo " -> ${NAME}.json" - fi -done - -echo "" -echo "Done. Load engines in the playground via FaustEngineBase:" -echo " import { AdditiveEngine } from './js/synth/additive-engine.js';" diff --git a/playground/faust/eoc-compressor-processor.js b/playground/faust/eoc-compressor-processor.js deleted file mode 100644 index c77e727..0000000 --- a/playground/faust/eoc-compressor-processor.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * eoc-compressor-processor.js — AudioWorklet processor for the stereo compressor - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads eoc-compressor.wasm compiled from eoc-compressor.dsp. - * - * 7 params (alphabetical order as emitted by Faust JSON): - * 0: attack (ms) - * 1: knee (dB) - * 2: makeup (dB) - * 3: mix (0–1) - * 4: ratio - * 5: release (ms) - * 6: threshold (dB) - */ - -// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first. - -class EOCCompressorProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._exports = null; - this._heap = null; - this._heapi32 = null; - this._mem = null; - this._paramAddresses = []; - this._inputL = null; - this._inputR = null; - this._sampleRate = 48000; - } - - // --------------------------------------------------------------------------- - // FaustWorkletProcessor overrides - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - this._exports = instance.exports; - this._mem = memory; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - - const exps = this._exports; - - if (exps.createDSPInstance) { - this._dsp = exps.createDSPInstance(); - } else if (exps.eoc_compressor) { - this._dsp = exps.eoc_compressor(); - } else { - const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); - console.warn('[EOCCompressorProcessor] No DSP constructor found; exports:', fnKeys); - return; - } - - exps.init(this._dsp, sr); - this._buildParamIndex(exps, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items) { - for (const item of items) { - const type = item.type ?? ''; - if (['hslider', 'vslider', 'nentry'].includes(type)) { - addresses.push(item.address ?? ''); - } else if (item.items) { - walk(item.items); - } - } - } - - walk(desc.ui ?? []); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr !== undefined && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - - const exps = this._exports; - const mem = this._mem; - const heap = this._heap; - const i32 = this._heapi32; - - const heapWords = mem.buffer.byteLength >> 2; - const inLOff = heapWords - blockSize * 6 - 32; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - const srcL = this._inputL; - const srcR = this._inputR; - if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; - if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; - - exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } - - process(inputs, outputs, params) { - const inp = inputs[0]; - this._inputL = inp?.[0] ?? null; - this._inputR = inp?.[1] ?? inp?.[0] ?? null; - return super.process(inputs, outputs, params); - } -} - -registerProcessor('eoc-compressor-processor', EOCCompressorProcessor); diff --git a/playground/faust/eoc-compressor.dsp b/playground/faust/eoc-compressor.dsp deleted file mode 100644 index 3b467ba..0000000 --- a/playground/faust/eoc-compressor.dsp +++ /dev/null @@ -1,34 +0,0 @@ -// eoc-compressor.dsp — Stereo feed-forward compressor for MEMLNaut EOC chain -// -// 7 params: threshold, ratio, attack, release, knee, makeup, mix -// -// Compile: -// faust -lang wasm -cn eoc_compressor -e eoc-compressor.dsp -o eoc-compressor.wasm -json - -import("stdfaust.lib"); - -threshold = hslider("threshold[unit:dB]", -24.0, -60.0, 0.0, 0.1); -ratio = hslider("ratio", 4.0, 1.0, 20.0, 0.1); -attack = hslider("attack[unit:ms]", 10.0, 0.1, 200.0, 0.1); -release = hslider("release[unit:ms]", 100.0, 10.0, 2000.0, 1.0); -knee = hslider("knee[unit:dB]", 6.0, 0.0, 24.0, 0.1); -makeup = hslider("makeup[unit:dB]", 0.0, 0.0, 24.0, 0.1); -mix = hslider("mix", 1.0, 0.0, 1.0, 0.001); - -// Convert ms to seconds for Faust -attackSec = attack / 1000.0; -releaseSec = release / 1000.0; - -// Soft-knee threshold adjustment (shift threshold down by half the knee) -threshKnee = threshold - knee / 2.0; - -// Makeup gain as linear multiplier -makeupLin = ba.db2linear(makeup); - -// Compressor on a single channel with makeup applied -compCh(x) = co.compressor_mono(ratio, threshKnee, attackSec, releaseSec, x) * makeupLin; - -// Parallel compression (dry/wet blend) -parallelComp(x) = (1.0 - mix) * x + mix * compCh(x); - -process = parallelComp, parallelComp; diff --git a/playground/faust/eoc-compressor.json b/playground/faust/eoc-compressor.json deleted file mode 100644 index 827bf86..0000000 --- a/playground/faust/eoc-compressor.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-compressor","filename": "eoc-compressor.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_compressor -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/compressors.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/analyzers.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 68,"code": "Ly8gZW9jLWNvbXByZXNzb3IuZHNwIOKAlCBTdGVyZW8gZmVlZC1mb3J3YXJkIGNvbXByZXNzb3IgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyA3IHBhcmFtczogdGhyZXNob2xkLCByYXRpbywgYXR0YWNrLCByZWxlYXNlLCBrbmVlLCBtYWtldXAsIG1peAovLwovLyBDb21waWxlOgovLyAgIGZhdXN0IC1sYW5nIHdhc20gLWNuIGVvY19jb21wcmVzc29yIC1lIGVvYy1jb21wcmVzc29yLmRzcCAtbyBlb2MtY29tcHJlc3Nvci53YXNtIC1qc29uCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKdGhyZXNob2xkID0gaHNsaWRlcigidGhyZXNob2xkW3VuaXQ6ZEJdIiwgLTI0LjAsIC02MC4wLCAgIDAuMCwgIDAuMSk7CnJhdGlvICAgICA9IGhzbGlkZXIoInJhdGlvIiwgICAgICAgICAgICAgICAgNC4wLCAgIDEuMCwgIDIwLjAsICAwLjEpOwphdHRhY2sgICAgPSBoc2xpZGVyKCJhdHRhY2tbdW5pdDptc10iLCAgICAgMTAuMCwgICAwLjEsIDIwMC4wLCAgMC4xKTsKcmVsZWFzZSAgID0gaHNsaWRlcigicmVsZWFzZVt1bml0Om1zXSIsICAgMTAwLjAsICAxMC4wLCAyMDAwLjAsIDEuMCk7CmtuZWUgICAgICA9IGhzbGlkZXIoImtuZWVbdW5pdDpkQl0iLCAgICAgICAgNi4wLCAgIDAuMCwgIDI0LjAsICAwLjEpOwptYWtldXAgICAgPSBoc2xpZGVyKCJtYWtldXBbdW5pdDpkQl0iLCAgICAgIDAuMCwgICAwLjAsICAyNC4wLCAgMC4xKTsKbWl4ICAgICAgID0gaHNsaWRlcigibWl4IiwgICAgICAgICAgICAgICAgICAxLjAsICAgMC4wLCAgIDEuMCwgIDAuMDAxKTsKCi8vIENvbnZlcnQgbXMgdG8gc2Vjb25kcyBmb3IgRmF1c3QKYXR0YWNrU2VjICA9IGF0dGFjayAgLyAxMDAwLjA7CnJlbGVhc2VTZWMgPSByZWxlYXNlIC8gMTAwMC4wOwoKLy8gU29mdC1rbmVlIHRocmVzaG9sZCBhZGp1c3RtZW50IChzaGlmdCB0aHJlc2hvbGQgZG93biBieSBoYWxmIHRoZSBrbmVlKQp0aHJlc2hLbmVlID0gdGhyZXNob2xkIC0ga25lZSAvIDIuMDsKCi8vIE1ha2V1cCBnYWluIGFzIGxpbmVhciBtdWx0aXBsaWVyCm1ha2V1cExpbiA9IGJhLmRiMmxpbmVhcihtYWtldXApOwoKLy8gQ29tcHJlc3NvciBvbiBhIHNpbmdsZSBjaGFubmVsIHdpdGggbWFrZXVwIGFwcGxpZWQKY29tcENoKHgpID0gY28uY29tcHJlc3Nvcl9tb25vKHJhdGlvLCB0aHJlc2hLbmVlLCBhdHRhY2tTZWMsIHJlbGVhc2VTZWMsIHgpICogbWFrZXVwTGluOwoKLy8gUGFyYWxsZWwgY29tcHJlc3Npb24gKGRyeS93ZXQgYmxlbmQpCnBhcmFsbGVsQ29tcCh4KSA9ICgxLjAgLSBtaXgpICogeCArIG1peCAqIGNvbXBDaCh4KTsKCnByb2Nlc3MgPSBwYXJhbGxlbENvbXAsIHBhcmFsbGVsQ29tcDsK","inputs": 2,"outputs": 2,"meta": [ { "analyzers.lib/amp_follower_ar:author": "Jonatan Liljedahl, revised by Romain Michon" },{ "analyzers.lib/name": "Faust Analyzer Library" },{ "analyzers.lib/version": "1.2.0" },{ "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn eoc_compressor -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "compressors.lib/compression_gain_mono:author": "Julius O. Smith III" },{ "compressors.lib/compression_gain_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compression_gain_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/compressor_lad_mono:author": "Julius O. Smith III" },{ "compressors.lib/compressor_lad_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compressor_lad_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/compressor_mono:author": "Julius O. Smith III" },{ "compressors.lib/compressor_mono:copyright": "Copyright (C) 2014-2020 by Julius O. Smith III " },{ "compressors.lib/compressor_mono:license": "MIT-style STK-4.3 license" },{ "compressors.lib/name": "Faust Compressor Effect Library" },{ "compressors.lib/version": "1.6.0" },{ "filename": "eoc-compressor.dsp" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-compressor" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/onePoleSwitching:author": "Jonatan Liljedahl, revised by Dario Sanfilippo" },{ "signals.lib/onePoleSwitching:licence": "STK-4.3" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "eoc-compressor","items": [ {"type": "hslider","label": "attack","varname": "fHslider0","shortname": "attack","address": "/eoc-compressor/attack","index": 0,"meta": [{ "unit": "ms" }],"init": 10,"min": 0.1,"max": 200,"step": 0.1},{"type": "hslider","label": "knee","varname": "fHslider3","shortname": "knee","address": "/eoc-compressor/knee","index": 28,"meta": [{ "unit": "dB" }],"init": 6,"min": 0,"max": 24,"step": 0.1},{"type": "hslider","label": "makeup","varname": "fHslider5","shortname": "makeup","address": "/eoc-compressor/makeup","index": 44,"meta": [{ "unit": "dB" }],"init": 0,"min": 0,"max": 24,"step": 0.1},{"type": "hslider","label": "mix","varname": "fHslider6","shortname": "mix","address": "/eoc-compressor/mix","index": 48,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "ratio","varname": "fHslider4","shortname": "ratio","address": "/eoc-compressor/ratio","index": 32,"init": 4,"min": 1,"max": 20,"step": 0.1},{"type": "hslider","label": "release","varname": "fHslider2","shortname": "release","address": "/eoc-compressor/release","index": 16,"meta": [{ "unit": "ms" }],"init": 100,"min": 10,"max": 2000,"step": 1},{"type": "hslider","label": "threshold","varname": "fHslider1","shortname": "threshold","address": "/eoc-compressor/threshold","index": 12,"meta": [{ "unit": "dB" }],"init": -24,"min": -60,"max": 0,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-compressor.wasm b/playground/faust/eoc-compressor.wasm deleted file mode 100644 index 90a88da5b9baa6189c207b6f34d692f788d2f5a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7859 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 diff --git a/playground/faust/eoc-delay-processor.js b/playground/faust/eoc-delay-processor.js deleted file mode 100644 index 9d05be2..0000000 --- a/playground/faust/eoc-delay-processor.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * eoc-delay-processor.js — AudioWorklet processor for the EOC Stereo Delay. - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads eoc-delay.wasm compiled from eoc-delay.dsp. - * - * Parameter index order (alphabetical within group, matching eoc-delay.json): - * 0 feedback [0, 0.95] - * 1 lp_cutoff [500, 20000] - * 2 mix [0, 1] - * 3 ping_pong [0, 1] - * 4 spread [0, 1] - * 5 sync [0, 3] (nentry) - * 6 time [1, 2000] - */ - -// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js. - -class EOCDelayProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._paramAddresses = []; - this._blockSize = 128; - this._sampleRate = 48000; - } - - // --------------------------------------------------------------------------- - // FaustWorkletProcessor overrides - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - const exports = instance.exports; - - this._exports = exports; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - this._mem = memory; - - // Create DSP instance - if (exports.createDSPInstance) { - this._dsp = exports.createDSPInstance(); - } else if (exports.eoc_delay) { - this._dsp = exports.eoc_delay(); - } else { - console.warn('[EOCDelayProcessor] No DSP factory found; exports:', Object.keys(exports)); - return; - } - - exports.init(this._dsp, sr); - this._buildParamIndex(exports, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items, path) { - for (const item of items) { - const label = item.label ?? ''; - const type = item.type ?? ''; - const addr = item.address ?? (path + '/' + label); - if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) { - addresses.push(addr); - } else if (item.items) { - walk(item.items, path + '/' + label); - } - } - } - - walk(desc.ui ?? [], ''); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - const exports = this._exports; - const mem = this._mem; - const heap = this._heap; - - const heapBytes = mem.buffer.byteLength; - const inLOff = (heapBytes >> 2) - blockSize * 4 - 128; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - - // Zero input buffers (delay is an effect — pass through audio) - // Input pointers - const i32 = this._heapi32; - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - exports.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } -} - -registerProcessor('eoc-delay-processor', EOCDelayProcessor); diff --git a/playground/faust/eoc-delay.dsp b/playground/faust/eoc-delay.dsp deleted file mode 100644 index d93bed1..0000000 --- a/playground/faust/eoc-delay.dsp +++ /dev/null @@ -1,103 +0,0 @@ -// eoc-delay.dsp — Stereo delay effect for the EOC rack. -// -// Parameters (7): -// time [250ms, 1-2000ms] delay time (ms) when sync=0 -// feedback [0.3, 0-0.95] feedback amount -// lp_cutoff [8000, 500-20000] LP filter cutoff on feedback path (Hz) -// ping_pong [0.0, 0-1] 0=normal stereo, 1=full ping-pong L<->R -// spread [0.5, 0-1] stereo width of delay tails -// sync [0] nentry: 0=free, 1=half, 2=quarter, 3=eighth (120bpm) -// mix [0.3, 0-1] dry/wet mix - -import("stdfaust.lib"); - -// --------------------------------------------------------------------------- -// UI -// --------------------------------------------------------------------------- - -time = hslider("time[unit:ms]", 250, 1, 2000, 0.1); -feedback = hslider("feedback", 0.3, 0, 0.95, 0.001); -lp_cutoff = hslider("lp_cutoff[unit:Hz]", 8000, 500, 20000, 1); -ping_pong = hslider("ping_pong", 0.0, 0, 1, 0.001); -spread = hslider("spread", 0.5, 0, 1, 0.001); -sync = nentry("sync", 0, 0, 3, 1); -mix = hslider("mix", 0.3, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Derived delay time: free or tempo-synced at 120bpm -// --------------------------------------------------------------------------- - -bpm = 120.0; -beat_ms = 60000.0 / bpm; - -sync_time_ms = - ba.if(sync < 0.5, time, - ba.if(sync < 1.5, beat_ms * 2.0, - ba.if(sync < 2.5, beat_ms, - beat_ms * 0.5))); - -// Fixed max delay: 96000 samples (1s at 96kHz, covers all tempos for eighth notes at 30bpm+) -max_delay_samp = 96000; - -// --------------------------------------------------------------------------- -// Stereo ping-pong delay. -// Uses f ~ (_, _) pattern for stereo feedback loop. -// de.delay used for variable delay (no LP on feedback path inside the loop — -// LP is applied to the whole wet signal for efficiency). -// --------------------------------------------------------------------------- - -dSamp = int(sync_time_ms * float(ma.SR) / 1000.0); - -delayCore(inL, inR, fbL, fbR) = wetL, wetR -with { - pp = ping_pong; - fb = feedback; - mixL = inL + (fbL * (1.0 - pp) + fbR * pp) * fb; - mixR = inR + (fbR * (1.0 - pp) + fbL * pp) * fb; - wetL = de.delay(max_delay_samp, dSamp, mixL); - wetR = de.delay(max_delay_samp, dSamp, mixR); -}; - -stereoDelay = (_, _) : delayCore ~ (_, _); - -// --------------------------------------------------------------------------- -// Post-delay LP filter (tone shaping on the feedback tail) -// --------------------------------------------------------------------------- - -delayLP(x) = fi.lowpass(1, lp_cutoff, x); - -// --------------------------------------------------------------------------- -// Mid-side stereo spread -// --------------------------------------------------------------------------- - -msSpread(inL, inR) = outL, outR -with { - w = spread; - mid = (inL + inR) * 0.5; - side = (inL - inR) * 0.5; - outL = mid + side * w * 2.0; - outR = mid - side * w * 2.0; -}; - -// --------------------------------------------------------------------------- -// Main process: dry/wet mix -// --------------------------------------------------------------------------- - -// Delay + LP + spread on two signals -delayAndProcess(inL, inR) = wideL, wideR -with { - wetL0 = stereoDelay(inL, inR) : _,!; - wetR0 = stereoDelay(inL, inR) : !,_; - wetL1 = delayLP(wetL0); - wetR1 = delayLP(wetR0); - wideL = msSpread(wetL1, wetR1) : _,!; - wideR = msSpread(wetL1, wetR1) : !,_; -}; - -process(inL, inR) = outL, outR -with { - wideL = delayAndProcess(inL, inR) : _,!; - wideR = delayAndProcess(inL, inR) : !,_; - outL = inL * (1.0 - mix) + wideL * mix; - outR = inR * (1.0 - mix) + wideR * mix; -}; diff --git a/playground/faust/eoc-delay.json b/playground/faust/eoc-delay.json deleted file mode 100644 index 1373391..0000000 --- a/playground/faust/eoc-delay.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-delay","filename": "eoc-delay.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_delay -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/delays.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 1048656,"code": "Ly8gZW9jLWRlbGF5LmRzcCDigJQgU3RlcmVvIGRlbGF5IGVmZmVjdCBmb3IgdGhlIEVPQyByYWNrLgovLwovLyBQYXJhbWV0ZXJzICg3KToKLy8gICB0aW1lICAgICAgWzI1MG1zLCAxLTIwMDBtc10gICBkZWxheSB0aW1lIChtcykgd2hlbiBzeW5jPTAKLy8gICBmZWVkYmFjayAgWzAuMywgIDAtMC45NV0gICAgICBmZWVkYmFjayBhbW91bnQKLy8gICBscF9jdXRvZmYgWzgwMDAsIDUwMC0yMDAwMF0gICBMUCBmaWx0ZXIgY3V0b2ZmIG9uIGZlZWRiYWNrIHBhdGggKEh6KQovLyAgIHBpbmdfcG9uZyBbMC4wLCAgMC0xXSAgICAgICAgIDA9bm9ybWFsIHN0ZXJlbywgMT1mdWxsIHBpbmctcG9uZyBMPC0+UgovLyAgIHNwcmVhZCAgICBbMC41LCAgMC0xXSAgICAgICAgIHN0ZXJlbyB3aWR0aCBvZiBkZWxheSB0YWlscwovLyAgIHN5bmMgICAgICBbMF0gICAgICAgICAgICAgICAgIG5lbnRyeTogMD1mcmVlLCAxPWhhbGYsIDI9cXVhcnRlciwgMz1laWdodGggKDEyMGJwbSkKLy8gICBtaXggICAgICAgWzAuMywgIDAtMV0gICAgICAgICBkcnkvd2V0IG1peAoKaW1wb3J0KCJzdGRmYXVzdC5saWIiKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBVSQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCnRpbWUgICAgICA9IGhzbGlkZXIoInRpbWVbdW5pdDptc10iLCAgICAgIDI1MCwgMSwgICAyMDAwLCAgMC4xKTsKZmVlZGJhY2sgID0gaHNsaWRlcigiZmVlZGJhY2siLCAgICAgICAgICAgMC4zLCAwLCAgIDAuOTUsICAwLjAwMSk7CmxwX2N1dG9mZiA9IGhzbGlkZXIoImxwX2N1dG9mZlt1bml0Okh6XSIsIDgwMDAsIDUwMCwgMjAwMDAsIDEpOwpwaW5nX3BvbmcgPSBoc2xpZGVyKCJwaW5nX3BvbmciLCAgICAgICAgICAwLjAsIDAsICAgMSwgICAgIDAuMDAxKTsKc3ByZWFkICAgID0gaHNsaWRlcigic3ByZWFkIiwgICAgICAgICAgICAgMC41LCAwLCAgIDEsICAgICAwLjAwMSk7CnN5bmMgICAgICA9IG5lbnRyeSgic3luYyIsICAgICAgICAgICAgICAgIDAsICAgMCwgICAzLCAgICAgMSk7Cm1peCAgICAgICA9IGhzbGlkZXIoIm1peCIsICAgICAgICAgICAgICAgIDAuMywgMCwgICAxLCAgICAgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIERlcml2ZWQgZGVsYXkgdGltZTogZnJlZSBvciB0ZW1wby1zeW5jZWQgYXQgMTIwYnBtCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKYnBtID0gMTIwLjA7CmJlYXRfbXMgPSA2MDAwMC4wIC8gYnBtOwoKc3luY190aW1lX21zID0KICAgIGJhLmlmKHN5bmMgPCAwLjUsIHRpbWUsCiAgICBiYS5pZihzeW5jIDwgMS41LCBiZWF0X21zICogMi4wLAogICAgYmEuaWYoc3luYyA8IDIuNSwgYmVhdF9tcywKICAgICAgICAgICAgICAgICAgICAgIGJlYXRfbXMgKiAwLjUpKSk7CgovLyBGaXhlZCBtYXggZGVsYXk6IDk2MDAwIHNhbXBsZXMgKDFzIGF0IDk2a0h6LCBjb3ZlcnMgYWxsIHRlbXBvcyBmb3IgZWlnaHRoIG5vdGVzIGF0IDMwYnBtKykKbWF4X2RlbGF5X3NhbXAgPSA5NjAwMDsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTdGVyZW8gcGluZy1wb25nIGRlbGF5LgovLyBVc2VzIGYgfiAoXywgXykgcGF0dGVybiBmb3Igc3RlcmVvIGZlZWRiYWNrIGxvb3AuCi8vIGRlLmRlbGF5IHVzZWQgZm9yIHZhcmlhYmxlIGRlbGF5IChubyBMUCBvbiBmZWVkYmFjayBwYXRoIGluc2lkZSB0aGUgbG9vcCDigJQKLy8gTFAgaXMgYXBwbGllZCB0byB0aGUgd2hvbGUgd2V0IHNpZ25hbCBmb3IgZWZmaWNpZW5jeSkuCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZFNhbXAgPSBpbnQoc3luY190aW1lX21zICogZmxvYXQobWEuU1IpIC8gMTAwMC4wKTsKCmRlbGF5Q29yZShpbkwsIGluUiwgZmJMLCBmYlIpID0gd2V0TCwgd2V0Ugp3aXRoIHsKICAgIHBwICA9IHBpbmdfcG9uZzsKICAgIGZiICA9IGZlZWRiYWNrOwogICAgbWl4TCA9IGluTCArIChmYkwgKiAoMS4wIC0gcHApICsgZmJSICogcHApICogZmI7CiAgICBtaXhSID0gaW5SICsgKGZiUiAqICgxLjAgLSBwcCkgKyBmYkwgKiBwcCkgKiBmYjsKICAgIHdldEwgPSBkZS5kZWxheShtYXhfZGVsYXlfc2FtcCwgZFNhbXAsIG1peEwpOwogICAgd2V0UiA9IGRlLmRlbGF5KG1heF9kZWxheV9zYW1wLCBkU2FtcCwgbWl4Uik7Cn07CgpzdGVyZW9EZWxheSA9IChfLCBfKSA6IGRlbGF5Q29yZSB+IChfLCBfKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBQb3N0LWRlbGF5IExQIGZpbHRlciAodG9uZSBzaGFwaW5nIG9uIHRoZSBmZWVkYmFjayB0YWlsKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmRlbGF5TFAoeCkgPSBmaS5sb3dwYXNzKDEsIGxwX2N1dG9mZiwgeCk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gTWlkLXNpZGUgc3RlcmVvIHNwcmVhZAovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCm1zU3ByZWFkKGluTCwgaW5SKSA9IG91dEwsIG91dFIKd2l0aCB7CiAgICB3ICAgID0gc3ByZWFkOwogICAgbWlkICA9IChpbkwgKyBpblIpICogMC41OwogICAgc2lkZSA9IChpbkwgLSBpblIpICogMC41OwogICAgb3V0TCA9IG1pZCArIHNpZGUgKiB3ICogMi4wOwogICAgb3V0UiA9IG1pZCAtIHNpZGUgKiB3ICogMi4wOwp9OwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIE1haW4gcHJvY2VzczogZHJ5L3dldCBtaXgKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgovLyBEZWxheSArIExQICsgc3ByZWFkIG9uIHR3byBzaWduYWxzCmRlbGF5QW5kUHJvY2VzcyhpbkwsIGluUikgPSB3aWRlTCwgd2lkZVIKd2l0aCB7CiAgICB3ZXRMMCA9IHN0ZXJlb0RlbGF5KGluTCwgaW5SKSA6IF8sITsKICAgIHdldFIwID0gc3RlcmVvRGVsYXkoaW5MLCBpblIpIDogISxfOwogICAgd2V0TDEgPSBkZWxheUxQKHdldEwwKTsKICAgIHdldFIxID0gZGVsYXlMUCh3ZXRSMCk7CiAgICB3aWRlTCA9IG1zU3ByZWFkKHdldEwxLCB3ZXRSMSkgOiBfLCE7CiAgICB3aWRlUiA9IG1zU3ByZWFkKHdldEwxLCB3ZXRSMSkgOiAhLF87Cn07Cgpwcm9jZXNzKGluTCwgaW5SKSA9IG91dEwsIG91dFIKd2l0aCB7CiAgICB3aWRlTCA9IGRlbGF5QW5kUHJvY2VzcyhpbkwsIGluUikgOiBfLCE7CiAgICB3aWRlUiA9IGRlbGF5QW5kUHJvY2VzcyhpbkwsIGluUikgOiAhLF87CiAgICBvdXRMID0gaW5MICogKDEuMCAtIG1peCkgKyB3aWRlTCAqIG1peDsKICAgIG91dFIgPSBpblIgKiAoMS4wIC0gbWl4KSArIHdpZGVSICogbWl4Owp9Owo=","inputs": 2,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn eoc_delay -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "delays.lib/name": "Faust Delay Library" },{ "delays.lib/version": "1.1.0" },{ "filename": "eoc-delay.dsp" },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-delay" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "eoc-delay","items": [ {"type": "hslider","label": "feedback","varname": "fHslider1","shortname": "feedback","address": "/eoc-delay/feedback","index": 4,"init": 0.3,"min": 0,"max": 0.95,"step": 0.001},{"type": "hslider","label": "lp_cutoff","varname": "fHslider3","shortname": "lp_cutoff","address": "/eoc-delay/lp_cutoff","index": 1048624,"meta": [{ "unit": "Hz" }],"init": 8000,"min": 500,"max": 20000,"step": 1},{"type": "hslider","label": "mix","varname": "fHslider5","shortname": "mix","address": "/eoc-delay/mix","index": 1048652,"init": 0.3,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "ping_pong","varname": "fHslider0","shortname": "ping_pong","address": "/eoc-delay/ping_pong","index": 0,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "spread","varname": "fHslider4","shortname": "spread","address": "/eoc-delay/spread","index": 1048648,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "nentry","label": "sync","varname": "fEntry0","shortname": "sync","address": "/eoc-delay/sync","index": 524300,"init": 0,"min": 0,"max": 3,"step": 1},{"type": "hslider","label": "time","varname": "fHslider2","shortname": "time","address": "/eoc-delay/time","index": 524304,"meta": [{ "unit": "ms" }],"init": 250,"min": 1,"max": 2000,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-delay.wasm b/playground/faust/eoc-delay.wasm deleted file mode 100644 index bfcc5f29afa9b0420c200881de69564cfacaed3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10699 zcmcgyTW=#rb|$IIT&TI&Yp;Pkwe;k8M^cj%WztF3A}NY&QPfDH#9LsDyPK_Mi`^uL z-MktDDG3rRl1;LhkvG{S5Brb@KLp4Hc`$+iF@glhLl6Xc&40)b*q684?Yc?pZh+YKgWN*PyOfqbN&aT{%7MZ z&G1I;u4v%nt{SO1*W>@7TTC| z2XaTYin7_&d<*nkkgc|<9m!a}IBzr!$9LO+Kxzr^mm#5QKZg{^L*Pq1gw4pIitFY~(8%!OUozKMPy@`4_1L8chX*!OJfmE-_xUpE3<>VPSSGcp$C~G#FFz!*|G3y%ev3N)i4NHS6mP#+ zW@eb%3T#1;J7=WHJHou%dp7|k-VfYP{PLH#o+bj$87=>7=&&RNg?BH7yTUt{&o7_H z0#ME+6>c}i)L<41{QN>le1YFfeE#NQi}87f_`Eye;}O5a=?hYqkLGY?=Uo(w;~Tz*ejgkgm5 zk?L;??+M@Mx(m1XC=_9%FFcDdmHlU%Frbf^WAhl9k3T`^7>UfEuyKs^;ZN8$7J$sc z9XQ$Y9cl!>nVsKS3M~2I3c=rJFi8X+O!a`D3!hK$34S`p2f;t&G?3!*LU<7TlkxOJ zKM_5=8~j(G&F5b{2NL}F1@MD^Pvb4&W704C$wLxLf}gwqXW;YU(DCFyTlS;5xVSwL zjyN%L=nLdbF9^_r01eKN&oOcwJU}%~K1bwp7`)1V-d*sOPne%sF;6SzX>ewP1xEkn z1?U%M=og57fd*&j7a9FmM87ygzex0pG&rFT{+DZL!M{nSCMUi4jVwbLxjlyd;m>{= z3;a2oYQ6n?ACWWQ37(380P_ERAZ@v0pT`K+!JkYH>yF@i7{u>(@PFKWJo{tiU;ON6 zKa2$sAl)&<8`dACVia3g{}<^P#RyLGaf|{1`g|o~!PH*a`E$X)Aozk0t_=DX1V08@ z(%~K^1mFO{;FteS9^^~-f`6a>Y^5n%+R8`5iq=wBYnmw!S01gL8m9Kr_*%_wW3H!l zY@^krCE>LX<7*L&QU1b`C#|-FDVye2O}VKHeI#CCRds|A{x${d@r3OmtZFumS!zuP zZKAEA6~i0U8w!z#uxcAk-PDBDQ)eWI*upfFj@%hOF%8=R|KDC&ZyJMj+i7*Q_2Jpm zwi=cuOdeLsyLF{h6szUk zk(AOSx#L!j_L5RbQ7(l{DV0D!p_fKdD4z+9_EL$#-m%orZzml!BobYtS{l@~f=6F> z)L}!fh3lqbBu82)dUkM}@OWC)Qn697cFyD>@l15{!@e$UC!Bn0Bf4KCezae5iY^KH z;vuDZ=<%@Co%mU;eAKI274Xm@OTw16Px|?kI7Gjn-(hm)Pryem4WOH(S0Y7G30EyC z6Yolys#z@^8DtA7o2=I|x}HndH*<$%6YQT&wiT;(s)DvUOe&DA550BpACwE+9!$>d zL|n1rL#4E1OWA$0p{YQh{Bg*tl?E6iKC0vL$seS|ADocw*&h3SoLRlf^`$*R*X}Xq zn~cb%BT-HzdsSm%^GeCIRd$vn_kL8d@}3P9Zigv(*ZeJ-hyz_A6vhMDAm)afGGDlXWTpZO>QCluaM()_orHoilN{P;1;XD4Z zb59lv{QSTB{R&XIdDK=)C!XEnQl>sqGG+t5+LG8@QK^-p?b>#maAuHiaGwJXQ#g|X zaFa?ff9HO)F(8`(_hvP-Td#y|;7Zix>^|&C=cpS~a}$0pU-%zkoB28L*nNC_VuOEw z?+kbI01X16kg&QtuxeNR@L9mdGkY;SH`;9;`n zifw7`l{{JeB!2+U!I&rFReqc7%VYU>jvwHYVKq&8$P8CYhdR#Cu9T4nGNw~SoT)Z< z&1xapQ;nopCEO1~EH@BsrF@8(DfKJOBC1TFDAk) zm%x@%Dy9=H*^`}dD9&?FQ2w`J}FtlXeyM$ZhY8q|A;; zbFx_9gv@73q-v_oyk23s4z))a=R4#zN3~=&ZM__sD??8ihgP&T4m(3@v)N$rW= zj>u%!tj#f%O#=If@zeF-$eqhn4K_DbOCmln>?tL4OcByo0Bx ztJwtPwn?6X1fBRZ=_Y;X#tPI`^#aS`r6_1@J%>F{jKova=>xelLY;Jtpz@ot`~;T=bX4P4v9R>(Q&KIIRl53>!+`4ivG zWkPjrC(ij5Z#$_kB6-1;!bFxuBqhZf+Az-&AOyue;)ccfe`Y|=ZIhsg-btB4z3 z4e4P7{@!5mAjRShVY01IjG=svn$n}=@tXNR#U;Yq zPV;zA-(^k)hdJVv?z7xD#d|=!-5h`#qaJ~rg*?RtUaPu#F~6GNh4aF@Mv~JP8v0`+ ziCCVVS?BuW*Q`s__jY1lj==Yj_*s?W)i=Z>w?E0r?Rxl{^Ln+czsg!yUT&WrzCq>P z{G2CDBF=Vk&JN3;JTH4Te9FeQz1o4r7CIw3!rF_T>+j=E(HHQ}|3+nS8um_rB8a&=+nYNndq^w5eHI(-HRgC-W8I z*`wJ-Q=jldYvItE=#qTrpYgdQlP|?r#NVdthbv-F5zmM{k^0q_6@0s?&&c?~+i%OZ zEk3CmdYyV9*mNZ?9k1HX5Z4Wb#Ytk+U=S~*A^bH z2?fh=>VhOmuV(SB*Qu@cumkz&9%LouJqYioeq9KQVq`TehT?)U6t0m*_|3DHy``!h zOJ1XYB7kd!+L~7D{_r(odbaq6hMM)v9sVu<%Hd8M);SG*V~fz58?pv%^(`%drS`W` z&Avh1>~E{w3@ATb3%QOoz64vJpd)jX$fsYLpEmckNnEaaV%4RcbruGZ1MOfg) zy-?TSk$uG(w%LVHk8Y0WvelFo%_NvlBtedFG>ct+37?Hu)NRwKX`SgMrr?!UW=$JFtPM(72E`$9E%Io^GU!@Z zL`SAsaV;LjCXUu-526@CB6^*~X8VcSby}yV*NaRy%`+lT(_b(1G=(QLyM7BpbT6eM zId-`gE7=jeiQP3atzx1`+Ay`EtjiQbl%)q(v#-<2G6vV{6TPYrr@UShJ~yL76g9$Y z%pV#{D6W0I?c2C-f6{I>_3LG&EBG1PPt#v7@ic`eH>~DK?AdkfF5gUEyWP>`+V#S2 zToraqe7&UOX-`b%A{()ZuxoJSE8hJYahs^VI`r=-#_+X=P4$YbX`*?_tS;dzIbFEec5fiazQX_(veu#)(=)G diff --git a/playground/faust/eoc-eq-processor.js b/playground/faust/eoc-eq-processor.js deleted file mode 100644 index 168968b..0000000 --- a/playground/faust/eoc-eq-processor.js +++ /dev/null @@ -1,183 +0,0 @@ -/** - * eoc-eq-processor.js — AudioWorklet processor for the 4-band parametric EQ - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads eoc-eq.wasm compiled from eoc-eq.dsp. - * - * 10 exposed params (4 bands × freq/gain; mid bands also have Q): - * Band 1 (Low Shelf): freq1, gain1 - * Band 2 (Low-Mid): freq2, gain2, q2 - * Band 3 (High-Mid): freq3, gain3, q3 - * Band 4 (High Shelf): freq4, gain4 - * - * Note: shelf bands (1 and 4) omit Q — Faust fi.low_shelf / fi.high_shelf - * take only freq and gain. - * - * Parameter index order follows the Faust JSON descriptor (group declaration order, - * alphabetical within groups — confirmed by eoc-eq.json). - */ - -// Runs in AudioWorkletGlobalScope — faust-worklet-processor.js must be loaded first. - -class EOCEQProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._exports = null; - this._heap = null; - this._heapi32 = null; - this._mem = null; - this._paramAddresses = []; - this._sampleRate = 48000; - } - - // --------------------------------------------------------------------------- - // FaustWorkletProcessor overrides - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - this._exports = instance.exports; - this._mem = memory; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - - const exps = this._exports; - - // Create DSP instance - if (exps.createDSPInstance) { - this._dsp = exps.createDSPInstance(); - } else if (exps.eoc_eq) { - this._dsp = exps.eoc_eq(); - } else { - const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); - console.warn('[EOCEQProcessor] No DSP constructor found; exports:', fnKeys); - return; - } - - exps.init(this._dsp, sr); - this._buildParamIndex(exps, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items) { - for (const item of items) { - const type = item.type ?? ''; - if (['hslider', 'vslider', 'nentry'].includes(type)) { - addresses.push(item.address ?? ''); - } else if (item.items) { - walk(item.items); - } - } - } - - walk(desc.ui ?? []); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr !== undefined && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - - const exps = this._exports; - const mem = this._mem; - const heap = this._heap; - const i32 = this._heapi32; - - // Allocate input + output buffers at end of WASM heap - const heapWords = mem.buffer.byteLength >> 2; - const inLOff = heapWords - blockSize * 6 - 32; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - - // Copy inputs (silence — effect processes in-place from AudioWorklet inputs) - // The AudioWorklet process() provides inputs[0] to the raw in buffers via - // the overridden process() below. - // For now fill with stored input (set by process()). - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - // Copy input from staging buffers (filled in process()) - const srcL = this._inputL; - const srcR = this._inputR; - if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; - if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; - - exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } - - // Override process() to capture inputs before calling parent - process(inputs, outputs, params) { - const inp = inputs[0]; - this._inputL = inp?.[0] ?? null; - this._inputR = inp?.[1] ?? inp?.[0] ?? null; // mono fallback to L - return super.process(inputs, outputs, params); - } -} - -registerProcessor('eoc-eq-processor', EOCEQProcessor); diff --git a/playground/faust/eoc-eq.dsp b/playground/faust/eoc-eq.dsp deleted file mode 100644 index e6b949f..0000000 --- a/playground/faust/eoc-eq.dsp +++ /dev/null @@ -1,38 +0,0 @@ -// eoc-eq.dsp — 4-band parametric EQ for MEMLNaut EOC chain -// -// Band 1: Low Shelf (default 80 Hz, range 20–500) -// Band 2: Low-Mid bell (default 400 Hz, range 100–2000) -// Band 3: High-Mid bell (default 2500 Hz, range 500–8000) -// Band 4: High Shelf (default 8000 Hz, range 2000–20000) -// -// Compile: -// faust -lang wasm -cn eoc_eq -e eoc-eq.dsp -o eoc-eq.wasm -json - -import("stdfaust.lib"); - -// Band 1 — Low Shelf -freq1 = hslider("Band 1 (Low Shelf)/freq1[unit:Hz]", 80, 20, 500, 0.1); -gain1 = hslider("Band 1 (Low Shelf)/gain1[unit:dB]", 0, -12, 12, 0.1); -q1 = hslider("Band 1 (Low Shelf)/q1", 1.0, 0.1, 10.0, 0.01); - -// Band 2 — Low-Mid bell -freq2 = hslider("Band 2 (Low-Mid)/freq2[unit:Hz]", 400, 100, 2000, 1.0); -gain2 = hslider("Band 2 (Low-Mid)/gain2[unit:dB]", 0, -12, 12, 0.1); -q2 = hslider("Band 2 (Low-Mid)/q2", 1.0, 0.1, 10.0, 0.01); - -// Band 3 — High-Mid bell -freq3 = hslider("Band 3 (High-Mid)/freq3[unit:Hz]", 2500, 500, 8000, 1.0); -gain3 = hslider("Band 3 (High-Mid)/gain3[unit:dB]", 0, -12, 12, 0.1); -q3 = hslider("Band 3 (High-Mid)/q3", 1.0, 0.1, 10.0, 0.01); - -// Band 4 — High Shelf -freq4 = hslider("Band 4 (High Shelf)/freq4[unit:Hz]", 8000, 2000, 20000, 10.0); -gain4 = hslider("Band 4 (High Shelf)/gain4[unit:dB]", 0, -12, 12, 0.1); -q4 = hslider("Band 4 (High Shelf)/q4", 1.0, 0.1, 10.0, 0.01); - -eqChain = fi.low_shelf(gain1, freq1) : - fi.peak_eq(gain2, freq2, q2) : - fi.peak_eq(gain3, freq3, q3) : - fi.high_shelf(gain4, freq4); - -process = eqChain, eqChain; diff --git a/playground/faust/eoc-eq.json b/playground/faust/eoc-eq.json deleted file mode 100644 index 5ab4f90..0000000 --- a/playground/faust/eoc-eq.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-eq","filename": "eoc-eq.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_eq -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/analyzers.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 296,"code": "Ly8gZW9jLWVxLmRzcCDigJQgNC1iYW5kIHBhcmFtZXRyaWMgRVEgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyBCYW5kIDE6IExvdyBTaGVsZiAgKGRlZmF1bHQgODAgSHosIHJhbmdlIDIw4oCTNTAwKQovLyBCYW5kIDI6IExvdy1NaWQgYmVsbCAoZGVmYXVsdCA0MDAgSHosIHJhbmdlIDEwMOKAkzIwMDApCi8vIEJhbmQgMzogSGlnaC1NaWQgYmVsbCAoZGVmYXVsdCAyNTAwIEh6LCByYW5nZSA1MDDigJM4MDAwKQovLyBCYW5kIDQ6IEhpZ2ggU2hlbGYgKGRlZmF1bHQgODAwMCBIeiwgcmFuZ2UgMjAwMOKAkzIwMDAwKQovLwovLyBDb21waWxlOgovLyAgIGZhdXN0IC1sYW5nIHdhc20gLWNuIGVvY19lcSAtZSBlb2MtZXEuZHNwIC1vIGVvYy1lcS53YXNtIC1qc29uCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKLy8gQmFuZCAxIOKAlCBMb3cgU2hlbGYKZnJlcTEgID0gaHNsaWRlcigiQmFuZCAxIChMb3cgU2hlbGYpL2ZyZXExW3VuaXQ6SHpdIiwgIDgwLCAgICAyMCwgICAgNTAwLCAgIDAuMSk7CmdhaW4xICA9IGhzbGlkZXIoIkJhbmQgMSAoTG93IFNoZWxmKS9nYWluMVt1bml0OmRCXSIsICAgMCwgICAtMTIsICAgIDEyLCAgICAwLjEpOwpxMSAgICAgPSBoc2xpZGVyKCJCYW5kIDEgKExvdyBTaGVsZikvcTEiLCAgICAgICAgICAgICAgIDEuMCwgICAwLjEsICAxMC4wLCAgMC4wMSk7CgovLyBCYW5kIDIg4oCUIExvdy1NaWQgYmVsbApmcmVxMiAgPSBoc2xpZGVyKCJCYW5kIDIgKExvdy1NaWQpL2ZyZXEyW3VuaXQ6SHpdIiwgICA0MDAsICAgMTAwLCAgMjAwMCwgICAxLjApOwpnYWluMiAgPSBoc2xpZGVyKCJCYW5kIDIgKExvdy1NaWQpL2dhaW4yW3VuaXQ6ZEJdIiwgICAgIDAsICAgLTEyLCAgICAxMiwgICAgMC4xKTsKcTIgICAgID0gaHNsaWRlcigiQmFuZCAyIChMb3ctTWlkKS9xMiIsICAgICAgICAgICAgICAgICAxLjAsICAgMC4xLCAgMTAuMCwgIDAuMDEpOwoKLy8gQmFuZCAzIOKAlCBIaWdoLU1pZCBiZWxsCmZyZXEzICA9IGhzbGlkZXIoIkJhbmQgMyAoSGlnaC1NaWQpL2ZyZXEzW3VuaXQ6SHpdIiwgMjUwMCwgICA1MDAsICA4MDAwLCAgIDEuMCk7CmdhaW4zICA9IGhzbGlkZXIoIkJhbmQgMyAoSGlnaC1NaWQpL2dhaW4zW3VuaXQ6ZEJdIiwgICAgMCwgICAtMTIsICAgIDEyLCAgICAwLjEpOwpxMyAgICAgPSBoc2xpZGVyKCJCYW5kIDMgKEhpZ2gtTWlkKS9xMyIsICAgICAgICAgICAgICAgIDEuMCwgICAwLjEsICAxMC4wLCAgMC4wMSk7CgovLyBCYW5kIDQg4oCUIEhpZ2ggU2hlbGYKZnJlcTQgID0gaHNsaWRlcigiQmFuZCA0IChIaWdoIFNoZWxmKS9mcmVxNFt1bml0Okh6XSIsIDgwMDAsIDIwMDAsIDIwMDAwLCAgMTAuMCk7CmdhaW40ICA9IGhzbGlkZXIoIkJhbmQgNCAoSGlnaCBTaGVsZikvZ2FpbjRbdW5pdDpkQl0iLCAgICAwLCAgLTEyLCAgICAxMiwgICAgMC4xKTsKcTQgICAgID0gaHNsaWRlcigiQmFuZCA0IChIaWdoIFNoZWxmKS9xNCIsICAgICAgICAgICAgICAgMS4wLCAgIDAuMSwgIDEwLjAsICAwLjAxKTsKCmVxQ2hhaW4gPSBmaS5sb3dfc2hlbGYoZ2FpbjEsIGZyZXExKSA6CiAgICAgICAgICAgZmkucGVha19lcShnYWluMiwgZnJlcTIsIHEyKSA6CiAgICAgICAgICAgZmkucGVha19lcShnYWluMywgZnJlcTMsIHEzKSA6CiAgICAgICAgICAgZmkuaGlnaF9zaGVsZihnYWluNCwgZnJlcTQpOwoKcHJvY2VzcyA9IGVxQ2hhaW4sIGVxQ2hhaW47Cg==","inputs": 2,"outputs": 2,"meta": [ { "analyzers.lib/name": "Faust Analyzer Library" },{ "analyzers.lib/version": "1.2.0" },{ "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn eoc_eq -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "filename": "eoc-eq.dsp" },{ "filters.lib/filterbank:author": "Julius O. Smith III" },{ "filters.lib/filterbank:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/filterbank:license": "MIT-style STK-4.3 license" },{ "filters.lib/fir:author": "Julius O. Smith III" },{ "filters.lib/fir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/fir:license": "MIT-style STK-4.3 license" },{ "filters.lib/highpass:author": "Julius O. Smith III" },{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/highshelf:author": "Julius O. Smith III" },{ "filters.lib/highshelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/highshelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/iir:author": "Julius O. Smith III" },{ "filters.lib/iir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/iir:license": "MIT-style STK-4.3 license" },{ "filters.lib/low_shelf:author": "Julius O. Smith III" },{ "filters.lib/low_shelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/low_shelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowshelf:author": "Julius O. Smith III" },{ "filters.lib/lowshelf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowshelf:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/peak_eq:author": "Julius O. Smith III" },{ "filters.lib/peak_eq:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/peak_eq:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf2:author": "Julius O. Smith III" },{ "filters.lib/tf2:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf2:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf2s:author": "Julius O. Smith III" },{ "filters.lib/tf2s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf2s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-eq" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "eoc-eq","items": [ {"type": "hslider","label": "Band 1 (Low Shelf)/freq1","varname": "fHslider7","shortname": "Band_1_Low_Shelf_freq1","address": "/eoc-eq/Band_1__Low_Shelf__freq1","index": 44,"meta": [{ "unit": "Hz" }],"init": 80,"min": 20,"max": 500,"step": 0.1},{"type": "hslider","label": "Band 1 (Low Shelf)/gain1","varname": "fHslider8","shortname": "Band_1_Low_Shelf_gain1","address": "/eoc-eq/Band_1__Low_Shelf__gain1","index": 76,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 2 (Low-Mid)/freq2","varname": "fHslider5","shortname": "Band_2_Low_Mid_freq2","address": "/eoc-eq/Band_2__Low-Mid__freq2","index": 36,"meta": [{ "unit": "Hz" }],"init": 400,"min": 100,"max": 2000,"step": 1},{"type": "hslider","label": "Band 2 (Low-Mid)/gain2","varname": "fHslider4","shortname": "Band_2_Low_Mid_gain2","address": "/eoc-eq/Band_2__Low-Mid__gain2","index": 32,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 2 (Low-Mid)/q2","varname": "fHslider6","shortname": "Band_2_Low_Mid_q2","address": "/eoc-eq/Band_2__Low-Mid__q2","index": 40,"init": 1,"min": 0.1,"max": 10,"step": 0.01},{"type": "hslider","label": "Band 3 (High-Mid)/freq3","varname": "fHslider2","shortname": "Band_3_High_Mid_freq3","address": "/eoc-eq/Band_3__High-Mid__freq3","index": 20,"meta": [{ "unit": "Hz" }],"init": 2500,"min": 500,"max": 8000,"step": 1},{"type": "hslider","label": "Band 3 (High-Mid)/gain3","varname": "fHslider1","shortname": "Band_3_High_Mid_gain3","address": "/eoc-eq/Band_3__High-Mid__gain3","index": 16,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1},{"type": "hslider","label": "Band 3 (High-Mid)/q3","varname": "fHslider3","shortname": "Band_3_High_Mid_q3","address": "/eoc-eq/Band_3__High-Mid__q3","index": 28,"init": 1,"min": 0.1,"max": 10,"step": 0.01},{"type": "hslider","label": "Band 4 (High Shelf)/freq4","varname": "fHslider0","shortname": "Band_4_High_Shelf_freq4","address": "/eoc-eq/Band_4__High_Shelf__freq4","index": 0,"meta": [{ "unit": "Hz" }],"init": 8000,"min": 2000,"max": 20000,"step": 10},{"type": "hslider","label": "Band 4 (High Shelf)/gain4","varname": "fHslider9","shortname": "Band_4_High_Shelf_gain4","address": "/eoc-eq/Band_4__High_Shelf__gain4","index": 152,"meta": [{ "unit": "dB" }],"init": 0,"min": -12,"max": 12,"step": 0.1}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-eq.wasm b/playground/faust/eoc-eq.wasm deleted file mode 100644 index 9640bf6362632354c60f478f5c979f3452ec96c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14053 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 diff --git a/playground/faust/eoc-master-processor.js b/playground/faust/eoc-master-processor.js deleted file mode 100644 index 6d9de4b..0000000 --- a/playground/faust/eoc-master-processor.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * eoc-master-processor.js — AudioWorklet processor for the EOC Master Bus. - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads eoc-master.wasm compiled from eoc-master.dsp. - * - * Parameter index order (alphabetical within group, matching eoc-master.json): - * 0 dc_block [0, 1] (nentry) - * 1 gain [0, 2] - * 2 limiter_thresh [-12, 0] - * 3 width [0, 2] - */ - -// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js. - -class EOCMasterProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._paramAddresses = []; - this._blockSize = 128; - this._sampleRate = 48000; - } - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - const exports = instance.exports; - - this._exports = exports; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - this._mem = memory; - - if (exports.createDSPInstance) { - this._dsp = exports.createDSPInstance(); - } else if (exports.eoc_master) { - this._dsp = exports.eoc_master(); - } else { - console.warn('[EOCMasterProcessor] No DSP factory found; exports:', Object.keys(exports)); - return; - } - - exports.init(this._dsp, sr); - this._buildParamIndex(exports, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items, path) { - for (const item of items) { - const label = item.label ?? ''; - const type = item.type ?? ''; - const addr = item.address ?? (path + '/' + label); - if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) { - addresses.push(addr); - } else if (item.items) { - walk(item.items, path + '/' + label); - } - } - } - - walk(desc.ui ?? [], ''); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - const exports = this._exports; - const mem = this._mem; - const heap = this._heap; - - const heapBytes = mem.buffer.byteLength; - const inLOff = (heapBytes >> 2) - blockSize * 4 - 128; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - - const i32 = this._heapi32; - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - exports.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } -} - -registerProcessor('eoc-master-processor', EOCMasterProcessor); diff --git a/playground/faust/eoc-master.dsp b/playground/faust/eoc-master.dsp deleted file mode 100644 index f2f08fc..0000000 --- a/playground/faust/eoc-master.dsp +++ /dev/null @@ -1,85 +0,0 @@ -// eoc-master.dsp — Master bus effect for the EOC rack. -// -// Parameters (4): -// gain [1.0, 0-2] output gain multiplier -// width [1.0, 0-2] stereo width: 0=mono, 1=normal, 2=extra-wide -// limiter_thresh [-1.0, -12 to 0] brick-wall limiter threshold (dB) -// dc_block [1] nentry: 0=off, 1=on — DC blocking filter - -import("stdfaust.lib"); - -// --------------------------------------------------------------------------- -// UI -// --------------------------------------------------------------------------- - -gain = hslider("gain", 1.0, 0, 2, 0.001); -width = hslider("width", 1.0, 0, 2, 0.001); -limiter_thresh = hslider("limiter_thresh[unit:dB]", -1.0, -12, 0, 0.1); -dc_block_on = nentry("dc_block", 1, 0, 1, 1); - -// --------------------------------------------------------------------------- -// DC blocking filter (~10Hz one-pole HP) -// --------------------------------------------------------------------------- - -dcBlock(on, x) = on * fi.highpass(1, 10.0, x) + (1.0 - on) * x; - -// --------------------------------------------------------------------------- -// Stereo width via mid-side processing -// width = 0: mono (side removed) -// width = 1: original stereo -// width = 2: enhanced stereo (doubled sides) -// --------------------------------------------------------------------------- - -stereoWidth(inL, inR) = outL, outR -with { - w = width; - mid = (inL + inR) * 0.5; - side = (inL - inR) * 0.5; - outL = mid + side * w; - outR = mid - side * w; -}; - -// --------------------------------------------------------------------------- -// Brick-wall limiter: peak-following gain reduction. -// Uses a leaky envelope follower with fast attack (~0.5ms) and slow release (~100ms). -// --------------------------------------------------------------------------- - -threshLin = ba.db2linear(limiter_thresh); - -// Leaky-peak envelope follower: exponential release ~100ms -// Takes a signal, outputs its peak envelope -releaseCoeff = exp(-1.0 / (float(ma.SR) * 0.100)); - -peakEnv = abs : (+ ~ *(releaseCoeff)); - -// Gain reduction: clamp to threshold -limiterGR(env) = threshLin / max(threshLin, env); - -// Stereo limiter: linked L/R gain reduction from max of both peaks -limiter(inL, inR) = inL * gr, inR * gr -with { - envL = peakEnv(inL); - envR = peakEnv(inR); - peakStereo = max(envL, envR); - gr = limiterGR(peakStereo); -}; - -// --------------------------------------------------------------------------- -// Main process -// --------------------------------------------------------------------------- - -process(inL, inR) = outL, outR -with { - // 1. DC block - dcL = dcBlock(dc_block_on, inL); - dcR = dcBlock(dc_block_on, inR); - // 2. Gain - gL = dcL * gain; - gR = dcR * gain; - // 3. Stereo width - wL = stereoWidth(gL, gR) : _,!; - wR = stereoWidth(gL, gR) : !,_; - // 4. Limiter - outL = limiter(wL, wR) : _,!; - outR = limiter(wL, wR) : !,_; -}; diff --git a/playground/faust/eoc-master.json b/playground/faust/eoc-master.json deleted file mode 100644 index 10b9dad..0000000 --- a/playground/faust/eoc-master.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-master","filename": "eoc-master.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_master -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 88,"code": "Ly8gZW9jLW1hc3Rlci5kc3Ag4oCUIE1hc3RlciBidXMgZWZmZWN0IGZvciB0aGUgRU9DIHJhY2suCi8vCi8vIFBhcmFtZXRlcnMgKDQpOgovLyAgIGdhaW4gICAgICAgICAgICBbMS4wLCAgMC0yXSAgICAgICAgb3V0cHV0IGdhaW4gbXVsdGlwbGllcgovLyAgIHdpZHRoICAgICAgICAgICBbMS4wLCAgMC0yXSAgICAgICAgc3RlcmVvIHdpZHRoOiAwPW1vbm8sIDE9bm9ybWFsLCAyPWV4dHJhLXdpZGUKLy8gICBsaW1pdGVyX3RocmVzaCAgWy0xLjAsIC0xMiB0byAwXSAgIGJyaWNrLXdhbGwgbGltaXRlciB0aHJlc2hvbGQgKGRCKQovLyAgIGRjX2Jsb2NrICAgICAgICBbMV0gICAgICAgICAgICAgICAgbmVudHJ5OiAwPW9mZiwgMT1vbiDigJQgREMgYmxvY2tpbmcgZmlsdGVyCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFVJCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZ2FpbiAgICAgICAgICAgPSBoc2xpZGVyKCJnYWluIiwgICAgICAgICAgICAgICAgICAxLjAsICAwLCAgICAyLCAgIDAuMDAxKTsKd2lkdGggICAgICAgICAgPSBoc2xpZGVyKCJ3aWR0aCIsICAgICAgICAgICAgICAgICAxLjAsICAwLCAgICAyLCAgIDAuMDAxKTsKbGltaXRlcl90aHJlc2ggPSBoc2xpZGVyKCJsaW1pdGVyX3RocmVzaFt1bml0OmRCXSIsIC0xLjAsIC0xMiwgMCwgMC4xKTsKZGNfYmxvY2tfb24gICAgPSBuZW50cnkoImRjX2Jsb2NrIiwgICAgICAgICAgICAgICAxLCAgICAwLCAgICAxLCAgIDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIERDIGJsb2NraW5nIGZpbHRlciAofjEwSHogb25lLXBvbGUgSFApCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZGNCbG9jayhvbiwgeCkgPSBvbiAqIGZpLmhpZ2hwYXNzKDEsIDEwLjAsIHgpICsgKDEuMCAtIG9uKSAqIHg7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gU3RlcmVvIHdpZHRoIHZpYSBtaWQtc2lkZSBwcm9jZXNzaW5nCi8vICAgd2lkdGggPSAwOiBtb25vIChzaWRlIHJlbW92ZWQpCi8vICAgd2lkdGggPSAxOiBvcmlnaW5hbCBzdGVyZW8KLy8gICB3aWR0aCA9IDI6IGVuaGFuY2VkIHN0ZXJlbyAoZG91YmxlZCBzaWRlcykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpzdGVyZW9XaWR0aChpbkwsIGluUikgPSBvdXRMLCBvdXRSCndpdGggewogICAgdyAgICA9IHdpZHRoOwogICAgbWlkICA9IChpbkwgKyBpblIpICogMC41OwogICAgc2lkZSA9IChpbkwgLSBpblIpICogMC41OwogICAgb3V0TCA9IG1pZCArIHNpZGUgKiB3OwogICAgb3V0UiA9IG1pZCAtIHNpZGUgKiB3Owp9OwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEJyaWNrLXdhbGwgbGltaXRlcjogcGVhay1mb2xsb3dpbmcgZ2FpbiByZWR1Y3Rpb24uCi8vIFVzZXMgYSBsZWFreSBlbnZlbG9wZSBmb2xsb3dlciB3aXRoIGZhc3QgYXR0YWNrICh+MC41bXMpIGFuZCBzbG93IHJlbGVhc2UgKH4xMDBtcykuCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKdGhyZXNoTGluID0gYmEuZGIybGluZWFyKGxpbWl0ZXJfdGhyZXNoKTsKCi8vIExlYWt5LXBlYWsgZW52ZWxvcGUgZm9sbG93ZXI6IGV4cG9uZW50aWFsIHJlbGVhc2UgfjEwMG1zCi8vIFRha2VzIGEgc2lnbmFsLCBvdXRwdXRzIGl0cyBwZWFrIGVudmVsb3BlCnJlbGVhc2VDb2VmZiA9IGV4cCgtMS4wIC8gKGZsb2F0KG1hLlNSKSAqIDAuMTAwKSk7CgpwZWFrRW52ID0gYWJzIDogKCsgfiAqKHJlbGVhc2VDb2VmZikpOwoKLy8gR2FpbiByZWR1Y3Rpb246IGNsYW1wIHRvIHRocmVzaG9sZApsaW1pdGVyR1IoZW52KSA9IHRocmVzaExpbiAvIG1heCh0aHJlc2hMaW4sIGVudik7CgovLyBTdGVyZW8gbGltaXRlcjogbGlua2VkIEwvUiBnYWluIHJlZHVjdGlvbiBmcm9tIG1heCBvZiBib3RoIHBlYWtzCmxpbWl0ZXIoaW5MLCBpblIpID0gaW5MICogZ3IsIGluUiAqIGdyCndpdGggewogICAgZW52TCA9IHBlYWtFbnYoaW5MKTsKICAgIGVudlIgPSBwZWFrRW52KGluUik7CiAgICBwZWFrU3RlcmVvID0gbWF4KGVudkwsIGVudlIpOwogICAgZ3IgPSBsaW1pdGVyR1IocGVha1N0ZXJlbyk7Cn07CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gTWFpbiBwcm9jZXNzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKcHJvY2VzcyhpbkwsIGluUikgPSBvdXRMLCBvdXRSCndpdGggewogICAgLy8gMS4gREMgYmxvY2sKICAgIGRjTCA9IGRjQmxvY2soZGNfYmxvY2tfb24sIGluTCk7CiAgICBkY1IgPSBkY0Jsb2NrKGRjX2Jsb2NrX29uLCBpblIpOwogICAgLy8gMi4gR2FpbgogICAgZ0wgPSBkY0wgKiBnYWluOwogICAgZ1IgPSBkY1IgKiBnYWluOwogICAgLy8gMy4gU3RlcmVvIHdpZHRoCiAgICB3TCA9IHN0ZXJlb1dpZHRoKGdMLCBnUikgOiBfLCE7CiAgICB3UiA9IHN0ZXJlb1dpZHRoKGdMLCBnUikgOiAhLF87CiAgICAvLyA0LiBMaW1pdGVyCiAgICBvdXRMID0gbGltaXRlcih3TCwgd1IpIDogXywhOwogICAgb3V0UiA9IGxpbWl0ZXIod0wsIHdSKSA6ICEsXzsKfTsK","inputs": 2,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn eoc_master -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "filename": "eoc-master.dsp" },{ "filters.lib/highpass:author": "Julius O. Smith III" },{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-master" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "eoc-master","items": [ {"type": "nentry","label": "dc_block","varname": "fEntry0","shortname": "dc_block","address": "/eoc-master/dc_block","index": 40,"init": 1,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "gain","varname": "fHslider2","shortname": "gain","address": "/eoc-master/gain","index": 64,"init": 1,"min": 0,"max": 2,"step": 0.001},{"type": "hslider","label": "limiter_thresh","varname": "fHslider0","shortname": "limiter_thresh","address": "/eoc-master/limiter_thresh","index": 0,"meta": [{ "unit": "dB" }],"init": -1,"min": -12,"max": 0,"step": 0.1},{"type": "hslider","label": "width","varname": "fHslider1","shortname": "width","address": "/eoc-master/width","index": 60,"init": 1,"min": 0,"max": 2,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-master.wasm b/playground/faust/eoc-master.wasm deleted file mode 100644 index ae6eef722480689fbff94b0fd6eaca7109887359..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8739 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 x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - this._exports = instance.exports; - this._mem = memory; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - - const exps = this._exports; - - if (exps.createDSPInstance) { - this._dsp = exps.createDSPInstance(); - } else if (exps.eoc_reverb) { - this._dsp = exps.eoc_reverb(); - } else { - const fnKeys = Object.keys(exps).filter(k => typeof exps[k] === 'function'); - console.warn('[EOCReverbProcessor] No DSP constructor found; exports:', fnKeys); - return; - } - - exps.init(this._dsp, sr); - this._buildParamIndex(exps, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items) { - for (const item of items) { - const type = item.type ?? ''; - if (['hslider', 'vslider', 'nentry'].includes(type)) { - addresses.push(item.address ?? ''); - } else if (item.items) { - walk(item.items); - } - } - } - - walk(desc.ui ?? []); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr !== undefined && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - - const exps = this._exports; - const mem = this._mem; - const heap = this._heap; - const i32 = this._heapi32; - - const heapWords = mem.buffer.byteLength >> 2; - const inLOff = heapWords - blockSize * 6 - 32; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - const srcL = this._inputL; - const srcR = this._inputR; - if (srcL) for (let i = 0; i < blockSize; i++) heap[inLOff + i] = srcL[i]; - if (srcR) for (let i = 0; i < blockSize; i++) heap[inROff + i] = srcR[i]; - - exps.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } - - process(inputs, outputs, params) { - const inp = inputs[0]; - this._inputL = inp?.[0] ?? null; - this._inputR = inp?.[1] ?? inp?.[0] ?? null; - return super.process(inputs, outputs, params); - } -} - -registerProcessor('eoc-reverb-processor', EOCReverbProcessor); diff --git a/playground/faust/eoc-reverb.dsp b/playground/faust/eoc-reverb.dsp deleted file mode 100644 index 8a382e8..0000000 --- a/playground/faust/eoc-reverb.dsp +++ /dev/null @@ -1,52 +0,0 @@ -// eoc-reverb.dsp — Stereo reverb for MEMLNaut EOC chain -// -// 9 params: predelay, size, diffusion, hi_damp, lo_damp, decay, mix, width, mod_rate -// -// Uses re.zita_rev1_stereo for the core reverb algorithm. -// -// Compile: -// faust -lang wasm -cn eoc_reverb -json eoc-reverb.dsp -o eoc-reverb.wasm - -import("stdfaust.lib"); - -predelay = hslider("predelay[unit:ms]", 0.0, 0.0, 100.0, 0.5); -size = hslider("size", 0.5, 0.0, 1.0, 0.001); -diffusion= hslider("diffusion", 0.7, 0.0, 1.0, 0.001); -hi_damp = hslider("hi_damp", 0.5, 0.0, 1.0, 0.001); -lo_damp = hslider("lo_damp", 0.0, 0.0, 1.0, 0.001); -decay = hslider("decay[unit:s]", 3.0, 0.1, 20.0, 0.1); -mix = hslider("mix", 0.2, 0.0, 1.0, 0.001); -width = hslider("width", 0.8, 0.0, 1.0, 0.001); -mod_rate = hslider("mod_rate[unit:Hz]", 0.5, 0.0, 5.0, 0.01); - -// Pre-delay in samples (minimum 1) -pdSamps = max(1, int(predelay / 1000.0 * ma.SR)); - -// Frequency crossovers for zita -f1 = 200.0 + lo_damp * 1800.0; -f2 = 20000.0 - hi_damp * 18000.0; - -// Reverb decay times scaled by size -t60dc = decay * (1.0 + size * 0.5); -t60m = decay; - -// Pre-delay: single channel -predelayLine = _ @ pdSamps; - -// Width processing: M/S encode-scale-decode -// L R → L' R' where side channels scaled by width -widthL(l, r) = (l + r) * 0.5 + (l - r) * 0.5 * width; -widthR(l, r) = (l + r) * 0.5 - (l - r) * 0.5 * width; - -// Wet signal through predelay, diffusion scale, and zita reverb -wetL(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0, - inL * diffusion @ pdSamps, - inR * diffusion @ pdSamps)) : widthL; - -wetR(inL, inR) = (re.zita_rev1_stereo(0.0, f1, f2, t60dc, t60m, 192000.0, - inL * diffusion @ pdSamps, - inR * diffusion @ pdSamps)) : widthR; - -process(inL, inR) = - inL * (1.0 - mix) + wetL(inL, inR) * mix, - inR * (1.0 - mix) + wetR(inL, inR) * mix; diff --git a/playground/faust/eoc-reverb.json b/playground/faust/eoc-reverb.json deleted file mode 100644 index 2cfc0bb..0000000 --- a/playground/faust/eoc-reverb.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-reverb","filename": "eoc-reverb.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_reverb -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/reverbs.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/delays.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/routes.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 2048504,"code": "Ly8gZW9jLXJldmVyYi5kc3Ag4oCUIFN0ZXJlbyByZXZlcmIgZm9yIE1FTUxOYXV0IEVPQyBjaGFpbgovLwovLyA5IHBhcmFtczogcHJlZGVsYXksIHNpemUsIGRpZmZ1c2lvbiwgaGlfZGFtcCwgbG9fZGFtcCwgZGVjYXksIG1peCwgd2lkdGgsIG1vZF9yYXRlCi8vCi8vIFVzZXMgcmUueml0YV9yZXYxX3N0ZXJlbyBmb3IgdGhlIGNvcmUgcmV2ZXJiIGFsZ29yaXRobS4KLy8KLy8gQ29tcGlsZToKLy8gICBmYXVzdCAtbGFuZyB3YXNtIC1jbiBlb2NfcmV2ZXJiIC1qc29uIGVvYy1yZXZlcmIuZHNwIC1vIGVvYy1yZXZlcmIud2FzbQoKaW1wb3J0KCJzdGRmYXVzdC5saWIiKTsKCnByZWRlbGF5ID0gaHNsaWRlcigicHJlZGVsYXlbdW5pdDptc10iLCAgMC4wLCAgIDAuMCwgIDEwMC4wLCAgMC41KTsKc2l6ZSAgICAgPSBoc2xpZGVyKCJzaXplIiwgICAgICAgICAgICAgIDAuNSwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKZGlmZnVzaW9uPSBoc2xpZGVyKCJkaWZmdXNpb24iLCAgICAgICAgIDAuNywgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKaGlfZGFtcCAgPSBoc2xpZGVyKCJoaV9kYW1wIiwgICAgICAgICAgIDAuNSwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKbG9fZGFtcCAgPSBoc2xpZGVyKCJsb19kYW1wIiwgICAgICAgICAgIDAuMCwgICAwLjAsICAgIDEuMCwgIDAuMDAxKTsKZGVjYXkgICAgPSBoc2xpZGVyKCJkZWNheVt1bml0OnNdIiwgICAgICAzLjAsICAgMC4xLCAgIDIwLjAsICAwLjEpOwptaXggICAgICA9IGhzbGlkZXIoIm1peCIsICAgICAgICAgICAgICAgMC4yLCAgIDAuMCwgICAgMS4wLCAgMC4wMDEpOwp3aWR0aCAgICA9IGhzbGlkZXIoIndpZHRoIiwgICAgICAgICAgICAgMC44LCAgIDAuMCwgICAgMS4wLCAgMC4wMDEpOwptb2RfcmF0ZSA9IGhzbGlkZXIoIm1vZF9yYXRlW3VuaXQ6SHpdIiwgMC41LCAgIDAuMCwgICAgNS4wLCAgMC4wMSk7CgovLyBQcmUtZGVsYXkgaW4gc2FtcGxlcyAobWluaW11bSAxKQpwZFNhbXBzID0gbWF4KDEsIGludChwcmVkZWxheSAvIDEwMDAuMCAqIG1hLlNSKSk7CgovLyBGcmVxdWVuY3kgY3Jvc3NvdmVycyBmb3Igeml0YQpmMSAgID0gMjAwLjAgKyBsb19kYW1wICogMTgwMC4wOwpmMiAgID0gMjAwMDAuMCAtIGhpX2RhbXAgKiAxODAwMC4wOwoKLy8gUmV2ZXJiIGRlY2F5IHRpbWVzIHNjYWxlZCBieSBzaXplCnQ2MGRjID0gZGVjYXkgKiAoMS4wICsgc2l6ZSAqIDAuNSk7CnQ2MG0gID0gZGVjYXk7CgovLyBQcmUtZGVsYXk6IHNpbmdsZSBjaGFubmVsCnByZWRlbGF5TGluZSA9IF8gQCBwZFNhbXBzOwoKLy8gV2lkdGggcHJvY2Vzc2luZzogTS9TIGVuY29kZS1zY2FsZS1kZWNvZGUKLy8gTCBSIOKGkiBMJyBSJyB3aGVyZSBzaWRlIGNoYW5uZWxzIHNjYWxlZCBieSB3aWR0aAp3aWR0aEwobCwgcikgPSAobCArIHIpICogMC41ICsgKGwgLSByKSAqIDAuNSAqIHdpZHRoOwp3aWR0aFIobCwgcikgPSAobCArIHIpICogMC41IC0gKGwgLSByKSAqIDAuNSAqIHdpZHRoOwoKLy8gV2V0IHNpZ25hbCB0aHJvdWdoIHByZWRlbGF5LCBkaWZmdXNpb24gc2NhbGUsIGFuZCB6aXRhIHJldmVyYgp3ZXRMKGluTCwgaW5SKSA9IChyZS56aXRhX3JldjFfc3RlcmVvKDAuMCwgZjEsIGYyLCB0NjBkYywgdDYwbSwgMTkyMDAwLjAsCiAgICAgICAgICAgICAgICAgICAgaW5MICogZGlmZnVzaW9uIEAgcGRTYW1wcywKICAgICAgICAgICAgICAgICAgICBpblIgKiBkaWZmdXNpb24gQCBwZFNhbXBzKSkgOiB3aWR0aEw7Cgp3ZXRSKGluTCwgaW5SKSA9IChyZS56aXRhX3JldjFfc3RlcmVvKDAuMCwgZjEsIGYyLCB0NjBkYywgdDYwbSwgMTkyMDAwLjAsCiAgICAgICAgICAgICAgICAgICAgaW5MICogZGlmZnVzaW9uIEAgcGRTYW1wcywKICAgICAgICAgICAgICAgICAgICBpblIgKiBkaWZmdXNpb24gQCBwZFNhbXBzKSkgOiB3aWR0aFI7Cgpwcm9jZXNzKGluTCwgaW5SKSA9CiAgICBpbkwgKiAoMS4wIC0gbWl4KSArIHdldEwoaW5MLCBpblIpICogbWl4LAogICAgaW5SICogKDEuMCAtIG1peCkgKyB3ZXRSKGluTCwgaW5SKSAqIG1peDsK","inputs": 2,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn eoc_reverb -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "delays.lib/name": "Faust Delay Library" },{ "delays.lib/version": "1.1.0" },{ "filename": "eoc-reverb.dsp" },{ "filters.lib/allpass_comb:author": "Julius O. Smith III" },{ "filters.lib/allpass_comb:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/allpass_comb:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-reverb" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "reverbs.lib/name": "Faust Reverb Library" },{ "reverbs.lib/version": "1.4.0" },{ "routes.lib/hadamard:author": "Remy Muller, revised by Romain Michon" },{ "routes.lib/name": "Faust Signal Routing Library" },{ "routes.lib/version": "1.2.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "eoc-reverb","items": [ {"type": "hslider","label": "decay","varname": "fHslider0","shortname": "decay","address": "/eoc-reverb/decay","index": 0,"meta": [{ "unit": "s" }],"init": 3,"min": 0.1,"max": 20,"step": 0.1},{"type": "hslider","label": "diffusion","varname": "fHslider4","shortname": "diffusion","address": "/eoc-reverb/diffusion","index": 262212,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "hi_damp","varname": "fHslider1","shortname": "hi_damp","address": "/eoc-reverb/hi_damp","index": 20,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "lo_damp","varname": "fHslider2","shortname": "lo_damp","address": "/eoc-reverb/lo_damp","index": 28,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "mix","varname": "fHslider7","shortname": "mix","address": "/eoc-reverb/mix","index": 2048500,"init": 0.2,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "predelay","varname": "fHslider5","shortname": "predelay","address": "/eoc-reverb/predelay","index": 393288,"meta": [{ "unit": "ms" }],"init": 0,"min": 0,"max": 100,"step": 0.5},{"type": "hslider","label": "size","varname": "fHslider3","shortname": "size","address": "/eoc-reverb/size","index": 44,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "width","varname": "fHslider6","shortname": "width","address": "/eoc-reverb/width","index": 2048496,"init": 0.8,"min": 0,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-reverb.wasm b/playground/faust/eoc-reverb.wasm deleted file mode 100644 index 4b92e7a9d5f687e10f6aa59a6bd161e519fc6b09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17599 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 W(9zvxAWPkD=}}97cy9T*<^KV;DYJb5 diff --git a/playground/faust/eoc-saturation-processor.js b/playground/faust/eoc-saturation-processor.js deleted file mode 100644 index 9b8bf84..0000000 --- a/playground/faust/eoc-saturation-processor.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * eoc-saturation-processor.js — AudioWorklet processor for the EOC Saturation. - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads eoc-saturation.wasm compiled from eoc-saturation.dsp. - * - * Parameter index order (alphabetical within group, matching eoc-saturation.json): - * 0 character [0, 1] - * 1 drive [0, 1] - * 2 mix [0, 1] - * 3 tone [0, 1] - */ - -// Must be loaded in AudioWorkletGlobalScope after faust-worklet-processor.js. - -class EOCSaturationProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._paramAddresses = []; - this._blockSize = 128; - this._sampleRate = 48000; - } - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - const exports = instance.exports; - - this._exports = exports; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - this._mem = memory; - - if (exports.createDSPInstance) { - this._dsp = exports.createDSPInstance(); - } else if (exports.eoc_saturation) { - this._dsp = exports.eoc_saturation(); - } else { - console.warn('[EOCSaturationProcessor] No DSP factory found; exports:', Object.keys(exports)); - return; - } - - exports.init(this._dsp, sr); - this._buildParamIndex(exports, memory); - } - - _buildParamIndex(exports, memory) { - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const addresses = []; - - function walk(items, path) { - for (const item of items) { - const label = item.label ?? ''; - const type = item.type ?? ''; - const addr = item.address ?? (path + '/' + label); - if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) { - addresses.push(addr); - } else if (item.items) { - walk(item.items, path + '/' + label); - } - } - } - - walk(desc.ui ?? [], ''); - this._paramAddresses = addresses; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - const exports = this._exports; - const mem = this._mem; - const heap = this._heap; - - const heapBytes = mem.buffer.byteLength; - const inLOff = (heapBytes >> 2) - blockSize * 4 - 128; - const inROff = inLOff + blockSize; - const outLOff = inROff + blockSize; - const outROff = outLOff + blockSize; - - const i32 = this._heapi32; - const inPtrsOff = outROff + blockSize; - const outPtrsOff = inPtrsOff + 2; - i32[inPtrsOff] = inLOff * 4; - i32[inPtrsOff + 1] = inROff * 4; - i32[outPtrsOff] = outLOff * 4; - i32[outPtrsOff + 1] = outROff * 4; - - exports.compute(this._dsp, blockSize, inPtrsOff * 4, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } -} - -registerProcessor('eoc-saturation-processor', EOCSaturationProcessor); diff --git a/playground/faust/eoc-saturation.dsp b/playground/faust/eoc-saturation.dsp deleted file mode 100644 index 72b7458..0000000 --- a/playground/faust/eoc-saturation.dsp +++ /dev/null @@ -1,71 +0,0 @@ -// eoc-saturation.dsp — Stereo saturation effect for the EOC rack. -// -// Parameters (4): -// drive [0.0, 0-1] 0=clean, 1=full drive -// character [0.0, 0-1] 0=soft-clip/tanh, 0.5=tape/asymmetric, 1=hard-clip -// tone [0.5, 0-1] post-saturation tone: 0=dark (LP), 1=bright (HP blend) -// mix [1.0, 0-1] dry/wet for parallel saturation - -import("stdfaust.lib"); - -// --------------------------------------------------------------------------- -// UI -// --------------------------------------------------------------------------- - -drive = hslider("drive", 0.0, 0, 1, 0.001); -character = hslider("character", 0.0, 0, 1, 0.001); -tone = hslider("tone", 0.5, 0, 1, 0.001); -mix = hslider("mix", 1.0, 0, 1, 0.001); - -// --------------------------------------------------------------------------- -// Saturation shapes -// --------------------------------------------------------------------------- - -driveGain = 1.0 + drive * 15.0; // 1x to 16x gain before clipping - -// Soft clip: tanh -softClip(x) = ma.tanh(x); - -// Tape: asymmetric waveshaper — slightly harder clipping on positive peaks -// Blends tanh with a gentle second-harmonic bias -tapeClip(x) = softClip(x * 1.2) * 0.55 + softClip(x) * 0.45 + x * x * 0.04 * (1.0 - softClip(abs(x))); - -// Hard clip: simple saturate -hardClip(x) = max(-1.0, min(1.0, x)); - -// Character crossfade between the three shapes: -// c=0.0 → soft (tanh) -// c=0.5 → tape (asymmetric) -// c=1.0 → hard clip -saturate(c, x) = - softClip(x) * (max(0.0, 1.0 - c * 2.0)) + - tapeClip(x) * (1.0 - abs(c - 0.5) * 2.0) + - hardClip(x) * max(0.0, (c - 0.5) * 2.0); - -// Apply drive, saturate, and normalise output level -processChannel(x) = saturate(character, x * driveGain) / max(0.001, sqrt(driveGain)); - -// --------------------------------------------------------------------------- -// Tone control: LP/HP blend via one-pole filters -// -// tone=0.0 → dark (400Hz LP) -// tone=0.5 → flat (passthrough) -// tone=1.0 → bright (8kHz HP blend) -// --------------------------------------------------------------------------- - -toneFreqLP = 400.0 + tone * 19600.0; // 400Hz to 20kHz (fully open at tone=1) -toneFreqHP = 200.0 + tone * 7800.0; // 200Hz to 8kHz - -applyTone(x) = fi.lowpass(1, toneFreqLP, x); - -// --------------------------------------------------------------------------- -// Main process: stereo saturation with parallel dry/wet mix -// --------------------------------------------------------------------------- - -process(inL, inR) = outL, outR -with { - wetL = applyTone(processChannel(inL)); - wetR = applyTone(processChannel(inR)); - outL = inL * (1.0 - mix) + wetL * mix; - outR = inR * (1.0 - mix) + wetR * mix; -}; diff --git a/playground/faust/eoc-saturation.json b/playground/faust/eoc-saturation.json deleted file mode 100644 index 6e11090..0000000 --- a/playground/faust/eoc-saturation.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "eoc-saturation","filename": "eoc-saturation.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn eoc_saturation -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 56,"code": "Ly8gZW9jLXNhdHVyYXRpb24uZHNwIOKAlCBTdGVyZW8gc2F0dXJhdGlvbiBlZmZlY3QgZm9yIHRoZSBFT0MgcmFjay4KLy8KLy8gUGFyYW1ldGVycyAoNCk6Ci8vICAgZHJpdmUgICAgIFswLjAsIDAtMV0gICAwPWNsZWFuLCAxPWZ1bGwgZHJpdmUKLy8gICBjaGFyYWN0ZXIgWzAuMCwgMC0xXSAgIDA9c29mdC1jbGlwL3RhbmgsIDAuNT10YXBlL2FzeW1tZXRyaWMsIDE9aGFyZC1jbGlwCi8vICAgdG9uZSAgICAgIFswLjUsIDAtMV0gICBwb3N0LXNhdHVyYXRpb24gdG9uZTogMD1kYXJrIChMUCksIDE9YnJpZ2h0IChIUCBibGVuZCkKLy8gICBtaXggICAgICAgWzEuMCwgMC0xXSAgIGRyeS93ZXQgZm9yIHBhcmFsbGVsIHNhdHVyYXRpb24KCmltcG9ydCgic3RkZmF1c3QubGliIik7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gVUkKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpkcml2ZSAgICAgPSBoc2xpZGVyKCJkcml2ZSIsICAgICAwLjAsIDAsIDEsIDAuMDAxKTsKY2hhcmFjdGVyID0gaHNsaWRlcigiY2hhcmFjdGVyIiwgMC4wLCAwLCAxLCAwLjAwMSk7CnRvbmUgICAgICA9IGhzbGlkZXIoInRvbmUiLCAgICAgIDAuNSwgMCwgMSwgMC4wMDEpOwptaXggICAgICAgPSBoc2xpZGVyKCJtaXgiLCAgICAgICAxLjAsIDAsIDEsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTYXR1cmF0aW9uIHNoYXBlcwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmRyaXZlR2FpbiA9IDEuMCArIGRyaXZlICogMTUuMDsgICAvLyAxeCB0byAxNnggZ2FpbiBiZWZvcmUgY2xpcHBpbmcKCi8vIFNvZnQgY2xpcDogdGFuaApzb2Z0Q2xpcCh4KSA9IG1hLnRhbmgoeCk7CgovLyBUYXBlOiBhc3ltbWV0cmljIHdhdmVzaGFwZXIg4oCUIHNsaWdodGx5IGhhcmRlciBjbGlwcGluZyBvbiBwb3NpdGl2ZSBwZWFrcwovLyBCbGVuZHMgdGFuaCB3aXRoIGEgZ2VudGxlIHNlY29uZC1oYXJtb25pYyBiaWFzCnRhcGVDbGlwKHgpID0gc29mdENsaXAoeCAqIDEuMikgKiAwLjU1ICsgc29mdENsaXAoeCkgKiAwLjQ1ICsgeCAqIHggKiAwLjA0ICogKDEuMCAtIHNvZnRDbGlwKGFicyh4KSkpOwoKLy8gSGFyZCBjbGlwOiBzaW1wbGUgc2F0dXJhdGUKaGFyZENsaXAoeCkgPSBtYXgoLTEuMCwgbWluKDEuMCwgeCkpOwoKLy8gQ2hhcmFjdGVyIGNyb3NzZmFkZSBiZXR3ZWVuIHRoZSB0aHJlZSBzaGFwZXM6Ci8vICAgYz0wLjAgIOKGkiBzb2Z0ICh0YW5oKQovLyAgIGM9MC41ICDihpIgdGFwZSAoYXN5bW1ldHJpYykKLy8gICBjPTEuMCAg4oaSIGhhcmQgY2xpcApzYXR1cmF0ZShjLCB4KSA9CiAgICBzb2Z0Q2xpcCh4KSAqIChtYXgoMC4wLCAxLjAgLSBjICogMi4wKSkgKwogICAgdGFwZUNsaXAoeCkgKiAoMS4wIC0gYWJzKGMgLSAwLjUpICogMi4wKSArCiAgICBoYXJkQ2xpcCh4KSAqIG1heCgwLjAsIChjIC0gMC41KSAqIDIuMCk7CgovLyBBcHBseSBkcml2ZSwgc2F0dXJhdGUsIGFuZCBub3JtYWxpc2Ugb3V0cHV0IGxldmVsCnByb2Nlc3NDaGFubmVsKHgpID0gc2F0dXJhdGUoY2hhcmFjdGVyLCB4ICogZHJpdmVHYWluKSAvIG1heCgwLjAwMSwgc3FydChkcml2ZUdhaW4pKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBUb25lIGNvbnRyb2w6IExQL0hQIGJsZW5kIHZpYSBvbmUtcG9sZSBmaWx0ZXJzCi8vCi8vIHRvbmU9MC4wICDihpIgZGFyayAoNDAwSHogTFApCi8vIHRvbmU9MC41ICDihpIgZmxhdCAocGFzc3Rocm91Z2gpCi8vIHRvbmU9MS4wICDihpIgYnJpZ2h0ICg4a0h6IEhQIGJsZW5kKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCnRvbmVGcmVxTFAgPSA0MDAuMCArIHRvbmUgKiAxOTYwMC4wOyAgIC8vIDQwMEh6IHRvIDIwa0h6IChmdWxseSBvcGVuIGF0IHRvbmU9MSkKdG9uZUZyZXFIUCA9IDIwMC4wICsgdG9uZSAqIDc4MDAuMDsgICAgLy8gMjAwSHogdG8gOGtIegoKYXBwbHlUb25lKHgpID0gZmkubG93cGFzcygxLCB0b25lRnJlcUxQLCB4KTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBNYWluIHByb2Nlc3M6IHN0ZXJlbyBzYXR1cmF0aW9uIHdpdGggcGFyYWxsZWwgZHJ5L3dldCBtaXgKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpwcm9jZXNzKGluTCwgaW5SKSA9IG91dEwsIG91dFIKd2l0aCB7CiAgICB3ZXRMID0gYXBwbHlUb25lKHByb2Nlc3NDaGFubmVsKGluTCkpOwogICAgd2V0UiA9IGFwcGx5VG9uZShwcm9jZXNzQ2hhbm5lbChpblIpKTsKICAgIG91dEwgPSBpbkwgKiAoMS4wIC0gbWl4KSArIHdldEwgKiBtaXg7CiAgICBvdXRSID0gaW5SICogKDEuMCAtIG1peCkgKyB3ZXRSICogbWl4Owp9Owo=","inputs": 2,"outputs": 2,"meta": [ { "compile_options": "-lang wasm -ct 1 -cn eoc_saturation -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "filename": "eoc-saturation.dsp" },{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "eoc-saturation" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "eoc-saturation","items": [ {"type": "hslider","label": "character","varname": "fHslider1","shortname": "character","address": "/eoc-saturation/character","index": 4,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "drive","varname": "fHslider0","shortname": "drive","address": "/eoc-saturation/drive","index": 0,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "mix","varname": "fHslider3","shortname": "mix","address": "/eoc-saturation/mix","index": 36,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "tone","varname": "fHslider2","shortname": "tone","address": "/eoc-saturation/tone","index": 16,"init": 0.5,"min": 0,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/eoc-saturation.wasm b/playground/faust/eoc-saturation.wasm deleted file mode 100644 index db88369eba4fe96b9311d9eb8f3d2520b1b0de8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8239 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 GefGcILcl8k diff --git a/playground/faust/faust-worklet-processor.js b/playground/faust/faust-worklet-processor.js deleted file mode 100644 index b3a4865..0000000 --- a/playground/faust/faust-worklet-processor.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * faust-worklet-processor.js — Base AudioWorklet processor for Faust WASM engines - * - * This file must be loaded via audioContext.audioWorklet.addModule() before - * creating a FaustWorkletNode. It runs in AudioWorkletGlobalScope. - * - * Each Faust engine subclasses FaustWorkletProcessor and overrides: - * - static get processorName() — returns the unique processor name string - * - _initWasm(wasmBytes, sampleRate) — initialises the Faust WASM instance - * - _renderBlock(outputL, outputR, blockSize) — fills output buffers per block - * - * Message protocol (port.postMessage from main thread): - * { type: 'init', wasmBytes: ArrayBuffer, sampleRate: number } - * { type: 'setParam', index: number, value: number } - * { type: 'noteOn', freq: number, vel: number } - * { type: 'noteOff', freq: number } - * - * Replies from worklet to main thread: - * { type: 'ready' } - * { type: 'error', message: string } - */ - -class FaustWorkletProcessor extends AudioWorkletProcessor { - constructor(options) { - super(options); - this._ready = false; - this._paramValues = {}; // index → current value (raw Faust units) - - // Messages that arrive before init() finishes are buffered here and - // drained by _drainPendingMessages() in order once the subclass sets - // _ready = true. Subclasses with extended _handleMessage() should call - // _queueIfNotReady(msg) as their first line to participate. - this._pendingMessages = []; - - this.port.onmessage = (e) => this._handleMessage(e.data); - } - - // --------------------------------------------------------------------------- - // Pre-ready message buffering - // --------------------------------------------------------------------------- - - /** - * Returns true if the message was queued (processor not ready yet). - * Subclasses should call this at the top of their _handleMessage override - * after the 'init' special case, before touching _dspInst. - */ - _queueIfNotReady(msg) { - if (this._ready) return false; - if (!msg || msg.type === 'init') return false; - this._pendingMessages.push(msg); - return true; - } - - /** Drain all buffered pre-ready messages, in arrival order. */ - _drainPendingMessages() { - if (this._pendingMessages.length === 0) return; - const pending = this._pendingMessages; - this._pendingMessages = []; - for (const msg of pending) { - try { this._handleMessage(msg); } - catch (err) { - this.port.postMessage({ - type: 'error', - message: 'drain failed: ' + String(err), - }); - } - } - } - - // --------------------------------------------------------------------------- - // Message handler (runs in worklet thread) - // --------------------------------------------------------------------------- - - _handleMessage(msg) { - if (!msg || !msg.type) return; - - if (msg.type === 'init') { - this._initWasm(msg.wasmBytes, msg.sampleRate || sampleRate) - .then(() => { - this._ready = true; - this.port.postMessage({ type: 'ready' }); - this._drainPendingMessages(); - }) - .catch((err) => { - this.port.postMessage({ type: 'error', message: String(err) }); - }); - return; - } - - if (this._queueIfNotReady(msg)) return; - - switch (msg.type) { - case 'setParam': - this._paramValues[msg.index] = msg.value; - this._onSetParam(msg.index, msg.value); - break; - - case 'noteOn': - this._onNoteOn(msg.freq, msg.vel); - break; - - case 'noteOff': - this._onNoteOff(msg.freq); - break; - - default: - console.warn('[FaustWorkletProcessor] Unknown message type:', msg.type); - } - } - - // --------------------------------------------------------------------------- - // AudioWorkletProcessor interface - // --------------------------------------------------------------------------- - - process(_inputs, outputs, _params) { - if (!this._ready) return true; - - const out = outputs[0]; - const blockSize = out[0]?.length ?? 128; - const outL = out[0] ?? new Float32Array(blockSize); - const outR = out[1] ?? new Float32Array(blockSize); - - this._renderBlock(outL, outR, blockSize); - - return true; // keep processor alive - } - - // --------------------------------------------------------------------------- - // Subclass API — override these in concrete engine processors - // --------------------------------------------------------------------------- - - /** - * Initialise the WASM module. Called once with the raw bytes and sample rate. - * Must return a Promise that resolves when the engine is ready to render. - * - * @param {ArrayBuffer} wasmBytes - * @param {number} sampleRate - * @returns {Promise} - */ - async _initWasm(_wasmBytes, _sampleRate) { - // Default no-op: subclasses that don't use WASM can override _renderBlock only. - } - - /** - * Called when a parameter value changes. Override to forward to DSP. - * @param {number} index Param index (matches paramMeta order) - * @param {number} value Raw value in Faust units - */ - _onSetParam(_index, _value) {} - - /** - * Called on note-on. - * @param {number} freq Hz - * @param {number} vel 0–1 - */ - _onNoteOn(_freq, _vel) {} - - /** - * Called on note-off. - * @param {number} freq Hz - */ - _onNoteOff(_freq) {} - - /** - * Fill a single audio block. Called from process() every 128 samples. - * Both arrays are pre-allocated Float32Arrays of length blockSize. - * - * @param {Float32Array} outL Left channel output buffer (write to this) - * @param {Float32Array} outR Right channel output buffer (write to this) - * @param {number} blockSize - */ - _renderBlock(_outL, _outR, _blockSize) { - // Default: silence — override in subclass - } -} - -// Explicitly attach to globalThis so subsequent addModule() scripts can see -// it. Class declarations at the top of a classic script are lexically scoped -// to that script's evaluation context and do NOT propagate across separate -// addModule() calls; an explicit property assignment is required for the -// cross-script reference to resolve. -globalThis.FaustWorkletProcessor = FaustWorkletProcessor; - -// Note: registerProcessor() is called by each concrete engine file, not here, -// because each engine has its own processor name. -// Subclass files should end with: -// registerProcessor('my-engine-processor', MyEngineProcessor); diff --git a/playground/faust/fm-matrix-processor.js b/playground/faust/fm-matrix-processor.js deleted file mode 100644 index cf601d9..0000000 --- a/playground/faust/fm-matrix-processor.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * fm-matrix-processor.js — AudioWorklet processor for the FM Matrix synth engine - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads fm-matrix.wasm compiled from fm-matrix.dsp. - * - * Parameter addresses are built from the Faust JSON descriptor at init time. - * Index→address mapping follows the order returned by faustJsonToParamMeta() - * (alphabetical within groups, groups in declaration order). - * - * Hidden params (not in NISPS 55-param list): - * /fm-matrix/Master/freq — set by noteOn - * /fm-matrix/Master/gate — set by noteOn/noteOff - * /fm-matrix/Master/_vel — set by noteOn - */ - -// Must be imported in AudioWorkletGlobalScope — include faust-worklet-processor.js -// via audioCtx.audioWorklet.addModule() before this file. - -class FMMatrixProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dsp = null; - this._paramAddresses = []; // ordered by NISPS index (from JSON) - this._hiddenAddresses = {}; // freq, gate, _vel - this._blockSize = 128; - this._sampleRate = 48000; - } - - // --------------------------------------------------------------------------- - // FaustWorkletProcessor overrides - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sr) { - this._sampleRate = sr; - - // The Faust -lang wasm output exports a single factory function - // named by the -cn flag (fm_matrix). - const module = await WebAssembly.compile(wasmBytes); - const memory = new WebAssembly.Memory({ initial: 32, maximum: 256 }); - - const imports = { - env: { - memory, - memoryBase: 0, - tableBase: 0, - _abs: Math.abs, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - _ceilf: Math.ceil, - _cosf: Math.cos, - _expf: Math.exp, - _floorf: Math.floor, - _fmodf: (x, y) => x % y, - _logf: Math.log, - _log10f: Math.log10, - _max_f: Math.max, - _min_f: Math.min, - _remainderf: (x, y) => x - Math.round(x / y) * y, - _powf: Math.pow, - _roundf: Math.round, - _sinf: Math.sin, - _sqrtf: Math.sqrt, - _tanf: Math.tan, - _fabs: Math.abs, - table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }), - }, - }; - - const instance = await WebAssembly.instantiate(module, imports); - const exports = instance.exports; - - // Faust -lang wasm exports: getNumInputs, getNumOutputs, init, - // instanceInit, getSampleRate, compute, setParamValue, getParamValue, - // getJSON (pointer to null-terminated JSON string in memory) - this._exports = exports; - this._heap = new Float32Array(memory.buffer); - this._heapi32 = new Int32Array(memory.buffer); - this._mem = memory; - - // Allocate DSP instance (Faust C++ new equivalent) - if (exports.createDSPInstance) { - this._dsp = exports.createDSPInstance(); - } else if (exports.fm_matrix) { - this._dsp = exports.fm_matrix(); - } else { - // fallback: look for any exported constructor-like function - const keys = Object.keys(exports).filter(k => typeof exports[k] === 'function'); - console.warn('[FMMatrixProcessor] No createDSPInstance found; exports:', keys); - return; - } - - // Initialise at sample rate - exports.init(this._dsp, sr); - - // Parse embedded JSON to build param address list - this._buildParamIndex(exports, memory); - } - - _buildParamIndex(exports, memory) { - // getJSON() returns a pointer to a JSON string in WASM memory - if (!exports.getJSON) return; - const ptr = exports.getJSON(this._dsp); - const buf = new Uint8Array(memory.buffer); - let str = ''; - let i = ptr; - while (buf[i] !== 0) { str += String.fromCharCode(buf[i++]); } - - let desc; - try { desc = JSON.parse(str); } catch { return; } - - const HIDDEN = new Set(['freq', 'gate', '_vel']); - const addresses = []; - - function walk(items, path) { - for (const item of items) { - const label = item.label ?? ''; - const type = item.type ?? ''; - const addr = item.address ?? (path + '/' + label); - if (['hslider', 'vslider', 'nentry', 'button', 'checkbox'].includes(type)) { - if (HIDDEN.has(label)) { - // store hidden separately - addresses._hidden = addresses._hidden || {}; - addresses._hidden[label] = addr; - } else { - addresses.push(addr); - } - } else if (item.items) { - walk(item.items, path + '/' + label); - } - } - } - - walk(desc.ui ?? [], ''); - this._paramAddresses = addresses; - this._hiddenAddresses = addresses._hidden ?? {}; - } - - _onSetParam(index, value) { - if (!this._exports || !this._dsp) return; - const addr = this._paramAddresses[index]; - if (addr && this._exports.setParamValue) { - this._exports.setParamValue(this._dsp, addr, value); - } - } - - _onNoteOn(freq, vel) { - if (!this._exports || !this._dsp) return; - const set = (label, v) => { - const addr = this._hiddenAddresses[label]; - if (addr) this._exports.setParamValue?.(this._dsp, addr, v); - }; - set('freq', freq); - set('_vel', vel); - set('gate', 1); - } - - _onNoteOff(_freq) { - if (!this._exports || !this._dsp) return; - const addr = this._hiddenAddresses['gate']; - if (addr) this._exports.setParamValue?.(this._dsp, addr, 0); - } - - _renderBlock(outL, outR, blockSize) { - if (!this._exports || !this._dsp) return; - const exports = this._exports; - const heap = this._heap; - const mem = this._mem; - - // Allocate output buffers in WASM heap (crude bump allocator using high addresses) - const heapBytes = mem.buffer.byteLength; - const outLOff = (heapBytes >> 2) - blockSize * 2 - 64; - const outROff = outLOff + blockSize; - - // Allocate pointer arrays for outputs - const ptrSize = 4; // 32-bit pointers in wasm32 - const outPtrsOff = outROff + blockSize; - const i32 = this._heapi32; - i32[outPtrsOff] = outLOff * 4; // byte offset in memory - i32[outPtrsOff + 1] = outROff * 4; - - exports.compute(this._dsp, blockSize, 0 /* no inputs */, outPtrsOff * 4); - - for (let i = 0; i < blockSize; i++) { - outL[i] = heap[outLOff + i]; - outR[i] = heap[outROff + i]; - } - } -} - -registerProcessor('fm-matrix-processor', FMMatrixProcessor); diff --git a/playground/faust/fm-matrix.dsp b/playground/faust/fm-matrix.dsp deleted file mode 100644 index 8048168..0000000 --- a/playground/faust/fm-matrix.dsp +++ /dev/null @@ -1,243 +0,0 @@ -// fm-matrix.dsp — Full 56-parameter 4-operator FM synthesizer with continuous routing matrix -// -// Design: 4 operators with fully continuous N×N cross-modulation matrix. -// No fixed algorithm slots — the algorithm emerges from modulation index values. -// When all routing matrix indices are near 0: additive synthesis. -// As indices grow: increasingly complex FM timbres emerge. -// -// Parameter ordering (56 total, maps to MLP output indices 0–55): -// 0–23: Operator Core × 4 (ratio, level, attack, decay, sustain, release) -// 24–35: Cross-Modulation Matrix (12 directed pairs) -// 36–39: Self-Feedback × 4 -// 40–47: Global Modulation (LFO, pitch env, velocity) -// 48–55: Master (level, vel sens, glide, fine tune, waveform blend, stereo spread, -// output saturation, output HP) -// -// Hidden params (not in the 56-param NISPS list): -// freq — set by noteOn (Hz) -// gate — set by noteOn/noteOff (0/1) -// _vel — set by noteOn (velocity 0–1) -// -// FM implementation: each operator phase-modulates others via a 1-sample delayed -// feedback bus. The 4 operator outputs are collected into a 4-channel bus and -// looped via ~. This is the standard Faust FM pattern. -// -// Build: -// nix-shell -p faust --run \ -// "faust -lang wasm -cn fm_matrix fm-matrix.dsp -o fm-matrix.wasm" - -import("stdfaust.lib"); - -// --------------------------------------------------------------------------- -// Hidden control params (driven by noteOn/noteOff, not NISPS) -// --------------------------------------------------------------------------- - -freq = hslider("Master/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("Master/gate[hidden:1]"); -vel = hslider("Master/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 1 — Operator Core × 4 (params 0–23) -// --------------------------------------------------------------------------- - -op1_ratio = hslider("Operators/Op 1/op1_ratio", 1.0, 0.125, 16.0, 0.001) : si.smoo; -op1_level = hslider("Operators/Op 1/op1_level", 0.8, 0.0, 1.0, 0.001) : si.smoo; -op1_attack = hslider("Operators/Op 1/op1_attack", 0.01, 0.001, 5.0, 0.001); -op1_decay = hslider("Operators/Op 1/op1_decay", 0.3, 0.001, 10.0, 0.001); -op1_sustain = hslider("Operators/Op 1/op1_sustain", 0.7, 0.0, 1.0, 0.001); -op1_release = hslider("Operators/Op 1/op1_release", 0.5, 0.01, 10.0, 0.001); - -op2_ratio = hslider("Operators/Op 2/op2_ratio", 2.0, 0.125, 16.0, 0.001) : si.smoo; -op2_level = hslider("Operators/Op 2/op2_level", 0.6, 0.0, 1.0, 0.001) : si.smoo; -op2_attack = hslider("Operators/Op 2/op2_attack", 0.01, 0.001, 5.0, 0.001); -op2_decay = hslider("Operators/Op 2/op2_decay", 0.3, 0.001, 10.0, 0.001); -op2_sustain = hslider("Operators/Op 2/op2_sustain", 0.7, 0.0, 1.0, 0.001); -op2_release = hslider("Operators/Op 2/op2_release", 0.5, 0.01, 10.0, 0.001); - -op3_ratio = hslider("Operators/Op 3/op3_ratio", 3.0, 0.125, 16.0, 0.001) : si.smoo; -op3_level = hslider("Operators/Op 3/op3_level", 0.4, 0.0, 1.0, 0.001) : si.smoo; -op3_attack = hslider("Operators/Op 3/op3_attack", 0.01, 0.001, 5.0, 0.001); -op3_decay = hslider("Operators/Op 3/op3_decay", 0.3, 0.001, 10.0, 0.001); -op3_sustain = hslider("Operators/Op 3/op3_sustain", 0.7, 0.0, 1.0, 0.001); -op3_release = hslider("Operators/Op 3/op3_release", 0.5, 0.01, 10.0, 0.001); - -op4_ratio = hslider("Operators/Op 4/op4_ratio", 0.5, 0.125, 16.0, 0.001) : si.smoo; -op4_level = hslider("Operators/Op 4/op4_level", 0.3, 0.0, 1.0, 0.001) : si.smoo; -op4_attack = hslider("Operators/Op 4/op4_attack", 0.01, 0.001, 5.0, 0.001); -op4_decay = hslider("Operators/Op 4/op4_decay", 0.3, 0.001, 10.0, 0.001); -op4_sustain = hslider("Operators/Op 4/op4_sustain", 0.7, 0.0, 1.0, 0.001); -op4_release = hslider("Operators/Op 4/op4_release", 0.5, 0.01, 10.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Cross-Modulation Matrix (params 24–35) -// mXY = op X modulates op Y (X's output is added to Y's phase) -// --------------------------------------------------------------------------- - -m12 = hslider("Matrix/m12", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m13 = hslider("Matrix/m13", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m14 = hslider("Matrix/m14", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m21 = hslider("Matrix/m21", 1.0, 0.0, 10.0, 0.001) : si.smoo; -m23 = hslider("Matrix/m23", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m24 = hslider("Matrix/m24", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m31 = hslider("Matrix/m31", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m32 = hslider("Matrix/m32", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m34 = hslider("Matrix/m34", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m41 = hslider("Matrix/m41", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m42 = hslider("Matrix/m42", 0.0, 0.0, 10.0, 0.001) : si.smoo; -m43 = hslider("Matrix/m43", 0.0, 0.0, 10.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 3 — Self-Feedback × 4 (params 36–39) -// --------------------------------------------------------------------------- - -fb1 = hslider("Feedback/fb1", 0.0, 0.0, 1.0, 0.001) : si.smoo; -fb2 = hslider("Feedback/fb2", 0.0, 0.0, 1.0, 0.001) : si.smoo; -fb3 = hslider("Feedback/fb3", 0.0, 0.0, 1.0, 0.001) : si.smoo; -fb4 = hslider("Feedback/fb4", 0.0, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 4 — Global Modulation (params 40–47) -// --------------------------------------------------------------------------- - -lfo_rate = hslider("Global/lfo_rate", 5.0, 0.01, 20.0, 0.001); -lfo_depth = hslider("Global/lfo_depth", 0.0, 0.0, 1.0, 0.001) : si.smoo; -lfo_pitch = hslider("Global/lfo_pitch", 0.5, 0.0, 1.0, 0.001) : si.smoo; -lfo_levels = hslider("Global/lfo_levels", 0.5, 0.0, 1.0, 0.001) : si.smoo; -lfo_waveform = hslider("Global/lfo_waveform", 0.0, 0.0, 1.0, 0.001); -pitch_env_amount = hslider("Global/pitch_env_amount", 0.0, 0.0, 1.0, 0.001) : si.smoo; -pitch_env_decay = hslider("Global/pitch_env_decay", 0.1, 0.001, 2.0, 0.001); -vel_index_scale = hslider("Global/vel_index_scale", 0.0, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 5 — Master (params 48–55) -// --------------------------------------------------------------------------- - -master_level = hslider("Master/level", 0.7, 0.0, 1.0, 0.001) : si.smoo; -vel_sens = hslider("Master/vel_sens", 0.5, 0.0, 1.0, 0.001); -pitch_glide = hslider("Master/pitch_glide", 0.0, 0.0, 10.0, 0.001); -fine_tune = hslider("Master/fine_tune[unit:cents]", 0.0, -50.0, 50.0, 0.01); -waveform_blend = hslider("Master/waveform_blend", 0.0, 0.0, 1.0, 0.001) : si.smoo; -stereo_spread = hslider("Master/stereo_spread", 0.1, 0.0, 1.0, 0.001) : si.smoo; -output_saturation = hslider("Master/output_saturation", 0.0, 0.0, 1.0, 0.001) : si.smoo; -output_hp = hslider("Master/output_hp[unit:Hz]", 20.0, 20.0, 200.0, 0.1); - -// --------------------------------------------------------------------------- -// Derived values -// --------------------------------------------------------------------------- - -// Cents → frequency ratio -cent2ratio(c) = pow(2.0, c / 1200.0); - -// Base frequency with fine-tune -base_freq = freq * cent2ratio(fine_tune); - -// Velocity gain: blend between 1.0 (no velocity) and actual velocity -vel_gain = (1.0 - vel_sens) + vel_sens * vel; - -// Velocity-scaled FM index multiplier -// vel_index_scale=0: velocity doesn't affect FM indices -// vel_index_scale=1: FM indices are fully scaled by velocity -vel_idx_mult = (1.0 - vel_index_scale) + vel_index_scale * vel; - -// LFO: 4 waveforms crossfaded -// Segments: [0,0.333)=sine→tri, [0.333,0.667)=tri→saw, [0.667,1]=saw→square -lfo_sig = - ba.if(lfo_waveform < 0.333, - (1.0 - lfo_waveform*3.0) * os.osc(lfo_rate) + lfo_waveform*3.0 * os.triangle(lfo_rate), - ba.if(lfo_waveform < 0.667, - (1.0 - (lfo_waveform-0.333)*3.0) * os.triangle(lfo_rate) + (lfo_waveform-0.333)*3.0 * os.sawtooth(lfo_rate), - (1.0 - (lfo_waveform-0.667)*3.0) * os.sawtooth(lfo_rate) + (lfo_waveform-0.667)*3.0 * os.square(lfo_rate))); - -// Pitch envelope: fast attack, configurable decay, triggered by gate -pitch_env = en.ar(0.001, pitch_env_decay, gate); - -// Modulated frequency: fine-tune + LFO pitch + pitch envelope -// LFO pitch: ±0.0833 semitones per unit depth (subtle vibrato) -// Pitch env: up to +1 octave (ratio +1.0 = +octave) -lfo_pitch_offset = lfo_sig * lfo_depth * lfo_pitch * 0.0083; // ~0.1 semitone max -pitch_env_offset = pitch_env * pitch_env_amount; // 0..1 oct -mod_freq = base_freq * pow(2.0, lfo_pitch_offset + pitch_env_offset); - -// LFO level modulation (tremolo): 1.0 when depth=0, dips to (1-0.5*depth*lfo_levels) at trough -lfo_level_factor = 1.0 - lfo_depth * lfo_levels * 0.5 * (1.0 - lfo_sig) * 0.5; - -// Per-operator oscillator: blend sine (0) → triangle (1) -op_osc(f) = (1.0 - waveform_blend) * os.osc(f) + waveform_blend * os.triangle(f); - -// Per-operator ADSR envelopes -env1 = en.adsr(op1_attack, op1_decay, op1_sustain, op1_release, gate); -env2 = en.adsr(op2_attack, op2_decay, op2_sustain, op2_release, gate); -env3 = en.adsr(op3_attack, op3_decay, op3_sustain, op3_release, gate); -env4 = en.adsr(op4_attack, op4_decay, op4_sustain, op4_release, gate); - -// --------------------------------------------------------------------------- -// 4-Operator FM Routing -// -// Implementation strategy: build a recursive 4-channel bus using ~ -// The bus carries (op1_out, op2_out, op3_out, op4_out) with 1-sample delay. -// Each frame, we compute new outputs from delayed previous outputs. -// -// fmbus: (d1,d2,d3,d4) → (op1_out,op2_out,op3_out,op4_out) -// where dN are the 1-sample-delayed previous outputs (via ~) -// -// FM phase deviation: deviation_hz = index * modulator_out * carrier_freq -// This is the standard "frequency modulation" formula where modulator_out ∈ [-1,1] -// --------------------------------------------------------------------------- - -fmbus(d1,d2,d3,d4) = - op1_out, op2_out, op3_out, op4_out -with { - // Effective indices scaled by velocity - m12e = m12 * vel_idx_mult; m13e = m13 * vel_idx_mult; m14e = m14 * vel_idx_mult; - m21e = m21 * vel_idx_mult; m23e = m23 * vel_idx_mult; m24e = m24 * vel_idx_mult; - m31e = m31 * vel_idx_mult; m32e = m32 * vel_idx_mult; m34e = m34 * vel_idx_mult; - m41e = m41 * vel_idx_mult; m42e = m42 * vel_idx_mult; m43e = m43 * vel_idx_mult; - fb1e = fb1 * vel_idx_mult; fb2e = fb2 * vel_idx_mult; - fb3e = fb3 * vel_idx_mult; fb4e = fb4 * vel_idx_mult; - - f1 = mod_freq * op1_ratio; - f2 = mod_freq * op2_ratio; - f3 = mod_freq * op3_ratio; - f4 = mod_freq * op4_ratio; - - op1_out = env1 * op1_level * lfo_level_factor * vel_gain * - op_osc(f1 + (m21e*d2 + m31e*d3 + m41e*d4 + fb1e*d1) * f1); - - op2_out = env2 * op2_level * lfo_level_factor * vel_gain * - op_osc(f2 + (m12e*d1 + m32e*d3 + m42e*d4 + fb2e*d2) * f2); - - op3_out = env3 * op3_level * lfo_level_factor * vel_gain * - op_osc(f3 + (m13e*d1 + m23e*d2 + m43e*d4 + fb3e*d3) * f3); - - op4_out = env4 * op4_level * lfo_level_factor * vel_gain * - op_osc(f4 + (m14e*d1 + m24e*d2 + m34e*d3 + fb4e*d4) * f4); -}; - -// Route 4-channel bus through feedback loop (introduces mandatory 1-sample delay) -// Output of fmbus is (op1,op2,op3,op4); sum all for audio out -fm_out = (fmbus ~ (si.bus(4))) : (_, _, _, _) :> _; - -// --------------------------------------------------------------------------- -// Post-processing -// --------------------------------------------------------------------------- - -// Soft clip: tanh approximation (drive controlled by output_saturation) -// At 0: linear passthrough. At 1: clips at ±~0.6 -soft_clip(x) = x / max(0.001, 1.0 + output_saturation * abs(x)); - -// Stereo spread via slight pitch detuning L vs R -spread_cents = stereo_spread * 5.0; // ±5 cents max - -// HP filter removes DC offset from heavy feedback FM -hp_out(x) = fi.highpass(1, output_hp, x); - -// --------------------------------------------------------------------------- -// process: mono FM sum → scale → soft-clip → HP → stereo spread -// --------------------------------------------------------------------------- - -process = - fm_out - : *(master_level * 0.25) // 4 ops summing: scale to safe range - : soft_clip - : hp_out - <: *(cent2ratio(spread_cents)), *(cent2ratio(0.0 - spread_cents)); diff --git a/playground/faust/fm-matrix.json b/playground/faust/fm-matrix.json deleted file mode 100644 index 0cbea0e..0000000 --- a/playground/faust/fm-matrix.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "fm-matrix","filename": "fm-matrix.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn fm_matrix -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/envelopes.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/oscillators.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 344892,"code": "Ly8gZm0tbWF0cml4LmRzcCDigJQgRnVsbCA1Ni1wYXJhbWV0ZXIgNC1vcGVyYXRvciBGTSBzeW50aGVzaXplciB3aXRoIGNvbnRpbnVvdXMgcm91dGluZyBtYXRyaXgKLy8KLy8gRGVzaWduOiA0IG9wZXJhdG9ycyB3aXRoIGZ1bGx5IGNvbnRpbnVvdXMgTsOXTiBjcm9zcy1tb2R1bGF0aW9uIG1hdHJpeC4KLy8gTm8gZml4ZWQgYWxnb3JpdGhtIHNsb3RzIOKAlCB0aGUgYWxnb3JpdGhtIGVtZXJnZXMgZnJvbSBtb2R1bGF0aW9uIGluZGV4IHZhbHVlcy4KLy8gV2hlbiBhbGwgcm91dGluZyBtYXRyaXggaW5kaWNlcyBhcmUgbmVhciAwOiBhZGRpdGl2ZSBzeW50aGVzaXMuCi8vIEFzIGluZGljZXMgZ3JvdzogaW5jcmVhc2luZ2x5IGNvbXBsZXggRk0gdGltYnJlcyBlbWVyZ2UuCi8vCi8vIFBhcmFtZXRlciBvcmRlcmluZyAoNTYgdG90YWwsIG1hcHMgdG8gTUxQIG91dHB1dCBpbmRpY2VzIDDigJM1NSk6Ci8vICAgMOKAkzIzOiAgT3BlcmF0b3IgQ29yZSDDlyA0IChyYXRpbywgbGV2ZWwsIGF0dGFjaywgZGVjYXksIHN1c3RhaW4sIHJlbGVhc2UpCi8vICAgMjTigJMzNTogQ3Jvc3MtTW9kdWxhdGlvbiBNYXRyaXggKDEyIGRpcmVjdGVkIHBhaXJzKQovLyAgIDM24oCTMzk6IFNlbGYtRmVlZGJhY2sgw5cgNAovLyAgIDQw4oCTNDc6IEdsb2JhbCBNb2R1bGF0aW9uIChMRk8sIHBpdGNoIGVudiwgdmVsb2NpdHkpCi8vICAgNDjigJM1NTogTWFzdGVyIChsZXZlbCwgdmVsIHNlbnMsIGdsaWRlLCBmaW5lIHR1bmUsIHdhdmVmb3JtIGJsZW5kLCBzdGVyZW8gc3ByZWFkLAovLyAgICAgICAgICAgb3V0cHV0IHNhdHVyYXRpb24sIG91dHB1dCBIUCkKLy8KLy8gSGlkZGVuIHBhcmFtcyAobm90IGluIHRoZSA1Ni1wYXJhbSBOSVNQUyBsaXN0KToKLy8gICBmcmVxICDigJQgc2V0IGJ5IG5vdGVPbiAoSHopCi8vICAgZ2F0ZSAg4oCUIHNldCBieSBub3RlT24vbm90ZU9mZiAoMC8xKQovLyAgIF92ZWwgIOKAlCBzZXQgYnkgbm90ZU9uICh2ZWxvY2l0eSAw4oCTMSkKLy8KLy8gRk0gaW1wbGVtZW50YXRpb246IGVhY2ggb3BlcmF0b3IgcGhhc2UtbW9kdWxhdGVzIG90aGVycyB2aWEgYSAxLXNhbXBsZSBkZWxheWVkCi8vIGZlZWRiYWNrIGJ1cy4gVGhlIDQgb3BlcmF0b3Igb3V0cHV0cyBhcmUgY29sbGVjdGVkIGludG8gYSA0LWNoYW5uZWwgYnVzIGFuZAovLyBsb29wZWQgdmlhIH4uIFRoaXMgaXMgdGhlIHN0YW5kYXJkIEZhdXN0IEZNIHBhdHRlcm4uCi8vCi8vIEJ1aWxkOgovLyAgIG5peC1zaGVsbCAtcCBmYXVzdCAtLXJ1biBcCi8vICAgICAiZmF1c3QgLWxhbmcgd2FzbSAtY24gZm1fbWF0cml4IGZtLW1hdHJpeC5kc3AgLW8gZm0tbWF0cml4Lndhc20iCgppbXBvcnQoInN0ZGZhdXN0LmxpYiIpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEhpZGRlbiBjb250cm9sIHBhcmFtcyAoZHJpdmVuIGJ5IG5vdGVPbi9ub3RlT2ZmLCBub3QgTklTUFMpCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZnJlcSA9IGhzbGlkZXIoIk1hc3Rlci9mcmVxW2hpZGRlbjoxXVt1bml0Okh6XSIsIDIyMCwgMjAsIDQwMDAsIDAuMDEpOwpnYXRlID0gYnV0dG9uKCJNYXN0ZXIvZ2F0ZVtoaWRkZW46MV0iKTsKdmVsICA9IGhzbGlkZXIoIk1hc3Rlci9fdmVsW2hpZGRlbjoxXSIsIDAuNywgMC4wLCAxLjAsIDAuMDAxKSA6IHNpLnNtb287CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gR3JvdXAgMSDigJQgT3BlcmF0b3IgQ29yZSDDlyA0ICAocGFyYW1zIDDigJMyMykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpvcDFfcmF0aW8gICA9IGhzbGlkZXIoIk9wZXJhdG9ycy9PcCAxL29wMV9yYXRpbyIsICAgMS4wLCAwLjEyNSwgMTYuMCwgMC4wMDEpIDogc2kuc21vbzsKb3AxX2xldmVsICAgPSBoc2xpZGVyKCJPcGVyYXRvcnMvT3AgMS9vcDFfbGV2ZWwiLCAgIDAuOCwgMC4wLCAgIDEuMCwgIDAuMDAxKSA6IHNpLnNtb287Cm9wMV9hdHRhY2sgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDEvb3AxX2F0dGFjayIsICAwLjAxLCAwLjAwMSwgNS4wLCAwLjAwMSk7Cm9wMV9kZWNheSAgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDEvb3AxX2RlY2F5IiwgICAwLjMsIDAuMDAxLCAxMC4wLCAwLjAwMSk7Cm9wMV9zdXN0YWluID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDEvb3AxX3N1c3RhaW4iLCAwLjcsIDAuMCwgICAxLjAsICAwLjAwMSk7Cm9wMV9yZWxlYXNlID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDEvb3AxX3JlbGVhc2UiLCAwLjUsIDAuMDEsICAxMC4wLCAwLjAwMSk7CgpvcDJfcmF0aW8gICA9IGhzbGlkZXIoIk9wZXJhdG9ycy9PcCAyL29wMl9yYXRpbyIsICAgMi4wLCAwLjEyNSwgMTYuMCwgMC4wMDEpIDogc2kuc21vbzsKb3AyX2xldmVsICAgPSBoc2xpZGVyKCJPcGVyYXRvcnMvT3AgMi9vcDJfbGV2ZWwiLCAgIDAuNiwgMC4wLCAgIDEuMCwgIDAuMDAxKSA6IHNpLnNtb287Cm9wMl9hdHRhY2sgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDIvb3AyX2F0dGFjayIsICAwLjAxLCAwLjAwMSwgNS4wLCAwLjAwMSk7Cm9wMl9kZWNheSAgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDIvb3AyX2RlY2F5IiwgICAwLjMsIDAuMDAxLCAxMC4wLCAwLjAwMSk7Cm9wMl9zdXN0YWluID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDIvb3AyX3N1c3RhaW4iLCAwLjcsIDAuMCwgICAxLjAsICAwLjAwMSk7Cm9wMl9yZWxlYXNlID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDIvb3AyX3JlbGVhc2UiLCAwLjUsIDAuMDEsICAxMC4wLCAwLjAwMSk7CgpvcDNfcmF0aW8gICA9IGhzbGlkZXIoIk9wZXJhdG9ycy9PcCAzL29wM19yYXRpbyIsICAgMy4wLCAwLjEyNSwgMTYuMCwgMC4wMDEpIDogc2kuc21vbzsKb3AzX2xldmVsICAgPSBoc2xpZGVyKCJPcGVyYXRvcnMvT3AgMy9vcDNfbGV2ZWwiLCAgIDAuNCwgMC4wLCAgIDEuMCwgIDAuMDAxKSA6IHNpLnNtb287Cm9wM19hdHRhY2sgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDMvb3AzX2F0dGFjayIsICAwLjAxLCAwLjAwMSwgNS4wLCAwLjAwMSk7Cm9wM19kZWNheSAgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDMvb3AzX2RlY2F5IiwgICAwLjMsIDAuMDAxLCAxMC4wLCAwLjAwMSk7Cm9wM19zdXN0YWluID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDMvb3AzX3N1c3RhaW4iLCAwLjcsIDAuMCwgICAxLjAsICAwLjAwMSk7Cm9wM19yZWxlYXNlID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDMvb3AzX3JlbGVhc2UiLCAwLjUsIDAuMDEsICAxMC4wLCAwLjAwMSk7CgpvcDRfcmF0aW8gICA9IGhzbGlkZXIoIk9wZXJhdG9ycy9PcCA0L29wNF9yYXRpbyIsICAgMC41LCAwLjEyNSwgMTYuMCwgMC4wMDEpIDogc2kuc21vbzsKb3A0X2xldmVsICAgPSBoc2xpZGVyKCJPcGVyYXRvcnMvT3AgNC9vcDRfbGV2ZWwiLCAgIDAuMywgMC4wLCAgIDEuMCwgIDAuMDAxKSA6IHNpLnNtb287Cm9wNF9hdHRhY2sgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDQvb3A0X2F0dGFjayIsICAwLjAxLCAwLjAwMSwgNS4wLCAwLjAwMSk7Cm9wNF9kZWNheSAgID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDQvb3A0X2RlY2F5IiwgICAwLjMsIDAuMDAxLCAxMC4wLCAwLjAwMSk7Cm9wNF9zdXN0YWluID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDQvb3A0X3N1c3RhaW4iLCAwLjcsIDAuMCwgICAxLjAsICAwLjAwMSk7Cm9wNF9yZWxlYXNlID0gaHNsaWRlcigiT3BlcmF0b3JzL09wIDQvb3A0X3JlbGVhc2UiLCAwLjUsIDAuMDEsICAxMC4wLCAwLjAwMSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gR3JvdXAgMiDigJQgQ3Jvc3MtTW9kdWxhdGlvbiBNYXRyaXggKHBhcmFtcyAyNOKAkzM1KQovLyBtWFkgPSBvcCBYIG1vZHVsYXRlcyBvcCBZIChYJ3Mgb3V0cHV0IGlzIGFkZGVkIHRvIFkncyBwaGFzZSkKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgptMTIgPSBoc2xpZGVyKCJNYXRyaXgvbTEyIiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKSA6IHNpLnNtb287Cm0xMyA9IGhzbGlkZXIoIk1hdHJpeC9tMTMiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpIDogc2kuc21vbzsKbTE0ID0gaHNsaWRlcigiTWF0cml4L20xNCIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSkgOiBzaS5zbW9vOwptMjEgPSBoc2xpZGVyKCJNYXRyaXgvbTIxIiwgMS4wLCAwLjAsIDEwLjAsIDAuMDAxKSA6IHNpLnNtb287Cm0yMyA9IGhzbGlkZXIoIk1hdHJpeC9tMjMiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpIDogc2kuc21vbzsKbTI0ID0gaHNsaWRlcigiTWF0cml4L20yNCIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSkgOiBzaS5zbW9vOwptMzEgPSBoc2xpZGVyKCJNYXRyaXgvbTMxIiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKSA6IHNpLnNtb287Cm0zMiA9IGhzbGlkZXIoIk1hdHJpeC9tMzIiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpIDogc2kuc21vbzsKbTM0ID0gaHNsaWRlcigiTWF0cml4L20zNCIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSkgOiBzaS5zbW9vOwptNDEgPSBoc2xpZGVyKCJNYXRyaXgvbTQxIiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKSA6IHNpLnNtb287Cm00MiA9IGhzbGlkZXIoIk1hdHJpeC9tNDIiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpIDogc2kuc21vbzsKbTQzID0gaHNsaWRlcigiTWF0cml4L200MyIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSkgOiBzaS5zbW9vOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDMg4oCUIFNlbGYtRmVlZGJhY2sgw5cgNCAgKHBhcmFtcyAzNuKAkzM5KQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmZiMSA9IGhzbGlkZXIoIkZlZWRiYWNrL2ZiMSIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKSA6IHNpLnNtb287CmZiMiA9IGhzbGlkZXIoIkZlZWRiYWNrL2ZiMiIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKSA6IHNpLnNtb287CmZiMyA9IGhzbGlkZXIoIkZlZWRiYWNrL2ZiMyIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKSA6IHNpLnNtb287CmZiNCA9IGhzbGlkZXIoIkZlZWRiYWNrL2ZiNCIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKSA6IHNpLnNtb287CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gR3JvdXAgNCDigJQgR2xvYmFsIE1vZHVsYXRpb24gIChwYXJhbXMgNDDigJM0NykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpsZm9fcmF0ZSAgICAgICAgID0gaHNsaWRlcigiR2xvYmFsL2xmb19yYXRlIiwgICAgICAgICA1LjAsICAgMC4wMSwgIDIwLjAsICAwLjAwMSk7Cmxmb19kZXB0aCAgICAgICAgPSBoc2xpZGVyKCJHbG9iYWwvbGZvX2RlcHRoIiwgICAgICAgIDAuMCwgICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287Cmxmb19waXRjaCAgICAgICAgPSBoc2xpZGVyKCJHbG9iYWwvbGZvX3BpdGNoIiwgICAgICAgIDAuNSwgICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287Cmxmb19sZXZlbHMgICAgICAgPSBoc2xpZGVyKCJHbG9iYWwvbGZvX2xldmVscyIsICAgICAgIDAuNSwgICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287Cmxmb193YXZlZm9ybSAgICAgPSBoc2xpZGVyKCJHbG9iYWwvbGZvX3dhdmVmb3JtIiwgICAgIDAuMCwgICAwLjAsICAgMS4wLCAgIDAuMDAxKTsKcGl0Y2hfZW52X2Ftb3VudCA9IGhzbGlkZXIoIkdsb2JhbC9waXRjaF9lbnZfYW1vdW50IiwgMC4wLCAgIDAuMCwgICAxLjAsICAgMC4wMDEpIDogc2kuc21vbzsKcGl0Y2hfZW52X2RlY2F5ICA9IGhzbGlkZXIoIkdsb2JhbC9waXRjaF9lbnZfZGVjYXkiLCAgMC4xLCAgIDAuMDAxLCAyLjAsICAgMC4wMDEpOwp2ZWxfaW5kZXhfc2NhbGUgID0gaHNsaWRlcigiR2xvYmFsL3ZlbF9pbmRleF9zY2FsZSIsICAwLjAsICAgMC4wLCAgIDEuMCwgICAwLjAwMSkgOiBzaS5zbW9vOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDUg4oCUIE1hc3RlciAgKHBhcmFtcyA0OOKAkzU1KQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCm1hc3Rlcl9sZXZlbCAgICAgICA9IGhzbGlkZXIoIk1hc3Rlci9sZXZlbCIsICAgICAgICAgICAgICAwLjcsICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287CnZlbF9zZW5zICAgICAgICAgICA9IGhzbGlkZXIoIk1hc3Rlci92ZWxfc2VucyIsICAgICAgICAgICAwLjUsICAwLjAsICAgMS4wLCAgIDAuMDAxKTsKcGl0Y2hfZ2xpZGUgICAgICAgID0gaHNsaWRlcigiTWFzdGVyL3BpdGNoX2dsaWRlIiwgICAgICAgIDAuMCwgIDAuMCwgICAxMC4wLCAgMC4wMDEpOwpmaW5lX3R1bmUgICAgICAgICAgPSBoc2xpZGVyKCJNYXN0ZXIvZmluZV90dW5lW3VuaXQ6Y2VudHNdIiwgMC4wLCAtNTAuMCwgNTAuMCwgMC4wMSk7CndhdmVmb3JtX2JsZW5kICAgICA9IGhzbGlkZXIoIk1hc3Rlci93YXZlZm9ybV9ibGVuZCIsICAgICAwLjAsICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287CnN0ZXJlb19zcHJlYWQgICAgICA9IGhzbGlkZXIoIk1hc3Rlci9zdGVyZW9fc3ByZWFkIiwgICAgICAwLjEsICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287Cm91dHB1dF9zYXR1cmF0aW9uICA9IGhzbGlkZXIoIk1hc3Rlci9vdXRwdXRfc2F0dXJhdGlvbiIsICAwLjAsICAwLjAsICAgMS4wLCAgIDAuMDAxKSA6IHNpLnNtb287Cm91dHB1dF9ocCAgICAgICAgICA9IGhzbGlkZXIoIk1hc3Rlci9vdXRwdXRfaHBbdW5pdDpIel0iLCAyMC4wLCAyMC4wLCAyMDAuMCwgIDAuMSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gRGVyaXZlZCB2YWx1ZXMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgovLyBDZW50cyDihpIgZnJlcXVlbmN5IHJhdGlvCmNlbnQycmF0aW8oYykgPSBwb3coMi4wLCBjIC8gMTIwMC4wKTsKCi8vIEJhc2UgZnJlcXVlbmN5IHdpdGggZmluZS10dW5lCmJhc2VfZnJlcSA9IGZyZXEgKiBjZW50MnJhdGlvKGZpbmVfdHVuZSk7CgovLyBWZWxvY2l0eSBnYWluOiBibGVuZCBiZXR3ZWVuIDEuMCAobm8gdmVsb2NpdHkpIGFuZCBhY3R1YWwgdmVsb2NpdHkKdmVsX2dhaW4gPSAoMS4wIC0gdmVsX3NlbnMpICsgdmVsX3NlbnMgKiB2ZWw7CgovLyBWZWxvY2l0eS1zY2FsZWQgRk0gaW5kZXggbXVsdGlwbGllcgovLyB2ZWxfaW5kZXhfc2NhbGU9MDogdmVsb2NpdHkgZG9lc24ndCBhZmZlY3QgRk0gaW5kaWNlcwovLyB2ZWxfaW5kZXhfc2NhbGU9MTogRk0gaW5kaWNlcyBhcmUgZnVsbHkgc2NhbGVkIGJ5IHZlbG9jaXR5CnZlbF9pZHhfbXVsdCA9ICgxLjAgLSB2ZWxfaW5kZXhfc2NhbGUpICsgdmVsX2luZGV4X3NjYWxlICogdmVsOwoKLy8gTEZPOiA0IHdhdmVmb3JtcyBjcm9zc2ZhZGVkCi8vIFNlZ21lbnRzOiBbMCwwLjMzMyk9c2luZeKGknRyaSwgWzAuMzMzLDAuNjY3KT10cmnihpJzYXcsIFswLjY2NywxXT1zYXfihpJzcXVhcmUKbGZvX3NpZyA9CiAgYmEuaWYobGZvX3dhdmVmb3JtIDwgMC4zMzMsCiAgICAoMS4wIC0gbGZvX3dhdmVmb3JtKjMuMCkgKiBvcy5vc2MobGZvX3JhdGUpICAgICAgKyBsZm9fd2F2ZWZvcm0qMy4wICAgICAgKiBvcy50cmlhbmdsZShsZm9fcmF0ZSksCiAgICBiYS5pZihsZm9fd2F2ZWZvcm0gPCAwLjY2NywKICAgICAgKDEuMCAtIChsZm9fd2F2ZWZvcm0tMC4zMzMpKjMuMCkgKiBvcy50cmlhbmdsZShsZm9fcmF0ZSkgKyAobGZvX3dhdmVmb3JtLTAuMzMzKSozLjAgKiBvcy5zYXd0b290aChsZm9fcmF0ZSksCiAgICAgICgxLjAgLSAobGZvX3dhdmVmb3JtLTAuNjY3KSozLjApICogb3Muc2F3dG9vdGgobGZvX3JhdGUpICsgKGxmb193YXZlZm9ybS0wLjY2NykqMy4wICogb3Muc3F1YXJlKGxmb19yYXRlKSkpOwoKLy8gUGl0Y2ggZW52ZWxvcGU6IGZhc3QgYXR0YWNrLCBjb25maWd1cmFibGUgZGVjYXksIHRyaWdnZXJlZCBieSBnYXRlCnBpdGNoX2VudiA9IGVuLmFyKDAuMDAxLCBwaXRjaF9lbnZfZGVjYXksIGdhdGUpOwoKLy8gTW9kdWxhdGVkIGZyZXF1ZW5jeTogZmluZS10dW5lICsgTEZPIHBpdGNoICsgcGl0Y2ggZW52ZWxvcGUKLy8gTEZPIHBpdGNoOiDCsTAuMDgzMyBzZW1pdG9uZXMgcGVyIHVuaXQgZGVwdGggKHN1YnRsZSB2aWJyYXRvKQovLyBQaXRjaCBlbnY6IHVwIHRvICsxIG9jdGF2ZSAocmF0aW8gKzEuMCA9ICtvY3RhdmUpCmxmb19waXRjaF9vZmZzZXQgPSBsZm9fc2lnICogbGZvX2RlcHRoICogbGZvX3BpdGNoICogMC4wMDgzOyAgIC8vIH4wLjEgc2VtaXRvbmUgbWF4CnBpdGNoX2Vudl9vZmZzZXQgPSBwaXRjaF9lbnYgKiBwaXRjaF9lbnZfYW1vdW50OyAgICAgICAgICAgICAgICAgLy8gMC4uMSBvY3QKbW9kX2ZyZXEgPSBiYXNlX2ZyZXEgKiBwb3coMi4wLCBsZm9fcGl0Y2hfb2Zmc2V0ICsgcGl0Y2hfZW52X29mZnNldCk7CgovLyBMRk8gbGV2ZWwgbW9kdWxhdGlvbiAodHJlbW9sbyk6IDEuMCB3aGVuIGRlcHRoPTAsIGRpcHMgdG8gKDEtMC41KmRlcHRoKmxmb19sZXZlbHMpIGF0IHRyb3VnaApsZm9fbGV2ZWxfZmFjdG9yID0gMS4wIC0gbGZvX2RlcHRoICogbGZvX2xldmVscyAqIDAuNSAqICgxLjAgLSBsZm9fc2lnKSAqIDAuNTsKCi8vIFBlci1vcGVyYXRvciBvc2NpbGxhdG9yOiBibGVuZCBzaW5lICgwKSDihpIgdHJpYW5nbGUgKDEpCm9wX29zYyhmKSA9ICgxLjAgLSB3YXZlZm9ybV9ibGVuZCkgKiBvcy5vc2MoZikgKyB3YXZlZm9ybV9ibGVuZCAqIG9zLnRyaWFuZ2xlKGYpOwoKLy8gUGVyLW9wZXJhdG9yIEFEU1IgZW52ZWxvcGVzCmVudjEgPSBlbi5hZHNyKG9wMV9hdHRhY2ssIG9wMV9kZWNheSwgb3AxX3N1c3RhaW4sIG9wMV9yZWxlYXNlLCBnYXRlKTsKZW52MiA9IGVuLmFkc3Iob3AyX2F0dGFjaywgb3AyX2RlY2F5LCBvcDJfc3VzdGFpbiwgb3AyX3JlbGVhc2UsIGdhdGUpOwplbnYzID0gZW4uYWRzcihvcDNfYXR0YWNrLCBvcDNfZGVjYXksIG9wM19zdXN0YWluLCBvcDNfcmVsZWFzZSwgZ2F0ZSk7CmVudjQgPSBlbi5hZHNyKG9wNF9hdHRhY2ssIG9wNF9kZWNheSwgb3A0X3N1c3RhaW4sIG9wNF9yZWxlYXNlLCBnYXRlKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyA0LU9wZXJhdG9yIEZNIFJvdXRpbmcKLy8KLy8gSW1wbGVtZW50YXRpb24gc3RyYXRlZ3k6IGJ1aWxkIGEgcmVjdXJzaXZlIDQtY2hhbm5lbCBidXMgdXNpbmcgfgovLyBUaGUgYnVzIGNhcnJpZXMgKG9wMV9vdXQsIG9wMl9vdXQsIG9wM19vdXQsIG9wNF9vdXQpIHdpdGggMS1zYW1wbGUgZGVsYXkuCi8vIEVhY2ggZnJhbWUsIHdlIGNvbXB1dGUgbmV3IG91dHB1dHMgZnJvbSBkZWxheWVkIHByZXZpb3VzIG91dHB1dHMuCi8vCi8vIGZtYnVzOiAoZDEsZDIsZDMsZDQpIOKGkiAob3AxX291dCxvcDJfb3V0LG9wM19vdXQsb3A0X291dCkKLy8gd2hlcmUgZE4gYXJlIHRoZSAxLXNhbXBsZS1kZWxheWVkIHByZXZpb3VzIG91dHB1dHMgKHZpYSB+KQovLwovLyBGTSBwaGFzZSBkZXZpYXRpb246IGRldmlhdGlvbl9oeiA9IGluZGV4ICogbW9kdWxhdG9yX291dCAqIGNhcnJpZXJfZnJlcQovLyBUaGlzIGlzIHRoZSBzdGFuZGFyZCAiZnJlcXVlbmN5IG1vZHVsYXRpb24iIGZvcm11bGEgd2hlcmUgbW9kdWxhdG9yX291dCDiiIggWy0xLDFdCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKZm1idXMoZDEsZDIsZDMsZDQpID0KICBvcDFfb3V0LCBvcDJfb3V0LCBvcDNfb3V0LCBvcDRfb3V0CndpdGggewogIC8vIEVmZmVjdGl2ZSBpbmRpY2VzIHNjYWxlZCBieSB2ZWxvY2l0eQogIG0xMmUgPSBtMTIgKiB2ZWxfaWR4X211bHQ7ICBtMTNlID0gbTEzICogdmVsX2lkeF9tdWx0OyAgbTE0ZSA9IG0xNCAqIHZlbF9pZHhfbXVsdDsKICBtMjFlID0gbTIxICogdmVsX2lkeF9tdWx0OyAgbTIzZSA9IG0yMyAqIHZlbF9pZHhfbXVsdDsgIG0yNGUgPSBtMjQgKiB2ZWxfaWR4X211bHQ7CiAgbTMxZSA9IG0zMSAqIHZlbF9pZHhfbXVsdDsgIG0zMmUgPSBtMzIgKiB2ZWxfaWR4X211bHQ7ICBtMzRlID0gbTM0ICogdmVsX2lkeF9tdWx0OwogIG00MWUgPSBtNDEgKiB2ZWxfaWR4X211bHQ7ICBtNDJlID0gbTQyICogdmVsX2lkeF9tdWx0OyAgbTQzZSA9IG00MyAqIHZlbF9pZHhfbXVsdDsKICBmYjFlID0gZmIxICogdmVsX2lkeF9tdWx0OyAgZmIyZSA9IGZiMiAqIHZlbF9pZHhfbXVsdDsKICBmYjNlID0gZmIzICogdmVsX2lkeF9tdWx0OyAgZmI0ZSA9IGZiNCAqIHZlbF9pZHhfbXVsdDsKCiAgZjEgPSBtb2RfZnJlcSAqIG9wMV9yYXRpbzsKICBmMiA9IG1vZF9mcmVxICogb3AyX3JhdGlvOwogIGYzID0gbW9kX2ZyZXEgKiBvcDNfcmF0aW87CiAgZjQgPSBtb2RfZnJlcSAqIG9wNF9yYXRpbzsKCiAgb3AxX291dCA9IGVudjEgKiBvcDFfbGV2ZWwgKiBsZm9fbGV2ZWxfZmFjdG9yICogdmVsX2dhaW4gKgogICAgb3Bfb3NjKGYxICsgKG0yMWUqZDIgKyBtMzFlKmQzICsgbTQxZSpkNCArIGZiMWUqZDEpICogZjEpOwoKICBvcDJfb3V0ID0gZW52MiAqIG9wMl9sZXZlbCAqIGxmb19sZXZlbF9mYWN0b3IgKiB2ZWxfZ2FpbiAqCiAgICBvcF9vc2MoZjIgKyAobTEyZSpkMSArIG0zMmUqZDMgKyBtNDJlKmQ0ICsgZmIyZSpkMikgKiBmMik7CgogIG9wM19vdXQgPSBlbnYzICogb3AzX2xldmVsICogbGZvX2xldmVsX2ZhY3RvciAqIHZlbF9nYWluICoKICAgIG9wX29zYyhmMyArIChtMTNlKmQxICsgbTIzZSpkMiArIG00M2UqZDQgKyBmYjNlKmQzKSAqIGYzKTsKCiAgb3A0X291dCA9IGVudjQgKiBvcDRfbGV2ZWwgKiBsZm9fbGV2ZWxfZmFjdG9yICogdmVsX2dhaW4gKgogICAgb3Bfb3NjKGY0ICsgKG0xNGUqZDEgKyBtMjRlKmQyICsgbTM0ZSpkMyArIGZiNGUqZDQpICogZjQpOwp9OwoKLy8gUm91dGUgNC1jaGFubmVsIGJ1cyB0aHJvdWdoIGZlZWRiYWNrIGxvb3AgKGludHJvZHVjZXMgbWFuZGF0b3J5IDEtc2FtcGxlIGRlbGF5KQovLyBPdXRwdXQgb2YgZm1idXMgaXMgKG9wMSxvcDIsb3AzLG9wNCk7IHN1bSBhbGwgZm9yIGF1ZGlvIG91dApmbV9vdXQgPSAoZm1idXMgfiAoc2kuYnVzKDQpKSkgOiAoXywgXywgXywgXykgOj4gXzsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBQb3N0LXByb2Nlc3NpbmcKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgovLyBTb2Z0IGNsaXA6IHRhbmggYXBwcm94aW1hdGlvbiAoZHJpdmUgY29udHJvbGxlZCBieSBvdXRwdXRfc2F0dXJhdGlvbikKLy8gQXQgMDogbGluZWFyIHBhc3N0aHJvdWdoLiBBdCAxOiBjbGlwcyBhdCDCsX4wLjYKc29mdF9jbGlwKHgpID0geCAvIG1heCgwLjAwMSwgMS4wICsgb3V0cHV0X3NhdHVyYXRpb24gKiBhYnMoeCkpOwoKLy8gU3RlcmVvIHNwcmVhZCB2aWEgc2xpZ2h0IHBpdGNoIGRldHVuaW5nIEwgdnMgUgpzcHJlYWRfY2VudHMgPSBzdGVyZW9fc3ByZWFkICogNS4wOyAgIC8vIMKxNSBjZW50cyBtYXgKCi8vIEhQIGZpbHRlciByZW1vdmVzIERDIG9mZnNldCBmcm9tIGhlYXZ5IGZlZWRiYWNrIEZNCmhwX291dCh4KSA9IGZpLmhpZ2hwYXNzKDEsIG91dHB1dF9ocCwgeCk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gcHJvY2VzczogbW9ubyBGTSBzdW0g4oaSIHNjYWxlIOKGkiBzb2Z0LWNsaXAg4oaSIEhQIOKGkiBzdGVyZW8gc3ByZWFkCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKcHJvY2VzcyA9CiAgZm1fb3V0CiAgOiAqKG1hc3Rlcl9sZXZlbCAqIDAuMjUpICAgICAgIC8vIDQgb3BzIHN1bW1pbmc6IHNjYWxlIHRvIHNhZmUgcmFuZ2UKICA6IHNvZnRfY2xpcAogIDogaHBfb3V0CiAgPDogKihjZW50MnJhdGlvKHNwcmVhZF9jZW50cykpLCAqKGNlbnQycmF0aW8oMC4wIC0gc3ByZWFkX2NlbnRzKSk7Cg==","inputs": 0,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn fm_matrix -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "envelopes.lib/adsr:author": "Yann Orlarey and Andrey Bundin" },{ "envelopes.lib/ar:author": "Yann Orlarey, Stéphane Letz" },{ "envelopes.lib/author": "GRAME" },{ "envelopes.lib/copyright": "GRAME" },{ "envelopes.lib/license": "LGPL with exception" },{ "envelopes.lib/name": "Faust Envelope Library" },{ "envelopes.lib/version": "1.3.0" },{ "filename": "fm-matrix.dsp" },{ "filters.lib/highpass:author": "Julius O. Smith III" },{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/pole:author": "Julius O. Smith III" },{ "filters.lib/pole:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/pole:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "fm-matrix" },{ "oscillators.lib/lf_sawpos:author": "Bart Brouns, revised by Stéphane Letz" },{ "oscillators.lib/lf_sawpos:licence": "STK-4.3" },{ "oscillators.lib/name": "Faust Oscillator Library" },{ "oscillators.lib/saw2ptr:author": "Julius O. Smith III" },{ "oscillators.lib/saw2ptr:license": "STK-4.3" },{ "oscillators.lib/sawN:author": "Julius O. Smith III" },{ "oscillators.lib/sawN:license": "STK-4.3" },{ "oscillators.lib/version": "1.6.0" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "fm-matrix","items": [ {"type": "hslider","label": "Feedback/fb1","varname": "fHslider3","shortname": "Feedback_fb1","address": "/fm-matrix/Feedback_fb1","index": 262200,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Feedback/fb2","varname": "fHslider26","shortname": "Feedback_fb2","address": "/fm-matrix/Feedback_fb2","index": 295312,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Feedback/fb3","varname": "fHslider36","shortname": "Feedback_fb3","address": "/fm-matrix/Feedback_fb3","index": 311824,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Feedback/fb4","varname": "fHslider46","shortname": "Feedback_fb4","address": "/fm-matrix/Feedback_fb4","index": 328336,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/lfo_depth","varname": "fHslider14","shortname": "Global_lfo_depth","address": "/fm-matrix/Global_lfo_depth","index": 278784,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/lfo_levels","varname": "fHslider20","shortname": "Global_lfo_levels","address": "/fm-matrix/Global_lfo_levels","index": 295248,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/lfo_pitch","varname": "fHslider13","shortname": "Global_lfo_pitch","address": "/fm-matrix/Global_lfo_pitch","index": 278772,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/lfo_rate","varname": "fHslider12","shortname": "Global_lfo_rate","address": "/fm-matrix/Global_lfo_rate","index": 262320,"init": 5,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "Global/lfo_waveform","varname": "fHslider11","shortname": "Global_lfo_waveform","address": "/fm-matrix/Global_lfo_waveform","index": 262316,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/pitch_env_amount","varname": "fHslider10","shortname": "Global_pitch_env_amount","address": "/fm-matrix/Global_pitch_env_amount","index": 262304,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Global/pitch_env_decay","varname": "fHslider9","shortname": "Global_pitch_env_decay","address": "/fm-matrix/Global_pitch_env_decay","index": 262300,"init": 0.1,"min": 0.001,"max": 2,"step": 0.001},{"type": "hslider","label": "Global/vel_index_scale","varname": "fHslider8","shortname": "Global_vel_index_scale","address": "/fm-matrix/Global_vel_index_scale","index": 262260,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/_vel","varname": "fHslider7","shortname": "Master_vel","address": "/fm-matrix/Master__vel","index": 262248,"meta": [{ "hidden": "1" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/fine_tune","varname": "fHslider16","shortname": "Master_fine_tune","address": "/fm-matrix/Master_fine_tune","index": 278808,"meta": [{ "unit": "cents" }],"init": 0,"min": -50,"max": 50,"step": 0.01},{"type": "hslider","label": "Master/freq","varname": "fHslider17","shortname": "Master_freq","address": "/fm-matrix/Master_freq","index": 278812,"meta": [{ "hidden": "1" },{ "unit": "Hz" }],"init": 220,"min": 20,"max": 4000,"step": 0.01},{"type": "button","label": "Master/gate","varname": "fButton0","shortname": "Master_gate","address": "/fm-matrix/Master_gate","index": 262272,"meta": [{ "hidden": "1" }]},{"type": "hslider","label": "Master/level","varname": "fHslider2","shortname": "Master_level","address": "/fm-matrix/Master_level","index": 262188,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/output_hp","varname": "fHslider1","shortname": "Master_output_hp","address": "/fm-matrix/Master_output_hp","index": 262180,"meta": [{ "unit": "Hz" }],"init": 20,"min": 20,"max": 200,"step": 0.1},{"type": "hslider","label": "Master/output_saturation","varname": "fHslider56","shortname": "Master_output_saturation","address": "/fm-matrix/Master_output_saturation","index": 344848,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/stereo_spread","varname": "fHslider0","shortname": "Master_stereo_spread","address": "/fm-matrix/Master_stereo_spread","index": 262168,"init": 0.1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/vel_sens","varname": "fHslider19","shortname": "Master_vel_sens","address": "/fm-matrix/Master_vel_sens","index": 295244,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Master/waveform_blend","varname": "fHslider18","shortname": "Master_waveform_blend","address": "/fm-matrix/Master_waveform_blend","index": 295224,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Matrix/m12","varname": "fHslider29","shortname": "Matrix_m12","address": "/fm-matrix/Matrix_m12","index": 295348,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m13","varname": "fHslider39","shortname": "Matrix_m13","address": "/fm-matrix/Matrix_m13","index": 311860,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m14","varname": "fHslider49","shortname": "Matrix_m14","address": "/fm-matrix/Matrix_m14","index": 328372,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m21","varname": "fHslider6","shortname": "Matrix_m21","address": "/fm-matrix/Matrix_m21","index": 262236,"init": 1,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m23","varname": "fHslider38","shortname": "Matrix_m23","address": "/fm-matrix/Matrix_m23","index": 311848,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m24","varname": "fHslider48","shortname": "Matrix_m24","address": "/fm-matrix/Matrix_m24","index": 328360,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m31","varname": "fHslider5","shortname": "Matrix_m31","address": "/fm-matrix/Matrix_m31","index": 262224,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m32","varname": "fHslider28","shortname": "Matrix_m32","address": "/fm-matrix/Matrix_m32","index": 295336,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m34","varname": "fHslider47","shortname": "Matrix_m34","address": "/fm-matrix/Matrix_m34","index": 328348,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m41","varname": "fHslider4","shortname": "Matrix_m41","address": "/fm-matrix/Matrix_m41","index": 262212,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m42","varname": "fHslider27","shortname": "Matrix_m42","address": "/fm-matrix/Matrix_m42","index": 295324,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Matrix/m43","varname": "fHslider37","shortname": "Matrix_m43","address": "/fm-matrix/Matrix_m43","index": 311836,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_attack","varname": "fHslider22","shortname": "Operators_Op_1_op1_attack","address": "/fm-matrix/Operators_Op_1_op1_attack","index": 295280,"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_decay","varname": "fHslider23","shortname": "Operators_Op_1_op1_decay","address": "/fm-matrix/Operators_Op_1_op1_decay","index": 295284,"init": 0.3,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_level","varname": "fHslider25","shortname": "Operators_Op_1_op1_level","address": "/fm-matrix/Operators_Op_1_op1_level","index": 295292,"init": 0.8,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_ratio","varname": "fHslider15","shortname": "Operators_Op_1_op1_ratio","address": "/fm-matrix/Operators_Op_1_op1_ratio","index": 278796,"init": 1,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_release","varname": "fHslider21","shortname": "Operators_Op_1_op1_release","address": "/fm-matrix/Operators_Op_1_op1_release","index": 295268,"init": 0.5,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 1/op1_sustain","varname": "fHslider24","shortname": "Operators_Op_1_op1_sustain","address": "/fm-matrix/Operators_Op_1_op1_sustain","index": 295288,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_attack","varname": "fHslider32","shortname": "Operators_Op_2_op2_attack","address": "/fm-matrix/Operators_Op_2_op2_attack","index": 311792,"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_decay","varname": "fHslider33","shortname": "Operators_Op_2_op2_decay","address": "/fm-matrix/Operators_Op_2_op2_decay","index": 311796,"init": 0.3,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_level","varname": "fHslider35","shortname": "Operators_Op_2_op2_level","address": "/fm-matrix/Operators_Op_2_op2_level","index": 311804,"init": 0.6,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_ratio","varname": "fHslider30","shortname": "Operators_Op_2_op2_ratio","address": "/fm-matrix/Operators_Op_2_op2_ratio","index": 295360,"init": 2,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_release","varname": "fHslider31","shortname": "Operators_Op_2_op2_release","address": "/fm-matrix/Operators_Op_2_op2_release","index": 311788,"init": 0.5,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 2/op2_sustain","varname": "fHslider34","shortname": "Operators_Op_2_op2_sustain","address": "/fm-matrix/Operators_Op_2_op2_sustain","index": 311800,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_attack","varname": "fHslider42","shortname": "Operators_Op_3_op3_attack","address": "/fm-matrix/Operators_Op_3_op3_attack","index": 328304,"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_decay","varname": "fHslider43","shortname": "Operators_Op_3_op3_decay","address": "/fm-matrix/Operators_Op_3_op3_decay","index": 328308,"init": 0.3,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_level","varname": "fHslider45","shortname": "Operators_Op_3_op3_level","address": "/fm-matrix/Operators_Op_3_op3_level","index": 328316,"init": 0.4,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_ratio","varname": "fHslider40","shortname": "Operators_Op_3_op3_ratio","address": "/fm-matrix/Operators_Op_3_op3_ratio","index": 311872,"init": 3,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_release","varname": "fHslider41","shortname": "Operators_Op_3_op3_release","address": "/fm-matrix/Operators_Op_3_op3_release","index": 328300,"init": 0.5,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 3/op3_sustain","varname": "fHslider44","shortname": "Operators_Op_3_op3_sustain","address": "/fm-matrix/Operators_Op_3_op3_sustain","index": 328312,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_attack","varname": "fHslider52","shortname": "Operators_Op_4_op4_attack","address": "/fm-matrix/Operators_Op_4_op4_attack","index": 344816,"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_decay","varname": "fHslider53","shortname": "Operators_Op_4_op4_decay","address": "/fm-matrix/Operators_Op_4_op4_decay","index": 344820,"init": 0.3,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_level","varname": "fHslider55","shortname": "Operators_Op_4_op4_level","address": "/fm-matrix/Operators_Op_4_op4_level","index": 344828,"init": 0.3,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_ratio","varname": "fHslider50","shortname": "Operators_Op_4_op4_ratio","address": "/fm-matrix/Operators_Op_4_op4_ratio","index": 328384,"init": 0.5,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_release","varname": "fHslider51","shortname": "Operators_Op_4_op4_release","address": "/fm-matrix/Operators_Op_4_op4_release","index": 344812,"init": 0.5,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "Operators/Op 4/op4_sustain","varname": "fHslider54","shortname": "Operators_Op_4_op4_sustain","address": "/fm-matrix/Operators_Op_4_op4_sustain","index": 344824,"init": 0.7,"min": 0,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/fm-matrix.wasm b/playground/faust/fm-matrix.wasm deleted file mode 100644 index ecc29c55afe4994e3b651307bcf4def26f83f9b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46317 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 diff --git a/playground/faust/gen-modular-dsp.py b/playground/faust/gen-modular-dsp.py deleted file mode 100644 index 0e38e27..0000000 --- a/playground/faust/gen-modular-dsp.py +++ /dev/null @@ -1,737 +0,0 @@ -#!/usr/bin/env python3 -""" -gen-modular-dsp.py — Emit modular-*.dsp files for the Faust "Modular" mode. - -The modulation matrix has 48 sources × 10 destinations and every amount must be -a unique hslider (Faust slider labels are compile-time string literals). Hand- -writing 480+ sliders is impractical, so this script produces a fully expanded -.dsp file with one hslider per (source, destination) cell plus an explicit -48-term weighted-sum expression for each destination. - -The generator emits three engines in one run: - - modular-subtractive.dsp Minimoog-style 3-osc + moog filter voice - modular-additive.dsp 64-partial additive voice (spectral descriptors) - modular-fm.dsp 4-operator FM voice with full cross-mod matrix - -Each engine has its own 48×10 modulation matrix and its own set of sound -parameters. All modulation (envelopes, LFOs) comes from the shared -mod-pool.lib — the engines themselves contain ZERO internal envs/LFOs. - -Run: - python3 gen-modular-dsp.py - -Re-running this script is reproducible: same inputs produce byte-identical -output. Please DO NOT hand-edit the generated .dsp files — edit this -generator and re-run it instead. -""" - -import os - -N_SRC = 48 - -# --------------------------------------------------------------------------- -# Engine definitions. One dict per engine; each has: -# name file stem ("modular-.dsp") -# title header comment title -# description short audio-path description -# destinations list of (idx, short_name, doc) — exactly 10 entries -# sound_params Faust source for engine sound params (hsliders/buttons) -# signal_flow Faust source for derived signals + process line -# --------------------------------------------------------------------------- - - -# Common hidden-controls block (identical for all engines) -HIDDEN_BLOCK = """\ -// --------------------------------------------------------------------------- -// Hidden controls -// --------------------------------------------------------------------------- -freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("0_Hidden/gate[hidden:1]"); -vel = hslider("0_Hidden/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; -""" - - -# --------------------------------------------------------------------------- -# Engine 1 — Subtractive (Minimoog-style) -# --------------------------------------------------------------------------- - -SUBTRACTIVE_DESTS = [ - (0, "pitch", "semitones, applied to all osc frequencies"), - (1, "osc2_detune", "cents offset added to osc2 detune knob"), - (2, "osc3_detune", "cents offset added to osc3 detune knob"), - (3, "osc_mix_bal", "-1..+1 crossfades osc1<->osc3"), - (4, "noise_level", "added to noise level knob"), - (5, "cutoff", "+/-5 octaves of filter cutoff modulation"), - (6, "resonance", "added to res knob"), - (7, "filter_env_amt", "second independent +/-5 octave cutoff mod channel"), - (8, "amp", "added to amp knob (route an ADSR here for a VCA env)"), - (9, "pan", "added to pan knob"), -] - -SUBTRACTIVE_PARAMS = """\ -// --------------------------------------------------------------------------- -// Group 1 — Oscillators -// --------------------------------------------------------------------------- -osc1_wave = hslider("1_Oscillators/00_osc1_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); -osc1_range = hslider("1_Oscillators/01_osc1_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); -osc1_level = hslider("1_Oscillators/02_osc1_level", 0.8, 0.0, 1.0, 0.001); - -osc2_wave = hslider("1_Oscillators/03_osc2_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); -osc2_range = hslider("1_Oscillators/04_osc2_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); -osc2_detune = hslider("1_Oscillators/05_osc2_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -osc2_level = hslider("1_Oscillators/06_osc2_level", 0.6, 0.0, 1.0, 0.001); - -osc3_wave = hslider("1_Oscillators/07_osc3_wave[tooltip:0=saw,0.5=tri,1=square]", 0.5, 0.0, 1.0, 0.001); -osc3_range = hslider("1_Oscillators/08_osc3_range[unit:oct]", -1.0, -2.0, 2.0, 0.01); -osc3_detune = hslider("1_Oscillators/09_osc3_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -osc3_level = hslider("1_Oscillators/10_osc3_level", 0.4, 0.0, 1.0, 0.001); -osc3_kb_track = hslider("1_Oscillators/11_osc3_kb_track[tooltip:1=tracks keyboard,0=LFO]", 1.0, 0.0, 1.0, 1.0); - -// --------------------------------------------------------------------------- -// Group 2 — Mixer -// --------------------------------------------------------------------------- -noise_type = hslider("2_Mixer/00_noise_type[tooltip:0=white,1=pink]", 0.0, 0.0, 1.0, 1.0); -noise_level = hslider("2_Mixer/01_noise_level", 0.0, 0.0, 1.0, 0.001); -mixer_drive = hslider("2_Mixer/02_mixer_drive[tooltip:Pre-filter overdrive]", 1.0, 0.5, 4.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Filter -// --------------------------------------------------------------------------- -cutoff = hslider("3_Filter/00_cutoff[scale:log][unit:Hz]", 1200.0, 20.0, 20000.0, 0.1); -resonance = hslider("3_Filter/01_resonance", 0.3, 0.0, 1.0, 0.001); -filter_kb = hslider("3_Filter/02_filter_kb_track", 0.5, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 4 — Master -// --------------------------------------------------------------------------- -master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); -master_glide = hslider("4_Master/01_master_glide[unit:s][scale:log]", 0.0, 0.0, 2.0, 0.001); -master_tune = hslider("4_Master/02_master_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -master_pan = hslider("4_Master/03_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor. At 1.0 the voice is always fully open and d08_amp -// modulation is purely additive decoration. Drop to 0 for classic -// ADSR-gated VCA behaviour (route s00_d08_amp to an ADSR). -base_amp = hslider("4_Master/04_base_amp", 1.0, 0.0, 1.0, 0.001); -""" - -SUBTRACTIVE_FLOW = """\ -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -cent2ratio(c) = pow(2.0, c / 1200.0); -oct2ratio(o) = pow(2.0, o); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Portamento -glide_tau = 0.0001 + master_glide * 1.0; -freq_glided = freq : si.smooth(ba.tau2pole(glide_tau)); - -// Base (glide + master fine tune) -base_freq = freq_glided * cent2ratio(master_tune); - -// --------------------------------------------------------------------------- -// Per-oscillator wavetable (saw → tri → square crossfade) -// 0.0 → saw, 0.5 → triangle, 1.0 → square. Output in [-1,+1]. -// --------------------------------------------------------------------------- -osc_wave(shape, f) = - ba.if(shape < 0.5, - (1.0 - shape*2.0) * os.lf_saw(f) + (shape*2.0) * os.lf_triangle(f), - (1.0 - (shape-0.5)*2.0) * os.lf_triangle(f) + ((shape-0.5)*2.0) * os.lf_squarewave(f)); - -// --------------------------------------------------------------------------- -// Oscillator frequencies (pitch modulation: 1 unit = 12 semitones) -// --------------------------------------------------------------------------- -pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); - -osc1_freq = base_freq * oct2ratio(osc1_range) * pitch_ratio; -osc2_freq = base_freq * oct2ratio(osc2_range) * pitch_ratio - * cent2ratio(osc2_detune + mod_osc2_detune(gate) * 50.0); -osc3_freq_tracked = base_freq * oct2ratio(osc3_range) * pitch_ratio - * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); -// When kb_track=0, osc3 becomes a free-running sub/LFO source at ~55 Hz * oct -osc3_freq_untracked = 55.0 * oct2ratio(osc3_range) - * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); -osc3_freq = osc3_kb_track * osc3_freq_tracked + (1.0 - osc3_kb_track) * osc3_freq_untracked; - -// --------------------------------------------------------------------------- -// Oscillators -// --------------------------------------------------------------------------- -o1 = osc_wave(osc1_wave, osc1_freq); -o2 = osc_wave(osc2_wave, osc2_freq); -o3 = osc_wave(osc3_wave, osc3_freq); - -// Osc mix balance: -1 = all osc1, +1 = all osc3, 0 = both equal. osc2 unaffected. -mix_bal = max(-1.0, min(1.0, mod_osc_mix_bal(gate))); -mix_t = (mix_bal + 1.0) * 0.5; // 0..1 -w_o1 = 1.0 - mix_t; -w_o3 = mix_t; - -// --------------------------------------------------------------------------- -// Noise: white (noise_type<0.5) / pink (else) -// --------------------------------------------------------------------------- -white_noise = no.noise; -pink_noise = no.pink_noise; -noise_raw = ba.if(noise_type < 0.5, white_noise, pink_noise); -noise_lvl = max(0.0, min(1.0, noise_level + mod_noise_level(gate))); - -// --------------------------------------------------------------------------- -// Mixer sum -// --------------------------------------------------------------------------- -mix_sum = - o1 * osc1_level * w_o1 - + o2 * osc2_level - + o3 * osc3_level * w_o3 - + noise_raw * noise_lvl; - -mix_driven = ma.tanh(mix_sum * mixer_drive); - -// --------------------------------------------------------------------------- -// Filter — Moog ladder. Cutoff is modulated in octaves (±5 oct per mod unit). -// --------------------------------------------------------------------------- -cutoff_mod_oct = (mod_cutoff(gate) + mod_filter_env_amt(gate)) * 5.0; -kb_scale = pow(freq_glided / 440.0, filter_kb); // 1.0 at A4 when kb=1 -eff_cutoff_raw = cutoff * kb_scale * oct2ratio(cutoff_mod_oct); -eff_cutoff = max(20.0, min(18000.0, eff_cutoff_raw)); -eff_res = max(0.0, min(0.99, resonance + mod_resonance(gate))); - -filtered = mix_driven : ve.moog_vcf(eff_res, eff_cutoff); - -// --------------------------------------------------------------------------- -// Amp and pan -// --------------------------------------------------------------------------- -// Velocity gain: blend 1.0 (no velocity) -> vel. Referenced here so Faust -// keeps the _vel hidden param alive in the JSON descriptor. -vel_gain = (1.0 - 0.3) + 0.3 * vel; // 30% velocity sensitivity, fixed -// d08_amp modulation is additive on top of base_amp (positive only). A -// fully untrained or adversarial matrix therefore cannot silence the -// voice while base_amp is high — which is the safety the "main amp gate -// on at all times" default relies on. Users who want envelope-gated -// voices set base_amp=0 and route a positive ADSR/LFO amount. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain; -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); - -// equal-power pan -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -signal = filtered * amp_val; -signal_L = signal * pan_l; -signal_R = signal * pan_r; - -process = signal_L, signal_R; -""" - - -# --------------------------------------------------------------------------- -# Engine 2 — Additive (64-partial spectral descriptors) -# --------------------------------------------------------------------------- - -ADDITIVE_DESTS = [ - (0, "pitch", "semitones, applied to the fundamental frequency"), - (1, "bright", "high-partial group amplitude boost/cut (replaces bright env)"), - (2, "tilt", "added to spectral_tilt knob"), - (3, "inharmonicity", "added to inharmonicity knob"), - (4, "odd_even", "added to odd/even balance knob"), - (5, "formant_ctr", "shifts both formant centre frequencies (harmonic index)"), - (6, "formant_depth", "added to formant_depth knob"), - (7, "noise_mix", "added to noise_floor knob (clamped 0..1)"), - (8, "amp", "master amplitude dest (route an ADSR here for a VCA env)"), - (9, "pan", "stereo balance"), -] - -ADDITIVE_PARAMS = """\ -// --------------------------------------------------------------------------- -// Group 1 — Spectral Shape (harmonic bank + descriptors) -// --------------------------------------------------------------------------- -h1_amp = hslider("1_Spectral/00_h1_amp", 0.8, 0.0, 1.0, 0.001); -h2_amp = hslider("1_Spectral/01_h2_amp", 0.5, 0.0, 1.0, 0.001); -h3_amp = hslider("1_Spectral/02_h3_amp", 0.35, 0.0, 1.0, 0.001); -h4_amp = hslider("1_Spectral/03_h4_amp", 0.25, 0.0, 1.0, 0.001); -h5_amp = hslider("1_Spectral/04_h5_amp", 0.18, 0.0, 1.0, 0.001); -h6_amp = hslider("1_Spectral/05_h6_amp", 0.12, 0.0, 1.0, 0.001); -h7_amp = hslider("1_Spectral/06_h7_amp", 0.08, 0.0, 1.0, 0.001); -h8_amp = hslider("1_Spectral/07_h8_amp", 0.06, 0.0, 1.0, 0.001); -h9_16_amp = hslider("1_Spectral/08_h9_16_amp", 0.05, 0.0, 1.0, 0.001); -h17_32_amp = hslider("1_Spectral/09_h17_32_amp", 0.025, 0.0, 1.0, 0.001); -h33_64_amp = hslider("1_Spectral/10_h33_64_amp", 0.01, 0.0, 1.0, 0.001); -spectral_tilt = hslider("1_Spectral/11_spectral_tilt", 0.0, -1.0, 1.0, 0.001); -inharmonicity = hslider("1_Spectral/12_inharmonicity", 0.0, 0.0, 0.15, 0.0001); -odd_even = hslider("1_Spectral/13_odd_even", 0.5, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Formants & Noise -// --------------------------------------------------------------------------- -formant1_freq = hslider("2_Formants/00_formant1_freq[tooltip:Formant 1 harmonic idx]", 3.0, 1.0, 16.0, 0.01); -formant2_freq = hslider("2_Formants/01_formant2_freq[tooltip:Formant 2 harmonic idx]", 6.0, 1.0, 16.0, 0.01); -formant_depth = hslider("2_Formants/02_formant_depth", 0.0, 0.0, 1.0, 0.001); -noise_floor = hslider("2_Formants/03_noise_floor", 0.0, 0.0, 0.2, 0.001); -noise_color = hslider("2_Formants/04_noise_color", 0.5, 0.0, 1.0, 0.001); -sub_harmonic = hslider("2_Formants/05_sub_harmonic", 0.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Master -// --------------------------------------------------------------------------- -level = hslider("3_Master/00_level", 0.7, 0.0, 1.0, 0.001); -fine_tune = hslider("3_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -saturation = hslider("3_Master/02_saturation", 0.0, 0.0, 1.0, 0.001); -stereo_phase_spread= hslider("3_Master/03_stereo_phase_spread",0.1, 0.0, 1.0, 0.001); -master_pan = hslider("3_Master/04_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor — see subtractive engine for full explanation. -base_amp = hslider("3_Master/05_base_amp", 1.0, 0.0, 1.0, 0.001); -""" - -ADDITIVE_FLOW = """\ -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -N = 64; -PI = ma.PI; - -cent2ratio(c) = pow(2.0, c / 1200.0); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Pitch: modulation (semitones), no glide -fine_ratio = cent2ratio(fine_tune); -pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); -base_freq = freq * fine_ratio * pitch_ratio; - -// --------------------------------------------------------------------------- -// Per-harmonic base amplitude from group sliders -// --------------------------------------------------------------------------- -group_amp(k) = - ba.if(k == 1, h1_amp, - ba.if(k == 2, h2_amp, - ba.if(k == 3, h3_amp, - ba.if(k == 4, h4_amp, - ba.if(k == 5, h5_amp, - ba.if(k == 6, h6_amp, - ba.if(k == 7, h7_amp, - ba.if(k == 8, h8_amp, - ba.if(k <= 16, h9_16_amp, - ba.if(k <= 32, h17_32_amp, - h33_64_amp)))))))))); - -// Spectral tilt (modulated) — amp *= k^tilt -eff_tilt = spectral_tilt + mod_tilt(gate); -tilt_factor(k) = pow(float(k), eff_tilt); - -// Odd/even balance (modulated, clamped) -eff_odd_even = max(0.0, min(1.0, odd_even + mod_odd_even(gate))); -odd_weight = (1.0 - eff_odd_even) * 2.0; -even_weight = eff_odd_even * 2.0; -odd_even_factor(k) = ba.if(k % 2 == 0, even_weight, odd_weight); - -// Brightness destination: progressive boost/cut on upper partials. -// bright_blend(k): 0 at k=1, 1 at k=N. mod_bright is [-1,+1]. -// factor(k) = 1 + bright_blend(k) * mod_bright. At mod=0 -> unity (neutral). -bright_blend(k) = float(k - 1) / float(N - 1); -mb = mod_bright(gate); -bright_factor(k) = 1.0 + bright_blend(k) * mb; - -// Inharmonicity (modulated, clamped to keep partials monotonic) -eff_inharm = max(0.0, min(0.2, inharmonicity + mod_inharmonicity(gate) * 0.15)); - -// Formant centres (modulated together — mod_formant_ctr shifts both by same amt) -eff_formant_shift = mod_formant_ctr(gate) * 8.0; // ±8 harmonics -eff_f1 = max(1.0, min(32.0, formant1_freq + eff_formant_shift)); -eff_f2 = max(1.0, min(32.0, formant2_freq + eff_formant_shift)); -eff_formant_depth = max(0.0, min(1.0, formant_depth + mod_formant_depth(gate))); - -// Formant shaping — two Gaussian bumps in harmonic-index space -sigma_sq = 1.5 * 1.5; -formant_bump(k, ctr) = exp(-0.5 * (float(k) - ctr) * (float(k) - ctr) / sigma_sq); -formant_factor(k) = - 1.0 + eff_formant_depth * (formant_bump(k, eff_f1) + formant_bump(k, eff_f2)); - -// Combined per-harmonic amplitude -harm_amp(k) = - group_amp(k) * tilt_factor(k) * odd_even_factor(k) * - bright_factor(k) * formant_factor(k); - -// Inharmonic partial frequency: freq_k = k*f0*(1 + B*(k^2 - 1)) -harm_freq(k) = base_freq * float(k) - * (1.0 + eff_inharm * (float(k) * float(k) - 1.0)); - -// Stereo spread — R channel gets a small per-harmonic pitch offset -stereo_spread_freq(k) = stereo_phase_spread * float(k) * 0.01; - -// --------------------------------------------------------------------------- -// Additive oscillator sums — L and R -// --------------------------------------------------------------------------- -additive_L = sum(k, N, harm_amp(k+1) * os.osc(harm_freq(k+1))); -additive_R = sum(k, N, - harm_amp(k+1) * os.osc(harm_freq(k+1) * (1.0 + stereo_spread_freq(k+1)))); - -// Sub-harmonic (0.5× fundamental) -sub_osc = sub_harmonic * os.osc(base_freq * 0.5); - -// --------------------------------------------------------------------------- -// Noise floor — coloured via one-pole LP -// --------------------------------------------------------------------------- -eff_noise = max(0.0, min(1.0, noise_floor + mod_noise_mix(gate))); -noise_lp_cutoff = 200.0 + (1.0 - noise_color) * 19800.0; -noise_signal = no.noise : fi.lowpass(1, noise_lp_cutoff); -noise_out = eff_noise * noise_signal; - -// --------------------------------------------------------------------------- -// Soft-clip saturation — tanh waveshaper -// --------------------------------------------------------------------------- -drive = 1.0 + saturation * 9.0; -softclip(x) = ma.tanh(x * drive) / drive; - -// --------------------------------------------------------------------------- -// Velocity gain — keeps _vel alive in the JSON descriptor -// --------------------------------------------------------------------------- -vel_gain = (1.0 - 0.3) + 0.3 * vel; - -// --------------------------------------------------------------------------- -// Amplitude and pan (both mod-driven) -// --------------------------------------------------------------------------- -// See subtractive engine for the positive-only mod_amp rationale. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * level * vel_gain; -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); - -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -raw_L = (additive_L + sub_osc + noise_out) * amp_val; -raw_R = (additive_R + sub_osc + noise_out) * amp_val; - -signal_L = softclip(raw_L) * pan_l; -signal_R = softclip(raw_R) * pan_r; - -process = signal_L, signal_R; -""" - - -# --------------------------------------------------------------------------- -# Engine 3 — FM (4-operator with cross-mod matrix) -# --------------------------------------------------------------------------- - -FM_DESTS = [ - (0, "pitch", "semitones added to the base frequency"), - (1, "op1_level", "added to op1 level knob"), - (2, "op2_level", "added to op2 level knob"), - (3, "op3_level", "added to op3 level knob"), - (4, "op4_level", "added to op4 level knob"), - (5, "cross_mod_global", "global FM depth multiplier on all 12 cross-mod cells"), - (6, "feedback_global", "global feedback multiplier on all 4 self-feedback cells"), - (7, "global_ratio", "additive scale factor on all 4 operator ratios"), - (8, "amp", "master amplitude dest (route an ADSR here for a VCA env)"), - (9, "pan", "stereo balance"), -] - -FM_PARAMS = """\ -// --------------------------------------------------------------------------- -// Group 1 — Operators (ratio & level per op) -// --------------------------------------------------------------------------- -op1_ratio = hslider("1_Operators/00_op1_ratio", 1.0, 0.125, 16.0, 0.001); -op1_level = hslider("1_Operators/01_op1_level", 0.8, 0.0, 1.0, 0.001); -op2_ratio = hslider("1_Operators/02_op2_ratio", 2.0, 0.125, 16.0, 0.001); -op2_level = hslider("1_Operators/03_op2_level", 0.6, 0.0, 1.0, 0.001); -op3_ratio = hslider("1_Operators/04_op3_ratio", 3.0, 0.125, 16.0, 0.001); -op3_level = hslider("1_Operators/05_op3_level", 0.4, 0.0, 1.0, 0.001); -op4_ratio = hslider("1_Operators/06_op4_ratio", 0.5, 0.125, 16.0, 0.001); -op4_level = hslider("1_Operators/07_op4_level", 0.3, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Cross-mod Matrix (mXY = op X modulates op Y) -// --------------------------------------------------------------------------- -m12 = hslider("2_CrossMod/00_m12", 0.0, 0.0, 10.0, 0.001); -m13 = hslider("2_CrossMod/01_m13", 0.0, 0.0, 10.0, 0.001); -m14 = hslider("2_CrossMod/02_m14", 0.0, 0.0, 10.0, 0.001); -m21 = hslider("2_CrossMod/03_m21", 1.0, 0.0, 10.0, 0.001); -m23 = hslider("2_CrossMod/04_m23", 0.0, 0.0, 10.0, 0.001); -m24 = hslider("2_CrossMod/05_m24", 0.0, 0.0, 10.0, 0.001); -m31 = hslider("2_CrossMod/06_m31", 0.0, 0.0, 10.0, 0.001); -m32 = hslider("2_CrossMod/07_m32", 0.0, 0.0, 10.0, 0.001); -m34 = hslider("2_CrossMod/08_m34", 0.0, 0.0, 10.0, 0.001); -m41 = hslider("2_CrossMod/09_m41", 0.0, 0.0, 10.0, 0.001); -m42 = hslider("2_CrossMod/10_m42", 0.0, 0.0, 10.0, 0.001); -m43 = hslider("2_CrossMod/11_m43", 0.0, 0.0, 10.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Self-feedback -// --------------------------------------------------------------------------- -fb1 = hslider("3_Feedback/00_fb1", 0.0, 0.0, 1.0, 0.001); -fb2 = hslider("3_Feedback/01_fb2", 0.0, 0.0, 1.0, 0.001); -fb3 = hslider("3_Feedback/02_fb3", 0.0, 0.0, 1.0, 0.001); -fb4 = hslider("3_Feedback/03_fb4", 0.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 4 — Master -// --------------------------------------------------------------------------- -master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); -fine_tune = hslider("4_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -stereo_spread = hslider("4_Master/02_stereo_spread", 0.1, 0.0, 1.0, 0.001); -output_saturation = hslider("4_Master/03_output_saturation", 0.0, 0.0, 1.0, 0.001); -output_hp = hslider("4_Master/04_output_hp[unit:Hz]",20.0, 20.0, 200.0,0.1); -master_pan = hslider("4_Master/05_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor — see subtractive engine for full explanation. -base_amp = hslider("4_Master/06_base_amp", 1.0, 0.0, 1.0, 0.001); -""" - -FM_FLOW = """\ -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -cent2ratio(c) = pow(2.0, c / 1200.0); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Base frequency: fine tune + pitch mod (semitones, ±12) -base_freq = freq * cent2ratio(fine_tune) * semi2ratio(mod_pitch(gate) * 12.0); - -// Velocity gain — keeps _vel alive -vel_gain = (1.0 - 0.3) + 0.3 * vel; - -// --------------------------------------------------------------------------- -// Effective per-operator ratios and levels (mod applied) -// global_ratio mod is an additive ratio offset (±8) -// --------------------------------------------------------------------------- -ratio_offset = mod_global_ratio(gate) * 8.0; -r1 = max(0.01, op1_ratio + ratio_offset); -r2 = max(0.01, op2_ratio + ratio_offset); -r3 = max(0.01, op3_ratio + ratio_offset); -r4 = max(0.01, op4_ratio + ratio_offset); - -lev1 = max(0.0, min(1.0, op1_level + mod_op1_level(gate))); -lev2 = max(0.0, min(1.0, op2_level + mod_op2_level(gate))); -lev3 = max(0.0, min(1.0, op3_level + mod_op3_level(gate))); -lev4 = max(0.0, min(1.0, op4_level + mod_op4_level(gate))); - -// --------------------------------------------------------------------------- -// Global multipliers for cross-mod and feedback. -// (1 + mod) in [0, 2] roughly; clamp to stay sane. -// --------------------------------------------------------------------------- -cross_mul = max(0.0, 1.0 + mod_cross_mod_global(gate)); -fb_mul = max(0.0, 1.0 + mod_feedback_global(gate)); - -m12e = m12 * cross_mul; m13e = m13 * cross_mul; m14e = m14 * cross_mul; -m21e = m21 * cross_mul; m23e = m23 * cross_mul; m24e = m24 * cross_mul; -m31e = m31 * cross_mul; m32e = m32 * cross_mul; m34e = m34 * cross_mul; -m41e = m41 * cross_mul; m42e = m42 * cross_mul; m43e = m43 * cross_mul; - -fb1e = fb1 * fb_mul; -fb2e = fb2 * fb_mul; -fb3e = fb3 * fb_mul; -fb4e = fb4 * fb_mul; - -// --------------------------------------------------------------------------- -// 4-operator FM bus — feedback loop via ~ on a 4-channel bus -// --------------------------------------------------------------------------- -fmbus(d1,d2,d3,d4) = - op1_out, op2_out, op3_out, op4_out -with { - f1 = base_freq * r1; - f2 = base_freq * r2; - f3 = base_freq * r3; - f4 = base_freq * r4; - - op1_out = lev1 * - os.osc(f1 + (m21e*d2 + m31e*d3 + m41e*d4 + fb1e*d1) * f1); - - op2_out = lev2 * - os.osc(f2 + (m12e*d1 + m32e*d3 + m42e*d4 + fb2e*d2) * f2); - - op3_out = lev3 * - os.osc(f3 + (m13e*d1 + m23e*d2 + m43e*d4 + fb3e*d3) * f3); - - op4_out = lev4 * - os.osc(f4 + (m14e*d1 + m24e*d2 + m34e*d3 + fb4e*d4) * f4); -}; - -// Route 4-channel bus through feedback loop, sum all ops -fm_out = (fmbus ~ (si.bus(4))) : (_, _, _, _) :> _; - -// --------------------------------------------------------------------------- -// Post-processing -// --------------------------------------------------------------------------- -soft_clip(x) = x / max(0.001, 1.0 + output_saturation * abs(x)); -hp_out(x) = fi.highpass(1, output_hp, x); - -// Master amplitude destination (route an ADSR here for VCA) -// See subtractive engine for the positive-only mod_amp rationale. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain * 0.25; - -// Pan -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -// Stereo spread via slight detuning (apply after main FM sum) -spread_cents = stereo_spread * 5.0; -spread_l = cent2ratio(spread_cents); -spread_r = cent2ratio(0.0 - spread_cents); - -mono_post = fm_out * amp_val : soft_clip : hp_out; - -signal_L = mono_post * spread_l * pan_l; -signal_R = mono_post * spread_r * pan_r; - -process = signal_L, signal_R; -""" - - -ENGINES = [ - { - "name": "subtractive", - "title": "Minimoog-style subtractive voice", - "description": "3 oscillators + noise → mixer → moog ladder filter → amp/pan.", - "destinations": SUBTRACTIVE_DESTS, - "sound_params": SUBTRACTIVE_PARAMS, - "signal_flow": SUBTRACTIVE_FLOW, - }, - { - "name": "additive", - "title": "64-partial additive voice", - "description": "64 harmonic sines with spectral descriptors (tilt, odd/even, formants).", - "destinations": ADDITIVE_DESTS, - "sound_params": ADDITIVE_PARAMS, - "signal_flow": ADDITIVE_FLOW, - }, - { - "name": "fm", - "title": "4-operator FM voice", - "description": "4 ops with 12-cell cross-mod matrix and per-op feedback.", - "destinations": FM_DESTS, - "sound_params": FM_PARAMS, - "signal_flow": FM_FLOW, - }, -] - - -# --------------------------------------------------------------------------- -# Shared matrix-emission helpers -# --------------------------------------------------------------------------- - -def src_name(s): - return f"s{s:02d}" - - -def dest_label(d, name): - return f"d{d:02d}_{name}" - - -def amt_slider_line(s, d, name): - label = f"MM_Matrix/{src_name(s)}_{dest_label(d, name)}" - varname = f"amt_{src_name(s)}_{dest_label(d, name)}" - return f'{varname} = hslider("{label}", 0.0, -1.0, 1.0, 0.001);' - - -def dest_expression(d, name): - varname = f"mod_{name}" - lines = [f"{varname}(gate) ="] - for s in range(N_SRC): - amt_var = f"amt_{src_name(s)}_{dest_label(d, name)}" - src_call = f"mp.src{s:02d}(gate)" - if s < N_SRC - 1: - lines.append(f" {src_call} * {amt_var} +") - else: - lines.append(f" {src_call} * {amt_var};") - return "\n".join(lines) - - -def gen_matrix_sliders(dests): - blocks = [] - for (d, name, _doc) in dests: - blocks.append(f"// Matrix column: destination {d} = {name}") - for s in range(N_SRC): - blocks.append(amt_slider_line(s, d, name)) - blocks.append("") - return "\n".join(blocks) - - -def gen_dest_expressions(dests): - blocks = [] - for (d, name, _doc) in dests: - blocks.append(dest_expression(d, name)) - blocks.append("") - return "\n".join(blocks) - - -def emit_engine(cfg, out_dir): - name = cfg["name"] - title = cfg["title"] - description = cfg["description"] - dests = cfg["destinations"] - sound_params = cfg["sound_params"] - signal_flow = cfg["signal_flow"] - - n_dest = len(dests) - assert n_dest == 10, f"engine {name}: expected 10 destinations, got {n_dest}" - - # Header comment with per-engine destination docs - dest_doc_lines = [] - for (d, dname, doc) in dests: - dest_doc_lines.append(f"// {d} {dname:<17} {doc}") - dest_doc = "\n".join(dest_doc_lines) - - header = f"""\ -// modular-{name}.dsp — {title} for the "Modular" mode. -// -// AUTO-GENERATED by gen-modular-dsp.py. DO NOT EDIT BY HAND. -// Regenerate with: -// cd playground/faust && python3 gen-modular-dsp.py -// -// Audio path: {description} -// All envelopes and LFOs come from mod-pool.lib via a 48-source × {n_dest}-dest -// modulation matrix. No internal envs/LFOs. -// -// Modulation destinations ({n_dest}): -{dest_doc} -// -// Hidden controls (driven by noteOn/noteOff, not by the ML engine): -// freq, gate, _vel -// -// Build: -// cd playground/faust && ./build.sh -// -// --------------------------------------------------------------------------- - -import("stdfaust.lib"); -mp = library("mod-pool.lib"); - -{HIDDEN_BLOCK} -{sound_params} -// --------------------------------------------------------------------------- -// Modulation matrix — 48 sources × {n_dest} destinations. -// Each hslider is one "amount" entry in [-1, +1]. Zero = no connection. -// Group "MM_Matrix" so the paramMeta parser can recognise matrix params. -// --------------------------------------------------------------------------- -""" - - parts = [ - header, - gen_matrix_sliders(dests), - "// ---------------------------------------------------------------------------", - "// Destination signals — one per modulation destination. Each is a 48-term", - "// weighted sum of sources × amounts. The `gate` argument is threaded through", - "// so that mp.src??(gate) can drive ADSR envelopes.", - "// ---------------------------------------------------------------------------", - "", - gen_dest_expressions(dests), - signal_flow, - ] - - out_path = os.path.join(out_dir, f"modular-{name}.dsp") - with open(out_path, "w") as f: - f.write("\n".join(parts)) - - n_matrix = N_SRC * n_dest - print(f"Wrote {out_path}") - print(f" destinations: {n_dest}") - print(f" matrix sliders: {n_matrix}") - - -def main(): - out_dir = os.path.dirname(os.path.abspath(__file__)) - for cfg in ENGINES: - emit_engine(cfg, out_dir) - - -if __name__ == "__main__": - main() diff --git a/playground/faust/mod-pool.lib b/playground/faust/mod-pool.lib deleted file mode 100644 index b20cb33..0000000 --- a/playground/faust/mod-pool.lib +++ /dev/null @@ -1,435 +0,0 @@ -// mod-pool.lib — Shared modulation pool (ADSRs + LFOs) for the "Modular" mode. -// -// Provides 16 ADSR slots + 32 LFO slots that can be routed through a -// per-engine modulation matrix to any parameter in the host .dsp. -// Engines import this via: -// -// mp = library("mod-pool.lib"); -// sources = mp.sources(gate); // 48-wide parallel bus -// mod_x = sum(s, 48, sources : ba.selector(s, 48) * amount(s)); -// -// PUBLIC API -// ---------- -// -// mp.N_ADSR integer — 16 -// mp.N_LFO integer — 32 -// mp.N_SOURCES integer — 48 (= N_ADSR + N_LFO) -// -// mp.adsrs(gate) bus — 16-wide parallel bus, each element [0,1] -// mp.lfos bus — 32-wide parallel bus, each element [-1,+1] -// mp.sources(gate) bus — 48-wide parallel bus (adsrs(gate), lfos) -// -// PARAMETER NAMESPACE CONVENTION -// ------------------------------ -// All mod-pool sliders live under groups prefixed with "MM_" so that the -// paramMeta parser can segregate them from engine sound params: -// -// MM_ADSR/NN_adsrNN_{attack,decay,sustain,release,enable} (16 slots) -// MM_LFO /NN_lfoNN_{rate,morph,enable} (32 slots) -// -// Engine-side matrix sliders (declared in the engine .dsp, not in this file) -// should follow: -// -// MM_Matrix/sNN_dNN_ (48 * n_dst) -// -// where sNN = source index (0..47; 0..15 = ADSRs, 16..47 = LFOs), -// dNN = destination index, and = short destination label. -// -// MUTING -// ------ -// Each slot has an "enable" hslider (0 or 1). The slot's output is multiplied -// by the enable value so a muted slot contributes 0 at the matrix input. This -// is cheap but does NOT let Faust eliminate the underlying env/LFO compute. -// Compile-time dead-code elimination of unused slots is a future concern. -// -// LFO WAVEMORPH -// ------------- -// A single "morph" slider (0..1) crossfades through four shapes: -// -// [0.000, 0.333) : sine -> triangle -// [0.333, 0.666) : triangle -> square -// [0.666, 1.000] : square -> saw -// -// Implemented via nested ba.if so the chosen pair is a linear crossfade. -// The output is always in [-1,+1]. -// -// --------------------------------------------------------------------------- - -import("stdfaust.lib"); - -// Counts (engine code can reference mp.N_SOURCES etc.) -N_ADSR = 16; -N_LFO = 32; -N_SOURCES = 48; - -// --------------------------------------------------------------------------- -// Per-slot hslider helpers. Unique label per slot index. -// Slot indices are 1-based in labels (adsr01..adsr16, lfo01..lfo32) to keep -// alphabetical ordering stable in the Faust JSON output. -// --------------------------------------------------------------------------- - -// ADSR param factories — declared per-slot because Faust slider labels must -// be string literals known at compile time. One function per slot index. - -adsr01_a = hslider("MM_ADSR/00_adsr01_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr01_d = hslider("MM_ADSR/00_adsr01_decay", 0.200, 0.001, 10.0, 0.001); -adsr01_s = hslider("MM_ADSR/00_adsr01_sustain", 0.700, 0.0, 1.0, 0.001); -adsr01_r = hslider("MM_ADSR/00_adsr01_release", 0.300, 0.01, 10.0, 0.001); -adsr01_e = hslider("MM_ADSR/00_adsr01_enable", 1.0, 0.0, 1.0, 1.0); - -adsr02_a = hslider("MM_ADSR/01_adsr02_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr02_d = hslider("MM_ADSR/01_adsr02_decay", 0.200, 0.001, 10.0, 0.001); -adsr02_s = hslider("MM_ADSR/01_adsr02_sustain", 0.700, 0.0, 1.0, 0.001); -adsr02_r = hslider("MM_ADSR/01_adsr02_release", 0.300, 0.01, 10.0, 0.001); -adsr02_e = hslider("MM_ADSR/01_adsr02_enable", 0.0, 0.0, 1.0, 1.0); - -adsr03_a = hslider("MM_ADSR/02_adsr03_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr03_d = hslider("MM_ADSR/02_adsr03_decay", 0.200, 0.001, 10.0, 0.001); -adsr03_s = hslider("MM_ADSR/02_adsr03_sustain", 0.700, 0.0, 1.0, 0.001); -adsr03_r = hslider("MM_ADSR/02_adsr03_release", 0.300, 0.01, 10.0, 0.001); -adsr03_e = hslider("MM_ADSR/02_adsr03_enable", 0.0, 0.0, 1.0, 1.0); - -adsr04_a = hslider("MM_ADSR/03_adsr04_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr04_d = hslider("MM_ADSR/03_adsr04_decay", 0.200, 0.001, 10.0, 0.001); -adsr04_s = hslider("MM_ADSR/03_adsr04_sustain", 0.700, 0.0, 1.0, 0.001); -adsr04_r = hslider("MM_ADSR/03_adsr04_release", 0.300, 0.01, 10.0, 0.001); -adsr04_e = hslider("MM_ADSR/03_adsr04_enable", 0.0, 0.0, 1.0, 1.0); - -adsr05_a = hslider("MM_ADSR/04_adsr05_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr05_d = hslider("MM_ADSR/04_adsr05_decay", 0.200, 0.001, 10.0, 0.001); -adsr05_s = hslider("MM_ADSR/04_adsr05_sustain", 0.700, 0.0, 1.0, 0.001); -adsr05_r = hslider("MM_ADSR/04_adsr05_release", 0.300, 0.01, 10.0, 0.001); -adsr05_e = hslider("MM_ADSR/04_adsr05_enable", 0.0, 0.0, 1.0, 1.0); - -adsr06_a = hslider("MM_ADSR/05_adsr06_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr06_d = hslider("MM_ADSR/05_adsr06_decay", 0.200, 0.001, 10.0, 0.001); -adsr06_s = hslider("MM_ADSR/05_adsr06_sustain", 0.700, 0.0, 1.0, 0.001); -adsr06_r = hslider("MM_ADSR/05_adsr06_release", 0.300, 0.01, 10.0, 0.001); -adsr06_e = hslider("MM_ADSR/05_adsr06_enable", 0.0, 0.0, 1.0, 1.0); - -adsr07_a = hslider("MM_ADSR/06_adsr07_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr07_d = hslider("MM_ADSR/06_adsr07_decay", 0.200, 0.001, 10.0, 0.001); -adsr07_s = hslider("MM_ADSR/06_adsr07_sustain", 0.700, 0.0, 1.0, 0.001); -adsr07_r = hslider("MM_ADSR/06_adsr07_release", 0.300, 0.01, 10.0, 0.001); -adsr07_e = hslider("MM_ADSR/06_adsr07_enable", 0.0, 0.0, 1.0, 1.0); - -adsr08_a = hslider("MM_ADSR/07_adsr08_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr08_d = hslider("MM_ADSR/07_adsr08_decay", 0.200, 0.001, 10.0, 0.001); -adsr08_s = hslider("MM_ADSR/07_adsr08_sustain", 0.700, 0.0, 1.0, 0.001); -adsr08_r = hslider("MM_ADSR/07_adsr08_release", 0.300, 0.01, 10.0, 0.001); -adsr08_e = hslider("MM_ADSR/07_adsr08_enable", 0.0, 0.0, 1.0, 1.0); - -adsr09_a = hslider("MM_ADSR/08_adsr09_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr09_d = hslider("MM_ADSR/08_adsr09_decay", 0.200, 0.001, 10.0, 0.001); -adsr09_s = hslider("MM_ADSR/08_adsr09_sustain", 0.700, 0.0, 1.0, 0.001); -adsr09_r = hslider("MM_ADSR/08_adsr09_release", 0.300, 0.01, 10.0, 0.001); -adsr09_e = hslider("MM_ADSR/08_adsr09_enable", 0.0, 0.0, 1.0, 1.0); - -adsr10_a = hslider("MM_ADSR/09_adsr10_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr10_d = hslider("MM_ADSR/09_adsr10_decay", 0.200, 0.001, 10.0, 0.001); -adsr10_s = hslider("MM_ADSR/09_adsr10_sustain", 0.700, 0.0, 1.0, 0.001); -adsr10_r = hslider("MM_ADSR/09_adsr10_release", 0.300, 0.01, 10.0, 0.001); -adsr10_e = hslider("MM_ADSR/09_adsr10_enable", 0.0, 0.0, 1.0, 1.0); - -adsr11_a = hslider("MM_ADSR/10_adsr11_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr11_d = hslider("MM_ADSR/10_adsr11_decay", 0.200, 0.001, 10.0, 0.001); -adsr11_s = hslider("MM_ADSR/10_adsr11_sustain", 0.700, 0.0, 1.0, 0.001); -adsr11_r = hslider("MM_ADSR/10_adsr11_release", 0.300, 0.01, 10.0, 0.001); -adsr11_e = hslider("MM_ADSR/10_adsr11_enable", 0.0, 0.0, 1.0, 1.0); - -adsr12_a = hslider("MM_ADSR/11_adsr12_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr12_d = hslider("MM_ADSR/11_adsr12_decay", 0.200, 0.001, 10.0, 0.001); -adsr12_s = hslider("MM_ADSR/11_adsr12_sustain", 0.700, 0.0, 1.0, 0.001); -adsr12_r = hslider("MM_ADSR/11_adsr12_release", 0.300, 0.01, 10.0, 0.001); -adsr12_e = hslider("MM_ADSR/11_adsr12_enable", 0.0, 0.0, 1.0, 1.0); - -adsr13_a = hslider("MM_ADSR/12_adsr13_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr13_d = hslider("MM_ADSR/12_adsr13_decay", 0.200, 0.001, 10.0, 0.001); -adsr13_s = hslider("MM_ADSR/12_adsr13_sustain", 0.700, 0.0, 1.0, 0.001); -adsr13_r = hslider("MM_ADSR/12_adsr13_release", 0.300, 0.01, 10.0, 0.001); -adsr13_e = hslider("MM_ADSR/12_adsr13_enable", 0.0, 0.0, 1.0, 1.0); - -adsr14_a = hslider("MM_ADSR/13_adsr14_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr14_d = hslider("MM_ADSR/13_adsr14_decay", 0.200, 0.001, 10.0, 0.001); -adsr14_s = hslider("MM_ADSR/13_adsr14_sustain", 0.700, 0.0, 1.0, 0.001); -adsr14_r = hslider("MM_ADSR/13_adsr14_release", 0.300, 0.01, 10.0, 0.001); -adsr14_e = hslider("MM_ADSR/13_adsr14_enable", 0.0, 0.0, 1.0, 1.0); - -adsr15_a = hslider("MM_ADSR/14_adsr15_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr15_d = hslider("MM_ADSR/14_adsr15_decay", 0.200, 0.001, 10.0, 0.001); -adsr15_s = hslider("MM_ADSR/14_adsr15_sustain", 0.700, 0.0, 1.0, 0.001); -adsr15_r = hslider("MM_ADSR/14_adsr15_release", 0.300, 0.01, 10.0, 0.001); -adsr15_e = hslider("MM_ADSR/14_adsr15_enable", 0.0, 0.0, 1.0, 1.0); - -adsr16_a = hslider("MM_ADSR/15_adsr16_attack[scale:log]", 0.010, 0.001, 5.0, 0.001); -adsr16_d = hslider("MM_ADSR/15_adsr16_decay", 0.200, 0.001, 10.0, 0.001); -adsr16_s = hslider("MM_ADSR/15_adsr16_sustain", 0.700, 0.0, 1.0, 0.001); -adsr16_r = hslider("MM_ADSR/15_adsr16_release", 0.300, 0.01, 10.0, 0.001); -adsr16_e = hslider("MM_ADSR/15_adsr16_enable", 0.0, 0.0, 1.0, 1.0); - -// Single-slot ADSR expression. The enable multiplier zeros muted slots. -adsr_slot(a, d, s, r, e, gate) = en.adsr(a, d, s, r, gate) : *(e); - -// 16-wide parallel bus of ADSR outputs. Takes the global gate as an argument -// so engines can bind their own gate signal (noteOn/noteOff). -adsrs(gate) = - adsr_slot(adsr01_a, adsr01_d, adsr01_s, adsr01_r, adsr01_e, gate), - adsr_slot(adsr02_a, adsr02_d, adsr02_s, adsr02_r, adsr02_e, gate), - adsr_slot(adsr03_a, adsr03_d, adsr03_s, adsr03_r, adsr03_e, gate), - adsr_slot(adsr04_a, adsr04_d, adsr04_s, adsr04_r, adsr04_e, gate), - adsr_slot(adsr05_a, adsr05_d, adsr05_s, adsr05_r, adsr05_e, gate), - adsr_slot(adsr06_a, adsr06_d, adsr06_s, adsr06_r, adsr06_e, gate), - adsr_slot(adsr07_a, adsr07_d, adsr07_s, adsr07_r, adsr07_e, gate), - adsr_slot(adsr08_a, adsr08_d, adsr08_s, adsr08_r, adsr08_e, gate), - adsr_slot(adsr09_a, adsr09_d, adsr09_s, adsr09_r, adsr09_e, gate), - adsr_slot(adsr10_a, adsr10_d, adsr10_s, adsr10_r, adsr10_e, gate), - adsr_slot(adsr11_a, adsr11_d, adsr11_s, adsr11_r, adsr11_e, gate), - adsr_slot(adsr12_a, adsr12_d, adsr12_s, adsr12_r, adsr12_e, gate), - adsr_slot(adsr13_a, adsr13_d, adsr13_s, adsr13_r, adsr13_e, gate), - adsr_slot(adsr14_a, adsr14_d, adsr14_s, adsr14_r, adsr14_e, gate), - adsr_slot(adsr15_a, adsr15_d, adsr15_s, adsr15_r, adsr15_e, gate), - adsr_slot(adsr16_a, adsr16_d, adsr16_s, adsr16_r, adsr16_e, gate); - -// --------------------------------------------------------------------------- -// LFO slots (32) -// --------------------------------------------------------------------------- - -lfo01_r = hslider("MM_LFO/00_lfo01_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo01_m = hslider("MM_LFO/00_lfo01_morph", 0.0, 0.0, 1.0, 0.001); -lfo01_e = hslider("MM_LFO/00_lfo01_enable", 0.0, 0.0, 1.0, 1.0); - -lfo02_r = hslider("MM_LFO/01_lfo02_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo02_m = hslider("MM_LFO/01_lfo02_morph", 0.0, 0.0, 1.0, 0.001); -lfo02_e = hslider("MM_LFO/01_lfo02_enable", 0.0, 0.0, 1.0, 1.0); - -lfo03_r = hslider("MM_LFO/02_lfo03_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo03_m = hslider("MM_LFO/02_lfo03_morph", 0.0, 0.0, 1.0, 0.001); -lfo03_e = hslider("MM_LFO/02_lfo03_enable", 0.0, 0.0, 1.0, 1.0); - -lfo04_r = hslider("MM_LFO/03_lfo04_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo04_m = hslider("MM_LFO/03_lfo04_morph", 0.0, 0.0, 1.0, 0.001); -lfo04_e = hslider("MM_LFO/03_lfo04_enable", 0.0, 0.0, 1.0, 1.0); - -lfo05_r = hslider("MM_LFO/04_lfo05_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo05_m = hslider("MM_LFO/04_lfo05_morph", 0.0, 0.0, 1.0, 0.001); -lfo05_e = hslider("MM_LFO/04_lfo05_enable", 0.0, 0.0, 1.0, 1.0); - -lfo06_r = hslider("MM_LFO/05_lfo06_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo06_m = hslider("MM_LFO/05_lfo06_morph", 0.0, 0.0, 1.0, 0.001); -lfo06_e = hslider("MM_LFO/05_lfo06_enable", 0.0, 0.0, 1.0, 1.0); - -lfo07_r = hslider("MM_LFO/06_lfo07_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo07_m = hslider("MM_LFO/06_lfo07_morph", 0.0, 0.0, 1.0, 0.001); -lfo07_e = hslider("MM_LFO/06_lfo07_enable", 0.0, 0.0, 1.0, 1.0); - -lfo08_r = hslider("MM_LFO/07_lfo08_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo08_m = hslider("MM_LFO/07_lfo08_morph", 0.0, 0.0, 1.0, 0.001); -lfo08_e = hslider("MM_LFO/07_lfo08_enable", 0.0, 0.0, 1.0, 1.0); - -lfo09_r = hslider("MM_LFO/08_lfo09_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo09_m = hslider("MM_LFO/08_lfo09_morph", 0.0, 0.0, 1.0, 0.001); -lfo09_e = hslider("MM_LFO/08_lfo09_enable", 0.0, 0.0, 1.0, 1.0); - -lfo10_r = hslider("MM_LFO/09_lfo10_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo10_m = hslider("MM_LFO/09_lfo10_morph", 0.0, 0.0, 1.0, 0.001); -lfo10_e = hslider("MM_LFO/09_lfo10_enable", 0.0, 0.0, 1.0, 1.0); - -lfo11_r = hslider("MM_LFO/10_lfo11_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo11_m = hslider("MM_LFO/10_lfo11_morph", 0.0, 0.0, 1.0, 0.001); -lfo11_e = hslider("MM_LFO/10_lfo11_enable", 0.0, 0.0, 1.0, 1.0); - -lfo12_r = hslider("MM_LFO/11_lfo12_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo12_m = hslider("MM_LFO/11_lfo12_morph", 0.0, 0.0, 1.0, 0.001); -lfo12_e = hslider("MM_LFO/11_lfo12_enable", 0.0, 0.0, 1.0, 1.0); - -lfo13_r = hslider("MM_LFO/12_lfo13_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo13_m = hslider("MM_LFO/12_lfo13_morph", 0.0, 0.0, 1.0, 0.001); -lfo13_e = hslider("MM_LFO/12_lfo13_enable", 0.0, 0.0, 1.0, 1.0); - -lfo14_r = hslider("MM_LFO/13_lfo14_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo14_m = hslider("MM_LFO/13_lfo14_morph", 0.0, 0.0, 1.0, 0.001); -lfo14_e = hslider("MM_LFO/13_lfo14_enable", 0.0, 0.0, 1.0, 1.0); - -lfo15_r = hslider("MM_LFO/14_lfo15_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo15_m = hslider("MM_LFO/14_lfo15_morph", 0.0, 0.0, 1.0, 0.001); -lfo15_e = hslider("MM_LFO/14_lfo15_enable", 0.0, 0.0, 1.0, 1.0); - -lfo16_r = hslider("MM_LFO/15_lfo16_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo16_m = hslider("MM_LFO/15_lfo16_morph", 0.0, 0.0, 1.0, 0.001); -lfo16_e = hslider("MM_LFO/15_lfo16_enable", 0.0, 0.0, 1.0, 1.0); - -lfo17_r = hslider("MM_LFO/16_lfo17_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo17_m = hslider("MM_LFO/16_lfo17_morph", 0.0, 0.0, 1.0, 0.001); -lfo17_e = hslider("MM_LFO/16_lfo17_enable", 0.0, 0.0, 1.0, 1.0); - -lfo18_r = hslider("MM_LFO/17_lfo18_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo18_m = hslider("MM_LFO/17_lfo18_morph", 0.0, 0.0, 1.0, 0.001); -lfo18_e = hslider("MM_LFO/17_lfo18_enable", 0.0, 0.0, 1.0, 1.0); - -lfo19_r = hslider("MM_LFO/18_lfo19_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo19_m = hslider("MM_LFO/18_lfo19_morph", 0.0, 0.0, 1.0, 0.001); -lfo19_e = hslider("MM_LFO/18_lfo19_enable", 0.0, 0.0, 1.0, 1.0); - -lfo20_r = hslider("MM_LFO/19_lfo20_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo20_m = hslider("MM_LFO/19_lfo20_morph", 0.0, 0.0, 1.0, 0.001); -lfo20_e = hslider("MM_LFO/19_lfo20_enable", 0.0, 0.0, 1.0, 1.0); - -lfo21_r = hslider("MM_LFO/20_lfo21_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo21_m = hslider("MM_LFO/20_lfo21_morph", 0.0, 0.0, 1.0, 0.001); -lfo21_e = hslider("MM_LFO/20_lfo21_enable", 0.0, 0.0, 1.0, 1.0); - -lfo22_r = hslider("MM_LFO/21_lfo22_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo22_m = hslider("MM_LFO/21_lfo22_morph", 0.0, 0.0, 1.0, 0.001); -lfo22_e = hslider("MM_LFO/21_lfo22_enable", 0.0, 0.0, 1.0, 1.0); - -lfo23_r = hslider("MM_LFO/22_lfo23_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo23_m = hslider("MM_LFO/22_lfo23_morph", 0.0, 0.0, 1.0, 0.001); -lfo23_e = hslider("MM_LFO/22_lfo23_enable", 0.0, 0.0, 1.0, 1.0); - -lfo24_r = hslider("MM_LFO/23_lfo24_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo24_m = hslider("MM_LFO/23_lfo24_morph", 0.0, 0.0, 1.0, 0.001); -lfo24_e = hslider("MM_LFO/23_lfo24_enable", 0.0, 0.0, 1.0, 1.0); - -lfo25_r = hslider("MM_LFO/24_lfo25_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo25_m = hslider("MM_LFO/24_lfo25_morph", 0.0, 0.0, 1.0, 0.001); -lfo25_e = hslider("MM_LFO/24_lfo25_enable", 0.0, 0.0, 1.0, 1.0); - -lfo26_r = hslider("MM_LFO/25_lfo26_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo26_m = hslider("MM_LFO/25_lfo26_morph", 0.0, 0.0, 1.0, 0.001); -lfo26_e = hslider("MM_LFO/25_lfo26_enable", 0.0, 0.0, 1.0, 1.0); - -lfo27_r = hslider("MM_LFO/26_lfo27_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo27_m = hslider("MM_LFO/26_lfo27_morph", 0.0, 0.0, 1.0, 0.001); -lfo27_e = hslider("MM_LFO/26_lfo27_enable", 0.0, 0.0, 1.0, 1.0); - -lfo28_r = hslider("MM_LFO/27_lfo28_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo28_m = hslider("MM_LFO/27_lfo28_morph", 0.0, 0.0, 1.0, 0.001); -lfo28_e = hslider("MM_LFO/27_lfo28_enable", 0.0, 0.0, 1.0, 1.0); - -lfo29_r = hslider("MM_LFO/28_lfo29_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo29_m = hslider("MM_LFO/28_lfo29_morph", 0.0, 0.0, 1.0, 0.001); -lfo29_e = hslider("MM_LFO/28_lfo29_enable", 0.0, 0.0, 1.0, 1.0); - -lfo30_r = hslider("MM_LFO/29_lfo30_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo30_m = hslider("MM_LFO/29_lfo30_morph", 0.0, 0.0, 1.0, 0.001); -lfo30_e = hslider("MM_LFO/29_lfo30_enable", 0.0, 0.0, 1.0, 1.0); - -lfo31_r = hslider("MM_LFO/30_lfo31_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo31_m = hslider("MM_LFO/30_lfo31_morph", 0.0, 0.0, 1.0, 0.001); -lfo31_e = hslider("MM_LFO/30_lfo31_enable", 0.0, 0.0, 1.0, 1.0); - -lfo32_r = hslider("MM_LFO/31_lfo32_rate[scale:log]", 1.0, 0.01, 20.0, 0.001); -lfo32_m = hslider("MM_LFO/31_lfo32_morph", 0.0, 0.0, 1.0, 0.001); -lfo32_e = hslider("MM_LFO/31_lfo32_enable", 0.0, 0.0, 1.0, 1.0); - -// Wavemorph — 0..1 crossfades sine -> triangle -> square -> saw. -// Uses nested ba.if. Each shape is already in [-1,+1]. -wavemorph(rate, morph) = - ba.if(morph < 0.333, - (1.0 - morph*3.0) * os.osc(rate) + (morph*3.0) * os.lf_triangle(rate), - ba.if(morph < 0.666, - (1.0 - (morph-0.333)*3.0) * os.lf_triangle(rate) + ((morph-0.333)*3.0) * os.lf_squarewave(rate), - (1.0 - (morph-0.666)*3.0) * os.lf_squarewave(rate) + ((morph-0.666)*3.0) * os.lf_saw(rate))); - -lfo_slot(rate, morph, en_flag) = wavemorph(rate, morph) : *(en_flag); - -lfos = - lfo_slot(lfo01_r, lfo01_m, lfo01_e), - lfo_slot(lfo02_r, lfo02_m, lfo02_e), - lfo_slot(lfo03_r, lfo03_m, lfo03_e), - lfo_slot(lfo04_r, lfo04_m, lfo04_e), - lfo_slot(lfo05_r, lfo05_m, lfo05_e), - lfo_slot(lfo06_r, lfo06_m, lfo06_e), - lfo_slot(lfo07_r, lfo07_m, lfo07_e), - lfo_slot(lfo08_r, lfo08_m, lfo08_e), - lfo_slot(lfo09_r, lfo09_m, lfo09_e), - lfo_slot(lfo10_r, lfo10_m, lfo10_e), - lfo_slot(lfo11_r, lfo11_m, lfo11_e), - lfo_slot(lfo12_r, lfo12_m, lfo12_e), - lfo_slot(lfo13_r, lfo13_m, lfo13_e), - lfo_slot(lfo14_r, lfo14_m, lfo14_e), - lfo_slot(lfo15_r, lfo15_m, lfo15_e), - lfo_slot(lfo16_r, lfo16_m, lfo16_e), - lfo_slot(lfo17_r, lfo17_m, lfo17_e), - lfo_slot(lfo18_r, lfo18_m, lfo18_e), - lfo_slot(lfo19_r, lfo19_m, lfo19_e), - lfo_slot(lfo20_r, lfo20_m, lfo20_e), - lfo_slot(lfo21_r, lfo21_m, lfo21_e), - lfo_slot(lfo22_r, lfo22_m, lfo22_e), - lfo_slot(lfo23_r, lfo23_m, lfo23_e), - lfo_slot(lfo24_r, lfo24_m, lfo24_e), - lfo_slot(lfo25_r, lfo25_m, lfo25_e), - lfo_slot(lfo26_r, lfo26_m, lfo26_e), - lfo_slot(lfo27_r, lfo27_m, lfo27_e), - lfo_slot(lfo28_r, lfo28_m, lfo28_e), - lfo_slot(lfo29_r, lfo29_m, lfo29_e), - lfo_slot(lfo30_r, lfo30_m, lfo30_e), - lfo_slot(lfo31_r, lfo31_m, lfo31_e), - lfo_slot(lfo32_r, lfo32_m, lfo32_e); - -// --------------------------------------------------------------------------- -// sources(gate) — concatenated 48-wide mod-source bus. -// Order: adsr01..adsr16, lfo01..lfo32 -// --------------------------------------------------------------------------- - -sources(gate) = adsrs(gate), lfos; - -// --------------------------------------------------------------------------- -// Individual named source functions — 48 total, in canonical order. -// Engines can reference these directly in matrix sums, since each hslider -// inside a matrix amount must have a unique compile-time label. -// -// Canonical source ordering (source index 0..47): -// 0..15 : adsr01..adsr16 -// 16..47 : lfo01..lfo32 -// --------------------------------------------------------------------------- - -src00(gate) = adsr_slot(adsr01_a, adsr01_d, adsr01_s, adsr01_r, adsr01_e, gate); -src01(gate) = adsr_slot(adsr02_a, adsr02_d, adsr02_s, adsr02_r, adsr02_e, gate); -src02(gate) = adsr_slot(adsr03_a, adsr03_d, adsr03_s, adsr03_r, adsr03_e, gate); -src03(gate) = adsr_slot(adsr04_a, adsr04_d, adsr04_s, adsr04_r, adsr04_e, gate); -src04(gate) = adsr_slot(adsr05_a, adsr05_d, adsr05_s, adsr05_r, adsr05_e, gate); -src05(gate) = adsr_slot(adsr06_a, adsr06_d, adsr06_s, adsr06_r, adsr06_e, gate); -src06(gate) = adsr_slot(adsr07_a, adsr07_d, adsr07_s, adsr07_r, adsr07_e, gate); -src07(gate) = adsr_slot(adsr08_a, adsr08_d, adsr08_s, adsr08_r, adsr08_e, gate); -src08(gate) = adsr_slot(adsr09_a, adsr09_d, adsr09_s, adsr09_r, adsr09_e, gate); -src09(gate) = adsr_slot(adsr10_a, adsr10_d, adsr10_s, adsr10_r, adsr10_e, gate); -src10(gate) = adsr_slot(adsr11_a, adsr11_d, adsr11_s, adsr11_r, adsr11_e, gate); -src11(gate) = adsr_slot(adsr12_a, adsr12_d, adsr12_s, adsr12_r, adsr12_e, gate); -src12(gate) = adsr_slot(adsr13_a, adsr13_d, adsr13_s, adsr13_r, adsr13_e, gate); -src13(gate) = adsr_slot(adsr14_a, adsr14_d, adsr14_s, adsr14_r, adsr14_e, gate); -src14(gate) = adsr_slot(adsr15_a, adsr15_d, adsr15_s, adsr15_r, adsr15_e, gate); -src15(gate) = adsr_slot(adsr16_a, adsr16_d, adsr16_s, adsr16_r, adsr16_e, gate); - -src16(gate) = lfo_slot(lfo01_r, lfo01_m, lfo01_e); -src17(gate) = lfo_slot(lfo02_r, lfo02_m, lfo02_e); -src18(gate) = lfo_slot(lfo03_r, lfo03_m, lfo03_e); -src19(gate) = lfo_slot(lfo04_r, lfo04_m, lfo04_e); -src20(gate) = lfo_slot(lfo05_r, lfo05_m, lfo05_e); -src21(gate) = lfo_slot(lfo06_r, lfo06_m, lfo06_e); -src22(gate) = lfo_slot(lfo07_r, lfo07_m, lfo07_e); -src23(gate) = lfo_slot(lfo08_r, lfo08_m, lfo08_e); -src24(gate) = lfo_slot(lfo09_r, lfo09_m, lfo09_e); -src25(gate) = lfo_slot(lfo10_r, lfo10_m, lfo10_e); -src26(gate) = lfo_slot(lfo11_r, lfo11_m, lfo11_e); -src27(gate) = lfo_slot(lfo12_r, lfo12_m, lfo12_e); -src28(gate) = lfo_slot(lfo13_r, lfo13_m, lfo13_e); -src29(gate) = lfo_slot(lfo14_r, lfo14_m, lfo14_e); -src30(gate) = lfo_slot(lfo15_r, lfo15_m, lfo15_e); -src31(gate) = lfo_slot(lfo16_r, lfo16_m, lfo16_e); -src32(gate) = lfo_slot(lfo17_r, lfo17_m, lfo17_e); -src33(gate) = lfo_slot(lfo18_r, lfo18_m, lfo18_e); -src34(gate) = lfo_slot(lfo19_r, lfo19_m, lfo19_e); -src35(gate) = lfo_slot(lfo20_r, lfo20_m, lfo20_e); -src36(gate) = lfo_slot(lfo21_r, lfo21_m, lfo21_e); -src37(gate) = lfo_slot(lfo22_r, lfo22_m, lfo22_e); -src38(gate) = lfo_slot(lfo23_r, lfo23_m, lfo23_e); -src39(gate) = lfo_slot(lfo24_r, lfo24_m, lfo24_e); -src40(gate) = lfo_slot(lfo25_r, lfo25_m, lfo25_e); -src41(gate) = lfo_slot(lfo26_r, lfo26_m, lfo26_e); -src42(gate) = lfo_slot(lfo27_r, lfo27_m, lfo27_e); -src43(gate) = lfo_slot(lfo28_r, lfo28_m, lfo28_e); -src44(gate) = lfo_slot(lfo29_r, lfo29_m, lfo29_e); -src45(gate) = lfo_slot(lfo30_r, lfo30_m, lfo30_e); -src46(gate) = lfo_slot(lfo31_r, lfo31_m, lfo31_e); -src47(gate) = lfo_slot(lfo32_r, lfo32_m, lfo32_e); diff --git a/playground/faust/modular-additive-processor.js b/playground/faust/modular-additive-processor.js deleted file mode 100644 index 8048889..0000000 --- a/playground/faust/modular-additive-processor.js +++ /dev/null @@ -1,257 +0,0 @@ -/** - * modular-additive-processor.js — AudioWorklet processor for the - * "Modular" mode additive voice (modular-additive.dsp). - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads modular-subtractive.wasm compiled by `faust -lang wasm`. - * - * Param-index strategy: - * modular-additive.dsp has ~700 parameters (25 engine + 80 ADSR + 96 LFO - * + 480 matrix + 3 hidden). Hand-maintained zone tables (additive-processor - * style) do not scale. Instead we use the wasm-native JSON descriptor - * produced by `faust -lang wasm`, which has a numeric `index` field on each - * leaf item. That index is the direct memory offset that setParamValue - * expects as its zone argument — no sentinel scanning, no address strings. - * - * The main thread fetches modular-additive.json, parses it, and passes - * the UI tree to the worklet via the init message. - * - * Calling convention (faust -lang wasm, single-instance): - * init(dsp, sampleRate) dsp is always 0 - * compute(dsp, n, inputs, outputs) - * setParamValue(dsp, zone, value) zone = numeric memory address from JSON - * getParamValue(dsp, zone) -> float - * - * Hidden params (not in the engine's ML-controllable list): - * 0_Hidden/freq — set by noteOn - * 0_Hidden/gate — set by noteOn/noteOff (button, value 0/1) - * 0_Hidden/_vel — set by noteOn - */ - -// Must be imported in AudioWorkletGlobalScope — include faust-worklet-processor.js -// via audioCtx.audioWorklet.addModule() before this file. - -const DSP = 0; -const BLOCK_SIZE = 128; - -class ModularAdditiveProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dspInst = null; - this._dspMemory = null; // live Float32Array view - this._sampleRate = 48000; - - // Zone tables, populated once the init message has been processed. - this._paramZones = []; // NISPS index (order from JSON walk) -> zone - this._paramZonesByLabel = {}; // label -> zone (for smoke test) - this._hiddenZones = {}; // {freq, gate, _vel} -> zone - - // Audio output buffer pointers - this._outPtrsAddr = 0; - this._outLAddr = 0; - this._outRAddr = 0; - } - - // --------------------------------------------------------------------------- - // _initWasm — called by FaustWorkletProcessor.handleMessage on 'init'. - // - // Extra fields on the init message: - // uiJson — parsed Faust UI descriptor (the .json file contents) for - // zone discovery. Faust's -lang wasm embeds numeric `index` - // fields that we use directly as zone memory addresses. - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sampleRate, uiJson) { - this._sampleRate = sampleRate; - - const importObj = { - env: { - _sinf: Math.sin, - _cosf: Math.cos, - _tanf: Math.tan, - _expf: Math.exp, - _logf: Math.log, - _log10f: Math.log10, - _powf: Math.pow, - _tanhf: Math.tanh, - _sqrtf: Math.sqrt, - _fabsf: Math.abs, - _floorf: Math.floor, - _ceilf: Math.ceil, - _remainderf: (a, b) => a - Math.round(a / b) * b, - _fmodf: (a, b) => a % b, - _roundf: Math.round, - _truncf: Math.trunc, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - }, - }; - - const result = await WebAssembly.instantiate(wasmBytes, importObj); - this._dspInst = result.instance; - const ex = this._dspInst.exports; - - // Grow memory by 2 pages (128 KB) so the output buffer region sits past - // Faust's allocated parameter zones. After growth we have at least - // 3 * BLOCK_SIZE * 4 bytes of scratch at the high end of memory. - ex.memory.grow(2); - - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Initialise the DSP instance (single-instance convention: dsp = 0) - ex.init(DSP, sampleRate); - - // Allocate output buffers at the top of WASM memory. - const memBytes = ex.memory.buffer.byteLength; - this._outLAddr = memBytes - BLOCK_SIZE * 4 * 3; - this._outRAddr = this._outLAddr + BLOCK_SIZE * 4; - this._outPtrsAddr = this._outRAddr + BLOCK_SIZE * 4; - - const u32 = new Uint32Array(ex.memory.buffer); - u32[this._outPtrsAddr / 4] = this._outLAddr; - u32[this._outPtrsAddr / 4 + 1] = this._outRAddr; - - // Refresh the float view after any potential reallocation - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Build the zone index from the JSON we were passed. - if (uiJson) { - this._buildZoneIndexFromJson(uiJson); - } else { - console.warn('[ModularAdditiveProcessor] no uiJson in init message — ' + - 'setParam will silently no-op.'); - } - } - - // ------------------------------------------------------------------------- - // _buildZoneIndexFromJson — walk the Faust UI tree and collect numeric - // zone indexes, separating hidden params from the main NISPS-controllable - // list. Preserves the UI tree's traversal order, which matches the order - // the playground's faustJsonToParamMeta() parser will produce. - // ------------------------------------------------------------------------- - - _buildZoneIndexFromJson(faustJson) { - const HIDDEN_TAIL = new Set(['freq', 'gate', '_vel']); - const zones = []; - const byLabel = {}; - const hidden = {}; - - const walk = (items) => { - if (!Array.isArray(items)) return; - for (const item of items) { - const type = item?.type; - if (type === 'hslider' || type === 'vslider' || - type === 'nentry' || type === 'button' || - type === 'checkbox') { - const label = item.label ?? ''; - const idx = item.index; - if (typeof idx !== 'number') continue; - - const hasHiddenMeta = Array.isArray(item.meta) && - item.meta.some(m => m.hidden === '1' || m.hidden === 1); - const tail = label.includes('/') ? label.split('/').pop() : label; - - if (hasHiddenMeta || HIDDEN_TAIL.has(tail)) { - hidden[tail] = idx; - } else { - zones.push(idx); - byLabel[label] = idx; - } - } else if (item?.items) { - walk(item.items); - } - } - }; - - walk(faustJson.ui ?? []); - - this._paramZones = zones; - this._paramZonesByLabel = byLabel; - this._hiddenZones = hidden; - } - - // ------------------------------------------------------------------------- - // _onSetParam — look up the zone by NISPS index and poke the DSP - // ------------------------------------------------------------------------- - - _onSetParam(index, value) { - if (!this._dspInst) return; - const zone = this._paramZones[index]; - if (zone === undefined) return; - this._dspInst.exports.setParamValue(DSP, zone, value); - } - - _onNoteOn(freq, vel) { - if (!this._dspInst) return; - const ex = this._dspInst.exports; - const z = this._hiddenZones; - if (z.freq !== undefined) ex.setParamValue(DSP, z.freq, freq); - if (z._vel !== undefined) ex.setParamValue(DSP, z._vel, vel ?? 0.7); - if (z.gate !== undefined) ex.setParamValue(DSP, z.gate, 1.0); - } - - _onNoteOff(_freq) { - if (!this._dspInst) return; - const z = this._hiddenZones; - if (z.gate !== undefined) { - this._dspInst.exports.setParamValue(DSP, z.gate, 0.0); - } - } - - // ------------------------------------------------------------------------- - // Extended message handling — override for setByLabel + init-with-json - // ------------------------------------------------------------------------- - - _handleMessage(msg) { - if (!msg || !msg.type) return; - - if (msg.type === 'init') { - // Override the base class's init path so we can thread `uiJson` through. - this._initWasm(msg.wasmBytes, msg.sampleRate || sampleRate, msg.uiJson) - .then(() => { - this._ready = true; - this.port.postMessage({ type: 'ready' }); - this._drainPendingMessages(); - }) - .catch((err) => { - this.port.postMessage({ type: 'error', message: String(err) }); - }); - return; - } - - // Buffer anything that arrives before init() finishes. See - // faust-worklet-processor.js._queueIfNotReady. - if (this._queueIfNotReady(msg)) return; - - if (msg.type === 'setByLabel') { - const zone = this._paramZonesByLabel[msg.label]; - if (zone !== undefined) { - this._dspInst.exports.setParamValue(DSP, zone, msg.value); - } - return; - } - - // Delegate everything else (setParam, noteOn, noteOff) to the base class. - super._handleMessage(msg); - } - - // ------------------------------------------------------------------------- - // _renderBlock — call Faust compute, copy output buffers - // ------------------------------------------------------------------------- - - _renderBlock(outL, outR, blockSize) { - if (!this._dspInst || !this._dspMemory) return; - - const ex = this._dspInst.exports; - ex.compute(DSP, blockSize, 0, this._outPtrsAddr); - - const wL = new Float32Array(ex.memory.buffer, this._outLAddr, blockSize); - const wR = new Float32Array(ex.memory.buffer, this._outRAddr, blockSize); - outL.set(wL); - outR.set(wR); - } -} - -registerProcessor('modular-additive-processor', ModularAdditiveProcessor); diff --git a/playground/faust/modular-additive.dsp b/playground/faust/modular-additive.dsp deleted file mode 100644 index 0c313b5..0000000 --- a/playground/faust/modular-additive.dsp +++ /dev/null @@ -1,1212 +0,0 @@ -// modular-additive.dsp — 64-partial additive voice for the "Modular" mode. -// -// AUTO-GENERATED by gen-modular-dsp.py. DO NOT EDIT BY HAND. -// Regenerate with: -// cd playground/faust && python3 gen-modular-dsp.py -// -// Audio path: 64 harmonic sines with spectral descriptors (tilt, odd/even, formants). -// All envelopes and LFOs come from mod-pool.lib via a 48-source × 10-dest -// modulation matrix. No internal envs/LFOs. -// -// Modulation destinations (10): -// 0 pitch semitones, applied to the fundamental frequency -// 1 bright high-partial group amplitude boost/cut (replaces bright env) -// 2 tilt added to spectral_tilt knob -// 3 inharmonicity added to inharmonicity knob -// 4 odd_even added to odd/even balance knob -// 5 formant_ctr shifts both formant centre frequencies (harmonic index) -// 6 formant_depth added to formant_depth knob -// 7 noise_mix added to noise_floor knob (clamped 0..1) -// 8 amp master amplitude dest (route an ADSR here for a VCA env) -// 9 pan stereo balance -// -// Hidden controls (driven by noteOn/noteOff, not by the ML engine): -// freq, gate, _vel -// -// Build: -// cd playground/faust && ./build.sh -// -// --------------------------------------------------------------------------- - -import("stdfaust.lib"); -mp = library("mod-pool.lib"); - -// --------------------------------------------------------------------------- -// Hidden controls -// --------------------------------------------------------------------------- -freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("0_Hidden/gate[hidden:1]"); -vel = hslider("0_Hidden/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 1 — Spectral Shape (harmonic bank + descriptors) -// --------------------------------------------------------------------------- -h1_amp = hslider("1_Spectral/00_h1_amp", 0.8, 0.0, 1.0, 0.001); -h2_amp = hslider("1_Spectral/01_h2_amp", 0.5, 0.0, 1.0, 0.001); -h3_amp = hslider("1_Spectral/02_h3_amp", 0.35, 0.0, 1.0, 0.001); -h4_amp = hslider("1_Spectral/03_h4_amp", 0.25, 0.0, 1.0, 0.001); -h5_amp = hslider("1_Spectral/04_h5_amp", 0.18, 0.0, 1.0, 0.001); -h6_amp = hslider("1_Spectral/05_h6_amp", 0.12, 0.0, 1.0, 0.001); -h7_amp = hslider("1_Spectral/06_h7_amp", 0.08, 0.0, 1.0, 0.001); -h8_amp = hslider("1_Spectral/07_h8_amp", 0.06, 0.0, 1.0, 0.001); -h9_16_amp = hslider("1_Spectral/08_h9_16_amp", 0.05, 0.0, 1.0, 0.001); -h17_32_amp = hslider("1_Spectral/09_h17_32_amp", 0.025, 0.0, 1.0, 0.001); -h33_64_amp = hslider("1_Spectral/10_h33_64_amp", 0.01, 0.0, 1.0, 0.001); -spectral_tilt = hslider("1_Spectral/11_spectral_tilt", 0.0, -1.0, 1.0, 0.001); -inharmonicity = hslider("1_Spectral/12_inharmonicity", 0.0, 0.0, 0.15, 0.0001); -odd_even = hslider("1_Spectral/13_odd_even", 0.5, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Formants & Noise -// --------------------------------------------------------------------------- -formant1_freq = hslider("2_Formants/00_formant1_freq[tooltip:Formant 1 harmonic idx]", 3.0, 1.0, 16.0, 0.01); -formant2_freq = hslider("2_Formants/01_formant2_freq[tooltip:Formant 2 harmonic idx]", 6.0, 1.0, 16.0, 0.01); -formant_depth = hslider("2_Formants/02_formant_depth", 0.0, 0.0, 1.0, 0.001); -noise_floor = hslider("2_Formants/03_noise_floor", 0.0, 0.0, 0.2, 0.001); -noise_color = hslider("2_Formants/04_noise_color", 0.5, 0.0, 1.0, 0.001); -sub_harmonic = hslider("2_Formants/05_sub_harmonic", 0.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Master -// --------------------------------------------------------------------------- -level = hslider("3_Master/00_level", 0.7, 0.0, 1.0, 0.001); -fine_tune = hslider("3_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -saturation = hslider("3_Master/02_saturation", 0.0, 0.0, 1.0, 0.001); -stereo_phase_spread= hslider("3_Master/03_stereo_phase_spread",0.1, 0.0, 1.0, 0.001); -master_pan = hslider("3_Master/04_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor — see subtractive engine for full explanation. -base_amp = hslider("3_Master/05_base_amp", 1.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Modulation matrix — 48 sources × 10 destinations. -// Each hslider is one "amount" entry in [-1, +1]. Zero = no connection. -// Group "MM_Matrix" so the paramMeta parser can recognise matrix params. -// --------------------------------------------------------------------------- - -// Matrix column: destination 0 = pitch -amt_s00_d00_pitch = hslider("MM_Matrix/s00_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s01_d00_pitch = hslider("MM_Matrix/s01_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s02_d00_pitch = hslider("MM_Matrix/s02_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s03_d00_pitch = hslider("MM_Matrix/s03_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s04_d00_pitch = hslider("MM_Matrix/s04_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s05_d00_pitch = hslider("MM_Matrix/s05_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s06_d00_pitch = hslider("MM_Matrix/s06_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s07_d00_pitch = hslider("MM_Matrix/s07_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s08_d00_pitch = hslider("MM_Matrix/s08_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s09_d00_pitch = hslider("MM_Matrix/s09_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s10_d00_pitch = hslider("MM_Matrix/s10_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s11_d00_pitch = hslider("MM_Matrix/s11_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s12_d00_pitch = hslider("MM_Matrix/s12_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s13_d00_pitch = hslider("MM_Matrix/s13_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s14_d00_pitch = hslider("MM_Matrix/s14_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s15_d00_pitch = hslider("MM_Matrix/s15_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s16_d00_pitch = hslider("MM_Matrix/s16_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s17_d00_pitch = hslider("MM_Matrix/s17_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s18_d00_pitch = hslider("MM_Matrix/s18_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s19_d00_pitch = hslider("MM_Matrix/s19_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s20_d00_pitch = hslider("MM_Matrix/s20_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s21_d00_pitch = hslider("MM_Matrix/s21_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s22_d00_pitch = hslider("MM_Matrix/s22_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s23_d00_pitch = hslider("MM_Matrix/s23_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s24_d00_pitch = hslider("MM_Matrix/s24_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s25_d00_pitch = hslider("MM_Matrix/s25_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s26_d00_pitch = hslider("MM_Matrix/s26_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s27_d00_pitch = hslider("MM_Matrix/s27_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s28_d00_pitch = hslider("MM_Matrix/s28_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s29_d00_pitch = hslider("MM_Matrix/s29_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s30_d00_pitch = hslider("MM_Matrix/s30_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s31_d00_pitch = hslider("MM_Matrix/s31_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s32_d00_pitch = hslider("MM_Matrix/s32_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s33_d00_pitch = hslider("MM_Matrix/s33_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s34_d00_pitch = hslider("MM_Matrix/s34_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s35_d00_pitch = hslider("MM_Matrix/s35_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s36_d00_pitch = hslider("MM_Matrix/s36_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s37_d00_pitch = hslider("MM_Matrix/s37_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s38_d00_pitch = hslider("MM_Matrix/s38_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s39_d00_pitch = hslider("MM_Matrix/s39_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s40_d00_pitch = hslider("MM_Matrix/s40_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s41_d00_pitch = hslider("MM_Matrix/s41_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s42_d00_pitch = hslider("MM_Matrix/s42_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s43_d00_pitch = hslider("MM_Matrix/s43_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s44_d00_pitch = hslider("MM_Matrix/s44_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s45_d00_pitch = hslider("MM_Matrix/s45_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s46_d00_pitch = hslider("MM_Matrix/s46_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s47_d00_pitch = hslider("MM_Matrix/s47_d00_pitch", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 1 = bright -amt_s00_d01_bright = hslider("MM_Matrix/s00_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s01_d01_bright = hslider("MM_Matrix/s01_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s02_d01_bright = hslider("MM_Matrix/s02_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s03_d01_bright = hslider("MM_Matrix/s03_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s04_d01_bright = hslider("MM_Matrix/s04_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s05_d01_bright = hslider("MM_Matrix/s05_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s06_d01_bright = hslider("MM_Matrix/s06_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s07_d01_bright = hslider("MM_Matrix/s07_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s08_d01_bright = hslider("MM_Matrix/s08_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s09_d01_bright = hslider("MM_Matrix/s09_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s10_d01_bright = hslider("MM_Matrix/s10_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s11_d01_bright = hslider("MM_Matrix/s11_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s12_d01_bright = hslider("MM_Matrix/s12_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s13_d01_bright = hslider("MM_Matrix/s13_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s14_d01_bright = hslider("MM_Matrix/s14_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s15_d01_bright = hslider("MM_Matrix/s15_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s16_d01_bright = hslider("MM_Matrix/s16_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s17_d01_bright = hslider("MM_Matrix/s17_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s18_d01_bright = hslider("MM_Matrix/s18_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s19_d01_bright = hslider("MM_Matrix/s19_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s20_d01_bright = hslider("MM_Matrix/s20_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s21_d01_bright = hslider("MM_Matrix/s21_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s22_d01_bright = hslider("MM_Matrix/s22_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s23_d01_bright = hslider("MM_Matrix/s23_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s24_d01_bright = hslider("MM_Matrix/s24_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s25_d01_bright = hslider("MM_Matrix/s25_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s26_d01_bright = hslider("MM_Matrix/s26_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s27_d01_bright = hslider("MM_Matrix/s27_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s28_d01_bright = hslider("MM_Matrix/s28_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s29_d01_bright = hslider("MM_Matrix/s29_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s30_d01_bright = hslider("MM_Matrix/s30_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s31_d01_bright = hslider("MM_Matrix/s31_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s32_d01_bright = hslider("MM_Matrix/s32_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s33_d01_bright = hslider("MM_Matrix/s33_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s34_d01_bright = hslider("MM_Matrix/s34_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s35_d01_bright = hslider("MM_Matrix/s35_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s36_d01_bright = hslider("MM_Matrix/s36_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s37_d01_bright = hslider("MM_Matrix/s37_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s38_d01_bright = hslider("MM_Matrix/s38_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s39_d01_bright = hslider("MM_Matrix/s39_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s40_d01_bright = hslider("MM_Matrix/s40_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s41_d01_bright = hslider("MM_Matrix/s41_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s42_d01_bright = hslider("MM_Matrix/s42_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s43_d01_bright = hslider("MM_Matrix/s43_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s44_d01_bright = hslider("MM_Matrix/s44_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s45_d01_bright = hslider("MM_Matrix/s45_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s46_d01_bright = hslider("MM_Matrix/s46_d01_bright", 0.0, -1.0, 1.0, 0.001); -amt_s47_d01_bright = hslider("MM_Matrix/s47_d01_bright", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 2 = tilt -amt_s00_d02_tilt = hslider("MM_Matrix/s00_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s01_d02_tilt = hslider("MM_Matrix/s01_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s02_d02_tilt = hslider("MM_Matrix/s02_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s03_d02_tilt = hslider("MM_Matrix/s03_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s04_d02_tilt = hslider("MM_Matrix/s04_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s05_d02_tilt = hslider("MM_Matrix/s05_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s06_d02_tilt = hslider("MM_Matrix/s06_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s07_d02_tilt = hslider("MM_Matrix/s07_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s08_d02_tilt = hslider("MM_Matrix/s08_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s09_d02_tilt = hslider("MM_Matrix/s09_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s10_d02_tilt = hslider("MM_Matrix/s10_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s11_d02_tilt = hslider("MM_Matrix/s11_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s12_d02_tilt = hslider("MM_Matrix/s12_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s13_d02_tilt = hslider("MM_Matrix/s13_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s14_d02_tilt = hslider("MM_Matrix/s14_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s15_d02_tilt = hslider("MM_Matrix/s15_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s16_d02_tilt = hslider("MM_Matrix/s16_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s17_d02_tilt = hslider("MM_Matrix/s17_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s18_d02_tilt = hslider("MM_Matrix/s18_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s19_d02_tilt = hslider("MM_Matrix/s19_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s20_d02_tilt = hslider("MM_Matrix/s20_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s21_d02_tilt = hslider("MM_Matrix/s21_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s22_d02_tilt = hslider("MM_Matrix/s22_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s23_d02_tilt = hslider("MM_Matrix/s23_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s24_d02_tilt = hslider("MM_Matrix/s24_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s25_d02_tilt = hslider("MM_Matrix/s25_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s26_d02_tilt = hslider("MM_Matrix/s26_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s27_d02_tilt = hslider("MM_Matrix/s27_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s28_d02_tilt = hslider("MM_Matrix/s28_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s29_d02_tilt = hslider("MM_Matrix/s29_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s30_d02_tilt = hslider("MM_Matrix/s30_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s31_d02_tilt = hslider("MM_Matrix/s31_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s32_d02_tilt = hslider("MM_Matrix/s32_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s33_d02_tilt = hslider("MM_Matrix/s33_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s34_d02_tilt = hslider("MM_Matrix/s34_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s35_d02_tilt = hslider("MM_Matrix/s35_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s36_d02_tilt = hslider("MM_Matrix/s36_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s37_d02_tilt = hslider("MM_Matrix/s37_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s38_d02_tilt = hslider("MM_Matrix/s38_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s39_d02_tilt = hslider("MM_Matrix/s39_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s40_d02_tilt = hslider("MM_Matrix/s40_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s41_d02_tilt = hslider("MM_Matrix/s41_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s42_d02_tilt = hslider("MM_Matrix/s42_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s43_d02_tilt = hslider("MM_Matrix/s43_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s44_d02_tilt = hslider("MM_Matrix/s44_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s45_d02_tilt = hslider("MM_Matrix/s45_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s46_d02_tilt = hslider("MM_Matrix/s46_d02_tilt", 0.0, -1.0, 1.0, 0.001); -amt_s47_d02_tilt = hslider("MM_Matrix/s47_d02_tilt", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 3 = inharmonicity -amt_s00_d03_inharmonicity = hslider("MM_Matrix/s00_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s01_d03_inharmonicity = hslider("MM_Matrix/s01_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s02_d03_inharmonicity = hslider("MM_Matrix/s02_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s03_d03_inharmonicity = hslider("MM_Matrix/s03_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s04_d03_inharmonicity = hslider("MM_Matrix/s04_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s05_d03_inharmonicity = hslider("MM_Matrix/s05_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s06_d03_inharmonicity = hslider("MM_Matrix/s06_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s07_d03_inharmonicity = hslider("MM_Matrix/s07_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s08_d03_inharmonicity = hslider("MM_Matrix/s08_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s09_d03_inharmonicity = hslider("MM_Matrix/s09_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s10_d03_inharmonicity = hslider("MM_Matrix/s10_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s11_d03_inharmonicity = hslider("MM_Matrix/s11_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s12_d03_inharmonicity = hslider("MM_Matrix/s12_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s13_d03_inharmonicity = hslider("MM_Matrix/s13_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s14_d03_inharmonicity = hslider("MM_Matrix/s14_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s15_d03_inharmonicity = hslider("MM_Matrix/s15_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s16_d03_inharmonicity = hslider("MM_Matrix/s16_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s17_d03_inharmonicity = hslider("MM_Matrix/s17_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s18_d03_inharmonicity = hslider("MM_Matrix/s18_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s19_d03_inharmonicity = hslider("MM_Matrix/s19_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s20_d03_inharmonicity = hslider("MM_Matrix/s20_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s21_d03_inharmonicity = hslider("MM_Matrix/s21_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s22_d03_inharmonicity = hslider("MM_Matrix/s22_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s23_d03_inharmonicity = hslider("MM_Matrix/s23_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s24_d03_inharmonicity = hslider("MM_Matrix/s24_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s25_d03_inharmonicity = hslider("MM_Matrix/s25_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s26_d03_inharmonicity = hslider("MM_Matrix/s26_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s27_d03_inharmonicity = hslider("MM_Matrix/s27_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s28_d03_inharmonicity = hslider("MM_Matrix/s28_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s29_d03_inharmonicity = hslider("MM_Matrix/s29_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s30_d03_inharmonicity = hslider("MM_Matrix/s30_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s31_d03_inharmonicity = hslider("MM_Matrix/s31_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s32_d03_inharmonicity = hslider("MM_Matrix/s32_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s33_d03_inharmonicity = hslider("MM_Matrix/s33_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s34_d03_inharmonicity = hslider("MM_Matrix/s34_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s35_d03_inharmonicity = hslider("MM_Matrix/s35_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s36_d03_inharmonicity = hslider("MM_Matrix/s36_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s37_d03_inharmonicity = hslider("MM_Matrix/s37_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s38_d03_inharmonicity = hslider("MM_Matrix/s38_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s39_d03_inharmonicity = hslider("MM_Matrix/s39_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s40_d03_inharmonicity = hslider("MM_Matrix/s40_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s41_d03_inharmonicity = hslider("MM_Matrix/s41_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s42_d03_inharmonicity = hslider("MM_Matrix/s42_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s43_d03_inharmonicity = hslider("MM_Matrix/s43_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s44_d03_inharmonicity = hslider("MM_Matrix/s44_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s45_d03_inharmonicity = hslider("MM_Matrix/s45_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s46_d03_inharmonicity = hslider("MM_Matrix/s46_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); -amt_s47_d03_inharmonicity = hslider("MM_Matrix/s47_d03_inharmonicity", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 4 = odd_even -amt_s00_d04_odd_even = hslider("MM_Matrix/s00_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s01_d04_odd_even = hslider("MM_Matrix/s01_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s02_d04_odd_even = hslider("MM_Matrix/s02_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s03_d04_odd_even = hslider("MM_Matrix/s03_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s04_d04_odd_even = hslider("MM_Matrix/s04_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s05_d04_odd_even = hslider("MM_Matrix/s05_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s06_d04_odd_even = hslider("MM_Matrix/s06_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s07_d04_odd_even = hslider("MM_Matrix/s07_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s08_d04_odd_even = hslider("MM_Matrix/s08_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s09_d04_odd_even = hslider("MM_Matrix/s09_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s10_d04_odd_even = hslider("MM_Matrix/s10_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s11_d04_odd_even = hslider("MM_Matrix/s11_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s12_d04_odd_even = hslider("MM_Matrix/s12_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s13_d04_odd_even = hslider("MM_Matrix/s13_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s14_d04_odd_even = hslider("MM_Matrix/s14_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s15_d04_odd_even = hslider("MM_Matrix/s15_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s16_d04_odd_even = hslider("MM_Matrix/s16_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s17_d04_odd_even = hslider("MM_Matrix/s17_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s18_d04_odd_even = hslider("MM_Matrix/s18_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s19_d04_odd_even = hslider("MM_Matrix/s19_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s20_d04_odd_even = hslider("MM_Matrix/s20_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s21_d04_odd_even = hslider("MM_Matrix/s21_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s22_d04_odd_even = hslider("MM_Matrix/s22_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s23_d04_odd_even = hslider("MM_Matrix/s23_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s24_d04_odd_even = hslider("MM_Matrix/s24_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s25_d04_odd_even = hslider("MM_Matrix/s25_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s26_d04_odd_even = hslider("MM_Matrix/s26_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s27_d04_odd_even = hslider("MM_Matrix/s27_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s28_d04_odd_even = hslider("MM_Matrix/s28_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s29_d04_odd_even = hslider("MM_Matrix/s29_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s30_d04_odd_even = hslider("MM_Matrix/s30_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s31_d04_odd_even = hslider("MM_Matrix/s31_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s32_d04_odd_even = hslider("MM_Matrix/s32_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s33_d04_odd_even = hslider("MM_Matrix/s33_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s34_d04_odd_even = hslider("MM_Matrix/s34_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s35_d04_odd_even = hslider("MM_Matrix/s35_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s36_d04_odd_even = hslider("MM_Matrix/s36_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s37_d04_odd_even = hslider("MM_Matrix/s37_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s38_d04_odd_even = hslider("MM_Matrix/s38_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s39_d04_odd_even = hslider("MM_Matrix/s39_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s40_d04_odd_even = hslider("MM_Matrix/s40_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s41_d04_odd_even = hslider("MM_Matrix/s41_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s42_d04_odd_even = hslider("MM_Matrix/s42_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s43_d04_odd_even = hslider("MM_Matrix/s43_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s44_d04_odd_even = hslider("MM_Matrix/s44_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s45_d04_odd_even = hslider("MM_Matrix/s45_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s46_d04_odd_even = hslider("MM_Matrix/s46_d04_odd_even", 0.0, -1.0, 1.0, 0.001); -amt_s47_d04_odd_even = hslider("MM_Matrix/s47_d04_odd_even", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 5 = formant_ctr -amt_s00_d05_formant_ctr = hslider("MM_Matrix/s00_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s01_d05_formant_ctr = hslider("MM_Matrix/s01_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s02_d05_formant_ctr = hslider("MM_Matrix/s02_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s03_d05_formant_ctr = hslider("MM_Matrix/s03_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s04_d05_formant_ctr = hslider("MM_Matrix/s04_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s05_d05_formant_ctr = hslider("MM_Matrix/s05_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s06_d05_formant_ctr = hslider("MM_Matrix/s06_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s07_d05_formant_ctr = hslider("MM_Matrix/s07_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s08_d05_formant_ctr = hslider("MM_Matrix/s08_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s09_d05_formant_ctr = hslider("MM_Matrix/s09_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s10_d05_formant_ctr = hslider("MM_Matrix/s10_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s11_d05_formant_ctr = hslider("MM_Matrix/s11_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s12_d05_formant_ctr = hslider("MM_Matrix/s12_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s13_d05_formant_ctr = hslider("MM_Matrix/s13_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s14_d05_formant_ctr = hslider("MM_Matrix/s14_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s15_d05_formant_ctr = hslider("MM_Matrix/s15_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s16_d05_formant_ctr = hslider("MM_Matrix/s16_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s17_d05_formant_ctr = hslider("MM_Matrix/s17_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s18_d05_formant_ctr = hslider("MM_Matrix/s18_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s19_d05_formant_ctr = hslider("MM_Matrix/s19_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s20_d05_formant_ctr = hslider("MM_Matrix/s20_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s21_d05_formant_ctr = hslider("MM_Matrix/s21_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s22_d05_formant_ctr = hslider("MM_Matrix/s22_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s23_d05_formant_ctr = hslider("MM_Matrix/s23_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s24_d05_formant_ctr = hslider("MM_Matrix/s24_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s25_d05_formant_ctr = hslider("MM_Matrix/s25_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s26_d05_formant_ctr = hslider("MM_Matrix/s26_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s27_d05_formant_ctr = hslider("MM_Matrix/s27_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s28_d05_formant_ctr = hslider("MM_Matrix/s28_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s29_d05_formant_ctr = hslider("MM_Matrix/s29_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s30_d05_formant_ctr = hslider("MM_Matrix/s30_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s31_d05_formant_ctr = hslider("MM_Matrix/s31_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s32_d05_formant_ctr = hslider("MM_Matrix/s32_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s33_d05_formant_ctr = hslider("MM_Matrix/s33_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s34_d05_formant_ctr = hslider("MM_Matrix/s34_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s35_d05_formant_ctr = hslider("MM_Matrix/s35_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s36_d05_formant_ctr = hslider("MM_Matrix/s36_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s37_d05_formant_ctr = hslider("MM_Matrix/s37_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s38_d05_formant_ctr = hslider("MM_Matrix/s38_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s39_d05_formant_ctr = hslider("MM_Matrix/s39_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s40_d05_formant_ctr = hslider("MM_Matrix/s40_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s41_d05_formant_ctr = hslider("MM_Matrix/s41_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s42_d05_formant_ctr = hslider("MM_Matrix/s42_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s43_d05_formant_ctr = hslider("MM_Matrix/s43_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s44_d05_formant_ctr = hslider("MM_Matrix/s44_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s45_d05_formant_ctr = hslider("MM_Matrix/s45_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s46_d05_formant_ctr = hslider("MM_Matrix/s46_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); -amt_s47_d05_formant_ctr = hslider("MM_Matrix/s47_d05_formant_ctr", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 6 = formant_depth -amt_s00_d06_formant_depth = hslider("MM_Matrix/s00_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s01_d06_formant_depth = hslider("MM_Matrix/s01_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s02_d06_formant_depth = hslider("MM_Matrix/s02_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s03_d06_formant_depth = hslider("MM_Matrix/s03_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s04_d06_formant_depth = hslider("MM_Matrix/s04_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s05_d06_formant_depth = hslider("MM_Matrix/s05_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s06_d06_formant_depth = hslider("MM_Matrix/s06_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s07_d06_formant_depth = hslider("MM_Matrix/s07_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s08_d06_formant_depth = hslider("MM_Matrix/s08_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s09_d06_formant_depth = hslider("MM_Matrix/s09_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s10_d06_formant_depth = hslider("MM_Matrix/s10_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s11_d06_formant_depth = hslider("MM_Matrix/s11_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s12_d06_formant_depth = hslider("MM_Matrix/s12_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s13_d06_formant_depth = hslider("MM_Matrix/s13_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s14_d06_formant_depth = hslider("MM_Matrix/s14_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s15_d06_formant_depth = hslider("MM_Matrix/s15_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s16_d06_formant_depth = hslider("MM_Matrix/s16_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s17_d06_formant_depth = hslider("MM_Matrix/s17_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s18_d06_formant_depth = hslider("MM_Matrix/s18_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s19_d06_formant_depth = hslider("MM_Matrix/s19_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s20_d06_formant_depth = hslider("MM_Matrix/s20_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s21_d06_formant_depth = hslider("MM_Matrix/s21_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s22_d06_formant_depth = hslider("MM_Matrix/s22_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s23_d06_formant_depth = hslider("MM_Matrix/s23_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s24_d06_formant_depth = hslider("MM_Matrix/s24_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s25_d06_formant_depth = hslider("MM_Matrix/s25_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s26_d06_formant_depth = hslider("MM_Matrix/s26_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s27_d06_formant_depth = hslider("MM_Matrix/s27_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s28_d06_formant_depth = hslider("MM_Matrix/s28_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s29_d06_formant_depth = hslider("MM_Matrix/s29_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s30_d06_formant_depth = hslider("MM_Matrix/s30_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s31_d06_formant_depth = hslider("MM_Matrix/s31_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s32_d06_formant_depth = hslider("MM_Matrix/s32_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s33_d06_formant_depth = hslider("MM_Matrix/s33_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s34_d06_formant_depth = hslider("MM_Matrix/s34_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s35_d06_formant_depth = hslider("MM_Matrix/s35_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s36_d06_formant_depth = hslider("MM_Matrix/s36_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s37_d06_formant_depth = hslider("MM_Matrix/s37_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s38_d06_formant_depth = hslider("MM_Matrix/s38_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s39_d06_formant_depth = hslider("MM_Matrix/s39_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s40_d06_formant_depth = hslider("MM_Matrix/s40_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s41_d06_formant_depth = hslider("MM_Matrix/s41_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s42_d06_formant_depth = hslider("MM_Matrix/s42_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s43_d06_formant_depth = hslider("MM_Matrix/s43_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s44_d06_formant_depth = hslider("MM_Matrix/s44_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s45_d06_formant_depth = hslider("MM_Matrix/s45_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s46_d06_formant_depth = hslider("MM_Matrix/s46_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); -amt_s47_d06_formant_depth = hslider("MM_Matrix/s47_d06_formant_depth", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 7 = noise_mix -amt_s00_d07_noise_mix = hslider("MM_Matrix/s00_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s01_d07_noise_mix = hslider("MM_Matrix/s01_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s02_d07_noise_mix = hslider("MM_Matrix/s02_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s03_d07_noise_mix = hslider("MM_Matrix/s03_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s04_d07_noise_mix = hslider("MM_Matrix/s04_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s05_d07_noise_mix = hslider("MM_Matrix/s05_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s06_d07_noise_mix = hslider("MM_Matrix/s06_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s07_d07_noise_mix = hslider("MM_Matrix/s07_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s08_d07_noise_mix = hslider("MM_Matrix/s08_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s09_d07_noise_mix = hslider("MM_Matrix/s09_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s10_d07_noise_mix = hslider("MM_Matrix/s10_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s11_d07_noise_mix = hslider("MM_Matrix/s11_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s12_d07_noise_mix = hslider("MM_Matrix/s12_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s13_d07_noise_mix = hslider("MM_Matrix/s13_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s14_d07_noise_mix = hslider("MM_Matrix/s14_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s15_d07_noise_mix = hslider("MM_Matrix/s15_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s16_d07_noise_mix = hslider("MM_Matrix/s16_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s17_d07_noise_mix = hslider("MM_Matrix/s17_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s18_d07_noise_mix = hslider("MM_Matrix/s18_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s19_d07_noise_mix = hslider("MM_Matrix/s19_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s20_d07_noise_mix = hslider("MM_Matrix/s20_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s21_d07_noise_mix = hslider("MM_Matrix/s21_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s22_d07_noise_mix = hslider("MM_Matrix/s22_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s23_d07_noise_mix = hslider("MM_Matrix/s23_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s24_d07_noise_mix = hslider("MM_Matrix/s24_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s25_d07_noise_mix = hslider("MM_Matrix/s25_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s26_d07_noise_mix = hslider("MM_Matrix/s26_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s27_d07_noise_mix = hslider("MM_Matrix/s27_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s28_d07_noise_mix = hslider("MM_Matrix/s28_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s29_d07_noise_mix = hslider("MM_Matrix/s29_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s30_d07_noise_mix = hslider("MM_Matrix/s30_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s31_d07_noise_mix = hslider("MM_Matrix/s31_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s32_d07_noise_mix = hslider("MM_Matrix/s32_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s33_d07_noise_mix = hslider("MM_Matrix/s33_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s34_d07_noise_mix = hslider("MM_Matrix/s34_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s35_d07_noise_mix = hslider("MM_Matrix/s35_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s36_d07_noise_mix = hslider("MM_Matrix/s36_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s37_d07_noise_mix = hslider("MM_Matrix/s37_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s38_d07_noise_mix = hslider("MM_Matrix/s38_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s39_d07_noise_mix = hslider("MM_Matrix/s39_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s40_d07_noise_mix = hslider("MM_Matrix/s40_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s41_d07_noise_mix = hslider("MM_Matrix/s41_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s42_d07_noise_mix = hslider("MM_Matrix/s42_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s43_d07_noise_mix = hslider("MM_Matrix/s43_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s44_d07_noise_mix = hslider("MM_Matrix/s44_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s45_d07_noise_mix = hslider("MM_Matrix/s45_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s46_d07_noise_mix = hslider("MM_Matrix/s46_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); -amt_s47_d07_noise_mix = hslider("MM_Matrix/s47_d07_noise_mix", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 8 = amp -amt_s00_d08_amp = hslider("MM_Matrix/s00_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s01_d08_amp = hslider("MM_Matrix/s01_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s02_d08_amp = hslider("MM_Matrix/s02_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s03_d08_amp = hslider("MM_Matrix/s03_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s04_d08_amp = hslider("MM_Matrix/s04_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s05_d08_amp = hslider("MM_Matrix/s05_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s06_d08_amp = hslider("MM_Matrix/s06_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s07_d08_amp = hslider("MM_Matrix/s07_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s08_d08_amp = hslider("MM_Matrix/s08_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s09_d08_amp = hslider("MM_Matrix/s09_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s10_d08_amp = hslider("MM_Matrix/s10_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s11_d08_amp = hslider("MM_Matrix/s11_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s12_d08_amp = hslider("MM_Matrix/s12_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s13_d08_amp = hslider("MM_Matrix/s13_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s14_d08_amp = hslider("MM_Matrix/s14_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s15_d08_amp = hslider("MM_Matrix/s15_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s16_d08_amp = hslider("MM_Matrix/s16_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s17_d08_amp = hslider("MM_Matrix/s17_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s18_d08_amp = hslider("MM_Matrix/s18_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s19_d08_amp = hslider("MM_Matrix/s19_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s20_d08_amp = hslider("MM_Matrix/s20_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s21_d08_amp = hslider("MM_Matrix/s21_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s22_d08_amp = hslider("MM_Matrix/s22_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s23_d08_amp = hslider("MM_Matrix/s23_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s24_d08_amp = hslider("MM_Matrix/s24_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s25_d08_amp = hslider("MM_Matrix/s25_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s26_d08_amp = hslider("MM_Matrix/s26_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s27_d08_amp = hslider("MM_Matrix/s27_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s28_d08_amp = hslider("MM_Matrix/s28_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s29_d08_amp = hslider("MM_Matrix/s29_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s30_d08_amp = hslider("MM_Matrix/s30_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s31_d08_amp = hslider("MM_Matrix/s31_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s32_d08_amp = hslider("MM_Matrix/s32_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s33_d08_amp = hslider("MM_Matrix/s33_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s34_d08_amp = hslider("MM_Matrix/s34_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s35_d08_amp = hslider("MM_Matrix/s35_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s36_d08_amp = hslider("MM_Matrix/s36_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s37_d08_amp = hslider("MM_Matrix/s37_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s38_d08_amp = hslider("MM_Matrix/s38_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s39_d08_amp = hslider("MM_Matrix/s39_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s40_d08_amp = hslider("MM_Matrix/s40_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s41_d08_amp = hslider("MM_Matrix/s41_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s42_d08_amp = hslider("MM_Matrix/s42_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s43_d08_amp = hslider("MM_Matrix/s43_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s44_d08_amp = hslider("MM_Matrix/s44_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s45_d08_amp = hslider("MM_Matrix/s45_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s46_d08_amp = hslider("MM_Matrix/s46_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s47_d08_amp = hslider("MM_Matrix/s47_d08_amp", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 9 = pan -amt_s00_d09_pan = hslider("MM_Matrix/s00_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s01_d09_pan = hslider("MM_Matrix/s01_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s02_d09_pan = hslider("MM_Matrix/s02_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s03_d09_pan = hslider("MM_Matrix/s03_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s04_d09_pan = hslider("MM_Matrix/s04_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s05_d09_pan = hslider("MM_Matrix/s05_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s06_d09_pan = hslider("MM_Matrix/s06_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s07_d09_pan = hslider("MM_Matrix/s07_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s08_d09_pan = hslider("MM_Matrix/s08_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s09_d09_pan = hslider("MM_Matrix/s09_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s10_d09_pan = hslider("MM_Matrix/s10_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s11_d09_pan = hslider("MM_Matrix/s11_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s12_d09_pan = hslider("MM_Matrix/s12_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s13_d09_pan = hslider("MM_Matrix/s13_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s14_d09_pan = hslider("MM_Matrix/s14_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s15_d09_pan = hslider("MM_Matrix/s15_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s16_d09_pan = hslider("MM_Matrix/s16_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s17_d09_pan = hslider("MM_Matrix/s17_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s18_d09_pan = hslider("MM_Matrix/s18_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s19_d09_pan = hslider("MM_Matrix/s19_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s20_d09_pan = hslider("MM_Matrix/s20_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s21_d09_pan = hslider("MM_Matrix/s21_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s22_d09_pan = hslider("MM_Matrix/s22_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s23_d09_pan = hslider("MM_Matrix/s23_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s24_d09_pan = hslider("MM_Matrix/s24_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s25_d09_pan = hslider("MM_Matrix/s25_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s26_d09_pan = hslider("MM_Matrix/s26_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s27_d09_pan = hslider("MM_Matrix/s27_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s28_d09_pan = hslider("MM_Matrix/s28_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s29_d09_pan = hslider("MM_Matrix/s29_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s30_d09_pan = hslider("MM_Matrix/s30_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s31_d09_pan = hslider("MM_Matrix/s31_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s32_d09_pan = hslider("MM_Matrix/s32_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s33_d09_pan = hslider("MM_Matrix/s33_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s34_d09_pan = hslider("MM_Matrix/s34_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s35_d09_pan = hslider("MM_Matrix/s35_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s36_d09_pan = hslider("MM_Matrix/s36_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s37_d09_pan = hslider("MM_Matrix/s37_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s38_d09_pan = hslider("MM_Matrix/s38_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s39_d09_pan = hslider("MM_Matrix/s39_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s40_d09_pan = hslider("MM_Matrix/s40_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s41_d09_pan = hslider("MM_Matrix/s41_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s42_d09_pan = hslider("MM_Matrix/s42_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s43_d09_pan = hslider("MM_Matrix/s43_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s44_d09_pan = hslider("MM_Matrix/s44_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s45_d09_pan = hslider("MM_Matrix/s45_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s46_d09_pan = hslider("MM_Matrix/s46_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s47_d09_pan = hslider("MM_Matrix/s47_d09_pan", 0.0, -1.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Destination signals — one per modulation destination. Each is a 48-term -// weighted sum of sources × amounts. The `gate` argument is threaded through -// so that mp.src??(gate) can drive ADSR envelopes. -// --------------------------------------------------------------------------- - -mod_pitch(gate) = - mp.src00(gate) * amt_s00_d00_pitch + - mp.src01(gate) * amt_s01_d00_pitch + - mp.src02(gate) * amt_s02_d00_pitch + - mp.src03(gate) * amt_s03_d00_pitch + - mp.src04(gate) * amt_s04_d00_pitch + - mp.src05(gate) * amt_s05_d00_pitch + - mp.src06(gate) * amt_s06_d00_pitch + - mp.src07(gate) * amt_s07_d00_pitch + - mp.src08(gate) * amt_s08_d00_pitch + - mp.src09(gate) * amt_s09_d00_pitch + - mp.src10(gate) * amt_s10_d00_pitch + - mp.src11(gate) * amt_s11_d00_pitch + - mp.src12(gate) * amt_s12_d00_pitch + - mp.src13(gate) * amt_s13_d00_pitch + - mp.src14(gate) * amt_s14_d00_pitch + - mp.src15(gate) * amt_s15_d00_pitch + - mp.src16(gate) * amt_s16_d00_pitch + - mp.src17(gate) * amt_s17_d00_pitch + - mp.src18(gate) * amt_s18_d00_pitch + - mp.src19(gate) * amt_s19_d00_pitch + - mp.src20(gate) * amt_s20_d00_pitch + - mp.src21(gate) * amt_s21_d00_pitch + - mp.src22(gate) * amt_s22_d00_pitch + - mp.src23(gate) * amt_s23_d00_pitch + - mp.src24(gate) * amt_s24_d00_pitch + - mp.src25(gate) * amt_s25_d00_pitch + - mp.src26(gate) * amt_s26_d00_pitch + - mp.src27(gate) * amt_s27_d00_pitch + - mp.src28(gate) * amt_s28_d00_pitch + - mp.src29(gate) * amt_s29_d00_pitch + - mp.src30(gate) * amt_s30_d00_pitch + - mp.src31(gate) * amt_s31_d00_pitch + - mp.src32(gate) * amt_s32_d00_pitch + - mp.src33(gate) * amt_s33_d00_pitch + - mp.src34(gate) * amt_s34_d00_pitch + - mp.src35(gate) * amt_s35_d00_pitch + - mp.src36(gate) * amt_s36_d00_pitch + - mp.src37(gate) * amt_s37_d00_pitch + - mp.src38(gate) * amt_s38_d00_pitch + - mp.src39(gate) * amt_s39_d00_pitch + - mp.src40(gate) * amt_s40_d00_pitch + - mp.src41(gate) * amt_s41_d00_pitch + - mp.src42(gate) * amt_s42_d00_pitch + - mp.src43(gate) * amt_s43_d00_pitch + - mp.src44(gate) * amt_s44_d00_pitch + - mp.src45(gate) * amt_s45_d00_pitch + - mp.src46(gate) * amt_s46_d00_pitch + - mp.src47(gate) * amt_s47_d00_pitch; - -mod_bright(gate) = - mp.src00(gate) * amt_s00_d01_bright + - mp.src01(gate) * amt_s01_d01_bright + - mp.src02(gate) * amt_s02_d01_bright + - mp.src03(gate) * amt_s03_d01_bright + - mp.src04(gate) * amt_s04_d01_bright + - mp.src05(gate) * amt_s05_d01_bright + - mp.src06(gate) * amt_s06_d01_bright + - mp.src07(gate) * amt_s07_d01_bright + - mp.src08(gate) * amt_s08_d01_bright + - mp.src09(gate) * amt_s09_d01_bright + - mp.src10(gate) * amt_s10_d01_bright + - mp.src11(gate) * amt_s11_d01_bright + - mp.src12(gate) * amt_s12_d01_bright + - mp.src13(gate) * amt_s13_d01_bright + - mp.src14(gate) * amt_s14_d01_bright + - mp.src15(gate) * amt_s15_d01_bright + - mp.src16(gate) * amt_s16_d01_bright + - mp.src17(gate) * amt_s17_d01_bright + - mp.src18(gate) * amt_s18_d01_bright + - mp.src19(gate) * amt_s19_d01_bright + - mp.src20(gate) * amt_s20_d01_bright + - mp.src21(gate) * amt_s21_d01_bright + - mp.src22(gate) * amt_s22_d01_bright + - mp.src23(gate) * amt_s23_d01_bright + - mp.src24(gate) * amt_s24_d01_bright + - mp.src25(gate) * amt_s25_d01_bright + - mp.src26(gate) * amt_s26_d01_bright + - mp.src27(gate) * amt_s27_d01_bright + - mp.src28(gate) * amt_s28_d01_bright + - mp.src29(gate) * amt_s29_d01_bright + - mp.src30(gate) * amt_s30_d01_bright + - mp.src31(gate) * amt_s31_d01_bright + - mp.src32(gate) * amt_s32_d01_bright + - mp.src33(gate) * amt_s33_d01_bright + - mp.src34(gate) * amt_s34_d01_bright + - mp.src35(gate) * amt_s35_d01_bright + - mp.src36(gate) * amt_s36_d01_bright + - mp.src37(gate) * amt_s37_d01_bright + - mp.src38(gate) * amt_s38_d01_bright + - mp.src39(gate) * amt_s39_d01_bright + - mp.src40(gate) * amt_s40_d01_bright + - mp.src41(gate) * amt_s41_d01_bright + - mp.src42(gate) * amt_s42_d01_bright + - mp.src43(gate) * amt_s43_d01_bright + - mp.src44(gate) * amt_s44_d01_bright + - mp.src45(gate) * amt_s45_d01_bright + - mp.src46(gate) * amt_s46_d01_bright + - mp.src47(gate) * amt_s47_d01_bright; - -mod_tilt(gate) = - mp.src00(gate) * amt_s00_d02_tilt + - mp.src01(gate) * amt_s01_d02_tilt + - mp.src02(gate) * amt_s02_d02_tilt + - mp.src03(gate) * amt_s03_d02_tilt + - mp.src04(gate) * amt_s04_d02_tilt + - mp.src05(gate) * amt_s05_d02_tilt + - mp.src06(gate) * amt_s06_d02_tilt + - mp.src07(gate) * amt_s07_d02_tilt + - mp.src08(gate) * amt_s08_d02_tilt + - mp.src09(gate) * amt_s09_d02_tilt + - mp.src10(gate) * amt_s10_d02_tilt + - mp.src11(gate) * amt_s11_d02_tilt + - mp.src12(gate) * amt_s12_d02_tilt + - mp.src13(gate) * amt_s13_d02_tilt + - mp.src14(gate) * amt_s14_d02_tilt + - mp.src15(gate) * amt_s15_d02_tilt + - mp.src16(gate) * amt_s16_d02_tilt + - mp.src17(gate) * amt_s17_d02_tilt + - mp.src18(gate) * amt_s18_d02_tilt + - mp.src19(gate) * amt_s19_d02_tilt + - mp.src20(gate) * amt_s20_d02_tilt + - mp.src21(gate) * amt_s21_d02_tilt + - mp.src22(gate) * amt_s22_d02_tilt + - mp.src23(gate) * amt_s23_d02_tilt + - mp.src24(gate) * amt_s24_d02_tilt + - mp.src25(gate) * amt_s25_d02_tilt + - mp.src26(gate) * amt_s26_d02_tilt + - mp.src27(gate) * amt_s27_d02_tilt + - mp.src28(gate) * amt_s28_d02_tilt + - mp.src29(gate) * amt_s29_d02_tilt + - mp.src30(gate) * amt_s30_d02_tilt + - mp.src31(gate) * amt_s31_d02_tilt + - mp.src32(gate) * amt_s32_d02_tilt + - mp.src33(gate) * amt_s33_d02_tilt + - mp.src34(gate) * amt_s34_d02_tilt + - mp.src35(gate) * amt_s35_d02_tilt + - mp.src36(gate) * amt_s36_d02_tilt + - mp.src37(gate) * amt_s37_d02_tilt + - mp.src38(gate) * amt_s38_d02_tilt + - mp.src39(gate) * amt_s39_d02_tilt + - mp.src40(gate) * amt_s40_d02_tilt + - mp.src41(gate) * amt_s41_d02_tilt + - mp.src42(gate) * amt_s42_d02_tilt + - mp.src43(gate) * amt_s43_d02_tilt + - mp.src44(gate) * amt_s44_d02_tilt + - mp.src45(gate) * amt_s45_d02_tilt + - mp.src46(gate) * amt_s46_d02_tilt + - mp.src47(gate) * amt_s47_d02_tilt; - -mod_inharmonicity(gate) = - mp.src00(gate) * amt_s00_d03_inharmonicity + - mp.src01(gate) * amt_s01_d03_inharmonicity + - mp.src02(gate) * amt_s02_d03_inharmonicity + - mp.src03(gate) * amt_s03_d03_inharmonicity + - mp.src04(gate) * amt_s04_d03_inharmonicity + - mp.src05(gate) * amt_s05_d03_inharmonicity + - mp.src06(gate) * amt_s06_d03_inharmonicity + - mp.src07(gate) * amt_s07_d03_inharmonicity + - mp.src08(gate) * amt_s08_d03_inharmonicity + - mp.src09(gate) * amt_s09_d03_inharmonicity + - mp.src10(gate) * amt_s10_d03_inharmonicity + - mp.src11(gate) * amt_s11_d03_inharmonicity + - mp.src12(gate) * amt_s12_d03_inharmonicity + - mp.src13(gate) * amt_s13_d03_inharmonicity + - mp.src14(gate) * amt_s14_d03_inharmonicity + - mp.src15(gate) * amt_s15_d03_inharmonicity + - mp.src16(gate) * amt_s16_d03_inharmonicity + - mp.src17(gate) * amt_s17_d03_inharmonicity + - mp.src18(gate) * amt_s18_d03_inharmonicity + - mp.src19(gate) * amt_s19_d03_inharmonicity + - mp.src20(gate) * amt_s20_d03_inharmonicity + - mp.src21(gate) * amt_s21_d03_inharmonicity + - mp.src22(gate) * amt_s22_d03_inharmonicity + - mp.src23(gate) * amt_s23_d03_inharmonicity + - mp.src24(gate) * amt_s24_d03_inharmonicity + - mp.src25(gate) * amt_s25_d03_inharmonicity + - mp.src26(gate) * amt_s26_d03_inharmonicity + - mp.src27(gate) * amt_s27_d03_inharmonicity + - mp.src28(gate) * amt_s28_d03_inharmonicity + - mp.src29(gate) * amt_s29_d03_inharmonicity + - mp.src30(gate) * amt_s30_d03_inharmonicity + - mp.src31(gate) * amt_s31_d03_inharmonicity + - mp.src32(gate) * amt_s32_d03_inharmonicity + - mp.src33(gate) * amt_s33_d03_inharmonicity + - mp.src34(gate) * amt_s34_d03_inharmonicity + - mp.src35(gate) * amt_s35_d03_inharmonicity + - mp.src36(gate) * amt_s36_d03_inharmonicity + - mp.src37(gate) * amt_s37_d03_inharmonicity + - mp.src38(gate) * amt_s38_d03_inharmonicity + - mp.src39(gate) * amt_s39_d03_inharmonicity + - mp.src40(gate) * amt_s40_d03_inharmonicity + - mp.src41(gate) * amt_s41_d03_inharmonicity + - mp.src42(gate) * amt_s42_d03_inharmonicity + - mp.src43(gate) * amt_s43_d03_inharmonicity + - mp.src44(gate) * amt_s44_d03_inharmonicity + - mp.src45(gate) * amt_s45_d03_inharmonicity + - mp.src46(gate) * amt_s46_d03_inharmonicity + - mp.src47(gate) * amt_s47_d03_inharmonicity; - -mod_odd_even(gate) = - mp.src00(gate) * amt_s00_d04_odd_even + - mp.src01(gate) * amt_s01_d04_odd_even + - mp.src02(gate) * amt_s02_d04_odd_even + - mp.src03(gate) * amt_s03_d04_odd_even + - mp.src04(gate) * amt_s04_d04_odd_even + - mp.src05(gate) * amt_s05_d04_odd_even + - mp.src06(gate) * amt_s06_d04_odd_even + - mp.src07(gate) * amt_s07_d04_odd_even + - mp.src08(gate) * amt_s08_d04_odd_even + - mp.src09(gate) * amt_s09_d04_odd_even + - mp.src10(gate) * amt_s10_d04_odd_even + - mp.src11(gate) * amt_s11_d04_odd_even + - mp.src12(gate) * amt_s12_d04_odd_even + - mp.src13(gate) * amt_s13_d04_odd_even + - mp.src14(gate) * amt_s14_d04_odd_even + - mp.src15(gate) * amt_s15_d04_odd_even + - mp.src16(gate) * amt_s16_d04_odd_even + - mp.src17(gate) * amt_s17_d04_odd_even + - mp.src18(gate) * amt_s18_d04_odd_even + - mp.src19(gate) * amt_s19_d04_odd_even + - mp.src20(gate) * amt_s20_d04_odd_even + - mp.src21(gate) * amt_s21_d04_odd_even + - mp.src22(gate) * amt_s22_d04_odd_even + - mp.src23(gate) * amt_s23_d04_odd_even + - mp.src24(gate) * amt_s24_d04_odd_even + - mp.src25(gate) * amt_s25_d04_odd_even + - mp.src26(gate) * amt_s26_d04_odd_even + - mp.src27(gate) * amt_s27_d04_odd_even + - mp.src28(gate) * amt_s28_d04_odd_even + - mp.src29(gate) * amt_s29_d04_odd_even + - mp.src30(gate) * amt_s30_d04_odd_even + - mp.src31(gate) * amt_s31_d04_odd_even + - mp.src32(gate) * amt_s32_d04_odd_even + - mp.src33(gate) * amt_s33_d04_odd_even + - mp.src34(gate) * amt_s34_d04_odd_even + - mp.src35(gate) * amt_s35_d04_odd_even + - mp.src36(gate) * amt_s36_d04_odd_even + - mp.src37(gate) * amt_s37_d04_odd_even + - mp.src38(gate) * amt_s38_d04_odd_even + - mp.src39(gate) * amt_s39_d04_odd_even + - mp.src40(gate) * amt_s40_d04_odd_even + - mp.src41(gate) * amt_s41_d04_odd_even + - mp.src42(gate) * amt_s42_d04_odd_even + - mp.src43(gate) * amt_s43_d04_odd_even + - mp.src44(gate) * amt_s44_d04_odd_even + - mp.src45(gate) * amt_s45_d04_odd_even + - mp.src46(gate) * amt_s46_d04_odd_even + - mp.src47(gate) * amt_s47_d04_odd_even; - -mod_formant_ctr(gate) = - mp.src00(gate) * amt_s00_d05_formant_ctr + - mp.src01(gate) * amt_s01_d05_formant_ctr + - mp.src02(gate) * amt_s02_d05_formant_ctr + - mp.src03(gate) * amt_s03_d05_formant_ctr + - mp.src04(gate) * amt_s04_d05_formant_ctr + - mp.src05(gate) * amt_s05_d05_formant_ctr + - mp.src06(gate) * amt_s06_d05_formant_ctr + - mp.src07(gate) * amt_s07_d05_formant_ctr + - mp.src08(gate) * amt_s08_d05_formant_ctr + - mp.src09(gate) * amt_s09_d05_formant_ctr + - mp.src10(gate) * amt_s10_d05_formant_ctr + - mp.src11(gate) * amt_s11_d05_formant_ctr + - mp.src12(gate) * amt_s12_d05_formant_ctr + - mp.src13(gate) * amt_s13_d05_formant_ctr + - mp.src14(gate) * amt_s14_d05_formant_ctr + - mp.src15(gate) * amt_s15_d05_formant_ctr + - mp.src16(gate) * amt_s16_d05_formant_ctr + - mp.src17(gate) * amt_s17_d05_formant_ctr + - mp.src18(gate) * amt_s18_d05_formant_ctr + - mp.src19(gate) * amt_s19_d05_formant_ctr + - mp.src20(gate) * amt_s20_d05_formant_ctr + - mp.src21(gate) * amt_s21_d05_formant_ctr + - mp.src22(gate) * amt_s22_d05_formant_ctr + - mp.src23(gate) * amt_s23_d05_formant_ctr + - mp.src24(gate) * amt_s24_d05_formant_ctr + - mp.src25(gate) * amt_s25_d05_formant_ctr + - mp.src26(gate) * amt_s26_d05_formant_ctr + - mp.src27(gate) * amt_s27_d05_formant_ctr + - mp.src28(gate) * amt_s28_d05_formant_ctr + - mp.src29(gate) * amt_s29_d05_formant_ctr + - mp.src30(gate) * amt_s30_d05_formant_ctr + - mp.src31(gate) * amt_s31_d05_formant_ctr + - mp.src32(gate) * amt_s32_d05_formant_ctr + - mp.src33(gate) * amt_s33_d05_formant_ctr + - mp.src34(gate) * amt_s34_d05_formant_ctr + - mp.src35(gate) * amt_s35_d05_formant_ctr + - mp.src36(gate) * amt_s36_d05_formant_ctr + - mp.src37(gate) * amt_s37_d05_formant_ctr + - mp.src38(gate) * amt_s38_d05_formant_ctr + - mp.src39(gate) * amt_s39_d05_formant_ctr + - mp.src40(gate) * amt_s40_d05_formant_ctr + - mp.src41(gate) * amt_s41_d05_formant_ctr + - mp.src42(gate) * amt_s42_d05_formant_ctr + - mp.src43(gate) * amt_s43_d05_formant_ctr + - mp.src44(gate) * amt_s44_d05_formant_ctr + - mp.src45(gate) * amt_s45_d05_formant_ctr + - mp.src46(gate) * amt_s46_d05_formant_ctr + - mp.src47(gate) * amt_s47_d05_formant_ctr; - -mod_formant_depth(gate) = - mp.src00(gate) * amt_s00_d06_formant_depth + - mp.src01(gate) * amt_s01_d06_formant_depth + - mp.src02(gate) * amt_s02_d06_formant_depth + - mp.src03(gate) * amt_s03_d06_formant_depth + - mp.src04(gate) * amt_s04_d06_formant_depth + - mp.src05(gate) * amt_s05_d06_formant_depth + - mp.src06(gate) * amt_s06_d06_formant_depth + - mp.src07(gate) * amt_s07_d06_formant_depth + - mp.src08(gate) * amt_s08_d06_formant_depth + - mp.src09(gate) * amt_s09_d06_formant_depth + - mp.src10(gate) * amt_s10_d06_formant_depth + - mp.src11(gate) * amt_s11_d06_formant_depth + - mp.src12(gate) * amt_s12_d06_formant_depth + - mp.src13(gate) * amt_s13_d06_formant_depth + - mp.src14(gate) * amt_s14_d06_formant_depth + - mp.src15(gate) * amt_s15_d06_formant_depth + - mp.src16(gate) * amt_s16_d06_formant_depth + - mp.src17(gate) * amt_s17_d06_formant_depth + - mp.src18(gate) * amt_s18_d06_formant_depth + - mp.src19(gate) * amt_s19_d06_formant_depth + - mp.src20(gate) * amt_s20_d06_formant_depth + - mp.src21(gate) * amt_s21_d06_formant_depth + - mp.src22(gate) * amt_s22_d06_formant_depth + - mp.src23(gate) * amt_s23_d06_formant_depth + - mp.src24(gate) * amt_s24_d06_formant_depth + - mp.src25(gate) * amt_s25_d06_formant_depth + - mp.src26(gate) * amt_s26_d06_formant_depth + - mp.src27(gate) * amt_s27_d06_formant_depth + - mp.src28(gate) * amt_s28_d06_formant_depth + - mp.src29(gate) * amt_s29_d06_formant_depth + - mp.src30(gate) * amt_s30_d06_formant_depth + - mp.src31(gate) * amt_s31_d06_formant_depth + - mp.src32(gate) * amt_s32_d06_formant_depth + - mp.src33(gate) * amt_s33_d06_formant_depth + - mp.src34(gate) * amt_s34_d06_formant_depth + - mp.src35(gate) * amt_s35_d06_formant_depth + - mp.src36(gate) * amt_s36_d06_formant_depth + - mp.src37(gate) * amt_s37_d06_formant_depth + - mp.src38(gate) * amt_s38_d06_formant_depth + - mp.src39(gate) * amt_s39_d06_formant_depth + - mp.src40(gate) * amt_s40_d06_formant_depth + - mp.src41(gate) * amt_s41_d06_formant_depth + - mp.src42(gate) * amt_s42_d06_formant_depth + - mp.src43(gate) * amt_s43_d06_formant_depth + - mp.src44(gate) * amt_s44_d06_formant_depth + - mp.src45(gate) * amt_s45_d06_formant_depth + - mp.src46(gate) * amt_s46_d06_formant_depth + - mp.src47(gate) * amt_s47_d06_formant_depth; - -mod_noise_mix(gate) = - mp.src00(gate) * amt_s00_d07_noise_mix + - mp.src01(gate) * amt_s01_d07_noise_mix + - mp.src02(gate) * amt_s02_d07_noise_mix + - mp.src03(gate) * amt_s03_d07_noise_mix + - mp.src04(gate) * amt_s04_d07_noise_mix + - mp.src05(gate) * amt_s05_d07_noise_mix + - mp.src06(gate) * amt_s06_d07_noise_mix + - mp.src07(gate) * amt_s07_d07_noise_mix + - mp.src08(gate) * amt_s08_d07_noise_mix + - mp.src09(gate) * amt_s09_d07_noise_mix + - mp.src10(gate) * amt_s10_d07_noise_mix + - mp.src11(gate) * amt_s11_d07_noise_mix + - mp.src12(gate) * amt_s12_d07_noise_mix + - mp.src13(gate) * amt_s13_d07_noise_mix + - mp.src14(gate) * amt_s14_d07_noise_mix + - mp.src15(gate) * amt_s15_d07_noise_mix + - mp.src16(gate) * amt_s16_d07_noise_mix + - mp.src17(gate) * amt_s17_d07_noise_mix + - mp.src18(gate) * amt_s18_d07_noise_mix + - mp.src19(gate) * amt_s19_d07_noise_mix + - mp.src20(gate) * amt_s20_d07_noise_mix + - mp.src21(gate) * amt_s21_d07_noise_mix + - mp.src22(gate) * amt_s22_d07_noise_mix + - mp.src23(gate) * amt_s23_d07_noise_mix + - mp.src24(gate) * amt_s24_d07_noise_mix + - mp.src25(gate) * amt_s25_d07_noise_mix + - mp.src26(gate) * amt_s26_d07_noise_mix + - mp.src27(gate) * amt_s27_d07_noise_mix + - mp.src28(gate) * amt_s28_d07_noise_mix + - mp.src29(gate) * amt_s29_d07_noise_mix + - mp.src30(gate) * amt_s30_d07_noise_mix + - mp.src31(gate) * amt_s31_d07_noise_mix + - mp.src32(gate) * amt_s32_d07_noise_mix + - mp.src33(gate) * amt_s33_d07_noise_mix + - mp.src34(gate) * amt_s34_d07_noise_mix + - mp.src35(gate) * amt_s35_d07_noise_mix + - mp.src36(gate) * amt_s36_d07_noise_mix + - mp.src37(gate) * amt_s37_d07_noise_mix + - mp.src38(gate) * amt_s38_d07_noise_mix + - mp.src39(gate) * amt_s39_d07_noise_mix + - mp.src40(gate) * amt_s40_d07_noise_mix + - mp.src41(gate) * amt_s41_d07_noise_mix + - mp.src42(gate) * amt_s42_d07_noise_mix + - mp.src43(gate) * amt_s43_d07_noise_mix + - mp.src44(gate) * amt_s44_d07_noise_mix + - mp.src45(gate) * amt_s45_d07_noise_mix + - mp.src46(gate) * amt_s46_d07_noise_mix + - mp.src47(gate) * amt_s47_d07_noise_mix; - -mod_amp(gate) = - mp.src00(gate) * amt_s00_d08_amp + - mp.src01(gate) * amt_s01_d08_amp + - mp.src02(gate) * amt_s02_d08_amp + - mp.src03(gate) * amt_s03_d08_amp + - mp.src04(gate) * amt_s04_d08_amp + - mp.src05(gate) * amt_s05_d08_amp + - mp.src06(gate) * amt_s06_d08_amp + - mp.src07(gate) * amt_s07_d08_amp + - mp.src08(gate) * amt_s08_d08_amp + - mp.src09(gate) * amt_s09_d08_amp + - mp.src10(gate) * amt_s10_d08_amp + - mp.src11(gate) * amt_s11_d08_amp + - mp.src12(gate) * amt_s12_d08_amp + - mp.src13(gate) * amt_s13_d08_amp + - mp.src14(gate) * amt_s14_d08_amp + - mp.src15(gate) * amt_s15_d08_amp + - mp.src16(gate) * amt_s16_d08_amp + - mp.src17(gate) * amt_s17_d08_amp + - mp.src18(gate) * amt_s18_d08_amp + - mp.src19(gate) * amt_s19_d08_amp + - mp.src20(gate) * amt_s20_d08_amp + - mp.src21(gate) * amt_s21_d08_amp + - mp.src22(gate) * amt_s22_d08_amp + - mp.src23(gate) * amt_s23_d08_amp + - mp.src24(gate) * amt_s24_d08_amp + - mp.src25(gate) * amt_s25_d08_amp + - mp.src26(gate) * amt_s26_d08_amp + - mp.src27(gate) * amt_s27_d08_amp + - mp.src28(gate) * amt_s28_d08_amp + - mp.src29(gate) * amt_s29_d08_amp + - mp.src30(gate) * amt_s30_d08_amp + - mp.src31(gate) * amt_s31_d08_amp + - mp.src32(gate) * amt_s32_d08_amp + - mp.src33(gate) * amt_s33_d08_amp + - mp.src34(gate) * amt_s34_d08_amp + - mp.src35(gate) * amt_s35_d08_amp + - mp.src36(gate) * amt_s36_d08_amp + - mp.src37(gate) * amt_s37_d08_amp + - mp.src38(gate) * amt_s38_d08_amp + - mp.src39(gate) * amt_s39_d08_amp + - mp.src40(gate) * amt_s40_d08_amp + - mp.src41(gate) * amt_s41_d08_amp + - mp.src42(gate) * amt_s42_d08_amp + - mp.src43(gate) * amt_s43_d08_amp + - mp.src44(gate) * amt_s44_d08_amp + - mp.src45(gate) * amt_s45_d08_amp + - mp.src46(gate) * amt_s46_d08_amp + - mp.src47(gate) * amt_s47_d08_amp; - -mod_pan(gate) = - mp.src00(gate) * amt_s00_d09_pan + - mp.src01(gate) * amt_s01_d09_pan + - mp.src02(gate) * amt_s02_d09_pan + - mp.src03(gate) * amt_s03_d09_pan + - mp.src04(gate) * amt_s04_d09_pan + - mp.src05(gate) * amt_s05_d09_pan + - mp.src06(gate) * amt_s06_d09_pan + - mp.src07(gate) * amt_s07_d09_pan + - mp.src08(gate) * amt_s08_d09_pan + - mp.src09(gate) * amt_s09_d09_pan + - mp.src10(gate) * amt_s10_d09_pan + - mp.src11(gate) * amt_s11_d09_pan + - mp.src12(gate) * amt_s12_d09_pan + - mp.src13(gate) * amt_s13_d09_pan + - mp.src14(gate) * amt_s14_d09_pan + - mp.src15(gate) * amt_s15_d09_pan + - mp.src16(gate) * amt_s16_d09_pan + - mp.src17(gate) * amt_s17_d09_pan + - mp.src18(gate) * amt_s18_d09_pan + - mp.src19(gate) * amt_s19_d09_pan + - mp.src20(gate) * amt_s20_d09_pan + - mp.src21(gate) * amt_s21_d09_pan + - mp.src22(gate) * amt_s22_d09_pan + - mp.src23(gate) * amt_s23_d09_pan + - mp.src24(gate) * amt_s24_d09_pan + - mp.src25(gate) * amt_s25_d09_pan + - mp.src26(gate) * amt_s26_d09_pan + - mp.src27(gate) * amt_s27_d09_pan + - mp.src28(gate) * amt_s28_d09_pan + - mp.src29(gate) * amt_s29_d09_pan + - mp.src30(gate) * amt_s30_d09_pan + - mp.src31(gate) * amt_s31_d09_pan + - mp.src32(gate) * amt_s32_d09_pan + - mp.src33(gate) * amt_s33_d09_pan + - mp.src34(gate) * amt_s34_d09_pan + - mp.src35(gate) * amt_s35_d09_pan + - mp.src36(gate) * amt_s36_d09_pan + - mp.src37(gate) * amt_s37_d09_pan + - mp.src38(gate) * amt_s38_d09_pan + - mp.src39(gate) * amt_s39_d09_pan + - mp.src40(gate) * amt_s40_d09_pan + - mp.src41(gate) * amt_s41_d09_pan + - mp.src42(gate) * amt_s42_d09_pan + - mp.src43(gate) * amt_s43_d09_pan + - mp.src44(gate) * amt_s44_d09_pan + - mp.src45(gate) * amt_s45_d09_pan + - mp.src46(gate) * amt_s46_d09_pan + - mp.src47(gate) * amt_s47_d09_pan; - -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -N = 64; -PI = ma.PI; - -cent2ratio(c) = pow(2.0, c / 1200.0); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Pitch: modulation (semitones), no glide -fine_ratio = cent2ratio(fine_tune); -pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); -base_freq = freq * fine_ratio * pitch_ratio; - -// --------------------------------------------------------------------------- -// Per-harmonic base amplitude from group sliders -// --------------------------------------------------------------------------- -group_amp(k) = - ba.if(k == 1, h1_amp, - ba.if(k == 2, h2_amp, - ba.if(k == 3, h3_amp, - ba.if(k == 4, h4_amp, - ba.if(k == 5, h5_amp, - ba.if(k == 6, h6_amp, - ba.if(k == 7, h7_amp, - ba.if(k == 8, h8_amp, - ba.if(k <= 16, h9_16_amp, - ba.if(k <= 32, h17_32_amp, - h33_64_amp)))))))))); - -// Spectral tilt (modulated) — amp *= k^tilt -eff_tilt = spectral_tilt + mod_tilt(gate); -tilt_factor(k) = pow(float(k), eff_tilt); - -// Odd/even balance (modulated, clamped) -eff_odd_even = max(0.0, min(1.0, odd_even + mod_odd_even(gate))); -odd_weight = (1.0 - eff_odd_even) * 2.0; -even_weight = eff_odd_even * 2.0; -odd_even_factor(k) = ba.if(k % 2 == 0, even_weight, odd_weight); - -// Brightness destination: progressive boost/cut on upper partials. -// bright_blend(k): 0 at k=1, 1 at k=N. mod_bright is [-1,+1]. -// factor(k) = 1 + bright_blend(k) * mod_bright. At mod=0 -> unity (neutral). -bright_blend(k) = float(k - 1) / float(N - 1); -mb = mod_bright(gate); -bright_factor(k) = 1.0 + bright_blend(k) * mb; - -// Inharmonicity (modulated, clamped to keep partials monotonic) -eff_inharm = max(0.0, min(0.2, inharmonicity + mod_inharmonicity(gate) * 0.15)); - -// Formant centres (modulated together — mod_formant_ctr shifts both by same amt) -eff_formant_shift = mod_formant_ctr(gate) * 8.0; // ±8 harmonics -eff_f1 = max(1.0, min(32.0, formant1_freq + eff_formant_shift)); -eff_f2 = max(1.0, min(32.0, formant2_freq + eff_formant_shift)); -eff_formant_depth = max(0.0, min(1.0, formant_depth + mod_formant_depth(gate))); - -// Formant shaping — two Gaussian bumps in harmonic-index space -sigma_sq = 1.5 * 1.5; -formant_bump(k, ctr) = exp(-0.5 * (float(k) - ctr) * (float(k) - ctr) / sigma_sq); -formant_factor(k) = - 1.0 + eff_formant_depth * (formant_bump(k, eff_f1) + formant_bump(k, eff_f2)); - -// Combined per-harmonic amplitude -harm_amp(k) = - group_amp(k) * tilt_factor(k) * odd_even_factor(k) * - bright_factor(k) * formant_factor(k); - -// Inharmonic partial frequency: freq_k = k*f0*(1 + B*(k^2 - 1)) -harm_freq(k) = base_freq * float(k) - * (1.0 + eff_inharm * (float(k) * float(k) - 1.0)); - -// Stereo spread — R channel gets a small per-harmonic pitch offset -stereo_spread_freq(k) = stereo_phase_spread * float(k) * 0.01; - -// --------------------------------------------------------------------------- -// Additive oscillator sums — L and R -// --------------------------------------------------------------------------- -additive_L = sum(k, N, harm_amp(k+1) * os.osc(harm_freq(k+1))); -additive_R = sum(k, N, - harm_amp(k+1) * os.osc(harm_freq(k+1) * (1.0 + stereo_spread_freq(k+1)))); - -// Sub-harmonic (0.5× fundamental) -sub_osc = sub_harmonic * os.osc(base_freq * 0.5); - -// --------------------------------------------------------------------------- -// Noise floor — coloured via one-pole LP -// --------------------------------------------------------------------------- -eff_noise = max(0.0, min(1.0, noise_floor + mod_noise_mix(gate))); -noise_lp_cutoff = 200.0 + (1.0 - noise_color) * 19800.0; -noise_signal = no.noise : fi.lowpass(1, noise_lp_cutoff); -noise_out = eff_noise * noise_signal; - -// --------------------------------------------------------------------------- -// Soft-clip saturation — tanh waveshaper -// --------------------------------------------------------------------------- -drive = 1.0 + saturation * 9.0; -softclip(x) = ma.tanh(x * drive) / drive; - -// --------------------------------------------------------------------------- -// Velocity gain — keeps _vel alive in the JSON descriptor -// --------------------------------------------------------------------------- -vel_gain = (1.0 - 0.3) + 0.3 * vel; - -// --------------------------------------------------------------------------- -// Amplitude and pan (both mod-driven) -// --------------------------------------------------------------------------- -// See subtractive engine for the positive-only mod_amp rationale. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * level * vel_gain; -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); - -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -raw_L = (additive_L + sub_osc + noise_out) * amp_val; -raw_R = (additive_R + sub_osc + noise_out) * amp_val; - -signal_L = softclip(raw_L) * pan_l; -signal_R = softclip(raw_R) * pan_r; - -process = signal_L, signal_R; diff --git a/playground/faust/modular-additive.json b/playground/faust/modular-additive.json deleted file mode 100644 index 55dc0be..0000000 --- a/playground/faust/modular-additive.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "modular-additive","filename": "modular-additive.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn modular_additive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust/mod-pool.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/oscillators.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/envelopes.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/noises.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 266532,"code": "Ly8gbW9kdWxhci1hZGRpdGl2ZS5kc3Ag4oCUIDY0LXBhcnRpYWwgYWRkaXRpdmUgdm9pY2UgZm9yIHRoZSAiTW9kdWxhciIgbW9kZS4KLy8KLy8gQVVUTy1HRU5FUkFURUQgYnkgZ2VuLW1vZHVsYXItZHNwLnB5LiBETyBOT1QgRURJVCBCWSBIQU5ELgovLyBSZWdlbmVyYXRlIHdpdGg6Ci8vICAgICBjZCBwbGF5Z3JvdW5kL2ZhdXN0ICYmIHB5dGhvbjMgZ2VuLW1vZHVsYXItZHNwLnB5Ci8vCi8vIEF1ZGlvIHBhdGg6IDY0IGhhcm1vbmljIHNpbmVzIHdpdGggc3BlY3RyYWwgZGVzY3JpcHRvcnMgKHRpbHQsIG9kZC9ldmVuLCBmb3JtYW50cykuCi8vIEFsbCBlbnZlbG9wZXMgYW5kIExGT3MgY29tZSBmcm9tIG1vZC1wb29sLmxpYiB2aWEgYSA0OC1zb3VyY2Ugw5cgMTAtZGVzdAovLyBtb2R1bGF0aW9uIG1hdHJpeC4gTm8gaW50ZXJuYWwgZW52cy9MRk9zLgovLwovLyBNb2R1bGF0aW9uIGRlc3RpbmF0aW9ucyAoMTApOgovLyAgICAgMCBwaXRjaCAgICAgICAgICAgICBzZW1pdG9uZXMsIGFwcGxpZWQgdG8gdGhlIGZ1bmRhbWVudGFsIGZyZXF1ZW5jeQovLyAgICAgMSBicmlnaHQgICAgICAgICAgICBoaWdoLXBhcnRpYWwgZ3JvdXAgYW1wbGl0dWRlIGJvb3N0L2N1dCAocmVwbGFjZXMgYnJpZ2h0IGVudikKLy8gICAgIDIgdGlsdCAgICAgICAgICAgICAgYWRkZWQgdG8gc3BlY3RyYWxfdGlsdCBrbm9iCi8vICAgICAzIGluaGFybW9uaWNpdHkgICAgIGFkZGVkIHRvIGluaGFybW9uaWNpdHkga25vYgovLyAgICAgNCBvZGRfZXZlbiAgICAgICAgICBhZGRlZCB0byBvZGQvZXZlbiBiYWxhbmNlIGtub2IKLy8gICAgIDUgZm9ybWFudF9jdHIgICAgICAgc2hpZnRzIGJvdGggZm9ybWFudCBjZW50cmUgZnJlcXVlbmNpZXMgKGhhcm1vbmljIGluZGV4KQovLyAgICAgNiBmb3JtYW50X2RlcHRoICAgICBhZGRlZCB0byBmb3JtYW50X2RlcHRoIGtub2IKLy8gICAgIDcgbm9pc2VfbWl4ICAgICAgICAgYWRkZWQgdG8gbm9pc2VfZmxvb3Iga25vYiAoY2xhbXBlZCAwLi4xKQovLyAgICAgOCBhbXAgICAgICAgICAgICAgICBtYXN0ZXIgYW1wbGl0dWRlIGRlc3QgKHJvdXRlIGFuIEFEU1IgaGVyZSBmb3IgYSBWQ0EgZW52KQovLyAgICAgOSBwYW4gICAgICAgICAgICAgICBzdGVyZW8gYmFsYW5jZQovLwovLyBIaWRkZW4gY29udHJvbHMgKGRyaXZlbiBieSBub3RlT24vbm90ZU9mZiwgbm90IGJ5IHRoZSBNTCBlbmdpbmUpOgovLyAgICAgZnJlcSwgZ2F0ZSwgX3ZlbAovLwovLyBCdWlsZDoKLy8gICAgIGNkIHBsYXlncm91bmQvZmF1c3QgJiYgLi9idWlsZC5zaAovLwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmltcG9ydCgic3RkZmF1c3QubGliIik7Cm1wID0gbGlicmFyeSgibW9kLXBvb2wubGliIik7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gSGlkZGVuIGNvbnRyb2xzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmcmVxID0gaHNsaWRlcigiMF9IaWRkZW4vZnJlcVtoaWRkZW46MV1bdW5pdDpIel0iLCAyMjAsIDIwLCA0MDAwLCAwLjAxKTsKZ2F0ZSA9IGJ1dHRvbigiMF9IaWRkZW4vZ2F0ZVtoaWRkZW46MV0iKTsKdmVsICA9IGhzbGlkZXIoIjBfSGlkZGVuL192ZWxbaGlkZGVuOjFdIiwgMC43LCAwLjAsIDEuMCwgMC4wMDEpIDogc2kuc21vbzsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAxIOKAlCBTcGVjdHJhbCBTaGFwZSAoaGFybW9uaWMgYmFuayArIGRlc2NyaXB0b3JzKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KaDFfYW1wICAgICAgICA9IGhzbGlkZXIoIjFfU3BlY3RyYWwvMDBfaDFfYW1wIiwgICAgICAgIDAuOCwgICAwLjAsIDEuMCwgMC4wMDEpOwpoMl9hbXAgICAgICAgID0gaHNsaWRlcigiMV9TcGVjdHJhbC8wMV9oMl9hbXAiLCAgICAgICAgMC41LCAgIDAuMCwgMS4wLCAwLjAwMSk7CmgzX2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsLzAyX2gzX2FtcCIsICAgICAgICAwLjM1LCAgMC4wLCAxLjAsIDAuMDAxKTsKaDRfYW1wICAgICAgICA9IGhzbGlkZXIoIjFfU3BlY3RyYWwvMDNfaDRfYW1wIiwgICAgICAgIDAuMjUsICAwLjAsIDEuMCwgMC4wMDEpOwpoNV9hbXAgICAgICAgID0gaHNsaWRlcigiMV9TcGVjdHJhbC8wNF9oNV9hbXAiLCAgICAgICAgMC4xOCwgIDAuMCwgMS4wLCAwLjAwMSk7Cmg2X2FtcCAgICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsLzA1X2g2X2FtcCIsICAgICAgICAwLjEyLCAgMC4wLCAxLjAsIDAuMDAxKTsKaDdfYW1wICAgICAgICA9IGhzbGlkZXIoIjFfU3BlY3RyYWwvMDZfaDdfYW1wIiwgICAgICAgIDAuMDgsICAwLjAsIDEuMCwgMC4wMDEpOwpoOF9hbXAgICAgICAgID0gaHNsaWRlcigiMV9TcGVjdHJhbC8wN19oOF9hbXAiLCAgICAgICAgMC4wNiwgIDAuMCwgMS4wLCAwLjAwMSk7Cmg5XzE2X2FtcCAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsLzA4X2g5XzE2X2FtcCIsICAgICAwLjA1LCAgMC4wLCAxLjAsIDAuMDAxKTsKaDE3XzMyX2FtcCAgICA9IGhzbGlkZXIoIjFfU3BlY3RyYWwvMDlfaDE3XzMyX2FtcCIsICAgIDAuMDI1LCAwLjAsIDEuMCwgMC4wMDEpOwpoMzNfNjRfYW1wICAgID0gaHNsaWRlcigiMV9TcGVjdHJhbC8xMF9oMzNfNjRfYW1wIiwgICAgMC4wMSwgIDAuMCwgMS4wLCAwLjAwMSk7CnNwZWN0cmFsX3RpbHQgPSBoc2xpZGVyKCIxX1NwZWN0cmFsLzExX3NwZWN0cmFsX3RpbHQiLCAwLjAsICAtMS4wLCAxLjAsIDAuMDAxKTsKaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIjFfU3BlY3RyYWwvMTJfaW5oYXJtb25pY2l0eSIsIDAuMCwgICAwLjAsIDAuMTUsIDAuMDAwMSk7Cm9kZF9ldmVuICAgICAgPSBoc2xpZGVyKCIxX1NwZWN0cmFsLzEzX29kZF9ldmVuIiwgICAgICAwLjUsICAgMC4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAyIOKAlCBGb3JtYW50cyAmIE5vaXNlCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmb3JtYW50MV9mcmVxID0gaHNsaWRlcigiMl9Gb3JtYW50cy8wMF9mb3JtYW50MV9mcmVxW3Rvb2x0aXA6Rm9ybWFudCAxIGhhcm1vbmljIGlkeF0iLCAzLjAsIDEuMCwgMTYuMCwgMC4wMSk7CmZvcm1hbnQyX2ZyZXEgPSBoc2xpZGVyKCIyX0Zvcm1hbnRzLzAxX2Zvcm1hbnQyX2ZyZXFbdG9vbHRpcDpGb3JtYW50IDIgaGFybW9uaWMgaWR4XSIsIDYuMCwgMS4wLCAxNi4wLCAwLjAxKTsKZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIjJfRm9ybWFudHMvMDJfZm9ybWFudF9kZXB0aCIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKbm9pc2VfZmxvb3IgICA9IGhzbGlkZXIoIjJfRm9ybWFudHMvMDNfbm9pc2VfZmxvb3IiLCAgIDAuMCwgMC4wLCAwLjIsIDAuMDAxKTsKbm9pc2VfY29sb3IgICA9IGhzbGlkZXIoIjJfRm9ybWFudHMvMDRfbm9pc2VfY29sb3IiLCAgIDAuNSwgMC4wLCAxLjAsIDAuMDAxKTsKc3ViX2hhcm1vbmljICA9IGhzbGlkZXIoIjJfRm9ybWFudHMvMDVfc3ViX2hhcm1vbmljIiwgIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAzIOKAlCBNYXN0ZXIKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmxldmVsICAgICAgICAgICAgICA9IGhzbGlkZXIoIjNfTWFzdGVyLzAwX2xldmVsIiwgICAgICAgICAgICAgIDAuNywgIDAuMCwgMS4wLCAwLjAwMSk7CmZpbmVfdHVuZSAgICAgICAgICA9IGhzbGlkZXIoIjNfTWFzdGVyLzAxX2ZpbmVfdHVuZVt1bml0OmN0XSIsIDAuMCwgLTUwLjAsIDUwLjAsIDAuMSk7CnNhdHVyYXRpb24gICAgICAgICA9IGhzbGlkZXIoIjNfTWFzdGVyLzAyX3NhdHVyYXRpb24iLCAgICAgICAgIDAuMCwgIDAuMCwgMS4wLCAwLjAwMSk7CnN0ZXJlb19waGFzZV9zcHJlYWQ9IGhzbGlkZXIoIjNfTWFzdGVyLzAzX3N0ZXJlb19waGFzZV9zcHJlYWQiLDAuMSwgIDAuMCwgMS4wLCAwLjAwMSk7Cm1hc3Rlcl9wYW4gICAgICAgICA9IGhzbGlkZXIoIjNfTWFzdGVyLzA0X21hc3Rlcl9wYW4iLCAgICAgICAgIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7Ci8vIFN0YXRpYyBhbXAgZmxvb3Ig4oCUIHNlZSBzdWJ0cmFjdGl2ZSBlbmdpbmUgZm9yIGZ1bGwgZXhwbGFuYXRpb24uCmJhc2VfYW1wICAgICAgICAgICA9IGhzbGlkZXIoIjNfTWFzdGVyLzA1X2Jhc2VfYW1wIiwgICAgICAgICAgIDEuMCwgIDAuMCwgMS4wLCAwLjAwMSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gTW9kdWxhdGlvbiBtYXRyaXgg4oCUIDQ4IHNvdXJjZXMgw5cgMTAgZGVzdGluYXRpb25zLgovLyBFYWNoIGhzbGlkZXIgaXMgb25lICJhbW91bnQiIGVudHJ5IGluIFstMSwgKzFdLiBaZXJvID0gbm8gY29ubmVjdGlvbi4KLy8gR3JvdXAgIk1NX01hdHJpeCIgc28gdGhlIHBhcmFtTWV0YSBwYXJzZXIgY2FuIHJlY29nbmlzZSBtYXRyaXggcGFyYW1zLgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDAgPSBwaXRjaAphbXRfczAwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMSA9IGJyaWdodAphbXRfczAwX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwMV9icmlnaHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwMV9icmlnaHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMiA9IHRpbHQKYW10X3MwMF9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDJfdGlsdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDAyX3RpbHQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwMl90aWx0ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDJfdGlsdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDAyX3RpbHQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwMl90aWx0IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDMgPSBpbmhhcm1vbmljaXR5CmFtdF9zMDBfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDFfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDNfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDRfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDZfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDdfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDlfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTBfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTJfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTNfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTVfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTZfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMThfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTlfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjFfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjJfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjRfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjVfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjdfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjhfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzBfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzFfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzNfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzRfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzZfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzdfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzlfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDBfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDJfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDNfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDVfZDAzX2luaGFybW9uaWNpdHkgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwM19pbmhhcm1vbmljaXR5IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDNfaW5oYXJtb25pY2l0eSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDZfZDAzX2luaGFybW9uaWNpdHkiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwM19pbmhhcm1vbmljaXR5ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDNfaW5oYXJtb25pY2l0eSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CgovLyBNYXRyaXggY29sdW1uOiBkZXN0aW5hdGlvbiA0ID0gb2RkX2V2ZW4KYW10X3MwMF9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDFfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDJfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDRfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDVfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDdfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDhfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTBfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTFfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTNfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTRfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTZfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTdfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTlfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjBfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjJfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjNfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjVfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjZfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjhfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjlfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzFfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzJfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzRfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzVfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzdfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzhfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDBfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDFfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDNfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDRfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDRfb2RkX2V2ZW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwNF9vZGRfZXZlbiIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDZfZDA0X29kZF9ldmVuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDRfb2RkX2V2ZW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwNF9vZGRfZXZlbiA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDdfZDA0X29kZF9ldmVuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDUgPSBmb3JtYW50X2N0cgphbXRfczAwX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwNV9mb3JtYW50X2N0ciA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDA1X2Zvcm1hbnRfY3RyIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDVfZm9ybWFudF9jdHIgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwNV9mb3JtYW50X2N0ciIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDA1X2Zvcm1hbnRfY3RyID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDVfZm9ybWFudF9jdHIiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gNiA9IGZvcm1hbnRfZGVwdGgKYW10X3MwMF9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDZfZm9ybWFudF9kZXB0aCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDA2X2Zvcm1hbnRfZGVwdGgiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwNl9mb3JtYW50X2RlcHRoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDZfZm9ybWFudF9kZXB0aCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDA2X2Zvcm1hbnRfZGVwdGggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwNl9mb3JtYW50X2RlcHRoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDcgPSBub2lzZV9taXgKYW10X3MwMF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDdfbm9pc2VfbWl4ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDdfbm9pc2VfbWl4IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDggPSBhbXAKYW10X3MwMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDkgPSBwYW4KYW10X3MwMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBEZXN0aW5hdGlvbiBzaWduYWxzIOKAlCBvbmUgcGVyIG1vZHVsYXRpb24gZGVzdGluYXRpb24uIEVhY2ggaXMgYSA0OC10ZXJtCi8vIHdlaWdodGVkIHN1bSBvZiBzb3VyY2VzIMOXIGFtb3VudHMuIFRoZSBgZ2F0ZWAgYXJndW1lbnQgaXMgdGhyZWFkZWQgdGhyb3VnaAovLyBzbyB0aGF0IG1wLnNyYz8/KGdhdGUpIGNhbiBkcml2ZSBBRFNSIGVudmVsb3Blcy4KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgptb2RfcGl0Y2goZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwMF9waXRjaCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDAwX3BpdGNoICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwMF9waXRjaCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwMF9waXRjaCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwMF9waXRjaCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwMF9waXRjaCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwMF9waXRjaCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwMF9waXRjaCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDAwX3BpdGNoICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwMF9waXRjaCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDAwX3BpdGNoICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwMF9waXRjaCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwMF9waXRjaCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwMF9waXRjaCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDAwX3BpdGNoICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwMF9waXRjaCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwMF9waXRjaCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwMF9waXRjaCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwMF9waXRjaCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwMF9waXRjaCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDBfcGl0Y2g7Cgptb2RfYnJpZ2h0KGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDFfYnJpZ2h0ICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDFfYnJpZ2h0OwoKbW9kX3RpbHQoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwMl90aWx0ICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDJfdGlsdCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDAyX3RpbHQgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwMl90aWx0ICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDJfdGlsdCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDAyX3RpbHQgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwMl90aWx0ICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDJfdGlsdCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDAyX3RpbHQgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwMl90aWx0ICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDJfdGlsdCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDAyX3RpbHQgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwMl90aWx0ICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDJfdGlsdCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDAyX3RpbHQgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwMl90aWx0ICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDJfdGlsdCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDAyX3RpbHQgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwMl90aWx0ICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDJfdGlsdCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDAyX3RpbHQgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwMl90aWx0ICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDJfdGlsdCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDAyX3RpbHQgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwMl90aWx0ICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDJfdGlsdCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDAyX3RpbHQgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwMl90aWx0ICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDJfdGlsdCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDAyX3RpbHQgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwMl90aWx0ICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDJfdGlsdCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDAyX3RpbHQgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwMl90aWx0ICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDJfdGlsdCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDAyX3RpbHQgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwMl90aWx0ICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDJfdGlsdCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDAyX3RpbHQgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwMl90aWx0ICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDJfdGlsdCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDAyX3RpbHQgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwMl90aWx0ICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDJfdGlsdCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDAyX3RpbHQgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwMl90aWx0ICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDJfdGlsdCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDAyX3RpbHQ7Cgptb2RfaW5oYXJtb25pY2l0eShnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDNfaW5oYXJtb25pY2l0eSArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAzX2luaGFybW9uaWNpdHkgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwM19pbmhhcm1vbmljaXR5ICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDNfaW5oYXJtb25pY2l0eTsKCm1vZF9vZGRfZXZlbihnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwNF9vZGRfZXZlbiArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA0X29kZF9ldmVuICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDRfb2RkX2V2ZW4gKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwNF9vZGRfZXZlbjsKCm1vZF9mb3JtYW50X2N0cihnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwNV9mb3JtYW50X2N0ciArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA1X2Zvcm1hbnRfY3RyICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDVfZm9ybWFudF9jdHIgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwNV9mb3JtYW50X2N0cjsKCm1vZF9mb3JtYW50X2RlcHRoKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwNl9mb3JtYW50X2RlcHRoICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDZfZm9ybWFudF9kZXB0aCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDA2X2Zvcm1hbnRfZGVwdGggKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwNl9mb3JtYW50X2RlcHRoOwoKbW9kX25vaXNlX21peChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDA3X25vaXNlX21peCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDA3X25vaXNlX21peDsKCm1vZF9hbXAoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwOF9hbXAgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwOF9hbXAgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwOF9hbXAgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwOF9hbXAgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwOF9hbXAgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwOF9hbXAgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwOF9hbXAgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwOF9hbXAgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwOF9hbXAgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwOF9hbXAgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwOF9hbXAgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwOF9hbXAgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwOF9hbXAgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwOF9hbXAgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwOF9hbXAgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwOF9hbXAgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwOF9hbXAgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwOF9hbXAgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwOF9hbXAgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwOF9hbXAgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwOF9hbXAgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwOF9hbXAgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwOF9hbXAgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwOF9hbXAgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwOF9hbXAgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwOF9hbXAgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwOF9hbXAgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwOF9hbXAgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwOF9hbXAgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwOF9hbXAgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwOF9hbXAgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwOF9hbXAgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwOF9hbXAgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwOF9hbXAgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwOF9hbXAgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwOF9hbXAgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwOF9hbXAgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwOF9hbXAgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwOF9hbXAgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwOF9hbXAgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwOF9hbXAgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwOF9hbXAgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwOF9hbXAgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwOF9hbXAgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwOF9hbXAgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwOF9hbXAgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwOF9hbXAgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwOF9hbXA7Cgptb2RfcGFuKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDlfcGFuICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDlfcGFuICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDlfcGFuICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDlfcGFuICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDlfcGFuICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDlfcGFuICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDlfcGFuICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDlfcGFuICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDlfcGFuICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDlfcGFuICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDlfcGFuICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDlfcGFuICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDlfcGFuICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDlfcGFuICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDlfcGFuICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDlfcGFuICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDlfcGFuICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDlfcGFuICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDlfcGFuICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDlfcGFuICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDlfcGFuICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDlfcGFuICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDlfcGFuICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDlfcGFuICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDlfcGFuICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDlfcGFuICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDlfcGFuICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDlfcGFuICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDlfcGFuICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDlfcGFuICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDlfcGFuICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDlfcGFuICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDlfcGFuICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDlfcGFuICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDlfcGFuICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDlfcGFuICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDlfcGFuICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDlfcGFuICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDlfcGFuICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDlfcGFuICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDlfcGFuICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDlfcGFuICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDlfcGFuICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDlfcGFuICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDlfcGFuICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDlfcGFuICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDlfcGFuICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDlfcGFuOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIERlcml2ZWQgc2lnbmFscwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCk4gID0gNjQ7ClBJID0gbWEuUEk7CgpjZW50MnJhdGlvKGMpID0gcG93KDIuMCwgYyAvIDEyMDAuMCk7CnNlbWkycmF0aW8ocykgPSBwb3coMi4wLCBzIC8gMTIuMCk7CgovLyBQaXRjaDogbW9kdWxhdGlvbiAoc2VtaXRvbmVzKSwgbm8gZ2xpZGUKZmluZV9yYXRpbyAgPSBjZW50MnJhdGlvKGZpbmVfdHVuZSk7CnBpdGNoX3JhdGlvID0gc2VtaTJyYXRpbyhtb2RfcGl0Y2goZ2F0ZSkgKiAxMi4wKTsKYmFzZV9mcmVxICAgPSBmcmVxICogZmluZV9yYXRpbyAqIHBpdGNoX3JhdGlvOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFBlci1oYXJtb25pYyBiYXNlIGFtcGxpdHVkZSBmcm9tIGdyb3VwIHNsaWRlcnMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmdyb3VwX2FtcChrKSA9CiAgYmEuaWYoayA9PSAxLCBoMV9hbXAsCiAgYmEuaWYoayA9PSAyLCBoMl9hbXAsCiAgYmEuaWYoayA9PSAzLCBoM19hbXAsCiAgYmEuaWYoayA9PSA0LCBoNF9hbXAsCiAgYmEuaWYoayA9PSA1LCBoNV9hbXAsCiAgYmEuaWYoayA9PSA2LCBoNl9hbXAsCiAgYmEuaWYoayA9PSA3LCBoN19hbXAsCiAgYmEuaWYoayA9PSA4LCBoOF9hbXAsCiAgYmEuaWYoayA8PSAxNiwgaDlfMTZfYW1wLAogIGJhLmlmKGsgPD0gMzIsIGgxN18zMl9hbXAsCiAgICAgICAgICAgICAgICAgaDMzXzY0X2FtcCkpKSkpKSkpKSk7CgovLyBTcGVjdHJhbCB0aWx0IChtb2R1bGF0ZWQpIOKAlCBhbXAgKj0ga150aWx0CmVmZl90aWx0ID0gc3BlY3RyYWxfdGlsdCArIG1vZF90aWx0KGdhdGUpOwp0aWx0X2ZhY3RvcihrKSA9IHBvdyhmbG9hdChrKSwgZWZmX3RpbHQpOwoKLy8gT2RkL2V2ZW4gYmFsYW5jZSAobW9kdWxhdGVkLCBjbGFtcGVkKQplZmZfb2RkX2V2ZW4gID0gbWF4KDAuMCwgbWluKDEuMCwgb2RkX2V2ZW4gKyBtb2Rfb2RkX2V2ZW4oZ2F0ZSkpKTsKb2RkX3dlaWdodCAgICA9ICgxLjAgLSBlZmZfb2RkX2V2ZW4pICogMi4wOwpldmVuX3dlaWdodCAgID0gZWZmX29kZF9ldmVuICogMi4wOwpvZGRfZXZlbl9mYWN0b3IoaykgPSBiYS5pZihrICUgMiA9PSAwLCBldmVuX3dlaWdodCwgb2RkX3dlaWdodCk7CgovLyBCcmlnaHRuZXNzIGRlc3RpbmF0aW9uOiBwcm9ncmVzc2l2ZSBib29zdC9jdXQgb24gdXBwZXIgcGFydGlhbHMuCi8vIGJyaWdodF9ibGVuZChrKTogMCBhdCBrPTEsIDEgYXQgaz1OLiBtb2RfYnJpZ2h0IGlzIFstMSwrMV0uCi8vIGZhY3RvcihrKSA9IDEgKyBicmlnaHRfYmxlbmQoaykgKiBtb2RfYnJpZ2h0LiBBdCBtb2Q9MCAtPiB1bml0eSAobmV1dHJhbCkuCmJyaWdodF9ibGVuZChrKSAgPSBmbG9hdChrIC0gMSkgLyBmbG9hdChOIC0gMSk7Cm1iICAgICAgICAgICAgICAgPSBtb2RfYnJpZ2h0KGdhdGUpOwpicmlnaHRfZmFjdG9yKGspID0gMS4wICsgYnJpZ2h0X2JsZW5kKGspICogbWI7CgovLyBJbmhhcm1vbmljaXR5IChtb2R1bGF0ZWQsIGNsYW1wZWQgdG8ga2VlcCBwYXJ0aWFscyBtb25vdG9uaWMpCmVmZl9pbmhhcm0gICAgICAgPSBtYXgoMC4wLCBtaW4oMC4yLCBpbmhhcm1vbmljaXR5ICsgbW9kX2luaGFybW9uaWNpdHkoZ2F0ZSkgKiAwLjE1KSk7CgovLyBGb3JtYW50IGNlbnRyZXMgKG1vZHVsYXRlZCB0b2dldGhlciDigJQgbW9kX2Zvcm1hbnRfY3RyIHNoaWZ0cyBib3RoIGJ5IHNhbWUgYW10KQplZmZfZm9ybWFudF9zaGlmdCA9IG1vZF9mb3JtYW50X2N0cihnYXRlKSAqIDguMDsgICAgIC8vIMKxOCBoYXJtb25pY3MKZWZmX2YxICAgICAgICAgICAgPSBtYXgoMS4wLCBtaW4oMzIuMCwgZm9ybWFudDFfZnJlcSArIGVmZl9mb3JtYW50X3NoaWZ0KSk7CmVmZl9mMiAgICAgICAgICAgID0gbWF4KDEuMCwgbWluKDMyLjAsIGZvcm1hbnQyX2ZyZXEgKyBlZmZfZm9ybWFudF9zaGlmdCkpOwplZmZfZm9ybWFudF9kZXB0aCA9IG1heCgwLjAsIG1pbigxLjAsIGZvcm1hbnRfZGVwdGggKyBtb2RfZm9ybWFudF9kZXB0aChnYXRlKSkpOwoKLy8gRm9ybWFudCBzaGFwaW5nIOKAlCB0d28gR2F1c3NpYW4gYnVtcHMgaW4gaGFybW9uaWMtaW5kZXggc3BhY2UKc2lnbWFfc3EgPSAxLjUgKiAxLjU7CmZvcm1hbnRfYnVtcChrLCBjdHIpID0gZXhwKC0wLjUgKiAoZmxvYXQoaykgLSBjdHIpICogKGZsb2F0KGspIC0gY3RyKSAvIHNpZ21hX3NxKTsKZm9ybWFudF9mYWN0b3IoaykgPQogIDEuMCArIGVmZl9mb3JtYW50X2RlcHRoICogKGZvcm1hbnRfYnVtcChrLCBlZmZfZjEpICsgZm9ybWFudF9idW1wKGssIGVmZl9mMikpOwoKLy8gQ29tYmluZWQgcGVyLWhhcm1vbmljIGFtcGxpdHVkZQpoYXJtX2FtcChrKSA9CiAgZ3JvdXBfYW1wKGspICogdGlsdF9mYWN0b3IoaykgKiBvZGRfZXZlbl9mYWN0b3IoaykgKgogIGJyaWdodF9mYWN0b3IoaykgKiBmb3JtYW50X2ZhY3RvcihrKTsKCi8vIEluaGFybW9uaWMgcGFydGlhbCBmcmVxdWVuY3k6IGZyZXFfayA9IGsqZjAqKDEgKyBCKihrXjIgLSAxKSkKaGFybV9mcmVxKGspID0gYmFzZV9mcmVxICogZmxvYXQoaykKICAgICAgICAgICAgICAgKiAoMS4wICsgZWZmX2luaGFybSAqIChmbG9hdChrKSAqIGZsb2F0KGspIC0gMS4wKSk7CgovLyBTdGVyZW8gc3ByZWFkIOKAlCBSIGNoYW5uZWwgZ2V0cyBhIHNtYWxsIHBlci1oYXJtb25pYyBwaXRjaCBvZmZzZXQKc3RlcmVvX3NwcmVhZF9mcmVxKGspID0gc3RlcmVvX3BoYXNlX3NwcmVhZCAqIGZsb2F0KGspICogMC4wMTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBBZGRpdGl2ZSBvc2NpbGxhdG9yIHN1bXMg4oCUIEwgYW5kIFIKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmFkZGl0aXZlX0wgPSBzdW0oaywgTiwgaGFybV9hbXAoaysxKSAqIG9zLm9zYyhoYXJtX2ZyZXEoaysxKSkpOwphZGRpdGl2ZV9SID0gc3VtKGssIE4sCiAgaGFybV9hbXAoaysxKSAqIG9zLm9zYyhoYXJtX2ZyZXEoaysxKSAqICgxLjAgKyBzdGVyZW9fc3ByZWFkX2ZyZXEoaysxKSkpKTsKCi8vIFN1Yi1oYXJtb25pYyAoMC41w5cgZnVuZGFtZW50YWwpCnN1Yl9vc2MgPSBzdWJfaGFybW9uaWMgKiBvcy5vc2MoYmFzZV9mcmVxICogMC41KTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBOb2lzZSBmbG9vciDigJQgY29sb3VyZWQgdmlhIG9uZS1wb2xlIExQCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQplZmZfbm9pc2UgPSBtYXgoMC4wLCBtaW4oMS4wLCBub2lzZV9mbG9vciArIG1vZF9ub2lzZV9taXgoZ2F0ZSkpKTsKbm9pc2VfbHBfY3V0b2ZmID0gMjAwLjAgKyAoMS4wIC0gbm9pc2VfY29sb3IpICogMTk4MDAuMDsKbm9pc2Vfc2lnbmFsICAgID0gbm8ubm9pc2UgOiBmaS5sb3dwYXNzKDEsIG5vaXNlX2xwX2N1dG9mZik7Cm5vaXNlX291dCAgICAgICA9IGVmZl9ub2lzZSAqIG5vaXNlX3NpZ25hbDsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTb2Z0LWNsaXAgc2F0dXJhdGlvbiDigJQgdGFuaCB3YXZlc2hhcGVyCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpkcml2ZSAgICAgICA9IDEuMCArIHNhdHVyYXRpb24gKiA5LjA7CnNvZnRjbGlwKHgpID0gbWEudGFuaCh4ICogZHJpdmUpIC8gZHJpdmU7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gVmVsb2NpdHkgZ2FpbiDigJQga2VlcHMgX3ZlbCBhbGl2ZSBpbiB0aGUgSlNPTiBkZXNjcmlwdG9yCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQp2ZWxfZ2FpbiA9ICgxLjAgLSAwLjMpICsgMC4zICogdmVsOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEFtcGxpdHVkZSBhbmQgcGFuIChib3RoIG1vZC1kcml2ZW4pCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBTZWUgc3VidHJhY3RpdmUgZW5naW5lIGZvciB0aGUgcG9zaXRpdmUtb25seSBtb2RfYW1wIHJhdGlvbmFsZS4KYW1wX3ZhbCA9IG1heCgwLjAsIG1pbigxLjAsIGJhc2VfYW1wICsgbWF4KDAuMCwgbW9kX2FtcChnYXRlKSkpKSAqIGxldmVsICogdmVsX2dhaW47CnBhbl92YWwgPSBtYXgoLTEuMCwgbWluKDEuMCwgbWFzdGVyX3BhbiArIG1vZF9wYW4oZ2F0ZSkpKTsKCnBhbl9sID0gY29zKChwYW5fdmFsICsgMS4wKSAqIDAuMjUgKiBtYS5QSSk7CnBhbl9yID0gc2luKChwYW5fdmFsICsgMS4wKSAqIDAuMjUgKiBtYS5QSSk7CgpyYXdfTCA9IChhZGRpdGl2ZV9MICsgc3ViX29zYyArIG5vaXNlX291dCkgKiBhbXBfdmFsOwpyYXdfUiA9IChhZGRpdGl2ZV9SICsgc3ViX29zYyArIG5vaXNlX291dCkgKiBhbXBfdmFsOwoKc2lnbmFsX0wgPSBzb2Z0Y2xpcChyYXdfTCkgKiBwYW5fbDsKc2lnbmFsX1IgPSBzb2Z0Y2xpcChyYXdfUikgKiBwYW5fcjsKCnByb2Nlc3MgPSBzaWduYWxfTCwgc2lnbmFsX1I7Cg==","inputs": 0,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn modular_additive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "envelopes.lib/adsr:author": "Yann Orlarey and Andrey Bundin" },{ "envelopes.lib/author": "GRAME" },{ "envelopes.lib/copyright": "GRAME" },{ "envelopes.lib/license": "LGPL with exception" },{ "envelopes.lib/name": "Faust Envelope Library" },{ "envelopes.lib/version": "1.3.0" },{ "filename": "modular-additive.dsp" },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/lowpass:author": "Julius O. Smith III" },{ "filters.lib/lowpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass:license": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "modular-additive" },{ "noises.lib/name": "Faust Noise Generator Library" },{ "noises.lib/version": "1.4.1" },{ "oscillators.lib/lf_sawpos:author": "Bart Brouns, revised by Stéphane Letz" },{ "oscillators.lib/lf_sawpos:licence": "STK-4.3" },{ "oscillators.lib/lf_triangle:author": "Bart Brouns" },{ "oscillators.lib/lf_triangle:licence": "STK-4.3" },{ "oscillators.lib/name": "Faust Oscillator Library" },{ "oscillators.lib/saw1:author": "Bart Brouns" },{ "oscillators.lib/saw1:licence": "STK-4.3" },{ "oscillators.lib/version": "1.6.0" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "modular-additive","items": [ {"type": "hslider","label": "0_Hidden/_vel","varname": "fHslider225","shortname": "0_Hidden_vel","address": "/modular-additive/0_Hidden__vel","index": 263356,"meta": [{ "hidden": "1" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "0_Hidden/freq","varname": "fHslider374","shortname": "0_Hidden_freq","address": "/modular-additive/0_Hidden_freq","index": 263988,"meta": [{ "hidden": "1" },{ "unit": "Hz" }],"init": 220,"min": 20,"max": 4000,"step": 0.01},{"type": "button","label": "0_Hidden/gate","varname": "fButton0","shortname": "0_Hidden_gate","address": "/modular-additive/0_Hidden_gate","index": 262932,"meta": [{ "hidden": "1" }]},{"type": "hslider","label": "1_Spectral/00_h1_amp","varname": "fHslider680","shortname": "1_Spectral_00_h1_amp","address": "/modular-additive/1_Spectral_00_h1_amp","index": 265988,"init": 0.8,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/01_h2_amp","varname": "fHslider679","shortname": "1_Spectral_01_h2_amp","address": "/modular-additive/1_Spectral_01_h2_amp","index": 265976,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/02_h3_amp","varname": "fHslider678","shortname": "1_Spectral_02_h3_amp","address": "/modular-additive/1_Spectral_02_h3_amp","index": 265960,"init": 0.35,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/03_h4_amp","varname": "fHslider677","shortname": "1_Spectral_03_h4_amp","address": "/modular-additive/1_Spectral_03_h4_amp","index": 265944,"init": 0.25,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/04_h5_amp","varname": "fHslider676","shortname": "1_Spectral_04_h5_amp","address": "/modular-additive/1_Spectral_04_h5_amp","index": 265928,"init": 0.18,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/05_h6_amp","varname": "fHslider675","shortname": "1_Spectral_05_h6_amp","address": "/modular-additive/1_Spectral_05_h6_amp","index": 265912,"init": 0.12,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/06_h7_amp","varname": "fHslider674","shortname": "1_Spectral_06_h7_amp","address": "/modular-additive/1_Spectral_06_h7_amp","index": 265896,"init": 0.08,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/07_h8_amp","varname": "fHslider673","shortname": "1_Spectral_07_h8_amp","address": "/modular-additive/1_Spectral_07_h8_amp","index": 265880,"init": 0.06,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/08_h9_16_amp","varname": "fHslider672","shortname": "1_Spectral_08_h9_16_amp","address": "/modular-additive/1_Spectral_08_h9_16_amp","index": 265780,"init": 0.05,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/09_h17_32_amp","varname": "fHslider671","shortname": "1_Spectral_09_h17_32_amp","address": "/modular-additive/1_Spectral_09_h17_32_amp","index": 265584,"init": 0.025,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/10_h33_64_amp","varname": "fHslider670","shortname": "1_Spectral_10_h33_64_amp","address": "/modular-additive/1_Spectral_10_h33_64_amp","index": 265196,"init": 0.01,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/11_spectral_tilt","varname": "fHslider620","shortname": "1_Spectral_11_spectral_tilt","address": "/modular-additive/1_Spectral_11_spectral_tilt","index": 264996,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Spectral/12_inharmonicity","varname": "fHslider424","shortname": "1_Spectral_12_inharmonicity","address": "/modular-additive/1_Spectral_12_inharmonicity","index": 264200,"init": 0,"min": 0,"max": 0.15,"step": 0.0001},{"type": "hslider","label": "1_Spectral/13_odd_even","varname": "fHslider669","shortname": "1_Spectral_13_odd_even","address": "/modular-additive/1_Spectral_13_odd_even","index": 265192,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Formants/00_formant1_freq","varname": "fHslider474","shortname": "2_Formants_00_formant1_freq","address": "/modular-additive/2_Formants_00_formant1_freq","index": 264412,"meta": [{ "tooltip": "Formant 1 harmonic idx" }],"init": 3,"min": 1,"max": 16,"step": 0.01},{"type": "hslider","label": "2_Formants/01_formant2_freq","varname": "fHslider473","shortname": "2_Formants_01_formant2_freq","address": "/modular-additive/2_Formants_01_formant2_freq","index": 264408,"meta": [{ "tooltip": "Formant 2 harmonic idx" }],"init": 6,"min": 1,"max": 16,"step": 0.01},{"type": "hslider","label": "2_Formants/02_formant_depth","varname": "fHslider523","shortname": "2_Formants_02_formant_depth","address": "/modular-additive/2_Formants_02_formant_depth","index": 264608,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Formants/03_noise_floor","varname": "fHslider323","shortname": "2_Formants_03_noise_floor","address": "/modular-additive/2_Formants_03_noise_floor","index": 263756,"init": 0,"min": 0,"max": 0.2,"step": 0.001},{"type": "hslider","label": "2_Formants/04_noise_color","varname": "fHslider324","shortname": "2_Formants_04_noise_color","address": "/modular-additive/2_Formants_04_noise_color","index": 263760,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Formants/05_sub_harmonic","varname": "fHslider375","shortname": "2_Formants_05_sub_harmonic","address": "/modular-additive/2_Formants_05_sub_harmonic","index": 264004,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Master/00_level","varname": "fHslider682","shortname": "3_Master_00_level","address": "/modular-additive/3_Master_00_level","index": 265996,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Master/01_fine_tune","varname": "fHslider373","shortname": "3_Master_01_fine_tune","address": "/modular-additive/3_Master_01_fine_tune","index": 263984,"meta": [{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1},{"type": "hslider","label": "3_Master/02_saturation","varname": "fHslider681","shortname": "3_Master_02_saturation","address": "/modular-additive/3_Master_02_saturation","index": 265992,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Master/03_stereo_phase_spread","varname": "fHslider683","shortname": "3_Master_03_stereo_phase_spread","address": "/modular-additive/3_Master_03_stereo_phase_spread","index": 266000,"init": 0.1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Master/04_master_pan","varname": "fHslider224","shortname": "3_Master_04_master_pan","address": "/modular-additive/3_Master_04_master_pan","index": 263344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Master/05_base_amp","varname": "fHslider274","shortname": "3_Master_05_base_amp","address": "/modular-additive/3_Master_05_base_amp","index": 263560,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_attack","varname": "fHslider219","shortname": "MM_ADSR_00_adsr01_attack","address": "/modular-additive/MM_ADSR_00_adsr01_attack","index": 263324,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_decay","varname": "fHslider220","shortname": "MM_ADSR_00_adsr01_decay","address": "/modular-additive/MM_ADSR_00_adsr01_decay","index": 263328,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_enable","varname": "fHslider223","shortname": "MM_ADSR_00_adsr01_enable","address": "/modular-additive/MM_ADSR_00_adsr01_enable","index": 263340,"init": 1,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/00_adsr01_release","varname": "fHslider218","shortname": "MM_ADSR_00_adsr01_release","address": "/modular-additive/MM_ADSR_00_adsr01_release","index": 263320,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_sustain","varname": "fHslider221","shortname": "MM_ADSR_00_adsr01_sustain","address": "/modular-additive/MM_ADSR_00_adsr01_sustain","index": 263332,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_attack","varname": "fHslider213","shortname": "MM_ADSR_01_adsr02_attack","address": "/modular-additive/MM_ADSR_01_adsr02_attack","index": 263300,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_decay","varname": "fHslider214","shortname": "MM_ADSR_01_adsr02_decay","address": "/modular-additive/MM_ADSR_01_adsr02_decay","index": 263304,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_enable","varname": "fHslider217","shortname": "MM_ADSR_01_adsr02_enable","address": "/modular-additive/MM_ADSR_01_adsr02_enable","index": 263316,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/01_adsr02_release","varname": "fHslider212","shortname": "MM_ADSR_01_adsr02_release","address": "/modular-additive/MM_ADSR_01_adsr02_release","index": 263296,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_sustain","varname": "fHslider215","shortname": "MM_ADSR_01_adsr02_sustain","address": "/modular-additive/MM_ADSR_01_adsr02_sustain","index": 263308,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_attack","varname": "fHslider207","shortname": "MM_ADSR_02_adsr03_attack","address": "/modular-additive/MM_ADSR_02_adsr03_attack","index": 263276,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_decay","varname": "fHslider208","shortname": "MM_ADSR_02_adsr03_decay","address": "/modular-additive/MM_ADSR_02_adsr03_decay","index": 263280,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_enable","varname": "fHslider211","shortname": "MM_ADSR_02_adsr03_enable","address": "/modular-additive/MM_ADSR_02_adsr03_enable","index": 263292,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/02_adsr03_release","varname": "fHslider206","shortname": "MM_ADSR_02_adsr03_release","address": "/modular-additive/MM_ADSR_02_adsr03_release","index": 263272,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_sustain","varname": "fHslider209","shortname": "MM_ADSR_02_adsr03_sustain","address": "/modular-additive/MM_ADSR_02_adsr03_sustain","index": 263284,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_attack","varname": "fHslider201","shortname": "MM_ADSR_03_adsr04_attack","address": "/modular-additive/MM_ADSR_03_adsr04_attack","index": 263252,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_decay","varname": "fHslider202","shortname": "MM_ADSR_03_adsr04_decay","address": "/modular-additive/MM_ADSR_03_adsr04_decay","index": 263256,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_enable","varname": "fHslider205","shortname": "MM_ADSR_03_adsr04_enable","address": "/modular-additive/MM_ADSR_03_adsr04_enable","index": 263268,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/03_adsr04_release","varname": "fHslider200","shortname": "MM_ADSR_03_adsr04_release","address": "/modular-additive/MM_ADSR_03_adsr04_release","index": 263248,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_sustain","varname": "fHslider203","shortname": "MM_ADSR_03_adsr04_sustain","address": "/modular-additive/MM_ADSR_03_adsr04_sustain","index": 263260,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_attack","varname": "fHslider195","shortname": "MM_ADSR_04_adsr05_attack","address": "/modular-additive/MM_ADSR_04_adsr05_attack","index": 263228,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_decay","varname": "fHslider196","shortname": "MM_ADSR_04_adsr05_decay","address": "/modular-additive/MM_ADSR_04_adsr05_decay","index": 263232,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_enable","varname": "fHslider199","shortname": "MM_ADSR_04_adsr05_enable","address": "/modular-additive/MM_ADSR_04_adsr05_enable","index": 263244,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/04_adsr05_release","varname": "fHslider194","shortname": "MM_ADSR_04_adsr05_release","address": "/modular-additive/MM_ADSR_04_adsr05_release","index": 263224,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_sustain","varname": "fHslider197","shortname": "MM_ADSR_04_adsr05_sustain","address": "/modular-additive/MM_ADSR_04_adsr05_sustain","index": 263236,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_attack","varname": "fHslider189","shortname": "MM_ADSR_05_adsr06_attack","address": "/modular-additive/MM_ADSR_05_adsr06_attack","index": 263204,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_decay","varname": "fHslider190","shortname": "MM_ADSR_05_adsr06_decay","address": "/modular-additive/MM_ADSR_05_adsr06_decay","index": 263208,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_enable","varname": "fHslider193","shortname": "MM_ADSR_05_adsr06_enable","address": "/modular-additive/MM_ADSR_05_adsr06_enable","index": 263220,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/05_adsr06_release","varname": "fHslider188","shortname": "MM_ADSR_05_adsr06_release","address": "/modular-additive/MM_ADSR_05_adsr06_release","index": 263200,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_sustain","varname": "fHslider191","shortname": "MM_ADSR_05_adsr06_sustain","address": "/modular-additive/MM_ADSR_05_adsr06_sustain","index": 263212,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_attack","varname": "fHslider183","shortname": "MM_ADSR_06_adsr07_attack","address": "/modular-additive/MM_ADSR_06_adsr07_attack","index": 263180,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_decay","varname": "fHslider184","shortname": "MM_ADSR_06_adsr07_decay","address": "/modular-additive/MM_ADSR_06_adsr07_decay","index": 263184,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_enable","varname": "fHslider187","shortname": "MM_ADSR_06_adsr07_enable","address": "/modular-additive/MM_ADSR_06_adsr07_enable","index": 263196,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/06_adsr07_release","varname": "fHslider182","shortname": "MM_ADSR_06_adsr07_release","address": "/modular-additive/MM_ADSR_06_adsr07_release","index": 263176,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_sustain","varname": "fHslider185","shortname": "MM_ADSR_06_adsr07_sustain","address": "/modular-additive/MM_ADSR_06_adsr07_sustain","index": 263188,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_attack","varname": "fHslider177","shortname": "MM_ADSR_07_adsr08_attack","address": "/modular-additive/MM_ADSR_07_adsr08_attack","index": 263156,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_decay","varname": "fHslider178","shortname": "MM_ADSR_07_adsr08_decay","address": "/modular-additive/MM_ADSR_07_adsr08_decay","index": 263160,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_enable","varname": "fHslider181","shortname": "MM_ADSR_07_adsr08_enable","address": "/modular-additive/MM_ADSR_07_adsr08_enable","index": 263172,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/07_adsr08_release","varname": "fHslider176","shortname": "MM_ADSR_07_adsr08_release","address": "/modular-additive/MM_ADSR_07_adsr08_release","index": 263152,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_sustain","varname": "fHslider179","shortname": "MM_ADSR_07_adsr08_sustain","address": "/modular-additive/MM_ADSR_07_adsr08_sustain","index": 263164,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_attack","varname": "fHslider171","shortname": "MM_ADSR_08_adsr09_attack","address": "/modular-additive/MM_ADSR_08_adsr09_attack","index": 263132,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_decay","varname": "fHslider172","shortname": "MM_ADSR_08_adsr09_decay","address": "/modular-additive/MM_ADSR_08_adsr09_decay","index": 263136,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_enable","varname": "fHslider175","shortname": "MM_ADSR_08_adsr09_enable","address": "/modular-additive/MM_ADSR_08_adsr09_enable","index": 263148,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/08_adsr09_release","varname": "fHslider170","shortname": "MM_ADSR_08_adsr09_release","address": "/modular-additive/MM_ADSR_08_adsr09_release","index": 263128,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_sustain","varname": "fHslider173","shortname": "MM_ADSR_08_adsr09_sustain","address": "/modular-additive/MM_ADSR_08_adsr09_sustain","index": 263140,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_attack","varname": "fHslider165","shortname": "MM_ADSR_09_adsr10_attack","address": "/modular-additive/MM_ADSR_09_adsr10_attack","index": 263108,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_decay","varname": "fHslider166","shortname": "MM_ADSR_09_adsr10_decay","address": "/modular-additive/MM_ADSR_09_adsr10_decay","index": 263112,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_enable","varname": "fHslider169","shortname": "MM_ADSR_09_adsr10_enable","address": "/modular-additive/MM_ADSR_09_adsr10_enable","index": 263124,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/09_adsr10_release","varname": "fHslider164","shortname": "MM_ADSR_09_adsr10_release","address": "/modular-additive/MM_ADSR_09_adsr10_release","index": 263104,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_sustain","varname": "fHslider167","shortname": "MM_ADSR_09_adsr10_sustain","address": "/modular-additive/MM_ADSR_09_adsr10_sustain","index": 263116,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_attack","varname": "fHslider159","shortname": "MM_ADSR_10_adsr11_attack","address": "/modular-additive/MM_ADSR_10_adsr11_attack","index": 263084,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_decay","varname": "fHslider160","shortname": "MM_ADSR_10_adsr11_decay","address": "/modular-additive/MM_ADSR_10_adsr11_decay","index": 263088,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_enable","varname": "fHslider163","shortname": "MM_ADSR_10_adsr11_enable","address": "/modular-additive/MM_ADSR_10_adsr11_enable","index": 263100,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/10_adsr11_release","varname": "fHslider158","shortname": "MM_ADSR_10_adsr11_release","address": "/modular-additive/MM_ADSR_10_adsr11_release","index": 263080,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_sustain","varname": "fHslider161","shortname": "MM_ADSR_10_adsr11_sustain","address": "/modular-additive/MM_ADSR_10_adsr11_sustain","index": 263092,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_attack","varname": "fHslider153","shortname": "MM_ADSR_11_adsr12_attack","address": "/modular-additive/MM_ADSR_11_adsr12_attack","index": 263060,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_decay","varname": "fHslider154","shortname": "MM_ADSR_11_adsr12_decay","address": "/modular-additive/MM_ADSR_11_adsr12_decay","index": 263064,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_enable","varname": "fHslider157","shortname": "MM_ADSR_11_adsr12_enable","address": "/modular-additive/MM_ADSR_11_adsr12_enable","index": 263076,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/11_adsr12_release","varname": "fHslider152","shortname": "MM_ADSR_11_adsr12_release","address": "/modular-additive/MM_ADSR_11_adsr12_release","index": 263056,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_sustain","varname": "fHslider155","shortname": "MM_ADSR_11_adsr12_sustain","address": "/modular-additive/MM_ADSR_11_adsr12_sustain","index": 263068,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_attack","varname": "fHslider147","shortname": "MM_ADSR_12_adsr13_attack","address": "/modular-additive/MM_ADSR_12_adsr13_attack","index": 263036,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_decay","varname": "fHslider148","shortname": "MM_ADSR_12_adsr13_decay","address": "/modular-additive/MM_ADSR_12_adsr13_decay","index": 263040,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_enable","varname": "fHslider151","shortname": "MM_ADSR_12_adsr13_enable","address": "/modular-additive/MM_ADSR_12_adsr13_enable","index": 263052,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/12_adsr13_release","varname": "fHslider146","shortname": "MM_ADSR_12_adsr13_release","address": "/modular-additive/MM_ADSR_12_adsr13_release","index": 263032,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_sustain","varname": "fHslider149","shortname": "MM_ADSR_12_adsr13_sustain","address": "/modular-additive/MM_ADSR_12_adsr13_sustain","index": 263044,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_attack","varname": "fHslider141","shortname": "MM_ADSR_13_adsr14_attack","address": "/modular-additive/MM_ADSR_13_adsr14_attack","index": 263012,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_decay","varname": "fHslider142","shortname": "MM_ADSR_13_adsr14_decay","address": "/modular-additive/MM_ADSR_13_adsr14_decay","index": 263016,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_enable","varname": "fHslider145","shortname": "MM_ADSR_13_adsr14_enable","address": "/modular-additive/MM_ADSR_13_adsr14_enable","index": 263028,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/13_adsr14_release","varname": "fHslider140","shortname": "MM_ADSR_13_adsr14_release","address": "/modular-additive/MM_ADSR_13_adsr14_release","index": 263008,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_sustain","varname": "fHslider143","shortname": "MM_ADSR_13_adsr14_sustain","address": "/modular-additive/MM_ADSR_13_adsr14_sustain","index": 263020,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_attack","varname": "fHslider135","shortname": "MM_ADSR_14_adsr15_attack","address": "/modular-additive/MM_ADSR_14_adsr15_attack","index": 262988,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_decay","varname": "fHslider136","shortname": "MM_ADSR_14_adsr15_decay","address": "/modular-additive/MM_ADSR_14_adsr15_decay","index": 262992,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_enable","varname": "fHslider139","shortname": "MM_ADSR_14_adsr15_enable","address": "/modular-additive/MM_ADSR_14_adsr15_enable","index": 263004,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/14_adsr15_release","varname": "fHslider134","shortname": "MM_ADSR_14_adsr15_release","address": "/modular-additive/MM_ADSR_14_adsr15_release","index": 262984,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_sustain","varname": "fHslider137","shortname": "MM_ADSR_14_adsr15_sustain","address": "/modular-additive/MM_ADSR_14_adsr15_sustain","index": 262996,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_attack","varname": "fHslider129","shortname": "MM_ADSR_15_adsr16_attack","address": "/modular-additive/MM_ADSR_15_adsr16_attack","index": 262964,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_decay","varname": "fHslider130","shortname": "MM_ADSR_15_adsr16_decay","address": "/modular-additive/MM_ADSR_15_adsr16_decay","index": 262968,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_enable","varname": "fHslider133","shortname": "MM_ADSR_15_adsr16_enable","address": "/modular-additive/MM_ADSR_15_adsr16_enable","index": 262980,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/15_adsr16_release","varname": "fHslider128","shortname": "MM_ADSR_15_adsr16_release","address": "/modular-additive/MM_ADSR_15_adsr16_release","index": 262952,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_sustain","varname": "fHslider131","shortname": "MM_ADSR_15_adsr16_sustain","address": "/modular-additive/MM_ADSR_15_adsr16_sustain","index": 262972,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_enable","varname": "fHslider127","shortname": "MM_LFO_00_lfo01_enable","address": "/modular-additive/MM_LFO_00_lfo01_enable","index": 262928,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/00_lfo01_morph","varname": "fHslider124","shortname": "MM_LFO_00_lfo01_morph","address": "/modular-additive/MM_LFO_00_lfo01_morph","index": 262908,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_rate","varname": "fHslider125","shortname": "MM_LFO_00_lfo01_rate","address": "/modular-additive/MM_LFO_00_lfo01_rate","index": 262912,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_enable","varname": "fHslider123","shortname": "MM_LFO_01_lfo02_enable","address": "/modular-additive/MM_LFO_01_lfo02_enable","index": 262904,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/01_lfo02_morph","varname": "fHslider120","shortname": "MM_LFO_01_lfo02_morph","address": "/modular-additive/MM_LFO_01_lfo02_morph","index": 262884,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_rate","varname": "fHslider121","shortname": "MM_LFO_01_lfo02_rate","address": "/modular-additive/MM_LFO_01_lfo02_rate","index": 262888,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_enable","varname": "fHslider119","shortname": "MM_LFO_02_lfo03_enable","address": "/modular-additive/MM_LFO_02_lfo03_enable","index": 262880,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/02_lfo03_morph","varname": "fHslider116","shortname": "MM_LFO_02_lfo03_morph","address": "/modular-additive/MM_LFO_02_lfo03_morph","index": 262860,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_rate","varname": "fHslider117","shortname": "MM_LFO_02_lfo03_rate","address": "/modular-additive/MM_LFO_02_lfo03_rate","index": 262864,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_enable","varname": "fHslider115","shortname": "MM_LFO_03_lfo04_enable","address": "/modular-additive/MM_LFO_03_lfo04_enable","index": 262856,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/03_lfo04_morph","varname": "fHslider112","shortname": "MM_LFO_03_lfo04_morph","address": "/modular-additive/MM_LFO_03_lfo04_morph","index": 262836,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_rate","varname": "fHslider113","shortname": "MM_LFO_03_lfo04_rate","address": "/modular-additive/MM_LFO_03_lfo04_rate","index": 262840,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_enable","varname": "fHslider111","shortname": "MM_LFO_04_lfo05_enable","address": "/modular-additive/MM_LFO_04_lfo05_enable","index": 262832,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/04_lfo05_morph","varname": "fHslider108","shortname": "MM_LFO_04_lfo05_morph","address": "/modular-additive/MM_LFO_04_lfo05_morph","index": 262812,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_rate","varname": "fHslider109","shortname": "MM_LFO_04_lfo05_rate","address": "/modular-additive/MM_LFO_04_lfo05_rate","index": 262816,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_enable","varname": "fHslider107","shortname": "MM_LFO_05_lfo06_enable","address": "/modular-additive/MM_LFO_05_lfo06_enable","index": 262808,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/05_lfo06_morph","varname": "fHslider104","shortname": "MM_LFO_05_lfo06_morph","address": "/modular-additive/MM_LFO_05_lfo06_morph","index": 262788,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_rate","varname": "fHslider105","shortname": "MM_LFO_05_lfo06_rate","address": "/modular-additive/MM_LFO_05_lfo06_rate","index": 262792,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_enable","varname": "fHslider103","shortname": "MM_LFO_06_lfo07_enable","address": "/modular-additive/MM_LFO_06_lfo07_enable","index": 262784,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/06_lfo07_morph","varname": "fHslider100","shortname": "MM_LFO_06_lfo07_morph","address": "/modular-additive/MM_LFO_06_lfo07_morph","index": 262764,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_rate","varname": "fHslider101","shortname": "MM_LFO_06_lfo07_rate","address": "/modular-additive/MM_LFO_06_lfo07_rate","index": 262768,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_enable","varname": "fHslider99","shortname": "MM_LFO_07_lfo08_enable","address": "/modular-additive/MM_LFO_07_lfo08_enable","index": 262760,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/07_lfo08_morph","varname": "fHslider96","shortname": "MM_LFO_07_lfo08_morph","address": "/modular-additive/MM_LFO_07_lfo08_morph","index": 262740,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_rate","varname": "fHslider97","shortname": "MM_LFO_07_lfo08_rate","address": "/modular-additive/MM_LFO_07_lfo08_rate","index": 262744,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_enable","varname": "fHslider95","shortname": "MM_LFO_08_lfo09_enable","address": "/modular-additive/MM_LFO_08_lfo09_enable","index": 262736,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/08_lfo09_morph","varname": "fHslider92","shortname": "MM_LFO_08_lfo09_morph","address": "/modular-additive/MM_LFO_08_lfo09_morph","index": 262716,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_rate","varname": "fHslider93","shortname": "MM_LFO_08_lfo09_rate","address": "/modular-additive/MM_LFO_08_lfo09_rate","index": 262720,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_enable","varname": "fHslider91","shortname": "MM_LFO_09_lfo10_enable","address": "/modular-additive/MM_LFO_09_lfo10_enable","index": 262712,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/09_lfo10_morph","varname": "fHslider88","shortname": "MM_LFO_09_lfo10_morph","address": "/modular-additive/MM_LFO_09_lfo10_morph","index": 262692,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_rate","varname": "fHslider89","shortname": "MM_LFO_09_lfo10_rate","address": "/modular-additive/MM_LFO_09_lfo10_rate","index": 262696,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_enable","varname": "fHslider87","shortname": "MM_LFO_10_lfo11_enable","address": "/modular-additive/MM_LFO_10_lfo11_enable","index": 262688,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/10_lfo11_morph","varname": "fHslider84","shortname": "MM_LFO_10_lfo11_morph","address": "/modular-additive/MM_LFO_10_lfo11_morph","index": 262668,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_rate","varname": "fHslider85","shortname": "MM_LFO_10_lfo11_rate","address": "/modular-additive/MM_LFO_10_lfo11_rate","index": 262672,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_enable","varname": "fHslider83","shortname": "MM_LFO_11_lfo12_enable","address": "/modular-additive/MM_LFO_11_lfo12_enable","index": 262664,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/11_lfo12_morph","varname": "fHslider80","shortname": "MM_LFO_11_lfo12_morph","address": "/modular-additive/MM_LFO_11_lfo12_morph","index": 262644,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_rate","varname": "fHslider81","shortname": "MM_LFO_11_lfo12_rate","address": "/modular-additive/MM_LFO_11_lfo12_rate","index": 262648,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_enable","varname": "fHslider79","shortname": "MM_LFO_12_lfo13_enable","address": "/modular-additive/MM_LFO_12_lfo13_enable","index": 262640,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/12_lfo13_morph","varname": "fHslider76","shortname": "MM_LFO_12_lfo13_morph","address": "/modular-additive/MM_LFO_12_lfo13_morph","index": 262620,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_rate","varname": "fHslider77","shortname": "MM_LFO_12_lfo13_rate","address": "/modular-additive/MM_LFO_12_lfo13_rate","index": 262624,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_enable","varname": "fHslider75","shortname": "MM_LFO_13_lfo14_enable","address": "/modular-additive/MM_LFO_13_lfo14_enable","index": 262616,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/13_lfo14_morph","varname": "fHslider72","shortname": "MM_LFO_13_lfo14_morph","address": "/modular-additive/MM_LFO_13_lfo14_morph","index": 262596,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_rate","varname": "fHslider73","shortname": "MM_LFO_13_lfo14_rate","address": "/modular-additive/MM_LFO_13_lfo14_rate","index": 262600,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_enable","varname": "fHslider71","shortname": "MM_LFO_14_lfo15_enable","address": "/modular-additive/MM_LFO_14_lfo15_enable","index": 262592,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/14_lfo15_morph","varname": "fHslider68","shortname": "MM_LFO_14_lfo15_morph","address": "/modular-additive/MM_LFO_14_lfo15_morph","index": 262572,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_rate","varname": "fHslider69","shortname": "MM_LFO_14_lfo15_rate","address": "/modular-additive/MM_LFO_14_lfo15_rate","index": 262576,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_enable","varname": "fHslider67","shortname": "MM_LFO_15_lfo16_enable","address": "/modular-additive/MM_LFO_15_lfo16_enable","index": 262568,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/15_lfo16_morph","varname": "fHslider64","shortname": "MM_LFO_15_lfo16_morph","address": "/modular-additive/MM_LFO_15_lfo16_morph","index": 262548,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_rate","varname": "fHslider65","shortname": "MM_LFO_15_lfo16_rate","address": "/modular-additive/MM_LFO_15_lfo16_rate","index": 262552,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_enable","varname": "fHslider63","shortname": "MM_LFO_16_lfo17_enable","address": "/modular-additive/MM_LFO_16_lfo17_enable","index": 262544,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/16_lfo17_morph","varname": "fHslider60","shortname": "MM_LFO_16_lfo17_morph","address": "/modular-additive/MM_LFO_16_lfo17_morph","index": 262524,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_rate","varname": "fHslider61","shortname": "MM_LFO_16_lfo17_rate","address": "/modular-additive/MM_LFO_16_lfo17_rate","index": 262528,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_enable","varname": "fHslider59","shortname": "MM_LFO_17_lfo18_enable","address": "/modular-additive/MM_LFO_17_lfo18_enable","index": 262520,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/17_lfo18_morph","varname": "fHslider56","shortname": "MM_LFO_17_lfo18_morph","address": "/modular-additive/MM_LFO_17_lfo18_morph","index": 262500,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_rate","varname": "fHslider57","shortname": "MM_LFO_17_lfo18_rate","address": "/modular-additive/MM_LFO_17_lfo18_rate","index": 262504,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_enable","varname": "fHslider55","shortname": "MM_LFO_18_lfo19_enable","address": "/modular-additive/MM_LFO_18_lfo19_enable","index": 262496,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/18_lfo19_morph","varname": "fHslider52","shortname": "MM_LFO_18_lfo19_morph","address": "/modular-additive/MM_LFO_18_lfo19_morph","index": 262476,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_rate","varname": "fHslider53","shortname": "MM_LFO_18_lfo19_rate","address": "/modular-additive/MM_LFO_18_lfo19_rate","index": 262480,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_enable","varname": "fHslider51","shortname": "MM_LFO_19_lfo20_enable","address": "/modular-additive/MM_LFO_19_lfo20_enable","index": 262472,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/19_lfo20_morph","varname": "fHslider48","shortname": "MM_LFO_19_lfo20_morph","address": "/modular-additive/MM_LFO_19_lfo20_morph","index": 262452,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_rate","varname": "fHslider49","shortname": "MM_LFO_19_lfo20_rate","address": "/modular-additive/MM_LFO_19_lfo20_rate","index": 262456,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_enable","varname": "fHslider47","shortname": "MM_LFO_20_lfo21_enable","address": "/modular-additive/MM_LFO_20_lfo21_enable","index": 262448,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/20_lfo21_morph","varname": "fHslider44","shortname": "MM_LFO_20_lfo21_morph","address": "/modular-additive/MM_LFO_20_lfo21_morph","index": 262428,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_rate","varname": "fHslider45","shortname": "MM_LFO_20_lfo21_rate","address": "/modular-additive/MM_LFO_20_lfo21_rate","index": 262432,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_enable","varname": "fHslider43","shortname": "MM_LFO_21_lfo22_enable","address": "/modular-additive/MM_LFO_21_lfo22_enable","index": 262424,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/21_lfo22_morph","varname": "fHslider40","shortname": "MM_LFO_21_lfo22_morph","address": "/modular-additive/MM_LFO_21_lfo22_morph","index": 262404,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_rate","varname": "fHslider41","shortname": "MM_LFO_21_lfo22_rate","address": "/modular-additive/MM_LFO_21_lfo22_rate","index": 262408,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_enable","varname": "fHslider39","shortname": "MM_LFO_22_lfo23_enable","address": "/modular-additive/MM_LFO_22_lfo23_enable","index": 262400,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/22_lfo23_morph","varname": "fHslider36","shortname": "MM_LFO_22_lfo23_morph","address": "/modular-additive/MM_LFO_22_lfo23_morph","index": 262380,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_rate","varname": "fHslider37","shortname": "MM_LFO_22_lfo23_rate","address": "/modular-additive/MM_LFO_22_lfo23_rate","index": 262384,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_enable","varname": "fHslider35","shortname": "MM_LFO_23_lfo24_enable","address": "/modular-additive/MM_LFO_23_lfo24_enable","index": 262376,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/23_lfo24_morph","varname": "fHslider32","shortname": "MM_LFO_23_lfo24_morph","address": "/modular-additive/MM_LFO_23_lfo24_morph","index": 262356,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_rate","varname": "fHslider33","shortname": "MM_LFO_23_lfo24_rate","address": "/modular-additive/MM_LFO_23_lfo24_rate","index": 262360,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_enable","varname": "fHslider31","shortname": "MM_LFO_24_lfo25_enable","address": "/modular-additive/MM_LFO_24_lfo25_enable","index": 262352,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/24_lfo25_morph","varname": "fHslider28","shortname": "MM_LFO_24_lfo25_morph","address": "/modular-additive/MM_LFO_24_lfo25_morph","index": 262332,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_rate","varname": "fHslider29","shortname": "MM_LFO_24_lfo25_rate","address": "/modular-additive/MM_LFO_24_lfo25_rate","index": 262336,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_enable","varname": "fHslider27","shortname": "MM_LFO_25_lfo26_enable","address": "/modular-additive/MM_LFO_25_lfo26_enable","index": 262328,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/25_lfo26_morph","varname": "fHslider24","shortname": "MM_LFO_25_lfo26_morph","address": "/modular-additive/MM_LFO_25_lfo26_morph","index": 262308,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_rate","varname": "fHslider25","shortname": "MM_LFO_25_lfo26_rate","address": "/modular-additive/MM_LFO_25_lfo26_rate","index": 262312,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_enable","varname": "fHslider23","shortname": "MM_LFO_26_lfo27_enable","address": "/modular-additive/MM_LFO_26_lfo27_enable","index": 262304,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/26_lfo27_morph","varname": "fHslider20","shortname": "MM_LFO_26_lfo27_morph","address": "/modular-additive/MM_LFO_26_lfo27_morph","index": 262284,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_rate","varname": "fHslider21","shortname": "MM_LFO_26_lfo27_rate","address": "/modular-additive/MM_LFO_26_lfo27_rate","index": 262288,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_enable","varname": "fHslider19","shortname": "MM_LFO_27_lfo28_enable","address": "/modular-additive/MM_LFO_27_lfo28_enable","index": 262280,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/27_lfo28_morph","varname": "fHslider16","shortname": "MM_LFO_27_lfo28_morph","address": "/modular-additive/MM_LFO_27_lfo28_morph","index": 262260,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_rate","varname": "fHslider17","shortname": "MM_LFO_27_lfo28_rate","address": "/modular-additive/MM_LFO_27_lfo28_rate","index": 262264,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_enable","varname": "fHslider15","shortname": "MM_LFO_28_lfo29_enable","address": "/modular-additive/MM_LFO_28_lfo29_enable","index": 262256,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/28_lfo29_morph","varname": "fHslider12","shortname": "MM_LFO_28_lfo29_morph","address": "/modular-additive/MM_LFO_28_lfo29_morph","index": 262236,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_rate","varname": "fHslider13","shortname": "MM_LFO_28_lfo29_rate","address": "/modular-additive/MM_LFO_28_lfo29_rate","index": 262240,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_enable","varname": "fHslider11","shortname": "MM_LFO_29_lfo30_enable","address": "/modular-additive/MM_LFO_29_lfo30_enable","index": 262232,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/29_lfo30_morph","varname": "fHslider8","shortname": "MM_LFO_29_lfo30_morph","address": "/modular-additive/MM_LFO_29_lfo30_morph","index": 262212,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_rate","varname": "fHslider9","shortname": "MM_LFO_29_lfo30_rate","address": "/modular-additive/MM_LFO_29_lfo30_rate","index": 262216,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_enable","varname": "fHslider7","shortname": "MM_LFO_30_lfo31_enable","address": "/modular-additive/MM_LFO_30_lfo31_enable","index": 262208,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/30_lfo31_morph","varname": "fHslider4","shortname": "MM_LFO_30_lfo31_morph","address": "/modular-additive/MM_LFO_30_lfo31_morph","index": 262188,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_rate","varname": "fHslider5","shortname": "MM_LFO_30_lfo31_rate","address": "/modular-additive/MM_LFO_30_lfo31_rate","index": 262192,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_enable","varname": "fHslider3","shortname": "MM_LFO_31_lfo32_enable","address": "/modular-additive/MM_LFO_31_lfo32_enable","index": 262184,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/31_lfo32_morph","varname": "fHslider0","shortname": "MM_LFO_31_lfo32_morph","address": "/modular-additive/MM_LFO_31_lfo32_morph","index": 262144,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_rate","varname": "fHslider1","shortname": "MM_LFO_31_lfo32_rate","address": "/modular-additive/MM_LFO_31_lfo32_rate","index": 262156,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d00_pitch","varname": "fHslider372","shortname": "MM_Matrix_s00_d00_pitch","address": "/modular-additive/MM_Matrix_s00_d00_pitch","index": 263980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d01_bright","varname": "fHslider571","shortname": "MM_Matrix_s00_d01_bright","address": "/modular-additive/MM_Matrix_s00_d01_bright","index": 264800,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d02_tilt","varname": "fHslider619","shortname": "MM_Matrix_s00_d02_tilt","address": "/modular-additive/MM_Matrix_s00_d02_tilt","index": 264992,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d03_inharmonicity","varname": "fHslider423","shortname": "MM_Matrix_s00_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s00_d03_inharmonicity","index": 264196,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d04_odd_even","varname": "fHslider668","shortname": "MM_Matrix_s00_d04_odd_even","address": "/modular-additive/MM_Matrix_s00_d04_odd_even","index": 265188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d05_formant_ctr","varname": "fHslider472","shortname": "MM_Matrix_s00_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s00_d05_formant_ctr","index": 264404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d06_formant_depth","varname": "fHslider522","shortname": "MM_Matrix_s00_d06_formant_depth","address": "/modular-additive/MM_Matrix_s00_d06_formant_depth","index": 264604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d07_noise_mix","varname": "fHslider322","shortname": "MM_Matrix_s00_d07_noise_mix","address": "/modular-additive/MM_Matrix_s00_d07_noise_mix","index": 263752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d08_amp","varname": "fHslider273","shortname": "MM_Matrix_s00_d08_amp","address": "/modular-additive/MM_Matrix_s00_d08_amp","index": 263556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d09_pan","varname": "fHslider222","shortname": "MM_Matrix_s00_d09_pan","address": "/modular-additive/MM_Matrix_s00_d09_pan","index": 263336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d00_pitch","varname": "fHslider371","shortname": "MM_Matrix_s01_d00_pitch","address": "/modular-additive/MM_Matrix_s01_d00_pitch","index": 263976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d01_bright","varname": "fHslider570","shortname": "MM_Matrix_s01_d01_bright","address": "/modular-additive/MM_Matrix_s01_d01_bright","index": 264796,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d02_tilt","varname": "fHslider618","shortname": "MM_Matrix_s01_d02_tilt","address": "/modular-additive/MM_Matrix_s01_d02_tilt","index": 264988,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d03_inharmonicity","varname": "fHslider422","shortname": "MM_Matrix_s01_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s01_d03_inharmonicity","index": 264192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d04_odd_even","varname": "fHslider667","shortname": "MM_Matrix_s01_d04_odd_even","address": "/modular-additive/MM_Matrix_s01_d04_odd_even","index": 265184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d05_formant_ctr","varname": "fHslider471","shortname": "MM_Matrix_s01_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s01_d05_formant_ctr","index": 264400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d06_formant_depth","varname": "fHslider521","shortname": "MM_Matrix_s01_d06_formant_depth","address": "/modular-additive/MM_Matrix_s01_d06_formant_depth","index": 264600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d07_noise_mix","varname": "fHslider321","shortname": "MM_Matrix_s01_d07_noise_mix","address": "/modular-additive/MM_Matrix_s01_d07_noise_mix","index": 263748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d08_amp","varname": "fHslider272","shortname": "MM_Matrix_s01_d08_amp","address": "/modular-additive/MM_Matrix_s01_d08_amp","index": 263552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d09_pan","varname": "fHslider216","shortname": "MM_Matrix_s01_d09_pan","address": "/modular-additive/MM_Matrix_s01_d09_pan","index": 263312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d00_pitch","varname": "fHslider370","shortname": "MM_Matrix_s02_d00_pitch","address": "/modular-additive/MM_Matrix_s02_d00_pitch","index": 263972,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d01_bright","varname": "fHslider569","shortname": "MM_Matrix_s02_d01_bright","address": "/modular-additive/MM_Matrix_s02_d01_bright","index": 264792,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d02_tilt","varname": "fHslider617","shortname": "MM_Matrix_s02_d02_tilt","address": "/modular-additive/MM_Matrix_s02_d02_tilt","index": 264984,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d03_inharmonicity","varname": "fHslider421","shortname": "MM_Matrix_s02_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s02_d03_inharmonicity","index": 264188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d04_odd_even","varname": "fHslider666","shortname": "MM_Matrix_s02_d04_odd_even","address": "/modular-additive/MM_Matrix_s02_d04_odd_even","index": 265180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d05_formant_ctr","varname": "fHslider470","shortname": "MM_Matrix_s02_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s02_d05_formant_ctr","index": 264396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d06_formant_depth","varname": "fHslider520","shortname": "MM_Matrix_s02_d06_formant_depth","address": "/modular-additive/MM_Matrix_s02_d06_formant_depth","index": 264596,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d07_noise_mix","varname": "fHslider320","shortname": "MM_Matrix_s02_d07_noise_mix","address": "/modular-additive/MM_Matrix_s02_d07_noise_mix","index": 263744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d08_amp","varname": "fHslider271","shortname": "MM_Matrix_s02_d08_amp","address": "/modular-additive/MM_Matrix_s02_d08_amp","index": 263548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d09_pan","varname": "fHslider210","shortname": "MM_Matrix_s02_d09_pan","address": "/modular-additive/MM_Matrix_s02_d09_pan","index": 263288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d00_pitch","varname": "fHslider369","shortname": "MM_Matrix_s03_d00_pitch","address": "/modular-additive/MM_Matrix_s03_d00_pitch","index": 263968,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d01_bright","varname": "fHslider568","shortname": "MM_Matrix_s03_d01_bright","address": "/modular-additive/MM_Matrix_s03_d01_bright","index": 264788,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d02_tilt","varname": "fHslider616","shortname": "MM_Matrix_s03_d02_tilt","address": "/modular-additive/MM_Matrix_s03_d02_tilt","index": 264980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d03_inharmonicity","varname": "fHslider420","shortname": "MM_Matrix_s03_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s03_d03_inharmonicity","index": 264184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d04_odd_even","varname": "fHslider665","shortname": "MM_Matrix_s03_d04_odd_even","address": "/modular-additive/MM_Matrix_s03_d04_odd_even","index": 265176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d05_formant_ctr","varname": "fHslider469","shortname": "MM_Matrix_s03_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s03_d05_formant_ctr","index": 264392,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d06_formant_depth","varname": "fHslider519","shortname": "MM_Matrix_s03_d06_formant_depth","address": "/modular-additive/MM_Matrix_s03_d06_formant_depth","index": 264592,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d07_noise_mix","varname": "fHslider319","shortname": "MM_Matrix_s03_d07_noise_mix","address": "/modular-additive/MM_Matrix_s03_d07_noise_mix","index": 263740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d08_amp","varname": "fHslider270","shortname": "MM_Matrix_s03_d08_amp","address": "/modular-additive/MM_Matrix_s03_d08_amp","index": 263544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d09_pan","varname": "fHslider204","shortname": "MM_Matrix_s03_d09_pan","address": "/modular-additive/MM_Matrix_s03_d09_pan","index": 263264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d00_pitch","varname": "fHslider368","shortname": "MM_Matrix_s04_d00_pitch","address": "/modular-additive/MM_Matrix_s04_d00_pitch","index": 263964,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d01_bright","varname": "fHslider567","shortname": "MM_Matrix_s04_d01_bright","address": "/modular-additive/MM_Matrix_s04_d01_bright","index": 264784,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d02_tilt","varname": "fHslider615","shortname": "MM_Matrix_s04_d02_tilt","address": "/modular-additive/MM_Matrix_s04_d02_tilt","index": 264976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d03_inharmonicity","varname": "fHslider419","shortname": "MM_Matrix_s04_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s04_d03_inharmonicity","index": 264180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d04_odd_even","varname": "fHslider664","shortname": "MM_Matrix_s04_d04_odd_even","address": "/modular-additive/MM_Matrix_s04_d04_odd_even","index": 265172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d05_formant_ctr","varname": "fHslider468","shortname": "MM_Matrix_s04_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s04_d05_formant_ctr","index": 264388,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d06_formant_depth","varname": "fHslider518","shortname": "MM_Matrix_s04_d06_formant_depth","address": "/modular-additive/MM_Matrix_s04_d06_formant_depth","index": 264588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d07_noise_mix","varname": "fHslider318","shortname": "MM_Matrix_s04_d07_noise_mix","address": "/modular-additive/MM_Matrix_s04_d07_noise_mix","index": 263736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d08_amp","varname": "fHslider269","shortname": "MM_Matrix_s04_d08_amp","address": "/modular-additive/MM_Matrix_s04_d08_amp","index": 263540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d09_pan","varname": "fHslider198","shortname": "MM_Matrix_s04_d09_pan","address": "/modular-additive/MM_Matrix_s04_d09_pan","index": 263240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d00_pitch","varname": "fHslider367","shortname": "MM_Matrix_s05_d00_pitch","address": "/modular-additive/MM_Matrix_s05_d00_pitch","index": 263960,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d01_bright","varname": "fHslider566","shortname": "MM_Matrix_s05_d01_bright","address": "/modular-additive/MM_Matrix_s05_d01_bright","index": 264780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d02_tilt","varname": "fHslider614","shortname": "MM_Matrix_s05_d02_tilt","address": "/modular-additive/MM_Matrix_s05_d02_tilt","index": 264972,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d03_inharmonicity","varname": "fHslider418","shortname": "MM_Matrix_s05_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s05_d03_inharmonicity","index": 264176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d04_odd_even","varname": "fHslider663","shortname": "MM_Matrix_s05_d04_odd_even","address": "/modular-additive/MM_Matrix_s05_d04_odd_even","index": 265168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d05_formant_ctr","varname": "fHslider467","shortname": "MM_Matrix_s05_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s05_d05_formant_ctr","index": 264384,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d06_formant_depth","varname": "fHslider517","shortname": "MM_Matrix_s05_d06_formant_depth","address": "/modular-additive/MM_Matrix_s05_d06_formant_depth","index": 264584,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d07_noise_mix","varname": "fHslider317","shortname": "MM_Matrix_s05_d07_noise_mix","address": "/modular-additive/MM_Matrix_s05_d07_noise_mix","index": 263732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d08_amp","varname": "fHslider268","shortname": "MM_Matrix_s05_d08_amp","address": "/modular-additive/MM_Matrix_s05_d08_amp","index": 263536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d09_pan","varname": "fHslider192","shortname": "MM_Matrix_s05_d09_pan","address": "/modular-additive/MM_Matrix_s05_d09_pan","index": 263216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d00_pitch","varname": "fHslider366","shortname": "MM_Matrix_s06_d00_pitch","address": "/modular-additive/MM_Matrix_s06_d00_pitch","index": 263956,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d01_bright","varname": "fHslider565","shortname": "MM_Matrix_s06_d01_bright","address": "/modular-additive/MM_Matrix_s06_d01_bright","index": 264776,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d02_tilt","varname": "fHslider613","shortname": "MM_Matrix_s06_d02_tilt","address": "/modular-additive/MM_Matrix_s06_d02_tilt","index": 264968,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d03_inharmonicity","varname": "fHslider417","shortname": "MM_Matrix_s06_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s06_d03_inharmonicity","index": 264172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d04_odd_even","varname": "fHslider662","shortname": "MM_Matrix_s06_d04_odd_even","address": "/modular-additive/MM_Matrix_s06_d04_odd_even","index": 265164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d05_formant_ctr","varname": "fHslider466","shortname": "MM_Matrix_s06_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s06_d05_formant_ctr","index": 264380,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d06_formant_depth","varname": "fHslider516","shortname": "MM_Matrix_s06_d06_formant_depth","address": "/modular-additive/MM_Matrix_s06_d06_formant_depth","index": 264580,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d07_noise_mix","varname": "fHslider316","shortname": "MM_Matrix_s06_d07_noise_mix","address": "/modular-additive/MM_Matrix_s06_d07_noise_mix","index": 263728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d08_amp","varname": "fHslider267","shortname": "MM_Matrix_s06_d08_amp","address": "/modular-additive/MM_Matrix_s06_d08_amp","index": 263532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d09_pan","varname": "fHslider186","shortname": "MM_Matrix_s06_d09_pan","address": "/modular-additive/MM_Matrix_s06_d09_pan","index": 263192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d00_pitch","varname": "fHslider365","shortname": "MM_Matrix_s07_d00_pitch","address": "/modular-additive/MM_Matrix_s07_d00_pitch","index": 263952,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d01_bright","varname": "fHslider564","shortname": "MM_Matrix_s07_d01_bright","address": "/modular-additive/MM_Matrix_s07_d01_bright","index": 264772,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d02_tilt","varname": "fHslider612","shortname": "MM_Matrix_s07_d02_tilt","address": "/modular-additive/MM_Matrix_s07_d02_tilt","index": 264964,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d03_inharmonicity","varname": "fHslider416","shortname": "MM_Matrix_s07_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s07_d03_inharmonicity","index": 264168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d04_odd_even","varname": "fHslider661","shortname": "MM_Matrix_s07_d04_odd_even","address": "/modular-additive/MM_Matrix_s07_d04_odd_even","index": 265160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d05_formant_ctr","varname": "fHslider465","shortname": "MM_Matrix_s07_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s07_d05_formant_ctr","index": 264376,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d06_formant_depth","varname": "fHslider515","shortname": "MM_Matrix_s07_d06_formant_depth","address": "/modular-additive/MM_Matrix_s07_d06_formant_depth","index": 264576,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d07_noise_mix","varname": "fHslider315","shortname": "MM_Matrix_s07_d07_noise_mix","address": "/modular-additive/MM_Matrix_s07_d07_noise_mix","index": 263724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d08_amp","varname": "fHslider266","shortname": "MM_Matrix_s07_d08_amp","address": "/modular-additive/MM_Matrix_s07_d08_amp","index": 263528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d09_pan","varname": "fHslider180","shortname": "MM_Matrix_s07_d09_pan","address": "/modular-additive/MM_Matrix_s07_d09_pan","index": 263168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d00_pitch","varname": "fHslider364","shortname": "MM_Matrix_s08_d00_pitch","address": "/modular-additive/MM_Matrix_s08_d00_pitch","index": 263948,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d01_bright","varname": "fHslider563","shortname": "MM_Matrix_s08_d01_bright","address": "/modular-additive/MM_Matrix_s08_d01_bright","index": 264768,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d02_tilt","varname": "fHslider611","shortname": "MM_Matrix_s08_d02_tilt","address": "/modular-additive/MM_Matrix_s08_d02_tilt","index": 264960,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d03_inharmonicity","varname": "fHslider415","shortname": "MM_Matrix_s08_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s08_d03_inharmonicity","index": 264164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d04_odd_even","varname": "fHslider660","shortname": "MM_Matrix_s08_d04_odd_even","address": "/modular-additive/MM_Matrix_s08_d04_odd_even","index": 265156,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d05_formant_ctr","varname": "fHslider464","shortname": "MM_Matrix_s08_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s08_d05_formant_ctr","index": 264372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d06_formant_depth","varname": "fHslider514","shortname": "MM_Matrix_s08_d06_formant_depth","address": "/modular-additive/MM_Matrix_s08_d06_formant_depth","index": 264572,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d07_noise_mix","varname": "fHslider314","shortname": "MM_Matrix_s08_d07_noise_mix","address": "/modular-additive/MM_Matrix_s08_d07_noise_mix","index": 263720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d08_amp","varname": "fHslider265","shortname": "MM_Matrix_s08_d08_amp","address": "/modular-additive/MM_Matrix_s08_d08_amp","index": 263524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d09_pan","varname": "fHslider174","shortname": "MM_Matrix_s08_d09_pan","address": "/modular-additive/MM_Matrix_s08_d09_pan","index": 263144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d00_pitch","varname": "fHslider363","shortname": "MM_Matrix_s09_d00_pitch","address": "/modular-additive/MM_Matrix_s09_d00_pitch","index": 263944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d01_bright","varname": "fHslider562","shortname": "MM_Matrix_s09_d01_bright","address": "/modular-additive/MM_Matrix_s09_d01_bright","index": 264764,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d02_tilt","varname": "fHslider610","shortname": "MM_Matrix_s09_d02_tilt","address": "/modular-additive/MM_Matrix_s09_d02_tilt","index": 264956,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d03_inharmonicity","varname": "fHslider414","shortname": "MM_Matrix_s09_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s09_d03_inharmonicity","index": 264160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d04_odd_even","varname": "fHslider659","shortname": "MM_Matrix_s09_d04_odd_even","address": "/modular-additive/MM_Matrix_s09_d04_odd_even","index": 265152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d05_formant_ctr","varname": "fHslider463","shortname": "MM_Matrix_s09_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s09_d05_formant_ctr","index": 264368,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d06_formant_depth","varname": "fHslider513","shortname": "MM_Matrix_s09_d06_formant_depth","address": "/modular-additive/MM_Matrix_s09_d06_formant_depth","index": 264568,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d07_noise_mix","varname": "fHslider313","shortname": "MM_Matrix_s09_d07_noise_mix","address": "/modular-additive/MM_Matrix_s09_d07_noise_mix","index": 263716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d08_amp","varname": "fHslider264","shortname": "MM_Matrix_s09_d08_amp","address": "/modular-additive/MM_Matrix_s09_d08_amp","index": 263520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d09_pan","varname": "fHslider168","shortname": "MM_Matrix_s09_d09_pan","address": "/modular-additive/MM_Matrix_s09_d09_pan","index": 263120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d00_pitch","varname": "fHslider362","shortname": "MM_Matrix_s10_d00_pitch","address": "/modular-additive/MM_Matrix_s10_d00_pitch","index": 263940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d01_bright","varname": "fHslider561","shortname": "MM_Matrix_s10_d01_bright","address": "/modular-additive/MM_Matrix_s10_d01_bright","index": 264760,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d02_tilt","varname": "fHslider609","shortname": "MM_Matrix_s10_d02_tilt","address": "/modular-additive/MM_Matrix_s10_d02_tilt","index": 264952,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d03_inharmonicity","varname": "fHslider413","shortname": "MM_Matrix_s10_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s10_d03_inharmonicity","index": 264156,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d04_odd_even","varname": "fHslider658","shortname": "MM_Matrix_s10_d04_odd_even","address": "/modular-additive/MM_Matrix_s10_d04_odd_even","index": 265148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d05_formant_ctr","varname": "fHslider462","shortname": "MM_Matrix_s10_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s10_d05_formant_ctr","index": 264364,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d06_formant_depth","varname": "fHslider512","shortname": "MM_Matrix_s10_d06_formant_depth","address": "/modular-additive/MM_Matrix_s10_d06_formant_depth","index": 264564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d07_noise_mix","varname": "fHslider312","shortname": "MM_Matrix_s10_d07_noise_mix","address": "/modular-additive/MM_Matrix_s10_d07_noise_mix","index": 263712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d08_amp","varname": "fHslider263","shortname": "MM_Matrix_s10_d08_amp","address": "/modular-additive/MM_Matrix_s10_d08_amp","index": 263516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d09_pan","varname": "fHslider162","shortname": "MM_Matrix_s10_d09_pan","address": "/modular-additive/MM_Matrix_s10_d09_pan","index": 263096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d00_pitch","varname": "fHslider361","shortname": "MM_Matrix_s11_d00_pitch","address": "/modular-additive/MM_Matrix_s11_d00_pitch","index": 263936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d01_bright","varname": "fHslider560","shortname": "MM_Matrix_s11_d01_bright","address": "/modular-additive/MM_Matrix_s11_d01_bright","index": 264756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d02_tilt","varname": "fHslider608","shortname": "MM_Matrix_s11_d02_tilt","address": "/modular-additive/MM_Matrix_s11_d02_tilt","index": 264948,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d03_inharmonicity","varname": "fHslider412","shortname": "MM_Matrix_s11_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s11_d03_inharmonicity","index": 264152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d04_odd_even","varname": "fHslider657","shortname": "MM_Matrix_s11_d04_odd_even","address": "/modular-additive/MM_Matrix_s11_d04_odd_even","index": 265144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d05_formant_ctr","varname": "fHslider461","shortname": "MM_Matrix_s11_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s11_d05_formant_ctr","index": 264360,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d06_formant_depth","varname": "fHslider511","shortname": "MM_Matrix_s11_d06_formant_depth","address": "/modular-additive/MM_Matrix_s11_d06_formant_depth","index": 264560,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d07_noise_mix","varname": "fHslider311","shortname": "MM_Matrix_s11_d07_noise_mix","address": "/modular-additive/MM_Matrix_s11_d07_noise_mix","index": 263708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d08_amp","varname": "fHslider262","shortname": "MM_Matrix_s11_d08_amp","address": "/modular-additive/MM_Matrix_s11_d08_amp","index": 263512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d09_pan","varname": "fHslider156","shortname": "MM_Matrix_s11_d09_pan","address": "/modular-additive/MM_Matrix_s11_d09_pan","index": 263072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d00_pitch","varname": "fHslider360","shortname": "MM_Matrix_s12_d00_pitch","address": "/modular-additive/MM_Matrix_s12_d00_pitch","index": 263932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d01_bright","varname": "fHslider559","shortname": "MM_Matrix_s12_d01_bright","address": "/modular-additive/MM_Matrix_s12_d01_bright","index": 264752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d02_tilt","varname": "fHslider607","shortname": "MM_Matrix_s12_d02_tilt","address": "/modular-additive/MM_Matrix_s12_d02_tilt","index": 264944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d03_inharmonicity","varname": "fHslider411","shortname": "MM_Matrix_s12_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s12_d03_inharmonicity","index": 264148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d04_odd_even","varname": "fHslider656","shortname": "MM_Matrix_s12_d04_odd_even","address": "/modular-additive/MM_Matrix_s12_d04_odd_even","index": 265140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d05_formant_ctr","varname": "fHslider460","shortname": "MM_Matrix_s12_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s12_d05_formant_ctr","index": 264356,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d06_formant_depth","varname": "fHslider510","shortname": "MM_Matrix_s12_d06_formant_depth","address": "/modular-additive/MM_Matrix_s12_d06_formant_depth","index": 264556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d07_noise_mix","varname": "fHslider310","shortname": "MM_Matrix_s12_d07_noise_mix","address": "/modular-additive/MM_Matrix_s12_d07_noise_mix","index": 263704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d08_amp","varname": "fHslider261","shortname": "MM_Matrix_s12_d08_amp","address": "/modular-additive/MM_Matrix_s12_d08_amp","index": 263508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d09_pan","varname": "fHslider150","shortname": "MM_Matrix_s12_d09_pan","address": "/modular-additive/MM_Matrix_s12_d09_pan","index": 263048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d00_pitch","varname": "fHslider359","shortname": "MM_Matrix_s13_d00_pitch","address": "/modular-additive/MM_Matrix_s13_d00_pitch","index": 263928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d01_bright","varname": "fHslider558","shortname": "MM_Matrix_s13_d01_bright","address": "/modular-additive/MM_Matrix_s13_d01_bright","index": 264748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d02_tilt","varname": "fHslider606","shortname": "MM_Matrix_s13_d02_tilt","address": "/modular-additive/MM_Matrix_s13_d02_tilt","index": 264940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d03_inharmonicity","varname": "fHslider410","shortname": "MM_Matrix_s13_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s13_d03_inharmonicity","index": 264144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d04_odd_even","varname": "fHslider655","shortname": "MM_Matrix_s13_d04_odd_even","address": "/modular-additive/MM_Matrix_s13_d04_odd_even","index": 265136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d05_formant_ctr","varname": "fHslider459","shortname": "MM_Matrix_s13_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s13_d05_formant_ctr","index": 264352,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d06_formant_depth","varname": "fHslider509","shortname": "MM_Matrix_s13_d06_formant_depth","address": "/modular-additive/MM_Matrix_s13_d06_formant_depth","index": 264552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d07_noise_mix","varname": "fHslider309","shortname": "MM_Matrix_s13_d07_noise_mix","address": "/modular-additive/MM_Matrix_s13_d07_noise_mix","index": 263700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d08_amp","varname": "fHslider260","shortname": "MM_Matrix_s13_d08_amp","address": "/modular-additive/MM_Matrix_s13_d08_amp","index": 263504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d09_pan","varname": "fHslider144","shortname": "MM_Matrix_s13_d09_pan","address": "/modular-additive/MM_Matrix_s13_d09_pan","index": 263024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d00_pitch","varname": "fHslider358","shortname": "MM_Matrix_s14_d00_pitch","address": "/modular-additive/MM_Matrix_s14_d00_pitch","index": 263924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d01_bright","varname": "fHslider557","shortname": "MM_Matrix_s14_d01_bright","address": "/modular-additive/MM_Matrix_s14_d01_bright","index": 264744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d02_tilt","varname": "fHslider605","shortname": "MM_Matrix_s14_d02_tilt","address": "/modular-additive/MM_Matrix_s14_d02_tilt","index": 264936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d03_inharmonicity","varname": "fHslider409","shortname": "MM_Matrix_s14_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s14_d03_inharmonicity","index": 264140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d04_odd_even","varname": "fHslider654","shortname": "MM_Matrix_s14_d04_odd_even","address": "/modular-additive/MM_Matrix_s14_d04_odd_even","index": 265132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d05_formant_ctr","varname": "fHslider458","shortname": "MM_Matrix_s14_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s14_d05_formant_ctr","index": 264348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d06_formant_depth","varname": "fHslider508","shortname": "MM_Matrix_s14_d06_formant_depth","address": "/modular-additive/MM_Matrix_s14_d06_formant_depth","index": 264548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d07_noise_mix","varname": "fHslider308","shortname": "MM_Matrix_s14_d07_noise_mix","address": "/modular-additive/MM_Matrix_s14_d07_noise_mix","index": 263696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d08_amp","varname": "fHslider259","shortname": "MM_Matrix_s14_d08_amp","address": "/modular-additive/MM_Matrix_s14_d08_amp","index": 263500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d09_pan","varname": "fHslider138","shortname": "MM_Matrix_s14_d09_pan","address": "/modular-additive/MM_Matrix_s14_d09_pan","index": 263000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d00_pitch","varname": "fHslider357","shortname": "MM_Matrix_s15_d00_pitch","address": "/modular-additive/MM_Matrix_s15_d00_pitch","index": 263920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d01_bright","varname": "fHslider556","shortname": "MM_Matrix_s15_d01_bright","address": "/modular-additive/MM_Matrix_s15_d01_bright","index": 264740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d02_tilt","varname": "fHslider604","shortname": "MM_Matrix_s15_d02_tilt","address": "/modular-additive/MM_Matrix_s15_d02_tilt","index": 264932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d03_inharmonicity","varname": "fHslider408","shortname": "MM_Matrix_s15_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s15_d03_inharmonicity","index": 264136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d04_odd_even","varname": "fHslider653","shortname": "MM_Matrix_s15_d04_odd_even","address": "/modular-additive/MM_Matrix_s15_d04_odd_even","index": 265128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d05_formant_ctr","varname": "fHslider457","shortname": "MM_Matrix_s15_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s15_d05_formant_ctr","index": 264344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d06_formant_depth","varname": "fHslider507","shortname": "MM_Matrix_s15_d06_formant_depth","address": "/modular-additive/MM_Matrix_s15_d06_formant_depth","index": 264544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d07_noise_mix","varname": "fHslider307","shortname": "MM_Matrix_s15_d07_noise_mix","address": "/modular-additive/MM_Matrix_s15_d07_noise_mix","index": 263692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d08_amp","varname": "fHslider258","shortname": "MM_Matrix_s15_d08_amp","address": "/modular-additive/MM_Matrix_s15_d08_amp","index": 263496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d09_pan","varname": "fHslider132","shortname": "MM_Matrix_s15_d09_pan","address": "/modular-additive/MM_Matrix_s15_d09_pan","index": 262976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d00_pitch","varname": "fHslider356","shortname": "MM_Matrix_s16_d00_pitch","address": "/modular-additive/MM_Matrix_s16_d00_pitch","index": 263916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d01_bright","varname": "fHslider555","shortname": "MM_Matrix_s16_d01_bright","address": "/modular-additive/MM_Matrix_s16_d01_bright","index": 264736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d02_tilt","varname": "fHslider603","shortname": "MM_Matrix_s16_d02_tilt","address": "/modular-additive/MM_Matrix_s16_d02_tilt","index": 264928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d03_inharmonicity","varname": "fHslider407","shortname": "MM_Matrix_s16_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s16_d03_inharmonicity","index": 264132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d04_odd_even","varname": "fHslider652","shortname": "MM_Matrix_s16_d04_odd_even","address": "/modular-additive/MM_Matrix_s16_d04_odd_even","index": 265124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d05_formant_ctr","varname": "fHslider456","shortname": "MM_Matrix_s16_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s16_d05_formant_ctr","index": 264340,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d06_formant_depth","varname": "fHslider506","shortname": "MM_Matrix_s16_d06_formant_depth","address": "/modular-additive/MM_Matrix_s16_d06_formant_depth","index": 264540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d07_noise_mix","varname": "fHslider306","shortname": "MM_Matrix_s16_d07_noise_mix","address": "/modular-additive/MM_Matrix_s16_d07_noise_mix","index": 263688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d08_amp","varname": "fHslider257","shortname": "MM_Matrix_s16_d08_amp","address": "/modular-additive/MM_Matrix_s16_d08_amp","index": 263492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d09_pan","varname": "fHslider126","shortname": "MM_Matrix_s16_d09_pan","address": "/modular-additive/MM_Matrix_s16_d09_pan","index": 262924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d00_pitch","varname": "fHslider355","shortname": "MM_Matrix_s17_d00_pitch","address": "/modular-additive/MM_Matrix_s17_d00_pitch","index": 263912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d01_bright","varname": "fHslider554","shortname": "MM_Matrix_s17_d01_bright","address": "/modular-additive/MM_Matrix_s17_d01_bright","index": 264732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d02_tilt","varname": "fHslider602","shortname": "MM_Matrix_s17_d02_tilt","address": "/modular-additive/MM_Matrix_s17_d02_tilt","index": 264924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d03_inharmonicity","varname": "fHslider406","shortname": "MM_Matrix_s17_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s17_d03_inharmonicity","index": 264128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d04_odd_even","varname": "fHslider651","shortname": "MM_Matrix_s17_d04_odd_even","address": "/modular-additive/MM_Matrix_s17_d04_odd_even","index": 265120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d05_formant_ctr","varname": "fHslider455","shortname": "MM_Matrix_s17_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s17_d05_formant_ctr","index": 264336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d06_formant_depth","varname": "fHslider505","shortname": "MM_Matrix_s17_d06_formant_depth","address": "/modular-additive/MM_Matrix_s17_d06_formant_depth","index": 264536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d07_noise_mix","varname": "fHslider305","shortname": "MM_Matrix_s17_d07_noise_mix","address": "/modular-additive/MM_Matrix_s17_d07_noise_mix","index": 263684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d08_amp","varname": "fHslider256","shortname": "MM_Matrix_s17_d08_amp","address": "/modular-additive/MM_Matrix_s17_d08_amp","index": 263488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d09_pan","varname": "fHslider122","shortname": "MM_Matrix_s17_d09_pan","address": "/modular-additive/MM_Matrix_s17_d09_pan","index": 262900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d00_pitch","varname": "fHslider354","shortname": "MM_Matrix_s18_d00_pitch","address": "/modular-additive/MM_Matrix_s18_d00_pitch","index": 263908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d01_bright","varname": "fHslider553","shortname": "MM_Matrix_s18_d01_bright","address": "/modular-additive/MM_Matrix_s18_d01_bright","index": 264728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d02_tilt","varname": "fHslider601","shortname": "MM_Matrix_s18_d02_tilt","address": "/modular-additive/MM_Matrix_s18_d02_tilt","index": 264920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d03_inharmonicity","varname": "fHslider405","shortname": "MM_Matrix_s18_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s18_d03_inharmonicity","index": 264124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d04_odd_even","varname": "fHslider650","shortname": "MM_Matrix_s18_d04_odd_even","address": "/modular-additive/MM_Matrix_s18_d04_odd_even","index": 265116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d05_formant_ctr","varname": "fHslider454","shortname": "MM_Matrix_s18_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s18_d05_formant_ctr","index": 264332,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d06_formant_depth","varname": "fHslider504","shortname": "MM_Matrix_s18_d06_formant_depth","address": "/modular-additive/MM_Matrix_s18_d06_formant_depth","index": 264532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d07_noise_mix","varname": "fHslider304","shortname": "MM_Matrix_s18_d07_noise_mix","address": "/modular-additive/MM_Matrix_s18_d07_noise_mix","index": 263680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d08_amp","varname": "fHslider255","shortname": "MM_Matrix_s18_d08_amp","address": "/modular-additive/MM_Matrix_s18_d08_amp","index": 263484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d09_pan","varname": "fHslider118","shortname": "MM_Matrix_s18_d09_pan","address": "/modular-additive/MM_Matrix_s18_d09_pan","index": 262876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d00_pitch","varname": "fHslider353","shortname": "MM_Matrix_s19_d00_pitch","address": "/modular-additive/MM_Matrix_s19_d00_pitch","index": 263904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d01_bright","varname": "fHslider552","shortname": "MM_Matrix_s19_d01_bright","address": "/modular-additive/MM_Matrix_s19_d01_bright","index": 264724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d02_tilt","varname": "fHslider600","shortname": "MM_Matrix_s19_d02_tilt","address": "/modular-additive/MM_Matrix_s19_d02_tilt","index": 264916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d03_inharmonicity","varname": "fHslider404","shortname": "MM_Matrix_s19_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s19_d03_inharmonicity","index": 264120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d04_odd_even","varname": "fHslider649","shortname": "MM_Matrix_s19_d04_odd_even","address": "/modular-additive/MM_Matrix_s19_d04_odd_even","index": 265112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d05_formant_ctr","varname": "fHslider453","shortname": "MM_Matrix_s19_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s19_d05_formant_ctr","index": 264328,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d06_formant_depth","varname": "fHslider503","shortname": "MM_Matrix_s19_d06_formant_depth","address": "/modular-additive/MM_Matrix_s19_d06_formant_depth","index": 264528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d07_noise_mix","varname": "fHslider303","shortname": "MM_Matrix_s19_d07_noise_mix","address": "/modular-additive/MM_Matrix_s19_d07_noise_mix","index": 263676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d08_amp","varname": "fHslider254","shortname": "MM_Matrix_s19_d08_amp","address": "/modular-additive/MM_Matrix_s19_d08_amp","index": 263480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d09_pan","varname": "fHslider114","shortname": "MM_Matrix_s19_d09_pan","address": "/modular-additive/MM_Matrix_s19_d09_pan","index": 262852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d00_pitch","varname": "fHslider352","shortname": "MM_Matrix_s20_d00_pitch","address": "/modular-additive/MM_Matrix_s20_d00_pitch","index": 263900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d01_bright","varname": "fHslider551","shortname": "MM_Matrix_s20_d01_bright","address": "/modular-additive/MM_Matrix_s20_d01_bright","index": 264720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d02_tilt","varname": "fHslider599","shortname": "MM_Matrix_s20_d02_tilt","address": "/modular-additive/MM_Matrix_s20_d02_tilt","index": 264912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d03_inharmonicity","varname": "fHslider403","shortname": "MM_Matrix_s20_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s20_d03_inharmonicity","index": 264116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d04_odd_even","varname": "fHslider648","shortname": "MM_Matrix_s20_d04_odd_even","address": "/modular-additive/MM_Matrix_s20_d04_odd_even","index": 265108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d05_formant_ctr","varname": "fHslider452","shortname": "MM_Matrix_s20_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s20_d05_formant_ctr","index": 264324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d06_formant_depth","varname": "fHslider502","shortname": "MM_Matrix_s20_d06_formant_depth","address": "/modular-additive/MM_Matrix_s20_d06_formant_depth","index": 264524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d07_noise_mix","varname": "fHslider302","shortname": "MM_Matrix_s20_d07_noise_mix","address": "/modular-additive/MM_Matrix_s20_d07_noise_mix","index": 263672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d08_amp","varname": "fHslider253","shortname": "MM_Matrix_s20_d08_amp","address": "/modular-additive/MM_Matrix_s20_d08_amp","index": 263476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d09_pan","varname": "fHslider110","shortname": "MM_Matrix_s20_d09_pan","address": "/modular-additive/MM_Matrix_s20_d09_pan","index": 262828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d00_pitch","varname": "fHslider351","shortname": "MM_Matrix_s21_d00_pitch","address": "/modular-additive/MM_Matrix_s21_d00_pitch","index": 263896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d01_bright","varname": "fHslider550","shortname": "MM_Matrix_s21_d01_bright","address": "/modular-additive/MM_Matrix_s21_d01_bright","index": 264716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d02_tilt","varname": "fHslider598","shortname": "MM_Matrix_s21_d02_tilt","address": "/modular-additive/MM_Matrix_s21_d02_tilt","index": 264908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d03_inharmonicity","varname": "fHslider402","shortname": "MM_Matrix_s21_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s21_d03_inharmonicity","index": 264112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d04_odd_even","varname": "fHslider647","shortname": "MM_Matrix_s21_d04_odd_even","address": "/modular-additive/MM_Matrix_s21_d04_odd_even","index": 265104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d05_formant_ctr","varname": "fHslider451","shortname": "MM_Matrix_s21_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s21_d05_formant_ctr","index": 264320,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d06_formant_depth","varname": "fHslider501","shortname": "MM_Matrix_s21_d06_formant_depth","address": "/modular-additive/MM_Matrix_s21_d06_formant_depth","index": 264520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d07_noise_mix","varname": "fHslider301","shortname": "MM_Matrix_s21_d07_noise_mix","address": "/modular-additive/MM_Matrix_s21_d07_noise_mix","index": 263668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d08_amp","varname": "fHslider252","shortname": "MM_Matrix_s21_d08_amp","address": "/modular-additive/MM_Matrix_s21_d08_amp","index": 263472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d09_pan","varname": "fHslider106","shortname": "MM_Matrix_s21_d09_pan","address": "/modular-additive/MM_Matrix_s21_d09_pan","index": 262804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d00_pitch","varname": "fHslider350","shortname": "MM_Matrix_s22_d00_pitch","address": "/modular-additive/MM_Matrix_s22_d00_pitch","index": 263892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d01_bright","varname": "fHslider549","shortname": "MM_Matrix_s22_d01_bright","address": "/modular-additive/MM_Matrix_s22_d01_bright","index": 264712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d02_tilt","varname": "fHslider597","shortname": "MM_Matrix_s22_d02_tilt","address": "/modular-additive/MM_Matrix_s22_d02_tilt","index": 264904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d03_inharmonicity","varname": "fHslider401","shortname": "MM_Matrix_s22_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s22_d03_inharmonicity","index": 264108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d04_odd_even","varname": "fHslider646","shortname": "MM_Matrix_s22_d04_odd_even","address": "/modular-additive/MM_Matrix_s22_d04_odd_even","index": 265100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d05_formant_ctr","varname": "fHslider450","shortname": "MM_Matrix_s22_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s22_d05_formant_ctr","index": 264316,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d06_formant_depth","varname": "fHslider500","shortname": "MM_Matrix_s22_d06_formant_depth","address": "/modular-additive/MM_Matrix_s22_d06_formant_depth","index": 264516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d07_noise_mix","varname": "fHslider300","shortname": "MM_Matrix_s22_d07_noise_mix","address": "/modular-additive/MM_Matrix_s22_d07_noise_mix","index": 263664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d08_amp","varname": "fHslider251","shortname": "MM_Matrix_s22_d08_amp","address": "/modular-additive/MM_Matrix_s22_d08_amp","index": 263468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d09_pan","varname": "fHslider102","shortname": "MM_Matrix_s22_d09_pan","address": "/modular-additive/MM_Matrix_s22_d09_pan","index": 262780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d00_pitch","varname": "fHslider349","shortname": "MM_Matrix_s23_d00_pitch","address": "/modular-additive/MM_Matrix_s23_d00_pitch","index": 263888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d01_bright","varname": "fHslider548","shortname": "MM_Matrix_s23_d01_bright","address": "/modular-additive/MM_Matrix_s23_d01_bright","index": 264708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d02_tilt","varname": "fHslider596","shortname": "MM_Matrix_s23_d02_tilt","address": "/modular-additive/MM_Matrix_s23_d02_tilt","index": 264900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d03_inharmonicity","varname": "fHslider400","shortname": "MM_Matrix_s23_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s23_d03_inharmonicity","index": 264104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d04_odd_even","varname": "fHslider645","shortname": "MM_Matrix_s23_d04_odd_even","address": "/modular-additive/MM_Matrix_s23_d04_odd_even","index": 265096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d05_formant_ctr","varname": "fHslider449","shortname": "MM_Matrix_s23_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s23_d05_formant_ctr","index": 264312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d06_formant_depth","varname": "fHslider499","shortname": "MM_Matrix_s23_d06_formant_depth","address": "/modular-additive/MM_Matrix_s23_d06_formant_depth","index": 264512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d07_noise_mix","varname": "fHslider299","shortname": "MM_Matrix_s23_d07_noise_mix","address": "/modular-additive/MM_Matrix_s23_d07_noise_mix","index": 263660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d08_amp","varname": "fHslider250","shortname": "MM_Matrix_s23_d08_amp","address": "/modular-additive/MM_Matrix_s23_d08_amp","index": 263464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d09_pan","varname": "fHslider98","shortname": "MM_Matrix_s23_d09_pan","address": "/modular-additive/MM_Matrix_s23_d09_pan","index": 262756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d00_pitch","varname": "fHslider348","shortname": "MM_Matrix_s24_d00_pitch","address": "/modular-additive/MM_Matrix_s24_d00_pitch","index": 263884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d01_bright","varname": "fHslider547","shortname": "MM_Matrix_s24_d01_bright","address": "/modular-additive/MM_Matrix_s24_d01_bright","index": 264704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d02_tilt","varname": "fHslider595","shortname": "MM_Matrix_s24_d02_tilt","address": "/modular-additive/MM_Matrix_s24_d02_tilt","index": 264896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d03_inharmonicity","varname": "fHslider399","shortname": "MM_Matrix_s24_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s24_d03_inharmonicity","index": 264100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d04_odd_even","varname": "fHslider644","shortname": "MM_Matrix_s24_d04_odd_even","address": "/modular-additive/MM_Matrix_s24_d04_odd_even","index": 265092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d05_formant_ctr","varname": "fHslider448","shortname": "MM_Matrix_s24_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s24_d05_formant_ctr","index": 264308,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d06_formant_depth","varname": "fHslider498","shortname": "MM_Matrix_s24_d06_formant_depth","address": "/modular-additive/MM_Matrix_s24_d06_formant_depth","index": 264508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d07_noise_mix","varname": "fHslider298","shortname": "MM_Matrix_s24_d07_noise_mix","address": "/modular-additive/MM_Matrix_s24_d07_noise_mix","index": 263656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d08_amp","varname": "fHslider249","shortname": "MM_Matrix_s24_d08_amp","address": "/modular-additive/MM_Matrix_s24_d08_amp","index": 263460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d09_pan","varname": "fHslider94","shortname": "MM_Matrix_s24_d09_pan","address": "/modular-additive/MM_Matrix_s24_d09_pan","index": 262732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d00_pitch","varname": "fHslider347","shortname": "MM_Matrix_s25_d00_pitch","address": "/modular-additive/MM_Matrix_s25_d00_pitch","index": 263880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d01_bright","varname": "fHslider546","shortname": "MM_Matrix_s25_d01_bright","address": "/modular-additive/MM_Matrix_s25_d01_bright","index": 264700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d02_tilt","varname": "fHslider594","shortname": "MM_Matrix_s25_d02_tilt","address": "/modular-additive/MM_Matrix_s25_d02_tilt","index": 264892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d03_inharmonicity","varname": "fHslider398","shortname": "MM_Matrix_s25_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s25_d03_inharmonicity","index": 264096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d04_odd_even","varname": "fHslider643","shortname": "MM_Matrix_s25_d04_odd_even","address": "/modular-additive/MM_Matrix_s25_d04_odd_even","index": 265088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d05_formant_ctr","varname": "fHslider447","shortname": "MM_Matrix_s25_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s25_d05_formant_ctr","index": 264304,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d06_formant_depth","varname": "fHslider497","shortname": "MM_Matrix_s25_d06_formant_depth","address": "/modular-additive/MM_Matrix_s25_d06_formant_depth","index": 264504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d07_noise_mix","varname": "fHslider297","shortname": "MM_Matrix_s25_d07_noise_mix","address": "/modular-additive/MM_Matrix_s25_d07_noise_mix","index": 263652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d08_amp","varname": "fHslider248","shortname": "MM_Matrix_s25_d08_amp","address": "/modular-additive/MM_Matrix_s25_d08_amp","index": 263456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d09_pan","varname": "fHslider90","shortname": "MM_Matrix_s25_d09_pan","address": "/modular-additive/MM_Matrix_s25_d09_pan","index": 262708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d00_pitch","varname": "fHslider346","shortname": "MM_Matrix_s26_d00_pitch","address": "/modular-additive/MM_Matrix_s26_d00_pitch","index": 263876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d01_bright","varname": "fHslider545","shortname": "MM_Matrix_s26_d01_bright","address": "/modular-additive/MM_Matrix_s26_d01_bright","index": 264696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d02_tilt","varname": "fHslider593","shortname": "MM_Matrix_s26_d02_tilt","address": "/modular-additive/MM_Matrix_s26_d02_tilt","index": 264888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d03_inharmonicity","varname": "fHslider397","shortname": "MM_Matrix_s26_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s26_d03_inharmonicity","index": 264092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d04_odd_even","varname": "fHslider642","shortname": "MM_Matrix_s26_d04_odd_even","address": "/modular-additive/MM_Matrix_s26_d04_odd_even","index": 265084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d05_formant_ctr","varname": "fHslider446","shortname": "MM_Matrix_s26_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s26_d05_formant_ctr","index": 264300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d06_formant_depth","varname": "fHslider496","shortname": "MM_Matrix_s26_d06_formant_depth","address": "/modular-additive/MM_Matrix_s26_d06_formant_depth","index": 264500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d07_noise_mix","varname": "fHslider296","shortname": "MM_Matrix_s26_d07_noise_mix","address": "/modular-additive/MM_Matrix_s26_d07_noise_mix","index": 263648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d08_amp","varname": "fHslider247","shortname": "MM_Matrix_s26_d08_amp","address": "/modular-additive/MM_Matrix_s26_d08_amp","index": 263452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d09_pan","varname": "fHslider86","shortname": "MM_Matrix_s26_d09_pan","address": "/modular-additive/MM_Matrix_s26_d09_pan","index": 262684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d00_pitch","varname": "fHslider345","shortname": "MM_Matrix_s27_d00_pitch","address": "/modular-additive/MM_Matrix_s27_d00_pitch","index": 263872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d01_bright","varname": "fHslider544","shortname": "MM_Matrix_s27_d01_bright","address": "/modular-additive/MM_Matrix_s27_d01_bright","index": 264692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d02_tilt","varname": "fHslider592","shortname": "MM_Matrix_s27_d02_tilt","address": "/modular-additive/MM_Matrix_s27_d02_tilt","index": 264884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d03_inharmonicity","varname": "fHslider396","shortname": "MM_Matrix_s27_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s27_d03_inharmonicity","index": 264088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d04_odd_even","varname": "fHslider641","shortname": "MM_Matrix_s27_d04_odd_even","address": "/modular-additive/MM_Matrix_s27_d04_odd_even","index": 265080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d05_formant_ctr","varname": "fHslider445","shortname": "MM_Matrix_s27_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s27_d05_formant_ctr","index": 264296,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d06_formant_depth","varname": "fHslider495","shortname": "MM_Matrix_s27_d06_formant_depth","address": "/modular-additive/MM_Matrix_s27_d06_formant_depth","index": 264496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d07_noise_mix","varname": "fHslider295","shortname": "MM_Matrix_s27_d07_noise_mix","address": "/modular-additive/MM_Matrix_s27_d07_noise_mix","index": 263644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d08_amp","varname": "fHslider246","shortname": "MM_Matrix_s27_d08_amp","address": "/modular-additive/MM_Matrix_s27_d08_amp","index": 263448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d09_pan","varname": "fHslider82","shortname": "MM_Matrix_s27_d09_pan","address": "/modular-additive/MM_Matrix_s27_d09_pan","index": 262660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d00_pitch","varname": "fHslider344","shortname": "MM_Matrix_s28_d00_pitch","address": "/modular-additive/MM_Matrix_s28_d00_pitch","index": 263868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d01_bright","varname": "fHslider543","shortname": "MM_Matrix_s28_d01_bright","address": "/modular-additive/MM_Matrix_s28_d01_bright","index": 264688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d02_tilt","varname": "fHslider591","shortname": "MM_Matrix_s28_d02_tilt","address": "/modular-additive/MM_Matrix_s28_d02_tilt","index": 264880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d03_inharmonicity","varname": "fHslider395","shortname": "MM_Matrix_s28_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s28_d03_inharmonicity","index": 264084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d04_odd_even","varname": "fHslider640","shortname": "MM_Matrix_s28_d04_odd_even","address": "/modular-additive/MM_Matrix_s28_d04_odd_even","index": 265076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d05_formant_ctr","varname": "fHslider444","shortname": "MM_Matrix_s28_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s28_d05_formant_ctr","index": 264292,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d06_formant_depth","varname": "fHslider494","shortname": "MM_Matrix_s28_d06_formant_depth","address": "/modular-additive/MM_Matrix_s28_d06_formant_depth","index": 264492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d07_noise_mix","varname": "fHslider294","shortname": "MM_Matrix_s28_d07_noise_mix","address": "/modular-additive/MM_Matrix_s28_d07_noise_mix","index": 263640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d08_amp","varname": "fHslider245","shortname": "MM_Matrix_s28_d08_amp","address": "/modular-additive/MM_Matrix_s28_d08_amp","index": 263444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d09_pan","varname": "fHslider78","shortname": "MM_Matrix_s28_d09_pan","address": "/modular-additive/MM_Matrix_s28_d09_pan","index": 262636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d00_pitch","varname": "fHslider343","shortname": "MM_Matrix_s29_d00_pitch","address": "/modular-additive/MM_Matrix_s29_d00_pitch","index": 263864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d01_bright","varname": "fHslider542","shortname": "MM_Matrix_s29_d01_bright","address": "/modular-additive/MM_Matrix_s29_d01_bright","index": 264684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d02_tilt","varname": "fHslider590","shortname": "MM_Matrix_s29_d02_tilt","address": "/modular-additive/MM_Matrix_s29_d02_tilt","index": 264876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d03_inharmonicity","varname": "fHslider394","shortname": "MM_Matrix_s29_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s29_d03_inharmonicity","index": 264080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d04_odd_even","varname": "fHslider639","shortname": "MM_Matrix_s29_d04_odd_even","address": "/modular-additive/MM_Matrix_s29_d04_odd_even","index": 265072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d05_formant_ctr","varname": "fHslider443","shortname": "MM_Matrix_s29_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s29_d05_formant_ctr","index": 264288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d06_formant_depth","varname": "fHslider493","shortname": "MM_Matrix_s29_d06_formant_depth","address": "/modular-additive/MM_Matrix_s29_d06_formant_depth","index": 264488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d07_noise_mix","varname": "fHslider293","shortname": "MM_Matrix_s29_d07_noise_mix","address": "/modular-additive/MM_Matrix_s29_d07_noise_mix","index": 263636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d08_amp","varname": "fHslider244","shortname": "MM_Matrix_s29_d08_amp","address": "/modular-additive/MM_Matrix_s29_d08_amp","index": 263440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d09_pan","varname": "fHslider74","shortname": "MM_Matrix_s29_d09_pan","address": "/modular-additive/MM_Matrix_s29_d09_pan","index": 262612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d00_pitch","varname": "fHslider342","shortname": "MM_Matrix_s30_d00_pitch","address": "/modular-additive/MM_Matrix_s30_d00_pitch","index": 263860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d01_bright","varname": "fHslider541","shortname": "MM_Matrix_s30_d01_bright","address": "/modular-additive/MM_Matrix_s30_d01_bright","index": 264680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d02_tilt","varname": "fHslider589","shortname": "MM_Matrix_s30_d02_tilt","address": "/modular-additive/MM_Matrix_s30_d02_tilt","index": 264872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d03_inharmonicity","varname": "fHslider393","shortname": "MM_Matrix_s30_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s30_d03_inharmonicity","index": 264076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d04_odd_even","varname": "fHslider638","shortname": "MM_Matrix_s30_d04_odd_even","address": "/modular-additive/MM_Matrix_s30_d04_odd_even","index": 265068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d05_formant_ctr","varname": "fHslider442","shortname": "MM_Matrix_s30_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s30_d05_formant_ctr","index": 264284,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d06_formant_depth","varname": "fHslider492","shortname": "MM_Matrix_s30_d06_formant_depth","address": "/modular-additive/MM_Matrix_s30_d06_formant_depth","index": 264484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d07_noise_mix","varname": "fHslider292","shortname": "MM_Matrix_s30_d07_noise_mix","address": "/modular-additive/MM_Matrix_s30_d07_noise_mix","index": 263632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d08_amp","varname": "fHslider243","shortname": "MM_Matrix_s30_d08_amp","address": "/modular-additive/MM_Matrix_s30_d08_amp","index": 263436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d09_pan","varname": "fHslider70","shortname": "MM_Matrix_s30_d09_pan","address": "/modular-additive/MM_Matrix_s30_d09_pan","index": 262588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d00_pitch","varname": "fHslider341","shortname": "MM_Matrix_s31_d00_pitch","address": "/modular-additive/MM_Matrix_s31_d00_pitch","index": 263856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d01_bright","varname": "fHslider540","shortname": "MM_Matrix_s31_d01_bright","address": "/modular-additive/MM_Matrix_s31_d01_bright","index": 264676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d02_tilt","varname": "fHslider588","shortname": "MM_Matrix_s31_d02_tilt","address": "/modular-additive/MM_Matrix_s31_d02_tilt","index": 264868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d03_inharmonicity","varname": "fHslider392","shortname": "MM_Matrix_s31_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s31_d03_inharmonicity","index": 264072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d04_odd_even","varname": "fHslider637","shortname": "MM_Matrix_s31_d04_odd_even","address": "/modular-additive/MM_Matrix_s31_d04_odd_even","index": 265064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d05_formant_ctr","varname": "fHslider441","shortname": "MM_Matrix_s31_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s31_d05_formant_ctr","index": 264280,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d06_formant_depth","varname": "fHslider491","shortname": "MM_Matrix_s31_d06_formant_depth","address": "/modular-additive/MM_Matrix_s31_d06_formant_depth","index": 264480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d07_noise_mix","varname": "fHslider291","shortname": "MM_Matrix_s31_d07_noise_mix","address": "/modular-additive/MM_Matrix_s31_d07_noise_mix","index": 263628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d08_amp","varname": "fHslider242","shortname": "MM_Matrix_s31_d08_amp","address": "/modular-additive/MM_Matrix_s31_d08_amp","index": 263432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d09_pan","varname": "fHslider66","shortname": "MM_Matrix_s31_d09_pan","address": "/modular-additive/MM_Matrix_s31_d09_pan","index": 262564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d00_pitch","varname": "fHslider340","shortname": "MM_Matrix_s32_d00_pitch","address": "/modular-additive/MM_Matrix_s32_d00_pitch","index": 263852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d01_bright","varname": "fHslider539","shortname": "MM_Matrix_s32_d01_bright","address": "/modular-additive/MM_Matrix_s32_d01_bright","index": 264672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d02_tilt","varname": "fHslider587","shortname": "MM_Matrix_s32_d02_tilt","address": "/modular-additive/MM_Matrix_s32_d02_tilt","index": 264864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d03_inharmonicity","varname": "fHslider391","shortname": "MM_Matrix_s32_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s32_d03_inharmonicity","index": 264068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d04_odd_even","varname": "fHslider636","shortname": "MM_Matrix_s32_d04_odd_even","address": "/modular-additive/MM_Matrix_s32_d04_odd_even","index": 265060,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d05_formant_ctr","varname": "fHslider440","shortname": "MM_Matrix_s32_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s32_d05_formant_ctr","index": 264276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d06_formant_depth","varname": "fHslider490","shortname": "MM_Matrix_s32_d06_formant_depth","address": "/modular-additive/MM_Matrix_s32_d06_formant_depth","index": 264476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d07_noise_mix","varname": "fHslider290","shortname": "MM_Matrix_s32_d07_noise_mix","address": "/modular-additive/MM_Matrix_s32_d07_noise_mix","index": 263624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d08_amp","varname": "fHslider241","shortname": "MM_Matrix_s32_d08_amp","address": "/modular-additive/MM_Matrix_s32_d08_amp","index": 263428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d09_pan","varname": "fHslider62","shortname": "MM_Matrix_s32_d09_pan","address": "/modular-additive/MM_Matrix_s32_d09_pan","index": 262540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d00_pitch","varname": "fHslider339","shortname": "MM_Matrix_s33_d00_pitch","address": "/modular-additive/MM_Matrix_s33_d00_pitch","index": 263848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d01_bright","varname": "fHslider538","shortname": "MM_Matrix_s33_d01_bright","address": "/modular-additive/MM_Matrix_s33_d01_bright","index": 264668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d02_tilt","varname": "fHslider586","shortname": "MM_Matrix_s33_d02_tilt","address": "/modular-additive/MM_Matrix_s33_d02_tilt","index": 264860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d03_inharmonicity","varname": "fHslider390","shortname": "MM_Matrix_s33_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s33_d03_inharmonicity","index": 264064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d04_odd_even","varname": "fHslider635","shortname": "MM_Matrix_s33_d04_odd_even","address": "/modular-additive/MM_Matrix_s33_d04_odd_even","index": 265056,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d05_formant_ctr","varname": "fHslider439","shortname": "MM_Matrix_s33_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s33_d05_formant_ctr","index": 264272,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d06_formant_depth","varname": "fHslider489","shortname": "MM_Matrix_s33_d06_formant_depth","address": "/modular-additive/MM_Matrix_s33_d06_formant_depth","index": 264472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d07_noise_mix","varname": "fHslider289","shortname": "MM_Matrix_s33_d07_noise_mix","address": "/modular-additive/MM_Matrix_s33_d07_noise_mix","index": 263620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d08_amp","varname": "fHslider240","shortname": "MM_Matrix_s33_d08_amp","address": "/modular-additive/MM_Matrix_s33_d08_amp","index": 263424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d09_pan","varname": "fHslider58","shortname": "MM_Matrix_s33_d09_pan","address": "/modular-additive/MM_Matrix_s33_d09_pan","index": 262516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d00_pitch","varname": "fHslider338","shortname": "MM_Matrix_s34_d00_pitch","address": "/modular-additive/MM_Matrix_s34_d00_pitch","index": 263844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d01_bright","varname": "fHslider537","shortname": "MM_Matrix_s34_d01_bright","address": "/modular-additive/MM_Matrix_s34_d01_bright","index": 264664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d02_tilt","varname": "fHslider585","shortname": "MM_Matrix_s34_d02_tilt","address": "/modular-additive/MM_Matrix_s34_d02_tilt","index": 264856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d03_inharmonicity","varname": "fHslider389","shortname": "MM_Matrix_s34_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s34_d03_inharmonicity","index": 264060,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d04_odd_even","varname": "fHslider634","shortname": "MM_Matrix_s34_d04_odd_even","address": "/modular-additive/MM_Matrix_s34_d04_odd_even","index": 265052,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d05_formant_ctr","varname": "fHslider438","shortname": "MM_Matrix_s34_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s34_d05_formant_ctr","index": 264268,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d06_formant_depth","varname": "fHslider488","shortname": "MM_Matrix_s34_d06_formant_depth","address": "/modular-additive/MM_Matrix_s34_d06_formant_depth","index": 264468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d07_noise_mix","varname": "fHslider288","shortname": "MM_Matrix_s34_d07_noise_mix","address": "/modular-additive/MM_Matrix_s34_d07_noise_mix","index": 263616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d08_amp","varname": "fHslider239","shortname": "MM_Matrix_s34_d08_amp","address": "/modular-additive/MM_Matrix_s34_d08_amp","index": 263420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d09_pan","varname": "fHslider54","shortname": "MM_Matrix_s34_d09_pan","address": "/modular-additive/MM_Matrix_s34_d09_pan","index": 262492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d00_pitch","varname": "fHslider337","shortname": "MM_Matrix_s35_d00_pitch","address": "/modular-additive/MM_Matrix_s35_d00_pitch","index": 263840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d01_bright","varname": "fHslider536","shortname": "MM_Matrix_s35_d01_bright","address": "/modular-additive/MM_Matrix_s35_d01_bright","index": 264660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d02_tilt","varname": "fHslider584","shortname": "MM_Matrix_s35_d02_tilt","address": "/modular-additive/MM_Matrix_s35_d02_tilt","index": 264852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d03_inharmonicity","varname": "fHslider388","shortname": "MM_Matrix_s35_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s35_d03_inharmonicity","index": 264056,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d04_odd_even","varname": "fHslider633","shortname": "MM_Matrix_s35_d04_odd_even","address": "/modular-additive/MM_Matrix_s35_d04_odd_even","index": 265048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d05_formant_ctr","varname": "fHslider437","shortname": "MM_Matrix_s35_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s35_d05_formant_ctr","index": 264264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d06_formant_depth","varname": "fHslider487","shortname": "MM_Matrix_s35_d06_formant_depth","address": "/modular-additive/MM_Matrix_s35_d06_formant_depth","index": 264464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d07_noise_mix","varname": "fHslider287","shortname": "MM_Matrix_s35_d07_noise_mix","address": "/modular-additive/MM_Matrix_s35_d07_noise_mix","index": 263612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d08_amp","varname": "fHslider238","shortname": "MM_Matrix_s35_d08_amp","address": "/modular-additive/MM_Matrix_s35_d08_amp","index": 263416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d09_pan","varname": "fHslider50","shortname": "MM_Matrix_s35_d09_pan","address": "/modular-additive/MM_Matrix_s35_d09_pan","index": 262468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d00_pitch","varname": "fHslider336","shortname": "MM_Matrix_s36_d00_pitch","address": "/modular-additive/MM_Matrix_s36_d00_pitch","index": 263836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d01_bright","varname": "fHslider535","shortname": "MM_Matrix_s36_d01_bright","address": "/modular-additive/MM_Matrix_s36_d01_bright","index": 264656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d02_tilt","varname": "fHslider583","shortname": "MM_Matrix_s36_d02_tilt","address": "/modular-additive/MM_Matrix_s36_d02_tilt","index": 264848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d03_inharmonicity","varname": "fHslider387","shortname": "MM_Matrix_s36_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s36_d03_inharmonicity","index": 264052,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d04_odd_even","varname": "fHslider632","shortname": "MM_Matrix_s36_d04_odd_even","address": "/modular-additive/MM_Matrix_s36_d04_odd_even","index": 265044,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d05_formant_ctr","varname": "fHslider436","shortname": "MM_Matrix_s36_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s36_d05_formant_ctr","index": 264260,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d06_formant_depth","varname": "fHslider486","shortname": "MM_Matrix_s36_d06_formant_depth","address": "/modular-additive/MM_Matrix_s36_d06_formant_depth","index": 264460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d07_noise_mix","varname": "fHslider286","shortname": "MM_Matrix_s36_d07_noise_mix","address": "/modular-additive/MM_Matrix_s36_d07_noise_mix","index": 263608,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d08_amp","varname": "fHslider237","shortname": "MM_Matrix_s36_d08_amp","address": "/modular-additive/MM_Matrix_s36_d08_amp","index": 263412,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d09_pan","varname": "fHslider46","shortname": "MM_Matrix_s36_d09_pan","address": "/modular-additive/MM_Matrix_s36_d09_pan","index": 262444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d00_pitch","varname": "fHslider335","shortname": "MM_Matrix_s37_d00_pitch","address": "/modular-additive/MM_Matrix_s37_d00_pitch","index": 263832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d01_bright","varname": "fHslider534","shortname": "MM_Matrix_s37_d01_bright","address": "/modular-additive/MM_Matrix_s37_d01_bright","index": 264652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d02_tilt","varname": "fHslider582","shortname": "MM_Matrix_s37_d02_tilt","address": "/modular-additive/MM_Matrix_s37_d02_tilt","index": 264844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d03_inharmonicity","varname": "fHslider386","shortname": "MM_Matrix_s37_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s37_d03_inharmonicity","index": 264048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d04_odd_even","varname": "fHslider631","shortname": "MM_Matrix_s37_d04_odd_even","address": "/modular-additive/MM_Matrix_s37_d04_odd_even","index": 265040,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d05_formant_ctr","varname": "fHslider435","shortname": "MM_Matrix_s37_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s37_d05_formant_ctr","index": 264256,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d06_formant_depth","varname": "fHslider485","shortname": "MM_Matrix_s37_d06_formant_depth","address": "/modular-additive/MM_Matrix_s37_d06_formant_depth","index": 264456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d07_noise_mix","varname": "fHslider285","shortname": "MM_Matrix_s37_d07_noise_mix","address": "/modular-additive/MM_Matrix_s37_d07_noise_mix","index": 263604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d08_amp","varname": "fHslider236","shortname": "MM_Matrix_s37_d08_amp","address": "/modular-additive/MM_Matrix_s37_d08_amp","index": 263408,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d09_pan","varname": "fHslider42","shortname": "MM_Matrix_s37_d09_pan","address": "/modular-additive/MM_Matrix_s37_d09_pan","index": 262420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d00_pitch","varname": "fHslider334","shortname": "MM_Matrix_s38_d00_pitch","address": "/modular-additive/MM_Matrix_s38_d00_pitch","index": 263828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d01_bright","varname": "fHslider533","shortname": "MM_Matrix_s38_d01_bright","address": "/modular-additive/MM_Matrix_s38_d01_bright","index": 264648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d02_tilt","varname": "fHslider581","shortname": "MM_Matrix_s38_d02_tilt","address": "/modular-additive/MM_Matrix_s38_d02_tilt","index": 264840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d03_inharmonicity","varname": "fHslider385","shortname": "MM_Matrix_s38_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s38_d03_inharmonicity","index": 264044,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d04_odd_even","varname": "fHslider630","shortname": "MM_Matrix_s38_d04_odd_even","address": "/modular-additive/MM_Matrix_s38_d04_odd_even","index": 265036,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d05_formant_ctr","varname": "fHslider434","shortname": "MM_Matrix_s38_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s38_d05_formant_ctr","index": 264252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d06_formant_depth","varname": "fHslider484","shortname": "MM_Matrix_s38_d06_formant_depth","address": "/modular-additive/MM_Matrix_s38_d06_formant_depth","index": 264452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d07_noise_mix","varname": "fHslider284","shortname": "MM_Matrix_s38_d07_noise_mix","address": "/modular-additive/MM_Matrix_s38_d07_noise_mix","index": 263600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d08_amp","varname": "fHslider235","shortname": "MM_Matrix_s38_d08_amp","address": "/modular-additive/MM_Matrix_s38_d08_amp","index": 263404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d09_pan","varname": "fHslider38","shortname": "MM_Matrix_s38_d09_pan","address": "/modular-additive/MM_Matrix_s38_d09_pan","index": 262396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d00_pitch","varname": "fHslider333","shortname": "MM_Matrix_s39_d00_pitch","address": "/modular-additive/MM_Matrix_s39_d00_pitch","index": 263824,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d01_bright","varname": "fHslider532","shortname": "MM_Matrix_s39_d01_bright","address": "/modular-additive/MM_Matrix_s39_d01_bright","index": 264644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d02_tilt","varname": "fHslider580","shortname": "MM_Matrix_s39_d02_tilt","address": "/modular-additive/MM_Matrix_s39_d02_tilt","index": 264836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d03_inharmonicity","varname": "fHslider384","shortname": "MM_Matrix_s39_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s39_d03_inharmonicity","index": 264040,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d04_odd_even","varname": "fHslider629","shortname": "MM_Matrix_s39_d04_odd_even","address": "/modular-additive/MM_Matrix_s39_d04_odd_even","index": 265032,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d05_formant_ctr","varname": "fHslider433","shortname": "MM_Matrix_s39_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s39_d05_formant_ctr","index": 264248,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d06_formant_depth","varname": "fHslider483","shortname": "MM_Matrix_s39_d06_formant_depth","address": "/modular-additive/MM_Matrix_s39_d06_formant_depth","index": 264448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d07_noise_mix","varname": "fHslider283","shortname": "MM_Matrix_s39_d07_noise_mix","address": "/modular-additive/MM_Matrix_s39_d07_noise_mix","index": 263596,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d08_amp","varname": "fHslider234","shortname": "MM_Matrix_s39_d08_amp","address": "/modular-additive/MM_Matrix_s39_d08_amp","index": 263400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d09_pan","varname": "fHslider34","shortname": "MM_Matrix_s39_d09_pan","address": "/modular-additive/MM_Matrix_s39_d09_pan","index": 262372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d00_pitch","varname": "fHslider332","shortname": "MM_Matrix_s40_d00_pitch","address": "/modular-additive/MM_Matrix_s40_d00_pitch","index": 263820,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d01_bright","varname": "fHslider531","shortname": "MM_Matrix_s40_d01_bright","address": "/modular-additive/MM_Matrix_s40_d01_bright","index": 264640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d02_tilt","varname": "fHslider579","shortname": "MM_Matrix_s40_d02_tilt","address": "/modular-additive/MM_Matrix_s40_d02_tilt","index": 264832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d03_inharmonicity","varname": "fHslider383","shortname": "MM_Matrix_s40_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s40_d03_inharmonicity","index": 264036,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d04_odd_even","varname": "fHslider628","shortname": "MM_Matrix_s40_d04_odd_even","address": "/modular-additive/MM_Matrix_s40_d04_odd_even","index": 265028,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d05_formant_ctr","varname": "fHslider432","shortname": "MM_Matrix_s40_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s40_d05_formant_ctr","index": 264244,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d06_formant_depth","varname": "fHslider482","shortname": "MM_Matrix_s40_d06_formant_depth","address": "/modular-additive/MM_Matrix_s40_d06_formant_depth","index": 264444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d07_noise_mix","varname": "fHslider282","shortname": "MM_Matrix_s40_d07_noise_mix","address": "/modular-additive/MM_Matrix_s40_d07_noise_mix","index": 263592,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d08_amp","varname": "fHslider233","shortname": "MM_Matrix_s40_d08_amp","address": "/modular-additive/MM_Matrix_s40_d08_amp","index": 263396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d09_pan","varname": "fHslider30","shortname": "MM_Matrix_s40_d09_pan","address": "/modular-additive/MM_Matrix_s40_d09_pan","index": 262348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d00_pitch","varname": "fHslider331","shortname": "MM_Matrix_s41_d00_pitch","address": "/modular-additive/MM_Matrix_s41_d00_pitch","index": 263816,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d01_bright","varname": "fHslider530","shortname": "MM_Matrix_s41_d01_bright","address": "/modular-additive/MM_Matrix_s41_d01_bright","index": 264636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d02_tilt","varname": "fHslider578","shortname": "MM_Matrix_s41_d02_tilt","address": "/modular-additive/MM_Matrix_s41_d02_tilt","index": 264828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d03_inharmonicity","varname": "fHslider382","shortname": "MM_Matrix_s41_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s41_d03_inharmonicity","index": 264032,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d04_odd_even","varname": "fHslider627","shortname": "MM_Matrix_s41_d04_odd_even","address": "/modular-additive/MM_Matrix_s41_d04_odd_even","index": 265024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d05_formant_ctr","varname": "fHslider431","shortname": "MM_Matrix_s41_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s41_d05_formant_ctr","index": 264240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d06_formant_depth","varname": "fHslider481","shortname": "MM_Matrix_s41_d06_formant_depth","address": "/modular-additive/MM_Matrix_s41_d06_formant_depth","index": 264440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d07_noise_mix","varname": "fHslider281","shortname": "MM_Matrix_s41_d07_noise_mix","address": "/modular-additive/MM_Matrix_s41_d07_noise_mix","index": 263588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d08_amp","varname": "fHslider232","shortname": "MM_Matrix_s41_d08_amp","address": "/modular-additive/MM_Matrix_s41_d08_amp","index": 263392,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d09_pan","varname": "fHslider26","shortname": "MM_Matrix_s41_d09_pan","address": "/modular-additive/MM_Matrix_s41_d09_pan","index": 262324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d00_pitch","varname": "fHslider330","shortname": "MM_Matrix_s42_d00_pitch","address": "/modular-additive/MM_Matrix_s42_d00_pitch","index": 263812,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d01_bright","varname": "fHslider529","shortname": "MM_Matrix_s42_d01_bright","address": "/modular-additive/MM_Matrix_s42_d01_bright","index": 264632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d02_tilt","varname": "fHslider577","shortname": "MM_Matrix_s42_d02_tilt","address": "/modular-additive/MM_Matrix_s42_d02_tilt","index": 264824,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d03_inharmonicity","varname": "fHslider381","shortname": "MM_Matrix_s42_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s42_d03_inharmonicity","index": 264028,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d04_odd_even","varname": "fHslider626","shortname": "MM_Matrix_s42_d04_odd_even","address": "/modular-additive/MM_Matrix_s42_d04_odd_even","index": 265020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d05_formant_ctr","varname": "fHslider430","shortname": "MM_Matrix_s42_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s42_d05_formant_ctr","index": 264236,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d06_formant_depth","varname": "fHslider480","shortname": "MM_Matrix_s42_d06_formant_depth","address": "/modular-additive/MM_Matrix_s42_d06_formant_depth","index": 264436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d07_noise_mix","varname": "fHslider280","shortname": "MM_Matrix_s42_d07_noise_mix","address": "/modular-additive/MM_Matrix_s42_d07_noise_mix","index": 263584,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d08_amp","varname": "fHslider231","shortname": "MM_Matrix_s42_d08_amp","address": "/modular-additive/MM_Matrix_s42_d08_amp","index": 263388,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d09_pan","varname": "fHslider22","shortname": "MM_Matrix_s42_d09_pan","address": "/modular-additive/MM_Matrix_s42_d09_pan","index": 262300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d00_pitch","varname": "fHslider329","shortname": "MM_Matrix_s43_d00_pitch","address": "/modular-additive/MM_Matrix_s43_d00_pitch","index": 263808,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d01_bright","varname": "fHslider528","shortname": "MM_Matrix_s43_d01_bright","address": "/modular-additive/MM_Matrix_s43_d01_bright","index": 264628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d02_tilt","varname": "fHslider576","shortname": "MM_Matrix_s43_d02_tilt","address": "/modular-additive/MM_Matrix_s43_d02_tilt","index": 264820,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d03_inharmonicity","varname": "fHslider380","shortname": "MM_Matrix_s43_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s43_d03_inharmonicity","index": 264024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d04_odd_even","varname": "fHslider625","shortname": "MM_Matrix_s43_d04_odd_even","address": "/modular-additive/MM_Matrix_s43_d04_odd_even","index": 265016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d05_formant_ctr","varname": "fHslider429","shortname": "MM_Matrix_s43_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s43_d05_formant_ctr","index": 264232,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d06_formant_depth","varname": "fHslider479","shortname": "MM_Matrix_s43_d06_formant_depth","address": "/modular-additive/MM_Matrix_s43_d06_formant_depth","index": 264432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d07_noise_mix","varname": "fHslider279","shortname": "MM_Matrix_s43_d07_noise_mix","address": "/modular-additive/MM_Matrix_s43_d07_noise_mix","index": 263580,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d08_amp","varname": "fHslider230","shortname": "MM_Matrix_s43_d08_amp","address": "/modular-additive/MM_Matrix_s43_d08_amp","index": 263384,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d09_pan","varname": "fHslider18","shortname": "MM_Matrix_s43_d09_pan","address": "/modular-additive/MM_Matrix_s43_d09_pan","index": 262276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d00_pitch","varname": "fHslider328","shortname": "MM_Matrix_s44_d00_pitch","address": "/modular-additive/MM_Matrix_s44_d00_pitch","index": 263804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d01_bright","varname": "fHslider527","shortname": "MM_Matrix_s44_d01_bright","address": "/modular-additive/MM_Matrix_s44_d01_bright","index": 264624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d02_tilt","varname": "fHslider575","shortname": "MM_Matrix_s44_d02_tilt","address": "/modular-additive/MM_Matrix_s44_d02_tilt","index": 264816,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d03_inharmonicity","varname": "fHslider379","shortname": "MM_Matrix_s44_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s44_d03_inharmonicity","index": 264020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d04_odd_even","varname": "fHslider624","shortname": "MM_Matrix_s44_d04_odd_even","address": "/modular-additive/MM_Matrix_s44_d04_odd_even","index": 265012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d05_formant_ctr","varname": "fHslider428","shortname": "MM_Matrix_s44_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s44_d05_formant_ctr","index": 264228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d06_formant_depth","varname": "fHslider478","shortname": "MM_Matrix_s44_d06_formant_depth","address": "/modular-additive/MM_Matrix_s44_d06_formant_depth","index": 264428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d07_noise_mix","varname": "fHslider278","shortname": "MM_Matrix_s44_d07_noise_mix","address": "/modular-additive/MM_Matrix_s44_d07_noise_mix","index": 263576,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d08_amp","varname": "fHslider229","shortname": "MM_Matrix_s44_d08_amp","address": "/modular-additive/MM_Matrix_s44_d08_amp","index": 263380,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d09_pan","varname": "fHslider14","shortname": "MM_Matrix_s44_d09_pan","address": "/modular-additive/MM_Matrix_s44_d09_pan","index": 262252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d00_pitch","varname": "fHslider327","shortname": "MM_Matrix_s45_d00_pitch","address": "/modular-additive/MM_Matrix_s45_d00_pitch","index": 263800,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d01_bright","varname": "fHslider526","shortname": "MM_Matrix_s45_d01_bright","address": "/modular-additive/MM_Matrix_s45_d01_bright","index": 264620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d02_tilt","varname": "fHslider574","shortname": "MM_Matrix_s45_d02_tilt","address": "/modular-additive/MM_Matrix_s45_d02_tilt","index": 264812,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d03_inharmonicity","varname": "fHslider378","shortname": "MM_Matrix_s45_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s45_d03_inharmonicity","index": 264016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d04_odd_even","varname": "fHslider623","shortname": "MM_Matrix_s45_d04_odd_even","address": "/modular-additive/MM_Matrix_s45_d04_odd_even","index": 265008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d05_formant_ctr","varname": "fHslider427","shortname": "MM_Matrix_s45_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s45_d05_formant_ctr","index": 264224,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d06_formant_depth","varname": "fHslider477","shortname": "MM_Matrix_s45_d06_formant_depth","address": "/modular-additive/MM_Matrix_s45_d06_formant_depth","index": 264424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d07_noise_mix","varname": "fHslider277","shortname": "MM_Matrix_s45_d07_noise_mix","address": "/modular-additive/MM_Matrix_s45_d07_noise_mix","index": 263572,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d08_amp","varname": "fHslider228","shortname": "MM_Matrix_s45_d08_amp","address": "/modular-additive/MM_Matrix_s45_d08_amp","index": 263376,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d09_pan","varname": "fHslider10","shortname": "MM_Matrix_s45_d09_pan","address": "/modular-additive/MM_Matrix_s45_d09_pan","index": 262228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d00_pitch","varname": "fHslider326","shortname": "MM_Matrix_s46_d00_pitch","address": "/modular-additive/MM_Matrix_s46_d00_pitch","index": 263796,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d01_bright","varname": "fHslider525","shortname": "MM_Matrix_s46_d01_bright","address": "/modular-additive/MM_Matrix_s46_d01_bright","index": 264616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d02_tilt","varname": "fHslider573","shortname": "MM_Matrix_s46_d02_tilt","address": "/modular-additive/MM_Matrix_s46_d02_tilt","index": 264808,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d03_inharmonicity","varname": "fHslider377","shortname": "MM_Matrix_s46_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s46_d03_inharmonicity","index": 264012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d04_odd_even","varname": "fHslider622","shortname": "MM_Matrix_s46_d04_odd_even","address": "/modular-additive/MM_Matrix_s46_d04_odd_even","index": 265004,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d05_formant_ctr","varname": "fHslider426","shortname": "MM_Matrix_s46_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s46_d05_formant_ctr","index": 264220,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d06_formant_depth","varname": "fHslider476","shortname": "MM_Matrix_s46_d06_formant_depth","address": "/modular-additive/MM_Matrix_s46_d06_formant_depth","index": 264420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d07_noise_mix","varname": "fHslider276","shortname": "MM_Matrix_s46_d07_noise_mix","address": "/modular-additive/MM_Matrix_s46_d07_noise_mix","index": 263568,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d08_amp","varname": "fHslider227","shortname": "MM_Matrix_s46_d08_amp","address": "/modular-additive/MM_Matrix_s46_d08_amp","index": 263372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d09_pan","varname": "fHslider6","shortname": "MM_Matrix_s46_d09_pan","address": "/modular-additive/MM_Matrix_s46_d09_pan","index": 262204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d00_pitch","varname": "fHslider325","shortname": "MM_Matrix_s47_d00_pitch","address": "/modular-additive/MM_Matrix_s47_d00_pitch","index": 263792,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d01_bright","varname": "fHslider524","shortname": "MM_Matrix_s47_d01_bright","address": "/modular-additive/MM_Matrix_s47_d01_bright","index": 264612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d02_tilt","varname": "fHslider572","shortname": "MM_Matrix_s47_d02_tilt","address": "/modular-additive/MM_Matrix_s47_d02_tilt","index": 264804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d03_inharmonicity","varname": "fHslider376","shortname": "MM_Matrix_s47_d03_inharmonicity","address": "/modular-additive/MM_Matrix_s47_d03_inharmonicity","index": 264008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d04_odd_even","varname": "fHslider621","shortname": "MM_Matrix_s47_d04_odd_even","address": "/modular-additive/MM_Matrix_s47_d04_odd_even","index": 265000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d05_formant_ctr","varname": "fHslider425","shortname": "MM_Matrix_s47_d05_formant_ctr","address": "/modular-additive/MM_Matrix_s47_d05_formant_ctr","index": 264216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d06_formant_depth","varname": "fHslider475","shortname": "MM_Matrix_s47_d06_formant_depth","address": "/modular-additive/MM_Matrix_s47_d06_formant_depth","index": 264416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d07_noise_mix","varname": "fHslider275","shortname": "MM_Matrix_s47_d07_noise_mix","address": "/modular-additive/MM_Matrix_s47_d07_noise_mix","index": 263564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d08_amp","varname": "fHslider226","shortname": "MM_Matrix_s47_d08_amp","address": "/modular-additive/MM_Matrix_s47_d08_amp","index": 263368,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d09_pan","varname": "fHslider2","shortname": "MM_Matrix_s47_d09_pan","address": "/modular-additive/MM_Matrix_s47_d09_pan","index": 262180,"init": 0,"min": -1,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/modular-additive.wasm b/playground/faust/modular-additive.wasm deleted file mode 100644 index b51710c7cc8d5cc7049108eb9dc4185261e7d2b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 351294 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%0A+fMuYVey=AXjq$ia2yDDqXZm?bHSd06i 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 float - * - * Hidden params (not in the engine's ML-controllable list): - * 0_Hidden/freq — set by noteOn - * 0_Hidden/gate — set by noteOn/noteOff (button, value 0/1) - * 0_Hidden/_vel — set by noteOn - */ - -// Must be imported in AudioWorkletGlobalScope — include faust-worklet-processor.js -// via audioCtx.audioWorklet.addModule() before this file. - -const DSP = 0; -const BLOCK_SIZE = 128; - -class ModularFmProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dspInst = null; - this._dspMemory = null; // live Float32Array view - this._sampleRate = 48000; - - // Zone tables, populated once the init message has been processed. - this._paramZones = []; // NISPS index (order from JSON walk) -> zone - this._paramZonesByLabel = {}; // label -> zone (for smoke test) - this._hiddenZones = {}; // {freq, gate, _vel} -> zone - - // Audio output buffer pointers - this._outPtrsAddr = 0; - this._outLAddr = 0; - this._outRAddr = 0; - } - - // --------------------------------------------------------------------------- - // _initWasm — called by FaustWorkletProcessor.handleMessage on 'init'. - // - // Extra fields on the init message: - // uiJson — parsed Faust UI descriptor (the .json file contents) for - // zone discovery. Faust's -lang wasm embeds numeric `index` - // fields that we use directly as zone memory addresses. - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sampleRate, uiJson) { - this._sampleRate = sampleRate; - - const importObj = { - env: { - _sinf: Math.sin, - _cosf: Math.cos, - _tanf: Math.tan, - _expf: Math.exp, - _logf: Math.log, - _log10f: Math.log10, - _powf: Math.pow, - _tanhf: Math.tanh, - _sqrtf: Math.sqrt, - _fabsf: Math.abs, - _floorf: Math.floor, - _ceilf: Math.ceil, - _remainderf: (a, b) => a - Math.round(a / b) * b, - _fmodf: (a, b) => a % b, - _roundf: Math.round, - _truncf: Math.trunc, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - }, - }; - - const result = await WebAssembly.instantiate(wasmBytes, importObj); - this._dspInst = result.instance; - const ex = this._dspInst.exports; - - // Grow memory by 2 pages (128 KB) so the output buffer region sits past - // Faust's allocated parameter zones. After growth we have at least - // 3 * BLOCK_SIZE * 4 bytes of scratch at the high end of memory. - ex.memory.grow(2); - - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Initialise the DSP instance (single-instance convention: dsp = 0) - ex.init(DSP, sampleRate); - - // Allocate output buffers at the top of WASM memory. - const memBytes = ex.memory.buffer.byteLength; - this._outLAddr = memBytes - BLOCK_SIZE * 4 * 3; - this._outRAddr = this._outLAddr + BLOCK_SIZE * 4; - this._outPtrsAddr = this._outRAddr + BLOCK_SIZE * 4; - - const u32 = new Uint32Array(ex.memory.buffer); - u32[this._outPtrsAddr / 4] = this._outLAddr; - u32[this._outPtrsAddr / 4 + 1] = this._outRAddr; - - // Refresh the float view after any potential reallocation - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Build the zone index from the JSON we were passed. - if (uiJson) { - this._buildZoneIndexFromJson(uiJson); - } else { - console.warn('[ModularFmProcessor] no uiJson in init message — ' + - 'setParam will silently no-op.'); - } - } - - // ------------------------------------------------------------------------- - // _buildZoneIndexFromJson — walk the Faust UI tree and collect numeric - // zone indexes, separating hidden params from the main NISPS-controllable - // list. Preserves the UI tree's traversal order, which matches the order - // the playground's faustJsonToParamMeta() parser will produce. - // ------------------------------------------------------------------------- - - _buildZoneIndexFromJson(faustJson) { - const HIDDEN_TAIL = new Set(['freq', 'gate', '_vel']); - const zones = []; - const byLabel = {}; - const hidden = {}; - - const walk = (items) => { - if (!Array.isArray(items)) return; - for (const item of items) { - const type = item?.type; - if (type === 'hslider' || type === 'vslider' || - type === 'nentry' || type === 'button' || - type === 'checkbox') { - const label = item.label ?? ''; - const idx = item.index; - if (typeof idx !== 'number') continue; - - const hasHiddenMeta = Array.isArray(item.meta) && - item.meta.some(m => m.hidden === '1' || m.hidden === 1); - const tail = label.includes('/') ? label.split('/').pop() : label; - - if (hasHiddenMeta || HIDDEN_TAIL.has(tail)) { - hidden[tail] = idx; - } else { - zones.push(idx); - byLabel[label] = idx; - } - } else if (item?.items) { - walk(item.items); - } - } - }; - - walk(faustJson.ui ?? []); - - this._paramZones = zones; - this._paramZonesByLabel = byLabel; - this._hiddenZones = hidden; - } - - // ------------------------------------------------------------------------- - // _onSetParam — look up the zone by NISPS index and poke the DSP - // ------------------------------------------------------------------------- - - _onSetParam(index, value) { - if (!this._dspInst) return; - const zone = this._paramZones[index]; - if (zone === undefined) return; - this._dspInst.exports.setParamValue(DSP, zone, value); - } - - _onNoteOn(freq, vel) { - if (!this._dspInst) return; - const ex = this._dspInst.exports; - const z = this._hiddenZones; - if (z.freq !== undefined) ex.setParamValue(DSP, z.freq, freq); - if (z._vel !== undefined) ex.setParamValue(DSP, z._vel, vel ?? 0.7); - if (z.gate !== undefined) ex.setParamValue(DSP, z.gate, 1.0); - } - - _onNoteOff(_freq) { - if (!this._dspInst) return; - const z = this._hiddenZones; - if (z.gate !== undefined) { - this._dspInst.exports.setParamValue(DSP, z.gate, 0.0); - } - } - - // ------------------------------------------------------------------------- - // Extended message handling — override for setByLabel + init-with-json - // ------------------------------------------------------------------------- - - _handleMessage(msg) { - if (!msg || !msg.type) return; - - if (msg.type === 'init') { - // Override the base class's init path so we can thread `uiJson` through. - this._initWasm(msg.wasmBytes, msg.sampleRate || sampleRate, msg.uiJson) - .then(() => { - this._ready = true; - this.port.postMessage({ type: 'ready' }); - this._drainPendingMessages(); - }) - .catch((err) => { - this.port.postMessage({ type: 'error', message: String(err) }); - }); - return; - } - - // Buffer anything that arrives before init() finishes. See - // faust-worklet-processor.js._queueIfNotReady. - if (this._queueIfNotReady(msg)) return; - - if (msg.type === 'setByLabel') { - const zone = this._paramZonesByLabel[msg.label]; - if (zone !== undefined) { - this._dspInst.exports.setParamValue(DSP, zone, msg.value); - } - return; - } - - // Delegate everything else (setParam, noteOn, noteOff) to the base class. - super._handleMessage(msg); - } - - // ------------------------------------------------------------------------- - // _renderBlock — call Faust compute, copy output buffers - // ------------------------------------------------------------------------- - - _renderBlock(outL, outR, blockSize) { - if (!this._dspInst || !this._dspMemory) return; - - const ex = this._dspInst.exports; - ex.compute(DSP, blockSize, 0, this._outPtrsAddr); - - const wL = new Float32Array(ex.memory.buffer, this._outLAddr, blockSize); - const wR = new Float32Array(ex.memory.buffer, this._outRAddr, blockSize); - outL.set(wL); - outR.set(wR); - } -} - -registerProcessor('modular-fm-processor', ModularFmProcessor); diff --git a/playground/faust/modular-fm.dsp b/playground/faust/modular-fm.dsp deleted file mode 100644 index bd37807..0000000 --- a/playground/faust/modular-fm.dsp +++ /dev/null @@ -1,1198 +0,0 @@ -// modular-fm.dsp — 4-operator FM voice for the "Modular" mode. -// -// AUTO-GENERATED by gen-modular-dsp.py. DO NOT EDIT BY HAND. -// Regenerate with: -// cd playground/faust && python3 gen-modular-dsp.py -// -// Audio path: 4 ops with 12-cell cross-mod matrix and per-op feedback. -// All envelopes and LFOs come from mod-pool.lib via a 48-source × 10-dest -// modulation matrix. No internal envs/LFOs. -// -// Modulation destinations (10): -// 0 pitch semitones added to the base frequency -// 1 op1_level added to op1 level knob -// 2 op2_level added to op2 level knob -// 3 op3_level added to op3 level knob -// 4 op4_level added to op4 level knob -// 5 cross_mod_global global FM depth multiplier on all 12 cross-mod cells -// 6 feedback_global global feedback multiplier on all 4 self-feedback cells -// 7 global_ratio additive scale factor on all 4 operator ratios -// 8 amp master amplitude dest (route an ADSR here for a VCA env) -// 9 pan stereo balance -// -// Hidden controls (driven by noteOn/noteOff, not by the ML engine): -// freq, gate, _vel -// -// Build: -// cd playground/faust && ./build.sh -// -// --------------------------------------------------------------------------- - -import("stdfaust.lib"); -mp = library("mod-pool.lib"); - -// --------------------------------------------------------------------------- -// Hidden controls -// --------------------------------------------------------------------------- -freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("0_Hidden/gate[hidden:1]"); -vel = hslider("0_Hidden/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 1 — Operators (ratio & level per op) -// --------------------------------------------------------------------------- -op1_ratio = hslider("1_Operators/00_op1_ratio", 1.0, 0.125, 16.0, 0.001); -op1_level = hslider("1_Operators/01_op1_level", 0.8, 0.0, 1.0, 0.001); -op2_ratio = hslider("1_Operators/02_op2_ratio", 2.0, 0.125, 16.0, 0.001); -op2_level = hslider("1_Operators/03_op2_level", 0.6, 0.0, 1.0, 0.001); -op3_ratio = hslider("1_Operators/04_op3_ratio", 3.0, 0.125, 16.0, 0.001); -op3_level = hslider("1_Operators/05_op3_level", 0.4, 0.0, 1.0, 0.001); -op4_ratio = hslider("1_Operators/06_op4_ratio", 0.5, 0.125, 16.0, 0.001); -op4_level = hslider("1_Operators/07_op4_level", 0.3, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 2 — Cross-mod Matrix (mXY = op X modulates op Y) -// --------------------------------------------------------------------------- -m12 = hslider("2_CrossMod/00_m12", 0.0, 0.0, 10.0, 0.001); -m13 = hslider("2_CrossMod/01_m13", 0.0, 0.0, 10.0, 0.001); -m14 = hslider("2_CrossMod/02_m14", 0.0, 0.0, 10.0, 0.001); -m21 = hslider("2_CrossMod/03_m21", 1.0, 0.0, 10.0, 0.001); -m23 = hslider("2_CrossMod/04_m23", 0.0, 0.0, 10.0, 0.001); -m24 = hslider("2_CrossMod/05_m24", 0.0, 0.0, 10.0, 0.001); -m31 = hslider("2_CrossMod/06_m31", 0.0, 0.0, 10.0, 0.001); -m32 = hslider("2_CrossMod/07_m32", 0.0, 0.0, 10.0, 0.001); -m34 = hslider("2_CrossMod/08_m34", 0.0, 0.0, 10.0, 0.001); -m41 = hslider("2_CrossMod/09_m41", 0.0, 0.0, 10.0, 0.001); -m42 = hslider("2_CrossMod/10_m42", 0.0, 0.0, 10.0, 0.001); -m43 = hslider("2_CrossMod/11_m43", 0.0, 0.0, 10.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Self-feedback -// --------------------------------------------------------------------------- -fb1 = hslider("3_Feedback/00_fb1", 0.0, 0.0, 1.0, 0.001); -fb2 = hslider("3_Feedback/01_fb2", 0.0, 0.0, 1.0, 0.001); -fb3 = hslider("3_Feedback/02_fb3", 0.0, 0.0, 1.0, 0.001); -fb4 = hslider("3_Feedback/03_fb4", 0.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 4 — Master -// --------------------------------------------------------------------------- -master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); -fine_tune = hslider("4_Master/01_fine_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -stereo_spread = hslider("4_Master/02_stereo_spread", 0.1, 0.0, 1.0, 0.001); -output_saturation = hslider("4_Master/03_output_saturation", 0.0, 0.0, 1.0, 0.001); -output_hp = hslider("4_Master/04_output_hp[unit:Hz]",20.0, 20.0, 200.0,0.1); -master_pan = hslider("4_Master/05_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor — see subtractive engine for full explanation. -base_amp = hslider("4_Master/06_base_amp", 1.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Modulation matrix — 48 sources × 10 destinations. -// Each hslider is one "amount" entry in [-1, +1]. Zero = no connection. -// Group "MM_Matrix" so the paramMeta parser can recognise matrix params. -// --------------------------------------------------------------------------- - -// Matrix column: destination 0 = pitch -amt_s00_d00_pitch = hslider("MM_Matrix/s00_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s01_d00_pitch = hslider("MM_Matrix/s01_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s02_d00_pitch = hslider("MM_Matrix/s02_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s03_d00_pitch = hslider("MM_Matrix/s03_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s04_d00_pitch = hslider("MM_Matrix/s04_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s05_d00_pitch = hslider("MM_Matrix/s05_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s06_d00_pitch = hslider("MM_Matrix/s06_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s07_d00_pitch = hslider("MM_Matrix/s07_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s08_d00_pitch = hslider("MM_Matrix/s08_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s09_d00_pitch = hslider("MM_Matrix/s09_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s10_d00_pitch = hslider("MM_Matrix/s10_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s11_d00_pitch = hslider("MM_Matrix/s11_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s12_d00_pitch = hslider("MM_Matrix/s12_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s13_d00_pitch = hslider("MM_Matrix/s13_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s14_d00_pitch = hslider("MM_Matrix/s14_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s15_d00_pitch = hslider("MM_Matrix/s15_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s16_d00_pitch = hslider("MM_Matrix/s16_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s17_d00_pitch = hslider("MM_Matrix/s17_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s18_d00_pitch = hslider("MM_Matrix/s18_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s19_d00_pitch = hslider("MM_Matrix/s19_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s20_d00_pitch = hslider("MM_Matrix/s20_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s21_d00_pitch = hslider("MM_Matrix/s21_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s22_d00_pitch = hslider("MM_Matrix/s22_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s23_d00_pitch = hslider("MM_Matrix/s23_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s24_d00_pitch = hslider("MM_Matrix/s24_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s25_d00_pitch = hslider("MM_Matrix/s25_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s26_d00_pitch = hslider("MM_Matrix/s26_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s27_d00_pitch = hslider("MM_Matrix/s27_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s28_d00_pitch = hslider("MM_Matrix/s28_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s29_d00_pitch = hslider("MM_Matrix/s29_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s30_d00_pitch = hslider("MM_Matrix/s30_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s31_d00_pitch = hslider("MM_Matrix/s31_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s32_d00_pitch = hslider("MM_Matrix/s32_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s33_d00_pitch = hslider("MM_Matrix/s33_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s34_d00_pitch = hslider("MM_Matrix/s34_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s35_d00_pitch = hslider("MM_Matrix/s35_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s36_d00_pitch = hslider("MM_Matrix/s36_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s37_d00_pitch = hslider("MM_Matrix/s37_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s38_d00_pitch = hslider("MM_Matrix/s38_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s39_d00_pitch = hslider("MM_Matrix/s39_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s40_d00_pitch = hslider("MM_Matrix/s40_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s41_d00_pitch = hslider("MM_Matrix/s41_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s42_d00_pitch = hslider("MM_Matrix/s42_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s43_d00_pitch = hslider("MM_Matrix/s43_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s44_d00_pitch = hslider("MM_Matrix/s44_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s45_d00_pitch = hslider("MM_Matrix/s45_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s46_d00_pitch = hslider("MM_Matrix/s46_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s47_d00_pitch = hslider("MM_Matrix/s47_d00_pitch", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 1 = op1_level -amt_s00_d01_op1_level = hslider("MM_Matrix/s00_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s01_d01_op1_level = hslider("MM_Matrix/s01_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s02_d01_op1_level = hslider("MM_Matrix/s02_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s03_d01_op1_level = hslider("MM_Matrix/s03_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s04_d01_op1_level = hslider("MM_Matrix/s04_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s05_d01_op1_level = hslider("MM_Matrix/s05_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s06_d01_op1_level = hslider("MM_Matrix/s06_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s07_d01_op1_level = hslider("MM_Matrix/s07_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s08_d01_op1_level = hslider("MM_Matrix/s08_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s09_d01_op1_level = hslider("MM_Matrix/s09_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s10_d01_op1_level = hslider("MM_Matrix/s10_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s11_d01_op1_level = hslider("MM_Matrix/s11_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s12_d01_op1_level = hslider("MM_Matrix/s12_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s13_d01_op1_level = hslider("MM_Matrix/s13_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s14_d01_op1_level = hslider("MM_Matrix/s14_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s15_d01_op1_level = hslider("MM_Matrix/s15_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s16_d01_op1_level = hslider("MM_Matrix/s16_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s17_d01_op1_level = hslider("MM_Matrix/s17_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s18_d01_op1_level = hslider("MM_Matrix/s18_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s19_d01_op1_level = hslider("MM_Matrix/s19_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s20_d01_op1_level = hslider("MM_Matrix/s20_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s21_d01_op1_level = hslider("MM_Matrix/s21_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s22_d01_op1_level = hslider("MM_Matrix/s22_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s23_d01_op1_level = hslider("MM_Matrix/s23_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s24_d01_op1_level = hslider("MM_Matrix/s24_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s25_d01_op1_level = hslider("MM_Matrix/s25_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s26_d01_op1_level = hslider("MM_Matrix/s26_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s27_d01_op1_level = hslider("MM_Matrix/s27_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s28_d01_op1_level = hslider("MM_Matrix/s28_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s29_d01_op1_level = hslider("MM_Matrix/s29_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s30_d01_op1_level = hslider("MM_Matrix/s30_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s31_d01_op1_level = hslider("MM_Matrix/s31_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s32_d01_op1_level = hslider("MM_Matrix/s32_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s33_d01_op1_level = hslider("MM_Matrix/s33_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s34_d01_op1_level = hslider("MM_Matrix/s34_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s35_d01_op1_level = hslider("MM_Matrix/s35_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s36_d01_op1_level = hslider("MM_Matrix/s36_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s37_d01_op1_level = hslider("MM_Matrix/s37_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s38_d01_op1_level = hslider("MM_Matrix/s38_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s39_d01_op1_level = hslider("MM_Matrix/s39_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s40_d01_op1_level = hslider("MM_Matrix/s40_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s41_d01_op1_level = hslider("MM_Matrix/s41_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s42_d01_op1_level = hslider("MM_Matrix/s42_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s43_d01_op1_level = hslider("MM_Matrix/s43_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s44_d01_op1_level = hslider("MM_Matrix/s44_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s45_d01_op1_level = hslider("MM_Matrix/s45_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s46_d01_op1_level = hslider("MM_Matrix/s46_d01_op1_level", 0.0, -1.0, 1.0, 0.001); -amt_s47_d01_op1_level = hslider("MM_Matrix/s47_d01_op1_level", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 2 = op2_level -amt_s00_d02_op2_level = hslider("MM_Matrix/s00_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s01_d02_op2_level = hslider("MM_Matrix/s01_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s02_d02_op2_level = hslider("MM_Matrix/s02_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s03_d02_op2_level = hslider("MM_Matrix/s03_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s04_d02_op2_level = hslider("MM_Matrix/s04_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s05_d02_op2_level = hslider("MM_Matrix/s05_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s06_d02_op2_level = hslider("MM_Matrix/s06_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s07_d02_op2_level = hslider("MM_Matrix/s07_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s08_d02_op2_level = hslider("MM_Matrix/s08_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s09_d02_op2_level = hslider("MM_Matrix/s09_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s10_d02_op2_level = hslider("MM_Matrix/s10_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s11_d02_op2_level = hslider("MM_Matrix/s11_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s12_d02_op2_level = hslider("MM_Matrix/s12_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s13_d02_op2_level = hslider("MM_Matrix/s13_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s14_d02_op2_level = hslider("MM_Matrix/s14_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s15_d02_op2_level = hslider("MM_Matrix/s15_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s16_d02_op2_level = hslider("MM_Matrix/s16_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s17_d02_op2_level = hslider("MM_Matrix/s17_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s18_d02_op2_level = hslider("MM_Matrix/s18_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s19_d02_op2_level = hslider("MM_Matrix/s19_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s20_d02_op2_level = hslider("MM_Matrix/s20_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s21_d02_op2_level = hslider("MM_Matrix/s21_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s22_d02_op2_level = hslider("MM_Matrix/s22_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s23_d02_op2_level = hslider("MM_Matrix/s23_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s24_d02_op2_level = hslider("MM_Matrix/s24_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s25_d02_op2_level = hslider("MM_Matrix/s25_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s26_d02_op2_level = hslider("MM_Matrix/s26_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s27_d02_op2_level = hslider("MM_Matrix/s27_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s28_d02_op2_level = hslider("MM_Matrix/s28_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s29_d02_op2_level = hslider("MM_Matrix/s29_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s30_d02_op2_level = hslider("MM_Matrix/s30_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s31_d02_op2_level = hslider("MM_Matrix/s31_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s32_d02_op2_level = hslider("MM_Matrix/s32_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s33_d02_op2_level = hslider("MM_Matrix/s33_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s34_d02_op2_level = hslider("MM_Matrix/s34_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s35_d02_op2_level = hslider("MM_Matrix/s35_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s36_d02_op2_level = hslider("MM_Matrix/s36_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s37_d02_op2_level = hslider("MM_Matrix/s37_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s38_d02_op2_level = hslider("MM_Matrix/s38_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s39_d02_op2_level = hslider("MM_Matrix/s39_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s40_d02_op2_level = hslider("MM_Matrix/s40_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s41_d02_op2_level = hslider("MM_Matrix/s41_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s42_d02_op2_level = hslider("MM_Matrix/s42_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s43_d02_op2_level = hslider("MM_Matrix/s43_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s44_d02_op2_level = hslider("MM_Matrix/s44_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s45_d02_op2_level = hslider("MM_Matrix/s45_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s46_d02_op2_level = hslider("MM_Matrix/s46_d02_op2_level", 0.0, -1.0, 1.0, 0.001); -amt_s47_d02_op2_level = hslider("MM_Matrix/s47_d02_op2_level", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 3 = op3_level -amt_s00_d03_op3_level = hslider("MM_Matrix/s00_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s01_d03_op3_level = hslider("MM_Matrix/s01_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s02_d03_op3_level = hslider("MM_Matrix/s02_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s03_d03_op3_level = hslider("MM_Matrix/s03_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s04_d03_op3_level = hslider("MM_Matrix/s04_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s05_d03_op3_level = hslider("MM_Matrix/s05_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s06_d03_op3_level = hslider("MM_Matrix/s06_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s07_d03_op3_level = hslider("MM_Matrix/s07_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s08_d03_op3_level = hslider("MM_Matrix/s08_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s09_d03_op3_level = hslider("MM_Matrix/s09_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s10_d03_op3_level = hslider("MM_Matrix/s10_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s11_d03_op3_level = hslider("MM_Matrix/s11_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s12_d03_op3_level = hslider("MM_Matrix/s12_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s13_d03_op3_level = hslider("MM_Matrix/s13_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s14_d03_op3_level = hslider("MM_Matrix/s14_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s15_d03_op3_level = hslider("MM_Matrix/s15_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s16_d03_op3_level = hslider("MM_Matrix/s16_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s17_d03_op3_level = hslider("MM_Matrix/s17_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s18_d03_op3_level = hslider("MM_Matrix/s18_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s19_d03_op3_level = hslider("MM_Matrix/s19_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s20_d03_op3_level = hslider("MM_Matrix/s20_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s21_d03_op3_level = hslider("MM_Matrix/s21_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s22_d03_op3_level = hslider("MM_Matrix/s22_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s23_d03_op3_level = hslider("MM_Matrix/s23_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s24_d03_op3_level = hslider("MM_Matrix/s24_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s25_d03_op3_level = hslider("MM_Matrix/s25_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s26_d03_op3_level = hslider("MM_Matrix/s26_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s27_d03_op3_level = hslider("MM_Matrix/s27_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s28_d03_op3_level = hslider("MM_Matrix/s28_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s29_d03_op3_level = hslider("MM_Matrix/s29_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s30_d03_op3_level = hslider("MM_Matrix/s30_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s31_d03_op3_level = hslider("MM_Matrix/s31_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s32_d03_op3_level = hslider("MM_Matrix/s32_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s33_d03_op3_level = hslider("MM_Matrix/s33_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s34_d03_op3_level = hslider("MM_Matrix/s34_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s35_d03_op3_level = hslider("MM_Matrix/s35_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s36_d03_op3_level = hslider("MM_Matrix/s36_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s37_d03_op3_level = hslider("MM_Matrix/s37_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s38_d03_op3_level = hslider("MM_Matrix/s38_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s39_d03_op3_level = hslider("MM_Matrix/s39_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s40_d03_op3_level = hslider("MM_Matrix/s40_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s41_d03_op3_level = hslider("MM_Matrix/s41_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s42_d03_op3_level = hslider("MM_Matrix/s42_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s43_d03_op3_level = hslider("MM_Matrix/s43_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s44_d03_op3_level = hslider("MM_Matrix/s44_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s45_d03_op3_level = hslider("MM_Matrix/s45_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s46_d03_op3_level = hslider("MM_Matrix/s46_d03_op3_level", 0.0, -1.0, 1.0, 0.001); -amt_s47_d03_op3_level = hslider("MM_Matrix/s47_d03_op3_level", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 4 = op4_level -amt_s00_d04_op4_level = hslider("MM_Matrix/s00_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s01_d04_op4_level = hslider("MM_Matrix/s01_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s02_d04_op4_level = hslider("MM_Matrix/s02_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s03_d04_op4_level = hslider("MM_Matrix/s03_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s04_d04_op4_level = hslider("MM_Matrix/s04_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s05_d04_op4_level = hslider("MM_Matrix/s05_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s06_d04_op4_level = hslider("MM_Matrix/s06_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s07_d04_op4_level = hslider("MM_Matrix/s07_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s08_d04_op4_level = hslider("MM_Matrix/s08_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s09_d04_op4_level = hslider("MM_Matrix/s09_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s10_d04_op4_level = hslider("MM_Matrix/s10_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s11_d04_op4_level = hslider("MM_Matrix/s11_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s12_d04_op4_level = hslider("MM_Matrix/s12_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s13_d04_op4_level = hslider("MM_Matrix/s13_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s14_d04_op4_level = hslider("MM_Matrix/s14_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s15_d04_op4_level = hslider("MM_Matrix/s15_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s16_d04_op4_level = hslider("MM_Matrix/s16_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s17_d04_op4_level = hslider("MM_Matrix/s17_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s18_d04_op4_level = hslider("MM_Matrix/s18_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s19_d04_op4_level = hslider("MM_Matrix/s19_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s20_d04_op4_level = hslider("MM_Matrix/s20_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s21_d04_op4_level = hslider("MM_Matrix/s21_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s22_d04_op4_level = hslider("MM_Matrix/s22_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s23_d04_op4_level = hslider("MM_Matrix/s23_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s24_d04_op4_level = hslider("MM_Matrix/s24_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s25_d04_op4_level = hslider("MM_Matrix/s25_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s26_d04_op4_level = hslider("MM_Matrix/s26_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s27_d04_op4_level = hslider("MM_Matrix/s27_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s28_d04_op4_level = hslider("MM_Matrix/s28_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s29_d04_op4_level = hslider("MM_Matrix/s29_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s30_d04_op4_level = hslider("MM_Matrix/s30_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s31_d04_op4_level = hslider("MM_Matrix/s31_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s32_d04_op4_level = hslider("MM_Matrix/s32_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s33_d04_op4_level = hslider("MM_Matrix/s33_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s34_d04_op4_level = hslider("MM_Matrix/s34_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s35_d04_op4_level = hslider("MM_Matrix/s35_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s36_d04_op4_level = hslider("MM_Matrix/s36_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s37_d04_op4_level = hslider("MM_Matrix/s37_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s38_d04_op4_level = hslider("MM_Matrix/s38_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s39_d04_op4_level = hslider("MM_Matrix/s39_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s40_d04_op4_level = hslider("MM_Matrix/s40_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s41_d04_op4_level = hslider("MM_Matrix/s41_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s42_d04_op4_level = hslider("MM_Matrix/s42_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s43_d04_op4_level = hslider("MM_Matrix/s43_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s44_d04_op4_level = hslider("MM_Matrix/s44_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s45_d04_op4_level = hslider("MM_Matrix/s45_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s46_d04_op4_level = hslider("MM_Matrix/s46_d04_op4_level", 0.0, -1.0, 1.0, 0.001); -amt_s47_d04_op4_level = hslider("MM_Matrix/s47_d04_op4_level", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 5 = cross_mod_global -amt_s00_d05_cross_mod_global = hslider("MM_Matrix/s00_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s01_d05_cross_mod_global = hslider("MM_Matrix/s01_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s02_d05_cross_mod_global = hslider("MM_Matrix/s02_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s03_d05_cross_mod_global = hslider("MM_Matrix/s03_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s04_d05_cross_mod_global = hslider("MM_Matrix/s04_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s05_d05_cross_mod_global = hslider("MM_Matrix/s05_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s06_d05_cross_mod_global = hslider("MM_Matrix/s06_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s07_d05_cross_mod_global = hslider("MM_Matrix/s07_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s08_d05_cross_mod_global = hslider("MM_Matrix/s08_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s09_d05_cross_mod_global = hslider("MM_Matrix/s09_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s10_d05_cross_mod_global = hslider("MM_Matrix/s10_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s11_d05_cross_mod_global = hslider("MM_Matrix/s11_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s12_d05_cross_mod_global = hslider("MM_Matrix/s12_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s13_d05_cross_mod_global = hslider("MM_Matrix/s13_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s14_d05_cross_mod_global = hslider("MM_Matrix/s14_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s15_d05_cross_mod_global = hslider("MM_Matrix/s15_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s16_d05_cross_mod_global = hslider("MM_Matrix/s16_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s17_d05_cross_mod_global = hslider("MM_Matrix/s17_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s18_d05_cross_mod_global = hslider("MM_Matrix/s18_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s19_d05_cross_mod_global = hslider("MM_Matrix/s19_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s20_d05_cross_mod_global = hslider("MM_Matrix/s20_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s21_d05_cross_mod_global = hslider("MM_Matrix/s21_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s22_d05_cross_mod_global = hslider("MM_Matrix/s22_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s23_d05_cross_mod_global = hslider("MM_Matrix/s23_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s24_d05_cross_mod_global = hslider("MM_Matrix/s24_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s25_d05_cross_mod_global = hslider("MM_Matrix/s25_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s26_d05_cross_mod_global = hslider("MM_Matrix/s26_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s27_d05_cross_mod_global = hslider("MM_Matrix/s27_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s28_d05_cross_mod_global = hslider("MM_Matrix/s28_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s29_d05_cross_mod_global = hslider("MM_Matrix/s29_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s30_d05_cross_mod_global = hslider("MM_Matrix/s30_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s31_d05_cross_mod_global = hslider("MM_Matrix/s31_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s32_d05_cross_mod_global = hslider("MM_Matrix/s32_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s33_d05_cross_mod_global = hslider("MM_Matrix/s33_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s34_d05_cross_mod_global = hslider("MM_Matrix/s34_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s35_d05_cross_mod_global = hslider("MM_Matrix/s35_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s36_d05_cross_mod_global = hslider("MM_Matrix/s36_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s37_d05_cross_mod_global = hslider("MM_Matrix/s37_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s38_d05_cross_mod_global = hslider("MM_Matrix/s38_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s39_d05_cross_mod_global = hslider("MM_Matrix/s39_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s40_d05_cross_mod_global = hslider("MM_Matrix/s40_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s41_d05_cross_mod_global = hslider("MM_Matrix/s41_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s42_d05_cross_mod_global = hslider("MM_Matrix/s42_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s43_d05_cross_mod_global = hslider("MM_Matrix/s43_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s44_d05_cross_mod_global = hslider("MM_Matrix/s44_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s45_d05_cross_mod_global = hslider("MM_Matrix/s45_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s46_d05_cross_mod_global = hslider("MM_Matrix/s46_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); -amt_s47_d05_cross_mod_global = hslider("MM_Matrix/s47_d05_cross_mod_global", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 6 = feedback_global -amt_s00_d06_feedback_global = hslider("MM_Matrix/s00_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s01_d06_feedback_global = hslider("MM_Matrix/s01_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s02_d06_feedback_global = hslider("MM_Matrix/s02_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s03_d06_feedback_global = hslider("MM_Matrix/s03_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s04_d06_feedback_global = hslider("MM_Matrix/s04_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s05_d06_feedback_global = hslider("MM_Matrix/s05_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s06_d06_feedback_global = hslider("MM_Matrix/s06_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s07_d06_feedback_global = hslider("MM_Matrix/s07_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s08_d06_feedback_global = hslider("MM_Matrix/s08_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s09_d06_feedback_global = hslider("MM_Matrix/s09_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s10_d06_feedback_global = hslider("MM_Matrix/s10_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s11_d06_feedback_global = hslider("MM_Matrix/s11_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s12_d06_feedback_global = hslider("MM_Matrix/s12_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s13_d06_feedback_global = hslider("MM_Matrix/s13_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s14_d06_feedback_global = hslider("MM_Matrix/s14_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s15_d06_feedback_global = hslider("MM_Matrix/s15_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s16_d06_feedback_global = hslider("MM_Matrix/s16_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s17_d06_feedback_global = hslider("MM_Matrix/s17_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s18_d06_feedback_global = hslider("MM_Matrix/s18_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s19_d06_feedback_global = hslider("MM_Matrix/s19_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s20_d06_feedback_global = hslider("MM_Matrix/s20_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s21_d06_feedback_global = hslider("MM_Matrix/s21_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s22_d06_feedback_global = hslider("MM_Matrix/s22_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s23_d06_feedback_global = hslider("MM_Matrix/s23_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s24_d06_feedback_global = hslider("MM_Matrix/s24_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s25_d06_feedback_global = hslider("MM_Matrix/s25_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s26_d06_feedback_global = hslider("MM_Matrix/s26_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s27_d06_feedback_global = hslider("MM_Matrix/s27_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s28_d06_feedback_global = hslider("MM_Matrix/s28_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s29_d06_feedback_global = hslider("MM_Matrix/s29_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s30_d06_feedback_global = hslider("MM_Matrix/s30_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s31_d06_feedback_global = hslider("MM_Matrix/s31_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s32_d06_feedback_global = hslider("MM_Matrix/s32_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s33_d06_feedback_global = hslider("MM_Matrix/s33_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s34_d06_feedback_global = hslider("MM_Matrix/s34_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s35_d06_feedback_global = hslider("MM_Matrix/s35_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s36_d06_feedback_global = hslider("MM_Matrix/s36_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s37_d06_feedback_global = hslider("MM_Matrix/s37_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s38_d06_feedback_global = hslider("MM_Matrix/s38_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s39_d06_feedback_global = hslider("MM_Matrix/s39_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s40_d06_feedback_global = hslider("MM_Matrix/s40_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s41_d06_feedback_global = hslider("MM_Matrix/s41_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s42_d06_feedback_global = hslider("MM_Matrix/s42_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s43_d06_feedback_global = hslider("MM_Matrix/s43_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s44_d06_feedback_global = hslider("MM_Matrix/s44_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s45_d06_feedback_global = hslider("MM_Matrix/s45_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s46_d06_feedback_global = hslider("MM_Matrix/s46_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); -amt_s47_d06_feedback_global = hslider("MM_Matrix/s47_d06_feedback_global", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 7 = global_ratio -amt_s00_d07_global_ratio = hslider("MM_Matrix/s00_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s01_d07_global_ratio = hslider("MM_Matrix/s01_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s02_d07_global_ratio = hslider("MM_Matrix/s02_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s03_d07_global_ratio = hslider("MM_Matrix/s03_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s04_d07_global_ratio = hslider("MM_Matrix/s04_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s05_d07_global_ratio = hslider("MM_Matrix/s05_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s06_d07_global_ratio = hslider("MM_Matrix/s06_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s07_d07_global_ratio = hslider("MM_Matrix/s07_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s08_d07_global_ratio = hslider("MM_Matrix/s08_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s09_d07_global_ratio = hslider("MM_Matrix/s09_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s10_d07_global_ratio = hslider("MM_Matrix/s10_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s11_d07_global_ratio = hslider("MM_Matrix/s11_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s12_d07_global_ratio = hslider("MM_Matrix/s12_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s13_d07_global_ratio = hslider("MM_Matrix/s13_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s14_d07_global_ratio = hslider("MM_Matrix/s14_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s15_d07_global_ratio = hslider("MM_Matrix/s15_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s16_d07_global_ratio = hslider("MM_Matrix/s16_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s17_d07_global_ratio = hslider("MM_Matrix/s17_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s18_d07_global_ratio = hslider("MM_Matrix/s18_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s19_d07_global_ratio = hslider("MM_Matrix/s19_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s20_d07_global_ratio = hslider("MM_Matrix/s20_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s21_d07_global_ratio = hslider("MM_Matrix/s21_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s22_d07_global_ratio = hslider("MM_Matrix/s22_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s23_d07_global_ratio = hslider("MM_Matrix/s23_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s24_d07_global_ratio = hslider("MM_Matrix/s24_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s25_d07_global_ratio = hslider("MM_Matrix/s25_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s26_d07_global_ratio = hslider("MM_Matrix/s26_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s27_d07_global_ratio = hslider("MM_Matrix/s27_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s28_d07_global_ratio = hslider("MM_Matrix/s28_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s29_d07_global_ratio = hslider("MM_Matrix/s29_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s30_d07_global_ratio = hslider("MM_Matrix/s30_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s31_d07_global_ratio = hslider("MM_Matrix/s31_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s32_d07_global_ratio = hslider("MM_Matrix/s32_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s33_d07_global_ratio = hslider("MM_Matrix/s33_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s34_d07_global_ratio = hslider("MM_Matrix/s34_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s35_d07_global_ratio = hslider("MM_Matrix/s35_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s36_d07_global_ratio = hslider("MM_Matrix/s36_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s37_d07_global_ratio = hslider("MM_Matrix/s37_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s38_d07_global_ratio = hslider("MM_Matrix/s38_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s39_d07_global_ratio = hslider("MM_Matrix/s39_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s40_d07_global_ratio = hslider("MM_Matrix/s40_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s41_d07_global_ratio = hslider("MM_Matrix/s41_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s42_d07_global_ratio = hslider("MM_Matrix/s42_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s43_d07_global_ratio = hslider("MM_Matrix/s43_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s44_d07_global_ratio = hslider("MM_Matrix/s44_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s45_d07_global_ratio = hslider("MM_Matrix/s45_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s46_d07_global_ratio = hslider("MM_Matrix/s46_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); -amt_s47_d07_global_ratio = hslider("MM_Matrix/s47_d07_global_ratio", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 8 = amp -amt_s00_d08_amp = hslider("MM_Matrix/s00_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s01_d08_amp = hslider("MM_Matrix/s01_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s02_d08_amp = hslider("MM_Matrix/s02_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s03_d08_amp = hslider("MM_Matrix/s03_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s04_d08_amp = hslider("MM_Matrix/s04_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s05_d08_amp = hslider("MM_Matrix/s05_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s06_d08_amp = hslider("MM_Matrix/s06_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s07_d08_amp = hslider("MM_Matrix/s07_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s08_d08_amp = hslider("MM_Matrix/s08_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s09_d08_amp = hslider("MM_Matrix/s09_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s10_d08_amp = hslider("MM_Matrix/s10_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s11_d08_amp = hslider("MM_Matrix/s11_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s12_d08_amp = hslider("MM_Matrix/s12_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s13_d08_amp = hslider("MM_Matrix/s13_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s14_d08_amp = hslider("MM_Matrix/s14_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s15_d08_amp = hslider("MM_Matrix/s15_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s16_d08_amp = hslider("MM_Matrix/s16_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s17_d08_amp = hslider("MM_Matrix/s17_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s18_d08_amp = hslider("MM_Matrix/s18_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s19_d08_amp = hslider("MM_Matrix/s19_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s20_d08_amp = hslider("MM_Matrix/s20_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s21_d08_amp = hslider("MM_Matrix/s21_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s22_d08_amp = hslider("MM_Matrix/s22_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s23_d08_amp = hslider("MM_Matrix/s23_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s24_d08_amp = hslider("MM_Matrix/s24_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s25_d08_amp = hslider("MM_Matrix/s25_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s26_d08_amp = hslider("MM_Matrix/s26_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s27_d08_amp = hslider("MM_Matrix/s27_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s28_d08_amp = hslider("MM_Matrix/s28_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s29_d08_amp = hslider("MM_Matrix/s29_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s30_d08_amp = hslider("MM_Matrix/s30_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s31_d08_amp = hslider("MM_Matrix/s31_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s32_d08_amp = hslider("MM_Matrix/s32_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s33_d08_amp = hslider("MM_Matrix/s33_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s34_d08_amp = hslider("MM_Matrix/s34_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s35_d08_amp = hslider("MM_Matrix/s35_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s36_d08_amp = hslider("MM_Matrix/s36_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s37_d08_amp = hslider("MM_Matrix/s37_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s38_d08_amp = hslider("MM_Matrix/s38_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s39_d08_amp = hslider("MM_Matrix/s39_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s40_d08_amp = hslider("MM_Matrix/s40_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s41_d08_amp = hslider("MM_Matrix/s41_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s42_d08_amp = hslider("MM_Matrix/s42_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s43_d08_amp = hslider("MM_Matrix/s43_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s44_d08_amp = hslider("MM_Matrix/s44_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s45_d08_amp = hslider("MM_Matrix/s45_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s46_d08_amp = hslider("MM_Matrix/s46_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s47_d08_amp = hslider("MM_Matrix/s47_d08_amp", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 9 = pan -amt_s00_d09_pan = hslider("MM_Matrix/s00_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s01_d09_pan = hslider("MM_Matrix/s01_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s02_d09_pan = hslider("MM_Matrix/s02_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s03_d09_pan = hslider("MM_Matrix/s03_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s04_d09_pan = hslider("MM_Matrix/s04_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s05_d09_pan = hslider("MM_Matrix/s05_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s06_d09_pan = hslider("MM_Matrix/s06_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s07_d09_pan = hslider("MM_Matrix/s07_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s08_d09_pan = hslider("MM_Matrix/s08_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s09_d09_pan = hslider("MM_Matrix/s09_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s10_d09_pan = hslider("MM_Matrix/s10_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s11_d09_pan = hslider("MM_Matrix/s11_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s12_d09_pan = hslider("MM_Matrix/s12_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s13_d09_pan = hslider("MM_Matrix/s13_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s14_d09_pan = hslider("MM_Matrix/s14_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s15_d09_pan = hslider("MM_Matrix/s15_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s16_d09_pan = hslider("MM_Matrix/s16_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s17_d09_pan = hslider("MM_Matrix/s17_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s18_d09_pan = hslider("MM_Matrix/s18_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s19_d09_pan = hslider("MM_Matrix/s19_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s20_d09_pan = hslider("MM_Matrix/s20_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s21_d09_pan = hslider("MM_Matrix/s21_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s22_d09_pan = hslider("MM_Matrix/s22_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s23_d09_pan = hslider("MM_Matrix/s23_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s24_d09_pan = hslider("MM_Matrix/s24_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s25_d09_pan = hslider("MM_Matrix/s25_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s26_d09_pan = hslider("MM_Matrix/s26_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s27_d09_pan = hslider("MM_Matrix/s27_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s28_d09_pan = hslider("MM_Matrix/s28_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s29_d09_pan = hslider("MM_Matrix/s29_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s30_d09_pan = hslider("MM_Matrix/s30_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s31_d09_pan = hslider("MM_Matrix/s31_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s32_d09_pan = hslider("MM_Matrix/s32_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s33_d09_pan = hslider("MM_Matrix/s33_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s34_d09_pan = hslider("MM_Matrix/s34_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s35_d09_pan = hslider("MM_Matrix/s35_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s36_d09_pan = hslider("MM_Matrix/s36_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s37_d09_pan = hslider("MM_Matrix/s37_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s38_d09_pan = hslider("MM_Matrix/s38_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s39_d09_pan = hslider("MM_Matrix/s39_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s40_d09_pan = hslider("MM_Matrix/s40_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s41_d09_pan = hslider("MM_Matrix/s41_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s42_d09_pan = hslider("MM_Matrix/s42_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s43_d09_pan = hslider("MM_Matrix/s43_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s44_d09_pan = hslider("MM_Matrix/s44_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s45_d09_pan = hslider("MM_Matrix/s45_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s46_d09_pan = hslider("MM_Matrix/s46_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s47_d09_pan = hslider("MM_Matrix/s47_d09_pan", 0.0, -1.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Destination signals — one per modulation destination. Each is a 48-term -// weighted sum of sources × amounts. The `gate` argument is threaded through -// so that mp.src??(gate) can drive ADSR envelopes. -// --------------------------------------------------------------------------- - -mod_pitch(gate) = - mp.src00(gate) * amt_s00_d00_pitch + - mp.src01(gate) * amt_s01_d00_pitch + - mp.src02(gate) * amt_s02_d00_pitch + - mp.src03(gate) * amt_s03_d00_pitch + - mp.src04(gate) * amt_s04_d00_pitch + - mp.src05(gate) * amt_s05_d00_pitch + - mp.src06(gate) * amt_s06_d00_pitch + - mp.src07(gate) * amt_s07_d00_pitch + - mp.src08(gate) * amt_s08_d00_pitch + - mp.src09(gate) * amt_s09_d00_pitch + - mp.src10(gate) * amt_s10_d00_pitch + - mp.src11(gate) * amt_s11_d00_pitch + - mp.src12(gate) * amt_s12_d00_pitch + - mp.src13(gate) * amt_s13_d00_pitch + - mp.src14(gate) * amt_s14_d00_pitch + - mp.src15(gate) * amt_s15_d00_pitch + - mp.src16(gate) * amt_s16_d00_pitch + - mp.src17(gate) * amt_s17_d00_pitch + - mp.src18(gate) * amt_s18_d00_pitch + - mp.src19(gate) * amt_s19_d00_pitch + - mp.src20(gate) * amt_s20_d00_pitch + - mp.src21(gate) * amt_s21_d00_pitch + - mp.src22(gate) * amt_s22_d00_pitch + - mp.src23(gate) * amt_s23_d00_pitch + - mp.src24(gate) * amt_s24_d00_pitch + - mp.src25(gate) * amt_s25_d00_pitch + - mp.src26(gate) * amt_s26_d00_pitch + - mp.src27(gate) * amt_s27_d00_pitch + - mp.src28(gate) * amt_s28_d00_pitch + - mp.src29(gate) * amt_s29_d00_pitch + - mp.src30(gate) * amt_s30_d00_pitch + - mp.src31(gate) * amt_s31_d00_pitch + - mp.src32(gate) * amt_s32_d00_pitch + - mp.src33(gate) * amt_s33_d00_pitch + - mp.src34(gate) * amt_s34_d00_pitch + - mp.src35(gate) * amt_s35_d00_pitch + - mp.src36(gate) * amt_s36_d00_pitch + - mp.src37(gate) * amt_s37_d00_pitch + - mp.src38(gate) * amt_s38_d00_pitch + - mp.src39(gate) * amt_s39_d00_pitch + - mp.src40(gate) * amt_s40_d00_pitch + - mp.src41(gate) * amt_s41_d00_pitch + - mp.src42(gate) * amt_s42_d00_pitch + - mp.src43(gate) * amt_s43_d00_pitch + - mp.src44(gate) * amt_s44_d00_pitch + - mp.src45(gate) * amt_s45_d00_pitch + - mp.src46(gate) * amt_s46_d00_pitch + - mp.src47(gate) * amt_s47_d00_pitch; - -mod_op1_level(gate) = - mp.src00(gate) * amt_s00_d01_op1_level + - mp.src01(gate) * amt_s01_d01_op1_level + - mp.src02(gate) * amt_s02_d01_op1_level + - mp.src03(gate) * amt_s03_d01_op1_level + - mp.src04(gate) * amt_s04_d01_op1_level + - mp.src05(gate) * amt_s05_d01_op1_level + - mp.src06(gate) * amt_s06_d01_op1_level + - mp.src07(gate) * amt_s07_d01_op1_level + - mp.src08(gate) * amt_s08_d01_op1_level + - mp.src09(gate) * amt_s09_d01_op1_level + - mp.src10(gate) * amt_s10_d01_op1_level + - mp.src11(gate) * amt_s11_d01_op1_level + - mp.src12(gate) * amt_s12_d01_op1_level + - mp.src13(gate) * amt_s13_d01_op1_level + - mp.src14(gate) * amt_s14_d01_op1_level + - mp.src15(gate) * amt_s15_d01_op1_level + - mp.src16(gate) * amt_s16_d01_op1_level + - mp.src17(gate) * amt_s17_d01_op1_level + - mp.src18(gate) * amt_s18_d01_op1_level + - mp.src19(gate) * amt_s19_d01_op1_level + - mp.src20(gate) * amt_s20_d01_op1_level + - mp.src21(gate) * amt_s21_d01_op1_level + - mp.src22(gate) * amt_s22_d01_op1_level + - mp.src23(gate) * amt_s23_d01_op1_level + - mp.src24(gate) * amt_s24_d01_op1_level + - mp.src25(gate) * amt_s25_d01_op1_level + - mp.src26(gate) * amt_s26_d01_op1_level + - mp.src27(gate) * amt_s27_d01_op1_level + - mp.src28(gate) * amt_s28_d01_op1_level + - mp.src29(gate) * amt_s29_d01_op1_level + - mp.src30(gate) * amt_s30_d01_op1_level + - mp.src31(gate) * amt_s31_d01_op1_level + - mp.src32(gate) * amt_s32_d01_op1_level + - mp.src33(gate) * amt_s33_d01_op1_level + - mp.src34(gate) * amt_s34_d01_op1_level + - mp.src35(gate) * amt_s35_d01_op1_level + - mp.src36(gate) * amt_s36_d01_op1_level + - mp.src37(gate) * amt_s37_d01_op1_level + - mp.src38(gate) * amt_s38_d01_op1_level + - mp.src39(gate) * amt_s39_d01_op1_level + - mp.src40(gate) * amt_s40_d01_op1_level + - mp.src41(gate) * amt_s41_d01_op1_level + - mp.src42(gate) * amt_s42_d01_op1_level + - mp.src43(gate) * amt_s43_d01_op1_level + - mp.src44(gate) * amt_s44_d01_op1_level + - mp.src45(gate) * amt_s45_d01_op1_level + - mp.src46(gate) * amt_s46_d01_op1_level + - mp.src47(gate) * amt_s47_d01_op1_level; - -mod_op2_level(gate) = - mp.src00(gate) * amt_s00_d02_op2_level + - mp.src01(gate) * amt_s01_d02_op2_level + - mp.src02(gate) * amt_s02_d02_op2_level + - mp.src03(gate) * amt_s03_d02_op2_level + - mp.src04(gate) * amt_s04_d02_op2_level + - mp.src05(gate) * amt_s05_d02_op2_level + - mp.src06(gate) * amt_s06_d02_op2_level + - mp.src07(gate) * amt_s07_d02_op2_level + - mp.src08(gate) * amt_s08_d02_op2_level + - mp.src09(gate) * amt_s09_d02_op2_level + - mp.src10(gate) * amt_s10_d02_op2_level + - mp.src11(gate) * amt_s11_d02_op2_level + - mp.src12(gate) * amt_s12_d02_op2_level + - mp.src13(gate) * amt_s13_d02_op2_level + - mp.src14(gate) * amt_s14_d02_op2_level + - mp.src15(gate) * amt_s15_d02_op2_level + - mp.src16(gate) * amt_s16_d02_op2_level + - mp.src17(gate) * amt_s17_d02_op2_level + - mp.src18(gate) * amt_s18_d02_op2_level + - mp.src19(gate) * amt_s19_d02_op2_level + - mp.src20(gate) * amt_s20_d02_op2_level + - mp.src21(gate) * amt_s21_d02_op2_level + - mp.src22(gate) * amt_s22_d02_op2_level + - mp.src23(gate) * amt_s23_d02_op2_level + - mp.src24(gate) * amt_s24_d02_op2_level + - mp.src25(gate) * amt_s25_d02_op2_level + - mp.src26(gate) * amt_s26_d02_op2_level + - mp.src27(gate) * amt_s27_d02_op2_level + - mp.src28(gate) * amt_s28_d02_op2_level + - mp.src29(gate) * amt_s29_d02_op2_level + - mp.src30(gate) * amt_s30_d02_op2_level + - mp.src31(gate) * amt_s31_d02_op2_level + - mp.src32(gate) * amt_s32_d02_op2_level + - mp.src33(gate) * amt_s33_d02_op2_level + - mp.src34(gate) * amt_s34_d02_op2_level + - mp.src35(gate) * amt_s35_d02_op2_level + - mp.src36(gate) * amt_s36_d02_op2_level + - mp.src37(gate) * amt_s37_d02_op2_level + - mp.src38(gate) * amt_s38_d02_op2_level + - mp.src39(gate) * amt_s39_d02_op2_level + - mp.src40(gate) * amt_s40_d02_op2_level + - mp.src41(gate) * amt_s41_d02_op2_level + - mp.src42(gate) * amt_s42_d02_op2_level + - mp.src43(gate) * amt_s43_d02_op2_level + - mp.src44(gate) * amt_s44_d02_op2_level + - mp.src45(gate) * amt_s45_d02_op2_level + - mp.src46(gate) * amt_s46_d02_op2_level + - mp.src47(gate) * amt_s47_d02_op2_level; - -mod_op3_level(gate) = - mp.src00(gate) * amt_s00_d03_op3_level + - mp.src01(gate) * amt_s01_d03_op3_level + - mp.src02(gate) * amt_s02_d03_op3_level + - mp.src03(gate) * amt_s03_d03_op3_level + - mp.src04(gate) * amt_s04_d03_op3_level + - mp.src05(gate) * amt_s05_d03_op3_level + - mp.src06(gate) * amt_s06_d03_op3_level + - mp.src07(gate) * amt_s07_d03_op3_level + - mp.src08(gate) * amt_s08_d03_op3_level + - mp.src09(gate) * amt_s09_d03_op3_level + - mp.src10(gate) * amt_s10_d03_op3_level + - mp.src11(gate) * amt_s11_d03_op3_level + - mp.src12(gate) * amt_s12_d03_op3_level + - mp.src13(gate) * amt_s13_d03_op3_level + - mp.src14(gate) * amt_s14_d03_op3_level + - mp.src15(gate) * amt_s15_d03_op3_level + - mp.src16(gate) * amt_s16_d03_op3_level + - mp.src17(gate) * amt_s17_d03_op3_level + - mp.src18(gate) * amt_s18_d03_op3_level + - mp.src19(gate) * amt_s19_d03_op3_level + - mp.src20(gate) * amt_s20_d03_op3_level + - mp.src21(gate) * amt_s21_d03_op3_level + - mp.src22(gate) * amt_s22_d03_op3_level + - mp.src23(gate) * amt_s23_d03_op3_level + - mp.src24(gate) * amt_s24_d03_op3_level + - mp.src25(gate) * amt_s25_d03_op3_level + - mp.src26(gate) * amt_s26_d03_op3_level + - mp.src27(gate) * amt_s27_d03_op3_level + - mp.src28(gate) * amt_s28_d03_op3_level + - mp.src29(gate) * amt_s29_d03_op3_level + - mp.src30(gate) * amt_s30_d03_op3_level + - mp.src31(gate) * amt_s31_d03_op3_level + - mp.src32(gate) * amt_s32_d03_op3_level + - mp.src33(gate) * amt_s33_d03_op3_level + - mp.src34(gate) * amt_s34_d03_op3_level + - mp.src35(gate) * amt_s35_d03_op3_level + - mp.src36(gate) * amt_s36_d03_op3_level + - mp.src37(gate) * amt_s37_d03_op3_level + - mp.src38(gate) * amt_s38_d03_op3_level + - mp.src39(gate) * amt_s39_d03_op3_level + - mp.src40(gate) * amt_s40_d03_op3_level + - mp.src41(gate) * amt_s41_d03_op3_level + - mp.src42(gate) * amt_s42_d03_op3_level + - mp.src43(gate) * amt_s43_d03_op3_level + - mp.src44(gate) * amt_s44_d03_op3_level + - mp.src45(gate) * amt_s45_d03_op3_level + - mp.src46(gate) * amt_s46_d03_op3_level + - mp.src47(gate) * amt_s47_d03_op3_level; - -mod_op4_level(gate) = - mp.src00(gate) * amt_s00_d04_op4_level + - mp.src01(gate) * amt_s01_d04_op4_level + - mp.src02(gate) * amt_s02_d04_op4_level + - mp.src03(gate) * amt_s03_d04_op4_level + - mp.src04(gate) * amt_s04_d04_op4_level + - mp.src05(gate) * amt_s05_d04_op4_level + - mp.src06(gate) * amt_s06_d04_op4_level + - mp.src07(gate) * amt_s07_d04_op4_level + - mp.src08(gate) * amt_s08_d04_op4_level + - mp.src09(gate) * amt_s09_d04_op4_level + - mp.src10(gate) * amt_s10_d04_op4_level + - mp.src11(gate) * amt_s11_d04_op4_level + - mp.src12(gate) * amt_s12_d04_op4_level + - mp.src13(gate) * amt_s13_d04_op4_level + - mp.src14(gate) * amt_s14_d04_op4_level + - mp.src15(gate) * amt_s15_d04_op4_level + - mp.src16(gate) * amt_s16_d04_op4_level + - mp.src17(gate) * amt_s17_d04_op4_level + - mp.src18(gate) * amt_s18_d04_op4_level + - mp.src19(gate) * amt_s19_d04_op4_level + - mp.src20(gate) * amt_s20_d04_op4_level + - mp.src21(gate) * amt_s21_d04_op4_level + - mp.src22(gate) * amt_s22_d04_op4_level + - mp.src23(gate) * amt_s23_d04_op4_level + - mp.src24(gate) * amt_s24_d04_op4_level + - mp.src25(gate) * amt_s25_d04_op4_level + - mp.src26(gate) * amt_s26_d04_op4_level + - mp.src27(gate) * amt_s27_d04_op4_level + - mp.src28(gate) * amt_s28_d04_op4_level + - mp.src29(gate) * amt_s29_d04_op4_level + - mp.src30(gate) * amt_s30_d04_op4_level + - mp.src31(gate) * amt_s31_d04_op4_level + - mp.src32(gate) * amt_s32_d04_op4_level + - mp.src33(gate) * amt_s33_d04_op4_level + - mp.src34(gate) * amt_s34_d04_op4_level + - mp.src35(gate) * amt_s35_d04_op4_level + - mp.src36(gate) * amt_s36_d04_op4_level + - mp.src37(gate) * amt_s37_d04_op4_level + - mp.src38(gate) * amt_s38_d04_op4_level + - mp.src39(gate) * amt_s39_d04_op4_level + - mp.src40(gate) * amt_s40_d04_op4_level + - mp.src41(gate) * amt_s41_d04_op4_level + - mp.src42(gate) * amt_s42_d04_op4_level + - mp.src43(gate) * amt_s43_d04_op4_level + - mp.src44(gate) * amt_s44_d04_op4_level + - mp.src45(gate) * amt_s45_d04_op4_level + - mp.src46(gate) * amt_s46_d04_op4_level + - mp.src47(gate) * amt_s47_d04_op4_level; - -mod_cross_mod_global(gate) = - mp.src00(gate) * amt_s00_d05_cross_mod_global + - mp.src01(gate) * amt_s01_d05_cross_mod_global + - mp.src02(gate) * amt_s02_d05_cross_mod_global + - mp.src03(gate) * amt_s03_d05_cross_mod_global + - mp.src04(gate) * amt_s04_d05_cross_mod_global + - mp.src05(gate) * amt_s05_d05_cross_mod_global + - mp.src06(gate) * amt_s06_d05_cross_mod_global + - mp.src07(gate) * amt_s07_d05_cross_mod_global + - mp.src08(gate) * amt_s08_d05_cross_mod_global + - mp.src09(gate) * amt_s09_d05_cross_mod_global + - mp.src10(gate) * amt_s10_d05_cross_mod_global + - mp.src11(gate) * amt_s11_d05_cross_mod_global + - mp.src12(gate) * amt_s12_d05_cross_mod_global + - mp.src13(gate) * amt_s13_d05_cross_mod_global + - mp.src14(gate) * amt_s14_d05_cross_mod_global + - mp.src15(gate) * amt_s15_d05_cross_mod_global + - mp.src16(gate) * amt_s16_d05_cross_mod_global + - mp.src17(gate) * amt_s17_d05_cross_mod_global + - mp.src18(gate) * amt_s18_d05_cross_mod_global + - mp.src19(gate) * amt_s19_d05_cross_mod_global + - mp.src20(gate) * amt_s20_d05_cross_mod_global + - mp.src21(gate) * amt_s21_d05_cross_mod_global + - mp.src22(gate) * amt_s22_d05_cross_mod_global + - mp.src23(gate) * amt_s23_d05_cross_mod_global + - mp.src24(gate) * amt_s24_d05_cross_mod_global + - mp.src25(gate) * amt_s25_d05_cross_mod_global + - mp.src26(gate) * amt_s26_d05_cross_mod_global + - mp.src27(gate) * amt_s27_d05_cross_mod_global + - mp.src28(gate) * amt_s28_d05_cross_mod_global + - mp.src29(gate) * amt_s29_d05_cross_mod_global + - mp.src30(gate) * amt_s30_d05_cross_mod_global + - mp.src31(gate) * amt_s31_d05_cross_mod_global + - mp.src32(gate) * amt_s32_d05_cross_mod_global + - mp.src33(gate) * amt_s33_d05_cross_mod_global + - mp.src34(gate) * amt_s34_d05_cross_mod_global + - mp.src35(gate) * amt_s35_d05_cross_mod_global + - mp.src36(gate) * amt_s36_d05_cross_mod_global + - mp.src37(gate) * amt_s37_d05_cross_mod_global + - mp.src38(gate) * amt_s38_d05_cross_mod_global + - mp.src39(gate) * amt_s39_d05_cross_mod_global + - mp.src40(gate) * amt_s40_d05_cross_mod_global + - mp.src41(gate) * amt_s41_d05_cross_mod_global + - mp.src42(gate) * amt_s42_d05_cross_mod_global + - mp.src43(gate) * amt_s43_d05_cross_mod_global + - mp.src44(gate) * amt_s44_d05_cross_mod_global + - mp.src45(gate) * amt_s45_d05_cross_mod_global + - mp.src46(gate) * amt_s46_d05_cross_mod_global + - mp.src47(gate) * amt_s47_d05_cross_mod_global; - -mod_feedback_global(gate) = - mp.src00(gate) * amt_s00_d06_feedback_global + - mp.src01(gate) * amt_s01_d06_feedback_global + - mp.src02(gate) * amt_s02_d06_feedback_global + - mp.src03(gate) * amt_s03_d06_feedback_global + - mp.src04(gate) * amt_s04_d06_feedback_global + - mp.src05(gate) * amt_s05_d06_feedback_global + - mp.src06(gate) * amt_s06_d06_feedback_global + - mp.src07(gate) * amt_s07_d06_feedback_global + - mp.src08(gate) * amt_s08_d06_feedback_global + - mp.src09(gate) * amt_s09_d06_feedback_global + - mp.src10(gate) * amt_s10_d06_feedback_global + - mp.src11(gate) * amt_s11_d06_feedback_global + - mp.src12(gate) * amt_s12_d06_feedback_global + - mp.src13(gate) * amt_s13_d06_feedback_global + - mp.src14(gate) * amt_s14_d06_feedback_global + - mp.src15(gate) * amt_s15_d06_feedback_global + - mp.src16(gate) * amt_s16_d06_feedback_global + - mp.src17(gate) * amt_s17_d06_feedback_global + - mp.src18(gate) * amt_s18_d06_feedback_global + - mp.src19(gate) * amt_s19_d06_feedback_global + - mp.src20(gate) * amt_s20_d06_feedback_global + - mp.src21(gate) * amt_s21_d06_feedback_global + - mp.src22(gate) * amt_s22_d06_feedback_global + - mp.src23(gate) * amt_s23_d06_feedback_global + - mp.src24(gate) * amt_s24_d06_feedback_global + - mp.src25(gate) * amt_s25_d06_feedback_global + - mp.src26(gate) * amt_s26_d06_feedback_global + - mp.src27(gate) * amt_s27_d06_feedback_global + - mp.src28(gate) * amt_s28_d06_feedback_global + - mp.src29(gate) * amt_s29_d06_feedback_global + - mp.src30(gate) * amt_s30_d06_feedback_global + - mp.src31(gate) * amt_s31_d06_feedback_global + - mp.src32(gate) * amt_s32_d06_feedback_global + - mp.src33(gate) * amt_s33_d06_feedback_global + - mp.src34(gate) * amt_s34_d06_feedback_global + - mp.src35(gate) * amt_s35_d06_feedback_global + - mp.src36(gate) * amt_s36_d06_feedback_global + - mp.src37(gate) * amt_s37_d06_feedback_global + - mp.src38(gate) * amt_s38_d06_feedback_global + - mp.src39(gate) * amt_s39_d06_feedback_global + - mp.src40(gate) * amt_s40_d06_feedback_global + - mp.src41(gate) * amt_s41_d06_feedback_global + - mp.src42(gate) * amt_s42_d06_feedback_global + - mp.src43(gate) * amt_s43_d06_feedback_global + - mp.src44(gate) * amt_s44_d06_feedback_global + - mp.src45(gate) * amt_s45_d06_feedback_global + - mp.src46(gate) * amt_s46_d06_feedback_global + - mp.src47(gate) * amt_s47_d06_feedback_global; - -mod_global_ratio(gate) = - mp.src00(gate) * amt_s00_d07_global_ratio + - mp.src01(gate) * amt_s01_d07_global_ratio + - mp.src02(gate) * amt_s02_d07_global_ratio + - mp.src03(gate) * amt_s03_d07_global_ratio + - mp.src04(gate) * amt_s04_d07_global_ratio + - mp.src05(gate) * amt_s05_d07_global_ratio + - mp.src06(gate) * amt_s06_d07_global_ratio + - mp.src07(gate) * amt_s07_d07_global_ratio + - mp.src08(gate) * amt_s08_d07_global_ratio + - mp.src09(gate) * amt_s09_d07_global_ratio + - mp.src10(gate) * amt_s10_d07_global_ratio + - mp.src11(gate) * amt_s11_d07_global_ratio + - mp.src12(gate) * amt_s12_d07_global_ratio + - mp.src13(gate) * amt_s13_d07_global_ratio + - mp.src14(gate) * amt_s14_d07_global_ratio + - mp.src15(gate) * amt_s15_d07_global_ratio + - mp.src16(gate) * amt_s16_d07_global_ratio + - mp.src17(gate) * amt_s17_d07_global_ratio + - mp.src18(gate) * amt_s18_d07_global_ratio + - mp.src19(gate) * amt_s19_d07_global_ratio + - mp.src20(gate) * amt_s20_d07_global_ratio + - mp.src21(gate) * amt_s21_d07_global_ratio + - mp.src22(gate) * amt_s22_d07_global_ratio + - mp.src23(gate) * amt_s23_d07_global_ratio + - mp.src24(gate) * amt_s24_d07_global_ratio + - mp.src25(gate) * amt_s25_d07_global_ratio + - mp.src26(gate) * amt_s26_d07_global_ratio + - mp.src27(gate) * amt_s27_d07_global_ratio + - mp.src28(gate) * amt_s28_d07_global_ratio + - mp.src29(gate) * amt_s29_d07_global_ratio + - mp.src30(gate) * amt_s30_d07_global_ratio + - mp.src31(gate) * amt_s31_d07_global_ratio + - mp.src32(gate) * amt_s32_d07_global_ratio + - mp.src33(gate) * amt_s33_d07_global_ratio + - mp.src34(gate) * amt_s34_d07_global_ratio + - mp.src35(gate) * amt_s35_d07_global_ratio + - mp.src36(gate) * amt_s36_d07_global_ratio + - mp.src37(gate) * amt_s37_d07_global_ratio + - mp.src38(gate) * amt_s38_d07_global_ratio + - mp.src39(gate) * amt_s39_d07_global_ratio + - mp.src40(gate) * amt_s40_d07_global_ratio + - mp.src41(gate) * amt_s41_d07_global_ratio + - mp.src42(gate) * amt_s42_d07_global_ratio + - mp.src43(gate) * amt_s43_d07_global_ratio + - mp.src44(gate) * amt_s44_d07_global_ratio + - mp.src45(gate) * amt_s45_d07_global_ratio + - mp.src46(gate) * amt_s46_d07_global_ratio + - mp.src47(gate) * amt_s47_d07_global_ratio; - -mod_amp(gate) = - mp.src00(gate) * amt_s00_d08_amp + - mp.src01(gate) * amt_s01_d08_amp + - mp.src02(gate) * amt_s02_d08_amp + - mp.src03(gate) * amt_s03_d08_amp + - mp.src04(gate) * amt_s04_d08_amp + - mp.src05(gate) * amt_s05_d08_amp + - mp.src06(gate) * amt_s06_d08_amp + - mp.src07(gate) * amt_s07_d08_amp + - mp.src08(gate) * amt_s08_d08_amp + - mp.src09(gate) * amt_s09_d08_amp + - mp.src10(gate) * amt_s10_d08_amp + - mp.src11(gate) * amt_s11_d08_amp + - mp.src12(gate) * amt_s12_d08_amp + - mp.src13(gate) * amt_s13_d08_amp + - mp.src14(gate) * amt_s14_d08_amp + - mp.src15(gate) * amt_s15_d08_amp + - mp.src16(gate) * amt_s16_d08_amp + - mp.src17(gate) * amt_s17_d08_amp + - mp.src18(gate) * amt_s18_d08_amp + - mp.src19(gate) * amt_s19_d08_amp + - mp.src20(gate) * amt_s20_d08_amp + - mp.src21(gate) * amt_s21_d08_amp + - mp.src22(gate) * amt_s22_d08_amp + - mp.src23(gate) * amt_s23_d08_amp + - mp.src24(gate) * amt_s24_d08_amp + - mp.src25(gate) * amt_s25_d08_amp + - mp.src26(gate) * amt_s26_d08_amp + - mp.src27(gate) * amt_s27_d08_amp + - mp.src28(gate) * amt_s28_d08_amp + - mp.src29(gate) * amt_s29_d08_amp + - mp.src30(gate) * amt_s30_d08_amp + - mp.src31(gate) * amt_s31_d08_amp + - mp.src32(gate) * amt_s32_d08_amp + - mp.src33(gate) * amt_s33_d08_amp + - mp.src34(gate) * amt_s34_d08_amp + - mp.src35(gate) * amt_s35_d08_amp + - mp.src36(gate) * amt_s36_d08_amp + - mp.src37(gate) * amt_s37_d08_amp + - mp.src38(gate) * amt_s38_d08_amp + - mp.src39(gate) * amt_s39_d08_amp + - mp.src40(gate) * amt_s40_d08_amp + - mp.src41(gate) * amt_s41_d08_amp + - mp.src42(gate) * amt_s42_d08_amp + - mp.src43(gate) * amt_s43_d08_amp + - mp.src44(gate) * amt_s44_d08_amp + - mp.src45(gate) * amt_s45_d08_amp + - mp.src46(gate) * amt_s46_d08_amp + - mp.src47(gate) * amt_s47_d08_amp; - -mod_pan(gate) = - mp.src00(gate) * amt_s00_d09_pan + - mp.src01(gate) * amt_s01_d09_pan + - mp.src02(gate) * amt_s02_d09_pan + - mp.src03(gate) * amt_s03_d09_pan + - mp.src04(gate) * amt_s04_d09_pan + - mp.src05(gate) * amt_s05_d09_pan + - mp.src06(gate) * amt_s06_d09_pan + - mp.src07(gate) * amt_s07_d09_pan + - mp.src08(gate) * amt_s08_d09_pan + - mp.src09(gate) * amt_s09_d09_pan + - mp.src10(gate) * amt_s10_d09_pan + - mp.src11(gate) * amt_s11_d09_pan + - mp.src12(gate) * amt_s12_d09_pan + - mp.src13(gate) * amt_s13_d09_pan + - mp.src14(gate) * amt_s14_d09_pan + - mp.src15(gate) * amt_s15_d09_pan + - mp.src16(gate) * amt_s16_d09_pan + - mp.src17(gate) * amt_s17_d09_pan + - mp.src18(gate) * amt_s18_d09_pan + - mp.src19(gate) * amt_s19_d09_pan + - mp.src20(gate) * amt_s20_d09_pan + - mp.src21(gate) * amt_s21_d09_pan + - mp.src22(gate) * amt_s22_d09_pan + - mp.src23(gate) * amt_s23_d09_pan + - mp.src24(gate) * amt_s24_d09_pan + - mp.src25(gate) * amt_s25_d09_pan + - mp.src26(gate) * amt_s26_d09_pan + - mp.src27(gate) * amt_s27_d09_pan + - mp.src28(gate) * amt_s28_d09_pan + - mp.src29(gate) * amt_s29_d09_pan + - mp.src30(gate) * amt_s30_d09_pan + - mp.src31(gate) * amt_s31_d09_pan + - mp.src32(gate) * amt_s32_d09_pan + - mp.src33(gate) * amt_s33_d09_pan + - mp.src34(gate) * amt_s34_d09_pan + - mp.src35(gate) * amt_s35_d09_pan + - mp.src36(gate) * amt_s36_d09_pan + - mp.src37(gate) * amt_s37_d09_pan + - mp.src38(gate) * amt_s38_d09_pan + - mp.src39(gate) * amt_s39_d09_pan + - mp.src40(gate) * amt_s40_d09_pan + - mp.src41(gate) * amt_s41_d09_pan + - mp.src42(gate) * amt_s42_d09_pan + - mp.src43(gate) * amt_s43_d09_pan + - mp.src44(gate) * amt_s44_d09_pan + - mp.src45(gate) * amt_s45_d09_pan + - mp.src46(gate) * amt_s46_d09_pan + - mp.src47(gate) * amt_s47_d09_pan; - -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -cent2ratio(c) = pow(2.0, c / 1200.0); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Base frequency: fine tune + pitch mod (semitones, ±12) -base_freq = freq * cent2ratio(fine_tune) * semi2ratio(mod_pitch(gate) * 12.0); - -// Velocity gain — keeps _vel alive -vel_gain = (1.0 - 0.3) + 0.3 * vel; - -// --------------------------------------------------------------------------- -// Effective per-operator ratios and levels (mod applied) -// global_ratio mod is an additive ratio offset (±8) -// --------------------------------------------------------------------------- -ratio_offset = mod_global_ratio(gate) * 8.0; -r1 = max(0.01, op1_ratio + ratio_offset); -r2 = max(0.01, op2_ratio + ratio_offset); -r3 = max(0.01, op3_ratio + ratio_offset); -r4 = max(0.01, op4_ratio + ratio_offset); - -lev1 = max(0.0, min(1.0, op1_level + mod_op1_level(gate))); -lev2 = max(0.0, min(1.0, op2_level + mod_op2_level(gate))); -lev3 = max(0.0, min(1.0, op3_level + mod_op3_level(gate))); -lev4 = max(0.0, min(1.0, op4_level + mod_op4_level(gate))); - -// --------------------------------------------------------------------------- -// Global multipliers for cross-mod and feedback. -// (1 + mod) in [0, 2] roughly; clamp to stay sane. -// --------------------------------------------------------------------------- -cross_mul = max(0.0, 1.0 + mod_cross_mod_global(gate)); -fb_mul = max(0.0, 1.0 + mod_feedback_global(gate)); - -m12e = m12 * cross_mul; m13e = m13 * cross_mul; m14e = m14 * cross_mul; -m21e = m21 * cross_mul; m23e = m23 * cross_mul; m24e = m24 * cross_mul; -m31e = m31 * cross_mul; m32e = m32 * cross_mul; m34e = m34 * cross_mul; -m41e = m41 * cross_mul; m42e = m42 * cross_mul; m43e = m43 * cross_mul; - -fb1e = fb1 * fb_mul; -fb2e = fb2 * fb_mul; -fb3e = fb3 * fb_mul; -fb4e = fb4 * fb_mul; - -// --------------------------------------------------------------------------- -// 4-operator FM bus — feedback loop via ~ on a 4-channel bus -// --------------------------------------------------------------------------- -fmbus(d1,d2,d3,d4) = - op1_out, op2_out, op3_out, op4_out -with { - f1 = base_freq * r1; - f2 = base_freq * r2; - f3 = base_freq * r3; - f4 = base_freq * r4; - - op1_out = lev1 * - os.osc(f1 + (m21e*d2 + m31e*d3 + m41e*d4 + fb1e*d1) * f1); - - op2_out = lev2 * - os.osc(f2 + (m12e*d1 + m32e*d3 + m42e*d4 + fb2e*d2) * f2); - - op3_out = lev3 * - os.osc(f3 + (m13e*d1 + m23e*d2 + m43e*d4 + fb3e*d3) * f3); - - op4_out = lev4 * - os.osc(f4 + (m14e*d1 + m24e*d2 + m34e*d3 + fb4e*d4) * f4); -}; - -// Route 4-channel bus through feedback loop, sum all ops -fm_out = (fmbus ~ (si.bus(4))) : (_, _, _, _) :> _; - -// --------------------------------------------------------------------------- -// Post-processing -// --------------------------------------------------------------------------- -soft_clip(x) = x / max(0.001, 1.0 + output_saturation * abs(x)); -hp_out(x) = fi.highpass(1, output_hp, x); - -// Master amplitude destination (route an ADSR here for VCA) -// See subtractive engine for the positive-only mod_amp rationale. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain * 0.25; - -// Pan -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -// Stereo spread via slight detuning (apply after main FM sum) -spread_cents = stereo_spread * 5.0; -spread_l = cent2ratio(spread_cents); -spread_r = cent2ratio(0.0 - spread_cents); - -mono_post = fm_out * amp_val : soft_clip : hp_out; - -signal_L = mono_post * spread_l * pan_l; -signal_R = mono_post * spread_r * pan_r; - -process = signal_L, signal_R; diff --git a/playground/faust/modular-fm.json b/playground/faust/modular-fm.json deleted file mode 100644 index 7b46305..0000000 --- a/playground/faust/modular-fm.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "modular-fm","filename": "modular-fm.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn modular_fm -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust/mod-pool.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/oscillators.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/envelopes.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 265320,"code": "Ly8gbW9kdWxhci1mbS5kc3Ag4oCUIDQtb3BlcmF0b3IgRk0gdm9pY2UgZm9yIHRoZSAiTW9kdWxhciIgbW9kZS4KLy8KLy8gQVVUTy1HRU5FUkFURUQgYnkgZ2VuLW1vZHVsYXItZHNwLnB5LiBETyBOT1QgRURJVCBCWSBIQU5ELgovLyBSZWdlbmVyYXRlIHdpdGg6Ci8vICAgICBjZCBwbGF5Z3JvdW5kL2ZhdXN0ICYmIHB5dGhvbjMgZ2VuLW1vZHVsYXItZHNwLnB5Ci8vCi8vIEF1ZGlvIHBhdGg6IDQgb3BzIHdpdGggMTItY2VsbCBjcm9zcy1tb2QgbWF0cml4IGFuZCBwZXItb3AgZmVlZGJhY2suCi8vIEFsbCBlbnZlbG9wZXMgYW5kIExGT3MgY29tZSBmcm9tIG1vZC1wb29sLmxpYiB2aWEgYSA0OC1zb3VyY2Ugw5cgMTAtZGVzdAovLyBtb2R1bGF0aW9uIG1hdHJpeC4gTm8gaW50ZXJuYWwgZW52cy9MRk9zLgovLwovLyBNb2R1bGF0aW9uIGRlc3RpbmF0aW9ucyAoMTApOgovLyAgICAgMCBwaXRjaCAgICAgICAgICAgICBzZW1pdG9uZXMgYWRkZWQgdG8gdGhlIGJhc2UgZnJlcXVlbmN5Ci8vICAgICAxIG9wMV9sZXZlbCAgICAgICAgIGFkZGVkIHRvIG9wMSBsZXZlbCBrbm9iCi8vICAgICAyIG9wMl9sZXZlbCAgICAgICAgIGFkZGVkIHRvIG9wMiBsZXZlbCBrbm9iCi8vICAgICAzIG9wM19sZXZlbCAgICAgICAgIGFkZGVkIHRvIG9wMyBsZXZlbCBrbm9iCi8vICAgICA0IG9wNF9sZXZlbCAgICAgICAgIGFkZGVkIHRvIG9wNCBsZXZlbCBrbm9iCi8vICAgICA1IGNyb3NzX21vZF9nbG9iYWwgIGdsb2JhbCBGTSBkZXB0aCBtdWx0aXBsaWVyIG9uIGFsbCAxMiBjcm9zcy1tb2QgY2VsbHMKLy8gICAgIDYgZmVlZGJhY2tfZ2xvYmFsICAgZ2xvYmFsIGZlZWRiYWNrIG11bHRpcGxpZXIgb24gYWxsIDQgc2VsZi1mZWVkYmFjayBjZWxscwovLyAgICAgNyBnbG9iYWxfcmF0aW8gICAgICBhZGRpdGl2ZSBzY2FsZSBmYWN0b3Igb24gYWxsIDQgb3BlcmF0b3IgcmF0aW9zCi8vICAgICA4IGFtcCAgICAgICAgICAgICAgIG1hc3RlciBhbXBsaXR1ZGUgZGVzdCAocm91dGUgYW4gQURTUiBoZXJlIGZvciBhIFZDQSBlbnYpCi8vICAgICA5IHBhbiAgICAgICAgICAgICAgIHN0ZXJlbyBiYWxhbmNlCi8vCi8vIEhpZGRlbiBjb250cm9scyAoZHJpdmVuIGJ5IG5vdGVPbi9ub3RlT2ZmLCBub3QgYnkgdGhlIE1MIGVuZ2luZSk6Ci8vICAgICBmcmVxLCBnYXRlLCBfdmVsCi8vCi8vIEJ1aWxkOgovLyAgICAgY2QgcGxheWdyb3VuZC9mYXVzdCAmJiAuL2J1aWxkLnNoCi8vCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKaW1wb3J0KCJzdGRmYXVzdC5saWIiKTsKbXAgPSBsaWJyYXJ5KCJtb2QtcG9vbC5saWIiKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBIaWRkZW4gY29udHJvbHMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmZyZXEgPSBoc2xpZGVyKCIwX0hpZGRlbi9mcmVxW2hpZGRlbjoxXVt1bml0Okh6XSIsIDIyMCwgMjAsIDQwMDAsIDAuMDEpOwpnYXRlID0gYnV0dG9uKCIwX0hpZGRlbi9nYXRlW2hpZGRlbjoxXSIpOwp2ZWwgID0gaHNsaWRlcigiMF9IaWRkZW4vX3ZlbFtoaWRkZW46MV0iLCAwLjcsIDAuMCwgMS4wLCAwLjAwMSkgOiBzaS5zbW9vOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDEg4oCUIE9wZXJhdG9ycyAocmF0aW8gJiBsZXZlbCBwZXIgb3ApCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpvcDFfcmF0aW8gPSBoc2xpZGVyKCIxX09wZXJhdG9ycy8wMF9vcDFfcmF0aW8iLCAxLjAsIDAuMTI1LCAxNi4wLCAwLjAwMSk7Cm9wMV9sZXZlbCA9IGhzbGlkZXIoIjFfT3BlcmF0b3JzLzAxX29wMV9sZXZlbCIsIDAuOCwgMC4wLCAgIDEuMCwgIDAuMDAxKTsKb3AyX3JhdGlvID0gaHNsaWRlcigiMV9PcGVyYXRvcnMvMDJfb3AyX3JhdGlvIiwgMi4wLCAwLjEyNSwgMTYuMCwgMC4wMDEpOwpvcDJfbGV2ZWwgPSBoc2xpZGVyKCIxX09wZXJhdG9ycy8wM19vcDJfbGV2ZWwiLCAwLjYsIDAuMCwgICAxLjAsICAwLjAwMSk7Cm9wM19yYXRpbyA9IGhzbGlkZXIoIjFfT3BlcmF0b3JzLzA0X29wM19yYXRpbyIsIDMuMCwgMC4xMjUsIDE2LjAsIDAuMDAxKTsKb3AzX2xldmVsID0gaHNsaWRlcigiMV9PcGVyYXRvcnMvMDVfb3AzX2xldmVsIiwgMC40LCAwLjAsICAgMS4wLCAgMC4wMDEpOwpvcDRfcmF0aW8gPSBoc2xpZGVyKCIxX09wZXJhdG9ycy8wNl9vcDRfcmF0aW8iLCAwLjUsIDAuMTI1LCAxNi4wLCAwLjAwMSk7Cm9wNF9sZXZlbCA9IGhzbGlkZXIoIjFfT3BlcmF0b3JzLzA3X29wNF9sZXZlbCIsIDAuMywgMC4wLCAgIDEuMCwgIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAyIOKAlCBDcm9zcy1tb2QgTWF0cml4IChtWFkgPSBvcCBYIG1vZHVsYXRlcyBvcCBZKQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KbTEyID0gaHNsaWRlcigiMl9Dcm9zc01vZC8wMF9tMTIiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpOwptMTMgPSBoc2xpZGVyKCIyX0Nyb3NzTW9kLzAxX20xMyIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSk7Cm0xNCA9IGhzbGlkZXIoIjJfQ3Jvc3NNb2QvMDJfbTE0IiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKTsKbTIxID0gaHNsaWRlcigiMl9Dcm9zc01vZC8wM19tMjEiLCAxLjAsIDAuMCwgMTAuMCwgMC4wMDEpOwptMjMgPSBoc2xpZGVyKCIyX0Nyb3NzTW9kLzA0X20yMyIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSk7Cm0yNCA9IGhzbGlkZXIoIjJfQ3Jvc3NNb2QvMDVfbTI0IiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKTsKbTMxID0gaHNsaWRlcigiMl9Dcm9zc01vZC8wNl9tMzEiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpOwptMzIgPSBoc2xpZGVyKCIyX0Nyb3NzTW9kLzA3X20zMiIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSk7Cm0zNCA9IGhzbGlkZXIoIjJfQ3Jvc3NNb2QvMDhfbTM0IiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKTsKbTQxID0gaHNsaWRlcigiMl9Dcm9zc01vZC8wOV9tNDEiLCAwLjAsIDAuMCwgMTAuMCwgMC4wMDEpOwptNDIgPSBoc2xpZGVyKCIyX0Nyb3NzTW9kLzEwX200MiIsIDAuMCwgMC4wLCAxMC4wLCAwLjAwMSk7Cm00MyA9IGhzbGlkZXIoIjJfQ3Jvc3NNb2QvMTFfbTQzIiwgMC4wLCAwLjAsIDEwLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCAzIOKAlCBTZWxmLWZlZWRiYWNrCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmYjEgPSBoc2xpZGVyKCIzX0ZlZWRiYWNrLzAwX2ZiMSIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKZmIyID0gaHNsaWRlcigiM19GZWVkYmFjay8wMV9mYjIiLCAwLjAsIDAuMCwgMS4wLCAwLjAwMSk7CmZiMyA9IGhzbGlkZXIoIjNfRmVlZGJhY2svMDJfZmIzIiwgMC4wLCAwLjAsIDEuMCwgMC4wMDEpOwpmYjQgPSBoc2xpZGVyKCIzX0ZlZWRiYWNrLzAzX2ZiNCIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHcm91cCA0IOKAlCBNYXN0ZXIKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCm1hc3Rlcl9sZXZlbCAgICAgID0gaHNsaWRlcigiNF9NYXN0ZXIvMDBfbWFzdGVyX2xldmVsIiwgICAgICAwLjcsICAwLjAsICAxLjAsICAwLjAwMSk7CmZpbmVfdHVuZSAgICAgICAgID0gaHNsaWRlcigiNF9NYXN0ZXIvMDFfZmluZV90dW5lW3VuaXQ6Y3RdIiwgMC4wLCAtNTAuMCwgNTAuMCwgMC4xKTsKc3RlcmVvX3NwcmVhZCAgICAgPSBoc2xpZGVyKCI0X01hc3Rlci8wMl9zdGVyZW9fc3ByZWFkIiwgICAgIDAuMSwgIDAuMCwgIDEuMCwgIDAuMDAxKTsKb3V0cHV0X3NhdHVyYXRpb24gPSBoc2xpZGVyKCI0X01hc3Rlci8wM19vdXRwdXRfc2F0dXJhdGlvbiIsIDAuMCwgIDAuMCwgIDEuMCwgIDAuMDAxKTsKb3V0cHV0X2hwICAgICAgICAgPSBoc2xpZGVyKCI0X01hc3Rlci8wNF9vdXRwdXRfaHBbdW5pdDpIel0iLDIwLjAsIDIwLjAsIDIwMC4wLDAuMSk7Cm1hc3Rlcl9wYW4gICAgICAgID0gaHNsaWRlcigiNF9NYXN0ZXIvMDVfbWFzdGVyX3BhbiIsICAgICAgICAwLjAsIC0xLjAsICAxLjAsICAwLjAwMSk7Ci8vIFN0YXRpYyBhbXAgZmxvb3Ig4oCUIHNlZSBzdWJ0cmFjdGl2ZSBlbmdpbmUgZm9yIGZ1bGwgZXhwbGFuYXRpb24uCmJhc2VfYW1wICAgICAgICAgID0gaHNsaWRlcigiNF9NYXN0ZXIvMDZfYmFzZV9hbXAiLCAgICAgICAgICAxLjAsICAwLjAsICAxLjAsICAwLjAwMSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gTW9kdWxhdGlvbiBtYXRyaXgg4oCUIDQ4IHNvdXJjZXMgw5cgMTAgZGVzdGluYXRpb25zLgovLyBFYWNoIGhzbGlkZXIgaXMgb25lICJhbW91bnQiIGVudHJ5IGluIFstMSwgKzFdLiBaZXJvID0gbm8gY29ubmVjdGlvbi4KLy8gR3JvdXAgIk1NX01hdHJpeCIgc28gdGhlIHBhcmFtTWV0YSBwYXJzZXIgY2FuIHJlY29nbmlzZSBtYXRyaXggcGFyYW1zLgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDAgPSBwaXRjaAphbXRfczAwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMSA9IG9wMV9sZXZlbAphbXRfczAwX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwMV9vcDFfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwMV9vcDFfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMiA9IG9wMl9sZXZlbAphbXRfczAwX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwMl9vcDJfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwMl9vcDJfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMyA9IG9wM19sZXZlbAphbXRfczAwX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwM19vcDNfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwM19vcDNfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gNCA9IG9wNF9sZXZlbAphbXRfczAwX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwNF9vcDRfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwNF9vcDRfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gNSA9IGNyb3NzX21vZF9nbG9iYWwKYW10X3MwMF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDA1X2Nyb3NzX21vZF9nbG9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwNV9jcm9zc19tb2RfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDA1X2Nyb3NzX21vZF9nbG9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwNV9jcm9zc19tb2RfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDYgPSBmZWVkYmFja19nbG9iYWwKYW10X3MwMF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDZfZmVlZGJhY2tfZ2xvYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDZfZmVlZGJhY2tfZ2xvYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDcgPSBnbG9iYWxfcmF0aW8KYW10X3MwMF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDdfZ2xvYmFsX3JhdGlvID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDdfZ2xvYmFsX3JhdGlvIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDggPSBhbXAKYW10X3MwMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDhfYW1wID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDhfYW1wIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDkgPSBwYW4KYW10X3MwMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDlfcGFuID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDlfcGFuIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBEZXN0aW5hdGlvbiBzaWduYWxzIOKAlCBvbmUgcGVyIG1vZHVsYXRpb24gZGVzdGluYXRpb24uIEVhY2ggaXMgYSA0OC10ZXJtCi8vIHdlaWdodGVkIHN1bSBvZiBzb3VyY2VzIMOXIGFtb3VudHMuIFRoZSBgZ2F0ZWAgYXJndW1lbnQgaXMgdGhyZWFkZWQgdGhyb3VnaAovLyBzbyB0aGF0IG1wLnNyYz8/KGdhdGUpIGNhbiBkcml2ZSBBRFNSIGVudmVsb3Blcy4KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgptb2RfcGl0Y2goZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwMF9waXRjaCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDAwX3BpdGNoICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwMF9waXRjaCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwMF9waXRjaCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwMF9waXRjaCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwMF9waXRjaCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwMF9waXRjaCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwMF9waXRjaCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDAwX3BpdGNoICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwMF9waXRjaCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDAwX3BpdGNoICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwMF9waXRjaCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwMF9waXRjaCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwMF9waXRjaCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDAwX3BpdGNoICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwMF9waXRjaCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwMF9waXRjaCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwMF9waXRjaCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwMF9waXRjaCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwMF9waXRjaCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDBfcGl0Y2g7Cgptb2Rfb3AxX2xldmVsKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDFfb3AxX2xldmVsICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDFfb3AxX2xldmVsOwoKbW9kX29wMl9sZXZlbChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDAyX29wMl9sZXZlbCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDAyX29wMl9sZXZlbDsKCm1vZF9vcDNfbGV2ZWwoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwM19vcDNfbGV2ZWwgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwM19vcDNfbGV2ZWw7Cgptb2Rfb3A0X2xldmVsKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDRfb3A0X2xldmVsICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDRfb3A0X2xldmVsOwoKbW9kX2Nyb3NzX21vZF9nbG9iYWwoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDA1X2Nyb3NzX21vZF9nbG9iYWwgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwNV9jcm9zc19tb2RfZ2xvYmFsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDVfY3Jvc3NfbW9kX2dsb2JhbCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDA1X2Nyb3NzX21vZF9nbG9iYWw7Cgptb2RfZmVlZGJhY2tfZ2xvYmFsKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDZfZmVlZGJhY2tfZ2xvYmFsICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDZfZmVlZGJhY2tfZ2xvYmFsOwoKbW9kX2dsb2JhbF9yYXRpbyhnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDA3X2dsb2JhbF9yYXRpbyArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDA3X2dsb2JhbF9yYXRpbzsKCm1vZF9hbXAoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwOF9hbXAgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwOF9hbXAgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwOF9hbXAgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwOF9hbXAgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwOF9hbXAgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwOF9hbXAgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwOF9hbXAgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwOF9hbXAgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwOF9hbXAgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwOF9hbXAgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwOF9hbXAgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwOF9hbXAgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwOF9hbXAgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwOF9hbXAgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwOF9hbXAgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwOF9hbXAgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwOF9hbXAgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwOF9hbXAgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwOF9hbXAgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwOF9hbXAgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwOF9hbXAgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwOF9hbXAgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwOF9hbXAgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwOF9hbXAgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwOF9hbXAgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwOF9hbXAgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwOF9hbXAgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwOF9hbXAgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwOF9hbXAgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwOF9hbXAgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwOF9hbXAgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwOF9hbXAgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwOF9hbXAgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwOF9hbXAgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwOF9hbXAgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwOF9hbXAgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwOF9hbXAgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwOF9hbXAgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwOF9hbXAgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwOF9hbXAgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwOF9hbXAgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwOF9hbXAgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwOF9hbXAgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwOF9hbXAgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwOF9hbXAgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwOF9hbXAgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwOF9hbXAgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwOF9hbXA7Cgptb2RfcGFuKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDlfcGFuICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDlfcGFuICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDlfcGFuICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDlfcGFuICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDlfcGFuICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDlfcGFuICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDlfcGFuICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDlfcGFuICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDlfcGFuICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDlfcGFuICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDlfcGFuICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDlfcGFuICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDlfcGFuICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDlfcGFuICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDlfcGFuICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDlfcGFuICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDlfcGFuICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDlfcGFuICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDlfcGFuICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDlfcGFuICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDlfcGFuICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDlfcGFuICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDlfcGFuICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDlfcGFuICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDlfcGFuICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDlfcGFuICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDlfcGFuICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDlfcGFuICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDlfcGFuICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDlfcGFuICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDlfcGFuICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDlfcGFuICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDlfcGFuICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDlfcGFuICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDlfcGFuICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDlfcGFuICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDlfcGFuICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDlfcGFuICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDlfcGFuICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDlfcGFuICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDlfcGFuICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDlfcGFuICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDlfcGFuICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDlfcGFuICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDlfcGFuICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDlfcGFuICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDlfcGFuICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDlfcGFuOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIERlcml2ZWQgc2lnbmFscwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmNlbnQycmF0aW8oYykgPSBwb3coMi4wLCBjIC8gMTIwMC4wKTsKc2VtaTJyYXRpbyhzKSA9IHBvdygyLjAsIHMgLyAxMi4wKTsKCi8vIEJhc2UgZnJlcXVlbmN5OiBmaW5lIHR1bmUgKyBwaXRjaCBtb2QgKHNlbWl0b25lcywgwrExMikKYmFzZV9mcmVxID0gZnJlcSAqIGNlbnQycmF0aW8oZmluZV90dW5lKSAqIHNlbWkycmF0aW8obW9kX3BpdGNoKGdhdGUpICogMTIuMCk7CgovLyBWZWxvY2l0eSBnYWluIOKAlCBrZWVwcyBfdmVsIGFsaXZlCnZlbF9nYWluID0gKDEuMCAtIDAuMykgKyAwLjMgKiB2ZWw7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gRWZmZWN0aXZlIHBlci1vcGVyYXRvciByYXRpb3MgYW5kIGxldmVscyAobW9kIGFwcGxpZWQpCi8vIGdsb2JhbF9yYXRpbyBtb2QgaXMgYW4gYWRkaXRpdmUgcmF0aW8gb2Zmc2V0ICjCsTgpCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpyYXRpb19vZmZzZXQgPSBtb2RfZ2xvYmFsX3JhdGlvKGdhdGUpICogOC4wOwpyMSA9IG1heCgwLjAxLCBvcDFfcmF0aW8gKyByYXRpb19vZmZzZXQpOwpyMiA9IG1heCgwLjAxLCBvcDJfcmF0aW8gKyByYXRpb19vZmZzZXQpOwpyMyA9IG1heCgwLjAxLCBvcDNfcmF0aW8gKyByYXRpb19vZmZzZXQpOwpyNCA9IG1heCgwLjAxLCBvcDRfcmF0aW8gKyByYXRpb19vZmZzZXQpOwoKbGV2MSA9IG1heCgwLjAsIG1pbigxLjAsIG9wMV9sZXZlbCArIG1vZF9vcDFfbGV2ZWwoZ2F0ZSkpKTsKbGV2MiA9IG1heCgwLjAsIG1pbigxLjAsIG9wMl9sZXZlbCArIG1vZF9vcDJfbGV2ZWwoZ2F0ZSkpKTsKbGV2MyA9IG1heCgwLjAsIG1pbigxLjAsIG9wM19sZXZlbCArIG1vZF9vcDNfbGV2ZWwoZ2F0ZSkpKTsKbGV2NCA9IG1heCgwLjAsIG1pbigxLjAsIG9wNF9sZXZlbCArIG1vZF9vcDRfbGV2ZWwoZ2F0ZSkpKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBHbG9iYWwgbXVsdGlwbGllcnMgZm9yIGNyb3NzLW1vZCBhbmQgZmVlZGJhY2suCi8vICgxICsgbW9kKSBpbiBbMCwgMl0gcm91Z2hseTsgY2xhbXAgdG8gc3RheSBzYW5lLgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KY3Jvc3NfbXVsID0gbWF4KDAuMCwgMS4wICsgbW9kX2Nyb3NzX21vZF9nbG9iYWwoZ2F0ZSkpOwpmYl9tdWwgICAgPSBtYXgoMC4wLCAxLjAgKyBtb2RfZmVlZGJhY2tfZ2xvYmFsKGdhdGUpKTsKCm0xMmUgPSBtMTIgKiBjcm9zc19tdWw7ICBtMTNlID0gbTEzICogY3Jvc3NfbXVsOyAgbTE0ZSA9IG0xNCAqIGNyb3NzX211bDsKbTIxZSA9IG0yMSAqIGNyb3NzX211bDsgIG0yM2UgPSBtMjMgKiBjcm9zc19tdWw7ICBtMjRlID0gbTI0ICogY3Jvc3NfbXVsOwptMzFlID0gbTMxICogY3Jvc3NfbXVsOyAgbTMyZSA9IG0zMiAqIGNyb3NzX211bDsgIG0zNGUgPSBtMzQgKiBjcm9zc19tdWw7Cm00MWUgPSBtNDEgKiBjcm9zc19tdWw7ICBtNDJlID0gbTQyICogY3Jvc3NfbXVsOyAgbTQzZSA9IG00MyAqIGNyb3NzX211bDsKCmZiMWUgPSBmYjEgKiBmYl9tdWw7CmZiMmUgPSBmYjIgKiBmYl9tdWw7CmZiM2UgPSBmYjMgKiBmYl9tdWw7CmZiNGUgPSBmYjQgKiBmYl9tdWw7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gNC1vcGVyYXRvciBGTSBidXMg4oCUIGZlZWRiYWNrIGxvb3AgdmlhIH4gb24gYSA0LWNoYW5uZWwgYnVzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpmbWJ1cyhkMSxkMixkMyxkNCkgPQogIG9wMV9vdXQsIG9wMl9vdXQsIG9wM19vdXQsIG9wNF9vdXQKd2l0aCB7CiAgZjEgPSBiYXNlX2ZyZXEgKiByMTsKICBmMiA9IGJhc2VfZnJlcSAqIHIyOwogIGYzID0gYmFzZV9mcmVxICogcjM7CiAgZjQgPSBiYXNlX2ZyZXEgKiByNDsKCiAgb3AxX291dCA9IGxldjEgKgogICAgb3Mub3NjKGYxICsgKG0yMWUqZDIgKyBtMzFlKmQzICsgbTQxZSpkNCArIGZiMWUqZDEpICogZjEpOwoKICBvcDJfb3V0ID0gbGV2MiAqCiAgICBvcy5vc2MoZjIgKyAobTEyZSpkMSArIG0zMmUqZDMgKyBtNDJlKmQ0ICsgZmIyZSpkMikgKiBmMik7CgogIG9wM19vdXQgPSBsZXYzICoKICAgIG9zLm9zYyhmMyArIChtMTNlKmQxICsgbTIzZSpkMiArIG00M2UqZDQgKyBmYjNlKmQzKSAqIGYzKTsKCiAgb3A0X291dCA9IGxldjQgKgogICAgb3Mub3NjKGY0ICsgKG0xNGUqZDEgKyBtMjRlKmQyICsgbTM0ZSpkMyArIGZiNGUqZDQpICogZjQpOwp9OwoKLy8gUm91dGUgNC1jaGFubmVsIGJ1cyB0aHJvdWdoIGZlZWRiYWNrIGxvb3AsIHN1bSBhbGwgb3BzCmZtX291dCA9IChmbWJ1cyB+IChzaS5idXMoNCkpKSA6IChfLCBfLCBfLCBfKSA6PiBfOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFBvc3QtcHJvY2Vzc2luZwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0Kc29mdF9jbGlwKHgpID0geCAvIG1heCgwLjAwMSwgMS4wICsgb3V0cHV0X3NhdHVyYXRpb24gKiBhYnMoeCkpOwpocF9vdXQoeCkgICAgPSBmaS5oaWdocGFzcygxLCBvdXRwdXRfaHAsIHgpOwoKLy8gTWFzdGVyIGFtcGxpdHVkZSBkZXN0aW5hdGlvbiAocm91dGUgYW4gQURTUiBoZXJlIGZvciBWQ0EpCi8vIFNlZSBzdWJ0cmFjdGl2ZSBlbmdpbmUgZm9yIHRoZSBwb3NpdGl2ZS1vbmx5IG1vZF9hbXAgcmF0aW9uYWxlLgphbXBfdmFsID0gbWF4KDAuMCwgbWluKDEuMCwgYmFzZV9hbXAgKyBtYXgoMC4wLCBtb2RfYW1wKGdhdGUpKSkpICogbWFzdGVyX2xldmVsICogdmVsX2dhaW4gKiAwLjI1OwoKLy8gUGFuCnBhbl92YWwgPSBtYXgoLTEuMCwgbWluKDEuMCwgbWFzdGVyX3BhbiArIG1vZF9wYW4oZ2F0ZSkpKTsKcGFuX2wgICA9IGNvcygocGFuX3ZhbCArIDEuMCkgKiAwLjI1ICogbWEuUEkpOwpwYW5fciAgID0gc2luKChwYW5fdmFsICsgMS4wKSAqIDAuMjUgKiBtYS5QSSk7CgovLyBTdGVyZW8gc3ByZWFkIHZpYSBzbGlnaHQgZGV0dW5pbmcgKGFwcGx5IGFmdGVyIG1haW4gRk0gc3VtKQpzcHJlYWRfY2VudHMgPSBzdGVyZW9fc3ByZWFkICogNS4wOwpzcHJlYWRfbCAgICAgPSBjZW50MnJhdGlvKHNwcmVhZF9jZW50cyk7CnNwcmVhZF9yICAgICA9IGNlbnQycmF0aW8oMC4wIC0gc3ByZWFkX2NlbnRzKTsKCm1vbm9fcG9zdCA9IGZtX291dCAqIGFtcF92YWwgOiBzb2Z0X2NsaXAgOiBocF9vdXQ7CgpzaWduYWxfTCA9IG1vbm9fcG9zdCAqIHNwcmVhZF9sICogcGFuX2w7CnNpZ25hbF9SID0gbW9ub19wb3N0ICogc3ByZWFkX3IgKiBwYW5fcjsKCnByb2Nlc3MgPSBzaWduYWxfTCwgc2lnbmFsX1I7Cg==","inputs": 0,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn modular_fm -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "envelopes.lib/adsr:author": "Yann Orlarey and Andrey Bundin" },{ "envelopes.lib/author": "GRAME" },{ "envelopes.lib/copyright": "GRAME" },{ "envelopes.lib/license": "LGPL with exception" },{ "envelopes.lib/name": "Faust Envelope Library" },{ "envelopes.lib/version": "1.3.0" },{ "filename": "modular-fm.dsp" },{ "filters.lib/highpass:author": "Julius O. Smith III" },{ "filters.lib/highpass:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1:author": "Julius O. Smith III" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/tf1:author": "Julius O. Smith III" },{ "filters.lib/tf1:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1:license": "MIT-style STK-4.3 license" },{ "filters.lib/tf1s:author": "Julius O. Smith III" },{ "filters.lib/tf1s:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/tf1s:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "modular-fm" },{ "oscillators.lib/lf_sawpos:author": "Bart Brouns, revised by Stéphane Letz" },{ "oscillators.lib/lf_sawpos:licence": "STK-4.3" },{ "oscillators.lib/lf_triangle:author": "Bart Brouns" },{ "oscillators.lib/lf_triangle:licence": "STK-4.3" },{ "oscillators.lib/name": "Faust Oscillator Library" },{ "oscillators.lib/saw1:author": "Bart Brouns" },{ "oscillators.lib/saw1:licence": "STK-4.3" },{ "oscillators.lib/version": "1.6.0" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" }],"ui": [ {"type": "vgroup","label": "modular-fm","items": [ {"type": "hslider","label": "0_Hidden/_vel","varname": "fHslider225","shortname": "0_Hidden_vel","address": "/modular-fm/0_Hidden__vel","index": 263356,"meta": [{ "hidden": "1" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "0_Hidden/freq","varname": "fHslider473","shortname": "0_Hidden_freq","address": "/modular-fm/0_Hidden_freq","index": 264356,"meta": [{ "hidden": "1" },{ "unit": "Hz" }],"init": 220,"min": 20,"max": 4000,"step": 0.01},{"type": "button","label": "0_Hidden/gate","varname": "fButton0","shortname": "0_Hidden_gate","address": "/modular-fm/0_Hidden_gate","index": 262932,"meta": [{ "hidden": "1" }]},{"type": "hslider","label": "1_Operators/00_op1_ratio","varname": "fHslider423","shortname": "1_Operators_00_op1_ratio","address": "/modular-fm/1_Operators_00_op1_ratio","index": 264156,"init": 1,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "1_Operators/01_op1_level","varname": "fHslider522","shortname": "1_Operators_01_op1_level","address": "/modular-fm/1_Operators_01_op1_level","index": 264560,"init": 0.8,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Operators/02_op2_ratio","varname": "fHslider527","shortname": "1_Operators_02_op2_ratio","address": "/modular-fm/1_Operators_02_op2_ratio","index": 264588,"init": 2,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "1_Operators/03_op2_level","varname": "fHslider576","shortname": "1_Operators_03_op2_level","address": "/modular-fm/1_Operators_03_op2_level","index": 264792,"init": 0.6,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Operators/04_op3_ratio","varname": "fHslider581","shortname": "1_Operators_04_op3_ratio","address": "/modular-fm/1_Operators_04_op3_ratio","index": 264820,"init": 3,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "1_Operators/05_op3_level","varname": "fHslider630","shortname": "1_Operators_05_op3_level","address": "/modular-fm/1_Operators_05_op3_level","index": 265024,"init": 0.4,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Operators/06_op4_ratio","varname": "fHslider635","shortname": "1_Operators_06_op4_ratio","address": "/modular-fm/1_Operators_06_op4_ratio","index": 265052,"init": 0.5,"min": 0.125,"max": 16,"step": 0.001},{"type": "hslider","label": "1_Operators/07_op4_level","varname": "fHslider684","shortname": "1_Operators_07_op4_level","address": "/modular-fm/1_Operators_07_op4_level","index": 265256,"init": 0.3,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_CrossMod/00_m12","varname": "fHslider526","shortname": "2_CrossMod_00_m12","address": "/modular-fm/2_CrossMod_00_m12","index": 264584,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/01_m13","varname": "fHslider580","shortname": "2_CrossMod_01_m13","address": "/modular-fm/2_CrossMod_01_m13","index": 264816,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/02_m14","varname": "fHslider634","shortname": "2_CrossMod_02_m14","address": "/modular-fm/2_CrossMod_02_m14","index": 265048,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/03_m21","varname": "fHslider326","shortname": "2_CrossMod_03_m21","address": "/modular-fm/2_CrossMod_03_m21","index": 263768,"init": 1,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/04_m23","varname": "fHslider579","shortname": "2_CrossMod_04_m23","address": "/modular-fm/2_CrossMod_04_m23","index": 264812,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/05_m24","varname": "fHslider633","shortname": "2_CrossMod_05_m24","address": "/modular-fm/2_CrossMod_05_m24","index": 265044,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/06_m31","varname": "fHslider325","shortname": "2_CrossMod_06_m31","address": "/modular-fm/2_CrossMod_06_m31","index": 263764,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/07_m32","varname": "fHslider525","shortname": "2_CrossMod_07_m32","address": "/modular-fm/2_CrossMod_07_m32","index": 264580,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/08_m34","varname": "fHslider632","shortname": "2_CrossMod_08_m34","address": "/modular-fm/2_CrossMod_08_m34","index": 265040,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/09_m41","varname": "fHslider324","shortname": "2_CrossMod_09_m41","address": "/modular-fm/2_CrossMod_09_m41","index": 263760,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/10_m42","varname": "fHslider524","shortname": "2_CrossMod_10_m42","address": "/modular-fm/2_CrossMod_10_m42","index": 264576,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "2_CrossMod/11_m43","varname": "fHslider578","shortname": "2_CrossMod_11_m43","address": "/modular-fm/2_CrossMod_11_m43","index": 264808,"init": 0,"min": 0,"max": 10,"step": 0.001},{"type": "hslider","label": "3_Feedback/00_fb1","varname": "fHslider323","shortname": "3_Feedback_00_fb1","address": "/modular-fm/3_Feedback_00_fb1","index": 263756,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Feedback/01_fb2","varname": "fHslider523","shortname": "3_Feedback_01_fb2","address": "/modular-fm/3_Feedback_01_fb2","index": 264572,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Feedback/02_fb3","varname": "fHslider577","shortname": "3_Feedback_02_fb3","address": "/modular-fm/3_Feedback_02_fb3","index": 264804,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Feedback/03_fb4","varname": "fHslider631","shortname": "3_Feedback_03_fb4","address": "/modular-fm/3_Feedback_03_fb4","index": 265036,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/00_master_level","varname": "fHslider685","shortname": "4_Master_00_master_level","address": "/modular-fm/4_Master_00_master_level","index": 265268,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/01_fine_tune","varname": "fHslider472","shortname": "4_Master_01_fine_tune","address": "/modular-fm/4_Master_01_fine_tune","index": 264352,"meta": [{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1},{"type": "hslider","label": "4_Master/02_stereo_spread","varname": "fHslider688","shortname": "4_Master_02_stereo_spread","address": "/modular-fm/4_Master_02_stereo_spread","index": 265300,"init": 0.1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/03_output_saturation","varname": "fHslider686","shortname": "4_Master_03_output_saturation","address": "/modular-fm/4_Master_03_output_saturation","index": 265272,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/04_output_hp","varname": "fHslider687","shortname": "4_Master_04_output_hp","address": "/modular-fm/4_Master_04_output_hp","index": 265284,"meta": [{ "unit": "Hz" }],"init": 20,"min": 20,"max": 200,"step": 0.1},{"type": "hslider","label": "4_Master/05_master_pan","varname": "fHslider224","shortname": "4_Master_05_master_pan","address": "/modular-fm/4_Master_05_master_pan","index": 263344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/06_base_amp","varname": "fHslider274","shortname": "4_Master_06_base_amp","address": "/modular-fm/4_Master_06_base_amp","index": 263560,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_attack","varname": "fHslider219","shortname": "MM_ADSR_00_adsr01_attack","address": "/modular-fm/MM_ADSR_00_adsr01_attack","index": 263324,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_decay","varname": "fHslider220","shortname": "MM_ADSR_00_adsr01_decay","address": "/modular-fm/MM_ADSR_00_adsr01_decay","index": 263328,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_enable","varname": "fHslider223","shortname": "MM_ADSR_00_adsr01_enable","address": "/modular-fm/MM_ADSR_00_adsr01_enable","index": 263340,"init": 1,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/00_adsr01_release","varname": "fHslider218","shortname": "MM_ADSR_00_adsr01_release","address": "/modular-fm/MM_ADSR_00_adsr01_release","index": 263320,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_sustain","varname": "fHslider221","shortname": "MM_ADSR_00_adsr01_sustain","address": "/modular-fm/MM_ADSR_00_adsr01_sustain","index": 263332,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_attack","varname": "fHslider213","shortname": "MM_ADSR_01_adsr02_attack","address": "/modular-fm/MM_ADSR_01_adsr02_attack","index": 263300,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_decay","varname": "fHslider214","shortname": "MM_ADSR_01_adsr02_decay","address": "/modular-fm/MM_ADSR_01_adsr02_decay","index": 263304,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_enable","varname": "fHslider217","shortname": "MM_ADSR_01_adsr02_enable","address": "/modular-fm/MM_ADSR_01_adsr02_enable","index": 263316,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/01_adsr02_release","varname": "fHslider212","shortname": "MM_ADSR_01_adsr02_release","address": "/modular-fm/MM_ADSR_01_adsr02_release","index": 263296,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_sustain","varname": "fHslider215","shortname": "MM_ADSR_01_adsr02_sustain","address": "/modular-fm/MM_ADSR_01_adsr02_sustain","index": 263308,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_attack","varname": "fHslider207","shortname": "MM_ADSR_02_adsr03_attack","address": "/modular-fm/MM_ADSR_02_adsr03_attack","index": 263276,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_decay","varname": "fHslider208","shortname": "MM_ADSR_02_adsr03_decay","address": "/modular-fm/MM_ADSR_02_adsr03_decay","index": 263280,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_enable","varname": "fHslider211","shortname": "MM_ADSR_02_adsr03_enable","address": "/modular-fm/MM_ADSR_02_adsr03_enable","index": 263292,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/02_adsr03_release","varname": "fHslider206","shortname": "MM_ADSR_02_adsr03_release","address": "/modular-fm/MM_ADSR_02_adsr03_release","index": 263272,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_sustain","varname": "fHslider209","shortname": "MM_ADSR_02_adsr03_sustain","address": "/modular-fm/MM_ADSR_02_adsr03_sustain","index": 263284,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_attack","varname": "fHslider201","shortname": "MM_ADSR_03_adsr04_attack","address": "/modular-fm/MM_ADSR_03_adsr04_attack","index": 263252,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_decay","varname": "fHslider202","shortname": "MM_ADSR_03_adsr04_decay","address": "/modular-fm/MM_ADSR_03_adsr04_decay","index": 263256,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_enable","varname": "fHslider205","shortname": "MM_ADSR_03_adsr04_enable","address": "/modular-fm/MM_ADSR_03_adsr04_enable","index": 263268,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/03_adsr04_release","varname": "fHslider200","shortname": "MM_ADSR_03_adsr04_release","address": "/modular-fm/MM_ADSR_03_adsr04_release","index": 263248,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_sustain","varname": "fHslider203","shortname": "MM_ADSR_03_adsr04_sustain","address": "/modular-fm/MM_ADSR_03_adsr04_sustain","index": 263260,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_attack","varname": "fHslider195","shortname": "MM_ADSR_04_adsr05_attack","address": "/modular-fm/MM_ADSR_04_adsr05_attack","index": 263228,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_decay","varname": "fHslider196","shortname": "MM_ADSR_04_adsr05_decay","address": "/modular-fm/MM_ADSR_04_adsr05_decay","index": 263232,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_enable","varname": "fHslider199","shortname": "MM_ADSR_04_adsr05_enable","address": "/modular-fm/MM_ADSR_04_adsr05_enable","index": 263244,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/04_adsr05_release","varname": "fHslider194","shortname": "MM_ADSR_04_adsr05_release","address": "/modular-fm/MM_ADSR_04_adsr05_release","index": 263224,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_sustain","varname": "fHslider197","shortname": "MM_ADSR_04_adsr05_sustain","address": "/modular-fm/MM_ADSR_04_adsr05_sustain","index": 263236,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_attack","varname": "fHslider189","shortname": "MM_ADSR_05_adsr06_attack","address": "/modular-fm/MM_ADSR_05_adsr06_attack","index": 263204,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_decay","varname": "fHslider190","shortname": "MM_ADSR_05_adsr06_decay","address": "/modular-fm/MM_ADSR_05_adsr06_decay","index": 263208,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_enable","varname": "fHslider193","shortname": "MM_ADSR_05_adsr06_enable","address": "/modular-fm/MM_ADSR_05_adsr06_enable","index": 263220,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/05_adsr06_release","varname": "fHslider188","shortname": "MM_ADSR_05_adsr06_release","address": "/modular-fm/MM_ADSR_05_adsr06_release","index": 263200,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_sustain","varname": "fHslider191","shortname": "MM_ADSR_05_adsr06_sustain","address": "/modular-fm/MM_ADSR_05_adsr06_sustain","index": 263212,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_attack","varname": "fHslider183","shortname": "MM_ADSR_06_adsr07_attack","address": "/modular-fm/MM_ADSR_06_adsr07_attack","index": 263180,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_decay","varname": "fHslider184","shortname": "MM_ADSR_06_adsr07_decay","address": "/modular-fm/MM_ADSR_06_adsr07_decay","index": 263184,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_enable","varname": "fHslider187","shortname": "MM_ADSR_06_adsr07_enable","address": "/modular-fm/MM_ADSR_06_adsr07_enable","index": 263196,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/06_adsr07_release","varname": "fHslider182","shortname": "MM_ADSR_06_adsr07_release","address": "/modular-fm/MM_ADSR_06_adsr07_release","index": 263176,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_sustain","varname": "fHslider185","shortname": "MM_ADSR_06_adsr07_sustain","address": "/modular-fm/MM_ADSR_06_adsr07_sustain","index": 263188,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_attack","varname": "fHslider177","shortname": "MM_ADSR_07_adsr08_attack","address": "/modular-fm/MM_ADSR_07_adsr08_attack","index": 263156,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_decay","varname": "fHslider178","shortname": "MM_ADSR_07_adsr08_decay","address": "/modular-fm/MM_ADSR_07_adsr08_decay","index": 263160,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_enable","varname": "fHslider181","shortname": "MM_ADSR_07_adsr08_enable","address": "/modular-fm/MM_ADSR_07_adsr08_enable","index": 263172,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/07_adsr08_release","varname": "fHslider176","shortname": "MM_ADSR_07_adsr08_release","address": "/modular-fm/MM_ADSR_07_adsr08_release","index": 263152,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_sustain","varname": "fHslider179","shortname": "MM_ADSR_07_adsr08_sustain","address": "/modular-fm/MM_ADSR_07_adsr08_sustain","index": 263164,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_attack","varname": "fHslider171","shortname": "MM_ADSR_08_adsr09_attack","address": "/modular-fm/MM_ADSR_08_adsr09_attack","index": 263132,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_decay","varname": "fHslider172","shortname": "MM_ADSR_08_adsr09_decay","address": "/modular-fm/MM_ADSR_08_adsr09_decay","index": 263136,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_enable","varname": "fHslider175","shortname": "MM_ADSR_08_adsr09_enable","address": "/modular-fm/MM_ADSR_08_adsr09_enable","index": 263148,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/08_adsr09_release","varname": "fHslider170","shortname": "MM_ADSR_08_adsr09_release","address": "/modular-fm/MM_ADSR_08_adsr09_release","index": 263128,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_sustain","varname": "fHslider173","shortname": "MM_ADSR_08_adsr09_sustain","address": "/modular-fm/MM_ADSR_08_adsr09_sustain","index": 263140,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_attack","varname": "fHslider165","shortname": "MM_ADSR_09_adsr10_attack","address": "/modular-fm/MM_ADSR_09_adsr10_attack","index": 263108,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_decay","varname": "fHslider166","shortname": "MM_ADSR_09_adsr10_decay","address": "/modular-fm/MM_ADSR_09_adsr10_decay","index": 263112,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_enable","varname": "fHslider169","shortname": "MM_ADSR_09_adsr10_enable","address": "/modular-fm/MM_ADSR_09_adsr10_enable","index": 263124,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/09_adsr10_release","varname": "fHslider164","shortname": "MM_ADSR_09_adsr10_release","address": "/modular-fm/MM_ADSR_09_adsr10_release","index": 263104,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_sustain","varname": "fHslider167","shortname": "MM_ADSR_09_adsr10_sustain","address": "/modular-fm/MM_ADSR_09_adsr10_sustain","index": 263116,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_attack","varname": "fHslider159","shortname": "MM_ADSR_10_adsr11_attack","address": "/modular-fm/MM_ADSR_10_adsr11_attack","index": 263084,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_decay","varname": "fHslider160","shortname": "MM_ADSR_10_adsr11_decay","address": "/modular-fm/MM_ADSR_10_adsr11_decay","index": 263088,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_enable","varname": "fHslider163","shortname": "MM_ADSR_10_adsr11_enable","address": "/modular-fm/MM_ADSR_10_adsr11_enable","index": 263100,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/10_adsr11_release","varname": "fHslider158","shortname": "MM_ADSR_10_adsr11_release","address": "/modular-fm/MM_ADSR_10_adsr11_release","index": 263080,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_sustain","varname": "fHslider161","shortname": "MM_ADSR_10_adsr11_sustain","address": "/modular-fm/MM_ADSR_10_adsr11_sustain","index": 263092,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_attack","varname": "fHslider153","shortname": "MM_ADSR_11_adsr12_attack","address": "/modular-fm/MM_ADSR_11_adsr12_attack","index": 263060,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_decay","varname": "fHslider154","shortname": "MM_ADSR_11_adsr12_decay","address": "/modular-fm/MM_ADSR_11_adsr12_decay","index": 263064,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_enable","varname": "fHslider157","shortname": "MM_ADSR_11_adsr12_enable","address": "/modular-fm/MM_ADSR_11_adsr12_enable","index": 263076,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/11_adsr12_release","varname": "fHslider152","shortname": "MM_ADSR_11_adsr12_release","address": "/modular-fm/MM_ADSR_11_adsr12_release","index": 263056,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_sustain","varname": "fHslider155","shortname": "MM_ADSR_11_adsr12_sustain","address": "/modular-fm/MM_ADSR_11_adsr12_sustain","index": 263068,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_attack","varname": "fHslider147","shortname": "MM_ADSR_12_adsr13_attack","address": "/modular-fm/MM_ADSR_12_adsr13_attack","index": 263036,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_decay","varname": "fHslider148","shortname": "MM_ADSR_12_adsr13_decay","address": "/modular-fm/MM_ADSR_12_adsr13_decay","index": 263040,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_enable","varname": "fHslider151","shortname": "MM_ADSR_12_adsr13_enable","address": "/modular-fm/MM_ADSR_12_adsr13_enable","index": 263052,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/12_adsr13_release","varname": "fHslider146","shortname": "MM_ADSR_12_adsr13_release","address": "/modular-fm/MM_ADSR_12_adsr13_release","index": 263032,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_sustain","varname": "fHslider149","shortname": "MM_ADSR_12_adsr13_sustain","address": "/modular-fm/MM_ADSR_12_adsr13_sustain","index": 263044,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_attack","varname": "fHslider141","shortname": "MM_ADSR_13_adsr14_attack","address": "/modular-fm/MM_ADSR_13_adsr14_attack","index": 263012,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_decay","varname": "fHslider142","shortname": "MM_ADSR_13_adsr14_decay","address": "/modular-fm/MM_ADSR_13_adsr14_decay","index": 263016,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_enable","varname": "fHslider145","shortname": "MM_ADSR_13_adsr14_enable","address": "/modular-fm/MM_ADSR_13_adsr14_enable","index": 263028,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/13_adsr14_release","varname": "fHslider140","shortname": "MM_ADSR_13_adsr14_release","address": "/modular-fm/MM_ADSR_13_adsr14_release","index": 263008,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_sustain","varname": "fHslider143","shortname": "MM_ADSR_13_adsr14_sustain","address": "/modular-fm/MM_ADSR_13_adsr14_sustain","index": 263020,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_attack","varname": "fHslider135","shortname": "MM_ADSR_14_adsr15_attack","address": "/modular-fm/MM_ADSR_14_adsr15_attack","index": 262988,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_decay","varname": "fHslider136","shortname": "MM_ADSR_14_adsr15_decay","address": "/modular-fm/MM_ADSR_14_adsr15_decay","index": 262992,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_enable","varname": "fHslider139","shortname": "MM_ADSR_14_adsr15_enable","address": "/modular-fm/MM_ADSR_14_adsr15_enable","index": 263004,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/14_adsr15_release","varname": "fHslider134","shortname": "MM_ADSR_14_adsr15_release","address": "/modular-fm/MM_ADSR_14_adsr15_release","index": 262984,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_sustain","varname": "fHslider137","shortname": "MM_ADSR_14_adsr15_sustain","address": "/modular-fm/MM_ADSR_14_adsr15_sustain","index": 262996,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_attack","varname": "fHslider129","shortname": "MM_ADSR_15_adsr16_attack","address": "/modular-fm/MM_ADSR_15_adsr16_attack","index": 262964,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_decay","varname": "fHslider130","shortname": "MM_ADSR_15_adsr16_decay","address": "/modular-fm/MM_ADSR_15_adsr16_decay","index": 262968,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_enable","varname": "fHslider133","shortname": "MM_ADSR_15_adsr16_enable","address": "/modular-fm/MM_ADSR_15_adsr16_enable","index": 262980,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/15_adsr16_release","varname": "fHslider128","shortname": "MM_ADSR_15_adsr16_release","address": "/modular-fm/MM_ADSR_15_adsr16_release","index": 262952,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_sustain","varname": "fHslider131","shortname": "MM_ADSR_15_adsr16_sustain","address": "/modular-fm/MM_ADSR_15_adsr16_sustain","index": 262972,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_enable","varname": "fHslider127","shortname": "MM_LFO_00_lfo01_enable","address": "/modular-fm/MM_LFO_00_lfo01_enable","index": 262928,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/00_lfo01_morph","varname": "fHslider124","shortname": "MM_LFO_00_lfo01_morph","address": "/modular-fm/MM_LFO_00_lfo01_morph","index": 262908,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_rate","varname": "fHslider125","shortname": "MM_LFO_00_lfo01_rate","address": "/modular-fm/MM_LFO_00_lfo01_rate","index": 262912,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_enable","varname": "fHslider123","shortname": "MM_LFO_01_lfo02_enable","address": "/modular-fm/MM_LFO_01_lfo02_enable","index": 262904,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/01_lfo02_morph","varname": "fHslider120","shortname": "MM_LFO_01_lfo02_morph","address": "/modular-fm/MM_LFO_01_lfo02_morph","index": 262884,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_rate","varname": "fHslider121","shortname": "MM_LFO_01_lfo02_rate","address": "/modular-fm/MM_LFO_01_lfo02_rate","index": 262888,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_enable","varname": "fHslider119","shortname": "MM_LFO_02_lfo03_enable","address": "/modular-fm/MM_LFO_02_lfo03_enable","index": 262880,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/02_lfo03_morph","varname": "fHslider116","shortname": "MM_LFO_02_lfo03_morph","address": "/modular-fm/MM_LFO_02_lfo03_morph","index": 262860,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_rate","varname": "fHslider117","shortname": "MM_LFO_02_lfo03_rate","address": "/modular-fm/MM_LFO_02_lfo03_rate","index": 262864,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_enable","varname": "fHslider115","shortname": "MM_LFO_03_lfo04_enable","address": "/modular-fm/MM_LFO_03_lfo04_enable","index": 262856,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/03_lfo04_morph","varname": "fHslider112","shortname": "MM_LFO_03_lfo04_morph","address": "/modular-fm/MM_LFO_03_lfo04_morph","index": 262836,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_rate","varname": "fHslider113","shortname": "MM_LFO_03_lfo04_rate","address": "/modular-fm/MM_LFO_03_lfo04_rate","index": 262840,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_enable","varname": "fHslider111","shortname": "MM_LFO_04_lfo05_enable","address": "/modular-fm/MM_LFO_04_lfo05_enable","index": 262832,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/04_lfo05_morph","varname": "fHslider108","shortname": "MM_LFO_04_lfo05_morph","address": "/modular-fm/MM_LFO_04_lfo05_morph","index": 262812,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_rate","varname": "fHslider109","shortname": "MM_LFO_04_lfo05_rate","address": "/modular-fm/MM_LFO_04_lfo05_rate","index": 262816,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_enable","varname": "fHslider107","shortname": "MM_LFO_05_lfo06_enable","address": "/modular-fm/MM_LFO_05_lfo06_enable","index": 262808,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/05_lfo06_morph","varname": "fHslider104","shortname": "MM_LFO_05_lfo06_morph","address": "/modular-fm/MM_LFO_05_lfo06_morph","index": 262788,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_rate","varname": "fHslider105","shortname": "MM_LFO_05_lfo06_rate","address": "/modular-fm/MM_LFO_05_lfo06_rate","index": 262792,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_enable","varname": "fHslider103","shortname": "MM_LFO_06_lfo07_enable","address": "/modular-fm/MM_LFO_06_lfo07_enable","index": 262784,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/06_lfo07_morph","varname": "fHslider100","shortname": "MM_LFO_06_lfo07_morph","address": "/modular-fm/MM_LFO_06_lfo07_morph","index": 262764,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_rate","varname": "fHslider101","shortname": "MM_LFO_06_lfo07_rate","address": "/modular-fm/MM_LFO_06_lfo07_rate","index": 262768,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_enable","varname": "fHslider99","shortname": "MM_LFO_07_lfo08_enable","address": "/modular-fm/MM_LFO_07_lfo08_enable","index": 262760,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/07_lfo08_morph","varname": "fHslider96","shortname": "MM_LFO_07_lfo08_morph","address": "/modular-fm/MM_LFO_07_lfo08_morph","index": 262740,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_rate","varname": "fHslider97","shortname": "MM_LFO_07_lfo08_rate","address": "/modular-fm/MM_LFO_07_lfo08_rate","index": 262744,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_enable","varname": "fHslider95","shortname": "MM_LFO_08_lfo09_enable","address": "/modular-fm/MM_LFO_08_lfo09_enable","index": 262736,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/08_lfo09_morph","varname": "fHslider92","shortname": "MM_LFO_08_lfo09_morph","address": "/modular-fm/MM_LFO_08_lfo09_morph","index": 262716,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_rate","varname": "fHslider93","shortname": "MM_LFO_08_lfo09_rate","address": "/modular-fm/MM_LFO_08_lfo09_rate","index": 262720,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_enable","varname": "fHslider91","shortname": "MM_LFO_09_lfo10_enable","address": "/modular-fm/MM_LFO_09_lfo10_enable","index": 262712,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/09_lfo10_morph","varname": "fHslider88","shortname": "MM_LFO_09_lfo10_morph","address": "/modular-fm/MM_LFO_09_lfo10_morph","index": 262692,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_rate","varname": "fHslider89","shortname": "MM_LFO_09_lfo10_rate","address": "/modular-fm/MM_LFO_09_lfo10_rate","index": 262696,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_enable","varname": "fHslider87","shortname": "MM_LFO_10_lfo11_enable","address": "/modular-fm/MM_LFO_10_lfo11_enable","index": 262688,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/10_lfo11_morph","varname": "fHslider84","shortname": "MM_LFO_10_lfo11_morph","address": "/modular-fm/MM_LFO_10_lfo11_morph","index": 262668,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_rate","varname": "fHslider85","shortname": "MM_LFO_10_lfo11_rate","address": "/modular-fm/MM_LFO_10_lfo11_rate","index": 262672,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_enable","varname": "fHslider83","shortname": "MM_LFO_11_lfo12_enable","address": "/modular-fm/MM_LFO_11_lfo12_enable","index": 262664,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/11_lfo12_morph","varname": "fHslider80","shortname": "MM_LFO_11_lfo12_morph","address": "/modular-fm/MM_LFO_11_lfo12_morph","index": 262644,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_rate","varname": "fHslider81","shortname": "MM_LFO_11_lfo12_rate","address": "/modular-fm/MM_LFO_11_lfo12_rate","index": 262648,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_enable","varname": "fHslider79","shortname": "MM_LFO_12_lfo13_enable","address": "/modular-fm/MM_LFO_12_lfo13_enable","index": 262640,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/12_lfo13_morph","varname": "fHslider76","shortname": "MM_LFO_12_lfo13_morph","address": "/modular-fm/MM_LFO_12_lfo13_morph","index": 262620,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_rate","varname": "fHslider77","shortname": "MM_LFO_12_lfo13_rate","address": "/modular-fm/MM_LFO_12_lfo13_rate","index": 262624,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_enable","varname": "fHslider75","shortname": "MM_LFO_13_lfo14_enable","address": "/modular-fm/MM_LFO_13_lfo14_enable","index": 262616,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/13_lfo14_morph","varname": "fHslider72","shortname": "MM_LFO_13_lfo14_morph","address": "/modular-fm/MM_LFO_13_lfo14_morph","index": 262596,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_rate","varname": "fHslider73","shortname": "MM_LFO_13_lfo14_rate","address": "/modular-fm/MM_LFO_13_lfo14_rate","index": 262600,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_enable","varname": "fHslider71","shortname": "MM_LFO_14_lfo15_enable","address": "/modular-fm/MM_LFO_14_lfo15_enable","index": 262592,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/14_lfo15_morph","varname": "fHslider68","shortname": "MM_LFO_14_lfo15_morph","address": "/modular-fm/MM_LFO_14_lfo15_morph","index": 262572,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_rate","varname": "fHslider69","shortname": "MM_LFO_14_lfo15_rate","address": "/modular-fm/MM_LFO_14_lfo15_rate","index": 262576,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_enable","varname": "fHslider67","shortname": "MM_LFO_15_lfo16_enable","address": "/modular-fm/MM_LFO_15_lfo16_enable","index": 262568,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/15_lfo16_morph","varname": "fHslider64","shortname": "MM_LFO_15_lfo16_morph","address": "/modular-fm/MM_LFO_15_lfo16_morph","index": 262548,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_rate","varname": "fHslider65","shortname": "MM_LFO_15_lfo16_rate","address": "/modular-fm/MM_LFO_15_lfo16_rate","index": 262552,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_enable","varname": "fHslider63","shortname": "MM_LFO_16_lfo17_enable","address": "/modular-fm/MM_LFO_16_lfo17_enable","index": 262544,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/16_lfo17_morph","varname": "fHslider60","shortname": "MM_LFO_16_lfo17_morph","address": "/modular-fm/MM_LFO_16_lfo17_morph","index": 262524,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_rate","varname": "fHslider61","shortname": "MM_LFO_16_lfo17_rate","address": "/modular-fm/MM_LFO_16_lfo17_rate","index": 262528,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_enable","varname": "fHslider59","shortname": "MM_LFO_17_lfo18_enable","address": "/modular-fm/MM_LFO_17_lfo18_enable","index": 262520,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/17_lfo18_morph","varname": "fHslider56","shortname": "MM_LFO_17_lfo18_morph","address": "/modular-fm/MM_LFO_17_lfo18_morph","index": 262500,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_rate","varname": "fHslider57","shortname": "MM_LFO_17_lfo18_rate","address": "/modular-fm/MM_LFO_17_lfo18_rate","index": 262504,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_enable","varname": "fHslider55","shortname": "MM_LFO_18_lfo19_enable","address": "/modular-fm/MM_LFO_18_lfo19_enable","index": 262496,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/18_lfo19_morph","varname": "fHslider52","shortname": "MM_LFO_18_lfo19_morph","address": "/modular-fm/MM_LFO_18_lfo19_morph","index": 262476,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_rate","varname": "fHslider53","shortname": "MM_LFO_18_lfo19_rate","address": "/modular-fm/MM_LFO_18_lfo19_rate","index": 262480,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_enable","varname": "fHslider51","shortname": "MM_LFO_19_lfo20_enable","address": "/modular-fm/MM_LFO_19_lfo20_enable","index": 262472,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/19_lfo20_morph","varname": "fHslider48","shortname": "MM_LFO_19_lfo20_morph","address": "/modular-fm/MM_LFO_19_lfo20_morph","index": 262452,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_rate","varname": "fHslider49","shortname": "MM_LFO_19_lfo20_rate","address": "/modular-fm/MM_LFO_19_lfo20_rate","index": 262456,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_enable","varname": "fHslider47","shortname": "MM_LFO_20_lfo21_enable","address": "/modular-fm/MM_LFO_20_lfo21_enable","index": 262448,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/20_lfo21_morph","varname": "fHslider44","shortname": "MM_LFO_20_lfo21_morph","address": "/modular-fm/MM_LFO_20_lfo21_morph","index": 262428,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_rate","varname": "fHslider45","shortname": "MM_LFO_20_lfo21_rate","address": "/modular-fm/MM_LFO_20_lfo21_rate","index": 262432,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_enable","varname": "fHslider43","shortname": "MM_LFO_21_lfo22_enable","address": "/modular-fm/MM_LFO_21_lfo22_enable","index": 262424,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/21_lfo22_morph","varname": "fHslider40","shortname": "MM_LFO_21_lfo22_morph","address": "/modular-fm/MM_LFO_21_lfo22_morph","index": 262404,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_rate","varname": "fHslider41","shortname": "MM_LFO_21_lfo22_rate","address": "/modular-fm/MM_LFO_21_lfo22_rate","index": 262408,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_enable","varname": "fHslider39","shortname": "MM_LFO_22_lfo23_enable","address": "/modular-fm/MM_LFO_22_lfo23_enable","index": 262400,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/22_lfo23_morph","varname": "fHslider36","shortname": "MM_LFO_22_lfo23_morph","address": "/modular-fm/MM_LFO_22_lfo23_morph","index": 262380,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_rate","varname": "fHslider37","shortname": "MM_LFO_22_lfo23_rate","address": "/modular-fm/MM_LFO_22_lfo23_rate","index": 262384,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_enable","varname": "fHslider35","shortname": "MM_LFO_23_lfo24_enable","address": "/modular-fm/MM_LFO_23_lfo24_enable","index": 262376,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/23_lfo24_morph","varname": "fHslider32","shortname": "MM_LFO_23_lfo24_morph","address": "/modular-fm/MM_LFO_23_lfo24_morph","index": 262356,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_rate","varname": "fHslider33","shortname": "MM_LFO_23_lfo24_rate","address": "/modular-fm/MM_LFO_23_lfo24_rate","index": 262360,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_enable","varname": "fHslider31","shortname": "MM_LFO_24_lfo25_enable","address": "/modular-fm/MM_LFO_24_lfo25_enable","index": 262352,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/24_lfo25_morph","varname": "fHslider28","shortname": "MM_LFO_24_lfo25_morph","address": "/modular-fm/MM_LFO_24_lfo25_morph","index": 262332,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_rate","varname": "fHslider29","shortname": "MM_LFO_24_lfo25_rate","address": "/modular-fm/MM_LFO_24_lfo25_rate","index": 262336,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_enable","varname": "fHslider27","shortname": "MM_LFO_25_lfo26_enable","address": "/modular-fm/MM_LFO_25_lfo26_enable","index": 262328,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/25_lfo26_morph","varname": "fHslider24","shortname": "MM_LFO_25_lfo26_morph","address": "/modular-fm/MM_LFO_25_lfo26_morph","index": 262308,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_rate","varname": "fHslider25","shortname": "MM_LFO_25_lfo26_rate","address": "/modular-fm/MM_LFO_25_lfo26_rate","index": 262312,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_enable","varname": "fHslider23","shortname": "MM_LFO_26_lfo27_enable","address": "/modular-fm/MM_LFO_26_lfo27_enable","index": 262304,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/26_lfo27_morph","varname": "fHslider20","shortname": "MM_LFO_26_lfo27_morph","address": "/modular-fm/MM_LFO_26_lfo27_morph","index": 262284,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_rate","varname": "fHslider21","shortname": "MM_LFO_26_lfo27_rate","address": "/modular-fm/MM_LFO_26_lfo27_rate","index": 262288,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_enable","varname": "fHslider19","shortname": "MM_LFO_27_lfo28_enable","address": "/modular-fm/MM_LFO_27_lfo28_enable","index": 262280,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/27_lfo28_morph","varname": "fHslider16","shortname": "MM_LFO_27_lfo28_morph","address": "/modular-fm/MM_LFO_27_lfo28_morph","index": 262260,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_rate","varname": "fHslider17","shortname": "MM_LFO_27_lfo28_rate","address": "/modular-fm/MM_LFO_27_lfo28_rate","index": 262264,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_enable","varname": "fHslider15","shortname": "MM_LFO_28_lfo29_enable","address": "/modular-fm/MM_LFO_28_lfo29_enable","index": 262256,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/28_lfo29_morph","varname": "fHslider12","shortname": "MM_LFO_28_lfo29_morph","address": "/modular-fm/MM_LFO_28_lfo29_morph","index": 262236,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_rate","varname": "fHslider13","shortname": "MM_LFO_28_lfo29_rate","address": "/modular-fm/MM_LFO_28_lfo29_rate","index": 262240,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_enable","varname": "fHslider11","shortname": "MM_LFO_29_lfo30_enable","address": "/modular-fm/MM_LFO_29_lfo30_enable","index": 262232,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/29_lfo30_morph","varname": "fHslider8","shortname": "MM_LFO_29_lfo30_morph","address": "/modular-fm/MM_LFO_29_lfo30_morph","index": 262212,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_rate","varname": "fHslider9","shortname": "MM_LFO_29_lfo30_rate","address": "/modular-fm/MM_LFO_29_lfo30_rate","index": 262216,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_enable","varname": "fHslider7","shortname": "MM_LFO_30_lfo31_enable","address": "/modular-fm/MM_LFO_30_lfo31_enable","index": 262208,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/30_lfo31_morph","varname": "fHslider4","shortname": "MM_LFO_30_lfo31_morph","address": "/modular-fm/MM_LFO_30_lfo31_morph","index": 262188,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_rate","varname": "fHslider5","shortname": "MM_LFO_30_lfo31_rate","address": "/modular-fm/MM_LFO_30_lfo31_rate","index": 262192,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_enable","varname": "fHslider3","shortname": "MM_LFO_31_lfo32_enable","address": "/modular-fm/MM_LFO_31_lfo32_enable","index": 262184,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/31_lfo32_morph","varname": "fHslider0","shortname": "MM_LFO_31_lfo32_morph","address": "/modular-fm/MM_LFO_31_lfo32_morph","index": 262144,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_rate","varname": "fHslider1","shortname": "MM_LFO_31_lfo32_rate","address": "/modular-fm/MM_LFO_31_lfo32_rate","index": 262156,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d00_pitch","varname": "fHslider471","shortname": "MM_Matrix_s00_d00_pitch","address": "/modular-fm/MM_Matrix_s00_d00_pitch","index": 264348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d01_op1_level","varname": "fHslider521","shortname": "MM_Matrix_s00_d01_op1_level","address": "/modular-fm/MM_Matrix_s00_d01_op1_level","index": 264556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d02_op2_level","varname": "fHslider575","shortname": "MM_Matrix_s00_d02_op2_level","address": "/modular-fm/MM_Matrix_s00_d02_op2_level","index": 264788,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d03_op3_level","varname": "fHslider629","shortname": "MM_Matrix_s00_d03_op3_level","address": "/modular-fm/MM_Matrix_s00_d03_op3_level","index": 265020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d04_op4_level","varname": "fHslider683","shortname": "MM_Matrix_s00_d04_op4_level","address": "/modular-fm/MM_Matrix_s00_d04_op4_level","index": 265252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d05_cross_mod_global","varname": "fHslider374","shortname": "MM_Matrix_s00_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s00_d05_cross_mod_global","index": 263960,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d06_feedback_global","varname": "fHslider322","shortname": "MM_Matrix_s00_d06_feedback_global","address": "/modular-fm/MM_Matrix_s00_d06_feedback_global","index": 263752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d07_global_ratio","varname": "fHslider422","shortname": "MM_Matrix_s00_d07_global_ratio","address": "/modular-fm/MM_Matrix_s00_d07_global_ratio","index": 264152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d08_amp","varname": "fHslider273","shortname": "MM_Matrix_s00_d08_amp","address": "/modular-fm/MM_Matrix_s00_d08_amp","index": 263556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d09_pan","varname": "fHslider222","shortname": "MM_Matrix_s00_d09_pan","address": "/modular-fm/MM_Matrix_s00_d09_pan","index": 263336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d00_pitch","varname": "fHslider470","shortname": "MM_Matrix_s01_d00_pitch","address": "/modular-fm/MM_Matrix_s01_d00_pitch","index": 264344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d01_op1_level","varname": "fHslider520","shortname": "MM_Matrix_s01_d01_op1_level","address": "/modular-fm/MM_Matrix_s01_d01_op1_level","index": 264552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d02_op2_level","varname": "fHslider574","shortname": "MM_Matrix_s01_d02_op2_level","address": "/modular-fm/MM_Matrix_s01_d02_op2_level","index": 264784,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d03_op3_level","varname": "fHslider628","shortname": "MM_Matrix_s01_d03_op3_level","address": "/modular-fm/MM_Matrix_s01_d03_op3_level","index": 265016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d04_op4_level","varname": "fHslider682","shortname": "MM_Matrix_s01_d04_op4_level","address": "/modular-fm/MM_Matrix_s01_d04_op4_level","index": 265248,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d05_cross_mod_global","varname": "fHslider373","shortname": "MM_Matrix_s01_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s01_d05_cross_mod_global","index": 263956,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d06_feedback_global","varname": "fHslider321","shortname": "MM_Matrix_s01_d06_feedback_global","address": "/modular-fm/MM_Matrix_s01_d06_feedback_global","index": 263748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d07_global_ratio","varname": "fHslider421","shortname": "MM_Matrix_s01_d07_global_ratio","address": "/modular-fm/MM_Matrix_s01_d07_global_ratio","index": 264148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d08_amp","varname": "fHslider272","shortname": "MM_Matrix_s01_d08_amp","address": "/modular-fm/MM_Matrix_s01_d08_amp","index": 263552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d09_pan","varname": "fHslider216","shortname": "MM_Matrix_s01_d09_pan","address": "/modular-fm/MM_Matrix_s01_d09_pan","index": 263312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d00_pitch","varname": "fHslider469","shortname": "MM_Matrix_s02_d00_pitch","address": "/modular-fm/MM_Matrix_s02_d00_pitch","index": 264340,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d01_op1_level","varname": "fHslider519","shortname": "MM_Matrix_s02_d01_op1_level","address": "/modular-fm/MM_Matrix_s02_d01_op1_level","index": 264548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d02_op2_level","varname": "fHslider573","shortname": "MM_Matrix_s02_d02_op2_level","address": "/modular-fm/MM_Matrix_s02_d02_op2_level","index": 264780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d03_op3_level","varname": "fHslider627","shortname": "MM_Matrix_s02_d03_op3_level","address": "/modular-fm/MM_Matrix_s02_d03_op3_level","index": 265012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d04_op4_level","varname": "fHslider681","shortname": "MM_Matrix_s02_d04_op4_level","address": "/modular-fm/MM_Matrix_s02_d04_op4_level","index": 265244,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d05_cross_mod_global","varname": "fHslider372","shortname": "MM_Matrix_s02_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s02_d05_cross_mod_global","index": 263952,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d06_feedback_global","varname": "fHslider320","shortname": "MM_Matrix_s02_d06_feedback_global","address": "/modular-fm/MM_Matrix_s02_d06_feedback_global","index": 263744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d07_global_ratio","varname": "fHslider420","shortname": "MM_Matrix_s02_d07_global_ratio","address": "/modular-fm/MM_Matrix_s02_d07_global_ratio","index": 264144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d08_amp","varname": "fHslider271","shortname": "MM_Matrix_s02_d08_amp","address": "/modular-fm/MM_Matrix_s02_d08_amp","index": 263548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d09_pan","varname": "fHslider210","shortname": "MM_Matrix_s02_d09_pan","address": "/modular-fm/MM_Matrix_s02_d09_pan","index": 263288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d00_pitch","varname": "fHslider468","shortname": "MM_Matrix_s03_d00_pitch","address": "/modular-fm/MM_Matrix_s03_d00_pitch","index": 264336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d01_op1_level","varname": "fHslider518","shortname": "MM_Matrix_s03_d01_op1_level","address": "/modular-fm/MM_Matrix_s03_d01_op1_level","index": 264544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d02_op2_level","varname": "fHslider572","shortname": "MM_Matrix_s03_d02_op2_level","address": "/modular-fm/MM_Matrix_s03_d02_op2_level","index": 264776,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d03_op3_level","varname": "fHslider626","shortname": "MM_Matrix_s03_d03_op3_level","address": "/modular-fm/MM_Matrix_s03_d03_op3_level","index": 265008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d04_op4_level","varname": "fHslider680","shortname": "MM_Matrix_s03_d04_op4_level","address": "/modular-fm/MM_Matrix_s03_d04_op4_level","index": 265240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d05_cross_mod_global","varname": "fHslider371","shortname": "MM_Matrix_s03_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s03_d05_cross_mod_global","index": 263948,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d06_feedback_global","varname": "fHslider319","shortname": "MM_Matrix_s03_d06_feedback_global","address": "/modular-fm/MM_Matrix_s03_d06_feedback_global","index": 263740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d07_global_ratio","varname": "fHslider419","shortname": "MM_Matrix_s03_d07_global_ratio","address": "/modular-fm/MM_Matrix_s03_d07_global_ratio","index": 264140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d08_amp","varname": "fHslider270","shortname": "MM_Matrix_s03_d08_amp","address": "/modular-fm/MM_Matrix_s03_d08_amp","index": 263544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d09_pan","varname": "fHslider204","shortname": "MM_Matrix_s03_d09_pan","address": "/modular-fm/MM_Matrix_s03_d09_pan","index": 263264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d00_pitch","varname": "fHslider467","shortname": "MM_Matrix_s04_d00_pitch","address": "/modular-fm/MM_Matrix_s04_d00_pitch","index": 264332,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d01_op1_level","varname": "fHslider517","shortname": "MM_Matrix_s04_d01_op1_level","address": "/modular-fm/MM_Matrix_s04_d01_op1_level","index": 264540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d02_op2_level","varname": "fHslider571","shortname": "MM_Matrix_s04_d02_op2_level","address": "/modular-fm/MM_Matrix_s04_d02_op2_level","index": 264772,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d03_op3_level","varname": "fHslider625","shortname": "MM_Matrix_s04_d03_op3_level","address": "/modular-fm/MM_Matrix_s04_d03_op3_level","index": 265004,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d04_op4_level","varname": "fHslider679","shortname": "MM_Matrix_s04_d04_op4_level","address": "/modular-fm/MM_Matrix_s04_d04_op4_level","index": 265236,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d05_cross_mod_global","varname": "fHslider370","shortname": "MM_Matrix_s04_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s04_d05_cross_mod_global","index": 263944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d06_feedback_global","varname": "fHslider318","shortname": "MM_Matrix_s04_d06_feedback_global","address": "/modular-fm/MM_Matrix_s04_d06_feedback_global","index": 263736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d07_global_ratio","varname": "fHslider418","shortname": "MM_Matrix_s04_d07_global_ratio","address": "/modular-fm/MM_Matrix_s04_d07_global_ratio","index": 264136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d08_amp","varname": "fHslider269","shortname": "MM_Matrix_s04_d08_amp","address": "/modular-fm/MM_Matrix_s04_d08_amp","index": 263540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d09_pan","varname": "fHslider198","shortname": "MM_Matrix_s04_d09_pan","address": "/modular-fm/MM_Matrix_s04_d09_pan","index": 263240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d00_pitch","varname": "fHslider466","shortname": "MM_Matrix_s05_d00_pitch","address": "/modular-fm/MM_Matrix_s05_d00_pitch","index": 264328,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d01_op1_level","varname": "fHslider516","shortname": "MM_Matrix_s05_d01_op1_level","address": "/modular-fm/MM_Matrix_s05_d01_op1_level","index": 264536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d02_op2_level","varname": "fHslider570","shortname": "MM_Matrix_s05_d02_op2_level","address": "/modular-fm/MM_Matrix_s05_d02_op2_level","index": 264768,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d03_op3_level","varname": "fHslider624","shortname": "MM_Matrix_s05_d03_op3_level","address": "/modular-fm/MM_Matrix_s05_d03_op3_level","index": 265000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d04_op4_level","varname": "fHslider678","shortname": "MM_Matrix_s05_d04_op4_level","address": "/modular-fm/MM_Matrix_s05_d04_op4_level","index": 265232,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d05_cross_mod_global","varname": "fHslider369","shortname": "MM_Matrix_s05_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s05_d05_cross_mod_global","index": 263940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d06_feedback_global","varname": "fHslider317","shortname": "MM_Matrix_s05_d06_feedback_global","address": "/modular-fm/MM_Matrix_s05_d06_feedback_global","index": 263732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d07_global_ratio","varname": "fHslider417","shortname": "MM_Matrix_s05_d07_global_ratio","address": "/modular-fm/MM_Matrix_s05_d07_global_ratio","index": 264132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d08_amp","varname": "fHslider268","shortname": "MM_Matrix_s05_d08_amp","address": "/modular-fm/MM_Matrix_s05_d08_amp","index": 263536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d09_pan","varname": "fHslider192","shortname": "MM_Matrix_s05_d09_pan","address": "/modular-fm/MM_Matrix_s05_d09_pan","index": 263216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d00_pitch","varname": "fHslider465","shortname": "MM_Matrix_s06_d00_pitch","address": "/modular-fm/MM_Matrix_s06_d00_pitch","index": 264324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d01_op1_level","varname": "fHslider515","shortname": "MM_Matrix_s06_d01_op1_level","address": "/modular-fm/MM_Matrix_s06_d01_op1_level","index": 264532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d02_op2_level","varname": "fHslider569","shortname": "MM_Matrix_s06_d02_op2_level","address": "/modular-fm/MM_Matrix_s06_d02_op2_level","index": 264764,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d03_op3_level","varname": "fHslider623","shortname": "MM_Matrix_s06_d03_op3_level","address": "/modular-fm/MM_Matrix_s06_d03_op3_level","index": 264996,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d04_op4_level","varname": "fHslider677","shortname": "MM_Matrix_s06_d04_op4_level","address": "/modular-fm/MM_Matrix_s06_d04_op4_level","index": 265228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d05_cross_mod_global","varname": "fHslider368","shortname": "MM_Matrix_s06_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s06_d05_cross_mod_global","index": 263936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d06_feedback_global","varname": "fHslider316","shortname": "MM_Matrix_s06_d06_feedback_global","address": "/modular-fm/MM_Matrix_s06_d06_feedback_global","index": 263728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d07_global_ratio","varname": "fHslider416","shortname": "MM_Matrix_s06_d07_global_ratio","address": "/modular-fm/MM_Matrix_s06_d07_global_ratio","index": 264128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d08_amp","varname": "fHslider267","shortname": "MM_Matrix_s06_d08_amp","address": "/modular-fm/MM_Matrix_s06_d08_amp","index": 263532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d09_pan","varname": "fHslider186","shortname": "MM_Matrix_s06_d09_pan","address": "/modular-fm/MM_Matrix_s06_d09_pan","index": 263192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d00_pitch","varname": "fHslider464","shortname": "MM_Matrix_s07_d00_pitch","address": "/modular-fm/MM_Matrix_s07_d00_pitch","index": 264320,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d01_op1_level","varname": "fHslider514","shortname": "MM_Matrix_s07_d01_op1_level","address": "/modular-fm/MM_Matrix_s07_d01_op1_level","index": 264528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d02_op2_level","varname": "fHslider568","shortname": "MM_Matrix_s07_d02_op2_level","address": "/modular-fm/MM_Matrix_s07_d02_op2_level","index": 264760,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d03_op3_level","varname": "fHslider622","shortname": "MM_Matrix_s07_d03_op3_level","address": "/modular-fm/MM_Matrix_s07_d03_op3_level","index": 264992,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d04_op4_level","varname": "fHslider676","shortname": "MM_Matrix_s07_d04_op4_level","address": "/modular-fm/MM_Matrix_s07_d04_op4_level","index": 265224,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d05_cross_mod_global","varname": "fHslider367","shortname": "MM_Matrix_s07_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s07_d05_cross_mod_global","index": 263932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d06_feedback_global","varname": "fHslider315","shortname": "MM_Matrix_s07_d06_feedback_global","address": "/modular-fm/MM_Matrix_s07_d06_feedback_global","index": 263724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d07_global_ratio","varname": "fHslider415","shortname": "MM_Matrix_s07_d07_global_ratio","address": "/modular-fm/MM_Matrix_s07_d07_global_ratio","index": 264124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d08_amp","varname": "fHslider266","shortname": "MM_Matrix_s07_d08_amp","address": "/modular-fm/MM_Matrix_s07_d08_amp","index": 263528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d09_pan","varname": "fHslider180","shortname": "MM_Matrix_s07_d09_pan","address": "/modular-fm/MM_Matrix_s07_d09_pan","index": 263168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d00_pitch","varname": "fHslider463","shortname": "MM_Matrix_s08_d00_pitch","address": "/modular-fm/MM_Matrix_s08_d00_pitch","index": 264316,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d01_op1_level","varname": "fHslider513","shortname": "MM_Matrix_s08_d01_op1_level","address": "/modular-fm/MM_Matrix_s08_d01_op1_level","index": 264524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d02_op2_level","varname": "fHslider567","shortname": "MM_Matrix_s08_d02_op2_level","address": "/modular-fm/MM_Matrix_s08_d02_op2_level","index": 264756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d03_op3_level","varname": "fHslider621","shortname": "MM_Matrix_s08_d03_op3_level","address": "/modular-fm/MM_Matrix_s08_d03_op3_level","index": 264988,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d04_op4_level","varname": "fHslider675","shortname": "MM_Matrix_s08_d04_op4_level","address": "/modular-fm/MM_Matrix_s08_d04_op4_level","index": 265220,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d05_cross_mod_global","varname": "fHslider366","shortname": "MM_Matrix_s08_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s08_d05_cross_mod_global","index": 263928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d06_feedback_global","varname": "fHslider314","shortname": "MM_Matrix_s08_d06_feedback_global","address": "/modular-fm/MM_Matrix_s08_d06_feedback_global","index": 263720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d07_global_ratio","varname": "fHslider414","shortname": "MM_Matrix_s08_d07_global_ratio","address": "/modular-fm/MM_Matrix_s08_d07_global_ratio","index": 264120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d08_amp","varname": "fHslider265","shortname": "MM_Matrix_s08_d08_amp","address": "/modular-fm/MM_Matrix_s08_d08_amp","index": 263524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d09_pan","varname": "fHslider174","shortname": "MM_Matrix_s08_d09_pan","address": "/modular-fm/MM_Matrix_s08_d09_pan","index": 263144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d00_pitch","varname": "fHslider462","shortname": "MM_Matrix_s09_d00_pitch","address": "/modular-fm/MM_Matrix_s09_d00_pitch","index": 264312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d01_op1_level","varname": "fHslider512","shortname": "MM_Matrix_s09_d01_op1_level","address": "/modular-fm/MM_Matrix_s09_d01_op1_level","index": 264520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d02_op2_level","varname": "fHslider566","shortname": "MM_Matrix_s09_d02_op2_level","address": "/modular-fm/MM_Matrix_s09_d02_op2_level","index": 264752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d03_op3_level","varname": "fHslider620","shortname": "MM_Matrix_s09_d03_op3_level","address": "/modular-fm/MM_Matrix_s09_d03_op3_level","index": 264984,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d04_op4_level","varname": "fHslider674","shortname": "MM_Matrix_s09_d04_op4_level","address": "/modular-fm/MM_Matrix_s09_d04_op4_level","index": 265216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d05_cross_mod_global","varname": "fHslider365","shortname": "MM_Matrix_s09_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s09_d05_cross_mod_global","index": 263924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d06_feedback_global","varname": "fHslider313","shortname": "MM_Matrix_s09_d06_feedback_global","address": "/modular-fm/MM_Matrix_s09_d06_feedback_global","index": 263716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d07_global_ratio","varname": "fHslider413","shortname": "MM_Matrix_s09_d07_global_ratio","address": "/modular-fm/MM_Matrix_s09_d07_global_ratio","index": 264116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d08_amp","varname": "fHslider264","shortname": "MM_Matrix_s09_d08_amp","address": "/modular-fm/MM_Matrix_s09_d08_amp","index": 263520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d09_pan","varname": "fHslider168","shortname": "MM_Matrix_s09_d09_pan","address": "/modular-fm/MM_Matrix_s09_d09_pan","index": 263120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d00_pitch","varname": "fHslider461","shortname": "MM_Matrix_s10_d00_pitch","address": "/modular-fm/MM_Matrix_s10_d00_pitch","index": 264308,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d01_op1_level","varname": "fHslider511","shortname": "MM_Matrix_s10_d01_op1_level","address": "/modular-fm/MM_Matrix_s10_d01_op1_level","index": 264516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d02_op2_level","varname": "fHslider565","shortname": "MM_Matrix_s10_d02_op2_level","address": "/modular-fm/MM_Matrix_s10_d02_op2_level","index": 264748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d03_op3_level","varname": "fHslider619","shortname": "MM_Matrix_s10_d03_op3_level","address": "/modular-fm/MM_Matrix_s10_d03_op3_level","index": 264980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d04_op4_level","varname": "fHslider673","shortname": "MM_Matrix_s10_d04_op4_level","address": "/modular-fm/MM_Matrix_s10_d04_op4_level","index": 265212,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d05_cross_mod_global","varname": "fHslider364","shortname": "MM_Matrix_s10_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s10_d05_cross_mod_global","index": 263920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d06_feedback_global","varname": "fHslider312","shortname": "MM_Matrix_s10_d06_feedback_global","address": "/modular-fm/MM_Matrix_s10_d06_feedback_global","index": 263712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d07_global_ratio","varname": "fHslider412","shortname": "MM_Matrix_s10_d07_global_ratio","address": "/modular-fm/MM_Matrix_s10_d07_global_ratio","index": 264112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d08_amp","varname": "fHslider263","shortname": "MM_Matrix_s10_d08_amp","address": "/modular-fm/MM_Matrix_s10_d08_amp","index": 263516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d09_pan","varname": "fHslider162","shortname": "MM_Matrix_s10_d09_pan","address": "/modular-fm/MM_Matrix_s10_d09_pan","index": 263096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d00_pitch","varname": "fHslider460","shortname": "MM_Matrix_s11_d00_pitch","address": "/modular-fm/MM_Matrix_s11_d00_pitch","index": 264304,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d01_op1_level","varname": "fHslider510","shortname": "MM_Matrix_s11_d01_op1_level","address": "/modular-fm/MM_Matrix_s11_d01_op1_level","index": 264512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d02_op2_level","varname": "fHslider564","shortname": "MM_Matrix_s11_d02_op2_level","address": "/modular-fm/MM_Matrix_s11_d02_op2_level","index": 264744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d03_op3_level","varname": "fHslider618","shortname": "MM_Matrix_s11_d03_op3_level","address": "/modular-fm/MM_Matrix_s11_d03_op3_level","index": 264976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d04_op4_level","varname": "fHslider672","shortname": "MM_Matrix_s11_d04_op4_level","address": "/modular-fm/MM_Matrix_s11_d04_op4_level","index": 265208,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d05_cross_mod_global","varname": "fHslider363","shortname": "MM_Matrix_s11_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s11_d05_cross_mod_global","index": 263916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d06_feedback_global","varname": "fHslider311","shortname": "MM_Matrix_s11_d06_feedback_global","address": "/modular-fm/MM_Matrix_s11_d06_feedback_global","index": 263708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d07_global_ratio","varname": "fHslider411","shortname": "MM_Matrix_s11_d07_global_ratio","address": "/modular-fm/MM_Matrix_s11_d07_global_ratio","index": 264108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d08_amp","varname": "fHslider262","shortname": "MM_Matrix_s11_d08_amp","address": "/modular-fm/MM_Matrix_s11_d08_amp","index": 263512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d09_pan","varname": "fHslider156","shortname": "MM_Matrix_s11_d09_pan","address": "/modular-fm/MM_Matrix_s11_d09_pan","index": 263072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d00_pitch","varname": "fHslider459","shortname": "MM_Matrix_s12_d00_pitch","address": "/modular-fm/MM_Matrix_s12_d00_pitch","index": 264300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d01_op1_level","varname": "fHslider509","shortname": "MM_Matrix_s12_d01_op1_level","address": "/modular-fm/MM_Matrix_s12_d01_op1_level","index": 264508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d02_op2_level","varname": "fHslider563","shortname": "MM_Matrix_s12_d02_op2_level","address": "/modular-fm/MM_Matrix_s12_d02_op2_level","index": 264740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d03_op3_level","varname": "fHslider617","shortname": "MM_Matrix_s12_d03_op3_level","address": "/modular-fm/MM_Matrix_s12_d03_op3_level","index": 264972,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d04_op4_level","varname": "fHslider671","shortname": "MM_Matrix_s12_d04_op4_level","address": "/modular-fm/MM_Matrix_s12_d04_op4_level","index": 265204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d05_cross_mod_global","varname": "fHslider362","shortname": "MM_Matrix_s12_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s12_d05_cross_mod_global","index": 263912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d06_feedback_global","varname": "fHslider310","shortname": "MM_Matrix_s12_d06_feedback_global","address": "/modular-fm/MM_Matrix_s12_d06_feedback_global","index": 263704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d07_global_ratio","varname": "fHslider410","shortname": "MM_Matrix_s12_d07_global_ratio","address": "/modular-fm/MM_Matrix_s12_d07_global_ratio","index": 264104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d08_amp","varname": "fHslider261","shortname": "MM_Matrix_s12_d08_amp","address": "/modular-fm/MM_Matrix_s12_d08_amp","index": 263508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d09_pan","varname": "fHslider150","shortname": "MM_Matrix_s12_d09_pan","address": "/modular-fm/MM_Matrix_s12_d09_pan","index": 263048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d00_pitch","varname": "fHslider458","shortname": "MM_Matrix_s13_d00_pitch","address": "/modular-fm/MM_Matrix_s13_d00_pitch","index": 264296,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d01_op1_level","varname": "fHslider508","shortname": "MM_Matrix_s13_d01_op1_level","address": "/modular-fm/MM_Matrix_s13_d01_op1_level","index": 264504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d02_op2_level","varname": "fHslider562","shortname": "MM_Matrix_s13_d02_op2_level","address": "/modular-fm/MM_Matrix_s13_d02_op2_level","index": 264736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d03_op3_level","varname": "fHslider616","shortname": "MM_Matrix_s13_d03_op3_level","address": "/modular-fm/MM_Matrix_s13_d03_op3_level","index": 264968,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d04_op4_level","varname": "fHslider670","shortname": "MM_Matrix_s13_d04_op4_level","address": "/modular-fm/MM_Matrix_s13_d04_op4_level","index": 265200,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d05_cross_mod_global","varname": "fHslider361","shortname": "MM_Matrix_s13_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s13_d05_cross_mod_global","index": 263908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d06_feedback_global","varname": "fHslider309","shortname": "MM_Matrix_s13_d06_feedback_global","address": "/modular-fm/MM_Matrix_s13_d06_feedback_global","index": 263700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d07_global_ratio","varname": "fHslider409","shortname": "MM_Matrix_s13_d07_global_ratio","address": "/modular-fm/MM_Matrix_s13_d07_global_ratio","index": 264100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d08_amp","varname": "fHslider260","shortname": "MM_Matrix_s13_d08_amp","address": "/modular-fm/MM_Matrix_s13_d08_amp","index": 263504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d09_pan","varname": "fHslider144","shortname": "MM_Matrix_s13_d09_pan","address": "/modular-fm/MM_Matrix_s13_d09_pan","index": 263024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d00_pitch","varname": "fHslider457","shortname": "MM_Matrix_s14_d00_pitch","address": "/modular-fm/MM_Matrix_s14_d00_pitch","index": 264292,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d01_op1_level","varname": "fHslider507","shortname": "MM_Matrix_s14_d01_op1_level","address": "/modular-fm/MM_Matrix_s14_d01_op1_level","index": 264500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d02_op2_level","varname": "fHslider561","shortname": "MM_Matrix_s14_d02_op2_level","address": "/modular-fm/MM_Matrix_s14_d02_op2_level","index": 264732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d03_op3_level","varname": "fHslider615","shortname": "MM_Matrix_s14_d03_op3_level","address": "/modular-fm/MM_Matrix_s14_d03_op3_level","index": 264964,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d04_op4_level","varname": "fHslider669","shortname": "MM_Matrix_s14_d04_op4_level","address": "/modular-fm/MM_Matrix_s14_d04_op4_level","index": 265196,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d05_cross_mod_global","varname": "fHslider360","shortname": "MM_Matrix_s14_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s14_d05_cross_mod_global","index": 263904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d06_feedback_global","varname": "fHslider308","shortname": "MM_Matrix_s14_d06_feedback_global","address": "/modular-fm/MM_Matrix_s14_d06_feedback_global","index": 263696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d07_global_ratio","varname": "fHslider408","shortname": "MM_Matrix_s14_d07_global_ratio","address": "/modular-fm/MM_Matrix_s14_d07_global_ratio","index": 264096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d08_amp","varname": "fHslider259","shortname": "MM_Matrix_s14_d08_amp","address": "/modular-fm/MM_Matrix_s14_d08_amp","index": 263500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d09_pan","varname": "fHslider138","shortname": "MM_Matrix_s14_d09_pan","address": "/modular-fm/MM_Matrix_s14_d09_pan","index": 263000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d00_pitch","varname": "fHslider456","shortname": "MM_Matrix_s15_d00_pitch","address": "/modular-fm/MM_Matrix_s15_d00_pitch","index": 264288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d01_op1_level","varname": "fHslider506","shortname": "MM_Matrix_s15_d01_op1_level","address": "/modular-fm/MM_Matrix_s15_d01_op1_level","index": 264496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d02_op2_level","varname": "fHslider560","shortname": "MM_Matrix_s15_d02_op2_level","address": "/modular-fm/MM_Matrix_s15_d02_op2_level","index": 264728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d03_op3_level","varname": "fHslider614","shortname": "MM_Matrix_s15_d03_op3_level","address": "/modular-fm/MM_Matrix_s15_d03_op3_level","index": 264960,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d04_op4_level","varname": "fHslider668","shortname": "MM_Matrix_s15_d04_op4_level","address": "/modular-fm/MM_Matrix_s15_d04_op4_level","index": 265192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d05_cross_mod_global","varname": "fHslider359","shortname": "MM_Matrix_s15_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s15_d05_cross_mod_global","index": 263900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d06_feedback_global","varname": "fHslider307","shortname": "MM_Matrix_s15_d06_feedback_global","address": "/modular-fm/MM_Matrix_s15_d06_feedback_global","index": 263692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d07_global_ratio","varname": "fHslider407","shortname": "MM_Matrix_s15_d07_global_ratio","address": "/modular-fm/MM_Matrix_s15_d07_global_ratio","index": 264092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d08_amp","varname": "fHslider258","shortname": "MM_Matrix_s15_d08_amp","address": "/modular-fm/MM_Matrix_s15_d08_amp","index": 263496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d09_pan","varname": "fHslider132","shortname": "MM_Matrix_s15_d09_pan","address": "/modular-fm/MM_Matrix_s15_d09_pan","index": 262976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d00_pitch","varname": "fHslider455","shortname": "MM_Matrix_s16_d00_pitch","address": "/modular-fm/MM_Matrix_s16_d00_pitch","index": 264284,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d01_op1_level","varname": "fHslider505","shortname": "MM_Matrix_s16_d01_op1_level","address": "/modular-fm/MM_Matrix_s16_d01_op1_level","index": 264492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d02_op2_level","varname": "fHslider559","shortname": "MM_Matrix_s16_d02_op2_level","address": "/modular-fm/MM_Matrix_s16_d02_op2_level","index": 264724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d03_op3_level","varname": "fHslider613","shortname": "MM_Matrix_s16_d03_op3_level","address": "/modular-fm/MM_Matrix_s16_d03_op3_level","index": 264956,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d04_op4_level","varname": "fHslider667","shortname": "MM_Matrix_s16_d04_op4_level","address": "/modular-fm/MM_Matrix_s16_d04_op4_level","index": 265188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d05_cross_mod_global","varname": "fHslider358","shortname": "MM_Matrix_s16_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s16_d05_cross_mod_global","index": 263896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d06_feedback_global","varname": "fHslider306","shortname": "MM_Matrix_s16_d06_feedback_global","address": "/modular-fm/MM_Matrix_s16_d06_feedback_global","index": 263688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d07_global_ratio","varname": "fHslider406","shortname": "MM_Matrix_s16_d07_global_ratio","address": "/modular-fm/MM_Matrix_s16_d07_global_ratio","index": 264088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d08_amp","varname": "fHslider257","shortname": "MM_Matrix_s16_d08_amp","address": "/modular-fm/MM_Matrix_s16_d08_amp","index": 263492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d09_pan","varname": "fHslider126","shortname": "MM_Matrix_s16_d09_pan","address": "/modular-fm/MM_Matrix_s16_d09_pan","index": 262924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d00_pitch","varname": "fHslider454","shortname": "MM_Matrix_s17_d00_pitch","address": "/modular-fm/MM_Matrix_s17_d00_pitch","index": 264280,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d01_op1_level","varname": "fHslider504","shortname": "MM_Matrix_s17_d01_op1_level","address": "/modular-fm/MM_Matrix_s17_d01_op1_level","index": 264488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d02_op2_level","varname": "fHslider558","shortname": "MM_Matrix_s17_d02_op2_level","address": "/modular-fm/MM_Matrix_s17_d02_op2_level","index": 264720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d03_op3_level","varname": "fHslider612","shortname": "MM_Matrix_s17_d03_op3_level","address": "/modular-fm/MM_Matrix_s17_d03_op3_level","index": 264952,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d04_op4_level","varname": "fHslider666","shortname": "MM_Matrix_s17_d04_op4_level","address": "/modular-fm/MM_Matrix_s17_d04_op4_level","index": 265184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d05_cross_mod_global","varname": "fHslider357","shortname": "MM_Matrix_s17_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s17_d05_cross_mod_global","index": 263892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d06_feedback_global","varname": "fHslider305","shortname": "MM_Matrix_s17_d06_feedback_global","address": "/modular-fm/MM_Matrix_s17_d06_feedback_global","index": 263684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d07_global_ratio","varname": "fHslider405","shortname": "MM_Matrix_s17_d07_global_ratio","address": "/modular-fm/MM_Matrix_s17_d07_global_ratio","index": 264084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d08_amp","varname": "fHslider256","shortname": "MM_Matrix_s17_d08_amp","address": "/modular-fm/MM_Matrix_s17_d08_amp","index": 263488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d09_pan","varname": "fHslider122","shortname": "MM_Matrix_s17_d09_pan","address": "/modular-fm/MM_Matrix_s17_d09_pan","index": 262900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d00_pitch","varname": "fHslider453","shortname": "MM_Matrix_s18_d00_pitch","address": "/modular-fm/MM_Matrix_s18_d00_pitch","index": 264276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d01_op1_level","varname": "fHslider503","shortname": "MM_Matrix_s18_d01_op1_level","address": "/modular-fm/MM_Matrix_s18_d01_op1_level","index": 264484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d02_op2_level","varname": "fHslider557","shortname": "MM_Matrix_s18_d02_op2_level","address": "/modular-fm/MM_Matrix_s18_d02_op2_level","index": 264716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d03_op3_level","varname": "fHslider611","shortname": "MM_Matrix_s18_d03_op3_level","address": "/modular-fm/MM_Matrix_s18_d03_op3_level","index": 264948,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d04_op4_level","varname": "fHslider665","shortname": "MM_Matrix_s18_d04_op4_level","address": "/modular-fm/MM_Matrix_s18_d04_op4_level","index": 265180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d05_cross_mod_global","varname": "fHslider356","shortname": "MM_Matrix_s18_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s18_d05_cross_mod_global","index": 263888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d06_feedback_global","varname": "fHslider304","shortname": "MM_Matrix_s18_d06_feedback_global","address": "/modular-fm/MM_Matrix_s18_d06_feedback_global","index": 263680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d07_global_ratio","varname": "fHslider404","shortname": "MM_Matrix_s18_d07_global_ratio","address": "/modular-fm/MM_Matrix_s18_d07_global_ratio","index": 264080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d08_amp","varname": "fHslider255","shortname": "MM_Matrix_s18_d08_amp","address": "/modular-fm/MM_Matrix_s18_d08_amp","index": 263484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d09_pan","varname": "fHslider118","shortname": "MM_Matrix_s18_d09_pan","address": "/modular-fm/MM_Matrix_s18_d09_pan","index": 262876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d00_pitch","varname": "fHslider452","shortname": "MM_Matrix_s19_d00_pitch","address": "/modular-fm/MM_Matrix_s19_d00_pitch","index": 264272,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d01_op1_level","varname": "fHslider502","shortname": "MM_Matrix_s19_d01_op1_level","address": "/modular-fm/MM_Matrix_s19_d01_op1_level","index": 264480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d02_op2_level","varname": "fHslider556","shortname": "MM_Matrix_s19_d02_op2_level","address": "/modular-fm/MM_Matrix_s19_d02_op2_level","index": 264712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d03_op3_level","varname": "fHslider610","shortname": "MM_Matrix_s19_d03_op3_level","address": "/modular-fm/MM_Matrix_s19_d03_op3_level","index": 264944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d04_op4_level","varname": "fHslider664","shortname": "MM_Matrix_s19_d04_op4_level","address": "/modular-fm/MM_Matrix_s19_d04_op4_level","index": 265176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d05_cross_mod_global","varname": "fHslider355","shortname": "MM_Matrix_s19_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s19_d05_cross_mod_global","index": 263884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d06_feedback_global","varname": "fHslider303","shortname": "MM_Matrix_s19_d06_feedback_global","address": "/modular-fm/MM_Matrix_s19_d06_feedback_global","index": 263676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d07_global_ratio","varname": "fHslider403","shortname": "MM_Matrix_s19_d07_global_ratio","address": "/modular-fm/MM_Matrix_s19_d07_global_ratio","index": 264076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d08_amp","varname": "fHslider254","shortname": "MM_Matrix_s19_d08_amp","address": "/modular-fm/MM_Matrix_s19_d08_amp","index": 263480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d09_pan","varname": "fHslider114","shortname": "MM_Matrix_s19_d09_pan","address": "/modular-fm/MM_Matrix_s19_d09_pan","index": 262852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d00_pitch","varname": "fHslider451","shortname": "MM_Matrix_s20_d00_pitch","address": "/modular-fm/MM_Matrix_s20_d00_pitch","index": 264268,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d01_op1_level","varname": "fHslider501","shortname": "MM_Matrix_s20_d01_op1_level","address": "/modular-fm/MM_Matrix_s20_d01_op1_level","index": 264476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d02_op2_level","varname": "fHslider555","shortname": "MM_Matrix_s20_d02_op2_level","address": "/modular-fm/MM_Matrix_s20_d02_op2_level","index": 264708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d03_op3_level","varname": "fHslider609","shortname": "MM_Matrix_s20_d03_op3_level","address": "/modular-fm/MM_Matrix_s20_d03_op3_level","index": 264940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d04_op4_level","varname": "fHslider663","shortname": "MM_Matrix_s20_d04_op4_level","address": "/modular-fm/MM_Matrix_s20_d04_op4_level","index": 265172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d05_cross_mod_global","varname": "fHslider354","shortname": "MM_Matrix_s20_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s20_d05_cross_mod_global","index": 263880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d06_feedback_global","varname": "fHslider302","shortname": "MM_Matrix_s20_d06_feedback_global","address": "/modular-fm/MM_Matrix_s20_d06_feedback_global","index": 263672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d07_global_ratio","varname": "fHslider402","shortname": "MM_Matrix_s20_d07_global_ratio","address": "/modular-fm/MM_Matrix_s20_d07_global_ratio","index": 264072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d08_amp","varname": "fHslider253","shortname": "MM_Matrix_s20_d08_amp","address": "/modular-fm/MM_Matrix_s20_d08_amp","index": 263476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d09_pan","varname": "fHslider110","shortname": "MM_Matrix_s20_d09_pan","address": "/modular-fm/MM_Matrix_s20_d09_pan","index": 262828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d00_pitch","varname": "fHslider450","shortname": "MM_Matrix_s21_d00_pitch","address": "/modular-fm/MM_Matrix_s21_d00_pitch","index": 264264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d01_op1_level","varname": "fHslider500","shortname": "MM_Matrix_s21_d01_op1_level","address": "/modular-fm/MM_Matrix_s21_d01_op1_level","index": 264472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d02_op2_level","varname": "fHslider554","shortname": "MM_Matrix_s21_d02_op2_level","address": "/modular-fm/MM_Matrix_s21_d02_op2_level","index": 264704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d03_op3_level","varname": "fHslider608","shortname": "MM_Matrix_s21_d03_op3_level","address": "/modular-fm/MM_Matrix_s21_d03_op3_level","index": 264936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d04_op4_level","varname": "fHslider662","shortname": "MM_Matrix_s21_d04_op4_level","address": "/modular-fm/MM_Matrix_s21_d04_op4_level","index": 265168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d05_cross_mod_global","varname": "fHslider353","shortname": "MM_Matrix_s21_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s21_d05_cross_mod_global","index": 263876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d06_feedback_global","varname": "fHslider301","shortname": "MM_Matrix_s21_d06_feedback_global","address": "/modular-fm/MM_Matrix_s21_d06_feedback_global","index": 263668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d07_global_ratio","varname": "fHslider401","shortname": "MM_Matrix_s21_d07_global_ratio","address": "/modular-fm/MM_Matrix_s21_d07_global_ratio","index": 264068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d08_amp","varname": "fHslider252","shortname": "MM_Matrix_s21_d08_amp","address": "/modular-fm/MM_Matrix_s21_d08_amp","index": 263472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d09_pan","varname": "fHslider106","shortname": "MM_Matrix_s21_d09_pan","address": "/modular-fm/MM_Matrix_s21_d09_pan","index": 262804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d00_pitch","varname": "fHslider449","shortname": "MM_Matrix_s22_d00_pitch","address": "/modular-fm/MM_Matrix_s22_d00_pitch","index": 264260,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d01_op1_level","varname": "fHslider499","shortname": "MM_Matrix_s22_d01_op1_level","address": "/modular-fm/MM_Matrix_s22_d01_op1_level","index": 264468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d02_op2_level","varname": "fHslider553","shortname": "MM_Matrix_s22_d02_op2_level","address": "/modular-fm/MM_Matrix_s22_d02_op2_level","index": 264700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d03_op3_level","varname": "fHslider607","shortname": "MM_Matrix_s22_d03_op3_level","address": "/modular-fm/MM_Matrix_s22_d03_op3_level","index": 264932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d04_op4_level","varname": "fHslider661","shortname": "MM_Matrix_s22_d04_op4_level","address": "/modular-fm/MM_Matrix_s22_d04_op4_level","index": 265164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d05_cross_mod_global","varname": "fHslider352","shortname": "MM_Matrix_s22_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s22_d05_cross_mod_global","index": 263872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d06_feedback_global","varname": "fHslider300","shortname": "MM_Matrix_s22_d06_feedback_global","address": "/modular-fm/MM_Matrix_s22_d06_feedback_global","index": 263664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d07_global_ratio","varname": "fHslider400","shortname": "MM_Matrix_s22_d07_global_ratio","address": "/modular-fm/MM_Matrix_s22_d07_global_ratio","index": 264064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d08_amp","varname": "fHslider251","shortname": "MM_Matrix_s22_d08_amp","address": "/modular-fm/MM_Matrix_s22_d08_amp","index": 263468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d09_pan","varname": "fHslider102","shortname": "MM_Matrix_s22_d09_pan","address": "/modular-fm/MM_Matrix_s22_d09_pan","index": 262780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d00_pitch","varname": "fHslider448","shortname": "MM_Matrix_s23_d00_pitch","address": "/modular-fm/MM_Matrix_s23_d00_pitch","index": 264256,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d01_op1_level","varname": "fHslider498","shortname": "MM_Matrix_s23_d01_op1_level","address": "/modular-fm/MM_Matrix_s23_d01_op1_level","index": 264464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d02_op2_level","varname": "fHslider552","shortname": "MM_Matrix_s23_d02_op2_level","address": "/modular-fm/MM_Matrix_s23_d02_op2_level","index": 264696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d03_op3_level","varname": "fHslider606","shortname": "MM_Matrix_s23_d03_op3_level","address": "/modular-fm/MM_Matrix_s23_d03_op3_level","index": 264928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d04_op4_level","varname": "fHslider660","shortname": "MM_Matrix_s23_d04_op4_level","address": "/modular-fm/MM_Matrix_s23_d04_op4_level","index": 265160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d05_cross_mod_global","varname": "fHslider351","shortname": "MM_Matrix_s23_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s23_d05_cross_mod_global","index": 263868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d06_feedback_global","varname": "fHslider299","shortname": "MM_Matrix_s23_d06_feedback_global","address": "/modular-fm/MM_Matrix_s23_d06_feedback_global","index": 263660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d07_global_ratio","varname": "fHslider399","shortname": "MM_Matrix_s23_d07_global_ratio","address": "/modular-fm/MM_Matrix_s23_d07_global_ratio","index": 264060,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d08_amp","varname": "fHslider250","shortname": "MM_Matrix_s23_d08_amp","address": "/modular-fm/MM_Matrix_s23_d08_amp","index": 263464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d09_pan","varname": "fHslider98","shortname": "MM_Matrix_s23_d09_pan","address": "/modular-fm/MM_Matrix_s23_d09_pan","index": 262756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d00_pitch","varname": "fHslider447","shortname": "MM_Matrix_s24_d00_pitch","address": "/modular-fm/MM_Matrix_s24_d00_pitch","index": 264252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d01_op1_level","varname": "fHslider497","shortname": "MM_Matrix_s24_d01_op1_level","address": "/modular-fm/MM_Matrix_s24_d01_op1_level","index": 264460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d02_op2_level","varname": "fHslider551","shortname": "MM_Matrix_s24_d02_op2_level","address": "/modular-fm/MM_Matrix_s24_d02_op2_level","index": 264692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d03_op3_level","varname": "fHslider605","shortname": "MM_Matrix_s24_d03_op3_level","address": "/modular-fm/MM_Matrix_s24_d03_op3_level","index": 264924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d04_op4_level","varname": "fHslider659","shortname": "MM_Matrix_s24_d04_op4_level","address": "/modular-fm/MM_Matrix_s24_d04_op4_level","index": 265156,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d05_cross_mod_global","varname": "fHslider350","shortname": "MM_Matrix_s24_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s24_d05_cross_mod_global","index": 263864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d06_feedback_global","varname": "fHslider298","shortname": "MM_Matrix_s24_d06_feedback_global","address": "/modular-fm/MM_Matrix_s24_d06_feedback_global","index": 263656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d07_global_ratio","varname": "fHslider398","shortname": "MM_Matrix_s24_d07_global_ratio","address": "/modular-fm/MM_Matrix_s24_d07_global_ratio","index": 264056,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d08_amp","varname": "fHslider249","shortname": "MM_Matrix_s24_d08_amp","address": "/modular-fm/MM_Matrix_s24_d08_amp","index": 263460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d09_pan","varname": "fHslider94","shortname": "MM_Matrix_s24_d09_pan","address": "/modular-fm/MM_Matrix_s24_d09_pan","index": 262732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d00_pitch","varname": "fHslider446","shortname": "MM_Matrix_s25_d00_pitch","address": "/modular-fm/MM_Matrix_s25_d00_pitch","index": 264248,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d01_op1_level","varname": "fHslider496","shortname": "MM_Matrix_s25_d01_op1_level","address": "/modular-fm/MM_Matrix_s25_d01_op1_level","index": 264456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d02_op2_level","varname": "fHslider550","shortname": "MM_Matrix_s25_d02_op2_level","address": "/modular-fm/MM_Matrix_s25_d02_op2_level","index": 264688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d03_op3_level","varname": "fHslider604","shortname": "MM_Matrix_s25_d03_op3_level","address": "/modular-fm/MM_Matrix_s25_d03_op3_level","index": 264920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d04_op4_level","varname": "fHslider658","shortname": "MM_Matrix_s25_d04_op4_level","address": "/modular-fm/MM_Matrix_s25_d04_op4_level","index": 265152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d05_cross_mod_global","varname": "fHslider349","shortname": "MM_Matrix_s25_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s25_d05_cross_mod_global","index": 263860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d06_feedback_global","varname": "fHslider297","shortname": "MM_Matrix_s25_d06_feedback_global","address": "/modular-fm/MM_Matrix_s25_d06_feedback_global","index": 263652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d07_global_ratio","varname": "fHslider397","shortname": "MM_Matrix_s25_d07_global_ratio","address": "/modular-fm/MM_Matrix_s25_d07_global_ratio","index": 264052,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d08_amp","varname": "fHslider248","shortname": "MM_Matrix_s25_d08_amp","address": "/modular-fm/MM_Matrix_s25_d08_amp","index": 263456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d09_pan","varname": "fHslider90","shortname": "MM_Matrix_s25_d09_pan","address": "/modular-fm/MM_Matrix_s25_d09_pan","index": 262708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d00_pitch","varname": "fHslider445","shortname": "MM_Matrix_s26_d00_pitch","address": "/modular-fm/MM_Matrix_s26_d00_pitch","index": 264244,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d01_op1_level","varname": "fHslider495","shortname": "MM_Matrix_s26_d01_op1_level","address": "/modular-fm/MM_Matrix_s26_d01_op1_level","index": 264452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d02_op2_level","varname": "fHslider549","shortname": "MM_Matrix_s26_d02_op2_level","address": "/modular-fm/MM_Matrix_s26_d02_op2_level","index": 264684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d03_op3_level","varname": "fHslider603","shortname": "MM_Matrix_s26_d03_op3_level","address": "/modular-fm/MM_Matrix_s26_d03_op3_level","index": 264916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d04_op4_level","varname": "fHslider657","shortname": "MM_Matrix_s26_d04_op4_level","address": "/modular-fm/MM_Matrix_s26_d04_op4_level","index": 265148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d05_cross_mod_global","varname": "fHslider348","shortname": "MM_Matrix_s26_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s26_d05_cross_mod_global","index": 263856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d06_feedback_global","varname": "fHslider296","shortname": "MM_Matrix_s26_d06_feedback_global","address": "/modular-fm/MM_Matrix_s26_d06_feedback_global","index": 263648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d07_global_ratio","varname": "fHslider396","shortname": "MM_Matrix_s26_d07_global_ratio","address": "/modular-fm/MM_Matrix_s26_d07_global_ratio","index": 264048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d08_amp","varname": "fHslider247","shortname": "MM_Matrix_s26_d08_amp","address": "/modular-fm/MM_Matrix_s26_d08_amp","index": 263452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d09_pan","varname": "fHslider86","shortname": "MM_Matrix_s26_d09_pan","address": "/modular-fm/MM_Matrix_s26_d09_pan","index": 262684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d00_pitch","varname": "fHslider444","shortname": "MM_Matrix_s27_d00_pitch","address": "/modular-fm/MM_Matrix_s27_d00_pitch","index": 264240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d01_op1_level","varname": "fHslider494","shortname": "MM_Matrix_s27_d01_op1_level","address": "/modular-fm/MM_Matrix_s27_d01_op1_level","index": 264448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d02_op2_level","varname": "fHslider548","shortname": "MM_Matrix_s27_d02_op2_level","address": "/modular-fm/MM_Matrix_s27_d02_op2_level","index": 264680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d03_op3_level","varname": "fHslider602","shortname": "MM_Matrix_s27_d03_op3_level","address": "/modular-fm/MM_Matrix_s27_d03_op3_level","index": 264912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d04_op4_level","varname": "fHslider656","shortname": "MM_Matrix_s27_d04_op4_level","address": "/modular-fm/MM_Matrix_s27_d04_op4_level","index": 265144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d05_cross_mod_global","varname": "fHslider347","shortname": "MM_Matrix_s27_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s27_d05_cross_mod_global","index": 263852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d06_feedback_global","varname": "fHslider295","shortname": "MM_Matrix_s27_d06_feedback_global","address": "/modular-fm/MM_Matrix_s27_d06_feedback_global","index": 263644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d07_global_ratio","varname": "fHslider395","shortname": "MM_Matrix_s27_d07_global_ratio","address": "/modular-fm/MM_Matrix_s27_d07_global_ratio","index": 264044,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d08_amp","varname": "fHslider246","shortname": "MM_Matrix_s27_d08_amp","address": "/modular-fm/MM_Matrix_s27_d08_amp","index": 263448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d09_pan","varname": "fHslider82","shortname": "MM_Matrix_s27_d09_pan","address": "/modular-fm/MM_Matrix_s27_d09_pan","index": 262660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d00_pitch","varname": "fHslider443","shortname": "MM_Matrix_s28_d00_pitch","address": "/modular-fm/MM_Matrix_s28_d00_pitch","index": 264236,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d01_op1_level","varname": "fHslider493","shortname": "MM_Matrix_s28_d01_op1_level","address": "/modular-fm/MM_Matrix_s28_d01_op1_level","index": 264444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d02_op2_level","varname": "fHslider547","shortname": "MM_Matrix_s28_d02_op2_level","address": "/modular-fm/MM_Matrix_s28_d02_op2_level","index": 264676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d03_op3_level","varname": "fHslider601","shortname": "MM_Matrix_s28_d03_op3_level","address": "/modular-fm/MM_Matrix_s28_d03_op3_level","index": 264908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d04_op4_level","varname": "fHslider655","shortname": "MM_Matrix_s28_d04_op4_level","address": "/modular-fm/MM_Matrix_s28_d04_op4_level","index": 265140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d05_cross_mod_global","varname": "fHslider346","shortname": "MM_Matrix_s28_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s28_d05_cross_mod_global","index": 263848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d06_feedback_global","varname": "fHslider294","shortname": "MM_Matrix_s28_d06_feedback_global","address": "/modular-fm/MM_Matrix_s28_d06_feedback_global","index": 263640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d07_global_ratio","varname": "fHslider394","shortname": "MM_Matrix_s28_d07_global_ratio","address": "/modular-fm/MM_Matrix_s28_d07_global_ratio","index": 264040,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d08_amp","varname": "fHslider245","shortname": "MM_Matrix_s28_d08_amp","address": "/modular-fm/MM_Matrix_s28_d08_amp","index": 263444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d09_pan","varname": "fHslider78","shortname": "MM_Matrix_s28_d09_pan","address": "/modular-fm/MM_Matrix_s28_d09_pan","index": 262636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d00_pitch","varname": "fHslider442","shortname": "MM_Matrix_s29_d00_pitch","address": "/modular-fm/MM_Matrix_s29_d00_pitch","index": 264232,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d01_op1_level","varname": "fHslider492","shortname": "MM_Matrix_s29_d01_op1_level","address": "/modular-fm/MM_Matrix_s29_d01_op1_level","index": 264440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d02_op2_level","varname": "fHslider546","shortname": "MM_Matrix_s29_d02_op2_level","address": "/modular-fm/MM_Matrix_s29_d02_op2_level","index": 264672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d03_op3_level","varname": "fHslider600","shortname": "MM_Matrix_s29_d03_op3_level","address": "/modular-fm/MM_Matrix_s29_d03_op3_level","index": 264904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d04_op4_level","varname": "fHslider654","shortname": "MM_Matrix_s29_d04_op4_level","address": "/modular-fm/MM_Matrix_s29_d04_op4_level","index": 265136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d05_cross_mod_global","varname": "fHslider345","shortname": "MM_Matrix_s29_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s29_d05_cross_mod_global","index": 263844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d06_feedback_global","varname": "fHslider293","shortname": "MM_Matrix_s29_d06_feedback_global","address": "/modular-fm/MM_Matrix_s29_d06_feedback_global","index": 263636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d07_global_ratio","varname": "fHslider393","shortname": "MM_Matrix_s29_d07_global_ratio","address": "/modular-fm/MM_Matrix_s29_d07_global_ratio","index": 264036,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d08_amp","varname": "fHslider244","shortname": "MM_Matrix_s29_d08_amp","address": "/modular-fm/MM_Matrix_s29_d08_amp","index": 263440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d09_pan","varname": "fHslider74","shortname": "MM_Matrix_s29_d09_pan","address": "/modular-fm/MM_Matrix_s29_d09_pan","index": 262612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d00_pitch","varname": "fHslider441","shortname": "MM_Matrix_s30_d00_pitch","address": "/modular-fm/MM_Matrix_s30_d00_pitch","index": 264228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d01_op1_level","varname": "fHslider491","shortname": "MM_Matrix_s30_d01_op1_level","address": "/modular-fm/MM_Matrix_s30_d01_op1_level","index": 264436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d02_op2_level","varname": "fHslider545","shortname": "MM_Matrix_s30_d02_op2_level","address": "/modular-fm/MM_Matrix_s30_d02_op2_level","index": 264668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d03_op3_level","varname": "fHslider599","shortname": "MM_Matrix_s30_d03_op3_level","address": "/modular-fm/MM_Matrix_s30_d03_op3_level","index": 264900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d04_op4_level","varname": "fHslider653","shortname": "MM_Matrix_s30_d04_op4_level","address": "/modular-fm/MM_Matrix_s30_d04_op4_level","index": 265132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d05_cross_mod_global","varname": "fHslider344","shortname": "MM_Matrix_s30_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s30_d05_cross_mod_global","index": 263840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d06_feedback_global","varname": "fHslider292","shortname": "MM_Matrix_s30_d06_feedback_global","address": "/modular-fm/MM_Matrix_s30_d06_feedback_global","index": 263632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d07_global_ratio","varname": "fHslider392","shortname": "MM_Matrix_s30_d07_global_ratio","address": "/modular-fm/MM_Matrix_s30_d07_global_ratio","index": 264032,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d08_amp","varname": "fHslider243","shortname": "MM_Matrix_s30_d08_amp","address": "/modular-fm/MM_Matrix_s30_d08_amp","index": 263436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d09_pan","varname": "fHslider70","shortname": "MM_Matrix_s30_d09_pan","address": "/modular-fm/MM_Matrix_s30_d09_pan","index": 262588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d00_pitch","varname": "fHslider440","shortname": "MM_Matrix_s31_d00_pitch","address": "/modular-fm/MM_Matrix_s31_d00_pitch","index": 264224,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d01_op1_level","varname": "fHslider490","shortname": "MM_Matrix_s31_d01_op1_level","address": "/modular-fm/MM_Matrix_s31_d01_op1_level","index": 264432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d02_op2_level","varname": "fHslider544","shortname": "MM_Matrix_s31_d02_op2_level","address": "/modular-fm/MM_Matrix_s31_d02_op2_level","index": 264664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d03_op3_level","varname": "fHslider598","shortname": "MM_Matrix_s31_d03_op3_level","address": "/modular-fm/MM_Matrix_s31_d03_op3_level","index": 264896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d04_op4_level","varname": "fHslider652","shortname": "MM_Matrix_s31_d04_op4_level","address": "/modular-fm/MM_Matrix_s31_d04_op4_level","index": 265128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d05_cross_mod_global","varname": "fHslider343","shortname": "MM_Matrix_s31_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s31_d05_cross_mod_global","index": 263836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d06_feedback_global","varname": "fHslider291","shortname": "MM_Matrix_s31_d06_feedback_global","address": "/modular-fm/MM_Matrix_s31_d06_feedback_global","index": 263628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d07_global_ratio","varname": "fHslider391","shortname": "MM_Matrix_s31_d07_global_ratio","address": "/modular-fm/MM_Matrix_s31_d07_global_ratio","index": 264028,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d08_amp","varname": "fHslider242","shortname": "MM_Matrix_s31_d08_amp","address": "/modular-fm/MM_Matrix_s31_d08_amp","index": 263432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d09_pan","varname": "fHslider66","shortname": "MM_Matrix_s31_d09_pan","address": "/modular-fm/MM_Matrix_s31_d09_pan","index": 262564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d00_pitch","varname": "fHslider439","shortname": "MM_Matrix_s32_d00_pitch","address": "/modular-fm/MM_Matrix_s32_d00_pitch","index": 264220,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d01_op1_level","varname": "fHslider489","shortname": "MM_Matrix_s32_d01_op1_level","address": "/modular-fm/MM_Matrix_s32_d01_op1_level","index": 264428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d02_op2_level","varname": "fHslider543","shortname": "MM_Matrix_s32_d02_op2_level","address": "/modular-fm/MM_Matrix_s32_d02_op2_level","index": 264660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d03_op3_level","varname": "fHslider597","shortname": "MM_Matrix_s32_d03_op3_level","address": "/modular-fm/MM_Matrix_s32_d03_op3_level","index": 264892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d04_op4_level","varname": "fHslider651","shortname": "MM_Matrix_s32_d04_op4_level","address": "/modular-fm/MM_Matrix_s32_d04_op4_level","index": 265124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d05_cross_mod_global","varname": "fHslider342","shortname": "MM_Matrix_s32_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s32_d05_cross_mod_global","index": 263832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d06_feedback_global","varname": "fHslider290","shortname": "MM_Matrix_s32_d06_feedback_global","address": "/modular-fm/MM_Matrix_s32_d06_feedback_global","index": 263624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d07_global_ratio","varname": "fHslider390","shortname": "MM_Matrix_s32_d07_global_ratio","address": "/modular-fm/MM_Matrix_s32_d07_global_ratio","index": 264024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d08_amp","varname": "fHslider241","shortname": "MM_Matrix_s32_d08_amp","address": "/modular-fm/MM_Matrix_s32_d08_amp","index": 263428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d09_pan","varname": "fHslider62","shortname": "MM_Matrix_s32_d09_pan","address": "/modular-fm/MM_Matrix_s32_d09_pan","index": 262540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d00_pitch","varname": "fHslider438","shortname": "MM_Matrix_s33_d00_pitch","address": "/modular-fm/MM_Matrix_s33_d00_pitch","index": 264216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d01_op1_level","varname": "fHslider488","shortname": "MM_Matrix_s33_d01_op1_level","address": "/modular-fm/MM_Matrix_s33_d01_op1_level","index": 264424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d02_op2_level","varname": "fHslider542","shortname": "MM_Matrix_s33_d02_op2_level","address": "/modular-fm/MM_Matrix_s33_d02_op2_level","index": 264656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d03_op3_level","varname": "fHslider596","shortname": "MM_Matrix_s33_d03_op3_level","address": "/modular-fm/MM_Matrix_s33_d03_op3_level","index": 264888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d04_op4_level","varname": "fHslider650","shortname": "MM_Matrix_s33_d04_op4_level","address": "/modular-fm/MM_Matrix_s33_d04_op4_level","index": 265120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d05_cross_mod_global","varname": "fHslider341","shortname": "MM_Matrix_s33_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s33_d05_cross_mod_global","index": 263828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d06_feedback_global","varname": "fHslider289","shortname": "MM_Matrix_s33_d06_feedback_global","address": "/modular-fm/MM_Matrix_s33_d06_feedback_global","index": 263620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d07_global_ratio","varname": "fHslider389","shortname": "MM_Matrix_s33_d07_global_ratio","address": "/modular-fm/MM_Matrix_s33_d07_global_ratio","index": 264020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d08_amp","varname": "fHslider240","shortname": "MM_Matrix_s33_d08_amp","address": "/modular-fm/MM_Matrix_s33_d08_amp","index": 263424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d09_pan","varname": "fHslider58","shortname": "MM_Matrix_s33_d09_pan","address": "/modular-fm/MM_Matrix_s33_d09_pan","index": 262516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d00_pitch","varname": "fHslider437","shortname": "MM_Matrix_s34_d00_pitch","address": "/modular-fm/MM_Matrix_s34_d00_pitch","index": 264212,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d01_op1_level","varname": "fHslider487","shortname": "MM_Matrix_s34_d01_op1_level","address": "/modular-fm/MM_Matrix_s34_d01_op1_level","index": 264420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d02_op2_level","varname": "fHslider541","shortname": "MM_Matrix_s34_d02_op2_level","address": "/modular-fm/MM_Matrix_s34_d02_op2_level","index": 264652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d03_op3_level","varname": "fHslider595","shortname": "MM_Matrix_s34_d03_op3_level","address": "/modular-fm/MM_Matrix_s34_d03_op3_level","index": 264884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d04_op4_level","varname": "fHslider649","shortname": "MM_Matrix_s34_d04_op4_level","address": "/modular-fm/MM_Matrix_s34_d04_op4_level","index": 265116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d05_cross_mod_global","varname": "fHslider340","shortname": "MM_Matrix_s34_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s34_d05_cross_mod_global","index": 263824,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d06_feedback_global","varname": "fHslider288","shortname": "MM_Matrix_s34_d06_feedback_global","address": "/modular-fm/MM_Matrix_s34_d06_feedback_global","index": 263616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d07_global_ratio","varname": "fHslider388","shortname": "MM_Matrix_s34_d07_global_ratio","address": "/modular-fm/MM_Matrix_s34_d07_global_ratio","index": 264016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d08_amp","varname": "fHslider239","shortname": "MM_Matrix_s34_d08_amp","address": "/modular-fm/MM_Matrix_s34_d08_amp","index": 263420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d09_pan","varname": "fHslider54","shortname": "MM_Matrix_s34_d09_pan","address": "/modular-fm/MM_Matrix_s34_d09_pan","index": 262492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d00_pitch","varname": "fHslider436","shortname": "MM_Matrix_s35_d00_pitch","address": "/modular-fm/MM_Matrix_s35_d00_pitch","index": 264208,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d01_op1_level","varname": "fHslider486","shortname": "MM_Matrix_s35_d01_op1_level","address": "/modular-fm/MM_Matrix_s35_d01_op1_level","index": 264416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d02_op2_level","varname": "fHslider540","shortname": "MM_Matrix_s35_d02_op2_level","address": "/modular-fm/MM_Matrix_s35_d02_op2_level","index": 264648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d03_op3_level","varname": "fHslider594","shortname": "MM_Matrix_s35_d03_op3_level","address": "/modular-fm/MM_Matrix_s35_d03_op3_level","index": 264880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d04_op4_level","varname": "fHslider648","shortname": "MM_Matrix_s35_d04_op4_level","address": "/modular-fm/MM_Matrix_s35_d04_op4_level","index": 265112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d05_cross_mod_global","varname": "fHslider339","shortname": "MM_Matrix_s35_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s35_d05_cross_mod_global","index": 263820,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d06_feedback_global","varname": "fHslider287","shortname": "MM_Matrix_s35_d06_feedback_global","address": "/modular-fm/MM_Matrix_s35_d06_feedback_global","index": 263612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d07_global_ratio","varname": "fHslider387","shortname": "MM_Matrix_s35_d07_global_ratio","address": "/modular-fm/MM_Matrix_s35_d07_global_ratio","index": 264012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d08_amp","varname": "fHslider238","shortname": "MM_Matrix_s35_d08_amp","address": "/modular-fm/MM_Matrix_s35_d08_amp","index": 263416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d09_pan","varname": "fHslider50","shortname": "MM_Matrix_s35_d09_pan","address": "/modular-fm/MM_Matrix_s35_d09_pan","index": 262468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d00_pitch","varname": "fHslider435","shortname": "MM_Matrix_s36_d00_pitch","address": "/modular-fm/MM_Matrix_s36_d00_pitch","index": 264204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d01_op1_level","varname": "fHslider485","shortname": "MM_Matrix_s36_d01_op1_level","address": "/modular-fm/MM_Matrix_s36_d01_op1_level","index": 264412,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d02_op2_level","varname": "fHslider539","shortname": "MM_Matrix_s36_d02_op2_level","address": "/modular-fm/MM_Matrix_s36_d02_op2_level","index": 264644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d03_op3_level","varname": "fHslider593","shortname": "MM_Matrix_s36_d03_op3_level","address": "/modular-fm/MM_Matrix_s36_d03_op3_level","index": 264876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d04_op4_level","varname": "fHslider647","shortname": "MM_Matrix_s36_d04_op4_level","address": "/modular-fm/MM_Matrix_s36_d04_op4_level","index": 265108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d05_cross_mod_global","varname": "fHslider338","shortname": "MM_Matrix_s36_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s36_d05_cross_mod_global","index": 263816,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d06_feedback_global","varname": "fHslider286","shortname": "MM_Matrix_s36_d06_feedback_global","address": "/modular-fm/MM_Matrix_s36_d06_feedback_global","index": 263608,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d07_global_ratio","varname": "fHslider386","shortname": "MM_Matrix_s36_d07_global_ratio","address": "/modular-fm/MM_Matrix_s36_d07_global_ratio","index": 264008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d08_amp","varname": "fHslider237","shortname": "MM_Matrix_s36_d08_amp","address": "/modular-fm/MM_Matrix_s36_d08_amp","index": 263412,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d09_pan","varname": "fHslider46","shortname": "MM_Matrix_s36_d09_pan","address": "/modular-fm/MM_Matrix_s36_d09_pan","index": 262444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d00_pitch","varname": "fHslider434","shortname": "MM_Matrix_s37_d00_pitch","address": "/modular-fm/MM_Matrix_s37_d00_pitch","index": 264200,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d01_op1_level","varname": "fHslider484","shortname": "MM_Matrix_s37_d01_op1_level","address": "/modular-fm/MM_Matrix_s37_d01_op1_level","index": 264408,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d02_op2_level","varname": "fHslider538","shortname": "MM_Matrix_s37_d02_op2_level","address": "/modular-fm/MM_Matrix_s37_d02_op2_level","index": 264640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d03_op3_level","varname": "fHslider592","shortname": "MM_Matrix_s37_d03_op3_level","address": "/modular-fm/MM_Matrix_s37_d03_op3_level","index": 264872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d04_op4_level","varname": "fHslider646","shortname": "MM_Matrix_s37_d04_op4_level","address": "/modular-fm/MM_Matrix_s37_d04_op4_level","index": 265104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d05_cross_mod_global","varname": "fHslider337","shortname": "MM_Matrix_s37_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s37_d05_cross_mod_global","index": 263812,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d06_feedback_global","varname": "fHslider285","shortname": "MM_Matrix_s37_d06_feedback_global","address": "/modular-fm/MM_Matrix_s37_d06_feedback_global","index": 263604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d07_global_ratio","varname": "fHslider385","shortname": "MM_Matrix_s37_d07_global_ratio","address": "/modular-fm/MM_Matrix_s37_d07_global_ratio","index": 264004,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d08_amp","varname": "fHslider236","shortname": "MM_Matrix_s37_d08_amp","address": "/modular-fm/MM_Matrix_s37_d08_amp","index": 263408,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d09_pan","varname": "fHslider42","shortname": "MM_Matrix_s37_d09_pan","address": "/modular-fm/MM_Matrix_s37_d09_pan","index": 262420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d00_pitch","varname": "fHslider433","shortname": "MM_Matrix_s38_d00_pitch","address": "/modular-fm/MM_Matrix_s38_d00_pitch","index": 264196,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d01_op1_level","varname": "fHslider483","shortname": "MM_Matrix_s38_d01_op1_level","address": "/modular-fm/MM_Matrix_s38_d01_op1_level","index": 264404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d02_op2_level","varname": "fHslider537","shortname": "MM_Matrix_s38_d02_op2_level","address": "/modular-fm/MM_Matrix_s38_d02_op2_level","index": 264636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d03_op3_level","varname": "fHslider591","shortname": "MM_Matrix_s38_d03_op3_level","address": "/modular-fm/MM_Matrix_s38_d03_op3_level","index": 264868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d04_op4_level","varname": "fHslider645","shortname": "MM_Matrix_s38_d04_op4_level","address": "/modular-fm/MM_Matrix_s38_d04_op4_level","index": 265100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d05_cross_mod_global","varname": "fHslider336","shortname": "MM_Matrix_s38_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s38_d05_cross_mod_global","index": 263808,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d06_feedback_global","varname": "fHslider284","shortname": "MM_Matrix_s38_d06_feedback_global","address": "/modular-fm/MM_Matrix_s38_d06_feedback_global","index": 263600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d07_global_ratio","varname": "fHslider384","shortname": "MM_Matrix_s38_d07_global_ratio","address": "/modular-fm/MM_Matrix_s38_d07_global_ratio","index": 264000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d08_amp","varname": "fHslider235","shortname": "MM_Matrix_s38_d08_amp","address": "/modular-fm/MM_Matrix_s38_d08_amp","index": 263404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d09_pan","varname": "fHslider38","shortname": "MM_Matrix_s38_d09_pan","address": "/modular-fm/MM_Matrix_s38_d09_pan","index": 262396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d00_pitch","varname": "fHslider432","shortname": "MM_Matrix_s39_d00_pitch","address": "/modular-fm/MM_Matrix_s39_d00_pitch","index": 264192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d01_op1_level","varname": "fHslider482","shortname": "MM_Matrix_s39_d01_op1_level","address": "/modular-fm/MM_Matrix_s39_d01_op1_level","index": 264400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d02_op2_level","varname": "fHslider536","shortname": "MM_Matrix_s39_d02_op2_level","address": "/modular-fm/MM_Matrix_s39_d02_op2_level","index": 264632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d03_op3_level","varname": "fHslider590","shortname": "MM_Matrix_s39_d03_op3_level","address": "/modular-fm/MM_Matrix_s39_d03_op3_level","index": 264864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d04_op4_level","varname": "fHslider644","shortname": "MM_Matrix_s39_d04_op4_level","address": "/modular-fm/MM_Matrix_s39_d04_op4_level","index": 265096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d05_cross_mod_global","varname": "fHslider335","shortname": "MM_Matrix_s39_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s39_d05_cross_mod_global","index": 263804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d06_feedback_global","varname": "fHslider283","shortname": "MM_Matrix_s39_d06_feedback_global","address": "/modular-fm/MM_Matrix_s39_d06_feedback_global","index": 263596,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d07_global_ratio","varname": "fHslider383","shortname": "MM_Matrix_s39_d07_global_ratio","address": "/modular-fm/MM_Matrix_s39_d07_global_ratio","index": 263996,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d08_amp","varname": "fHslider234","shortname": "MM_Matrix_s39_d08_amp","address": "/modular-fm/MM_Matrix_s39_d08_amp","index": 263400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d09_pan","varname": "fHslider34","shortname": "MM_Matrix_s39_d09_pan","address": "/modular-fm/MM_Matrix_s39_d09_pan","index": 262372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d00_pitch","varname": "fHslider431","shortname": "MM_Matrix_s40_d00_pitch","address": "/modular-fm/MM_Matrix_s40_d00_pitch","index": 264188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d01_op1_level","varname": "fHslider481","shortname": "MM_Matrix_s40_d01_op1_level","address": "/modular-fm/MM_Matrix_s40_d01_op1_level","index": 264396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d02_op2_level","varname": "fHslider535","shortname": "MM_Matrix_s40_d02_op2_level","address": "/modular-fm/MM_Matrix_s40_d02_op2_level","index": 264628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d03_op3_level","varname": "fHslider589","shortname": "MM_Matrix_s40_d03_op3_level","address": "/modular-fm/MM_Matrix_s40_d03_op3_level","index": 264860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d04_op4_level","varname": "fHslider643","shortname": "MM_Matrix_s40_d04_op4_level","address": "/modular-fm/MM_Matrix_s40_d04_op4_level","index": 265092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d05_cross_mod_global","varname": "fHslider334","shortname": "MM_Matrix_s40_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s40_d05_cross_mod_global","index": 263800,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d06_feedback_global","varname": "fHslider282","shortname": "MM_Matrix_s40_d06_feedback_global","address": "/modular-fm/MM_Matrix_s40_d06_feedback_global","index": 263592,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d07_global_ratio","varname": "fHslider382","shortname": "MM_Matrix_s40_d07_global_ratio","address": "/modular-fm/MM_Matrix_s40_d07_global_ratio","index": 263992,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d08_amp","varname": "fHslider233","shortname": "MM_Matrix_s40_d08_amp","address": "/modular-fm/MM_Matrix_s40_d08_amp","index": 263396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d09_pan","varname": "fHslider30","shortname": "MM_Matrix_s40_d09_pan","address": "/modular-fm/MM_Matrix_s40_d09_pan","index": 262348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d00_pitch","varname": "fHslider430","shortname": "MM_Matrix_s41_d00_pitch","address": "/modular-fm/MM_Matrix_s41_d00_pitch","index": 264184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d01_op1_level","varname": "fHslider480","shortname": "MM_Matrix_s41_d01_op1_level","address": "/modular-fm/MM_Matrix_s41_d01_op1_level","index": 264392,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d02_op2_level","varname": "fHslider534","shortname": "MM_Matrix_s41_d02_op2_level","address": "/modular-fm/MM_Matrix_s41_d02_op2_level","index": 264624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d03_op3_level","varname": "fHslider588","shortname": "MM_Matrix_s41_d03_op3_level","address": "/modular-fm/MM_Matrix_s41_d03_op3_level","index": 264856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d04_op4_level","varname": "fHslider642","shortname": "MM_Matrix_s41_d04_op4_level","address": "/modular-fm/MM_Matrix_s41_d04_op4_level","index": 265088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d05_cross_mod_global","varname": "fHslider333","shortname": "MM_Matrix_s41_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s41_d05_cross_mod_global","index": 263796,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d06_feedback_global","varname": "fHslider281","shortname": "MM_Matrix_s41_d06_feedback_global","address": "/modular-fm/MM_Matrix_s41_d06_feedback_global","index": 263588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d07_global_ratio","varname": "fHslider381","shortname": "MM_Matrix_s41_d07_global_ratio","address": "/modular-fm/MM_Matrix_s41_d07_global_ratio","index": 263988,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d08_amp","varname": "fHslider232","shortname": "MM_Matrix_s41_d08_amp","address": "/modular-fm/MM_Matrix_s41_d08_amp","index": 263392,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d09_pan","varname": "fHslider26","shortname": "MM_Matrix_s41_d09_pan","address": "/modular-fm/MM_Matrix_s41_d09_pan","index": 262324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d00_pitch","varname": "fHslider429","shortname": "MM_Matrix_s42_d00_pitch","address": "/modular-fm/MM_Matrix_s42_d00_pitch","index": 264180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d01_op1_level","varname": "fHslider479","shortname": "MM_Matrix_s42_d01_op1_level","address": "/modular-fm/MM_Matrix_s42_d01_op1_level","index": 264388,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d02_op2_level","varname": "fHslider533","shortname": "MM_Matrix_s42_d02_op2_level","address": "/modular-fm/MM_Matrix_s42_d02_op2_level","index": 264620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d03_op3_level","varname": "fHslider587","shortname": "MM_Matrix_s42_d03_op3_level","address": "/modular-fm/MM_Matrix_s42_d03_op3_level","index": 264852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d04_op4_level","varname": "fHslider641","shortname": "MM_Matrix_s42_d04_op4_level","address": "/modular-fm/MM_Matrix_s42_d04_op4_level","index": 265084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d05_cross_mod_global","varname": "fHslider332","shortname": "MM_Matrix_s42_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s42_d05_cross_mod_global","index": 263792,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d06_feedback_global","varname": "fHslider280","shortname": "MM_Matrix_s42_d06_feedback_global","address": "/modular-fm/MM_Matrix_s42_d06_feedback_global","index": 263584,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d07_global_ratio","varname": "fHslider380","shortname": "MM_Matrix_s42_d07_global_ratio","address": "/modular-fm/MM_Matrix_s42_d07_global_ratio","index": 263984,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d08_amp","varname": "fHslider231","shortname": "MM_Matrix_s42_d08_amp","address": "/modular-fm/MM_Matrix_s42_d08_amp","index": 263388,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d09_pan","varname": "fHslider22","shortname": "MM_Matrix_s42_d09_pan","address": "/modular-fm/MM_Matrix_s42_d09_pan","index": 262300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d00_pitch","varname": "fHslider428","shortname": "MM_Matrix_s43_d00_pitch","address": "/modular-fm/MM_Matrix_s43_d00_pitch","index": 264176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d01_op1_level","varname": "fHslider478","shortname": "MM_Matrix_s43_d01_op1_level","address": "/modular-fm/MM_Matrix_s43_d01_op1_level","index": 264384,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d02_op2_level","varname": "fHslider532","shortname": "MM_Matrix_s43_d02_op2_level","address": "/modular-fm/MM_Matrix_s43_d02_op2_level","index": 264616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d03_op3_level","varname": "fHslider586","shortname": "MM_Matrix_s43_d03_op3_level","address": "/modular-fm/MM_Matrix_s43_d03_op3_level","index": 264848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d04_op4_level","varname": "fHslider640","shortname": "MM_Matrix_s43_d04_op4_level","address": "/modular-fm/MM_Matrix_s43_d04_op4_level","index": 265080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d05_cross_mod_global","varname": "fHslider331","shortname": "MM_Matrix_s43_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s43_d05_cross_mod_global","index": 263788,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d06_feedback_global","varname": "fHslider279","shortname": "MM_Matrix_s43_d06_feedback_global","address": "/modular-fm/MM_Matrix_s43_d06_feedback_global","index": 263580,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d07_global_ratio","varname": "fHslider379","shortname": "MM_Matrix_s43_d07_global_ratio","address": "/modular-fm/MM_Matrix_s43_d07_global_ratio","index": 263980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d08_amp","varname": "fHslider230","shortname": "MM_Matrix_s43_d08_amp","address": "/modular-fm/MM_Matrix_s43_d08_amp","index": 263384,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d09_pan","varname": "fHslider18","shortname": "MM_Matrix_s43_d09_pan","address": "/modular-fm/MM_Matrix_s43_d09_pan","index": 262276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d00_pitch","varname": "fHslider427","shortname": "MM_Matrix_s44_d00_pitch","address": "/modular-fm/MM_Matrix_s44_d00_pitch","index": 264172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d01_op1_level","varname": "fHslider477","shortname": "MM_Matrix_s44_d01_op1_level","address": "/modular-fm/MM_Matrix_s44_d01_op1_level","index": 264380,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d02_op2_level","varname": "fHslider531","shortname": "MM_Matrix_s44_d02_op2_level","address": "/modular-fm/MM_Matrix_s44_d02_op2_level","index": 264612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d03_op3_level","varname": "fHslider585","shortname": "MM_Matrix_s44_d03_op3_level","address": "/modular-fm/MM_Matrix_s44_d03_op3_level","index": 264844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d04_op4_level","varname": "fHslider639","shortname": "MM_Matrix_s44_d04_op4_level","address": "/modular-fm/MM_Matrix_s44_d04_op4_level","index": 265076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d05_cross_mod_global","varname": "fHslider330","shortname": "MM_Matrix_s44_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s44_d05_cross_mod_global","index": 263784,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d06_feedback_global","varname": "fHslider278","shortname": "MM_Matrix_s44_d06_feedback_global","address": "/modular-fm/MM_Matrix_s44_d06_feedback_global","index": 263576,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d07_global_ratio","varname": "fHslider378","shortname": "MM_Matrix_s44_d07_global_ratio","address": "/modular-fm/MM_Matrix_s44_d07_global_ratio","index": 263976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d08_amp","varname": "fHslider229","shortname": "MM_Matrix_s44_d08_amp","address": "/modular-fm/MM_Matrix_s44_d08_amp","index": 263380,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d09_pan","varname": "fHslider14","shortname": "MM_Matrix_s44_d09_pan","address": "/modular-fm/MM_Matrix_s44_d09_pan","index": 262252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d00_pitch","varname": "fHslider426","shortname": "MM_Matrix_s45_d00_pitch","address": "/modular-fm/MM_Matrix_s45_d00_pitch","index": 264168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d01_op1_level","varname": "fHslider476","shortname": "MM_Matrix_s45_d01_op1_level","address": "/modular-fm/MM_Matrix_s45_d01_op1_level","index": 264376,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d02_op2_level","varname": "fHslider530","shortname": "MM_Matrix_s45_d02_op2_level","address": "/modular-fm/MM_Matrix_s45_d02_op2_level","index": 264608,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d03_op3_level","varname": "fHslider584","shortname": "MM_Matrix_s45_d03_op3_level","address": "/modular-fm/MM_Matrix_s45_d03_op3_level","index": 264840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d04_op4_level","varname": "fHslider638","shortname": "MM_Matrix_s45_d04_op4_level","address": "/modular-fm/MM_Matrix_s45_d04_op4_level","index": 265072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d05_cross_mod_global","varname": "fHslider329","shortname": "MM_Matrix_s45_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s45_d05_cross_mod_global","index": 263780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d06_feedback_global","varname": "fHslider277","shortname": "MM_Matrix_s45_d06_feedback_global","address": "/modular-fm/MM_Matrix_s45_d06_feedback_global","index": 263572,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d07_global_ratio","varname": "fHslider377","shortname": "MM_Matrix_s45_d07_global_ratio","address": "/modular-fm/MM_Matrix_s45_d07_global_ratio","index": 263972,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d08_amp","varname": "fHslider228","shortname": "MM_Matrix_s45_d08_amp","address": "/modular-fm/MM_Matrix_s45_d08_amp","index": 263376,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d09_pan","varname": "fHslider10","shortname": "MM_Matrix_s45_d09_pan","address": "/modular-fm/MM_Matrix_s45_d09_pan","index": 262228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d00_pitch","varname": "fHslider425","shortname": "MM_Matrix_s46_d00_pitch","address": "/modular-fm/MM_Matrix_s46_d00_pitch","index": 264164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d01_op1_level","varname": "fHslider475","shortname": "MM_Matrix_s46_d01_op1_level","address": "/modular-fm/MM_Matrix_s46_d01_op1_level","index": 264372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d02_op2_level","varname": "fHslider529","shortname": "MM_Matrix_s46_d02_op2_level","address": "/modular-fm/MM_Matrix_s46_d02_op2_level","index": 264604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d03_op3_level","varname": "fHslider583","shortname": "MM_Matrix_s46_d03_op3_level","address": "/modular-fm/MM_Matrix_s46_d03_op3_level","index": 264836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d04_op4_level","varname": "fHslider637","shortname": "MM_Matrix_s46_d04_op4_level","address": "/modular-fm/MM_Matrix_s46_d04_op4_level","index": 265068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d05_cross_mod_global","varname": "fHslider328","shortname": "MM_Matrix_s46_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s46_d05_cross_mod_global","index": 263776,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d06_feedback_global","varname": "fHslider276","shortname": "MM_Matrix_s46_d06_feedback_global","address": "/modular-fm/MM_Matrix_s46_d06_feedback_global","index": 263568,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d07_global_ratio","varname": "fHslider376","shortname": "MM_Matrix_s46_d07_global_ratio","address": "/modular-fm/MM_Matrix_s46_d07_global_ratio","index": 263968,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d08_amp","varname": "fHslider227","shortname": "MM_Matrix_s46_d08_amp","address": "/modular-fm/MM_Matrix_s46_d08_amp","index": 263372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d09_pan","varname": "fHslider6","shortname": "MM_Matrix_s46_d09_pan","address": "/modular-fm/MM_Matrix_s46_d09_pan","index": 262204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d00_pitch","varname": "fHslider424","shortname": "MM_Matrix_s47_d00_pitch","address": "/modular-fm/MM_Matrix_s47_d00_pitch","index": 264160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d01_op1_level","varname": "fHslider474","shortname": "MM_Matrix_s47_d01_op1_level","address": "/modular-fm/MM_Matrix_s47_d01_op1_level","index": 264368,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d02_op2_level","varname": "fHslider528","shortname": "MM_Matrix_s47_d02_op2_level","address": "/modular-fm/MM_Matrix_s47_d02_op2_level","index": 264600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d03_op3_level","varname": "fHslider582","shortname": "MM_Matrix_s47_d03_op3_level","address": "/modular-fm/MM_Matrix_s47_d03_op3_level","index": 264832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d04_op4_level","varname": "fHslider636","shortname": "MM_Matrix_s47_d04_op4_level","address": "/modular-fm/MM_Matrix_s47_d04_op4_level","index": 265064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d05_cross_mod_global","varname": "fHslider327","shortname": "MM_Matrix_s47_d05_cross_mod_global","address": "/modular-fm/MM_Matrix_s47_d05_cross_mod_global","index": 263772,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d06_feedback_global","varname": "fHslider275","shortname": "MM_Matrix_s47_d06_feedback_global","address": "/modular-fm/MM_Matrix_s47_d06_feedback_global","index": 263564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d07_global_ratio","varname": "fHslider375","shortname": "MM_Matrix_s47_d07_global_ratio","address": "/modular-fm/MM_Matrix_s47_d07_global_ratio","index": 263964,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d08_amp","varname": "fHslider226","shortname": "MM_Matrix_s47_d08_amp","address": "/modular-fm/MM_Matrix_s47_d08_amp","index": 263368,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d09_pan","varname": "fHslider2","shortname": "MM_Matrix_s47_d09_pan","address": "/modular-fm/MM_Matrix_s47_d09_pan","index": 262180,"init": 0,"min": -1,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/modular-fm.wasm b/playground/faust/modular-fm.wasm deleted file mode 100644 index 6dcacb107b63bc96b75e75e919fb9ae26c3be8c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 313094 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
  • FCF8Kk-X#*x&_~a+ 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 i@c^!BH{}u?1eXh7PF9KL9`|TX;la57_^? diff --git a/playground/faust/modular-subtractive-processor.js b/playground/faust/modular-subtractive-processor.js deleted file mode 100644 index 69027b6..0000000 --- a/playground/faust/modular-subtractive-processor.js +++ /dev/null @@ -1,261 +0,0 @@ -/** - * modular-subtractive-processor.js — AudioWorklet processor for the - * "Modular" mode subtractive voice (modular-subtractive.dsp). - * - * Extends FaustWorkletProcessor (faust-worklet-processor.js). - * Loads modular-subtractive.wasm compiled by `faust -lang wasm`. - * - * Param-index strategy: - * modular-subtractive.dsp has ~681 parameters (22 engine + 80 ADSR + 96 LFO - * + 480 matrix + 3 hidden). Hand-maintained zone tables (additive-processor - * style) do not scale. Instead we use the wasm-native JSON descriptor - * produced by `faust -lang wasm`, which has a numeric `index` field on each - * leaf item. That index is the direct memory offset that setParamValue - * expects as its zone argument — no sentinel scanning, no address strings. - * - * The main thread fetches modular-subtractive.json, parses it, and passes - * the UI tree to the worklet via the init message. - * - * Calling convention (faust -lang wasm, single-instance): - * init(dsp, sampleRate) dsp is always 0 - * compute(dsp, n, inputs, outputs) - * setParamValue(dsp, zone, value) zone = numeric memory address from JSON - * getParamValue(dsp, zone) -> float - * - * Hidden params (not in the engine's ML-controllable list): - * 0_Hidden/freq — set by noteOn - * 0_Hidden/gate — set by noteOn/noteOff (button, value 0/1) - * 0_Hidden/_vel — set by noteOn - */ - -// Must be imported in AudioWorkletGlobalScope — include faust-worklet-processor.js -// via audioCtx.audioWorklet.addModule() before this file. - -const DSP = 0; -const BLOCK_SIZE = 128; - -class ModularSubtractiveProcessor extends FaustWorkletProcessor { - constructor(options) { - super(options); - this._dspInst = null; - this._dspMemory = null; // live Float32Array view - this._sampleRate = 48000; - - // Zone tables, populated once the init message has been processed. - this._paramZones = []; // NISPS index (order from JSON walk) -> zone - this._paramZonesByLabel = {}; // label -> zone (for smoke test) - this._hiddenZones = {}; // {freq, gate, _vel} -> zone - - // Audio output buffer pointers - this._outPtrsAddr = 0; - this._outLAddr = 0; - this._outRAddr = 0; - } - - // --------------------------------------------------------------------------- - // _initWasm — called by FaustWorkletProcessor.handleMessage on 'init'. - // - // Extra fields on the init message: - // uiJson — parsed Faust UI descriptor (the .json file contents) for - // zone discovery. Faust's -lang wasm embeds numeric `index` - // fields that we use directly as zone memory addresses. - // --------------------------------------------------------------------------- - - async _initWasm(wasmBytes, sampleRate, uiJson) { - this._sampleRate = sampleRate; - - const importObj = { - env: { - _sinf: Math.sin, - _cosf: Math.cos, - _tanf: Math.tan, - _expf: Math.exp, - _logf: Math.log, - _log10f: Math.log10, - _powf: Math.pow, - _tanhf: Math.tanh, - _sqrtf: Math.sqrt, - _fabsf: Math.abs, - _floorf: Math.floor, - _ceilf: Math.ceil, - _remainderf: (a, b) => a - Math.round(a / b) * b, - _fmodf: (a, b) => a % b, - _roundf: Math.round, - _truncf: Math.trunc, - _acosf: Math.acos, - _asinf: Math.asin, - _atanf: Math.atan, - _atan2f: Math.atan2, - }, - }; - - const result = await WebAssembly.instantiate(wasmBytes, importObj); - this._dspInst = result.instance; - const ex = this._dspInst.exports; - - // Grow memory by 2 pages (128 KB) so the output buffer region sits past - // Faust's allocated parameter zones. The parameter zone region in - // modular-subtractive lives around bytes 262144..265280; after growth we - // have at least 3 * BLOCK_SIZE * 4 bytes at the high end. - ex.memory.grow(2); - - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Initialise the DSP instance (single-instance convention: dsp = 0) - ex.init(DSP, sampleRate); - - // Allocate output buffers at the top of WASM memory. - const memBytes = ex.memory.buffer.byteLength; - this._outLAddr = memBytes - BLOCK_SIZE * 4 * 3; - this._outRAddr = this._outLAddr + BLOCK_SIZE * 4; - this._outPtrsAddr = this._outRAddr + BLOCK_SIZE * 4; - - const u32 = new Uint32Array(ex.memory.buffer); - u32[this._outPtrsAddr / 4] = this._outLAddr; - u32[this._outPtrsAddr / 4 + 1] = this._outRAddr; - - // Refresh the float view after any potential reallocation - this._dspMemory = new Float32Array(ex.memory.buffer); - - // Build the zone index from the JSON we were passed. - if (uiJson) { - this._buildZoneIndexFromJson(uiJson); - } else { - console.warn('[ModularSubtractiveProcessor] no uiJson in init message — ' + - 'setParam will silently no-op.'); - } - } - - // ------------------------------------------------------------------------- - // _buildZoneIndexFromJson — walk the Faust UI tree and collect numeric - // zone indexes, separating hidden params from the main NISPS-controllable - // list. Preserves the UI tree's traversal order, which matches the order - // the playground's faustJsonToParamMeta() parser will produce. - // ------------------------------------------------------------------------- - - _buildZoneIndexFromJson(faustJson) { - const HIDDEN_TAIL = new Set(['freq', 'gate', '_vel']); - const zones = []; - const byLabel = {}; - const hidden = {}; - - const walk = (items) => { - if (!Array.isArray(items)) return; - for (const item of items) { - const type = item?.type; - if (type === 'hslider' || type === 'vslider' || - type === 'nentry' || type === 'button' || - type === 'checkbox') { - const label = item.label ?? ''; - const idx = item.index; - if (typeof idx !== 'number') continue; - - const hasHiddenMeta = Array.isArray(item.meta) && - item.meta.some(m => m.hidden === '1' || m.hidden === 1); - const tail = label.includes('/') ? label.split('/').pop() : label; - - if (hasHiddenMeta || HIDDEN_TAIL.has(tail)) { - hidden[tail] = idx; - } else { - zones.push(idx); - byLabel[label] = idx; - } - } else if (item?.items) { - walk(item.items); - } - } - }; - - walk(faustJson.ui ?? []); - - this._paramZones = zones; - this._paramZonesByLabel = byLabel; - this._hiddenZones = hidden; - } - - // ------------------------------------------------------------------------- - // _onSetParam — look up the zone by NISPS index and poke the DSP - // ------------------------------------------------------------------------- - - _onSetParam(index, value) { - if (!this._dspInst) return; - const zone = this._paramZones[index]; - if (zone === undefined) return; - this._dspInst.exports.setParamValue(DSP, zone, value); - } - - _onNoteOn(freq, vel) { - if (!this._dspInst) return; - const ex = this._dspInst.exports; - const z = this._hiddenZones; - if (z.freq !== undefined) ex.setParamValue(DSP, z.freq, freq); - if (z._vel !== undefined) ex.setParamValue(DSP, z._vel, vel ?? 0.7); - if (z.gate !== undefined) ex.setParamValue(DSP, z.gate, 1.0); - } - - _onNoteOff(_freq) { - if (!this._dspInst) return; - const z = this._hiddenZones; - if (z.gate !== undefined) { - this._dspInst.exports.setParamValue(DSP, z.gate, 0.0); - } - } - - // ------------------------------------------------------------------------- - // Extended message handling — override for setByLabel + init-with-json - // ------------------------------------------------------------------------- - - _handleMessage(msg) { - if (!msg || !msg.type) return; - - if (msg.type === 'init') { - // Override the base class's init path so we can thread `uiJson` through. - this._initWasm(msg.wasmBytes, msg.sampleRate || sampleRate, msg.uiJson) - .then(() => { - this._ready = true; - this.port.postMessage({ type: 'ready' }); - this._drainPendingMessages(); - }) - .catch((err) => { - this.port.postMessage({ type: 'error', message: String(err) }); - }); - return; - } - - // Buffer anything that arrives before init() finishes, so the main - // thread can disable slots / push matrix values eagerly without worrying - // about worklet readiness. See faust-worklet-processor.js. - if (this._queueIfNotReady(msg)) return; - - if (msg.type === 'setByLabel') { - const zone = this._paramZonesByLabel[msg.label]; - if (zone !== undefined) { - this._dspInst.exports.setParamValue(DSP, zone, msg.value); - } - return; - } - - // Delegate everything else (setParam, noteOn, noteOff) to the base class. - // Note: the base class also calls _queueIfNotReady, but since we already - // passed it above, we won't re-queue here. - super._handleMessage(msg); - } - - // ------------------------------------------------------------------------- - // _renderBlock — call Faust compute, copy output buffers - // ------------------------------------------------------------------------- - - _renderBlock(outL, outR, blockSize) { - if (!this._dspInst || !this._dspMemory) return; - - const ex = this._dspInst.exports; - ex.compute(DSP, blockSize, 0, this._outPtrsAddr); - - const wL = new Float32Array(ex.memory.buffer, this._outLAddr, blockSize); - const wR = new Float32Array(ex.memory.buffer, this._outRAddr, blockSize); - outL.set(wL); - outR.set(wR); - } -} - -registerProcessor('modular-subtractive-processor', ModularSubtractiveProcessor); diff --git a/playground/faust/modular-subtractive-smoke.html b/playground/faust/modular-subtractive-smoke.html deleted file mode 100644 index f7b25b8..0000000 --- a/playground/faust/modular-subtractive-smoke.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - - Modular Subtractive — Smoke Test - - - -

    modular-subtractive smoke test

    -

    Phase A gate. Loads the worklet, installs a default patch (ADSR 1 → amp), plays a 1 s note.

    - - - - - -

    - Output level: - 0.000 -

    - -
    - - - - diff --git a/playground/faust/modular-subtractive.dsp b/playground/faust/modular-subtractive.dsp deleted file mode 100644 index 28c0f0c..0000000 --- a/playground/faust/modular-subtractive.dsp +++ /dev/null @@ -1,1201 +0,0 @@ -// modular-subtractive.dsp — Minimoog-style subtractive voice for the "Modular" mode. -// -// AUTO-GENERATED by gen-modular-dsp.py. DO NOT EDIT BY HAND. -// Regenerate with: -// cd playground/faust && python3 gen-modular-dsp.py -// -// Audio path: 3 oscillators + noise → mixer → moog ladder filter → amp/pan. -// All envelopes and LFOs come from mod-pool.lib via a 48-source × 10-dest -// modulation matrix. No internal envs/LFOs. -// -// Modulation destinations (10): -// 0 pitch semitones, applied to all osc frequencies -// 1 osc2_detune cents offset added to osc2 detune knob -// 2 osc3_detune cents offset added to osc3 detune knob -// 3 osc_mix_bal -1..+1 crossfades osc1<->osc3 -// 4 noise_level added to noise level knob -// 5 cutoff +/-5 octaves of filter cutoff modulation -// 6 resonance added to res knob -// 7 filter_env_amt second independent +/-5 octave cutoff mod channel -// 8 amp added to amp knob (route an ADSR here for a VCA env) -// 9 pan added to pan knob -// -// Hidden controls (driven by noteOn/noteOff, not by the ML engine): -// freq, gate, _vel -// -// Build: -// cd playground/faust && ./build.sh -// -// --------------------------------------------------------------------------- - -import("stdfaust.lib"); -mp = library("mod-pool.lib"); - -// --------------------------------------------------------------------------- -// Hidden controls -// --------------------------------------------------------------------------- -freq = hslider("0_Hidden/freq[hidden:1][unit:Hz]", 220, 20, 4000, 0.01); -gate = button("0_Hidden/gate[hidden:1]"); -vel = hslider("0_Hidden/_vel[hidden:1]", 0.7, 0.0, 1.0, 0.001) : si.smoo; - -// --------------------------------------------------------------------------- -// Group 1 — Oscillators -// --------------------------------------------------------------------------- -osc1_wave = hslider("1_Oscillators/00_osc1_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); -osc1_range = hslider("1_Oscillators/01_osc1_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); -osc1_level = hslider("1_Oscillators/02_osc1_level", 0.8, 0.0, 1.0, 0.001); - -osc2_wave = hslider("1_Oscillators/03_osc2_wave[tooltip:0=saw,0.5=tri,1=square]", 0.0, 0.0, 1.0, 0.001); -osc2_range = hslider("1_Oscillators/04_osc2_range[unit:oct]", 0.0, -2.0, 2.0, 0.01); -osc2_detune = hslider("1_Oscillators/05_osc2_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -osc2_level = hslider("1_Oscillators/06_osc2_level", 0.6, 0.0, 1.0, 0.001); - -osc3_wave = hslider("1_Oscillators/07_osc3_wave[tooltip:0=saw,0.5=tri,1=square]", 0.5, 0.0, 1.0, 0.001); -osc3_range = hslider("1_Oscillators/08_osc3_range[unit:oct]", -1.0, -2.0, 2.0, 0.01); -osc3_detune = hslider("1_Oscillators/09_osc3_detune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -osc3_level = hslider("1_Oscillators/10_osc3_level", 0.4, 0.0, 1.0, 0.001); -osc3_kb_track = hslider("1_Oscillators/11_osc3_kb_track[tooltip:1=tracks keyboard,0=LFO]", 1.0, 0.0, 1.0, 1.0); - -// --------------------------------------------------------------------------- -// Group 2 — Mixer -// --------------------------------------------------------------------------- -noise_type = hslider("2_Mixer/00_noise_type[tooltip:0=white,1=pink]", 0.0, 0.0, 1.0, 1.0); -noise_level = hslider("2_Mixer/01_noise_level", 0.0, 0.0, 1.0, 0.001); -mixer_drive = hslider("2_Mixer/02_mixer_drive[tooltip:Pre-filter overdrive]", 1.0, 0.5, 4.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 3 — Filter -// --------------------------------------------------------------------------- -cutoff = hslider("3_Filter/00_cutoff[scale:log][unit:Hz]", 1200.0, 20.0, 20000.0, 0.1); -resonance = hslider("3_Filter/01_resonance", 0.3, 0.0, 1.0, 0.001); -filter_kb = hslider("3_Filter/02_filter_kb_track", 0.5, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Group 4 — Master -// --------------------------------------------------------------------------- -master_level = hslider("4_Master/00_master_level", 0.7, 0.0, 1.0, 0.001); -master_glide = hslider("4_Master/01_master_glide[unit:s][scale:log]", 0.0, 0.0, 2.0, 0.001); -master_tune = hslider("4_Master/02_master_tune[unit:ct]", 0.0, -50.0, 50.0, 0.1); -master_pan = hslider("4_Master/03_master_pan", 0.0, -1.0, 1.0, 0.001); -// Static amp floor. At 1.0 the voice is always fully open and d08_amp -// modulation is purely additive decoration. Drop to 0 for classic -// ADSR-gated VCA behaviour (route s00_d08_amp to an ADSR). -base_amp = hslider("4_Master/04_base_amp", 1.0, 0.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Modulation matrix — 48 sources × 10 destinations. -// Each hslider is one "amount" entry in [-1, +1]. Zero = no connection. -// Group "MM_Matrix" so the paramMeta parser can recognise matrix params. -// --------------------------------------------------------------------------- - -// Matrix column: destination 0 = pitch -amt_s00_d00_pitch = hslider("MM_Matrix/s00_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s01_d00_pitch = hslider("MM_Matrix/s01_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s02_d00_pitch = hslider("MM_Matrix/s02_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s03_d00_pitch = hslider("MM_Matrix/s03_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s04_d00_pitch = hslider("MM_Matrix/s04_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s05_d00_pitch = hslider("MM_Matrix/s05_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s06_d00_pitch = hslider("MM_Matrix/s06_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s07_d00_pitch = hslider("MM_Matrix/s07_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s08_d00_pitch = hslider("MM_Matrix/s08_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s09_d00_pitch = hslider("MM_Matrix/s09_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s10_d00_pitch = hslider("MM_Matrix/s10_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s11_d00_pitch = hslider("MM_Matrix/s11_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s12_d00_pitch = hslider("MM_Matrix/s12_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s13_d00_pitch = hslider("MM_Matrix/s13_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s14_d00_pitch = hslider("MM_Matrix/s14_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s15_d00_pitch = hslider("MM_Matrix/s15_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s16_d00_pitch = hslider("MM_Matrix/s16_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s17_d00_pitch = hslider("MM_Matrix/s17_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s18_d00_pitch = hslider("MM_Matrix/s18_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s19_d00_pitch = hslider("MM_Matrix/s19_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s20_d00_pitch = hslider("MM_Matrix/s20_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s21_d00_pitch = hslider("MM_Matrix/s21_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s22_d00_pitch = hslider("MM_Matrix/s22_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s23_d00_pitch = hslider("MM_Matrix/s23_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s24_d00_pitch = hslider("MM_Matrix/s24_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s25_d00_pitch = hslider("MM_Matrix/s25_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s26_d00_pitch = hslider("MM_Matrix/s26_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s27_d00_pitch = hslider("MM_Matrix/s27_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s28_d00_pitch = hslider("MM_Matrix/s28_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s29_d00_pitch = hslider("MM_Matrix/s29_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s30_d00_pitch = hslider("MM_Matrix/s30_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s31_d00_pitch = hslider("MM_Matrix/s31_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s32_d00_pitch = hslider("MM_Matrix/s32_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s33_d00_pitch = hslider("MM_Matrix/s33_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s34_d00_pitch = hslider("MM_Matrix/s34_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s35_d00_pitch = hslider("MM_Matrix/s35_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s36_d00_pitch = hslider("MM_Matrix/s36_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s37_d00_pitch = hslider("MM_Matrix/s37_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s38_d00_pitch = hslider("MM_Matrix/s38_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s39_d00_pitch = hslider("MM_Matrix/s39_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s40_d00_pitch = hslider("MM_Matrix/s40_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s41_d00_pitch = hslider("MM_Matrix/s41_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s42_d00_pitch = hslider("MM_Matrix/s42_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s43_d00_pitch = hslider("MM_Matrix/s43_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s44_d00_pitch = hslider("MM_Matrix/s44_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s45_d00_pitch = hslider("MM_Matrix/s45_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s46_d00_pitch = hslider("MM_Matrix/s46_d00_pitch", 0.0, -1.0, 1.0, 0.001); -amt_s47_d00_pitch = hslider("MM_Matrix/s47_d00_pitch", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 1 = osc2_detune -amt_s00_d01_osc2_detune = hslider("MM_Matrix/s00_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s01_d01_osc2_detune = hslider("MM_Matrix/s01_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s02_d01_osc2_detune = hslider("MM_Matrix/s02_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s03_d01_osc2_detune = hslider("MM_Matrix/s03_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s04_d01_osc2_detune = hslider("MM_Matrix/s04_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s05_d01_osc2_detune = hslider("MM_Matrix/s05_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s06_d01_osc2_detune = hslider("MM_Matrix/s06_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s07_d01_osc2_detune = hslider("MM_Matrix/s07_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s08_d01_osc2_detune = hslider("MM_Matrix/s08_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s09_d01_osc2_detune = hslider("MM_Matrix/s09_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s10_d01_osc2_detune = hslider("MM_Matrix/s10_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s11_d01_osc2_detune = hslider("MM_Matrix/s11_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s12_d01_osc2_detune = hslider("MM_Matrix/s12_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s13_d01_osc2_detune = hslider("MM_Matrix/s13_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s14_d01_osc2_detune = hslider("MM_Matrix/s14_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s15_d01_osc2_detune = hslider("MM_Matrix/s15_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s16_d01_osc2_detune = hslider("MM_Matrix/s16_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s17_d01_osc2_detune = hslider("MM_Matrix/s17_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s18_d01_osc2_detune = hslider("MM_Matrix/s18_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s19_d01_osc2_detune = hslider("MM_Matrix/s19_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s20_d01_osc2_detune = hslider("MM_Matrix/s20_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s21_d01_osc2_detune = hslider("MM_Matrix/s21_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s22_d01_osc2_detune = hslider("MM_Matrix/s22_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s23_d01_osc2_detune = hslider("MM_Matrix/s23_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s24_d01_osc2_detune = hslider("MM_Matrix/s24_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s25_d01_osc2_detune = hslider("MM_Matrix/s25_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s26_d01_osc2_detune = hslider("MM_Matrix/s26_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s27_d01_osc2_detune = hslider("MM_Matrix/s27_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s28_d01_osc2_detune = hslider("MM_Matrix/s28_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s29_d01_osc2_detune = hslider("MM_Matrix/s29_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s30_d01_osc2_detune = hslider("MM_Matrix/s30_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s31_d01_osc2_detune = hslider("MM_Matrix/s31_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s32_d01_osc2_detune = hslider("MM_Matrix/s32_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s33_d01_osc2_detune = hslider("MM_Matrix/s33_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s34_d01_osc2_detune = hslider("MM_Matrix/s34_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s35_d01_osc2_detune = hslider("MM_Matrix/s35_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s36_d01_osc2_detune = hslider("MM_Matrix/s36_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s37_d01_osc2_detune = hslider("MM_Matrix/s37_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s38_d01_osc2_detune = hslider("MM_Matrix/s38_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s39_d01_osc2_detune = hslider("MM_Matrix/s39_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s40_d01_osc2_detune = hslider("MM_Matrix/s40_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s41_d01_osc2_detune = hslider("MM_Matrix/s41_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s42_d01_osc2_detune = hslider("MM_Matrix/s42_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s43_d01_osc2_detune = hslider("MM_Matrix/s43_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s44_d01_osc2_detune = hslider("MM_Matrix/s44_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s45_d01_osc2_detune = hslider("MM_Matrix/s45_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s46_d01_osc2_detune = hslider("MM_Matrix/s46_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); -amt_s47_d01_osc2_detune = hslider("MM_Matrix/s47_d01_osc2_detune", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 2 = osc3_detune -amt_s00_d02_osc3_detune = hslider("MM_Matrix/s00_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s01_d02_osc3_detune = hslider("MM_Matrix/s01_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s02_d02_osc3_detune = hslider("MM_Matrix/s02_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s03_d02_osc3_detune = hslider("MM_Matrix/s03_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s04_d02_osc3_detune = hslider("MM_Matrix/s04_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s05_d02_osc3_detune = hslider("MM_Matrix/s05_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s06_d02_osc3_detune = hslider("MM_Matrix/s06_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s07_d02_osc3_detune = hslider("MM_Matrix/s07_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s08_d02_osc3_detune = hslider("MM_Matrix/s08_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s09_d02_osc3_detune = hslider("MM_Matrix/s09_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s10_d02_osc3_detune = hslider("MM_Matrix/s10_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s11_d02_osc3_detune = hslider("MM_Matrix/s11_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s12_d02_osc3_detune = hslider("MM_Matrix/s12_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s13_d02_osc3_detune = hslider("MM_Matrix/s13_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s14_d02_osc3_detune = hslider("MM_Matrix/s14_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s15_d02_osc3_detune = hslider("MM_Matrix/s15_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s16_d02_osc3_detune = hslider("MM_Matrix/s16_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s17_d02_osc3_detune = hslider("MM_Matrix/s17_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s18_d02_osc3_detune = hslider("MM_Matrix/s18_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s19_d02_osc3_detune = hslider("MM_Matrix/s19_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s20_d02_osc3_detune = hslider("MM_Matrix/s20_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s21_d02_osc3_detune = hslider("MM_Matrix/s21_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s22_d02_osc3_detune = hslider("MM_Matrix/s22_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s23_d02_osc3_detune = hslider("MM_Matrix/s23_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s24_d02_osc3_detune = hslider("MM_Matrix/s24_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s25_d02_osc3_detune = hslider("MM_Matrix/s25_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s26_d02_osc3_detune = hslider("MM_Matrix/s26_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s27_d02_osc3_detune = hslider("MM_Matrix/s27_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s28_d02_osc3_detune = hslider("MM_Matrix/s28_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s29_d02_osc3_detune = hslider("MM_Matrix/s29_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s30_d02_osc3_detune = hslider("MM_Matrix/s30_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s31_d02_osc3_detune = hslider("MM_Matrix/s31_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s32_d02_osc3_detune = hslider("MM_Matrix/s32_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s33_d02_osc3_detune = hslider("MM_Matrix/s33_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s34_d02_osc3_detune = hslider("MM_Matrix/s34_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s35_d02_osc3_detune = hslider("MM_Matrix/s35_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s36_d02_osc3_detune = hslider("MM_Matrix/s36_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s37_d02_osc3_detune = hslider("MM_Matrix/s37_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s38_d02_osc3_detune = hslider("MM_Matrix/s38_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s39_d02_osc3_detune = hslider("MM_Matrix/s39_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s40_d02_osc3_detune = hslider("MM_Matrix/s40_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s41_d02_osc3_detune = hslider("MM_Matrix/s41_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s42_d02_osc3_detune = hslider("MM_Matrix/s42_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s43_d02_osc3_detune = hslider("MM_Matrix/s43_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s44_d02_osc3_detune = hslider("MM_Matrix/s44_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s45_d02_osc3_detune = hslider("MM_Matrix/s45_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s46_d02_osc3_detune = hslider("MM_Matrix/s46_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); -amt_s47_d02_osc3_detune = hslider("MM_Matrix/s47_d02_osc3_detune", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 3 = osc_mix_bal -amt_s00_d03_osc_mix_bal = hslider("MM_Matrix/s00_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s01_d03_osc_mix_bal = hslider("MM_Matrix/s01_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s02_d03_osc_mix_bal = hslider("MM_Matrix/s02_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s03_d03_osc_mix_bal = hslider("MM_Matrix/s03_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s04_d03_osc_mix_bal = hslider("MM_Matrix/s04_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s05_d03_osc_mix_bal = hslider("MM_Matrix/s05_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s06_d03_osc_mix_bal = hslider("MM_Matrix/s06_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s07_d03_osc_mix_bal = hslider("MM_Matrix/s07_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s08_d03_osc_mix_bal = hslider("MM_Matrix/s08_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s09_d03_osc_mix_bal = hslider("MM_Matrix/s09_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s10_d03_osc_mix_bal = hslider("MM_Matrix/s10_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s11_d03_osc_mix_bal = hslider("MM_Matrix/s11_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s12_d03_osc_mix_bal = hslider("MM_Matrix/s12_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s13_d03_osc_mix_bal = hslider("MM_Matrix/s13_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s14_d03_osc_mix_bal = hslider("MM_Matrix/s14_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s15_d03_osc_mix_bal = hslider("MM_Matrix/s15_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s16_d03_osc_mix_bal = hslider("MM_Matrix/s16_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s17_d03_osc_mix_bal = hslider("MM_Matrix/s17_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s18_d03_osc_mix_bal = hslider("MM_Matrix/s18_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s19_d03_osc_mix_bal = hslider("MM_Matrix/s19_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s20_d03_osc_mix_bal = hslider("MM_Matrix/s20_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s21_d03_osc_mix_bal = hslider("MM_Matrix/s21_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s22_d03_osc_mix_bal = hslider("MM_Matrix/s22_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s23_d03_osc_mix_bal = hslider("MM_Matrix/s23_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s24_d03_osc_mix_bal = hslider("MM_Matrix/s24_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s25_d03_osc_mix_bal = hslider("MM_Matrix/s25_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s26_d03_osc_mix_bal = hslider("MM_Matrix/s26_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s27_d03_osc_mix_bal = hslider("MM_Matrix/s27_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s28_d03_osc_mix_bal = hslider("MM_Matrix/s28_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s29_d03_osc_mix_bal = hslider("MM_Matrix/s29_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s30_d03_osc_mix_bal = hslider("MM_Matrix/s30_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s31_d03_osc_mix_bal = hslider("MM_Matrix/s31_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s32_d03_osc_mix_bal = hslider("MM_Matrix/s32_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s33_d03_osc_mix_bal = hslider("MM_Matrix/s33_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s34_d03_osc_mix_bal = hslider("MM_Matrix/s34_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s35_d03_osc_mix_bal = hslider("MM_Matrix/s35_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s36_d03_osc_mix_bal = hslider("MM_Matrix/s36_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s37_d03_osc_mix_bal = hslider("MM_Matrix/s37_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s38_d03_osc_mix_bal = hslider("MM_Matrix/s38_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s39_d03_osc_mix_bal = hslider("MM_Matrix/s39_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s40_d03_osc_mix_bal = hslider("MM_Matrix/s40_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s41_d03_osc_mix_bal = hslider("MM_Matrix/s41_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s42_d03_osc_mix_bal = hslider("MM_Matrix/s42_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s43_d03_osc_mix_bal = hslider("MM_Matrix/s43_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s44_d03_osc_mix_bal = hslider("MM_Matrix/s44_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s45_d03_osc_mix_bal = hslider("MM_Matrix/s45_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s46_d03_osc_mix_bal = hslider("MM_Matrix/s46_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); -amt_s47_d03_osc_mix_bal = hslider("MM_Matrix/s47_d03_osc_mix_bal", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 4 = noise_level -amt_s00_d04_noise_level = hslider("MM_Matrix/s00_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s01_d04_noise_level = hslider("MM_Matrix/s01_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s02_d04_noise_level = hslider("MM_Matrix/s02_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s03_d04_noise_level = hslider("MM_Matrix/s03_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s04_d04_noise_level = hslider("MM_Matrix/s04_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s05_d04_noise_level = hslider("MM_Matrix/s05_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s06_d04_noise_level = hslider("MM_Matrix/s06_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s07_d04_noise_level = hslider("MM_Matrix/s07_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s08_d04_noise_level = hslider("MM_Matrix/s08_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s09_d04_noise_level = hslider("MM_Matrix/s09_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s10_d04_noise_level = hslider("MM_Matrix/s10_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s11_d04_noise_level = hslider("MM_Matrix/s11_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s12_d04_noise_level = hslider("MM_Matrix/s12_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s13_d04_noise_level = hslider("MM_Matrix/s13_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s14_d04_noise_level = hslider("MM_Matrix/s14_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s15_d04_noise_level = hslider("MM_Matrix/s15_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s16_d04_noise_level = hslider("MM_Matrix/s16_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s17_d04_noise_level = hslider("MM_Matrix/s17_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s18_d04_noise_level = hslider("MM_Matrix/s18_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s19_d04_noise_level = hslider("MM_Matrix/s19_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s20_d04_noise_level = hslider("MM_Matrix/s20_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s21_d04_noise_level = hslider("MM_Matrix/s21_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s22_d04_noise_level = hslider("MM_Matrix/s22_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s23_d04_noise_level = hslider("MM_Matrix/s23_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s24_d04_noise_level = hslider("MM_Matrix/s24_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s25_d04_noise_level = hslider("MM_Matrix/s25_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s26_d04_noise_level = hslider("MM_Matrix/s26_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s27_d04_noise_level = hslider("MM_Matrix/s27_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s28_d04_noise_level = hslider("MM_Matrix/s28_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s29_d04_noise_level = hslider("MM_Matrix/s29_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s30_d04_noise_level = hslider("MM_Matrix/s30_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s31_d04_noise_level = hslider("MM_Matrix/s31_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s32_d04_noise_level = hslider("MM_Matrix/s32_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s33_d04_noise_level = hslider("MM_Matrix/s33_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s34_d04_noise_level = hslider("MM_Matrix/s34_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s35_d04_noise_level = hslider("MM_Matrix/s35_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s36_d04_noise_level = hslider("MM_Matrix/s36_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s37_d04_noise_level = hslider("MM_Matrix/s37_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s38_d04_noise_level = hslider("MM_Matrix/s38_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s39_d04_noise_level = hslider("MM_Matrix/s39_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s40_d04_noise_level = hslider("MM_Matrix/s40_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s41_d04_noise_level = hslider("MM_Matrix/s41_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s42_d04_noise_level = hslider("MM_Matrix/s42_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s43_d04_noise_level = hslider("MM_Matrix/s43_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s44_d04_noise_level = hslider("MM_Matrix/s44_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s45_d04_noise_level = hslider("MM_Matrix/s45_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s46_d04_noise_level = hslider("MM_Matrix/s46_d04_noise_level", 0.0, -1.0, 1.0, 0.001); -amt_s47_d04_noise_level = hslider("MM_Matrix/s47_d04_noise_level", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 5 = cutoff -amt_s00_d05_cutoff = hslider("MM_Matrix/s00_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s01_d05_cutoff = hslider("MM_Matrix/s01_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s02_d05_cutoff = hslider("MM_Matrix/s02_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s03_d05_cutoff = hslider("MM_Matrix/s03_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s04_d05_cutoff = hslider("MM_Matrix/s04_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s05_d05_cutoff = hslider("MM_Matrix/s05_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s06_d05_cutoff = hslider("MM_Matrix/s06_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s07_d05_cutoff = hslider("MM_Matrix/s07_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s08_d05_cutoff = hslider("MM_Matrix/s08_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s09_d05_cutoff = hslider("MM_Matrix/s09_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s10_d05_cutoff = hslider("MM_Matrix/s10_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s11_d05_cutoff = hslider("MM_Matrix/s11_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s12_d05_cutoff = hslider("MM_Matrix/s12_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s13_d05_cutoff = hslider("MM_Matrix/s13_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s14_d05_cutoff = hslider("MM_Matrix/s14_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s15_d05_cutoff = hslider("MM_Matrix/s15_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s16_d05_cutoff = hslider("MM_Matrix/s16_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s17_d05_cutoff = hslider("MM_Matrix/s17_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s18_d05_cutoff = hslider("MM_Matrix/s18_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s19_d05_cutoff = hslider("MM_Matrix/s19_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s20_d05_cutoff = hslider("MM_Matrix/s20_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s21_d05_cutoff = hslider("MM_Matrix/s21_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s22_d05_cutoff = hslider("MM_Matrix/s22_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s23_d05_cutoff = hslider("MM_Matrix/s23_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s24_d05_cutoff = hslider("MM_Matrix/s24_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s25_d05_cutoff = hslider("MM_Matrix/s25_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s26_d05_cutoff = hslider("MM_Matrix/s26_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s27_d05_cutoff = hslider("MM_Matrix/s27_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s28_d05_cutoff = hslider("MM_Matrix/s28_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s29_d05_cutoff = hslider("MM_Matrix/s29_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s30_d05_cutoff = hslider("MM_Matrix/s30_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s31_d05_cutoff = hslider("MM_Matrix/s31_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s32_d05_cutoff = hslider("MM_Matrix/s32_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s33_d05_cutoff = hslider("MM_Matrix/s33_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s34_d05_cutoff = hslider("MM_Matrix/s34_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s35_d05_cutoff = hslider("MM_Matrix/s35_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s36_d05_cutoff = hslider("MM_Matrix/s36_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s37_d05_cutoff = hslider("MM_Matrix/s37_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s38_d05_cutoff = hslider("MM_Matrix/s38_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s39_d05_cutoff = hslider("MM_Matrix/s39_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s40_d05_cutoff = hslider("MM_Matrix/s40_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s41_d05_cutoff = hslider("MM_Matrix/s41_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s42_d05_cutoff = hslider("MM_Matrix/s42_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s43_d05_cutoff = hslider("MM_Matrix/s43_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s44_d05_cutoff = hslider("MM_Matrix/s44_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s45_d05_cutoff = hslider("MM_Matrix/s45_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s46_d05_cutoff = hslider("MM_Matrix/s46_d05_cutoff", 0.0, -1.0, 1.0, 0.001); -amt_s47_d05_cutoff = hslider("MM_Matrix/s47_d05_cutoff", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 6 = resonance -amt_s00_d06_resonance = hslider("MM_Matrix/s00_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s01_d06_resonance = hslider("MM_Matrix/s01_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s02_d06_resonance = hslider("MM_Matrix/s02_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s03_d06_resonance = hslider("MM_Matrix/s03_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s04_d06_resonance = hslider("MM_Matrix/s04_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s05_d06_resonance = hslider("MM_Matrix/s05_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s06_d06_resonance = hslider("MM_Matrix/s06_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s07_d06_resonance = hslider("MM_Matrix/s07_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s08_d06_resonance = hslider("MM_Matrix/s08_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s09_d06_resonance = hslider("MM_Matrix/s09_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s10_d06_resonance = hslider("MM_Matrix/s10_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s11_d06_resonance = hslider("MM_Matrix/s11_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s12_d06_resonance = hslider("MM_Matrix/s12_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s13_d06_resonance = hslider("MM_Matrix/s13_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s14_d06_resonance = hslider("MM_Matrix/s14_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s15_d06_resonance = hslider("MM_Matrix/s15_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s16_d06_resonance = hslider("MM_Matrix/s16_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s17_d06_resonance = hslider("MM_Matrix/s17_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s18_d06_resonance = hslider("MM_Matrix/s18_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s19_d06_resonance = hslider("MM_Matrix/s19_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s20_d06_resonance = hslider("MM_Matrix/s20_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s21_d06_resonance = hslider("MM_Matrix/s21_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s22_d06_resonance = hslider("MM_Matrix/s22_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s23_d06_resonance = hslider("MM_Matrix/s23_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s24_d06_resonance = hslider("MM_Matrix/s24_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s25_d06_resonance = hslider("MM_Matrix/s25_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s26_d06_resonance = hslider("MM_Matrix/s26_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s27_d06_resonance = hslider("MM_Matrix/s27_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s28_d06_resonance = hslider("MM_Matrix/s28_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s29_d06_resonance = hslider("MM_Matrix/s29_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s30_d06_resonance = hslider("MM_Matrix/s30_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s31_d06_resonance = hslider("MM_Matrix/s31_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s32_d06_resonance = hslider("MM_Matrix/s32_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s33_d06_resonance = hslider("MM_Matrix/s33_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s34_d06_resonance = hslider("MM_Matrix/s34_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s35_d06_resonance = hslider("MM_Matrix/s35_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s36_d06_resonance = hslider("MM_Matrix/s36_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s37_d06_resonance = hslider("MM_Matrix/s37_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s38_d06_resonance = hslider("MM_Matrix/s38_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s39_d06_resonance = hslider("MM_Matrix/s39_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s40_d06_resonance = hslider("MM_Matrix/s40_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s41_d06_resonance = hslider("MM_Matrix/s41_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s42_d06_resonance = hslider("MM_Matrix/s42_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s43_d06_resonance = hslider("MM_Matrix/s43_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s44_d06_resonance = hslider("MM_Matrix/s44_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s45_d06_resonance = hslider("MM_Matrix/s45_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s46_d06_resonance = hslider("MM_Matrix/s46_d06_resonance", 0.0, -1.0, 1.0, 0.001); -amt_s47_d06_resonance = hslider("MM_Matrix/s47_d06_resonance", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 7 = filter_env_amt -amt_s00_d07_filter_env_amt = hslider("MM_Matrix/s00_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s01_d07_filter_env_amt = hslider("MM_Matrix/s01_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s02_d07_filter_env_amt = hslider("MM_Matrix/s02_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s03_d07_filter_env_amt = hslider("MM_Matrix/s03_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s04_d07_filter_env_amt = hslider("MM_Matrix/s04_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s05_d07_filter_env_amt = hslider("MM_Matrix/s05_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s06_d07_filter_env_amt = hslider("MM_Matrix/s06_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s07_d07_filter_env_amt = hslider("MM_Matrix/s07_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s08_d07_filter_env_amt = hslider("MM_Matrix/s08_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s09_d07_filter_env_amt = hslider("MM_Matrix/s09_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s10_d07_filter_env_amt = hslider("MM_Matrix/s10_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s11_d07_filter_env_amt = hslider("MM_Matrix/s11_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s12_d07_filter_env_amt = hslider("MM_Matrix/s12_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s13_d07_filter_env_amt = hslider("MM_Matrix/s13_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s14_d07_filter_env_amt = hslider("MM_Matrix/s14_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s15_d07_filter_env_amt = hslider("MM_Matrix/s15_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s16_d07_filter_env_amt = hslider("MM_Matrix/s16_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s17_d07_filter_env_amt = hslider("MM_Matrix/s17_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s18_d07_filter_env_amt = hslider("MM_Matrix/s18_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s19_d07_filter_env_amt = hslider("MM_Matrix/s19_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s20_d07_filter_env_amt = hslider("MM_Matrix/s20_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s21_d07_filter_env_amt = hslider("MM_Matrix/s21_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s22_d07_filter_env_amt = hslider("MM_Matrix/s22_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s23_d07_filter_env_amt = hslider("MM_Matrix/s23_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s24_d07_filter_env_amt = hslider("MM_Matrix/s24_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s25_d07_filter_env_amt = hslider("MM_Matrix/s25_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s26_d07_filter_env_amt = hslider("MM_Matrix/s26_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s27_d07_filter_env_amt = hslider("MM_Matrix/s27_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s28_d07_filter_env_amt = hslider("MM_Matrix/s28_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s29_d07_filter_env_amt = hslider("MM_Matrix/s29_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s30_d07_filter_env_amt = hslider("MM_Matrix/s30_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s31_d07_filter_env_amt = hslider("MM_Matrix/s31_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s32_d07_filter_env_amt = hslider("MM_Matrix/s32_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s33_d07_filter_env_amt = hslider("MM_Matrix/s33_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s34_d07_filter_env_amt = hslider("MM_Matrix/s34_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s35_d07_filter_env_amt = hslider("MM_Matrix/s35_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s36_d07_filter_env_amt = hslider("MM_Matrix/s36_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s37_d07_filter_env_amt = hslider("MM_Matrix/s37_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s38_d07_filter_env_amt = hslider("MM_Matrix/s38_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s39_d07_filter_env_amt = hslider("MM_Matrix/s39_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s40_d07_filter_env_amt = hslider("MM_Matrix/s40_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s41_d07_filter_env_amt = hslider("MM_Matrix/s41_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s42_d07_filter_env_amt = hslider("MM_Matrix/s42_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s43_d07_filter_env_amt = hslider("MM_Matrix/s43_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s44_d07_filter_env_amt = hslider("MM_Matrix/s44_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s45_d07_filter_env_amt = hslider("MM_Matrix/s45_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s46_d07_filter_env_amt = hslider("MM_Matrix/s46_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); -amt_s47_d07_filter_env_amt = hslider("MM_Matrix/s47_d07_filter_env_amt", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 8 = amp -amt_s00_d08_amp = hslider("MM_Matrix/s00_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s01_d08_amp = hslider("MM_Matrix/s01_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s02_d08_amp = hslider("MM_Matrix/s02_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s03_d08_amp = hslider("MM_Matrix/s03_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s04_d08_amp = hslider("MM_Matrix/s04_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s05_d08_amp = hslider("MM_Matrix/s05_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s06_d08_amp = hslider("MM_Matrix/s06_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s07_d08_amp = hslider("MM_Matrix/s07_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s08_d08_amp = hslider("MM_Matrix/s08_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s09_d08_amp = hslider("MM_Matrix/s09_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s10_d08_amp = hslider("MM_Matrix/s10_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s11_d08_amp = hslider("MM_Matrix/s11_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s12_d08_amp = hslider("MM_Matrix/s12_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s13_d08_amp = hslider("MM_Matrix/s13_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s14_d08_amp = hslider("MM_Matrix/s14_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s15_d08_amp = hslider("MM_Matrix/s15_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s16_d08_amp = hslider("MM_Matrix/s16_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s17_d08_amp = hslider("MM_Matrix/s17_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s18_d08_amp = hslider("MM_Matrix/s18_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s19_d08_amp = hslider("MM_Matrix/s19_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s20_d08_amp = hslider("MM_Matrix/s20_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s21_d08_amp = hslider("MM_Matrix/s21_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s22_d08_amp = hslider("MM_Matrix/s22_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s23_d08_amp = hslider("MM_Matrix/s23_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s24_d08_amp = hslider("MM_Matrix/s24_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s25_d08_amp = hslider("MM_Matrix/s25_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s26_d08_amp = hslider("MM_Matrix/s26_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s27_d08_amp = hslider("MM_Matrix/s27_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s28_d08_amp = hslider("MM_Matrix/s28_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s29_d08_amp = hslider("MM_Matrix/s29_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s30_d08_amp = hslider("MM_Matrix/s30_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s31_d08_amp = hslider("MM_Matrix/s31_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s32_d08_amp = hslider("MM_Matrix/s32_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s33_d08_amp = hslider("MM_Matrix/s33_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s34_d08_amp = hslider("MM_Matrix/s34_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s35_d08_amp = hslider("MM_Matrix/s35_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s36_d08_amp = hslider("MM_Matrix/s36_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s37_d08_amp = hslider("MM_Matrix/s37_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s38_d08_amp = hslider("MM_Matrix/s38_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s39_d08_amp = hslider("MM_Matrix/s39_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s40_d08_amp = hslider("MM_Matrix/s40_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s41_d08_amp = hslider("MM_Matrix/s41_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s42_d08_amp = hslider("MM_Matrix/s42_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s43_d08_amp = hslider("MM_Matrix/s43_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s44_d08_amp = hslider("MM_Matrix/s44_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s45_d08_amp = hslider("MM_Matrix/s45_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s46_d08_amp = hslider("MM_Matrix/s46_d08_amp", 0.0, -1.0, 1.0, 0.001); -amt_s47_d08_amp = hslider("MM_Matrix/s47_d08_amp", 0.0, -1.0, 1.0, 0.001); - -// Matrix column: destination 9 = pan -amt_s00_d09_pan = hslider("MM_Matrix/s00_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s01_d09_pan = hslider("MM_Matrix/s01_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s02_d09_pan = hslider("MM_Matrix/s02_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s03_d09_pan = hslider("MM_Matrix/s03_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s04_d09_pan = hslider("MM_Matrix/s04_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s05_d09_pan = hslider("MM_Matrix/s05_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s06_d09_pan = hslider("MM_Matrix/s06_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s07_d09_pan = hslider("MM_Matrix/s07_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s08_d09_pan = hslider("MM_Matrix/s08_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s09_d09_pan = hslider("MM_Matrix/s09_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s10_d09_pan = hslider("MM_Matrix/s10_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s11_d09_pan = hslider("MM_Matrix/s11_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s12_d09_pan = hslider("MM_Matrix/s12_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s13_d09_pan = hslider("MM_Matrix/s13_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s14_d09_pan = hslider("MM_Matrix/s14_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s15_d09_pan = hslider("MM_Matrix/s15_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s16_d09_pan = hslider("MM_Matrix/s16_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s17_d09_pan = hslider("MM_Matrix/s17_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s18_d09_pan = hslider("MM_Matrix/s18_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s19_d09_pan = hslider("MM_Matrix/s19_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s20_d09_pan = hslider("MM_Matrix/s20_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s21_d09_pan = hslider("MM_Matrix/s21_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s22_d09_pan = hslider("MM_Matrix/s22_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s23_d09_pan = hslider("MM_Matrix/s23_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s24_d09_pan = hslider("MM_Matrix/s24_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s25_d09_pan = hslider("MM_Matrix/s25_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s26_d09_pan = hslider("MM_Matrix/s26_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s27_d09_pan = hslider("MM_Matrix/s27_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s28_d09_pan = hslider("MM_Matrix/s28_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s29_d09_pan = hslider("MM_Matrix/s29_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s30_d09_pan = hslider("MM_Matrix/s30_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s31_d09_pan = hslider("MM_Matrix/s31_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s32_d09_pan = hslider("MM_Matrix/s32_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s33_d09_pan = hslider("MM_Matrix/s33_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s34_d09_pan = hslider("MM_Matrix/s34_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s35_d09_pan = hslider("MM_Matrix/s35_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s36_d09_pan = hslider("MM_Matrix/s36_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s37_d09_pan = hslider("MM_Matrix/s37_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s38_d09_pan = hslider("MM_Matrix/s38_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s39_d09_pan = hslider("MM_Matrix/s39_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s40_d09_pan = hslider("MM_Matrix/s40_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s41_d09_pan = hslider("MM_Matrix/s41_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s42_d09_pan = hslider("MM_Matrix/s42_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s43_d09_pan = hslider("MM_Matrix/s43_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s44_d09_pan = hslider("MM_Matrix/s44_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s45_d09_pan = hslider("MM_Matrix/s45_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s46_d09_pan = hslider("MM_Matrix/s46_d09_pan", 0.0, -1.0, 1.0, 0.001); -amt_s47_d09_pan = hslider("MM_Matrix/s47_d09_pan", 0.0, -1.0, 1.0, 0.001); - -// --------------------------------------------------------------------------- -// Destination signals — one per modulation destination. Each is a 48-term -// weighted sum of sources × amounts. The `gate` argument is threaded through -// so that mp.src??(gate) can drive ADSR envelopes. -// --------------------------------------------------------------------------- - -mod_pitch(gate) = - mp.src00(gate) * amt_s00_d00_pitch + - mp.src01(gate) * amt_s01_d00_pitch + - mp.src02(gate) * amt_s02_d00_pitch + - mp.src03(gate) * amt_s03_d00_pitch + - mp.src04(gate) * amt_s04_d00_pitch + - mp.src05(gate) * amt_s05_d00_pitch + - mp.src06(gate) * amt_s06_d00_pitch + - mp.src07(gate) * amt_s07_d00_pitch + - mp.src08(gate) * amt_s08_d00_pitch + - mp.src09(gate) * amt_s09_d00_pitch + - mp.src10(gate) * amt_s10_d00_pitch + - mp.src11(gate) * amt_s11_d00_pitch + - mp.src12(gate) * amt_s12_d00_pitch + - mp.src13(gate) * amt_s13_d00_pitch + - mp.src14(gate) * amt_s14_d00_pitch + - mp.src15(gate) * amt_s15_d00_pitch + - mp.src16(gate) * amt_s16_d00_pitch + - mp.src17(gate) * amt_s17_d00_pitch + - mp.src18(gate) * amt_s18_d00_pitch + - mp.src19(gate) * amt_s19_d00_pitch + - mp.src20(gate) * amt_s20_d00_pitch + - mp.src21(gate) * amt_s21_d00_pitch + - mp.src22(gate) * amt_s22_d00_pitch + - mp.src23(gate) * amt_s23_d00_pitch + - mp.src24(gate) * amt_s24_d00_pitch + - mp.src25(gate) * amt_s25_d00_pitch + - mp.src26(gate) * amt_s26_d00_pitch + - mp.src27(gate) * amt_s27_d00_pitch + - mp.src28(gate) * amt_s28_d00_pitch + - mp.src29(gate) * amt_s29_d00_pitch + - mp.src30(gate) * amt_s30_d00_pitch + - mp.src31(gate) * amt_s31_d00_pitch + - mp.src32(gate) * amt_s32_d00_pitch + - mp.src33(gate) * amt_s33_d00_pitch + - mp.src34(gate) * amt_s34_d00_pitch + - mp.src35(gate) * amt_s35_d00_pitch + - mp.src36(gate) * amt_s36_d00_pitch + - mp.src37(gate) * amt_s37_d00_pitch + - mp.src38(gate) * amt_s38_d00_pitch + - mp.src39(gate) * amt_s39_d00_pitch + - mp.src40(gate) * amt_s40_d00_pitch + - mp.src41(gate) * amt_s41_d00_pitch + - mp.src42(gate) * amt_s42_d00_pitch + - mp.src43(gate) * amt_s43_d00_pitch + - mp.src44(gate) * amt_s44_d00_pitch + - mp.src45(gate) * amt_s45_d00_pitch + - mp.src46(gate) * amt_s46_d00_pitch + - mp.src47(gate) * amt_s47_d00_pitch; - -mod_osc2_detune(gate) = - mp.src00(gate) * amt_s00_d01_osc2_detune + - mp.src01(gate) * amt_s01_d01_osc2_detune + - mp.src02(gate) * amt_s02_d01_osc2_detune + - mp.src03(gate) * amt_s03_d01_osc2_detune + - mp.src04(gate) * amt_s04_d01_osc2_detune + - mp.src05(gate) * amt_s05_d01_osc2_detune + - mp.src06(gate) * amt_s06_d01_osc2_detune + - mp.src07(gate) * amt_s07_d01_osc2_detune + - mp.src08(gate) * amt_s08_d01_osc2_detune + - mp.src09(gate) * amt_s09_d01_osc2_detune + - mp.src10(gate) * amt_s10_d01_osc2_detune + - mp.src11(gate) * amt_s11_d01_osc2_detune + - mp.src12(gate) * amt_s12_d01_osc2_detune + - mp.src13(gate) * amt_s13_d01_osc2_detune + - mp.src14(gate) * amt_s14_d01_osc2_detune + - mp.src15(gate) * amt_s15_d01_osc2_detune + - mp.src16(gate) * amt_s16_d01_osc2_detune + - mp.src17(gate) * amt_s17_d01_osc2_detune + - mp.src18(gate) * amt_s18_d01_osc2_detune + - mp.src19(gate) * amt_s19_d01_osc2_detune + - mp.src20(gate) * amt_s20_d01_osc2_detune + - mp.src21(gate) * amt_s21_d01_osc2_detune + - mp.src22(gate) * amt_s22_d01_osc2_detune + - mp.src23(gate) * amt_s23_d01_osc2_detune + - mp.src24(gate) * amt_s24_d01_osc2_detune + - mp.src25(gate) * amt_s25_d01_osc2_detune + - mp.src26(gate) * amt_s26_d01_osc2_detune + - mp.src27(gate) * amt_s27_d01_osc2_detune + - mp.src28(gate) * amt_s28_d01_osc2_detune + - mp.src29(gate) * amt_s29_d01_osc2_detune + - mp.src30(gate) * amt_s30_d01_osc2_detune + - mp.src31(gate) * amt_s31_d01_osc2_detune + - mp.src32(gate) * amt_s32_d01_osc2_detune + - mp.src33(gate) * amt_s33_d01_osc2_detune + - mp.src34(gate) * amt_s34_d01_osc2_detune + - mp.src35(gate) * amt_s35_d01_osc2_detune + - mp.src36(gate) * amt_s36_d01_osc2_detune + - mp.src37(gate) * amt_s37_d01_osc2_detune + - mp.src38(gate) * amt_s38_d01_osc2_detune + - mp.src39(gate) * amt_s39_d01_osc2_detune + - mp.src40(gate) * amt_s40_d01_osc2_detune + - mp.src41(gate) * amt_s41_d01_osc2_detune + - mp.src42(gate) * amt_s42_d01_osc2_detune + - mp.src43(gate) * amt_s43_d01_osc2_detune + - mp.src44(gate) * amt_s44_d01_osc2_detune + - mp.src45(gate) * amt_s45_d01_osc2_detune + - mp.src46(gate) * amt_s46_d01_osc2_detune + - mp.src47(gate) * amt_s47_d01_osc2_detune; - -mod_osc3_detune(gate) = - mp.src00(gate) * amt_s00_d02_osc3_detune + - mp.src01(gate) * amt_s01_d02_osc3_detune + - mp.src02(gate) * amt_s02_d02_osc3_detune + - mp.src03(gate) * amt_s03_d02_osc3_detune + - mp.src04(gate) * amt_s04_d02_osc3_detune + - mp.src05(gate) * amt_s05_d02_osc3_detune + - mp.src06(gate) * amt_s06_d02_osc3_detune + - mp.src07(gate) * amt_s07_d02_osc3_detune + - mp.src08(gate) * amt_s08_d02_osc3_detune + - mp.src09(gate) * amt_s09_d02_osc3_detune + - mp.src10(gate) * amt_s10_d02_osc3_detune + - mp.src11(gate) * amt_s11_d02_osc3_detune + - mp.src12(gate) * amt_s12_d02_osc3_detune + - mp.src13(gate) * amt_s13_d02_osc3_detune + - mp.src14(gate) * amt_s14_d02_osc3_detune + - mp.src15(gate) * amt_s15_d02_osc3_detune + - mp.src16(gate) * amt_s16_d02_osc3_detune + - mp.src17(gate) * amt_s17_d02_osc3_detune + - mp.src18(gate) * amt_s18_d02_osc3_detune + - mp.src19(gate) * amt_s19_d02_osc3_detune + - mp.src20(gate) * amt_s20_d02_osc3_detune + - mp.src21(gate) * amt_s21_d02_osc3_detune + - mp.src22(gate) * amt_s22_d02_osc3_detune + - mp.src23(gate) * amt_s23_d02_osc3_detune + - mp.src24(gate) * amt_s24_d02_osc3_detune + - mp.src25(gate) * amt_s25_d02_osc3_detune + - mp.src26(gate) * amt_s26_d02_osc3_detune + - mp.src27(gate) * amt_s27_d02_osc3_detune + - mp.src28(gate) * amt_s28_d02_osc3_detune + - mp.src29(gate) * amt_s29_d02_osc3_detune + - mp.src30(gate) * amt_s30_d02_osc3_detune + - mp.src31(gate) * amt_s31_d02_osc3_detune + - mp.src32(gate) * amt_s32_d02_osc3_detune + - mp.src33(gate) * amt_s33_d02_osc3_detune + - mp.src34(gate) * amt_s34_d02_osc3_detune + - mp.src35(gate) * amt_s35_d02_osc3_detune + - mp.src36(gate) * amt_s36_d02_osc3_detune + - mp.src37(gate) * amt_s37_d02_osc3_detune + - mp.src38(gate) * amt_s38_d02_osc3_detune + - mp.src39(gate) * amt_s39_d02_osc3_detune + - mp.src40(gate) * amt_s40_d02_osc3_detune + - mp.src41(gate) * amt_s41_d02_osc3_detune + - mp.src42(gate) * amt_s42_d02_osc3_detune + - mp.src43(gate) * amt_s43_d02_osc3_detune + - mp.src44(gate) * amt_s44_d02_osc3_detune + - mp.src45(gate) * amt_s45_d02_osc3_detune + - mp.src46(gate) * amt_s46_d02_osc3_detune + - mp.src47(gate) * amt_s47_d02_osc3_detune; - -mod_osc_mix_bal(gate) = - mp.src00(gate) * amt_s00_d03_osc_mix_bal + - mp.src01(gate) * amt_s01_d03_osc_mix_bal + - mp.src02(gate) * amt_s02_d03_osc_mix_bal + - mp.src03(gate) * amt_s03_d03_osc_mix_bal + - mp.src04(gate) * amt_s04_d03_osc_mix_bal + - mp.src05(gate) * amt_s05_d03_osc_mix_bal + - mp.src06(gate) * amt_s06_d03_osc_mix_bal + - mp.src07(gate) * amt_s07_d03_osc_mix_bal + - mp.src08(gate) * amt_s08_d03_osc_mix_bal + - mp.src09(gate) * amt_s09_d03_osc_mix_bal + - mp.src10(gate) * amt_s10_d03_osc_mix_bal + - mp.src11(gate) * amt_s11_d03_osc_mix_bal + - mp.src12(gate) * amt_s12_d03_osc_mix_bal + - mp.src13(gate) * amt_s13_d03_osc_mix_bal + - mp.src14(gate) * amt_s14_d03_osc_mix_bal + - mp.src15(gate) * amt_s15_d03_osc_mix_bal + - mp.src16(gate) * amt_s16_d03_osc_mix_bal + - mp.src17(gate) * amt_s17_d03_osc_mix_bal + - mp.src18(gate) * amt_s18_d03_osc_mix_bal + - mp.src19(gate) * amt_s19_d03_osc_mix_bal + - mp.src20(gate) * amt_s20_d03_osc_mix_bal + - mp.src21(gate) * amt_s21_d03_osc_mix_bal + - mp.src22(gate) * amt_s22_d03_osc_mix_bal + - mp.src23(gate) * amt_s23_d03_osc_mix_bal + - mp.src24(gate) * amt_s24_d03_osc_mix_bal + - mp.src25(gate) * amt_s25_d03_osc_mix_bal + - mp.src26(gate) * amt_s26_d03_osc_mix_bal + - mp.src27(gate) * amt_s27_d03_osc_mix_bal + - mp.src28(gate) * amt_s28_d03_osc_mix_bal + - mp.src29(gate) * amt_s29_d03_osc_mix_bal + - mp.src30(gate) * amt_s30_d03_osc_mix_bal + - mp.src31(gate) * amt_s31_d03_osc_mix_bal + - mp.src32(gate) * amt_s32_d03_osc_mix_bal + - mp.src33(gate) * amt_s33_d03_osc_mix_bal + - mp.src34(gate) * amt_s34_d03_osc_mix_bal + - mp.src35(gate) * amt_s35_d03_osc_mix_bal + - mp.src36(gate) * amt_s36_d03_osc_mix_bal + - mp.src37(gate) * amt_s37_d03_osc_mix_bal + - mp.src38(gate) * amt_s38_d03_osc_mix_bal + - mp.src39(gate) * amt_s39_d03_osc_mix_bal + - mp.src40(gate) * amt_s40_d03_osc_mix_bal + - mp.src41(gate) * amt_s41_d03_osc_mix_bal + - mp.src42(gate) * amt_s42_d03_osc_mix_bal + - mp.src43(gate) * amt_s43_d03_osc_mix_bal + - mp.src44(gate) * amt_s44_d03_osc_mix_bal + - mp.src45(gate) * amt_s45_d03_osc_mix_bal + - mp.src46(gate) * amt_s46_d03_osc_mix_bal + - mp.src47(gate) * amt_s47_d03_osc_mix_bal; - -mod_noise_level(gate) = - mp.src00(gate) * amt_s00_d04_noise_level + - mp.src01(gate) * amt_s01_d04_noise_level + - mp.src02(gate) * amt_s02_d04_noise_level + - mp.src03(gate) * amt_s03_d04_noise_level + - mp.src04(gate) * amt_s04_d04_noise_level + - mp.src05(gate) * amt_s05_d04_noise_level + - mp.src06(gate) * amt_s06_d04_noise_level + - mp.src07(gate) * amt_s07_d04_noise_level + - mp.src08(gate) * amt_s08_d04_noise_level + - mp.src09(gate) * amt_s09_d04_noise_level + - mp.src10(gate) * amt_s10_d04_noise_level + - mp.src11(gate) * amt_s11_d04_noise_level + - mp.src12(gate) * amt_s12_d04_noise_level + - mp.src13(gate) * amt_s13_d04_noise_level + - mp.src14(gate) * amt_s14_d04_noise_level + - mp.src15(gate) * amt_s15_d04_noise_level + - mp.src16(gate) * amt_s16_d04_noise_level + - mp.src17(gate) * amt_s17_d04_noise_level + - mp.src18(gate) * amt_s18_d04_noise_level + - mp.src19(gate) * amt_s19_d04_noise_level + - mp.src20(gate) * amt_s20_d04_noise_level + - mp.src21(gate) * amt_s21_d04_noise_level + - mp.src22(gate) * amt_s22_d04_noise_level + - mp.src23(gate) * amt_s23_d04_noise_level + - mp.src24(gate) * amt_s24_d04_noise_level + - mp.src25(gate) * amt_s25_d04_noise_level + - mp.src26(gate) * amt_s26_d04_noise_level + - mp.src27(gate) * amt_s27_d04_noise_level + - mp.src28(gate) * amt_s28_d04_noise_level + - mp.src29(gate) * amt_s29_d04_noise_level + - mp.src30(gate) * amt_s30_d04_noise_level + - mp.src31(gate) * amt_s31_d04_noise_level + - mp.src32(gate) * amt_s32_d04_noise_level + - mp.src33(gate) * amt_s33_d04_noise_level + - mp.src34(gate) * amt_s34_d04_noise_level + - mp.src35(gate) * amt_s35_d04_noise_level + - mp.src36(gate) * amt_s36_d04_noise_level + - mp.src37(gate) * amt_s37_d04_noise_level + - mp.src38(gate) * amt_s38_d04_noise_level + - mp.src39(gate) * amt_s39_d04_noise_level + - mp.src40(gate) * amt_s40_d04_noise_level + - mp.src41(gate) * amt_s41_d04_noise_level + - mp.src42(gate) * amt_s42_d04_noise_level + - mp.src43(gate) * amt_s43_d04_noise_level + - mp.src44(gate) * amt_s44_d04_noise_level + - mp.src45(gate) * amt_s45_d04_noise_level + - mp.src46(gate) * amt_s46_d04_noise_level + - mp.src47(gate) * amt_s47_d04_noise_level; - -mod_cutoff(gate) = - mp.src00(gate) * amt_s00_d05_cutoff + - mp.src01(gate) * amt_s01_d05_cutoff + - mp.src02(gate) * amt_s02_d05_cutoff + - mp.src03(gate) * amt_s03_d05_cutoff + - mp.src04(gate) * amt_s04_d05_cutoff + - mp.src05(gate) * amt_s05_d05_cutoff + - mp.src06(gate) * amt_s06_d05_cutoff + - mp.src07(gate) * amt_s07_d05_cutoff + - mp.src08(gate) * amt_s08_d05_cutoff + - mp.src09(gate) * amt_s09_d05_cutoff + - mp.src10(gate) * amt_s10_d05_cutoff + - mp.src11(gate) * amt_s11_d05_cutoff + - mp.src12(gate) * amt_s12_d05_cutoff + - mp.src13(gate) * amt_s13_d05_cutoff + - mp.src14(gate) * amt_s14_d05_cutoff + - mp.src15(gate) * amt_s15_d05_cutoff + - mp.src16(gate) * amt_s16_d05_cutoff + - mp.src17(gate) * amt_s17_d05_cutoff + - mp.src18(gate) * amt_s18_d05_cutoff + - mp.src19(gate) * amt_s19_d05_cutoff + - mp.src20(gate) * amt_s20_d05_cutoff + - mp.src21(gate) * amt_s21_d05_cutoff + - mp.src22(gate) * amt_s22_d05_cutoff + - mp.src23(gate) * amt_s23_d05_cutoff + - mp.src24(gate) * amt_s24_d05_cutoff + - mp.src25(gate) * amt_s25_d05_cutoff + - mp.src26(gate) * amt_s26_d05_cutoff + - mp.src27(gate) * amt_s27_d05_cutoff + - mp.src28(gate) * amt_s28_d05_cutoff + - mp.src29(gate) * amt_s29_d05_cutoff + - mp.src30(gate) * amt_s30_d05_cutoff + - mp.src31(gate) * amt_s31_d05_cutoff + - mp.src32(gate) * amt_s32_d05_cutoff + - mp.src33(gate) * amt_s33_d05_cutoff + - mp.src34(gate) * amt_s34_d05_cutoff + - mp.src35(gate) * amt_s35_d05_cutoff + - mp.src36(gate) * amt_s36_d05_cutoff + - mp.src37(gate) * amt_s37_d05_cutoff + - mp.src38(gate) * amt_s38_d05_cutoff + - mp.src39(gate) * amt_s39_d05_cutoff + - mp.src40(gate) * amt_s40_d05_cutoff + - mp.src41(gate) * amt_s41_d05_cutoff + - mp.src42(gate) * amt_s42_d05_cutoff + - mp.src43(gate) * amt_s43_d05_cutoff + - mp.src44(gate) * amt_s44_d05_cutoff + - mp.src45(gate) * amt_s45_d05_cutoff + - mp.src46(gate) * amt_s46_d05_cutoff + - mp.src47(gate) * amt_s47_d05_cutoff; - -mod_resonance(gate) = - mp.src00(gate) * amt_s00_d06_resonance + - mp.src01(gate) * amt_s01_d06_resonance + - mp.src02(gate) * amt_s02_d06_resonance + - mp.src03(gate) * amt_s03_d06_resonance + - mp.src04(gate) * amt_s04_d06_resonance + - mp.src05(gate) * amt_s05_d06_resonance + - mp.src06(gate) * amt_s06_d06_resonance + - mp.src07(gate) * amt_s07_d06_resonance + - mp.src08(gate) * amt_s08_d06_resonance + - mp.src09(gate) * amt_s09_d06_resonance + - mp.src10(gate) * amt_s10_d06_resonance + - mp.src11(gate) * amt_s11_d06_resonance + - mp.src12(gate) * amt_s12_d06_resonance + - mp.src13(gate) * amt_s13_d06_resonance + - mp.src14(gate) * amt_s14_d06_resonance + - mp.src15(gate) * amt_s15_d06_resonance + - mp.src16(gate) * amt_s16_d06_resonance + - mp.src17(gate) * amt_s17_d06_resonance + - mp.src18(gate) * amt_s18_d06_resonance + - mp.src19(gate) * amt_s19_d06_resonance + - mp.src20(gate) * amt_s20_d06_resonance + - mp.src21(gate) * amt_s21_d06_resonance + - mp.src22(gate) * amt_s22_d06_resonance + - mp.src23(gate) * amt_s23_d06_resonance + - mp.src24(gate) * amt_s24_d06_resonance + - mp.src25(gate) * amt_s25_d06_resonance + - mp.src26(gate) * amt_s26_d06_resonance + - mp.src27(gate) * amt_s27_d06_resonance + - mp.src28(gate) * amt_s28_d06_resonance + - mp.src29(gate) * amt_s29_d06_resonance + - mp.src30(gate) * amt_s30_d06_resonance + - mp.src31(gate) * amt_s31_d06_resonance + - mp.src32(gate) * amt_s32_d06_resonance + - mp.src33(gate) * amt_s33_d06_resonance + - mp.src34(gate) * amt_s34_d06_resonance + - mp.src35(gate) * amt_s35_d06_resonance + - mp.src36(gate) * amt_s36_d06_resonance + - mp.src37(gate) * amt_s37_d06_resonance + - mp.src38(gate) * amt_s38_d06_resonance + - mp.src39(gate) * amt_s39_d06_resonance + - mp.src40(gate) * amt_s40_d06_resonance + - mp.src41(gate) * amt_s41_d06_resonance + - mp.src42(gate) * amt_s42_d06_resonance + - mp.src43(gate) * amt_s43_d06_resonance + - mp.src44(gate) * amt_s44_d06_resonance + - mp.src45(gate) * amt_s45_d06_resonance + - mp.src46(gate) * amt_s46_d06_resonance + - mp.src47(gate) * amt_s47_d06_resonance; - -mod_filter_env_amt(gate) = - mp.src00(gate) * amt_s00_d07_filter_env_amt + - mp.src01(gate) * amt_s01_d07_filter_env_amt + - mp.src02(gate) * amt_s02_d07_filter_env_amt + - mp.src03(gate) * amt_s03_d07_filter_env_amt + - mp.src04(gate) * amt_s04_d07_filter_env_amt + - mp.src05(gate) * amt_s05_d07_filter_env_amt + - mp.src06(gate) * amt_s06_d07_filter_env_amt + - mp.src07(gate) * amt_s07_d07_filter_env_amt + - mp.src08(gate) * amt_s08_d07_filter_env_amt + - mp.src09(gate) * amt_s09_d07_filter_env_amt + - mp.src10(gate) * amt_s10_d07_filter_env_amt + - mp.src11(gate) * amt_s11_d07_filter_env_amt + - mp.src12(gate) * amt_s12_d07_filter_env_amt + - mp.src13(gate) * amt_s13_d07_filter_env_amt + - mp.src14(gate) * amt_s14_d07_filter_env_amt + - mp.src15(gate) * amt_s15_d07_filter_env_amt + - mp.src16(gate) * amt_s16_d07_filter_env_amt + - mp.src17(gate) * amt_s17_d07_filter_env_amt + - mp.src18(gate) * amt_s18_d07_filter_env_amt + - mp.src19(gate) * amt_s19_d07_filter_env_amt + - mp.src20(gate) * amt_s20_d07_filter_env_amt + - mp.src21(gate) * amt_s21_d07_filter_env_amt + - mp.src22(gate) * amt_s22_d07_filter_env_amt + - mp.src23(gate) * amt_s23_d07_filter_env_amt + - mp.src24(gate) * amt_s24_d07_filter_env_amt + - mp.src25(gate) * amt_s25_d07_filter_env_amt + - mp.src26(gate) * amt_s26_d07_filter_env_amt + - mp.src27(gate) * amt_s27_d07_filter_env_amt + - mp.src28(gate) * amt_s28_d07_filter_env_amt + - mp.src29(gate) * amt_s29_d07_filter_env_amt + - mp.src30(gate) * amt_s30_d07_filter_env_amt + - mp.src31(gate) * amt_s31_d07_filter_env_amt + - mp.src32(gate) * amt_s32_d07_filter_env_amt + - mp.src33(gate) * amt_s33_d07_filter_env_amt + - mp.src34(gate) * amt_s34_d07_filter_env_amt + - mp.src35(gate) * amt_s35_d07_filter_env_amt + - mp.src36(gate) * amt_s36_d07_filter_env_amt + - mp.src37(gate) * amt_s37_d07_filter_env_amt + - mp.src38(gate) * amt_s38_d07_filter_env_amt + - mp.src39(gate) * amt_s39_d07_filter_env_amt + - mp.src40(gate) * amt_s40_d07_filter_env_amt + - mp.src41(gate) * amt_s41_d07_filter_env_amt + - mp.src42(gate) * amt_s42_d07_filter_env_amt + - mp.src43(gate) * amt_s43_d07_filter_env_amt + - mp.src44(gate) * amt_s44_d07_filter_env_amt + - mp.src45(gate) * amt_s45_d07_filter_env_amt + - mp.src46(gate) * amt_s46_d07_filter_env_amt + - mp.src47(gate) * amt_s47_d07_filter_env_amt; - -mod_amp(gate) = - mp.src00(gate) * amt_s00_d08_amp + - mp.src01(gate) * amt_s01_d08_amp + - mp.src02(gate) * amt_s02_d08_amp + - mp.src03(gate) * amt_s03_d08_amp + - mp.src04(gate) * amt_s04_d08_amp + - mp.src05(gate) * amt_s05_d08_amp + - mp.src06(gate) * amt_s06_d08_amp + - mp.src07(gate) * amt_s07_d08_amp + - mp.src08(gate) * amt_s08_d08_amp + - mp.src09(gate) * amt_s09_d08_amp + - mp.src10(gate) * amt_s10_d08_amp + - mp.src11(gate) * amt_s11_d08_amp + - mp.src12(gate) * amt_s12_d08_amp + - mp.src13(gate) * amt_s13_d08_amp + - mp.src14(gate) * amt_s14_d08_amp + - mp.src15(gate) * amt_s15_d08_amp + - mp.src16(gate) * amt_s16_d08_amp + - mp.src17(gate) * amt_s17_d08_amp + - mp.src18(gate) * amt_s18_d08_amp + - mp.src19(gate) * amt_s19_d08_amp + - mp.src20(gate) * amt_s20_d08_amp + - mp.src21(gate) * amt_s21_d08_amp + - mp.src22(gate) * amt_s22_d08_amp + - mp.src23(gate) * amt_s23_d08_amp + - mp.src24(gate) * amt_s24_d08_amp + - mp.src25(gate) * amt_s25_d08_amp + - mp.src26(gate) * amt_s26_d08_amp + - mp.src27(gate) * amt_s27_d08_amp + - mp.src28(gate) * amt_s28_d08_amp + - mp.src29(gate) * amt_s29_d08_amp + - mp.src30(gate) * amt_s30_d08_amp + - mp.src31(gate) * amt_s31_d08_amp + - mp.src32(gate) * amt_s32_d08_amp + - mp.src33(gate) * amt_s33_d08_amp + - mp.src34(gate) * amt_s34_d08_amp + - mp.src35(gate) * amt_s35_d08_amp + - mp.src36(gate) * amt_s36_d08_amp + - mp.src37(gate) * amt_s37_d08_amp + - mp.src38(gate) * amt_s38_d08_amp + - mp.src39(gate) * amt_s39_d08_amp + - mp.src40(gate) * amt_s40_d08_amp + - mp.src41(gate) * amt_s41_d08_amp + - mp.src42(gate) * amt_s42_d08_amp + - mp.src43(gate) * amt_s43_d08_amp + - mp.src44(gate) * amt_s44_d08_amp + - mp.src45(gate) * amt_s45_d08_amp + - mp.src46(gate) * amt_s46_d08_amp + - mp.src47(gate) * amt_s47_d08_amp; - -mod_pan(gate) = - mp.src00(gate) * amt_s00_d09_pan + - mp.src01(gate) * amt_s01_d09_pan + - mp.src02(gate) * amt_s02_d09_pan + - mp.src03(gate) * amt_s03_d09_pan + - mp.src04(gate) * amt_s04_d09_pan + - mp.src05(gate) * amt_s05_d09_pan + - mp.src06(gate) * amt_s06_d09_pan + - mp.src07(gate) * amt_s07_d09_pan + - mp.src08(gate) * amt_s08_d09_pan + - mp.src09(gate) * amt_s09_d09_pan + - mp.src10(gate) * amt_s10_d09_pan + - mp.src11(gate) * amt_s11_d09_pan + - mp.src12(gate) * amt_s12_d09_pan + - mp.src13(gate) * amt_s13_d09_pan + - mp.src14(gate) * amt_s14_d09_pan + - mp.src15(gate) * amt_s15_d09_pan + - mp.src16(gate) * amt_s16_d09_pan + - mp.src17(gate) * amt_s17_d09_pan + - mp.src18(gate) * amt_s18_d09_pan + - mp.src19(gate) * amt_s19_d09_pan + - mp.src20(gate) * amt_s20_d09_pan + - mp.src21(gate) * amt_s21_d09_pan + - mp.src22(gate) * amt_s22_d09_pan + - mp.src23(gate) * amt_s23_d09_pan + - mp.src24(gate) * amt_s24_d09_pan + - mp.src25(gate) * amt_s25_d09_pan + - mp.src26(gate) * amt_s26_d09_pan + - mp.src27(gate) * amt_s27_d09_pan + - mp.src28(gate) * amt_s28_d09_pan + - mp.src29(gate) * amt_s29_d09_pan + - mp.src30(gate) * amt_s30_d09_pan + - mp.src31(gate) * amt_s31_d09_pan + - mp.src32(gate) * amt_s32_d09_pan + - mp.src33(gate) * amt_s33_d09_pan + - mp.src34(gate) * amt_s34_d09_pan + - mp.src35(gate) * amt_s35_d09_pan + - mp.src36(gate) * amt_s36_d09_pan + - mp.src37(gate) * amt_s37_d09_pan + - mp.src38(gate) * amt_s38_d09_pan + - mp.src39(gate) * amt_s39_d09_pan + - mp.src40(gate) * amt_s40_d09_pan + - mp.src41(gate) * amt_s41_d09_pan + - mp.src42(gate) * amt_s42_d09_pan + - mp.src43(gate) * amt_s43_d09_pan + - mp.src44(gate) * amt_s44_d09_pan + - mp.src45(gate) * amt_s45_d09_pan + - mp.src46(gate) * amt_s46_d09_pan + - mp.src47(gate) * amt_s47_d09_pan; - -// --------------------------------------------------------------------------- -// Derived signals -// --------------------------------------------------------------------------- - -cent2ratio(c) = pow(2.0, c / 1200.0); -oct2ratio(o) = pow(2.0, o); -semi2ratio(s) = pow(2.0, s / 12.0); - -// Portamento -glide_tau = 0.0001 + master_glide * 1.0; -freq_glided = freq : si.smooth(ba.tau2pole(glide_tau)); - -// Base (glide + master fine tune) -base_freq = freq_glided * cent2ratio(master_tune); - -// --------------------------------------------------------------------------- -// Per-oscillator wavetable (saw → tri → square crossfade) -// 0.0 → saw, 0.5 → triangle, 1.0 → square. Output in [-1,+1]. -// --------------------------------------------------------------------------- -osc_wave(shape, f) = - ba.if(shape < 0.5, - (1.0 - shape*2.0) * os.lf_saw(f) + (shape*2.0) * os.lf_triangle(f), - (1.0 - (shape-0.5)*2.0) * os.lf_triangle(f) + ((shape-0.5)*2.0) * os.lf_squarewave(f)); - -// --------------------------------------------------------------------------- -// Oscillator frequencies (pitch modulation: 1 unit = 12 semitones) -// --------------------------------------------------------------------------- -pitch_ratio = semi2ratio(mod_pitch(gate) * 12.0); - -osc1_freq = base_freq * oct2ratio(osc1_range) * pitch_ratio; -osc2_freq = base_freq * oct2ratio(osc2_range) * pitch_ratio - * cent2ratio(osc2_detune + mod_osc2_detune(gate) * 50.0); -osc3_freq_tracked = base_freq * oct2ratio(osc3_range) * pitch_ratio - * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); -// When kb_track=0, osc3 becomes a free-running sub/LFO source at ~55 Hz * oct -osc3_freq_untracked = 55.0 * oct2ratio(osc3_range) - * cent2ratio(osc3_detune + mod_osc3_detune(gate) * 50.0); -osc3_freq = osc3_kb_track * osc3_freq_tracked + (1.0 - osc3_kb_track) * osc3_freq_untracked; - -// --------------------------------------------------------------------------- -// Oscillators -// --------------------------------------------------------------------------- -o1 = osc_wave(osc1_wave, osc1_freq); -o2 = osc_wave(osc2_wave, osc2_freq); -o3 = osc_wave(osc3_wave, osc3_freq); - -// Osc mix balance: -1 = all osc1, +1 = all osc3, 0 = both equal. osc2 unaffected. -mix_bal = max(-1.0, min(1.0, mod_osc_mix_bal(gate))); -mix_t = (mix_bal + 1.0) * 0.5; // 0..1 -w_o1 = 1.0 - mix_t; -w_o3 = mix_t; - -// --------------------------------------------------------------------------- -// Noise: white (noise_type<0.5) / pink (else) -// --------------------------------------------------------------------------- -white_noise = no.noise; -pink_noise = no.pink_noise; -noise_raw = ba.if(noise_type < 0.5, white_noise, pink_noise); -noise_lvl = max(0.0, min(1.0, noise_level + mod_noise_level(gate))); - -// --------------------------------------------------------------------------- -// Mixer sum -// --------------------------------------------------------------------------- -mix_sum = - o1 * osc1_level * w_o1 - + o2 * osc2_level - + o3 * osc3_level * w_o3 - + noise_raw * noise_lvl; - -mix_driven = ma.tanh(mix_sum * mixer_drive); - -// --------------------------------------------------------------------------- -// Filter — Moog ladder. Cutoff is modulated in octaves (±5 oct per mod unit). -// --------------------------------------------------------------------------- -cutoff_mod_oct = (mod_cutoff(gate) + mod_filter_env_amt(gate)) * 5.0; -kb_scale = pow(freq_glided / 440.0, filter_kb); // 1.0 at A4 when kb=1 -eff_cutoff_raw = cutoff * kb_scale * oct2ratio(cutoff_mod_oct); -eff_cutoff = max(20.0, min(18000.0, eff_cutoff_raw)); -eff_res = max(0.0, min(0.99, resonance + mod_resonance(gate))); - -filtered = mix_driven : ve.moog_vcf(eff_res, eff_cutoff); - -// --------------------------------------------------------------------------- -// Amp and pan -// --------------------------------------------------------------------------- -// Velocity gain: blend 1.0 (no velocity) -> vel. Referenced here so Faust -// keeps the _vel hidden param alive in the JSON descriptor. -vel_gain = (1.0 - 0.3) + 0.3 * vel; // 30% velocity sensitivity, fixed -// d08_amp modulation is additive on top of base_amp (positive only). A -// fully untrained or adversarial matrix therefore cannot silence the -// voice while base_amp is high — which is the safety the "main amp gate -// on at all times" default relies on. Users who want envelope-gated -// voices set base_amp=0 and route a positive ADSR/LFO amount. -amp_val = max(0.0, min(1.0, base_amp + max(0.0, mod_amp(gate)))) * master_level * vel_gain; -pan_val = max(-1.0, min(1.0, master_pan + mod_pan(gate))); - -// equal-power pan -pan_l = cos((pan_val + 1.0) * 0.25 * ma.PI); -pan_r = sin((pan_val + 1.0) * 0.25 * ma.PI); - -signal = filtered * amp_val; -signal_L = signal * pan_l; -signal_R = signal * pan_r; - -process = signal_L, signal_R; diff --git a/playground/faust/modular-subtractive.json b/playground/faust/modular-subtractive.json deleted file mode 100644 index e04fbc2..0000000 --- a/playground/faust/modular-subtractive.json +++ /dev/null @@ -1 +0,0 @@ -{"name": "modular-subtractive","filename": "modular-subtractive.dsp","version": "2.79.3","compile_options": "-lang wasm -ct 1 -cn modular_subtractive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0","library_list": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/stdfaust.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/maths.lib","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust/mod-pool.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/oscillators.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/platform.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/basics.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/envelopes.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/signals.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/noises.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/filters.lib","/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust/vaeffects.lib"],"include_pathnames": ["/nix/store/yjqpc2a6b2m08zwwrgsdlqsi40m5wnpp-faust-2.79.3/share/faust","/usr/local/share/faust","/usr/share/faust",".","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust","/home/w1n5t0n/src/MEMLNaut-NISPS/playground/faust"],"size": 265304,"code": "Ly8gbW9kdWxhci1zdWJ0cmFjdGl2ZS5kc3Ag4oCUIE1pbmltb29nLXN0eWxlIHN1YnRyYWN0aXZlIHZvaWNlIGZvciB0aGUgIk1vZHVsYXIiIG1vZGUuCi8vCi8vIEFVVE8tR0VORVJBVEVEIGJ5IGdlbi1tb2R1bGFyLWRzcC5weS4gRE8gTk9UIEVESVQgQlkgSEFORC4KLy8gUmVnZW5lcmF0ZSB3aXRoOgovLyAgICAgY2QgcGxheWdyb3VuZC9mYXVzdCAmJiBweXRob24zIGdlbi1tb2R1bGFyLWRzcC5weQovLwovLyBBdWRpbyBwYXRoOiAzIG9zY2lsbGF0b3JzICsgbm9pc2Ug4oaSIG1peGVyIOKGkiBtb29nIGxhZGRlciBmaWx0ZXIg4oaSIGFtcC9wYW4uCi8vIEFsbCBlbnZlbG9wZXMgYW5kIExGT3MgY29tZSBmcm9tIG1vZC1wb29sLmxpYiB2aWEgYSA0OC1zb3VyY2Ugw5cgMTAtZGVzdAovLyBtb2R1bGF0aW9uIG1hdHJpeC4gTm8gaW50ZXJuYWwgZW52cy9MRk9zLgovLwovLyBNb2R1bGF0aW9uIGRlc3RpbmF0aW9ucyAoMTApOgovLyAgICAgMCBwaXRjaCAgICAgICAgICAgICBzZW1pdG9uZXMsIGFwcGxpZWQgdG8gYWxsIG9zYyBmcmVxdWVuY2llcwovLyAgICAgMSBvc2MyX2RldHVuZSAgICAgICBjZW50cyBvZmZzZXQgYWRkZWQgdG8gb3NjMiBkZXR1bmUga25vYgovLyAgICAgMiBvc2MzX2RldHVuZSAgICAgICBjZW50cyBvZmZzZXQgYWRkZWQgdG8gb3NjMyBkZXR1bmUga25vYgovLyAgICAgMyBvc2NfbWl4X2JhbCAgICAgICAtMS4uKzEgY3Jvc3NmYWRlcyBvc2MxPC0+b3NjMwovLyAgICAgNCBub2lzZV9sZXZlbCAgICAgICBhZGRlZCB0byBub2lzZSBsZXZlbCBrbm9iCi8vICAgICA1IGN1dG9mZiAgICAgICAgICAgICsvLTUgb2N0YXZlcyBvZiBmaWx0ZXIgY3V0b2ZmIG1vZHVsYXRpb24KLy8gICAgIDYgcmVzb25hbmNlICAgICAgICAgYWRkZWQgdG8gcmVzIGtub2IKLy8gICAgIDcgZmlsdGVyX2Vudl9hbXQgICAgc2Vjb25kIGluZGVwZW5kZW50ICsvLTUgb2N0YXZlIGN1dG9mZiBtb2QgY2hhbm5lbAovLyAgICAgOCBhbXAgICAgICAgICAgICAgICBhZGRlZCB0byBhbXAga25vYiAocm91dGUgYW4gQURTUiBoZXJlIGZvciBhIFZDQSBlbnYpCi8vICAgICA5IHBhbiAgICAgICAgICAgICAgIGFkZGVkIHRvIHBhbiBrbm9iCi8vCi8vIEhpZGRlbiBjb250cm9scyAoZHJpdmVuIGJ5IG5vdGVPbi9ub3RlT2ZmLCBub3QgYnkgdGhlIE1MIGVuZ2luZSk6Ci8vICAgICBmcmVxLCBnYXRlLCBfdmVsCi8vCi8vIEJ1aWxkOgovLyAgICAgY2QgcGxheWdyb3VuZC9mYXVzdCAmJiAuL2J1aWxkLnNoCi8vCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKaW1wb3J0KCJzdGRmYXVzdC5saWIiKTsKbXAgPSBsaWJyYXJ5KCJtb2QtcG9vbC5saWIiKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBIaWRkZW4gY29udHJvbHMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmZyZXEgPSBoc2xpZGVyKCIwX0hpZGRlbi9mcmVxW2hpZGRlbjoxXVt1bml0Okh6XSIsIDIyMCwgMjAsIDQwMDAsIDAuMDEpOwpnYXRlID0gYnV0dG9uKCIwX0hpZGRlbi9nYXRlW2hpZGRlbjoxXSIpOwp2ZWwgID0gaHNsaWRlcigiMF9IaWRkZW4vX3ZlbFtoaWRkZW46MV0iLCAwLjcsIDAuMCwgMS4wLCAwLjAwMSkgOiBzaS5zbW9vOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDEg4oCUIE9zY2lsbGF0b3JzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpvc2MxX3dhdmUgID0gaHNsaWRlcigiMV9Pc2NpbGxhdG9ycy8wMF9vc2MxX3dhdmVbdG9vbHRpcDowPXNhdywwLjU9dHJpLDE9c3F1YXJlXSIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKb3NjMV9yYW5nZSA9IGhzbGlkZXIoIjFfT3NjaWxsYXRvcnMvMDFfb3NjMV9yYW5nZVt1bml0Om9jdF0iLCAwLjAsIC0yLjAsIDIuMCwgMC4wMSk7Cm9zYzFfbGV2ZWwgPSBoc2xpZGVyKCIxX09zY2lsbGF0b3JzLzAyX29zYzFfbGV2ZWwiLCAwLjgsIDAuMCwgMS4wLCAwLjAwMSk7Cgpvc2MyX3dhdmUgICA9IGhzbGlkZXIoIjFfT3NjaWxsYXRvcnMvMDNfb3NjMl93YXZlW3Rvb2x0aXA6MD1zYXcsMC41PXRyaSwxPXNxdWFyZV0iLCAwLjAsIDAuMCwgMS4wLCAwLjAwMSk7Cm9zYzJfcmFuZ2UgID0gaHNsaWRlcigiMV9Pc2NpbGxhdG9ycy8wNF9vc2MyX3JhbmdlW3VuaXQ6b2N0XSIsIDAuMCwgLTIuMCwgMi4wLCAwLjAxKTsKb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCIxX09zY2lsbGF0b3JzLzA1X29zYzJfZGV0dW5lW3VuaXQ6Y3RdIiwgMC4wLCAtNTAuMCwgNTAuMCwgMC4xKTsKb3NjMl9sZXZlbCAgPSBoc2xpZGVyKCIxX09zY2lsbGF0b3JzLzA2X29zYzJfbGV2ZWwiLCAwLjYsIDAuMCwgMS4wLCAwLjAwMSk7Cgpvc2MzX3dhdmUgICAgID0gaHNsaWRlcigiMV9Pc2NpbGxhdG9ycy8wN19vc2MzX3dhdmVbdG9vbHRpcDowPXNhdywwLjU9dHJpLDE9c3F1YXJlXSIsIDAuNSwgMC4wLCAxLjAsIDAuMDAxKTsKb3NjM19yYW5nZSAgICA9IGhzbGlkZXIoIjFfT3NjaWxsYXRvcnMvMDhfb3NjM19yYW5nZVt1bml0Om9jdF0iLCAtMS4wLCAtMi4wLCAyLjAsIDAuMDEpOwpvc2MzX2RldHVuZSAgID0gaHNsaWRlcigiMV9Pc2NpbGxhdG9ycy8wOV9vc2MzX2RldHVuZVt1bml0OmN0XSIsIDAuMCwgLTUwLjAsIDUwLjAsIDAuMSk7Cm9zYzNfbGV2ZWwgICAgPSBoc2xpZGVyKCIxX09zY2lsbGF0b3JzLzEwX29zYzNfbGV2ZWwiLCAwLjQsIDAuMCwgMS4wLCAwLjAwMSk7Cm9zYzNfa2JfdHJhY2sgPSBoc2xpZGVyKCIxX09zY2lsbGF0b3JzLzExX29zYzNfa2JfdHJhY2tbdG9vbHRpcDoxPXRyYWNrcyBrZXlib2FyZCwwPUxGT10iLCAxLjAsIDAuMCwgMS4wLCAxLjApOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDIg4oCUIE1peGVyCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpub2lzZV90eXBlICA9IGhzbGlkZXIoIjJfTWl4ZXIvMDBfbm9pc2VfdHlwZVt0b29sdGlwOjA9d2hpdGUsMT1waW5rXSIsIDAuMCwgMC4wLCAxLjAsIDEuMCk7Cm5vaXNlX2xldmVsID0gaHNsaWRlcigiMl9NaXhlci8wMV9ub2lzZV9sZXZlbCIsIDAuMCwgMC4wLCAxLjAsIDAuMDAxKTsKbWl4ZXJfZHJpdmUgPSBoc2xpZGVyKCIyX01peGVyLzAyX21peGVyX2RyaXZlW3Rvb2x0aXA6UHJlLWZpbHRlciBvdmVyZHJpdmVdIiwgMS4wLCAwLjUsIDQuMCwgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDMg4oCUIEZpbHRlcgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KY3V0b2ZmICAgICAgPSBoc2xpZGVyKCIzX0ZpbHRlci8wMF9jdXRvZmZbc2NhbGU6bG9nXVt1bml0Okh6XSIsIDEyMDAuMCwgMjAuMCwgMjAwMDAuMCwgMC4xKTsKcmVzb25hbmNlICAgPSBoc2xpZGVyKCIzX0ZpbHRlci8wMV9yZXNvbmFuY2UiLCAwLjMsIDAuMCwgMS4wLCAwLjAwMSk7CmZpbHRlcl9rYiAgID0gaHNsaWRlcigiM19GaWx0ZXIvMDJfZmlsdGVyX2tiX3RyYWNrIiwgMC41LCAwLjAsIDEuMCwgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEdyb3VwIDQg4oCUIE1hc3RlcgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KbWFzdGVyX2xldmVsID0gaHNsaWRlcigiNF9NYXN0ZXIvMDBfbWFzdGVyX2xldmVsIiwgMC43LCAwLjAsIDEuMCwgMC4wMDEpOwptYXN0ZXJfZ2xpZGUgPSBoc2xpZGVyKCI0X01hc3Rlci8wMV9tYXN0ZXJfZ2xpZGVbdW5pdDpzXVtzY2FsZTpsb2ddIiwgMC4wLCAwLjAsIDIuMCwgMC4wMDEpOwptYXN0ZXJfdHVuZSAgPSBoc2xpZGVyKCI0X01hc3Rlci8wMl9tYXN0ZXJfdHVuZVt1bml0OmN0XSIsIDAuMCwgLTUwLjAsIDUwLjAsIDAuMSk7Cm1hc3Rlcl9wYW4gICA9IGhzbGlkZXIoIjRfTWFzdGVyLzAzX21hc3Rlcl9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwovLyBTdGF0aWMgYW1wIGZsb29yLiBBdCAxLjAgdGhlIHZvaWNlIGlzIGFsd2F5cyBmdWxseSBvcGVuIGFuZCBkMDhfYW1wCi8vIG1vZHVsYXRpb24gaXMgcHVyZWx5IGFkZGl0aXZlIGRlY29yYXRpb24uIERyb3AgdG8gMCBmb3IgY2xhc3NpYwovLyBBRFNSLWdhdGVkIFZDQSBiZWhhdmlvdXIgKHJvdXRlIHMwMF9kMDhfYW1wIHRvIGFuIEFEU1IpLgpiYXNlX2FtcCAgICAgPSBoc2xpZGVyKCI0X01hc3Rlci8wNF9iYXNlX2FtcCIsIDEuMCwgMC4wLCAxLjAsIDAuMDAxKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBNb2R1bGF0aW9uIG1hdHJpeCDigJQgNDggc291cmNlcyDDlyAxMCBkZXN0aW5hdGlvbnMuCi8vIEVhY2ggaHNsaWRlciBpcyBvbmUgImFtb3VudCIgZW50cnkgaW4gWy0xLCArMV0uIFplcm8gPSBubyBjb25uZWN0aW9uLgovLyBHcm91cCAiTU1fTWF0cml4IiBzbyB0aGUgcGFyYW1NZXRhIHBhcnNlciBjYW4gcmVjb2duaXNlIG1hdHJpeCBwYXJhbXMuCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMCA9IHBpdGNoCmFtdF9zMDBfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDFfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDNfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDRfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDZfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDdfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDlfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMThfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTlfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjFfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjJfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjRfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjVfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjdfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjhfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzBfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzFfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzNfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzRfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzZfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzdfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzlfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDBfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDJfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDNfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDVfZDAwX3BpdGNoID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDBfcGl0Y2giLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwMF9waXRjaCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDZfZDAwX3BpdGNoIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDBfcGl0Y2ggPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwMF9waXRjaCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CgovLyBNYXRyaXggY29sdW1uOiBkZXN0aW5hdGlvbiAxID0gb3NjMl9kZXR1bmUKYW10X3MwMF9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDFfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDJfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDRfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDVfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDdfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDhfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTBfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTFfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTNfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTRfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTZfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTdfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTlfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjBfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjJfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjNfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjVfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjZfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjhfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjlfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzFfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzJfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzRfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzVfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzdfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzhfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDBfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDFfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDNfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDRfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDFfb3NjMl9kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwMV9vc2MyX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDZfZDAxX29zYzJfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDFfb3NjMl9kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwMV9vc2MyX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDdfZDAxX29zYzJfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDIgPSBvc2MzX2RldHVuZQphbXRfczAwX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwMl9vc2MzX2RldHVuZSA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDAyX29zYzNfZGV0dW5lIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDJfb3NjM19kZXR1bmUgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwMl9vc2MzX2RldHVuZSIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDAyX29zYzNfZGV0dW5lID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDJfb3NjM19kZXR1bmUiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gMyA9IG9zY19taXhfYmFsCmFtdF9zMDBfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDFfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDNfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDRfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDZfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDdfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDlfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTBfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTJfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTNfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTVfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTZfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMThfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTlfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjFfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjJfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjRfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjVfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjdfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjhfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzBfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzFfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzNfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzRfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzZfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzdfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzlfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDBfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDJfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDNfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDVfZDAzX29zY19taXhfYmFsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDNfb3NjX21peF9iYWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwM19vc2NfbWl4X2JhbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDZfZDAzX29zY19taXhfYmFsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDNfb3NjX21peF9iYWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwM19vc2NfbWl4X2JhbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CgovLyBNYXRyaXggY29sdW1uOiBkZXN0aW5hdGlvbiA0ID0gbm9pc2VfbGV2ZWwKYW10X3MwMF9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDFfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDJfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDRfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDVfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDdfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDhfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTBfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTFfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTNfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTRfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTZfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTdfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTlfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjBfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjJfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjNfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjVfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjZfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjhfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjlfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzFfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzJfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzRfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzVfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzdfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzhfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDBfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDFfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDNfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDRfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDRfbm9pc2VfbGV2ZWwgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwNF9ub2lzZV9sZXZlbCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDZfZDA0X25vaXNlX2xldmVsID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDRfbm9pc2VfbGV2ZWwiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwNF9ub2lzZV9sZXZlbCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDdfZDA0X25vaXNlX2xldmVsIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDUgPSBjdXRvZmYKYW10X3MwMF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDVfY3V0b2ZmID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDVfY3V0b2ZmIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDYgPSByZXNvbmFuY2UKYW10X3MwMF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwM19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwM19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwN19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwOV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxM19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxN19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MxOV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyM19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyN19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyN19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzM19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzM19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzN19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzOV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Ml9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Ml9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0M19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NF9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0NV9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NV9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0Nl9kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0N19kMDZfcmVzb25hbmNlID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDZfcmVzb25hbmNlIiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKCi8vIE1hdHJpeCBjb2x1bW46IGRlc3RpbmF0aW9uIDcgPSBmaWx0ZXJfZW52X2FtdAphbXRfczAwX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDBfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwMV9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDJfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwMl9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDNfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwNF9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDVfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwNV9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDZfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MwN19kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMDhfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MwOF9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMDlfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxMF9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTFfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxMV9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTJfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxM19kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTRfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxNF9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMTVfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxNl9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMTdfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MxN19kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMThfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MxOV9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjBfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyMF9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjFfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyMl9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjNfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyM19kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjRfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyNV9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjZfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyNl9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMjdfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MyOF9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMjlfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MyOV9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzBfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzMV9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzJfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzMl9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzNfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzNF9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzVfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzNV9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzZfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3MzN19kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zMzhfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3MzOF9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zMzlfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0MF9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDFfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0MV9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDJfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0M19kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDRfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0NF9kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwN19maWx0ZXJfZW52X2FtdCA9IGhzbGlkZXIoIk1NX01hdHJpeC9zNDVfZDA3X2ZpbHRlcl9lbnZfYW10IiwgMC4wLCAtMS4wLCAxLjAsIDAuMDAxKTsKYW10X3M0Nl9kMDdfZmlsdGVyX2Vudl9hbXQgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwN19maWx0ZXJfZW52X2FtdCIsIDAuMCwgLTEuMCwgMS4wLCAwLjAwMSk7CmFtdF9zNDdfZDA3X2ZpbHRlcl9lbnZfYW10ID0gaHNsaWRlcigiTU1fTWF0cml4L3M0N19kMDdfZmlsdGVyX2Vudl9hbXQiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gOCA9IGFtcAphbXRfczAwX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwOF9hbXAgPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwOF9hbXAiLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gTWF0cml4IGNvbHVtbjogZGVzdGluYXRpb24gOSA9IHBhbgphbXRfczAwX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAwX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAxX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAxX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAyX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAyX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczAzX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczAzX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA0X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA0X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA1X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA1X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA2X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA2X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA3X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA3X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA4X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA4X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczA5X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczA5X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEwX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEwX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczExX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczExX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEyX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEyX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczEzX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczEzX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE0X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE0X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE1X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE1X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE2X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE2X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE3X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE3X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE4X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE4X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczE5X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczE5X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIwX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIwX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIxX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIxX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIyX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIyX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczIzX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczIzX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI0X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI0X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI1X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI1X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI2X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI2X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI3X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI3X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI4X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI4X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczI5X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczI5X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMwX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMwX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMxX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMxX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMyX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMyX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczMzX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczMzX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM0X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM0X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM1X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM1X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM2X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM2X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM3X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM3X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM4X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM4X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczM5X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczM5X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQwX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQwX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQxX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQxX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQyX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQyX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQzX2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQzX2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ0X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ0X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ1X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ1X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ2X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ2X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwphbXRfczQ3X2QwOV9wYW4gPSBoc2xpZGVyKCJNTV9NYXRyaXgvczQ3X2QwOV9wYW4iLCAwLjAsIC0xLjAsIDEuMCwgMC4wMDEpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIERlc3RpbmF0aW9uIHNpZ25hbHMg4oCUIG9uZSBwZXIgbW9kdWxhdGlvbiBkZXN0aW5hdGlvbi4gRWFjaCBpcyBhIDQ4LXRlcm0KLy8gd2VpZ2h0ZWQgc3VtIG9mIHNvdXJjZXMgw5cgYW1vdW50cy4gVGhlIGBnYXRlYCBhcmd1bWVudCBpcyB0aHJlYWRlZCB0aHJvdWdoCi8vIHNvIHRoYXQgbXAuc3JjPz8oZ2F0ZSkgY2FuIGRyaXZlIEFEU1IgZW52ZWxvcGVzLgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCm1vZF9waXRjaChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwMF9waXRjaCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwMF9waXRjaCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwMF9waXRjaCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAwX3BpdGNoICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwMF9waXRjaCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAwX3BpdGNoICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwMF9waXRjaCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwMF9waXRjaCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAwX3BpdGNoICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwMF9waXRjaCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAwX3BpdGNoICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwMF9waXRjaCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwMF9waXRjaCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAwX3BpdGNoICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwMF9waXRjaCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAwX3BpdGNoICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwMF9waXRjaCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwMF9waXRjaCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAwX3BpdGNoICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDBfcGl0Y2ggKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwMF9waXRjaCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwMF9waXRjaCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwMF9waXRjaCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAwX3BpdGNoICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDBfcGl0Y2ggKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwMF9waXRjaDsKCm1vZF9vc2MyX2RldHVuZShnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwMV9vc2MyX2RldHVuZSArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAxX29zYzJfZGV0dW5lICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDFfb3NjMl9kZXR1bmUgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwMV9vc2MyX2RldHVuZTsKCm1vZF9vc2MzX2RldHVuZShnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwMl9vc2MzX2RldHVuZSArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAyX29zYzNfZGV0dW5lICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDJfb3NjM19kZXR1bmUgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwMl9vc2MzX2RldHVuZTsKCm1vZF9vc2NfbWl4X2JhbChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwM19vc2NfbWl4X2JhbCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDAzX29zY19taXhfYmFsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDNfb3NjX21peF9iYWwgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwM19vc2NfbWl4X2JhbDsKCm1vZF9ub2lzZV9sZXZlbChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwNF9ub2lzZV9sZXZlbCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA0X25vaXNlX2xldmVsICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDRfbm9pc2VfbGV2ZWwgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwNF9ub2lzZV9sZXZlbDsKCm1vZF9jdXRvZmYoZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwNV9jdXRvZmYgKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwNV9jdXRvZmY7Cgptb2RfcmVzb25hbmNlKGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzAxKGdhdGUpICogYW10X3MwMV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzAyKGdhdGUpICogYW10X3MwMl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA0KGdhdGUpICogYW10X3MwNF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA1KGdhdGUpICogYW10X3MwNV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA3KGdhdGUpICogYW10X3MwN19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA4KGdhdGUpICogYW10X3MwOF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzEwKGdhdGUpICogYW10X3MxMF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzExKGdhdGUpICogYW10X3MxMV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzEzKGdhdGUpICogYW10X3MxM19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE0KGdhdGUpICogYW10X3MxNF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE2KGdhdGUpICogYW10X3MxNl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE3KGdhdGUpICogYW10X3MxN19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzE5KGdhdGUpICogYW10X3MxOV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzIwKGdhdGUpICogYW10X3MyMF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzIyKGdhdGUpICogYW10X3MyMl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzIzKGdhdGUpICogYW10X3MyM19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI1KGdhdGUpICogYW10X3MyNV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI2KGdhdGUpICogYW10X3MyNl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI4KGdhdGUpICogYW10X3MyOF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzI5KGdhdGUpICogYW10X3MyOV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzMxKGdhdGUpICogYW10X3MzMV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzMyKGdhdGUpICogYW10X3MzMl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM0KGdhdGUpICogYW10X3MzNF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM1KGdhdGUpICogYW10X3MzNV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM3KGdhdGUpICogYW10X3MzN19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM4KGdhdGUpICogYW10X3MzOF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQwKGdhdGUpICogYW10X3M0MF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQxKGdhdGUpICogYW10X3M0MV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQzKGdhdGUpICogYW10X3M0M19kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQ0KGdhdGUpICogYW10X3M0NF9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQ2KGdhdGUpICogYW10X3M0Nl9kMDZfcmVzb25hbmNlICsKICAgIG1wLnNyYzQ3KGdhdGUpICogYW10X3M0N19kMDZfcmVzb25hbmNlOwoKbW9kX2ZpbHRlcl9lbnZfYW10KGdhdGUpID0KICAgIG1wLnNyYzAwKGdhdGUpICogYW10X3MwMF9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzAzKGdhdGUpICogYW10X3MwM19kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzA2KGdhdGUpICogYW10X3MwNl9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzA5KGdhdGUpICogYW10X3MwOV9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzEyKGdhdGUpICogYW10X3MxMl9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzE1KGdhdGUpICogYW10X3MxNV9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzE4KGdhdGUpICogYW10X3MxOF9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzIxKGdhdGUpICogYW10X3MyMV9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzI0KGdhdGUpICogYW10X3MyNF9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzI3KGdhdGUpICogYW10X3MyN19kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzMwKGdhdGUpICogYW10X3MzMF9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzMzKGdhdGUpICogYW10X3MzM19kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzM2KGdhdGUpICogYW10X3MzNl9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzM5KGdhdGUpICogYW10X3MzOV9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzQyKGdhdGUpICogYW10X3M0Ml9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDA3X2ZpbHRlcl9lbnZfYW10ICsKICAgIG1wLnNyYzQ1KGdhdGUpICogYW10X3M0NV9kMDdfZmlsdGVyX2Vudl9hbXQgKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwN19maWx0ZXJfZW52X2FtdCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDA3X2ZpbHRlcl9lbnZfYW10OwoKbW9kX2FtcChnYXRlKSA9CiAgICBtcC5zcmMwMChnYXRlKSAqIGFtdF9zMDBfZDA4X2FtcCArCiAgICBtcC5zcmMwMShnYXRlKSAqIGFtdF9zMDFfZDA4X2FtcCArCiAgICBtcC5zcmMwMihnYXRlKSAqIGFtdF9zMDJfZDA4X2FtcCArCiAgICBtcC5zcmMwMyhnYXRlKSAqIGFtdF9zMDNfZDA4X2FtcCArCiAgICBtcC5zcmMwNChnYXRlKSAqIGFtdF9zMDRfZDA4X2FtcCArCiAgICBtcC5zcmMwNShnYXRlKSAqIGFtdF9zMDVfZDA4X2FtcCArCiAgICBtcC5zcmMwNihnYXRlKSAqIGFtdF9zMDZfZDA4X2FtcCArCiAgICBtcC5zcmMwNyhnYXRlKSAqIGFtdF9zMDdfZDA4X2FtcCArCiAgICBtcC5zcmMwOChnYXRlKSAqIGFtdF9zMDhfZDA4X2FtcCArCiAgICBtcC5zcmMwOShnYXRlKSAqIGFtdF9zMDlfZDA4X2FtcCArCiAgICBtcC5zcmMxMChnYXRlKSAqIGFtdF9zMTBfZDA4X2FtcCArCiAgICBtcC5zcmMxMShnYXRlKSAqIGFtdF9zMTFfZDA4X2FtcCArCiAgICBtcC5zcmMxMihnYXRlKSAqIGFtdF9zMTJfZDA4X2FtcCArCiAgICBtcC5zcmMxMyhnYXRlKSAqIGFtdF9zMTNfZDA4X2FtcCArCiAgICBtcC5zcmMxNChnYXRlKSAqIGFtdF9zMTRfZDA4X2FtcCArCiAgICBtcC5zcmMxNShnYXRlKSAqIGFtdF9zMTVfZDA4X2FtcCArCiAgICBtcC5zcmMxNihnYXRlKSAqIGFtdF9zMTZfZDA4X2FtcCArCiAgICBtcC5zcmMxNyhnYXRlKSAqIGFtdF9zMTdfZDA4X2FtcCArCiAgICBtcC5zcmMxOChnYXRlKSAqIGFtdF9zMThfZDA4X2FtcCArCiAgICBtcC5zcmMxOShnYXRlKSAqIGFtdF9zMTlfZDA4X2FtcCArCiAgICBtcC5zcmMyMChnYXRlKSAqIGFtdF9zMjBfZDA4X2FtcCArCiAgICBtcC5zcmMyMShnYXRlKSAqIGFtdF9zMjFfZDA4X2FtcCArCiAgICBtcC5zcmMyMihnYXRlKSAqIGFtdF9zMjJfZDA4X2FtcCArCiAgICBtcC5zcmMyMyhnYXRlKSAqIGFtdF9zMjNfZDA4X2FtcCArCiAgICBtcC5zcmMyNChnYXRlKSAqIGFtdF9zMjRfZDA4X2FtcCArCiAgICBtcC5zcmMyNShnYXRlKSAqIGFtdF9zMjVfZDA4X2FtcCArCiAgICBtcC5zcmMyNihnYXRlKSAqIGFtdF9zMjZfZDA4X2FtcCArCiAgICBtcC5zcmMyNyhnYXRlKSAqIGFtdF9zMjdfZDA4X2FtcCArCiAgICBtcC5zcmMyOChnYXRlKSAqIGFtdF9zMjhfZDA4X2FtcCArCiAgICBtcC5zcmMyOShnYXRlKSAqIGFtdF9zMjlfZDA4X2FtcCArCiAgICBtcC5zcmMzMChnYXRlKSAqIGFtdF9zMzBfZDA4X2FtcCArCiAgICBtcC5zcmMzMShnYXRlKSAqIGFtdF9zMzFfZDA4X2FtcCArCiAgICBtcC5zcmMzMihnYXRlKSAqIGFtdF9zMzJfZDA4X2FtcCArCiAgICBtcC5zcmMzMyhnYXRlKSAqIGFtdF9zMzNfZDA4X2FtcCArCiAgICBtcC5zcmMzNChnYXRlKSAqIGFtdF9zMzRfZDA4X2FtcCArCiAgICBtcC5zcmMzNShnYXRlKSAqIGFtdF9zMzVfZDA4X2FtcCArCiAgICBtcC5zcmMzNihnYXRlKSAqIGFtdF9zMzZfZDA4X2FtcCArCiAgICBtcC5zcmMzNyhnYXRlKSAqIGFtdF9zMzdfZDA4X2FtcCArCiAgICBtcC5zcmMzOChnYXRlKSAqIGFtdF9zMzhfZDA4X2FtcCArCiAgICBtcC5zcmMzOShnYXRlKSAqIGFtdF9zMzlfZDA4X2FtcCArCiAgICBtcC5zcmM0MChnYXRlKSAqIGFtdF9zNDBfZDA4X2FtcCArCiAgICBtcC5zcmM0MShnYXRlKSAqIGFtdF9zNDFfZDA4X2FtcCArCiAgICBtcC5zcmM0MihnYXRlKSAqIGFtdF9zNDJfZDA4X2FtcCArCiAgICBtcC5zcmM0MyhnYXRlKSAqIGFtdF9zNDNfZDA4X2FtcCArCiAgICBtcC5zcmM0NChnYXRlKSAqIGFtdF9zNDRfZDA4X2FtcCArCiAgICBtcC5zcmM0NShnYXRlKSAqIGFtdF9zNDVfZDA4X2FtcCArCiAgICBtcC5zcmM0NihnYXRlKSAqIGFtdF9zNDZfZDA4X2FtcCArCiAgICBtcC5zcmM0NyhnYXRlKSAqIGFtdF9zNDdfZDA4X2FtcDsKCm1vZF9wYW4oZ2F0ZSkgPQogICAgbXAuc3JjMDAoZ2F0ZSkgKiBhbXRfczAwX2QwOV9wYW4gKwogICAgbXAuc3JjMDEoZ2F0ZSkgKiBhbXRfczAxX2QwOV9wYW4gKwogICAgbXAuc3JjMDIoZ2F0ZSkgKiBhbXRfczAyX2QwOV9wYW4gKwogICAgbXAuc3JjMDMoZ2F0ZSkgKiBhbXRfczAzX2QwOV9wYW4gKwogICAgbXAuc3JjMDQoZ2F0ZSkgKiBhbXRfczA0X2QwOV9wYW4gKwogICAgbXAuc3JjMDUoZ2F0ZSkgKiBhbXRfczA1X2QwOV9wYW4gKwogICAgbXAuc3JjMDYoZ2F0ZSkgKiBhbXRfczA2X2QwOV9wYW4gKwogICAgbXAuc3JjMDcoZ2F0ZSkgKiBhbXRfczA3X2QwOV9wYW4gKwogICAgbXAuc3JjMDgoZ2F0ZSkgKiBhbXRfczA4X2QwOV9wYW4gKwogICAgbXAuc3JjMDkoZ2F0ZSkgKiBhbXRfczA5X2QwOV9wYW4gKwogICAgbXAuc3JjMTAoZ2F0ZSkgKiBhbXRfczEwX2QwOV9wYW4gKwogICAgbXAuc3JjMTEoZ2F0ZSkgKiBhbXRfczExX2QwOV9wYW4gKwogICAgbXAuc3JjMTIoZ2F0ZSkgKiBhbXRfczEyX2QwOV9wYW4gKwogICAgbXAuc3JjMTMoZ2F0ZSkgKiBhbXRfczEzX2QwOV9wYW4gKwogICAgbXAuc3JjMTQoZ2F0ZSkgKiBhbXRfczE0X2QwOV9wYW4gKwogICAgbXAuc3JjMTUoZ2F0ZSkgKiBhbXRfczE1X2QwOV9wYW4gKwogICAgbXAuc3JjMTYoZ2F0ZSkgKiBhbXRfczE2X2QwOV9wYW4gKwogICAgbXAuc3JjMTcoZ2F0ZSkgKiBhbXRfczE3X2QwOV9wYW4gKwogICAgbXAuc3JjMTgoZ2F0ZSkgKiBhbXRfczE4X2QwOV9wYW4gKwogICAgbXAuc3JjMTkoZ2F0ZSkgKiBhbXRfczE5X2QwOV9wYW4gKwogICAgbXAuc3JjMjAoZ2F0ZSkgKiBhbXRfczIwX2QwOV9wYW4gKwogICAgbXAuc3JjMjEoZ2F0ZSkgKiBhbXRfczIxX2QwOV9wYW4gKwogICAgbXAuc3JjMjIoZ2F0ZSkgKiBhbXRfczIyX2QwOV9wYW4gKwogICAgbXAuc3JjMjMoZ2F0ZSkgKiBhbXRfczIzX2QwOV9wYW4gKwogICAgbXAuc3JjMjQoZ2F0ZSkgKiBhbXRfczI0X2QwOV9wYW4gKwogICAgbXAuc3JjMjUoZ2F0ZSkgKiBhbXRfczI1X2QwOV9wYW4gKwogICAgbXAuc3JjMjYoZ2F0ZSkgKiBhbXRfczI2X2QwOV9wYW4gKwogICAgbXAuc3JjMjcoZ2F0ZSkgKiBhbXRfczI3X2QwOV9wYW4gKwogICAgbXAuc3JjMjgoZ2F0ZSkgKiBhbXRfczI4X2QwOV9wYW4gKwogICAgbXAuc3JjMjkoZ2F0ZSkgKiBhbXRfczI5X2QwOV9wYW4gKwogICAgbXAuc3JjMzAoZ2F0ZSkgKiBhbXRfczMwX2QwOV9wYW4gKwogICAgbXAuc3JjMzEoZ2F0ZSkgKiBhbXRfczMxX2QwOV9wYW4gKwogICAgbXAuc3JjMzIoZ2F0ZSkgKiBhbXRfczMyX2QwOV9wYW4gKwogICAgbXAuc3JjMzMoZ2F0ZSkgKiBhbXRfczMzX2QwOV9wYW4gKwogICAgbXAuc3JjMzQoZ2F0ZSkgKiBhbXRfczM0X2QwOV9wYW4gKwogICAgbXAuc3JjMzUoZ2F0ZSkgKiBhbXRfczM1X2QwOV9wYW4gKwogICAgbXAuc3JjMzYoZ2F0ZSkgKiBhbXRfczM2X2QwOV9wYW4gKwogICAgbXAuc3JjMzcoZ2F0ZSkgKiBhbXRfczM3X2QwOV9wYW4gKwogICAgbXAuc3JjMzgoZ2F0ZSkgKiBhbXRfczM4X2QwOV9wYW4gKwogICAgbXAuc3JjMzkoZ2F0ZSkgKiBhbXRfczM5X2QwOV9wYW4gKwogICAgbXAuc3JjNDAoZ2F0ZSkgKiBhbXRfczQwX2QwOV9wYW4gKwogICAgbXAuc3JjNDEoZ2F0ZSkgKiBhbXRfczQxX2QwOV9wYW4gKwogICAgbXAuc3JjNDIoZ2F0ZSkgKiBhbXRfczQyX2QwOV9wYW4gKwogICAgbXAuc3JjNDMoZ2F0ZSkgKiBhbXRfczQzX2QwOV9wYW4gKwogICAgbXAuc3JjNDQoZ2F0ZSkgKiBhbXRfczQ0X2QwOV9wYW4gKwogICAgbXAuc3JjNDUoZ2F0ZSkgKiBhbXRfczQ1X2QwOV9wYW4gKwogICAgbXAuc3JjNDYoZ2F0ZSkgKiBhbXRfczQ2X2QwOV9wYW4gKwogICAgbXAuc3JjNDcoZ2F0ZSkgKiBhbXRfczQ3X2QwOV9wYW47CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gRGVyaXZlZCBzaWduYWxzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKY2VudDJyYXRpbyhjKSA9IHBvdygyLjAsIGMgLyAxMjAwLjApOwpvY3QycmF0aW8obykgID0gcG93KDIuMCwgbyk7CnNlbWkycmF0aW8ocykgPSBwb3coMi4wLCBzIC8gMTIuMCk7CgovLyBQb3J0YW1lbnRvCmdsaWRlX3RhdSAgID0gMC4wMDAxICsgbWFzdGVyX2dsaWRlICogMS4wOwpmcmVxX2dsaWRlZCA9IGZyZXEgOiBzaS5zbW9vdGgoYmEudGF1MnBvbGUoZ2xpZGVfdGF1KSk7CgovLyBCYXNlIChnbGlkZSArIG1hc3RlciBmaW5lIHR1bmUpCmJhc2VfZnJlcSA9IGZyZXFfZ2xpZGVkICogY2VudDJyYXRpbyhtYXN0ZXJfdHVuZSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gUGVyLW9zY2lsbGF0b3Igd2F2ZXRhYmxlIChzYXcg4oaSIHRyaSDihpIgc3F1YXJlIGNyb3NzZmFkZSkKLy8gMC4wIOKGkiBzYXcsIDAuNSDihpIgdHJpYW5nbGUsIDEuMCDihpIgc3F1YXJlLiBPdXRwdXQgaW4gWy0xLCsxXS4KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCm9zY193YXZlKHNoYXBlLCBmKSA9CiAgICBiYS5pZihzaGFwZSA8IDAuNSwKICAgICAgICAgICgxLjAgLSBzaGFwZSoyLjApICogb3MubGZfc2F3KGYpICsgKHNoYXBlKjIuMCkgKiBvcy5sZl90cmlhbmdsZShmKSwKICAgICAgICAgICgxLjAgLSAoc2hhcGUtMC41KSoyLjApICogb3MubGZfdHJpYW5nbGUoZikgKyAoKHNoYXBlLTAuNSkqMi4wKSAqIG9zLmxmX3NxdWFyZXdhdmUoZikpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIE9zY2lsbGF0b3IgZnJlcXVlbmNpZXMgKHBpdGNoIG1vZHVsYXRpb246IDEgdW5pdCA9IDEyIHNlbWl0b25lcykKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCnBpdGNoX3JhdGlvID0gc2VtaTJyYXRpbyhtb2RfcGl0Y2goZ2F0ZSkgKiAxMi4wKTsKCm9zYzFfZnJlcSA9IGJhc2VfZnJlcSAqIG9jdDJyYXRpbyhvc2MxX3JhbmdlKSAqIHBpdGNoX3JhdGlvOwpvc2MyX2ZyZXEgPSBiYXNlX2ZyZXEgKiBvY3QycmF0aW8ob3NjMl9yYW5nZSkgKiBwaXRjaF9yYXRpbwogICAgICAgICAgKiBjZW50MnJhdGlvKG9zYzJfZGV0dW5lICsgbW9kX29zYzJfZGV0dW5lKGdhdGUpICogNTAuMCk7Cm9zYzNfZnJlcV90cmFja2VkICAgPSBiYXNlX2ZyZXEgKiBvY3QycmF0aW8ob3NjM19yYW5nZSkgKiBwaXRjaF9yYXRpbwogICAgICAgICAgICAgICAgICAgICogY2VudDJyYXRpbyhvc2MzX2RldHVuZSArIG1vZF9vc2MzX2RldHVuZShnYXRlKSAqIDUwLjApOwovLyBXaGVuIGtiX3RyYWNrPTAsIG9zYzMgYmVjb21lcyBhIGZyZWUtcnVubmluZyBzdWIvTEZPIHNvdXJjZSBhdCB+NTUgSHogKiBvY3QKb3NjM19mcmVxX3VudHJhY2tlZCA9IDU1LjAgKiBvY3QycmF0aW8ob3NjM19yYW5nZSkKICAgICAgICAgICAgICAgICAgICAqIGNlbnQycmF0aW8ob3NjM19kZXR1bmUgKyBtb2Rfb3NjM19kZXR1bmUoZ2F0ZSkgKiA1MC4wKTsKb3NjM19mcmVxID0gb3NjM19rYl90cmFjayAqIG9zYzNfZnJlcV90cmFja2VkICsgKDEuMCAtIG9zYzNfa2JfdHJhY2spICogb3NjM19mcmVxX3VudHJhY2tlZDsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBPc2NpbGxhdG9ycwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KbzEgPSBvc2Nfd2F2ZShvc2MxX3dhdmUsIG9zYzFfZnJlcSk7Cm8yID0gb3NjX3dhdmUob3NjMl93YXZlLCBvc2MyX2ZyZXEpOwpvMyA9IG9zY193YXZlKG9zYzNfd2F2ZSwgb3NjM19mcmVxKTsKCi8vIE9zYyBtaXggYmFsYW5jZTogLTEgPSBhbGwgb3NjMSwgKzEgPSBhbGwgb3NjMywgMCA9IGJvdGggZXF1YWwuIG9zYzIgdW5hZmZlY3RlZC4KbWl4X2JhbCA9IG1heCgtMS4wLCBtaW4oMS4wLCBtb2Rfb3NjX21peF9iYWwoZ2F0ZSkpKTsKbWl4X3QgICA9IChtaXhfYmFsICsgMS4wKSAqIDAuNTsgICAvLyAwLi4xCndfbzEgICAgPSAxLjAgLSBtaXhfdDsKd19vMyAgICA9IG1peF90OwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIE5vaXNlOiB3aGl0ZSAobm9pc2VfdHlwZTwwLjUpIC8gcGluayAoZWxzZSkKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCndoaXRlX25vaXNlID0gbm8ubm9pc2U7CnBpbmtfbm9pc2UgID0gbm8ucGlua19ub2lzZTsKbm9pc2VfcmF3ICAgPSBiYS5pZihub2lzZV90eXBlIDwgMC41LCB3aGl0ZV9ub2lzZSwgcGlua19ub2lzZSk7Cm5vaXNlX2x2bCAgID0gbWF4KDAuMCwgbWluKDEuMCwgbm9pc2VfbGV2ZWwgKyBtb2Rfbm9pc2VfbGV2ZWwoZ2F0ZSkpKTsKCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBNaXhlciBzdW0KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCm1peF9zdW0gPQogICAgbzEgKiBvc2MxX2xldmVsICogd19vMQogICsgbzIgKiBvc2MyX2xldmVsCiAgKyBvMyAqIG9zYzNfbGV2ZWwgKiB3X28zCiAgKyBub2lzZV9yYXcgKiBub2lzZV9sdmw7CgptaXhfZHJpdmVuID0gbWEudGFuaChtaXhfc3VtICogbWl4ZXJfZHJpdmUpOwoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEZpbHRlciDigJQgTW9vZyBsYWRkZXIuIEN1dG9mZiBpcyBtb2R1bGF0ZWQgaW4gb2N0YXZlcyAowrE1IG9jdCBwZXIgbW9kIHVuaXQpLgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KY3V0b2ZmX21vZF9vY3QgPSAobW9kX2N1dG9mZihnYXRlKSArIG1vZF9maWx0ZXJfZW52X2FtdChnYXRlKSkgKiA1LjA7CmtiX3NjYWxlICAgICAgID0gcG93KGZyZXFfZ2xpZGVkIC8gNDQwLjAsIGZpbHRlcl9rYik7ICAvLyAxLjAgYXQgQTQgd2hlbiBrYj0xCmVmZl9jdXRvZmZfcmF3ID0gY3V0b2ZmICoga2Jfc2NhbGUgKiBvY3QycmF0aW8oY3V0b2ZmX21vZF9vY3QpOwplZmZfY3V0b2ZmICAgICA9IG1heCgyMC4wLCBtaW4oMTgwMDAuMCwgZWZmX2N1dG9mZl9yYXcpKTsKZWZmX3JlcyAgICAgICAgPSBtYXgoMC4wLCBtaW4oMC45OSwgcmVzb25hbmNlICsgbW9kX3Jlc29uYW5jZShnYXRlKSkpOwoKZmlsdGVyZWQgPSBtaXhfZHJpdmVuIDogdmUubW9vZ192Y2YoZWZmX3JlcywgZWZmX2N1dG9mZik7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gQW1wIGFuZCBwYW4KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFZlbG9jaXR5IGdhaW46IGJsZW5kIDEuMCAobm8gdmVsb2NpdHkpIC0+IHZlbC4gUmVmZXJlbmNlZCBoZXJlIHNvIEZhdXN0Ci8vIGtlZXBzIHRoZSBfdmVsIGhpZGRlbiBwYXJhbSBhbGl2ZSBpbiB0aGUgSlNPTiBkZXNjcmlwdG9yLgp2ZWxfZ2FpbiA9ICgxLjAgLSAwLjMpICsgMC4zICogdmVsOyAgLy8gMzAlIHZlbG9jaXR5IHNlbnNpdGl2aXR5LCBmaXhlZAovLyBkMDhfYW1wIG1vZHVsYXRpb24gaXMgYWRkaXRpdmUgb24gdG9wIG9mIGJhc2VfYW1wIChwb3NpdGl2ZSBvbmx5KS4gQQovLyBmdWxseSB1bnRyYWluZWQgb3IgYWR2ZXJzYXJpYWwgbWF0cml4IHRoZXJlZm9yZSBjYW5ub3Qgc2lsZW5jZSB0aGUKLy8gdm9pY2Ugd2hpbGUgYmFzZV9hbXAgaXMgaGlnaCDigJQgd2hpY2ggaXMgdGhlIHNhZmV0eSB0aGUgIm1haW4gYW1wIGdhdGUKLy8gb24gYXQgYWxsIHRpbWVzIiBkZWZhdWx0IHJlbGllcyBvbi4gVXNlcnMgd2hvIHdhbnQgZW52ZWxvcGUtZ2F0ZWQKLy8gdm9pY2VzIHNldCBiYXNlX2FtcD0wIGFuZCByb3V0ZSBhIHBvc2l0aXZlIEFEU1IvTEZPIGFtb3VudC4KYW1wX3ZhbCA9IG1heCgwLjAsIG1pbigxLjAsIGJhc2VfYW1wICsgbWF4KDAuMCwgbW9kX2FtcChnYXRlKSkpKSAqIG1hc3Rlcl9sZXZlbCAqIHZlbF9nYWluOwpwYW5fdmFsID0gbWF4KC0xLjAsIG1pbigxLjAsIG1hc3Rlcl9wYW4gKyBtb2RfcGFuKGdhdGUpKSk7CgovLyBlcXVhbC1wb3dlciBwYW4KcGFuX2wgPSBjb3MoKHBhbl92YWwgKyAxLjApICogMC4yNSAqIG1hLlBJKTsKcGFuX3IgPSBzaW4oKHBhbl92YWwgKyAxLjApICogMC4yNSAqIG1hLlBJKTsKCnNpZ25hbCA9IGZpbHRlcmVkICogYW1wX3ZhbDsKc2lnbmFsX0wgPSBzaWduYWwgKiBwYW5fbDsKc2lnbmFsX1IgPSBzaWduYWwgKiBwYW5fcjsKCnByb2Nlc3MgPSBzaWduYWxfTCwgc2lnbmFsX1I7Cg==","inputs": 0,"outputs": 2,"meta": [ { "basics.lib/name": "Faust Basic Element Library" },{ "basics.lib/version": "1.21.0" },{ "compile_options": "-lang wasm -ct 1 -cn modular_subtractive -es 1 -mcd 16 -mdd 1024 -mdy 33 -single -ftz 0" },{ "envelopes.lib/adsr:author": "Yann Orlarey and Andrey Bundin" },{ "envelopes.lib/author": "GRAME" },{ "envelopes.lib/copyright": "GRAME" },{ "envelopes.lib/license": "LGPL with exception" },{ "envelopes.lib/name": "Faust Envelope Library" },{ "envelopes.lib/version": "1.3.0" },{ "filename": "modular-subtractive.dsp" },{ "filters.lib/fir:author": "Julius O. Smith III" },{ "filters.lib/fir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/fir:license": "MIT-style STK-4.3 license" },{ "filters.lib/iir:author": "Julius O. Smith III" },{ "filters.lib/iir:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/iir:license": "MIT-style STK-4.3 license" },{ "filters.lib/lowpass0_highpass1": "MIT-style STK-4.3 license" },{ "filters.lib/name": "Faust Filters Library" },{ "filters.lib/pole:author": "Julius O. Smith III" },{ "filters.lib/pole:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "filters.lib/pole:license": "MIT-style STK-4.3 license" },{ "filters.lib/version": "1.7.1" },{ "maths.lib/author": "GRAME" },{ "maths.lib/copyright": "GRAME" },{ "maths.lib/license": "LGPL with exception" },{ "maths.lib/name": "Faust Math Library" },{ "maths.lib/version": "2.8.1" },{ "name": "modular-subtractive" },{ "noises.lib/name": "Faust Noise Generator Library" },{ "noises.lib/version": "1.4.1" },{ "oscillators.lib/lf_sawpos:author": "Bart Brouns, revised by Stéphane Letz" },{ "oscillators.lib/lf_sawpos:licence": "STK-4.3" },{ "oscillators.lib/lf_triangle:author": "Bart Brouns" },{ "oscillators.lib/lf_triangle:licence": "STK-4.3" },{ "oscillators.lib/name": "Faust Oscillator Library" },{ "oscillators.lib/saw1:author": "Bart Brouns" },{ "oscillators.lib/saw1:licence": "STK-4.3" },{ "oscillators.lib/version": "1.6.0" },{ "platform.lib/name": "Generic Platform Library" },{ "platform.lib/version": "1.3.0" },{ "signals.lib/name": "Faust Signal Routing Library" },{ "signals.lib/version": "1.6.0" },{ "vaeffects.lib/moog_vcf:author": "Julius O. Smith III" },{ "vaeffects.lib/moog_vcf:copyright": "Copyright (C) 2003-2019 by Julius O. Smith III " },{ "vaeffects.lib/moog_vcf:license": "MIT-style STK-4.3 license" },{ "vaeffects.lib/name": "Faust Virtual Analog Filter Effect Library" },{ "vaeffects.lib/version": "1.3.0" }],"ui": [ {"type": "vgroup","label": "modular-subtractive","items": [ {"type": "hslider","label": "0_Hidden/_vel","varname": "fHslider225","shortname": "0_Hidden_vel","address": "/modular-subtractive/0_Hidden__vel","index": 263356,"meta": [{ "hidden": "1" }],"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "0_Hidden/freq","varname": "fHslider372","shortname": "0_Hidden_freq","address": "/modular-subtractive/0_Hidden_freq","index": 263952,"meta": [{ "hidden": "1" },{ "unit": "Hz" }],"init": 220,"min": 20,"max": 4000,"step": 0.01},{"type": "button","label": "0_Hidden/gate","varname": "fButton0","shortname": "0_Hidden_gate","address": "/modular-subtractive/0_Hidden_gate","index": 262932,"meta": [{ "hidden": "1" }]},{"type": "hslider","label": "1_Oscillators/00_osc1_wave","varname": "fHslider676","shortname": "1_Oscillators_00_osc1_wave","address": "/modular-subtractive/1_Oscillators_00_osc1_wave","index": 265220,"meta": [{ "tooltip": "0=saw,0.5=tri,1=square" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/01_osc1_range","varname": "fHslider677","shortname": "1_Oscillators_01_osc1_range","address": "/modular-subtractive/1_Oscillators_01_osc1_range","index": 265224,"meta": [{ "unit": "oct" }],"init": 0,"min": -2,"max": 2,"step": 0.01},{"type": "hslider","label": "1_Oscillators/02_osc1_level","varname": "fHslider678","shortname": "1_Oscillators_02_osc1_level","address": "/modular-subtractive/1_Oscillators_02_osc1_level","index": 265236,"init": 0.8,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/03_osc2_wave","varname": "fHslider624","shortname": "1_Oscillators_03_osc2_wave","address": "/modular-subtractive/1_Oscillators_03_osc2_wave","index": 265004,"meta": [{ "tooltip": "0=saw,0.5=tri,1=square" }],"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/04_osc2_range","varname": "fHslider674","shortname": "1_Oscillators_04_osc2_range","address": "/modular-subtractive/1_Oscillators_04_osc2_range","index": 265204,"meta": [{ "unit": "oct" }],"init": 0,"min": -2,"max": 2,"step": 0.01},{"type": "hslider","label": "1_Oscillators/05_osc2_detune","varname": "fHslider673","shortname": "1_Oscillators_05_osc2_detune","address": "/modular-subtractive/1_Oscillators_05_osc2_detune","index": 265200,"meta": [{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1},{"type": "hslider","label": "1_Oscillators/06_osc2_level","varname": "fHslider675","shortname": "1_Oscillators_06_osc2_level","address": "/modular-subtractive/1_Oscillators_06_osc2_level","index": 265216,"init": 0.6,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/07_osc3_wave","varname": "fHslider474","shortname": "1_Oscillators_07_osc3_wave","address": "/modular-subtractive/1_Oscillators_07_osc3_wave","index": 264396,"meta": [{ "tooltip": "0=saw,0.5=tri,1=square" }],"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/08_osc3_range","varname": "fHslider574","shortname": "1_Oscillators_08_osc3_range","address": "/modular-subtractive/1_Oscillators_08_osc3_range","index": 264796,"meta": [{ "unit": "oct" }],"init": -1,"min": -2,"max": 2,"step": 0.01},{"type": "hslider","label": "1_Oscillators/09_osc3_detune","varname": "fHslider573","shortname": "1_Oscillators_09_osc3_detune","address": "/modular-subtractive/1_Oscillators_09_osc3_detune","index": 264792,"meta": [{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1},{"type": "hslider","label": "1_Oscillators/10_osc3_level","varname": "fHslider623","shortname": "1_Oscillators_10_osc3_level","address": "/modular-subtractive/1_Oscillators_10_osc3_level","index": 265000,"init": 0.4,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "1_Oscillators/11_osc3_kb_track","varname": "fHslider524","shortname": "1_Oscillators_11_osc3_kb_track","address": "/modular-subtractive/1_Oscillators_11_osc3_kb_track","index": 264596,"meta": [{ "tooltip": "1=tracks keyboard,0=LFO" }],"init": 1,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "2_Mixer/00_noise_type","varname": "fHslider473","shortname": "2_Mixer_00_noise_type","address": "/modular-subtractive/2_Mixer_00_noise_type","index": 264368,"meta": [{ "tooltip": "0=white,1=pink" }],"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "2_Mixer/01_noise_level","varname": "fHslider472","shortname": "2_Mixer_01_noise_level","address": "/modular-subtractive/2_Mixer_01_noise_level","index": 264364,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "2_Mixer/02_mixer_drive","varname": "fHslider679","shortname": "2_Mixer_02_mixer_drive","address": "/modular-subtractive/2_Mixer_02_mixer_drive","index": 265240,"meta": [{ "tooltip": "Pre-filter overdrive" }],"init": 1,"min": 0.5,"max": 4,"step": 0.001},{"type": "hslider","label": "3_Filter/00_cutoff","varname": "fHslider374","shortname": "3_Filter_00_cutoff","address": "/modular-subtractive/3_Filter_00_cutoff","index": 263968,"meta": [{ "scale": "log" },{ "unit": "Hz" }],"init": 1200,"min": 20,"max": 20000,"step": 0.1},{"type": "hslider","label": "3_Filter/01_resonance","varname": "fHslider423","shortname": "3_Filter_01_resonance","address": "/modular-subtractive/3_Filter_01_resonance","index": 264168,"init": 0.3,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "3_Filter/02_filter_kb_track","varname": "fHslider373","shortname": "3_Filter_02_filter_kb_track","address": "/modular-subtractive/3_Filter_02_filter_kb_track","index": 263964,"init": 0.5,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/00_master_level","varname": "fHslider680","shortname": "4_Master_00_master_level","address": "/modular-subtractive/4_Master_00_master_level","index": 265284,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/01_master_glide","varname": "fHslider371","shortname": "4_Master_01_master_glide","address": "/modular-subtractive/4_Master_01_master_glide","index": 263948,"meta": [{ "scale": "log" },{ "unit": "s" }],"init": 0,"min": 0,"max": 2,"step": 0.001},{"type": "hslider","label": "4_Master/02_master_tune","varname": "fHslider523","shortname": "4_Master_02_master_tune","address": "/modular-subtractive/4_Master_02_master_tune","index": 264592,"meta": [{ "unit": "ct" }],"init": 0,"min": -50,"max": 50,"step": 0.1},{"type": "hslider","label": "4_Master/03_master_pan","varname": "fHslider224","shortname": "4_Master_03_master_pan","address": "/modular-subtractive/4_Master_03_master_pan","index": 263344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "4_Master/04_base_amp","varname": "fHslider274","shortname": "4_Master_04_base_amp","address": "/modular-subtractive/4_Master_04_base_amp","index": 263560,"init": 1,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_attack","varname": "fHslider219","shortname": "MM_ADSR_00_adsr01_attack","address": "/modular-subtractive/MM_ADSR_00_adsr01_attack","index": 263324,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_decay","varname": "fHslider220","shortname": "MM_ADSR_00_adsr01_decay","address": "/modular-subtractive/MM_ADSR_00_adsr01_decay","index": 263328,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_enable","varname": "fHslider223","shortname": "MM_ADSR_00_adsr01_enable","address": "/modular-subtractive/MM_ADSR_00_adsr01_enable","index": 263340,"init": 1,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/00_adsr01_release","varname": "fHslider218","shortname": "MM_ADSR_00_adsr01_release","address": "/modular-subtractive/MM_ADSR_00_adsr01_release","index": 263320,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/00_adsr01_sustain","varname": "fHslider221","shortname": "MM_ADSR_00_adsr01_sustain","address": "/modular-subtractive/MM_ADSR_00_adsr01_sustain","index": 263332,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_attack","varname": "fHslider213","shortname": "MM_ADSR_01_adsr02_attack","address": "/modular-subtractive/MM_ADSR_01_adsr02_attack","index": 263300,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_decay","varname": "fHslider214","shortname": "MM_ADSR_01_adsr02_decay","address": "/modular-subtractive/MM_ADSR_01_adsr02_decay","index": 263304,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_enable","varname": "fHslider217","shortname": "MM_ADSR_01_adsr02_enable","address": "/modular-subtractive/MM_ADSR_01_adsr02_enable","index": 263316,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/01_adsr02_release","varname": "fHslider212","shortname": "MM_ADSR_01_adsr02_release","address": "/modular-subtractive/MM_ADSR_01_adsr02_release","index": 263296,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/01_adsr02_sustain","varname": "fHslider215","shortname": "MM_ADSR_01_adsr02_sustain","address": "/modular-subtractive/MM_ADSR_01_adsr02_sustain","index": 263308,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_attack","varname": "fHslider207","shortname": "MM_ADSR_02_adsr03_attack","address": "/modular-subtractive/MM_ADSR_02_adsr03_attack","index": 263276,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_decay","varname": "fHslider208","shortname": "MM_ADSR_02_adsr03_decay","address": "/modular-subtractive/MM_ADSR_02_adsr03_decay","index": 263280,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_enable","varname": "fHslider211","shortname": "MM_ADSR_02_adsr03_enable","address": "/modular-subtractive/MM_ADSR_02_adsr03_enable","index": 263292,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/02_adsr03_release","varname": "fHslider206","shortname": "MM_ADSR_02_adsr03_release","address": "/modular-subtractive/MM_ADSR_02_adsr03_release","index": 263272,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/02_adsr03_sustain","varname": "fHslider209","shortname": "MM_ADSR_02_adsr03_sustain","address": "/modular-subtractive/MM_ADSR_02_adsr03_sustain","index": 263284,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_attack","varname": "fHslider201","shortname": "MM_ADSR_03_adsr04_attack","address": "/modular-subtractive/MM_ADSR_03_adsr04_attack","index": 263252,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_decay","varname": "fHslider202","shortname": "MM_ADSR_03_adsr04_decay","address": "/modular-subtractive/MM_ADSR_03_adsr04_decay","index": 263256,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_enable","varname": "fHslider205","shortname": "MM_ADSR_03_adsr04_enable","address": "/modular-subtractive/MM_ADSR_03_adsr04_enable","index": 263268,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/03_adsr04_release","varname": "fHslider200","shortname": "MM_ADSR_03_adsr04_release","address": "/modular-subtractive/MM_ADSR_03_adsr04_release","index": 263248,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/03_adsr04_sustain","varname": "fHslider203","shortname": "MM_ADSR_03_adsr04_sustain","address": "/modular-subtractive/MM_ADSR_03_adsr04_sustain","index": 263260,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_attack","varname": "fHslider195","shortname": "MM_ADSR_04_adsr05_attack","address": "/modular-subtractive/MM_ADSR_04_adsr05_attack","index": 263228,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_decay","varname": "fHslider196","shortname": "MM_ADSR_04_adsr05_decay","address": "/modular-subtractive/MM_ADSR_04_adsr05_decay","index": 263232,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_enable","varname": "fHslider199","shortname": "MM_ADSR_04_adsr05_enable","address": "/modular-subtractive/MM_ADSR_04_adsr05_enable","index": 263244,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/04_adsr05_release","varname": "fHslider194","shortname": "MM_ADSR_04_adsr05_release","address": "/modular-subtractive/MM_ADSR_04_adsr05_release","index": 263224,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/04_adsr05_sustain","varname": "fHslider197","shortname": "MM_ADSR_04_adsr05_sustain","address": "/modular-subtractive/MM_ADSR_04_adsr05_sustain","index": 263236,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_attack","varname": "fHslider189","shortname": "MM_ADSR_05_adsr06_attack","address": "/modular-subtractive/MM_ADSR_05_adsr06_attack","index": 263204,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_decay","varname": "fHslider190","shortname": "MM_ADSR_05_adsr06_decay","address": "/modular-subtractive/MM_ADSR_05_adsr06_decay","index": 263208,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_enable","varname": "fHslider193","shortname": "MM_ADSR_05_adsr06_enable","address": "/modular-subtractive/MM_ADSR_05_adsr06_enable","index": 263220,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/05_adsr06_release","varname": "fHslider188","shortname": "MM_ADSR_05_adsr06_release","address": "/modular-subtractive/MM_ADSR_05_adsr06_release","index": 263200,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/05_adsr06_sustain","varname": "fHslider191","shortname": "MM_ADSR_05_adsr06_sustain","address": "/modular-subtractive/MM_ADSR_05_adsr06_sustain","index": 263212,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_attack","varname": "fHslider183","shortname": "MM_ADSR_06_adsr07_attack","address": "/modular-subtractive/MM_ADSR_06_adsr07_attack","index": 263180,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_decay","varname": "fHslider184","shortname": "MM_ADSR_06_adsr07_decay","address": "/modular-subtractive/MM_ADSR_06_adsr07_decay","index": 263184,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_enable","varname": "fHslider187","shortname": "MM_ADSR_06_adsr07_enable","address": "/modular-subtractive/MM_ADSR_06_adsr07_enable","index": 263196,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/06_adsr07_release","varname": "fHslider182","shortname": "MM_ADSR_06_adsr07_release","address": "/modular-subtractive/MM_ADSR_06_adsr07_release","index": 263176,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/06_adsr07_sustain","varname": "fHslider185","shortname": "MM_ADSR_06_adsr07_sustain","address": "/modular-subtractive/MM_ADSR_06_adsr07_sustain","index": 263188,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_attack","varname": "fHslider177","shortname": "MM_ADSR_07_adsr08_attack","address": "/modular-subtractive/MM_ADSR_07_adsr08_attack","index": 263156,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_decay","varname": "fHslider178","shortname": "MM_ADSR_07_adsr08_decay","address": "/modular-subtractive/MM_ADSR_07_adsr08_decay","index": 263160,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_enable","varname": "fHslider181","shortname": "MM_ADSR_07_adsr08_enable","address": "/modular-subtractive/MM_ADSR_07_adsr08_enable","index": 263172,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/07_adsr08_release","varname": "fHslider176","shortname": "MM_ADSR_07_adsr08_release","address": "/modular-subtractive/MM_ADSR_07_adsr08_release","index": 263152,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/07_adsr08_sustain","varname": "fHslider179","shortname": "MM_ADSR_07_adsr08_sustain","address": "/modular-subtractive/MM_ADSR_07_adsr08_sustain","index": 263164,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_attack","varname": "fHslider171","shortname": "MM_ADSR_08_adsr09_attack","address": "/modular-subtractive/MM_ADSR_08_adsr09_attack","index": 263132,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_decay","varname": "fHslider172","shortname": "MM_ADSR_08_adsr09_decay","address": "/modular-subtractive/MM_ADSR_08_adsr09_decay","index": 263136,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_enable","varname": "fHslider175","shortname": "MM_ADSR_08_adsr09_enable","address": "/modular-subtractive/MM_ADSR_08_adsr09_enable","index": 263148,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/08_adsr09_release","varname": "fHslider170","shortname": "MM_ADSR_08_adsr09_release","address": "/modular-subtractive/MM_ADSR_08_adsr09_release","index": 263128,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/08_adsr09_sustain","varname": "fHslider173","shortname": "MM_ADSR_08_adsr09_sustain","address": "/modular-subtractive/MM_ADSR_08_adsr09_sustain","index": 263140,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_attack","varname": "fHslider165","shortname": "MM_ADSR_09_adsr10_attack","address": "/modular-subtractive/MM_ADSR_09_adsr10_attack","index": 263108,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_decay","varname": "fHslider166","shortname": "MM_ADSR_09_adsr10_decay","address": "/modular-subtractive/MM_ADSR_09_adsr10_decay","index": 263112,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_enable","varname": "fHslider169","shortname": "MM_ADSR_09_adsr10_enable","address": "/modular-subtractive/MM_ADSR_09_adsr10_enable","index": 263124,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/09_adsr10_release","varname": "fHslider164","shortname": "MM_ADSR_09_adsr10_release","address": "/modular-subtractive/MM_ADSR_09_adsr10_release","index": 263104,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/09_adsr10_sustain","varname": "fHslider167","shortname": "MM_ADSR_09_adsr10_sustain","address": "/modular-subtractive/MM_ADSR_09_adsr10_sustain","index": 263116,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_attack","varname": "fHslider159","shortname": "MM_ADSR_10_adsr11_attack","address": "/modular-subtractive/MM_ADSR_10_adsr11_attack","index": 263084,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_decay","varname": "fHslider160","shortname": "MM_ADSR_10_adsr11_decay","address": "/modular-subtractive/MM_ADSR_10_adsr11_decay","index": 263088,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_enable","varname": "fHslider163","shortname": "MM_ADSR_10_adsr11_enable","address": "/modular-subtractive/MM_ADSR_10_adsr11_enable","index": 263100,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/10_adsr11_release","varname": "fHslider158","shortname": "MM_ADSR_10_adsr11_release","address": "/modular-subtractive/MM_ADSR_10_adsr11_release","index": 263080,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/10_adsr11_sustain","varname": "fHslider161","shortname": "MM_ADSR_10_adsr11_sustain","address": "/modular-subtractive/MM_ADSR_10_adsr11_sustain","index": 263092,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_attack","varname": "fHslider153","shortname": "MM_ADSR_11_adsr12_attack","address": "/modular-subtractive/MM_ADSR_11_adsr12_attack","index": 263060,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_decay","varname": "fHslider154","shortname": "MM_ADSR_11_adsr12_decay","address": "/modular-subtractive/MM_ADSR_11_adsr12_decay","index": 263064,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_enable","varname": "fHslider157","shortname": "MM_ADSR_11_adsr12_enable","address": "/modular-subtractive/MM_ADSR_11_adsr12_enable","index": 263076,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/11_adsr12_release","varname": "fHslider152","shortname": "MM_ADSR_11_adsr12_release","address": "/modular-subtractive/MM_ADSR_11_adsr12_release","index": 263056,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/11_adsr12_sustain","varname": "fHslider155","shortname": "MM_ADSR_11_adsr12_sustain","address": "/modular-subtractive/MM_ADSR_11_adsr12_sustain","index": 263068,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_attack","varname": "fHslider147","shortname": "MM_ADSR_12_adsr13_attack","address": "/modular-subtractive/MM_ADSR_12_adsr13_attack","index": 263036,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_decay","varname": "fHslider148","shortname": "MM_ADSR_12_adsr13_decay","address": "/modular-subtractive/MM_ADSR_12_adsr13_decay","index": 263040,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_enable","varname": "fHslider151","shortname": "MM_ADSR_12_adsr13_enable","address": "/modular-subtractive/MM_ADSR_12_adsr13_enable","index": 263052,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/12_adsr13_release","varname": "fHslider146","shortname": "MM_ADSR_12_adsr13_release","address": "/modular-subtractive/MM_ADSR_12_adsr13_release","index": 263032,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/12_adsr13_sustain","varname": "fHslider149","shortname": "MM_ADSR_12_adsr13_sustain","address": "/modular-subtractive/MM_ADSR_12_adsr13_sustain","index": 263044,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_attack","varname": "fHslider141","shortname": "MM_ADSR_13_adsr14_attack","address": "/modular-subtractive/MM_ADSR_13_adsr14_attack","index": 263012,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_decay","varname": "fHslider142","shortname": "MM_ADSR_13_adsr14_decay","address": "/modular-subtractive/MM_ADSR_13_adsr14_decay","index": 263016,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_enable","varname": "fHslider145","shortname": "MM_ADSR_13_adsr14_enable","address": "/modular-subtractive/MM_ADSR_13_adsr14_enable","index": 263028,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/13_adsr14_release","varname": "fHslider140","shortname": "MM_ADSR_13_adsr14_release","address": "/modular-subtractive/MM_ADSR_13_adsr14_release","index": 263008,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/13_adsr14_sustain","varname": "fHslider143","shortname": "MM_ADSR_13_adsr14_sustain","address": "/modular-subtractive/MM_ADSR_13_adsr14_sustain","index": 263020,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_attack","varname": "fHslider135","shortname": "MM_ADSR_14_adsr15_attack","address": "/modular-subtractive/MM_ADSR_14_adsr15_attack","index": 262988,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_decay","varname": "fHslider136","shortname": "MM_ADSR_14_adsr15_decay","address": "/modular-subtractive/MM_ADSR_14_adsr15_decay","index": 262992,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_enable","varname": "fHslider139","shortname": "MM_ADSR_14_adsr15_enable","address": "/modular-subtractive/MM_ADSR_14_adsr15_enable","index": 263004,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/14_adsr15_release","varname": "fHslider134","shortname": "MM_ADSR_14_adsr15_release","address": "/modular-subtractive/MM_ADSR_14_adsr15_release","index": 262984,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/14_adsr15_sustain","varname": "fHslider137","shortname": "MM_ADSR_14_adsr15_sustain","address": "/modular-subtractive/MM_ADSR_14_adsr15_sustain","index": 262996,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_attack","varname": "fHslider129","shortname": "MM_ADSR_15_adsr16_attack","address": "/modular-subtractive/MM_ADSR_15_adsr16_attack","index": 262964,"meta": [{ "scale": "log" }],"init": 0.01,"min": 0.001,"max": 5,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_decay","varname": "fHslider130","shortname": "MM_ADSR_15_adsr16_decay","address": "/modular-subtractive/MM_ADSR_15_adsr16_decay","index": 262968,"init": 0.2,"min": 0.001,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_enable","varname": "fHslider133","shortname": "MM_ADSR_15_adsr16_enable","address": "/modular-subtractive/MM_ADSR_15_adsr16_enable","index": 262980,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_ADSR/15_adsr16_release","varname": "fHslider128","shortname": "MM_ADSR_15_adsr16_release","address": "/modular-subtractive/MM_ADSR_15_adsr16_release","index": 262952,"init": 0.3,"min": 0.01,"max": 10,"step": 0.001},{"type": "hslider","label": "MM_ADSR/15_adsr16_sustain","varname": "fHslider131","shortname": "MM_ADSR_15_adsr16_sustain","address": "/modular-subtractive/MM_ADSR_15_adsr16_sustain","index": 262972,"init": 0.7,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_enable","varname": "fHslider127","shortname": "MM_LFO_00_lfo01_enable","address": "/modular-subtractive/MM_LFO_00_lfo01_enable","index": 262928,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/00_lfo01_morph","varname": "fHslider124","shortname": "MM_LFO_00_lfo01_morph","address": "/modular-subtractive/MM_LFO_00_lfo01_morph","index": 262908,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/00_lfo01_rate","varname": "fHslider125","shortname": "MM_LFO_00_lfo01_rate","address": "/modular-subtractive/MM_LFO_00_lfo01_rate","index": 262912,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_enable","varname": "fHslider123","shortname": "MM_LFO_01_lfo02_enable","address": "/modular-subtractive/MM_LFO_01_lfo02_enable","index": 262904,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/01_lfo02_morph","varname": "fHslider120","shortname": "MM_LFO_01_lfo02_morph","address": "/modular-subtractive/MM_LFO_01_lfo02_morph","index": 262884,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/01_lfo02_rate","varname": "fHslider121","shortname": "MM_LFO_01_lfo02_rate","address": "/modular-subtractive/MM_LFO_01_lfo02_rate","index": 262888,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_enable","varname": "fHslider119","shortname": "MM_LFO_02_lfo03_enable","address": "/modular-subtractive/MM_LFO_02_lfo03_enable","index": 262880,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/02_lfo03_morph","varname": "fHslider116","shortname": "MM_LFO_02_lfo03_morph","address": "/modular-subtractive/MM_LFO_02_lfo03_morph","index": 262860,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/02_lfo03_rate","varname": "fHslider117","shortname": "MM_LFO_02_lfo03_rate","address": "/modular-subtractive/MM_LFO_02_lfo03_rate","index": 262864,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_enable","varname": "fHslider115","shortname": "MM_LFO_03_lfo04_enable","address": "/modular-subtractive/MM_LFO_03_lfo04_enable","index": 262856,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/03_lfo04_morph","varname": "fHslider112","shortname": "MM_LFO_03_lfo04_morph","address": "/modular-subtractive/MM_LFO_03_lfo04_morph","index": 262836,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/03_lfo04_rate","varname": "fHslider113","shortname": "MM_LFO_03_lfo04_rate","address": "/modular-subtractive/MM_LFO_03_lfo04_rate","index": 262840,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_enable","varname": "fHslider111","shortname": "MM_LFO_04_lfo05_enable","address": "/modular-subtractive/MM_LFO_04_lfo05_enable","index": 262832,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/04_lfo05_morph","varname": "fHslider108","shortname": "MM_LFO_04_lfo05_morph","address": "/modular-subtractive/MM_LFO_04_lfo05_morph","index": 262812,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/04_lfo05_rate","varname": "fHslider109","shortname": "MM_LFO_04_lfo05_rate","address": "/modular-subtractive/MM_LFO_04_lfo05_rate","index": 262816,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_enable","varname": "fHslider107","shortname": "MM_LFO_05_lfo06_enable","address": "/modular-subtractive/MM_LFO_05_lfo06_enable","index": 262808,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/05_lfo06_morph","varname": "fHslider104","shortname": "MM_LFO_05_lfo06_morph","address": "/modular-subtractive/MM_LFO_05_lfo06_morph","index": 262788,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/05_lfo06_rate","varname": "fHslider105","shortname": "MM_LFO_05_lfo06_rate","address": "/modular-subtractive/MM_LFO_05_lfo06_rate","index": 262792,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_enable","varname": "fHslider103","shortname": "MM_LFO_06_lfo07_enable","address": "/modular-subtractive/MM_LFO_06_lfo07_enable","index": 262784,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/06_lfo07_morph","varname": "fHslider100","shortname": "MM_LFO_06_lfo07_morph","address": "/modular-subtractive/MM_LFO_06_lfo07_morph","index": 262764,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/06_lfo07_rate","varname": "fHslider101","shortname": "MM_LFO_06_lfo07_rate","address": "/modular-subtractive/MM_LFO_06_lfo07_rate","index": 262768,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_enable","varname": "fHslider99","shortname": "MM_LFO_07_lfo08_enable","address": "/modular-subtractive/MM_LFO_07_lfo08_enable","index": 262760,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/07_lfo08_morph","varname": "fHslider96","shortname": "MM_LFO_07_lfo08_morph","address": "/modular-subtractive/MM_LFO_07_lfo08_morph","index": 262740,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/07_lfo08_rate","varname": "fHslider97","shortname": "MM_LFO_07_lfo08_rate","address": "/modular-subtractive/MM_LFO_07_lfo08_rate","index": 262744,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_enable","varname": "fHslider95","shortname": "MM_LFO_08_lfo09_enable","address": "/modular-subtractive/MM_LFO_08_lfo09_enable","index": 262736,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/08_lfo09_morph","varname": "fHslider92","shortname": "MM_LFO_08_lfo09_morph","address": "/modular-subtractive/MM_LFO_08_lfo09_morph","index": 262716,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/08_lfo09_rate","varname": "fHslider93","shortname": "MM_LFO_08_lfo09_rate","address": "/modular-subtractive/MM_LFO_08_lfo09_rate","index": 262720,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_enable","varname": "fHslider91","shortname": "MM_LFO_09_lfo10_enable","address": "/modular-subtractive/MM_LFO_09_lfo10_enable","index": 262712,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/09_lfo10_morph","varname": "fHslider88","shortname": "MM_LFO_09_lfo10_morph","address": "/modular-subtractive/MM_LFO_09_lfo10_morph","index": 262692,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/09_lfo10_rate","varname": "fHslider89","shortname": "MM_LFO_09_lfo10_rate","address": "/modular-subtractive/MM_LFO_09_lfo10_rate","index": 262696,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_enable","varname": "fHslider87","shortname": "MM_LFO_10_lfo11_enable","address": "/modular-subtractive/MM_LFO_10_lfo11_enable","index": 262688,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/10_lfo11_morph","varname": "fHslider84","shortname": "MM_LFO_10_lfo11_morph","address": "/modular-subtractive/MM_LFO_10_lfo11_morph","index": 262668,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/10_lfo11_rate","varname": "fHslider85","shortname": "MM_LFO_10_lfo11_rate","address": "/modular-subtractive/MM_LFO_10_lfo11_rate","index": 262672,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_enable","varname": "fHslider83","shortname": "MM_LFO_11_lfo12_enable","address": "/modular-subtractive/MM_LFO_11_lfo12_enable","index": 262664,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/11_lfo12_morph","varname": "fHslider80","shortname": "MM_LFO_11_lfo12_morph","address": "/modular-subtractive/MM_LFO_11_lfo12_morph","index": 262644,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/11_lfo12_rate","varname": "fHslider81","shortname": "MM_LFO_11_lfo12_rate","address": "/modular-subtractive/MM_LFO_11_lfo12_rate","index": 262648,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_enable","varname": "fHslider79","shortname": "MM_LFO_12_lfo13_enable","address": "/modular-subtractive/MM_LFO_12_lfo13_enable","index": 262640,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/12_lfo13_morph","varname": "fHslider76","shortname": "MM_LFO_12_lfo13_morph","address": "/modular-subtractive/MM_LFO_12_lfo13_morph","index": 262620,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/12_lfo13_rate","varname": "fHslider77","shortname": "MM_LFO_12_lfo13_rate","address": "/modular-subtractive/MM_LFO_12_lfo13_rate","index": 262624,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_enable","varname": "fHslider75","shortname": "MM_LFO_13_lfo14_enable","address": "/modular-subtractive/MM_LFO_13_lfo14_enable","index": 262616,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/13_lfo14_morph","varname": "fHslider72","shortname": "MM_LFO_13_lfo14_morph","address": "/modular-subtractive/MM_LFO_13_lfo14_morph","index": 262596,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/13_lfo14_rate","varname": "fHslider73","shortname": "MM_LFO_13_lfo14_rate","address": "/modular-subtractive/MM_LFO_13_lfo14_rate","index": 262600,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_enable","varname": "fHslider71","shortname": "MM_LFO_14_lfo15_enable","address": "/modular-subtractive/MM_LFO_14_lfo15_enable","index": 262592,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/14_lfo15_morph","varname": "fHslider68","shortname": "MM_LFO_14_lfo15_morph","address": "/modular-subtractive/MM_LFO_14_lfo15_morph","index": 262572,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/14_lfo15_rate","varname": "fHslider69","shortname": "MM_LFO_14_lfo15_rate","address": "/modular-subtractive/MM_LFO_14_lfo15_rate","index": 262576,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_enable","varname": "fHslider67","shortname": "MM_LFO_15_lfo16_enable","address": "/modular-subtractive/MM_LFO_15_lfo16_enable","index": 262568,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/15_lfo16_morph","varname": "fHslider64","shortname": "MM_LFO_15_lfo16_morph","address": "/modular-subtractive/MM_LFO_15_lfo16_morph","index": 262548,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/15_lfo16_rate","varname": "fHslider65","shortname": "MM_LFO_15_lfo16_rate","address": "/modular-subtractive/MM_LFO_15_lfo16_rate","index": 262552,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_enable","varname": "fHslider63","shortname": "MM_LFO_16_lfo17_enable","address": "/modular-subtractive/MM_LFO_16_lfo17_enable","index": 262544,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/16_lfo17_morph","varname": "fHslider60","shortname": "MM_LFO_16_lfo17_morph","address": "/modular-subtractive/MM_LFO_16_lfo17_morph","index": 262524,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/16_lfo17_rate","varname": "fHslider61","shortname": "MM_LFO_16_lfo17_rate","address": "/modular-subtractive/MM_LFO_16_lfo17_rate","index": 262528,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_enable","varname": "fHslider59","shortname": "MM_LFO_17_lfo18_enable","address": "/modular-subtractive/MM_LFO_17_lfo18_enable","index": 262520,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/17_lfo18_morph","varname": "fHslider56","shortname": "MM_LFO_17_lfo18_morph","address": "/modular-subtractive/MM_LFO_17_lfo18_morph","index": 262500,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/17_lfo18_rate","varname": "fHslider57","shortname": "MM_LFO_17_lfo18_rate","address": "/modular-subtractive/MM_LFO_17_lfo18_rate","index": 262504,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_enable","varname": "fHslider55","shortname": "MM_LFO_18_lfo19_enable","address": "/modular-subtractive/MM_LFO_18_lfo19_enable","index": 262496,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/18_lfo19_morph","varname": "fHslider52","shortname": "MM_LFO_18_lfo19_morph","address": "/modular-subtractive/MM_LFO_18_lfo19_morph","index": 262476,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/18_lfo19_rate","varname": "fHslider53","shortname": "MM_LFO_18_lfo19_rate","address": "/modular-subtractive/MM_LFO_18_lfo19_rate","index": 262480,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_enable","varname": "fHslider51","shortname": "MM_LFO_19_lfo20_enable","address": "/modular-subtractive/MM_LFO_19_lfo20_enable","index": 262472,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/19_lfo20_morph","varname": "fHslider48","shortname": "MM_LFO_19_lfo20_morph","address": "/modular-subtractive/MM_LFO_19_lfo20_morph","index": 262452,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/19_lfo20_rate","varname": "fHslider49","shortname": "MM_LFO_19_lfo20_rate","address": "/modular-subtractive/MM_LFO_19_lfo20_rate","index": 262456,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_enable","varname": "fHslider47","shortname": "MM_LFO_20_lfo21_enable","address": "/modular-subtractive/MM_LFO_20_lfo21_enable","index": 262448,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/20_lfo21_morph","varname": "fHslider44","shortname": "MM_LFO_20_lfo21_morph","address": "/modular-subtractive/MM_LFO_20_lfo21_morph","index": 262428,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/20_lfo21_rate","varname": "fHslider45","shortname": "MM_LFO_20_lfo21_rate","address": "/modular-subtractive/MM_LFO_20_lfo21_rate","index": 262432,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_enable","varname": "fHslider43","shortname": "MM_LFO_21_lfo22_enable","address": "/modular-subtractive/MM_LFO_21_lfo22_enable","index": 262424,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/21_lfo22_morph","varname": "fHslider40","shortname": "MM_LFO_21_lfo22_morph","address": "/modular-subtractive/MM_LFO_21_lfo22_morph","index": 262404,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/21_lfo22_rate","varname": "fHslider41","shortname": "MM_LFO_21_lfo22_rate","address": "/modular-subtractive/MM_LFO_21_lfo22_rate","index": 262408,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_enable","varname": "fHslider39","shortname": "MM_LFO_22_lfo23_enable","address": "/modular-subtractive/MM_LFO_22_lfo23_enable","index": 262400,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/22_lfo23_morph","varname": "fHslider36","shortname": "MM_LFO_22_lfo23_morph","address": "/modular-subtractive/MM_LFO_22_lfo23_morph","index": 262380,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/22_lfo23_rate","varname": "fHslider37","shortname": "MM_LFO_22_lfo23_rate","address": "/modular-subtractive/MM_LFO_22_lfo23_rate","index": 262384,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_enable","varname": "fHslider35","shortname": "MM_LFO_23_lfo24_enable","address": "/modular-subtractive/MM_LFO_23_lfo24_enable","index": 262376,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/23_lfo24_morph","varname": "fHslider32","shortname": "MM_LFO_23_lfo24_morph","address": "/modular-subtractive/MM_LFO_23_lfo24_morph","index": 262356,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/23_lfo24_rate","varname": "fHslider33","shortname": "MM_LFO_23_lfo24_rate","address": "/modular-subtractive/MM_LFO_23_lfo24_rate","index": 262360,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_enable","varname": "fHslider31","shortname": "MM_LFO_24_lfo25_enable","address": "/modular-subtractive/MM_LFO_24_lfo25_enable","index": 262352,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/24_lfo25_morph","varname": "fHslider28","shortname": "MM_LFO_24_lfo25_morph","address": "/modular-subtractive/MM_LFO_24_lfo25_morph","index": 262332,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/24_lfo25_rate","varname": "fHslider29","shortname": "MM_LFO_24_lfo25_rate","address": "/modular-subtractive/MM_LFO_24_lfo25_rate","index": 262336,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_enable","varname": "fHslider27","shortname": "MM_LFO_25_lfo26_enable","address": "/modular-subtractive/MM_LFO_25_lfo26_enable","index": 262328,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/25_lfo26_morph","varname": "fHslider24","shortname": "MM_LFO_25_lfo26_morph","address": "/modular-subtractive/MM_LFO_25_lfo26_morph","index": 262308,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/25_lfo26_rate","varname": "fHslider25","shortname": "MM_LFO_25_lfo26_rate","address": "/modular-subtractive/MM_LFO_25_lfo26_rate","index": 262312,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_enable","varname": "fHslider23","shortname": "MM_LFO_26_lfo27_enable","address": "/modular-subtractive/MM_LFO_26_lfo27_enable","index": 262304,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/26_lfo27_morph","varname": "fHslider20","shortname": "MM_LFO_26_lfo27_morph","address": "/modular-subtractive/MM_LFO_26_lfo27_morph","index": 262284,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/26_lfo27_rate","varname": "fHslider21","shortname": "MM_LFO_26_lfo27_rate","address": "/modular-subtractive/MM_LFO_26_lfo27_rate","index": 262288,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_enable","varname": "fHslider19","shortname": "MM_LFO_27_lfo28_enable","address": "/modular-subtractive/MM_LFO_27_lfo28_enable","index": 262280,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/27_lfo28_morph","varname": "fHslider16","shortname": "MM_LFO_27_lfo28_morph","address": "/modular-subtractive/MM_LFO_27_lfo28_morph","index": 262260,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/27_lfo28_rate","varname": "fHslider17","shortname": "MM_LFO_27_lfo28_rate","address": "/modular-subtractive/MM_LFO_27_lfo28_rate","index": 262264,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_enable","varname": "fHslider15","shortname": "MM_LFO_28_lfo29_enable","address": "/modular-subtractive/MM_LFO_28_lfo29_enable","index": 262256,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/28_lfo29_morph","varname": "fHslider12","shortname": "MM_LFO_28_lfo29_morph","address": "/modular-subtractive/MM_LFO_28_lfo29_morph","index": 262236,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/28_lfo29_rate","varname": "fHslider13","shortname": "MM_LFO_28_lfo29_rate","address": "/modular-subtractive/MM_LFO_28_lfo29_rate","index": 262240,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_enable","varname": "fHslider11","shortname": "MM_LFO_29_lfo30_enable","address": "/modular-subtractive/MM_LFO_29_lfo30_enable","index": 262232,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/29_lfo30_morph","varname": "fHslider8","shortname": "MM_LFO_29_lfo30_morph","address": "/modular-subtractive/MM_LFO_29_lfo30_morph","index": 262212,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/29_lfo30_rate","varname": "fHslider9","shortname": "MM_LFO_29_lfo30_rate","address": "/modular-subtractive/MM_LFO_29_lfo30_rate","index": 262216,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_enable","varname": "fHslider7","shortname": "MM_LFO_30_lfo31_enable","address": "/modular-subtractive/MM_LFO_30_lfo31_enable","index": 262208,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/30_lfo31_morph","varname": "fHslider4","shortname": "MM_LFO_30_lfo31_morph","address": "/modular-subtractive/MM_LFO_30_lfo31_morph","index": 262188,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/30_lfo31_rate","varname": "fHslider5","shortname": "MM_LFO_30_lfo31_rate","address": "/modular-subtractive/MM_LFO_30_lfo31_rate","index": 262192,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_enable","varname": "fHslider3","shortname": "MM_LFO_31_lfo32_enable","address": "/modular-subtractive/MM_LFO_31_lfo32_enable","index": 262184,"init": 0,"min": 0,"max": 1,"step": 1},{"type": "hslider","label": "MM_LFO/31_lfo32_morph","varname": "fHslider0","shortname": "MM_LFO_31_lfo32_morph","address": "/modular-subtractive/MM_LFO_31_lfo32_morph","index": 262144,"init": 0,"min": 0,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_LFO/31_lfo32_rate","varname": "fHslider1","shortname": "MM_LFO_31_lfo32_rate","address": "/modular-subtractive/MM_LFO_31_lfo32_rate","index": 262156,"meta": [{ "scale": "log" }],"init": 1,"min": 0.01,"max": 20,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d00_pitch","varname": "fHslider522","shortname": "MM_Matrix_s00_d00_pitch","address": "/modular-subtractive/MM_Matrix_s00_d00_pitch","index": 264588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d01_osc2_detune","varname": "fHslider672","shortname": "MM_Matrix_s00_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s00_d01_osc2_detune","index": 265196,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d02_osc3_detune","varname": "fHslider572","shortname": "MM_Matrix_s00_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s00_d02_osc3_detune","index": 264788,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d03_osc_mix_bal","varname": "fHslider622","shortname": "MM_Matrix_s00_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s00_d03_osc_mix_bal","index": 264996,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d04_noise_level","varname": "fHslider471","shortname": "MM_Matrix_s00_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s00_d04_noise_level","index": 264360,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d05_cutoff","varname": "fHslider277","shortname": "MM_Matrix_s00_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s00_d05_cutoff","index": 263572,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d06_resonance","varname": "fHslider422","shortname": "MM_Matrix_s00_d06_resonance","address": "/modular-subtractive/MM_Matrix_s00_d06_resonance","index": 264164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d07_filter_env_amt","varname": "fHslider278","shortname": "MM_Matrix_s00_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s00_d07_filter_env_amt","index": 263576,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d08_amp","varname": "fHslider273","shortname": "MM_Matrix_s00_d08_amp","address": "/modular-subtractive/MM_Matrix_s00_d08_amp","index": 263556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s00_d09_pan","varname": "fHslider222","shortname": "MM_Matrix_s00_d09_pan","address": "/modular-subtractive/MM_Matrix_s00_d09_pan","index": 263336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d00_pitch","varname": "fHslider521","shortname": "MM_Matrix_s01_d00_pitch","address": "/modular-subtractive/MM_Matrix_s01_d00_pitch","index": 264584,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d01_osc2_detune","varname": "fHslider671","shortname": "MM_Matrix_s01_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s01_d01_osc2_detune","index": 265192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d02_osc3_detune","varname": "fHslider571","shortname": "MM_Matrix_s01_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s01_d02_osc3_detune","index": 264784,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d03_osc_mix_bal","varname": "fHslider621","shortname": "MM_Matrix_s01_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s01_d03_osc_mix_bal","index": 264992,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d04_noise_level","varname": "fHslider470","shortname": "MM_Matrix_s01_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s01_d04_noise_level","index": 264356,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d05_cutoff","varname": "fHslider275","shortname": "MM_Matrix_s01_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s01_d05_cutoff","index": 263564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d06_resonance","varname": "fHslider421","shortname": "MM_Matrix_s01_d06_resonance","address": "/modular-subtractive/MM_Matrix_s01_d06_resonance","index": 264160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d07_filter_env_amt","varname": "fHslider276","shortname": "MM_Matrix_s01_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s01_d07_filter_env_amt","index": 263568,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d08_amp","varname": "fHslider272","shortname": "MM_Matrix_s01_d08_amp","address": "/modular-subtractive/MM_Matrix_s01_d08_amp","index": 263552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s01_d09_pan","varname": "fHslider216","shortname": "MM_Matrix_s01_d09_pan","address": "/modular-subtractive/MM_Matrix_s01_d09_pan","index": 263312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d00_pitch","varname": "fHslider520","shortname": "MM_Matrix_s02_d00_pitch","address": "/modular-subtractive/MM_Matrix_s02_d00_pitch","index": 264580,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d01_osc2_detune","varname": "fHslider670","shortname": "MM_Matrix_s02_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s02_d01_osc2_detune","index": 265188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d02_osc3_detune","varname": "fHslider570","shortname": "MM_Matrix_s02_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s02_d02_osc3_detune","index": 264780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d03_osc_mix_bal","varname": "fHslider620","shortname": "MM_Matrix_s02_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s02_d03_osc_mix_bal","index": 264988,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d04_noise_level","varname": "fHslider469","shortname": "MM_Matrix_s02_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s02_d04_noise_level","index": 264352,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d05_cutoff","varname": "fHslider279","shortname": "MM_Matrix_s02_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s02_d05_cutoff","index": 263580,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d06_resonance","varname": "fHslider420","shortname": "MM_Matrix_s02_d06_resonance","address": "/modular-subtractive/MM_Matrix_s02_d06_resonance","index": 264156,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d07_filter_env_amt","varname": "fHslider280","shortname": "MM_Matrix_s02_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s02_d07_filter_env_amt","index": 263584,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d08_amp","varname": "fHslider271","shortname": "MM_Matrix_s02_d08_amp","address": "/modular-subtractive/MM_Matrix_s02_d08_amp","index": 263548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s02_d09_pan","varname": "fHslider210","shortname": "MM_Matrix_s02_d09_pan","address": "/modular-subtractive/MM_Matrix_s02_d09_pan","index": 263288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d00_pitch","varname": "fHslider519","shortname": "MM_Matrix_s03_d00_pitch","address": "/modular-subtractive/MM_Matrix_s03_d00_pitch","index": 264576,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d01_osc2_detune","varname": "fHslider669","shortname": "MM_Matrix_s03_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s03_d01_osc2_detune","index": 265184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d02_osc3_detune","varname": "fHslider569","shortname": "MM_Matrix_s03_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s03_d02_osc3_detune","index": 264776,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d03_osc_mix_bal","varname": "fHslider619","shortname": "MM_Matrix_s03_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s03_d03_osc_mix_bal","index": 264984,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d04_noise_level","varname": "fHslider468","shortname": "MM_Matrix_s03_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s03_d04_noise_level","index": 264348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d05_cutoff","varname": "fHslider281","shortname": "MM_Matrix_s03_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s03_d05_cutoff","index": 263588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d06_resonance","varname": "fHslider419","shortname": "MM_Matrix_s03_d06_resonance","address": "/modular-subtractive/MM_Matrix_s03_d06_resonance","index": 264152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d07_filter_env_amt","varname": "fHslider282","shortname": "MM_Matrix_s03_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s03_d07_filter_env_amt","index": 263592,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d08_amp","varname": "fHslider270","shortname": "MM_Matrix_s03_d08_amp","address": "/modular-subtractive/MM_Matrix_s03_d08_amp","index": 263544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s03_d09_pan","varname": "fHslider204","shortname": "MM_Matrix_s03_d09_pan","address": "/modular-subtractive/MM_Matrix_s03_d09_pan","index": 263264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d00_pitch","varname": "fHslider518","shortname": "MM_Matrix_s04_d00_pitch","address": "/modular-subtractive/MM_Matrix_s04_d00_pitch","index": 264572,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d01_osc2_detune","varname": "fHslider668","shortname": "MM_Matrix_s04_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s04_d01_osc2_detune","index": 265180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d02_osc3_detune","varname": "fHslider568","shortname": "MM_Matrix_s04_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s04_d02_osc3_detune","index": 264772,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d03_osc_mix_bal","varname": "fHslider618","shortname": "MM_Matrix_s04_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s04_d03_osc_mix_bal","index": 264980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d04_noise_level","varname": "fHslider467","shortname": "MM_Matrix_s04_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s04_d04_noise_level","index": 264344,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d05_cutoff","varname": "fHslider283","shortname": "MM_Matrix_s04_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s04_d05_cutoff","index": 263596,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d06_resonance","varname": "fHslider418","shortname": "MM_Matrix_s04_d06_resonance","address": "/modular-subtractive/MM_Matrix_s04_d06_resonance","index": 264148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d07_filter_env_amt","varname": "fHslider284","shortname": "MM_Matrix_s04_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s04_d07_filter_env_amt","index": 263600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d08_amp","varname": "fHslider269","shortname": "MM_Matrix_s04_d08_amp","address": "/modular-subtractive/MM_Matrix_s04_d08_amp","index": 263540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s04_d09_pan","varname": "fHslider198","shortname": "MM_Matrix_s04_d09_pan","address": "/modular-subtractive/MM_Matrix_s04_d09_pan","index": 263240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d00_pitch","varname": "fHslider517","shortname": "MM_Matrix_s05_d00_pitch","address": "/modular-subtractive/MM_Matrix_s05_d00_pitch","index": 264568,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d01_osc2_detune","varname": "fHslider667","shortname": "MM_Matrix_s05_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s05_d01_osc2_detune","index": 265176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d02_osc3_detune","varname": "fHslider567","shortname": "MM_Matrix_s05_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s05_d02_osc3_detune","index": 264768,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d03_osc_mix_bal","varname": "fHslider617","shortname": "MM_Matrix_s05_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s05_d03_osc_mix_bal","index": 264976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d04_noise_level","varname": "fHslider466","shortname": "MM_Matrix_s05_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s05_d04_noise_level","index": 264340,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d05_cutoff","varname": "fHslider285","shortname": "MM_Matrix_s05_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s05_d05_cutoff","index": 263604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d06_resonance","varname": "fHslider417","shortname": "MM_Matrix_s05_d06_resonance","address": "/modular-subtractive/MM_Matrix_s05_d06_resonance","index": 264144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d07_filter_env_amt","varname": "fHslider286","shortname": "MM_Matrix_s05_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s05_d07_filter_env_amt","index": 263608,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d08_amp","varname": "fHslider268","shortname": "MM_Matrix_s05_d08_amp","address": "/modular-subtractive/MM_Matrix_s05_d08_amp","index": 263536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s05_d09_pan","varname": "fHslider192","shortname": "MM_Matrix_s05_d09_pan","address": "/modular-subtractive/MM_Matrix_s05_d09_pan","index": 263216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d00_pitch","varname": "fHslider516","shortname": "MM_Matrix_s06_d00_pitch","address": "/modular-subtractive/MM_Matrix_s06_d00_pitch","index": 264564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d01_osc2_detune","varname": "fHslider666","shortname": "MM_Matrix_s06_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s06_d01_osc2_detune","index": 265172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d02_osc3_detune","varname": "fHslider566","shortname": "MM_Matrix_s06_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s06_d02_osc3_detune","index": 264764,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d03_osc_mix_bal","varname": "fHslider616","shortname": "MM_Matrix_s06_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s06_d03_osc_mix_bal","index": 264972,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d04_noise_level","varname": "fHslider465","shortname": "MM_Matrix_s06_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s06_d04_noise_level","index": 264336,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d05_cutoff","varname": "fHslider287","shortname": "MM_Matrix_s06_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s06_d05_cutoff","index": 263612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d06_resonance","varname": "fHslider416","shortname": "MM_Matrix_s06_d06_resonance","address": "/modular-subtractive/MM_Matrix_s06_d06_resonance","index": 264140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d07_filter_env_amt","varname": "fHslider288","shortname": "MM_Matrix_s06_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s06_d07_filter_env_amt","index": 263616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d08_amp","varname": "fHslider267","shortname": "MM_Matrix_s06_d08_amp","address": "/modular-subtractive/MM_Matrix_s06_d08_amp","index": 263532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s06_d09_pan","varname": "fHslider186","shortname": "MM_Matrix_s06_d09_pan","address": "/modular-subtractive/MM_Matrix_s06_d09_pan","index": 263192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d00_pitch","varname": "fHslider515","shortname": "MM_Matrix_s07_d00_pitch","address": "/modular-subtractive/MM_Matrix_s07_d00_pitch","index": 264560,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d01_osc2_detune","varname": "fHslider665","shortname": "MM_Matrix_s07_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s07_d01_osc2_detune","index": 265168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d02_osc3_detune","varname": "fHslider565","shortname": "MM_Matrix_s07_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s07_d02_osc3_detune","index": 264760,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d03_osc_mix_bal","varname": "fHslider615","shortname": "MM_Matrix_s07_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s07_d03_osc_mix_bal","index": 264968,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d04_noise_level","varname": "fHslider464","shortname": "MM_Matrix_s07_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s07_d04_noise_level","index": 264332,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d05_cutoff","varname": "fHslider289","shortname": "MM_Matrix_s07_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s07_d05_cutoff","index": 263620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d06_resonance","varname": "fHslider415","shortname": "MM_Matrix_s07_d06_resonance","address": "/modular-subtractive/MM_Matrix_s07_d06_resonance","index": 264136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d07_filter_env_amt","varname": "fHslider290","shortname": "MM_Matrix_s07_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s07_d07_filter_env_amt","index": 263624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d08_amp","varname": "fHslider266","shortname": "MM_Matrix_s07_d08_amp","address": "/modular-subtractive/MM_Matrix_s07_d08_amp","index": 263528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s07_d09_pan","varname": "fHslider180","shortname": "MM_Matrix_s07_d09_pan","address": "/modular-subtractive/MM_Matrix_s07_d09_pan","index": 263168,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d00_pitch","varname": "fHslider514","shortname": "MM_Matrix_s08_d00_pitch","address": "/modular-subtractive/MM_Matrix_s08_d00_pitch","index": 264556,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d01_osc2_detune","varname": "fHslider664","shortname": "MM_Matrix_s08_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s08_d01_osc2_detune","index": 265164,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d02_osc3_detune","varname": "fHslider564","shortname": "MM_Matrix_s08_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s08_d02_osc3_detune","index": 264756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d03_osc_mix_bal","varname": "fHslider614","shortname": "MM_Matrix_s08_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s08_d03_osc_mix_bal","index": 264964,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d04_noise_level","varname": "fHslider463","shortname": "MM_Matrix_s08_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s08_d04_noise_level","index": 264328,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d05_cutoff","varname": "fHslider291","shortname": "MM_Matrix_s08_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s08_d05_cutoff","index": 263628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d06_resonance","varname": "fHslider414","shortname": "MM_Matrix_s08_d06_resonance","address": "/modular-subtractive/MM_Matrix_s08_d06_resonance","index": 264132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d07_filter_env_amt","varname": "fHslider292","shortname": "MM_Matrix_s08_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s08_d07_filter_env_amt","index": 263632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d08_amp","varname": "fHslider265","shortname": "MM_Matrix_s08_d08_amp","address": "/modular-subtractive/MM_Matrix_s08_d08_amp","index": 263524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s08_d09_pan","varname": "fHslider174","shortname": "MM_Matrix_s08_d09_pan","address": "/modular-subtractive/MM_Matrix_s08_d09_pan","index": 263144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d00_pitch","varname": "fHslider513","shortname": "MM_Matrix_s09_d00_pitch","address": "/modular-subtractive/MM_Matrix_s09_d00_pitch","index": 264552,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d01_osc2_detune","varname": "fHslider663","shortname": "MM_Matrix_s09_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s09_d01_osc2_detune","index": 265160,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d02_osc3_detune","varname": "fHslider563","shortname": "MM_Matrix_s09_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s09_d02_osc3_detune","index": 264752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d03_osc_mix_bal","varname": "fHslider613","shortname": "MM_Matrix_s09_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s09_d03_osc_mix_bal","index": 264960,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d04_noise_level","varname": "fHslider462","shortname": "MM_Matrix_s09_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s09_d04_noise_level","index": 264324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d05_cutoff","varname": "fHslider293","shortname": "MM_Matrix_s09_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s09_d05_cutoff","index": 263636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d06_resonance","varname": "fHslider413","shortname": "MM_Matrix_s09_d06_resonance","address": "/modular-subtractive/MM_Matrix_s09_d06_resonance","index": 264128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d07_filter_env_amt","varname": "fHslider294","shortname": "MM_Matrix_s09_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s09_d07_filter_env_amt","index": 263640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d08_amp","varname": "fHslider264","shortname": "MM_Matrix_s09_d08_amp","address": "/modular-subtractive/MM_Matrix_s09_d08_amp","index": 263520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s09_d09_pan","varname": "fHslider168","shortname": "MM_Matrix_s09_d09_pan","address": "/modular-subtractive/MM_Matrix_s09_d09_pan","index": 263120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d00_pitch","varname": "fHslider512","shortname": "MM_Matrix_s10_d00_pitch","address": "/modular-subtractive/MM_Matrix_s10_d00_pitch","index": 264548,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d01_osc2_detune","varname": "fHslider662","shortname": "MM_Matrix_s10_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s10_d01_osc2_detune","index": 265156,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d02_osc3_detune","varname": "fHslider562","shortname": "MM_Matrix_s10_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s10_d02_osc3_detune","index": 264748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d03_osc_mix_bal","varname": "fHslider612","shortname": "MM_Matrix_s10_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s10_d03_osc_mix_bal","index": 264956,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d04_noise_level","varname": "fHslider461","shortname": "MM_Matrix_s10_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s10_d04_noise_level","index": 264320,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d05_cutoff","varname": "fHslider295","shortname": "MM_Matrix_s10_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s10_d05_cutoff","index": 263644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d06_resonance","varname": "fHslider412","shortname": "MM_Matrix_s10_d06_resonance","address": "/modular-subtractive/MM_Matrix_s10_d06_resonance","index": 264124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d07_filter_env_amt","varname": "fHslider296","shortname": "MM_Matrix_s10_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s10_d07_filter_env_amt","index": 263648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d08_amp","varname": "fHslider263","shortname": "MM_Matrix_s10_d08_amp","address": "/modular-subtractive/MM_Matrix_s10_d08_amp","index": 263516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s10_d09_pan","varname": "fHslider162","shortname": "MM_Matrix_s10_d09_pan","address": "/modular-subtractive/MM_Matrix_s10_d09_pan","index": 263096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d00_pitch","varname": "fHslider511","shortname": "MM_Matrix_s11_d00_pitch","address": "/modular-subtractive/MM_Matrix_s11_d00_pitch","index": 264544,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d01_osc2_detune","varname": "fHslider661","shortname": "MM_Matrix_s11_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s11_d01_osc2_detune","index": 265152,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d02_osc3_detune","varname": "fHslider561","shortname": "MM_Matrix_s11_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s11_d02_osc3_detune","index": 264744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d03_osc_mix_bal","varname": "fHslider611","shortname": "MM_Matrix_s11_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s11_d03_osc_mix_bal","index": 264952,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d04_noise_level","varname": "fHslider460","shortname": "MM_Matrix_s11_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s11_d04_noise_level","index": 264316,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d05_cutoff","varname": "fHslider297","shortname": "MM_Matrix_s11_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s11_d05_cutoff","index": 263652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d06_resonance","varname": "fHslider411","shortname": "MM_Matrix_s11_d06_resonance","address": "/modular-subtractive/MM_Matrix_s11_d06_resonance","index": 264120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d07_filter_env_amt","varname": "fHslider298","shortname": "MM_Matrix_s11_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s11_d07_filter_env_amt","index": 263656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d08_amp","varname": "fHslider262","shortname": "MM_Matrix_s11_d08_amp","address": "/modular-subtractive/MM_Matrix_s11_d08_amp","index": 263512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s11_d09_pan","varname": "fHslider156","shortname": "MM_Matrix_s11_d09_pan","address": "/modular-subtractive/MM_Matrix_s11_d09_pan","index": 263072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d00_pitch","varname": "fHslider510","shortname": "MM_Matrix_s12_d00_pitch","address": "/modular-subtractive/MM_Matrix_s12_d00_pitch","index": 264540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d01_osc2_detune","varname": "fHslider660","shortname": "MM_Matrix_s12_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s12_d01_osc2_detune","index": 265148,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d02_osc3_detune","varname": "fHslider560","shortname": "MM_Matrix_s12_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s12_d02_osc3_detune","index": 264740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d03_osc_mix_bal","varname": "fHslider610","shortname": "MM_Matrix_s12_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s12_d03_osc_mix_bal","index": 264948,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d04_noise_level","varname": "fHslider459","shortname": "MM_Matrix_s12_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s12_d04_noise_level","index": 264312,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d05_cutoff","varname": "fHslider299","shortname": "MM_Matrix_s12_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s12_d05_cutoff","index": 263660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d06_resonance","varname": "fHslider410","shortname": "MM_Matrix_s12_d06_resonance","address": "/modular-subtractive/MM_Matrix_s12_d06_resonance","index": 264116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d07_filter_env_amt","varname": "fHslider300","shortname": "MM_Matrix_s12_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s12_d07_filter_env_amt","index": 263664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d08_amp","varname": "fHslider261","shortname": "MM_Matrix_s12_d08_amp","address": "/modular-subtractive/MM_Matrix_s12_d08_amp","index": 263508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s12_d09_pan","varname": "fHslider150","shortname": "MM_Matrix_s12_d09_pan","address": "/modular-subtractive/MM_Matrix_s12_d09_pan","index": 263048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d00_pitch","varname": "fHslider509","shortname": "MM_Matrix_s13_d00_pitch","address": "/modular-subtractive/MM_Matrix_s13_d00_pitch","index": 264536,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d01_osc2_detune","varname": "fHslider659","shortname": "MM_Matrix_s13_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s13_d01_osc2_detune","index": 265144,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d02_osc3_detune","varname": "fHslider559","shortname": "MM_Matrix_s13_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s13_d02_osc3_detune","index": 264736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d03_osc_mix_bal","varname": "fHslider609","shortname": "MM_Matrix_s13_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s13_d03_osc_mix_bal","index": 264944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d04_noise_level","varname": "fHslider458","shortname": "MM_Matrix_s13_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s13_d04_noise_level","index": 264308,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d05_cutoff","varname": "fHslider301","shortname": "MM_Matrix_s13_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s13_d05_cutoff","index": 263668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d06_resonance","varname": "fHslider409","shortname": "MM_Matrix_s13_d06_resonance","address": "/modular-subtractive/MM_Matrix_s13_d06_resonance","index": 264112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d07_filter_env_amt","varname": "fHslider302","shortname": "MM_Matrix_s13_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s13_d07_filter_env_amt","index": 263672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d08_amp","varname": "fHslider260","shortname": "MM_Matrix_s13_d08_amp","address": "/modular-subtractive/MM_Matrix_s13_d08_amp","index": 263504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s13_d09_pan","varname": "fHslider144","shortname": "MM_Matrix_s13_d09_pan","address": "/modular-subtractive/MM_Matrix_s13_d09_pan","index": 263024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d00_pitch","varname": "fHslider508","shortname": "MM_Matrix_s14_d00_pitch","address": "/modular-subtractive/MM_Matrix_s14_d00_pitch","index": 264532,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d01_osc2_detune","varname": "fHslider658","shortname": "MM_Matrix_s14_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s14_d01_osc2_detune","index": 265140,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d02_osc3_detune","varname": "fHslider558","shortname": "MM_Matrix_s14_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s14_d02_osc3_detune","index": 264732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d03_osc_mix_bal","varname": "fHslider608","shortname": "MM_Matrix_s14_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s14_d03_osc_mix_bal","index": 264940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d04_noise_level","varname": "fHslider457","shortname": "MM_Matrix_s14_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s14_d04_noise_level","index": 264304,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d05_cutoff","varname": "fHslider303","shortname": "MM_Matrix_s14_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s14_d05_cutoff","index": 263676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d06_resonance","varname": "fHslider408","shortname": "MM_Matrix_s14_d06_resonance","address": "/modular-subtractive/MM_Matrix_s14_d06_resonance","index": 264108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d07_filter_env_amt","varname": "fHslider304","shortname": "MM_Matrix_s14_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s14_d07_filter_env_amt","index": 263680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d08_amp","varname": "fHslider259","shortname": "MM_Matrix_s14_d08_amp","address": "/modular-subtractive/MM_Matrix_s14_d08_amp","index": 263500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s14_d09_pan","varname": "fHslider138","shortname": "MM_Matrix_s14_d09_pan","address": "/modular-subtractive/MM_Matrix_s14_d09_pan","index": 263000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d00_pitch","varname": "fHslider507","shortname": "MM_Matrix_s15_d00_pitch","address": "/modular-subtractive/MM_Matrix_s15_d00_pitch","index": 264528,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d01_osc2_detune","varname": "fHslider657","shortname": "MM_Matrix_s15_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s15_d01_osc2_detune","index": 265136,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d02_osc3_detune","varname": "fHslider557","shortname": "MM_Matrix_s15_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s15_d02_osc3_detune","index": 264728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d03_osc_mix_bal","varname": "fHslider607","shortname": "MM_Matrix_s15_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s15_d03_osc_mix_bal","index": 264936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d04_noise_level","varname": "fHslider456","shortname": "MM_Matrix_s15_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s15_d04_noise_level","index": 264300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d05_cutoff","varname": "fHslider305","shortname": "MM_Matrix_s15_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s15_d05_cutoff","index": 263684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d06_resonance","varname": "fHslider407","shortname": "MM_Matrix_s15_d06_resonance","address": "/modular-subtractive/MM_Matrix_s15_d06_resonance","index": 264104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d07_filter_env_amt","varname": "fHslider306","shortname": "MM_Matrix_s15_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s15_d07_filter_env_amt","index": 263688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d08_amp","varname": "fHslider258","shortname": "MM_Matrix_s15_d08_amp","address": "/modular-subtractive/MM_Matrix_s15_d08_amp","index": 263496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s15_d09_pan","varname": "fHslider132","shortname": "MM_Matrix_s15_d09_pan","address": "/modular-subtractive/MM_Matrix_s15_d09_pan","index": 262976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d00_pitch","varname": "fHslider506","shortname": "MM_Matrix_s16_d00_pitch","address": "/modular-subtractive/MM_Matrix_s16_d00_pitch","index": 264524,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d01_osc2_detune","varname": "fHslider656","shortname": "MM_Matrix_s16_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s16_d01_osc2_detune","index": 265132,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d02_osc3_detune","varname": "fHslider556","shortname": "MM_Matrix_s16_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s16_d02_osc3_detune","index": 264724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d03_osc_mix_bal","varname": "fHslider606","shortname": "MM_Matrix_s16_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s16_d03_osc_mix_bal","index": 264932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d04_noise_level","varname": "fHslider455","shortname": "MM_Matrix_s16_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s16_d04_noise_level","index": 264296,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d05_cutoff","varname": "fHslider307","shortname": "MM_Matrix_s16_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s16_d05_cutoff","index": 263692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d06_resonance","varname": "fHslider406","shortname": "MM_Matrix_s16_d06_resonance","address": "/modular-subtractive/MM_Matrix_s16_d06_resonance","index": 264100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d07_filter_env_amt","varname": "fHslider308","shortname": "MM_Matrix_s16_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s16_d07_filter_env_amt","index": 263696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d08_amp","varname": "fHslider257","shortname": "MM_Matrix_s16_d08_amp","address": "/modular-subtractive/MM_Matrix_s16_d08_amp","index": 263492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s16_d09_pan","varname": "fHslider126","shortname": "MM_Matrix_s16_d09_pan","address": "/modular-subtractive/MM_Matrix_s16_d09_pan","index": 262924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d00_pitch","varname": "fHslider505","shortname": "MM_Matrix_s17_d00_pitch","address": "/modular-subtractive/MM_Matrix_s17_d00_pitch","index": 264520,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d01_osc2_detune","varname": "fHslider655","shortname": "MM_Matrix_s17_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s17_d01_osc2_detune","index": 265128,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d02_osc3_detune","varname": "fHslider555","shortname": "MM_Matrix_s17_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s17_d02_osc3_detune","index": 264720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d03_osc_mix_bal","varname": "fHslider605","shortname": "MM_Matrix_s17_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s17_d03_osc_mix_bal","index": 264928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d04_noise_level","varname": "fHslider454","shortname": "MM_Matrix_s17_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s17_d04_noise_level","index": 264292,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d05_cutoff","varname": "fHslider309","shortname": "MM_Matrix_s17_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s17_d05_cutoff","index": 263700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d06_resonance","varname": "fHslider405","shortname": "MM_Matrix_s17_d06_resonance","address": "/modular-subtractive/MM_Matrix_s17_d06_resonance","index": 264096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d07_filter_env_amt","varname": "fHslider310","shortname": "MM_Matrix_s17_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s17_d07_filter_env_amt","index": 263704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d08_amp","varname": "fHslider256","shortname": "MM_Matrix_s17_d08_amp","address": "/modular-subtractive/MM_Matrix_s17_d08_amp","index": 263488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s17_d09_pan","varname": "fHslider122","shortname": "MM_Matrix_s17_d09_pan","address": "/modular-subtractive/MM_Matrix_s17_d09_pan","index": 262900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d00_pitch","varname": "fHslider504","shortname": "MM_Matrix_s18_d00_pitch","address": "/modular-subtractive/MM_Matrix_s18_d00_pitch","index": 264516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d01_osc2_detune","varname": "fHslider654","shortname": "MM_Matrix_s18_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s18_d01_osc2_detune","index": 265124,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d02_osc3_detune","varname": "fHslider554","shortname": "MM_Matrix_s18_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s18_d02_osc3_detune","index": 264716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d03_osc_mix_bal","varname": "fHslider604","shortname": "MM_Matrix_s18_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s18_d03_osc_mix_bal","index": 264924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d04_noise_level","varname": "fHslider453","shortname": "MM_Matrix_s18_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s18_d04_noise_level","index": 264288,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d05_cutoff","varname": "fHslider311","shortname": "MM_Matrix_s18_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s18_d05_cutoff","index": 263708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d06_resonance","varname": "fHslider404","shortname": "MM_Matrix_s18_d06_resonance","address": "/modular-subtractive/MM_Matrix_s18_d06_resonance","index": 264092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d07_filter_env_amt","varname": "fHslider312","shortname": "MM_Matrix_s18_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s18_d07_filter_env_amt","index": 263712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d08_amp","varname": "fHslider255","shortname": "MM_Matrix_s18_d08_amp","address": "/modular-subtractive/MM_Matrix_s18_d08_amp","index": 263484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s18_d09_pan","varname": "fHslider118","shortname": "MM_Matrix_s18_d09_pan","address": "/modular-subtractive/MM_Matrix_s18_d09_pan","index": 262876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d00_pitch","varname": "fHslider503","shortname": "MM_Matrix_s19_d00_pitch","address": "/modular-subtractive/MM_Matrix_s19_d00_pitch","index": 264512,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d01_osc2_detune","varname": "fHslider653","shortname": "MM_Matrix_s19_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s19_d01_osc2_detune","index": 265120,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d02_osc3_detune","varname": "fHslider553","shortname": "MM_Matrix_s19_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s19_d02_osc3_detune","index": 264712,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d03_osc_mix_bal","varname": "fHslider603","shortname": "MM_Matrix_s19_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s19_d03_osc_mix_bal","index": 264920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d04_noise_level","varname": "fHslider452","shortname": "MM_Matrix_s19_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s19_d04_noise_level","index": 264284,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d05_cutoff","varname": "fHslider313","shortname": "MM_Matrix_s19_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s19_d05_cutoff","index": 263716,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d06_resonance","varname": "fHslider403","shortname": "MM_Matrix_s19_d06_resonance","address": "/modular-subtractive/MM_Matrix_s19_d06_resonance","index": 264088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d07_filter_env_amt","varname": "fHslider314","shortname": "MM_Matrix_s19_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s19_d07_filter_env_amt","index": 263720,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d08_amp","varname": "fHslider254","shortname": "MM_Matrix_s19_d08_amp","address": "/modular-subtractive/MM_Matrix_s19_d08_amp","index": 263480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s19_d09_pan","varname": "fHslider114","shortname": "MM_Matrix_s19_d09_pan","address": "/modular-subtractive/MM_Matrix_s19_d09_pan","index": 262852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d00_pitch","varname": "fHslider502","shortname": "MM_Matrix_s20_d00_pitch","address": "/modular-subtractive/MM_Matrix_s20_d00_pitch","index": 264508,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d01_osc2_detune","varname": "fHslider652","shortname": "MM_Matrix_s20_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s20_d01_osc2_detune","index": 265116,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d02_osc3_detune","varname": "fHslider552","shortname": "MM_Matrix_s20_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s20_d02_osc3_detune","index": 264708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d03_osc_mix_bal","varname": "fHslider602","shortname": "MM_Matrix_s20_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s20_d03_osc_mix_bal","index": 264916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d04_noise_level","varname": "fHslider451","shortname": "MM_Matrix_s20_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s20_d04_noise_level","index": 264280,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d05_cutoff","varname": "fHslider315","shortname": "MM_Matrix_s20_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s20_d05_cutoff","index": 263724,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d06_resonance","varname": "fHslider402","shortname": "MM_Matrix_s20_d06_resonance","address": "/modular-subtractive/MM_Matrix_s20_d06_resonance","index": 264084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d07_filter_env_amt","varname": "fHslider316","shortname": "MM_Matrix_s20_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s20_d07_filter_env_amt","index": 263728,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d08_amp","varname": "fHslider253","shortname": "MM_Matrix_s20_d08_amp","address": "/modular-subtractive/MM_Matrix_s20_d08_amp","index": 263476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s20_d09_pan","varname": "fHslider110","shortname": "MM_Matrix_s20_d09_pan","address": "/modular-subtractive/MM_Matrix_s20_d09_pan","index": 262828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d00_pitch","varname": "fHslider501","shortname": "MM_Matrix_s21_d00_pitch","address": "/modular-subtractive/MM_Matrix_s21_d00_pitch","index": 264504,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d01_osc2_detune","varname": "fHslider651","shortname": "MM_Matrix_s21_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s21_d01_osc2_detune","index": 265112,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d02_osc3_detune","varname": "fHslider551","shortname": "MM_Matrix_s21_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s21_d02_osc3_detune","index": 264704,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d03_osc_mix_bal","varname": "fHslider601","shortname": "MM_Matrix_s21_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s21_d03_osc_mix_bal","index": 264912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d04_noise_level","varname": "fHslider450","shortname": "MM_Matrix_s21_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s21_d04_noise_level","index": 264276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d05_cutoff","varname": "fHslider317","shortname": "MM_Matrix_s21_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s21_d05_cutoff","index": 263732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d06_resonance","varname": "fHslider401","shortname": "MM_Matrix_s21_d06_resonance","address": "/modular-subtractive/MM_Matrix_s21_d06_resonance","index": 264080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d07_filter_env_amt","varname": "fHslider318","shortname": "MM_Matrix_s21_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s21_d07_filter_env_amt","index": 263736,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d08_amp","varname": "fHslider252","shortname": "MM_Matrix_s21_d08_amp","address": "/modular-subtractive/MM_Matrix_s21_d08_amp","index": 263472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s21_d09_pan","varname": "fHslider106","shortname": "MM_Matrix_s21_d09_pan","address": "/modular-subtractive/MM_Matrix_s21_d09_pan","index": 262804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d00_pitch","varname": "fHslider500","shortname": "MM_Matrix_s22_d00_pitch","address": "/modular-subtractive/MM_Matrix_s22_d00_pitch","index": 264500,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d01_osc2_detune","varname": "fHslider650","shortname": "MM_Matrix_s22_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s22_d01_osc2_detune","index": 265108,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d02_osc3_detune","varname": "fHslider550","shortname": "MM_Matrix_s22_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s22_d02_osc3_detune","index": 264700,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d03_osc_mix_bal","varname": "fHslider600","shortname": "MM_Matrix_s22_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s22_d03_osc_mix_bal","index": 264908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d04_noise_level","varname": "fHslider449","shortname": "MM_Matrix_s22_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s22_d04_noise_level","index": 264272,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d05_cutoff","varname": "fHslider319","shortname": "MM_Matrix_s22_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s22_d05_cutoff","index": 263740,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d06_resonance","varname": "fHslider400","shortname": "MM_Matrix_s22_d06_resonance","address": "/modular-subtractive/MM_Matrix_s22_d06_resonance","index": 264076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d07_filter_env_amt","varname": "fHslider320","shortname": "MM_Matrix_s22_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s22_d07_filter_env_amt","index": 263744,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d08_amp","varname": "fHslider251","shortname": "MM_Matrix_s22_d08_amp","address": "/modular-subtractive/MM_Matrix_s22_d08_amp","index": 263468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s22_d09_pan","varname": "fHslider102","shortname": "MM_Matrix_s22_d09_pan","address": "/modular-subtractive/MM_Matrix_s22_d09_pan","index": 262780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d00_pitch","varname": "fHslider499","shortname": "MM_Matrix_s23_d00_pitch","address": "/modular-subtractive/MM_Matrix_s23_d00_pitch","index": 264496,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d01_osc2_detune","varname": "fHslider649","shortname": "MM_Matrix_s23_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s23_d01_osc2_detune","index": 265104,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d02_osc3_detune","varname": "fHslider549","shortname": "MM_Matrix_s23_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s23_d02_osc3_detune","index": 264696,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d03_osc_mix_bal","varname": "fHslider599","shortname": "MM_Matrix_s23_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s23_d03_osc_mix_bal","index": 264904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d04_noise_level","varname": "fHslider448","shortname": "MM_Matrix_s23_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s23_d04_noise_level","index": 264268,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d05_cutoff","varname": "fHslider321","shortname": "MM_Matrix_s23_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s23_d05_cutoff","index": 263748,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d06_resonance","varname": "fHslider399","shortname": "MM_Matrix_s23_d06_resonance","address": "/modular-subtractive/MM_Matrix_s23_d06_resonance","index": 264072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d07_filter_env_amt","varname": "fHslider322","shortname": "MM_Matrix_s23_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s23_d07_filter_env_amt","index": 263752,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d08_amp","varname": "fHslider250","shortname": "MM_Matrix_s23_d08_amp","address": "/modular-subtractive/MM_Matrix_s23_d08_amp","index": 263464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s23_d09_pan","varname": "fHslider98","shortname": "MM_Matrix_s23_d09_pan","address": "/modular-subtractive/MM_Matrix_s23_d09_pan","index": 262756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d00_pitch","varname": "fHslider498","shortname": "MM_Matrix_s24_d00_pitch","address": "/modular-subtractive/MM_Matrix_s24_d00_pitch","index": 264492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d01_osc2_detune","varname": "fHslider648","shortname": "MM_Matrix_s24_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s24_d01_osc2_detune","index": 265100,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d02_osc3_detune","varname": "fHslider548","shortname": "MM_Matrix_s24_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s24_d02_osc3_detune","index": 264692,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d03_osc_mix_bal","varname": "fHslider598","shortname": "MM_Matrix_s24_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s24_d03_osc_mix_bal","index": 264900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d04_noise_level","varname": "fHslider447","shortname": "MM_Matrix_s24_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s24_d04_noise_level","index": 264264,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d05_cutoff","varname": "fHslider323","shortname": "MM_Matrix_s24_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s24_d05_cutoff","index": 263756,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d06_resonance","varname": "fHslider398","shortname": "MM_Matrix_s24_d06_resonance","address": "/modular-subtractive/MM_Matrix_s24_d06_resonance","index": 264068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d07_filter_env_amt","varname": "fHslider324","shortname": "MM_Matrix_s24_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s24_d07_filter_env_amt","index": 263760,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d08_amp","varname": "fHslider249","shortname": "MM_Matrix_s24_d08_amp","address": "/modular-subtractive/MM_Matrix_s24_d08_amp","index": 263460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s24_d09_pan","varname": "fHslider94","shortname": "MM_Matrix_s24_d09_pan","address": "/modular-subtractive/MM_Matrix_s24_d09_pan","index": 262732,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d00_pitch","varname": "fHslider497","shortname": "MM_Matrix_s25_d00_pitch","address": "/modular-subtractive/MM_Matrix_s25_d00_pitch","index": 264488,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d01_osc2_detune","varname": "fHslider647","shortname": "MM_Matrix_s25_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s25_d01_osc2_detune","index": 265096,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d02_osc3_detune","varname": "fHslider547","shortname": "MM_Matrix_s25_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s25_d02_osc3_detune","index": 264688,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d03_osc_mix_bal","varname": "fHslider597","shortname": "MM_Matrix_s25_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s25_d03_osc_mix_bal","index": 264896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d04_noise_level","varname": "fHslider446","shortname": "MM_Matrix_s25_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s25_d04_noise_level","index": 264260,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d05_cutoff","varname": "fHslider325","shortname": "MM_Matrix_s25_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s25_d05_cutoff","index": 263764,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d06_resonance","varname": "fHslider397","shortname": "MM_Matrix_s25_d06_resonance","address": "/modular-subtractive/MM_Matrix_s25_d06_resonance","index": 264064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d07_filter_env_amt","varname": "fHslider326","shortname": "MM_Matrix_s25_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s25_d07_filter_env_amt","index": 263768,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d08_amp","varname": "fHslider248","shortname": "MM_Matrix_s25_d08_amp","address": "/modular-subtractive/MM_Matrix_s25_d08_amp","index": 263456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s25_d09_pan","varname": "fHslider90","shortname": "MM_Matrix_s25_d09_pan","address": "/modular-subtractive/MM_Matrix_s25_d09_pan","index": 262708,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d00_pitch","varname": "fHslider496","shortname": "MM_Matrix_s26_d00_pitch","address": "/modular-subtractive/MM_Matrix_s26_d00_pitch","index": 264484,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d01_osc2_detune","varname": "fHslider646","shortname": "MM_Matrix_s26_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s26_d01_osc2_detune","index": 265092,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d02_osc3_detune","varname": "fHslider546","shortname": "MM_Matrix_s26_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s26_d02_osc3_detune","index": 264684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d03_osc_mix_bal","varname": "fHslider596","shortname": "MM_Matrix_s26_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s26_d03_osc_mix_bal","index": 264892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d04_noise_level","varname": "fHslider445","shortname": "MM_Matrix_s26_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s26_d04_noise_level","index": 264256,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d05_cutoff","varname": "fHslider327","shortname": "MM_Matrix_s26_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s26_d05_cutoff","index": 263772,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d06_resonance","varname": "fHslider396","shortname": "MM_Matrix_s26_d06_resonance","address": "/modular-subtractive/MM_Matrix_s26_d06_resonance","index": 264060,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d07_filter_env_amt","varname": "fHslider328","shortname": "MM_Matrix_s26_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s26_d07_filter_env_amt","index": 263776,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d08_amp","varname": "fHslider247","shortname": "MM_Matrix_s26_d08_amp","address": "/modular-subtractive/MM_Matrix_s26_d08_amp","index": 263452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s26_d09_pan","varname": "fHslider86","shortname": "MM_Matrix_s26_d09_pan","address": "/modular-subtractive/MM_Matrix_s26_d09_pan","index": 262684,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d00_pitch","varname": "fHslider495","shortname": "MM_Matrix_s27_d00_pitch","address": "/modular-subtractive/MM_Matrix_s27_d00_pitch","index": 264480,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d01_osc2_detune","varname": "fHslider645","shortname": "MM_Matrix_s27_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s27_d01_osc2_detune","index": 265088,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d02_osc3_detune","varname": "fHslider545","shortname": "MM_Matrix_s27_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s27_d02_osc3_detune","index": 264680,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d03_osc_mix_bal","varname": "fHslider595","shortname": "MM_Matrix_s27_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s27_d03_osc_mix_bal","index": 264888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d04_noise_level","varname": "fHslider444","shortname": "MM_Matrix_s27_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s27_d04_noise_level","index": 264252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d05_cutoff","varname": "fHslider329","shortname": "MM_Matrix_s27_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s27_d05_cutoff","index": 263780,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d06_resonance","varname": "fHslider395","shortname": "MM_Matrix_s27_d06_resonance","address": "/modular-subtractive/MM_Matrix_s27_d06_resonance","index": 264056,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d07_filter_env_amt","varname": "fHslider330","shortname": "MM_Matrix_s27_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s27_d07_filter_env_amt","index": 263784,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d08_amp","varname": "fHslider246","shortname": "MM_Matrix_s27_d08_amp","address": "/modular-subtractive/MM_Matrix_s27_d08_amp","index": 263448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s27_d09_pan","varname": "fHslider82","shortname": "MM_Matrix_s27_d09_pan","address": "/modular-subtractive/MM_Matrix_s27_d09_pan","index": 262660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d00_pitch","varname": "fHslider494","shortname": "MM_Matrix_s28_d00_pitch","address": "/modular-subtractive/MM_Matrix_s28_d00_pitch","index": 264476,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d01_osc2_detune","varname": "fHslider644","shortname": "MM_Matrix_s28_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s28_d01_osc2_detune","index": 265084,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d02_osc3_detune","varname": "fHslider544","shortname": "MM_Matrix_s28_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s28_d02_osc3_detune","index": 264676,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d03_osc_mix_bal","varname": "fHslider594","shortname": "MM_Matrix_s28_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s28_d03_osc_mix_bal","index": 264884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d04_noise_level","varname": "fHslider443","shortname": "MM_Matrix_s28_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s28_d04_noise_level","index": 264248,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d05_cutoff","varname": "fHslider331","shortname": "MM_Matrix_s28_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s28_d05_cutoff","index": 263788,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d06_resonance","varname": "fHslider394","shortname": "MM_Matrix_s28_d06_resonance","address": "/modular-subtractive/MM_Matrix_s28_d06_resonance","index": 264052,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d07_filter_env_amt","varname": "fHslider332","shortname": "MM_Matrix_s28_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s28_d07_filter_env_amt","index": 263792,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d08_amp","varname": "fHslider245","shortname": "MM_Matrix_s28_d08_amp","address": "/modular-subtractive/MM_Matrix_s28_d08_amp","index": 263444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s28_d09_pan","varname": "fHslider78","shortname": "MM_Matrix_s28_d09_pan","address": "/modular-subtractive/MM_Matrix_s28_d09_pan","index": 262636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d00_pitch","varname": "fHslider493","shortname": "MM_Matrix_s29_d00_pitch","address": "/modular-subtractive/MM_Matrix_s29_d00_pitch","index": 264472,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d01_osc2_detune","varname": "fHslider643","shortname": "MM_Matrix_s29_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s29_d01_osc2_detune","index": 265080,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d02_osc3_detune","varname": "fHslider543","shortname": "MM_Matrix_s29_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s29_d02_osc3_detune","index": 264672,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d03_osc_mix_bal","varname": "fHslider593","shortname": "MM_Matrix_s29_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s29_d03_osc_mix_bal","index": 264880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d04_noise_level","varname": "fHslider442","shortname": "MM_Matrix_s29_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s29_d04_noise_level","index": 264244,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d05_cutoff","varname": "fHslider333","shortname": "MM_Matrix_s29_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s29_d05_cutoff","index": 263796,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d06_resonance","varname": "fHslider393","shortname": "MM_Matrix_s29_d06_resonance","address": "/modular-subtractive/MM_Matrix_s29_d06_resonance","index": 264048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d07_filter_env_amt","varname": "fHslider334","shortname": "MM_Matrix_s29_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s29_d07_filter_env_amt","index": 263800,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d08_amp","varname": "fHslider244","shortname": "MM_Matrix_s29_d08_amp","address": "/modular-subtractive/MM_Matrix_s29_d08_amp","index": 263440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s29_d09_pan","varname": "fHslider74","shortname": "MM_Matrix_s29_d09_pan","address": "/modular-subtractive/MM_Matrix_s29_d09_pan","index": 262612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d00_pitch","varname": "fHslider492","shortname": "MM_Matrix_s30_d00_pitch","address": "/modular-subtractive/MM_Matrix_s30_d00_pitch","index": 264468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d01_osc2_detune","varname": "fHslider642","shortname": "MM_Matrix_s30_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s30_d01_osc2_detune","index": 265076,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d02_osc3_detune","varname": "fHslider542","shortname": "MM_Matrix_s30_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s30_d02_osc3_detune","index": 264668,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d03_osc_mix_bal","varname": "fHslider592","shortname": "MM_Matrix_s30_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s30_d03_osc_mix_bal","index": 264876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d04_noise_level","varname": "fHslider441","shortname": "MM_Matrix_s30_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s30_d04_noise_level","index": 264240,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d05_cutoff","varname": "fHslider335","shortname": "MM_Matrix_s30_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s30_d05_cutoff","index": 263804,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d06_resonance","varname": "fHslider392","shortname": "MM_Matrix_s30_d06_resonance","address": "/modular-subtractive/MM_Matrix_s30_d06_resonance","index": 264044,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d07_filter_env_amt","varname": "fHslider336","shortname": "MM_Matrix_s30_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s30_d07_filter_env_amt","index": 263808,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d08_amp","varname": "fHslider243","shortname": "MM_Matrix_s30_d08_amp","address": "/modular-subtractive/MM_Matrix_s30_d08_amp","index": 263436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s30_d09_pan","varname": "fHslider70","shortname": "MM_Matrix_s30_d09_pan","address": "/modular-subtractive/MM_Matrix_s30_d09_pan","index": 262588,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d00_pitch","varname": "fHslider491","shortname": "MM_Matrix_s31_d00_pitch","address": "/modular-subtractive/MM_Matrix_s31_d00_pitch","index": 264464,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d01_osc2_detune","varname": "fHslider641","shortname": "MM_Matrix_s31_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s31_d01_osc2_detune","index": 265072,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d02_osc3_detune","varname": "fHslider541","shortname": "MM_Matrix_s31_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s31_d02_osc3_detune","index": 264664,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d03_osc_mix_bal","varname": "fHslider591","shortname": "MM_Matrix_s31_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s31_d03_osc_mix_bal","index": 264872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d04_noise_level","varname": "fHslider440","shortname": "MM_Matrix_s31_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s31_d04_noise_level","index": 264236,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d05_cutoff","varname": "fHslider337","shortname": "MM_Matrix_s31_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s31_d05_cutoff","index": 263812,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d06_resonance","varname": "fHslider391","shortname": "MM_Matrix_s31_d06_resonance","address": "/modular-subtractive/MM_Matrix_s31_d06_resonance","index": 264040,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d07_filter_env_amt","varname": "fHslider338","shortname": "MM_Matrix_s31_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s31_d07_filter_env_amt","index": 263816,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d08_amp","varname": "fHslider242","shortname": "MM_Matrix_s31_d08_amp","address": "/modular-subtractive/MM_Matrix_s31_d08_amp","index": 263432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s31_d09_pan","varname": "fHslider66","shortname": "MM_Matrix_s31_d09_pan","address": "/modular-subtractive/MM_Matrix_s31_d09_pan","index": 262564,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d00_pitch","varname": "fHslider490","shortname": "MM_Matrix_s32_d00_pitch","address": "/modular-subtractive/MM_Matrix_s32_d00_pitch","index": 264460,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d01_osc2_detune","varname": "fHslider640","shortname": "MM_Matrix_s32_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s32_d01_osc2_detune","index": 265068,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d02_osc3_detune","varname": "fHslider540","shortname": "MM_Matrix_s32_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s32_d02_osc3_detune","index": 264660,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d03_osc_mix_bal","varname": "fHslider590","shortname": "MM_Matrix_s32_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s32_d03_osc_mix_bal","index": 264868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d04_noise_level","varname": "fHslider439","shortname": "MM_Matrix_s32_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s32_d04_noise_level","index": 264232,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d05_cutoff","varname": "fHslider339","shortname": "MM_Matrix_s32_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s32_d05_cutoff","index": 263820,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d06_resonance","varname": "fHslider390","shortname": "MM_Matrix_s32_d06_resonance","address": "/modular-subtractive/MM_Matrix_s32_d06_resonance","index": 264036,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d07_filter_env_amt","varname": "fHslider340","shortname": "MM_Matrix_s32_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s32_d07_filter_env_amt","index": 263824,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d08_amp","varname": "fHslider241","shortname": "MM_Matrix_s32_d08_amp","address": "/modular-subtractive/MM_Matrix_s32_d08_amp","index": 263428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s32_d09_pan","varname": "fHslider62","shortname": "MM_Matrix_s32_d09_pan","address": "/modular-subtractive/MM_Matrix_s32_d09_pan","index": 262540,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d00_pitch","varname": "fHslider489","shortname": "MM_Matrix_s33_d00_pitch","address": "/modular-subtractive/MM_Matrix_s33_d00_pitch","index": 264456,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d01_osc2_detune","varname": "fHslider639","shortname": "MM_Matrix_s33_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s33_d01_osc2_detune","index": 265064,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d02_osc3_detune","varname": "fHslider539","shortname": "MM_Matrix_s33_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s33_d02_osc3_detune","index": 264656,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d03_osc_mix_bal","varname": "fHslider589","shortname": "MM_Matrix_s33_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s33_d03_osc_mix_bal","index": 264864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d04_noise_level","varname": "fHslider438","shortname": "MM_Matrix_s33_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s33_d04_noise_level","index": 264228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d05_cutoff","varname": "fHslider341","shortname": "MM_Matrix_s33_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s33_d05_cutoff","index": 263828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d06_resonance","varname": "fHslider389","shortname": "MM_Matrix_s33_d06_resonance","address": "/modular-subtractive/MM_Matrix_s33_d06_resonance","index": 264032,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d07_filter_env_amt","varname": "fHslider342","shortname": "MM_Matrix_s33_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s33_d07_filter_env_amt","index": 263832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d08_amp","varname": "fHslider240","shortname": "MM_Matrix_s33_d08_amp","address": "/modular-subtractive/MM_Matrix_s33_d08_amp","index": 263424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s33_d09_pan","varname": "fHslider58","shortname": "MM_Matrix_s33_d09_pan","address": "/modular-subtractive/MM_Matrix_s33_d09_pan","index": 262516,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d00_pitch","varname": "fHslider488","shortname": "MM_Matrix_s34_d00_pitch","address": "/modular-subtractive/MM_Matrix_s34_d00_pitch","index": 264452,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d01_osc2_detune","varname": "fHslider638","shortname": "MM_Matrix_s34_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s34_d01_osc2_detune","index": 265060,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d02_osc3_detune","varname": "fHslider538","shortname": "MM_Matrix_s34_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s34_d02_osc3_detune","index": 264652,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d03_osc_mix_bal","varname": "fHslider588","shortname": "MM_Matrix_s34_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s34_d03_osc_mix_bal","index": 264860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d04_noise_level","varname": "fHslider437","shortname": "MM_Matrix_s34_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s34_d04_noise_level","index": 264224,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d05_cutoff","varname": "fHslider343","shortname": "MM_Matrix_s34_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s34_d05_cutoff","index": 263836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d06_resonance","varname": "fHslider388","shortname": "MM_Matrix_s34_d06_resonance","address": "/modular-subtractive/MM_Matrix_s34_d06_resonance","index": 264028,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d07_filter_env_amt","varname": "fHslider344","shortname": "MM_Matrix_s34_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s34_d07_filter_env_amt","index": 263840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d08_amp","varname": "fHslider239","shortname": "MM_Matrix_s34_d08_amp","address": "/modular-subtractive/MM_Matrix_s34_d08_amp","index": 263420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s34_d09_pan","varname": "fHslider54","shortname": "MM_Matrix_s34_d09_pan","address": "/modular-subtractive/MM_Matrix_s34_d09_pan","index": 262492,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d00_pitch","varname": "fHslider487","shortname": "MM_Matrix_s35_d00_pitch","address": "/modular-subtractive/MM_Matrix_s35_d00_pitch","index": 264448,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d01_osc2_detune","varname": "fHslider637","shortname": "MM_Matrix_s35_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s35_d01_osc2_detune","index": 265056,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d02_osc3_detune","varname": "fHslider537","shortname": "MM_Matrix_s35_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s35_d02_osc3_detune","index": 264648,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d03_osc_mix_bal","varname": "fHslider587","shortname": "MM_Matrix_s35_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s35_d03_osc_mix_bal","index": 264856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d04_noise_level","varname": "fHslider436","shortname": "MM_Matrix_s35_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s35_d04_noise_level","index": 264220,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d05_cutoff","varname": "fHslider345","shortname": "MM_Matrix_s35_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s35_d05_cutoff","index": 263844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d06_resonance","varname": "fHslider387","shortname": "MM_Matrix_s35_d06_resonance","address": "/modular-subtractive/MM_Matrix_s35_d06_resonance","index": 264024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d07_filter_env_amt","varname": "fHslider346","shortname": "MM_Matrix_s35_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s35_d07_filter_env_amt","index": 263848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d08_amp","varname": "fHslider238","shortname": "MM_Matrix_s35_d08_amp","address": "/modular-subtractive/MM_Matrix_s35_d08_amp","index": 263416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s35_d09_pan","varname": "fHslider50","shortname": "MM_Matrix_s35_d09_pan","address": "/modular-subtractive/MM_Matrix_s35_d09_pan","index": 262468,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d00_pitch","varname": "fHslider486","shortname": "MM_Matrix_s36_d00_pitch","address": "/modular-subtractive/MM_Matrix_s36_d00_pitch","index": 264444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d01_osc2_detune","varname": "fHslider636","shortname": "MM_Matrix_s36_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s36_d01_osc2_detune","index": 265052,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d02_osc3_detune","varname": "fHslider536","shortname": "MM_Matrix_s36_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s36_d02_osc3_detune","index": 264644,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d03_osc_mix_bal","varname": "fHslider586","shortname": "MM_Matrix_s36_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s36_d03_osc_mix_bal","index": 264852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d04_noise_level","varname": "fHslider435","shortname": "MM_Matrix_s36_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s36_d04_noise_level","index": 264216,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d05_cutoff","varname": "fHslider347","shortname": "MM_Matrix_s36_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s36_d05_cutoff","index": 263852,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d06_resonance","varname": "fHslider386","shortname": "MM_Matrix_s36_d06_resonance","address": "/modular-subtractive/MM_Matrix_s36_d06_resonance","index": 264020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d07_filter_env_amt","varname": "fHslider348","shortname": "MM_Matrix_s36_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s36_d07_filter_env_amt","index": 263856,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d08_amp","varname": "fHslider237","shortname": "MM_Matrix_s36_d08_amp","address": "/modular-subtractive/MM_Matrix_s36_d08_amp","index": 263412,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s36_d09_pan","varname": "fHslider46","shortname": "MM_Matrix_s36_d09_pan","address": "/modular-subtractive/MM_Matrix_s36_d09_pan","index": 262444,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d00_pitch","varname": "fHslider485","shortname": "MM_Matrix_s37_d00_pitch","address": "/modular-subtractive/MM_Matrix_s37_d00_pitch","index": 264440,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d01_osc2_detune","varname": "fHslider635","shortname": "MM_Matrix_s37_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s37_d01_osc2_detune","index": 265048,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d02_osc3_detune","varname": "fHslider535","shortname": "MM_Matrix_s37_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s37_d02_osc3_detune","index": 264640,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d03_osc_mix_bal","varname": "fHslider585","shortname": "MM_Matrix_s37_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s37_d03_osc_mix_bal","index": 264848,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d04_noise_level","varname": "fHslider434","shortname": "MM_Matrix_s37_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s37_d04_noise_level","index": 264212,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d05_cutoff","varname": "fHslider349","shortname": "MM_Matrix_s37_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s37_d05_cutoff","index": 263860,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d06_resonance","varname": "fHslider385","shortname": "MM_Matrix_s37_d06_resonance","address": "/modular-subtractive/MM_Matrix_s37_d06_resonance","index": 264016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d07_filter_env_amt","varname": "fHslider350","shortname": "MM_Matrix_s37_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s37_d07_filter_env_amt","index": 263864,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d08_amp","varname": "fHslider236","shortname": "MM_Matrix_s37_d08_amp","address": "/modular-subtractive/MM_Matrix_s37_d08_amp","index": 263408,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s37_d09_pan","varname": "fHslider42","shortname": "MM_Matrix_s37_d09_pan","address": "/modular-subtractive/MM_Matrix_s37_d09_pan","index": 262420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d00_pitch","varname": "fHslider484","shortname": "MM_Matrix_s38_d00_pitch","address": "/modular-subtractive/MM_Matrix_s38_d00_pitch","index": 264436,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d01_osc2_detune","varname": "fHslider634","shortname": "MM_Matrix_s38_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s38_d01_osc2_detune","index": 265044,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d02_osc3_detune","varname": "fHslider534","shortname": "MM_Matrix_s38_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s38_d02_osc3_detune","index": 264636,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d03_osc_mix_bal","varname": "fHslider584","shortname": "MM_Matrix_s38_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s38_d03_osc_mix_bal","index": 264844,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d04_noise_level","varname": "fHslider433","shortname": "MM_Matrix_s38_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s38_d04_noise_level","index": 264208,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d05_cutoff","varname": "fHslider351","shortname": "MM_Matrix_s38_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s38_d05_cutoff","index": 263868,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d06_resonance","varname": "fHslider384","shortname": "MM_Matrix_s38_d06_resonance","address": "/modular-subtractive/MM_Matrix_s38_d06_resonance","index": 264012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d07_filter_env_amt","varname": "fHslider352","shortname": "MM_Matrix_s38_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s38_d07_filter_env_amt","index": 263872,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d08_amp","varname": "fHslider235","shortname": "MM_Matrix_s38_d08_amp","address": "/modular-subtractive/MM_Matrix_s38_d08_amp","index": 263404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s38_d09_pan","varname": "fHslider38","shortname": "MM_Matrix_s38_d09_pan","address": "/modular-subtractive/MM_Matrix_s38_d09_pan","index": 262396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d00_pitch","varname": "fHslider483","shortname": "MM_Matrix_s39_d00_pitch","address": "/modular-subtractive/MM_Matrix_s39_d00_pitch","index": 264432,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d01_osc2_detune","varname": "fHslider633","shortname": "MM_Matrix_s39_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s39_d01_osc2_detune","index": 265040,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d02_osc3_detune","varname": "fHslider533","shortname": "MM_Matrix_s39_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s39_d02_osc3_detune","index": 264632,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d03_osc_mix_bal","varname": "fHslider583","shortname": "MM_Matrix_s39_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s39_d03_osc_mix_bal","index": 264840,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d04_noise_level","varname": "fHslider432","shortname": "MM_Matrix_s39_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s39_d04_noise_level","index": 264204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d05_cutoff","varname": "fHslider353","shortname": "MM_Matrix_s39_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s39_d05_cutoff","index": 263876,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d06_resonance","varname": "fHslider383","shortname": "MM_Matrix_s39_d06_resonance","address": "/modular-subtractive/MM_Matrix_s39_d06_resonance","index": 264008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d07_filter_env_amt","varname": "fHslider354","shortname": "MM_Matrix_s39_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s39_d07_filter_env_amt","index": 263880,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d08_amp","varname": "fHslider234","shortname": "MM_Matrix_s39_d08_amp","address": "/modular-subtractive/MM_Matrix_s39_d08_amp","index": 263400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s39_d09_pan","varname": "fHslider34","shortname": "MM_Matrix_s39_d09_pan","address": "/modular-subtractive/MM_Matrix_s39_d09_pan","index": 262372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d00_pitch","varname": "fHslider482","shortname": "MM_Matrix_s40_d00_pitch","address": "/modular-subtractive/MM_Matrix_s40_d00_pitch","index": 264428,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d01_osc2_detune","varname": "fHslider632","shortname": "MM_Matrix_s40_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s40_d01_osc2_detune","index": 265036,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d02_osc3_detune","varname": "fHslider532","shortname": "MM_Matrix_s40_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s40_d02_osc3_detune","index": 264628,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d03_osc_mix_bal","varname": "fHslider582","shortname": "MM_Matrix_s40_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s40_d03_osc_mix_bal","index": 264836,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d04_noise_level","varname": "fHslider431","shortname": "MM_Matrix_s40_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s40_d04_noise_level","index": 264200,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d05_cutoff","varname": "fHslider355","shortname": "MM_Matrix_s40_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s40_d05_cutoff","index": 263884,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d06_resonance","varname": "fHslider382","shortname": "MM_Matrix_s40_d06_resonance","address": "/modular-subtractive/MM_Matrix_s40_d06_resonance","index": 264004,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d07_filter_env_amt","varname": "fHslider356","shortname": "MM_Matrix_s40_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s40_d07_filter_env_amt","index": 263888,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d08_amp","varname": "fHslider233","shortname": "MM_Matrix_s40_d08_amp","address": "/modular-subtractive/MM_Matrix_s40_d08_amp","index": 263396,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s40_d09_pan","varname": "fHslider30","shortname": "MM_Matrix_s40_d09_pan","address": "/modular-subtractive/MM_Matrix_s40_d09_pan","index": 262348,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d00_pitch","varname": "fHslider481","shortname": "MM_Matrix_s41_d00_pitch","address": "/modular-subtractive/MM_Matrix_s41_d00_pitch","index": 264424,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d01_osc2_detune","varname": "fHslider631","shortname": "MM_Matrix_s41_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s41_d01_osc2_detune","index": 265032,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d02_osc3_detune","varname": "fHslider531","shortname": "MM_Matrix_s41_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s41_d02_osc3_detune","index": 264624,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d03_osc_mix_bal","varname": "fHslider581","shortname": "MM_Matrix_s41_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s41_d03_osc_mix_bal","index": 264832,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d04_noise_level","varname": "fHslider430","shortname": "MM_Matrix_s41_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s41_d04_noise_level","index": 264196,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d05_cutoff","varname": "fHslider357","shortname": "MM_Matrix_s41_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s41_d05_cutoff","index": 263892,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d06_resonance","varname": "fHslider381","shortname": "MM_Matrix_s41_d06_resonance","address": "/modular-subtractive/MM_Matrix_s41_d06_resonance","index": 264000,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d07_filter_env_amt","varname": "fHslider358","shortname": "MM_Matrix_s41_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s41_d07_filter_env_amt","index": 263896,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d08_amp","varname": "fHslider232","shortname": "MM_Matrix_s41_d08_amp","address": "/modular-subtractive/MM_Matrix_s41_d08_amp","index": 263392,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s41_d09_pan","varname": "fHslider26","shortname": "MM_Matrix_s41_d09_pan","address": "/modular-subtractive/MM_Matrix_s41_d09_pan","index": 262324,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d00_pitch","varname": "fHslider480","shortname": "MM_Matrix_s42_d00_pitch","address": "/modular-subtractive/MM_Matrix_s42_d00_pitch","index": 264420,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d01_osc2_detune","varname": "fHslider630","shortname": "MM_Matrix_s42_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s42_d01_osc2_detune","index": 265028,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d02_osc3_detune","varname": "fHslider530","shortname": "MM_Matrix_s42_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s42_d02_osc3_detune","index": 264620,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d03_osc_mix_bal","varname": "fHslider580","shortname": "MM_Matrix_s42_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s42_d03_osc_mix_bal","index": 264828,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d04_noise_level","varname": "fHslider429","shortname": "MM_Matrix_s42_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s42_d04_noise_level","index": 264192,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d05_cutoff","varname": "fHslider359","shortname": "MM_Matrix_s42_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s42_d05_cutoff","index": 263900,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d06_resonance","varname": "fHslider380","shortname": "MM_Matrix_s42_d06_resonance","address": "/modular-subtractive/MM_Matrix_s42_d06_resonance","index": 263996,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d07_filter_env_amt","varname": "fHslider360","shortname": "MM_Matrix_s42_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s42_d07_filter_env_amt","index": 263904,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d08_amp","varname": "fHslider231","shortname": "MM_Matrix_s42_d08_amp","address": "/modular-subtractive/MM_Matrix_s42_d08_amp","index": 263388,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s42_d09_pan","varname": "fHslider22","shortname": "MM_Matrix_s42_d09_pan","address": "/modular-subtractive/MM_Matrix_s42_d09_pan","index": 262300,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d00_pitch","varname": "fHslider479","shortname": "MM_Matrix_s43_d00_pitch","address": "/modular-subtractive/MM_Matrix_s43_d00_pitch","index": 264416,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d01_osc2_detune","varname": "fHslider629","shortname": "MM_Matrix_s43_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s43_d01_osc2_detune","index": 265024,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d02_osc3_detune","varname": "fHslider529","shortname": "MM_Matrix_s43_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s43_d02_osc3_detune","index": 264616,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d03_osc_mix_bal","varname": "fHslider579","shortname": "MM_Matrix_s43_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s43_d03_osc_mix_bal","index": 264824,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d04_noise_level","varname": "fHslider428","shortname": "MM_Matrix_s43_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s43_d04_noise_level","index": 264188,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d05_cutoff","varname": "fHslider361","shortname": "MM_Matrix_s43_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s43_d05_cutoff","index": 263908,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d06_resonance","varname": "fHslider379","shortname": "MM_Matrix_s43_d06_resonance","address": "/modular-subtractive/MM_Matrix_s43_d06_resonance","index": 263992,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d07_filter_env_amt","varname": "fHslider362","shortname": "MM_Matrix_s43_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s43_d07_filter_env_amt","index": 263912,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d08_amp","varname": "fHslider230","shortname": "MM_Matrix_s43_d08_amp","address": "/modular-subtractive/MM_Matrix_s43_d08_amp","index": 263384,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s43_d09_pan","varname": "fHslider18","shortname": "MM_Matrix_s43_d09_pan","address": "/modular-subtractive/MM_Matrix_s43_d09_pan","index": 262276,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d00_pitch","varname": "fHslider478","shortname": "MM_Matrix_s44_d00_pitch","address": "/modular-subtractive/MM_Matrix_s44_d00_pitch","index": 264412,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d01_osc2_detune","varname": "fHslider628","shortname": "MM_Matrix_s44_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s44_d01_osc2_detune","index": 265020,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d02_osc3_detune","varname": "fHslider528","shortname": "MM_Matrix_s44_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s44_d02_osc3_detune","index": 264612,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d03_osc_mix_bal","varname": "fHslider578","shortname": "MM_Matrix_s44_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s44_d03_osc_mix_bal","index": 264820,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d04_noise_level","varname": "fHslider427","shortname": "MM_Matrix_s44_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s44_d04_noise_level","index": 264184,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d05_cutoff","varname": "fHslider363","shortname": "MM_Matrix_s44_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s44_d05_cutoff","index": 263916,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d06_resonance","varname": "fHslider378","shortname": "MM_Matrix_s44_d06_resonance","address": "/modular-subtractive/MM_Matrix_s44_d06_resonance","index": 263988,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d07_filter_env_amt","varname": "fHslider364","shortname": "MM_Matrix_s44_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s44_d07_filter_env_amt","index": 263920,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d08_amp","varname": "fHslider229","shortname": "MM_Matrix_s44_d08_amp","address": "/modular-subtractive/MM_Matrix_s44_d08_amp","index": 263380,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s44_d09_pan","varname": "fHslider14","shortname": "MM_Matrix_s44_d09_pan","address": "/modular-subtractive/MM_Matrix_s44_d09_pan","index": 262252,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d00_pitch","varname": "fHslider477","shortname": "MM_Matrix_s45_d00_pitch","address": "/modular-subtractive/MM_Matrix_s45_d00_pitch","index": 264408,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d01_osc2_detune","varname": "fHslider627","shortname": "MM_Matrix_s45_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s45_d01_osc2_detune","index": 265016,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d02_osc3_detune","varname": "fHslider527","shortname": "MM_Matrix_s45_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s45_d02_osc3_detune","index": 264608,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d03_osc_mix_bal","varname": "fHslider577","shortname": "MM_Matrix_s45_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s45_d03_osc_mix_bal","index": 264816,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d04_noise_level","varname": "fHslider426","shortname": "MM_Matrix_s45_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s45_d04_noise_level","index": 264180,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d05_cutoff","varname": "fHslider365","shortname": "MM_Matrix_s45_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s45_d05_cutoff","index": 263924,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d06_resonance","varname": "fHslider377","shortname": "MM_Matrix_s45_d06_resonance","address": "/modular-subtractive/MM_Matrix_s45_d06_resonance","index": 263984,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d07_filter_env_amt","varname": "fHslider366","shortname": "MM_Matrix_s45_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s45_d07_filter_env_amt","index": 263928,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d08_amp","varname": "fHslider228","shortname": "MM_Matrix_s45_d08_amp","address": "/modular-subtractive/MM_Matrix_s45_d08_amp","index": 263376,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s45_d09_pan","varname": "fHslider10","shortname": "MM_Matrix_s45_d09_pan","address": "/modular-subtractive/MM_Matrix_s45_d09_pan","index": 262228,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d00_pitch","varname": "fHslider476","shortname": "MM_Matrix_s46_d00_pitch","address": "/modular-subtractive/MM_Matrix_s46_d00_pitch","index": 264404,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d01_osc2_detune","varname": "fHslider626","shortname": "MM_Matrix_s46_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s46_d01_osc2_detune","index": 265012,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d02_osc3_detune","varname": "fHslider526","shortname": "MM_Matrix_s46_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s46_d02_osc3_detune","index": 264604,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d03_osc_mix_bal","varname": "fHslider576","shortname": "MM_Matrix_s46_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s46_d03_osc_mix_bal","index": 264812,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d04_noise_level","varname": "fHslider425","shortname": "MM_Matrix_s46_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s46_d04_noise_level","index": 264176,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d05_cutoff","varname": "fHslider367","shortname": "MM_Matrix_s46_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s46_d05_cutoff","index": 263932,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d06_resonance","varname": "fHslider376","shortname": "MM_Matrix_s46_d06_resonance","address": "/modular-subtractive/MM_Matrix_s46_d06_resonance","index": 263980,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d07_filter_env_amt","varname": "fHslider368","shortname": "MM_Matrix_s46_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s46_d07_filter_env_amt","index": 263936,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d08_amp","varname": "fHslider227","shortname": "MM_Matrix_s46_d08_amp","address": "/modular-subtractive/MM_Matrix_s46_d08_amp","index": 263372,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s46_d09_pan","varname": "fHslider6","shortname": "MM_Matrix_s46_d09_pan","address": "/modular-subtractive/MM_Matrix_s46_d09_pan","index": 262204,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d00_pitch","varname": "fHslider475","shortname": "MM_Matrix_s47_d00_pitch","address": "/modular-subtractive/MM_Matrix_s47_d00_pitch","index": 264400,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d01_osc2_detune","varname": "fHslider625","shortname": "MM_Matrix_s47_d01_osc2_detune","address": "/modular-subtractive/MM_Matrix_s47_d01_osc2_detune","index": 265008,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d02_osc3_detune","varname": "fHslider525","shortname": "MM_Matrix_s47_d02_osc3_detune","address": "/modular-subtractive/MM_Matrix_s47_d02_osc3_detune","index": 264600,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d03_osc_mix_bal","varname": "fHslider575","shortname": "MM_Matrix_s47_d03_osc_mix_bal","address": "/modular-subtractive/MM_Matrix_s47_d03_osc_mix_bal","index": 264808,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d04_noise_level","varname": "fHslider424","shortname": "MM_Matrix_s47_d04_noise_level","address": "/modular-subtractive/MM_Matrix_s47_d04_noise_level","index": 264172,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d05_cutoff","varname": "fHslider369","shortname": "MM_Matrix_s47_d05_cutoff","address": "/modular-subtractive/MM_Matrix_s47_d05_cutoff","index": 263940,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d06_resonance","varname": "fHslider375","shortname": "MM_Matrix_s47_d06_resonance","address": "/modular-subtractive/MM_Matrix_s47_d06_resonance","index": 263976,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d07_filter_env_amt","varname": "fHslider370","shortname": "MM_Matrix_s47_d07_filter_env_amt","address": "/modular-subtractive/MM_Matrix_s47_d07_filter_env_amt","index": 263944,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d08_amp","varname": "fHslider226","shortname": "MM_Matrix_s47_d08_amp","address": "/modular-subtractive/MM_Matrix_s47_d08_amp","index": 263368,"init": 0,"min": -1,"max": 1,"step": 0.001},{"type": "hslider","label": "MM_Matrix/s47_d09_pan","varname": "fHslider2","shortname": "MM_Matrix_s47_d09_pan","address": "/modular-subtractive/MM_Matrix_s47_d09_pan","index": 262180,"init": 0,"min": -1,"max": 1,"step": 0.001}]}]} \ No newline at end of file diff --git a/playground/faust/modular-subtractive.wasm b/playground/faust/modular-subtractive.wasm deleted file mode 100644 index ff056fef95ebdbb0eae086218b925a16bd5a2762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317527 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 - - - - - NISPS Playground - - - - - - diff --git a/playground/js/a-app.js b/playground/js/a-app.js deleted file mode 100644 index dd8be33..0000000 --- a/playground/js/a-app.js +++ /dev/null @@ -1,4521 +0,0 @@ -// NISPS Immersive — Design A -// Full-viewport flow field with floating overlays - -import { WasmIML } from './nisps/nisps-wasm.js'; -import { FlowFieldVisualizer } from './ui/visualizer.js'; -import { AudioCanvas } from './audio/audio-canvas.js'; -import { C15Adapter } from './synth/c15-adapter.js'; -import { Arpeggiator } from './synth/arpeggiator.js'; -import { MIDIInput } from './synth/midi-input.js'; -import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyCurve, applyGroupOverride } from './synth/param-map.js'; -import { MIDIOutput } from './midi/midi-output.js'; -import { loadCCMap, saveCCMap, createCCParam, exportCCMap, importCCMap, CC_NAMES } from './midi/midi-cc-map.js'; -import { listPresets as listMidiPresets, loadPreset as loadMidiPreset, loadPresetFromFile as loadMidiPresetFromFile } from './midi/midi-cc-presets.js'; -import { GamepadInput } from './ui/gamepad.js'; -import { HandTracker } from './ui/hand-tracker.js'; -import { createDevPanel } from './ui/dev-panel.js'; -import { SYNTH_PRESETS, PRESET_TIERS } from './synth/presets.js'; -import { ADDITIVE_PRESETS, ADDITIVE_PRESET_TIERS } from './synth/additive-presets.js'; -import { FM_PRESETS, FM_PRESET_TIERS } from './synth/fm-presets.js'; -import { EOCChain } from './eoc/index.js'; -import { EOCChainUI, moduleFactory } from './ui/eoc-chain-ui.js'; -import { EngineSwitcher } from './ui/engine-switcher.js'; -import { EOCJoystick } from './ui/eoc-joystick.js'; -import { initModularUI } from './ui/modular-ui.js'; -import { AdditiveEngine } from './synth/additive-engine.js'; -import { FMEngine } from './synth/fm-engine.js'; -import { ModularEngine } from './synth/modular-engine.js'; -import { MODULAR_PRESETS, applyPreset as applyModularPreset, findPreset as findModularPreset } from './synth/modular-presets.js'; - -// ---- Constants ---- -const N_JOY_INPUTS = 2; -const N_HAND_INPUTS = 14; -const N_INPUTS = N_JOY_INPUTS; // default (joystick) -const N_VISUAL_OUTPUTS = 20; -const N_SYNTH_OUTPUTS = SYNTH_PARAM_MAP.length; // 126 -// Audio canvas output count is dynamic — queried from audioCanvas.getOutputCount() -let N_OUTPUTS = N_SYNTH_OUTPUTS; // Dynamic — changes with output mode - -let audioCanvas = null; // lazily created on first switch to audio-canvas mode -const STORAGE_KEY = 'nisps-a-immersive'; - -// Phase E — deferred modular DSP state. loadState() stashes this; the -// engine-switch handler consumes it as soon as a ModularEngine exists. -let _pendingModularDspState = null; - -const VISUAL_PARAM_NAMES = [ - 'Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', - 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', - 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate', -]; -const VISUAL_PARAM_COLORS = [ - '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', - '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', - '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', - '#ffc15f', '#ff8a3d', -]; - -// ---- Presets ---- -const PRESETS = { - 'calm-to-chaotic': [ - { input: [0.1, 0.9], output: [0.25, 0.3, 0.1, 0.55, 0.2, 0.3, 0.02, 0.05, 0.9, 0.45, 0.25, 0.2, 0.9, 0.0, 0.0, 0.2, 0.05, 0.0, 0.0, 0.2] }, - { input: [0.9, 0.1], output: [0.75, 0.7, 0.9, 0.05, 0.8, 0.7, 0.9, 0.95, 0.3, 0.2, 0.85, 0.7, 0.25, 0.55, 1.0, 0.92, 0.02, 0.95, 0.8, 0.85] }, - { input: [0.5, 0.5], output: [0.5, 0.5, 0.5, 0.3, 0.5, 0.5, 0.4, 0.5, 0.7, 0.6, 0.5, 0.45, 0.55, 0.9, 0.5, 0.65, 0.08, 0.45, 0.4, 0.5] }, - ], - 'rainbow-sweep': [ - { input: [0.0, 0.5], output: [0.5, 0.5, 0.4, 0.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.4, 0.3, 0.8, 0.0, 0.0, 0.45, 0.08, 0.2, 0.25, 0.45] }, - { input: [0.5, 0.5], output: [0.5, 0.5, 0.4, 0.5, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.55, 0.35, 0.7, 0.0, 0.4, 0.45, 0.08, 0.45, 0.4, 0.6] }, - { input: [1.0, 0.5], output: [0.5, 0.5, 0.4, 1.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.75, 0.45, 0.6, 0.0, 0.8, 0.45, 0.08, 0.7, 0.55, 0.75] }, - ], - 'vortex': [ - { input: [0.5, 0.5], output: [0.0, 0.8, 0.8, 0.6, 0.1, 0.15, 0.02, 1.0, 1.0, 0.3, 0.95, 0.85, 0.25, 1.0, 0.5, 0.95, 0.01, 1.0, 1.0, 1.0] }, - { input: [0.0, 0.0], output: [0.5, 0.2, 0.3, 0.8, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.2, 0.35, 0.2, 0.15, 0.2, 0.25] }, - { input: [1.0, 1.0], output: [0.5, 0.2, 0.3, 0.2, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.8, 0.35, 0.2, 0.15, 0.2, 0.25] }, - { input: [0.0, 1.0], output: [0.3, 0.4, 0.5, 0.4, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.4, 0.7, 0.1, 0.5, 0.4, 0.55] }, - { input: [1.0, 0.0], output: [0.7, 0.4, 0.5, 0.0, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.9, 0.7, 0.1, 0.5, 0.4, 0.55] }, - ], - 'spiral': [ - { input: [0.5, 0.5], output: [0.0, 0.6, 0.6, 0.3, 0.15, 0.2, 0.03, 0.7, 0.9, 0.25, 0.7, 0.6, 0.35, 0.8, 0.65, 0.9, 0.02, 0.3, 0.5, 0.7] }, - { input: [0.0, 0.0], output: [0.8, 0.3, 0.4, 0.7, 0.8, 0.5, 0.06, 0.2, 0.5, 0.7, 0.3, 0.2, 0.7, 0.3, 0.3, 0.4, 0.15, 0.1, 0.1, 0.3] }, - { input: [1.0, 1.0], output: [0.2, 0.3, 0.4, 0.1, 0.8, 0.5, 0.06, 0.2, 0.5, 0.7, 0.3, 0.2, 0.7, 0.3, 0.9, 0.4, 0.15, 0.1, 0.1, 0.3] }, - ], - 'embers': [ - { input: [0.5, 0.5], output: [0.1, 0.4, 0.2, 0.05, 0.05, 0.6, 0.02, 0.15, 0.6, 0.3, 0.15, 0.9, 0.5, 0.7, 0.1, 0.3, 0.2, 0.0, 0.0, 0.2] }, - { input: [0.2, 0.8], output: [0.5, 0.6, 0.15, 0.08, 0.08, 0.8, 0.015, 0.1, 0.8, 0.5, 0.1, 0.5, 0.8, 0.4, 0.05, 0.5, 0.1, 0.0, 0.0, 0.15] }, - { input: [0.8, 0.2], output: [0.9, 0.3, 0.35, 0.02, 0.12, 0.35, 0.04, 0.3, 0.4, 0.2, 0.3, 1.0, 0.3, 0.9, 0.2, 0.15, 0.3, 0.0, 0.0, 0.3] }, - ], -}; - -// ---- ShapeSeq feature flag ---- -const shapeSeqEnabled = new URLSearchParams(location.search).get('shapeseq') === '1'; - -// Lazy-loaded ShapeSeq modules (only when shapeSeqEnabled) -let ShapeSeqEngine, StepVisualizer, ChainBuilderUI, EventBus, getDefaultBus; - -// ShapeSeq state -let shapeSeqEngine = null; -let shapeSeqViz = null; -let shapeSeqChainUI = null; -let shapeSeqBus = null; -let _shapeSeqInited = false; - -// ---- App state ---- -let iml; // active IML (points to imlJoy or imlHand) -let imlJoy; // IML for joystick mode (2 inputs) -let imlHand; // IML for hand tracking mode (14 inputs) -let inputMode = 'joystick'; // 'joystick' | 'hands' -let handTracker = null; -let visualizer; -let synthVisualizer; -let activeEngine = null; // active SynthEngine (currently always a C15Adapter) -let c15 = null; // alias kept for MIDIInput and volume controls that still reference it -let arpeggiator = null; -let midiInput = null; - -let outputMode = 'visual'; - -// EOC Effects Chain — module-scoped so audio-start and setActiveEngine can reference it -let eocChain = null; -let _eocInited = false; // guard: init once per AudioContext lifetime - -// EOC Linked/Independent mode — second IML driven by joystick(s), independent training -let imlEoc = null; // second IML for EOC params (Linked/Independent mode) -let eocTrainingTarget = 'synth'; // 'synth' | 'eoc' — which MLP RL feedback targets -let eocJoystick = null; // EOCJoystick instance for Independent mode - -// Modular-mode UI (Phase C) — created once during init(), toggled via show/hide -// by setActiveEngine() when the modular engine becomes active. -let modularUI = null; - -// ---- MIDI CC state ---- -let midiOutput = null; -// Storage key scoped per engine so CC assignments don't bleed between engines. -function midiCCStorageKey() { - return `nisps-midi-cc-map:${activeEngine?.id ?? 'shaper-feedback'}`; -} -let midiCCMap = loadCCMap(midiCCStorageKey()); -// Per-param overrides for MIDI CC mode (same shape as visual overrides) -let midiCCOverrides = midiCCMap.map(p => ({ - min: p.min, max: p.max, curve: p.curve, frozen: p.muted, fixedValue: p.fixedValue, -})); -const MIDI_CC_PARAM_COLORS = []; -// Generate distinct colors for CC params -function _generateCCColors(n) { - MIDI_CC_PARAM_COLORS.length = 0; - for (let i = 0; i < n; i++) { - const hue = (i * 137.508) % 360; // golden angle - MIDI_CC_PARAM_COLORS.push(`hsl(${hue}, 70%, 60%)`); - } -} -_generateCCColors(midiCCMap.length); - -/** Reload the MIDI CC map from localStorage for the current engine. */ -function reloadMidiCCMap() { - const saved = loadCCMap(midiCCStorageKey()); - midiCCMap.length = 0; - midiCCMap.push(...saved); - midiCCOverrides.length = 0; - midiCCOverrides.push(...saved.map(p => ({ - min: p.min, max: p.max, curve: p.curve, frozen: p.muted, fixedValue: p.fixedValue, - }))); - _generateCCColors(midiCCMap.length); -} - -// tame level is now baked into groupOverrides defaults at init time (see ?tame URL param) -let spreadLevel = 0.6; -let noiseLevel = 0.05; -let rlExplorationDecay = 0.97; - -// Joystick state -let joyX = 0.5; -let joyY = 0.5; -let joyDragging = false; -let joyFollowMode = false; -let joyTrail = []; - -// Gamepad -let gamepad; - -// Raw param slider values (for examples mode advanced editing) -let rawParamValues = new Array(N_OUTPUTS).fill(0.5); - -// ---- DOM refs ---- -let $canvas, $heatmapCells, $heatmapTooltip; -let $joystickContainer, $joyMap, $joyMapCtx; -let $noiseRing, $followBadge; -let $rlButtons, $btnThumbsUp, $btnThumbsDown; -let $statusText; -let $lossCanvas, $lossCtx; -let $engineParams; -let $followPill; -let $synthVisCanvas; - -// Undo stack — stores weight snapshots before RL actions -const undoStack = []; -const MAX_UNDO = 20; - -// ---- Visual Overrides: per-param curve/min/max/freeze for visual mode ---- -const visualOverrides = VISUAL_PARAM_NAMES.map(() => ({ - min: 0, - max: 1, - curve: 0.5, - frozen: false, - fixedValue: 0.5, -})); - -// ---- Audio Canvas Overrides ---- -// Dynamic array — resized when AudioCanvas output count changes -let audioCanvasOverrides = []; -function _ensureAudioCanvasOverrides(count) { - while (audioCanvasOverrides.length < count) { - audioCanvasOverrides.push({ min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5 }); - } - if (audioCanvasOverrides.length > count) audioCanvasOverrides.length = count; -} - -// ---- Synth Sections (for SynthVisualizer) ---- -const SYNTH_SECTIONS = [ - { name: 'Env A', count: 7, color: '#4488ff' }, - { name: 'Env B', count: 7, color: '#4488ff' }, - { name: 'Env C', count: 6, color: '#6688dd' }, - { name: 'Osc A', count: 5, color: '#ff8844' }, - { name: 'Osc B', count: 5, color: '#ff8844' }, - { name: 'Shp A', count: 6, color: '#ff4466' }, - { name: 'Shp B', count: 6, color: '#ff4466' }, - { name: 'Comb', count: 8, color: '#44ddaa' }, - { name: 'SVF', count: 9, color: '#44ddaa' }, - { name: 'Gap', count: 6, color: '#44bbaa' }, - { name: 'FB Mix', count: 9, color: '#ddaa44' }, - { name: 'Out Mix', count: 14, color: '#ddaa44' }, - { name: 'Cabinet', count: 8, color: '#aa88dd' }, - { name: 'Flanger', count: 13, color: '#dd88aa' }, - { name: 'Echo', count: 7, color: '#88aadd' }, - { name: 'Reverb', count: 6, color: '#88ddaa' }, - { name: 'Unison', count: 3, color: '#cccccc' }, - { name: 'Mono', count: 1, color: '#999999' }, -]; - -// ---- Group Overrides: per-group curve + per-param min/max/curve/mute ---- -// groupOverrides[sectionIndex] = { curve: 0.5, params: [{ min, max, curve, muted, fixedValue }, ...] } -// Defaults are seeded from safeMin/safeMax in SYNTH_PARAM_MAP, scaled by the -// ?tame URL parameter (0 = unconstrained, 1 = full safe limits). -// localStorage overrides on load. - -// Module-level tame so applyPreset() can use it -const _urlTame = parseFloat(new URLSearchParams(window.location.search).get('tame') ?? '1'); -const tameLevel = isNaN(_urlTame) ? 1 : Math.max(0, Math.min(1, _urlTame)); - -const groupOverrides = (() => { - const t = tameLevel; - let flatIdx = 0; - return SYNTH_SECTIONS.map(sec => ({ - curve: 0.5, - params: new Array(sec.count).fill(null).map(() => { - const p = SYNTH_PARAM_MAP[flatIdx++]; - // Interpolate: at tame=0 use full [0,1]; at tame=1 use [safeMin, safeMax] - const safeMin = p?.safeMin ?? 0; - const safeMax = p?.safeMax ?? 1; - return { - min: safeMin * t, - max: 1 - (1 - safeMax) * t, - curve: 0.5, - muted: false, - fixedValue: 0.5, - }; - }), - })); -})(); - -// Lookup: param name -> flat index (for preset application) -const paramNameToIndex = new Map(); -SYNTH_PARAM_MAP.forEach((p, i) => paramNameToIndex.set(p.name, i)); - -// Currently active synth preset id (null = no preset / manual) -let activeSynthPresetId = null; - -// Build a flat lookup: paramIndex -> { sectionIndex, localIndex } -const paramToSection = []; -{ - let idx = 0; - for (let si = 0; si < SYNTH_SECTIONS.length; si++) { - for (let li = 0; li < SYNTH_SECTIONS[si].count; li++) { - paramToSection.push({ si, li }); - idx++; - } - } - // Pad for any params beyond sections - while (paramToSection.length < N_SYNTH_OUTPUTS) { - paramToSection.push(null); - } -} - -/** - * Rebuild paramToSection from an engine's paramMeta groups. - * For C15, restores the static SYNTH_SECTIONS-based mapping. - * For non-C15 engines, derives sections from paramMeta group fields. - */ -function rebuildParamToSection(paramMeta) { - paramToSection.length = 0; - let si = 0, li = 0; - let currentGroup = null; - for (const pm of paramMeta) { - const group = pm.group ?? 'Other'; - if (group !== currentGroup) { - if (currentGroup !== null) si++; - currentGroup = group; - li = 0; - } - paramToSection.push({ si, li }); - li++; - } -} - -/** - * Dynamic section metadata for non-C15 engines (parallel to SYNTH_SECTIONS - * for C15). Indexed by section index (the `si` field in paramToSection). - * - * nonC15Sections[si] = { name, color, count, startIndex } - * nonC15GroupCurves[si] = scalar group-master curve [0,1] - * - * Group curves persist across engine switches keyed by section NAME, so a - * user's "Filter" curve survives a sub-engine swap that keeps the same - * group label. - */ -let nonC15Sections = []; -let nonC15GroupCurves = []; -const _nonC15GroupCurveMemory = new Map(); // groupName -> curve - -function rebuildNonC15Sections(paramMeta) { - nonC15Sections = []; - nonC15GroupCurves = []; - let currentGroup = null; - let si = -1; - for (let i = 0; i < paramMeta.length; i++) { - const g = paramMeta[i].group ?? 'Other'; - if (g !== currentGroup) { - si++; - currentGroup = g; - nonC15Sections.push({ - name: g, - color: _colorFromGroup(g), - count: 0, - startIndex: i, - }); - nonC15GroupCurves.push(_nonC15GroupCurveMemory.get(g) ?? 0.5); - } - nonC15Sections[si].count++; - } -} - -/** - * Restore paramToSection to the static C15 layout (from SYNTH_SECTIONS). - */ -function restoreC15ParamToSection() { - paramToSection.length = 0; - let idx = 0; - for (let si = 0; si < SYNTH_SECTIONS.length; si++) { - for (let li = 0; li < SYNTH_SECTIONS[si].count; li++) { - paramToSection.push({ si, li }); - idx++; - } - } - while (paramToSection.length < N_SYNTH_OUTPUTS) { - paramToSection.push(null); - } -} - -// ---- Engine-aware preset helpers ---- - -/** - * Return the preset array for the given engine id. - */ -function getPresetsForEngine(engineId) { - switch (engineId) { - case 'shaper-feedback': return SYNTH_PRESETS; - case 'additive': return ADDITIVE_PRESETS; - case 'fm': return FM_PRESETS; - default: return []; - } -} - -/** - * Return the tier labels for the given engine id. - */ -function getPresetTiersForEngine(engineId) { - switch (engineId) { - case 'shaper-feedback': return PRESET_TIERS; - case 'additive': return ADDITIVE_PRESET_TIERS; - case 'fm': return FM_PRESET_TIERS; - default: return []; - } -} - -// ---- Engine param overrides (for non-C15 engines) ---- -// Flat array of per-param overrides for the current Faust engine. -// null when C15 is active (uses groupOverrides instead). -// Each entry: { min, max, curve, muted, fixedValue } -let engineParamOverrides = null; - -/** - * Build a fresh engineParamOverrides array from the current engine's paramMeta. - * All params default to unmuted with [0,1] range and linear curve. - */ -function buildEngineParamOverrides() { - const meta = activeEngine?.paramMeta; - if (!meta) { engineParamOverrides = null; return; } - engineParamOverrides = meta.map(p => ({ - min: 0, - max: 1, - curve: 0.5, - muted: false, - fixedValue: 0.5, - })); -} - -/** - * Apply group overrides (per-param curve + min/max) to a single ML output value. - * Returns the remapped value, or fixedValue if the param is muted. - * - * For non-C15 engines, uses engineParamOverrides (flat array). - * For C15, uses the nested groupOverrides/paramToSection system. - */ -function applyGroupOverrides(rawValue, paramIndex) { - // Non-C15 engine: use flat engine param overrides - if (engineParamOverrides && paramIndex < engineParamOverrides.length) { - const p = engineParamOverrides[paramIndex]; - if (p.muted) return p.fixedValue; - return applyGroupOverride(rawValue, p.curve, p.min, p.max); - } - // C15 path: nested section/group overrides - const mapping = paramToSection[paramIndex]; - if (!mapping) return rawValue; - const ov = groupOverrides[mapping.si]; - const p = ov.params[mapping.li]; - if (p.muted) return p.fixedValue; - // Per-param curve overrides group curve (if param curve != 0.5, use it; else use group curve) - const curve = p.curve !== 0.5 ? p.curve : ov.curve; - return applyGroupOverride(rawValue, curve, p.min, p.max); -} - -/** Check if param at given index is muted */ -function isParamMuted(paramIndex) { - // Non-C15 engine: use flat engine param overrides - if (engineParamOverrides && paramIndex < engineParamOverrides.length) { - return engineParamOverrides[paramIndex].muted; - } - // C15 path - const mapping = paramToSection[paramIndex]; - if (!mapping) return false; - return groupOverrides[mapping.si].params[mapping.li].muted; -} - -// ---- Synth Preset Application ---- - -/** - * Apply a synth preset by id. - * Engine-aware: uses groupOverrides for C15, engineParamOverrides for Faust engines. - * Sets muted/active, min/max/curve/fixedValue for all params, - * re-routes outputs, saves state, and updates the UI dropdown. - */ -function applyPreset(presetId) { - const engineId = activeEngine?.id ?? 'shaper-feedback'; - const presets = getPresetsForEngine(engineId); - const preset = presets.find(p => p.id === presetId); - if (!preset) { - console.warn(`[NISPS] Unknown preset: ${presetId} (engine: ${engineId})`); - return; - } - - const allActive = preset.active === null; // null = all params active - const activeSet = allActive ? null : new Set(preset.active); - const overrides = preset.overrides || {}; - const mutedOv = preset.mutedOverrides || {}; - - if (engineId === 'shaper-feedback') { - // ---- C15 path: uses groupOverrides / paramToSection ---- - const t = tameLevel; - for (let i = 0; i < N_SYNTH_OUTPUTS; i++) { - const param = SYNTH_PARAM_MAP[i]; - const mapping = paramToSection[i]; - if (!mapping) continue; - const gp = groupOverrides[mapping.si].params[mapping.li]; - const paramName = param.name; - - const safeMin = param.safeMin ?? 0; - const safeMax = param.safeMax ?? 1; - const tameMin = safeMin * t; - const tameMax = 1 - (1 - safeMax) * t; - - const isActive = allActive || activeSet.has(paramName); - - if (isActive) { - gp.muted = false; - const ov = overrides[paramName]; - if (ov) { - gp.min = ov.min !== undefined ? ov.min : tameMin; - gp.max = ov.max !== undefined ? ov.max : tameMax; - gp.curve = ov.curve !== undefined ? ov.curve : 0.5; - gp.fixedValue = ov.fixedValue !== undefined ? ov.fixedValue : param.defaultValue; - } else { - gp.min = tameMin; - gp.max = tameMax; - gp.curve = 0.5; - gp.fixedValue = param.defaultValue; - } - } else { - gp.muted = true; - const mov = mutedOv[paramName]; - gp.fixedValue = (mov && mov.fixedValue !== undefined) ? mov.fixedValue : param.defaultValue; - gp.min = tameMin; - gp.max = tameMax; - gp.curve = 0.5; - } - } - - // Apply group curves - for (let si = 0; si < SYNTH_SECTIONS.length; si++) { - const gc = preset.groupCurves || {}; - const secName = SYNTH_SECTIONS[si].name; - groupOverrides[si].curve = (gc[secName] !== undefined) ? gc[secName] : 0.5; - } - } else { - // ---- Faust engine path: uses engineParamOverrides (flat array) ---- - const meta = activeEngine?.paramMeta ?? []; - // Build id -> index lookup - const idToIndex = new Map(); - meta.forEach((p, i) => idToIndex.set(p.id, i)); - - // Ensure engineParamOverrides exists with correct length - if (!engineParamOverrides || engineParamOverrides.length !== meta.length) { - buildEngineParamOverrides(); - } - - for (let i = 0; i < meta.length; i++) { - const paramId = meta[i].id; - const ep = engineParamOverrides[i]; - const isActive = allActive || activeSet.has(paramId); - - if (isActive) { - ep.muted = false; - const ov = overrides[paramId]; - if (ov) { - ep.min = ov.min !== undefined ? ov.min : 0; - ep.max = ov.max !== undefined ? ov.max : 1; - ep.curve = ov.curve !== undefined ? ov.curve : 0.5; - ep.fixedValue = ov.fixedValue !== undefined ? ov.fixedValue : 0.5; - } else { - ep.min = 0; - ep.max = 1; - ep.curve = 0.5; - ep.fixedValue = 0.5; - } - } else { - ep.muted = true; - const mov = mutedOv[paramId]; - ep.fixedValue = (mov && mov.fixedValue !== undefined) ? mov.fixedValue : 0.5; - ep.min = 0; - ep.max = 1; - ep.curve = 0.5; - } - } - } - - activeSynthPresetId = presetId; - - // Close the group drawer if open - hideGroupDrawer(); - - // Re-route outputs through updated overrides - if (iml) { - routeOutputs(iml.getOutputs()); - } - - // Update the preset dropdown - const $presetSelect = document.getElementById('synth-preset-select'); - if ($presetSelect) $presetSelect.value = presetId; - - // Save to storage - saveState(); - - console.log(`[NISPS] Applied synth preset: ${preset.name} (engine: ${engineId})`); -} - -// ---- SynthVisualizer class ---- -class SynthVisualizer { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.params = new Array(N_OUTPUTS).fill(0.5); - this.displayParams = new Array(N_OUTPUTS).fill(0.5); - this.lerpSpeed = 0.12; - this.topPadding = 40; - this.bottomPadding = 100; - - // Interaction state - this._dragging = false; - this._dragBarIndex = -1; - this._interactionEnabled = false; - - // Hover tooltip state - this._hoveredBar = -1; - this._tooltipEl = null; - - // Build section map - this.sectionMap = []; - let idx = 0; - for (const sec of SYNTH_SECTIONS) { - for (let i = 0; i < sec.count && idx < N_OUTPUTS; i++, idx++) { - this.sectionMap.push(sec); - } - } - // Fill remainder with generic - while (this.sectionMap.length < N_OUTPUTS) { - this.sectionMap.push({ name: 'Other', count: 1, color: '#666666' }); - } - - // Always-on hover tracking for tooltip (independent of enableInteraction) - this.canvas.addEventListener('pointermove', (e) => { - if (this._dragging) return; // interaction handler takes over - const idx = this.hitTest(e.clientX, e.clientY); - this._hoveredBar = idx; - // Store mouse position in canvas pixels for tooltip - const rect = this.canvas.getBoundingClientRect(); - const dpr = window.devicePixelRatio || 1; - this._mouseCanvasX = (e.clientX - rect.left) * dpr; - this._mouseCanvasY = (e.clientY - rect.top) * dpr; - }); - this.canvas.addEventListener('pointerleave', () => { - this._hoveredBar = -1; - }); - - this.resize(); - } - - /** - * Rebuild the visualizer for a new engine's param layout. - * Derives sections from paramMeta's group field so it works with any engine. - * @param {Array<{group?: string}>} paramMeta - */ - rebuild(paramMeta) { - if (!paramMeta || paramMeta.length === 0) return; - - // Derive sections from paramMeta groups - const sections = []; - let currentGroup = null; - let currentCount = 0; - for (const pm of paramMeta) { - const group = pm.group ?? 'Other'; - if (group !== currentGroup) { - if (currentGroup !== null) { - sections.push({ name: currentGroup, count: currentCount, color: _colorFromGroup(currentGroup) }); - } - currentGroup = group; - currentCount = 0; - } - currentCount++; - } - if (currentGroup !== null) { - sections.push({ name: currentGroup, count: currentCount, color: _colorFromGroup(currentGroup) }); - } - - // Rebuild sectionMap - this.sectionMap = []; - for (const sec of sections) { - for (let i = 0; i < sec.count; i++) { - this.sectionMap.push(sec); - } - } - - // Resize param arrays - this.params = new Array(paramMeta.length).fill(0.5); - this.displayParams = new Array(paramMeta.length).fill(0.5); - } - - resize() { - this.canvas.width = window.innerWidth * (window.devicePixelRatio || 1); - this.canvas.height = window.innerHeight * (window.devicePixelRatio || 1); - this.canvas.style.width = '100%'; - this.canvas.style.height = '100%'; - } - - setParams(outputs) { - for (let i = 0; i < N_OUTPUTS && i < outputs.length; i++) { - this.params[i] = outputs[i]; - } - } - - draw() { - const ctx = this.ctx; - const W = this.canvas.width; - const H = this.canvas.height; - const dpr = window.devicePixelRatio || 1; - const n = N_OUTPUTS; - - // Lerp display values toward target - for (let i = 0; i < n; i++) { - this.displayParams[i] += (this.params[i] - this.displayParams[i]) * this.lerpSpeed; - } - - // Background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, W, H); - - // Build list of visible (non-muted) param indices - const visibleIndices = []; - for (let i = 0; i < n; i++) { - if (!isParamMuted(i)) visibleIndices.push(i); - } - const nVisible = visibleIndices.length || 1; - - // Calculate section gap positions (among visible params only) - const sectionGaps = new Set(); - let prevSi = -1; - for (const vi of visibleIndices) { - const mapping = paramToSection[vi]; - const curSi = mapping ? mapping.si : -1; - if (prevSi >= 0 && curSi !== prevSi) { - sectionGaps.add(vi); - } - prevSi = curSi; - } - - const topPad = this.topPadding * dpr; - const bottomPad = this.bottomPadding * dpr; - const totalGapPx = sectionGaps.size * 2 * dpr; - const barAreaWidth = W - totalGapPx; - const barWidth = barAreaWidth / nVisible; - const usableHeight = H - topPad - bottomPad; - const maxBarHeight = usableHeight; - - // Store layout for interaction hit-testing - this._layout = { W, H, n, barWidth, totalGapPx, sectionGaps, topPad, bottomPad, usableHeight, dpr, visibleIndices }; - - // Draw bars - let x = 0; - let prevSection = null; - let sectionStartX = 0; - let sectionIndex = -1; - // Store bar x positions for hit testing (indexed by original param index) - this._barXPositions = new Array(n).fill(-1); - this._barWidths = new Array(n).fill(0); - // Store section label regions (in CSS pixels) for drawer hit testing - this._sectionLabelRegions = []; - - for (let vi = 0; vi < visibleIndices.length; vi++) { - const i = visibleIndices[vi]; - const sec = this.sectionMap[i] ?? { name: 'Other', count: 1, color: '#666666' }; - const mapping = paramToSection[i]; - const curSi = mapping ? mapping.si : -1; - - // Section divider gap - if (sectionGaps.has(i)) { - // Draw section label for the previous section at top - if (prevSection) { - this._drawSectionLabel(ctx, prevSection.name, sectionStartX, x, topPad, prevSection.color); - this._sectionLabelRegions.push({ - index: sectionIndex, - left: sectionStartX / dpr, - right: x / dpr, - top: 0, - bottom: topPad / dpr, - name: prevSection.name, - }); - } - sectionIndex = curSi; - x += 2 * dpr; - sectionStartX = x; - prevSection = sec; - } - - if (vi === 0) { - sectionStartX = x; - prevSection = sec; - sectionIndex = curSi; - } - - this._barXPositions[i] = x; - this._barWidths[i] = barWidth; - - const val = this.displayParams[i]; - const barH = val * maxBarHeight; - const barY = topPad + usableHeight - barH; - - // Bar with slight transparency - ctx.fillStyle = sec.color + 'cc'; - ctx.fillRect(x, barY, Math.max(barWidth - 0.5, 1), barH); - - // Bright top edge - ctx.fillStyle = sec.color; - ctx.fillRect(x, barY, Math.max(barWidth - 0.5, 1), Math.min(2 * dpr, barH)); - - x += barWidth; - } - - // Draw final section label at top - if (prevSection) { - this._drawSectionLabel(ctx, prevSection.name, sectionStartX, x, topPad, prevSection.color); - this._sectionLabelRegions.push({ - index: sectionIndex, - left: sectionStartX / dpr, - right: x / dpr, - top: 0, - bottom: topPad / dpr, - name: prevSection.name, - }); - } - - // Draw tooltip for hovered bar - this._drawTooltip(ctx, dpr); - } - - _drawSectionLabel(ctx, name, startX, endX, topPad, color) { - const dpr = window.devicePixelRatio || 1; - const fontSize = 9 * dpr; - ctx.save(); - ctx.font = `${fontSize}px 'JetBrains Mono', monospace`; - ctx.fillStyle = color + '88'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - const cx = (startX + endX) / 2; - ctx.fillText(name, cx, topPad - 4 * dpr); - ctx.restore(); - } - - // Returns section region at client coordinates, or null - hitTestSection(clientX, clientY) { - if (!this._sectionLabelRegions) return null; - const rect = this.canvas.getBoundingClientRect(); - const px = clientX - rect.left; - const py = clientY - rect.top; - for (const region of this._sectionLabelRegions) { - if (px >= region.left && px <= region.right && py >= region.top && py <= region.bottom) { - return region; - } - } - return null; - } - - // Returns bar index at canvas-relative pixel x, or -1 - hitTest(clientX, clientY) { - if (!this._barXPositions || !this._layout) return -1; - const rect = this.canvas.getBoundingClientRect(); - const dpr = this._layout.dpr; - const px = (clientX - rect.left) * dpr; - const n = this._layout.n; - - for (let i = 0; i < n; i++) { - const bx = this._barXPositions[i]; - if (bx < 0) continue; // muted - const bw = this._barWidths[i] || this._layout.barWidth; - if (px >= bx && px < bx + bw) return i; - } - return -1; - } - - // Returns 0-1 value from clientY (top of bar area = 1, bottom = 0) - yToValue(clientY) { - if (!this._layout) return 0.5; - const rect = this.canvas.getBoundingClientRect(); - const dpr = this._layout.dpr; - const py = (clientY - rect.top) * dpr; - const { topPad, usableHeight } = this._layout; - const val = 1 - (py - topPad) / usableHeight; - return Math.max(0, Math.min(1, val)); - } - - enableInteraction(enabled) { - if (enabled && !this._interactionEnabled) { - this._interactionEnabled = true; - this.canvas.style.cursor = 'crosshair'; - - this._onPointerDown = (e) => { - const idx = this.hitTest(e.clientX, e.clientY); - if (idx < 0) return; - e.preventDefault(); - this._dragging = true; - this._dragBarIndex = idx; - this.canvas.setPointerCapture(e.pointerId); - const val = this.yToValue(e.clientY); - rawParamValues[idx] = val; - this.params[idx] = val; - routeOutputs(rawParamValues); - updateHeatmap(rawParamValues); - }; - - this._onPointerMove = (e) => { - if (!this._dragging) return; - e.preventDefault(); - // Allow sliding to adjacent bars - let idx = this.hitTest(e.clientX, e.clientY); - if (idx < 0) idx = this._dragBarIndex; - this._dragBarIndex = idx; - const val = this.yToValue(e.clientY); - rawParamValues[idx] = val; - this.params[idx] = val; - routeOutputs(rawParamValues); - updateHeatmap(rawParamValues); - }; - - this._onPointerUp = (e) => { - this._dragging = false; - this._dragBarIndex = -1; - }; - - this.canvas.addEventListener('pointerdown', this._onPointerDown); - this.canvas.addEventListener('pointermove', this._onPointerMove); - this.canvas.addEventListener('pointerup', this._onPointerUp); - this.canvas.addEventListener('pointercancel', this._onPointerUp); - } else if (!enabled && this._interactionEnabled) { - this._interactionEnabled = false; - this.canvas.style.cursor = ''; - this._dragging = false; - this._dragBarIndex = -1; - this._hoveredBar = -1; - if (this._onPointerDown) { - this.canvas.removeEventListener('pointerdown', this._onPointerDown); - this.canvas.removeEventListener('pointermove', this._onPointerMove); - this.canvas.removeEventListener('pointerup', this._onPointerUp); - this.canvas.removeEventListener('pointercancel', this._onPointerUp); - } - } - } - - _drawTooltip(ctx, dpr) { - const i = this._hoveredBar; - if (i < 0 || !this._layout) return; - if (this._barXPositions[i] < 0) return; // muted - - const name = (activeEngine?.paramMeta?.[i]?.name) || SYNTH_PARAM_NAMES[i] || `p${i}`; - const val = this.displayParams[i]; - let rangeStr = '0.00 – 1.00'; - let curveStr = '0.50'; - if (engineParamOverrides && i < engineParamOverrides.length) { - // Non-C15 engine: use flat engine param overrides - const p = engineParamOverrides[i]; - rangeStr = `${p.min.toFixed(2)} – ${p.max.toFixed(2)}`; - curveStr = p.curve.toFixed(2); - } else { - // C15 path: nested section/group overrides - const mapping = paramToSection[i]; - if (mapping) { - const ov = groupOverrides[mapping.si]; - const p = ov.params[mapping.li]; - rangeStr = `${p.min.toFixed(2)} – ${p.max.toFixed(2)}`; - const curve = p.curve !== 0.5 ? p.curve : ov.curve; - curveStr = curve.toFixed(2); - } - } - - const lines = [name, `Val: ${val.toFixed(2)}`, `Range: ${rangeStr}`, `Curve: ${curveStr}`]; - const fontSize = 10 * dpr; - const lineHeight = fontSize * 1.4; - const padX = 8 * dpr; - const padY = 6 * dpr; - - ctx.save(); - ctx.font = `${fontSize}px 'JetBrains Mono', monospace`; - - // Measure text - let maxW = 0; - for (const line of lines) { - const m = ctx.measureText(line); - if (m.width > maxW) maxW = m.width; - } - const boxW = maxW + padX * 2; - const boxH = lines.length * lineHeight + padY * 2; - - // Position near the mouse cursor - const mx = this._mouseCanvasX || 0; - const my = this._mouseCanvasY || 0; - const offset = 12 * dpr; - let tx = mx + offset; - let ty = my - boxH - offset; - // Clamp to canvas - if (tx + boxW > this._layout.W - 2 * dpr) tx = mx - boxW - offset; - if (ty < 2 * dpr) ty = my + offset; - if (tx < 2 * dpr) tx = 2 * dpr; - - // Background - ctx.fillStyle = 'rgba(0, 0, 0, 0.88)'; - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.lineWidth = 1; - const r = 4 * dpr; - ctx.beginPath(); - ctx.roundRect(tx, ty, boxW, boxH, r); - ctx.fill(); - ctx.stroke(); - - // Text - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - for (let li = 0; li < lines.length; li++) { - ctx.fillStyle = li === 0 ? '#ffffff' : 'rgba(255,255,255,0.6)'; - if (li === 0) ctx.font = `bold ${fontSize}px 'JetBrains Mono', monospace`; - else ctx.font = `${fontSize}px 'JetBrains Mono', monospace`; - ctx.fillText(lines[li], tx + padX, ty + padY + li * lineHeight); - } - ctx.restore(); - } -} - -// ---- Preset padding ---- -function padPresetOutputs(outputs) { - const padded = new Array(N_OUTPUTS).fill(0.5); - for (let i = 0; i < outputs.length && i < padded.length; i++) padded[i] = outputs[i]; - return padded; -} - -// ---- MLP Resize ---- -// Returns the target output count for a given mode -function outputCountForMode(mode) { - if (mode === 'visual') return N_VISUAL_OUTPUTS; - if (mode === 'synth') return N_SYNTH_OUTPUTS; - if (mode === 'midi-cc') return midiCCMap.length; - if (mode === 'audio-canvas') return audioCanvas ? audioCanvas.getOutputCount() : 12; - return N_SYNTH_OUTPUTS; -} - -/** - * Total MLP output count: engine params + EOC params when in Shared mode. - * In all other NISPS modes (bypass/linked/independent) or when no EOC chain - * exists, only the engine's own param count is included. - * - * @returns {number} - */ -function totalOutputCount() { - const engineParams = activeEngine?.paramCount ?? N_SYNTH_OUTPUTS; - const eocParams = (eocChain?.nispsMode === 'shared') ? (eocChain?.paramCount ?? 0) : 0; - return engineParams + eocParams; -} - -/** - * Recreate IML instances with a new output count. - * Joystick IML uses warm-start weight transfer to preserve learned mappings. - * Training examples are always cleared (dataset is JS-side and output-count-specific). - */ -// ---- EOC audio graph wiring ---- -/** - * Wire the EOC chain into the audio graph after the C15 AudioContext is created. - * Safe to call multiple times — guarded by _eocInited flag. - * - * Call this immediately after any c15.start() / activeEngine.init() that brings - * the AudioContext into existence. - */ -async function _startEocChain() { - if (_eocInited || !eocChain) return; - // C15Adapter exposes _bridge.audioContext; Faust engines expose _audioCtx - const audioCtx = activeEngine?._bridge?.audioContext ?? activeEngine?._audioCtx; - if (!audioCtx) { - console.warn('[EOC] _startEocChain: no AudioContext yet — skipping'); - return; - } - - await eocChain.init(audioCtx); - _eocInited = true; - - // Disconnect the limiter from destination (it auto-connects there in C15Bridge.start()) - const outputNode = activeEngine.getOutputNode(); - try { outputNode.disconnect(); } catch (_) { /* already disconnected */ } - - eocChain.connect(outputNode, audioCtx.destination); - console.log('[EOC] chain wired into audio graph'); -} - -/** - * Lazily initialise ShapeSeq subsystem. - * Guarded by _shapeSeqInited — safe to call multiple times. - * Requires audio to be running (AudioContext must exist). - */ -async function _ensureShapeSeqInit() { - if (_shapeSeqInited || !shapeSeqEnabled) return; - - // Import modules lazily - if (!ShapeSeqEngine) { - ({ ShapeSeqEngine } = await import('./shapeseq/sequencer.js')); - ({ StepVisualizer } = await import('./shapeseq/step-viz.js')); - ({ ChainBuilderUI } = await import('./shapeseq/chain-ui.js')); - ({ EventBus, getDefaultBus } = await import('./shapeseq/event-bus.js')); - } - - const audioCtx = activeEngine?._bridge?.audioContext ?? activeEngine?._audioCtx; - if (!audioCtx) { - console.warn('[ShapeSeq] _ensureShapeSeqInit: no AudioContext yet — skipping'); - return; - } - - shapeSeqBus = getDefaultBus(audioCtx); - - // activeEngine (C15Adapter) exposes noteOn/noteOff directly — use it as the c15Bridge - shapeSeqEngine = new ShapeSeqEngine({ - audioContext: audioCtx, - eventBus: shapeSeqBus, - c15Bridge: activeEngine, - }); - - await shapeSeqEngine.init(); - _shapeSeqInited = true; - - // Show the ShapeSeq container - const container = document.getElementById('shapeseq-container'); - if (container && outputMode === 'synth') { - container.style.display = 'block'; - } - - console.log('[ShapeSeq] engine initialised'); -} - -/** - * Destroy the ShapeSeq engine (e.g. on engine switch). - */ -function _destroyShapeSeq() { - if (shapeSeqEngine) { - shapeSeqEngine.destroy(); - shapeSeqEngine = null; - } - shapeSeqViz = null; - shapeSeqChainUI = null; - shapeSeqBus = null; - _shapeSeqInited = false; - - const container = document.getElementById('shapeseq-container'); - if (container) container.style.display = 'none'; - - console.log('[ShapeSeq] engine destroyed'); -} - -// ---- Engine switching (stub for meml-phg UI) ---- -/** - * Hot-swap the active synth engine. - * Reinitialises the arpeggiator and resizes the MLP to match the new engine's - * param count. The new engine must already be initialised (init() called). - * - * @param {import('./synth/engine-interface.js').SynthEngine} engine - */ -async function setActiveEngine(engine) { - // Destroy ShapeSeq on engine switch — it holds a reference to the old engine - if (shapeSeqEnabled && _shapeSeqInited) { - _destroyShapeSeq(); - } - - activeEngine = engine; - c15 = engine; // keep alias in sync - arpeggiator.setEngine(engine); - if (midiInput) midiInput.setEngine(engine); - const btn = document.getElementById('synth-mode-btn'); - if (btn) btn.textContent = engine.displayName; - const synthDockBtn = document.querySelector('[data-drawer="synth"]'); - if (synthDockBtn) synthDockBtn.title = `Synth: ${engine.displayName}`; - EngineSwitcher.setActive(engine.id); - EngineSwitcher.setLoading(engine.id, false); - await resizeMLP(totalOutputCount()); - - // Reload MIDI CC map for the new engine (scoped storage key) - reloadMidiCCMap(); - - // Rebuild heatmap cells — include EOC params when in shared mode - if (eocChain?.nispsMode === 'shared') { - const combinedMeta = [...(engine.paramMeta ?? []), ...eocChain.paramMeta]; - rebuildHeatmap(combinedMeta); - } else { - rebuildHeatmap(engine.paramMeta); - } - document.getElementById('heatmap-cells')?.parentElement - ?.classList.toggle('shared-mode', eocChain?.nispsMode === 'shared'); - - // Rewire EOC chain to the new engine's output node - if (eocChain && _eocInited) { - const outputNode = engine.getOutputNode(); - if (outputNode) { - try { outputNode.disconnect(); } catch (_) { /* not yet connected */ } - // C15Adapter exposes _bridge.audioContext; Faust engines expose _audioCtx - const rewireCtx = engine._bridge?.audioContext ?? engine._audioCtx; - if (rewireCtx) { - eocChain.connect(outputNode, rewireCtx.destination); - } - } - } - - // Rebuild paramToSection and SynthVisualizer for the new engine - if (engine.id === 'shaper-feedback') { - restoreC15ParamToSection(); - // Restore C15 section map in SynthVisualizer - if (synthVisualizer) { - synthVisualizer.sectionMap = []; - let idx = 0; - for (const sec of SYNTH_SECTIONS) { - for (let i = 0; i < sec.count; i++, idx++) { - synthVisualizer.sectionMap.push(sec); - } - } - while (synthVisualizer.sectionMap.length < N_SYNTH_OUTPUTS) { - synthVisualizer.sectionMap.push({ name: 'Other', count: 1, color: '#666666' }); - } - synthVisualizer.params = new Array(N_SYNTH_OUTPUTS).fill(0.5); - synthVisualizer.displayParams = new Array(N_SYNTH_OUTPUTS).fill(0.5); - } - } else if (engine.paramMeta?.length > 0) { - rebuildParamToSection(engine.paramMeta); - rebuildNonC15Sections(engine.paramMeta); - if (synthVisualizer) { - synthVisualizer.rebuild(engine.paramMeta); - } - } - - // Build engine-specific param overrides (null for C15, flat array for Faust) - if (engine.id === 'shaper-feedback') { - engineParamOverrides = null; - } else { - buildEngineParamOverrides(); - } - - // Rebuild the preset selector for the new engine - rebuildPresetSelector(); - - // Clear active preset — it belongs to the previous engine - activeSynthPresetId = null; - - // Modular UI show/hide (Phase C). The UI is created once during init() - // and stays in the DOM; we toggle its visibility via the dock icon. - if (modularUI) { - if (engine.id === 'modular') modularUI.show(); - else modularUI.hide(); - } -} - -async function resizeMLP(newOutputCount) { - if (newOutputCount === N_OUTPUTS) return; - N_OUTPUTS = newOutputCount; - - // Extract joystick weights before destroying (for warm-start transfer) - const joySnapshot = imlJoy ? imlJoy.extractWeights() : null; - - // Destroy old IML instances (free WASM memory) - if (imlJoy) imlJoy.destroy(); - if (imlHand) imlHand.destroy(); - - // Joystick IML: warm-start from previous weights when possible - if (joySnapshot) { - imlJoy = await WasmIML.createWithWarmStart(joySnapshot, N_OUTPUTS, 1000, 1.0, 0.00001); - } else { - imlJoy = await WasmIML.create(N_JOY_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); - imlJoy.randomiseWeights(spreadLevel); - } - imlJoy.setLogger(msg => console.log('[NISPS:joy]', msg)); - - // Hand IML: fresh init (warm-start for 14-input networks is a future concern) - imlHand = await WasmIML.create(N_HAND_INPUTS, N_OUTPUTS, [48, 48, 64], 1000, 1.0, 0.00001); - imlHand.setLogger(msg => console.log('[NISPS:hand]', msg)); - imlHand.randomiseWeights(spreadLevel); - - iml = (inputMode === 'joystick') ? imlJoy : imlHand; - - // Reset dependent state - rawParamValues = new Array(N_OUTPUTS).fill(0.5); - _lastSentParams = new Float32Array(N_OUTPUTS); - - // Re-run inference - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - - console.log(`[NISPS] MLP resized to ${N_OUTPUTS} outputs (joystick IML warm-started)`); -} - -// ---- Init ---- -async function init() { - // Parse ?tame URL param - const urlParams = new URLSearchParams(window.location.search); - // tame is handled at groupOverrides init time, no longer needed here - spreadLevel = parseFloat(urlParams.get('spread') ?? '0.6'); - if (isNaN(spreadLevel)) spreadLevel = 0.6; - spreadLevel = Math.max(0, Math.min(1, spreadLevel)); - - // Dual IML instances — joystick (2 inputs) and hand tracking (14 inputs) - imlJoy = await WasmIML.create(N_JOY_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); - imlJoy.setLogger(msg => console.log('[NISPS:joy]', msg)); - imlHand = await WasmIML.create(N_HAND_INPUTS, N_OUTPUTS, [48, 48, 64], 1000, 1.0, 0.00001); - imlHand.setLogger(msg => console.log('[NISPS:hand]', msg)); - iml = imlJoy; // default to joystick - - // Canvas + Visualizer - $canvas = document.getElementById('vis-canvas'); - visualizer = new FlowFieldVisualizer($canvas); - - // Synth visualizer - $synthVisCanvas = document.getElementById('synth-vis-canvas'); - synthVisualizer = new SynthVisualizer($synthVisCanvas); - - // Synth — use C15Adapter to satisfy the SynthEngine interface - activeEngine = new C15Adapter(); - c15 = activeEngine; // alias: existing code that references c15 continues to work - activeEngine.onStatusChange = msg => { - const el = document.getElementById('synth-status'); - if (el) el.textContent = msg; - }; - arpeggiator = new Arpeggiator(activeEngine); - midiInput = new MIDIInput(activeEngine); - initMIDIControls(); - - // MIDI Output - midiOutput = new MIDIOutput(); - initMIDICCControls(); - - // DOM refs - $heatmapCells = document.getElementById('heatmap-cells'); - $heatmapTooltip = document.getElementById('heatmap-tooltip'); - - $joystickContainer = document.getElementById('joystick-container'); - $joyMap = document.getElementById('joy-map'); - $joyMapCtx = $joyMap.getContext('2d'); - $noiseRing = document.getElementById('noise-ring'); - $followBadge = document.getElementById('follow-badge'); - - $rlButtons = document.getElementById('rl-buttons'); - $btnThumbsUp = document.getElementById('btn-thumbsup'); - $btnThumbsDown = document.getElementById('btn-thumbsdown'); - - $statusText = document.getElementById('status-text'); - - $lossCanvas = document.getElementById('loss-canvas'); - $lossCtx = $lossCanvas.getContext('2d'); - $engineParams = document.getElementById('engine-params'); - - $followPill = document.getElementById('follow-pill'); - - // Build heatmap cells (engine-aware: uses activeEngine.paramMeta for synth mode) - rebuildHeatmap(activeEngine.paramMeta); - - // Build raw param sliders - buildEngineParams(); - - // Engine switcher — prepended above the tuning sliders in the Engine drawer - const ENGINES = [ - { - id: 'shaper-feedback', - displayName: 'C15 Shaper-Feedback', - paramCount: 126, - description: 'Phase-aligned waveshapers with feedback mixer. Complex harmonic textures.', - }, - { - id: 'additive', - displayName: 'Additive', - paramCount: 48, - description: 'Spectral envelope additive synthesis. 64 harmonics shaped by ML.', - }, - { - id: 'fm', - displayName: 'FM Matrix', - paramCount: 55, - description: '4-operator FM with continuous routing matrix. Algorithm emerges from exploration.', - }, - { - id: 'modular', - displayName: 'Modular', - paramCount: 512, - description: 'Shared mod pool (4 ADSRs + 8 LFOs) routed through a matrix into a swappable voice. Starts with a 3-osc subtractive sub-engine.', - }, - ]; - const engineSwitcherEl = document.getElementById('synth-engine-switcher'); - if (engineSwitcherEl) { - EngineSwitcher.init(engineSwitcherEl, ENGINES, async (engineId) => { - if (engineId === activeEngine?.id) return; - - EngineSwitcher.setLoading(engineId, true); - try { - let newEngine; - if (engineId === 'shaper-feedback') { - // Switch back to C15 — reuse the existing instance - newEngine = c15 instanceof C15Adapter ? c15 : new C15Adapter(); - } else if (engineId === 'additive') { - newEngine = new AdditiveEngine(); - } else if (engineId === 'fm') { - newEngine = new FMEngine(); - } else if (engineId === 'modular') { - newEngine = new ModularEngine(); - // When the modular engine's paramMeta changes (sub-engine swap or - // expose/unexpose toggles from Phase C UI), resize the MLP and - // rebuild downstream state that depends on paramCount. - newEngine.on('paramMeta:change', async () => { - if (activeEngine !== newEngine) return; - await resizeMLP(totalOutputCount()); - if (eocChain?.nispsMode === 'shared') { - const combinedMeta = [...(newEngine.paramMeta ?? []), ...eocChain.paramMeta]; - rebuildHeatmap(combinedMeta); - } else { - rebuildHeatmap(newEngine.paramMeta); - } - if (newEngine.paramMeta?.length > 0) { - rebuildParamToSection(newEngine.paramMeta); - rebuildNonC15Sections(newEngine.paramMeta); - if (synthVisualizer) synthVisualizer.rebuild(newEngine.paramMeta); - } - buildEngineParamOverrides(); - }); - } else { - showToast(`Unknown engine: ${engineId}`); - return; - } - - if (newEngine !== activeEngine) { - // Stop arpeggiator/ShapeSeq before switching - if (shapeSeqEnabled && shapeSeqEngine) shapeSeqEngine.stop(); - if (arpeggiator) arpeggiator.stop(); - - // Ensure paramMeta is available even if audio isn't started yet. - // This prevents MLP resizing to 0 outputs for Faust engines. - if (newEngine.loadParamMeta) { - await newEngine.loadParamMeta(); - } - - // If audio is already running, init the new engine now - const audioCtxForInit = activeEngine?._audioCtx - ?? activeEngine?._bridge?.audioContext - ?? null; - if (audioCtxForInit && !newEngine._running) { - await newEngine.init(audioCtxForInit); - } - - // Phase E — apply any pending modular DSP snapshot from loadState(). - // Runs BEFORE setActiveEngine so paramMeta is correct when the MLP - // resizes below. Note: Phase C's UI state (counts, sub-engine, - // exposed params, enables) is applied separately by modular-ui.js - // on its first refresh(); that runs AFTER setActiveEngine via - // modularUI.show(), so the load ordering is: - // 1. setState() here — restores raw DSP values + sub-engine - // 2. modular-ui refresh() — replays Phase C's UI state - // Step 2 may call setModSourceCount() which re-emits - // paramMeta:change; that's fine since the raw values are already - // in _lastRawByLabel. - if (engineId === 'modular' && _pendingModularDspState && - typeof newEngine.setState === 'function') { - try { - await newEngine.setState(_pendingModularDspState); - } catch (err) { - console.warn('[NISPS] modular setState on engine switch failed:', err); - } - _pendingModularDspState = null; - } - - await setActiveEngine(newEngine); - } - } catch (err) { - console.error('[EngineSwitcher] Failed to switch engine:', err); - showToast(`Failed to load ${engineId}: ${err.message}`); - } finally { - EngineSwitcher.setLoading(engineId, false); - } - }, { - hasTrainingData: () => (imlJoy?.exampleCount ?? 0) > 0 || (imlHand?.exampleCount ?? 0) > 0, - }); - EngineSwitcher.setActive(activeEngine.id); - const engineDockBtn = document.querySelector('[data-drawer="params"]'); - if (engineDockBtn) engineDockBtn.title = `Engine: ${activeEngine.displayName}`; - } - - // Wire events - wireJoystick(); - wireDock(); - - // Phase C: Modular UI. Mount once here (after dock + drawer-stack exist). - // The UI is hidden until the modular engine becomes active. - try { - modularUI = initModularUI({ - getEngine: () => activeEngine, - // Phase E: when modular UI mutates its state (preset applied, - // enables toggled, etc.) trigger the top-level save so the DSP - // snapshot is re-persisted alongside Phase C's UI-state key. - onStateChange: () => saveState(), - }); - if (activeEngine?.id === 'modular') modularUI.show(); - } catch (err) { - console.error('[NISPS] Failed to init modular UI:', err); - } - - wireControls(); - wireSynthControls(); - wireGamepad(); - wireKeyboard(); - wireInputToggle(); - createDevPanel(() => handTracker); - wireQuickPlayControls(); - wireGroupDrawer(); - wireParamPopup(); - wireHelp(); - - // Resize - window.addEventListener('resize', onResize); - onResize(); - - // Restore saved state (if any) - await loadState(); - - // Wire synth preset selector - wireSynthPresets(); - - // Check URL ?preset param (overrides localStorage) — search all engines - const urlPreset = urlParams.get('preset'); - const allPresets = [...SYNTH_PRESETS, ...ADDITIVE_PRESETS, ...FM_PRESETS]; - if (urlPreset && allPresets.some(p => p.id === urlPreset)) { - applyPreset(urlPreset); - } else if (activeSynthPresetId) { - // Restored from localStorage — sync the dropdown - const $ps = document.getElementById('synth-preset-select'); - if ($ps) $ps.value = activeSynthPresetId; - } - - // Initial inference - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - routeOutputs(iml.getOutputs()); - updateHeatmap(iml.getOutputs()); - updateStatus(); - drawJoyMap(); - drawLossPlot(); - - // Auto-save every 10 seconds - setInterval(saveState, 10000); - - // EOC Effects Chain — initialise chain and wire drawer UI - eocChain = new EOCChain(); - const eocDrawerBody = document.getElementById('eoc-drawer-body'); - if (eocDrawerBody) { - EOCChainUI.init(eocChain, eocDrawerBody); - } - - // EOC Independent mode joystick - const eocJoyCanvas = document.getElementById('eoc-joy-map'); - if (eocJoyCanvas) { - eocJoystick = new EOCJoystick(eocJoyCanvas); - eocJoystick.onChange = (x, y) => { - if (!imlEoc || eocChain?.nispsMode !== 'independent') return; - imlEoc.setInput(0, x); - imlEoc.setInput(1, y); - imlEoc.process(); - const eocOutputs = imlEoc.getOutputs(); - for (let i = 0; i < eocOutputs.length; i++) { - eocChain.setParam(i, eocOutputs[i]); - } - }; - } - - // Handle EOC structural changes — Shared mode resizes MLP; Linked mode manages second IML - window.addEventListener('eoc:change', async (e) => { - const reason = e.detail?.reason; - console.log('[EOC] chain changed, reason:', reason, 'paramCount:', eocChain.paramCount, 'nispsMode:', eocChain.nispsMode); - - if (reason === 'nispsMode-changed') { - // Linked/Independent mode lifecycle — both need a second IML - if (eocChain.nispsMode === 'linked' || eocChain.nispsMode === 'independent') { - await initEocIML(); - } else { - destroyEocIML(); - eocTrainingTarget = 'synth'; - } - // Show/hide EOC joystick (only visible in independent mode) - const eocJoyContainer = document.getElementById('eoc-joy-container'); - if (eocJoyContainer) { - eocJoyContainer.classList.toggle('hidden', eocChain.nispsMode !== 'independent'); - } - } - - // Shared mode: resize MLP and rebuild heatmap on mode switch or module change - if (reason === 'nispsMode-changed' || reason?.startsWith('module')) { - const newTotal = totalOutputCount(); - if (newTotal !== N_OUTPUTS) { - await resizeMLP(newTotal); - } - if (eocChain.nispsMode === 'shared') { - const combinedMeta = [ - ...(activeEngine?.paramMeta ?? []), - ...eocChain.paramMeta, - ]; - rebuildHeatmap(combinedMeta); - } else { - rebuildHeatmap(activeEngine?.paramMeta ?? []); - } - document.getElementById('heatmap-cells')?.parentElement - ?.classList.toggle('shared-mode', eocChain.nispsMode === 'shared'); - - // Linked/Independent mode: recreate EOC IML when modules change - if ((eocChain.nispsMode === 'linked' || eocChain.nispsMode === 'independent') && imlEoc) { - await initEocIML(); - } - } - - // Show/hide EOC RL buttons and Synth label based on linked mode - const isLinked = eocChain.nispsMode === 'linked'; - const eocRlContainer = document.getElementById('eoc-rl-buttons'); - if (eocRlContainer) eocRlContainer.classList.toggle('hidden', !isLinked); - const rlLabel = document.getElementById('rl-label'); - if (rlLabel) rlLabel.classList.toggle('hidden', !isLinked); - }); - - // Debug probe — exposed on window when ?debug=1 is in the URL. - // Used by Playwright e2e tests. Zero footprint in production. - if (new URLSearchParams(window.location.search).has('debug')) { - window.__nisps = { - get iml() { return iml; }, - get imlJoy() { return imlJoy; }, - get imlHand(){ return imlHand; }, - getOutputs: () => [...iml.getOutputs()], - getLoss: () => iml.lastLoss, - getWeights: () => iml._getFlatWeights(), - getExampleCount: () => iml.exampleCount, - setInputs: (x, y) => { - iml.setInput(0, x); - iml.setInput(1, y); - iml.process(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - }, - thumbsUp: () => onThumbsUp(), - thumbsDown: () => onThumbsDown(), - train: () => { - const loss = trainModel(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - drawLossPlot(); - return loss; - }, - trainAsync: () => new Promise(resolve => trainModelAsync(resolve)), - randomise: () => { - iml.randomiseWeights(spreadLevel); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - }, - clearExamples: () => { - iml.clearDataset(); - updateStatus(); - }, - saveState: () => saveState(), - evalLoss: () => iml.evalLoss(), - inferBatch: (points) => iml.inferBatch(points), - getLayerStats:() => iml.getLayerStats(), - eocChain, - // ---- Phase E — modular engine hooks ---- - get activeEngine() { return activeEngine; }, - get activeEngineId() { return activeEngine?.id ?? null; }, - get paramCount() { return activeEngine?.paramCount ?? 0; }, - /** Returns the modular engine's full DSP snapshot, or null if not active. */ - getModularState: () => { - if (activeEngine?.id !== 'modular' || - typeof activeEngine.getState !== 'function') return null; - return activeEngine.getState(); - }, - /** Restores a modular DSP snapshot. Returns a Promise. */ - setModularState: async (s) => { - if (activeEngine?.id !== 'modular' || - typeof activeEngine.setState !== 'function') return false; - await activeEngine.setState(s); - return true; - }, - /** Swap the active sub-engine (subtractive/additive/fm). */ - setModularSubEngine: async (id) => { - if (activeEngine?.id !== 'modular' || - typeof activeEngine.setSubEngine !== 'function') return false; - await activeEngine.setSubEngine(id); - return true; - }, - /** Apply a named modular preset. Returns a Promise. */ - applyModularPreset: async (presetId) => { - if (activeEngine?.id !== 'modular') return false; - const p = findModularPreset(presetId); - if (!p) return false; - await applyModularPreset(activeEngine, p); - return true; - }, - /** Change modular ADSR/LFO counts. */ - setModularSourceCount: (adsr, lfo) => { - if (activeEngine?.id !== 'modular' || - typeof activeEngine.setModSourceCount !== 'function') return false; - activeEngine.setModSourceCount(adsr, lfo); - return true; - }, - /** List of known modular preset ids. */ - listModularPresets: () => MODULAR_PRESETS.map(p => ({ id: p.id, name: p.name })), - }; - } - - // EOC Linked-mode probe — exposed unconditionally so the training-target UI - // (meml-0r0) can wire buttons without requiring ?debug=1. - window.__nispsEoc = { - get trainingTarget() { return eocTrainingTarget; }, - setTrainingTarget(t) { eocTrainingTarget = t; }, - get imlEoc() { return imlEoc; }, - }; - - // Start animation - requestAnimationFrame(animate); -} - -// ---- Heatmap helpers ---- - -/** - * Derive a stable HSL color string from a group name. - * Uses a simple djb2-style hash to map any group string to a consistent hue. - */ -function _colorFromGroup(group) { - let hash = 5381; - for (let i = 0; i < group.length; i++) { - hash = ((hash << 5) + hash) + group.charCodeAt(i); - hash |= 0; // force 32-bit int - } - const hue = ((hash >>> 0) % 360); - return `hsl(${hue}, 70%, 55%)`; -} - -/** - * Rebuild heatmap cells (and reset per-param state arrays) from a paramMeta array. - * Call this when the active engine switches to update the heatmap for the new param layout. - * @param {Array<{id:string, name:string, group:string}>} paramMeta - */ -function rebuildHeatmap(paramMeta) { - // Resize state arrays to match the new engine's param count - rawParamValues = new Array(paramMeta.length).fill(0.5); - _lastSentParams = new Float32Array(paramMeta.length); - buildHeatmap(); -} - -// ---- Heatmap (bar chart style) ---- -function buildHeatmap() { - $heatmapCells.innerHTML = ''; - const isSynth = outputMode === 'synth'; - const isMidiCC = outputMode === 'midi-cc'; - const isAudioCanvas = outputMode === 'audio-canvas'; - - // For synth mode, derive names and colors from the active engine's paramMeta. - // For all other modes, use the static arrays as before. - let count, names, colors; - if (isSynth && activeEngine?.paramMeta) { - const meta = activeEngine.paramMeta; - count = meta.length; - // C15 has curated per-param colors from SYNTH_PARAM_COLORS; other engines derive from group. - const useCuratedColors = activeEngine.id === 'shaper-feedback' && SYNTH_PARAM_COLORS.length === meta.length; - names = meta.map(p => p.name); - colors = useCuratedColors ? SYNTH_PARAM_COLORS : meta.map(p => _colorFromGroup(p.group)); - } else { - if (isAudioCanvas && audioCanvas) { - count = audioCanvas.getOutputCount(); - names = audioCanvas.getAudioParamNames(); - colors = audioCanvas.getAudioParamColors(); - _ensureAudioCanvasOverrides(count); - } else if (isMidiCC) { - count = midiCCMap.length; - names = midiCCMap.map(p => p.name); - colors = MIDI_CC_PARAM_COLORS; - } else { - count = N_VISUAL_OUTPUTS; - names = VISUAL_PARAM_NAMES; - colors = VISUAL_PARAM_COLORS; - } - } - - for (let i = 0; i < count; i++) { - const cell = document.createElement('div'); - cell.className = 'heatmap-cell'; - cell.dataset.index = i; - - const bar = document.createElement('div'); - bar.className = 'heatmap-cell-bar'; - bar.style.background = colors[i]; - bar.style.width = '30%'; // default - bar.style.height = '100%'; - cell.appendChild(bar); - - // Tooltip on hover (quick info) + click to open popup - cell.addEventListener('pointerenter', (e) => { - if (cell._dragging) return; - const outputs = iml.getOutputs(); - $heatmapTooltip.textContent = `${names[i]}: ${outputs[i].toFixed(3)} ▾`; - $heatmapTooltip.classList.add('visible'); - const rect = cell.getBoundingClientRect(); - $heatmapTooltip.style.left = `${rect.left}px`; - }); - cell.addEventListener('pointerleave', () => { - if (!cell._dragging) $heatmapTooltip.classList.remove('visible'); - // Delay hide popup so user can move to it - if (activePopupParam === i) { - popupHideTimer = setTimeout(() => hideParamPopup(), 300); - } - }); - - // Click opens popup; drag sets parameter value - cell.addEventListener('pointerdown', (e) => { - e.preventDefault(); - cell._downX = e.clientX; - cell._downY = e.clientY; - cell._didDrag = false; - cell._dragging = true; - cell.classList.add('dragging'); - cell.setPointerCapture(e.pointerId); - }); - cell.addEventListener('pointermove', (e) => { - if (!cell._dragging) return; - const dx = Math.abs(e.clientX - cell._downX); - const dy = Math.abs(e.clientY - cell._downY); - if (dx > 3 || dy > 3) cell._didDrag = true; - if (cell._didDrag) setHeatmapValue(i, e, cell); - }); - cell.addEventListener('pointerup', () => { - cell._dragging = false; - cell.classList.remove('dragging'); - if (!cell._didDrag) { - // Click — toggle popup - clearTimeout(popupHideTimer); - if (activePopupParam === i) { - hideParamPopup(); - } else { - showParamPopup(i, cell); - } - } - }); - cell.addEventListener('pointercancel', () => { - cell._dragging = false; - cell.classList.remove('dragging'); - }); - - $heatmapCells.appendChild(cell); - } -} - -function setHeatmapValue(paramIndex, e, cell) { - const rect = cell.getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - // If frozen, dragging updates the fixed value directly - const ov = getParamOverride(paramIndex); - if (ov && ov.frozen) { - ov.fixedValue = x; - } - rawParamValues[paramIndex] = x; - routeOutputs(rawParamValues); - updateHeatmap(rawParamValues); - syncRawParamsFromOutputs(rawParamValues); - // Update tooltip - const isSynth = outputMode === 'synth'; - const isMidiCC = outputMode === 'midi-cc'; - const isAudioCanvas = outputMode === 'audio-canvas'; - let paramName; - if (isSynth && activeEngine?.paramMeta?.[paramIndex]) { - paramName = activeEngine.paramMeta[paramIndex].name; - } else if (isMidiCC) { - paramName = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`; - } else if (isAudioCanvas && audioCanvas) { - const acNames = audioCanvas.getAudioParamNames(); - paramName = acNames[paramIndex] ?? `p${paramIndex}`; - } else { - paramName = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; - } - $heatmapTooltip.textContent = `${paramName}: ${x.toFixed(3)}`; - $heatmapTooltip.classList.add('visible'); - $heatmapTooltip.style.left = `${rect.left}px`; -} - -function updateHeatmap(outputs) { - const cells = $heatmapCells.children; - for (let i = 0; i < cells.length && i < outputs.length; i++) { - const bar = cells[i].querySelector('.heatmap-cell-bar'); - if (bar) { - const pct = Math.max(2, Math.round(outputs[i] * 100)); - bar.style.width = pct + '%'; - } - // Dim frozen/muted cells - const ov = getParamOverride(i); - if (ov) { - cells[i].classList.toggle('heatmap-cell-frozen', !!ov.frozen); - } - } -} - -/** Get the override object for a heatmap param (works in both visual & synth mode) */ -function getParamOverride(paramIndex) { - if (outputMode === 'synth') { - // Non-C15 engines use the flat engineParamOverrides array - if (engineParamOverrides && paramIndex < engineParamOverrides.length) { - const p = engineParamOverrides[paramIndex]; - return { - get min() { return p.min; }, set min(v) { p.min = v; }, - get max() { return p.max; }, set max(v) { p.max = v; }, - get curve() { return p.curve; }, set curve(v) { p.curve = v; }, - get frozen() { return p.muted; }, set frozen(v) { p.muted = v; }, - get fixedValue() { return p.fixedValue; }, set fixedValue(v) { p.fixedValue = v; }, - }; - } - // C15 path: use groupOverrides + paramToSection - const mapping = paramToSection[paramIndex]; - if (!mapping) return null; - const group = groupOverrides[mapping.si]; - if (!group) return null; - const p = group.params[mapping.li]; - if (!p) return null; - // Expose as { min, max, curve, frozen, fixedValue } — map 'muted' to 'frozen' - return { - get min() { return p.min; }, set min(v) { p.min = v; }, - get max() { return p.max; }, set max(v) { p.max = v; }, - get curve() { return p.curve; }, set curve(v) { p.curve = v; }, - get frozen() { return p.muted; }, set frozen(v) { p.muted = v; }, - get fixedValue() { return p.fixedValue; }, set fixedValue(v) { p.fixedValue = v; }, - }; - } else if (outputMode === 'midi-cc') { - return midiCCOverrides[paramIndex] || null; - } else if (outputMode === 'audio-canvas') { - return audioCanvasOverrides[paramIndex] || null; - } else { - return visualOverrides[paramIndex] || null; - } -} - -// ---- Param Popup (hover menu on heatmap cells) ---- -function wireParamPopup() { - $paramPopup = document.createElement('div'); - $paramPopup.className = 'param-popup'; - $paramPopup.innerHTML = '
    '; - document.body.appendChild($paramPopup); - - $paramPopup.addEventListener('pointerenter', () => { - clearTimeout(popupHideTimer); - }); - $paramPopup.addEventListener('pointerleave', () => { - popupHideTimer = setTimeout(() => hideParamPopup(), 300); - }); -} - -function showParamPopup(paramIndex, cell) { - if (activePopupParam === paramIndex) return; - activePopupParam = paramIndex; - - const isSynth = outputMode === 'synth'; - const isMidiCC = outputMode === 'midi-cc'; - const isAudioCanvas = outputMode === 'audio-canvas'; - let name, color; - if (isSynth && activeEngine?.paramMeta?.[paramIndex]) { - const pm = activeEngine.paramMeta[paramIndex]; - name = pm.name; - // Use curated C15 colors when available, else derive from group. - const useCuratedColors = activeEngine.id === 'shaper-feedback' && SYNTH_PARAM_COLORS.length > paramIndex; - color = useCuratedColors ? SYNTH_PARAM_COLORS[paramIndex] : _colorFromGroup(pm.group); - } else if (isMidiCC) { - name = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`; - color = MIDI_CC_PARAM_COLORS[paramIndex] ?? '#888'; - } else if (isAudioCanvas && audioCanvas) { - const acNames = audioCanvas.getAudioParamNames(); - const acColors = audioCanvas.getAudioParamColors(); - name = acNames[paramIndex] ?? `p${paramIndex}`; - color = acColors[paramIndex] ?? '#888'; - } else { - name = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; - color = VISUAL_PARAM_COLORS[paramIndex] ?? '#888'; - } - const ov = getParamOverride(paramIndex); - if (!ov) return; - - const header = $paramPopup.querySelector('.pp-header'); - header.textContent = name; - header.style.color = color; - - const body = $paramPopup.querySelector('.pp-body'); - body.innerHTML = ''; - - // -- MIDI CC specific: name, CC#, channel editors -- - if (isMidiCC) { - const ccParam = midiCCMap[paramIndex]; - // Editable name - const nameRow = document.createElement('div'); - nameRow.className = 'pp-row'; - const nameLabel = document.createElement('span'); - nameLabel.className = 'pp-label'; - nameLabel.textContent = 'Name'; - const nameInput = document.createElement('input'); - nameInput.type = 'text'; - nameInput.value = ccParam.name; - nameInput.className = 'pp-text-input'; - nameInput.addEventListener('change', () => { - ccParam.name = nameInput.value; - header.textContent = nameInput.value; - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - saveCCMap(midiCCMap, midiCCStorageKey()); - }); - nameRow.appendChild(nameLabel); - nameRow.appendChild(nameInput); - body.appendChild(nameRow); - - // CC number - const ccRow = document.createElement('div'); - ccRow.className = 'pp-row'; - const ccLabel = document.createElement('span'); - ccLabel.className = 'pp-label'; - ccLabel.textContent = 'CC#'; - const ccInput = document.createElement('input'); - ccInput.type = 'number'; - ccInput.min = '0'; - ccInput.max = '127'; - ccInput.value = ccParam.cc; - ccInput.className = 'pp-num-input'; - ccInput.addEventListener('change', () => { - ccParam.cc = parseInt(ccInput.value) || 0; - // Auto-name if name matches old CC name - if (CC_NAMES[ccParam.cc]) { - ccParam.name = CC_NAMES[ccParam.cc]; - nameInput.value = ccParam.name; - header.textContent = ccParam.name; - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - } - saveCCMap(midiCCMap, midiCCStorageKey()); - }); - ccRow.appendChild(ccLabel); - ccRow.appendChild(ccInput); - body.appendChild(ccRow); - - // Channel - const chRow = document.createElement('div'); - chRow.className = 'pp-row'; - const chLabel = document.createElement('span'); - chLabel.className = 'pp-label'; - chLabel.textContent = 'Ch'; - const chInput = document.createElement('input'); - chInput.type = 'number'; - chInput.min = '1'; - chInput.max = '16'; - chInput.value = ccParam.channel; - chInput.className = 'pp-num-input'; - chInput.addEventListener('change', () => { - ccParam.channel = Math.max(1, Math.min(16, parseInt(chInput.value) || 1)); - chInput.value = ccParam.channel; - saveCCMap(midiCCMap, midiCCStorageKey()); - }); - chRow.appendChild(chLabel); - chRow.appendChild(chInput); - body.appendChild(chRow); - } - - // -- Curve row -- - const curveRow = document.createElement('div'); - curveRow.className = 'pp-row'; - - const curveLabel = document.createElement('span'); - curveLabel.className = 'pp-label'; - curveLabel.textContent = 'Curve'; - - const curveCanvas = document.createElement('canvas'); - curveCanvas.className = 'pp-curve-canvas'; - curveCanvas.width = 36; - curveCanvas.height = 36; - - const curveVal = document.createElement('span'); - curveVal.className = 'pp-val'; - curveVal.textContent = ov.curve.toFixed(2); - - function redrawCurve() { - _drawCurveOnCanvas(curveCanvas, ov.curve, color); - } - - _wireCurveDrag(curveCanvas, () => ov.curve, (v) => { - ov.curve = v; - curveVal.textContent = v.toFixed(2); - redrawCurve(); - routeOutputs(iml.getOutputs()); - }); - redrawCurve(); - - curveRow.appendChild(curveLabel); - curveRow.appendChild(curveCanvas); - curveRow.appendChild(curveVal); - body.appendChild(curveRow); - - // -- Min/Max row -- - const rangeRow = document.createElement('div'); - rangeRow.className = 'pp-row'; - - const rangeLabel = document.createElement('span'); - rangeLabel.className = 'pp-label'; - rangeLabel.textContent = 'Range'; - - const rangeWrap = document.createElement('div'); - rangeWrap.className = 'pp-range-wrap'; - - const rangeFill = document.createElement('div'); - rangeFill.className = 'gd-range-fill'; - - const minSlider = document.createElement('input'); - minSlider.type = 'range'; minSlider.min = '0'; minSlider.max = '1'; minSlider.step = '0.01'; - minSlider.value = ov.min; - minSlider.className = 'gd-range-input gd-range-min'; - - const maxSlider = document.createElement('input'); - maxSlider.type = 'range'; maxSlider.min = '0'; maxSlider.max = '1'; maxSlider.step = '0.01'; - maxSlider.value = ov.max; - maxSlider.className = 'gd-range-input gd-range-max'; - - const rangeValSpan = document.createElement('span'); - rangeValSpan.className = 'pp-val'; - - function updateRangeFill() { - rangeFill.style.left = `${ov.min * 100}%`; - rangeFill.style.width = `${(ov.max - ov.min) * 100}%`; - rangeValSpan.textContent = `${ov.min.toFixed(2)}–${ov.max.toFixed(2)}`; - } - updateRangeFill(); - - minSlider.addEventListener('input', () => { - ov.min = parseFloat(minSlider.value); - if (ov.min > ov.max) { ov.max = ov.min; maxSlider.value = ov.max; } - updateRangeFill(); - routeOutputs(iml.getOutputs()); - }); - maxSlider.addEventListener('input', () => { - ov.max = parseFloat(maxSlider.value); - if (ov.max < ov.min) { ov.min = ov.max; minSlider.value = ov.min; } - updateRangeFill(); - routeOutputs(iml.getOutputs()); - }); - - rangeWrap.appendChild(rangeFill); - rangeWrap.appendChild(minSlider); - rangeWrap.appendChild(maxSlider); - - rangeRow.appendChild(rangeLabel); - rangeRow.appendChild(rangeWrap); - rangeRow.appendChild(rangeValSpan); - body.appendChild(rangeRow); - - // -- Freeze row -- - const freezeRow = document.createElement('div'); - freezeRow.className = 'pp-row pp-freeze-row'; - - const freezeBtn = document.createElement('button'); - freezeBtn.className = 'pp-freeze-btn' + (ov.frozen ? ' frozen' : ''); - freezeBtn.textContent = ov.frozen ? 'Frozen' : 'Freeze'; - freezeBtn.title = ov.frozen ? 'Unfreeze — re-enable NISPS control' : 'Freeze — lock value, disable NISPS'; - - const valSlider = document.createElement('input'); - valSlider.type = 'range'; valSlider.min = '0'; valSlider.max = '1'; valSlider.step = '0.01'; - valSlider.value = ov.fixedValue; - valSlider.className = 'pp-val-slider'; - if (!ov.frozen) valSlider.style.display = 'none'; - - const valDisplay = document.createElement('span'); - valDisplay.className = 'pp-val'; - valDisplay.textContent = ov.frozen ? ov.fixedValue.toFixed(2) : ''; - - freezeBtn.addEventListener('click', () => { - ov.frozen = !ov.frozen; - freezeBtn.classList.toggle('frozen', ov.frozen); - freezeBtn.textContent = ov.frozen ? 'Frozen' : 'Freeze'; - freezeBtn.title = ov.frozen ? 'Unfreeze — re-enable NISPS control' : 'Freeze — lock value, disable NISPS'; - valSlider.style.display = ov.frozen ? '' : 'none'; - valDisplay.textContent = ov.frozen ? ov.fixedValue.toFixed(2) : ''; - // When freezing, capture the current output value - if (ov.frozen) { - const outputs = iml.getOutputs(); - ov.fixedValue = outputs[paramIndex]; - valSlider.value = ov.fixedValue; - valDisplay.textContent = ov.fixedValue.toFixed(2); - } - routeOutputs(iml.getOutputs()); - }); - - valSlider.addEventListener('input', () => { - ov.fixedValue = parseFloat(valSlider.value); - valDisplay.textContent = ov.fixedValue.toFixed(2); - routeOutputs(iml.getOutputs()); - }); - - freezeRow.appendChild(freezeBtn); - freezeRow.appendChild(valSlider); - freezeRow.appendChild(valDisplay); - body.appendChild(freezeRow); - - // Position below the heatmap cell - const cellRect = cell.getBoundingClientRect(); - const popupWidth = 260; - let left = cellRect.left + cellRect.width / 2 - popupWidth / 2; - left = Math.max(4, Math.min(left, window.innerWidth - popupWidth - 4)); - $paramPopup.style.left = `${left}px`; - $paramPopup.style.top = `${cellRect.bottom + 4}px`; - $paramPopup.classList.add('visible'); -} - -function hideParamPopup() { - activePopupParam = -1; - if ($paramPopup) $paramPopup.classList.remove('visible'); -} - -// ---- Joy Map (merged joystick + minimap) ---- -function drawJoyMap() { - const canvas = $joyMap; - const ctx = $joyMapCtx; - const w = canvas.width; - const h = canvas.height; - const cx = w / 2; - const cy = h / 2; - const r = w / 2 - 2; - - // Clear with circle clip - ctx.save(); - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.clip(); - - // Background - ctx.fillStyle = 'rgba(13, 13, 13, 0.7)'; - ctx.fillRect(0, 0, w, h); - - // Grid lines - ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; - ctx.lineWidth = 0.5; - for (let frac = 0.25; frac < 1; frac += 0.25) { - ctx.beginPath(); - ctx.moveTo(frac * w, 0); ctx.lineTo(frac * w, h); - ctx.moveTo(0, frac * h); ctx.lineTo(w, frac * h); - ctx.stroke(); - } - - // Ring border - ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.stroke(); - - // Training example dots - const features = iml.dataset.features; - const labels = iml.dataset.labels; - - for (let i = 0; i < features.length; i++) { - const fx = features[i][0] * w; - const fy = (1 - features[i][1]) * h; - - let hue = 0; - if (labels[i]) { - hue = (labels[i][3] || 0) * 360; - } - - ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.85)`; - ctx.beginPath(); - ctx.arc(fx, fy, 3, 0, Math.PI * 2); - ctx.fill(); - } - - // Current position knob (accent dot with glow) - const px = joyX * w; - const py = (1 - joyY) * h; - - // Glow - ctx.shadowColor = 'rgba(255, 106, 0, 0.6)'; - ctx.shadowBlur = 12; - ctx.fillStyle = 'rgba(255, 106, 0, 0.9)'; - ctx.beginPath(); - ctx.arc(px, py, 8, 0, Math.PI * 2); - ctx.fill(); - - // Inner bright dot - ctx.shadowBlur = 0; - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - ctx.beginPath(); - ctx.arc(px, py, 3, 0, Math.PI * 2); - ctx.fill(); - - // Crosshair - ctx.strokeStyle = 'rgba(255, 106, 0, 0.2)'; - ctx.lineWidth = 0.5; - ctx.shadowBlur = 0; - ctx.beginPath(); - ctx.moveTo(px, 0); ctx.lineTo(px, h); - ctx.moveTo(0, py); ctx.lineTo(w, py); - ctx.stroke(); - - ctx.restore(); -} - -// ---- Joystick ---- -function wireJoystick() { - const canvas = $joyMap; - let startX, startY, startJX, startJY; - let lastDoubleTap = 0; - - function onStart(e) { - e.preventDefault(); - const touch = e.touches ? e.touches[0] : e; - - // Double-tap detection for follow mode - const now = Date.now(); - if (now - lastDoubleTap < 350) { - toggleFollowMode(); - lastDoubleTap = 0; - return; - } - lastDoubleTap = now; - - // Snap to tap position within the circle - const rect = canvas.getBoundingClientRect(); - const size = rect.width; - const dx = touch.clientX - rect.left - size / 2; - const dy = touch.clientY - rect.top - size / 2; - const maxR = size / 2 - 8; - - joyX = Math.max(0, Math.min(1, 0.5 + (dx / maxR) * 0.5)); - joyY = Math.max(0, Math.min(1, 0.5 - (dy / maxR) * 0.5)); - - drawJoyMap(); - onJoystickMove(); - - joyDragging = true; - startX = touch.clientX; - startY = touch.clientY; - startJX = joyX; - startJY = joyY; - } - - function onMove(e) { - if (!joyDragging && !joyFollowMode) return; - e.preventDefault(); - const touch = e.touches ? e.touches[0] : e; - - if (joyFollowMode) { - joyX = Math.max(0, Math.min(1, touch.clientX / window.innerWidth)); - joyY = Math.max(0, Math.min(1, 1 - touch.clientY / window.innerHeight)); - } else if (joyDragging) { - const dx = touch.clientX - startX; - const dy = touch.clientY - startY; - const size = canvas.getBoundingClientRect().width; - const maxR = size / 2 - 8; - const scale = 1 / maxR; - - joyX = Math.max(0, Math.min(1, startJX + dx * scale * 0.5)); - joyY = Math.max(0, Math.min(1, startJY - dy * scale * 0.5)); - } - - drawJoyMap(); - onJoystickMove(); - } - - function onEnd() { - joyDragging = false; - } - - canvas.addEventListener('pointerdown', onStart); - window.addEventListener('pointermove', onMove); - window.addEventListener('pointerup', onEnd); - window.addEventListener('pointercancel', onEnd); - - // Follow mode: track pointer on main canvas when active - $canvas.addEventListener('pointermove', (e) => { - if (!joyFollowMode) return; - joyX = Math.max(0, Math.min(1, e.clientX / window.innerWidth)); - joyY = Math.max(0, Math.min(1, 1 - e.clientY / window.innerHeight)); - drawJoyMap(); - onJoystickMove(); - }); -} - -function toggleFollowMode() { - joyFollowMode = !joyFollowMode; - console.log('[Joystick] Follow mode:', joyFollowMode); - updateFollowUI(); -} - -function updateFollowUI() { - if (joyFollowMode) { - $followBadge.classList.remove('hidden'); - $joystickContainer.classList.add('follow-active'); - $followPill.classList.add('active'); - } else { - $followBadge.classList.add('hidden'); - $joystickContainer.classList.remove('follow-active'); - $followPill.classList.remove('active'); - } -} - -function onJoystickMove() { - if (inputMode !== 'joystick') return; - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - - syncRawParamsFromOutputs(outputs); - - // EOC Linked mode: run EOC IML with same joystick inputs - if (imlEoc && eocChain?.nispsMode === 'linked') { - imlEoc.setInput(0, joyX); - imlEoc.setInput(1, joyY); - imlEoc.process(); - const eocOutputs = imlEoc.getOutputs(); - for (let i = 0; i < eocOutputs.length; i++) { - eocChain.setParam(i, eocOutputs[i]); - } - } - - // ShapeSeq: route joystick inputs to the sequence engine - if (shapeSeqEnabled && shapeSeqEngine && shapeSeqEngine.isPlaying) { - if (shapeSeqEngine.mlpMode.mode === 'unified') { - // Unified mode: timbre MLP already ran — pass its outputs to the sequencer - shapeSeqEngine.setSequenceOutputsFromTimbre(outputs); - } else { - // Dual mode: sequence engine runs its own MLP - shapeSeqEngine.setSequenceInputs([joyX, joyY]); - } - } - - // Trail - joyTrail.push({ x: joyX, y: joyY, t: Date.now() }); - if (joyTrail.length > 30) joyTrail.shift(); -} - -// ---- Output routing ---- -// Synth param throttling: only send values that changed beyond a dead zone. -// Prevents ring buffer flooding (126 params × 30fps = 3780 msg/s > 512 capacity). -let _lastSentParams = new Float32Array(N_OUTPUTS); -const PARAM_DEAD_ZONE = 0.002; // ~0.2% change threshold -let _lastParamSendTime = 0; -const PARAM_SEND_INTERVAL = 50; // max ~20fps for synth param updates - -function routeOutputs(outputs) { - if (outputMode === 'synth') { - // Modular cold-start: before the user has captured any training - // examples, the MLP output is arbitrary — an untrained sigmoid - // network with spread=0.6 on 512 outputs tends toward ~0, which - // denormalises matrix cells to raw=0 and silences the default - // patch's ADSR1→amp routing. Ignore the MLP entirely and drive the - // engine with the default-normalised vector. Once exampleCount > 0 - // the MLP has a target to hit and resumes driving normally. - let shiftedOutputs = outputs; - if (activeEngine?.id === 'modular' && - (iml?.exampleCount ?? 0) === 0 && - typeof activeEngine.getDefaultNormalizedOutputs === 'function') { - const defaults = activeEngine.getDefaultNormalizedOutputs(); - if (defaults && defaults.length === outputs.length) { - shiftedOutputs = defaults; - } - } - - const overridden = new Array(shiftedOutputs.length); - for (let i = 0; i < shiftedOutputs.length; i++) { - overridden[i] = applyGroupOverrides(shiftedOutputs[i], i); - } - // Visualizer always gets every frame (it's local, no buffer) - synthVisualizer.setParams(overridden); - - // Modular matrix UI: mirror live MLP outputs into visible matrix cells - // (the UI throttles its own DOM writes internally) - if (modularUI && activeEngine?.id === 'modular') { - modularUI.updateLive(overridden); - } - - // Engine param updates: throttle + dead-zone filter - if (activeEngine && activeEngine.running) { - const now = performance.now(); - if (now - _lastParamSendTime >= PARAM_SEND_INTERVAL) { - _lastParamSendTime = now; - const engineParamCount = activeEngine.paramCount; - for (let i = 0; i < overridden.length && i < N_OUTPUTS; i++) { - const v = overridden[i]; - if (Math.abs(v - _lastSentParams[i]) > PARAM_DEAD_ZONE) { - // Only send engine params (indices before engineParamCount) to the engine - if (i < engineParamCount) { - activeEngine.setParam(i, v); - } - _lastSentParams[i] = v; - } - } - } - } - - // In Shared mode, route outputs beyond engine params to the EOC chain - if (eocChain?.nispsMode === 'shared') { - const engineParamCount = activeEngine?.paramCount ?? 0; - const eocParamCount = eocChain.paramCount; - for (let i = 0; i < eocParamCount; i++) { - const outputIndex = engineParamCount + i; - if (outputIndex < outputs.length) { - eocChain.setParam(i, outputs[outputIndex]); - } - } - } - } else if (outputMode === 'midi-cc') { - // MIDI CC output — route through overrides then send CC messages - if (midiOutput && midiOutput.enabled && midiOutput.activeOutput) { - const messages = []; - for (let i = 0; i < midiCCMap.length && i < outputs.length; i++) { - const ccParam = midiCCMap[i]; - const ov = midiCCOverrides[i]; - if (!ov || ov.frozen) { - if (ov) messages.push({ channel: ccParam.channel, cc: ccParam.cc, value: Math.round(ov.fixedValue * 127) }); - continue; - } - const v = applyGroupOverride(outputs[i], ov.curve, ov.min, ov.max); - messages.push({ channel: ccParam.channel, cc: ccParam.cc, value: Math.round(v * 127) }); - } - midiOutput.sendBatch(messages); - } - } else if (outputMode === 'audio-canvas') { - if (audioCanvas) audioCanvas.setOutputs(outputs); - } else { - const vis = new Array(N_VISUAL_OUTPUTS); - for (let i = 0; i < N_VISUAL_OUTPUTS; i++) { - const vo = visualOverrides[i]; - if (vo.frozen) { - vis[i] = vo.fixedValue; - } else { - vis[i] = applyGroupOverride(outputs[i], vo.curve, vo.min, vo.max); - } - } - visualizer.setParams(vis); - } -} - -// ---- Bottom sheet ---- -// ---- Dock & Drawer system ---- -function wireDock() { - const dock = document.getElementById('dock'); - - // Toggle drawers on dock icon click - dock.addEventListener('click', (e) => { - const icon = e.target.closest('.dock-icon'); - if (!icon) return; - const drawerId = icon.dataset.drawer; - - // Special case: help opens modal instead of drawer - if (drawerId === 'help') { - document.getElementById('help-overlay').classList.remove('hidden'); - return; - } - - const drawer = document.getElementById(`drawer-${drawerId}`); - if (!drawer) return; - - const isOpen = !drawer.classList.contains('hidden'); - if (isOpen) { - drawer.classList.add('hidden'); - icon.classList.remove('active'); - } else { - drawer.classList.remove('hidden'); - icon.classList.add('active'); - } - }); - - // Close buttons inside drawers - document.querySelectorAll('.drawer-close').forEach(btn => { - btn.addEventListener('click', () => { - const drawerId = btn.dataset.drawer; - const drawer = document.getElementById(`drawer-${drawerId}`); - if (drawer) drawer.classList.add('hidden'); - const icon = dock.querySelector(`.dock-icon[data-drawer="${drawerId}"]`); - if (icon) icon.classList.remove('active'); - }); - }); - - // Follow pill toggle - $followPill.addEventListener('click', () => { - toggleFollowMode(); - }); -} - -// ---- Controls wiring ---- -function wireControls() { - // Output mode toggle (in mode drawer) - document.querySelectorAll('#output-toggle-float .pill-opt').forEach(btn => { - btn.addEventListener('click', () => { - syncOutputToggles(btn.dataset.mode); - setOutputMode(btn.dataset.mode); - }); - }); - - // Action buttons (in training drawer) - document.getElementById('btn-add-example').addEventListener('click', onAddExample); - document.getElementById('btn-train').addEventListener('click', onTrain); - document.getElementById('btn-clear').addEventListener('click', onClear); - document.getElementById('btn-clear-examples').addEventListener('click', onClearExamples); - document.getElementById('btn-randomize').addEventListener('click', onRandomize); - - // RL buttons - $btnThumbsUp.addEventListener('click', onThumbsUp); - $btnThumbsDown.addEventListener('click', onThumbsDown); - - // EOC RL buttons (Linked mode — target EOC IML directly) - const eocRlPlus = document.getElementById('eoc-rl-plus'); - const eocRlMinus = document.getElementById('eoc-rl-minus'); - if (eocRlPlus && eocRlMinus) { - eocRlPlus.addEventListener('click', () => { - if (!imlEoc) return; - const prevTarget = eocTrainingTarget; - eocTrainingTarget = 'eoc'; - onThumbsUp(); - eocTrainingTarget = prevTarget; - flash('eoc-rl-plus'); - }); - eocRlMinus.addEventListener('click', () => { - if (!imlEoc) return; - const prevTarget = eocTrainingTarget; - eocTrainingTarget = 'eoc'; - onThumbsDown(); - eocTrainingTarget = prevTarget; - flash('eoc-rl-minus'); - }); - } - - // Undo button - document.getElementById('btn-undo').addEventListener('click', onUndo); - - // Presets - document.querySelectorAll('.preset-chip').forEach(chip => { - chip.addEventListener('click', () => loadPreset(chip.dataset.preset)); - }); - - // Initial noise ring update - updateNoiseRing(); -} - -// ---- Input mode (joystick / hands) ---- -function wireInputToggle() { - document.querySelectorAll('#input-toggle .pill-opt').forEach(btn => { - btn.addEventListener('click', () => { - const mode = btn.dataset.input; - if (mode === inputMode) return; - setInputMode(mode); - syncInputToggle(mode); - }); - }); -} - -function syncInputToggle(mode) { - document.querySelectorAll('#input-toggle .pill-opt').forEach(b => - b.classList.toggle('active', b.dataset.input === mode) - ); -} - -let _inputModeSwitching = false; -async function setInputMode(mode) { - if (_inputModeSwitching) return; - _inputModeSwitching = true; - try { - await _setInputModeInner(mode); - } finally { - _inputModeSwitching = false; - } -} - -async function _setInputModeInner(mode) { - inputMode = mode; - const $pip = document.getElementById('hand-pip'); - - if (mode === 'hands') { - iml = imlHand; - $joystickContainer.style.display = 'none'; - $pip.classList.remove('hidden'); - - if (!handTracker) { - const $status = document.getElementById('hand-status'); - $status.textContent = 'Loading model...'; - - handTracker = new HandTracker({ - videoElement: document.getElementById('hand-video'), - overlayCanvas: document.getElementById('hand-overlay'), - onTrackingInput: onHandInput, - onGesture: onHandGesture, - onConnectionChange: (active) => { - console.log('[HandTracker] active:', active); - }, - }); - - try { - await handTracker.start(); - $status.textContent = 'Tracking'; - $status.classList.add('tracking'); - } catch (e) { - $status.textContent = 'Camera error'; - console.error('[HandTracker]', e); - setInputMode('joystick'); - syncInputToggle('joystick'); - return; - } - } else { - await handTracker.start(); - document.getElementById('hand-status').textContent = 'Tracking'; - document.getElementById('hand-status').classList.add('tracking'); - } - - iml.process(); - routeOutputs(iml.getOutputs()); - updateHeatmap(iml.getOutputs()); - } else { - iml = imlJoy; - $joystickContainer.style.display = ''; - $pip.classList.add('hidden'); - - if (handTracker) { - handTracker.stop(); - document.getElementById('hand-status').classList.remove('tracking'); - } - - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - routeOutputs(iml.getOutputs()); - updateHeatmap(iml.getOutputs()); - } - - updateStatus(); - drawJoyMap(); -} - -function getCurrentInputs() { - if (inputMode === 'hands' && handTracker) { - return [...handTracker.features]; - } - return [joyX, joyY]; -} - -function setCurrentInputs() { - if (inputMode === 'hands' && handTracker) { - const f = handTracker.features; - for (let i = 0; i < f.length; i++) iml.setInput(i, f[i]); - } else { - iml.setInput(0, joyX); - iml.setInput(1, joyY); - } -} - -function onHandInput(features) { - if (inputMode !== 'hands') return; - for (let i = 0; i < features.length; i++) { - iml.setInput(i, features[i]); - } - iml.process(); - - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateGestureIndicator(); -} - -function onHandGesture(gesture) { - if (gesture === 'thumbsup') { - onThumbsUp(); - } else if (gesture === 'thumbsdown') { - onThumbsDown(); - } -} - -function updateGestureIndicator() { - if (!handTracker) return; - const $indicator = document.getElementById('gesture-indicator'); - const $label = document.getElementById('gesture-label'); - const $progress = $indicator.querySelector('.gesture-ring-progress'); - - if (handTracker.gestureCandidate && handTracker.gestureProgress > 0) { - $indicator.classList.add('active'); - const circumference = 2 * Math.PI * 16; - const offset = circumference * (1 - handTracker.gestureProgress); - $progress.style.strokeDashoffset = offset; - $label.textContent = handTracker.gestureCandidate === 'thumbsup' ? '+' : '\u2212'; - } else { - $indicator.classList.remove('active'); - } - - const $status = document.getElementById('hand-status'); - if (handTracker.active) { - if (handTracker.trackingRight && handTracker.trackingLeft) { - $status.textContent = 'Both hands'; - } else if (handTracker.trackingRight) { - $status.textContent = 'Tracking'; - } else { - $status.textContent = 'No hand'; - } - $status.classList.toggle('tracking', handTracker.trackingRight); - } -} - -function syncOutputToggles(mode) { - document.querySelectorAll('#output-toggle-float .pill-opt').forEach(b => b.classList.toggle('active', b.dataset.mode === mode)); -} - -async function setOutputMode(mode, { skipConfirm = false } = {}) { - const heatmapStrip = document.getElementById('heatmap-strip'); - const synthQuickControls = document.getElementById('synth-quick-controls'); - const midiCCQuickControls = document.getElementById('midi-cc-quick-controls'); - const audioCanvasWrap = document.getElementById('audio-canvas-wrap'); - - // Lazily create AudioCanvas BEFORE computing targetOutputs so that - // outputCountForMode returns the real count (not the 12-output fallback). - if (mode === 'audio-canvas' && !audioCanvas) { - audioCanvas = new AudioCanvas(audioCanvasWrap); - _ensureAudioCanvasOverrides(audioCanvas.getOutputCount()); - // Listen for dynamic output count changes (loop count changes) - audioCanvasWrap.addEventListener('ac:outputcount-changed', async (e) => { - if (outputMode !== 'audio-canvas') return; - const newCount = e.detail.count; - _ensureAudioCanvasOverrides(newCount); - if (newCount !== N_OUTPUTS) { - await resizeMLP(newCount); - } - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - }); - } - - const targetOutputs = outputCountForMode(mode); - const needsResize = targetOutputs !== N_OUTPUTS; - - // Warn about weight reset if resizing (skip during state restore) - if (needsResize && !skipConfirm && iml.dataset.features.length > 0) { - if (!confirm(`Switching to ${mode} mode requires ${targetOutputs} outputs (currently ${N_OUTPUTS}). This will reset training examples. Network weights will be partially preserved. Continue?`)) { - syncOutputToggles(outputMode); // revert pill UI - return; - } - } - - outputMode = mode; - hideGroupDrawer(); - hideParamPopup(); - - // Resize MLP if needed - if (needsResize) { - await resizeMLP(targetOutputs); - } - - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - - // Hide/show audio-canvas wrap - if (audioCanvasWrap) audioCanvasWrap.style.display = mode === 'audio-canvas' ? 'block' : 'none'; - - // Show/hide ShapeSeq container (only visible in synth mode when shapeseq=1) - const shapeSeqContainer = document.getElementById('shapeseq-container'); - if (shapeSeqContainer) { - shapeSeqContainer.style.display = (shapeSeqEnabled && mode === 'synth') ? 'block' : 'none'; - } - - if (mode === 'synth') { - $canvas.classList.add('hidden-canvas'); - $synthVisCanvas.classList.add('active'); - heatmapStrip.classList.add('hidden'); - synthQuickControls.classList.remove('hidden'); - if (midiCCQuickControls) midiCCQuickControls.classList.add('hidden'); - synthVisualizer.enableInteraction(true); - const qp = document.getElementById('quick-play'); - if (qp) qp.classList.toggle('audio-needs-init', !(c15 && c15.running)); - } else if (mode === 'midi-cc') { - $canvas.classList.add('hidden-canvas'); - $synthVisCanvas.classList.remove('active'); - heatmapStrip.classList.remove('hidden'); - synthQuickControls.classList.add('hidden'); - if (midiCCQuickControls) midiCCQuickControls.classList.remove('hidden'); - synthVisualizer.enableInteraction(false); - } else if (mode === 'audio-canvas') { - $canvas.classList.add('hidden-canvas'); - $synthVisCanvas.classList.remove('active'); - heatmapStrip.classList.remove('hidden'); - synthQuickControls.classList.add('hidden'); - if (midiCCQuickControls) midiCCQuickControls.classList.add('hidden'); - synthVisualizer.enableInteraction(false); - } else { - $canvas.classList.remove('hidden-canvas'); - $synthVisCanvas.classList.remove('active'); - heatmapStrip.classList.remove('hidden'); - synthQuickControls.classList.add('hidden'); - if (midiCCQuickControls) midiCCQuickControls.classList.add('hidden'); - synthVisualizer.enableInteraction(false); - } - - routeOutputs(iml.getOutputs()); - buildEngineParams(); - syncRawParamsFromOutputs(iml.getOutputs()); -} - -function updateNoiseRing() { - if (noiseLevel > 0.15) { - $noiseRing.className = 'noise-ring active high'; - } else if (noiseLevel > 0.01) { - $noiseRing.className = 'noise-ring active'; - } else { - $noiseRing.className = 'noise-ring'; - } -} - -// ---- Examples mode ---- -function onAddExample() { - const inputs = getCurrentInputs(); - const outputs = [...rawParamValues]; - iml.addExample(inputs, outputs); - updateStatus(); - drawJoyMap(); - flash('btn-add-example'); -} - -function onTrain() { - if (iml.isTraining) return; - flash('btn-train'); - trainModelAsync(); -} - -function onRandomize() { - iml.randomiseWeights(spreadLevel); - setCurrentInputs(); - iml.process(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - noiseLevel = 0.05; - updateStatus(); -} - -function onClearExamples() { - iml.clearDataset(); - updateStatus(); - drawJoyMap(); -} - -function onClear() { - iml.clearDataset(); - iml.lossHistory = []; - iml.bestLoss = null; - iml.totalTrainingIterations = 0; - noiseLevel = 0.05; - clearState(); - updateStatus(); - drawLossPlot(); - drawJoyMap(); -} - -// ---- Undo ---- -function pushUndoSnapshot() { - const snapshot = { - weights: iml._getFlatWeights ? iml._getFlatWeights() : (iml.mlp ? iml.mlp.getWeights() : null), - noiseLevel, - exampleCount: iml.exampleCount, - }; - if (snapshot.weights) { - undoStack.push(snapshot); - if (undoStack.length > MAX_UNDO) undoStack.shift(); - updateUndoButton(); - } -} - -function onUndo() { - if (undoStack.length === 0) return; - const snapshot = undoStack.pop(); - if (iml._setFlatWeights && Array.isArray(snapshot.weights)) { - iml._setFlatWeights(snapshot.weights); - } else if (iml.mlp && snapshot.weights) { - iml.mlp.setWeights(snapshot.weights); - } - noiseLevel = snapshot.noiseLevel; - iml.process(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - updateNoiseRing(); - updateUndoButton(); - flash('btn-undo'); -} - -function updateUndoButton() { - const btn = document.getElementById('btn-undo'); - if (btn) btn.classList.toggle('has-undo', undoStack.length > 0); -} - -// ---- RL mode ---- - -/** - * Return the IML instance that RL feedback should target. - * In Linked/Independent mode the user may direct feedback to the EOC IML instead. - */ -function _rlTarget() { - const mode = eocChain?.nispsMode; - if ((mode === 'linked' || mode === 'independent') && eocTrainingTarget === 'eoc' && imlEoc) { - return imlEoc; - } - return iml; -} - -function onThumbsUp() { - const target = _rlTarget(); - if (!target || target.isTraining) return; - - pushUndoSnapshot(); - const inputs = getCurrentInputs(); - const outputs = [...rawParamValues]; - target.addExample(inputs, outputs); - - noiseLevel *= rlExplorationDecay; - noiseLevel = Math.max(noiseLevel, 0.005); - - flash('btn-thumbsup'); - updateNoiseRing(); - // Only run async training on the target; synth IML uses trainModelAsync, EOC IML trains directly - if (target === iml) { - trainModelAsync(); - } else { - target.trainAsync(({ loss }) => { - updateStatus(); - }); - } -} - -function onThumbsDown() { - const target = _rlTarget(); - if (!target) return; - - pushUndoSnapshot(); - const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel; - noiseLevel = Math.min(noiseLevel * 1.5, noiseCap); - - target.moveWeights(noiseLevel, spreadLevel); - - // Re-route outputs from whichever IML was affected - if (target === iml) { - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - } else if (imlEoc && (eocChain?.nispsMode === 'linked' || eocChain?.nispsMode === 'independent')) { - imlEoc.process(); - const eocOutputs = imlEoc.getOutputs(); - for (let i = 0; i < eocOutputs.length; i++) { - eocChain.setParam(i, eocOutputs[i]); - } - } - updateStatus(); - updateNoiseRing(); - flash('btn-thumbsdown'); -} - -// ---- Training ---- -// Sync — used for preset loading and state restore -function trainModel() { - return iml.train(); -} - -// Async — used for interactive training (thumbs-up, train button) -function trainModelAsync(onDone) { - iml.trainAsync(({ loss, outputs }) => { - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - drawLossPlot(); - drawJoyMap(); - if (onDone) onDone(); - }); -} - -// ---- Presets ---- -function loadPreset(name) { - const preset = PRESETS[name]; - if (!preset) return; - - // Visual presets have 2-element inputs — always apply to joystick IML - imlJoy.clearDataset(); - for (const ex of preset) { - imlJoy.addExample(ex.input, padPresetOutputs(ex.output)); - } - - // Temporarily point iml to imlJoy for training, then restore - const prevIml = iml; - iml = imlJoy; - const loss = trainModel(); - iml = prevIml; - - // Show results from joystick IML - imlJoy.setInput(0, joyX); - imlJoy.setInput(1, joyY); - imlJoy.process(); - const outputs = imlJoy.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - drawLossPlot(); - drawJoyMap(); -} - -// ---- Status ---- -function updateStatus() { - const count = iml.exampleCount; - const loss = iml.lastLoss; - let text = `${count} example${count !== 1 ? 's' : ''}`; - - if (loss !== null) { - text += ` \u00b7 loss ${loss.toFixed(5)}`; - } else { - text += ' \u00b7 untrained'; - } - - text += ` \u00b7 noise ${noiseLevel.toFixed(3)}`; - - $statusText.textContent = text; - syncEngineParams(); -} - -// ---- Loss plot ---- -function drawLossPlot() { - const ctx = $lossCtx; - const w = $lossCanvas.width; - const h = $lossCanvas.height; - const history = iml.lossHistory; - - ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; - ctx.fillRect(0, 0, w, h); - - if (history.length < 2) return; - - const maxLoss = Math.max(...history.slice(-200)) || 1; - const points = history.slice(-200); - - ctx.strokeStyle = 'rgba(255, 106, 0, 0.6)'; - ctx.lineWidth = 1.5; - ctx.beginPath(); - - for (let i = 0; i < points.length; i++) { - const x = (i / (points.length - 1)) * w; - const y = h - (points[i] / maxLoss) * h * 0.9; - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); -} - -// ---- Raw param sliders ---- -// ---- Engine Parameters (NISPS tuning) ---- -function buildEngineParams() { - if (!$engineParams) return; - // Use a sub-container so the EngineSwitcher section (prepended) is not clobbered. - let container = $engineParams.querySelector('.engine-tuning'); - if (!container) { - container = document.createElement('div'); - container.className = 'engine-tuning'; - $engineParams.appendChild(container); - } - container.innerHTML = ''; - - const params = [ - { - label: 'Spread', key: 'spread', - get: () => spreadLevel, set: (v) => { spreadLevel = v; }, - min: 0, max: 1, step: 0.01, - desc: 'Weight init scale & RL noise regime (0=polarised, 1=centered)', - }, - { - label: 'Noise', key: 'noise', - get: () => noiseLevel, set: (v) => { noiseLevel = v; updateNoiseRing(); }, - min: 0, max: 0.5, step: 0.001, - desc: 'Current RL exploration noise level', - }, - { - label: 'RL Decay', key: 'rlDecay', - get: () => rlExplorationDecay, set: (v) => { rlExplorationDecay = v; }, - min: 0.8, max: 1.0, step: 0.005, - desc: 'Noise decay per thumbs-up (lower = faster convergence)', - }, - { - label: 'Learn Rate', key: 'lr', - get: () => iml.learningRate, set: (v) => { iml.learningRate = v; }, - min: 0.01, max: 5.0, step: 0.01, - desc: 'MLP training learning rate', - }, - { - label: 'Max Iters', key: 'maxIter', - get: () => iml.maxIterations, set: (v) => { iml.maxIterations = Math.round(v); }, - min: 50, max: 5000, step: 50, - desc: 'Max training iterations per train() call', - }, - { - label: 'Convergence', key: 'conv', - get: () => iml.convergenceThreshold, set: (v) => { iml.convergenceThreshold = v; }, - min: 0.000001, max: 0.01, step: 0.000001, - desc: 'Stop training when loss drops below this', - }, - ]; - - for (const p of params) { - const row = document.createElement('div'); - row.className = 'engine-param'; - - const label = document.createElement('span'); - label.className = 'engine-param-label'; - label.textContent = p.label; - label.title = p.desc; - - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = String(p.min); - slider.max = String(p.max); - slider.step = String(p.step); - slider.value = p.get(); - - const val = document.createElement('span'); - val.className = 'engine-param-val'; - val.textContent = formatEngineVal(p.get(), p.step); - - slider.addEventListener('input', () => { - const v = parseFloat(slider.value); - p.set(v); - val.textContent = formatEngineVal(v, p.step); - }); - - row.appendChild(label); - row.appendChild(slider); - row.appendChild(val); - container.appendChild(row); - - // Store ref for live updates - row._paramDef = p; - row._slider = slider; - row._val = val; - } -} - -function formatEngineVal(v, step) { - if (step < 0.0001) return v.toExponential(1); - if (step < 0.01) return v.toFixed(3); - if (step < 1) return v.toFixed(2); - return String(Math.round(v)); -} - -function syncEngineParams() { - if (!$engineParams) return; - const rows = $engineParams.querySelectorAll('.engine-param'); - rows.forEach(row => { - const p = row._paramDef; - if (!p) return; - const v = p.get(); - row._slider.value = v; - row._val.textContent = formatEngineVal(v, parseFloat(row._slider.step)); - }); -} - -function syncRawParamsFromOutputs(outputs) { - rawParamValues = [...outputs]; -} - -// ---- Synth controls ---- -function wireSynthControls() { - const startBtn = document.getElementById('synth-start'); - const volumeSlider = document.getElementById('synth-volume'); - const arpToggle = document.getElementById('arp-toggle'); - const arpProgression = document.getElementById('arp-progression'); - const arpTempo = document.getElementById('arp-tempo'); - const arpOctaves = document.getElementById('arp-octaves'); - const arpOffset = document.getElementById('arp-offset'); - if (!startBtn || !volumeSlider || !arpToggle) return; - - startBtn.addEventListener('click', async () => { - const quickPlay = document.getElementById('quick-play'); - if (c15.running) { - if (shapeSeqEnabled && shapeSeqEngine) shapeSeqEngine.stop(); - arpeggiator.stop(); - arpToggle.textContent = 'Play'; - await c15.stop(); - startBtn.textContent = 'Start Audio'; - if (quickPlay) quickPlay.classList.add('audio-needs-init'); - } else { - await activeEngine.init(); - await _startEocChain(); - if (shapeSeqEnabled) await _ensureShapeSeqInit(); - startBtn.textContent = 'Stop Audio'; - if (quickPlay) quickPlay.classList.remove('audio-needs-init'); - routeOutputs(iml.getOutputs()); - } - }); - - volumeSlider.addEventListener('input', (e) => { - c15.setMasterVolume(parseFloat(e.target.value)); - const quickVol = document.getElementById('quick-vol'); - if (quickVol) quickVol.value = e.target.value; - }); - - arpToggle.addEventListener('click', () => { - if (!c15.running) return; - const isPlaying = shapeSeqEnabled - ? (shapeSeqEngine && shapeSeqEngine.isPlaying) - : arpeggiator.playing; - if (isPlaying) { - if (shapeSeqEnabled && shapeSeqEngine) shapeSeqEngine.stop(); - else arpeggiator.stop(); - arpToggle.textContent = 'Play'; - } else { - if (shapeSeqEnabled && shapeSeqEngine) shapeSeqEngine.start(); - else arpeggiator.start(); - arpToggle.textContent = 'Stop'; - } - }); - - arpProgression.addEventListener('change', (e) => { - arpeggiator.progression = e.target.value; - }); - - arpTempo.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.bpm = val; - document.getElementById('tempo-val').textContent = val; - const quickBpm = document.getElementById('quick-bpm'); - const quickBpmVal = document.getElementById('quick-bpm-val'); - if (quickBpm) quickBpm.value = val; - if (quickBpmVal) quickBpmVal.textContent = val; - }); - - arpOctaves.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.octaves = val; - document.getElementById('octaves-val').textContent = val; - }); - - arpOffset.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.octaveOffset = val; - document.getElementById('offset-val').textContent = val; - }); -} - -// ---- MIDI Input ---- -async function initMIDIControls() { - const row = document.getElementById('midi-row'); - const statusRow = document.getElementById('midi-status-row'); - const toggle = document.getElementById('midi-toggle'); - const select = document.getElementById('midi-select'); - const statusEl = document.getElementById('midi-status'); - if (!row || !toggle || !select) return; - - const available = await midiInput.init(); - if (!available) return; - - row.style.display = ''; - - function populateInputs() { - const inputs = midiInput.getInputs(); - select.innerHTML = '' + - inputs.map(i => ``).join(''); - } - - populateInputs(); - midiInput.onInputsChange = () => populateInputs(); - - midiInput.onStatusChange = (msg) => { - if (statusEl) { - statusEl.textContent = msg; - statusRow.style.display = ''; - } - }; - - midiInput.onCC = (cc, value) => { - if (cc === 1) { joyX = value; onJoystickMove(); drawJoyMap(); } - if (cc === 2) { joyY = value; onJoystickMove(); drawJoyMap(); } - }; - - toggle.addEventListener('click', () => { - if (!c15.running) return; - midiInput.toggle(); - toggle.textContent = midiInput.enabled ? 'Disable' : 'Enable'; - toggle.classList.toggle('playing', midiInput.enabled); - }); - - select.addEventListener('change', (e) => { - midiInput.selectInput(e.target.value || null); - }); -} - -// ---- MIDI CC Preset Application ---- -async function applyMidiCCPreset(preset) { - // Replace the CC map with preset params - midiCCMap.length = 0; - midiCCMap.push(...preset.params.map(p => ({ ...p }))); - - // Rebuild overrides - midiCCOverrides.length = 0; - midiCCOverrides.push(...preset.params.map(p => ({ - min: p.min ?? 0, max: p.max ?? 1, curve: p.curve ?? 0.5, - frozen: p.muted ?? false, fixedValue: p.fixedValue ?? 0.5, - }))); - - _generateCCColors(midiCCMap.length); - saveCCMap(midiCCMap, midiCCStorageKey()); - - // Resize MLP if in midi-cc mode - if (outputMode === 'midi-cc') { - await resizeMLP(midiCCMap.length); - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - } - - // Update UI - const countEl = document.getElementById('midi-cc-count'); - if (countEl) countEl.textContent = midiCCMap.length; - buildMidiCCParamList(); - - console.log(`[NISPS] Applied MIDI CC preset: ${preset.name} (${preset.params.length} params)`); -} - -// ---- MIDI CC Controls ---- -async function initMIDICCControls() { - const enableBtn = document.getElementById('midi-cc-enable-btn'); - const outputSelect = document.getElementById('midi-cc-output-select'); - const statusEl = document.getElementById('midi-cc-status'); - const countEl = document.getElementById('midi-cc-count'); - const addBtn = document.getElementById('midi-cc-add'); - const removeBtn = document.getElementById('midi-cc-remove'); - const importBtn = document.getElementById('midi-cc-import'); - const exportBtn = document.getElementById('midi-cc-export'); - const presetSelect = document.getElementById('midi-cc-preset-select'); - const fileImportBtn = document.getElementById('midi-cc-file-import'); - - if (!enableBtn || !outputSelect) return; - - const available = await midiOutput.init(); - - function populateOutputs() { - const outputs = midiOutput.getOutputs(); - outputSelect.innerHTML = '' + - outputs.map(o => ``).join(''); - } - - if (available) { - populateOutputs(); - midiOutput.onOutputsChange = () => populateOutputs(); - } - - midiOutput.onStatusChange = (msg) => { - if (statusEl) statusEl.textContent = msg; - }; - - enableBtn.addEventListener('click', () => { - midiOutput.toggle(); - enableBtn.classList.toggle('playing', midiOutput.enabled); - }); - - outputSelect.addEventListener('change', (e) => { - midiOutput.selectOutput(e.target.value || null); - if (e.target.value) midiOutput.enable(); - enableBtn.classList.toggle('playing', midiOutput.enabled); - }); - - // Update count display - function updateCountDisplay() { - if (countEl) countEl.textContent = midiCCMap.length; - } - updateCountDisplay(); - - // Add CC param - if (addBtn) { - addBtn.addEventListener('click', async () => { - midiCCMap.push(createCCParam(74, 1)); - midiCCOverrides.push({ min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5 }); - _generateCCColors(midiCCMap.length); - saveCCMap(midiCCMap, midiCCStorageKey()); - updateCountDisplay(); - - // Resize MLP if we're in midi-cc mode - if (outputMode === 'midi-cc') { - await resizeMLP(midiCCMap.length); - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - } - }); - } - - // Remove last CC param - if (removeBtn) { - removeBtn.addEventListener('click', async () => { - if (midiCCMap.length <= 1) return; - midiCCMap.pop(); - midiCCOverrides.pop(); - _generateCCColors(midiCCMap.length); - saveCCMap(midiCCMap, midiCCStorageKey()); - updateCountDisplay(); - - if (outputMode === 'midi-cc') { - await resizeMLP(midiCCMap.length); - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - } - }); - } - - // Export - if (exportBtn) { - exportBtn.addEventListener('click', () => { - const json = exportCCMap(midiCCMap); - navigator.clipboard.writeText(json).then( - () => { if (statusEl) statusEl.textContent = 'Copied to clipboard'; }, - () => { - // Fallback: prompt - prompt('Copy this JSON:', json); - } - ); - }); - } - - // Import - if (importBtn) { - importBtn.addEventListener('click', async () => { - const json = prompt('Paste MIDI CC map JSON:'); - if (!json) return; - const parsed = importCCMap(json); - if (!parsed) { - alert('Invalid MIDI CC map JSON'); - return; - } - midiCCMap.length = 0; - midiCCMap.push(...parsed); - midiCCOverrides.length = 0; - midiCCOverrides.push(...parsed.map(p => ({ - min: p.min ?? 0, max: p.max ?? 1, curve: p.curve ?? 0.5, - frozen: p.muted ?? false, fixedValue: p.fixedValue ?? 0.5, - }))); - _generateCCColors(midiCCMap.length); - saveCCMap(midiCCMap, midiCCStorageKey()); - updateCountDisplay(); - - if (outputMode === 'midi-cc') { - await resizeMLP(midiCCMap.length); - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - } - }); - } - - // Quick preset selector (in quick controls bar) - const quickPresetSelect = document.getElementById('midi-cc-quick-preset'); - if (quickPresetSelect) { - quickPresetSelect.innerHTML = ''; - for (const p of listMidiPresets()) { - const opt = document.createElement('option'); - opt.value = p.id; - opt.textContent = p.name; - quickPresetSelect.appendChild(opt); - } - quickPresetSelect.addEventListener('change', async () => { - const id = quickPresetSelect.value; - if (!id) return; - try { - const preset = await loadMidiPreset(id); - await applyMidiCCPreset(preset); - updateCountDisplay(); - if (statusEl) statusEl.textContent = `Loaded: ${preset.name}`; - // Sync drawer preset selector - if (presetSelect) presetSelect.value = id; - } catch (err) { - console.error('[MIDI CC] Failed to load preset:', err); - } - }); - } - - // Preset selector (in drawer) - if (presetSelect) { - // Populate with built-in presets - presetSelect.innerHTML = ''; - for (const p of listMidiPresets()) { - const opt = document.createElement('option'); - opt.value = p.id; - opt.textContent = p.name; - presetSelect.appendChild(opt); - } - presetSelect.addEventListener('change', async () => { - const id = presetSelect.value; - if (!id) return; // "Manual" selected — keep current config - try { - const preset = await loadMidiPreset(id); - await applyMidiCCPreset(preset); - updateCountDisplay(); - if (statusEl) statusEl.textContent = `Loaded: ${preset.name}`; - // Sync quick preset selector - if (quickPresetSelect) quickPresetSelect.value = id; - } catch (err) { - console.error('[MIDI CC] Failed to load preset:', err); - if (statusEl) statusEl.textContent = `Error: ${err.message}`; - } - }); - } - - // File import (JSON file from disk) - if (fileImportBtn) { - // Create hidden file input - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.json'; - fileInput.style.display = 'none'; - document.body.appendChild(fileInput); - - fileImportBtn.addEventListener('click', () => fileInput.click()); - fileInput.addEventListener('change', async () => { - const file = fileInput.files?.[0]; - if (!file) return; - const preset = await loadMidiPresetFromFile(file); - if (!preset) { - alert('Invalid MIDI CC preset file'); - return; - } - await applyMidiCCPreset(preset); - updateCountDisplay(); - if (statusEl) statusEl.textContent = `Loaded: ${preset.name || file.name}`; - // Reset file input so the same file can be re-selected - fileInput.value = ''; - }); - } - - // Build the per-param editor in the drawer - buildMidiCCParamList(); -} - -function buildMidiCCParamList() { - const container = document.getElementById('midi-cc-param-list'); - if (!container) return; - container.innerHTML = ''; - - for (let i = 0; i < midiCCMap.length; i++) { - const p = midiCCMap[i]; - const row = document.createElement('div'); - row.className = 'synth-row midi-cc-param-row'; - row.style.borderLeft = `3px solid ${MIDI_CC_PARAM_COLORS[i] || '#888'}`; - - const nameSpan = document.createElement('span'); - nameSpan.className = 'midi-cc-param-name'; - nameSpan.textContent = `${p.name}`; - nameSpan.style.flex = '1'; - nameSpan.style.fontSize = '0.75rem'; - - const ccSpan = document.createElement('span'); - ccSpan.style.opacity = '0.6'; - ccSpan.style.fontSize = '0.65rem'; - ccSpan.textContent = `CC${p.cc} Ch${p.channel}`; - - row.appendChild(nameSpan); - row.appendChild(ccSpan); - container.appendChild(row); - } -} - -// ---- Gamepad ---- -function wireGamepad() { - gamepad = new GamepadInput({ - invertY: true, - onMove: (x, y) => { - joyX = x; - joyY = y; - drawJoyMap(); - onJoystickMove(); - }, - onButton: (btn) => { - if (btn === 'lb') onThumbsDown(); - if (btn === 'rb') onThumbsUp(); - if (btn === 'a') onTrain(); - if (btn === 'x') onRandomize(); - if (btn === 'b') onClearExamples(); - }, - onConnectionChange: (connected) => { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = connected ? 'Gamepad connected' : 'Press any button to activate gamepad'; - }, - }); - - // Show hint immediately if no gamepad detected yet - if (!gamepad.connected) { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = 'Press any gamepad button to connect'; - } -} - -// ---- Keyboard ---- -function wireKeyboard() { - window.addEventListener('keydown', (e) => { - if (e.repeat) return; - - // Don't intercept keys when an input/select has focus - const tag = document.activeElement?.tagName; - if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return; - - if (e.key === '1' || e.code === 'Numpad1') { - e.preventDefault(); - onThumbsDown(); - } else if (e.key === '2' || e.code === 'Numpad2') { - e.preventDefault(); - onThumbsUp(); - } else if (e.key === '3' || e.code === 'Numpad3') { - e.preventDefault(); - if (imlEoc && eocChain?.nispsMode === 'linked') { - const prevTarget = eocTrainingTarget; - eocTrainingTarget = 'eoc'; - onThumbsDown(); - eocTrainingTarget = prevTarget; - flash('eoc-rl-minus'); - } - } else if (e.key === '4' || e.code === 'Numpad4') { - e.preventDefault(); - if (imlEoc && eocChain?.nispsMode === 'linked') { - const prevTarget = eocTrainingTarget; - eocTrainingTarget = 'eoc'; - onThumbsUp(); - eocTrainingTarget = prevTarget; - flash('eoc-rl-plus'); - } - } else if (e.key === 'z' || e.key === 'Z') { - e.preventDefault(); - onUndo(); - } - }); -} - -// ---- Quick play controls ---- -// ---- Synth Preset Selector ---- - -/** - * Rebuild the -
  • -
    -
    - -
    - - -
    - -
    -
    - -
    - - 0 clips -
    - - `; - this._container.appendChild(bar); - this._controlBar = bar; - - // Hidden file input for picker - this._fileInput = document.createElement('input'); - this._fileInput.type = 'file'; - this._fileInput.multiple = true; - this._fileInput.accept = 'audio/*,.mp3,.wav,.flac,.ogg,.m4a,.aac,.opus,.webm,.mp4,.wma,.aif,.aiff'; - this._fileInput.style.display = 'none'; - this._container.appendChild(this._fileInput); - this._fileInput.addEventListener('change', () => { - if (this._fileInput.files.length) this._handleDrop(this._fileInput.files); - this._fileInput.value = ''; - }); - bar.querySelector('#ac-add-files').addEventListener('click', () => this._fileInput.click()); - - // Submode buttons - bar.querySelectorAll('.ac-sbtn').forEach(btn => { - btn.addEventListener('click', () => this.setSubmode(btn.dataset.submode)); - }); - this._syncSubmodeUI(); - - // Timing buttons - bar.querySelectorAll('.ac-tbtn').forEach(btn => { - btn.addEventListener('click', () => { - this._beatSync = btn.dataset.sync === 'sync'; - bar.querySelectorAll('.ac-tbtn').forEach(b => { - b.style.background = b === btn ? 'rgba(255,255,255,0.1)' : 'transparent'; - b.style.color = b === btn ? '#fff' : '#aaa'; - }); - this._syncLoopLenUI(); - }); - }); - - bar.querySelector('#ac-bpm').addEventListener('change', e => { - this._bpm = Math.max(40, Math.min(240, parseFloat(e.target.value) || 120)); - }); - - // Loop length controls - bar.querySelector('#ac-loop-len').addEventListener('change', e => { - this._loopLengthValue = parseInt(e.target.value); - }); - - bar.querySelectorAll('.ac-ubtn').forEach(btn => { - btn.addEventListener('click', () => { - this._loopLengthUnit = btn.dataset.unit; - bar.querySelectorAll('.ac-ubtn').forEach(b => { - b.style.background = b === btn ? 'rgba(255,255,255,0.1)' : 'transparent'; - b.style.color = b === btn ? '#fff' : '#aaa'; - }); - }); - }); - - bar.querySelector('#ac-reset-overrides').addEventListener('click', () => { - this._perLoopLengthOverrides.clear(); - this._syncResetBtn(); - this._syncAllOverrideBadges(); - }); - - bar.querySelector('#ac-lock-pitch').addEventListener('click', () => { - this._lockPitch = !this._lockPitch; - this._syncLockPitchUI(); - }); - - bar.querySelector('#ac-play').addEventListener('click', () => { - if (this._running) this.stop(); else this.start(); - }); - } - - _syncLockPitchUI() { - const btn = this._controlBar.querySelector('#ac-lock-pitch'); - if (!btn) return; - btn.style.background = this._lockPitch ? 'rgba(255,255,255,0.15)' : 'transparent'; - btn.style.color = this._lockPitch ? '#fff' : '#aaa'; - } - - _syncSubmodeUI() { - this._controlBar.querySelectorAll('.ac-sbtn').forEach(btn => { - const active = btn.dataset.submode === this._submode; - btn.style.background = active ? 'rgba(255,255,255,0.15)' : 'transparent'; - btn.style.color = active ? '#fff' : '#aaa'; - }); - } - - // -- Pan/Zoom ---------------------------------------------------------------- - - _setupPanZoom() { - const pointers = new Map(); - let lastDist = null; - - this._container.addEventListener('pointerdown', e => { - if (e.target.closest('.ac-cell') || e.target.closest('[data-submode]') || - e.target.closest('[data-sync]') || e.target.closest('#ac-play') || - e.target.closest('#ac-bpm') || e.target.closest('#ac-loop-len-group') || - e.target.closest('#ac-add-files') || e.target.closest('.ac-loop-dropdown') || - e.target.closest('.ac-range-panel')) return; - pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); - this._container.setPointerCapture(e.pointerId); - if (pointers.size === 1) this._container.style.cursor = 'grabbing'; - }); - - this._container.addEventListener('pointermove', e => { - if (!pointers.has(e.pointerId)) return; - const prev = pointers.get(e.pointerId); - pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); - - if (pointers.size === 2) { - const pts = [...pointers.values()]; - const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y); - if (lastDist !== null) { - this._zoom = Math.max(0.15, Math.min(3, this._zoom * (dist / lastDist))); - this._applyTransform(); - } - lastDist = dist; - } else if (pointers.size === 1) { - this._panX += e.clientX - prev.x; - this._panY += e.clientY - prev.y; - this._applyTransform(); - } - }); - - this._container.addEventListener('pointerup', e => { - pointers.delete(e.pointerId); - if (pointers.size === 0) { - lastDist = null; - this._container.style.cursor = 'grab'; - } - }); - - this._container.addEventListener('wheel', e => { - e.preventDefault(); - this._zoom = Math.max(0.15, Math.min(3, this._zoom * (e.deltaY > 0 ? 0.9 : 1.1))); - this._applyTransform(); - }, { passive: false }); - } - - _applyTransform() { - this._canvas.style.transform = `translate(${this._panX}px, ${this._panY}px) scale(${this._zoom})`; - } - - // -- Audio file handling ----------------------------------------------------- - - async _handleDrop(files) { - if (!this._audioCtx) await this._initAudio(); - const valid = [...files].filter(f => - f.type.startsWith('audio/') || /\.(mp3|wav|flac|ogg|m4a|aac|opus|webm|mp4|wma|aif|aiff)$/i.test(f.name)); - for (const file of valid) await this._loadFile(file); - } - - async _loadFile(file) { - let buffer; - try { - const ab = await file.arrayBuffer(); - buffer = await this._audioCtx.decodeAudioData(ab); - } catch (err) { - console.warn('[AudioCanvas] decode failed:', file.name, err); - return; - } - const id = `clip-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; - const clip = { - id, - name: file.name.replace(/\.[^.]+$/, ''), - buffer, - duration: buffer.duration, - waveformData: this._extractWaveform(buffer), - }; - this._clips.push(clip); - this._loopCounts.push(1); // default 1 loop per clip - this._loopPlayers.push([this._makePlayer(clip)]); // one player for the default loop - this._cellView.push('overview'); - this._createCell(clip, this._clips.length - 1); - this._updateUI(); - this._resizeOutputs(); - this._emitOutputCountChanged(); - return id; - } - - _extractWaveform(buffer, resolution = 500) { - const ch = buffer.getChannelData(0); - const step = Math.max(1, Math.floor(ch.length / resolution)); - const data = new Float32Array(resolution); - for (let i = 0; i < resolution; i++) { - let peak = 0; - for (let j = 0; j < step; j++) peak = Math.max(peak, Math.abs(ch[i * step + j] || 0)); - data[i] = peak; - } - return data; - } - - _makePlayer(clip) { - return { - clip, source: null, gainNode: null, filterNode: null, - playing: false, loopStart: 0, loopEnd: clip.duration, - }; - } - - // -- Cell DOM ---------------------------------------------------------------- - - _createCell(clip, index) { - const { x, y } = cellPosition(index); - - const cell = document.createElement('div'); - cell.className = 'ac-cell'; - cell.dataset.clipId = clip.id; - cell.dataset.idx = index; - cell.style.cssText = ` - position: absolute; left: ${x}px; top: ${y}px; - width: ${CELL_W}px; height: ${CELL_H}px; - background: #fff; border: 1.5px solid #ddd; border-radius: 10px; - overflow: hidden; cursor: pointer; user-select: none; - transition: border-color 0.15s, box-shadow 0.15s; - box-shadow: 0 2px 8px rgba(0,0,0,0.06); - `; - - // Header - const hdr = document.createElement('div'); - hdr.style.cssText = ` - position: absolute; top: 0; left: 0; right: 0; height: 20px; - display: flex; justify-content: space-between; align-items: center; - padding: 0 8px; font-family: 'JetBrains Mono', monospace; - font-size: 0.58rem; color: #888; background: #fafafa; - border-bottom: 1px solid #eee; - `; - hdr.innerHTML = ` - ${clip.name.length > 20 ? clip.name.slice(0, 20) + '\u2026' : clip.name} - ${this._fmtDur(clip.duration)} - `; - - // Waveform canvases - const overviewC = document.createElement('canvas'); - overviewC.width = CELL_W * 2; overviewC.height = 120; - overviewC.style.cssText = `position:absolute;top:20px;left:0;width:${CELL_W}px;height:60px;`; - - const detailC = document.createElement('canvas'); - detailC.width = CELL_W * 2; detailC.height = 120; - detailC.style.cssText = `position:absolute;top:20px;left:0;width:${CELL_W}px;height:60px;display:none;`; - - // Status bar - const status = document.createElement('div'); - status.style.cssText = ` - position: absolute; bottom: 0; left: 0; right: 0; height: 24px; - display: flex; align-items: center; justify-content: space-between; - padding: 0 8px; font-family: 'JetBrains Mono', monospace; - font-size: 0.56rem; color: #bbb; background: #fafafa; - border-top: 1px solid #eee; - `; - status.innerHTML = ` - idle -
    -
    - - ${this._loopCounts[index]} - -
    -
    -
    -
    -
    - `; - - cell.appendChild(hdr); - cell.appendChild(overviewC); - cell.appendChild(detailC); - cell.appendChild(status); - - // Loop count stepper - status.querySelector('.ac-loop-dec').addEventListener('click', (e) => { - e.stopPropagation(); - this._setClipLoopCount(index, this._loopCounts[index] - 1); - }); - status.querySelector('.ac-loop-inc').addEventListener('click', (e) => { - e.stopPropagation(); - this._setClipLoopCount(index, this._loopCounts[index] + 1); - }); - - // Toggle overview/detail on tap; long-press or right-click for loop length dropdown - let tapStart = 0; - let longPressTimer = null; - let longPressFired = false; - cell.addEventListener('pointerdown', (e) => { - tapStart = Date.now(); - longPressFired = false; - longPressTimer = setTimeout(() => { - longPressFired = true; - const idx = parseInt(cell.dataset.idx); - this._showLoopLenDropdown(idx, cell); - }, 500); - }); - cell.addEventListener('pointermove', () => { - clearTimeout(longPressTimer); - }); - cell.addEventListener('pointerup', e => { - clearTimeout(longPressTimer); - if (!longPressFired && Date.now() - tapStart < 200) { - const idx = parseInt(cell.dataset.idx); - this._cellView[idx] = this._cellView[idx] === 'overview' ? 'detail' : 'overview'; - this._syncCellView(idx); - } - e.stopPropagation(); - }); - cell.addEventListener('contextmenu', e => { - e.preventDefault(); - const idx = parseInt(cell.dataset.idx); - this._showLoopLenDropdown(idx, cell); - }); - - this._canvas.appendChild(cell); - clip._cell = cell; - clip._overviewC = overviewC; - clip._detailC = detailC; - clip._status = status; - - this._drawOverview(clip); - this._syncCellView(index); - } - - _syncCellView(idx) { - const clip = this._clips[idx]; - if (!clip) return; - const v = this._cellView[idx]; - clip._overviewC.style.display = v === 'overview' ? 'block' : 'none'; - clip._detailC.style.display = v === 'detail' ? 'block' : 'none'; - } - - // -- Waveform drawing -------------------------------------------------------- - - _drawOverview(clip) { - const c = clip._overviewC; - const ctx = c.getContext('2d'); - const w = c.width, h = c.height; - const d = clip.waveformData; - ctx.clearRect(0, 0, w, h); - ctx.fillStyle = '#f0f4ff'; - ctx.fillRect(0, 0, w, h); - ctx.beginPath(); - for (let i = 0; i < d.length; i++) { - const x = (i / d.length) * w; - const a = d[i] * h * 0.44; - const mid = h / 2; - i === 0 ? ctx.moveTo(x, mid - a) : ctx.lineTo(x, mid - a); - } - for (let i = d.length - 1; i >= 0; i--) { - ctx.lineTo((i / d.length) * w, h / 2 + d[i] * h * 0.44); - } - ctx.closePath(); - ctx.fillStyle = 'rgba(74,144,217,0.25)'; - ctx.fill(); - ctx.strokeStyle = 'rgba(74,144,217,0.7)'; - ctx.lineWidth = 1; - ctx.stroke(); - } - - _drawOverviewWithLoop(clip, loopStart, loopEnd) { - this._drawOverview(clip); - const c = clip._overviewC; - const ctx = c.getContext('2d'); - const w = c.width, h = c.height; - const sx = loopStart * w, ex = loopEnd * w; - ctx.fillStyle = 'rgba(0,122,255,0.12)'; - ctx.fillRect(sx, 0, ex - sx, h); - ctx.strokeStyle = 'rgba(0,122,255,0.6)'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(sx, 0); ctx.lineTo(sx, h); - ctx.moveTo(ex, 0); ctx.lineTo(ex, h); - ctx.stroke(); - } - - _drawDetail(clip, loopStart, loopEnd, playhead) { - const c = clip._detailC; - const ctx = c.getContext('2d'); - const w = c.width, h = c.height; - const d = clip.waveformData; - const si = Math.floor(loopStart * d.length); - const ei = Math.ceil(loopEnd * d.length); - const sl = d.slice(si, ei); - ctx.clearRect(0, 0, w, h); - ctx.fillStyle = '#fff8f0'; - ctx.fillRect(0, 0, w, h); - ctx.beginPath(); - for (let i = 0; i < sl.length; i++) { - const x = (i / sl.length) * w; - const a = sl[i] * h * 0.44; - i === 0 ? ctx.moveTo(x, h / 2 - a) : ctx.lineTo(x, h / 2 - a); - } - for (let i = sl.length - 1; i >= 0; i--) { - ctx.lineTo((i / sl.length) * w, h / 2 + sl[i] * h * 0.44); - } - ctx.closePath(); - ctx.fillStyle = 'rgba(230,140,50,0.25)'; - ctx.fill(); - ctx.strokeStyle = 'rgba(230,140,50,0.8)'; - ctx.lineWidth = 1; - ctx.stroke(); - if (playhead != null) { - ctx.strokeStyle = '#ff3b30'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(playhead * w, 0); - ctx.lineTo(playhead * w, h); - ctx.stroke(); - } - } - - _setCellActive(clip, label, level, color = '#4a90d9') { - const s = clip._status; - s.querySelector('.ac-st-label').textContent = label; - s.querySelector('.ac-st-level').style.width = `${Math.round(level * 100)}%`; - s.querySelector('.ac-st-level').style.background = color; - clip._cell.style.borderColor = level > 0.05 - ? `${color}${Math.round(40 + level * 180).toString(16).padStart(2, '0')}` - : '#ddd'; - clip._cell.style.boxShadow = level > 0.05 - ? `0 0 ${level * 18}px ${color}${Math.round(level * 80).toString(16).padStart(2, '0')}` - : '0 2px 8px rgba(0,0,0,0.06)'; - } - - _setCellIdle(clip) { - this._setCellActive(clip, 'idle', 0); - } - - // -- Audio init -------------------------------------------------------------- - - async _initAudio() { - this._audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - this._masterGain = this._audioCtx.createGain(); - this._masterGain.gain.value = 0.75; - this._masterGain.connect(this._audioCtx.destination); - } - - // -- Playback ---------------------------------------------------------------- - - async start() { - if (this._running) return; - if (!this._audioCtx) await this._initAudio(); - if (this._audioCtx.state === 'suspended') await this._audioCtx.resume(); - this._running = true; - this._updatePlayBtn(true); - this._startPlayback(); - } - - stop() { - if (!this._running) return; - this._running = false; - this._updatePlayBtn(false); - this._stopAll(); - this._clips.forEach(c => this._setCellIdle(c)); - } - - _stopAll() { - clearInterval(this._loopUpdateTimer); - clearTimeout(this._slicerTimer); - this._loopUpdateTimer = null; - this._slicerTimer = null; - this._granularTimers.forEach((t, i) => { - if (t) clearInterval(t); - this._granularTimers[i] = null; - }); - this._granularTimers = []; - for (const loopArr of this._loopPlayers) { - for (const p of loopArr) { - if (p.source) { try { p.source.stop(); } catch (_) {} p.source = null; } - if (p.gainNode) { p.gainNode.disconnect(); p.gainNode = null; } - if (p.filterNode) { p.filterNode.disconnect(); p.filterNode = null; } - p.playing = false; - } - } - } - - _startPlayback() { - if (this._clips.length === 0 || this.getTotalLoops() === 0) return; - if (this._submode === 'remix' || this._submode === 'drone') { - this._startLooping(); - } else if (this._submode === 'granular') { - this._startGranular(); - } else if (this._submode === 'slicer') { - this._slicerStep = 0; - this._scheduleSlicer(); - } - } - - _updatePlayBtn(playing) { - const btn = this._controlBar.querySelector('#ac-play'); - btn.innerHTML = playing - ? `` - : ``; - } - - // -- Remix / Drone ----------------------------------------------------------- - - _startLooping() { - // Start a loop source for each active loop across all clips - for (let ci = 0; ci < this._clips.length; ci++) { - for (let li = 0; li < this._loopCounts[ci]; li++) { - this._startOneLoop(ci, li); - } - } - this._loopUpdateTimer = setInterval(() => this._tickLooping(), 60); - } - - _startOneLoop(clipIdx, loopIdx) { - const clip = this._clips[clipIdx]; - const player = this._loopPlayers[clipIdx]?.[loopIdx]; - if (!clip || !player) return; - - const filterNode = this._audioCtx.createBiquadFilter(); - filterNode.type = 'lowpass'; - filterNode.frequency.value = 4000; - filterNode.Q.value = 0.7; - - const gainNode = this._audioCtx.createGain(); - gainNode.gain.value = 0.5; - - filterNode.connect(gainNode); - gainNode.connect(this._masterGain); - - const source = this._audioCtx.createBufferSource(); - source.buffer = clip.buffer; - source.loop = true; - source.loopStart = 0; - source.loopEnd = clip.duration; - source.playbackRate.value = this._submode === 'drone' ? 0.05 : 1.0; - source.connect(filterNode); - source.start(); - - player.source = source; - player.gainNode = gainNode; - player.filterNode = filterNode; - player.playing = true; - } - - _tickLooping() { - if (!this._running) return; - const now = this._audioCtx.currentTime; - const isDrone = this._submode === 'drone'; - - let flatIdx = 0; - for (let ci = 0; ci < this._clips.length; ci++) { - const clip = this._clips[ci]; - let clipMaxVol = 0; - let clipLoopStart = 0, clipLoopEnd = 1; - - for (let li = 0; li < this._loopCounts[ci]; li++) { - const player = this._loopPlayers[ci]?.[li]; - if (!player || !player.playing) { flatIdx++; continue; } - - // Resolve per-loop params through global ranges - const volPos = this._getLoopParam(flatIdx, 0); - const pitchPos = this._getLoopParam(flatIdx, 1); - const posPos = this._getLoopParam(flatIdx, 2); - const filtPos = this._getLoopParam(flatIdx, 3); - - const vol = this._resolveLoopParam('volume', volPos); - const pitchNorm = this._resolveLoopParam('pitch', pitchPos); - const loopNorm = this._resolveLoopParam('position', posPos); - const filtNorm = this._resolveLoopParam('filter', filtPos); - - const speed = this._lockPitch - ? (isDrone ? 0.05 : 1.0) - : isDrone - ? 0.005 + pitchNorm * 0.095 // 0.005x-0.1x for drone - : 0.5 + pitchNorm * 1.5; // 0.5x-2.0x for remix - - // Loop window: treat loopNorm as start of a sliding window - const winSecs = this._beatSync - ? this._getLoopDuration(ci) - : (isDrone ? 120 : 15); - const loopStart = loopNorm * Math.max(0, clip.duration - winSecs); - const loopEnd = Math.min(loopStart + winSecs, clip.duration); - const filterHz = 200 * Math.pow(40, filtNorm); // 200 Hz - 8000 Hz log - - player.source.playbackRate.setTargetAtTime(speed, now, 0.08); - player.source.loopStart = loopStart; - player.source.loopEnd = loopEnd; - player.gainNode.gain.setTargetAtTime(isDrone ? vol * 0.7 : vol, now, 0.06); - player.filterNode.frequency.setTargetAtTime(filterHz, now, 0.06); - - // Track highest vol and last loop window for cell visuals - if (vol > clipMaxVol) clipMaxVol = vol; - clipLoopStart = loopStart / clip.duration; - clipLoopEnd = loopEnd / clip.duration; - - flatIdx++; - } - - // Visuals: use the last loop's window (good enough for overview) - if (this._loopCounts[ci] > 0) { - this._drawOverviewWithLoop(clip, clipLoopStart, clipLoopEnd); - if (this._cellView[ci] === 'detail') this._drawDetail(clip, clipLoopStart, clipLoopEnd, null); - const color = isDrone ? '#8040c0' : '#4a90d9'; - this._setCellActive(clip, isDrone ? 'drone' : 'remix', clipMaxVol, color); - } - } - } - - // -- Granular ---------------------------------------------------------------- - - _startGranular() { - this._granularTimers = []; - let flatIdx = 0; - for (let ci = 0; ci < this._clips.length; ci++) { - for (let li = 0; li < this._loopCounts[ci]; li++) { - const fi = flatIdx; - const ciCapture = ci; - const timer = setInterval(() => { - if (!this._running) { clearInterval(timer); return; } - this._spawnGrain(ciCapture, fi); - }, 35); - this._granularTimers.push(timer); - flatIdx++; - } - } - // Consolidated visual update per clip (avoids multi-loop redraw flicker) - this._loopUpdateTimer = setInterval(() => this._tickGranularVisuals(), 60); - } - - _tickGranularVisuals() { - if (!this._running) return; - let flatIdx = 0; - for (let ci = 0; ci < this._clips.length; ci++) { - const clip = this._clips[ci]; - if (this._loopCounts[ci] === 0) continue; - // Show the average grain position across loops for this clip - let posSum = 0, densityMax = 0; - for (let li = 0; li < this._loopCounts[ci]; li++) { - const posNorm = this._resolveLoopParam('position', this._getLoopParam(flatIdx, 2)); - const density = this._resolveLoopParam('volume', this._getLoopParam(flatIdx, 0)); - posSum += posNorm; - if (density > densityMax) densityMax = density; - flatIdx++; - } - const avgPos = posSum / this._loopCounts[ci]; - this._drawOverviewWithLoop(clip, Math.max(0, avgPos - 0.03), Math.min(1, avgPos + 0.03)); - this._setCellActive(clip, 'granular', densityMax, '#7040c0'); - } - } - - _spawnGrain(clipIdx, flatLoopIdx) { - const clip = this._clips[clipIdx]; - if (!clip) return; - - // Resolve per-loop params through global ranges - const density = this._resolveLoopParam('volume', this._getLoopParam(flatLoopIdx, 0)); - const pitchNorm = this._resolveLoopParam('pitch', this._getLoopParam(flatLoopIdx, 1)); - const posNorm = this._resolveLoopParam('position', this._getLoopParam(flatLoopIdx, 2)); - const sizeNorm = this._resolveLoopParam('filter', this._getLoopParam(flatLoopIdx, 3)); - - if (Math.random() > density) return; - - const grainDur = 0.02 + sizeNorm * 0.38; - const pitch = this._lockPitch ? 1.0 : 0.5 + pitchNorm * 1.5; - const scatter = 0.04; - const pos = Math.max(0, Math.min(1, posNorm + (Math.random() - 0.5) * scatter * 2)); - let startOff; - if (this._beatSync) { - const loopDur = this._getLoopDuration(clipIdx); - const loopWindow = Math.min(loopDur, clip.duration); - startOff = pos * Math.max(0, loopWindow - grainDur); - } else { - startOff = pos * Math.max(0, clip.duration - grainDur); - } - - const gain = this._audioCtx.createGain(); - const t = this._audioCtx.currentTime; - gain.gain.setValueAtTime(0, t); - gain.gain.linearRampToValueAtTime(0.4 * density, t + grainDur * 0.15); - gain.gain.linearRampToValueAtTime(0.4 * density, t + grainDur * 0.85); - gain.gain.linearRampToValueAtTime(0, t + grainDur); - gain.connect(this._masterGain); - - const src = this._audioCtx.createBufferSource(); - src.buffer = clip.buffer; - src.playbackRate.value = pitch; - src.connect(gain); - src.start(t, startOff, grainDur); - src.onended = () => gain.disconnect(); - } - - // -- Slicer ------------------------------------------------------------------ - - _scheduleSlicer() { - if (!this._running) return; - const stepMs = this._beatSync - ? (this._getLoopDuration(0) * 1000 / 8) // divide the loop into 8 slices - : (60000 / this._bpm); - this._slicerTimer = setTimeout(() => { - this._fireSlice(); - this._scheduleSlicer(); - }, stepMs); - } - - _fireSlice() { - const totalLoops = this.getTotalLoops(); - if (totalLoops === 0) return; - - const step = this._slicerStep % Math.max(1, totalLoops); - this._slicerStep++; - - // Find which clip and loop this step maps to - let flatIdx = 0; - let targetClipIdx = 0, targetLoopIdx = 0; - for (let ci = 0; ci < this._clips.length; ci++) { - for (let li = 0; li < this._loopCounts[ci]; li++) { - if (flatIdx === step) { targetClipIdx = ci; targetLoopIdx = li; } - flatIdx++; - } - } - - const clip = this._clips[targetClipIdx]; - if (!clip) return; - - // Resolve through param ranges - const clipNorm = this._resolveLoopParam('volume', this._getLoopParam(step, 0)); - const posNorm = this._resolveLoopParam('pitch', this._getLoopParam(step, 1)); - const speedNorm = this._resolveLoopParam('position', this._getLoopParam(step, 2)); - const sliceDur = Math.min(1.2, (this._beatSync ? 60 / this._bpm / 2 : 60 / this._bpm)); - const speed = this._lockPitch ? 1.0 : 0.5 + speedNorm * 1.5; - const startOff = posNorm * Math.max(0, clip.duration - sliceDur); - - const gain = this._audioCtx.createGain(); - const t = this._audioCtx.currentTime; - gain.gain.setValueAtTime(0, t); - gain.gain.linearRampToValueAtTime(0.7, t + 0.004); - gain.gain.linearRampToValueAtTime(0.7, t + sliceDur * 0.75); - gain.gain.linearRampToValueAtTime(0, t + sliceDur); - gain.connect(this._masterGain); - - const src = this._audioCtx.createBufferSource(); - src.buffer = clip.buffer; - src.playbackRate.value = speed; - src.connect(gain); - src.start(t, startOff, sliceDur / speed); - src.onended = () => gain.disconnect(); - - // Visuals: flash active cell, idle others - this._clips.forEach((c, i) => { - if (i === targetClipIdx) { - this._drawOverviewWithLoop(c, posNorm - 0.02, posNorm + 0.02); - this._setCellActive(c, `slice ${step}`, speedNorm, '#ff3b30'); - } else { - this._drawOverview(c); - this._setCellIdle(c); - } - }); - } - - // -- Loop management --------------------------------------------------------- - - /** Set loop count for a clip (0-4). Returns true if changed. */ - _setClipLoopCount(clipIdx, count) { - count = Math.max(0, Math.min(MAX_LOOPS_PER_CLIP, count)); - const old = this._loopCounts[clipIdx]; - if (count === old) return false; - - // Save for revert - this._lastLoopCountChange = { clipIdx, oldCount: old }; - - const wasRunning = this._running; - if (wasRunning) this.stop(); - - const clip = this._clips[clipIdx]; - const players = this._loopPlayers[clipIdx]; - - if (count > old) { - // Add new loops - for (let i = old; i < count; i++) { - players.push(this._makePlayer(clip)); - } - } else { - // Remove from the end - const removed = players.splice(count); - for (const p of removed) { - if (p.source) { try { p.source.stop(); } catch (_) {} } - if (p.gainNode) p.gainNode.disconnect(); - if (p.filterNode) p.filterNode.disconnect(); - } - } - - this._loopCounts[clipIdx] = count; - this._resizeOutputs(); - this._syncCellLoopCount(clipIdx); - this._emitOutputCountChanged(); - - if (wasRunning) this.start(); - return true; - } - - /** Revert the last loop count change (called when user cancels confirm dialog) */ - revertLastLoopChange() { - if (!this._lastLoopCountChange) return; - const { clipIdx, oldCount } = this._lastLoopCountChange; - this._lastLoopCountChange = null; - this._setClipLoopCount(clipIdx, oldCount); - } - - _syncCellLoopCount(clipIdx) { - const clip = this._clips[clipIdx]; - if (!clip || !clip._cell) return; - const countEl = clip._cell.querySelector('.ac-loop-count'); - if (countEl) countEl.textContent = this._loopCounts[clipIdx]; - // Dim cell when loop count is 0 - clip._cell.style.opacity = this._loopCounts[clipIdx] === 0 ? '0.5' : '1'; - } - - _resizeOutputs() { - const count = this.getOutputCount(); - if (this._outputs.length !== count) { - const old = this._outputs; - this._outputs = new Float32Array(count).fill(0.5); - // Preserve what we can from the old outputs - for (let i = 0; i < Math.min(old.length, count); i++) this._outputs[i] = old[i]; - } - // Keep _loopOutputs in sync (slice after the 8 range outputs) - this._loopOutputs = Array.from(this._outputs).slice(N_RANGE_OUTPUTS); - } - - _emitOutputCountChanged() { - this._container.dispatchEvent(new CustomEvent('ac:outputcount-changed', { - detail: { count: this.getOutputCount() }, - bubbles: true, - })); - } - - // -- Beat-synced loop length ------------------------------------------------- - - _getLoopDuration(clipIndex) { - const override = this._perLoopLengthOverrides.get(clipIndex); - const unit = override?.unit ?? this._loopLengthUnit; - const value = override?.value ?? this._loopLengthValue; - const beatDuration = 60 / this._bpm; // seconds per beat - if (unit === 'bars') return beatDuration * 4 * value; // 4 beats per bar - return beatDuration * value; - } - - _syncLoopLenUI() { - const grp = this._controlBar.querySelector('#ac-loop-len-group'); - if (!grp) return; - grp.style.opacity = this._beatSync ? '1' : '0.3'; - grp.style.pointerEvents = this._beatSync ? 'auto' : 'none'; - } - - _syncResetBtn() { - const btn = this._controlBar.querySelector('#ac-reset-overrides'); - if (btn) btn.style.display = this._perLoopLengthOverrides.size > 0 ? 'block' : 'none'; - } - - _syncAllOverrideBadges() { - for (let i = 0; i < this._clips.length; i++) { - this._syncOverrideBadge(i); - } - } - - _syncOverrideBadge(idx) { - const clip = this._clips[idx]; - if (!clip || !clip._cell) return; - let badge = clip._cell.querySelector('.ac-loop-badge'); - const override = this._perLoopLengthOverrides.get(idx); - if (override) { - if (!badge) { - badge = document.createElement('span'); - badge.className = 'ac-loop-badge'; - badge.style.cssText = ` - position:absolute; top:2px; right:4px; - padding:1px 5px; border-radius:8px; - background:rgba(0,122,255,0.15); color:rgba(0,122,255,0.8); - font-family:'JetBrains Mono',monospace; font-size:0.48rem; - pointer-events:none; z-index:2; - `; - clip._cell.appendChild(badge); - } - badge.textContent = `${override.value} ${override.unit}`; - badge.style.display = ''; - } else { - if (badge) badge.style.display = 'none'; - } - } - - _showLoopLenDropdown(idx, anchorEl) { - this._dismissLoopLenDropdown(); - const override = this._perLoopLengthOverrides.get(idx); - const curUnit = override?.unit ?? this._loopLengthUnit; - - const menu = document.createElement('div'); - menu.className = 'ac-loop-dropdown'; - menu.style.cssText = ` - position:fixed; z-index:100; - background:rgba(20,20,20,0.95); color:#fff; - border-radius:10px; padding:4px 0; - font-family:'JetBrains Mono',monospace; font-size:0.6rem; - box-shadow:0 6px 24px rgba(0,0,0,0.5); backdrop-filter:blur(10px); - min-width:110px; overflow:hidden; - `; - - const globalLabel = `Global (${this._loopLengthValue} ${this._loopLengthUnit})`; - const isGlobal = !override; - - const items = [ - { label: globalLabel, action: () => { this._perLoopLengthOverrides.delete(idx); } }, - '---', - ...[2, 4, 8, 16, 32, 64].map(v => ({ - label: `${v} ${curUnit}`, - selected: !isGlobal && override?.value === v && override?.unit === curUnit, - action: () => { this._perLoopLengthOverrides.set(idx, { unit: curUnit, value: v }); }, - })), - '---', - ]; - - // Unit toggle row - const unitRow = document.createElement('div'); - unitRow.style.cssText = 'display:flex;gap:2px;padding:4px 8px;justify-content:center'; - ['bars', 'beats'].forEach(u => { - const btn = document.createElement('button'); - btn.textContent = u; - btn.style.cssText = ` - padding:2px 6px; border-radius:6px; border:none; cursor:pointer; - font-family:inherit; font-size:0.56rem; - background:${u === curUnit ? 'rgba(255,255,255,0.15)' : 'transparent'}; - color:${u === curUnit ? '#fff' : '#888'}; - `; - btn.addEventListener('click', e => { - e.stopPropagation(); - const ov = this._perLoopLengthOverrides.get(idx); - const val = ov?.value ?? this._loopLengthValue; - this._perLoopLengthOverrides.set(idx, { unit: u, value: val }); - this._syncOverrideBadge(idx); - this._syncResetBtn(); - this._dismissLoopLenDropdown(); - this._showLoopLenDropdown(idx, anchorEl); - }); - unitRow.appendChild(btn); - }); - - for (const item of items) { - if (item === '---') { - const div = document.createElement('div'); - div.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:2px 0'; - menu.appendChild(div); - continue; - } - const row = document.createElement('div'); - row.style.cssText = ` - padding:5px 12px; cursor:pointer; - color:${item.selected || (item.label === globalLabel && isGlobal) ? '#fff' : '#aaa'}; - background:${item.selected || (item.label === globalLabel && isGlobal) ? 'rgba(0,122,255,0.2)' : 'transparent'}; - `; - row.textContent = item.label; - row.addEventListener('pointerenter', () => { row.style.background = 'rgba(255,255,255,0.08)'; }); - row.addEventListener('pointerleave', () => { - row.style.background = (item.selected || (item.label === globalLabel && isGlobal)) ? 'rgba(0,122,255,0.2)' : 'transparent'; - }); - row.addEventListener('click', e => { - e.stopPropagation(); - item.action(); - this._syncOverrideBadge(idx); - this._syncResetBtn(); - this._dismissLoopLenDropdown(); - }); - menu.appendChild(row); - } - menu.appendChild(unitRow); - - document.body.appendChild(menu); - this._activeDropdown = menu; - - // Position near the anchor cell - const rect = anchorEl.getBoundingClientRect(); - menu.style.left = `${Math.min(rect.right + 4, window.innerWidth - 130)}px`; - menu.style.top = `${Math.max(4, rect.top)}px`; - - // Dismiss on outside click - const dismiss = (e) => { - if (!menu.contains(e.target)) this._dismissLoopLenDropdown(); - }; - setTimeout(() => document.addEventListener('pointerdown', dismiss, { once: true }), 10); - menu._dismissHandler = dismiss; - } - - _dismissLoopLenDropdown() { - if (this._activeDropdown) { - this._activeDropdown.remove(); - this._activeDropdown = null; - } - } - - // -- Public API -------------------------------------------------------------- - - setOutputs(outputs) { - const count = this.getOutputCount(); - if (this._outputs.length !== count) this._resizeOutputs(); - for (let i = 0; i < Math.min(outputs.length, count); i++) { - this._outputs[i] = outputs[i]; - } - - // Range outputs (0-7) -- only update if not manually overridden - if (!this._rangeOverrides.volume) { - this._paramRanges.volume.min = outputs[0] ?? 0; - this._paramRanges.volume.max = outputs[1] ?? 1; - } - if (!this._rangeOverrides.pitch) { - this._paramRanges.pitch.min = outputs[2] ?? 0; - this._paramRanges.pitch.max = outputs[3] ?? 1; - } - if (!this._rangeOverrides.position) { - this._paramRanges.position.min = outputs[4] ?? 0; - this._paramRanges.position.max = outputs[5] ?? 1; - } - if (!this._rangeOverrides.filter) { - this._paramRanges.filter.min = outputs[6] ?? 0; - this._paramRanges.filter.max = outputs[7] ?? 1; - } - - // Per-loop position outputs (8+) - this._loopOutputs = Array.from(outputs).slice(N_RANGE_OUTPUTS); - - this._updateRangePanel(); - } - - /** Resolve a per-loop position value through the global param range */ - _resolveLoopParam(group, positionValue) { - const r = this._paramRanges[group]; - const lo = Math.min(r.min, r.max); - const hi = Math.max(r.min, r.max); - return Math.max(0, Math.min(1, lo + positionValue * (hi - lo))); - } - - /** Get a per-loop param from the loopOutputs array */ - _getLoopParam(flatLoopIdx, paramOffset) { - const baseOutput = flatLoopIdx * N_PARAMS_PER_LOOP; - return this._loopOutputs[baseOutput + paramOffset] ?? 0.5; - } - - getParamGroupInfo() { - return [ - { name: 'Volume', color: GROUP_COLORS.volume, range: this._paramRanges.volume }, - { name: 'Pitch', color: GROUP_COLORS.pitch, range: this._paramRanges.pitch }, - { name: 'Position', color: GROUP_COLORS.position, range: this._paramRanges.position }, - { name: 'Filter', color: GROUP_COLORS.filter, range: this._paramRanges.filter }, - ]; - } - - setSubmode(mode) { - if (!SUBMODES.includes(mode)) return; - const wasRunning = this._running; - if (wasRunning) this.stop(); - this._submode = mode; - this._syncSubmodeUI(); - if (wasRunning) this.start(); - } - - getOutputCount() { return N_RANGE_OUTPUTS + this.getTotalLoops() * N_PARAMS_PER_LOOP; } - getTotalLoops() { return this._loopCounts.reduce((s, c) => s + c, 0); } - isRunning() { return this._running; } - - /** Dynamic param names for heatmap */ - getAudioParamNames() { - const names = [...RANGE_NAMES]; - for (let ci = 0; ci < this._clips.length; ci++) { - for (let li = 0; li < this._loopCounts[ci]; li++) { - const tag = `L${ci + 1}.${li + 1}`; - names.push(`${tag} Vol`, `${tag} Pitch`, `${tag} Pos`, `${tag} Filt`); - } - } - return names; - } - - /** Dynamic param colors for heatmap */ - getAudioParamColors() { - const colors = [...RANGE_COLORS]; - for (let ci = 0; ci < this._clips.length; ci++) { - for (let li = 0; li < this._loopCounts[ci]; li++) { - colors.push(GROUP_COLORS.volume, GROUP_COLORS.pitch, GROUP_COLORS.position, GROUP_COLORS.filter); - } - } - return colors; - } - - destroy() { - this.stop(); - this._dismissLoopLenDropdown(); - if (this._audioCtx) { this._audioCtx.close(); this._audioCtx = null; } - this._container.innerHTML = ''; - } - - getState() { - return { - submode: this._submode, beatSync: this._beatSync, bpm: this._bpm, - panX: this._panX, panY: this._panY, zoom: this._zoom, - loopCounts: [...this._loopCounts], - loopLengthUnit: this._loopLengthUnit, - loopLengthValue: this._loopLengthValue, - perLoopLengthOverrides: Object.fromEntries(this._perLoopLengthOverrides), - paramRanges: JSON.parse(JSON.stringify(this._paramRanges)), - rangeOverrides: { ...this._rangeOverrides }, - lockPitch: this._lockPitch, - }; - } - - setState(s) { - if (s.submode) { this._submode = s.submode; this._syncSubmodeUI(); } - if (s.beatSync !== undefined) this._beatSync = s.beatSync; - if (s.bpm) this._bpm = s.bpm; - if (s.panX !== undefined) this._panX = s.panX; - if (s.panY !== undefined) this._panY = s.panY; - if (s.zoom) this._zoom = s.zoom; - // Restore loop counts - if (Array.isArray(s.loopCounts)) { - for (let i = 0; i < s.loopCounts.length && i < this._loopCounts.length; i++) { - if (s.loopCounts[i] !== this._loopCounts[i]) { - this._setClipLoopCount(i, s.loopCounts[i]); - } - } - } - // Restore beat-sync loop length - if (s.loopLengthUnit) this._loopLengthUnit = s.loopLengthUnit; - if (s.loopLengthValue) this._loopLengthValue = s.loopLengthValue; - if (s.perLoopLengthOverrides) { - this._perLoopLengthOverrides = new Map(Object.entries(s.perLoopLengthOverrides).map(([k, v]) => [parseInt(k), v])); - } - // Restore param ranges - if (s.paramRanges) { - for (const group of ['volume', 'pitch', 'position', 'filter']) { - if (s.paramRanges[group]) { - this._paramRanges[group].min = s.paramRanges[group].min ?? 0; - this._paramRanges[group].max = s.paramRanges[group].max ?? 1; - } - } - } - if (s.rangeOverrides) { - for (const group of ['volume', 'pitch', 'position', 'filter']) { - if (s.rangeOverrides[group] !== undefined) this._rangeOverrides[group] = s.rangeOverrides[group]; - } - } - if (s.lockPitch !== undefined) { this._lockPitch = s.lockPitch; this._syncLockPitchUI(); } - this._applyTransform(); - this._syncLoopLenUI(); - this._syncResetBtn(); - this._syncAllOverrideBadges(); - this._updateRangePanel(); - // Sync control bar values - const lenSelect = this._controlBar?.querySelector('#ac-loop-len'); - if (lenSelect) lenSelect.value = this._loopLengthValue; - this._controlBar?.querySelectorAll('.ac-ubtn').forEach(b => { - b.style.background = b.dataset.unit === this._loopLengthUnit ? 'rgba(255,255,255,0.1)' : 'transparent'; - b.style.color = b.dataset.unit === this._loopLengthUnit ? '#fff' : '#aaa'; - }); - // Sync beat sync buttons - this._controlBar?.querySelectorAll('.ac-tbtn').forEach(b => { - const isActive = (this._beatSync && b.dataset.sync === 'sync') || (!this._beatSync && b.dataset.sync === 'free'); - b.style.background = isActive ? 'rgba(255,255,255,0.1)' : 'transparent'; - b.style.color = isActive ? '#fff' : '#aaa'; - }); - } - - // -- Helpers ----------------------------------------------------------------- - - _fmtDur(s) { - return `${Math.floor(s / 60)}:${Math.floor(s % 60).toString().padStart(2, '0')}`; - } - - _updateUI() { - const ct = this._container.querySelector('#ac-clip-ct'); - if (ct) ct.textContent = `${this._clips.length} clip${this._clips.length !== 1 ? 's' : ''}`; - this._dropOverlay.style.opacity = this._clips.length > 0 ? '0' : '1'; - this._updateRangePanel(); - } -} diff --git a/playground/js/b-app.js b/playground/js/b-app.js deleted file mode 100644 index 553cd26..0000000 --- a/playground/js/b-app.js +++ /dev/null @@ -1,1290 +0,0 @@ -// NISPS Workbench (Design B) — Main application -// Dashboard layout with mapping visualization, merged input space - -import { IML } from './nisps/iml.js'; -import { FlowFieldVisualizer } from './ui/visualizer.js'; -import { C15Bridge } from './synth/c15-bridge.js'; -import { Arpeggiator } from './synth/arpeggiator.js'; -import { MIDIInput } from './synth/midi-input.js'; -import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyTame } from './synth/param-map.js'; -import { GamepadInput } from './ui/gamepad.js'; - -// --- Constants --- -const N_INPUTS = 2; -const N_VISUAL_OUTPUTS = 20; -const N_SYNTH_OUTPUTS = SYNTH_PARAM_MAP.length; // 126 -const N_OUTPUTS = N_SYNTH_OUTPUTS; // MLP always produces full output; visual uses first 20 -const STORAGE_KEY = 'nisps-b-workbench'; - -const VISUAL_PARAM_NAMES = [ - 'Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', - 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', - 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate' -]; -const VISUAL_PARAM_COLORS = [ - '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', - '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', - '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', - '#ffc15f', '#ff8a3d' -]; - -// Visual param groups: [groupName, accentColor, [indices]] -const VISUAL_GROUPS = [ - ['Motion', '--group-motion', [0, 2, 14, 15, 16]], - ['Color', '--group-color', [3, 4, 6]], - ['Particles', '--group-particles', [1, 5, 12, 13]], - ['Forces', '--group-forces', [8, 9, 7, 17, 18]], - ['Effects', '--group-effects', [10, 11, 19]], -]; - -const SYNTH_GROUPS = [ - ['Envelope A', '--group-osc', Array.from({length: 7}, (_, i) => i)], - ['Envelope B', '--group-osc', Array.from({length: 7}, (_, i) => i + 7)], - ['Envelope C', '--group-osc', Array.from({length: 6}, (_, i) => i + 14)], - ['Oscillator A','--group-osc', Array.from({length: 5}, (_, i) => i + 20)], - ['Oscillator B','--group-osc', Array.from({length: 5}, (_, i) => i + 25)], - ['Shaper A', '--group-shapers', Array.from({length: 6}, (_, i) => i + 30)], - ['Shaper B', '--group-shapers', Array.from({length: 6}, (_, i) => i + 36)], - ['Comb Filter', '--group-filters', Array.from({length: 8}, (_, i) => i + 42)], - ['SVF Filter', '--group-filters', Array.from({length: 7}, (_, i) => i + 50)], - ['FB Mixer', '--group-effects', Array.from({length: 10}, (_, i) => i + 57)], - ['Out Mixer', '--group-output', Array.from({length: 14}, (_, i) => i + 67)], - ['Cabinet', '--group-effects', Array.from({length: 8}, (_, i) => i + 81)], - ['Flanger', '--group-fx', Array.from({length: 13}, (_, i) => i + 89)], - ['Echo', '--group-fx', Array.from({length: 7}, (_, i) => i + 102)], - ['Reverb', '--group-fx', Array.from({length: 6}, (_, i) => i + 109)], - ['Unison', '--group-output', Array.from({length: 3}, (_, i) => i + 115)], - // Remaining params (118..125) if any - ...(N_SYNTH_OUTPUTS > 118 ? [['Mono/Extra', '--group-output', Array.from({length: N_SYNTH_OUTPUTS - 118}, (_, i) => i + 118)]] : []), -]; - -// Heatmap colormap: dark navy -> muted teal -> muted yellow -> muted orange -function heatmapColor(t) { - t = Math.max(0, Math.min(1, t)); - let r, g, b; - if (t < 0.25) { - // Dark navy -> muted teal - const s = t / 0.25; - r = Math.floor(8 + s * 20); - g = Math.floor(12 + s * 60); - b = Math.floor(30 + s * 55); - } else if (t < 0.5) { - // Muted teal -> muted teal-green - const s = (t - 0.25) / 0.25; - r = Math.floor(28 + s * 30); - g = Math.floor(72 + s * 50); - b = Math.floor(85 - s * 40); - } else if (t < 0.75) { - // Muted teal-green -> muted yellow - const s = (t - 0.5) / 0.25; - r = Math.floor(58 + s * 90); - g = Math.floor(122 + s * 30); - b = Math.floor(45 - s * 20); - } else { - // Muted yellow -> muted orange - const s = (t - 0.75) / 0.25; - r = Math.floor(148 + s * 30); - g = Math.floor(152 - s * 60); - b = Math.floor(25 - s * 10); - } - return `rgb(${r},${g},${b})`; -} - -// --- Tame URL param --- -const _urlParams = new URLSearchParams(window.location.search); -const tameLevel = parseFloat(_urlParams.get('tame') ?? '1'); -const spreadLevel = Math.max(0, Math.min(1, parseFloat(_urlParams.get('spread') ?? '0.6') || 0.6)); - -// --- State --- -let iml; -let visualizer; -let outputMode = 'visual'; -let noiseLevel = 0.05; -const rlExplorationDecay = 0.97; -let animating = true; - -// Joystick state -let joyX = 0.5, joyY = 0.5; -let joyDragging = false; - -// Follow mode -let followMode = false; - -// Gamepad -let gamepad; - -// Synth -let c15 = null; -let arpeggiator = null; -let midiInput = null; -let synthVisualizer = null; - -// Current param values (for dragging) -let paramValues = new Array(N_OUTPUTS).fill(0.5); - -// Current groups/names/colors based on output mode -let currentGroups = VISUAL_GROUPS; -let currentNames = VISUAL_PARAM_NAMES; -let currentColors = VISUAL_PARAM_COLORS; - -// DOM caches -let paramGroupsEl; -let groupElements = []; // [{el, bodyEl, indices, open}] - -// ============================ -// Pad preset outputs to N_OUTPUTS -// ============================ -function padPresetOutputs(outputs) { - const padded = new Array(N_OUTPUTS).fill(0.5); - for (let i = 0; i < outputs.length; i++) padded[i] = outputs[i]; - return padded; -} - -// ============================ -// Synth Visualizer (parameter landscape) -// ============================ -const SYNTH_SECTION_COLORS = [ - { start: 0, count: 7, color: '#4488ff', label: 'Env A' }, - { start: 7, count: 7, color: '#4488ff', label: 'Env B' }, - { start: 14, count: 6, color: '#4488ff', label: 'Env C' }, - { start: 20, count: 5, color: '#ff8844', label: 'Osc A' }, - { start: 25, count: 5, color: '#ff8844', label: 'Osc B' }, - { start: 30, count: 6, color: '#ff4466', label: 'Shp A' }, - { start: 36, count: 6, color: '#ff4466', label: 'Shp B' }, - { start: 42, count: 8, color: '#44ddaa', label: 'Comb' }, - { start: 50, count: 7, color: '#44ddaa', label: 'SVF' }, - { start: 57, count: 10, color: '#ddaa44', label: 'FB Mix' }, - { start: 67, count: 14, color: '#ddaa44', label: 'Out Mix' }, - { start: 81, count: 8, color: '#aa88dd', label: 'Cab' }, - { start: 89, count: 13, color: '#dd88aa', label: 'Flanger' }, - { start: 102, count: 7, color: '#88aadd', label: 'Echo' }, - { start: 109, count: 6, color: '#88ddaa', label: 'Reverb' }, - { start: 115, count: 11, color: '#999999', label: 'Uni/Mono' }, -]; - -class SynthVisualizer { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.params = new Array(N_SYNTH_OUTPUTS).fill(0.5); - this.displayParams = new Array(N_SYNTH_OUTPUTS).fill(0.5); - this._animId = null; - this._resize(); - window.addEventListener('resize', () => this._resize()); - } - - _resize() { - const rect = this.canvas.parentElement.getBoundingClientRect(); - this.canvas.width = rect.width * (window.devicePixelRatio || 1); - this.canvas.height = rect.height * (window.devicePixelRatio || 1); - } - - setParams(values) { - for (let i = 0; i < this.params.length && i < values.length; i++) { - this.params[i] = values[i]; - } - } - - draw() { - const ctx = this.ctx; - const w = this.canvas.width; - const h = this.canvas.height; - const n = N_SYNTH_OUTPUTS; - - // Lerp display toward target - for (let i = 0; i < n; i++) { - this.displayParams[i] += (this.params[i] - this.displayParams[i]) * 0.15; - } - - // Background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, w, h); - - const labelHeight = 28 * (window.devicePixelRatio || 1); - const topPad = 8 * (window.devicePixelRatio || 1); - const barAreaH = h - labelHeight - topPad; - const totalBarW = w / n; - const barW = Math.max(1, totalBarW * 0.75); - const gap = totalBarW - barW; - - // Draw section dividers and bars - let sectionIdx = 0; - for (const section of SYNTH_SECTION_COLORS) { - const sx = section.start * totalBarW; - const sw = section.count * totalBarW; - - // Section background - ctx.fillStyle = 'rgba(255,255,255,0.02)'; - if (sectionIdx % 2 === 0) { - ctx.fillRect(sx, topPad, sw, barAreaH); - } - - // Draw bars - for (let i = section.start; i < section.start + section.count && i < n; i++) { - const val = this.displayParams[i] || 0; - const x = i * totalBarW + gap / 2; - const barH = val * barAreaH; - const y = topPad + barAreaH - barH; - - ctx.fillStyle = section.color; - ctx.globalAlpha = 0.4 + val * 0.6; - ctx.fillRect(x, y, barW, barH); - ctx.globalAlpha = 1; - } - - // Section label - const fontSize = Math.max(7, Math.min(10, sw / (section.label.length * 0.7))); - ctx.fillStyle = section.color; - ctx.globalAlpha = 0.7; - ctx.font = `${fontSize * (window.devicePixelRatio || 1)}px monospace`; - ctx.textAlign = 'center'; - ctx.fillText(section.label, sx + sw / 2, h - 6 * (window.devicePixelRatio || 1)); - ctx.globalAlpha = 1; - - sectionIdx++; - } - - // Divider lines between sections - ctx.strokeStyle = 'rgba(255,255,255,0.08)'; - ctx.lineWidth = 1; - for (const section of SYNTH_SECTION_COLORS) { - if (section.start === 0) continue; - const x = section.start * totalBarW; - ctx.beginPath(); - ctx.moveTo(x, topPad); - ctx.lineTo(x, h - labelHeight); - ctx.stroke(); - } - } -} - -// ============================ -// Mapping Heatmap (merged joystick + mapping) -// ============================ -class MappingView { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.gridSize = 20; - this.dragging = false; - // Offscreen buffer for the heatmap so drawOverlay can re-composite cheaply - this._offscreen = document.createElement('canvas'); - this._offscreen.width = canvas.width; - this._offscreen.height = canvas.height; - this._offCtx = this._offscreen.getContext('2d'); - - // Pointer interaction for joystick input - canvas.addEventListener('pointerdown', (e) => this._onDown(e)); - canvas.addEventListener('pointermove', (e) => this._onMove(e)); - canvas.addEventListener('pointerup', (e) => this._onUp(e)); - canvas.addEventListener('pointerleave', (e) => this._onUp(e)); - canvas.addEventListener('pointercancel', (e) => this._onUp(e)); - canvas.addEventListener('dblclick', (e) => { - e.preventDefault(); - toggleFollowMode(); - }); - } - - _getPos(e) { - const rect = this.canvas.getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); - return [x, y]; - } - - _onDown(e) { - e.preventDefault(); - this.canvas.setPointerCapture(e.pointerId); - this.dragging = true; - joyDragging = true; - const [x, y] = this._getPos(e); - joyX = x; joyY = y; - onJoystickMove(); - this.drawOverlay(); - } - - _onMove(e) { - if (!this.dragging) return; - e.preventDefault(); - const [x, y] = this._getPos(e); - joyX = x; joyY = y; - onJoystickMove(); - this.drawOverlay(); - } - - _onUp(e) { - this.dragging = false; - joyDragging = false; - } - - update() { - const ctx = this._offCtx; - const w = this._offscreen.width; - const h = this._offscreen.height; - const gs = this.gridSize; - const cellW = w / gs; - const cellH = h / gs; - - // Sample grid through network - const savedInputs = [...iml.inputState]; - - for (let gy = 0; gy < gs; gy++) { - for (let gx = 0; gx < gs; gx++) { - const nx = (gx + 0.5) / gs; - const ny = (gy + 0.5) / gs; - - iml.setInput(0, nx); - iml.setInput(1, ny); - iml.inputUpdated = true; - iml.process(); - const out = iml.getOutputs(); - - let avg = 0; - for (let i = 0; i < out.length; i++) avg += out[i]; - avg /= out.length; - - let maxIdx = 0, maxVal = 0; - for (let i = 0; i < out.length; i++) { - if (out[i] > maxVal) { maxVal = out[i]; maxIdx = i; } - } - const hueInfluence = maxIdx / out.length; - - ctx.fillStyle = heatmapColor(avg * 0.8 + hueInfluence * 0.2); - ctx.fillRect(gx * cellW, gy * cellH, cellW + 0.5, cellH + 0.5); - } - } - - // Draw grid overlay every 5th cell - ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; - ctx.lineWidth = 0.5; - for (let i = 5; i < gs; i += 5) { - ctx.beginPath(); - ctx.moveTo(i * cellW, 0); - ctx.lineTo(i * cellW, h); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(0, i * cellH); - ctx.lineTo(w, i * cellH); - ctx.stroke(); - } - - // Restore inputs - iml.setInputs(savedInputs); - iml.inputUpdated = true; - iml.process(); - - this.drawOverlay(); - } - - drawOverlay() { - const ctx = this.ctx; - const w = this.canvas.width; - const h = this.canvas.height; - - // Blit cached heatmap then draw overlay on top - ctx.clearRect(0, 0, w, h); - ctx.drawImage(this._offscreen, 0, 0); - - // Training examples as bright dots - const features = iml.dataset.features; - if (features) { - for (let i = 0; i < features.length; i++) { - const f = features[i]; - if (!f || f.length < 2) continue; - const ex = f[0] * w; - const ey = f[1] * h; - ctx.fillStyle = '#fff'; - ctx.globalAlpha = 0.9; - ctx.beginPath(); - ctx.arc(ex, ey, 4, 0, Math.PI * 2); - ctx.fill(); - ctx.strokeStyle = '#000'; - ctx.lineWidth = 1; - ctx.stroke(); - ctx.globalAlpha = 1; - } - } - - // Current joystick position as crosshair - const px = joyX * w; - const py = joyY * h; - const accent = outputMode === 'synth' ? '#ff8c00' : '#ff6a00'; - - ctx.strokeStyle = accent; - ctx.lineWidth = 1; - ctx.globalAlpha = 0.7; - ctx.setLineDash([3, 3]); - ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, h); ctx.stroke(); - ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(w, py); ctx.stroke(); - ctx.setLineDash([]); - ctx.globalAlpha = 1; - - // Cursor dot - ctx.fillStyle = accent; - ctx.beginPath(); - ctx.arc(px, py, 6, 0, Math.PI * 2); - ctx.fill(); - ctx.strokeStyle = '#fff'; - ctx.lineWidth = 2; - ctx.stroke(); - - // Outer ring - ctx.strokeStyle = accent; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.arc(px, py, 10, 0, Math.PI * 2); - ctx.stroke(); - } -} - -// ============================ -// Constellation -// ============================ -function drawConstellation(canvas, values, nParams) { - const ctx = canvas.getContext('2d'); - const w = canvas.width; - const h = canvas.height; - const cx = w / 2; - const cy = h / 2; - const radius = Math.min(cx, cy) - 10; - - ctx.clearRect(0, 0, w, h); - - // Faint circle - ctx.strokeStyle = '#222'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.arc(cx, cy, radius, 0, Math.PI * 2); - ctx.stroke(); - - const n = nParams; - for (let i = 0; i < n; i++) { - const angle = (i / n) * Math.PI * 2 - Math.PI / 2; - const val = values[i] || 0; - const dotRadius = 2 + val * 4; - const x = cx + Math.cos(angle) * radius; - const y = cy + Math.sin(angle) * radius; - - const alpha = 0.2 + val * 0.8; - const color = currentColors[i % currentColors.length]; - - ctx.globalAlpha = alpha; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(x, y, dotRadius, 0, Math.PI * 2); - ctx.fill(); - } - ctx.globalAlpha = 1; -} - -// ============================ -// Loss Plot -// ============================ -function drawLossPlot(canvas, history) { - const ctx = canvas.getContext('2d'); - const w = canvas.width; - const h = canvas.height; - ctx.clearRect(0, 0, w, h); - - if (!history || history.length < 2) { - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, w, h); - ctx.fillStyle = '#333'; - ctx.font = '10px monospace'; - ctx.textAlign = 'center'; - ctx.fillText('No training data', w / 2, h / 2 + 3); - return; - } - - ctx.fillStyle = '#111'; - ctx.fillRect(0, 0, w, h); - - // Find range - let maxLoss = 0; - for (const v of history) if (v > maxLoss) maxLoss = v; - if (maxLoss === 0) maxLoss = 1; - - const accent = outputMode === 'synth' ? '#ff8c00' : '#ff6a00'; - - // Draw line - ctx.strokeStyle = accent; - ctx.lineWidth = 1.5; - ctx.beginPath(); - const step = w / (history.length - 1); - for (let i = 0; i < history.length; i++) { - const x = i * step; - const y = h - (history[i] / maxLoss) * (h - 4) - 2; - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - - // Fill under - ctx.lineTo(w, h); - ctx.lineTo(0, h); - ctx.closePath(); - ctx.fillStyle = accent.replace(')', ', 0.1)').replace('rgb', 'rgba').replace('#', ''); - // Use hex to rgba - ctx.globalAlpha = 0.15; - ctx.fillStyle = accent; - ctx.fill(); - ctx.globalAlpha = 1; -} - -// ============================ -// Param Groups UI -// ============================ -function buildParamGroups() { - paramGroupsEl.innerHTML = ''; - groupElements = []; - - for (const [name, colorVar, indices] of currentGroups) { - const group = document.createElement('div'); - group.className = 'wb-group'; - - // Resolve CSS variable for accent color - const accentColor = getComputedStyle(document.documentElement).getPropertyValue(colorVar).trim(); - - // Header - const header = document.createElement('div'); - header.className = 'wb-group-header'; - header.innerHTML = ` -
    - ${name} - - - `; - - // Body - const body = document.createElement('div'); - body.className = 'wb-group-body'; - const paramsDiv = document.createElement('div'); - paramsDiv.className = 'wb-group-params'; - - for (const idx of indices) { - const paramName = currentNames[idx] || `P${idx}`; - const color = currentColors[idx % currentColors.length]; - - const row = document.createElement('div'); - row.className = 'wb-param'; - row.innerHTML = ` - ${paramName} -
    -
    -
    - 0.50 - `; - paramsDiv.appendChild(row); - } - - body.appendChild(paramsDiv); - group.appendChild(header); - group.appendChild(body); - paramGroupsEl.appendChild(group); - - // Toggle collapse - let isOpen = false; - header.addEventListener('click', () => { - isOpen = !isOpen; - group.classList.toggle('open', isOpen); - }); - - // Dragging on param tracks - paramsDiv.addEventListener('pointerdown', (e) => { - const track = e.target.closest('.wb-param-track'); - if (!track) return; - e.preventDefault(); - track.setPointerCapture(e.pointerId); - const idx = parseInt(track.dataset.idx); - const setVal = (ev) => { - const rect = track.getBoundingClientRect(); - const v = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width)); - paramValues[idx] = v; - updateParamBar(track, v, idx); - // Push manually-set values to visualizer and synth in real time - routeOutputs(paramValues); - }; - setVal(e); - const onMove = (ev) => { ev.preventDefault(); setVal(ev); }; - const onUp = () => { - window.removeEventListener('pointermove', onMove); - window.removeEventListener('pointerup', onUp); - }; - window.addEventListener('pointermove', onMove); - window.addEventListener('pointerup', onUp); - }); - - groupElements.push({ el: group, bodyEl: paramsDiv, indices, sparklineCanvas: header.querySelector('.wb-group-sparkline') }); - } -} - -function updateParamBar(trackEl, value, idx) { - const fill = trackEl.querySelector('.wb-param-fill'); - if (fill) fill.style.width = (value * 100) + '%'; - // Update value text - const valEl = trackEl.parentElement.querySelector(`.wb-param-value[data-idx="${idx}"]`); - if (valEl) valEl.textContent = value.toFixed(2); -} - -function updateAllParamBars(values) { - paramValues = [...values]; - for (const ge of groupElements) { - let sum = 0; - for (const idx of ge.indices) { - const val = values[idx] || 0; - sum += val; - const track = ge.bodyEl.querySelector(`.wb-param-track[data-idx="${idx}"]`); - if (track) updateParamBar(track, val, idx); - } - - // Update sparkline - drawGroupSparkline(ge.sparklineCanvas, ge.indices, values); - } -} - -function drawGroupSparkline(canvas, indices, values) { - const ctx = canvas.getContext('2d'); - const w = canvas.width; - const h = canvas.height; - ctx.clearRect(0, 0, w, h); - - // Background - ctx.fillStyle = '#1a1a1a'; - ctx.fillRect(0, 0, w, h); - - const n = indices.length; - const barW = w / n; - for (let i = 0; i < n; i++) { - const val = values[indices[i]] || 0; - const barH = val * h; - const color = currentColors[indices[i] % currentColors.length]; - ctx.fillStyle = color; - ctx.globalAlpha = 0.7; - ctx.fillRect(i * barW, h - barH, barW - 0.5, barH); - } - ctx.globalAlpha = 1; -} - -// ============================ -// Routing -// ============================ -function routeOutputs(outputs) { - if (outputMode === 'visual') { - visualizer.setParams(outputs.slice(0, N_VISUAL_OUTPUTS)); - } else { - if (synthVisualizer) synthVisualizer.setParams(outputs); - if (c15 && c15.running) { - for (let i = 0; i < outputs.length && i < SYNTH_PARAM_MAP.length; i++) { - const tamed = applyTame(outputs[i], SYNTH_PARAM_MAP[i], tameLevel); - c15.setParameter(SYNTH_PARAM_MAP[i].id, tamed); - } - } - } -} - -// ============================ -// Application logic -// ============================ -let mappingView; -let lossCanvas; -let constellationCanvas; - -function onJoystickMove() { - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - - const outputs = iml.getOutputs(); - routeOutputs(outputs); - - updateAllParamBars(outputs); - - const constellationN = outputMode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - updateMetrics(); - - // Redraw mapping overlay (cursor position) - cheap - mappingView.drawOverlay(); -} - -function onAddExample() { - const inputs = [joyX, joyY]; - const outputs = [...paramValues]; - iml.addExample(inputs, outputs); - updateMetrics(); - mappingView.update(); - flash('btn-add'); -} - -function onTrain() { - const loss = trainModel(); - if (loss !== null) { - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateAllParamBars(outputs); - const constellationN = outputMode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - drawLossPlot(lossCanvas, iml.lossHistory); - mappingView.update(); - updateMetrics(); - flash('btn-train'); - } -} - -function onRandomize() { - iml.randomiseWeights(spreadLevel); - iml.setInput(0, joyX); - iml.setInput(1, joyY); - iml.process(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateAllParamBars(outputs); - const constellationN = outputMode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - noiseLevel = 0.05; - mappingView.update(); - updateMetrics(); -} - -function onClearExamples() { - iml.clearDataset(); - mappingView.update(); - updateMetrics(); -} - -function onClear() { - iml.clearDataset(); - iml.lossHistory = []; - iml.bestLoss = null; - iml.totalTrainingIterations = 0; - noiseLevel = 0.05; - clearState(); - drawLossPlot(lossCanvas, []); - mappingView.update(); - updateMetrics(); -} - -function onThumbsUp() { - const inputs = [joyX, joyY]; - const outputs = [...iml.getOutputs()]; - iml.addExample(inputs, outputs); - trainModel(); - const trainedOutputs = iml.getOutputs(); - routeOutputs(trainedOutputs); - updateAllParamBars(trainedOutputs); - noiseLevel *= rlExplorationDecay; - noiseLevel = Math.max(noiseLevel, 0.005); - drawLossPlot(lossCanvas, iml.lossHistory); - mappingView.update(); - updateMetrics(); - flash('btn-thumbsup'); -} - -function onThumbsDown() { - const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel; - noiseLevel = Math.min(noiseLevel * 1.5, noiseCap); - iml.moveWeights(noiseLevel, spreadLevel); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateAllParamBars(outputs); - const constellationN = outputMode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - mappingView.update(); - updateMetrics(); - flash('btn-thumbsdown'); -} - -function trainModel() { - let lastPlotUpdate = 0; - const loss = iml.train({ - onIteration: (iter, iterLoss) => { - if (iter - lastPlotUpdate < 8) return; - lastPlotUpdate = iter; - drawLossPlot(lossCanvas, [...iml.lossHistory, iterLoss]); - }, - }); - return loss; -} - -function updateMetrics() { - const el = (id) => document.getElementById(id); - el('metric-examples').textContent = iml.exampleCount; - el('metric-loss').textContent = iml.lastLoss !== null ? iml.lastLoss.toFixed(6) : '\u2014'; - el('metric-noise').textContent = noiseLevel.toFixed(3); - el('metric-joy').textContent = `${joyX.toFixed(2)}, ${joyY.toFixed(2)}`; -} - -function flash(id) { - const el = document.getElementById(id); - if (!el) return; - el.classList.add('flash'); - setTimeout(() => el.classList.remove('flash'), 200); -} - -// ============================ -// Toast notifications -// ============================ -function showToast(message) { - const toast = document.getElementById('toast'); - if (!toast) return; - toast.textContent = message; - toast.classList.remove('hidden', 'fade-out'); - // Clear any previous timeout - if (toast._timeout) clearTimeout(toast._timeout); - if (toast._fadeTimeout) clearTimeout(toast._fadeTimeout); - toast._timeout = setTimeout(() => { - toast.classList.add('fade-out'); - toast._fadeTimeout = setTimeout(() => { - toast.classList.add('hidden'); - toast.classList.remove('fade-out'); - }, 500); - }, 1500); -} - -// ============================ -// Follow mode -// ============================ -function toggleFollowMode() { - followMode = !followMode; - const badge = document.getElementById('follow-badge'); - const toggleBtn = document.getElementById('follow-toggle-btn'); - const panel = document.querySelector('.wb-mapping-panel'); - - if (followMode) { - badge.classList.remove('hidden'); - toggleBtn.classList.add('active'); - panel.classList.add('follow-active'); - showToast('Follow mode ON'); - } else { - badge.classList.add('hidden'); - toggleBtn.classList.remove('active'); - panel.classList.remove('follow-active'); - showToast('Follow mode OFF'); - } -} - -function onFollowPointerMove(e) { - if (!followMode) return; - // Map screen coordinates to 0-1 - const x = Math.max(0, Math.min(1, e.clientX / window.innerWidth)); - const y = Math.max(0, Math.min(1, e.clientY / window.innerHeight)); - joyX = x; - joyY = y; - onJoystickMove(); -} - -// ============================ -// Output mode switching -// ============================ -function setOutputMode(mode) { - outputMode = mode; - const badge = document.getElementById('mode-badge'); - const synthControls = document.getElementById('synth-controls'); - const presetsVisual = document.getElementById('presets-visual'); - const label = document.getElementById('constellation-label'); - const visualCanvas = document.getElementById('visual-canvas'); - const synthVisCanvas = document.getElementById('synth-vis-canvas'); - - if (mode === 'synth') { - document.body.classList.add('synth-mode'); - badge.textContent = 'Synth'; - badge.classList.add('synth'); - synthControls.classList.remove('hidden'); - if (presetsVisual) presetsVisual.style.display = 'none'; - currentGroups = SYNTH_GROUPS; - currentNames = SYNTH_PARAM_NAMES; - currentColors = SYNTH_PARAM_COLORS; - label.textContent = `${N_SYNTH_OUTPUTS} synth parameters`; - // Show synth canvas, hide visual canvas - visualCanvas.classList.add('hidden'); - synthVisCanvas.classList.remove('hidden'); - // Pulse Start Audio button if audio not yet started - const startBtn = document.getElementById('synth-start'); - if (startBtn) startBtn.classList.toggle('audio-needs-init', !(c15 && c15.running)); - } else { - document.body.classList.remove('synth-mode'); - badge.textContent = 'Visual'; - badge.classList.remove('synth'); - synthControls.classList.add('hidden'); - if (presetsVisual) presetsVisual.style.display = ''; - currentGroups = VISUAL_GROUPS; - currentNames = VISUAL_PARAM_NAMES; - currentColors = VISUAL_PARAM_COLORS; - label.textContent = `${N_VISUAL_OUTPUTS} parameters`; - // Show visual canvas, hide synth canvas - visualCanvas.classList.remove('hidden'); - synthVisCanvas.classList.add('hidden'); - } - - // Toggle button state - document.getElementById('toggle-visual').classList.toggle('active', mode === 'visual'); - document.getElementById('toggle-synth').classList.toggle('active', mode === 'synth'); - - // Resize the now-visible canvas so it picks up correct dimensions - if (mode === 'visual') { - requestAnimationFrame(() => { - visualizer.resize(); - visualizer.initParticles(); - }); - } else if (synthVisualizer) { - requestAnimationFrame(() => synthVisualizer._resize()); - } - - buildParamGroups(); - const outputs = iml.getOutputs(); - updateAllParamBars(outputs); - routeOutputs(outputs); - const constellationN = mode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - mappingView.update(); -} - -// ============================ -// Presets -// ============================ -function loadPreset(name) { - iml.clearDataset(); - - if (name === 'calm-to-chaotic') { - iml.addExample([0.1, 0.9], padPresetOutputs([0.25, 0.3, 0.1, 0.55, 0.2, 0.3, 0.02, 0.05, 0.9, 0.45, 0.25, 0.2, 0.9, 0.0, 0.0, 0.2, 0.05, 0.0, 0.0, 0.2])); - iml.addExample([0.9, 0.1], padPresetOutputs([0.75, 0.7, 0.9, 0.05, 0.8, 0.7, 0.9, 0.95, 0.3, 0.2, 0.85, 0.7, 0.25, 0.55, 1.0, 0.92, 0.02, 0.95, 0.8, 0.85])); - iml.addExample([0.5, 0.5], padPresetOutputs([0.5, 0.5, 0.5, 0.3, 0.5, 0.5, 0.4, 0.5, 0.7, 0.6, 0.5, 0.45, 0.55, 0.9, 0.5, 0.65, 0.08, 0.45, 0.4, 0.5])); - } else if (name === 'rainbow-sweep') { - iml.addExample([0.0, 0.5], padPresetOutputs([0.5, 0.5, 0.4, 0.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.4, 0.3, 0.8, 0.0, 0.0, 0.45, 0.08, 0.2, 0.25, 0.45])); - iml.addExample([0.5, 0.5], padPresetOutputs([0.5, 0.5, 0.4, 0.5, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.55, 0.35, 0.7, 0.0, 0.4, 0.45, 0.08, 0.45, 0.4, 0.6])); - iml.addExample([1.0, 0.5], padPresetOutputs([0.5, 0.5, 0.4, 1.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.75, 0.45, 0.6, 0.0, 0.8, 0.45, 0.08, 0.7, 0.55, 0.75])); - } else if (name === 'vortex') { - iml.addExample([0.5, 0.5], padPresetOutputs([0.0, 0.8, 0.8, 0.6, 0.1, 0.15, 0.02, 1.0, 1.0, 0.3, 0.95, 0.85, 0.25, 1.0, 0.5, 0.95, 0.01, 1.0, 1.0, 1.0])); - iml.addExample([0.0, 0.0], padPresetOutputs([0.5, 0.2, 0.3, 0.8, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.2, 0.35, 0.2, 0.15, 0.2, 0.25])); - iml.addExample([1.0, 1.0], padPresetOutputs([0.5, 0.2, 0.3, 0.2, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.8, 0.35, 0.2, 0.15, 0.2, 0.25])); - iml.addExample([0.0, 1.0], padPresetOutputs([0.3, 0.4, 0.5, 0.4, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.4, 0.7, 0.1, 0.5, 0.4, 0.55])); - iml.addExample([1.0, 0.0], padPresetOutputs([0.7, 0.4, 0.5, 0.0, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.9, 0.7, 0.1, 0.5, 0.4, 0.55])); - } - - const loss = trainModel(); - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateAllParamBars(outputs); - const constellationN = outputMode === 'synth' ? N_SYNTH_OUTPUTS : N_VISUAL_OUTPUTS; - drawConstellation(constellationCanvas, outputs, constellationN); - drawLossPlot(lossCanvas, iml.lossHistory); - mappingView.update(); - updateMetrics(); -} - -// ============================ -// Synth controls -// ============================ -function initSynthControls() { - const startBtn = document.getElementById('synth-start'); - const volumeSlider = document.getElementById('synth-volume'); - const arpToggleBtn = document.getElementById('arp-toggle'); - const arpProgression = document.getElementById('arp-progression'); - const arpTempo = document.getElementById('arp-tempo'); - const arpOctaves = document.getElementById('arp-octaves'); - const arpOffset = document.getElementById('arp-offset'); - - startBtn.addEventListener('click', async () => { - if (c15.running) { - arpeggiator.stop(); - arpToggleBtn.textContent = 'Arp: Play'; - await c15.stop(); - startBtn.textContent = 'Start Audio'; - startBtn.classList.add('audio-needs-init'); - } else { - await c15.start(); - startBtn.textContent = 'Stop Audio'; - startBtn.classList.remove('audio-needs-init'); - routeOutputs(iml.getOutputs()); - } - }); - - volumeSlider.addEventListener('input', (e) => { - c15.setMasterVolume(parseFloat(e.target.value)); - }); - - arpToggleBtn.addEventListener('click', () => { - if (!c15.running) return; - if (arpeggiator.playing) { - arpeggiator.stop(); - arpToggleBtn.textContent = 'Arp: Play'; - } else { - arpeggiator.start(); - arpToggleBtn.textContent = 'Arp: Stop'; - } - }); - - arpProgression.addEventListener('change', (e) => { - arpeggiator.progression = e.target.value; - }); - - arpTempo.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.bpm = val; - document.getElementById('tempo-val').textContent = val; - }); - - arpOctaves.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.octaves = val; - document.getElementById('octaves-val').textContent = val; - }); - - arpOffset.addEventListener('input', (e) => { - const val = parseInt(e.target.value); - arpeggiator.octaveOffset = val; - document.getElementById('offset-val').textContent = val; - }); -} - -// ============================ -// localStorage persistence -// ============================ -function saveState() { - try { - const features = iml.dataset.features; - const labels = iml.dataset.labels; - if (!features || features.length === 0) return; - const data = { features, labels }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); - } catch (e) { - console.warn('[NISPS] saveState failed:', e); - } -} - -// ---- MIDI Input ---- -async function initMIDIControls() { - const row = document.getElementById('midi-row'); - const statusRow = document.getElementById('midi-status-row'); - const toggle = document.getElementById('midi-toggle'); - const select = document.getElementById('midi-select'); - const statusEl = document.getElementById('midi-status'); - if (!row || !toggle || !select) return; - - const available = await midiInput.init(); - if (!available) return; - - row.style.display = ''; - - function populateInputs() { - const inputs = midiInput.getInputs(); - select.innerHTML = '' + - inputs.map(i => ``).join(''); - } - - populateInputs(); - midiInput.onInputsChange = () => populateInputs(); - - midiInput.onStatusChange = (msg) => { - if (statusEl) { - statusEl.textContent = msg; - statusRow.style.display = ''; - } - }; - - midiInput.onCC = (cc, value) => { - if (cc === 1) { joyX = value; onJoystickMove(); } - if (cc === 2) { joyY = value; onJoystickMove(); } - }; - - toggle.addEventListener('click', () => { - if (!c15.running) return; - midiInput.toggle(); - toggle.textContent = midiInput.enabled ? 'MIDI: On' : 'Enable'; - toggle.classList.toggle('wb-btn-primary', midiInput.enabled); - }); - - select.addEventListener('change', (e) => { - midiInput.selectInput(e.target.value || null); - }); -} - -function loadState() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return false; - const data = JSON.parse(raw); - if (!data.features || !data.labels || data.features.length === 0) return false; - for (let i = 0; i < data.features.length; i++) { - // Pad old saves (20 outputs) to current N_OUTPUTS with 0.5 defaults - let labels = data.labels[i]; - if (labels.length < N_OUTPUTS) { - labels = padPresetOutputs(labels); - } - iml.addExample(data.features[i], labels); - } - trainModel(); - routeOutputs(iml.getOutputs()); - return true; - } catch (e) { - console.warn('[NISPS] loadState failed:', e); - return false; - } -} - -function clearState() { - try { - localStorage.removeItem(STORAGE_KEY); - } catch (e) { - console.warn('[NISPS] clearState failed:', e); - } -} - -// ============================ -// Keyboard shortcuts -// ============================ -window.addEventListener('keydown', (e) => { - if (e.repeat) return; - const tag = e.target.tagName; - if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return; - // RL shortcuts: 1=negative, 2=positive - if (e.key === '1' || e.code === 'Numpad1') { e.preventDefault(); onThumbsDown(); } - if (e.key === '2' || e.code === 'Numpad2') { e.preventDefault(); onThumbsUp(); } - // Follow mode toggle: f - if (e.key === 'f' || e.key === 'F') { - e.preventDefault(); - toggleFollowMode(); - } -}); - -// ============================ -// Animation -// ============================ -function animate() { - if (!animating) return; - if (gamepad) gamepad.poll(); - if (outputMode === 'visual') { - visualizer.draw(); - } else if (synthVisualizer) { - synthVisualizer.draw(); - } - requestAnimationFrame(animate); -} - -// ============================ -// Init -// ============================ -function init() { - // IML — random weights, then restore saved data if available - iml = new IML(N_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); - iml.setLogger(msg => console.log('[NISPS]', msg)); - - // Flow field visualizer - const visCanvas = document.getElementById('visual-canvas'); - visualizer = new FlowFieldVisualizer(visCanvas); - - // Synth parameter landscape visualizer - const synthVisCanvas = document.getElementById('synth-vis-canvas'); - synthVisualizer = new SynthVisualizer(synthVisCanvas); - - // Mapping view (merged joystick + heatmap) - mappingView = new MappingView(document.getElementById('mapping-canvas')); - - // Canvases - lossCanvas = document.getElementById('loss-canvas'); - constellationCanvas = document.getElementById('constellation-canvas'); - - // Param groups - paramGroupsEl = document.getElementById('param-groups'); - buildParamGroups(); - - // Controls wiring - document.getElementById('btn-add').addEventListener('click', onAddExample); - document.getElementById('btn-train').addEventListener('click', onTrain); - document.getElementById('btn-randomize').addEventListener('click', onRandomize); - document.getElementById('btn-clear').addEventListener('click', onClear); - document.getElementById('btn-clear-examples').addEventListener('click', onClearExamples); - document.getElementById('btn-thumbsup').addEventListener('click', onThumbsUp); - document.getElementById('btn-thumbsdown').addEventListener('click', onThumbsDown); - - // Output mode - document.getElementById('toggle-visual').addEventListener('click', () => setOutputMode('visual')); - document.getElementById('toggle-synth').addEventListener('click', () => setOutputMode('synth')); - - // Follow mode toggle button - document.getElementById('follow-toggle-btn').addEventListener('click', () => toggleFollowMode()); - - // Follow mode: global pointer move - window.addEventListener('pointermove', onFollowPointerMove); - - // Presets - document.querySelectorAll('.preset-pill[data-preset]').forEach(btn => { - btn.addEventListener('click', () => loadPreset(btn.dataset.preset)); - }); - - // Help - const helpBtn = document.getElementById('help-btn'); - const helpOverlay = document.getElementById('help-overlay'); - if (helpBtn && helpOverlay) { - helpBtn.addEventListener('click', () => helpOverlay.classList.toggle('hidden')); - helpOverlay.addEventListener('click', () => helpOverlay.classList.add('hidden')); - } - - // Synth - c15 = new C15Bridge(); - c15.onStatusChange = (msg) => { - const el = document.getElementById('synth-status'); - if (el) el.textContent = msg; - }; - c15.loadParams(); - arpeggiator = new Arpeggiator(c15); - midiInput = new MIDIInput(c15); - initSynthControls(); - initMIDIControls(); - - // Gamepad - gamepad = new GamepadInput({ - onMove: (x, y) => { - joyX = x; - joyY = y; - onJoystickMove(); - mappingView.drawOverlay(); - }, - onButton: (btn) => { - if (btn === 'lb') onThumbsDown(); - if (btn === 'rb') onThumbsUp(); - if (btn === 'a') onTrain(); - if (btn === 'x') onRandomize(); - if (btn === 'b') onClearExamples(); - }, - onConnectionChange: (connected) => { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = connected ? 'Gamepad connected' : 'Press any button to activate gamepad'; - }, - }); - - if (!gamepad.connected) { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = 'Press any gamepad button to connect'; - } - - // Resize - window.addEventListener('resize', () => { - visualizer.resize(); - visualizer.initParticles(); - if (synthVisualizer) synthVisualizer._resize(); - }); - - // Restore saved data if available - const restored = loadState(); - - // Initial inference - iml.setInput(0, 0.5); - iml.setInput(1, 0.5); - iml.process(); - const outputs = iml.getOutputs(); - visualizer.setParams(outputs.slice(0, N_VISUAL_OUTPUTS)); - updateAllParamBars(outputs); - drawConstellation(constellationCanvas, outputs, N_VISUAL_OUTPUTS); - drawLossPlot(lossCanvas, restored ? iml.lossHistory : []); - mappingView.update(); - updateMetrics(); - - // Auto-save every 10 seconds - setInterval(saveState, 10000); - - // Start animation - animate(); -} - -document.addEventListener('DOMContentLoaded', init); diff --git a/playground/js/c-app.js b/playground/js/c-app.js deleted file mode 100644 index 8c42b52..0000000 --- a/playground/js/c-app.js +++ /dev/null @@ -1,1244 +0,0 @@ -// NISPS Journey — Design C -// Phases that dissolve: Explore -> Teach -> Perform - -import { IML } from './nisps/iml.js'; -import { FlowFieldVisualizer } from './ui/visualizer.js'; -import { C15Bridge } from './synth/c15-bridge.js'; -import { Arpeggiator } from './synth/arpeggiator.js'; -import { MIDIInput } from './synth/midi-input.js'; -import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyTame } from './synth/param-map.js'; -import { GamepadInput } from './ui/gamepad.js'; - -// ============================================================ -// Constants -// ============================================================ -const N_INPUTS = 2; -const N_VISUAL_OUTPUTS = 20; -const N_SYNTH_OUTPUTS = SYNTH_PARAM_MAP.length; // 126 -const N_OUTPUTS = N_SYNTH_OUTPUTS; -const STORAGE_KEY = 'nisps-c-journey'; -const IDLE_TIMEOUT = 5000; -const TWO_PI = Math.PI * 2; - -const VISUAL_PARAM_NAMES = [ - 'Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', - 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', - 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate', -]; -const VISUAL_PARAM_COLORS = [ - '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', - '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', - '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', - '#ffc15f', '#ff8a3d', -]; - -// 6 preset output arrays — each produces a distinct visual state -const PRESETS = [ - { name: 'Calm', synth: 'Soft Pad', - outputs: [0.1, 0.3, 0.15, 0.55, 0.2, 0.35, 0.02, 0.1, 0.5, 0.5, 0.3, 0.1, 0.8, 0.0, 0.0, 0.7, 0.05, 0.0, 0.0, 0.2] }, - { name: 'Storm', synth: 'Bright FM', - outputs: [0.7, 0.7, 0.9, 0.1, 0.9, 0.6, 0.12, 0.95, 1.0, 0.8, 0.8, 0.7, 0.3, 0.33, 0.15, 0.3, 0.15, 0.6, 0.4, 0.9] }, - { name: 'Spiral', synth: 'Resonant', - outputs: [0.5, 0.5, 0.5, 0.3, 0.4, 0.3, 0.04, 0.3, 0.9, 0.6, 0.5, 0.3, 0.6, 0.0, 0.5, 0.6, 0.08, 0.0, 0.0, 0.5] }, - { name: 'Pulse', synth: 'Pluck', - outputs: [0.0, 0.4, 0.7, 0.8, 0.5, 0.5, 0.08, 0.5, 0.3, 0.3, 4.0/8.2, 1.0, 0.15, 0.67, 0.3, 0.1, 0.02, 0.2, 0.2, 0.3] }, - { name: 'Drift', synth: 'Dark Pad', - outputs: [0.2, 0.2, 0.1, 0.7, 0.15, 0.2, 0.015, 0.05, 0.4, 0.9, 0.15, 0.05, 0.95, 0.0, 0.05, 0.95, 0.01, 0.0, 0.0, 0.1] }, - { name: 'Swarm', synth: 'Metallic', - outputs: [0.9, 0.8, 0.8, 0.45, 0.7, 0.2, 0.1, 0.8, 0.6, 0.4, 0.6, 0.5, 0.2, 0.33, 0.2, 0.15, 0.2, 0.9, 0.9, 0.95] }, -]; - -// --- Tame URL param --- -const _urlParams = new URLSearchParams(window.location.search); -const tameLevel = parseFloat(_urlParams.get('tame') ?? '1'); -const spreadLevel = Math.max(0, Math.min(1, parseFloat(_urlParams.get('spread') ?? '0.6') || 0.6)); - -// ============================================================ -// State -// ============================================================ -let iml; -let visualizer; -let c15 = null; -let arpeggiator = null; -let midiInput = null; - -let phase = 'explore'; // 'explore' | 'teach' | 'perform' -let outputMode = 'visual'; // 'visual' | 'synth' -// teachMode removed — Examples and RL controls are always visible together -let selectedPreset = -1; -let noiseLevel = 0.05; -let animating = true; -let joystickX = 0.5; -let joystickY = 0.5; -let joystickActive = false; -let joystickInteractionTime = 0; -let gamepad; -let ctasShown = false; -// rlHintShown removed — no separate RL mode hint needed -let idleTimer = null; -let currentParamNames = VISUAL_PARAM_NAMES; -let currentParamColors = VISUAL_PARAM_COLORS; - -// Synth visualizer -let synthVisualizer = null; - -// Follow mode -let followMode = false; - -// Joystick internal state -let joyDragging = false; -let joyKnobX = 0.5; -let joyKnobY = 0.5; - -// DOM refs -let app, visCanvas; - -// ============================================================ -// Synth sections for visualizer -// ============================================================ -const SYNTH_SECTIONS = [ - { name: 'Env A', count: 7, color: '#4488ff' }, - { name: 'Env B', count: 7, color: '#4488ff' }, - { name: 'Env C', count: 6, color: '#6688dd' }, - { name: 'Osc A', count: 5, color: '#ff8844' }, - { name: 'Osc B', count: 5, color: '#ff8844' }, - { name: 'Shp A', count: 6, color: '#ff4466' }, - { name: 'Shp B', count: 6, color: '#ff4466' }, - { name: 'Comb', count: 8, color: '#44ddaa' }, - { name: 'SVF', count: 7, color: '#44ddaa' }, - { name: 'FB Mix', count: 10, color: '#ddaa44' }, - { name: 'Out Mix', count: 14, color: '#ddaa44' }, - { name: 'Cabinet', count: 8, color: '#aa88dd' }, - { name: 'Flanger', count: 13, color: '#dd88aa' }, - { name: 'Echo', count: 7, color: '#88aadd' }, - { name: 'Reverb', count: 6, color: '#88ddaa' }, - { name: 'Unison', count: 3, color: '#cccccc' }, - { name: 'Mono', count: 1, color: '#999999' }, -]; - -// ============================================================ -// Pad outputs helper -// ============================================================ -function padOutputs(arr) { - const padded = new Array(N_OUTPUTS).fill(0.5); - for (let i = 0; i < arr.length; i++) padded[i] = arr[i]; - return padded; -} - -// ============================================================ -// SynthVisualizer — parameter landscape -// ============================================================ -class SynthVisualizer { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.targets = new Array(N_SYNTH_OUTPUTS).fill(0.5); - this.current = new Array(N_SYNTH_OUTPUTS).fill(0.5); - this.resize(); - } - - resize() { - const dpr = window.devicePixelRatio || 1; - const rect = this.canvas.getBoundingClientRect(); - this.canvas.width = rect.width * dpr; - this.canvas.height = rect.height * dpr; - this.ctx.scale(dpr, dpr); - this.w = rect.width; - this.h = rect.height; - } - - setParams(outputs) { - for (let i = 0; i < outputs.length && i < this.targets.length; i++) { - this.targets[i] = Math.max(0, Math.min(1, outputs[i])); - } - } - - draw() { - const { ctx, w, h, current, targets } = this; - const dpr = window.devicePixelRatio || 1; - ctx.save(); - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - - // Clear - ctx.fillStyle = '#0d0d0d'; - ctx.fillRect(0, 0, w, h); - - // Lerp current toward targets - for (let i = 0; i < current.length; i++) { - current[i] += (targets[i] - current[i]) * 0.12; - } - - const totalParams = N_SYNTH_OUTPUTS; - const margin = 40; - const bottomMargin = 24; - const topMargin = 12; - const barAreaW = w - margin * 2; - const barAreaH = h - topMargin - bottomMargin; - const gapBetweenSections = 2; - const totalGaps = (SYNTH_SECTIONS.length - 1) * gapBetweenSections; - const barWidth = Math.max(1, (barAreaW - totalGaps) / totalParams); - - let x = margin; - let paramIdx = 0; - - for (const section of SYNTH_SECTIONS) { - // Section label - const sectionWidth = barWidth * section.count; - ctx.fillStyle = 'rgba(255, 255, 255, 0.25)'; - ctx.font = '9px 'JetBrains Mono', monospace'; - ctx.textAlign = 'center'; - ctx.fillText(section.name, x + sectionWidth / 2, h - 6); - - // Section divider line at top - ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; - ctx.lineWidth = 0.5; - ctx.beginPath(); - ctx.moveTo(x, topMargin); - ctx.lineTo(x, h - bottomMargin); - ctx.stroke(); - - // Draw bars - for (let j = 0; j < section.count && paramIdx < totalParams; j++, paramIdx++) { - const val = current[paramIdx]; - const barH = val * barAreaH; - const barY = topMargin + barAreaH - barH; - - // Bar fill - ctx.fillStyle = section.color; - ctx.globalAlpha = 0.6 + val * 0.4; - ctx.fillRect(x, barY, Math.max(1, barWidth - 0.5), barH); - ctx.globalAlpha = 1; - - x += barWidth; - } - x += gapBetweenSections; - } - - ctx.restore(); - } -} - -// ============================================================ -// Init -// ============================================================ -function init() { - app = document.getElementById('app'); - visCanvas = document.getElementById('vis-canvas'); - - iml = new IML(N_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); - iml.setLogger(msg => console.log('[NISPS]', msg)); - - visualizer = new FlowFieldVisualizer(visCanvas); - - // Synth visualizer - const synthVisCanvas = document.getElementById('synth-vis-canvas'); - synthVisualizer = new SynthVisualizer(synthVisCanvas); - - // Synth - c15 = new C15Bridge(); - c15.onStatusChange = msg => { - const el = document.getElementById('synth-status'); - if (el) el.textContent = msg; - }; - c15.loadParams(); - arpeggiator = new Arpeggiator(c15); - midiInput = new MIDIInput(c15); - initMIDIControls(); - - // Build heatmap cells - buildHeatmap(); - - // Build param sliders - buildParamSliders(); - - // Render preset thumbnails - renderPresetThumbnails(); - - // Wire events - wirePhaseNav(); - wireModeToggle(); - wirePresetGrid(); - wireTeachActions(); - wireRLButtons(); - wireSynthControls(); - wirePerformUI(); - wireJoystick(); - wireHeatmapTooltip(); - - // Keyboard shortcuts for RL (work in all phases) - window.addEventListener('keydown', (e) => { - if (e.repeat) return; - const tag = e.target.tagName; - if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return; - if (e.key === '1' || e.code === 'Numpad1') { - e.preventDefault(); - onThumbsDown(); - } - if (e.key === '2' || e.code === 'Numpad2') { - e.preventDefault(); - onThumbsUp(); - } - }); - - // Gamepad - gamepad = new GamepadInput({ - onMove: (x, y) => { - updateJoystick(x, y); - drawJoystick(); - }, - onButton: (btn) => { - if (btn === 'lb') onThumbsDown(); - if (btn === 'rb') onThumbsUp(); - }, - onConnectionChange: (connected) => { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = connected ? 'Gamepad connected' : 'Press any button to activate gamepad'; - }, - }); - - if (!gamepad.connected) { - const el = document.getElementById('gamepad-status'); - if (el) el.textContent = 'Press any gamepad button to connect'; - } - - // Follow mode wiring - wireFollowMode(); - - // Resize - window.addEventListener('resize', onResize); - - // Try to load saved state - const loaded = loadState(); - if (loaded) { - setPhase('perform'); - } else { - setPhase('explore'); - } - - // Initial inference - iml.setInput(0, 0.5); - iml.setInput(1, 0.5); - iml.process(); - routeOutputs(iml.getOutputs()); - - // Start animation - animate(); -} - -// ============================================================ -// Phase management -// ============================================================ -function setPhase(p) { - phase = p; - app.className = `app phase-${p}`; - delete app.dataset.teachMode; - - // Update dots - document.querySelectorAll('.phase-dot').forEach(dot => { - dot.classList.toggle('active', dot.dataset.phase === p); - }); - - // Phase-specific setup - if (p === 'explore') { - ctasShown = false; - joystickInteractionTime = 0; - const prompt = document.getElementById('prompt-text'); - if (prompt) prompt.textContent = 'Move the joystick to explore'; - document.getElementById('explore-ctas')?.classList.add('hidden'); - } - - if (p === 'teach') { - updateTeachStatus(); - } - - if (p === 'perform') { - resetIdleTimer(); - // Show audio control if synth mode - const audioCtrl = document.getElementById('audio-control'); - if (audioCtrl) { - audioCtrl.classList.toggle('hidden', outputMode !== 'synth'); - } - } - - // Resize visualizer for canvas height change - requestAnimationFrame(() => { - setTimeout(() => onResize(), 450); // after CSS transition - }); -} - -function wirePhaseNav() { - document.querySelectorAll('.phase-dot').forEach(dot => { - dot.addEventListener('click', () => { - const target = dot.dataset.phase; - if (target === 'teach' || target === 'perform') { - setPhase(target); - } else { - setPhase('explore'); - } - }); - }); - - // Explore CTAs - document.getElementById('cta-teach')?.addEventListener('click', () => { - setPhase('teach'); - }); - - document.getElementById('cta-preset')?.addEventListener('click', () => { - // Load a preset, add examples at corners, train, go to perform - loadPresetAndPerform(0); - }); -} - -function loadPresetAndPerform(presetIdx) { - const preset = PRESETS[presetIdx]; - // Add examples at 4 corners + center with this preset's outputs - const positions = [[0.15, 0.15], [0.85, 0.15], [0.15, 0.85], [0.85, 0.85], [0.5, 0.5]]; - iml.clearDataset(); - const paddedOutputs = padOutputs(preset.outputs); - for (const [x, y] of positions) { - iml.addExample([x, y], paddedOutputs); - } - iml.train(); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - saveState(); - setPhase('perform'); -} - -// ============================================================ -// Output routing -// ============================================================ -function routeOutputs(outputs) { - if (outputMode === 'visual') { - visualizer.setParams(outputs.slice(0, N_VISUAL_OUTPUTS)); - } else { - synthVisualizer.setParams(outputs); - } - updateHeatmap(outputs); - - if (outputMode === 'synth' && c15 && c15.running) { - for (let i = 0; i < outputs.length && i < SYNTH_PARAM_MAP.length; i++) { - const tamed = applyTame(outputs[i], SYNTH_PARAM_MAP[i], tameLevel); - c15.setParameter(SYNTH_PARAM_MAP[i].id, tamed); - } - } -} - -// ============================================================ -// Heatmap strip -// ============================================================ -function buildHeatmap() { - const container = document.getElementById('heatmap-cells'); - container.innerHTML = ''; - const count = currentParamNames.length; - for (let i = 0; i < count; i++) { - const cell = document.createElement('div'); - cell.className = 'heatmap-cell'; - cell.dataset.index = i; - const bar = document.createElement('div'); - bar.className = 'heatmap-bar'; - bar.style.backgroundColor = currentParamColors[i] || '#ff6a00'; - bar.style.width = '50%'; - cell.appendChild(bar); - container.appendChild(cell); - } - const strip = document.getElementById('heatmap-strip'); - strip.classList.toggle('wide', count > 30); - document.getElementById('heatmap-count').textContent = String(count); -} - -function updateHeatmap(outputs) { - const bars = document.querySelectorAll('.heatmap-bar'); - for (let i = 0; i < bars.length && i < outputs.length; i++) { - bars[i].style.width = (Math.max(0, Math.min(1, outputs[i])) * 100) + '%'; - } -} - -function adjustBrightness(hex, brightness) { - // Parse hex to RGB, then create HSL-ish brightness adjustment - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - const factor = brightness / 100; - return `rgb(${Math.round(r * factor)}, ${Math.round(g * factor)}, ${Math.round(b * factor)})`; -} - -function wireHeatmapTooltip() { - const strip = document.getElementById('heatmap-strip'); - const tooltip = document.getElementById('heatmap-tooltip'); - - strip.addEventListener('pointerover', (e) => { - const cell = e.target.closest('.heatmap-cell'); - if (!cell) { tooltip.classList.remove('show'); return; } - const idx = parseInt(cell.dataset.index); - const outputs = iml.getOutputs(); - const name = currentParamNames[idx] || `Param ${idx}`; - const val = outputs[idx]?.toFixed(3) ?? '?'; - tooltip.textContent = `${name}: ${val}`; - tooltip.classList.add('show'); - }); - - strip.addEventListener('pointerout', () => { - tooltip.classList.remove('show'); - }); -} - -// ============================================================ -// Mode toggle (Visual/Synth) -// ============================================================ -function wireModeToggle() { - document.querySelectorAll('.mode-opt').forEach(btn => { - btn.addEventListener('click', () => { - const mode = btn.dataset.mode; - setOutputMode(mode); - document.querySelectorAll('.mode-opt').forEach(b => b.classList.toggle('active', b === btn)); - }); - }); -} - -function setOutputMode(mode) { - outputMode = mode; - const visC = document.getElementById('vis-canvas'); - const synthC = document.getElementById('synth-vis-canvas'); - if (mode === 'synth') { - currentParamNames = SYNTH_PARAM_NAMES; - currentParamColors = SYNTH_PARAM_COLORS; - document.getElementById('synth-section')?.classList.remove('hidden'); - // Pulse audio buttons if audio not yet started - const needsInit = !(c15 && c15.running); - document.getElementById('synth-start')?.classList.toggle('audio-needs-init', needsInit); - document.getElementById('audio-toggle')?.classList.toggle('audio-needs-init', needsInit); - // Toggle canvases - visC?.classList.add('hidden'); - synthC?.classList.remove('hidden'); - synthVisualizer?.resize(); - // Update preset labels to synth names - document.querySelectorAll('.preset-thumb span').forEach((span, i) => { - if (PRESETS[i]) span.textContent = PRESETS[i].synth; - }); - } else { - currentParamNames = VISUAL_PARAM_NAMES; - currentParamColors = VISUAL_PARAM_COLORS; - document.getElementById('synth-section')?.classList.add('hidden'); - // Toggle canvases - visC?.classList.remove('hidden'); - synthC?.classList.add('hidden'); - document.querySelectorAll('.preset-thumb span').forEach((span, i) => { - if (PRESETS[i]) span.textContent = PRESETS[i].name; - }); - } - buildHeatmap(); - updateHeatmap(iml.getOutputs()); - buildParamSliders(); - - // Show/hide audio control in perform phase - const audioCtrl = document.getElementById('audio-control'); - if (audioCtrl) audioCtrl.classList.toggle('hidden', mode !== 'synth' || phase !== 'perform'); -} - -// Teach tabs removed — unified teaching panel shows all controls together - -// ============================================================ -// Preset grid -// ============================================================ -function wirePresetGrid() { - document.querySelectorAll('.preset-thumb').forEach(btn => { - btn.addEventListener('click', () => { - const idx = parseInt(btn.dataset.preset); - selectedPreset = idx; - document.querySelectorAll('.preset-thumb').forEach(b => b.classList.toggle('selected', b === btn)); - - // Set the IML output state to this preset's values (for "Add Example") - const paddedPreset = padOutputs(PRESETS[idx].outputs); - iml.setOutputs(paddedPreset); - routeOutputs(paddedPreset); - }); - }); -} - -function renderPresetThumbnails() { - // Render a tiny visualization for each preset - const canvases = document.querySelectorAll('.preset-canvas'); - canvases.forEach((canvas, i) => { - if (!PRESETS[i]) return; - renderMiniPreview(canvas, PRESETS[i].outputs); - }); -} - -function renderMiniPreview(canvas, outputs) { - const ctx = canvas.getContext('2d'); - const w = canvas.width; - const h = canvas.height; - ctx.fillStyle = '#0d0d0d'; - ctx.fillRect(0, 0, w, h); - - // Draw a few colored dots to suggest the visual character - const hueBase = outputs[3] * 360; - const hueSpread = outputs[4] * 120; - const speed = outputs[2]; - const turb = outputs[7]; - const particleSize = 1 + outputs[5] * 3; - const count = 30; - - for (let p = 0; p < count; p++) { - const hue = (hueBase + (p / count) * hueSpread) % 360; - const x = w * 0.2 + Math.random() * w * 0.6; - const y = h * 0.2 + Math.random() * h * 0.6; - // Offset by speed/turbulence to hint at movement - const ox = (Math.random() - 0.5) * turb * 10; - const oy = (Math.random() - 0.5) * speed * 8; - ctx.fillStyle = `hsl(${hue}, 70%, 55%)`; - ctx.beginPath(); - ctx.arc(x + ox, y + oy, particleSize, 0, TWO_PI); - ctx.fill(); - } -} - -// ============================================================ -// Teach actions -// ============================================================ -function wireTeachActions() { - document.getElementById('btn-add-example')?.addEventListener('click', () => { - const outputs = iml.getOutputs(); - iml.addExample([joystickX, joystickY], outputs); - updateTeachStatus(); - drawMinimap(); - - // Pulse the train button - const trainBtn = document.getElementById('btn-train'); - trainBtn?.classList.add('pulse'); - }); - - document.getElementById('btn-train')?.addEventListener('click', () => { - if (iml.exampleCount === 0) return; - const trainBtn = document.getElementById('btn-train'); - trainBtn?.classList.remove('pulse'); - trainBtn.textContent = 'Training...'; - - // Use setTimeout to allow UI update - setTimeout(() => { - iml.train({ - onIteration: (iter, loss) => { - if (iter % 100 === 0) { - trainBtn.textContent = `Loss: ${loss.toFixed(5)}`; - } - }, - }); - - // Re-run inference - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - - trainBtn.textContent = 'Train'; - updateTeachStatus(); - saveState(); - }, 50); - }); - - document.getElementById('btn-clear-examples')?.addEventListener('click', () => { - iml.clearDataset(); - updateTeachStatus(); - drawMinimap(); - }); - - document.getElementById('btn-clear')?.addEventListener('click', () => { - iml.clearDataset(); - iml.randomiseWeights(spreadLevel); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - selectedPreset = -1; - document.querySelectorAll('.preset-thumb').forEach(b => b.classList.remove('selected')); - updateTeachStatus(); - drawMinimap(); - clearSavedState(); - }); - - document.getElementById('btn-randomize')?.addEventListener('click', () => { - iml.randomiseWeights(spreadLevel); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - updateTeachStatus(); - }); - - document.getElementById('btn-done')?.addEventListener('click', () => { - if (iml.exampleCount > 0 && iml.lastLoss === null) { - // Auto-train if there are untrained examples - iml.train(); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - } - saveState(); - setPhase('perform'); - }); -} - -function updateTeachStatus() { - const text = document.getElementById('status-text'); - if (!text) return; - const count = iml.exampleCount; - const lossStr = iml.lastLoss !== null ? ` \u00b7 loss ${iml.lastLoss.toFixed(5)}` : ''; - text.textContent = `${count} example${count !== 1 ? 's' : ''}${lossStr}`; -} - -// ============================================================ -// RL functions (standalone, callable from buttons or keyboard) -// ============================================================ -function onThumbsUp() { - const inputs = [joystickX, joystickY]; - const outputs = [...iml.getOutputs()]; - iml.addExample(inputs, outputs); - iml.train(); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - noiseLevel = Math.max(0.005, noiseLevel * 0.7); - updateNoiseRing(); - updateTeachStatus(); - drawMinimap(); - saveState(); -} - -function onThumbsDown() { - const noiseCap = 0.3 * (1 - spreadLevel) + 0.05 * spreadLevel; - noiseLevel = Math.min(noiseCap, noiseLevel * 1.5); - iml.moveWeights(noiseLevel, spreadLevel); - iml.setInput(0, joystickX); - iml.setInput(1, joystickY); - iml.process(); - routeOutputs(iml.getOutputs()); - updateNoiseRing(); -} - -function wireRLButtons() { - document.getElementById('btn-thumbs-up')?.addEventListener('click', onThumbsUp); - document.getElementById('btn-thumbs-down')?.addEventListener('click', onThumbsDown); -} - -function updateNoiseRing() { - const ring = document.getElementById('noise-ring'); - if (!ring) return; - if (noiseLevel > 0.01) { - const thickness = 2 + noiseLevel * 20; - const opacity = 0.2 + noiseLevel * 1.2; - ring.style.borderWidth = `${thickness}px`; - ring.style.opacity = Math.min(1, opacity); - ring.style.inset = `${-(8 + thickness)}px`; - } else { - ring.style.opacity = '0'; - } -} - -// ============================================================ -// Synth controls -// ============================================================ -function wireSynthControls() { - const startBtn = document.getElementById('synth-start'); - startBtn?.addEventListener('click', async () => { - if (c15.running) { - arpeggiator.stop(); - document.getElementById('arp-toggle').textContent = 'Play'; - await c15.stop(); - startBtn.textContent = 'Start Audio'; - startBtn.classList.add('audio-needs-init'); - document.getElementById('audio-toggle')?.classList.add('audio-needs-init'); - } else { - await c15.start(); - startBtn.textContent = 'Stop Audio'; - startBtn.classList.remove('audio-needs-init'); - document.getElementById('audio-toggle')?.classList.remove('audio-needs-init'); - routeOutputs(iml.getOutputs()); - } - }); - - document.getElementById('synth-volume')?.addEventListener('input', (e) => { - c15.setMasterVolume(parseFloat(e.target.value)); - }); - - document.getElementById('arp-toggle')?.addEventListener('click', () => { - if (!c15.running) return; - const btn = document.getElementById('arp-toggle'); - if (arpeggiator.playing) { - arpeggiator.stop(); - btn.textContent = 'Play'; - } else { - arpeggiator.start(); - btn.textContent = 'Stop'; - } - }); - - document.getElementById('arp-progression')?.addEventListener('change', (e) => { - arpeggiator.progression = e.target.value; - }); - - document.getElementById('arp-tempo')?.addEventListener('input', (e) => { - arpeggiator.bpm = parseInt(e.target.value); - const label = document.getElementById('tempo-val'); - if (label) label.textContent = e.target.value; - }); - - // Perform phase audio controls - document.getElementById('audio-toggle')?.addEventListener('click', async () => { - const btn = document.getElementById('audio-toggle'); - if (c15.running) { - arpeggiator.stop(); - await c15.stop(); - btn.textContent = '\u25B6'; - btn.classList.add('audio-needs-init'); - document.getElementById('synth-start')?.classList.add('audio-needs-init'); - } else { - await c15.start(); - arpeggiator.start(); - btn.textContent = '\u23F8'; - btn.classList.remove('audio-needs-init'); - document.getElementById('synth-start')?.classList.remove('audio-needs-init'); - routeOutputs(iml.getOutputs()); - } - }); - - document.getElementById('audio-vol')?.addEventListener('input', (e) => { - c15.setMasterVolume(parseFloat(e.target.value)); - }); -} - -// ============================================================ -// MIDI Input -// ============================================================ -async function initMIDIControls() { - const row = document.getElementById('midi-row'); - const statusRow = document.getElementById('midi-status-row'); - const toggle = document.getElementById('midi-toggle'); - const select = document.getElementById('midi-select'); - const statusEl = document.getElementById('midi-status'); - if (!row || !toggle || !select) return; - - const available = await midiInput.init(); - if (!available) return; - - row.style.display = ''; - - function populateInputs() { - const inputs = midiInput.getInputs(); - select.innerHTML = '' + - inputs.map(i => ``).join(''); - } - - populateInputs(); - midiInput.onInputsChange = () => populateInputs(); - - midiInput.onStatusChange = (msg) => { - if (statusEl) { - statusEl.textContent = msg; - statusRow.style.display = ''; - } - }; - - midiInput.onCC = (cc, value) => { - if (cc === 1) updateJoystick(value, joystickY); - if (cc === 2) updateJoystick(joystickX, value); - }; - - toggle.addEventListener('click', () => { - if (!c15.running) return; - midiInput.toggle(); - toggle.textContent = midiInput.enabled ? 'Disable' : 'Enable'; - toggle.classList.toggle('playing', midiInput.enabled); - }); - - select.addEventListener('change', (e) => { - midiInput.selectInput(e.target.value || null); - }); -} - -// ============================================================ -// Perform UI (idle fading, edit button) -// ============================================================ -function wirePerformUI() { - document.getElementById('btn-edit')?.addEventListener('click', () => { - setPhase('teach'); - }); -} - -function resetIdleTimer() { - app.classList.remove('idle'); - if (idleTimer) clearTimeout(idleTimer); - if (phase === 'perform') { - idleTimer = setTimeout(() => { - app.classList.add('idle'); - }, IDLE_TIMEOUT); - } -} - -// ============================================================ -// Joystick (canvas-based, inline) -// ============================================================ -function wireJoystick() { - const canvas = document.getElementById('joystick-canvas'); - if (!canvas) return; - - const getPos = (e) => { - const rect = canvas.getBoundingClientRect(); - const touch = e.touches ? e.touches[0] : e; - const x = (touch.clientX - rect.left) / rect.width; - const y = (touch.clientY - rect.top) / rect.height; - return [Math.max(0, Math.min(1, x)), Math.max(0, Math.min(1, y))]; - }; - - const onStart = (e) => { - e.preventDefault(); - joyDragging = true; - const [x, y] = getPos(e); - updateJoystick(x, y); - }; - - const onMove = (e) => { - e.preventDefault(); - if (!joyDragging) return; - const [x, y] = getPos(e); - updateJoystick(x, y); - }; - - const onEnd = (e) => { - e.preventDefault(); - joyDragging = false; - }; - - canvas.addEventListener('pointerdown', onStart); - canvas.addEventListener('pointermove', onMove); - canvas.addEventListener('pointerup', onEnd); - canvas.addEventListener('pointercancel', onEnd); - canvas.addEventListener('pointerleave', onEnd); - - // Draw initial state - drawJoystick(); -} - -function updateJoystick(x, y) { - joyKnobX = x; - joyKnobY = y; - joystickX = x; - joystickY = y; - joystickActive = true; - - iml.setInput(0, x); - iml.setInput(1, y); - iml.process(); - routeOutputs(iml.getOutputs()); - - // Explore phase: track interaction - if (phase === 'explore') { - joystickInteractionTime += 16; // rough frame time - if (joystickInteractionTime > 3000 && !ctasShown) { - ctasShown = true; - const prompt = document.getElementById('prompt-text'); - if (prompt) prompt.textContent = 'Ready to teach it?'; - document.getElementById('explore-ctas')?.classList.remove('hidden'); - } - } - - // Reset idle in perform - if (phase === 'perform') { - resetIdleTimer(); - } - - drawJoystick(); -} - -function drawJoystick() { - const canvas = document.getElementById('joystick-canvas'); - if (!canvas) return; - const ctx = canvas.getContext('2d'); - const dpr = window.devicePixelRatio || 1; - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width * dpr; - canvas.height = rect.height * dpr; - ctx.scale(dpr, dpr); - - const w = rect.width; - const h = rect.height; - const cx = w / 2; - const cy = h / 2; - const radius = Math.min(w, h) / 2 - 4; - - // Background ring - ctx.clearRect(0, 0, w, h); - ctx.beginPath(); - ctx.arc(cx, cy, radius, 0, TWO_PI); - ctx.fillStyle = 'rgba(13, 13, 13, 0.5)'; - ctx.fill(); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineWidth = 1.5; - ctx.stroke(); - - // Crosshair - ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(cx, cy - radius * 0.7); - ctx.lineTo(cx, cy + radius * 0.7); - ctx.moveTo(cx - radius * 0.7, cy); - ctx.lineTo(cx + radius * 0.7, cy); - ctx.stroke(); - - // Knob - const knobX = 4 + joyKnobX * (w - 8); - const knobY = 4 + joyKnobY * (h - 8); - const knobRadius = 10; - - ctx.beginPath(); - ctx.arc(knobX, knobY, knobRadius, 0, TWO_PI); - ctx.fillStyle = 'rgba(255, 106, 0, 0.7)'; - ctx.fill(); - ctx.strokeStyle = 'rgba(255, 106, 0, 0.4)'; - ctx.lineWidth = 2; - ctx.stroke(); - - // Glow - const glow = ctx.createRadialGradient(knobX, knobY, 0, knobX, knobY, knobRadius * 2.5); - glow.addColorStop(0, 'rgba(255, 106, 0, 0.15)'); - glow.addColorStop(1, 'rgba(255, 106, 0, 0)'); - ctx.fillStyle = glow; - ctx.beginPath(); - ctx.arc(knobX, knobY, knobRadius * 2.5, 0, TWO_PI); - ctx.fill(); -} - -// ============================================================ -// Follow mode -// ============================================================ -function wireFollowMode() { - const joyCanvas = document.getElementById('joystick-canvas'); - const followIndicator = document.getElementById('follow-indicator'); - const followPill = document.getElementById('follow-pill'); - - function toggleFollowMode() { - followMode = !followMode; - followIndicator?.classList.toggle('hidden', !followMode); - followPill?.classList.toggle('active', followMode); - } - - // Double-click joystick canvas toggles follow mode - joyCanvas?.addEventListener('dblclick', (e) => { - e.preventDefault(); - toggleFollowMode(); - }); - - // Follow pill toggle - followPill?.addEventListener('click', (e) => { - e.preventDefault(); - toggleFollowMode(); - }); - - // Global pointermove for follow mode - const visCanvas = document.getElementById('vis-canvas'); - window.addEventListener('pointermove', (e) => { - if (!followMode) return; - // Map cursor position to 0..1 relative to vis-canvas or full window - const target = visCanvas || document.body; - const rect = target.getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); - updateJoystick(x, y); - }); -} - -// ============================================================ -// Minimap (training examples positions) -// ============================================================ -function drawMinimap() { - const canvas = document.getElementById('minimap'); - if (!canvas) return; - const ctx = canvas.getContext('2d'); - const w = canvas.width; - const h = canvas.height; - - ctx.fillStyle = 'rgba(13, 13, 13, 0.9)'; - ctx.fillRect(0, 0, w, h); - - // Grid - ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; - ctx.lineWidth = 1; - for (let i = 1; i < 4; i++) { - const p = (i / 4) * w; - ctx.beginPath(); - ctx.moveTo(p, 0); ctx.lineTo(p, h); - ctx.moveTo(0, p); ctx.lineTo(w, p); - ctx.stroke(); - } - - // Example dots - const features = iml.dataset.features; - if (!features) return; - for (let i = 0; i < features.length; i++) { - const f = features[i]; - if (!f || f.length < 2) continue; - const x = f[0] * w; - const y = f[1] * h; - ctx.fillStyle = 'rgba(255, 106, 0, 0.8)'; - ctx.beginPath(); - ctx.arc(x, y, 3, 0, TWO_PI); - ctx.fill(); - } - - // Current position - ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.arc(joystickX * w, joystickY * h, 5, 0, TWO_PI); - ctx.stroke(); -} - -// ============================================================ -// Param sliders (fine-tune) -// ============================================================ -function buildParamSliders() { - const container = document.getElementById('param-sliders'); - if (!container) return; - container.innerHTML = ''; - - const outputs = iml.getOutputs(); - for (let i = 0; i < currentParamNames.length; i++) { - const row = document.createElement('div'); - row.className = 'param-slider-row'; - const label = document.createElement('label'); - label.textContent = currentParamNames[i]; - const input = document.createElement('input'); - input.type = 'range'; - input.min = '0'; - input.max = '1'; - input.step = '0.01'; - input.value = outputs[i] ?? 0.5; - input.dataset.index = i; - - input.addEventListener('input', (e) => { - const idx = parseInt(e.target.dataset.index); - iml.setOutput(idx, parseFloat(e.target.value)); - routeOutputs(iml.getOutputs()); - }); - - row.appendChild(label); - row.appendChild(input); - container.appendChild(row); - } -} - -function updateParamSliders() { - const sliders = document.querySelectorAll('.param-slider-row input[type="range"]'); - const outputs = iml.getOutputs(); - sliders.forEach(slider => { - const idx = parseInt(slider.dataset.index); - if (idx < outputs.length) { - slider.value = outputs[idx]; - } - }); -} - -// ============================================================ -// Resize -// ============================================================ -function onResize() { - visualizer.resize(); - visualizer.initParticles(); - if (synthVisualizer) synthVisualizer.resize(); - drawJoystick(); -} - -// ============================================================ -// Animation loop -// ============================================================ -function animate() { - if (!animating) return; - if (gamepad) gamepad.poll(); - - if (outputMode === 'synth') { - synthVisualizer.draw(); - } else { - visualizer.draw(); - } - - // Keep minimap updated if in teach phase - if (phase === 'teach') { - // Only redraw minimap occasionally - if (Math.random() < 0.05) drawMinimap(); - } - - requestAnimationFrame(animate); -} - -// ============================================================ -// Save / Load state -// ============================================================ -function saveState() { - try { - const data = { - features: iml.dataset.features.slice(0, iml.dataset.size), - labels: iml.dataset.labels.slice(0, iml.dataset.size), - weights: iml.mlp.getWeights(), - outputMode, - joystickX, - joystickY, - }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); - } catch (e) { - console.warn('Failed to save state:', e); - } -} - -function loadState() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return false; - const data = JSON.parse(raw); - if (!data.features || data.features.length === 0) return false; - - // Restore dataset - for (let i = 0; i < data.features.length; i++) { - iml.addExample(data.features[i], data.labels[i]); - } - - // Restore weights - if (data.weights) { - iml.mlp.setWeights(data.weights); - } - - // Restore output mode - if (data.outputMode) { - setOutputMode(data.outputMode); - document.querySelectorAll('.mode-opt').forEach(b => { - b.classList.toggle('active', b.dataset.mode === data.outputMode); - }); - } - - // Restore joystick position - if (data.joystickX !== undefined) { - joystickX = data.joystickX; - joystickY = data.joystickY; - joyKnobX = joystickX; - joyKnobY = joystickY; - } - - return true; - } catch (e) { - console.warn('Failed to load state:', e); - return false; - } -} - -function clearSavedState() { - localStorage.removeItem(STORAGE_KEY); -} - -// ============================================================ -// Boot -// ============================================================ -document.addEventListener('DOMContentLoaded', init); diff --git a/playground/js/eoc/eoc-chain.js b/playground/js/eoc/eoc-chain.js deleted file mode 100644 index 38c8136..0000000 --- a/playground/js/eoc/eoc-chain.js +++ /dev/null @@ -1,347 +0,0 @@ -// eoc-chain.js — End-of-Chain container. -// -// Manages an ordered list of EOCModule instances, wires them into the Web Audio -// graph, and exposes a flat NISPS parameter surface across all enabled modules. -// -// Canonical module order (used as insertion default, not enforced): -// Saturation → EQ → Compressor → Reverb → Delay → Master Bus -// -// Usage: -// const chain = new EOCChain(); -// await chain.init(audioCtx); -// chain.addModule(new SaturationModule()); -// chain.addModule(new ReverbModule()); -// chain.connect(synthOutputNode, audioCtx.destination); -// -// The chain dispatches 'eoc:change' CustomEvents on window whenever the -// structure changes (add/remove/reorder/bypass/nispsMode). - -import { EOCModule } from './eoc-module.js'; - -// Default slot order — used to sort modules by type when no explicit position -// is given. Modules not in this list are appended after the last known slot. -const DEFAULT_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'master']; - -export class EOCChain { - constructor() { - /** @type {EOCModule[]} */ - this._modules = []; - this._audioCtx = null; - this._inputNode = null; // stored after first connect() call - this._outputNode = null; // stored after first connect() call - this._initialized = false; - - // NISPS integration mode. - // 'bypass' — EOC params not used by NISPS at all (default) - // 'shared' — EOC params appended to synth param pool (future) - // 'linked' — EOC params driven by a separate, linked NISPS instance (future) - // 'independent'— EOC has its own independent NISPS instance (future) - this._nispsMode = 'bypass'; - } - - // --------------------------------------------------------------------------- - // Module management - // --------------------------------------------------------------------------- - - /** - * Insert a module at a given position. If no position is given, the module - * is appended in DEFAULT_ORDER slot sequence. - * - * @param {EOCModule} module - * @param {number} [position] — 0-based insertion index (optional) - */ - addModule(module, position) { - if (!(module instanceof EOCModule)) { - throw new TypeError('EOCChain.addModule: argument must be an EOCModule instance'); - } - if (this._modules.some(m => m.id === module.id)) { - throw new Error(`EOCChain.addModule: module '${module.id}' is already in the chain`); - } - - if (position !== undefined) { - this._modules.splice(Math.max(0, Math.min(position, this._modules.length)), 0, module); - } else { - // Insert at canonical slot position - const targetSlot = DEFAULT_ORDER.indexOf(module.id); - if (targetSlot === -1) { - // Unknown module type — append at end - this._modules.push(module); - } else { - // Find the first existing module whose canonical slot is after this one - const insertAt = this._modules.findIndex(m => { - const slot = DEFAULT_ORDER.indexOf(m.id); - return slot === -1 || slot > targetSlot; - }); - if (insertAt === -1) { - this._modules.push(module); - } else { - this._modules.splice(insertAt, 0, module); - } - } - } - - // If the chain is already initialised, init the new module immediately - if (this._initialized && this._audioCtx) { - module.init(this._audioCtx).then(() => { - this._rewire(); - this._dispatchChange('module-added'); - }); - } else { - this._dispatchChange('module-added'); - } - } - - /** - * Remove a module from the chain by id. - * Disposes the module and rewires. - * - * @param {string} id - */ - removeModule(id) { - const idx = this._modules.findIndex(m => m.id === id); - if (idx === -1) { - console.warn(`EOCChain.removeModule: no module with id '${id}'`); - return; - } - const [removed] = this._modules.splice(idx, 1); - removed.dispose(); - this._rewire(); - this._dispatchChange('module-removed'); - } - - /** - * Move a module to a new position. - * - * @param {string} id - * @param {number} newPosition — 0-based target index (after removal) - */ - moveModule(id, newPosition) { - const idx = this._modules.findIndex(m => m.id === id); - if (idx === -1) { - console.warn(`EOCChain.moveModule: no module with id '${id}'`); - return; - } - const [module] = this._modules.splice(idx, 1); - const clampedPos = Math.max(0, Math.min(newPosition, this._modules.length)); - this._modules.splice(clampedPos, 0, module); - this._rewire(); - this._dispatchChange('module-moved'); - } - - /** - * Retrieve a module by id. - * - * @param {string} id - * @returns {EOCModule|undefined} - */ - getModule(id) { - return this._modules.find(m => m.id === id); - } - - /** - * Ordered array of all modules in the chain (enabled and bypassed). - * @returns {EOCModule[]} - */ - get modules() { - return [...this._modules]; - } - - // --------------------------------------------------------------------------- - // Chain lifecycle - // --------------------------------------------------------------------------- - - /** - * Initialize all currently registered modules, then store the AudioContext. - * Safe to call before or after addModule() calls. - * - * @param {AudioContext} audioCtx - * @returns {Promise} - */ - async init(audioCtx) { - this._audioCtx = audioCtx; - await Promise.all(this._modules.map(m => m.init(audioCtx))); - this._initialized = true; - } - - /** - * Dispose all modules and disconnect the chain. - */ - dispose() { - this._disconnect(); - this._modules.forEach(m => m.dispose()); - this._modules = []; - this._audioCtx = null; - this._inputNode = null; - this._outputNode = null; - this._initialized = false; - } - - // --------------------------------------------------------------------------- - // Audio graph wiring - // --------------------------------------------------------------------------- - - /** - * Wire the full chain into the audio graph: - * inputNode → [enabled modules in order] → outputNode - * - * If no modules are enabled, inputNode connects directly to outputNode (all-bypass). - * - * Stores inputNode/outputNode for _rewire() calls triggered by later - * structural changes (add/remove/reorder/bypass toggle). - * - * @param {AudioNode} inputNode — upstream node (e.g. synth output GainNode) - * @param {AudioNode} outputNode — downstream node (e.g. AudioContext.destination) - */ - connect(inputNode, outputNode) { - this._inputNode = inputNode; - this._outputNode = outputNode; - this._rewire(); - } - - // --------------------------------------------------------------------------- - // NISPS integration - // --------------------------------------------------------------------------- - - /** - * Current NISPS integration mode. - * @returns {'bypass'|'shared'|'linked'|'independent'} - */ - get nispsMode() { - return this._nispsMode; - } - - /** - * Set NISPS integration mode. - * Only stores the value and dispatches an event; actual wiring is handled - * by the tasks that implement each mode (meml-xp3 and later). - * - * @param {'bypass'|'shared'|'linked'|'independent'} mode - */ - set nispsMode(mode) { - const valid = ['bypass', 'shared', 'linked', 'independent']; - if (!valid.includes(mode)) { - throw new Error(`EOCChain.nispsMode: invalid mode '${mode}'. Must be one of: ${valid.join(', ')}`); - } - this._nispsMode = mode; - this._dispatchChange('nispsMode-changed'); - } - - /** - * Total parameter count across all enabled modules. - * @returns {number} - */ - get paramCount() { - return this._enabledModules().reduce((sum, m) => sum + m.paramCount, 0); - } - - /** - * Flat parameter metadata array across all enabled modules, in chain order. - * Each entry carries an additional `_moduleId` field for routing. - * - * @returns {Array<{id:string, name:string, min:number, max:number, init:number, curve:number, group:string, _moduleId:string}>} - */ - get paramMeta() { - const result = []; - for (const module of this._enabledModules()) { - for (const meta of module.paramMeta) { - result.push({ ...meta, _moduleId: module.id }); - } - } - return result; - } - - /** - * Route a normalized [0,1] value to the correct module by global index. - * - * @param {number} globalIndex — flat index across all enabled modules' params - * @param {number} value — [0, 1] - */ - setParam(globalIndex, value) { - let offset = 0; - for (const module of this._enabledModules()) { - if (globalIndex < offset + module.paramCount) { - module.setParam(globalIndex - offset, value); - return; - } - offset += module.paramCount; - } - console.warn(`EOCChain.setParam: globalIndex ${globalIndex} out of range (paramCount=${this.paramCount})`); - } - - // --------------------------------------------------------------------------- - // Internal helpers - // --------------------------------------------------------------------------- - - /** - * Return only the currently enabled modules, in chain order. - * @returns {EOCModule[]} - */ - _enabledModules() { - return this._modules.filter(m => m.enabled); - } - - /** - * Disconnect all inter-module and endpoint connections, then reconnect - * in the current order. Safe to call at any time after connect() has been - * called at least once. - */ - _rewire() { - if (!this._inputNode || !this._outputNode) return; - - // Tear down: disconnect the input node and each module's output node. - // We only disconnect from the nodes we own in the chain to avoid clobbering - // any other connections the caller may have set up on inputNode/outputNode. - try { this._inputNode.disconnect(); } catch (_) { /* not yet connected */ } - for (const m of this._modules) { - try { m.getOutputNode().disconnect(); } catch (_) { /* not yet connected */ } - } - - const active = this._enabledModules(); - - if (active.length === 0) { - // All modules disabled (or chain is empty): straight wire - this._inputNode.connect(this._outputNode); - return; - } - - // inputNode → first module - this._inputNode.connect(active[0].getInputNode()); - - // module[i] → module[i+1] - for (let i = 0; i < active.length - 1; i++) { - active[i].getOutputNode().connect(active[i + 1].getInputNode()); - } - - // last module → outputNode - active[active.length - 1].getOutputNode().connect(this._outputNode); - } - - /** - * Disconnect everything the chain owns. - * Called during dispose(). - */ - _disconnect() { - if (!this._inputNode) return; - try { this._inputNode.disconnect(); } catch (_) { /* ok */ } - for (const m of this._modules) { - try { m.getOutputNode().disconnect(); } catch (_) { /* ok */ } - } - } - - /** - * Dispatch an 'eoc:change' CustomEvent on window. - * - * @param {string} reason — short description of what changed - */ - _dispatchChange(reason) { - window.dispatchEvent(new CustomEvent('eoc:change', { - detail: { - reason, - chain: this, - modules: this.modules, - paramCount: this.paramCount, - nispsMode: this.nispsMode, - } - })); - } -} diff --git a/playground/js/eoc/eoc-module.js b/playground/js/eoc/eoc-module.js deleted file mode 100644 index 2ed71d4..0000000 --- a/playground/js/eoc/eoc-module.js +++ /dev/null @@ -1,294 +0,0 @@ -// eoc-module.js — Base class for a single End-of-Chain effect module. -// -// All EOC effect modules extend this class. The contract mirrors the SynthEngine -// interface pattern (engine-interface.js) so both layers feel consistent. -// -// Subclasses MUST override: -// get id() — stable machine ID -// get displayName() — human label -// get paramMeta() — [{id, name, min, max, init, curve, group}] -// async init(audioCtx) — create AudioNodes, call super.init() first -// -// Subclasses MAY override: -// setParam(index, normalizedValue) -// dispose() -// -// Usage: -// class ReverbModule extends EOCModule { -// get id() { return 'reverb'; } -// get displayName() { return 'Reverb'; } -// get paramMeta() { return [...]; } -// async init(audioCtx) { -// await super.init(audioCtx); -// // create effect nodes, wire between this._bypassIn → effect → this._bypassOut -// } -// } - -export class EOCModule { - constructor() { - this._enabled = true; - this._audioCtx = null; - // Bypass graph nodes — allocated in init(), used by _applyBypass() - this._bypassIn = null; // GainNode: input entry point - this._bypassOut = null; // GainNode: output exit point - this._bypassDry = null; // GainNode: direct input→output path when bypassed - this._initialized = false; - // Stored normalized param values — set via setParam(), read by getCurrentParamValue() - this._paramValues = []; - } - - // --------------------------------------------------------------------------- - // Identity (override in subclass) - // --------------------------------------------------------------------------- - - /** - * Stable machine ID. - * One of: 'eq' | 'compressor' | 'reverb' | 'delay' | 'saturation' | 'master' - * @returns {string} - */ - get id() { - throw new Error(`${this.constructor.name}: id not implemented`); - } - - /** - * Human-readable label shown in the UI. - * @returns {string} - */ - get displayName() { - throw new Error(`${this.constructor.name}: displayName not implemented`); - } - - // --------------------------------------------------------------------------- - // Enable / bypass - // --------------------------------------------------------------------------- - - /** - * Whether this module is active in the signal chain. - * When false, audio passes directly from input to output (true bypass). - * @returns {boolean} - */ - get enabled() { - return this._enabled; - } - - /** - * Toggle bypass. Reconnects the internal audio graph immediately. - * @param {boolean} v - */ - set enabled(v) { - const changed = this._enabled !== !!v; - this._enabled = !!v; - if (changed && this._initialized) { - this._applyBypass(); - } - } - - // --------------------------------------------------------------------------- - // Parameter schema (override in subclass) - // --------------------------------------------------------------------------- - - /** - * Number of continuous parameters this module exposes. - * Derived automatically from paramMeta — override paramMeta, not this. - * @returns {number} - */ - get paramCount() { - return this.paramMeta.length; - } - - /** - * Array of parameter descriptors, one per controllable parameter. - * - * Each entry must have: - * id {string} — stable machine ID (used for presets / NISPS routing) - * name {string} — short display name - * min {number} — raw minimum value (units depend on param) - * max {number} — raw maximum value - * init {number} — default normalized value [0, 1] - * curve {number} — power-curve bias: 0.5 = linear, <0.5 = log, >0.5 = exp - * group {string} — section label (for group drawer / colour coding) - * - * @returns {Array<{id:string, name:string, min:number, max:number, init:number, curve:number, group:string}>} - */ - get paramMeta() { - throw new Error(`${this.constructor.name}: paramMeta not implemented`); - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - /** - * Allocate AudioNodes and connect the internal graph. - * Subclasses MUST call super.init(audioCtx) first, then wire their effect - * nodes between this._bypassIn and this._bypassOut. - * - * @param {AudioContext} audioCtx - * @returns {Promise} - */ - async init(audioCtx) { - if (this._initialized) return; - this._audioCtx = audioCtx; - - // Two GainNodes bracket the effect processing path. - // A third (dry) provides a direct signal route for bypass. - this._bypassIn = audioCtx.createGain(); - this._bypassOut = audioCtx.createGain(); - this._bypassDry = audioCtx.createGain(); - - // Wire the dry path (always present; gain toggled by _applyBypass) - this._bypassIn.connect(this._bypassDry); - this._bypassDry.connect(this._bypassOut); - - this._initialized = true; - // Note: subclass connects its effect nodes, then calls _applyBypass() - // via _finishInit() to set initial gain values correctly. - } - - /** - * Call from the end of a subclass init() once effect nodes are wired, to - * apply the correct initial bypass state. - * - * Subclass pattern: - * async init(audioCtx) { - * await super.init(audioCtx); - * // ... wire effect nodes ... - * this._finishInit(); - * } - */ - _finishInit() { - this._applyBypass(); - } - - /** - * Disconnect all AudioNodes and release resources. - * Override in subclasses to clean up effect-specific nodes. - */ - dispose() { - if (!this._initialized) return; - try { - this._bypassIn.disconnect(); - this._bypassOut.disconnect(); - this._bypassDry.disconnect(); - } catch (_) { /* already disconnected */ } - this._initialized = false; - this._audioCtx = null; - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - /** - * Set a single parameter by index. - * - * @param {number} index — 0-based index into paramMeta - * @param {number} normalizedValue — [0, 1] - */ - setParam(index, normalizedValue) { // eslint-disable-line no-unused-vars - // Store the value so getCurrentParamValue() can read it back - this._paramValues[index] = normalizedValue; - // default no-op — override in subclass for actual audio effect - } - - /** - * Get the last normalized value set for a parameter. - * Returns the param's init value (from paramMeta) if never explicitly set. - * - * @param {number} index — 0-based index into paramMeta - * @returns {number} normalized value [0, 1] - */ - getCurrentParamValue(index) { - if (this._paramValues[index] !== undefined) { - return this._paramValues[index]; - } - const meta = this.paramMeta[index]; - return meta ? (meta.init ?? 0) : 0; - } - - // --------------------------------------------------------------------------- - // Audio graph - // --------------------------------------------------------------------------- - - /** - * The AudioNode that upstream sources should connect to. - * Everything flows into this node. - * @returns {AudioNode} - */ - getInputNode() { - return this._bypassIn; - } - - /** - * The AudioNode to connect downstream (to next module or destination). - * Everything exits through this node. - * @returns {AudioNode} - */ - getOutputNode() { - return this._bypassOut; - } - - // --------------------------------------------------------------------------- - // Internal helpers - // --------------------------------------------------------------------------- - - /** - * Apply bypass state to the internal audio graph. - * - * When enabled: - * - dry path (bypassIn → bypassOut) gain = 0 - * - effect path is live (subclass manages its own nodes) - * - * When bypassed: - * - dry path gain = 1 (signal passes straight through) - * - effect processing gains are muted (subclass handles via _onBypassChange) - * - * Both _bypassIn and _bypassOut remain in the graph at all times so the - * EOCChain's wiring never needs to change when bypass is toggled. - */ - _applyBypass() { - if (!this._bypassDry) return; - const t = this._audioCtx.currentTime; - if (this._enabled) { - // Effect active: cut the dry path - this._bypassDry.gain.setTargetAtTime(0, t, 0.005); - } else { - // Bypassed: open dry path - this._bypassDry.gain.setTargetAtTime(1, t, 0.005); - } - // Let subclass mute/unmute its own effect nodes - this._onBypassChange(this._enabled); - } - - /** - * Called by _applyBypass() after the dry-path gain is updated. - * Subclasses should override to mute/unmute effect processing nodes. - * - * @param {boolean} enabled — true = effect active, false = bypassed - */ - _onBypassChange(enabled) { // eslint-disable-line no-unused-vars - // default no-op — override in subclass if effect has its own gain to manage - } - - // --------------------------------------------------------------------------- - // Utility: normalize a [0,1] value to the raw param range - // --------------------------------------------------------------------------- - - /** - * Convert a normalized [0,1] value to the raw range defined by paramMeta[index]. - * Applies the curve bias: value^(1/curve) gives log-like feel for small curve, - * value^curve gives exp-like feel for large curve. - * - * @param {number} index - * @param {number} normalizedValue [0, 1] - * @returns {number} raw value in [min, max] - */ - _denormalize(index, normalizedValue) { - const meta = this.paramMeta[index]; - if (!meta) return 0; - const { min, max, curve = 0.5 } = meta; - // Apply power curve: curve=0.5 is linear, <0.5 pulls toward min, >0.5 toward max - const shaped = Math.pow(Math.max(0, Math.min(1, normalizedValue)), curve === 0.5 ? 1 : 1 / (curve * 2)); - return min + shaped * (max - min); - } -} diff --git a/playground/js/eoc/index.js b/playground/js/eoc/index.js deleted file mode 100644 index 5edb993..0000000 --- a/playground/js/eoc/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// eoc/index.js — barrel re-export for the End-of-Chain effects system. -// -// Import from here to get the full EOC API: -// import { EOCModule, EOCChain } from './eoc/index.js'; - -export { EOCModule } from './eoc-module.js'; -export { EOCChain } from './eoc-chain.js'; - -// Effect module implementations -export { EQModule } from './modules/eq-module.js'; -export { CompressorModule } from './modules/compressor-module.js'; -export { ReverbModule } from './modules/reverb-module.js'; -export { DelayModule } from './modules/delay-module.js'; -export { SaturationModule } from './modules/saturation-module.js'; -export { MasterModule } from './modules/master-module.js'; diff --git a/playground/js/eoc/modules/compressor-module.js b/playground/js/eoc/modules/compressor-module.js deleted file mode 100644 index 65e4d7d..0000000 --- a/playground/js/eoc/modules/compressor-module.js +++ /dev/null @@ -1,129 +0,0 @@ -// playground/js/eoc/modules/compressor-module.js — Stereo compressor EOCModule -// -// Faust DSP: playground/faust/eoc-compressor.dsp -// WASM: playground/faust/eoc-compressor.wasm -// JSON: playground/faust/eoc-compressor.json -// -// 7 params (Faust JSON alphabetical order): -// 0: attack [0.1–200 ms, init 10] -// 1: knee [0–24 dB, init 6] -// 2: makeup [0–24 dB, init 0] -// 3: mix [0–1, init 1] (dry/wet for parallel compression) -// 4: ratio [1–20, init 4] -// 5: release [10–2000 ms, init 100] -// 6: threshold [-60–0 dBFS, init -24] - -import { EOCModule } from '../eoc-module.js'; -import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; - -const COMP_PARAM_META = [ - { id: 'attack', name: 'Attack', min: 0.1, max: 200, init: (10-0.1)/(200-0.1), curve: 0.3, group: 'Compressor' }, - { id: 'knee', name: 'Knee', min: 0, max: 24, init: 6/24, curve: 0.5, group: 'Compressor' }, - { id: 'makeup', name: 'Makeup', min: 0, max: 24, init: 0, curve: 0.5, group: 'Compressor' }, - { id: 'mix', name: 'Mix', min: 0, max: 1, init: 1, curve: 0.5, group: 'Compressor' }, - { id: 'ratio', name: 'Ratio', min: 1, max: 20, init: (4-1)/(20-1), curve: 0.4, group: 'Compressor' }, - { id: 'release', name: 'Release', min: 10, max: 2000, init: (100-10)/(2000-10), curve: 0.3, group: 'Compressor' }, - { id: 'threshold', name: 'Threshold', min: -60, max: 0, init: (-24-(-60))/(0-(-60)), curve: 0.5, group: 'Compressor' }, -]; - -export class CompressorModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; - this._paramMeta = COMP_PARAM_META; - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'compressor'; } - get displayName() { return 'Compressor'; } - get paramMeta() { return this._paramMeta; } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - try { - const fetched = await loadFaustParamMeta('faust/eoc-compressor.json'); - if (fetched && fetched.length > 0) this._paramMeta = fetched; - } catch (err) { - console.warn('[CompressorModule] Could not load eoc-compressor.json, using static paramMeta:', err.message); - } - - try { - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - } catch (_) { /* already registered */ } - await audioCtx.audioWorklet.addModule('faust/eoc-compressor-processor.js'); - - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-compressor-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - - const wasmResp = await fetch('faust/eoc-compressor.wasm'); - const wasmBytes = await wasmResp.arrayBuffer(); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('CompressorModule worklet init timeout')), 10000); - this._workletNode.port.onmessage = (e) => { - if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } - if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } - }; - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes], - ); - }); - - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const meta = this._paramMeta[index]; - if (!meta) return; - const raw = meta.min + normalizedValue * (meta.max - meta.min); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Dispose - // --------------------------------------------------------------------------- - - dispose() { - this._workletNode?.disconnect(); - this._effectGain?.disconnect(); - this._workletNode = null; - this._effectGain = null; - super.dispose(); - } -} diff --git a/playground/js/eoc/modules/delay-module.js b/playground/js/eoc/modules/delay-module.js deleted file mode 100644 index 5cf2e81..0000000 --- a/playground/js/eoc/modules/delay-module.js +++ /dev/null @@ -1,138 +0,0 @@ -// delay-module.js — EOC Stereo Delay module (meml-2sl). -// -// Faust source: playground/faust/eoc-delay.dsp -// Worklet processor: playground/faust/eoc-delay-processor.js -// Processor name: 'eoc-delay-processor' -// -// 7 parameters (in JSON / Faust alphabetical order): -// 0 feedback [0, 0.95] init=0.3 -// 1 lp_cutoff [500, 20000] init=8000 -// 2 mix [0, 1] init=0.3 -// 3 ping_pong [0, 1] init=0.0 -// 4 spread [0, 1] init=0.5 -// 5 sync [0, 3] init=0 (nentry) -// 6 time [1, 2000] init=250 - -import { EOCModule } from '../eoc-module.js'; -import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; - -export class DelayModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; // GainNode: muted when bypassed - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'delay'; } - get displayName() { return 'Delay'; } - - get paramMeta() { - // Normalized init values derived from Faust defaults - return [ - { id: 'feedback', name: 'Feedback', min: 0, max: 0.95, init: 0.3 / 0.95, curve: 0.5, group: 'Delay' }, - { id: 'lp_cutoff', name: 'LP Cutoff', min: 500, max: 20000, init: (8000 - 500) / 19500, curve: 0.35, group: 'Delay' }, - { id: 'mix', name: 'Mix', min: 0, max: 1, init: 0.3, curve: 0.5, group: 'Delay' }, - { id: 'ping_pong', name: 'Ping-Pong', min: 0, max: 1, init: 0.0, curve: 0.5, group: 'Delay' }, - { id: 'spread', name: 'Spread', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Delay' }, - { id: 'sync', name: 'Sync', min: 0, max: 3, init: 0.0, curve: 0.5, group: 'Delay' }, - { id: 'time', name: 'Time (ms)', min: 1, max: 2000, init: (250 - 1) / 1999, curve: 0.35, group: 'Delay' }, - ]; - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - // Fetch WASM bytes - const wasmResp = await fetch('faust/eoc-delay.wasm'); - if (!wasmResp.ok) throw new Error(`[DelayModule] Failed to fetch eoc-delay.wasm: ${wasmResp.status}`); - const wasmBytes = await wasmResp.arrayBuffer(); - - // Register worklet (browser deduplicates) - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - await audioCtx.audioWorklet.addModule('faust/eoc-delay-processor.js'); - - // Create worklet node: 2-in / 2-out stereo effect - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-delay-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - channelCount: 2, - channelCountMode: 'explicit', - }); - - // Listen for ready / error messages - this._workletNode.port.onmessage = (e) => { - if (e.data?.type === 'error') { - console.error('[DelayModule] Worklet error:', e.data.message); - } - }; - - // Effect gain node — muted when bypassed - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - // Wire: bypassIn → worklet → effectGain → bypassOut - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - // Send init message to worklet - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes] - ); - - // Apply initial default param values - this.paramMeta.forEach((_, i) => { - this.setParam(i, this.getCurrentParamValue(i)); - }); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const raw = this._denormalize(index, normalizedValue); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - dispose() { - if (this._workletNode) { - this._workletNode.disconnect(); - this._workletNode.port.onmessage = null; - this._workletNode = null; - } - if (this._effectGain) { - this._effectGain.disconnect(); - this._effectGain = null; - } - super.dispose(); - } -} diff --git a/playground/js/eoc/modules/eq-module.js b/playground/js/eoc/modules/eq-module.js deleted file mode 100644 index 556543b..0000000 --- a/playground/js/eoc/modules/eq-module.js +++ /dev/null @@ -1,143 +0,0 @@ -// playground/js/eoc/modules/eq-module.js — 4-band parametric EQ EOCModule -// -// Faust DSP: playground/faust/eoc-eq.dsp -// WASM: playground/faust/eoc-eq.wasm -// JSON: playground/faust/eoc-eq.json -// -// 10 params (shelf bands have no Q in Faust fi.low_shelf / fi.high_shelf): -// Band 1 (Low Shelf): freq1 [20–500 Hz, init 80], gain1 [-12–+12 dB, init 0] -// Band 2 (Low-Mid): freq2 [100–2000 Hz, init 400], gain2 [-12–+12 dB, init 0], q2 [0.1–10, init 1] -// Band 3 (High-Mid): freq3 [500–8000 Hz, init 2500], gain3 [-12–+12 dB, init 0], q3 [0.1–10, init 1] -// Band 4 (High Shelf): freq4 [2000–20000 Hz, init 8000], gain4 [-12–+12 dB, init 0] -// -// param order = Faust JSON alphabetical-within-group, groups in declaration order: -// 0 freq1, 1 gain1, 2 freq2, 3 gain2, 4 q2, 5 freq3, 6 gain3, 7 q3, 8 freq4, 9 gain4 - -import { EOCModule } from '../eoc-module.js'; -import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; - -// Static paramMeta — mirrors eoc-eq.json, used before/without JSON fetch -const EQ_PARAM_META = [ - // Band 1 (Low Shelf) - { id: 'band_1__low_shelf__freq1', name: 'Freq 1', min: 20, max: 500, init: (80-20)/(500-20), curve: 0.3, group: 'Band 1 (Low Shelf)' }, - { id: 'band_1__low_shelf__gain1', name: 'Gain 1', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 1 (Low Shelf)' }, - // Band 2 (Low-Mid bell) - { id: 'band_2__low-mid__freq2', name: 'Freq 2', min: 100, max: 2000, init: (400-100)/(2000-100), curve: 0.3, group: 'Band 2 (Low-Mid)' }, - { id: 'band_2__low-mid__gain2', name: 'Gain 2', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 2 (Low-Mid)' }, - { id: 'band_2__low-mid__q2', name: 'Q 2', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 2 (Low-Mid)' }, - // Band 3 (High-Mid bell) - { id: 'band_3__high-mid__freq3', name: 'Freq 3', min: 500, max: 8000, init: (2500-500)/(8000-500),curve: 0.3, group: 'Band 3 (High-Mid)' }, - { id: 'band_3__high-mid__gain3', name: 'Gain 3', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 3 (High-Mid)' }, - { id: 'band_3__high-mid__q3', name: 'Q 3', min: 0.1, max: 10, init: (1-0.1)/(10-0.1), curve: 0.3, group: 'Band 3 (High-Mid)' }, - // Band 4 (High Shelf) - { id: 'band_4__high_shelf__freq4',name: 'Freq 4', min: 2000, max: 20000, init: (8000-2000)/(20000-2000),curve: 0.3, group: 'Band 4 (High Shelf)' }, - { id: 'band_4__high_shelf__gain4',name: 'Gain 4', min: -12, max: 12, init: (0-(-12))/(12-(-12)), curve: 0.5, group: 'Band 4 (High Shelf)' }, -]; - -export class EQModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; - this._paramMeta = EQ_PARAM_META; - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'eq'; } - get displayName() { return 'Parametric EQ'; } - get paramMeta() { return this._paramMeta; } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - // Attempt to load richer paramMeta from JSON (non-fatal if fetch fails) - try { - const fetched = await loadFaustParamMeta('faust/eoc-eq.json'); - if (fetched && fetched.length > 0) this._paramMeta = fetched; - } catch (err) { - console.warn('[EQModule] Could not load eoc-eq.json, using static paramMeta:', err.message); - } - - // Register worklet module (idempotent across calls) - try { - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - } catch (_) { /* already registered */ } - await audioCtx.audioWorklet.addModule('faust/eoc-eq-processor.js'); - - // Create AudioWorkletNode - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-eq-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - - // Load WASM and send to worklet - const wasmResp = await fetch('faust/eoc-eq.wasm'); - const wasmBytes = await wasmResp.arrayBuffer(); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('EQModule worklet init timeout')), 10000); - this._workletNode.port.onmessage = (e) => { - if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } - if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } - }; - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes], - ); - }); - - // Gain node so bypass can mute the effect path - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - // Wire: bypassIn → worklet → effectGain → bypassOut - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const meta = this._paramMeta[index]; - if (!meta) return; - const raw = meta.min + normalizedValue * (meta.max - meta.min); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Dispose - // --------------------------------------------------------------------------- - - dispose() { - this._workletNode?.disconnect(); - this._effectGain?.disconnect(); - this._workletNode = null; - this._effectGain = null; - super.dispose(); - } -} diff --git a/playground/js/eoc/modules/master-module.js b/playground/js/eoc/modules/master-module.js deleted file mode 100644 index a4d80d0..0000000 --- a/playground/js/eoc/modules/master-module.js +++ /dev/null @@ -1,123 +0,0 @@ -// master-module.js — EOC Master Bus module (meml-2zm part 2). -// -// Faust source: playground/faust/eoc-master.dsp -// Worklet processor: playground/faust/eoc-master-processor.js -// Processor name: 'eoc-master-processor' -// -// 4 parameters (in JSON / Faust alphabetical order): -// 0 dc_block [0, 1] init=1 (nentry: off/on) -// 1 gain [0, 2] init=1.0 -// 2 limiter_thresh [-12, 0] init=-1.0 -// 3 width [0, 2] init=1.0 - -import { EOCModule } from '../eoc-module.js'; - -export class MasterModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'master'; } - get displayName() { return 'Master Bus'; } - - get paramMeta() { - return [ - { id: 'dc_block', name: 'DC Block', min: 0, max: 1, init: 1.0, curve: 0.5, group: 'Master' }, - { id: 'gain', name: 'Gain', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' }, - { id: 'limiter_thresh', name: 'Limiter (dB)', min: -12, max: 0, init: (-1.0 - (-12)) / 12, curve: 0.5, group: 'Master' }, - { id: 'width', name: 'Width', min: 0, max: 2, init: 1.0 / 2.0, curve: 0.5, group: 'Master' }, - ]; - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - const wasmResp = await fetch('faust/eoc-master.wasm'); - if (!wasmResp.ok) throw new Error(`[MasterModule] Failed to fetch eoc-master.wasm: ${wasmResp.status}`); - const wasmBytes = await wasmResp.arrayBuffer(); - - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - await audioCtx.audioWorklet.addModule('faust/eoc-master-processor.js'); - - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-master-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - channelCount: 2, - channelCountMode: 'explicit', - }); - - this._workletNode.port.onmessage = (e) => { - if (e.data?.type === 'error') { - console.error('[MasterModule] Worklet error:', e.data.message); - } - }; - - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - // Wire: bypassIn → worklet → effectGain → bypassOut - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes] - ); - - this.paramMeta.forEach((_, i) => { - this.setParam(i, this.getCurrentParamValue(i)); - }); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const raw = this._denormalize(index, normalizedValue); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - dispose() { - if (this._workletNode) { - this._workletNode.disconnect(); - this._workletNode.port.onmessage = null; - this._workletNode = null; - } - if (this._effectGain) { - this._effectGain.disconnect(); - this._effectGain = null; - } - super.dispose(); - } -} diff --git a/playground/js/eoc/modules/reverb-module.js b/playground/js/eoc/modules/reverb-module.js deleted file mode 100644 index 55c1af5..0000000 --- a/playground/js/eoc/modules/reverb-module.js +++ /dev/null @@ -1,147 +0,0 @@ -// playground/js/eoc/modules/reverb-module.js — Zita reverb EOCModule -// -// Faust DSP: playground/faust/eoc-reverb.dsp -// WASM: playground/faust/eoc-reverb.wasm -// JSON: playground/faust/eoc-reverb.json -// -// 8 params (Faust JSON alphabetical order; mod_rate is declared in DSP but not -// exposed by re.zita_rev1_stereo, so it does not appear in the JSON): -// 0: decay [0.1–20 s, init 3] -// 1: diffusion [0–1, init 0.7] -// 2: hi_damp [0–1, init 0.5] -// 3: lo_damp [0–1, init 0] -// 4: mix [0–1, init 0.2] -// 5: predelay [0–100 ms, init 0] -// 6: size [0–1, init 0.5] -// 7: width [0–1, init 0.8] -// -// Note: mod_rate param is listed in paramMeta as a placeholder (fixed at 0.5 Hz) -// to preserve the 9-param count described in the spec. It has no effect on audio -// until zita modulation is plumbed through. - -import { EOCModule } from '../eoc-module.js'; -import { loadFaustParamMeta } from '../../synth/faust-param-meta.js'; - -const REVERB_PARAM_META = [ - { id: 'decay', name: 'Decay', min: 0.1, max: 20, init: (3-0.1)/(20-0.1), curve: 0.3, group: 'Reverb' }, - { id: 'diffusion', name: 'Diffusion', min: 0, max: 1, init: 0.7, curve: 0.5, group: 'Reverb' }, - { id: 'hi_damp', name: 'Hi Damp', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' }, - { id: 'lo_damp', name: 'Lo Damp', min: 0, max: 1, init: 0, curve: 0.5, group: 'Reverb' }, - { id: 'mix', name: 'Mix', min: 0, max: 1, init: 0.2, curve: 0.5, group: 'Reverb' }, - { id: 'predelay', name: 'Predelay', min: 0, max: 100, init: 0, curve: 0.4, group: 'Reverb' }, - { id: 'size', name: 'Size', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Reverb' }, - { id: 'width', name: 'Width', min: 0, max: 1, init: 0.8, curve: 0.5, group: 'Reverb' }, - // Placeholder: mod_rate is not yet connected to zita internals - { id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' }, -]; - -export class ReverbModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; - this._paramMeta = REVERB_PARAM_META; - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'reverb'; } - get displayName() { return 'Reverb'; } - get paramMeta() { return this._paramMeta; } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - // After loading JSON, merge — but keep mod_rate placeholder (index 8) - try { - const fetched = await loadFaustParamMeta('faust/eoc-reverb.json'); - if (fetched && fetched.length > 0) { - // Append mod_rate placeholder so total stays 9 - this._paramMeta = [ - ...fetched, - { id: 'mod_rate', name: 'Mod Rate', min: 0, max: 5, init: 0.5/5, curve: 0.5, group: 'Reverb' }, - ]; - } - } catch (err) { - console.warn('[ReverbModule] Could not load eoc-reverb.json, using static paramMeta:', err.message); - } - - try { - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - } catch (_) { /* already registered */ } - await audioCtx.audioWorklet.addModule('faust/eoc-reverb-processor.js'); - - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-reverb-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - - const wasmResp = await fetch('faust/eoc-reverb.wasm'); - const wasmBytes = await wasmResp.arrayBuffer(); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('ReverbModule worklet init timeout')), 10000); - this._workletNode.port.onmessage = (e) => { - if (e.data.type === 'ready') { clearTimeout(timeout); resolve(); } - if (e.data.type === 'error') { clearTimeout(timeout); reject(new Error(e.data.message)); } - }; - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes], - ); - }); - - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const meta = this._paramMeta[index]; - if (!meta) return; - // mod_rate (index 8) is a placeholder — skip sending to worklet - if (meta.id === 'mod_rate') return; - const raw = meta.min + normalizedValue * (meta.max - meta.min); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Dispose - // --------------------------------------------------------------------------- - - dispose() { - this._workletNode?.disconnect(); - this._effectGain?.disconnect(); - this._workletNode = null; - this._effectGain = null; - super.dispose(); - } -} diff --git a/playground/js/eoc/modules/saturation-module.js b/playground/js/eoc/modules/saturation-module.js deleted file mode 100644 index a0e54bd..0000000 --- a/playground/js/eoc/modules/saturation-module.js +++ /dev/null @@ -1,123 +0,0 @@ -// saturation-module.js — EOC Saturation module (meml-2zm part 1). -// -// Faust source: playground/faust/eoc-saturation.dsp -// Worklet processor: playground/faust/eoc-saturation-processor.js -// Processor name: 'eoc-saturation-processor' -// -// 4 parameters (in JSON / Faust alphabetical order): -// 0 character [0, 1] init=0.0 -// 1 drive [0, 1] init=0.0 -// 2 mix [0, 1] init=1.0 -// 3 tone [0, 1] init=0.5 - -import { EOCModule } from '../eoc-module.js'; - -export class SaturationModule extends EOCModule { - constructor() { - super(); - this._workletNode = null; - this._effectGain = null; - } - - // --------------------------------------------------------------------------- - // EOCModule identity - // --------------------------------------------------------------------------- - - get id() { return 'saturation'; } - get displayName() { return 'Saturation'; } - - get paramMeta() { - return [ - { id: 'character', name: 'Character', min: 0, max: 1, init: 0.0, curve: 0.5, group: 'Saturation' }, - { id: 'drive', name: 'Drive', min: 0, max: 1, init: 0.0, curve: 0.6, group: 'Saturation' }, - { id: 'mix', name: 'Mix', min: 0, max: 1, init: 1.0, curve: 0.5, group: 'Saturation' }, - { id: 'tone', name: 'Tone', min: 0, max: 1, init: 0.5, curve: 0.5, group: 'Saturation' }, - ]; - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - async init(audioCtx) { - await super.init(audioCtx); - - const wasmResp = await fetch('faust/eoc-saturation.wasm'); - if (!wasmResp.ok) throw new Error(`[SaturationModule] Failed to fetch eoc-saturation.wasm: ${wasmResp.status}`); - const wasmBytes = await wasmResp.arrayBuffer(); - - await audioCtx.audioWorklet.addModule('faust/faust-worklet-processor.js'); - await audioCtx.audioWorklet.addModule('faust/eoc-saturation-processor.js'); - - this._workletNode = new AudioWorkletNode(audioCtx, 'eoc-saturation-processor', { - numberOfInputs: 1, - numberOfOutputs: 1, - outputChannelCount: [2], - channelCount: 2, - channelCountMode: 'explicit', - }); - - this._workletNode.port.onmessage = (e) => { - if (e.data?.type === 'error') { - console.error('[SaturationModule] Worklet error:', e.data.message); - } - }; - - this._effectGain = audioCtx.createGain(); - this._effectGain.gain.value = 1; - - // Wire: bypassIn → worklet → effectGain → bypassOut - this._bypassIn.connect(this._workletNode); - this._workletNode.connect(this._effectGain); - this._effectGain.connect(this._bypassOut); - - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes] - ); - - this.paramMeta.forEach((_, i) => { - this.setParam(i, this.getCurrentParamValue(i)); - }); - - this._finishInit(); - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - super.setParam(index, normalizedValue); - if (!this._workletNode) return; - const raw = this._denormalize(index, normalizedValue); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - // --------------------------------------------------------------------------- - // Bypass - // --------------------------------------------------------------------------- - - _onBypassChange(enabled) { - if (!this._effectGain) return; - const t = this._audioCtx.currentTime; - this._effectGain.gain.setTargetAtTime(enabled ? 1 : 0, t, 0.005); - } - - // --------------------------------------------------------------------------- - // Cleanup - // --------------------------------------------------------------------------- - - dispose() { - if (this._workletNode) { - this._workletNode.disconnect(); - this._workletNode.port.onmessage = null; - this._workletNode = null; - } - if (this._effectGain) { - this._effectGain.disconnect(); - this._effectGain = null; - } - super.dispose(); - } -} diff --git a/playground/js/event-bus.js b/playground/js/event-bus.js deleted file mode 100644 index abd7821..0000000 --- a/playground/js/event-bus.js +++ /dev/null @@ -1,117 +0,0 @@ -// Namespaced Event Bus -// Lightweight pub/sub with seq.*/ml.*/ui.* namespaces and wildcard support. - -// ── Event name constants ──────────────────────────────────────────── - -export const SEQ = Object.freeze({ - STEP: 'seq.step', - NOTE_ON: 'seq.noteOn', - NOTE_OFF: 'seq.noteOff', - PARAM_CHANGE: 'seq.paramChange', - LOOP_START: 'seq.loopStart', -}); - -export const ML = Object.freeze({ - TRAINED: 'ml.trained', - FROZEN: 'ml.frozen', - UNFROZEN: 'ml.unfrozen', - DELTA_UPDATE: 'ml.deltaUpdate', -}); - -export const UI = Object.freeze({ - PARAM_SELECT: 'ui.paramSelect', - CHAIN_EDIT: 'ui.chainEdit', - PRESET_LOAD: 'ui.presetLoad', - FREEZE_TOGGLE: 'ui.freezeToggle', -}); - -// ── Bus implementation ────────────────────────────────────────────── - -export class EventBus { - /** @param {AudioContext} [audioCtx] - optional, used for seq.* timestamps */ - constructor(audioCtx) { - this._listeners = new Map(); // event -> Set - this._wildcards = new Map(); // namespace prefix (e.g. "seq") -> Set - this._audioCtx = audioCtx ?? null; - } - - /** Provide or replace the AudioContext used for seq.* timestamps. */ - setAudioContext(ctx) { - this._audioCtx = ctx; - } - - /** - * Subscribe to an event or a wildcard namespace. - * on('seq.step', cb) — exact match - * on('seq.*', cb) — all events in the seq namespace - */ - on(event, callback) { - if (event.endsWith('.*')) { - const ns = event.slice(0, -2); // "seq.*" -> "seq" - if (!this._wildcards.has(ns)) this._wildcards.set(ns, new Set()); - this._wildcards.get(ns).add(callback); - } else { - if (!this._listeners.has(event)) this._listeners.set(event, new Set()); - this._listeners.get(event).add(callback); - } - } - - /** Unsubscribe. Mirrors the on() signature. */ - off(event, callback) { - if (event.endsWith('.*')) { - const ns = event.slice(0, -2); - const set = this._wildcards.get(ns); - if (set) { - set.delete(callback); - if (set.size === 0) this._wildcards.delete(ns); - } - } else { - const set = this._listeners.get(event); - if (set) { - set.delete(callback); - if (set.size === 0) this._listeners.delete(event); - } - } - } - - /** - * Emit an event. - * A `timestamp` field is added automatically: - * - seq.* events use AudioContext.currentTime (seconds) - * - all others use performance.now() (milliseconds) - */ - emit(event, data = {}) { - const ns = event.split('.')[0]; - const stamped = Object.assign({ timestamp: this._stamp(ns) }, data); - - // Exact listeners - const exact = this._listeners.get(event); - if (exact) { - for (const cb of exact) cb(stamped, event); - } - - // Wildcard listeners for this namespace - const wild = this._wildcards.get(ns); - if (wild) { - for (const cb of wild) cb(stamped, event); - } - } - - // ── private ── - - _stamp(ns) { - if (ns === 'seq' && this._audioCtx) return this._audioCtx.currentTime; - return performance.now(); - } -} - -// ── Singleton convenience ─────────────────────────────────────────── - -let _default = null; - -/** Return (and lazily create) the shared default bus. */ -export function getDefaultBus(audioCtx) { - if (!_default) _default = new EventBus(audioCtx); - else if (audioCtx) _default.setAudioContext(audioCtx); - return _default; -} diff --git a/playground/js/midi/midi-cc-map.js b/playground/js/midi/midi-cc-map.js deleted file mode 100644 index e94616e..0000000 --- a/playground/js/midi/midi-cc-map.js +++ /dev/null @@ -1,108 +0,0 @@ -// MIDI CC Map — user-defined CC parameter definitions -// Each param maps one MLP output to a MIDI CC message. - -const STORAGE_KEY = 'nisps-midi-cc-map'; -// Scoped key used when an engine ID is provided (avoids collision between engines). -// Falls back to STORAGE_KEY for legacy/unknown callers. - -// Default starter set — common CC numbers -const DEFAULT_CC_MAP = [ - { name: 'Cutoff', cc: 74, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Resonance', cc: 71, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Attack', cc: 73, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Release', cc: 72, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Mod Wheel', cc: 1, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Volume', cc: 7, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Pan', cc: 10, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, - { name: 'Expression',cc: 11, channel: 1, min: 0, max: 1, curve: 0.5, muted: false, fixedValue: 0.5 }, -]; - -// Well-known CC names for auto-labeling -const CC_NAMES = { - 1: 'Mod Wheel', 2: 'Breath', 5: 'Portamento Time', 7: 'Volume', 10: 'Pan', - 11: 'Expression', 64: 'Sustain', 65: 'Portamento', 66: 'Sostenuto', - 67: 'Soft Pedal', 70: 'Sound Variation', 71: 'Resonance', 72: 'Release', - 73: 'Attack', 74: 'Cutoff', 75: 'Decay', 76: 'Vib Rate', 77: 'Vib Depth', - 78: 'Vib Delay', 91: 'Reverb', 92: 'Tremolo', 93: 'Chorus', 94: 'Detune', - 95: 'Phaser', -}; - -/** - * Create a fresh CC param with defaults. - * @param {number} [cc=74] — CC number - * @param {number} [channel=1] — MIDI channel 1-16 - * @returns {object} CC param definition - */ -export function createCCParam(cc = 74, channel = 1) { - return { - name: CC_NAMES[cc] || `CC ${cc}`, - cc, - channel, - min: 0, - max: 1, - curve: 0.5, - muted: false, - fixedValue: 0.5, - }; -} - -/** - * Load CC map from localStorage, or return default. - * @param {string} [key] — optional storage key (default: STORAGE_KEY) - * @returns {Array} CC param definitions - */ -export function loadCCMap(key = STORAGE_KEY) { - try { - const raw = localStorage.getItem(key); - if (raw) { - const saved = JSON.parse(raw); - if (Array.isArray(saved) && saved.length > 0) return saved; - } - } catch (e) { - console.warn('[MIDI CC] Failed to load map:', e); - } - return DEFAULT_CC_MAP.map(p => ({ ...p })); -} - -/** - * Save CC map to localStorage. - * @param {Array} ccMap - * @param {string} [key] — optional storage key (default: STORAGE_KEY) - */ -export function saveCCMap(ccMap, key = STORAGE_KEY) { - try { - localStorage.setItem(key, JSON.stringify(ccMap)); - } catch (e) { - console.warn('[MIDI CC] Failed to save map:', e); - } -} - -/** - * Export CC map as JSON string (for sharing). - * @param {Array} ccMap - * @returns {string} - */ -export function exportCCMap(ccMap) { - return JSON.stringify(ccMap, null, 2); -} - -/** - * Import CC map from JSON string. - * @param {string} json - * @returns {Array|null} parsed map or null on failure - */ -export function importCCMap(json) { - try { - const parsed = JSON.parse(json); - if (!Array.isArray(parsed) || parsed.length === 0) return null; - // Validate shape - for (const p of parsed) { - if (typeof p.cc !== 'number' || typeof p.channel !== 'number') return null; - } - return parsed; - } catch (e) { - return null; - } -} - -export { CC_NAMES, DEFAULT_CC_MAP }; diff --git a/playground/js/midi/midi-cc-presets.js b/playground/js/midi/midi-cc-presets.js deleted file mode 100644 index 5fee26f..0000000 --- a/playground/js/midi/midi-cc-presets.js +++ /dev/null @@ -1,60 +0,0 @@ -// MIDI CC Presets — bundled device configurations -// Each preset is a JSON file with { name, description, channel, params[] } - -const PRESET_URLS = { - 'polybrute': './js/midi/presets/polybrute.json', -}; - -// In-memory cache -const _cache = new Map(); - -/** - * Get list of available preset IDs and names. - * @returns {Array<{id: string, name: string}>} - */ -export function listPresets() { - return [ - { id: 'polybrute', name: 'Arturia PolyBrute' }, - ]; -} - -/** - * Load a preset by ID. Returns the parsed preset object. - * @param {string} id — preset identifier - * @returns {Promise<{name: string, description: string, channel: number, params: Array}>} - */ -export async function loadPreset(id) { - if (_cache.has(id)) return _cache.get(id); - - const url = PRESET_URLS[id]; - if (!url) throw new Error(`Unknown MIDI CC preset: ${id}`); - - const resp = await fetch(url); - if (!resp.ok) throw new Error(`Failed to load preset ${id}: ${resp.status}`); - - const preset = await resp.json(); - _cache.set(id, preset); - return preset; -} - -/** - * Load a preset from a user-provided JSON file (File object). - * @param {File} file - * @returns {Promise<{name: string, description: string, channel: number, params: Array}|null>} - */ -export async function loadPresetFromFile(file) { - try { - const text = await file.text(); - const preset = JSON.parse(text); - if (!preset.params || !Array.isArray(preset.params) || preset.params.length === 0) { - return null; - } - // Validate param shape - for (const p of preset.params) { - if (typeof p.cc !== 'number' || typeof p.channel !== 'number') return null; - } - return preset; - } catch (e) { - return null; - } -} diff --git a/playground/js/midi/midi-output.js b/playground/js/midi/midi-output.js deleted file mode 100644 index 479d4d6..0000000 --- a/playground/js/midi/midi-output.js +++ /dev/null @@ -1,147 +0,0 @@ -// MIDI Output — sends CC messages to external MIDI devices -// Uses Web MIDI API, complementary to midi-input.js - -export class MIDIOutput { - constructor() { - this.enabled = false; - this.midiAccess = null; - this.selectedOutputId = null; - this.activeOutput = null; - this._onStatusChange = null; - this._onOutputsChange = null; - this._boundOnStateChange = this._onStateChange.bind(this); - - // Throttling - this._lastSentCC = new Map(); // key: `${channel}-${cc}` -> value - this._lastSendTime = 0; - this.sendInterval = 50; // ms — max ~20 sends/sec - this.deadZone = 1; // skip if CC value hasn't changed by at least 1 - } - - set onStatusChange(fn) { this._onStatusChange = fn; } - set onOutputsChange(fn) { this._onOutputsChange = fn; } - - _status(msg) { - console.log('[MIDI Out]', msg); - this._onStatusChange?.(msg); - } - - /** Request MIDI access. Returns true if available. */ - async init() { - if (this.midiAccess) return true; - - if (!navigator.requestMIDIAccess) { - this._status('Web MIDI not supported'); - return false; - } - - try { - this.midiAccess = await navigator.requestMIDIAccess({ sysex: false }); - this.midiAccess.onstatechange = this._boundOnStateChange; - this._status('MIDI output available'); - return true; - } catch (err) { - this._status(`MIDI access denied: ${err.message}`); - return false; - } - } - - /** Get list of available MIDI outputs as [{id, name}] */ - getOutputs() { - if (!this.midiAccess) return []; - const outputs = []; - for (const [id, output] of this.midiAccess.outputs) { - outputs.push({ id, name: output.name || `MIDI Output ${id}` }); - } - return outputs; - } - - /** Select a specific MIDI output by id */ - selectOutput(outputId) { - this.selectedOutputId = outputId; - this.activeOutput = null; - this._lastSentCC.clear(); - - if (!this.midiAccess || !outputId) return; - const output = this.midiAccess.outputs.get(outputId); - if (output) { - this.activeOutput = output; - this._status(`Selected: ${output.name}`); - } - } - - enable() { - if (this.enabled) return; - this.enabled = true; - this._status('MIDI output enabled'); - } - - disable() { - if (!this.enabled) return; - this.enabled = false; - this._status('MIDI output disabled'); - } - - toggle() { - if (this.enabled) this.disable(); - else this.enable(); - } - - /** - * Send a CC message. channel is 1-16, cc is 0-127, value is 0-127. - * Applies dead-zone filtering (skips if value unchanged). - */ - sendCC(channel, cc, value) { - if (!this.enabled || !this.activeOutput) return; - - const ch = Math.max(0, Math.min(15, (channel - 1) | 0)); - const ccNum = Math.max(0, Math.min(127, cc | 0)); - const val = Math.max(0, Math.min(127, value | 0)); - - // Dead-zone filter - const key = `${ch}-${ccNum}`; - const lastVal = this._lastSentCC.get(key); - if (lastVal !== undefined && Math.abs(val - lastVal) < this.deadZone) return; - - this._lastSentCC.set(key, val); - this.activeOutput.send([0xB0 | ch, ccNum, val]); - } - - /** - * Send multiple CC values at once (throttled). - * @param {Array<{channel: number, cc: number, value: number}>} messages - */ - sendBatch(messages) { - if (!this.enabled || !this.activeOutput) return; - - const now = performance.now(); - if (now - this._lastSendTime < this.sendInterval) return; - this._lastSendTime = now; - - for (const msg of messages) { - this.sendCC(msg.channel, msg.cc, msg.value); - } - } - - _onStateChange(e) { - this._onOutputsChange?.(this.getOutputs()); - // Reconnect if selected device was re-plugged - if (this.selectedOutputId && this.enabled) { - const output = this.midiAccess.outputs.get(this.selectedOutputId); - if (output) { - this.activeOutput = output; - } else { - this.activeOutput = null; - } - } - } - - destroy() { - this.disable(); - if (this.midiAccess) { - this.midiAccess.onstatechange = null; - } - this.midiAccess = null; - this.activeOutput = null; - } -} diff --git a/playground/js/midi/presets/polybrute.json b/playground/js/midi/presets/polybrute.json deleted file mode 100644 index dc48c62..0000000 --- a/playground/js/midi/presets/polybrute.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "Arturia PolyBrute", - "description": "Full CC map for PolyBrute analog synthesizer (firmware 3.0+)", - "channel": 1, - "params": [ - { "name": "Ladder Cutoff", "cc": 25, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Steiner Cutoff", "cc": 23, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Master Cutoff", "cc": 27, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Ladder Resonance", "cc": 87, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Steiner Resonance", "cc": 83, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Ladder Disto", "cc": 85, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Steiner Brute", "cc": 82, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Ladder Level", "cc": 8, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Steiner Level", "cc": 7, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 1 Tune", "cc": 66, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 Tune", "cc": 72, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 1 Metalizer", "cc": 70, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 1 Saw/Tri", "cc": 17, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 1 Saw/Sq", "cc": 12, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 1 PW", "cc": 69, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 Sub Mix", "cc": 14, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 Saw/Tri", "cc": 15, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 Saw/Sq", "cc": 16, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 PW", "cc": 75, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO 2 FM 2>1", "cc": 77, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mixer VCO 1", "cc": 18, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mixer VCO 2", "cc": 19, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mixer Noise", "cc": 21, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Noise Color", "cc": 22, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCO2>VCF1", "cc": 79, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Noise>VCF2", "cc": 80, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Amt Lad", "cc": 26, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Amt Stein", "cc": 24, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Lad Series/Para", "cc": 86, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Stein LP>HP>BP", "cc": 81, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Key Track", "cc": 71, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Attack", "cc": 102, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Decay", "cc": 103, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Sustain", "cc": 28, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCF Env Release", "cc": 104, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCA Env Attack", "cc": 105, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCA Env Decay", "cc": 106, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCA Env Sustain", "cc": 29, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "VCA Env Release", "cc": 107, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Env Delay", "cc": 108, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Env Attack", "cc": 109, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Env Decay", "cc": 110, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Env Sustain", "cc": 30, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Env Release", "cc": 111, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 1 Phase", "cc": 90, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 1 Rate", "cc": 91, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 2 Fade In", "cc": 92, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 2 Rate", "cc": 93, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 3 Curve", "cc": 67, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 3 Symmetry", "cc": 68, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "LFO 3 Rate", "cc": 73, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Intensity", "cc": 13, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Delay Level", "cc": 31, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Delay Time", "cc": 112, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Delay Regen", "cc": 113, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Reverb Level", "cc": 2, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Reverb Time", "cc": 78, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Reverb Damping", "cc": 76, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Stereo", "cc": 10, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Mod Wheel", "cc": 1, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }, - { "name": "Glide", "cc": 5, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 } - ] -} diff --git a/playground/js/nisps/dataset.js b/playground/js/nisps/dataset.js deleted file mode 100644 index 8b5c40e..0000000 --- a/playground/js/nisps/dataset.js +++ /dev/null @@ -1,121 +0,0 @@ -// NISPS Dataset - faithful port of nisps-core/include/nisps/dataset.hpp -// Manages feature-label pairs for training - -export class Dataset { - constructor(maxExamples = 100) { - this.features = []; - this.labels = []; - this.maxExamples = maxExamples; - } - - add(feature, label) { - if (this.features.length > 0) { - if (feature.length !== this.features[0].length || label.length !== this.labels[0].length) { - return false; - } - } - if (this.features.length >= this.maxExamples) { - // FIFO: remove oldest - this.features.shift(); - this.labels.shift(); - } - this.features.push([...feature]); - this.labels.push([...label]); - return true; - } - - clear() { - this.features = []; - this.labels = []; - } - - getFeatures(withBias = true) { - return this.features.map(f => withBias ? [...f, 1.0] : [...f]); - } - - getLabels() { - return this.labels; - } - - /** - * Compute per-sample training weights. Returns Float32Array normalized to sum to 1. - * @param {'global'|'local'|'combined'} mode - * @param {object} params - * @param {number} params.recencyBias - 0 = uniform, 1 = strong recency (global/combined) - * @param {number[]} [params.queryInput] - current input position (local/combined) - * @param {number} [params.radius] - spatial radius in input space (local/combined), default 0.15 - * @returns {Float32Array} weights summing to 1 - */ - computeWeights(mode = 'global', params = {}) { - const n = this.features.length; - if (n === 0) return new Float32Array(0); - if (n === 1) return new Float32Array([1.0]); - - const weights = new Float32Array(n).fill(1.0); - - // Global recency: exponential decay — newest = 1, each older *= decay - if (mode === 'global' || mode === 'combined') { - const bias = params.recencyBias ?? 0.6; - if (bias > 0) { - // decay per step: at bias=1, decay=0.7 (newest ~10x oldest for 10 examples) - // at bias=0.5, decay=0.85 (gentler) - const decay = 1 - 0.3 * bias; - for (let i = n - 2; i >= 0; i--) { - weights[i] = weights[i + 1] * decay; - } - } - } - - // Local recency: newer examples near the query suppress older nearby ones - if ((mode === 'local' || mode === 'combined') && params.queryInput) { - const query = params.queryInput; - const radius = params.radius ?? 0.15; - const radiusSq = radius * radius; - - for (let i = 0; i < n; i++) { - const feat = this.features[i]; - // Distance from this example to the query point - let distSq = 0; - for (let d = 0; d < feat.length; d++) { - const diff = feat[d] - (query[d] ?? 0); - distSq += diff * diff; - } - - if (distSq < radiusSq) { - // Count newer examples also within the radius - const proximity = 1 - Math.sqrt(distSq) / radius; // 1 = on top, 0 = at edge - let newerNearby = 0; - for (let j = i + 1; j < n; j++) { - let djSq = 0; - for (let d = 0; d < feat.length; d++) { - const diff = feat[d] - this.features[j][d]; - djSq += diff * diff; - } - if (djSq < radiusSq) newerNearby++; - } - // Suppress: more newer neighbors + closer to query = more suppression - if (newerNearby > 0) { - weights[i] *= Math.pow(1 - proximity, newerNearby); - } - } - } - } - - // Normalize to sum to 1 - let sum = 0; - for (let i = 0; i < n; i++) sum += weights[i]; - if (sum > 0) { - for (let i = 0; i < n; i++) weights[i] /= sum; - } - - return weights; - } - - get size() { - return this.features.length; - } - - isEmpty() { - return this.features.length === 0; - } -} diff --git a/playground/js/nisps/iml.js b/playground/js/nisps/iml.js deleted file mode 100644 index f6cd233..0000000 --- a/playground/js/nisps/iml.js +++ /dev/null @@ -1,324 +0,0 @@ -// NISPS IML - faithful port of nisps-core/include/nisps/iml.hpp + iml_impl.hpp -// Interactive Machine Learning interface - -import { MLP } from './mlp.js'; -import { Dataset } from './dataset.js'; - -export class IML { - /** - * @param {number} nInputs - * @param {number} nOutputs - * @param {number[]} hiddenLayers - * @param {number} maxIterations - * @param {number} learningRate - * @param {number} convergenceThreshold - */ - constructor( - nInputs, - nOutputs, - hiddenLayers = [10, 10, 14], - maxIterations = 1000, - learningRate = 1.0, - convergenceThreshold = 0.00001 - ) { - this.nInputs = nInputs; - this.nOutputs = nOutputs; - this.maxIterations = maxIterations; - this.learningRate = learningRate; - this.convergenceThreshold = convergenceThreshold; - - // Build layer sizes: input+bias, hidden..., output - const BIAS = 1; - const layerSizes = [nInputs + BIAS, ...hiddenLayers, nOutputs]; - - // Activation functions: RELU for hidden, SIGMOID for output - const activationNames = [ - ...hiddenLayers.map(() => 'relu'), - 'sigmoid', - ]; - - this.dataset = new Dataset(100); - this.mlp = new MLP(layerSizes, activationNames); - - this.inputState = new Array(nInputs).fill(0.5); - this.outputState = new Array(nOutputs).fill(0); - this.mode = 'inference'; - this.performInference = true; - this.inputUpdated = true; - this.storedWeights = null; - this.weightsRandomised = false; - this.lastLoss = null; - this.bestLoss = null; - this.lossHistory = []; - this.totalTrainingIterations = 0; - this.logFn = null; - this.recencyBias = 0.6; // 0 = uniform, 1 = strong recency - this.weightingMode = 'global'; // 'global' | 'local' | 'combined' - this.localRadius = 0.15; // input-space radius for local weighting - } - - setLogger(fn) { - this.logFn = fn; - } - - log(msg) { - if (this.logFn) this.logFn(msg); - } - - setInput(index, value) { - if (index >= this.nInputs) return; - this.inputState[index] = Math.max(0, Math.min(1, value)); - this.inputUpdated = true; - } - - setInputs(values) { - for (let i = 0; i < values.length && i < this.nInputs; i++) { - this.inputState[i] = Math.max(0, Math.min(1, values[i])); - } - this.inputUpdated = true; - } - - getOutputs() { - return this.outputState; - } - - setOutput(index, value) { - if (index >= this.nOutputs) return; - this.outputState[index] = Math.max(0, Math.min(1, value)); - } - - setOutputs(values) { - for (let i = 0; i < values.length && i < this.nOutputs; i++) { - this.outputState[i] = Math.max(0, Math.min(1, values[i])); - } - } - - process() { - if (!this.performInference || !this.inputUpdated) return; - - // Add bias term - const inputWithBias = [...this.inputState, 1.0]; - const { output } = this.mlp.getOutput(inputWithBias); - this.outputState = output; - this.inputUpdated = false; - } - - getMode() { - return this.mode; - } - - setMode(mode) { - if (mode === 'inference' && this.mode === 'training') { - this.train(); - } - this.mode = mode; - } - - // Two-step save example (hardware workflow) - saveExample() { - if (this.performInference) { - this.performInference = false; - this.log('Move to desired output position...'); - return; - } - - this.dataset.add(this.inputState, this.outputState); - this.performInference = true; - - // Run inference - const inputWithBias = [...this.inputState, 1.0]; - const { output } = this.mlp.getOutput(inputWithBias); - this.outputState = output; - - this.log('Example saved.'); - } - - // Direct programmatic example addition - addExample(inputs, outputs) { - const inVec = inputs.slice(0, this.nInputs); - while (inVec.length < this.nInputs) inVec.push(0); - const outVec = outputs.slice(0, this.nOutputs); - while (outVec.length < this.nOutputs) outVec.push(0); - this.dataset.add(inVec, outVec); - } - - clearDataset() { - this.dataset.clear(); - this.log('Dataset cleared.'); - } - - randomiseWeights(spread = 0) { - this.storedWeights = this.mlp.getWeights(); - this.mlp.drawWeights(spread); - this.weightsRandomised = true; - - // Run inference to show effect - const inputWithBias = [...this.inputState, 1.0]; - const { output } = this.mlp.getOutput(inputWithBias); - this.outputState = output; - - this.log('Weights randomised.'); - } - - // Add Gaussian noise to weights (for RL exploration) - // spread: 0 = flat noise, 1 = Xavier-scaled per layer - // outputPinMask: optional Uint8Array[nOutputs], 1 = skip that output node - moveWeights(speed, spread = 0, outputPinMask = null) { - this.mlp.moveWeights(speed, spread, outputPinMask); - // Run inference to show effect - this.inputUpdated = true; - this.process(); - } - - train(options = {}) { - // Restore weights if randomised - if (this.weightsRandomised && this.storedWeights) { - this.mlp.setWeights(this.storedWeights); - this.weightsRandomised = false; - } - - const features = this.dataset.getFeatures(true); // with bias - const labels = this.dataset.getLabels(); - - if (features.length === 0 || labels.length === 0) { - this.log('Empty dataset, skipping training.'); - return null; - } - - const sampleWeights = this.dataset.computeWeights(this.weightingMode, { - recencyBias: this.recencyBias, - queryInput: this.inputState, - radius: this.localRadius, - }); - - this.log('Training...'); - this.lastLoss = this.mlp.train( - features, - labels, - this.learningRate, - this.maxIterations, - this.convergenceThreshold, - { ...options, sampleWeights } - ); - - const latestHistory = this.mlp.lastTrainingHistory || []; - if (latestHistory.length > 0) { - this.lossHistory.push(...latestHistory); - this.totalTrainingIterations += latestHistory.length; - if (this.lossHistory.length > 1200) { - this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200); - } - const runBest = Math.min(...latestHistory); - this.bestLoss = this.bestLoss === null ? runBest : Math.min(this.bestLoss, runBest); - } - - // Run inference after training - const inputWithBias = [...this.inputState, 1.0]; - const { output } = this.mlp.getOutput(inputWithBias); - this.outputState = output; - - this.log(`Training complete. Loss: ${this.lastLoss.toFixed(6)}`); - return this.lastLoss; - } - - get exampleCount() { - return this.dataset.size; - } - - /** - * Export state as a .nisps-compatible JSON object. - * The weight format matches the VCV Rack serialization: a 3D array of - * connection weights [layer][node][weight], without bias values. - * Bias is stored in a separate parallel structure for lossless round-trip - * within the webapp, but VCV Rack will ignore it. - * - * @returns {object} .nisps JSON object - */ - exportState() { - const internalWeights = this.mlp.getWeights(); - - // Convert from JS format [{weights, bias}, ...] per layer - // to .nisps format: float[][][] (connection weights only) - const weights = internalWeights.map(layer => - layer.map(node => node.weights) - ); - - // Also capture bias values for lossless webapp round-trip - const biases = internalWeights.map(layer => - layer.map(node => node.bias) - ); - - // Features stored without bias term - const features = this.dataset.features.map(f => [...f]); - const labels = this.dataset.labels.map(l => [...l]); - - const activations = this.mlp.layers.map(layer => layer.activationName); - - return { - version: 1, - weights: weights, - biases: biases, - examples: { features, labels }, - mlpConfig: { - layers: [...this.mlp.layersNodes], - activations: activations, - }, - }; - } - - /** - * Import state from a .nisps JSON object. - * Accepts both the VCV Rack format (3D weight array without bias) and - * the webapp extended format (with separate biases array). - * - * @param {object} state - Parsed .nisps JSON - * @throws {Error} If version is invalid or architecture mismatches - */ - importState(state) { - if (!state || !state.version || state.version < 1) { - throw new Error('Invalid .nisps format: missing or unsupported version'); - } - - // Validate architecture compatibility if mlpConfig is present - if (state.mlpConfig && state.mlpConfig.layers) { - const expected = this.mlp.layersNodes; - const actual = state.mlpConfig.layers; - if (expected.length !== actual.length || - !expected.every((v, i) => v === actual[i])) { - throw new Error( - `Architecture mismatch: expected [${expected}], got [${actual}]` - ); - } - } - - // Load weights - if (state.weights) { - // Convert from .nisps 3D format to JS internal format - const internalWeights = state.weights.map((layer, li) => - layer.map((nodeWeights, ni) => ({ - weights: Array.isArray(nodeWeights) ? [...nodeWeights] : nodeWeights, - bias: (state.biases && state.biases[li] && state.biases[li][ni] !== undefined) - ? state.biases[li][ni] - : 0, - })) - ); - this.mlp.setWeights(internalWeights); - } - - // Load examples - if (state.examples) { - this.dataset.clear(); - const { features, labels } = state.examples; - if (features && labels) { - const count = Math.min(features.length, labels.length); - for (let i = 0; i < count; i++) { - this.dataset.add(features[i], labels[i]); - } - } - } - - // Re-run inference with current inputs - this.inputUpdated = true; - this.process(); - } -} diff --git a/playground/js/nisps/layer.js b/playground/js/nisps/layer.js deleted file mode 100644 index b39eaa9..0000000 --- a/playground/js/nisps/layer.js +++ /dev/null @@ -1,133 +0,0 @@ -// NISPS Layer - faithful port of nisps-core/include/nisps/layer.hpp -// Layer of neural network nodes with shared activation function - -import { Node } from './node.js'; - -// Activation functions matching C++ utils.hpp exactly -const RELU_SLOPE = 0.01; // kReLUSlope - -export const activations = { - relu: x => x > 0 ? x : RELU_SLOPE * x, - derivRelu: x => x > 0 ? 1 : RELU_SLOPE, - sigmoid: x => 1 / (1 + Math.exp(-x)), - derivSigmoid: x => { - const s = 1 / (1 + Math.exp(-x)); - return s * (1 - s); - }, - linear: x => x, - derivLinear: () => 1, - tanh: x => Math.tanh(x), - derivTanh: x => 1 - Math.pow(Math.tanh(x), 2), -}; - -// Map activation names to [fn, derivFn] pairs -const activationPairs = { - relu: [activations.relu, activations.derivRelu], - sigmoid: [activations.sigmoid, activations.derivSigmoid], - linear: [activations.linear, activations.derivLinear], - tanh: [activations.tanh, activations.derivTanh], -}; - -export class Layer { - constructor(numInputsPerNode, numNodes, activationName, useConstantInit = true, constantInit = 0.5) { - this.numInputsPerNode = numInputsPerNode; - this.numNodes = numNodes; - this.nodes = []; - - this.activationName = activationName; - const pair = activationPairs[activationName]; - this.activationFn = pair[0]; - this.derivActivationFn = pair[1]; - - for (let i = 0; i < numNodes; i++) { - this.nodes.push(new Node(numInputsPerNode, useConstantInit, constantInit)); - } - } - - getOutputAfterActivation(input) { - const output = new Array(this.numNodes); - for (let i = 0; i < this.numNodes; i++) { - output[i] = this.nodes[i].getOutputAfterActivation(input, this.activationFn); - } - return output; - } - - initializeGradientAccumulators() { - for (const node of this.nodes) { - node.initializeGradientAccumulator(); - } - } - - clearGradientAccumulators() { - for (const node of this.nodes) { - node.clearGradientAccumulator(); - } - } - - // Backprop with accumulation or direct update - updateWeights(inputLayerActivation, derivError, learningRate, accumulate = false) { - const deltas = new Array(this.numInputsPerNode).fill(0); - - if (accumulate) { - // Accumulate gradients mode - for (let i = 0; i < this.nodes.length; i++) { - const dE_doj = derivError[i]; - const doj_dnetj = this.derivActivationFn(this.nodes[i].innerProd); - const errorSignal = dE_doj * doj_dnetj; - - this.nodes[i].accumulateGradients(inputLayerActivation, errorSignal); - - for (let j = 0; j < this.numInputsPerNode; j++) { - deltas[j] += errorSignal * this.nodes[i].weights[j]; - } - } - } else { - // Direct update mode - for (let i = 0; i < this.nodes.length; i++) { - const dE_doj = derivError[i]; - const doj_dnetj = this.derivActivationFn(this.nodes[i].innerProd); - - for (let j = 0; j < this.numInputsPerNode; j++) { - deltas[j] += dE_doj * doj_dnetj * this.nodes[i].weights[j]; - const dnetj_dwij = inputLayerActivation[j]; - this.nodes[i].updateWeight(j, -(dE_doj * doj_dnetj * dnetj_dwij), learningRate); - } - } - } - return deltas; - } - - applyAccumulatedGradients(learningRate, batchSizeInv) { - for (const node of this.nodes) { - node.applyAccumulatedGradients(learningRate, batchSizeInv); - } - } - - getGradSumSquared(batchSizeInv) { - let sumsq = 0; - for (const node of this.nodes) { - sumsq += node.getGradSumSquared(batchSizeInv); - } - return sumsq; - } - - scaleAccumulatedGradients(clipCoef) { - for (const node of this.nodes) { - node.scaleAccumulatedGradients(clipCoef); - } - } - - resetOptimizerState() { - for (const node of this.nodes) { - node.resetOptimizerState(); - } - } - - checkAndFixWeights() { - let had = false; - for (const node of this.nodes) { - had |= node.checkAndFixWeights(); - } - return had; - } -} diff --git a/playground/js/nisps/mlp.js b/playground/js/nisps/mlp.js deleted file mode 100644 index 395a072..0000000 --- a/playground/js/nisps/mlp.js +++ /dev/null @@ -1,270 +0,0 @@ -// NISPS MLP - faithful port of nisps-core/include/nisps/mlp.hpp + mlp_impl.hpp -// Multi-layer perceptron with Train, TrainBatch, GetOutput, weight management - -import { Layer } from './layer.js'; - -// MSE loss function - port of loss.hpp -function mseLoss(expected, actual, lossDeriv, sampleSizeReciprocal) { - let accumLoss = 0; - const oneOverN = 1 / actual.length; - - for (let j = 0; j < actual.length; j++) { - const diff = expected[j] - actual[j]; - accumLoss += (diff * diff) * oneOverN; - lossDeriv[j] = -2 * oneOverN * diff * sampleSizeReciprocal; - } - accumLoss *= sampleSizeReciprocal; - return accumLoss; -} - -// Fisher-Yates shuffle -function shuffleArray(arr) { - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } -} - -export class MLP { - /** - * @param {number[]} layersNodes - e.g. [3, 10, 10, 14, 8] - * @param {string[]} layersActivations - e.g. ['relu', 'relu', 'relu', 'sigmoid'] - */ - constructor(layersNodes, layersActivations) { - this.layersNodes = layersNodes; - this.numInputs = layersNodes[0]; - this.numOutputs = layersNodes[layersNodes.length - 1]; - this.numHiddenLayers = layersNodes.length - 2; - this.layers = []; - this.progressCallback = null; - - for (let i = 0; i < layersNodes.length - 1; i++) { - this.layers.push( - new Layer(layersNodes[i], layersNodes[i + 1], layersActivations[i], false) - ); - } - } - - getOutput(input, forInference = true) { - if (input.length !== this.numInputs) return null; - - let tempIn = [...input]; - let tempOut; - const allActivations = []; - - for (let i = 0; i < this.layers.length; i++) { - if (i > 0) { - allActivations.push(tempIn); - tempIn = tempOut; - } - tempOut = this.layers[i].getOutputAfterActivation(tempIn); - } - - // Push last layer's input activation - allActivations.push(tempIn); - - return { output: tempOut, activations: allActivations }; - } - - // Per-sample SGD training (Train method from C++) - // sampleWeights: optional Float32Array of per-sample weights (normalized, sum to 1) - train(features, labels, learningRate, maxIterations = 1000, minError = 0.00001, options = {}) { - const sampleSizeRecip = 1 / features.length; - const sampleWeights = options.sampleWeights || null; - let loss = 0; - const history = []; - const onIteration = typeof options.onIteration === 'function' ? options.onIteration : null; - - for (let iter = 0; iter < maxIterations; iter++) { - loss = 0; - - for (let s = 0; s < features.length; s++) { - const weight = sampleWeights ? sampleWeights[s] : sampleSizeRecip; - const { output, activations } = this.getOutput(features[s], false); - const derivError = new Array(output.length); - - loss += mseLoss(labels[s], output, derivError, weight); - - // Backprop with direct weight update - let tempDerivError = derivError; - for (let i = this.numHiddenLayers; i >= 0; i--) { - const deltas = this.layers[i].updateWeights(activations[i], tempDerivError, learningRate, false); - if (i > 0) tempDerivError = deltas; - } - } - - // Custom weights already normalized — loss is already correctly scaled - if (!sampleWeights) loss *= sampleSizeRecip; - history.push(loss); - - if (onIteration) onIteration(iter, loss); - - if (this.progressCallback && (iter & 0x1F) === 0) { - this.progressCallback(iter, loss); - } - - if (loss < minError) break; - } - - this.lastTrainingHistory = history; - return loss; - } - - // Batch training with RMSProp (TrainBatch from C++) - trainBatch(features, labels, learningRate, maxIterations = 1000, batchSize = 8, minError = 0.00001, options = {}) { - const nSamples = features.length; - const nBatches = Math.ceil(nSamples / batchSize); - let epochLoss = 0; - const history = []; - const onIteration = typeof options.onIteration === 'function' ? options.onIteration : null; - - for (let iter = 0; iter < maxIterations; iter++) { - epochLoss = 0; - - // Shuffle indices - const indices = Array.from({ length: nSamples }, (_, i) => i); - shuffleArray(indices); - - let sampleIdx = 0; - - for (let batch = 0; batch < nBatches; batch++) { - const currentBatchSize = Math.min(batchSize, nSamples - sampleIdx); - const batchSizeRecip = 1 / currentBatchSize; - - // Initialize gradient accumulators - for (const layer of this.layers) { - layer.initializeGradientAccumulators(); - } - - let batchLoss = 0; - - for (let i = 0; i < currentBatchSize; i++) { - const idx = indices[sampleIdx++]; - const { output, activations } = this.getOutput(features[idx], false); - const derivError = new Array(output.length); - - batchLoss += mseLoss(labels[idx], output, derivError, 1.0); - - // Backprop with accumulation - let tempDerivError = derivError; - for (let li = this.numHiddenLayers; li >= 0; li--) { - const deltas = this.layers[li].updateWeights(activations[li], tempDerivError, 0, true); - if (li > 0) tempDerivError = deltas; - } - } - - // Gradient clipping (norm > 5.0) - let gradSumSq = 0; - for (const layer of this.layers) { - gradSumSq += layer.getGradSumSquared(batchSizeRecip); - } - const gradNorm = Math.sqrt(gradSumSq); - - if (gradNorm > 5.0) { - const clipCoef = 5.0 / gradNorm; - for (const layer of this.layers) { - layer.scaleAccumulatedGradients(clipCoef); - } - } - - // Apply accumulated gradients - for (const layer of this.layers) { - layer.applyAccumulatedGradients(learningRate, batchSizeRecip); - } - - epochLoss += batchLoss / currentBatchSize; - } - - epochLoss /= nBatches; - history.push(epochLoss); - if (onIteration) onIteration(iter, epochLoss); - - if (this.progressCallback) { - this.progressCallback(iter, epochLoss); - } - - if (epochLoss < minError) break; - } - - this.lastTrainingHistory = history; - return epochLoss; - } - - getWeights() { - return this.layers.map(layer => - layer.nodes.map(node => ({ - weights: node.getWeightsCopy(), - bias: node.bias, - })) - ); - } - - setWeights(weights) { - for (let l = 0; l < this.layers.length; l++) { - for (let n = 0; n < this.layers[l].nodes.length; n++) { - this.layers[l].nodes[n].setWeights(weights[l][n].weights); - this.layers[l].nodes[n].bias = weights[l][n].bias; - } - } - } - - // DrawWeights - randomize all weights - // spread: 0 = uniform [-1,1] (polarised sigmoid outputs), 1 = Xavier-scaled (centered outputs) - drawWeights(spread = 0) { - for (let l = 0; l < this.layers.length; l++) { - const fanIn = this.layersNodes[l]; - const xavierScale = 1 / Math.sqrt(fanIn); - const scale = 1 * (1 - spread) + xavierScale * spread; - for (const node of this.layers[l].nodes) { - for (let j = 0; j < node.weights.length; j++) { - node.weights[j] = (Math.random() * 2 - 1) * scale; - } - node.bias = 0; - } - } - } - - // MoveWeights - add Gaussian noise (port of gen_randn) - // spread: 0 = flat noise across all layers (original), 1 = scale noise by 1/sqrt(fan_in) - // per layer so perturbations stay proportional to Xavier-scale weights and don't - // saturate sigmoid outputs. - // Weight decay (proportional to spread) shrinks weights toward zero before adding noise, - // preventing unbounded magnitude drift from repeated thumbs-down. At spread=0 there is - // no decay (original behavior). At spread=1 each call decays weights by ~10%, creating - // a natural equilibrium where exploration can't permanently saturate sigmoid. - // - // outputPinMask: optional Uint8Array[numOutputs]. If provided, output-layer nodes where - // mask[i] === 1 are skipped (their weights are not perturbed). Only affects the last layer. - moveWeights(speed, spread = 0, outputPinMask = null) { - const decay = 1 - 0.1 * spread; // spread=0 → 1.0 (no decay), spread=1 → 0.9 - for (let l = 0; l < this.layers.length; l++) { - const fanIn = this.layersNodes[l]; - const xavierScale = 1 / Math.sqrt(fanIn); - const layerScale = 1 * (1 - spread) + xavierScale * spread; - const isOutputLayer = l === this.layers.length - 1; - const nodes = this.layers[l].nodes; - for (let ni = 0; ni < nodes.length; ni++) { - // Skip pinned output nodes in the final layer - if (isOutputLayer && outputPinMask && outputPinMask[ni]) continue; - - const node = nodes[ni]; - for (let j = 0; j < node.weights.length; j++) { - // Decay toward zero to prevent magnitude drift - node.weights[j] *= decay; - // gen_randn: sum of 3 uniform randoms * kN_times * stddev + mean - let accum = 0; - for (let n = 0; n < 3; n++) { - accum += Math.random() * 2 - 1; // gen_rand with range 2.0 - } - node.weights[j] += 3 * accum * speed * layerScale; - } - } - } - } - - resetOptimizerState() { - for (const layer of this.layers) { - layer.resetOptimizerState(); - } - } -} diff --git a/playground/js/nisps/nisps-wasm-worker.js b/playground/js/nisps/nisps-wasm-worker.js deleted file mode 100644 index 6f88909..0000000 --- a/playground/js/nisps/nisps-wasm-worker.js +++ /dev/null @@ -1,134 +0,0 @@ -// Web Worker for async WASM training (module worker) -// Loads its own nisps WASM instance for off-thread training - -import NispsModule from '../../wasm/nisps.js'; - -let mod = null; -let w = null; -let mlp = null; -let currentLayerSizes = null; - -async function ensureModule() { - if (mod) return; - mod = await NispsModule(); - w = { - mod, - create: mod.cwrap('nisps_mlp_create', 'number', ['number', 'number', 'number', 'number']), - destroy: mod.cwrap('nisps_mlp_destroy', null, ['number']), - weightCount: mod.cwrap('nisps_mlp_weight_count', 'number', ['number']), - getWeights: mod.cwrap('nisps_mlp_get_weights', null, ['number', 'number']), - setWeights: mod.cwrap('nisps_mlp_set_weights', null, ['number', 'number']), - train: mod.cwrap('nisps_mlp_train', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), - trainEx: mod.cwrap('nisps_mlp_train_ex', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), - alloc: mod.cwrap('nisps_alloc', 'number', ['number']), - free: mod.cwrap('nisps_free', null, ['number']), - allocInt: mod.cwrap('nisps_alloc_int', 'number', ['number']), - freeInt: mod.cwrap('nisps_free_int', null, ['number']), - }; -} - -function toHeapF32(arr) { - const ptr = w.alloc(arr.length); - w.mod.HEAPF32.set(arr, ptr >> 2); - return ptr; -} - -function toHeapI32(arr) { - const ptr = w.allocInt(arr.length); - w.mod.HEAP32.set(arr, ptr >> 2); - return ptr; -} - -function fromHeapF32(ptr, length) { - const offset = ptr >> 2; - return Array.from(w.mod.HEAPF32.subarray(offset, offset + length)); -} - -function arraysEqual(a, b) { - if (!a || !b || a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; - return true; -} - -self.onmessage = async function(e) { - const { type, payload } = e.data; - - if (type === 'train') { - await ensureModule(); - - const { - layerSizes, activationIds, weights, - features, labels, sampleWeights, - nInputs, nOutputs, - learningRate, maxIterations, convergenceThreshold, - } = payload; - - // Recreate MLP if architecture changed - if (!mlp || !arraysEqual(currentLayerSizes, layerSizes)) { - if (mlp) w.destroy(mlp); - const layerPtr = toHeapI32(new Int32Array(layerSizes)); - const actPtr = toHeapI32(new Int32Array(activationIds)); - mlp = w.create(layerPtr, layerSizes.length, actPtr, activationIds.length); - w.freeInt(layerPtr); - w.freeInt(actPtr); - currentLayerSizes = [...layerSizes]; - } - - // Load weights from main thread - const weightCount = w.weightCount(mlp); - const wPtr = toHeapF32(new Float32Array(weights)); - w.setWeights(mlp, wPtr); - w.free(wPtr); - - // Build flat training arrays with bias - const featureDim = nInputs + 1; - const nSamples = features.length; - const featFlat = new Float32Array(nSamples * featureDim); - const labFlat = new Float32Array(nSamples * nOutputs); - - for (let i = 0; i < nSamples; i++) { - for (let j = 0; j < nInputs; j++) { - featFlat[i * featureDim + j] = features[i][j]; - } - featFlat[i * featureDim + nInputs] = 1.0; - for (let j = 0; j < nOutputs; j++) { - labFlat[i * nOutputs + j] = labels[i][j] || 0; - } - } - - const featPtr = toHeapF32(featFlat); - const labPtr = toHeapF32(labFlat); - const weightPtr = sampleWeights ? toHeapF32(new Float32Array(sampleWeights)) : 0; - const lossHistPtr = w.alloc(maxIterations); - - // Train (extended — captures per-iteration loss history) - const itersRun = w.trainEx( - mlp, featPtr, nSamples, featureDim, - labPtr, nOutputs, - weightPtr, - learningRate, maxIterations, convergenceThreshold, - lossHistPtr - ); - - // Read per-iteration loss history - const lossHistory = fromHeapF32(lossHistPtr, itersRun); - - w.free(featPtr); - w.free(labPtr); - if (weightPtr) w.free(weightPtr); - w.free(lossHistPtr); - - const loss = itersRun > 0 ? lossHistory[itersRun - 1] : 0; - - // Extract trained weights - const outPtr = w.alloc(weightCount); - w.getWeights(mlp, outPtr); - const trainedWeights = fromHeapF32(outPtr, weightCount); - w.free(outPtr); - - self.postMessage({ - type: 'trained', - payload: { weights: trainedWeights, loss, lossHistory }, - }); - } -}; diff --git a/playground/js/nisps/nisps-wasm.js b/playground/js/nisps/nisps-wasm.js deleted file mode 100644 index f83cfc5..0000000 --- a/playground/js/nisps/nisps-wasm.js +++ /dev/null @@ -1,612 +0,0 @@ -// WASM-backed IML — drop-in replacement for the JS IML class. -// Uses nisps-core compiled to WASM for inference, training, and weight ops. -// Training runs in a Web Worker for non-blocking operation. - -import { Dataset } from './dataset.js'; - -// Activation function IDs matching C++ nisps::ACTIVATION_FUNCTIONS enum -const ACTIVATION = { SIGMOID: 0, TANH: 1, LINEAR: 2, RELU: 3 }; - -/** - * Load the Emscripten module. Returns the initialized module. - */ -async function loadNispsModule() { - const { default: NispsModule } = await import('../../wasm/nisps.js'); - const mod = await NispsModule(); - return mod; -} - -/** - * Wrap raw Emscripten module with typed JS helpers. - */ -function wrapModule(mod) { - return { - mod, - create: mod.cwrap('nisps_mlp_create', 'number', ['number', 'number', 'number', 'number']), - destroy: mod.cwrap('nisps_mlp_destroy', null, ['number']), - weightCount: mod.cwrap('nisps_mlp_weight_count', 'number', ['number']), - getWeights: mod.cwrap('nisps_mlp_get_weights', null, ['number', 'number']), - setWeights: mod.cwrap('nisps_mlp_set_weights', null, ['number', 'number']), - inference: mod.cwrap('nisps_mlp_inference', null, ['number', 'number', 'number', 'number', 'number']), - train: mod.cwrap('nisps_mlp_train', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), - drawWeightsSpread: mod.cwrap('nisps_mlp_draw_weights_spread', null, ['number', 'number']), - moveWeightsSpread: mod.cwrap('nisps_mlp_move_weights_spread', null, ['number', 'number', 'number']), - inferBatch: mod.cwrap('nisps_mlp_infer_batch', null, ['number', 'number', 'number', 'number', 'number', 'number']), - trainEx: mod.cwrap('nisps_mlp_train_ex', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), - moveWeightsEx: mod.cwrap('nisps_mlp_move_weights_ex', null, ['number', 'number', 'number', 'number', 'number']), - evalLoss: mod.cwrap('nisps_mlp_eval_loss', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number']), - getLayerStats: mod.cwrap('nisps_mlp_get_layer_stats', null, ['number', 'number', 'number']), - alloc: mod.cwrap('nisps_alloc', 'number', ['number']), - free: mod.cwrap('nisps_free', null, ['number']), - allocInt: mod.cwrap('nisps_alloc_int', 'number', ['number']), - freeInt: mod.cwrap('nisps_free_int', null, ['number']), - }; -} - -/** - * Write a JS array into WASM heap, returning the pointer. - * Caller must free with w.free(ptr). - */ -function toHeapF32(w, arr) { - const ptr = w.alloc(arr.length); - w.mod.HEAPF32.set(arr, ptr >> 2); - return ptr; -} - -function toHeapI32(w, arr) { - const ptr = w.allocInt(arr.length); - w.mod.HEAP32.set(arr, ptr >> 2); - return ptr; -} - -function fromHeapF32(w, ptr, length) { - const offset = ptr >> 2; - return Array.from(w.mod.HEAPF32.subarray(offset, offset + length)); -} - -/** - * WASM-backed IML class — API-compatible with the JS IML. - * - * Construction is async: use `await WasmIML.create(...)` instead of `new IML(...)`. - */ -export class WasmIML { - /** - * Async factory — loads WASM and constructs the IML. - */ - static async create( - nInputs, - nOutputs, - hiddenLayers = [10, 10, 14], - maxIterations = 1000, - learningRate = 1.0, - convergenceThreshold = 0.00001 - ) { - const mod = await loadNispsModule(); - const iml = new WasmIML(mod, nInputs, nOutputs, hiddenLayers, - maxIterations, learningRate, convergenceThreshold); - return iml; - } - - constructor(mod, nInputs, nOutputs, hiddenLayers, maxIterations, learningRate, convergenceThreshold) { - this.nInputs = nInputs; - this.nOutputs = nOutputs; - this.maxIterations = maxIterations; - this.learningRate = learningRate; - this.convergenceThreshold = convergenceThreshold; - this.recencyBias = 0.6; // 0 = uniform, 1 = strong recency - this.weightingMode = 'global'; // 'global' | 'local' | 'combined' - this.localRadius = 0.15; // input-space radius for local weighting - - // Layer sizes: input+bias, hidden..., output - const BIAS = 1; - this.layerSizes = [nInputs + BIAS, ...hiddenLayers, nOutputs]; - // Activations: RELU for hidden, SIGMOID for output - this.activationIds = [ - ...hiddenLayers.map(() => ACTIVATION.RELU), - ACTIVATION.SIGMOID, - ]; - - // WASM module + helpers - this._w = wrapModule(mod); - this._createMLP(); - - // State (JS-side, same as original IML) - this.inputState = new Array(nInputs).fill(0.5); - this.outputState = new Array(nOutputs).fill(0); - this.performInference = true; - this.inputUpdated = true; - this.storedWeights = null; - this.weightsRandomised = false; - this.lastLoss = null; - this.bestLoss = null; - this.lossHistory = []; - this.totalTrainingIterations = 0; - this.logFn = null; - - // Dataset (JS-side for persistence/visualization access and sample weighting) - this.dataset = new Dataset(100); - - // Persistent WASM buffers for inference (avoid alloc/free per frame) - const inputDim = nInputs + BIAS; - this._inputPtr = this._w.alloc(inputDim); - this._outputPtr = this._w.alloc(nOutputs); - this._inputDim = inputDim; - - // Worker for async training - this._worker = null; - this._training = false; - } - - _createMLP() { - const w = this._w; - const layerPtr = toHeapI32(w, new Int32Array(this.layerSizes)); - const actPtr = toHeapI32(w, new Int32Array(this.activationIds)); - this._mlp = w.create(layerPtr, this.layerSizes.length, actPtr, this.activationIds.length); - w.freeInt(layerPtr); - w.freeInt(actPtr); - this._weightCount = w.weightCount(this._mlp); - } - - // ---- Logging ---- - setLogger(fn) { this.logFn = fn; } - log(msg) { if (this.logFn) this.logFn(msg); } - - // ---- Input / Output ---- - setInput(index, value) { - if (index >= this.nInputs) return; - this.inputState[index] = Math.max(0, Math.min(1, value)); - this.inputUpdated = true; - } - - setInputs(values) { - for (let i = 0; i < values.length && i < this.nInputs; i++) { - this.inputState[i] = Math.max(0, Math.min(1, values[i])); - } - this.inputUpdated = true; - } - - getOutputs() { return this.outputState; } - - setOutput(index, value) { - if (index >= this.nOutputs) return; - this.outputState[index] = Math.max(0, Math.min(1, value)); - } - - setOutputs(values) { - for (let i = 0; i < values.length && i < this.nOutputs; i++) { - this.outputState[i] = Math.max(0, Math.min(1, values[i])); - } - } - - // ---- Inference (WASM, synchronous — fast) ---- - process() { - if (!this.performInference || !this.inputUpdated) return; - - // Write input + bias into persistent WASM buffer - const heap = this._w.mod.HEAPF32; - const inOff = this._inputPtr >> 2; - for (let i = 0; i < this.nInputs; i++) { - heap[inOff + i] = this.inputState[i]; - } - heap[inOff + this.nInputs] = 1.0; // bias - - this._w.inference(this._mlp, this._inputPtr, this._inputDim, this._outputPtr, this.nOutputs); - - // Read output - const outOff = this._outputPtr >> 2; - for (let i = 0; i < this.nOutputs; i++) { - this.outputState[i] = heap[outOff + i]; - } - - this.inputUpdated = false; - } - - // ---- Batch inference (WASM) ---- - inferBatch(inputPoints) { - // inputPoints: array of [x,y,...] arrays (each length nInputs) - // Returns: array of output arrays (each length nOutputs) - const nPoints = inputPoints.length; - const inputDim = this.nInputs + 1; // +bias - const inFlat = new Float32Array(nPoints * inputDim); - for (let i = 0; i < nPoints; i++) { - for (let j = 0; j < this.nInputs; j++) { - inFlat[i * inputDim + j] = inputPoints[i][j]; - } - inFlat[i * inputDim + this.nInputs] = 1.0; // bias - } - const inPtr = toHeapF32(this._w, inFlat); - const outPtr = this._w.alloc(nPoints * this.nOutputs); - this._w.inferBatch(this._mlp, inPtr, nPoints, inputDim, outPtr, this.nOutputs); - // Read outputs - const results = []; - const heap = this._w.mod.HEAPF32; - const outOff = outPtr >> 2; - for (let i = 0; i < nPoints; i++) { - const row = new Array(this.nOutputs); - for (let j = 0; j < this.nOutputs; j++) { - row[j] = heap[outOff + i * this.nOutputs + j]; - } - results.push(row); - } - this._w.free(inPtr); - this._w.free(outPtr); - return results; - } - - // ---- Dataset ---- - addExample(inputs, outputs) { - const inVec = inputs.slice(0, this.nInputs); - while (inVec.length < this.nInputs) inVec.push(0); - const outVec = outputs.slice(0, this.nOutputs); - while (outVec.length < this.nOutputs) outVec.push(0); - this.dataset.add(inVec, outVec); - } - - clearDataset() { - this.dataset.clear(); - this.log('Dataset cleared.'); - } - - get exampleCount() { return this.dataset.features.length; } - - // ---- Training (WASM, synchronous) ---- - train(options = {}) { - if (this.weightsRandomised && this.storedWeights) { - this._setFlatWeights(this.storedWeights); - this.weightsRandomised = false; - } - - if (this.dataset.features.length === 0) { - this.log('Empty dataset, skipping training.'); - return null; - } - - this.log('Training...'); - - const features = this.dataset.features; - const labels = this.dataset.labels; - - // Compute per-sample weights - const sampleWeights = this.dataset.computeWeights(this.weightingMode, { - recencyBias: this.recencyBias, - queryInput: this.inputState, - radius: this.localRadius, - }); - - // Build flat arrays with bias appended to features - const featureDim = this.nInputs + 1; // +bias - const nSamples = features.length; - const featFlat = new Float32Array(nSamples * featureDim); - const labFlat = new Float32Array(nSamples * this.nOutputs); - - for (let i = 0; i < nSamples; i++) { - for (let j = 0; j < this.nInputs; j++) { - featFlat[i * featureDim + j] = features[i][j]; - } - featFlat[i * featureDim + this.nInputs] = 1.0; // bias - for (let j = 0; j < this.nOutputs; j++) { - labFlat[i * this.nOutputs + j] = labels[i][j] || 0; - } - } - - const featPtr = toHeapF32(this._w, featFlat); - const labPtr = toHeapF32(this._w, labFlat); - const weightPtr = toHeapF32(this._w, sampleWeights); - const lossHistPtr = this._w.alloc(this.maxIterations); - - const itersRun = this._w.trainEx( - this._mlp, featPtr, nSamples, featureDim, - labPtr, this.nOutputs, - weightPtr, - this.learningRate, this.maxIterations, this.convergenceThreshold, - lossHistPtr - ); - - // Read per-iteration loss history - const lossHist = fromHeapF32(this._w, lossHistPtr, itersRun); - - this._w.free(featPtr); - this._w.free(labPtr); - this._w.free(weightPtr); - this._w.free(lossHistPtr); - - const loss = itersRun > 0 ? lossHist[itersRun - 1] : 0; - this.lastLoss = loss; - for (let i = 0; i < lossHist.length; i++) { - this.lossHistory.push(lossHist[i]); - } - this.totalTrainingIterations += itersRun; - if (this.lossHistory.length > 1200) { - this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200); - } - this.bestLoss = this.bestLoss === null ? loss : Math.min(this.bestLoss, loss); - - // Run inference after training - this.inputUpdated = true; - this.process(); - - this.log(`Training complete. Loss: ${loss.toFixed(6)}`); - return loss; - } - - // ---- Loss evaluation (WASM, no weight update) ---- - evalLoss() { - if (this.dataset.features.length === 0) return null; - - const features = this.dataset.features; - const labels = this.dataset.labels; - - const sampleWeights = this.dataset.computeWeights(this.weightingMode, { - recencyBias: this.recencyBias, - queryInput: this.inputState, - radius: this.localRadius, - }); - - const featureDim = this.nInputs + 1; - const nSamples = features.length; - const featFlat = new Float32Array(nSamples * featureDim); - const labFlat = new Float32Array(nSamples * this.nOutputs); - - for (let i = 0; i < nSamples; i++) { - for (let j = 0; j < this.nInputs; j++) { - featFlat[i * featureDim + j] = features[i][j]; - } - featFlat[i * featureDim + this.nInputs] = 1.0; - for (let j = 0; j < this.nOutputs; j++) { - labFlat[i * this.nOutputs + j] = labels[i][j] || 0; - } - } - - const featPtr = toHeapF32(this._w, featFlat); - const labPtr = toHeapF32(this._w, labFlat); - const weightPtr = toHeapF32(this._w, sampleWeights); - - const loss = this._w.evalLoss( - this._mlp, featPtr, nSamples, featureDim, - labPtr, this.nOutputs, - weightPtr - ); - - this._w.free(featPtr); - this._w.free(labPtr); - this._w.free(weightPtr); - - return loss; - } - - // ---- Weight manipulation ---- - randomiseWeights(spread = 0) { - this.storedWeights = this._getFlatWeights(); - this._w.drawWeightsSpread(this._mlp, spread); - this.weightsRandomised = true; - - this.inputUpdated = true; - this.process(); - this.log('Weights randomised.'); - } - - // outputPinMask: optional Uint8Array[nOutputs], 1 = skip that output node. - moveWeights(speed, spread = 0, outputPinMask = null) { - let pinMaskPtr = 0; - - if (outputPinMask && outputPinMask.some(v => v)) { - const pinI32 = new Int32Array(this.nOutputs); - for (let i = 0; i < this.nOutputs; i++) { - pinI32[i] = outputPinMask[i] ? 1 : 0; - } - pinMaskPtr = toHeapI32(this._w, pinI32); - } - - this._w.moveWeightsEx(this._mlp, speed, spread, pinMaskPtr, this.nOutputs); - - if (pinMaskPtr) this._w.freeInt(pinMaskPtr); - - this.inputUpdated = true; - this.process(); - } - - // ---- Public weight snapshot for warm-start transfer ---- - /** - * Returns a plain object describing the full network weights for later - * reinjection via WasmIML.createWithWarmStart(). - */ - extractWeights() { - return { - layerSizes: [...this.layerSizes], // e.g. [3, 32, 48, 64, 126] - weights: this._getFlatWeights(), // plain Array from fromHeapF32 - }; - } - - /** - * Async static factory: create a new WasmIML with newOutputCount outputs, - * transferring as much of snapshot.weights as possible. - * Hidden-layer weights are copied unchanged; output nodes beyond the old - * count retain their random-initialised values. - */ - static async createWithWarmStart(snapshot, newOutputCount, maxIter, learningRate, convergenceThreshold) { - const oldLayerSizes = snapshot.layerSizes; - const nInputs = oldLayerSizes[0] - 1; // stored with bias (+1), strip it - const hiddenLayers = oldLayerSizes.slice(1, -1); // e.g. [32, 48, 64] - const oldOutputCount = oldLayerSizes[oldLayerSizes.length - 1]; - - // Create fresh instance with new output count - const newIml = await WasmIML.create(nInputs, newOutputCount, hiddenLayers, maxIter, learningRate, convergenceThreshold); - - // Calculate prefix weight count (all layers except the final output layer). - // Each layer l: n_nodes[l] * (n_nodes[l-1] + 1) weights (inputs + bias). - // fullOldSizes: [nInputs+1, ...hiddenLayers, oldOutputCount] - let prefixCount = 0; - for (let l = 1; l < oldLayerSizes.length - 1; l++) { - prefixCount += oldLayerSizes[l] * (oldLayerSizes[l - 1] + 1); - } - - const lastHidden = hiddenLayers[hiddenLayers.length - 1]; // e.g. 64 - const weightsPerOutputNode = lastHidden + 1; // inputs from last hidden + bias - - // Build new weight array, starting from random init - const newWeights = newIml._getFlatWeights(); - const oldWeights = snapshot.weights; - - // Copy hidden layer weights unchanged - for (let i = 0; i < prefixCount; i++) { - newWeights[i] = oldWeights[i]; - } - - // Copy output layer weights for nodes that existed in the old network - const sharedOutputNodes = Math.min(oldOutputCount, newOutputCount); - for (let n = 0; n < sharedOutputNodes; n++) { - const oldOff = prefixCount + n * weightsPerOutputNode; - const newOff = prefixCount + n * weightsPerOutputNode; - for (let w = 0; w < weightsPerOutputNode; w++) { - newWeights[newOff + w] = oldWeights[oldOff + w]; - } - } - // Nodes beyond old count retain their random init — good for exploration - - newIml._setFlatWeights(newWeights); - return newIml; - } - - // ---- Flat weight get/set (for storedWeights save/restore) ---- - _getFlatWeights() { - const ptr = this._w.alloc(this._weightCount); - this._w.getWeights(this._mlp, ptr); - const weights = fromHeapF32(this._w, ptr, this._weightCount); - this._w.free(ptr); - return weights; - } - - _setFlatWeights(flatWeights) { - const ptr = toHeapF32(this._w, new Float32Array(flatWeights)); - this._w.setWeights(this._mlp, ptr); - this._w.free(ptr); - } - - // ---- Per-layer weight statistics (WASM) ---- - getLayerStats() { - const nLayers = this.layerSizes.length - 1; - const statsPtr = this._w.alloc(nLayers * 4); - this._w.getLayerStats(this._mlp, statsPtr, nLayers); - const stats = []; - const heap = this._w.mod.HEAPF32; - const off = statsPtr >> 2; - for (let l = 0; l < nLayers; l++) { - stats.push({ - meanAbs: heap[off + l * 4 + 0], - maxAbs: heap[off + l * 4 + 1], - deadFrac: heap[off + l * 4 + 2], - satFrac: heap[off + l * 4 + 3], - }); - } - this._w.free(statsPtr); - return stats; - } - - // ---- Async training via Web Worker ---- - get isTraining() { return this._training; } - - trainAsync(onComplete) { - if (this._training) { - this.log('Training already in progress, skipping.'); - return Promise.resolve(null); - } - - // Restore weights if randomised - if (this.weightsRandomised && this.storedWeights) { - this._setFlatWeights(this.storedWeights); - this.weightsRandomised = false; - } - - if (this.dataset.features.length === 0) { - this.log('Empty dataset, skipping training.'); - return Promise.resolve(null); - } - - this._training = true; - this.log('Training (async)...'); - - // Snapshot current weights + dataset for the worker - const flatWeights = this._getFlatWeights(); - const features = this.dataset.features; - const labels = this.dataset.labels; - const sampleWeights = Array.from(this.dataset.computeWeights(this.weightingMode, { - recencyBias: this.recencyBias, - queryInput: this.inputState, - radius: this.localRadius, - })); - - // Lazy-init worker - if (!this._worker) { - const workerUrl = new URL('./nisps-wasm-worker.js', import.meta.url); - this._worker = new Worker(workerUrl, { type: 'module' }); - } - - return new Promise((resolve) => { - const handler = (e) => { - if (e.data.type === 'trained') { - this._worker.removeEventListener('message', handler); - this._training = false; - - const { weights, loss, lossHistory } = e.data.payload; - - // Swap in trained weights - this._setFlatWeights(weights); - this.lastLoss = loss; - if (lossHistory && lossHistory.length > 0) { - for (let i = 0; i < lossHistory.length; i++) { - this.lossHistory.push(lossHistory[i]); - } - this.totalTrainingIterations += lossHistory.length; - } else { - this.lossHistory.push(loss); - this.totalTrainingIterations += 1; - } - if (this.lossHistory.length > 1200) { - this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200); - } - this.bestLoss = this.bestLoss === null ? loss : Math.min(this.bestLoss, loss); - - // Run inference with new weights - this.inputUpdated = true; - this.process(); - - this.log(`Training complete. Loss: ${loss.toFixed(6)}`); - if (onComplete) onComplete({ loss, outputs: [...this.outputState] }); - resolve(loss); - } - }; - - this._worker.addEventListener('message', handler); - this._worker.postMessage({ - type: 'train', - payload: { - layerSizes: this.layerSizes, - activationIds: this.activationIds, - weights: flatWeights, - features, - labels, - sampleWeights, - nInputs: this.nInputs, - nOutputs: this.nOutputs, - learningRate: this.learningRate, - maxIterations: this.maxIterations, - convergenceThreshold: this.convergenceThreshold, - }, - }); - }); - } - - // ---- Cleanup ---- - destroy() { - if (this._mlp) { - this._w.free(this._inputPtr); - this._w.free(this._outputPtr); - this._w.destroy(this._mlp); - this._mlp = null; - } - if (this._worker) { - this._worker.terminate(); - this._worker = null; - } - } -} diff --git a/playground/js/nisps/node.js b/playground/js/nisps/node.js deleted file mode 100644 index 80d7f6e..0000000 --- a/playground/js/nisps/node.js +++ /dev/null @@ -1,153 +0,0 @@ -// NISPS Node - faithful port of nisps-core/include/nisps/node.hpp -// Single neural network node with weights, bias, and RMSProp optimizer - -const RMSPROP_DECAY = 0.9; -const RMSPROP_DECAY_INV = 0.1; -const RMSPROP_EPSILON = 1e-6; -const MAX_SQUARED_GRAD_AVG = 1e6; -const MAX_ADJUSTED_LR = 1.0; -const GRADIENT_CLIP_VALUE = 10.0; - -export class Node { - constructor(numInputs, useConstantInit = true, constantInit = 0.5) { - this.numInputs = numInputs; - this.bias = 0.0; - this.weights = new Float64Array(numInputs); - this.squaredGradientAvg = new Float64Array(numInputs); - this.biasSquaredGradientAvg = 0; - this.gradientAccumulator = new Float64Array(numInputs); - this.biasGradientAccumulator = 0; - this.innerProd = 0; - - if (useConstantInit) { - this.weights.fill(constantInit); - } else { - // gen_rand(2.0) produces values in [-1, 1] - for (let i = 0; i < numInputs; i++) { - this.weights[i] = Math.random() * 2 - 1; - } - } - } - - getInputInnerProdWithWeights(input) { - let res = 0; - for (let j = 0; j < input.length; j++) { - res += input[j] * this.weights[j]; - } - res += this.bias; - this.innerProd = res; - return this.innerProd; - } - - getOutputAfterActivation(input, activationFn) { - this.getInputInnerProdWithWeights(input); - return activationFn(this.innerProd); - } - - initializeGradientAccumulator() { - this.gradientAccumulator = new Float64Array(this.weights.length); - this.biasGradientAccumulator = 0; - } - - clearGradientAccumulator() { - this.gradientAccumulator.fill(0); - } - - accumulateGradients(input, errorSignal) { - for (let i = 0; i < this.weights.length; i++) { - this.gradientAccumulator[i] += input[i] * errorSignal; - } - this.biasGradientAccumulator += errorSignal; - } - - applyAccumulatedGradients(learningRate, batchSizeInv) { - for (let i = 0; i < this.weights.length; i++) { - let gradient = this.gradientAccumulator[i] * batchSizeInv; - - // Clamp gradient - gradient = Math.max(Math.min(gradient, GRADIENT_CLIP_VALUE), -GRADIENT_CLIP_VALUE); - - this.squaredGradientAvg[i] = - RMSPROP_DECAY * this.squaredGradientAvg[i] + - RMSPROP_DECAY_INV * gradient * gradient; - - // Clamp squared gradient average - this.squaredGradientAvg[i] = Math.min(this.squaredGradientAvg[i], MAX_SQUARED_GRAD_AVG); - - let adjustedLR = learningRate / (Math.sqrt(this.squaredGradientAvg[i]) + RMSPROP_EPSILON); - - // Clamp adjusted learning rate - adjustedLR = Math.min(adjustedLR, MAX_ADJUSTED_LR); - - this.weights[i] -= adjustedLR * gradient; - this.gradientAccumulator[i] = 0; - } - - // Bias update - let biasGradient = this.biasGradientAccumulator * batchSizeInv; - biasGradient = Math.max(Math.min(biasGradient, GRADIENT_CLIP_VALUE), -GRADIENT_CLIP_VALUE); - - this.biasSquaredGradientAvg = - RMSPROP_DECAY * this.biasSquaredGradientAvg + - RMSPROP_DECAY_INV * biasGradient * biasGradient; - - this.biasSquaredGradientAvg = Math.min(this.biasSquaredGradientAvg, MAX_SQUARED_GRAD_AVG); - - let biasAdjustedLR = learningRate / (Math.sqrt(this.biasSquaredGradientAvg) + RMSPROP_EPSILON); - biasAdjustedLR = Math.min(biasAdjustedLR, MAX_ADJUSTED_LR); - - this.bias -= biasAdjustedLR * biasGradient; - this.biasGradientAccumulator = 0; - } - - getGradSumSquared(batchSizeInv) { - let sumsq = 0; - for (let i = 0; i < this.gradientAccumulator.length; i++) { - const scaled = this.gradientAccumulator[i] * batchSizeInv; - sumsq += scaled * scaled; - } - return sumsq; - } - - scaleAccumulatedGradients(clipCoef) { - for (let i = 0; i < this.gradientAccumulator.length; i++) { - this.gradientAccumulator[i] *= clipCoef; - } - } - - updateWeight(weightId, increment, learningRate) { - this.weights[weightId] += learningRate * increment; - } - - resetOptimizerState() { - this.squaredGradientAvg.fill(0); - this.biasSquaredGradientAvg = 0; - } - - checkAndFixWeights() { - let hadCorruption = false; - for (let i = 0; i < this.weights.length; i++) { - if (!isFinite(this.weights[i])) { - this.weights[i] = 0; - this.squaredGradientAvg[i] = 0; - hadCorruption = true; - } - } - if (!isFinite(this.bias)) { - this.bias = 0; - this.biasSquaredGradientAvg = 0; - hadCorruption = true; - } - return hadCorruption; - } - - getWeightsCopy() { - return Array.from(this.weights); - } - - setWeights(weights) { - for (let i = 0; i < this.weights.length; i++) { - this.weights[i] = weights[i]; - } - } -} diff --git a/playground/js/nisps/osc-client.js b/playground/js/nisps/osc-client.js deleted file mode 100644 index 9351ab0..0000000 --- a/playground/js/nisps/osc-client.js +++ /dev/null @@ -1,241 +0,0 @@ -// osc-client.js — WebSocket client for NISPS <-> OSC bridge -// Connects the webapp to VCV Rack MEMLNaut module (or any OSC target) -// via the bridge server (bridge.ts / bridge.mjs). -// -// Usage: -// import { NispsOscClient } from './osc-client.js'; -// const osc = new NispsOscClient('ws://localhost:8765'); -// osc.onOutputsReceived(values => console.log('outputs:', values)); -// osc.onInputsReceived(values => console.log('inputs:', values)); -// await osc.connect(); -// osc.sendState(stateJson); - -export class NispsOscClient extends EventTarget { - /** - * @param {string} wsUrl WebSocket URL of the bridge server - */ - constructor(wsUrl = 'ws://localhost:8765') { - super(); - this._wsUrl = wsUrl; - this._ws = null; - this._connected = false; - this._reconnect = false; - this._reconnectDelay = 1000; - this._reconnectTimer = null; - - // Registered callbacks - this._outputsCallbacks = []; - this._inputsCallbacks = []; - this._infoCallbacks = []; - } - - /** Current connection state */ - get connected() { return this._connected; } - - /** The WebSocket URL */ - get url() { return this._wsUrl; } - set url(val) { - if (this._connected) { - this.disconnect(); - } - this._wsUrl = val; - } - - /** - * Connect to the bridge server. - * @param {object} [opts] - * @param {boolean} [opts.reconnect=true] Auto-reconnect on disconnect - * @returns {Promise} Resolves when connected - */ - connect({ reconnect = true } = {}) { - this._reconnect = reconnect; - - return new Promise((resolve, reject) => { - if (this._connected && this._ws) { - resolve(); - return; - } - - try { - this._ws = new WebSocket(this._wsUrl); - } catch (err) { - reject(err); - return; - } - - this._ws.onopen = () => { - this._connected = true; - this._reconnectDelay = 1000; // reset backoff - this.dispatchEvent(new CustomEvent('connected')); - resolve(); - }; - - this._ws.onclose = () => { - const wasConnected = this._connected; - this._connected = false; - this._ws = null; - this.dispatchEvent(new CustomEvent('disconnected')); - - if (this._reconnect) { - this._scheduleReconnect(); - } - - if (!wasConnected) { - reject(new Error('WebSocket closed before connecting')); - } - }; - - this._ws.onerror = (e) => { - this.dispatchEvent(new CustomEvent('error', { detail: e })); - }; - - this._ws.onmessage = (e) => { - this._handleMessage(e.data); - }; - }); - } - - /** Disconnect from the bridge. */ - disconnect() { - this._reconnect = false; - if (this._reconnectTimer) { - clearTimeout(this._reconnectTimer); - this._reconnectTimer = null; - } - if (this._ws) { - this._ws.onclose = null; // prevent reconnect trigger - this._ws.close(); - this._ws = null; - } - this._connected = false; - this.dispatchEvent(new CustomEvent('disconnected')); - } - - // ── Send methods ───────────────────────────────────────────────── - - /** - * Send full .nisps state JSON to the VCV module. - * The module will call dataFromJson to apply it. - * @param {object|string} stateJson - */ - sendState(stateJson) { - const payload = typeof stateJson === 'string' ? JSON.parse(stateJson) : stateJson; - this._send({ type: 'state', payload }); - } - - /** - * Send just weights to the VCV module. - * @param {object} weightsObj { weights: [[[...]]] } - */ - sendWeights(weightsObj) { - const payload = typeof weightsObj === 'string' ? JSON.parse(weightsObj) : weightsObj; - this._send({ type: 'weights', payload }); - } - - /** - * Send individual parameter updates (legacy format, for synth params). - * @param {Array<[string, number]>} params e.g. [["Env_A_Att", 0.35], ...] - */ - sendParams(params) { - this._send({ type: 'params', payload: params }); - } - - /** - * Send a raw param batch in legacy format (backwards compatible). - * @param {Array<[string, number]>} batch - */ - sendParamBatch(batch) { - this._send(batch); - } - - // ── Receive handlers ───────────────────────────────────────────── - - /** - * Register a callback for output values from VCV. - * @param {function(number[]): void} callback - * @returns {function} unsubscribe function - */ - onOutputsReceived(callback) { - this._outputsCallbacks.push(callback); - return () => { - const idx = this._outputsCallbacks.indexOf(callback); - if (idx >= 0) this._outputsCallbacks.splice(idx, 1); - }; - } - - /** - * Register a callback for input values from VCV. - * @param {function(number[]): void} callback - * @returns {function} unsubscribe function - */ - onInputsReceived(callback) { - this._inputsCallbacks.push(callback); - return () => { - const idx = this._inputsCallbacks.indexOf(callback); - if (idx >= 0) this._inputsCallbacks.splice(idx, 1); - }; - } - - /** - * Register a callback for bridge info messages. - * @param {function(string): void} callback - * @returns {function} unsubscribe function - */ - onInfo(callback) { - this._infoCallbacks.push(callback); - return () => { - const idx = this._infoCallbacks.indexOf(callback); - if (idx >= 0) this._infoCallbacks.splice(idx, 1); - }; - } - - // ── Internal ───────────────────────────────────────────────────── - - _send(data) { - if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return; - this._ws.send(JSON.stringify(data)); - } - - _handleMessage(raw) { - try { - const msg = JSON.parse(raw); - - switch (msg.type) { - case 'outputs': - for (const cb of this._outputsCallbacks) cb(msg.values); - this.dispatchEvent(new CustomEvent('outputs', { detail: msg.values })); - break; - - case 'inputs': - for (const cb of this._inputsCallbacks) cb(msg.values); - this.dispatchEvent(new CustomEvent('inputs', { detail: msg.values })); - break; - - case 'info': - for (const cb of this._infoCallbacks) cb(msg.message); - this.dispatchEvent(new CustomEvent('info', { detail: msg.message })); - break; - - case 'osc': - // Generic OSC message passthrough - this.dispatchEvent(new CustomEvent('osc', { detail: msg })); - break; - } - } catch { - // ignore parse errors - } - } - - _scheduleReconnect() { - if (this._reconnectTimer) return; - this._reconnectTimer = setTimeout(() => { - this._reconnectTimer = null; - if (!this._connected && this._reconnect) { - this.connect({ reconnect: true }).catch(() => { - // increase backoff, max 30s - this._reconnectDelay = Math.min(this._reconnectDelay * 1.5, 30000); - }); - } - }, this._reconnectDelay); - } -} diff --git a/playground/js/shapeseq/chain-ui.js b/playground/js/shapeseq/chain-ui.js deleted file mode 100644 index 4022d9c..0000000 --- a/playground/js/shapeseq/chain-ui.js +++ /dev/null @@ -1,1250 +0,0 @@ -/** - * ShapeSeq Chain Builder UI — full-screen slide-up drawer with signal-based updates - * - * Pedalboard-style chain editor: expandable cards with param sliders, - * drag-to-reorder, delete, add-primitive palette, generator combine toggle. - * Mobile-first, dark glass theme matching the immersive playground. - * - * Uses vanilla signals for reactive slider updates at 60fps — O(changed params) - * instead of O(all sliders) via querySelectorAll. - * - * All styles are inline (no external CSS dependency). - * - * @module shapeseq/chain-ui - */ - -import { PRIMITIVE_REGISTRY } from './primitives.js'; -import { createArraySignal } from './signal.js'; - -// ── Category colors ────────────────────────────────────────────────── - -const CATEGORY_COLORS = { - generator: '#ff6a00', - processor: '#00ccff', - timing: '#ffcc00', - converter: '#88ff00', -}; - -// ── Inject styles once ─────────────────────────────────────────────── - -let stylesInjected = false; - -function injectStyles() { - if (stylesInjected) return; - stylesInjected = true; - - const css = ` -/* ── Chain drawer overlay + slide-up drawer ────────────────────── */ - -.chain-drawer-overlay { - position: fixed; - inset: 0; - z-index: 9998; - background: rgba(0, 0, 0, 0.5); - opacity: 0; - transition: opacity 0.3s ease; - pointer-events: none; -} - -.chain-drawer-overlay.open { - opacity: 1; - pointer-events: auto; -} - -.chain-drawer { - position: fixed; - bottom: 0; - left: 0; - right: 0; - max-height: 85vh; - z-index: 9999; - transform: translateY(100%); - transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); - background: rgba(13, 13, 13, 0.95); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-top: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px 16px 0 0; - display: flex; - flex-direction: column; - font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 13px; - color: #e0e0e0; -} - -.chain-drawer.open { - transform: translateY(0); -} - -.chain-drawer-handle { - display: flex; - justify-content: center; - padding: 10px 0 4px; - cursor: grab; -} - -.chain-drawer-handle::after { - content: ''; - width: 36px; - height: 4px; - border-radius: 2px; - background: rgba(255, 255, 255, 0.2); -} - -.chain-drawer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 4px 16px 8px; - font-size: 11px; - font-weight: 700; - color: #ff6a00; - letter-spacing: 0.5px; - text-transform: uppercase; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); -} - -.chain-drawer-close { - width: 24px; - height: 24px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.1); - background: transparent; - color: #888; - font-size: 16px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - line-height: 1; - transition: all 0.15s; -} - -.chain-drawer-close:hover { - border-color: #ff6a00; - color: #ff6a00; -} - -/* ── Chain Builder container ─────────────────────────────────── */ - -.chain-builder { - --cb-glass-bg: rgba(13, 13, 13, 0.72); - --cb-glass-border: rgba(255, 255, 255, 0.08); - --cb-glass-blur: 16px; - --cb-text: #e0e0e0; - --cb-text-dim: #888; - --cb-radius: 12px; - --cb-radius-sm: 8px; - --cb-accent: #ff6a00; - - display: flex; - flex-direction: column; - gap: 6px; - padding: 8px; - overflow-y: auto; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - flex: 1; - min-height: 0; -} - -/* ── Primitive card ──────────────────────────────────────────── */ - -.chain-card { - position: relative; - background: var(--cb-glass-bg); - backdrop-filter: blur(var(--cb-glass-blur)); - -webkit-backdrop-filter: blur(var(--cb-glass-blur)); - border: 1px solid var(--cb-glass-border); - border-radius: var(--cb-radius); - overflow: hidden; - transition: box-shadow 0.2s, transform 0.15s; -} - -.chain-card.dragging { - opacity: 0.7; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - transform: scale(1.03); - z-index: 100; -} - -.chain-card-left-border { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - border-radius: var(--cb-radius) 0 0 var(--cb-radius); -} - -/* ── Card header ─────────────────────────────────────────────── */ - -.chain-card-header { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 10px 10px 14px; - cursor: pointer; - user-select: none; - -webkit-user-select: none; -} - -.chain-card-drag { - display: flex; - flex-direction: column; - gap: 2px; - cursor: grab; - touch-action: none; - padding: 4px 2px; - opacity: 0.4; - transition: opacity 0.15s; -} - -.chain-card-drag:hover, -.chain-card-drag:active { - opacity: 0.8; -} - -.chain-card-drag span { - display: block; - width: 14px; - height: 2px; - background: var(--cb-text); - border-radius: 1px; -} - -.chain-card-name { - flex: 1; - font-weight: 600; - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chain-card-badge { - font-size: 9px; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 2px 6px; - border-radius: 4px; - background: rgba(255, 255, 255, 0.06); - color: var(--cb-text-dim); - white-space: nowrap; -} - -.chain-card-summary { - font-size: 11px; - color: var(--cb-text-dim); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 140px; -} - -.chain-card-delete { - background: none; - border: none; - color: var(--cb-text-dim); - font-size: 16px; - padding: 4px 6px; - cursor: pointer; - border-radius: 4px; - transition: color 0.15s, background 0.15s; - line-height: 1; -} - -.chain-card-delete:hover, -.chain-card-delete:active { - color: #ff4466; - background: rgba(255, 68, 102, 0.12); -} - -.chain-card-expand-icon { - font-size: 10px; - color: var(--cb-text-dim); - transition: transform 0.2s; -} - -.chain-card.expanded .chain-card-expand-icon { - transform: rotate(90deg); -} - -/* ── Card body (params) ──────────────────────────────────────── */ - -.chain-card-body { - max-height: 0; - overflow: hidden; - transition: max-height 0.25s ease; -} - -.chain-card.expanded .chain-card-body { - max-height: 600px; -} - -.chain-card-params { - display: flex; - flex-direction: column; - gap: 6px; - padding: 4px 14px 12px 14px; -} - -/* ── Param slider row ────────────────────────────────────────── */ - -.chain-param-row { - display: flex; - align-items: center; - gap: 8px; -} - -.chain-param-label { - font-size: 11px; - color: var(--cb-text-dim); - min-width: 80px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chain-param-slider { - flex: 1; - -webkit-appearance: none; - appearance: none; - height: 6px; - border-radius: 3px; - background: rgba(255, 255, 255, 0.08); - outline: none; - cursor: pointer; -} - -.chain-param-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--cb-text); - border: 2px solid rgba(0, 0, 0, 0.3); - cursor: pointer; -} - -.chain-param-slider::-moz-range-thumb { - width: 16px; - height: 16px; - border-radius: 50%; - background: var(--cb-text); - border: 2px solid rgba(0, 0, 0, 0.3); - cursor: pointer; -} - -.chain-param-value { - font-size: 11px; - color: var(--cb-text-dim); - min-width: 32px; - text-align: right; - font-variant-numeric: tabular-nums; -} - -/* ── NISPS live indicator ────────────────────────────────────── */ - -.chain-param-row.nisps-live .chain-param-slider { - box-shadow: 0 0 0 2px rgba(0, 140, 255, 0.5); - animation: nisps-pulse 1.5s ease-in-out infinite; -} - -@keyframes nisps-pulse { - 0%, 100% { box-shadow: 0 0 0 2px rgba(0, 140, 255, 0.3); } - 50% { box-shadow: 0 0 0 3px rgba(0, 140, 255, 0.7); } -} - -/* ── Add button ──────────────────────────────────────────────── */ - -.chain-add-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 12px; - border: 2px dashed rgba(255, 255, 255, 0.12); - border-radius: var(--cb-radius); - background: transparent; - color: var(--cb-text-dim); - font-family: inherit; - font-size: 13px; - cursor: pointer; - transition: border-color 0.15s, color 0.15s; -} - -.chain-add-btn:hover, -.chain-add-btn:active { - border-color: var(--cb-accent); - color: var(--cb-accent); -} - -/* ── Generator combine mode toggle ───────────────────────────── */ - -.chain-combine-toggle { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 8px 0 4px; -} - -.chain-combine-label { - font-size: 11px; - color: var(--cb-text-dim); -} - -.chain-combine-btn { - font-family: inherit; - font-size: 12px; - font-weight: 600; - padding: 4px 14px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 6px; - background: rgba(255, 255, 255, 0.04); - color: var(--cb-text); - cursor: pointer; - transition: border-color 0.15s, background 0.15s; - min-width: 60px; - text-align: center; -} - -.chain-combine-btn:hover, -.chain-combine-btn:active { - border-color: var(--cb-accent); - background: rgba(255, 106, 0, 0.1); -} - -/* ── Palette modal ───────────────────────────────────────────── */ - -.chain-palette-overlay { - position: fixed; - inset: 0; - z-index: 10000; - display: flex; - align-items: flex-end; - justify-content: center; - background: rgba(0, 0, 0, 0.6); - animation: palette-fade-in 0.15s ease; -} - -@keyframes palette-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -.chain-palette { - width: 100%; - max-width: 420px; - max-height: 80vh; - overflow-y: auto; - background: rgba(20, 20, 20, 0.95); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px 16px 0 0; - padding: 16px 12px 24px; - animation: palette-slide-up 0.2s ease; -} - -@keyframes palette-slide-up { - from { transform: translateY(40px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -.chain-palette-title { - font-size: 14px; - font-weight: 600; - color: var(--cb-text, #e0e0e0); - text-align: center; - margin-bottom: 12px; -} - -.chain-palette-item { - display: flex; - align-items: center; - gap: 10px; - padding: 12px; - border-radius: 10px; - cursor: pointer; - transition: background 0.12s; - border: none; - background: none; - width: 100%; - text-align: left; - font-family: inherit; - color: #e0e0e0; -} - -.chain-palette-item:hover, -.chain-palette-item:active { - background: rgba(255, 255, 255, 0.06); -} - -.chain-palette-item-color { - width: 4px; - height: 36px; - border-radius: 2px; - flex-shrink: 0; -} - -.chain-palette-item-info { - flex: 1; - min-width: 0; -} - -.chain-palette-item-name { - font-size: 13px; - font-weight: 600; -} - -.chain-palette-item-meta { - font-size: 11px; - color: #888; - margin-top: 2px; -} - -/* ── Drop indicator ──────────────────────────────────────────── */ - -.chain-drop-indicator { - height: 3px; - background: var(--cb-accent, #ff6a00); - border-radius: 2px; - margin: -2px 8px; - opacity: 0; - transition: opacity 0.1s; -} - -.chain-drop-indicator.active { - opacity: 1; -} -`; - - const style = document.createElement('style'); - style.textContent = css; - document.head.appendChild(style); -} - -// ── ChainBuilderUI ─────────────────────────────────────────────────── - -export class ChainBuilderUI { - /** - * @param {{ container: HTMLElement, chain: import('./chain.js').Chain, eventBus: { emit: Function, on: Function } }} opts - */ - constructor({ container, chain, eventBus }) { - injectStyles(); - - /** @type {HTMLElement} */ - this._container = container; - /** @type {import('./chain.js').Chain} */ - this._chain = chain; - /** @type {{ emit: Function, on: Function }} */ - this._bus = eventBus; - - /** @private @type {Set} flat param indices controlled by NISPS */ - this._liveParams = new Set(); - - /** @private @type {Function|null} */ - this._paramChangeCb = null; - - /** @private @type {Set} indices of expanded cards */ - this._expandedCards = new Set(); - - /** @private @type {Float32Array|null} current param values for display */ - this._currentParams = null; - - /** @private @type {import('./signal.js').ArraySignal|null} */ - this._paramSignal = null; - - /** @private @type {Function[]} signal unsubscribers for cleanup */ - this._signalUnsubs = []; - - /** @private @type {Map} chainIndex -> card element */ - this._cardMap = new Map(); - - // ── Drag state ── - /** @private */ - this._dragIndex = -1; - /** @private */ - this._dragEl = null; - /** @private */ - this._dragStartY = 0; - /** @private */ - this._dragOffsetY = 0; - /** @private @type {HTMLElement|null} */ - this._cardList = null; - - // ── Palette overlay ref ── - /** @private @type {HTMLElement|null} */ - this._paletteOverlay = null; - - // ── Drawer elements ── - /** @private @type {HTMLElement|null} */ - this._drawerOverlay = null; - /** @private @type {HTMLElement|null} */ - this._drawer = null; - /** @private @type {boolean} */ - this._isOpen = false; - - // ── Bound handlers for cleanup ── - this._onPointerMove = this._handlePointerMove.bind(this); - this._onPointerUp = this._handlePointerUp.bind(this); - - this._buildDrawer(); - this.render(); - } - - // ── Public API ───────────────────────────────────────────────────── - - /** - * Open the drawer. - */ - open() { - if (this._isOpen) return; - this._isOpen = true; - if (this._drawerOverlay) this._drawerOverlay.classList.add('open'); - if (this._drawer) this._drawer.classList.add('open'); - } - - /** - * Close the drawer. - */ - close() { - if (!this._isOpen) return; - this._isOpen = false; - if (this._drawerOverlay) this._drawerOverlay.classList.remove('open'); - if (this._drawer) this._drawer.classList.remove('open'); - } - - /** - * Rebuild the card list from current chain state. - * Uses keyed reconciliation: only adds/removes/moves the cards that changed. - */ - render() { - const primitives = this._chain.getPrimitives(); - const cardList = this._cardList; - if (!cardList) return; - - // Clean up old signal subscriptions - for (const unsub of this._signalUnsubs) unsub(); - this._signalUnsubs = []; - - // Compute total param count and create signal - let totalParams = 0; - for (const p of primitives) totalParams += p.paramCount; - this._paramSignal = createArraySignal(totalParams); - - // Build desired order: chainIndex list - const desiredKeys = primitives.map((_, i) => i); - const existingKeys = [...this._cardMap.keys()]; - - // Remove cards no longer present - for (const key of existingKeys) { - if (!desiredKeys.includes(key)) { - const el = this._cardMap.get(key); - if (el && el.parentElement) el.remove(); - this._cardMap.delete(key); - } - } - - // For a full reconciliation with reindexing, rebuild all cards - // (primitive indices change on add/remove/reorder) - const oldMap = this._cardMap; - this._cardMap = new Map(); - - // Remove all existing cards from DOM - while (cardList.firstChild) cardList.removeChild(cardList.firstChild); - - let flatOffset = 0; - for (let i = 0; i < primitives.length; i++) { - const prim = primitives[i]; - const card = this._createCard(prim, i, flatOffset); - this._cardMap.set(i, card); - cardList.appendChild(card); - flatOffset += prim.paramCount; - } - - // Seed signal with current param values if available - if (this._currentParams && this._currentParams.length >= totalParams) { - this._paramSignal.update(this._currentParams); - } - } - - /** - * Update which flat param indices are NISPS-controlled (pulsing indicator). - * @param {Array} paramIndices - */ - setLiveParams(paramIndices) { - this._liveParams = new Set(paramIndices); - if (!this._cardList) return; - const rows = this._cardList.querySelectorAll('.chain-param-row'); - rows.forEach((row) => { - const idx = parseInt(row.dataset.flatIndex, 10); - if (isNaN(idx)) return; - if (this._liveParams.has(idx)) { - row.classList.add('nisps-live'); - } else { - row.classList.remove('nisps-live'); - } - }); - } - - /** - * Register a callback for manual slider changes. - * @param {(flatParamIndex: number, value: number) => void} callback - */ - onParamChange(callback) { - this._paramChangeCb = callback; - } - - /** - * Update displayed param values without re-rendering. - * Backed by signal — only subscribers for changed indices are notified. - * @param {Float32Array|Array} params - flat param array - */ - updateParamValues(params) { - this._currentParams = params; - if (this._paramSignal) { - this._paramSignal.update(params); - } - } - - /** - * Cleanup all DOM and listeners. - */ - destroy() { - this._closePalette(); - this.close(); - document.removeEventListener('pointermove', this._onPointerMove); - document.removeEventListener('pointerup', this._onPointerUp); - - // Clean up signal subscriptions - for (const unsub of this._signalUnsubs) unsub(); - this._signalUnsubs = []; - - // Remove drawer from DOM - if (this._drawerOverlay) { - this._drawerOverlay.remove(); - this._drawerOverlay = null; - } - if (this._drawer) { - this._drawer.remove(); - this._drawer = null; - } - - this._container.innerHTML = ''; - this._container.classList.remove('chain-builder'); - this._paramChangeCb = null; - this._cardMap.clear(); - } - - // ── Drawer construction ─────────────────────────────────────────── - - /** - * @private - * Build the drawer shell (overlay + drawer container + header). - * The card list and controls go inside the drawer body. - */ - _buildDrawer() { - // Overlay - const overlay = document.createElement('div'); - overlay.className = 'chain-drawer-overlay'; - overlay.addEventListener('click', () => this.close()); - this._drawerOverlay = overlay; - - // Drawer - const drawer = document.createElement('div'); - drawer.className = 'chain-drawer'; - this._drawer = drawer; - - // Drag handle - const handle = document.createElement('div'); - handle.className = 'chain-drawer-handle'; - drawer.appendChild(handle); - - // Header - const header = document.createElement('div'); - header.className = 'chain-drawer-header'; - - const title = document.createElement('span'); - title.textContent = 'Chain Builder'; - header.appendChild(title); - - const closeBtn = document.createElement('button'); - closeBtn.className = 'chain-drawer-close'; - closeBtn.textContent = '\u00D7'; - closeBtn.addEventListener('click', () => this.close()); - header.appendChild(closeBtn); - - drawer.appendChild(header); - - // Body (scrollable card list + controls) - const body = document.createElement('div'); - body.classList.add('chain-builder'); - - const cardList = document.createElement('div'); - cardList.style.display = 'flex'; - cardList.style.flexDirection = 'column'; - cardList.style.gap = '6px'; - this._cardList = cardList; - body.appendChild(cardList); - - // Add button - const addBtn = document.createElement('button'); - addBtn.className = 'chain-add-btn'; - addBtn.innerHTML = '+ Add Primitive'; - addBtn.addEventListener('click', () => this._openPalette()); - body.appendChild(addBtn); - - // Generator combine mode toggle - this._combineToggle = this._createCombineToggle(); - body.appendChild(this._combineToggle); - - drawer.appendChild(body); - - // Mount to document body - document.body.appendChild(overlay); - document.body.appendChild(drawer); - } - - // ── Card creation ────────────────────────────────────────────────── - - /** - * @private - * @param {import('./primitive.js').Primitive} prim - * @param {number} chainIndex - * @param {number} flatOffset - flat param offset for this primitive - * @returns {HTMLElement} - */ - _createCard(prim, chainIndex, flatOffset) { - const card = document.createElement('div'); - card.className = 'chain-card'; - card.dataset.chainIndex = chainIndex; - if (this._expandedCards.has(chainIndex)) { - card.classList.add('expanded'); - } - - const catColor = CATEGORY_COLORS[prim.category] || '#888'; - - // Left color border - const leftBorder = document.createElement('div'); - leftBorder.className = 'chain-card-left-border'; - leftBorder.style.background = catColor; - card.appendChild(leftBorder); - - // Header - const header = document.createElement('div'); - header.className = 'chain-card-header'; - - // Drag handle - const drag = document.createElement('div'); - drag.className = 'chain-card-drag'; - drag.innerHTML = ''; - drag.addEventListener('pointerdown', (e) => this._handleDragStart(e, chainIndex, card)); - header.appendChild(drag); - - // Name - const name = document.createElement('span'); - name.className = 'chain-card-name'; - name.textContent = prim.name; - header.appendChild(name); - - // Category badge - const badge = document.createElement('span'); - badge.className = 'chain-card-badge'; - badge.textContent = prim.category; - badge.style.color = catColor; - header.appendChild(badge); - - // Summary (collapsed key params) - const summary = document.createElement('span'); - summary.className = 'chain-card-summary'; - summary.textContent = this._buildSummary(prim, flatOffset); - header.appendChild(summary); - - // Expand chevron - const chevron = document.createElement('span'); - chevron.className = 'chain-card-expand-icon'; - chevron.textContent = '\u25B6'; // right-pointing triangle - header.appendChild(chevron); - - // Delete button - const del = document.createElement('button'); - del.className = 'chain-card-delete'; - del.textContent = '\u00D7'; // multiplication sign as X - del.title = 'Remove'; - del.addEventListener('click', (e) => { - e.stopPropagation(); - this._removePrimitive(chainIndex); - }); - header.appendChild(del); - - // Toggle expand on header click (but not on drag handle or delete) - header.addEventListener('click', (e) => { - if (e.target.closest('.chain-card-drag') || e.target.closest('.chain-card-delete')) return; - this._toggleExpand(chainIndex, card); - }); - - card.appendChild(header); - - // Body (params) - const body = document.createElement('div'); - body.className = 'chain-card-body'; - - const paramsDiv = document.createElement('div'); - paramsDiv.className = 'chain-card-params'; - - for (let pi = 0; pi < prim.paramSchema.length; pi++) { - const schema = prim.paramSchema[pi]; - const flatIdx = flatOffset + pi; - const row = this._createParamRow(schema, flatIdx, prim); - paramsDiv.appendChild(row); - } - - body.appendChild(paramsDiv); - card.appendChild(body); - - // Subscribe summary to signal updates - if (this._paramSignal) { - const unsub = this._paramSignal.subscribe(() => { - summary.textContent = this._buildSummary(prim, flatOffset); - }); - this._signalUnsubs.push(unsub); - } - - return card; - } - - /** - * @private - */ - _buildSummary(prim, flatOffset) { - const schema = prim.paramSchema; - const maxShow = Math.min(3, schema.length); - const parts = []; - for (let i = 0; i < maxShow; i++) { - const val = this._currentParams - ? this._currentParams[flatOffset + i] - : schema[i].default; - parts.push(schema[i].name + ':' + (val !== undefined ? val.toFixed(2) : schema[i].default.toFixed(2))); - } - return parts.join(' '); - } - - /** - * @private - * Creates a param row with a slider that subscribes to the param signal - * for its specific flat index — O(1) updates per changed param. - */ - _createParamRow(schema, flatIdx, prim) { - const row = document.createElement('div'); - row.className = 'chain-param-row'; - row.dataset.flatIndex = flatIdx; - if (this._liveParams.has(flatIdx)) { - row.classList.add('nisps-live'); - } - - const label = document.createElement('span'); - label.className = 'chain-param-label'; - label.textContent = schema.name; - row.appendChild(label); - - const slider = document.createElement('input'); - slider.type = 'range'; - slider.className = 'chain-param-slider'; - slider.min = '0'; - slider.max = '1'; - slider.step = '0.001'; - slider.dataset.flatIndex = flatIdx; - - const currentVal = this._currentParams - ? this._currentParams[flatIdx] - : schema.default; - slider.value = currentVal !== undefined ? currentVal : schema.default; - - const valDisplay = document.createElement('span'); - valDisplay.className = 'chain-param-value'; - valDisplay.textContent = parseFloat(slider.value).toFixed(2); - - slider.addEventListener('input', () => { - const v = parseFloat(slider.value); - valDisplay.textContent = v.toFixed(2); - if (this._paramChangeCb) { - this._paramChangeCb(flatIdx, v); - } - }); - - row.appendChild(slider); - row.appendChild(valDisplay); - - // Subscribe to signal for reactive updates (only this slider's index) - if (this._paramSignal) { - const unsub = this._paramSignal.subscribe((params) => { - if (flatIdx < params.length) { - const newVal = params[flatIdx]; - slider.value = newVal; - valDisplay.textContent = newVal.toFixed(2); - } - }); - this._signalUnsubs.push(unsub); - } - - return row; - } - - // ── Expand / collapse ────────────────────────────────────────────── - - /** - * @private - */ - _toggleExpand(chainIndex, cardEl) { - if (this._expandedCards.has(chainIndex)) { - this._expandedCards.delete(chainIndex); - cardEl.classList.remove('expanded'); - } else { - this._expandedCards.add(chainIndex); - cardEl.classList.add('expanded'); - } - } - - // ── Add / remove primitives ──────────────────────────────────────── - - /** - * @private - */ - _removePrimitive(chainIndex) { - this._chain.removePrimitive(chainIndex); - // Adjust expanded set - const newExpanded = new Set(); - for (const idx of this._expandedCards) { - if (idx < chainIndex) newExpanded.add(idx); - else if (idx > chainIndex) newExpanded.add(idx - 1); - // idx === chainIndex is removed - } - this._expandedCards = newExpanded; - this.render(); - this._bus.emit('ui.chainEdit', { action: 'remove', index: chainIndex }); - } - - /** - * @private - */ - _addPrimitive(registryKey) { - const Ctor = PRIMITIVE_REGISTRY[registryKey]; - if (!Ctor) return; - const instance = new Ctor(); - this._chain.addPrimitive(instance); - this.render(); - this._bus.emit('ui.chainEdit', { action: 'add', name: registryKey }); - } - - // ── Palette ──────────────────────────────────────────────────────── - - /** - * @private - */ - _openPalette() { - if (this._paletteOverlay) return; - - const overlay = document.createElement('div'); - overlay.className = 'chain-palette-overlay'; - - const palette = document.createElement('div'); - palette.className = 'chain-palette'; - - const title = document.createElement('div'); - title.className = 'chain-palette-title'; - title.textContent = 'Add Primitive'; - palette.appendChild(title); - - const keys = Object.keys(PRIMITIVE_REGISTRY); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const CtorRef = PRIMITIVE_REGISTRY[key]; - // Instantiate temporarily to read metadata - const temp = new CtorRef(); - - const item = document.createElement('button'); - item.className = 'chain-palette-item'; - - const colorBar = document.createElement('div'); - colorBar.className = 'chain-palette-item-color'; - colorBar.style.background = CATEGORY_COLORS[temp.category] || '#888'; - item.appendChild(colorBar); - - const info = document.createElement('div'); - info.className = 'chain-palette-item-info'; - - const nameEl = document.createElement('div'); - nameEl.className = 'chain-palette-item-name'; - nameEl.textContent = temp.name; - info.appendChild(nameEl); - - const meta = document.createElement('div'); - meta.className = 'chain-palette-item-meta'; - meta.textContent = temp.category + ' \u00B7 ' + temp.paramCount + ' params'; - info.appendChild(meta); - - item.appendChild(info); - - item.addEventListener('click', () => { - this._closePalette(); - this._addPrimitive(key); - }); - - palette.appendChild(item); - } - - overlay.appendChild(palette); - - // Close on overlay click (outside palette) - overlay.addEventListener('click', (e) => { - if (e.target === overlay) { - this._closePalette(); - } - }); - - document.body.appendChild(overlay); - this._paletteOverlay = overlay; - } - - /** - * @private - */ - _closePalette() { - if (this._paletteOverlay) { - this._paletteOverlay.remove(); - this._paletteOverlay = null; - } - } - - // ── Generator combine mode toggle ────────────────────────────────── - - /** - * @private - */ - _createCombineToggle() { - const wrapper = document.createElement('div'); - wrapper.className = 'chain-combine-toggle'; - - const label = document.createElement('span'); - label.className = 'chain-combine-label'; - label.textContent = 'Gen combine:'; - wrapper.appendChild(label); - - const btn = document.createElement('button'); - btn.className = 'chain-combine-btn'; - btn.textContent = this._chain.generatorCombineMode === 'additive' ? 'Add' : 'Mul'; - - btn.addEventListener('click', () => { - if (this._chain.generatorCombineMode === 'additive') { - this._chain.generatorCombineMode = 'multiplicative'; - btn.textContent = 'Mul'; - } else { - this._chain.generatorCombineMode = 'additive'; - btn.textContent = 'Add'; - } - this._bus.emit('ui.chainEdit', { action: 'combineMode', mode: this._chain.generatorCombineMode }); - }); - - wrapper.appendChild(btn); - return wrapper; - } - - // ── Drag-to-reorder (pointer events) ─────────────────────────────── - - /** - * @private - */ - _handleDragStart(e, chainIndex, cardEl) { - e.preventDefault(); - e.stopPropagation(); - - this._dragIndex = chainIndex; - this._dragEl = cardEl; - this._dragStartY = e.clientY; - this._dragOffsetY = 0; - - cardEl.classList.add('dragging'); - cardEl.setPointerCapture(e.pointerId); - - document.addEventListener('pointermove', this._onPointerMove); - document.addEventListener('pointerup', this._onPointerUp); - } - - /** - * @private - */ - _handlePointerMove(e) { - if (this._dragIndex < 0 || !this._dragEl || !this._cardList) return; - - const dy = e.clientY - this._dragStartY; - this._dragOffsetY = dy; - this._dragEl.style.transform = 'translateY(' + dy + 'px) scale(1.03)'; - - // Determine drop target by checking which card we're over - const cards = this._cardList.querySelectorAll('.chain-card'); - const dragRect = this._dragEl.getBoundingClientRect(); - const dragCenter = dragRect.top + dragRect.height / 2; - - let targetIndex = this._dragIndex; - for (let i = 0; i < cards.length; i++) { - if (i === this._dragIndex) continue; - const rect = cards[i].getBoundingClientRect(); - const center = rect.top + rect.height / 2; - if (this._dragIndex < i && dragCenter > center) { - targetIndex = i; - } else if (this._dragIndex > i && dragCenter < center) { - targetIndex = i; - } - } - - this._dragTargetIndex = targetIndex; - } - - /** - * @private - */ - _handlePointerUp(e) { - document.removeEventListener('pointermove', this._onPointerMove); - document.removeEventListener('pointerup', this._onPointerUp); - - if (this._dragEl) { - this._dragEl.classList.remove('dragging'); - this._dragEl.style.transform = ''; - } - - const from = this._dragIndex; - const to = this._dragTargetIndex !== undefined ? this._dragTargetIndex : from; - - this._dragIndex = -1; - this._dragEl = null; - this._dragTargetIndex = undefined; - - if (from !== to && from >= 0) { - // Adjust expanded set for the move - const newExpanded = new Set(); - for (const idx of this._expandedCards) { - if (idx === from) { - newExpanded.add(to); - } else { - let adjusted = idx; - if (from < to) { - if (idx > from && idx <= to) adjusted = idx - 1; - } else { - if (idx >= to && idx < from) adjusted = idx + 1; - } - newExpanded.add(adjusted); - } - } - this._expandedCards = newExpanded; - - this._chain.movePrimitive(from, to); - this.render(); - this._bus.emit('ui.chainEdit', { action: 'reorder', from: from, to: to }); - } - } -} diff --git a/playground/js/shapeseq/chain.js b/playground/js/shapeseq/chain.js deleted file mode 100644 index 27cf5d1..0000000 --- a/playground/js/shapeseq/chain.js +++ /dev/null @@ -1,397 +0,0 @@ -/** - * ShapeSeq Chain — sequential pipeline runner with generator combination modes - * - * Evaluates an ordered list of primitives as a sequential pipeline: - * 1. Generators run first, combined via additive or multiplicative merge - * 2. Processors transform the pattern in chain order - * 3. Converters run in chain order - * 4. Timing modifiers annotate last - * - * Params are distributed flat across primitives in chain order. - * Each primitive gets a deterministic PRNG stream via fork(masterPRNG, index). - * - * Port-ready: explicit state, typed arrays, no closures in hot path. - * - * @module shapeseq/chain - */ - -import { createPattern, mergePatterns } from './pattern.js'; -import { createPRNG, fork } from './prng.js'; - -// ── Category execution order ──────────────────────────────────────── - -const PHASE_ORDER = ['generator', 'processor', 'converter', 'timing']; - -// ── Chain class ───────────────────────────────────────────────────── - -export class Chain { - constructor() { - /** @private @type {Array} */ - this._primitives = []; - - /** @type {'additive'|'multiplicative'} */ - this.generatorCombineMode = 'additive'; - - /** - * Per-generator step counts for polyrhythm support. - * Maps chain index → step count. Generators not in this map - * use the global stepCount passed to evaluate(). - * @private @type {Map} - */ - this._generatorStepCounts = new Map(); - - /** @private @type {number} */ - this._masterSeed = 0; - - /** - * Per-primitive state objects, indexed by position in chain. - * Populated after evaluate() calls; used for freeze support. - * @private @type {Array} - */ - this._primitiveStates = []; - } - - // ── Primitive management ──────────────────────────────────────── - - /** - * Append a primitive to the end of the chain. - * @param {import('./primitive.js').Primitive} primitive - */ - addPrimitive(primitive) { - this._primitives.push(primitive); - this._primitiveStates.push(primitive.getState()); - } - - /** - * Remove the primitive at the given index. - * @param {number} index - */ - removePrimitive(index) { - const idx = index | 0; - if (idx < 0 || idx >= this._primitives.length) { - throw new RangeError('removePrimitive: index ' + index + ' out of range [0, ' + (this._primitives.length - 1) + ']'); - } - this._primitives.splice(idx, 1); - this._primitiveStates.splice(idx, 1); - } - - /** - * Insert a primitive at the given index, shifting others right. - * @param {number} index - * @param {import('./primitive.js').Primitive} primitive - */ - insertPrimitive(index, primitive) { - const idx = index | 0; - if (idx < 0 || idx > this._primitives.length) { - throw new RangeError('insertPrimitive: index ' + index + ' out of range [0, ' + this._primitives.length + ']'); - } - this._primitives.splice(idx, 0, primitive); - this._primitiveStates.splice(idx, 0, primitive.getState()); - } - - /** - * Move a primitive from one position to another. - * @param {number} fromIndex - * @param {number} toIndex - */ - movePrimitive(fromIndex, toIndex) { - const from = fromIndex | 0; - const to = toIndex | 0; - const len = this._primitives.length; - if (from < 0 || from >= len) { - throw new RangeError('movePrimitive: fromIndex ' + fromIndex + ' out of range [0, ' + (len - 1) + ']'); - } - if (to < 0 || to >= len) { - throw new RangeError('movePrimitive: toIndex ' + toIndex + ' out of range [0, ' + (len - 1) + ']'); - } - - const [prim] = this._primitives.splice(from, 1); - const [state] = this._primitiveStates.splice(from, 1); - this._primitives.splice(to, 0, prim); - this._primitiveStates.splice(to, 0, state); - } - - /** - * Get the current list of primitives (shallow copy). - * @returns {Array} - */ - getPrimitives() { - return this._primitives.slice(); - } - - /** - * Returns true if any primitive in the chain has reEvalOnLoop === true. - * Used by the sequencer to decide whether to re-evaluate the pipeline - * on each loop start, even when inputs haven't changed. - * - * @returns {boolean} - */ - hasReEvalPrimitives() { - for (let i = 0; i < this._primitives.length; i++) { - if (this._primitives[i].reEvalOnLoop === true) { - return true; - } - } - return false; - } - - // ── Per-generator step counts (polyrhythm) ───────────────────── - - /** - * Set a per-generator step count for polyrhythm. - * The chain index must refer to a primitive in the chain. - * - * @param {number} chainIndex - Index in the chain's primitive list - * @param {number} stepCount - Step count for this generator (positive integer) - */ - setGeneratorStepCount(chainIndex, stepCount) { - const idx = chainIndex | 0; - const sc = stepCount | 0; - if (idx < 0 || idx >= this._primitives.length) { - throw new RangeError('setGeneratorStepCount: chainIndex ' + chainIndex + ' out of range [0, ' + (this._primitives.length - 1) + ']'); - } - if (sc < 1) { - throw new RangeError('setGeneratorStepCount: stepCount must be >= 1, got ' + stepCount); - } - this._generatorStepCounts.set(idx, sc); - } - - /** - * Get the per-generator step count, or null if using global. - * - * @param {number} chainIndex - * @returns {number|null} - */ - getGeneratorStepCount(chainIndex) { - const idx = chainIndex | 0; - return this._generatorStepCounts.has(idx) ? this._generatorStepCounts.get(idx) : null; - } - - /** - * Clear all per-generator step counts, reverting to global stepCount. - */ - clearGeneratorStepCounts() { - this._generatorStepCounts.clear(); - } - - // ── Configuration ─────────────────────────────────────────────── - - /** - * Total parameter count across all primitives in the chain. - * @returns {number} - */ - get totalParamCount() { - let total = 0; - for (let i = 0; i < this._primitives.length; i++) { - total += this._primitives[i].paramCount; - } - return total; - } - - /** - * Get a flat list of all param schemas across all primitives, - * annotated with their primitive and param indices. - * - * @returns {Array<{ primitiveIndex: number, paramIndex: number, schema: Object }>} - */ - getParamSchemas() { - const result = []; - for (let pi = 0; pi < this._primitives.length; pi++) { - const prim = this._primitives[pi]; - const schema = prim.paramSchema; - for (let si = 0; si < schema.length; si++) { - result.push({ - primitiveIndex: pi, - paramIndex: si, - schema: schema[si], - }); - } - } - return result; - } - - // ── Evaluation ────────────────────────────────────────────────── - - /** - * Evaluate the chain, producing a pattern description. - * - * Pipeline order: - * 1. Generators — combined via generatorCombineMode - * 2. Processors — sequential transform - * 3. Converters — sequential transform - * 4. Timing modifiers — annotate last - * - * @param {Float32Array|Array} params - flat param array distributed across primitives - * @param {number} stepCount - number of steps in the output pattern - * @param {number} masterSeed - seed for the master PRNG - * @param {Map|null} [generatorStepCounts=null] - optional per-generator step counts (chain index → step count). If null, uses this._generatorStepCounts. - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ - evaluate(params, stepCount, masterSeed, generatorStepCounts = null) { - const primitives = this._primitives; - const primCount = primitives.length; - - // Create master PRNG from seed - const masterPRNG = createPRNG(masterSeed >>> 0); - - // ── Bucket primitives by category, preserving chain order ── - - /** @type {Array<{ index: number, prim: Object }>} */ - const generators = []; - const processors = []; - const converters = []; - const timingMods = []; - - for (let i = 0; i < primCount; i++) { - const entry = { index: i, prim: primitives[i] }; - switch (primitives[i].category) { - case 'generator': generators.push(entry); break; - case 'processor': processors.push(entry); break; - case 'converter': converters.push(entry); break; - case 'timing': timingMods.push(entry); break; - } - } - - // ── Compute param offsets per primitive ── - - const paramOffsets = new Array(primCount); - let offset = 0; - for (let i = 0; i < primCount; i++) { - paramOffsets[i] = offset; - offset += primitives[i].paramCount; - } - - // ── Helper: run a single primitive ── - - const self = this; - - function runPrimitive(entry, inputPattern) { - const idx = entry.index; - const prim = entry.prim; - const pOffset = paramOffsets[idx]; - const pCount = prim.paramCount; - - // Slice params for this primitive - const primParams = new Float32Array(pCount); - for (let p = 0; p < pCount; p++) { - primParams[p] = pOffset + p < params.length ? +params[pOffset + p] : prim.paramSchema[p].default; - } - - // Fork a deterministic PRNG for this primitive - const primRNG = fork(masterPRNG, idx); - - // Get current state - const state = self._primitiveStates[idx] || prim.getState(); - - // Process - const result = prim.process(primParams, inputPattern, state, primRNG); - - // Store updated state - self._primitiveStates[idx] = result.nextState; - - return result.patternDesc; - } - - // ── Phase 1: Generators ── - - // Resolve per-generator step counts: explicit arg > instance map > global - const genStepMap = generatorStepCounts || this._generatorStepCounts; - - /** Look up the step count for a generator by its chain index. */ - function genStepsFor(chainIndex) { - if (genStepMap && genStepMap.size > 0) { - const override = genStepMap.get(chainIndex); - if (override != null) return override; - } - return stepCount; - } - - let pattern; - - if (generators.length === 0) { - // Default pattern: all steps triggered - pattern = createPattern(stepCount); - for (let i = 0; i < stepCount; i++) { - pattern.steps[i].trigger = true; - } - } else if (generators.length === 1) { - // Single generator — no merge needed - const gs = genStepsFor(generators[0].index); - pattern = runPrimitive(generators[0], createPattern(gs)); - } else { - // Multiple generators — run each with its own step count, then merge - let merged = runPrimitive(generators[0], createPattern(genStepsFor(generators[0].index))); - for (let g = 1; g < generators.length; g++) { - const gs = genStepsFor(generators[g].index); - const next = runPrimitive(generators[g], createPattern(gs)); - merged = mergePatterns(merged, next, this.generatorCombineMode); - } - pattern = merged; - } - - // ── Phase 2: Processors ── - - for (let i = 0; i < processors.length; i++) { - pattern = runPrimitive(processors[i], pattern); - } - - // ── Phase 3: Converters ── - - for (let i = 0; i < converters.length; i++) { - pattern = runPrimitive(converters[i], pattern); - } - - // ── Phase 4: Timing modifiers ── - - for (let i = 0; i < timingMods.length; i++) { - pattern = runPrimitive(timingMods[i], pattern); - } - - return pattern; - } - - // ── State management (for freeze) ────────────────────────────── - - /** - * Get serializable state for all primitives in the chain. - * @returns {Array} - */ - getState() { - const states = new Array(this._primitives.length); - for (let i = 0; i < this._primitives.length; i++) { - states[i] = this._primitiveStates[i] || this._primitives[i].getState(); - } - return states; - } - - /** - * Restore all primitive states from a previously serialized state array. - * @param {Array} states - */ - setState(states) { - if (!Array.isArray(states)) { - throw new TypeError('setState expects an array of state objects'); - } - const len = Math.min(states.length, this._primitives.length); - for (let i = 0; i < len; i++) { - this._primitives[i].setState(states[i]); - this._primitiveStates[i] = states[i]; - } - } - - /** - * Get the master PRNG seed. - * @returns {number} - */ - getMasterSeed() { - return this._masterSeed; - } - - /** - * Set the master PRNG seed. - * @param {number} seed - 32-bit integer seed - */ - setMasterSeed(seed) { - this._masterSeed = seed >>> 0; - } -} diff --git a/playground/js/shapeseq/clock.js b/playground/js/shapeseq/clock.js deleted file mode 100644 index 0594907..0000000 --- a/playground/js/shapeseq/clock.js +++ /dev/null @@ -1,269 +0,0 @@ -/** - * ShapeSeq Clock Engine - * - * AudioContext-based lookahead scheduler that steps through a pattern - * description, emitting seq.* events on the event bus with sample-accurate - * timing. Replaces the old setTimeout-based arpeggiator approach. - * - * Uses the standard Web Audio lookahead pattern: - * - setInterval (~25 ms) checks if events need scheduling in the next ~100 ms - * - Events scheduled via AudioContext.currentTime for sample-accurate timing - * - Visual updates decoupled from audio timing - * - * @module shapeseq/clock - */ - -import { SEQ } from './event-bus.js'; -import { validatePattern } from './pattern.js'; - -// ── Scheduling constants ────────────────────────────────────────────── - -const LOOKAHEAD_MS = 25; // how often the timer fires (ms) -const SCHEDULE_AHEAD = 0.1; // how far ahead to schedule (seconds) - -const SUBDIVISION_VEL_SCALE = 0.8; // velocity multiplier for ratchet hits - -// ── ClockEngine ─────────────────────────────────────────────────────── - -export class ClockEngine { - /** - * @param {AudioContext} audioContext - * @param {import('./event-bus.js').EventBus} [eventBus] - */ - constructor(audioContext, eventBus) { - if (!audioContext) { - throw new TypeError('ClockEngine requires an AudioContext'); - } - - this._ctx = audioContext; - this._bus = eventBus ?? null; - this._bpm = 120; - this._playing = false; - - // Pattern state - this._pattern = null; // current pattern description - this._currentStep = 0; - this._nextNoteTime = 0; // AudioContext time of the next step - - // Scheduler handle - this._timerId = null; - - // Direct callback listeners (besides the event bus) - this._callbacks = []; - - // Track the last scheduled noteOn so we can emit noteOff before the next - this._lastNote = null; // { stepIndex, time, pitch, velocity } - } - - // ── Public properties ─────────────────────────────────────────────── - - get bpm() { return this._bpm; } - set bpm(v) { this._bpm = Math.max(1, +v || 120); } - - get playing() { return this._playing; } - - // ── Public API ────────────────────────────────────────────────────── - - /** - * Start the clock. Begins scheduling from step 0 (or the current step - * if a pattern was hot-swapped while stopped). - */ - start() { - if (this._playing) return; - if (!this._pattern) return; // nothing to play - - this._playing = true; - this._currentStep = 0; - this._nextNoteTime = this._ctx.currentTime + 0.05; // tiny lead-in - this._lastNote = null; - - this._timerId = setInterval(() => this._scheduler(), LOOKAHEAD_MS); - } - - /** Stop the clock and send a final noteOff for the last sounding note. */ - stop() { - if (!this._playing) return; - this._playing = false; - - if (this._timerId !== null) { - clearInterval(this._timerId); - this._timerId = null; - } - - // Release lingering note - if (this._lastNote) { - this._emit(SEQ.NOTE_OFF, { - stepIndex: this._lastNote.stepIndex, - time: this._ctx.currentTime, - pitch: this._lastNote.pitch, - midiNote: this._lastNote.midiNote, - velocity: 0, - }); - this._lastNote = null; - } - } - - /** - * Update tempo. Takes effect on the next scheduled step. - * @param {number} bpm - */ - setTempo(bpm) { - this.bpm = bpm; - } - - /** - * Provide (or replace) the pattern the clock steps through. - * Safe to call while playing — the clock picks up the new pattern - * on the next scheduling pass. - * - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - */ - schedulePattern(patternDesc) { - if (!validatePattern(patternDesc)) { - throw new TypeError('Invalid pattern description'); - } - this._pattern = patternDesc; - - // If the current step is beyond the new pattern's length, wrap it - if (this._currentStep >= patternDesc.stepCount) { - this._currentStep = 0; - } - } - - /** - * Register a direct callback that fires for every scheduled event. - * @param {function} callback — receives (eventName, data) - */ - onEvent(callback) { - if (typeof callback === 'function') { - this._callbacks.push(callback); - } - } - - /** - * Return the current step index (useful for visualization sync). - * @returns {number} - */ - getCurrentStep() { - return this._currentStep; - } - - // ── Internals ─────────────────────────────────────────────────────── - - /** - * The core lookahead scheduler. Called every LOOKAHEAD_MS, it walks - * forward through the pattern scheduling any steps whose time falls - * within the lookahead window. - */ - _scheduler() { - if (!this._pattern) return; - - const deadline = this._ctx.currentTime + SCHEDULE_AHEAD; - - while (this._nextNoteTime < deadline) { - this._scheduleStep(this._currentStep, this._nextNoteTime); - this._advance(); - } - } - - /** - * Schedule all events for a single step (including subdivisions). - * - * @param {number} stepIndex - * @param {number} baseTime - AudioContext time for this step - */ - _scheduleStep(stepIndex, baseTime) { - const step = this._pattern.steps[stepIndex]; - const stepDuration = this._stepDuration(); - - // Apply swing / timeOffset: shift the step forward or back within - // [-0.5, 0.5] of one step's duration. - const offsetTime = baseTime + step.timeOffset * stepDuration; - - // Detect loop wraparound - if (stepIndex === 0) { - this._emit(SEQ.LOOP_START, { stepIndex: 0, time: offsetTime }); - } - - // Visual step event (always fires, even for rests) - this._emit(SEQ.STEP, { stepIndex, time: offsetTime }); - - // If this step has no trigger, we're done (rest) - if (!step.trigger) return; - - const subs = step.subdivisions; - - const midiNote = step.midiNote != null ? step.midiNote : null; - - if (subs <= 1) { - // Single hit - this._scheduleNote(stepIndex, offsetTime, step.pitch, step.velocity, step.accent, false, midiNote); - } else { - // Ratchet: evenly divide this step's duration - const subDur = stepDuration / subs; - for (let s = 0; s < subs; s++) { - const t = offsetTime + s * subDur; - const vel = s === 0 ? step.velocity : step.velocity * SUBDIVISION_VEL_SCALE; - this._scheduleNote(stepIndex, t, step.pitch, vel, step.accent, s > 0, midiNote); - } - } - } - - /** - * Schedule a single noteOn (with preceding noteOff for the previous note). - */ - _scheduleNote(stepIndex, time, pitch, velocity, accent, isSubdivision, midiNote) { - // NoteOff for previous note - if (this._lastNote) { - this._emit(SEQ.NOTE_OFF, { - stepIndex: this._lastNote.stepIndex, - time, - pitch: this._lastNote.pitch, - midiNote: this._lastNote.midiNote, - velocity: 0, - }); - } - - const noteData = { stepIndex, time, pitch, midiNote: midiNote ?? null, velocity, accent, isSubdivision }; - this._emit(SEQ.NOTE_ON, noteData); - this._lastNote = { stepIndex, time, pitch, midiNote: midiNote ?? null, velocity }; - } - - /** - * Advance the current step and compute the time for the next step. - */ - _advance() { - this._nextNoteTime += this._stepDuration(); - this._currentStep += 1; - - if (this._currentStep >= this._pattern.stepCount) { - this._currentStep = 0; - } - } - - /** - * Duration of one step in seconds at the current BPM. - * One step = one 16th note (4 steps per beat). - * At 120 BPM: one step = 0.125s, 8 steps = 1 second. - * @returns {number} - */ - _stepDuration() { - return 60 / this._bpm / 4; - } - - // ── Event emission ────────────────────────────────────────────────── - - /** - * Emit an event to both the event bus and any direct callbacks. - * @param {string} eventName - * @param {Object} data - */ - _emit(eventName, data) { - if (this._bus) { - this._bus.emit(eventName, data); - } - for (let i = 0; i < this._callbacks.length; i++) { - this._callbacks[i](eventName, data); - } - } -} diff --git a/playground/js/shapeseq/delta.js b/playground/js/shapeseq/delta.js deleted file mode 100644 index 1597964..0000000 --- a/playground/js/shapeseq/delta.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * ShapeSeq DeltaController — MLP delta application with boundary enforcement - * - * Applies MLP output deltas to frozen param values for live (unfrozen) params. - * Frozen params pass through unchanged. Live params get: - * frozenValue + (mlpValue - 0.5) * 2 * deltaScale - * with per-param boundary enforcement (clamp, wrap, scaled). - * - * @module shapeseq/delta - */ - -import { applyBoundary } from './primitive.js'; - -export class DeltaController { - /** - * Compute effective params by applying MLP-derived deltas to frozen values. - * - * For frozen params: returns the frozen value unchanged. - * For live params: frozenValue + delta, with boundary enforcement. - * - * The delta is derived from the MLP output: delta = (mlpValue - 0.5) * 2 * deltaScale - * This maps MLP [0,1] output to a [-deltaScale, +deltaScale] range centered on the frozen value. - * - * @param {Float32Array} frozenParams - captured param values from FreezeManager - * @param {Uint8Array} liveFlags - 1=live, 0=frozen from FreezeManager - * @param {Float32Array} mlpParams - current MLP-mapped param values [0,1] - * @param {Array<{schema: {boundary: string, scaledRange?: number}}>} paramSchemas - from chain.getParamSchemas() - * @param {number} deltaScale - global delta sensitivity (default 0.3) - * @returns {Float32Array} effective params ready for chain.evaluate() - */ - static computeEffective(frozenParams, liveFlags, mlpParams, paramSchemas, deltaScale = 0.3) { - const count = frozenParams.length; - const result = new Float32Array(count); - - for (let i = 0; i < count; i++) { - if (!liveFlags[i]) { - // Frozen: passthrough - result[i] = frozenParams[i]; - } else { - // Live: apply delta with boundary enforcement - const delta = (mlpParams[i] - 0.5) * 2 * deltaScale; - const raw = frozenParams[i] + delta; - const schema = paramSchemas[i]?.schema || { boundary: 'clamp' }; - result[i] = applyBoundary(raw, schema, frozenParams[i]); - } - } - - return result; - } -} diff --git a/playground/js/shapeseq/event-bus.js b/playground/js/shapeseq/event-bus.js deleted file mode 100644 index 4a8d585..0000000 --- a/playground/js/shapeseq/event-bus.js +++ /dev/null @@ -1,2 +0,0 @@ -// playground/js/shapeseq/event-bus.js — re-exports from shared module -export { EventBus, SEQ, ML, UI, getDefaultBus } from '../event-bus.js'; diff --git a/playground/js/shapeseq/freeze-ui.js b/playground/js/shapeseq/freeze-ui.js deleted file mode 100644 index 745c009..0000000 --- a/playground/js/shapeseq/freeze-ui.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Freeze UI — adds freeze/live toggle interaction to chain param rows. - * - * When frozen: - * - Param rows are dimmed by default (frozen state) - * - Tapping a param row toggles it to "live" (highlighted, receives ML deltas) - * - Live params show a secondary indicator (delta range bar) on the slider - * - * When not frozen: - * - All params show normally (no freeze indicators) - * - * @module shapeseq/freeze-ui - */ - -// ── Inject styles once ─────────────────────────────────────────────── - -let stylesInjected = false; - -function injectStyles() { - if (stylesInjected) return; - stylesInjected = true; - - const css = ` -/* ── Freeze UI states ───────────────────────────────────────── */ - -.param-frozen { - cursor: pointer; -} - -.param-frozen .chain-param-slider { - pointer-events: none; -} - -.param-live { - cursor: pointer; -} - -.param-live .chain-param-slider { - box-shadow: 0 0 0 2px rgba(0, 200, 100, 0.5); -} -`; - - const style = document.createElement('style'); - style.textContent = css; - document.head.appendChild(style); -} - -// ── FreezeUI ───────────────────────────────────────────────────────── - -export class FreezeUI { - /** - * @param {{ container: HTMLElement, freezeManager: import('./freeze.js').FreezeManager, onToggle: (flatIndex: number) => void }} opts - */ - constructor({ container, freezeManager, onToggle }) { - injectStyles(); - - this._container = container; - this._fm = freezeManager; - this._onToggle = onToggle; - this._clickHandler = this._handleClick.bind(this); - - // Delegate click handling on param rows - this._container.addEventListener('click', this._clickHandler); - } - - /** - * Update visual state of all param rows based on freeze state. - * Call this after freeze/unfreeze/toggle. - */ - update() { - if (!this._fm.isFrozen) { - // Remove all freeze styling - this._container.querySelectorAll('.chain-param-row').forEach(row => { - row.classList.remove('param-frozen', 'param-live'); - row.style.opacity = ''; - }); - return; - } - - const liveFlags = this._fm.getLiveFlags(); - if (!liveFlags) return; - - this._container.querySelectorAll('.chain-param-row').forEach(row => { - const idx = parseInt(row.dataset.flatIndex, 10); - if (isNaN(idx) || idx >= liveFlags.length) return; - - if (liveFlags[idx]) { - row.classList.add('param-live'); - row.classList.remove('param-frozen'); - row.style.opacity = ''; - } else { - row.classList.add('param-frozen'); - row.classList.remove('param-live'); - row.style.opacity = '0.4'; - } - }); - } - - /** - * Show delta indicators on live param sliders. - * @param {Float32Array} frozenParams - * @param {Float32Array} effectiveParams - */ - updateDeltaIndicators(frozenParams, effectiveParams) { - if (!this._fm.isFrozen || !frozenParams || !effectiveParams) return; - - this._container.querySelectorAll('.chain-param-row.param-live').forEach(row => { - const idx = parseInt(row.dataset.flatIndex, 10); - if (isNaN(idx)) return; - - // Show delta as a secondary mark on the slider - let indicator = row.querySelector('.delta-indicator'); - if (!indicator) { - indicator = document.createElement('div'); - indicator.className = 'delta-indicator'; - indicator.style.cssText = 'position:absolute;height:4px;background:rgba(0,140,255,0.6);border-radius:2px;bottom:0;pointer-events:none;transition:left 0.1s,width 0.1s;'; - const slider = row.querySelector('.chain-param-slider'); - if (slider) { - slider.parentElement.style.position = 'relative'; - slider.parentElement.appendChild(indicator); - } - } - - // Position indicator between frozen value and effective value - const frozen = frozenParams[idx]; - const effective = effectiveParams[idx]; - const left = Math.min(frozen, effective) * 100; - const width = Math.abs(effective - frozen) * 100; - indicator.style.left = left + '%'; - indicator.style.width = width + '%'; - }); - } - - /** - * @private - */ - _handleClick(e) { - if (!this._fm.isFrozen) return; - - const row = e.target.closest('.chain-param-row'); - if (!row) return; - - // Don't toggle when interacting with the slider itself - if (e.target.closest('.chain-param-slider')) return; - - const idx = parseInt(row.dataset.flatIndex, 10); - if (isNaN(idx)) return; - - this._onToggle(idx); - this.update(); - } - - /** - * Cleanup all DOM listeners. - */ - destroy() { - this._container.removeEventListener('click', this._clickHandler); - } -} diff --git a/playground/js/shapeseq/freeze.js b/playground/js/shapeseq/freeze.js deleted file mode 100644 index 4c56f42..0000000 --- a/playground/js/shapeseq/freeze.js +++ /dev/null @@ -1,246 +0,0 @@ -/** - * ShapeSeq FreezeManager — freeze-as-algorithm and freeze-as-pattern - * - * Two freeze modes: - * - * - **algorithm**: Captures all primitive param values + PRNG seeds. - * Frozen params hold their captured values; individual params can - * be selectively re-exposed as "live" for ML-driven exploration. - * - * - **pattern**: Captures the realized note pattern (the actual step - * events for one full loop). The sequencer bypasses the primitive - * chain entirely and just loops the frozen pattern. - * - * Delta math (proper delta+boundary logic) is a separate issue (meml-7goy). - * This module provides the simple version: frozen params use captured values, - * live params pass through current ML values. - * - * @module shapeseq/freeze - */ - -import { clonePattern } from './pattern.js'; - -export class FreezeManager { - constructor() { - /** @private */ this._frozen = false; - /** @private @type {'algorithm'|'pattern'|null} */ this._freezeMode = null; - /** @private @type {Float32Array|null} */ this._frozenParams = null; - /** @private @type {Array|null} */ this._frozenSeeds = null; - /** @private @type {Array|null} */ this._frozenStates = null; - /** @private @type {Uint8Array|null} */ this._liveFlags = null; - /** @private @type {number|null} */ this._masterSeed = null; - /** @private @type {Object|null} */ this._frozenPattern = null; - } - - // ── Freeze / unfreeze ─────────────────────────────────────────────── - - /** - * Capture a full snapshot of the current state. - * - * In **algorithm** mode (default): captures params, seeds, and states. - * All params start frozen (liveFlags = 0); use toggleParam() or - * setParamLive() to selectively re-expose params for ML control. - * - * In **pattern** mode: captures the realized note pattern via deep clone. - * The primitive chain is bypassed entirely — the sequencer just loops - * the frozen pattern. - * - * @param {import('./chain.js').Chain|null} chain - * @param {Float32Array|Array|null} currentParams - flat param array - * @param {number|null} masterSeed - * @param {'algorithm'|'pattern'} [mode='algorithm'] - * @param {Object|null} [currentPattern=null] - required when mode is 'pattern' - */ - freeze(chain, currentParams, masterSeed, mode = 'algorithm', currentPattern = null) { - this._freezeMode = mode; - - if (mode === 'pattern') { - // Pattern mode: capture the realized pattern, bypass chain - if (!currentPattern) { - throw new Error('freeze: pattern mode requires a currentPattern'); - } - this._frozenPattern = clonePattern(currentPattern); - // Clear algorithm-mode state - this._frozenParams = null; - this._frozenSeeds = null; - this._frozenStates = null; - this._liveFlags = null; - this._masterSeed = null; - } else { - // Algorithm mode: existing behavior - // Copy param values - this._frozenParams = new Float32Array(currentParams.length); - for (let i = 0; i < currentParams.length; i++) { - this._frozenParams[i] = currentParams[i]; - } - - // Capture per-primitive seeds - this._frozenSeeds = chain.getPrimitives().map(p => p.getSeed()); - - // Capture per-primitive states - this._frozenStates = chain.getState(); - - // Store master seed - this._masterSeed = masterSeed; - - // All params frozen by default - this._liveFlags = new Uint8Array(currentParams.length); - - // Clear pattern-mode state - this._frozenPattern = null; - } - - this._frozen = true; - } - - /** - * Clear all captured state and return to unfrozen mode. - */ - unfreeze() { - this._frozen = false; - this._freezeMode = null; - this._frozenParams = null; - this._frozenSeeds = null; - this._frozenStates = null; - this._liveFlags = null; - this._masterSeed = null; - this._frozenPattern = null; - } - - // ── Per-param live/frozen control ────────────────────────────────── - - /** - * Toggle a param between frozen (0) and live (1). - * Only valid when frozen; throws if not frozen. - * - * @param {number} flatIndex - index into the flat param array - */ - toggleParam(flatIndex) { - if (!this._frozen) { - throw new Error('toggleParam: cannot toggle when not frozen'); - } - const idx = flatIndex | 0; - if (idx < 0 || idx >= this._liveFlags.length) { - throw new RangeError('toggleParam: index ' + flatIndex + ' out of range [0, ' + (this._liveFlags.length - 1) + ']'); - } - this._liveFlags[idx] = this._liveFlags[idx] ? 0 : 1; - } - - /** - * Explicitly set a param's live state. - * Only valid when frozen; throws if not frozen. - * - * @param {number} flatIndex - index into the flat param array - * @param {boolean} isLive - true = live (receives ML values), false = frozen - */ - setParamLive(flatIndex, isLive) { - if (!this._frozen) { - throw new Error('setParamLive: cannot set when not frozen'); - } - const idx = flatIndex | 0; - if (idx < 0 || idx >= this._liveFlags.length) { - throw new RangeError('setParamLive: index ' + flatIndex + ' out of range [0, ' + (this._liveFlags.length - 1) + ']'); - } - this._liveFlags[idx] = isLive ? 1 : 0; - } - - // ── Getters ──────────────────────────────────────────────────────── - - /** @returns {boolean} */ - get isFrozen() { - return this._frozen; - } - - /** - * Returns the current freeze mode, or null if not frozen. - * @returns {'algorithm'|'pattern'|null} - */ - get freezeMode() { - return this._freezeMode; - } - - /** - * Returns the captured pattern (pattern mode only), or null. - * @returns {Object|null} - */ - getFrozenPattern() { - return this._frozenPattern; - } - - /** - * Returns the captured param values, or null if not frozen. - * @returns {Float32Array|null} - */ - getFrozenParams() { - return this._frozenParams; - } - - /** - * Returns the live flags array, or null if not frozen. - * @returns {Uint8Array|null} - */ - getLiveFlags() { - return this._liveFlags; - } - - /** - * Returns the captured per-primitive PRNG seeds, or null if not frozen. - * @returns {Array|null} - */ - getFrozenSeeds() { - return this._frozenSeeds; - } - - /** - * Returns the captured per-primitive states, or null if not frozen. - * @returns {Array|null} - */ - getFrozenStates() { - return this._frozenStates; - } - - /** - * Returns the captured master seed, or null if not frozen. - * @returns {number|null} - */ - getMasterSeed() { - return this._masterSeed; - } - - // ── Effective params ─────────────────────────────────────────────── - - /** - * Given current MLP-derived params, return the effective param array: - * frozen params use captured values, live params use currentMLParams. - * - * This is the simple version. The delta controller (meml-7goy) will - * replace this with proper delta+boundary logic later. - * - * Returns null if not frozen. - * - * @param {Float32Array|Array} currentMLParams - * @returns {Float32Array|null} - */ - getEffectiveParams(currentMLParams) { - if (!this._frozen) return null; - if (this._freezeMode === 'pattern') return null; - - const result = new Float32Array(this._frozenParams.length); - for (let i = 0; i < result.length; i++) { - result[i] = this._liveFlags[i] ? currentMLParams[i] : this._frozenParams[i]; - } - return result; - } - - // ── Sequencer integration ────────────────────────────────────────── - - /** - * Returns true when frozen — freeze-as-algorithm suppresses per-loop - * re-evaluation. The sequencer's _handleLoopStart should check this. - * - * @returns {boolean} - */ - shouldSuppressReEval() { - return this._frozen; - } -} diff --git a/playground/js/shapeseq/mlp-mode.js b/playground/js/shapeseq/mlp-mode.js deleted file mode 100644 index e547574..0000000 --- a/playground/js/shapeseq/mlp-mode.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * MLP mode manager — switches between unified and dual MLP operation. - * - * Unified: single MLP, outputs partitioned between timbre and sequence. - * Dual: separate MLPs for timbre and sequence, independent training. - */ - -export const MLP_MODES = Object.freeze({ UNIFIED: 'unified', DUAL: 'dual' }); - -export class MLPModeManager { - constructor() { - this._mode = MLP_MODES.DUAL; // default to dual (current architecture) - this._unifiedSliceStart = 0; // where sequence outputs start in unified mode - this._unifiedSliceCount = 16; // how many outputs go to sequence in unified mode - } - - get mode() { return this._mode; } - - /** - * Configure unified mode: which slice of the timbre MLP outputs feeds the sequencer. - * @param {number} start - first output index for sequence - * @param {number} count - number of outputs for sequence - */ - setUnifiedConfig(start, count) { - this._unifiedSliceStart = start; - this._unifiedSliceCount = count; - } - - get unifiedSliceStart() { return this._unifiedSliceStart; } - get unifiedSliceCount() { return this._unifiedSliceCount; } - - setMode(mode) { - if (mode !== MLP_MODES.UNIFIED && mode !== MLP_MODES.DUAL) { - throw new TypeError('Invalid MLP mode: ' + mode); - } - this._mode = mode; - } - - /** - * Extract sequence params from a timbre MLP's outputs (unified mode). - * @param {Float32Array} timbreOutputs - full output array from the timbre MLP - * @returns {Float32Array} slice for the sequence engine - */ - extractSequenceOutputs(timbreOutputs) { - const start = this._unifiedSliceStart; - const count = this._unifiedSliceCount; - const result = new Float32Array(count); - for (let i = 0; i < count; i++) { - const idx = start + i; - result[i] = idx < timbreOutputs.length ? timbreOutputs[idx] : 0.5; - } - return result; - } -} diff --git a/playground/js/shapeseq/param-map.js b/playground/js/shapeseq/param-map.js deleted file mode 100644 index f82cd2d..0000000 --- a/playground/js/shapeseq/param-map.js +++ /dev/null @@ -1,94 +0,0 @@ -// ShapeSeq param mapping layer -// Maps fixed-size MLP outputs (16 values [0,1]) to variable-count primitive params -// -// The sequence MLP always outputs a fixed number of values (default 16). -// The primitive chain has a variable number of params depending on which -// primitives are active. This module bridges the two via automatic distribution: -// - N <= mlpOutputCount: each param gets one dedicated output -// - N > mlpOutputCount: outputs distributed via linear interpolation -// -// Port-ready: Float32Array, no closures, pure functions. - -/** - * Create a param mapper instance for a given MLP output count. - * @param {number} mlpOutputCount - number of MLP outputs (e.g. 16) - * @returns {{ map: Function, mapWithSchema: Function, mlpOutputCount: number }} - */ -export function createParamMap(mlpOutputCount) { - if (!Number.isInteger(mlpOutputCount) || mlpOutputCount < 1) { - throw new Error('mlpOutputCount must be a positive integer'); - } - - return { - mlpOutputCount, - map, - mapWithSchema, - }; -} - -/** - * Map MLP outputs to N primitive params via automatic distribution. - * - * If paramCount <= mlpOutputCount, each param gets one dedicated output - * (first paramCount outputs used, rest ignored). - * - * If paramCount > mlpOutputCount, outputs are distributed via linear - * interpolation so that the first param maps to the first output and the - * last param maps to the last output, with intermediate params interpolated. - * - * @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1] - * @param {number} paramCount - number of primitive params to produce - * @returns {Float32Array} mapped values [0,1], length = paramCount - */ -export function map(mlpOutputs, paramCount) { - const mlpCount = mlpOutputs.length; - const result = new Float32Array(paramCount); - - if (paramCount === 0) return result; - - if (paramCount <= mlpCount) { - // Direct mapping: each param gets one dedicated output - for (let i = 0; i < paramCount; i++) { - result[i] = mlpOutputs[i]; - } - } else { - // Interpolated mapping: spread mlpCount outputs across paramCount params - // param[i] maps to a fractional position in the output array - // param[0] -> output[0], param[paramCount-1] -> output[mlpCount-1] - const scale = paramCount > 1 ? (mlpCount - 1) / (paramCount - 1) : 0; - - for (let i = 0; i < paramCount; i++) { - const pos = i * scale; - const lo = pos | 0; // floor - const hi = lo + 1 < mlpCount ? lo + 1 : lo; - const frac = pos - lo; - result[i] = mlpOutputs[lo] + (mlpOutputs[hi] - mlpOutputs[lo]) * frac; - } - } - - return result; -} - -/** - * Map MLP outputs to primitive params and apply per-param min/max scaling. - * - * Each param schema defines { min, max } (both [0,1]). The mapped [0,1] - * value is scaled into [min, max] for each param. - * - * @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1] - * @param {Array<{ min: number, max: number }>} paramSchemas - per-param range definitions - * @returns {Float32Array} scaled values, length = paramSchemas.length - */ -export function mapWithSchema(mlpOutputs, paramSchemas) { - const paramCount = paramSchemas.length; - const mapped = map(mlpOutputs, paramCount); - - for (let i = 0; i < paramCount; i++) { - const schema = paramSchemas[i]; - const min = schema.min !== undefined ? schema.min : 0; - const max = schema.max !== undefined ? schema.max : 1; - mapped[i] = min + mapped[i] * (max - min); - } - - return mapped; -} diff --git a/playground/js/shapeseq/pattern.js b/playground/js/shapeseq/pattern.js deleted file mode 100644 index 0da54c2..0000000 --- a/playground/js/shapeseq/pattern.js +++ /dev/null @@ -1,354 +0,0 @@ -/** - * ShapeSeq Pattern Description Data Structure - * - * The foundational data type for the ShapeSeq sequencing system. - * A pattern description is the symbolic output of the primitive chain -- - * a complete loop description that the clock engine steps through. - * - * Port-ready: typed arrays where possible, explicit construction, no closures. - * - * @module shapeseq/pattern - */ - -// --- Step defaults --- - -const DEFAULT_TRIGGER = false; -const DEFAULT_PITCH = 0.5; -const DEFAULT_VELOCITY = 0.7; -const DEFAULT_ACCENT = false; -const DEFAULT_TIME_OFFSET = 0.0; -const DEFAULT_SUBDIVISIONS = 1; - -/** - * Create a single step with default values. - * - * @returns {{ trigger: boolean, pitch: number, velocity: number, accent: boolean, timeOffset: number, subdivisions: number }} - */ -export function createStep() { - return { - trigger: DEFAULT_TRIGGER, - pitch: DEFAULT_PITCH, - velocity: DEFAULT_VELOCITY, - accent: DEFAULT_ACCENT, - timeOffset: DEFAULT_TIME_OFFSET, - subdivisions: DEFAULT_SUBDIVISIONS, - midiNote: null, - }; -} - -/** - * Create a pattern description with the given step count, all steps at defaults. - * - * @param {number} stepCount - Number of steps (must be positive integer) - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ -export function createPattern(stepCount) { - const count = stepCount | 0; // coerce to int - if (count < 1) { - throw new RangeError('stepCount must be >= 1, got ' + stepCount); - } - - const steps = new Array(count); - for (let i = 0; i < count; i++) { - steps[i] = createStep(); - } - - return { - steps: steps, - stepCount: count, - metadata: {}, - }; -} - -/** - * Deep clone a pattern description. - * Needed for chain processing where each primitive transforms a copy. - * - * @param {{ steps: Array, stepCount: number, metadata: Object }} pattern - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ -export function clonePattern(pattern) { - const count = pattern.stepCount; - const srcSteps = pattern.steps; - const steps = new Array(count); - - for (let i = 0; i < count; i++) { - const s = srcSteps[i]; - steps[i] = { - trigger: s.trigger, - pitch: s.pitch, - velocity: s.velocity, - accent: s.accent, - timeOffset: s.timeOffset, - subdivisions: s.subdivisions, - midiNote: s.midiNote, - }; - } - - // Shallow clone metadata (one level deep for plain-object metadata) - const srcMeta = pattern.metadata; - const metadata = {}; - const keys = Object.keys(srcMeta); - for (let i = 0; i < keys.length; i++) { - metadata[keys[i]] = srcMeta[keys[i]]; - } - - return { - steps: steps, - stepCount: count, - metadata: metadata, - }; -} - -// --- Polyrhythm utilities --- - -/** - * Greatest Common Divisor (Euclidean algorithm). - * @param {number} a - * @param {number} b - * @returns {number} - */ -function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a; } - -/** - * Least Common Multiple. - * @param {number} a - * @param {number} b - * @returns {number} - */ -export function lcm(a, b) { return (a / gcd(a, b)) * b; } - -/** - * Tile (repeat/wrap) a pattern to fill a longer step count. - * Returns a new pattern — does not mutate the input. - * - * @param {{ steps: Array, stepCount: number, metadata: Object }} pattern - * @param {number} targetStepCount - Must be >= pattern.stepCount - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ -export function tilePattern(pattern, targetStepCount) { - const target = targetStepCount | 0; - if (target < pattern.stepCount) { - throw new RangeError( - 'targetStepCount (' + target + ') must be >= pattern.stepCount (' + pattern.stepCount + ')' - ); - } - if (target === pattern.stepCount) { - return clonePattern(pattern); - } - - const srcSteps = pattern.steps; - const srcCount = pattern.stepCount; - const steps = new Array(target); - - for (let i = 0; i < target; i++) { - const s = srcSteps[i % srcCount]; - steps[i] = { - trigger: s.trigger, - pitch: s.pitch, - velocity: s.velocity, - accent: s.accent, - timeOffset: s.timeOffset, - subdivisions: s.subdivisions, - midiNote: s.midiNote, - }; - } - - // Shallow clone metadata - const srcMeta = pattern.metadata; - const metadata = {}; - const keys = Object.keys(srcMeta); - for (let i = 0; i < keys.length; i++) { - metadata[keys[i]] = srcMeta[keys[i]]; - } - - return { - steps: steps, - stepCount: target, - metadata: metadata, - }; -} - -/** - * Compute the LCM of all step counts in an array of patterns. - * - * @param {Array<{ stepCount: number }>} patterns - * @returns {number} - */ -export function lcmOfPatterns(patterns) { - if (patterns.length === 0) return 1; - let result = patterns[0].stepCount; - for (let i = 1; i < patterns.length; i++) { - result = lcm(result, patterns[i].stepCount); - } - return result; -} - -/** - * Merge two patterns together. - * - * If step counts differ, both patterns are tiled to their LCM before merging. - * Returns a new pattern (does not mutate inputs). - * - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternA - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternB - * @param {'additive'|'multiplicative'} mode - * - 'additive': triggers OR'd, pitch/velocity averaged - * - 'multiplicative': triggers AND'd, pitch/velocity averaged - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ -export function mergePatterns(patternA, patternB, mode) { - // Tile to LCM if step counts differ - if (patternA.stepCount !== patternB.stepCount) { - const target = lcm(patternA.stepCount, patternB.stepCount); - patternA = tilePattern(patternA, target); - patternB = tilePattern(patternB, target); - } - if (mode !== 'additive' && mode !== 'multiplicative') { - throw new TypeError("mode must be 'additive' or 'multiplicative', got '" + mode + "'"); - } - - const count = patternA.stepCount; - const stepsA = patternA.steps; - const stepsB = patternB.steps; - const steps = new Array(count); - const isAdditive = mode === 'additive'; - - for (let i = 0; i < count; i++) { - const a = stepsA[i]; - const b = stepsB[i]; - - const trigger = isAdditive ? (a.trigger || b.trigger) : (a.trigger && b.trigger); - const bothTriggered = a.trigger && b.trigger; - - let pitch, velocity, accent, timeOffset, subdivisions, midiNote; - - if (bothTriggered) { - // Both generators triggered: average continuous values, combine accent by mode - pitch = (a.pitch + b.pitch) * 0.5; - velocity = (a.velocity + b.velocity) * 0.5; - accent = isAdditive ? (a.accent || b.accent) : (a.accent && b.accent); - timeOffset = (a.timeOffset + b.timeOffset) * 0.5; - subdivisions = Math.max(a.subdivisions, b.subdivisions); - // midiNote: both set → average (rounded), one set → use it, neither → null - if (a.midiNote != null && b.midiNote != null) { - midiNote = Math.round((a.midiNote + b.midiNote) * 0.5); - } else if (a.midiNote != null) { - midiNote = a.midiNote; - } else if (b.midiNote != null) { - midiNote = b.midiNote; - } else { - midiNote = null; - } - } else if (a.trigger) { - // Only A triggered: use A's values directly - pitch = a.pitch; - velocity = a.velocity; - accent = a.accent; - timeOffset = a.timeOffset; - subdivisions = a.subdivisions; - midiNote = a.midiNote; - } else if (b.trigger) { - // Only B triggered: use B's values directly - pitch = b.pitch; - velocity = b.velocity; - accent = b.accent; - timeOffset = b.timeOffset; - subdivisions = b.subdivisions; - midiNote = b.midiNote; - } else { - // Neither triggered: defaults - pitch = DEFAULT_PITCH; - velocity = DEFAULT_VELOCITY; - accent = DEFAULT_ACCENT; - timeOffset = DEFAULT_TIME_OFFSET; - subdivisions = DEFAULT_SUBDIVISIONS; - midiNote = null; - } - - steps[i] = { - trigger: trigger, - pitch: pitch, - velocity: velocity, - accent: accent, - timeOffset: timeOffset, - subdivisions: subdivisions, - midiNote: midiNote, - }; - } - - return { - steps: steps, - stepCount: count, - metadata: {}, - }; -} - -/** - * Update a specific step in a pattern (mutates the pattern in place). - * - * Only the fields present in stepData are updated; others remain unchanged. - * - * @param {{ steps: Array, stepCount: number, metadata: Object }} pattern - * @param {number} index - Step index (0-based) - * @param {Object} stepData - Partial step data to apply - */ -export function setStep(pattern, index, stepData) { - const idx = index | 0; - if (idx < 0 || idx >= pattern.stepCount) { - throw new RangeError('Step index ' + index + ' out of range [0, ' + (pattern.stepCount - 1) + ']'); - } - - const step = pattern.steps[idx]; - - if (stepData.trigger !== undefined) step.trigger = !!stepData.trigger; - if (stepData.pitch !== undefined) step.pitch = +stepData.pitch; - if (stepData.velocity !== undefined) step.velocity = +stepData.velocity; - if (stepData.accent !== undefined) step.accent = !!stepData.accent; - if (stepData.timeOffset !== undefined) step.timeOffset = +stepData.timeOffset; - if (stepData.subdivisions !== undefined) step.subdivisions = stepData.subdivisions | 0; - if (stepData.midiNote !== undefined) step.midiNote = stepData.midiNote === null ? null : stepData.midiNote | 0; -} - -/** - * Validate a pattern description structure. - * - * Checks: - * - pattern is an object with steps array, stepCount int, metadata object - * - steps.length === stepCount - * - Each step has all required fields with correct types and in-range values - * - * @param {*} pattern - * @returns {boolean} - */ -export function validatePattern(pattern) { - if (pattern == null || typeof pattern !== 'object') return false; - if (typeof pattern.stepCount !== 'number' || (pattern.stepCount | 0) < 1) return false; - if (pattern.stepCount !== (pattern.stepCount | 0)) return false; - if (!Array.isArray(pattern.steps)) return false; - if (pattern.steps.length !== pattern.stepCount) return false; - if (pattern.metadata == null || typeof pattern.metadata !== 'object') return false; - - const steps = pattern.steps; - const count = pattern.stepCount; - - for (let i = 0; i < count; i++) { - const s = steps[i]; - if (s == null || typeof s !== 'object') return false; - if (typeof s.trigger !== 'boolean') return false; - if (typeof s.pitch !== 'number' || s.pitch < 0 || s.pitch > 1) return false; - if (typeof s.velocity !== 'number' || s.velocity < 0 || s.velocity > 1) return false; - if (typeof s.accent !== 'boolean') return false; - if (typeof s.timeOffset !== 'number' || s.timeOffset < -0.5 || s.timeOffset > 0.5) return false; - if (typeof s.subdivisions !== 'number' || (s.subdivisions | 0) < 1 || (s.subdivisions | 0) > 4) return false; - if (s.subdivisions !== (s.subdivisions | 0)) return false; - // midiNote: null (pre-quantization) or integer 0-127 - if (s.midiNote !== null) { - if (typeof s.midiNote !== 'number') return false; - if (s.midiNote !== (s.midiNote | 0)) return false; - if (s.midiNote < 0 || s.midiNote > 127) return false; - } - } - - return true; -} diff --git a/playground/js/shapeseq/primitive.js b/playground/js/shapeseq/primitive.js deleted file mode 100644 index ee29bf2..0000000 --- a/playground/js/shapeseq/primitive.js +++ /dev/null @@ -1,254 +0,0 @@ -/** - * ShapeSeq Primitive Base Class and Param Schema System - * - * Base class for all sequencing primitives (generators, processors, - * timing modifiers, converters). Defines the param schema format, - * symbolic process() interface, and state management for freeze support. - * - * Port-ready: explicit state, no closures, typed arrays where possible. - * - * @module shapeseq/primitive - */ - -// ── Valid primitive categories ────────────────────────────────────── - -export const CATEGORIES = Object.freeze([ - 'generator', - 'processor', - 'timing', - 'converter', -]); - -// ── Param schema defaults ─────────────────────────────────────────── - -const DEFAULT_SCALED_RANGE = 0.3; - -// ── Boundary enforcement helpers ──────────────────────────────────── - -/** - * Clamp a value to [0, 1]. - * @param {number} v - * @returns {number} - */ -function clamp01(v) { - return v < 0 ? 0 : v > 1 ? 1 : v; -} - -/** - * Wrap a value into [0, 1) with modular arithmetic. - * @param {number} v - * @returns {number} - */ -function wrap01(v) { - const m = v % 1; - return m < 0 ? m + 1 : m; -} - -/** - * Apply boundary enforcement to a raw param value. - * - * @param {number} value - raw [0,1] value (or delta-adjusted value) - * @param {{ boundary: string, scaledRange?: number }} schema - param schema entry - * @param {number|null} frozenValue - frozen value for 'scaled' boundary (null if not frozen) - * @returns {number} - */ -export function applyBoundary(value, schema, frozenValue) { - switch (schema.boundary) { - case 'wrap': - return wrap01(value); - case 'scaled': { - if (frozenValue === null || frozenValue === undefined) { - return clamp01(value); - } - const range = schema.scaledRange !== undefined ? schema.scaledRange : DEFAULT_SCALED_RANGE; - const lo = frozenValue - range; - const hi = frozenValue + range; - // Map [0,1] input to [lo, hi], then clamp to [0,1] - const mapped = lo + value * (hi - lo); - return clamp01(mapped); - } - case 'clamp': - default: - return clamp01(value); - } -} - -// ── Param schema validation ───────────────────────────────────────── - -/** - * Validate a single param schema entry. - * Throws on invalid entries for fast fail during development. - * - * @param {{ name: string, default: number, boundary: string, scaledRange?: number }} entry - * @param {number} index - position in schema array (for error messages) - */ -function validateSchemaEntry(entry, index) { - if (!entry || typeof entry !== 'object') { - throw new TypeError('paramSchema[' + index + '] must be an object'); - } - if (typeof entry.name !== 'string' || entry.name.length === 0) { - throw new TypeError('paramSchema[' + index + '].name must be a non-empty string'); - } - if (typeof entry.default !== 'number' || entry.default < 0 || entry.default > 1) { - throw new RangeError('paramSchema[' + index + '].default must be in [0,1], got ' + entry.default); - } - if (entry.boundary !== 'clamp' && entry.boundary !== 'wrap' && entry.boundary !== 'scaled') { - throw new TypeError( - "paramSchema[" + index + "].boundary must be 'clamp', 'wrap', or 'scaled', got '" + entry.boundary + "'" - ); - } - if (entry.boundary === 'scaled') { - const sr = entry.scaledRange; - if (sr !== undefined && (typeof sr !== 'number' || sr <= 0 || sr > 1)) { - throw new RangeError('paramSchema[' + index + '].scaledRange must be in (0,1], got ' + sr); - } - } -} - -// ── Primitive base class ──────────────────────────────────────────── - -export class Primitive { - /** - * @param {string} name - unique identifier for this primitive type - * @param {string} category - one of CATEGORIES - * @param {Array<{ name: string, default: number, boundary: string, scaledRange?: number }>} paramSchema - */ - constructor(name, category, paramSchema) { - if (typeof name !== 'string' || name.length === 0) { - throw new TypeError('Primitive name must be a non-empty string'); - } - if (CATEGORIES.indexOf(category) === -1) { - throw new TypeError( - "Primitive category must be one of [" + CATEGORIES.join(', ') + "], got '" + category + "'" - ); - } - if (!Array.isArray(paramSchema)) { - throw new TypeError('paramSchema must be an array'); - } - - // Validate each entry - for (let i = 0; i < paramSchema.length; i++) { - validateSchemaEntry(paramSchema[i], i); - } - - /** @type {string} */ - this.name = name; - - /** @type {string} */ - this.category = category; - - /** - * Frozen copy of the param schema. Each entry: - * { name: string, default: number, boundary: 'clamp'|'wrap'|'scaled', scaledRange?: number } - * @type {Array} - */ - this.paramSchema = Object.freeze(paramSchema.map(function (entry) { - const frozen = { - name: entry.name, - default: entry.default, - boundary: entry.boundary, - }; - if (entry.boundary === 'scaled') { - frozen.scaledRange = entry.scaledRange !== undefined ? entry.scaledRange : DEFAULT_SCALED_RANGE; - } - return Object.freeze(frozen); - })); - - /** - * When true, the sequencer re-evaluates the chain at each loop start - * even if inputs haven't changed. Useful for stateful primitives - * (e.g. PitchWalker) whose output evolves across evaluations. - * @type {boolean} - */ - this.reEvalOnLoop = false; - - /** @private */ - this._seed = 0; - } - - // ── Param utilities ───────────────────────────────────────────── - - /** - * Total number of parameters this primitive exposes. - * @returns {number} - */ - get paramCount() { - return this.paramSchema.length; - } - - /** - * Get default param values as a Float32Array, one per schema entry. - * @returns {Float32Array} - */ - getDefaults() { - const count = this.paramSchema.length; - const defaults = new Float32Array(count); - for (let i = 0; i < count; i++) { - defaults[i] = this.paramSchema[i].default; - } - return defaults; - } - - // ── Symbolic processing ───────────────────────────────────────── - - /** - * Transform a pattern description. Subclasses MUST override this. - * - * - Generators ignore patternDesc and create a new one (using createPattern()) - * - Processors/timing modifiers clone and transform patternDesc - * - The rng param is a PRNG state from prng.js; consume via next(rng) - * and return the consumed state in the result - * - * @param {Float32Array|Array} params - param values, one per schema entry, each [0,1] - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - input pattern - * @param {Object} state - primitive-specific state (from previous process() call or getState()) - * @param {{ state: number }} rng - PRNG state object from prng.js - * @returns {{ patternDesc: { steps: Array, stepCount: number, metadata: Object }, nextState: Object }} - */ - process(params, patternDesc, state, rng) { - void params; void patternDesc; void state; void rng; - throw new Error(this.name + '.process() must be overridden by subclass'); - } - - // ── State management (for freeze) ────────────────────────────── - - /** - * Get serializable state for this primitive. - * Stateless primitives return {}. Stateful primitives (e.g. Pitch Walker) - * override to include their internal state. - * - * @returns {Object} - */ - getState() { - return {}; - } - - /** - * Restore primitive state from a previously serialized state object. - * Stateless primitives are a no-op. Stateful primitives override. - * - * @param {Object} _state - */ - setState(_state) { - // no-op for stateless primitives - } - - /** - * Get the PRNG seed associated with this primitive. - * Used by freeze-as-algorithm to replay identical sequences. - * - * @returns {number} - */ - getSeed() { - return this._seed; - } - - /** - * Set the PRNG seed for this primitive. - * - * @param {number} seed - 32-bit integer seed - */ - setSeed(seed) { - this._seed = seed >>> 0; - } -} diff --git a/playground/js/shapeseq/primitives.js b/playground/js/shapeseq/primitives.js deleted file mode 100644 index d65dde0..0000000 --- a/playground/js/shapeseq/primitives.js +++ /dev/null @@ -1,524 +0,0 @@ -/** - * ShapeSeq Sequencing Primitives - * - * All 8 primitives for the ShapeSeq generative sequencing system. - * Each extends Primitive and implements process(params, patternDesc, state, rng). - * - * Port-ready: explicit state, no closures, seeded PRNG, typed arrays. - * - * @module shapeseq/primitives - */ - -import { Primitive } from './primitive.js'; -import { createPattern, clonePattern, setStep } from './pattern.js'; -import { next, nextInt } from './prng.js'; - -// ── Helper: map [0,1] float to integer range [lo, hi] ────────────── - -function mapToInt(value, lo, hi) { - const clamped = value < 0 ? 0 : value > 1 ? 1 : value; - return lo + Math.floor(clamped * (hi - lo + 1 - 1e-9)); -} - -// ── 1. EuclideanRhythm ────────────────────────────────────────────── - -/** - * Bjorklund algorithm: distribute `pulses` as evenly as possible - * across `steps`, then apply rotation. - */ -function bjorklund(steps, pulses) { - if (pulses >= steps) { - const result = new Array(steps); - for (let i = 0; i < steps; i++) result[i] = true; - return result; - } - if (pulses <= 0) { - const result = new Array(steps); - for (let i = 0; i < steps; i++) result[i] = false; - return result; - } - - // Build pattern using Bjorklund's algorithm - let groups = []; - for (let i = 0; i < pulses; i++) groups.push([true]); - for (let i = 0; i < steps - pulses; i++) groups.push([false]); - - while (true) { - const remainder = groups.length - pulses; - if (remainder <= 1) break; - const minLen = Math.min(pulses, remainder); - const newGroups = []; - for (let i = 0; i < minLen; i++) { - newGroups.push(groups[i].concat(groups[groups.length - 1 - i])); - } - // Keep any leftovers - const leftStart = minLen; - const leftEnd = groups.length - minLen; - for (let i = leftStart; i < leftEnd; i++) { - newGroups.push(groups[i]); - } - groups = newGroups; - pulses = minLen; - if (pulses <= 1) break; - } - - // Flatten groups - const result = []; - for (let i = 0; i < groups.length; i++) { - for (let j = 0; j < groups[i].length; j++) { - result.push(groups[i][j]); - } - } - return result; -} - -export class EuclideanRhythm extends Primitive { - constructor() { - super('EuclideanRhythm', 'generator', [ - { name: 'steps', default: 0.5, boundary: 'clamp' }, - { name: 'pulses', default: 0.5, boundary: 'clamp' }, - { name: 'rotation', default: 0.0, boundary: 'wrap' }, - ]); - } - - process(params, patternDesc, state, rng) { - const stepCount = patternDesc.stepCount; - const steps = mapToInt(params[0], 2, stepCount); - const pulses = mapToInt(params[1], 0, steps); - const rotation = mapToInt(params[2], 0, steps - 1); - - const rhythm = bjorklund(steps, pulses); - const pattern = createPattern(stepCount); - - for (let i = 0; i < stepCount; i++) { - if (i < steps) { - const srcIdx = (i - rotation + steps) % steps; - if (rhythm[srcIdx]) { - setStep(pattern, i, { trigger: true }); - } - } - // Steps beyond `steps` remain untriggered (default) - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 2. ProbabilityGate ────────────────────────────────────────────── - -export class ProbabilityGate extends Primitive { - constructor() { - super('ProbabilityGate', 'processor', [ - { name: 'density', default: 0.7, boundary: 'clamp' }, - { name: 'accentProbability', default: 0.3, boundary: 'clamp' }, - ]); - } - - process(params, patternDesc, state, rng) { - const density = params[0]; - const accentProb = params[1]; - const pattern = clonePattern(patternDesc); - let currentRng = rng; - - for (let i = 0; i < pattern.stepCount; i++) { - const step = pattern.steps[i]; - if (step.trigger) { - // Coin flip for survival - const r1 = next(currentRng); - currentRng = r1.nextState; - - if (r1.value >= density) { - step.trigger = false; - step.accent = false; - } else { - // Accent coin flip - const r2 = next(currentRng); - currentRng = r2.nextState; - step.accent = r2.value < accentProb; - } - } - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 3. PitchWalker ────────────────────────────────────────────────── - -export class PitchWalker extends Primitive { - constructor() { - super('PitchWalker', 'processor', [ - { name: 'stepSize', default: 0.3, boundary: 'clamp' }, - { name: 'directionBias', default: 0.5, boundary: 'clamp' }, - { name: 'gravity', default: 0.3, boundary: 'clamp' }, - { name: 'range', default: 0.8, boundary: 'clamp' }, - ]); - - // PitchWalker is stateful — its random walk evolves across evaluations, - // so re-evaluate on each loop start to produce evolving patterns. - this.reEvalOnLoop = true; - - /** @private */ - this._position = 0.5; - } - - getState() { - return { position: this._position }; - } - - setState(savedState) { - if (savedState && typeof savedState.position === 'number') { - this._position = savedState.position; - } - } - - process(params, patternDesc, state, rng) { - const stepSize = params[0]; - const directionBias = params[1]; - const gravity = params[2]; - const range = params[3]; - - // Restore position from state if provided - let position = (state && typeof state.position === 'number') - ? state.position - : this._position; - - const pattern = clonePattern(patternDesc); - let currentRng = rng; - - for (let i = 0; i < pattern.stepCount; i++) { - if (pattern.steps[i].trigger) { - // Random walk step - const r1 = next(currentRng); - currentRng = r1.nextState; - - // Direction: bias + gravity toward center - const gravityPull = (0.5 - position) * gravity; - const biasOffset = (directionBias - 0.5) * 2; // [-1, 1] - const direction = biasOffset + gravityPull; - - // Random component: [-1, 1] scaled by stepSize - const randomComponent = (r1.value * 2 - 1) * stepSize * range; - const delta = direction * stepSize * 0.5 + randomComponent; - - position = position + delta; - // Clamp to [0, 1] - if (position < 0) position = 0; - if (position > 1) position = 1; - - pattern.steps[i].pitch = position; - } - // Untriggered steps: preserve existing data unchanged - } - - this._position = position; - - return { - patternDesc: pattern, - nextState: { position: position }, - }; - } -} - -// ── 4. Ratchet ────────────────────────────────────────────────────── - -export class Ratchet extends Primitive { - constructor() { - super('Ratchet', 'timing', [ - { name: 'maxDivision', default: 0.5, boundary: 'clamp' }, - { name: 'probability', default: 0.5, boundary: 'clamp' }, - ]); - } - - process(params, patternDesc, state, rng) { - const maxDiv = mapToInt(params[0], 1, 4); - const probability = params[1]; - const pattern = clonePattern(patternDesc); - let currentRng = rng; - - for (let i = 0; i < pattern.stepCount; i++) { - const step = pattern.steps[i]; - if (step.trigger) { - const r1 = next(currentRng); - currentRng = r1.nextState; - - if (r1.value < probability && maxDiv > 1) { - // Pick a subdivision count in [2, maxDiv] - const r2 = nextInt(currentRng, 2, maxDiv); - currentRng = r2.nextState; - step.subdivisions = r2.value; - } - } - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 5. SwingGroove ────────────────────────────────────────────────── - -export class SwingGroove extends Primitive { - constructor() { - super('SwingGroove', 'timing', [ - { name: 'swingAmount', default: 0.0, boundary: 'clamp' }, - { name: 'swingGrid', default: 0.0, boundary: 'clamp' }, - ]); - } - - process(params, patternDesc, state, rng) { - const swingAmount = params[0]; - const swingGrid = params[1]; - const pattern = clonePattern(patternDesc); - - // Max swing = 0.33 (triplet feel) - const maxOffset = 0.33; - const offset = swingAmount * maxOffset; - - // swingGrid selects which subdivision gets swung: - // 0.0–0.33: every 2nd step (8th note feel) - // 0.34–0.66: every 4th step (16th note feel) - // 0.67–1.0: every 3rd step (triplet feel) - let period; - if (swingGrid < 0.34) { - period = 2; - } else if (swingGrid < 0.67) { - period = 4; - } else { - period = 3; - } - - // Apply swing to steps that fall on the swing grid - for (let i = 0; i < pattern.stepCount; i++) { - if (i % period === period - 1) { - pattern.steps[i].timeOffset = offset; - } - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 6. DensityMorph ───────────────────────────────────────────────── - -export class DensityMorph extends Primitive { - constructor() { - super('DensityMorph', 'generator', [ - { name: 'density', default: 0.5, boundary: 'clamp' }, - { name: 'clustering', default: 0.0, boundary: 'clamp' }, - ]); - } - - process(params, patternDesc, state, rng) { - const density = params[0]; - const clustering = params[1]; - const stepCount = patternDesc.stepCount; - const pattern = createPattern(stepCount); - const numTriggers = Math.floor(density * stepCount); - - if (numTriggers <= 0) { - return { patternDesc: pattern, nextState: {} }; - } - if (numTriggers >= stepCount) { - for (let i = 0; i < stepCount; i++) { - setStep(pattern, i, { trigger: true }); - } - return { patternDesc: pattern, nextState: {} }; - } - - let currentRng = rng; - - if (clustering < 0.01) { - // Even spread: Euclidean-like placement - for (let i = 0; i < numTriggers; i++) { - const idx = Math.floor((i * stepCount) / numTriggers); - setStep(pattern, idx, { trigger: true }); - } - } else if (clustering > 0.99) { - // Full clustering: contiguous burst - const r1 = nextInt(currentRng, 0, stepCount - 1); - currentRng = r1.nextState; - const startPos = r1.value; - for (let i = 0; i < numTriggers; i++) { - const idx = (startPos + i) % stepCount; - setStep(pattern, idx, { trigger: true }); - } - } else { - // Interpolate: place triggers with clustering-dependent spread - // Use a "center of mass" approach: - // Pick a random center, then distribute triggers around it - // with spread inversely proportional to clustering - const r1 = next(currentRng); - currentRng = r1.nextState; - const center = r1.value * stepCount; - - // Spread factor: low clustering = large spread, high = tight - const spreadRadius = (1 - clustering) * stepCount * 0.5; - - // Score each step by distance from center (wrapping) - const scores = new Float32Array(stepCount); - for (let i = 0; i < stepCount; i++) { - // Wrapped distance from center - let dist = Math.abs(i - center); - if (dist > stepCount * 0.5) dist = stepCount - dist; - // Add small random jitter to break ties - const r2 = next(currentRng); - currentRng = r2.nextState; - scores[i] = dist / (spreadRadius + 0.001) + r2.value * 0.01; - } - - // Select the numTriggers steps with lowest scores - const indices = new Array(stepCount); - for (let i = 0; i < stepCount; i++) indices[i] = i; - indices.sort(function (a, b) { return scores[a] - scores[b]; }); - - for (let i = 0; i < numTriggers; i++) { - setStep(pattern, indices[i], { trigger: true }); - } - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 7. IntervalLock ───────────────────────────────────────────────── - -const SCALES = [ - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // chromatic - [0, 2, 4, 5, 7, 9, 11], // major - [0, 2, 3, 5, 7, 8, 10], // natural minor - [0, 2, 3, 5, 7, 8, 11], // harmonic minor - [0, 2, 4, 7, 9], // pentatonic major - [0, 3, 5, 7, 10], // pentatonic minor - [0, 3, 5, 6, 7, 10], // blues - [0, 2, 3, 5, 7, 9, 10], // dorian - [0, 2, 4, 5, 7, 9, 10], // mixolydian - [0, 2, 4, 6, 8, 10], // whole tone - [0, 2, 3, 5, 6, 8, 9, 11], // diminished -]; - -export class IntervalLock extends Primitive { - constructor() { - super('IntervalLock', 'converter', [ - { name: 'root', default: 0.0, boundary: 'clamp' }, - { name: 'mode', default: 0.0, boundary: 'clamp' }, - { name: 'octaveRange', default: 0.25, boundary: 'clamp' }, - ]); - } - - process(params, patternDesc, state, rng) { - const root = mapToInt(params[0], 0, 11); - const scaleIdx = mapToInt(params[1], 0, SCALES.length - 1); - const octaveRange = mapToInt(params[2], 1, 4); - const scale = SCALES[scaleIdx]; - - const pattern = clonePattern(patternDesc); - - // Build the full set of MIDI notes in this scale + root + range - // Base octave offset: root param selects the note class (0-11), - // we start from C3 (MIDI 48) so that default output is in a playable range - const BASE_OCTAVE = 48; - const notes = []; - for (let oct = 0; oct < octaveRange; oct++) { - for (let i = 0; i < scale.length; i++) { - const midiNote = BASE_OCTAVE + root + scale[i] + oct * 12; - if (midiNote <= 127) { - notes.push(midiNote); - } - } - } - - if (notes.length === 0) { - return { patternDesc: pattern, nextState: {} }; - } - - for (let i = 0; i < pattern.stepCount; i++) { - const step = pattern.steps[i]; - // Quantize pitch [0,1] to nearest note in our scale - const targetIdx = Math.round(step.pitch * (notes.length - 1)); - const clampedIdx = targetIdx < 0 ? 0 : targetIdx >= notes.length ? notes.length - 1 : targetIdx; - // Store the integer MIDI note in midiNote; leave pitch as-is - // (pre-quantization [0,1] value) for other consumers. - step.midiNote = notes[clampedIdx]; - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── 8. VelocityShaper ─────────────────────────────────────────────── - -export class VelocityShaper extends Primitive { - constructor() { - super('VelocityShaper', 'processor', [ - { name: 'curveType', default: 0.0, boundary: 'clamp' }, - { name: 'depth', default: 0.5, boundary: 'clamp' }, - { name: 'phase', default: 0.0, boundary: 'wrap' }, - ]); - } - - process(params, patternDesc, state, rng) { - const curveIdx = mapToInt(params[0], 0, 4); - const depth = params[1]; - const phase = params[2]; - const pattern = clonePattern(patternDesc); - const stepCount = pattern.stepCount; - let currentRng = rng; - - for (let i = 0; i < stepCount; i++) { - const step = pattern.steps[i]; - if (!step.trigger) continue; - - // Phase-shifted position - const pos = ((i / stepCount) + phase) % 1; - let shapeValue; - - switch (curveIdx) { - case 0: // flat - shapeValue = 1.0; - break; - case 1: // accent-every-N (accent every 4th step) - shapeValue = ((i + Math.floor(phase * stepCount)) % 4 === 0) ? 1.0 : 0.5; - break; - case 2: // crescendo - shapeValue = pos; - break; - case 3: // decrescendo - shapeValue = 1.0 - pos; - break; - case 4: { // random - const r1 = next(currentRng); - currentRng = r1.nextState; - shapeValue = r1.value; - break; - } - default: - shapeValue = 1.0; - } - - // Apply depth: interpolate between uniform (1.0) and shaped - // depth=0 means all same velocity (base), depth=1 means full shape - const baseVelocity = 0.7; - const shaped = shapeValue; - step.velocity = baseVelocity * (1 - depth) + shaped * depth; - - // Clamp - if (step.velocity < 0) step.velocity = 0; - if (step.velocity > 1) step.velocity = 1; - } - - return { patternDesc: pattern, nextState: {} }; - } -} - -// ── Registry ──────────────────────────────────────────────────────── - -export const PRIMITIVE_REGISTRY = { - EuclideanRhythm: EuclideanRhythm, - ProbabilityGate: ProbabilityGate, - PitchWalker: PitchWalker, - Ratchet: Ratchet, - SwingGroove: SwingGroove, - DensityMorph: DensityMorph, - IntervalLock: IntervalLock, - VelocityShaper: VelocityShaper, -}; diff --git a/playground/js/shapeseq/prng.js b/playground/js/shapeseq/prng.js deleted file mode 100644 index b4adaf3..0000000 --- a/playground/js/shapeseq/prng.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * ShapeSeq PRNG — seedable pseudo-random number generator (mulberry32) - * - * Port-ready: explicit state as plain objects, no closures in hot path, - * all state is serializable. Pure-functional next/nextInt/fork — no mutation. - * - * @module shapeseq/prng - */ - -// --- Mulberry32 core --- - -/** - * Single step of the mulberry32 PRNG. - * Pure function: takes a 32-bit state, returns { value, nextState }. - * - * @param {number} state - unsigned 32-bit integer - * @returns {{ value: number, nextState: number }} - * value: float in [0, 1) - * nextState: next 32-bit state - */ -function mulberry32Step(state) { - let t = (state + 0x6D2B79F5) | 0; - const nextState = t >>> 0; // the incremented state IS the new state - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - const raw = ((t ^ (t >>> 14)) >>> 0); - return { value: raw / 0x100000000, nextState }; -} - -// --- Public API --- - -/** - * Create a PRNG instance from a seed. - * - * @param {number} seed - any 32-bit integer (will be coerced to unsigned) - * @returns {{ state: number }} serializable PRNG state object - */ -export function createPRNG(seed) { - return { state: (seed >>> 0) }; -} - -/** - * Produce the next random float in [0, 1). Pure — does not mutate input. - * - * @param {{ state: number }} prng - * @returns {{ value: number, nextState: { state: number } }} - */ -export function next(prng) { - const { value, nextState } = mulberry32Step(prng.state); - return { value, nextState: { state: nextState } }; -} - -/** - * Produce a random integer in [min, max] (inclusive). Pure — does not mutate input. - * - * @param {{ state: number }} prng - * @param {number} min - inclusive lower bound (integer) - * @param {number} max - inclusive upper bound (integer) - * @returns {{ value: number, nextState: { state: number } }} - */ -export function nextInt(prng, min, max) { - const { value, nextState } = next(prng); - const range = max - min + 1; - const intVal = min + Math.floor(value * range); - return { value: intVal, nextState }; -} - -/** - * Derive an independent PRNG stream from the current state + an id. - * Used to give each primitive in a chain its own deterministic stream. - * - * The child seed is produced by hashing the current state with the id - * using a simple but effective mixing function (splitmix-style). - * - * @param {{ state: number }} prng - * @param {number|string} id - primitive position index or string identifier - * @returns {{ state: number }} a new, independent PRNG state - */ -export function fork(prng, id) { - // Convert string ids to a numeric hash - let idNum; - if (typeof id === 'string') { - idNum = 0; - for (let i = 0; i < id.length; i++) { - idNum = ((idNum << 5) - idNum + id.charCodeAt(i)) | 0; - } - idNum = idNum >>> 0; - } else { - idNum = id >>> 0; - } - - // Mix state + id using splitmix-style finalizer - let h = (prng.state ^ idNum) >>> 0; - h = (h + 0x9E3779B9) >>> 0; - h = Math.imul(h ^ (h >>> 16), 0x85EBCA6B) >>> 0; - h = Math.imul(h ^ (h >>> 13), 0xC2B2AE35) >>> 0; - h = (h ^ (h >>> 16)) >>> 0; - - return { state: h }; -} - -/** - * Get serializable state from a PRNG instance (for freeze/restore). - * - * @param {{ state: number }} prng - * @returns {{ state: number }} - */ -export function getState(prng) { - return { state: prng.state }; -} - -/** - * Restore a PRNG instance from previously saved state. - * - * @param {{ state: number }} _prng - ignored (stateless restore) - * @param {{ state: number }} savedState - * @returns {{ state: number }} - */ -export function setState(_prng, savedState) { - return { state: savedState.state >>> 0 }; -} - -/** - * Generate a cryptographically random seed for initial seeding. - * Uses crypto.getRandomValues when available, falls back to Date.now(). - * - * @returns {number} unsigned 32-bit integer - */ -export function randomSeed() { - if (typeof crypto !== 'undefined' && crypto.getRandomValues) { - const buf = new Uint32Array(1); - crypto.getRandomValues(buf); - return buf[0]; - } - // Fallback: not cryptographic, but sufficient for musical randomness - return (Date.now() * 2654435761) >>> 0; -} diff --git a/playground/js/shapeseq/projection.js b/playground/js/shapeseq/projection.js deleted file mode 100644 index dfc766b..0000000 --- a/playground/js/shapeseq/projection.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * ShapeSeq Projection — 3 global post-chain knobs - * - * Applied after the primitive chain produces a pattern, before the clock plays it. - * - * Knobs: - * 1. velocityCurve: 'linear' | 'exponential' | 'sCurve' - * 2. gateThreshold: [0, 1] — global density filter (steps with velocity below threshold are muted) - * 3. pitchRange: { low: midiNote, high: midiNote } — clamp midiNote to range - * - * @module shapeseq/projection - */ - -import { clonePattern } from './pattern.js'; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -export const VELOCITY_CURVES = ['linear', 'exponential', 'sCurve']; - -// --------------------------------------------------------------------------- -// Velocity curve math -// --------------------------------------------------------------------------- - -/** - * Apply a velocity curve to a [0,1] value. - * - * @param {number} value - Input value in [0,1] - * @param {'linear'|'exponential'|'sCurve'} curve - * @returns {number} Transformed value in [0,1] - */ -function velocityCurveApply(value, curve) { - const v = value < 0 ? 0 : value > 1 ? 1 : value; - switch (curve) { - case 'exponential': - return v * v; - case 'sCurve': - return (3 - 2 * v) * v * v; // smoothstep: 3v² - 2v³ - case 'linear': - default: - return v; - } -} - -/** - * Fold a MIDI note number into a target range by shifting octaves. - * - * @param {number} note - MIDI note number - * @param {number} low - Low bound (inclusive) - * @param {number} high - High bound (inclusive) - * @returns {number} MIDI note within [low, high] - */ -function octaveFold(note, low, high) { - if (high <= low) return low; - let n = note | 0; - while (n < low) n += 12; - while (n > high) n -= 12; - // If 12-step folding overshoots (range < 12), clamp - if (n < low) n = low; - if (n > high) n = high; - return n; -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Create a projection config with defaults. - * - * @param {Object} [opts] - * @param {'linear'|'exponential'|'sCurve'} [opts.velocityCurve='linear'] - * @param {number} [opts.gateThreshold=0] - * @param {{ low?: number, high?: number }} [opts.pitchRange] - * @returns {{ velocityCurve: string, gateThreshold: number, pitchRange: { low: number, high: number } }} - */ -export function createProjection(opts = {}) { - return { - velocityCurve: opts.velocityCurve || 'linear', - gateThreshold: opts.gateThreshold ?? 0, - pitchRange: { - low: opts.pitchRange?.low ?? 0, - high: opts.pitchRange?.high ?? 127, - }, - }; -} - -/** - * Apply projection to a pattern. Returns a new pattern (no mutation). - * - * For each step: - * 1. Apply velocity curve to step.velocity - * 2. If step.velocity < gateThreshold, set step.trigger = false - * 3. If step.midiNote != null, fold into [pitchRange.low, pitchRange.high] by octave - * - * @param {{ velocityCurve: string, gateThreshold: number, pitchRange: { low: number, high: number } }} config - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - * @returns {{ steps: Array, stepCount: number, metadata: Object }} - */ -export function applyProjection(config, patternDesc) { - const result = clonePattern(patternDesc); - const steps = result.steps; - const count = result.stepCount; - - for (let s = 0; s < count; s++) { - const step = steps[s]; - - // 1. Velocity curve - step.velocity = velocityCurveApply(step.velocity, config.velocityCurve); - - // 2. Gate threshold (uses post-curve velocity) - if (step.velocity < config.gateThreshold) { - step.trigger = false; - } - - // 3. Pitch range (octave fold) - if (step.midiNote != null) { - step.midiNote = octaveFold(step.midiNote, config.pitchRange.low, config.pitchRange.high); - } - } - - return result; -} diff --git a/playground/js/shapeseq/seq-iml.js b/playground/js/shapeseq/seq-iml.js deleted file mode 100644 index 1604c3b..0000000 --- a/playground/js/shapeseq/seq-iml.js +++ /dev/null @@ -1,113 +0,0 @@ -// ShapeSeq — Sequence NISPS Instance -// -// Factory for creating a second WasmIML instance dedicated to sequence control. -// The MLP output count is configurable (default 16). A downstream param mapping -// layer (not in this module) fans out to however many primitive params the -// current chain requires. -// -// Input routing is configurable and wired externally (default: hand tracking -// features 0+1). This module only creates the IML instance — it does not -// subscribe to any input source. The integration layer (see meml-9tf) is -// responsible for calling setInputs() with routed values each frame. -// -// Usage: -// import { createSequenceIML } from './shapeseq/seq-iml.js'; -// const seqIML = await createSequenceIML(); // 16 outputs -// const seqIML = await createSequenceIML({ outputCount: 32 }); // 32 outputs -// // Per frame: -// seqIML.setInputs([x, y]); -// seqIML.process(); -// const params = seqIML.getOutputs(); - -import { WasmIML } from '../nisps/nisps-wasm.js'; - -// Default architecture for the sequence MLP. -// 2 inputs (routed externally), configurable outputs (param mapping fans out). -const SEQ_N_INPUTS = 2; -const SEQ_DEFAULT_OUTPUT_COUNT = 16; - -// Training hyperparameters — same defaults as the synth IML -const SEQ_MAX_ITERATIONS = 1000; -const SEQ_LEARNING_RATE = 1.0; -const SEQ_CONVERGENCE_THRESHOLD = 0.00001; - -/** - * Compute a hidden-layer architecture scaled to the requested output count. - * - * The strategy keeps three hidden layers with enough capacity for the output - * count while staying cheap for inference: - * - outputCount <= 8 → [8, 8, 8] - * - outputCount <= 16 → [16, 16, 16] - * - outputCount <= 32 → [24, 24, 32] - * - outputCount > 32 → [outputCount, outputCount, outputCount] - * - * @param {number} outputCount - * @returns {number[]} - */ -export function computeHiddenLayers(outputCount) { - if (outputCount <= 8) return [8, 8, 8]; - if (outputCount <= 16) return [16, 16, 16]; - if (outputCount <= 32) return [24, 24, 32]; - return [outputCount, outputCount, outputCount]; -} - -/** - * Validate and normalise an output count value. - * Must be a positive integer >= 1. - * - * @param {number} outputCount - * @returns {number} the validated count - * @throws {RangeError} if invalid - */ -export function validateOutputCount(outputCount) { - const n = Math.round(outputCount); - if (!Number.isFinite(n) || n < 1) { - throw new RangeError(`outputCount must be a positive integer, got ${outputCount}`); - } - return n; -} - -/** - * Create a WasmIML instance configured for sequence control. - * - * Follows the same creation pattern as imlJoy / imlHand in a-app.js: - * const iml = await WasmIML.create(nInputs, nOutputs, hiddenLayers, ...); - * - * The returned object is a standard WasmIML instance with the full interface: - * setInput / setInputs, getOutputs, process, - * addExample, clearDataset, train, trainAsync, - * randomiseWeights (drawWeights), moveWeights, - * exampleCount, destroy, etc. - * - * Additionally exposes SEQ_N_INPUTS, SEQ_N_OUTPUTS, SEQ_HIDDEN_LAYERS as - * properties on the returned object for introspection by downstream code. - * - * @param {{ outputCount?: number }} [opts] - * @returns {Promise} — the sequence IML instance (augmented with - * .SEQ_N_INPUTS, .SEQ_N_OUTPUTS, .SEQ_HIDDEN_LAYERS) - */ -export async function createSequenceIML({ outputCount = SEQ_DEFAULT_OUTPUT_COUNT } = {}) { - const nOutputs = validateOutputCount(outputCount); - const hiddenLayers = computeHiddenLayers(nOutputs); - - const seqIML = await WasmIML.create( - SEQ_N_INPUTS, - nOutputs, - hiddenLayers, - SEQ_MAX_ITERATIONS, - SEQ_LEARNING_RATE, - SEQ_CONVERGENCE_THRESHOLD - ); - - seqIML.setLogger(msg => console.log('[NISPS:seq]', msg)); - - // Attach architecture metadata for introspection - seqIML.SEQ_N_INPUTS = SEQ_N_INPUTS; - seqIML.SEQ_N_OUTPUTS = nOutputs; - seqIML.SEQ_HIDDEN_LAYERS = hiddenLayers; - - return seqIML; -} - -// Re-export constants for use by other modules (e.g., param mapping layer) -export { SEQ_N_INPUTS, SEQ_DEFAULT_OUTPUT_COUNT }; diff --git a/playground/js/shapeseq/sequencer.js b/playground/js/shapeseq/sequencer.js deleted file mode 100644 index 5ac7341..0000000 --- a/playground/js/shapeseq/sequencer.js +++ /dev/null @@ -1,607 +0,0 @@ -/** - * ShapeSeq Engine — central orchestrator - * - * Wires together the sequence MLP, param mapping, primitive chain, - * projection layer, clock engine, and C15 bridge. - * - * Main loop (triggered by setSequenceInputs): - * 1. Forward inputs to sequenceIML - * 2. Run MLP inference to get N outputs (configurable, default 16) - * 3. Map N outputs to M primitive params via param-map - * 4. Evaluate the chain to produce a pattern description - * 5. Apply projection transforms - * 6. Schedule the pattern on the clock - * - * Bridge integration: - * - Subscribes to seq.noteOn / seq.noteOff on the event bus - * - Forwards to C15Bridge.noteOn / noteOff - * - Tracks active notes to avoid orphans - * - * @module shapeseq/sequencer - */ - -import { createSequenceIML, SEQ_DEFAULT_OUTPUT_COUNT } from './seq-iml.js'; -import { MLPModeManager, MLP_MODES } from './mlp-mode.js'; -import { Chain } from './chain.js'; -import { ClockEngine } from './clock.js'; -import { map } from './param-map.js'; -import { createProjection, applyProjection } from './projection.js'; -import { SEQ, UI } from './event-bus.js'; -import { - EuclideanRhythm, - ProbabilityGate, - PitchWalker, - IntervalLock, - VelocityShaper, -} from './primitives.js'; -import { FreezeManager } from './freeze.js'; -import { DeltaController } from './delta.js'; - -// ── Defaults ───────────────────────────────────────────────────────── - -const DEFAULT_BPM = 120; -const DEFAULT_STEP_COUNT = 8; -const DEFAULT_MASTER_SEED = 42; -const DEFAULT_SPREAD = 0.6; - -// ── ShapeSeqEngine ─────────────────────────────────────────────────── - -export class ShapeSeqEngine { - /** - * @param {{ audioContext: AudioContext, eventBus: import('./event-bus.js').EventBus, c15Bridge: import('../synth/c15-bridge.js').C15Bridge }} opts - */ - constructor({ audioContext, eventBus, c15Bridge }) { - if (!audioContext) throw new TypeError('ShapeSeqEngine requires an audioContext'); - if (!eventBus) throw new TypeError('ShapeSeqEngine requires an eventBus'); - if (!c15Bridge) throw new TypeError('ShapeSeqEngine requires a c15Bridge'); - - /** @private */ this._audioCtx = audioContext; - /** @private */ this._bus = eventBus; - /** @private */ this._c15 = c15Bridge; - - /** @private */ this._sequenceIML = null; - /** @private */ this._chain = null; - /** @private */ this._clock = null; - /** @private */ this._projectionChain = null; - - /** @private */ this._outputCount = SEQ_DEFAULT_OUTPUT_COUNT; - /** @private */ this._stepCount = DEFAULT_STEP_COUNT; - /** @private */ this._masterSeed = DEFAULT_MASTER_SEED; - /** @private */ this._playing = false; - /** @private */ this._initialized = false; - - // MLP mode manager (unified vs dual) - /** @private */ this._mlpMode = new MLPModeManager(); - - // Freeze-as-algorithm manager - /** @private */ this._freezeManager = new FreezeManager(); - - // Last evaluated params — needed for freeze snapshot - /** @private @type {Float32Array|null} */ this._lastEvaluatedParams = null; - - // Last projected pattern — needed for freeze-as-pattern snapshot - /** @private @type {Object|null} */ this._lastPattern = null; - - // Dirty-check: skip re-evaluation when inputs haven't changed - /** @private */ this._lastInputs = [NaN, NaN]; - - // Generation counter: bumped on config changes to force re-evaluation - /** @private */ this._generation = 0; - /** @private */ this._lastGeneration = -1; - - // Voice mode: 'mono' (default) or 'poly' - /** @private @type {'mono'|'poly'} */ - this._voiceMode = 'mono'; - - // Last note tracker for mono mode - /** @private @type {{note: number, velocity: number}|null} */ - this._lastNote = null; - - // Track active notes for orphan prevention - /** @private @type {Set} */ - this._activeNotes = new Set(); - - // Bound handlers for event bus (stored for cleanup) - /** @private */ - this._onNoteOn = (data) => this._handleNoteOn(data); - /** @private */ - this._onNoteOff = (data) => this._handleNoteOff(data); - /** @private */ - this._onChainEdit = () => this._bumpGeneration(); - /** @private */ - this._onLoopStart = () => this._handleLoopStart(); - } - - // ── Lifecycle ────────────────────────────────────────────────────── - - /** - * Initialize all subsystems: create sequence IML, default chain, - * clock, and projection chain. Must be called before start(). - */ - async init() { - // 1. Create the sequence MLP - this._sequenceIML = await createSequenceIML({ outputCount: this._outputCount }); - - // Randomize weights with default spread - this._sequenceIML.randomiseWeights(DEFAULT_SPREAD); - - // 2. Create the default primitive chain - this._chain = new Chain(); - this._chain.addPrimitive(new EuclideanRhythm()); - this._chain.addPrimitive(new ProbabilityGate()); - this._chain.addPrimitive(new PitchWalker()); - this._chain.addPrimitive(new IntervalLock()); - this._chain.addPrimitive(new VelocityShaper()); - this._chain.setMasterSeed(this._masterSeed); - - // 3. Set up the clock - this._clock = new ClockEngine(this._audioCtx, this._bus); - this._clock.bpm = DEFAULT_BPM; - - // 4. Create default projection - this._projectionChain = createProjection(); - - // 5. Subscribe to event bus for C15 bridge integration - this._bus.on(SEQ.NOTE_ON, this._onNoteOn); - this._bus.on(SEQ.NOTE_OFF, this._onNoteOff); - - // 6. Subscribe to chain edits so config changes force re-evaluation - this._bus.on(UI.CHAIN_EDIT, this._onChainEdit); - - // 7. Subscribe to loop start for stateful primitive re-evaluation - this._bus.on(SEQ.LOOP_START, this._onLoopStart); - - this._initialized = true; - } - - /** - * Start the clock. Requires init() to have been called. - */ - start() { - if (!this._initialized) { - throw new Error('ShapeSeqEngine.start() called before init()'); - } - if (this._playing) return; - - this._playing = true; - this._clock.start(); - } - - /** - * Stop the clock and release all active notes. - */ - stop() { - if (!this._playing) return; - - this._playing = false; - this._clock.stop(); - this._releaseAllNotes(); - } - - /** - * Full cleanup: stop playback, unsubscribe from events, destroy IML. - */ - destroy() { - this.stop(); - - // Unsubscribe from event bus - this._bus.off(SEQ.NOTE_ON, this._onNoteOn); - this._bus.off(SEQ.NOTE_OFF, this._onNoteOff); - this._bus.off(UI.CHAIN_EDIT, this._onChainEdit); - this._bus.off(SEQ.LOOP_START, this._onLoopStart); - - // Destroy the sequence IML instance - if (this._sequenceIML) { - this._sequenceIML.destroy(); - this._sequenceIML = null; - } - - this._chain = null; - this._clock = null; - this._projectionChain = null; - this._initialized = false; - } - - // ── Configuration ────────────────────────────────────────────────── - - /** - * Update the clock tempo. - * @param {number} bpm - */ - setTempo(bpm) { - if (this._clock) { - this._clock.setTempo(bpm); - } - this._bumpGeneration(); - } - - /** - * Set the number of steps in the generated pattern. - * @param {number} count - */ - setStepCount(count) { - const c = Math.max(1, count | 0); - this._stepCount = c; - this._bumpGeneration(); - } - - /** - * Update the projection config. Accepts partial options merged with current. - * @param {Object} opts - Partial projection config - */ - setProjection(opts) { - const cur = this._projectionChain; - this._projectionChain = createProjection({ - velocityCurve: opts.velocityCurve ?? cur.velocityCurve, - gateThreshold: opts.gateThreshold ?? cur.gateThreshold, - pitchRange: { - low: opts.pitchRange?.low ?? cur.pitchRange.low, - high: opts.pitchRange?.high ?? cur.pitchRange.high, - }, - }); - this._bumpGeneration(); - } - - /** - * Get the current projection config. - * @returns {{ velocityCurve: string, gateThreshold: number, pitchRange: { low: number, high: number } }} - */ - getProjection() { - return this._projectionChain; - } - - /** - * Change the MLP output count. Destroys and recreates the sequence IML - * with a new architecture scaled to the requested count, then randomizes - * weights. Training examples are lost — callers should snapshot first if - * needed. - * - * @param {number} count - desired output count (e.g. 8, 16, 32) - * @returns {Promise} - */ - async setOutputCount(count) { - if (!this._initialized) { - throw new Error('setOutputCount() called before init()'); - } - - this._outputCount = count; - - // Tear down old instance - if (this._sequenceIML) { - this._sequenceIML.destroy(); - this._sequenceIML = null; - } - - // Create new instance with updated architecture - this._sequenceIML = await createSequenceIML({ outputCount: count }); - this._sequenceIML.randomiseWeights(DEFAULT_SPREAD); - - // TODO: bump generation counter (meml-06h) - this._bumpGeneration(); - } - - // ── Chain access (for UI binding) ────────────────────────────────── - - /** @returns {Chain} */ - getChain() { return this._chain; } - - /** @returns {ClockEngine} */ - getClock() { return this._clock; } - - /** @returns {WasmIML} */ - getSequenceIML() { return this._sequenceIML; } - - /** @returns {'mono'|'poly'} */ - get voiceMode() { return this._voiceMode; } - - /** - * Set voice mode. - * @param {'mono'|'poly'} mode - */ - setVoiceMode(mode) { - if (mode !== 'mono' && mode !== 'poly') { - throw new RangeError(`Invalid voice mode: "${mode}". Must be 'mono' or 'poly'.`); - } - if (mode === this._voiceMode) return; - - // Release all active notes when switching modes - this._releaseAllNotes(); - this._lastNote = null; - this._voiceMode = mode; - } - - /** @returns {FreezeManager} */ - get freezeManager() { return this._freezeManager; } - - /** @returns {MLPModeManager} */ - get mlpMode() { return this._mlpMode; } - - // ── MLP mode switching ──────────────────────────────────────────── - - /** - * Switch between unified and dual MLP mode. - * - * - unified: destroy the internal sequence MLP; inference is driven - * externally via setSequenceOutputsFromTimbre(). - * - dual: (re)create the internal sequence MLP via createSequenceIML. - * - * @param {'unified'|'dual'} mode - * @returns {Promise} - */ - async setMLPMode(mode) { - this._mlpMode.setMode(mode); // validates - - if (mode === MLP_MODES.UNIFIED) { - // Destroy internal sequence MLP — unified mode uses external outputs - if (this._sequenceIML) { - this._sequenceIML.destroy(); - this._sequenceIML = null; - } - } else { - // Dual mode: ensure we have a sequence MLP - if (!this._sequenceIML) { - this._sequenceIML = await createSequenceIML({ outputCount: this._outputCount }); - this._sequenceIML.randomiseWeights(DEFAULT_SPREAD); - } - } - - this._bumpGeneration(); - } - - /** - * Feed sequence outputs extracted from the timbre MLP (unified mode). - * - * Runs the downstream pipeline (param mapping -> chain -> projection -> clock) - * without running the internal sequence MLP inference. - * - * @param {Float32Array} timbreOutputs - full output array from the timbre MLP - */ - setSequenceOutputsFromTimbre(timbreOutputs) { - if (!this._initialized) return; - if (this._mlpMode.mode !== MLP_MODES.UNIFIED) return; - - const mlpOutputs = this._mlpMode.extractSequenceOutputs(timbreOutputs); - this._runPipeline(mlpOutputs); - } - - // ── Freeze-as-algorithm ──────────────────────────────────────────── - - /** - * Freeze the current state. - * - * In **algorithm** mode (default): capture param values, seeds, and states. - * All params start frozen; use toggleParamFreeze() to re-expose - * individual params for ML-driven exploration. - * - * In **pattern** mode: capture the last projected pattern. The sequencer - * bypasses the primitive chain entirely and loops the frozen pattern. - * - * @param {'algorithm'|'pattern'} [mode='algorithm'] - */ - freeze(mode = 'algorithm') { - if (mode === 'pattern') { - // Need the last evaluated pattern - this._freezeManager.freeze(null, null, null, 'pattern', this._lastPattern); - } else { - if (!this._chain) return; - const currentParams = this._lastEvaluatedParams || new Float32Array(this._chain.totalParamCount); - this._freezeManager.freeze(this._chain, currentParams, this._masterSeed, 'algorithm'); - } - } - - /** - * Unfreeze: clear captured state and return to normal ML control. - */ - unfreeze() { - this._freezeManager.unfreeze(); - this._bumpGeneration(); - } - - /** - * Toggle a single param between frozen and live. - * @param {number} flatIndex - */ - toggleParamFreeze(flatIndex) { - this._freezeManager.toggleParam(flatIndex); - this._bumpGeneration(); - } - - // ── Input routing ────────────────────────────────────────────────── - - /** - * Feed new input values to the sequence MLP and run the full pipeline: - * MLP inference -> param mapping -> chain evaluation -> projection -> clock scheduling. - * - * Call this each frame with the routed input values (e.g., [x, y]). - * - * @param {number[]} values - input array (typically [x, y]) - */ - setSequenceInputs(values) { - if (!this._initialized) return; - - // In unified mode, skip — inference is driven externally via - // setSequenceOutputsFromTimbre(). - if (this._mlpMode.mode === MLP_MODES.UNIFIED) return; - - // Dual mode requires the internal sequence MLP - if (!this._sequenceIML) return; - - // Freeze-as-pattern: skip entire pipeline, just keep looping frozen pattern - if (this._freezeManager.isFrozen && this._freezeManager.freezeMode === 'pattern') { - return; - } - - // Dirty-check: skip re-evaluation if inputs AND config haven't changed - const EPS = 1e-5; - const inputsSame = Math.abs(values[0] - this._lastInputs[0]) < EPS && - Math.abs(values[1] - this._lastInputs[1]) < EPS; - const generationSame = this._generation === this._lastGeneration; - if (inputsSame && generationSame) { - return; - } - this._lastInputs[0] = values[0]; - this._lastInputs[1] = values[1]; - this._lastGeneration = this._generation; - - // 1. Forward inputs to the sequence IML - this._sequenceIML.setInputs(values); - - // 2. Run MLP inference - this._sequenceIML.process(); - - // 3. Get the MLP outputs - const mlpOutputs = this._sequenceIML.getOutputs(); - - // 4–7. Run downstream pipeline - this._runPipeline(mlpOutputs); - } - - /** - * Shared downstream pipeline: param mapping -> chain -> projection -> clock. - * Used by both setSequenceInputs (dual mode) and setSequenceOutputsFromTimbre (unified mode). - * - * @private - * @param {Float32Array} mlpOutputs - raw MLP outputs (sequence slice) - */ - _runPipeline(mlpOutputs) { - // Map N outputs to M primitive params - const paramCount = this._chain.totalParamCount; - let mappedParams = map(mlpOutputs, paramCount); - - // Apply freeze: frozen params use captured values, live params get delta control - if (this._freezeManager.isFrozen) { - const schemas = this._chain.getParamSchemas(); - mappedParams = DeltaController.computeEffective( - this._freezeManager.getFrozenParams(), - this._freezeManager.getLiveFlags(), - mappedParams, - schemas, - 0.3 // deltaScale — could be configurable later - ); - } - - // Track last evaluated params for freeze snapshot - this._lastEvaluatedParams = mappedParams; - - // Evaluate the chain to produce a pattern description - const patternDesc = this._chain.evaluate(mappedParams, this._stepCount, this._masterSeed); - - // Apply projection transforms - const projectedPattern = applyProjection(this._projectionChain, patternDesc); - - // Track last pattern for freeze-as-pattern snapshot - this._lastPattern = projectedPattern; - - // Schedule the pattern on the clock - this._clock.schedulePattern(projectedPattern); - } - - // ── ML control ───────────────────────────────────────────────────── - - /** @returns {boolean} */ - get isPlaying() { - return this._playing; - } - - // ── Generation counter (private) ─────────────────────────────────── - - /** - * Increment the generation counter to force re-evaluation on next - * setSequenceInputs() call, even if inputs haven't changed. - * @private - */ - _bumpGeneration() { - this._generation++; - } - - // ── Loop re-evaluation (private) ────────────────────────────────── - - /** - * Handle seq.loopStart events. If the chain contains stateful primitives - * with reEvalOnLoop === true, force a pipeline re-evaluation using the - * last known inputs. This lets stateful generators (e.g. PitchWalker) - * produce evolving patterns across loops even when inputs stay still. - * - * When freeze-as-algorithm is active, re-evaluation is suppressed - * to preserve the frozen state. - * - * @private - */ - _handleLoopStart() { - if (!this._initialized || !this._chain || !this._sequenceIML) return; - if (this._freezeManager.shouldSuppressReEval()) return; - if (!this._chain.hasReEvalPrimitives()) return; - - // Bump the generation counter so the next setSequenceInputs() call - // bypasses the dirty-check and re-runs the full pipeline. - this._bumpGeneration(); - - // If we have cached inputs, force an immediate re-evaluation now - // (rather than waiting for the next setSequenceInputs() frame). - if (!isNaN(this._lastInputs[0]) && !isNaN(this._lastInputs[1])) { - this.setSequenceInputs(this._lastInputs); - } - } - - // ── Bridge integration (private) ─────────────────────────────────── - - /** - * Handle seq.noteOn events from the event bus. - * Converts [0,1] pitch to MIDI note number and forwards to C15. - * - * @private - * @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision } - */ - _handleNoteOn(data) { - // Use integer midiNote if set (post-IntervalLock), otherwise fall back - // to the old pitch*127 encoding for backward compatibility. - const midiNote = data.midiNote != null ? data.midiNote : (Math.round(data.pitch * 127) | 0); - const velocity = data.velocity; - - // Clamp to valid MIDI range - const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote; - const vel = velocity < 0 ? 0 : velocity > 1 ? 1 : velocity; - - if (this._voiceMode === 'mono') { - // Kill previous note (monophonic behavior) - if (this._lastNote) { - this._c15.noteOff(this._lastNote.note); - this._activeNotes.delete(this._lastNote.note); - } - this._lastNote = { note, velocity: vel }; - } else { - // Poly: retrigger if same pitch already active - if (this._activeNotes.has(note)) { - this._c15.noteOff(note); - } - } - - this._c15.noteOn(note, vel); - this._activeNotes.add(note); - } - - /** - * Handle seq.noteOff events from the event bus. - * - * @private - * @param {Object} data - { pitch, velocity, stepIndex, time } - */ - _handleNoteOff(data) { - const midiNote = data.midiNote != null ? data.midiNote : (Math.round(data.pitch * 127) | 0); - const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote; - - this._c15.noteOff(note); - this._activeNotes.delete(note); - } - - /** - * Release all currently active notes to avoid orphaned noteOns. - * @private - */ - _releaseAllNotes() { - for (const note of this._activeNotes) { - this._c15.noteOff(note); - } - this._activeNotes.clear(); - } -} diff --git a/playground/js/shapeseq/session.js b/playground/js/shapeseq/session.js deleted file mode 100644 index 1fad8f7..0000000 --- a/playground/js/shapeseq/session.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * ShapeSeq session state serialization. - * - * Captures and restores the complete ShapeSeq state: - * - Chain configuration (which primitives, in what order) - * - Primitive states (e.g., PitchWalker position) - * - Generator combine mode - * - Projection config (3 knobs) - * - Voice mode (mono/poly) - * - Sequence MLP weights (if in dual mode) - * - Freeze state (mode, frozen params, live flags, seeds) - * - Clock config (BPM, step count) - * - * @module shapeseq/session - */ - -import { PRIMITIVE_REGISTRY } from './primitives.js'; - -/** - * Serialize ShapeSeq engine state to a plain object (JSON-safe). - * @param {import('./sequencer.js').ShapeSeqEngine} engine - * @returns {Object} - */ -export function serializeShapeSeqState(engine) { - const chain = engine.getChain(); - const prims = chain.getPrimitives(); - - return { - version: 1, - chain: { - primitives: prims.map(p => ({ - name: p.name, - })), - states: chain.getState(), - generatorCombineMode: chain.generatorCombineMode, - }, - projection: engine.getProjection(), - voiceMode: engine.voiceMode, - clock: { - bpm: engine.getClock().bpm, - }, - // Sequence MLP weights (for dual mode restoration) - sequenceWeights: engine.getSequenceIML()?.getWeights?.() ?? null, - // Freeze state - freeze: serializeFreezeState(engine.freezeManager), - }; -} - -/** - * Serialize freeze manager state to a JSON-safe object. - * Returns null when not frozen. - * - * @param {import('./freeze.js').FreezeManager} fm - * @returns {Object|null} - */ -function serializeFreezeState(fm) { - if (!fm.isFrozen) return null; - return { - mode: fm.freezeMode, - frozenParams: fm.getFrozenParams() ? Array.from(fm.getFrozenParams()) : null, - liveFlags: fm.getLiveFlags() ? Array.from(fm.getLiveFlags()) : null, - seeds: fm.getFrozenSeeds(), - states: fm.getFrozenStates(), - masterSeed: fm.getMasterSeed(), - // For pattern mode: - frozenPattern: fm.getFrozenPattern() ?? null, - }; -} - -/** - * Restore ShapeSeq engine state from a serialized object. - * @param {import('./sequencer.js').ShapeSeqEngine} engine - * @param {Object} state - from serializeShapeSeqState - */ -export function restoreShapeSeqState(engine, state) { - if (!state || state.version !== 1) return; - - const chain = engine.getChain(); - - // Rebuild chain from primitive names - if (state.chain?.primitives) { - // Clear existing - const existing = chain.getPrimitives(); - for (let i = existing.length - 1; i >= 0; i--) { - chain.removePrimitive(i); - } - // Add from state - for (const p of state.chain.primitives) { - const Ctor = PRIMITIVE_REGISTRY[p.name]; - if (Ctor) chain.addPrimitive(new Ctor()); - } - // Restore states - if (state.chain.states) chain.setState(state.chain.states); - if (state.chain.generatorCombineMode) { - chain.generatorCombineMode = state.chain.generatorCombineMode; - } - } - - // Projection - if (state.projection) engine.setProjection(state.projection); - - // Voice mode - if (state.voiceMode) engine.setVoiceMode(state.voiceMode); - - // Clock - if (state.clock?.bpm) engine.setTempo(state.clock.bpm); - - // Sequence MLP weights - if (state.sequenceWeights) { - const iml = engine.getSequenceIML(); - if (iml?.setWeights) iml.setWeights(state.sequenceWeights); - } - - // Freeze state restoration is complex — skip for now, just ensure unfrozen on load - // TODO: restore freeze state properly -} diff --git a/playground/js/shapeseq/signal.js b/playground/js/shapeseq/signal.js deleted file mode 100644 index 88a098c..0000000 --- a/playground/js/shapeseq/signal.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Minimal signal/observable for reactive UI updates. - * No framework dependency. ~40 lines. - * - * @module shapeseq/signal - */ - -export function createSignal(initialValue) { - let value = initialValue; - const subscribers = new Set(); - - function get() { return value; } - - function set(newValue) { - if (newValue === value) return; - value = newValue; - for (const fn of subscribers) fn(value); - } - - function subscribe(fn) { - subscribers.add(fn); - return () => subscribers.delete(fn); - } - - return { get, set, subscribe }; -} - -/** - * Signal backed by a Float32Array for param vectors. - * Subscribers are called when any element changes. - */ -export function createArraySignal(length) { - const value = new Float32Array(length); - const subscribers = new Set(); - - function get() { return value; } - - function update(newArray) { - let changed = false; - for (let i = 0; i < value.length && i < newArray.length; i++) { - if (value[i] !== newArray[i]) { value[i] = newArray[i]; changed = true; } - } - if (changed) { - for (const fn of subscribers) fn(value); - } - } - - function subscribe(fn) { - subscribers.add(fn); - return () => subscribers.delete(fn); - } - - return { get, update, subscribe }; -} diff --git a/playground/js/shapeseq/step-viz.js b/playground/js/shapeseq/step-viz.js deleted file mode 100644 index 8682c0c..0000000 --- a/playground/js/shapeseq/step-viz.js +++ /dev/null @@ -1,328 +0,0 @@ -/** - * ShapeSeq Circular Step Visualizer - * - * Renders steps arranged in a circle (heptagon, tridecagon, etc.) - * with pitch mapped to radial distance, velocity to node size, - * and accent to color brightness. - * - * Designed for 60fps rendering — no allocations in the render loop. - * Port-ready: explicit state, no closures in hot paths. - * - * @module shapeseq/step-viz - */ - -import { SEQ } from './event-bus.js'; - -// ── Constants (pre-allocated, shared across instances) ────────────── - -const TWO_PI = Math.PI * 2; -const HALF_PI = Math.PI * 0.5; - -// Color constants -const COLOR_INACTIVE = 'rgba(255, 255, 255, 0.15)'; -const COLOR_ACTIVE = '#00ccff'; -const COLOR_CURRENT = '#ff6a00'; -const COLOR_ACCENT = '#ffcc00'; -const COLOR_BG = '#0d0d0d'; - -// Glow colors (pre-computed rgba strings) -const GLOW_ACTIVE = 'rgba(0, 204, 255, 0.3)'; -const GLOW_CURRENT = 'rgba(255, 106, 0, 0.4)'; -const GLOW_ACCENT = 'rgba(255, 204, 0, 0.35)'; - -// Layout -const PADDING_RATIO = 0.08; // canvas padding as fraction of min dimension -const OUTER_RADIUS_RATIO = 0.90; // outer ring at 90% of available radius -const INNER_RADIUS_RATIO = 0.30; // inner ring at 30% of available radius - -// Node sizing -const NODE_MIN_RADIUS = 4; -const NODE_MAX_RADIUS = 18; -const NODE_OUTLINE_WIDTH = 1.5; - -// Current-step indicator -const INDICATOR_EXTRA_RADIUS = 8; -const INDICATOR_LINE_WIDTH = 2; - -// Center dot -const CENTER_DOT_RADIUS = 3; - -// ── StepVisualizer ────────────────────────────────────────────────── - -export class StepVisualizer { - /** - * @param {{ canvas: HTMLCanvasElement, eventBus: import('./event-bus.js').EventBus }} opts - */ - constructor({ canvas, eventBus }) { - this._canvas = canvas; - this._ctx = canvas.getContext('2d'); - this._bus = eventBus; - - // State - this._pattern = null; // current pattern description - this._currentStep = -1; // playback position (-1 = none) - this._width = 0; - this._height = 0; - this._cx = 0; // center x - this._cy = 0; // center y - this._maxRadius = 0; // max ring radius in pixels - - // Pre-allocated arrays to avoid per-frame allocation. - // Sized lazily when pattern is set. - this._nodeX = null; // Float64Array — screen x per step - this._nodeY = null; // Float64Array — screen y per step - this._nodeR = null; // Float64Array — rendered radius per step - - // Interaction - this._tapCallback = null; - this._onPointerDown = this._handlePointerDown.bind(this); - - // Event bus subscription - this._onStep = this._handleStep.bind(this); - this._bus.on(SEQ.STEP, this._onStep); - - // Canvas interaction - this._canvas.addEventListener('pointerdown', this._onPointerDown); - - // Initial sizing - this.resize(canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height); - } - - // ── Public API ────────────────────────────────────────────────── - - /** - * Update the displayed pattern. - * @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - */ - setPattern(patternDesc) { - this._pattern = patternDesc; - const count = patternDesc ? patternDesc.stepCount : 0; - - // (Re)allocate coordinate buffers only when step count changes - if (!this._nodeX || this._nodeX.length !== count) { - this._nodeX = new Float64Array(count); - this._nodeY = new Float64Array(count); - this._nodeR = new Float64Array(count); - } - - this._computeLayout(); - } - - /** - * Update the playback position. - * @param {number} index — step index (0-based), or -1 for none - */ - setCurrentStep(index) { - this._currentStep = index; - } - - /** - * Draw one frame. Call from requestAnimationFrame. - */ - render() { - const ctx = this._ctx; - const w = this._width; - const h = this._height; - - // Clear - ctx.fillStyle = COLOR_BG; - ctx.fillRect(0, 0, w, h); - - if (!this._pattern || this._pattern.stepCount === 0) return; - - const steps = this._pattern.steps; - const count = this._pattern.stepCount; - const cx = this._cx; - const cy = this._cy; - - // Draw connecting ring (subtle guide circle at midpoint radius) - const midRadius = this._maxRadius * ((OUTER_RADIUS_RATIO + INNER_RADIUS_RATIO) * 0.5); - ctx.beginPath(); - ctx.arc(cx, cy, midRadius, 0, TWO_PI); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; - ctx.lineWidth = 1; - ctx.stroke(); - - // Center dot - ctx.beginPath(); - ctx.arc(cx, cy, CENTER_DOT_RADIUS, 0, TWO_PI); - ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; - ctx.fill(); - - // Draw connector line from center to current step - if (this._currentStep >= 0 && this._currentStep < count) { - const si = this._currentStep; - ctx.beginPath(); - ctx.moveTo(cx, cy); - ctx.lineTo(this._nodeX[si], this._nodeY[si]); - ctx.strokeStyle = 'rgba(255, 106, 0, 0.2)'; - ctx.lineWidth = INDICATOR_LINE_WIDTH; - ctx.stroke(); - } - - // Draw step nodes - for (let i = 0; i < count; i++) { - const step = steps[i]; - const nx = this._nodeX[i]; - const ny = this._nodeY[i]; - const nr = this._nodeR[i]; - const isCurrent = i === this._currentStep; - - if (isCurrent) { - // Outer glow for current step - ctx.beginPath(); - ctx.arc(nx, ny, nr + INDICATOR_EXTRA_RADIUS, 0, TWO_PI); - ctx.fillStyle = GLOW_CURRENT; - ctx.fill(); - } - - if (step.trigger) { - // Glow behind active nodes - if (!isCurrent) { - const glowColor = step.accent ? GLOW_ACCENT : GLOW_ACTIVE; - ctx.beginPath(); - ctx.arc(nx, ny, nr + 4, 0, TWO_PI); - ctx.fillStyle = glowColor; - ctx.fill(); - } - - // Filled node - ctx.beginPath(); - ctx.arc(nx, ny, nr, 0, TWO_PI); - if (isCurrent) { - ctx.fillStyle = COLOR_CURRENT; - } else if (step.accent) { - ctx.fillStyle = COLOR_ACCENT; - } else { - ctx.fillStyle = COLOR_ACTIVE; - } - ctx.fill(); - } else { - // Dim outline only for untriggered steps - ctx.beginPath(); - ctx.arc(nx, ny, nr, 0, TWO_PI); - ctx.strokeStyle = isCurrent ? COLOR_CURRENT : COLOR_INACTIVE; - ctx.lineWidth = NODE_OUTLINE_WIDTH; - ctx.stroke(); - } - } - } - - /** - * Handle canvas resize. - * @param {number} width — CSS pixels - * @param {number} height — CSS pixels - */ - resize(width, height) { - const dpr = window.devicePixelRatio || 1; - this._canvas.width = width * dpr; - this._canvas.height = height * dpr; - this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - - this._width = width; - this._height = height; - this._cx = width * 0.5; - this._cy = height * 0.5; - - const minDim = Math.min(width, height); - this._maxRadius = (minDim * 0.5) * (1 - PADDING_RATIO * 2); - - this._computeLayout(); - } - - /** - * Register a tap callback. - * @param {function(number): void} callback — receives step index - */ - onStepTap(callback) { - this._tapCallback = callback; - } - - /** - * Unsubscribe from event bus and remove DOM listeners. - */ - destroy() { - this._bus.off(SEQ.STEP, this._onStep); - this._canvas.removeEventListener('pointerdown', this._onPointerDown); - this._tapCallback = null; - this._pattern = null; - } - - // ── Private ───────────────────────────────────────────────────── - - /** - * Re-compute node positions from current pattern + canvas size. - * Called when pattern or size changes — NOT per frame. - */ - _computeLayout() { - if (!this._pattern || !this._nodeX) return; - - const steps = this._pattern.steps; - const count = this._pattern.stepCount; - const cx = this._cx; - const cy = this._cy; - const maxR = this._maxRadius; - const outerR = maxR * OUTER_RADIUS_RATIO; - const innerR = maxR * INNER_RADIUS_RATIO; - const radiusRange = outerR - innerR; - - // Angular step: start at top (-PI/2), go clockwise - const angleStep = TWO_PI / count; - - for (let i = 0; i < count; i++) { - const step = steps[i]; - const angle = -HALF_PI + angleStep * i; - - // Pitch -> radial distance: low pitch = outer, high pitch = inner - const pitchNorm = step.pitch; // 0 = low (outer), 1 = high (inner) - const r = outerR - pitchNorm * radiusRange; - - this._nodeX[i] = cx + Math.cos(angle) * r; - this._nodeY[i] = cy + Math.sin(angle) * r; - - // Velocity -> node size - this._nodeR[i] = NODE_MIN_RADIUS + step.velocity * (NODE_MAX_RADIUS - NODE_MIN_RADIUS); - } - } - - /** - * Handle seq.step events from the event bus. - */ - _handleStep(data) { - if (typeof data.stepIndex === 'number') { - this._currentStep = data.stepIndex; - } - } - - /** - * Handle pointer down on the canvas for tap interaction. - */ - _handlePointerDown(e) { - if (!this._tapCallback || !this._pattern) return; - - const rect = this._canvas.getBoundingClientRect(); - const px = e.clientX - rect.left; - const py = e.clientY - rect.top; - const count = this._pattern.stepCount; - - // Find closest step within hit radius - let bestIdx = -1; - let bestDistSq = Infinity; - - for (let i = 0; i < count; i++) { - const dx = px - this._nodeX[i]; - const dy = py - this._nodeY[i]; - const distSq = dx * dx + dy * dy; - // Hit area is the node radius + some tolerance - const hitR = this._nodeR[i] + 12; - if (distSq < hitR * hitR && distSq < bestDistSq) { - bestDistSq = distSq; - bestIdx = i; - } - } - - if (bestIdx >= 0) { - this._tapCallback(bestIdx); - } - } -} diff --git a/playground/js/shapeseq/tests/delta.test.js b/playground/js/shapeseq/tests/delta.test.js deleted file mode 100644 index b7c8734..0000000 --- a/playground/js/shapeseq/tests/delta.test.js +++ /dev/null @@ -1,193 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { DeltaController } from '../delta.js'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -const EPS = 1e-6; - -function approxEqual(actual, expected, msg) { - assert.ok( - Math.abs(actual - expected) < EPS, - `${msg || ''} expected ${expected}, got ${actual}` - ); -} - -function makeSchemas(count, boundary = 'clamp', scaledRange) { - const schemas = []; - for (let i = 0; i < count; i++) { - const entry = { boundary }; - if (boundary === 'scaled' && scaledRange !== undefined) { - entry.scaledRange = scaledRange; - } - schemas.push({ schema: entry }); - } - return schemas; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('DeltaController.computeEffective', () => { - - it('all frozen (liveFlags all 0): output === frozenParams', () => { - const frozen = new Float32Array([0.2, 0.5, 0.8]); - const live = new Uint8Array([0, 0, 0]); - const mlp = new Float32Array([0.0, 1.0, 0.5]); - const schemas = makeSchemas(3); - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas); - - approxEqual(result[0], 0.2, 'param 0'); - approxEqual(result[1], 0.5, 'param 1'); - approxEqual(result[2], 0.8, 'param 2'); - }); - - it('all live with clamp boundary: delta applied, result clamped to [0,1]', () => { - const frozen = new Float32Array([0.9, 0.1]); - const live = new Uint8Array([1, 1]); - const mlp = new Float32Array([1.0, 0.0]); // max positive, max negative - const schemas = makeSchemas(2, 'clamp'); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // param 0: 0.9 + (1.0 - 0.5)*2*0.3 = 0.9 + 0.3 = 1.2 -> clamped to 1.0 - approxEqual(result[0], 1.0, 'param 0 clamped high'); - // param 1: 0.1 + (0.0 - 0.5)*2*0.3 = 0.1 - 0.3 = -0.2 -> clamped to 0.0 - approxEqual(result[1], 0.0, 'param 1 clamped low'); - }); - - it('live with wrap boundary: values wrap around', () => { - const frozen = new Float32Array([0.9, 0.1]); - const live = new Uint8Array([1, 1]); - const mlp = new Float32Array([1.0, 0.0]); - const schemas = makeSchemas(2, 'wrap'); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // param 0: 0.9 + 0.3 = 1.2 -> wrap -> 0.2 - approxEqual(result[0], 0.2, 'param 0 wrapped high'); - // param 1: 0.1 - 0.3 = -0.2 -> wrap -> 0.8 - approxEqual(result[1], 0.8, 'param 1 wrapped low'); - }); - - it('live with scaled boundary: delta operates within scaledRange of frozen value', () => { - const frozen = new Float32Array([0.5]); - const live = new Uint8Array([1]); - // With scaled boundary, applyBoundary maps [0,1] input to [frozen-range, frozen+range] - // The raw value passed to applyBoundary is frozen + delta - // For scaled: lo = 0.5 - 0.3 = 0.2, hi = 0.5 + 0.3 = 0.8 - // mapped = 0.2 + raw * 0.6 - const mlp = new Float32Array([0.75]); // delta = (0.75 - 0.5)*2*0.3 = 0.15, raw = 0.65 - const schemas = makeSchemas(1, 'scaled', 0.3); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // raw = 0.5 + 0.15 = 0.65 - // scaled: lo = 0.5 - 0.3 = 0.2, hi = 0.5 + 0.3 = 0.8 - // mapped = 0.2 + 0.65 * (0.8 - 0.2) = 0.2 + 0.39 = 0.59 - approxEqual(result[0], 0.59, 'scaled boundary'); - }); - - it('MLP value 0.5 produces zero delta (frozen value unchanged)', () => { - const frozen = new Float32Array([0.3, 0.7]); - const live = new Uint8Array([1, 1]); - const mlp = new Float32Array([0.5, 0.5]); - const schemas = makeSchemas(2, 'clamp'); - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas); - - approxEqual(result[0], 0.3, 'param 0 unchanged'); - approxEqual(result[1], 0.7, 'param 1 unchanged'); - }); - - it('MLP value 0.0 produces max negative delta', () => { - const frozen = new Float32Array([0.5]); - const live = new Uint8Array([1]); - const mlp = new Float32Array([0.0]); - const schemas = makeSchemas(1, 'clamp'); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // 0.5 + (0.0 - 0.5)*2*0.3 = 0.5 - 0.3 = 0.2 - approxEqual(result[0], 0.2, 'max negative delta'); - }); - - it('MLP value 1.0 produces max positive delta', () => { - const frozen = new Float32Array([0.5]); - const live = new Uint8Array([1]); - const mlp = new Float32Array([1.0]); - const schemas = makeSchemas(1, 'clamp'); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // 0.5 + (1.0 - 0.5)*2*0.3 = 0.5 + 0.3 = 0.8 - approxEqual(result[0], 0.8, 'max positive delta'); - }); - - it('mixed frozen/live params', () => { - const frozen = new Float32Array([0.2, 0.5, 0.8, 0.4]); - const live = new Uint8Array([0, 1, 0, 1]); - const mlp = new Float32Array([0.9, 0.5, 0.1, 0.75]); - const schemas = makeSchemas(4, 'clamp'); - const deltaScale = 0.3; - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale); - - // param 0: frozen -> 0.2 - approxEqual(result[0], 0.2, 'frozen param 0'); - // param 1: live, mlp=0.5 -> zero delta -> 0.5 - approxEqual(result[1], 0.5, 'live param 1 zero delta'); - // param 2: frozen -> 0.8 - approxEqual(result[2], 0.8, 'frozen param 2'); - // param 3: live, 0.4 + (0.75 - 0.5)*2*0.3 = 0.4 + 0.15 = 0.55 - approxEqual(result[3], 0.55, 'live param 3'); - }); - - it('deltaScale affects magnitude', () => { - const frozen = new Float32Array([0.5]); - const live = new Uint8Array([1]); - const mlp = new Float32Array([1.0]); // max positive - const schemas = makeSchemas(1, 'clamp'); - - const r1 = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.1); - const r2 = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.5); - - // scale 0.1: 0.5 + 0.1 = 0.6 - approxEqual(r1[0], 0.6, 'small deltaScale'); - // scale 0.5: 0.5 + 0.5 = 1.0 - approxEqual(r2[0], 1.0, 'large deltaScale'); - }); - - it('defaults deltaScale to 0.3 when not provided', () => { - const frozen = new Float32Array([0.5]); - const live = new Uint8Array([1]); - const mlp = new Float32Array([1.0]); - const schemas = makeSchemas(1, 'clamp'); - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas); - - // 0.5 + (1.0 - 0.5)*2*0.3 = 0.8 - approxEqual(result[0], 0.8, 'default deltaScale 0.3'); - }); - - it('falls back to clamp when schema is missing', () => { - const frozen = new Float32Array([0.9]); - const live = new Uint8Array([1]); - const mlp = new Float32Array([1.0]); - const schemas = []; // empty — no schema for this index - - const result = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.3); - - // 0.9 + 0.3 = 1.2 -> clamped to 1.0 - approxEqual(result[0], 1.0, 'missing schema falls back to clamp'); - }); -}); diff --git a/playground/js/shapeseq/tests/freeze-pattern.test.js b/playground/js/shapeseq/tests/freeze-pattern.test.js deleted file mode 100644 index 96c66e6..0000000 --- a/playground/js/shapeseq/tests/freeze-pattern.test.js +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { FreezeManager } from '../freeze.js'; -import { Chain } from '../chain.js'; -import { EuclideanRhythm, ProbabilityGate, PitchWalker } from '../primitives.js'; -import { createPattern, clonePattern } from '../pattern.js'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function makeTestChain() { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new ProbabilityGate()); - chain.addPrimitive(new PitchWalker()); - return { chain, paramCount: chain.totalParamCount }; -} - -function makeParams(count) { - const params = new Float32Array(count); - for (let i = 0; i < count; i++) { - params[i] = (i + 1) / (count + 1); - } - return params; -} - -/** - * Create a pattern with some non-default step data for testing. - */ -function makeTestPattern(stepCount) { - const pattern = createPattern(stepCount); - for (let i = 0; i < stepCount; i++) { - pattern.steps[i].trigger = i % 2 === 0; - pattern.steps[i].pitch = (i + 1) / (stepCount + 1); - pattern.steps[i].velocity = 0.5 + (i * 0.05); - pattern.steps[i].accent = i === 0; - pattern.steps[i].midiNote = 60 + i; - } - pattern.metadata.source = 'test'; - return pattern; -} - -// --------------------------------------------------------------------------- -// freeze with mode='pattern' captures the pattern -// --------------------------------------------------------------------------- - -describe('FreezeManager freeze-as-pattern', () => { - it('freeze with mode=pattern captures the pattern', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - - assert.strictEqual(fm.isFrozen, true); - const frozen = fm.getFrozenPattern(); - assert.ok(frozen !== null, 'frozen pattern should not be null'); - assert.strictEqual(frozen.stepCount, 8); - assert.strictEqual(frozen.steps.length, 8); - }); - - it('getFrozenPattern returns the captured pattern', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(4); - - fm.freeze(null, null, null, 'pattern', pattern); - - const frozen = fm.getFrozenPattern(); - // Verify step data matches - for (let i = 0; i < 4; i++) { - assert.strictEqual(frozen.steps[i].trigger, pattern.steps[i].trigger, - 'step ' + i + ' trigger should match'); - assert.strictEqual(frozen.steps[i].pitch, pattern.steps[i].pitch, - 'step ' + i + ' pitch should match'); - assert.strictEqual(frozen.steps[i].velocity, pattern.steps[i].velocity, - 'step ' + i + ' velocity should match'); - assert.strictEqual(frozen.steps[i].midiNote, pattern.steps[i].midiNote, - 'step ' + i + ' midiNote should match'); - } - assert.strictEqual(frozen.metadata.source, 'test'); - }); - - it('freezeMode is pattern', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - - assert.strictEqual(fm.freezeMode, 'pattern'); - }); - - it('getEffectiveParams returns null in pattern mode', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - - const result = fm.getEffectiveParams(new Float32Array(8)); - assert.strictEqual(result, null); - }); - - it('shouldSuppressReEval returns true in pattern mode', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - - assert.strictEqual(fm.shouldSuppressReEval(), true); - }); - - it('unfreeze clears frozen pattern', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - assert.strictEqual(fm.isFrozen, true); - assert.ok(fm.getFrozenPattern() !== null); - - fm.unfreeze(); - - assert.strictEqual(fm.isFrozen, false); - assert.strictEqual(fm.freezeMode, null); - assert.strictEqual(fm.getFrozenPattern(), null); - assert.strictEqual(fm.getFrozenParams(), null); - assert.strictEqual(fm.getFrozenSeeds(), null); - assert.strictEqual(fm.getFrozenStates(), null); - assert.strictEqual(fm.getMasterSeed(), null); - }); - - it('freeze with mode=algorithm still works (backward compat)', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - fm.freeze(chain, params, 42, 'algorithm'); - - assert.strictEqual(fm.isFrozen, true); - assert.strictEqual(fm.freezeMode, 'algorithm'); - assert.ok(fm.getFrozenParams() instanceof Float32Array); - assert.strictEqual(fm.getFrozenParams().length, paramCount); - assert.ok(Array.isArray(fm.getFrozenSeeds())); - assert.ok(Array.isArray(fm.getFrozenStates())); - assert.strictEqual(fm.getMasterSeed(), 42); - assert.strictEqual(fm.getFrozenPattern(), null); - }); - - it('freeze with default mode is algorithm (backward compat)', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - // Call without mode argument — should default to 'algorithm' - fm.freeze(chain, params, 42); - - assert.strictEqual(fm.freezeMode, 'algorithm'); - assert.ok(fm.getFrozenParams() instanceof Float32Array); - assert.strictEqual(fm.getFrozenPattern(), null); - }); - - it('frozen pattern is a deep clone (mutating original does not affect frozen)', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(4); - - fm.freeze(null, null, null, 'pattern', pattern); - - // Mutate the original - pattern.steps[0].trigger = !pattern.steps[0].trigger; - pattern.steps[0].pitch = 0.999; - pattern.steps[0].midiNote = 127; - pattern.metadata.source = 'mutated'; - - // Frozen should be unaffected - const frozen = fm.getFrozenPattern(); - assert.strictEqual(frozen.steps[0].trigger, true, 'frozen trigger should be unchanged'); - assert.notStrictEqual(frozen.steps[0].pitch, 0.999, 'frozen pitch should be unchanged'); - assert.strictEqual(frozen.steps[0].midiNote, 60, 'frozen midiNote should be unchanged'); - assert.strictEqual(frozen.metadata.source, 'test', 'frozen metadata should be unchanged'); - }); - - it('pattern mode throws if currentPattern is null', () => { - const fm = new FreezeManager(); - assert.throws( - () => fm.freeze(null, null, null, 'pattern', null), - /pattern mode requires a currentPattern/ - ); - }); - - it('freezeMode is null when not frozen', () => { - const fm = new FreezeManager(); - assert.strictEqual(fm.freezeMode, null); - }); - - it('getFrozenPattern returns null when not frozen', () => { - const fm = new FreezeManager(); - assert.strictEqual(fm.getFrozenPattern(), null); - }); - - it('getFrozenPattern returns null in algorithm mode', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42, 'algorithm'); - assert.strictEqual(fm.getFrozenPattern(), null); - }); - - it('algorithm-mode state is null in pattern mode', () => { - const fm = new FreezeManager(); - const pattern = makeTestPattern(8); - - fm.freeze(null, null, null, 'pattern', pattern); - - assert.strictEqual(fm.getFrozenParams(), null); - assert.strictEqual(fm.getFrozenSeeds(), null); - assert.strictEqual(fm.getFrozenStates(), null); - assert.strictEqual(fm.getLiveFlags(), null); - assert.strictEqual(fm.getMasterSeed(), null); - }); -}); diff --git a/playground/js/shapeseq/tests/freeze-ui.test.js b/playground/js/shapeseq/tests/freeze-ui.test.js deleted file mode 100644 index 1e55803..0000000 --- a/playground/js/shapeseq/tests/freeze-ui.test.js +++ /dev/null @@ -1,366 +0,0 @@ -import { describe, it, beforeEach } from 'node:test'; -import { strict as assert } from 'node:assert'; - -// --------------------------------------------------------------------------- -// Minimal DOM stubs — just enough for FreezeUI's DOM operations -// --------------------------------------------------------------------------- - -class StubClassList { - constructor() { this._set = new Set(); } - add(...names) { names.forEach(n => this._set.add(n)); } - remove(...names) { names.forEach(n => this._set.delete(n)); } - contains(name) { return this._set.has(name); } -} - -class StubStyle { - constructor() { - this.opacity = ''; - this.position = ''; - this.cssText = ''; - this.left = ''; - this.width = ''; - } -} - -class StubElement { - constructor(tag) { - this.tagName = tag.toUpperCase(); - this._className = ''; - this.textContent = ''; - this.type = ''; - this.dataset = {}; - this.classList = new StubClassList(); - this.style = new StubStyle(); - this.children = []; - this.parentElement = null; - this._listeners = {}; - } - get className() { return this._className; } - set className(val) { - this._className = val; - // Sync classList with className string - this.classList._set.clear(); - val.split(/\s+/).filter(Boolean).forEach(c => this.classList._set.add(c)); - } - appendChild(child) { - child.parentElement = this; - this.children.push(child); - return child; - } - querySelector(sel) { - return this._queryAll(sel)[0] || null; - } - querySelectorAll(sel) { - return this._queryAll(sel); - } - _queryAll(sel) { - const results = []; - this._walk(el => { - if (matchesSelector(el, sel)) results.push(el); - }); - return results; - } - _walk(fn) { - for (const child of this.children) { - fn(child); - child._walk(fn); - } - } - closest(sel) { - let cur = this; - while (cur) { - if (matchesSelector(cur, sel)) return cur; - cur = cur.parentElement; - } - return null; - } - addEventListener(type, fn) { - if (!this._listeners[type]) this._listeners[type] = []; - this._listeners[type].push(fn); - } - removeEventListener(type, fn) { - if (!this._listeners[type]) return; - this._listeners[type] = this._listeners[type].filter(f => f !== fn); - } - dispatchEvent(evt) { - const handlers = this._listeners[evt.type] || []; - for (const h of handlers) h(evt); - // Bubble - if (evt.bubbles && this.parentElement) { - this.parentElement.dispatchEvent(evt); - } - } -} - -/** - * Very basic selector matching: handles .class, .class1.class2, tag selectors - */ -function matchesSelector(el, sel) { - // Handle compound selectors like ".chain-param-row.param-live" - const parts = sel.split('.'); - const tagPart = parts[0]; // empty string if selector starts with '.' - const classParts = parts.slice(1); - - if (tagPart && el.tagName !== tagPart.toUpperCase()) return false; - for (const cls of classParts) { - if (!el.classList.contains(cls)) return false; - } - // If selector is just a tag with no classes - if (classParts.length === 0 && tagPart) { - return el.tagName === tagPart.toUpperCase(); - } - return classParts.length > 0 || tagPart !== ''; -} - -function createElement(tag) { - return new StubElement(tag); -} - -// Stub out globalThis.document so injectStyles() doesn't crash -globalThis.document = { - createElement, - head: { appendChild() {} }, -}; - -// --------------------------------------------------------------------------- -// Minimal FreezeManager stub -// --------------------------------------------------------------------------- - -class StubFreezeManager { - constructor() { - this._frozen = false; - this._liveFlags = null; - } - get isFrozen() { return this._frozen; } - freeze(paramCount) { - this._frozen = true; - this._liveFlags = new Uint8Array(paramCount); - } - unfreeze() { - this._frozen = false; - this._liveFlags = null; - } - getLiveFlags() { return this._liveFlags; } - toggleParam(idx) { - if (!this._frozen) throw new Error('not frozen'); - this._liveFlags[idx] = this._liveFlags[idx] ? 0 : 1; - } -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function buildContainer(paramCount) { - const container = createElement('div'); - for (let i = 0; i < paramCount; i++) { - const row = createElement('div'); - row.classList.add('chain-param-row'); - row.dataset.flatIndex = String(i); - - const label = createElement('span'); - label.classList.add('chain-param-label'); - label.textContent = 'p' + i; - row.appendChild(label); - - const slider = createElement('input'); - slider.type = 'range'; - slider.classList.add('chain-param-slider'); - slider.dataset.flatIndex = String(i); - row.appendChild(slider); - - container.appendChild(row); - } - return container; -} - -function makeClickEvent(bubbles = true) { - return { type: 'click', bubbles, target: null }; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -describe('FreezeUI', () => { - let FreezeUI; - let freezeUI; - let container; - let fm; - let toggleLog; - - beforeEach(async () => { - if (!FreezeUI) { - const mod = await import('../freeze-ui.js'); - FreezeUI = mod.FreezeUI; - } - - container = buildContainer(4); - fm = new StubFreezeManager(); - toggleLog = []; - - freezeUI = new FreezeUI({ - container, - freezeManager: fm, - onToggle: (idx) => toggleLog.push(idx), - }); - }); - - // ── Construction ────────────────────────────────────────────────── - - it('can be constructed', () => { - assert.ok(freezeUI, 'FreezeUI instance should exist'); - assert.ok(freezeUI._container === container, 'container reference stored'); - }); - - // ── update() when frozen ────────────────────────────────────────── - - it('update() adds param-frozen class to all rows when frozen', () => { - fm.freeze(4); - freezeUI.update(); - - const rows = container.querySelectorAll('.chain-param-row'); - for (const row of rows) { - assert.ok(row.classList.contains('param-frozen'), - 'row ' + row.dataset.flatIndex + ' should have param-frozen'); - assert.ok(!row.classList.contains('param-live'), - 'row ' + row.dataset.flatIndex + ' should NOT have param-live'); - assert.strictEqual(row.style.opacity, '0.4', - 'frozen row should be dimmed'); - } - }); - - it('update() adds param-live class for live params when frozen', () => { - fm.freeze(4); - fm.toggleParam(1); - fm.toggleParam(3); - freezeUI.update(); - - const rows = container.querySelectorAll('.chain-param-row'); - - // Row 0: frozen - assert.ok(rows[0].classList.contains('param-frozen')); - assert.ok(!rows[0].classList.contains('param-live')); - assert.strictEqual(rows[0].style.opacity, '0.4'); - - // Row 1: live - assert.ok(rows[1].classList.contains('param-live')); - assert.ok(!rows[1].classList.contains('param-frozen')); - assert.notStrictEqual(rows[1].style.opacity, '0.4'); - - // Row 2: frozen - assert.ok(rows[2].classList.contains('param-frozen')); - - // Row 3: live - assert.ok(rows[3].classList.contains('param-live')); - }); - - // ── update() when NOT frozen ────────────────────────────────────── - - it('update() removes freeze styling when not frozen', () => { - fm.freeze(4); - fm.toggleParam(1); - freezeUI.update(); - - // Now unfreeze - fm.unfreeze(); - freezeUI.update(); - - const rows = container.querySelectorAll('.chain-param-row'); - for (const row of rows) { - assert.ok(!row.classList.contains('param-frozen'), - 'row should NOT have param-frozen after unfreeze'); - assert.ok(!row.classList.contains('param-live'), - 'row should NOT have param-live after unfreeze'); - assert.strictEqual(row.style.opacity, '', - 'opacity should be cleared'); - } - }); - - // ── Click toggle ────────────────────────────────────────────────── - - it('clicking a param row label calls onToggle with the flat index', () => { - fm.freeze(4); - freezeUI.update(); - - // Click on the label of row 2 - const row2 = container.querySelectorAll('.chain-param-row')[2]; - const label = row2.querySelector('.chain-param-label'); - - const evt = makeClickEvent(); - evt.target = label; - label.dispatchEvent(evt); - - assert.deepStrictEqual(toggleLog, [2], 'onToggle should be called with index 2'); - }); - - it('clicking does nothing when not frozen', () => { - const row0 = container.querySelectorAll('.chain-param-row')[0]; - const label = row0.querySelector('.chain-param-label'); - - const evt = makeClickEvent(); - evt.target = label; - label.dispatchEvent(evt); - - assert.deepStrictEqual(toggleLog, [], 'no toggle when not frozen'); - }); - - it('clicking the slider itself does not toggle', () => { - fm.freeze(4); - freezeUI.update(); - - const row1 = container.querySelectorAll('.chain-param-row')[1]; - const slider = row1.querySelector('.chain-param-slider'); - - const evt = makeClickEvent(); - evt.target = slider; - slider.dispatchEvent(evt); - - assert.deepStrictEqual(toggleLog, [], 'slider click should not trigger toggle'); - }); - - // ── destroy() ───────────────────────────────────────────────────── - - it('destroy() removes the click listener', () => { - fm.freeze(4); - freezeUI.update(); - freezeUI.destroy(); - - const row0 = container.querySelectorAll('.chain-param-row')[0]; - const label = row0.querySelector('.chain-param-label'); - - const evt = makeClickEvent(); - evt.target = label; - label.dispatchEvent(evt); - - assert.deepStrictEqual(toggleLog, [], 'no toggle after destroy'); - }); - - // ── updateDeltaIndicators() ─────────────────────────────────────── - - it('updateDeltaIndicators() creates delta-indicator elements on live rows', () => { - fm.freeze(4); - fm.toggleParam(1); - freezeUI.update(); - - const frozenParams = new Float32Array([0.3, 0.5, 0.7, 0.2]); - const effectiveParams = new Float32Array([0.3, 0.8, 0.7, 0.2]); - - freezeUI.updateDeltaIndicators(frozenParams, effectiveParams); - - // Row 1 is live — should have a delta indicator - const row1 = container.querySelectorAll('.chain-param-row')[1]; - const indicator = row1.querySelector('.delta-indicator'); - assert.ok(indicator, 'live row should have a delta-indicator'); - }); - - it('updateDeltaIndicators() does nothing when not frozen', () => { - const frozenParams = new Float32Array([0.3, 0.5, 0.7, 0.2]); - const effectiveParams = new Float32Array([0.3, 0.8, 0.7, 0.2]); - - freezeUI.updateDeltaIndicators(frozenParams, effectiveParams); - - const indicators = container.querySelectorAll('.delta-indicator'); - assert.strictEqual(indicators.length, 0, 'no indicators when not frozen'); - }); -}); diff --git a/playground/js/shapeseq/tests/freeze.test.js b/playground/js/shapeseq/tests/freeze.test.js deleted file mode 100644 index a3a490d..0000000 --- a/playground/js/shapeseq/tests/freeze.test.js +++ /dev/null @@ -1,293 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { FreezeManager } from '../freeze.js'; -import { Chain } from '../chain.js'; -import { EuclideanRhythm, ProbabilityGate, PitchWalker } from '../primitives.js'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Create a chain with known primitives for testing. - * Returns { chain, paramCount }. - */ -function makeTestChain() { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new ProbabilityGate()); - chain.addPrimitive(new PitchWalker()); - return { chain, paramCount: chain.totalParamCount }; -} - -/** - * Create a Float32Array of params with sequential values for easy identification. - */ -function makeParams(count) { - const params = new Float32Array(count); - for (let i = 0; i < count; i++) { - params[i] = (i + 1) / (count + 1); // spread across (0, 1) - } - return params; -} - -// --------------------------------------------------------------------------- -// freeze() captures params, seeds, states correctly -// --------------------------------------------------------------------------- - -describe('FreezeManager.freeze()', () => { - it('captures params as a Float32Array copy', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - const masterSeed = 42; - - fm.freeze(chain, params, masterSeed); - - const frozen = fm.getFrozenParams(); - assert.ok(frozen instanceof Float32Array, 'should be Float32Array'); - assert.strictEqual(frozen.length, paramCount); - - // Values should match - for (let i = 0; i < paramCount; i++) { - assert.strictEqual(frozen[i], params[i], 'param ' + i + ' should match'); - } - - // Should be a copy, not the same reference - assert.notStrictEqual(frozen, params); - }); - - it('captures per-primitive seeds', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - // Set known seeds - const prims = chain.getPrimitives(); - prims[0].setSeed(100); - prims[1].setSeed(200); - prims[2].setSeed(300); - - fm.freeze(chain, params, 42); - - const seeds = fm.getFrozenSeeds(); - assert.ok(Array.isArray(seeds)); - assert.strictEqual(seeds.length, 3); - assert.strictEqual(seeds[0], 100); - assert.strictEqual(seeds[1], 200); - assert.strictEqual(seeds[2], 300); - }); - - it('captures per-primitive states', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - fm.freeze(chain, params, 42); - - const states = fm.getFrozenStates(); - assert.ok(Array.isArray(states)); - assert.strictEqual(states.length, 3); - }); - - it('captures master seed', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - fm.freeze(chain, params, 12345); - assert.strictEqual(fm.getMasterSeed(), 12345); - }); - - it('initializes all live flags to 0 (frozen)', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const params = makeParams(paramCount); - - fm.freeze(chain, params, 42); - - const flags = fm.getLiveFlags(); - assert.ok(flags instanceof Uint8Array); - assert.strictEqual(flags.length, paramCount); - for (let i = 0; i < paramCount; i++) { - assert.strictEqual(flags[i], 0, 'param ' + i + ' should be frozen'); - } - }); -}); - -// --------------------------------------------------------------------------- -// isFrozen -// --------------------------------------------------------------------------- - -describe('FreezeManager.isFrozen', () => { - it('returns false initially', () => { - const fm = new FreezeManager(); - assert.strictEqual(fm.isFrozen, false); - }); - - it('returns true after freeze()', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - assert.strictEqual(fm.isFrozen, true); - }); -}); - -// --------------------------------------------------------------------------- -// toggleParam -// --------------------------------------------------------------------------- - -describe('FreezeManager.toggleParam()', () => { - it('flips a param from frozen to live and back', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - - assert.strictEqual(fm.getLiveFlags()[0], 0); - - fm.toggleParam(0); - assert.strictEqual(fm.getLiveFlags()[0], 1); - - fm.toggleParam(0); - assert.strictEqual(fm.getLiveFlags()[0], 0); - }); - - it('only affects the targeted param', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - - fm.toggleParam(2); - const flags = fm.getLiveFlags(); - assert.strictEqual(flags[0], 0); - assert.strictEqual(flags[1], 0); - assert.strictEqual(flags[2], 1); - assert.strictEqual(flags[3], 0); - }); - - it('throws when not frozen', () => { - const fm = new FreezeManager(); - assert.throws(() => fm.toggleParam(0), /cannot toggle when not frozen/); - }); - - it('throws on out-of-range index', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - assert.throws(() => fm.toggleParam(paramCount + 10), /out of range/); - }); -}); - -// --------------------------------------------------------------------------- -// setParamLive -// --------------------------------------------------------------------------- - -describe('FreezeManager.setParamLive()', () => { - it('explicitly sets live state', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - - fm.setParamLive(1, true); - assert.strictEqual(fm.getLiveFlags()[1], 1); - - fm.setParamLive(1, false); - assert.strictEqual(fm.getLiveFlags()[1], 0); - }); - - it('throws when not frozen', () => { - const fm = new FreezeManager(); - assert.throws(() => fm.setParamLive(0, true), /cannot set when not frozen/); - }); -}); - -// --------------------------------------------------------------------------- -// getEffectiveParams -// --------------------------------------------------------------------------- - -describe('FreezeManager.getEffectiveParams()', () => { - it('returns frozen values for frozen params, current for live', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - const frozenParams = makeParams(paramCount); - - fm.freeze(chain, frozenParams, 42); - - // Make param 2 live - fm.toggleParam(2); - - // Create different "current" ML params - const currentParams = new Float32Array(paramCount); - for (let i = 0; i < paramCount; i++) { - currentParams[i] = 0.99; - } - - const effective = fm.getEffectiveParams(currentParams); - assert.ok(effective instanceof Float32Array); - assert.strictEqual(effective.length, paramCount); - - // Param 0: frozen -> uses frozen value - assert.strictEqual(effective[0], frozenParams[0]); - // Param 1: frozen -> uses frozen value - assert.strictEqual(effective[1], frozenParams[1]); - // Param 2: live -> uses current ML value (Float32 precision) - assert.ok(Math.abs(effective[2] - 0.99) < 1e-5, 'live param should use current ML value'); - // Param 3: frozen -> uses frozen value - assert.strictEqual(effective[3], frozenParams[3]); - }); - - it('returns null when not frozen', () => { - const fm = new FreezeManager(); - const result = fm.getEffectiveParams(new Float32Array(4)); - assert.strictEqual(result, null); - }); -}); - -// --------------------------------------------------------------------------- -// unfreeze -// --------------------------------------------------------------------------- - -describe('FreezeManager.unfreeze()', () => { - it('clears all captured state', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - - assert.strictEqual(fm.isFrozen, true); - - fm.unfreeze(); - - assert.strictEqual(fm.isFrozen, false); - assert.strictEqual(fm.getFrozenParams(), null); - assert.strictEqual(fm.getLiveFlags(), null); - assert.strictEqual(fm.getFrozenSeeds(), null); - assert.strictEqual(fm.getFrozenStates(), null); - assert.strictEqual(fm.getMasterSeed(), null); - }); -}); - -// --------------------------------------------------------------------------- -// shouldSuppressReEval -// --------------------------------------------------------------------------- - -describe('FreezeManager.shouldSuppressReEval()', () => { - it('returns true when frozen', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - assert.strictEqual(fm.shouldSuppressReEval(), true); - }); - - it('returns false when not frozen', () => { - const fm = new FreezeManager(); - assert.strictEqual(fm.shouldSuppressReEval(), false); - }); - - it('returns false after unfreeze', () => { - const fm = new FreezeManager(); - const { chain, paramCount } = makeTestChain(); - fm.freeze(chain, makeParams(paramCount), 42); - fm.unfreeze(); - assert.strictEqual(fm.shouldSuppressReEval(), false); - }); -}); diff --git a/playground/js/shapeseq/tests/midi-note.test.js b/playground/js/shapeseq/tests/midi-note.test.js deleted file mode 100644 index 5218a9c..0000000 --- a/playground/js/shapeseq/tests/midi-note.test.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Tests for the midiNote field on pattern step data structures. - * - * Covers: createStep, clonePattern, setStep, validatePattern, - * mergePatterns, and IntervalLock.process() behavior. - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { - createStep, createPattern, clonePattern, - mergePatterns, setStep, validatePattern, -} from '../pattern.js'; - -import { IntervalLock } from '../primitives.js'; -import { createPRNG } from '../prng.js'; - -// ── createStep ────────────────────────────────────────────────────── - -describe('createStep – midiNote', () => { - it('has midiNote: null by default', () => { - const step = createStep(); - assert.strictEqual(step.midiNote, null); - }); -}); - -// ── clonePattern ──────────────────────────────────────────────────── - -describe('clonePattern – midiNote', () => { - it('preserves midiNote across clone', () => { - const p = createPattern(2); - p.steps[0].midiNote = 60; - p.steps[1].midiNote = null; - - const c = clonePattern(p); - assert.strictEqual(c.steps[0].midiNote, 60); - assert.strictEqual(c.steps[1].midiNote, null); - }); - - it('clone is independent (mutation does not propagate)', () => { - const p = createPattern(1); - p.steps[0].midiNote = 72; - const c = clonePattern(p); - c.steps[0].midiNote = 48; - assert.strictEqual(p.steps[0].midiNote, 72); - }); -}); - -// ── setStep ───────────────────────────────────────────────────────── - -describe('setStep – midiNote', () => { - it('can set midiNote to an integer', () => { - const p = createPattern(1); - setStep(p, 0, { midiNote: 64 }); - assert.strictEqual(p.steps[0].midiNote, 64); - }); - - it('can set midiNote to null', () => { - const p = createPattern(1); - p.steps[0].midiNote = 60; - setStep(p, 0, { midiNote: null }); - assert.strictEqual(p.steps[0].midiNote, null); - }); - - it('coerces midiNote to integer', () => { - const p = createPattern(1); - setStep(p, 0, { midiNote: 60.9 }); - assert.strictEqual(p.steps[0].midiNote, 60); - }); - - it('does not touch midiNote when not in stepData', () => { - const p = createPattern(1); - p.steps[0].midiNote = 72; - setStep(p, 0, { pitch: 0.3 }); - assert.strictEqual(p.steps[0].midiNote, 72); - }); -}); - -// ── validatePattern ───────────────────────────────────────────────── - -describe('validatePattern – midiNote', () => { - it('accepts midiNote: null', () => { - const p = createPattern(1); - assert.strictEqual(validatePattern(p), true); - }); - - it('accepts midiNote: 0', () => { - const p = createPattern(1); - p.steps[0].midiNote = 0; - assert.strictEqual(validatePattern(p), true); - }); - - it('accepts midiNote: 127', () => { - const p = createPattern(1); - p.steps[0].midiNote = 127; - assert.strictEqual(validatePattern(p), true); - }); - - it('accepts midiNote: 60 (middle C)', () => { - const p = createPattern(1); - p.steps[0].midiNote = 60; - assert.strictEqual(validatePattern(p), true); - }); - - it('rejects midiNote: float (60.5)', () => { - const p = createPattern(1); - p.steps[0].midiNote = 60.5; - assert.strictEqual(validatePattern(p), false); - }); - - it('rejects midiNote: -1', () => { - const p = createPattern(1); - p.steps[0].midiNote = -1; - assert.strictEqual(validatePattern(p), false); - }); - - it('rejects midiNote: 128', () => { - const p = createPattern(1); - p.steps[0].midiNote = 128; - assert.strictEqual(validatePattern(p), false); - }); - - it('rejects midiNote: string', () => { - const p = createPattern(1); - p.steps[0].midiNote = 'C4'; - assert.strictEqual(validatePattern(p), false); - }); -}); - -// ── mergePatterns ─────────────────────────────────────────────────── - -describe('mergePatterns – midiNote', () => { - it('both triggered, both have midiNote → average rounded', () => { - const a = createPattern(1); - setStep(a, 0, { trigger: true, midiNote: 60 }); - const b = createPattern(1); - setStep(b, 0, { trigger: true, midiNote: 65 }); - - const m = mergePatterns(a, b, 'additive'); - // (60 + 65) / 2 = 62.5 → 63 - assert.strictEqual(m.steps[0].midiNote, 63); - }); - - it('both triggered, only A has midiNote → use A', () => { - const a = createPattern(1); - setStep(a, 0, { trigger: true, midiNote: 72 }); - const b = createPattern(1); - setStep(b, 0, { trigger: true }); - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, 72); - }); - - it('both triggered, only B has midiNote → use B', () => { - const a = createPattern(1); - setStep(a, 0, { trigger: true }); - const b = createPattern(1); - setStep(b, 0, { trigger: true, midiNote: 48 }); - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, 48); - }); - - it('both triggered, neither has midiNote → null', () => { - const a = createPattern(1); - setStep(a, 0, { trigger: true }); - const b = createPattern(1); - setStep(b, 0, { trigger: true }); - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, null); - }); - - it('only A triggered → uses A midiNote', () => { - const a = createPattern(1); - setStep(a, 0, { trigger: true, midiNote: 55 }); - const b = createPattern(1); - // b not triggered (default) - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, 55); - }); - - it('only B triggered → uses B midiNote', () => { - const a = createPattern(1); - // a not triggered - const b = createPattern(1); - setStep(b, 0, { trigger: true, midiNote: 80 }); - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, 80); - }); - - it('neither triggered → null', () => { - const a = createPattern(1); - const b = createPattern(1); - - const m = mergePatterns(a, b, 'additive'); - assert.strictEqual(m.steps[0].midiNote, null); - }); -}); - -// ── IntervalLock.process() ────────────────────────────────────────── - -describe('IntervalLock – midiNote field', () => { - it('sets midiNote as integer and leaves pitch as [0,1]', () => { - const lock = new IntervalLock(); - const rng = createPRNG(42); - - // Create a pattern with a triggered step at pitch 0.5 - const input = createPattern(1); - setStep(input, 0, { trigger: true, pitch: 0.5 }); - - // root=0 (C), mode=0.1 (major), octaveRange=0.25 (1 octave) - const result = lock.process([0.0, 0.1, 0.25], input, {}, rng); - const step = result.patternDesc.steps[0]; - - // midiNote should be an integer (a MIDI note from the C major scale) - assert.strictEqual(typeof step.midiNote, 'number'); - assert.strictEqual(step.midiNote, step.midiNote | 0, 'midiNote should be integer'); - assert.ok(step.midiNote >= 0 && step.midiNote <= 127, 'midiNote in MIDI range'); - - // pitch should remain a [0,1] float (the original pre-quantization value) - assert.strictEqual(step.pitch, 0.5, 'pitch should be unchanged from input'); - }); - - it('produces different midiNote values for different pitch inputs', () => { - const lock = new IntervalLock(); - const rng = createPRNG(42); - - const input = createPattern(2); - setStep(input, 0, { trigger: true, pitch: 0.0 }); - setStep(input, 1, { trigger: true, pitch: 1.0 }); - - const result = lock.process([0.0, 0.1, 0.5], input, {}, rng); - const note0 = result.patternDesc.steps[0].midiNote; - const note1 = result.patternDesc.steps[1].midiNote; - - assert.notStrictEqual(note0, note1, 'different pitches should produce different MIDI notes'); - assert.ok(note0 < note1, 'higher pitch should produce higher MIDI note'); - }); -}); diff --git a/playground/js/shapeseq/tests/mlp-mode.test.js b/playground/js/shapeseq/tests/mlp-mode.test.js deleted file mode 100644 index b7fa7b1..0000000 --- a/playground/js/shapeseq/tests/mlp-mode.test.js +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Tests for mlp-mode.js — MLPModeManager and MLP_MODES. - * - * Covers mode switching, output slicing, and configuration. - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { MLPModeManager, MLP_MODES } from '../mlp-mode.js'; - -/** Approximate equality for Float32 values. */ -function assertClose(actual, expected, eps = 1e-6) { - assert.ok( - Math.abs(actual - expected) < eps, - `Expected ${actual} to be close to ${expected} (eps=${eps})` - ); -} - -// ── MLP_MODES constant ───────────────────────────────────────────── - -describe('MLP_MODES', () => { - it('has UNIFIED and DUAL values', () => { - assert.equal(MLP_MODES.UNIFIED, 'unified'); - assert.equal(MLP_MODES.DUAL, 'dual'); - }); - - it('is frozen', () => { - assert.throws(() => { MLP_MODES.FOO = 'bar'; }, TypeError); - }); -}); - -// ── MLPModeManager ────────────────────────────────────────────────── - -describe('MLPModeManager', () => { - it('defaults to dual mode', () => { - const mgr = new MLPModeManager(); - assert.equal(mgr.mode, MLP_MODES.DUAL); - }); - - it('setMode switches to unified', () => { - const mgr = new MLPModeManager(); - mgr.setMode(MLP_MODES.UNIFIED); - assert.equal(mgr.mode, MLP_MODES.UNIFIED); - }); - - it('setMode switches back to dual', () => { - const mgr = new MLPModeManager(); - mgr.setMode(MLP_MODES.UNIFIED); - mgr.setMode(MLP_MODES.DUAL); - assert.equal(mgr.mode, MLP_MODES.DUAL); - }); - - it('setMode throws on invalid mode', () => { - const mgr = new MLPModeManager(); - assert.throws(() => mgr.setMode('invalid'), TypeError); - assert.throws(() => mgr.setMode(''), TypeError); - assert.throws(() => mgr.setMode(null), TypeError); - assert.throws(() => mgr.setMode(undefined), TypeError); - }); - - it('setUnifiedConfig changes the slice window', () => { - const mgr = new MLPModeManager(); - assert.equal(mgr.unifiedSliceStart, 0); - assert.equal(mgr.unifiedSliceCount, 16); - - mgr.setUnifiedConfig(64, 32); - assert.equal(mgr.unifiedSliceStart, 64); - assert.equal(mgr.unifiedSliceCount, 32); - }); -}); - -// ── extractSequenceOutputs ────────────────────────────────────────── - -describe('extractSequenceOutputs', () => { - it('slices the correct range from timbre outputs', () => { - const mgr = new MLPModeManager(); - mgr.setUnifiedConfig(2, 4); - - const timbre = new Float32Array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]); - const result = mgr.extractSequenceOutputs(timbre); - - assert.equal(result.length, 4); - assertClose(result[0], 0.3); // index 2 - assertClose(result[1], 0.4); // index 3 - assertClose(result[2], 0.5); // index 4 - assertClose(result[3], 0.6); // index 5 - }); - - it('uses default slice (start=0, count=16)', () => { - const mgr = new MLPModeManager(); - const timbre = new Float32Array(126); - for (let i = 0; i < 126; i++) timbre[i] = i / 126; - - const result = mgr.extractSequenceOutputs(timbre); - assert.equal(result.length, 16); - assert.equal(result[0], timbre[0]); - assert.equal(result[15], timbre[15]); - }); - - it('fills with 0.5 when slice extends beyond timbre outputs', () => { - const mgr = new MLPModeManager(); - mgr.setUnifiedConfig(5, 4); - - // Only 6 elements — slice starts at 5, needs 4 (indices 5,6,7,8) - const timbre = new Float32Array([0.1, 0.2, 0.3, 0.4, 0.5, 0.9]); - const result = mgr.extractSequenceOutputs(timbre); - - assert.equal(result.length, 4); - assertClose(result[0], 0.9); // index 5 — last valid - assert.equal(result[1], 0.5); // index 6 — out of bounds - assert.equal(result[2], 0.5); // index 7 — out of bounds - assert.equal(result[3], 0.5); // index 8 — out of bounds - }); - - it('fills entirely with 0.5 when start is beyond array length', () => { - const mgr = new MLPModeManager(); - mgr.setUnifiedConfig(100, 3); - - const timbre = new Float32Array([0.1, 0.2, 0.3]); - const result = mgr.extractSequenceOutputs(timbre); - - assert.equal(result.length, 3); - assert.equal(result[0], 0.5); - assert.equal(result[1], 0.5); - assert.equal(result[2], 0.5); - }); - - it('returns Float32Array', () => { - const mgr = new MLPModeManager(); - const timbre = new Float32Array(20); - const result = mgr.extractSequenceOutputs(timbre); - assert.ok(result instanceof Float32Array); - }); - - it('handles empty timbre array', () => { - const mgr = new MLPModeManager(); - mgr.setUnifiedConfig(0, 4); - - const timbre = new Float32Array(0); - const result = mgr.extractSequenceOutputs(timbre); - - assert.equal(result.length, 4); - for (let i = 0; i < 4; i++) { - assert.equal(result[i], 0.5); - } - }); -}); diff --git a/playground/js/shapeseq/tests/pattern-merge.test.js b/playground/js/shapeseq/tests/pattern-merge.test.js deleted file mode 100644 index e92e27d..0000000 --- a/playground/js/shapeseq/tests/pattern-merge.test.js +++ /dev/null @@ -1,229 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { createPattern, setStep, mergePatterns } from '../pattern.js'; - -/** - * Helper: build a 4-step pattern and configure specific steps. - * stepConfigs is an array of { index, ...stepData } objects. - */ -function buildPattern(stepConfigs) { - const p = createPattern(4); - for (const { index, ...data } of stepConfigs) { - setStep(p, index, data); - } - return p; -} - -// --------------------------------------------------------------------------- -// Additive mode -// --------------------------------------------------------------------------- - -describe('mergePatterns — additive mode', () => { - it('both trigger: averages pitch, velocity, timeOffset; ORs accent; max subdivisions', () => { - const a = buildPattern([ - { index: 0, trigger: true, pitch: 0.8, velocity: 0.6, accent: true, timeOffset: 0.1, subdivisions: 2 }, - ]); - const b = buildPattern([ - { index: 0, trigger: true, pitch: 0.4, velocity: 1.0, accent: false, timeOffset: -0.1, subdivisions: 3 }, - ]); - - const merged = mergePatterns(a, b, 'additive'); - const s = merged.steps[0]; - - assert.equal(s.trigger, true); - assert.ok(Math.abs(s.pitch - 0.6) < 1e-9, `pitch should be 0.6, got ${s.pitch}`); - assert.ok(Math.abs(s.velocity - 0.8) < 1e-9, `velocity should be 0.8, got ${s.velocity}`); - assert.equal(s.accent, true); // OR - assert.ok(Math.abs(s.timeOffset - 0.0) < 1e-9, `timeOffset should be 0.0, got ${s.timeOffset}`); - assert.equal(s.subdivisions, 3); // max - }); - - it('only A triggers: uses A values directly', () => { - const a = buildPattern([ - { index: 1, trigger: true, pitch: 0.9, velocity: 0.3, accent: true, timeOffset: 0.2, subdivisions: 2 }, - ]); - const b = buildPattern([]); // step 1 stays default (trigger: false) - - const merged = mergePatterns(a, b, 'additive'); - const s = merged.steps[1]; - - assert.equal(s.trigger, true); - assert.equal(s.pitch, 0.9); - assert.equal(s.velocity, 0.3); - assert.equal(s.accent, true); - assert.equal(s.timeOffset, 0.2); - assert.equal(s.subdivisions, 2); - }); - - it('only B triggers: uses B values directly', () => { - const a = buildPattern([]); // step 2 stays default (trigger: false) - const b = buildPattern([ - { index: 2, trigger: true, pitch: 0.1, velocity: 0.95, accent: false, timeOffset: -0.3, subdivisions: 4 }, - ]); - - const merged = mergePatterns(a, b, 'additive'); - const s = merged.steps[2]; - - assert.equal(s.trigger, true); - assert.equal(s.pitch, 0.1); - assert.equal(s.velocity, 0.95); - assert.equal(s.accent, false); - assert.equal(s.timeOffset, -0.3); - assert.equal(s.subdivisions, 4); - }); - - it('neither triggers: step stays untriggered with defaults', () => { - const a = buildPattern([]); - const b = buildPattern([]); - - const merged = mergePatterns(a, b, 'additive'); - const s = merged.steps[0]; - - assert.equal(s.trigger, false); - assert.equal(s.pitch, 0.5); // DEFAULT_PITCH - assert.equal(s.velocity, 0.7); // DEFAULT_VELOCITY - assert.equal(s.accent, false); // DEFAULT_ACCENT - assert.equal(s.timeOffset, 0.0); // DEFAULT_TIME_OFFSET - assert.equal(s.subdivisions, 1); // DEFAULT_SUBDIVISIONS - }); - - it('timeOffset averaged when both trigger', () => { - const a = buildPattern([{ index: 0, trigger: true, timeOffset: 0.4 }]); - const b = buildPattern([{ index: 0, trigger: true, timeOffset: -0.2 }]); - - const merged = mergePatterns(a, b, 'additive'); - assert.ok(Math.abs(merged.steps[0].timeOffset - 0.1) < 1e-9); - }); - - it('subdivisions: max when both trigger, sole value when one triggers', () => { - const a = buildPattern([ - { index: 0, trigger: true, subdivisions: 1 }, - { index: 1, trigger: true, subdivisions: 3 }, - ]); - const b = buildPattern([ - { index: 0, trigger: true, subdivisions: 4 }, - // step 1: not triggered - ]); - - const merged = mergePatterns(a, b, 'additive'); - assert.equal(merged.steps[0].subdivisions, 4); // max(1, 4) - assert.equal(merged.steps[1].subdivisions, 3); // only A triggered - }); -}); - -// --------------------------------------------------------------------------- -// Multiplicative mode -// --------------------------------------------------------------------------- - -describe('mergePatterns — multiplicative mode', () => { - it('both trigger: triggers, averages values, ANDs accent', () => { - const a = buildPattern([ - { index: 0, trigger: true, pitch: 0.8, velocity: 0.6, accent: true, timeOffset: 0.1, subdivisions: 2 }, - ]); - const b = buildPattern([ - { index: 0, trigger: true, pitch: 0.4, velocity: 1.0, accent: false, timeOffset: -0.1, subdivisions: 3 }, - ]); - - const merged = mergePatterns(a, b, 'multiplicative'); - const s = merged.steps[0]; - - assert.equal(s.trigger, true); - assert.ok(Math.abs(s.pitch - 0.6) < 1e-9); - assert.ok(Math.abs(s.velocity - 0.8) < 1e-9); - assert.equal(s.accent, false); // AND: true && false - assert.ok(Math.abs(s.timeOffset - 0.0) < 1e-9); - assert.equal(s.subdivisions, 3); - }); - - it('only A triggers: no trigger in multiplicative (AND)', () => { - const a = buildPattern([ - { index: 0, trigger: true, pitch: 0.9, velocity: 0.3, accent: true }, - ]); - const b = buildPattern([]); - - const merged = mergePatterns(a, b, 'multiplicative'); - const s = merged.steps[0]; - - // AND: true && false = false, so step not triggered - assert.equal(s.trigger, false); - // Since only A triggered (but result is untriggered due to AND), - // values come from the A-only branch - assert.equal(s.pitch, 0.9); - assert.equal(s.velocity, 0.3); - }); - - it('only B triggers: no trigger in multiplicative (AND)', () => { - const a = buildPattern([]); - const b = buildPattern([ - { index: 0, trigger: true, pitch: 0.1, velocity: 0.95, accent: false, timeOffset: -0.3, subdivisions: 4 }, - ]); - - const merged = mergePatterns(a, b, 'multiplicative'); - const s = merged.steps[0]; - - assert.equal(s.trigger, false); - // Values from the B-only branch - assert.equal(s.pitch, 0.1); - assert.equal(s.velocity, 0.95); - }); - - it('neither triggers: untriggered with defaults', () => { - const a = buildPattern([]); - const b = buildPattern([]); - - const merged = mergePatterns(a, b, 'multiplicative'); - const s = merged.steps[0]; - - assert.equal(s.trigger, false); - assert.equal(s.pitch, 0.5); - assert.equal(s.velocity, 0.7); - assert.equal(s.accent, false); - assert.equal(s.timeOffset, 0.0); - assert.equal(s.subdivisions, 1); - }); - - it('timeOffset from sole triggering generator in multiplicative', () => { - const a = buildPattern([{ index: 0, trigger: true, timeOffset: 0.4 }]); - const b = buildPattern([]); - - const merged = mergePatterns(a, b, 'multiplicative'); - assert.equal(merged.steps[0].timeOffset, 0.4); - }); - - it('subdivisions from sole triggering generator in multiplicative', () => { - const a = buildPattern([]); - const b = buildPattern([{ index: 0, trigger: true, subdivisions: 3 }]); - - const merged = mergePatterns(a, b, 'multiplicative'); - assert.equal(merged.steps[0].subdivisions, 3); - }); -}); - -// --------------------------------------------------------------------------- -// Edge cases -// --------------------------------------------------------------------------- - -describe('mergePatterns — edge cases', () => { - it('tiles mismatched step counts to LCM instead of rejecting', () => { - const a = createPattern(4); - const b = createPattern(8); - const merged = mergePatterns(a, b, 'additive'); - assert.equal(merged.stepCount, 8); // lcm(4, 8) = 8 - }); - - it('rejects invalid mode', () => { - const a = createPattern(4); - const b = createPattern(4); - assert.throws(() => mergePatterns(a, b, 'xor'), TypeError); - }); - - it('does not mutate input patterns', () => { - const a = buildPattern([{ index: 0, trigger: true, pitch: 0.2 }]); - const b = buildPattern([{ index: 0, trigger: true, pitch: 0.8 }]); - - mergePatterns(a, b, 'additive'); - - assert.equal(a.steps[0].pitch, 0.2); - assert.equal(b.steps[0].pitch, 0.8); - }); -}); diff --git a/playground/js/shapeseq/tests/pitch-walker.test.js b/playground/js/shapeseq/tests/pitch-walker.test.js deleted file mode 100644 index 76e4ecd..0000000 --- a/playground/js/shapeseq/tests/pitch-walker.test.js +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Tests for PitchWalker reclassified as a processor. - * - * Verifies that PitchWalker: - * - Has category 'processor' - * - Preserves upstream trigger patterns - * - Preserves upstream step data (velocity, accent, timeOffset, subdivisions) - * - Only modifies pitch on triggered steps - * - Leaves untriggered steps completely unchanged - * - Advances position state - * - Integrates correctly in a Chain with a generator upstream - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { PitchWalker, EuclideanRhythm } from '../primitives.js'; -import { createPattern, setStep } from '../pattern.js'; -import { createPRNG } from '../prng.js'; -import { Chain } from '../chain.js'; - -// Helper: build a pattern with specific triggers and custom step data -function makeTestPattern(stepCount, triggerIndices, stepOverrides) { - const pattern = createPattern(stepCount); - for (const idx of triggerIndices) { - setStep(pattern, idx, { trigger: true }); - } - if (stepOverrides) { - for (const [idx, data] of Object.entries(stepOverrides)) { - setStep(pattern, Number(idx), data); - } - } - return pattern; -} - -describe('PitchWalker', () => { - it('has category "processor"', () => { - const pw = new PitchWalker(); - assert.equal(pw.category, 'processor'); - }); - - it('preserves upstream trigger pattern', () => { - const pw = new PitchWalker(); - const rng = createPRNG(42); - const triggers = [0, 2, 5, 7]; - const input = makeTestPattern(8, triggers); - - const { patternDesc } = pw.process( - new Float32Array([0.3, 0.5, 0.3, 0.8]), - input, {}, rng - ); - - for (let i = 0; i < 8; i++) { - const expected = triggers.includes(i); - assert.equal(patternDesc.steps[i].trigger, expected, - `step ${i} trigger should be ${expected}`); - } - }); - - it('preserves upstream velocity, accent, timeOffset, subdivisions on triggered steps', () => { - const pw = new PitchWalker(); - const rng = createPRNG(99); - const input = makeTestPattern(4, [1, 3], { - 1: { trigger: true, velocity: 0.9, accent: true, timeOffset: 0.1, subdivisions: 2 }, - 3: { trigger: true, velocity: 0.4, accent: false, timeOffset: -0.2, subdivisions: 3 }, - }); - - const { patternDesc } = pw.process( - new Float32Array([0.3, 0.5, 0.3, 0.8]), - input, {}, rng - ); - - // Step 1 - assert.equal(patternDesc.steps[1].velocity, 0.9); - assert.equal(patternDesc.steps[1].accent, true); - assert.equal(patternDesc.steps[1].timeOffset, 0.1); - assert.equal(patternDesc.steps[1].subdivisions, 2); - - // Step 3 - assert.equal(patternDesc.steps[3].velocity, 0.4); - assert.equal(patternDesc.steps[3].accent, false); - assert.equal(patternDesc.steps[3].timeOffset, -0.2); - assert.equal(patternDesc.steps[3].subdivisions, 3); - }); - - it('modifies pitch on triggered steps only', () => { - const pw = new PitchWalker(); - const rng = createPRNG(7); - const input = makeTestPattern(8, [0, 3, 6]); - - // Record original pitches - const origPitches = input.steps.map(s => s.pitch); - - const { patternDesc } = pw.process( - new Float32Array([0.5, 0.5, 0.5, 1.0]), - input, {}, rng - ); - - // Triggered steps should have pitch set by random walk (may differ from default 0.5) - // We just verify they are valid numbers in [0,1] - for (const idx of [0, 3, 6]) { - const p = patternDesc.steps[idx].pitch; - assert.equal(typeof p, 'number'); - assert.ok(p >= 0 && p <= 1, `pitch at step ${idx} should be in [0,1], got ${p}`); - } - }); - - it('does NOT modify untriggered steps at all', () => { - const pw = new PitchWalker(); - const rng = createPRNG(123); - const input = makeTestPattern(6, [1, 4], { - 0: { velocity: 0.3, accent: true, timeOffset: 0.15 }, - 2: { velocity: 0.8, accent: false, timeOffset: -0.1 }, - }); - - // Snapshot untriggered steps before - const untriggeredBefore = {}; - for (let i = 0; i < 6; i++) { - if (![1, 4].includes(i)) { - const s = input.steps[i]; - untriggeredBefore[i] = { - trigger: s.trigger, - pitch: s.pitch, - velocity: s.velocity, - accent: s.accent, - timeOffset: s.timeOffset, - subdivisions: s.subdivisions, - }; - } - } - - const { patternDesc } = pw.process( - new Float32Array([0.3, 0.5, 0.3, 0.8]), - input, {}, rng - ); - - for (const [idx, before] of Object.entries(untriggeredBefore)) { - const after = patternDesc.steps[Number(idx)]; - assert.equal(after.trigger, before.trigger, `step ${idx} trigger unchanged`); - assert.equal(after.pitch, before.pitch, `step ${idx} pitch unchanged`); - assert.equal(after.velocity, before.velocity, `step ${idx} velocity unchanged`); - assert.equal(after.accent, before.accent, `step ${idx} accent unchanged`); - assert.equal(after.timeOffset, before.timeOffset, `step ${idx} timeOffset unchanged`); - assert.equal(after.subdivisions, before.subdivisions, `step ${idx} subdivisions unchanged`); - } - }); - - it('advances the position state', () => { - const pw = new PitchWalker(); - const rng = createPRNG(55); - const input = makeTestPattern(8, [0, 1, 2, 3, 4, 5, 6, 7]); - - const initialPosition = 0.5; - const { nextState } = pw.process( - new Float32Array([0.5, 0.5, 0.3, 1.0]), - input, { position: initialPosition }, rng - ); - - assert.equal(typeof nextState.position, 'number'); - // With 8 triggered steps and nonzero stepSize/range, position should move - assert.notEqual(nextState.position, initialPosition, - 'position should change after processing triggered steps'); - }); - - it('does not mutate the input pattern', () => { - const pw = new PitchWalker(); - const rng = createPRNG(42); - const input = makeTestPattern(4, [0, 2]); - const origPitch0 = input.steps[0].pitch; - const origPitch1 = input.steps[1].pitch; - - pw.process(new Float32Array([0.3, 0.5, 0.3, 0.8]), input, {}, rng); - - // clonePattern should prevent mutation of the original - assert.equal(input.steps[0].pitch, origPitch0); - assert.equal(input.steps[1].pitch, origPitch1); - }); -}); - -describe('PitchWalker integration with Chain', () => { - it('runs after generators and receives their trigger pattern', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new PitchWalker()); - - // EuclideanRhythm params: steps=0.5, pulses=0.5, rotation=0.0 - // PitchWalker params: stepSize=0.3, directionBias=0.5, gravity=0.3, range=0.8 - const params = new Float32Array([0.5, 0.5, 0.0, 0.3, 0.5, 0.3, 0.8]); - const stepCount = 8; - const result = chain.evaluate(params, stepCount, 42); - - // The result should have stepCount steps - assert.equal(result.stepCount, stepCount); - - // Count triggers — EuclideanRhythm with steps=0.5 (maps to ~5) and pulses=0.5 - // should produce some triggers - const triggeredSteps = result.steps.filter(s => s.trigger); - assert.ok(triggeredSteps.length > 0, - 'should have at least one triggered step from EuclideanRhythm'); - - // Triggered steps should have pitch values set by PitchWalker (not default 0.5) - // With enough steps, at least some should differ from 0.5 - const pitches = triggeredSteps.map(s => s.pitch); - const allDefault = pitches.every(p => p === 0.5); - assert.ok(!allDefault || pitches.length <= 1, - 'PitchWalker should have modified pitch on triggered steps'); - - // Untriggered steps should retain default pitch - const untriggeredSteps = result.steps.filter(s => !s.trigger); - for (const s of untriggeredSteps) { - assert.equal(s.pitch, 0.5, 'untriggered steps should have default pitch'); - } - }); -}); diff --git a/playground/js/shapeseq/tests/polyrhythm-chain.test.js b/playground/js/shapeseq/tests/polyrhythm-chain.test.js deleted file mode 100644 index 5fda5e3..0000000 --- a/playground/js/shapeseq/tests/polyrhythm-chain.test.js +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; - -import { Chain } from '../chain.js'; -import { EuclideanRhythm, DensityMorph, VelocityShaper } from '../primitives.js'; -import { lcm } from '../pattern.js'; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Build a chain with two EuclideanRhythm generators. - * Returns { chain, gen0Index, gen1Index }. - */ -function twoGeneratorChain() { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); // index 0 - chain.addPrimitive(new EuclideanRhythm()); // index 1 - return { chain, gen0Index: 0, gen1Index: 1 }; -} - -/** - * Count triggered steps in a pattern. - */ -function countTriggers(pattern) { - let count = 0; - for (let i = 0; i < pattern.stepCount; i++) { - if (pattern.steps[i].trigger) count++; - } - return count; -} - -// --------------------------------------------------------------------------- -// Default behavior: all generators use global stepCount (backward compat) -// --------------------------------------------------------------------------- - -describe('polyrhythm chain — backward compatibility', () => { - it('without per-generator step counts, all generators use global stepCount', () => { - const { chain } = twoGeneratorChain(); - - // Both generators: steps=1.0 (max), pulses=0.5, rotation=0 - // With stepCount=8, steps param maps to 8, pulses maps to ~4 - const params = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0]); - const pattern = chain.evaluate(params, 8, 42); - - // Both use the same stepCount, so merged pattern should be 8 steps - assert.equal(pattern.stepCount, 8); - }); - - it('clearGeneratorStepCounts resets to default behavior', () => { - const { chain, gen0Index } = twoGeneratorChain(); - - // Set a custom step count - chain.setGeneratorStepCount(gen0Index, 3); - assert.equal(chain.getGeneratorStepCount(gen0Index), 3); - - // Clear - chain.clearGeneratorStepCounts(); - assert.equal(chain.getGeneratorStepCount(gen0Index), null); - - // Evaluate — should use global stepCount for both - const params = new Float32Array([1.0, 0.5, 0.0, 1.0, 0.5, 0.0]); - const pattern = chain.evaluate(params, 8, 42); - assert.equal(pattern.stepCount, 8); - }); -}); - -// --------------------------------------------------------------------------- -// Per-generator step counts -// --------------------------------------------------------------------------- - -describe('polyrhythm chain — per-generator step counts', () => { - it('two generators with different step counts produce LCM-length pattern', () => { - const { chain, gen0Index, gen1Index } = twoGeneratorChain(); - - chain.setGeneratorStepCount(gen0Index, 3); - chain.setGeneratorStepCount(gen1Index, 4); - - // All pulses high so we get triggers - const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]); - const pattern = chain.evaluate(params, 8, 42); - - // LCM(3, 4) = 12 - assert.equal(pattern.stepCount, lcm(3, 4)); - assert.equal(pattern.stepCount, 12); - }); - - it('triggers from both generators are present in merged output', () => { - const { chain, gen0Index, gen1Index } = twoGeneratorChain(); - - // Gen0: 3 steps with all triggers (steps=1.0 maps to max=3, pulses=1.0 fills all) - chain.setGeneratorStepCount(gen0Index, 3); - // Gen1: 4 steps with all triggers - chain.setGeneratorStepCount(gen1Index, 4); - - const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]); - const pattern = chain.evaluate(params, 8, 42); - - assert.equal(pattern.stepCount, 12); - - // With all pulses, both generators trigger every step in their respective - // cycle lengths. Tiled to 12: - // Gen0 (3-step cycle, all on): every step has a trigger - // Gen1 (4-step cycle, all on): every step has a trigger - // Additive merge: every step should be triggered - for (let i = 0; i < 12; i++) { - assert.equal(pattern.steps[i].trigger, true, `step ${i} should be triggered`); - } - }); - - it('only one generator with custom step count, other uses global', () => { - const { chain, gen0Index } = twoGeneratorChain(); - - // Gen0 gets 3 steps, Gen1 uses global (4) - chain.setGeneratorStepCount(gen0Index, 3); - - const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]); - const pattern = chain.evaluate(params, 4, 42); - - // LCM(3, 4) = 12 - assert.equal(pattern.stepCount, 12); - }); - - it('generatorStepCounts passed as evaluate() argument overrides instance map', () => { - const { chain, gen0Index, gen1Index } = twoGeneratorChain(); - - // Instance map says 3 and 4 - chain.setGeneratorStepCount(gen0Index, 3); - chain.setGeneratorStepCount(gen1Index, 4); - - // But explicit arg says 5 and 7 - const explicitMap = new Map([[gen0Index, 5], [gen1Index, 7]]); - const params = new Float32Array([1.0, 1.0, 0.0, 1.0, 1.0, 0.0]); - const pattern = chain.evaluate(params, 8, 42, explicitMap); - - // LCM(5, 7) = 35 - assert.equal(pattern.stepCount, lcm(5, 7)); - assert.equal(pattern.stepCount, 35); - }); - - it('getGeneratorStepCount returns null for unset generators', () => { - const { chain, gen1Index } = twoGeneratorChain(); - assert.equal(chain.getGeneratorStepCount(gen1Index), null); - }); - - it('setGeneratorStepCount validates chain index', () => { - const { chain } = twoGeneratorChain(); - assert.throws(() => chain.setGeneratorStepCount(99, 4), { name: 'RangeError' }); - assert.throws(() => chain.setGeneratorStepCount(-1, 4), { name: 'RangeError' }); - }); - - it('setGeneratorStepCount validates step count', () => { - const { chain } = twoGeneratorChain(); - assert.throws(() => chain.setGeneratorStepCount(0, 0), { name: 'RangeError' }); - assert.throws(() => chain.setGeneratorStepCount(0, -1), { name: 'RangeError' }); - }); -}); - -// --------------------------------------------------------------------------- -// Processors run on LCM-length pattern -// --------------------------------------------------------------------------- - -describe('polyrhythm chain — processors on LCM-length pattern', () => { - it('processor receives and outputs the LCM-length pattern', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); // index 0 — generator - chain.addPrimitive(new EuclideanRhythm()); // index 1 — generator - chain.addPrimitive(new VelocityShaper()); // index 2 — processor - - chain.setGeneratorStepCount(0, 3); - chain.setGeneratorStepCount(1, 4); - - // EuclideanRhythm: 3 params each, VelocityShaper: 3 params - // Fill all with moderate values - const params = new Float32Array([ - 1.0, 1.0, 0.0, // gen0: all triggers - 1.0, 1.0, 0.0, // gen1: all triggers - 0.0, 0.5, 0.0, // velocity shaper: flat curve, half depth, no phase - ]); - const pattern = chain.evaluate(params, 8, 42); - - // LCM(3, 4) = 12 — processor should preserve that length - assert.equal(pattern.stepCount, 12); - - // Velocity shaper should have modified velocities (not all default 0.7) - // With flat curve (idx 0) and depth 0.5, velocity = 0.7 * 0.5 + 1.0 * 0.5 = 0.85 - const triggeredSteps = pattern.steps.filter(s => s.trigger); - assert.ok(triggeredSteps.length > 0, 'should have triggered steps'); - }); -}); - -// --------------------------------------------------------------------------- -// Single generator with per-generator step count -// --------------------------------------------------------------------------- - -describe('polyrhythm chain — single generator', () => { - it('single generator uses its per-generator step count', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); // index 0 - - chain.setGeneratorStepCount(0, 5); - - const params = new Float32Array([1.0, 1.0, 0.0]); - const pattern = chain.evaluate(params, 8, 42); - - // Single generator with 5 steps — no merge, output is 5 steps - assert.equal(pattern.stepCount, 5); - }); -}); diff --git a/playground/js/shapeseq/tests/polyrhythm.test.js b/playground/js/shapeseq/tests/polyrhythm.test.js deleted file mode 100644 index e19dccb..0000000 --- a/playground/js/shapeseq/tests/polyrhythm.test.js +++ /dev/null @@ -1,183 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { - createPattern, setStep, clonePattern, mergePatterns, - lcm, tilePattern, lcmOfPatterns, -} from '../pattern.js'; - -// --------------------------------------------------------------------------- -// lcm -// --------------------------------------------------------------------------- - -describe('lcm', () => { - it('lcm(3, 4) = 12', () => assert.equal(lcm(3, 4), 12)); - it('lcm(5, 8) = 40', () => assert.equal(lcm(5, 8), 40)); - it('lcm(6, 6) = 6', () => assert.equal(lcm(6, 6), 6)); - it('lcm(1, 7) = 7', () => assert.equal(lcm(1, 7), 7)); - it('lcm(7, 1) = 7', () => assert.equal(lcm(7, 1), 7)); -}); - -// --------------------------------------------------------------------------- -// tilePattern -// --------------------------------------------------------------------------- - -describe('tilePattern', () => { - /** Build a 3-step pattern with distinct triggers on each step. */ - function make3StepPattern() { - const p = createPattern(3); - setStep(p, 0, { trigger: true, pitch: 0.1, velocity: 0.2, accent: true, timeOffset: 0.05, subdivisions: 2, midiNote: 60 }); - setStep(p, 1, { trigger: false, pitch: 0.3, velocity: 0.4, accent: false, timeOffset: -0.1, subdivisions: 1, midiNote: null }); - setStep(p, 2, { trigger: true, pitch: 0.9, velocity: 0.8, accent: true, timeOffset: 0.0, subdivisions: 3, midiNote: 72 }); - return p; - } - - it('tiles a 3-step pattern to 12 steps by wrapping', () => { - const src = make3StepPattern(); - const tiled = tilePattern(src, 12); - - assert.equal(tiled.stepCount, 12); - assert.equal(tiled.steps.length, 12); - - for (let i = 0; i < 12; i++) { - const srcStep = src.steps[i % 3]; - const tiledStep = tiled.steps[i]; - assert.equal(tiledStep.trigger, srcStep.trigger, `step ${i} trigger`); - assert.equal(tiledStep.pitch, srcStep.pitch, `step ${i} pitch`); - assert.equal(tiledStep.velocity, srcStep.velocity, `step ${i} velocity`); - assert.equal(tiledStep.accent, srcStep.accent, `step ${i} accent`); - assert.equal(tiledStep.timeOffset, srcStep.timeOffset, `step ${i} timeOffset`); - assert.equal(tiledStep.subdivisions, srcStep.subdivisions, `step ${i} subdivisions`); - assert.equal(tiledStep.midiNote, srcStep.midiNote, `step ${i} midiNote`); - } - }); - - it('same length returns a clone (no-op)', () => { - const src = make3StepPattern(); - const tiled = tilePattern(src, 3); - - assert.equal(tiled.stepCount, 3); - // Must be a separate object (clone), not the same reference - assert.notEqual(tiled, src); - assert.notEqual(tiled.steps, src.steps); - assert.notEqual(tiled.steps[0], src.steps[0]); - // But values match - assert.equal(tiled.steps[0].pitch, src.steps[0].pitch); - }); - - it('preserves all step fields (trigger, pitch, velocity, accent, timeOffset, subdivisions, midiNote)', () => { - const src = make3StepPattern(); - const tiled = tilePattern(src, 6); - - const step0 = tiled.steps[0]; - assert.equal(step0.trigger, true); - assert.equal(step0.pitch, 0.1); - assert.equal(step0.velocity, 0.2); - assert.equal(step0.accent, true); - assert.equal(step0.timeOffset, 0.05); - assert.equal(step0.subdivisions, 2); - assert.equal(step0.midiNote, 60); - - // Step 4 should be a copy of step 1 (4 % 3 = 1) - const step4 = tiled.steps[4]; - assert.equal(step4.trigger, false); - assert.equal(step4.pitch, 0.3); - assert.equal(step4.velocity, 0.4); - assert.equal(step4.midiNote, null); - }); - - it('throws if targetStepCount < pattern.stepCount', () => { - const src = make3StepPattern(); - assert.throws( - () => tilePattern(src, 2), - { name: 'RangeError' } - ); - }); -}); - -// --------------------------------------------------------------------------- -// mergePatterns — backward compatibility (same step counts) -// --------------------------------------------------------------------------- - -describe('mergePatterns — same step count (backward compat)', () => { - it('merges two 4-step patterns additively', () => { - const a = createPattern(4); - setStep(a, 0, { trigger: true, pitch: 0.8, velocity: 0.6 }); - setStep(a, 2, { trigger: true, pitch: 0.3, velocity: 0.9 }); - - const b = createPattern(4); - setStep(b, 1, { trigger: true, pitch: 0.5, velocity: 0.7 }); - setStep(b, 2, { trigger: true, pitch: 0.7, velocity: 0.5 }); - - const merged = mergePatterns(a, b, 'additive'); - assert.equal(merged.stepCount, 4); - assert.equal(merged.steps[0].trigger, true); // A only - assert.equal(merged.steps[1].trigger, true); // B only - assert.equal(merged.steps[2].trigger, true); // both - assert.equal(merged.steps[3].trigger, false); // neither - }); -}); - -// --------------------------------------------------------------------------- -// mergePatterns — different step counts (polyrhythm) -// --------------------------------------------------------------------------- - -describe('mergePatterns — different step counts (polyrhythm)', () => { - it('3+4 step patterns produce LCM=12 length result', () => { - const a = createPattern(3); - setStep(a, 0, { trigger: true, pitch: 0.2, velocity: 0.8 }); - // Steps 1, 2 are default (trigger: false) - - const b = createPattern(4); - setStep(b, 0, { trigger: true, pitch: 0.6, velocity: 0.4 }); - // Steps 1, 2, 3 are default (trigger: false) - - const merged = mergePatterns(a, b, 'additive'); - assert.equal(merged.stepCount, 12); - assert.equal(merged.steps.length, 12); - }); - - it('tiled pattern has correct trigger patterns from both inputs', () => { - // A: 3 steps, triggers on 0 and 2 → tiled to 12: triggers at 0,2,3,5,6,8,9,11 - const a = createPattern(3); - setStep(a, 0, { trigger: true }); - setStep(a, 2, { trigger: true }); - - // B: 4 steps, trigger on 1 → tiled to 12: triggers at 1,5,9 - const b = createPattern(4); - setStep(b, 1, { trigger: true }); - - const merged = mergePatterns(a, b, 'additive'); - assert.equal(merged.stepCount, 12); - - // A triggers at positions where i%3 is 0 or 2: 0,2,3,5,6,8,9,11 - // B triggers at positions where i%4 is 1: 1,5,9 - // Combined (additive = OR): 0,1,2,3,5,6,8,9,11 - const expectedTriggers = [true, true, true, true, false, true, true, false, true, true, false, true]; - for (let i = 0; i < 12; i++) { - assert.equal(merged.steps[i].trigger, expectedTriggers[i], `step ${i} trigger`); - } - }); -}); - -// --------------------------------------------------------------------------- -// lcmOfPatterns -// --------------------------------------------------------------------------- - -describe('lcmOfPatterns', () => { - it('single pattern returns its stepCount', () => { - const p = createPattern(5); - assert.equal(lcmOfPatterns([p]), 5); - }); - - it('multiple patterns returns LCM of all step counts', () => { - const a = createPattern(3); - const b = createPattern(4); - const c = createPattern(6); - // lcm(3,4) = 12, lcm(12,6) = 12 - assert.equal(lcmOfPatterns([a, b, c]), 12); - }); - - it('empty array returns 1', () => { - assert.equal(lcmOfPatterns([]), 1); - }); -}); diff --git a/playground/js/shapeseq/tests/projection.test.js b/playground/js/shapeseq/tests/projection.test.js deleted file mode 100644 index 1156582..0000000 --- a/playground/js/shapeseq/tests/projection.test.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Tests for the simplified 3-knob projection layer. - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; -import { createProjection, applyProjection, VELOCITY_CURVES } from '../projection.js'; -import { createPattern, setStep } from '../pattern.js'; - -// --------------------------------------------------------------------------- -// createProjection -// --------------------------------------------------------------------------- - -describe('createProjection', () => { - it('returns correct defaults', () => { - const p = createProjection(); - assert.equal(p.velocityCurve, 'linear'); - assert.equal(p.gateThreshold, 0); - assert.deepStrictEqual(p.pitchRange, { low: 0, high: 127 }); - }); - - it('accepts custom values', () => { - const p = createProjection({ - velocityCurve: 'sCurve', - gateThreshold: 0.4, - pitchRange: { low: 36, high: 72 }, - }); - assert.equal(p.velocityCurve, 'sCurve'); - assert.equal(p.gateThreshold, 0.4); - assert.deepStrictEqual(p.pitchRange, { low: 36, high: 72 }); - }); - - it('fills partial pitchRange with defaults', () => { - const p = createProjection({ pitchRange: { low: 24 } }); - assert.equal(p.pitchRange.low, 24); - assert.equal(p.pitchRange.high, 127); - }); -}); - -// --------------------------------------------------------------------------- -// VELOCITY_CURVES constant -// --------------------------------------------------------------------------- - -describe('VELOCITY_CURVES', () => { - it('contains the three expected curves', () => { - assert.deepStrictEqual(VELOCITY_CURVES, ['linear', 'exponential', 'sCurve']); - }); -}); - -// --------------------------------------------------------------------------- -// Velocity curves -// --------------------------------------------------------------------------- - -describe('velocity curves', () => { - it('linear is passthrough', () => { - const config = createProjection({ velocityCurve: 'linear' }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, velocity: 0.7 }); - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].velocity, 0.7); - }); - - it('exponential squares the value', () => { - const config = createProjection({ velocityCurve: 'exponential' }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, velocity: 0.5 }); - const out = applyProjection(config, pattern); - assert.ok(Math.abs(out.steps[0].velocity - 0.25) < 1e-9); - }); - - it('sCurve applies smoothstep', () => { - const config = createProjection({ velocityCurve: 'sCurve' }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, velocity: 0.5 }); - const out = applyProjection(config, pattern); - // smoothstep(0.5) = (3 - 2*0.5) * 0.5 * 0.5 = 2 * 0.25 = 0.5 - assert.ok(Math.abs(out.steps[0].velocity - 0.5) < 1e-9); - }); - - it('sCurve at 0 and 1 are identity', () => { - const config = createProjection({ velocityCurve: 'sCurve' }); - const pattern = createPattern(2); - setStep(pattern, 0, { trigger: true, velocity: 0.0 }); - setStep(pattern, 1, { trigger: true, velocity: 1.0 }); - const out = applyProjection(config, pattern); - assert.ok(Math.abs(out.steps[0].velocity - 0.0) < 1e-9); - assert.ok(Math.abs(out.steps[1].velocity - 1.0) < 1e-9); - }); -}); - -// --------------------------------------------------------------------------- -// Gate threshold -// --------------------------------------------------------------------------- - -describe('gate threshold', () => { - it('steps below threshold get trigger=false', () => { - const config = createProjection({ gateThreshold: 0.5 }); - const pattern = createPattern(3); - setStep(pattern, 0, { trigger: true, velocity: 0.3 }); - setStep(pattern, 1, { trigger: true, velocity: 0.6 }); - setStep(pattern, 2, { trigger: true, velocity: 0.5 }); // exactly at threshold - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].trigger, false); // 0.3 < 0.5 → muted - assert.equal(out.steps[1].trigger, true); // 0.6 >= 0.5 → kept - assert.equal(out.steps[2].trigger, true); // 0.5 is not < 0.5 → kept (at threshold) - }); - - it('threshold 0 mutes nothing', () => { - const config = createProjection({ gateThreshold: 0 }); - const pattern = createPattern(2); - setStep(pattern, 0, { trigger: true, velocity: 0.01 }); - setStep(pattern, 1, { trigger: true, velocity: 0.0 }); - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].trigger, true); - // velocity 0.0 is not < 0, so stays true - assert.equal(out.steps[1].trigger, true); - }); - - it('threshold 1 mutes everything below 1', () => { - const config = createProjection({ gateThreshold: 1 }); - const pattern = createPattern(2); - setStep(pattern, 0, { trigger: true, velocity: 0.99 }); - setStep(pattern, 1, { trigger: true, velocity: 1.0 }); - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].trigger, false); - assert.equal(out.steps[1].trigger, true); - }); -}); - -// --------------------------------------------------------------------------- -// Pitch range (octave fold) -// --------------------------------------------------------------------------- - -describe('pitch range', () => { - it('clamps midiNote into range by octave folding', () => { - const config = createProjection({ pitchRange: { low: 48, high: 72 } }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, midiNote: 84 }); // C6, should fold down - const out = applyProjection(config, pattern); - // 84 - 12 = 72 (within range) - assert.equal(out.steps[0].midiNote, 72); - }); - - it('folds up when below range', () => { - const config = createProjection({ pitchRange: { low: 48, high: 72 } }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, midiNote: 36 }); // C2 - const out = applyProjection(config, pattern); - // 36 + 12 = 48 (within range) - assert.equal(out.steps[0].midiNote, 48); - }); - - it('leaves note in range untouched', () => { - const config = createProjection({ pitchRange: { low: 48, high: 72 } }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, midiNote: 60 }); - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].midiNote, 60); - }); - - it('steps without midiNote (null) are unaffected', () => { - const config = createProjection({ pitchRange: { low: 48, high: 72 } }); - const pattern = createPattern(1); - // midiNote defaults to null from createStep - setStep(pattern, 0, { trigger: true }); - const out = applyProjection(config, pattern); - assert.equal(out.steps[0].midiNote, null); - }); - - it('handles narrow range (< 12 semitones) with clamping', () => { - const config = createProjection({ pitchRange: { low: 60, high: 65 } }); - const pattern = createPattern(1); - setStep(pattern, 0, { trigger: true, midiNote: 80 }); - const out = applyProjection(config, pattern); - // 80 - 12 = 68 > 65, -12 = 56 < 60, clamp to 60 - assert.ok(out.steps[0].midiNote >= 60 && out.steps[0].midiNote <= 65); - }); -}); - -// --------------------------------------------------------------------------- -// No mutation -// --------------------------------------------------------------------------- - -describe('applyProjection immutability', () => { - it('does not mutate the input pattern', () => { - const config = createProjection({ - velocityCurve: 'exponential', - gateThreshold: 0.5, - pitchRange: { low: 48, high: 72 }, - }); - const pattern = createPattern(2); - setStep(pattern, 0, { trigger: true, velocity: 0.3, midiNote: 84 }); - setStep(pattern, 1, { trigger: true, velocity: 0.8, midiNote: 60 }); - - // Snapshot original values - const origVel0 = pattern.steps[0].velocity; - const origVel1 = pattern.steps[1].velocity; - const origTrig0 = pattern.steps[0].trigger; - const origNote0 = pattern.steps[0].midiNote; - - applyProjection(config, pattern); - - assert.equal(pattern.steps[0].velocity, origVel0); - assert.equal(pattern.steps[1].velocity, origVel1); - assert.equal(pattern.steps[0].trigger, origTrig0); - assert.equal(pattern.steps[0].midiNote, origNote0); - }); -}); diff --git a/playground/js/shapeseq/tests/re-eval.test.js b/playground/js/shapeseq/tests/re-eval.test.js deleted file mode 100644 index 6554fba..0000000 --- a/playground/js/shapeseq/tests/re-eval.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { Primitive } from '../primitive.js'; -import { PitchWalker, EuclideanRhythm, ProbabilityGate } from '../primitives.js'; -import { Chain } from '../chain.js'; - -// --------------------------------------------------------------------------- -// Primitive.reEvalOnLoop default -// --------------------------------------------------------------------------- - -describe('Primitive.reEvalOnLoop', () => { - it('defaults to false on the base class', () => { - // Use a concrete subclass that does not override reEvalOnLoop - const prim = new EuclideanRhythm(); - assert.strictEqual(prim.reEvalOnLoop, false); - }); - - it('defaults to false for non-stateful primitives', () => { - const gate = new ProbabilityGate(); - assert.strictEqual(gate.reEvalOnLoop, false); - }); -}); - -// --------------------------------------------------------------------------- -// PitchWalker.reEvalOnLoop -// --------------------------------------------------------------------------- - -describe('PitchWalker.reEvalOnLoop', () => { - it('is true', () => { - const walker = new PitchWalker(); - assert.strictEqual(walker.reEvalOnLoop, true); - }); -}); - -// --------------------------------------------------------------------------- -// Chain.hasReEvalPrimitives() -// --------------------------------------------------------------------------- - -describe('Chain.hasReEvalPrimitives()', () => { - it('returns false for an empty chain', () => { - const chain = new Chain(); - assert.strictEqual(chain.hasReEvalPrimitives(), false); - }); - - it('returns false when no primitives have reEvalOnLoop', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new ProbabilityGate()); - assert.strictEqual(chain.hasReEvalPrimitives(), false); - }); - - it('returns true when at least one primitive has reEvalOnLoop', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new PitchWalker()); - assert.strictEqual(chain.hasReEvalPrimitives(), true); - }); - - it('returns false after removing the only reEvalOnLoop primitive', () => { - const chain = new Chain(); - chain.addPrimitive(new EuclideanRhythm()); - chain.addPrimitive(new PitchWalker()); - assert.strictEqual(chain.hasReEvalPrimitives(), true); - - // PitchWalker is at index 1 - chain.removePrimitive(1); - assert.strictEqual(chain.hasReEvalPrimitives(), false); - }); -}); - -// --------------------------------------------------------------------------- -// Sequencer integration (requires browser APIs: AudioContext, WASM, etc.) -// --------------------------------------------------------------------------- - -// NOTE: ShapeSeqEngine integration tests for loop-start re-evaluation -// require browser APIs (AudioContext, WASM IML, ClockEngine with -// requestAnimationFrame). These must be tested in a browser environment -// or with appropriate mocks (e.g. Playwright e2e tests). -// -// Key behaviors to verify in integration tests: -// 1. seq.loopStart event triggers _handleLoopStart() -// 2. _handleLoopStart() bumps generation and calls setSequenceInputs() -// when chain.hasReEvalPrimitives() is true -// 3. _handleLoopStart() is a no-op when chain has no reEval primitives -// 4. Unsubscribes from seq.loopStart on destroy() -// 5. freeze-as-algorithm (meml-9h1) suppresses re-evaluation (future) diff --git a/playground/js/shapeseq/tests/run.sh b/playground/js/shapeseq/tests/run.sh deleted file mode 100755 index ccfacc5..0000000 --- a/playground/js/shapeseq/tests/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -# Run all shapeseq tests using Node's built-in test runner. -# Usage: bash playground/js/shapeseq/tests/run.sh - -set -euo pipefail - -DIR="$(cd "$(dirname "$0")" && pwd)" - -node --test "$DIR"/*.test.js diff --git a/playground/js/shapeseq/tests/seq-iml.test.js b/playground/js/shapeseq/tests/seq-iml.test.js deleted file mode 100644 index f50480b..0000000 --- a/playground/js/shapeseq/tests/seq-iml.test.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Tests for seq-iml.js configuration logic. - * - * These tests verify the architecture scaling and validation functions - * without instantiating WasmIML (which requires a browser WASM runtime). - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -import { - computeHiddenLayers, - validateOutputCount, - SEQ_DEFAULT_OUTPUT_COUNT, -} from '../seq-iml.js'; - -// ── computeHiddenLayers ───────────────────────────────────────────── - -describe('computeHiddenLayers', () => { - it('returns [8,8,8] for outputCount <= 8', () => { - assert.deepStrictEqual(computeHiddenLayers(1), [8, 8, 8]); - assert.deepStrictEqual(computeHiddenLayers(4), [8, 8, 8]); - assert.deepStrictEqual(computeHiddenLayers(8), [8, 8, 8]); - }); - - it('returns [16,16,16] for outputCount 9–16', () => { - assert.deepStrictEqual(computeHiddenLayers(9), [16, 16, 16]); - assert.deepStrictEqual(computeHiddenLayers(16), [16, 16, 16]); - }); - - it('returns [24,24,32] for outputCount 17–32', () => { - assert.deepStrictEqual(computeHiddenLayers(17), [24, 24, 32]); - assert.deepStrictEqual(computeHiddenLayers(24), [24, 24, 32]); - assert.deepStrictEqual(computeHiddenLayers(32), [24, 24, 32]); - }); - - it('returns [n,n,n] for outputCount > 32', () => { - assert.deepStrictEqual(computeHiddenLayers(48), [48, 48, 48]); - assert.deepStrictEqual(computeHiddenLayers(64), [64, 64, 64]); - assert.deepStrictEqual(computeHiddenLayers(128), [128, 128, 128]); - }); - - it('always returns exactly 3 hidden layers', () => { - for (const n of [1, 8, 16, 32, 64]) { - assert.strictEqual(computeHiddenLayers(n).length, 3); - } - }); -}); - -// ── validateOutputCount ───────────────────────────────────────────── - -describe('validateOutputCount', () => { - it('accepts positive integers', () => { - assert.strictEqual(validateOutputCount(1), 1); - assert.strictEqual(validateOutputCount(8), 8); - assert.strictEqual(validateOutputCount(16), 16); - assert.strictEqual(validateOutputCount(32), 32); - }); - - it('rounds fractional values to nearest integer', () => { - assert.strictEqual(validateOutputCount(15.7), 16); - assert.strictEqual(validateOutputCount(8.3), 8); - }); - - it('rejects zero', () => { - assert.throws(() => validateOutputCount(0), RangeError); - }); - - it('rejects negative values', () => { - assert.throws(() => validateOutputCount(-1), RangeError); - assert.throws(() => validateOutputCount(-16), RangeError); - }); - - it('rejects NaN and Infinity', () => { - assert.throws(() => validateOutputCount(NaN), RangeError); - assert.throws(() => validateOutputCount(Infinity), RangeError); - assert.throws(() => validateOutputCount(-Infinity), RangeError); - }); -}); - -// ── SEQ_DEFAULT_OUTPUT_COUNT ──────────────────────────────────────── - -describe('SEQ_DEFAULT_OUTPUT_COUNT', () => { - it('equals 16 (backward compatible default)', () => { - assert.strictEqual(SEQ_DEFAULT_OUTPUT_COUNT, 16); - }); -}); diff --git a/playground/js/shapeseq/tests/sequencer-generation.test.js b/playground/js/shapeseq/tests/sequencer-generation.test.js deleted file mode 100644 index 0e9e524..0000000 --- a/playground/js/shapeseq/tests/sequencer-generation.test.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Tests for ShapeSeqEngine generation counter dirty-check. - * - * The full ShapeSeqEngine requires AudioContext, C15Bridge, and WASM, - * so we test the generation counter logic by creating a minimal subclass - * that stubs out the heavy dependencies while preserving the dirty-check - * and config-mutation methods. - * - * Run with Node >= 18: node --test playground/js/shapeseq/tests/sequencer-generation.test.js - */ - -import { describe, it, beforeEach } from 'node:test'; -import assert from 'node:assert/strict'; - -// ── Minimal stub of EventBus ──────────────────────────────────────── - -class StubEventBus { - constructor() { this._listeners = new Map(); } - on(event, fn) { - if (!this._listeners.has(event)) this._listeners.set(event, new Set()); - this._listeners.get(event).add(fn); - } - off(event, fn) { - this._listeners.get(event)?.delete(fn); - } - emit(event, data) { - for (const fn of this._listeners.get(event) ?? []) fn(data); - } -} - -// ── Minimal harness that replicates the generation + dirty-check logic -// from ShapeSeqEngine without needing AudioContext / WASM / C15 ──── - -class GenerationTestHarness { - constructor(eventBus) { - this._bus = eventBus; - - // Dirty-check state (mirrors sequencer.js) - this._lastInputs = [NaN, NaN]; - this._generation = 0; - this._lastGeneration = -1; - - // Track how many times the pipeline actually ran - this.evaluationCount = 0; - - // Bound handler for chain edits - this._onChainEdit = () => this._bumpGeneration(); - this._bus.on('ui.chainEdit', this._onChainEdit); - } - - // Config mutators (same as sequencer.js) - setTempo(_bpm) { this._bumpGeneration(); } - setStepCount(_count) { this._bumpGeneration(); } - setProjection(_name) { this._bumpGeneration(); } - - /** Mirrors ShapeSeqEngine.setSequenceInputs dirty-check exactly. */ - setSequenceInputs(values) { - const EPS = 1e-5; - const inputsSame = Math.abs(values[0] - this._lastInputs[0]) < EPS && - Math.abs(values[1] - this._lastInputs[1]) < EPS; - const generationSame = this._generation === this._lastGeneration; - if (inputsSame && generationSame) { - return; - } - this._lastInputs[0] = values[0]; - this._lastInputs[1] = values[1]; - this._lastGeneration = this._generation; - - // Instead of running MLP pipeline, just count. - this.evaluationCount++; - } - - _bumpGeneration() { this._generation++; } - - destroy() { - this._bus.off('ui.chainEdit', this._onChainEdit); - } -} - -// ── Tests ─────────────────────────────────────────────────────────── - -describe('ShapeSeqEngine generation counter', () => { - let bus; - let engine; - - beforeEach(() => { - bus = new StubEventBus(); - engine = new GenerationTestHarness(bus); - }); - - it('evaluates on first call even with default inputs', () => { - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 1); - }); - - it('skips evaluation when inputs are identical', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 1); - }); - - it('re-evaluates when inputs change', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setSequenceInputs([0.6, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('re-evaluates on setTempo even with same inputs', () => { - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 1); - - engine.setTempo(140); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('re-evaluates on setStepCount even with same inputs', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setStepCount(16); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('re-evaluates on setProjection() even with same inputs', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setProjection({ gateThreshold: 0.5 }); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('re-evaluates on ui.chainEdit event even with same inputs', () => { - engine.setSequenceInputs([0.5, 0.5]); - bus.emit('ui.chainEdit', { action: 'add', name: 'EuclideanRhythm' }); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('generation bump is consumed after one evaluation', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setTempo(140); - engine.setSequenceInputs([0.5, 0.5]); // consumes the bump - engine.setSequenceInputs([0.5, 0.5]); // should be skipped - assert.equal(engine.evaluationCount, 2); - }); - - it('multiple config changes before evaluation only cause one extra eval', () => { - engine.setSequenceInputs([0.5, 0.5]); - engine.setTempo(140); - engine.setStepCount(16); - engine.setProjection({ velocityCurve: 'sCurve' }); - bus.emit('ui.chainEdit', { action: 'remove', index: 0 }); - engine.setSequenceInputs([0.5, 0.5]); - assert.equal(engine.evaluationCount, 2); - }); - - it('generation counter increments correctly', () => { - assert.equal(engine._generation, 0); - engine.setTempo(100); - assert.equal(engine._generation, 1); - engine.setStepCount(4); - assert.equal(engine._generation, 2); - bus.emit('ui.chainEdit', { action: 'reorder', from: 0, to: 1 }); - assert.equal(engine._generation, 3); - }); -}); diff --git a/playground/js/shapeseq/tests/session.test.js b/playground/js/shapeseq/tests/session.test.js deleted file mode 100644 index 277e449..0000000 --- a/playground/js/shapeseq/tests/session.test.js +++ /dev/null @@ -1,394 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { serializeShapeSeqState, restoreShapeSeqState } from '../session.js'; -import { Chain } from '../chain.js'; -import { FreezeManager } from '../freeze.js'; -import { - EuclideanRhythm, - ProbabilityGate, - PitchWalker, - IntervalLock, - VelocityShaper, - PRIMITIVE_REGISTRY, -} from '../primitives.js'; -import { createProjection } from '../projection.js'; - -// --------------------------------------------------------------------------- -// Helpers — minimal engine stub -// --------------------------------------------------------------------------- - -/** - * Build a fake ShapeSeqEngine with the same accessor shape the session - * module expects, but without AudioContext/EventBus/C15 dependencies. - */ -function makeFakeEngine(opts = {}) { - const chain = new Chain(); - const prims = opts.primitives ?? [ - new EuclideanRhythm(), - new ProbabilityGate(), - new PitchWalker(), - new IntervalLock(), - new VelocityShaper(), - ]; - for (const p of prims) chain.addPrimitive(p); - if (opts.combineMode) chain.generatorCombineMode = opts.combineMode; - - const projection = opts.projection ?? createProjection(); - const fm = new FreezeManager(); - - let voiceMode = opts.voiceMode ?? 'mono'; - const bpm = opts.bpm ?? 120; - let seqWeights = opts.sequenceWeights ?? null; - - return { - getChain: () => chain, - getClock: () => ({ bpm }), - getSequenceIML: () => seqWeights - ? { getWeights: () => seqWeights, setWeights: (w) => { seqWeights = w; } } - : null, - getProjection: () => projection, - get voiceMode() { return voiceMode; }, - setVoiceMode: (m) => { voiceMode = m; }, - setProjection: (p) => { /* stub */ }, - setTempo: (t) => { /* stub */ }, - get freezeManager() { return fm; }, - }; -} - -// --------------------------------------------------------------------------- -// serializeShapeSeqState -// --------------------------------------------------------------------------- - -describe('serializeShapeSeqState', () => { - it('produces a version-1 object with all expected top-level keys', () => { - const engine = makeFakeEngine(); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.version, 1); - assert.ok('chain' in state); - assert.ok('projection' in state); - assert.ok('voiceMode' in state); - assert.ok('clock' in state); - assert.ok('sequenceWeights' in state); - assert.ok('freeze' in state); - }); - - it('serializes chain primitives by name', () => { - const engine = makeFakeEngine({ - primitives: [new EuclideanRhythm(), new PitchWalker()], - }); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.chain.primitives.length, 2); - assert.strictEqual(state.chain.primitives[0].name, 'EuclideanRhythm'); - assert.strictEqual(state.chain.primitives[1].name, 'PitchWalker'); - }); - - it('captures generator combine mode', () => { - const engine = makeFakeEngine({ combineMode: 'multiplicative' }); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.chain.generatorCombineMode, 'multiplicative'); - }); - - it('captures primitive states array', () => { - const engine = makeFakeEngine({ - primitives: [new PitchWalker()], - }); - const state = serializeShapeSeqState(engine); - - assert.ok(Array.isArray(state.chain.states)); - assert.strictEqual(state.chain.states.length, 1); - // PitchWalker default state has position - assert.strictEqual(typeof state.chain.states[0].position, 'number'); - }); - - it('captures projection config', () => { - const proj = createProjection({ - velocityCurve: 'exponential', - gateThreshold: 0.25, - pitchRange: { low: 36, high: 84 }, - }); - const engine = makeFakeEngine({ projection: proj }); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.projection.velocityCurve, 'exponential'); - assert.strictEqual(state.projection.gateThreshold, 0.25); - assert.strictEqual(state.projection.pitchRange.low, 36); - assert.strictEqual(state.projection.pitchRange.high, 84); - }); - - it('captures voice mode', () => { - const engine = makeFakeEngine({ voiceMode: 'poly' }); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.voiceMode, 'poly'); - }); - - it('captures clock BPM', () => { - const engine = makeFakeEngine({ bpm: 140 }); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.clock.bpm, 140); - }); - - it('captures sequence MLP weights when available', () => { - const weights = [[1, 2], [3, 4]]; - const engine = makeFakeEngine({ sequenceWeights: weights }); - const state = serializeShapeSeqState(engine); - - assert.deepStrictEqual(state.sequenceWeights, weights); - }); - - it('sets sequenceWeights to null when IML is unavailable', () => { - const engine = makeFakeEngine(); // no sequenceWeights - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.sequenceWeights, null); - }); - - it('freeze is null when not frozen', () => { - const engine = makeFakeEngine(); - const state = serializeShapeSeqState(engine); - - assert.strictEqual(state.freeze, null); - }); - - it('freeze state is serialized when frozen in algorithm mode', () => { - const engine = makeFakeEngine({ - primitives: [new EuclideanRhythm(), new ProbabilityGate()], - }); - const chain = engine.getChain(); - const paramCount = chain.totalParamCount; - const params = new Float32Array(paramCount); - for (let i = 0; i < paramCount; i++) params[i] = i / paramCount; - - engine.freezeManager.freeze(chain, params, 42); - - const state = serializeShapeSeqState(engine); - - assert.ok(state.freeze !== null); - assert.strictEqual(state.freeze.mode, 'algorithm'); - assert.ok(Array.isArray(state.freeze.frozenParams)); - assert.strictEqual(state.freeze.frozenParams.length, paramCount); - assert.ok(Array.isArray(state.freeze.liveFlags)); - assert.strictEqual(state.freeze.masterSeed, 42); - }); -}); - -// --------------------------------------------------------------------------- -// restoreShapeSeqState -// --------------------------------------------------------------------------- - -describe('restoreShapeSeqState', () => { - it('rebuilds chain from primitive names', () => { - const engine = makeFakeEngine({ primitives: [] }); - assert.strictEqual(engine.getChain().getPrimitives().length, 0); - - const state = { - version: 1, - chain: { - primitives: [ - { name: 'PitchWalker' }, - { name: 'EuclideanRhythm' }, - { name: 'VelocityShaper' }, - ], - states: null, - generatorCombineMode: 'additive', - }, - projection: null, - voiceMode: null, - clock: null, - sequenceWeights: null, - freeze: null, - }; - - restoreShapeSeqState(engine, state); - - const prims = engine.getChain().getPrimitives(); - assert.strictEqual(prims.length, 3); - assert.strictEqual(prims[0].name, 'PitchWalker'); - assert.strictEqual(prims[1].name, 'EuclideanRhythm'); - assert.strictEqual(prims[2].name, 'VelocityShaper'); - }); - - it('restores generator combine mode', () => { - const engine = makeFakeEngine(); - const state = { - version: 1, - chain: { - primitives: [{ name: 'EuclideanRhythm' }], - states: null, - generatorCombineMode: 'multiplicative', - }, - projection: null, - voiceMode: null, - clock: null, - sequenceWeights: null, - freeze: null, - }; - - restoreShapeSeqState(engine, state); - assert.strictEqual(engine.getChain().generatorCombineMode, 'multiplicative'); - }); - - it('restores projection config', () => { - let capturedProjection = null; - const engine = makeFakeEngine(); - engine.setProjection = (p) => { capturedProjection = p; }; - - const proj = { - velocityCurve: 'sCurve', - gateThreshold: 0.4, - pitchRange: { low: 24, high: 96 }, - }; - - restoreShapeSeqState(engine, { - version: 1, - chain: null, - projection: proj, - voiceMode: null, - clock: null, - sequenceWeights: null, - freeze: null, - }); - - assert.deepStrictEqual(capturedProjection, proj); - }); - - it('restores voice mode', () => { - let capturedMode = null; - const engine = makeFakeEngine(); - engine.setVoiceMode = (m) => { capturedMode = m; }; - - restoreShapeSeqState(engine, { - version: 1, - chain: null, - projection: null, - voiceMode: 'poly', - clock: null, - sequenceWeights: null, - freeze: null, - }); - - assert.strictEqual(capturedMode, 'poly'); - }); - - it('restores clock BPM', () => { - let capturedBPM = null; - const engine = makeFakeEngine(); - engine.setTempo = (t) => { capturedBPM = t; }; - - restoreShapeSeqState(engine, { - version: 1, - chain: null, - projection: null, - voiceMode: null, - clock: { bpm: 90 }, - sequenceWeights: null, - freeze: null, - }); - - assert.strictEqual(capturedBPM, 90); - }); - - it('restores sequence MLP weights when IML available', () => { - const weights = [[5, 6], [7, 8]]; - const engine = makeFakeEngine({ sequenceWeights: [[0, 0]] }); - - restoreShapeSeqState(engine, { - version: 1, - chain: null, - projection: null, - voiceMode: null, - clock: null, - sequenceWeights: weights, - freeze: null, - }); - - // The stub's setWeights should have been called - assert.deepStrictEqual(engine.getSequenceIML().getWeights(), weights); - }); - - it('skips unknown primitive names gracefully', () => { - const engine = makeFakeEngine({ primitives: [] }); - - const state = { - version: 1, - chain: { - primitives: [ - { name: 'EuclideanRhythm' }, - { name: 'NonExistentPrimitive' }, - { name: 'PitchWalker' }, - ], - states: null, - generatorCombineMode: 'additive', - }, - projection: null, - voiceMode: null, - clock: null, - sequenceWeights: null, - freeze: null, - }; - - // Should not throw - restoreShapeSeqState(engine, state); - - // Unknown primitive skipped, only valid ones added - const prims = engine.getChain().getPrimitives(); - assert.strictEqual(prims.length, 2); - assert.strictEqual(prims[0].name, 'EuclideanRhythm'); - assert.strictEqual(prims[1].name, 'PitchWalker'); - }); - - it('handles null state without throwing', () => { - const engine = makeFakeEngine(); - // Should not throw - restoreShapeSeqState(engine, null); - restoreShapeSeqState(engine, undefined); - }); - - it('handles wrong version without throwing', () => { - const engine = makeFakeEngine(); - restoreShapeSeqState(engine, { version: 99 }); - // Chain should be untouched - assert.strictEqual(engine.getChain().getPrimitives().length, 5); - }); - - it('handles state with missing optional fields', () => { - const engine = makeFakeEngine({ primitives: [] }); - - // Minimal valid state — only version and chain - restoreShapeSeqState(engine, { - version: 1, - chain: { - primitives: [{ name: 'EuclideanRhythm' }], - }, - }); - - assert.strictEqual(engine.getChain().getPrimitives().length, 1); - }); - - it('restores primitive states alongside chain rebuild', () => { - const engine = makeFakeEngine({ primitives: [] }); - - const state = { - version: 1, - chain: { - primitives: [{ name: 'PitchWalker' }], - states: [{ position: 0.75 }], - generatorCombineMode: 'additive', - }, - projection: null, - voiceMode: null, - clock: null, - sequenceWeights: null, - freeze: null, - }; - - restoreShapeSeqState(engine, state); - - const chainStates = engine.getChain().getState(); - assert.strictEqual(chainStates[0].position, 0.75); - }); -}); diff --git a/playground/js/shapeseq/tests/signal.test.js b/playground/js/shapeseq/tests/signal.test.js deleted file mode 100644 index 91659f9..0000000 --- a/playground/js/shapeseq/tests/signal.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, it } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { createSignal, createArraySignal } from '../signal.js'; - -// --------------------------------------------------------------------------- -// createSignal -// --------------------------------------------------------------------------- - -describe('createSignal', () => { - it('get returns the initial value', () => { - const s = createSignal(42); - assert.strictEqual(s.get(), 42); - }); - - it('set updates the value and notifies subscribers', () => { - const s = createSignal(0); - const calls = []; - s.subscribe(v => calls.push(v)); - s.set(10); - assert.strictEqual(s.get(), 10); - assert.deepStrictEqual(calls, [10]); - }); - - it('does not notify on same value', () => { - const s = createSignal('hello'); - const calls = []; - s.subscribe(v => calls.push(v)); - s.set('hello'); - assert.deepStrictEqual(calls, []); - assert.strictEqual(s.get(), 'hello'); - }); - - it('unsubscribe works', () => { - const s = createSignal(0); - const calls = []; - const unsub = s.subscribe(v => calls.push(v)); - s.set(1); - unsub(); - s.set(2); - assert.deepStrictEqual(calls, [1]); - }); - - it('multiple subscribers each get notified', () => { - const s = createSignal(0); - const a = [], b = []; - s.subscribe(v => a.push(v)); - s.subscribe(v => b.push(v)); - s.set(5); - assert.deepStrictEqual(a, [5]); - assert.deepStrictEqual(b, [5]); - }); -}); - -// --------------------------------------------------------------------------- -// createArraySignal -// --------------------------------------------------------------------------- - -describe('createArraySignal', () => { - it('initial value is all zeros', () => { - const s = createArraySignal(4); - const v = s.get(); - assert.strictEqual(v.length, 4); - for (let i = 0; i < 4; i++) assert.strictEqual(v[i], 0); - }); - - it('update triggers only on actual changes', () => { - const s = createArraySignal(3); - const calls = []; - s.subscribe(() => calls.push('changed')); - - // Same values (all zero) should not trigger - s.update(new Float32Array([0, 0, 0])); - assert.strictEqual(calls.length, 0); - - // Different values should trigger - s.update(new Float32Array([0.5, 0, 0])); - assert.strictEqual(calls.length, 1); - }); - - it('partial updates work (shorter array)', () => { - const s = createArraySignal(4); - s.update(new Float32Array([1, 2, 3, 4])); - assert.strictEqual(s.get()[3], 4); - - // Partial update: only first 2 elements - const calls = []; - s.subscribe(() => calls.push('changed')); - s.update(new Float32Array([10, 20])); - - // First two updated, last two unchanged - assert.strictEqual(s.get()[0], 10); - assert.strictEqual(s.get()[1], 20); - assert.strictEqual(s.get()[2], 3); - assert.strictEqual(s.get()[3], 4); - assert.strictEqual(calls.length, 1); - }); - - it('subscriber receives the backing array', () => { - const s = createArraySignal(2); - let received = null; - s.subscribe(v => { received = v; }); - s.update(new Float32Array([0.1, 0.2])); - assert.ok(received instanceof Float32Array); - assert.strictEqual(received[0], Float32Array.of(0.1)[0]); - }); - - it('unsubscribe works', () => { - const s = createArraySignal(2); - const calls = []; - const unsub = s.subscribe(() => calls.push('x')); - s.update(new Float32Array([1, 0])); - unsub(); - s.update(new Float32Array([2, 0])); - assert.strictEqual(calls.length, 1); - }); -}); diff --git a/playground/js/shapeseq/tests/smoke.test.js b/playground/js/shapeseq/tests/smoke.test.js deleted file mode 100644 index 2655878..0000000 --- a/playground/js/shapeseq/tests/smoke.test.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Smoke tests for shapeseq modules. - * - * Validates that every module can be imported and that non-browser modules - * construct correctly. Browser-dependent modules are imported inside - * try/catch blocks so the suite stays green in a pure Node environment. - */ - -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -// ── Pure modules (no browser APIs required) ───────────────────────── - -import { - createStep, createPattern, clonePattern, - mergePatterns, setStep, validatePattern, -} from '../pattern.js'; - -import { Primitive, CATEGORIES, applyBoundary } from '../primitive.js'; - -import { - EuclideanRhythm, ProbabilityGate, PitchWalker, - Ratchet, SwingGroove, DensityMorph, IntervalLock, - VelocityShaper, PRIMITIVE_REGISTRY, -} from '../primitives.js'; - -import { - createPRNG, next, nextInt, fork, getState, setState, randomSeed, -} from '../prng.js'; - -import { Chain } from '../chain.js'; - -import { - SEQ, ML, UI, EventBus, -} from '../event-bus.js'; - -import { createParamMap, map, mapWithSchema } from '../param-map.js'; - -import { - createProjection, applyProjection, VELOCITY_CURVES, -} from '../projection.js'; - -// ── Tests: pattern ────────────────────────────────────────────────── - -describe('pattern', () => { - it('createStep returns an object', () => { - const step = createStep(); - assert.equal(typeof step, 'object'); - }); - - it('createPattern builds a pattern with the requested step count', () => { - const pat = createPattern(8); - assert.ok(pat); - assert.ok(validatePattern(pat)); - }); - - it('clonePattern produces an independent copy', () => { - const a = createPattern(4); - const b = clonePattern(a); - assert.deepStrictEqual(a, b); - assert.notEqual(a, b); - }); - - it('mergePatterns does not throw', () => { - const a = createPattern(4); - const b = createPattern(4); - assert.ok(mergePatterns(a, b, 'additive')); - }); -}); - -// ── Tests: prng ───────────────────────────────────────────────────── - -describe('prng', () => { - it('createPRNG returns a deterministic generator', () => { - const a = createPRNG(42); - const b = createPRNG(42); - assert.deepStrictEqual(next(a), next(b)); - }); - - it('nextInt returns values within range', () => { - const rng = createPRNG(1); - for (let i = 0; i < 50; i++) { - const result = nextInt(rng, 0, 10); - // nextInt may return a number or { value, nextState } - const v = typeof result === 'number' ? result : result.value; - assert.ok(v >= 0 && v <= 10, `${v} out of range`); - } - }); - - it('fork creates a child PRNG', () => { - const rng = createPRNG(7); - const child = fork(rng, 'sub'); - assert.ok(child); - const result = next(child); - assert.equal(typeof result.value, 'number'); - }); - - it('getState / setState round-trips', () => { - const rng = createPRNG(99); - next(rng); next(rng); - const snap = getState(rng); - const v1 = next(rng); - setState(rng, snap); - const v2 = next(rng); - assert.deepStrictEqual(v1, v2); - }); - - it('randomSeed returns a number', () => { - assert.equal(typeof randomSeed(), 'number'); - }); -}); - -// ── Tests: primitive ──────────────────────────────────────────────── - -describe('primitive', () => { - it('CATEGORIES is a frozen array', () => { - assert.ok(Array.isArray(CATEGORIES)); - assert.ok(Object.isFrozen(CATEGORIES)); - }); - - it('Primitive can be constructed with a schema', () => { - // Grab the first registered primitive's schema as a reference - const key = Object.keys(PRIMITIVE_REGISTRY)[0]; - const Cls = PRIMITIVE_REGISTRY[key]; - const inst = new Cls(); - assert.ok(inst instanceof Primitive); - }); -}); - -// ── Tests: primitives ─────────────────────────────────────────────── - -describe('primitives', () => { - it('PRIMITIVE_REGISTRY contains all expected primitives', () => { - const expected = [ - 'EuclideanRhythm', 'ProbabilityGate', 'PitchWalker', 'Ratchet', - 'SwingGroove', 'DensityMorph', 'IntervalLock', 'VelocityShaper', - ]; - for (const name of expected) { - assert.ok(name in PRIMITIVE_REGISTRY, `missing: ${name}`); - } - }); - - it('each registered primitive instantiates without error', () => { - for (const [name, Cls] of Object.entries(PRIMITIVE_REGISTRY)) { - const inst = new Cls(); - assert.ok(inst instanceof Primitive, `${name} is not a Primitive`); - } - }); -}); - -// ── Tests: chain ──────────────────────────────────────────────────── - -describe('chain', () => { - it('Chain constructs with defaults', () => { - const c = new Chain(); - assert.ok(c); - }); -}); - -// ── Tests: event-bus ──────────────────────────────────────────────── - -describe('event-bus', () => { - it('SEQ, ML, UI are frozen namespace objects', () => { - assert.ok(Object.isFrozen(SEQ)); - assert.ok(Object.isFrozen(ML)); - assert.ok(Object.isFrozen(UI)); - }); - - it('EventBus can be constructed without AudioContext', () => { - const bus = new EventBus(); - assert.ok(bus); - }); - - it('on/emit round-trip works', () => { - const bus = new EventBus(); - let received = null; - bus.on(SEQ.STEP, (data) => { received = data; }); - bus.emit(SEQ.STEP, { idx: 3 }); - assert.equal(received.idx, 3); - }); -}); - -// ── Tests: param-map ──────────────────────────────────────────────── - -describe('param-map', () => { - it('createParamMap returns an object', () => { - const pm = createParamMap(8); - assert.ok(pm); - }); - - it('map transforms an array of outputs', () => { - const outputs = new Array(8).fill(0.5); - const result = map(outputs, 4); - assert.equal(result.length, 4); - }); -}); - -// ── Tests: projection ─────────────────────────────────────────────── - -describe('projection', () => { - it('createProjection returns a valid config object', () => { - const config = createProjection(); - assert.ok(config); - assert.equal(typeof config.velocityCurve, 'string'); - assert.equal(typeof config.gateThreshold, 'number'); - assert.ok(config.pitchRange); - }); - - it('VELOCITY_CURVES has 3 entries', () => { - assert.equal(VELOCITY_CURVES.length, 3); - }); -}); - -// ── Browser-dependent modules ─────────────────────────────────────── -// These modules reference AudioContext, DOM APIs, or parent-directory -// imports that may not resolve in Node. We verify the import itself -// doesn't throw at module-evaluation time (syntax / top-level errors). - -describe('browser-dependent modules (import-only)', () => { - it('clock.js imports without top-level error', async () => { - // ClockEngine requires AudioContext at *construction*, but the - // module should load fine. - const mod = await import('../clock.js'); - assert.ok(mod.ClockEngine, 'ClockEngine export exists'); - }); - - it('step-viz.js imports without top-level error', async () => { - const mod = await import('../step-viz.js'); - assert.ok(mod.StepVisualizer, 'StepVisualizer export exists'); - }); - - it('sequencer.js imports without top-level error', async () => { - const mod = await import('../sequencer.js'); - assert.ok(mod.ShapeSeqEngine, 'ShapeSeqEngine export exists'); - }); - - it('chain-ui.js imports without top-level error', async () => { - const mod = await import('../chain-ui.js'); - assert.ok(mod.ChainBuilderUI, 'ChainBuilderUI export exists'); - }); - - it('seq-iml.js imports without top-level error', async () => { - // seq-iml imports from ../nisps/nisps-wasm.js which may not - // resolve in Node. We catch and skip gracefully. - try { - const mod = await import('../seq-iml.js'); - assert.ok(mod.createSequenceIML, 'createSequenceIML export exists'); - } catch (err) { - // Expected — parent-directory import of nisps-wasm may fail - assert.ok(true, `skipped: ${err.message}`); - } - }); -}); diff --git a/playground/js/shapeseq/tests/voice-mode.test.js b/playground/js/shapeseq/tests/voice-mode.test.js deleted file mode 100644 index a0b2259..0000000 --- a/playground/js/shapeseq/tests/voice-mode.test.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Tests for configurable mono/poly voice mode in ShapeSeqEngine. - * - * Uses lightweight mocks for C15Bridge and EventBus since the full - * engine requires AudioContext and WASM which aren't available in Node. - */ - -import { describe, it, beforeEach } from 'node:test'; -import assert from 'node:assert/strict'; - -// We can't fully construct ShapeSeqEngine without AudioContext/WASM, -// so we test the voice mode logic by importing the class and exercising -// the parts we can reach via a minimal mock setup. - -import { ShapeSeqEngine } from '../sequencer.js'; - -// ── Mock factories ───────────────────────────────────────────────── - -function createMockC15() { - const calls = []; - return { - calls, - noteOn(note, vel) { calls.push({ type: 'noteOn', note, vel }); }, - noteOff(note) { calls.push({ type: 'noteOff', note }); }, - }; -} - -function createMockBus() { - const handlers = {}; - return { - on(event, fn) { - if (!handlers[event]) handlers[event] = []; - handlers[event].push(fn); - }, - off(event, fn) { - if (handlers[event]) { - handlers[event] = handlers[event].filter(h => h !== fn); - } - }, - emit(event, data) { - if (handlers[event]) { - for (const fn of handlers[event]) fn(data); - } - }, - handlers, - }; -} - -function createMockAudioContext() { - return { currentTime: 0 }; -} - -/** - * Build a ShapeSeqEngine with mocks injected but WITHOUT calling init() - * (which needs WASM). We can still test voice mode, noteOn/noteOff - * handling, and the getter/setter since those don't depend on init. - */ -function createEngine() { - const c15 = createMockC15(); - const bus = createMockBus(); - const audioCtx = createMockAudioContext(); - const engine = new ShapeSeqEngine({ audioContext: audioCtx, eventBus: bus, c15Bridge: c15 }); - return { engine, c15, bus }; -} - -// ── Voice mode getter / setter ───────────────────────────────────── - -describe('voiceMode property', () => { - it('defaults to mono', () => { - const { engine } = createEngine(); - assert.equal(engine.voiceMode, 'mono'); - }); - - it('setVoiceMode("poly") changes mode', () => { - const { engine } = createEngine(); - engine.setVoiceMode('poly'); - assert.equal(engine.voiceMode, 'poly'); - }); - - it('setVoiceMode("mono") sets back to mono', () => { - const { engine } = createEngine(); - engine.setVoiceMode('poly'); - engine.setVoiceMode('mono'); - assert.equal(engine.voiceMode, 'mono'); - }); - - it('rejects invalid values', () => { - const { engine } = createEngine(); - assert.throws(() => engine.setVoiceMode('duophonic'), RangeError); - assert.throws(() => engine.setVoiceMode(''), RangeError); - assert.throws(() => engine.setVoiceMode(null), RangeError); - assert.throws(() => engine.setVoiceMode(undefined), RangeError); - }); - - it('no-ops if mode is already set', () => { - const { engine, c15 } = createEngine(); - // Trigger a note so _activeNotes is non-empty, then set same mode - // — _releaseAllNotes should NOT be called (no noteOff emitted). - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - c15.calls.length = 0; // clear - engine.setVoiceMode('mono'); // already mono - assert.equal(c15.calls.length, 0, 'no noteOff for same-mode set'); - }); -}); - -// ── Mono mode note handling ──────────────────────────────────────── - -describe('mono mode noteOn/noteOff', () => { - let engine, c15; - - beforeEach(() => { - ({ engine, c15 } = createEngine()); - // default is mono - }); - - it('sends noteOff for previous note before new noteOn', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - - // Expected: noteOn(60), noteOff(60), noteOn(64) - assert.equal(c15.calls.length, 3); - assert.deepEqual(c15.calls[0], { type: 'noteOn', note: 60, vel: 0.8 }); - assert.deepEqual(c15.calls[1], { type: 'noteOff', note: 60 }); - assert.deepEqual(c15.calls[2], { type: 'noteOn', note: 64, vel: 0.7 }); - }); - - it('first note has no preceding noteOff', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.5 }); - assert.equal(c15.calls.length, 1); - assert.deepEqual(c15.calls[0], { type: 'noteOn', note: 60, vel: 0.5 }); - }); - - it('_activeNotes contains only the latest note', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - // After second noteOn, only 64 should be active (60 was killed) - assert.equal(engine._activeNotes.size, 1); - assert.ok(engine._activeNotes.has(64)); - }); -}); - -// ── Poly mode note handling ──────────────────────────────────────── - -describe('poly mode noteOn/noteOff', () => { - let engine, c15; - - beforeEach(() => { - ({ engine, c15 } = createEngine()); - engine.setVoiceMode('poly'); - }); - - it('does NOT send noteOff before noteOn for different pitches', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - - // Expected: noteOn(60), noteOn(64) — no noteOff - assert.equal(c15.calls.length, 2); - assert.deepEqual(c15.calls[0], { type: 'noteOn', note: 60, vel: 0.8 }); - assert.deepEqual(c15.calls[1], { type: 'noteOn', note: 64, vel: 0.7 }); - }); - - it('tracks multiple active notes', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - engine._handleNoteOn({ midiNote: 67, velocity: 0.6 }); - - assert.equal(engine._activeNotes.size, 3); - assert.ok(engine._activeNotes.has(60)); - assert.ok(engine._activeNotes.has(64)); - assert.ok(engine._activeNotes.has(67)); - }); - - it('retrigger: sends noteOff then noteOn for same pitch', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 60, velocity: 0.6 }); - - // Expected: noteOn(60, 0.8), noteOff(60), noteOn(60, 0.6) - assert.equal(c15.calls.length, 3); - assert.deepEqual(c15.calls[0], { type: 'noteOn', note: 60, vel: 0.8 }); - assert.deepEqual(c15.calls[1], { type: 'noteOff', note: 60 }); - assert.deepEqual(c15.calls[2], { type: 'noteOn', note: 60, vel: 0.6 }); - }); - - it('noteOff removes specific note from _activeNotes', () => { - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - engine._handleNoteOff({ midiNote: 60, velocity: 0 }); - - assert.equal(engine._activeNotes.size, 1); - assert.ok(engine._activeNotes.has(64)); - assert.ok(!engine._activeNotes.has(60)); - }); -}); - -// ── releaseAllNotes ──────────────────────────────────────────────── - -describe('releaseAllNotes', () => { - it('releases all notes in poly mode', () => { - const { engine, c15 } = createEngine(); - engine.setVoiceMode('poly'); - - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - engine._handleNoteOn({ midiNote: 67, velocity: 0.6 }); - c15.calls.length = 0; - - engine._releaseAllNotes(); - - // Should have 3 noteOff calls - const noteOffs = c15.calls.filter(c => c.type === 'noteOff'); - assert.equal(noteOffs.length, 3); - assert.equal(engine._activeNotes.size, 0); - }); -}); - -// ── Mode switching cleans up ─────────────────────────────────────── - -describe('mode switching cleanup', () => { - it('releases active notes when switching from poly to mono', () => { - const { engine, c15 } = createEngine(); - engine.setVoiceMode('poly'); - engine._handleNoteOn({ midiNote: 60, velocity: 0.8 }); - engine._handleNoteOn({ midiNote: 64, velocity: 0.7 }); - c15.calls.length = 0; - - engine.setVoiceMode('mono'); - - const noteOffs = c15.calls.filter(c => c.type === 'noteOff'); - assert.equal(noteOffs.length, 2); - assert.equal(engine._activeNotes.size, 0); - }); -}); diff --git a/playground/js/synth/additive-engine.js b/playground/js/synth/additive-engine.js deleted file mode 100644 index b3ceec5..0000000 --- a/playground/js/synth/additive-engine.js +++ /dev/null @@ -1,14 +0,0 @@ -import { FaustEngineBase } from './faust-engine-base.js'; - -export class AdditiveEngine extends FaustEngineBase { - constructor() { - super({ - id: 'additive', - displayName: 'Additive', - wasmUrl: 'faust/additive.wasm', - jsonUrl: 'faust/additive.json', - workletUrl: 'faust/additive-processor.js', - processorName: 'additive-processor', - }); - } -} diff --git a/playground/js/synth/additive-presets.js b/playground/js/synth/additive-presets.js deleted file mode 100644 index 32981cc..0000000 --- a/playground/js/synth/additive-presets.js +++ /dev/null @@ -1,204 +0,0 @@ -// Additive synth presets for NISPS playground -// Each preset defines which params are active (ML-controlled) and which are -// muted at safe defaults. Param IDs match faustParamAddress() output from -// additive.json (e.g. '1_spectral_shape_00_h1_amp'). -// -// 3 tiers of progressive complexity across 5 presets. - -export const ADDITIVE_PRESETS = [ - // ====================================================================== - // TIER 1 — Beginner (~12-15 active params) - // ====================================================================== - { - id: 'additive-1', - name: '1.1', - tier: 1, - description: 'Spectral Basics — H1-H4 amplitudes, global ADSR, spectral tilt, level', - active: [ - '1_spectral_shape_00_h1_amp', - '1_spectral_shape_01_h2_amp', - '1_spectral_shape_02_h3_amp', - '1_spectral_shape_03_h4_amp', - '1_spectral_shape_11_spectral_tilt', - '2_temporal_00_attack', - '2_temporal_01_decay', - '2_temporal_02_sustain', - '2_temporal_03_release', - '5_master_00_level', - '5_master_04_saturation', - '3_phase_04_stereo_phase_spread', - ], - overrides: { - '1_spectral_shape_00_h1_amp': { min: 0.3, max: 1.0, curve: 0.55 }, - '1_spectral_shape_01_h2_amp': { min: 0.0, max: 0.8, curve: 0.45 }, - '1_spectral_shape_02_h3_amp': { min: 0.0, max: 0.7, curve: 0.4 }, - '1_spectral_shape_03_h4_amp': { min: 0.0, max: 0.6, curve: 0.4 }, - '1_spectral_shape_11_spectral_tilt': { min: 0.2, max: 0.8, curve: 0.5 }, - '2_temporal_00_attack': { min: 0.0, max: 0.7, curve: 0.35 }, - '2_temporal_01_decay': { min: 0.05, max: 0.6, curve: 0.4 }, - '2_temporal_02_sustain': { min: 0.2, max: 0.9, curve: 0.55 }, - '2_temporal_03_release': { min: 0.1, max: 0.7, curve: 0.45 }, - '5_master_00_level': { min: 0.3, max: 0.85, curve: 0.5 }, - '5_master_04_saturation': { min: 0.0, max: 0.4, curve: 0.3 }, - '3_phase_04_stereo_phase_spread': { min: 0.0, max: 0.5, curve: 0.4 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'additive-2', - name: '1.2', - tier: 1, - description: 'Formant Play — H1-H4, formant shaping, ADSR, level', - active: [ - '1_spectral_shape_00_h1_amp', - '1_spectral_shape_01_h2_amp', - '1_spectral_shape_02_h3_amp', - '1_spectral_shape_03_h4_amp', - '4_modulation_07_formant1_freq', - '4_modulation_08_formant2_freq', - '4_modulation_09_formant_depth', - '2_temporal_00_attack', - '2_temporal_01_decay', - '2_temporal_02_sustain', - '2_temporal_03_release', - '5_master_00_level', - '1_spectral_shape_11_spectral_tilt', - ], - overrides: { - '1_spectral_shape_00_h1_amp': { min: 0.3, max: 1.0, curve: 0.55 }, - '1_spectral_shape_01_h2_amp': { min: 0.0, max: 0.8, curve: 0.45 }, - '1_spectral_shape_02_h3_amp': { min: 0.0, max: 0.7, curve: 0.4 }, - '1_spectral_shape_03_h4_amp': { min: 0.0, max: 0.6, curve: 0.4 }, - '4_modulation_07_formant1_freq': { min: 0.05, max: 0.7, curve: 0.45 }, - '4_modulation_08_formant2_freq': { min: 0.1, max: 0.8, curve: 0.5 }, - '4_modulation_09_formant_depth': { min: 0.0, max: 0.8, curve: 0.4 }, - '2_temporal_00_attack': { min: 0.0, max: 0.6, curve: 0.35 }, - '2_temporal_01_decay': { min: 0.05, max: 0.5, curve: 0.4 }, - '2_temporal_02_sustain': { min: 0.3, max: 0.9, curve: 0.55 }, - '2_temporal_03_release': { min: 0.1, max: 0.65, curve: 0.45 }, - '5_master_00_level': { min: 0.3, max: 0.85, curve: 0.5 }, - '1_spectral_shape_11_spectral_tilt': { min: 0.2, max: 0.8, curve: 0.5 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 2 — Intermediate (~25-30 active params) - // ====================================================================== - { - id: 'additive-3', - name: '2.1', - tier: 2, - description: 'Harmonic Sculptor — all H amps, spectral tilt, inharmonicity, ADSR, brightness envelope, vibrato, level', - active: [ - // All individual harmonics + groups - '1_spectral_shape_00_h1_amp', - '1_spectral_shape_01_h2_amp', - '1_spectral_shape_02_h3_amp', - '1_spectral_shape_03_h4_amp', - '1_spectral_shape_04_h5_amp', - '1_spectral_shape_05_h6_amp', - '1_spectral_shape_06_h7_amp', - '1_spectral_shape_07_h8_amp', - '1_spectral_shape_08_h9_16_amp', - '1_spectral_shape_09_h17_32_amp', - '1_spectral_shape_10_h33_64_amp', - '1_spectral_shape_11_spectral_tilt', - '1_spectral_shape_12_inharmonicity', - // Amplitude ADSR - '2_temporal_00_attack', - '2_temporal_01_decay', - '2_temporal_02_sustain', - '2_temporal_03_release', - // Brightness ADSR - '2_temporal_04_brightness_attack', - '2_temporal_05_brightness_decay', - '2_temporal_06_brightness_sustain', - '2_temporal_07_brightness_release', - // Vibrato - '4_modulation_00_vibrato_rate', - '4_modulation_01_vibrato_depth', - '4_modulation_02_vibrato_delay', - // Master - '5_master_00_level', - '5_master_04_saturation', - ], - overrides: { - '1_spectral_shape_12_inharmonicity': { min: 0.0, max: 0.5, curve: 0.3 }, - '4_modulation_01_vibrato_depth': { min: 0.0, max: 0.6, curve: 0.35 }, - '5_master_04_saturation': { min: 0.0, max: 0.5, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'additive-4', - name: '2.2', - tier: 2, - description: 'Phase Explorer — H1-H8, phase randomisation, beating, stereo spread, noise floor, ADSR, formants, level', - active: [ - // H1-H8 - '1_spectral_shape_00_h1_amp', - '1_spectral_shape_01_h2_amp', - '1_spectral_shape_02_h3_amp', - '1_spectral_shape_03_h4_amp', - '1_spectral_shape_04_h5_amp', - '1_spectral_shape_05_h6_amp', - '1_spectral_shape_06_h7_amp', - '1_spectral_shape_07_h8_amp', - // Phase controls - '3_phase_00_phase_random', - '3_phase_02_beating_depth', - '3_phase_03_beating_rate', - '3_phase_04_stereo_phase_spread', - '3_phase_05_noise_floor', - '3_phase_06_noise_color', - // Amplitude ADSR - '2_temporal_00_attack', - '2_temporal_01_decay', - '2_temporal_02_sustain', - '2_temporal_03_release', - // Formants - '4_modulation_07_formant1_freq', - '4_modulation_08_formant2_freq', - '4_modulation_09_formant_depth', - // Odd/even - '1_spectral_shape_13_odd_even', - // Master - '5_master_00_level', - '5_master_04_saturation', - '3_phase_07_sub_harmonic', - ], - overrides: { - '3_phase_05_noise_floor': { min: 0.0, max: 0.6, curve: 0.3 }, - '3_phase_02_beating_depth': { min: 0.0, max: 0.6, curve: 0.3 }, - '5_master_04_saturation': { min: 0.0, max: 0.5, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 3 — Advanced (all 48 params) - // ====================================================================== - { - id: 'additive-5', - name: '3.1', - tier: 3, - description: 'Full Spectrum — all 48 params active, no restrictions', - active: null, // null = all params active - overrides: {}, - mutedOverrides: {}, - groupCurves: {}, - }, -]; - -export const ADDITIVE_PRESET_TIERS = [ - { tier: 1, label: 'Beginner' }, - { tier: 2, label: 'Intermediate' }, - { tier: 3, label: 'Advanced' }, -]; diff --git a/playground/js/synth/arpeggiator-worker.js b/playground/js/synth/arpeggiator-worker.js deleted file mode 100644 index ca9f498..0000000 --- a/playground/js/synth/arpeggiator-worker.js +++ /dev/null @@ -1,166 +0,0 @@ -// Arpeggiator Worker — runs note scheduling on a dedicated thread -// for reliable timing independent of main thread load. -// Posts noteOn/noteOff messages back to the main thread, which forwards them -// through the active SynthEngine. This decouples the worker from C15's -// SharedArrayBuffer ring buffer so any engine can be used. - -// Cmin7 inversions: root [0,3,7,10], 1st [3,7,10,12], 2nd [7,10,12,15], 3rd [10,12,15,19] -const CMIN7_INVERSIONS = [ - [0, 3, 7, 10], - [3, 7, 10, 12], - [7, 10, 12, 15], - [10, 12, 15, 19], -]; - -const PROGRESSIONS = { - 'Cmin7-rand': null, // special case: random inversions - 'I-vi-IV-V': [[0,4,7],[9,12,16],[5,9,12],[7,11,14]], - 'I-IV-vi-V': [[0,4,7],[5,9,12],[9,12,16],[7,11,14]], - 'i-VI-III-VII': [[0,3,7],[8,12,15],[3,7,10],[10,14,17]], - 'I-V-vi-IV': [[0,4,7],[7,11,14],[9,12,16],[5,9,12]], -}; - -// Arpeggiator state -let playing = false; -let bpm = 120; -let octaves = 2; -let octaveOffset = 0; -let progression = 'Cmin7-rand'; -let direction = 'updown'; // 'up' or 'updown' -let chordIndex = 0; -let noteIndex = 0; -let ascending = true; // for updown mode -let currentInversion = null; // cached Cmin7 inversion for current cycle -let lastNote = -1; -let timer = null; - -function scheduleNext() { - if (!playing) return; - const msPerBeat = 60000 / bpm; - const noteDuration = msPerBeat / 4; // 16th notes - playNextNote(); - timer = setTimeout(scheduleNext, noteDuration); -} - -function getChordNotes() { - const baseNote = 48 + (octaveOffset * 12); - - if (progression === 'Cmin7-rand') { - // Pick a random inversion once per cycle, cache until next chord advance - if (!currentInversion) { - currentInversion = CMIN7_INVERSIONS[Math.floor(Math.random() * CMIN7_INVERSIONS.length)]; - } - const inv = currentInversion; - const notes = []; - for (let oct = 0; oct < octaves; oct++) { - for (const interval of inv) { - const note = baseNote + interval + (oct * 12); - if (note >= 0 && note <= 127) notes.push(note); - } - } - return notes; - } - - const chords = PROGRESSIONS[progression] || PROGRESSIONS['I-vi-IV-V']; - const chord = chords[chordIndex]; - const notes = []; - for (let oct = 0; oct < octaves; oct++) { - for (const interval of chord) { - const note = baseNote + interval + (oct * 12); - if (note >= 0 && note <= 127) notes.push(note); - } - } - return notes; -} - -function advanceIndex(notes) { - const chords = PROGRESSIONS[progression]; - const chordCount = (progression === 'Cmin7-rand') ? 1 : (chords ? chords.length : 1); - - if (direction === 'updown' && notes.length > 1) { - if (ascending) { - noteIndex++; - if (noteIndex >= notes.length) { - ascending = false; - noteIndex = notes.length - 2; // bounce back, skip the top note - } - } else { - noteIndex--; - if (noteIndex <= 0) { - ascending = true; - noteIndex = 0; - chordIndex = (chordIndex + 1) % chordCount; - currentInversion = null; // pick new random inversion next cycle - } - } - } else { - // up only - noteIndex++; - if (noteIndex >= notes.length) { - noteIndex = 0; - chordIndex = (chordIndex + 1) % chordCount; - currentInversion = null; // pick new random inversion next cycle - } - } -} - -function playNextNote() { - // Release previous note - if (lastNote >= 0) { - postMessage({ type: 'noteOff', data: { note: lastNote } }); - } - - const notes = getChordNotes(); - if (notes.length === 0) return; - - const note = notes[Math.min(noteIndex, notes.length - 1)]; - const velocity = 0.6 + Math.random() * 0.2; - postMessage({ type: 'noteOn', data: { note, velocity } }); - lastNote = note; - - advanceIndex(notes); -} - -function start() { - if (playing) return; - playing = true; - chordIndex = 0; - noteIndex = 0; - ascending = true; - currentInversion = null; - scheduleNext(); - postMessage({ type: 'state', data: { playing: true } }); -} - -function stop() { - playing = false; - if (timer) { - clearTimeout(timer); - timer = null; - } - if (lastNote >= 0) { - postMessage({ type: 'noteOff', data: { note: lastNote } }); - lastNote = -1; - } - postMessage({ type: 'state', data: { playing: false } }); -} - -// Handle messages from main thread -self.onmessage = (e) => { - const { type, data } = e.data; - switch (type) { - case 'start': - start(); - break; - case 'stop': - stop(); - break; - case 'set': - if (data && 'bpm' in data) bpm = data.bpm; - if (data && 'octaves' in data) octaves = data.octaves; - if (data && 'octaveOffset' in data) octaveOffset = data.octaveOffset; - if (data && 'progression' in data) progression = data.progression; - if (data && 'direction' in data) direction = data.direction; - break; - } -}; diff --git a/playground/js/synth/arpeggiator.js b/playground/js/synth/arpeggiator.js deleted file mode 100644 index c4bfb94..0000000 --- a/playground/js/synth/arpeggiator.js +++ /dev/null @@ -1,120 +0,0 @@ -// Arpeggiator — plays chord progressions through any SynthEngine. -// Delegates to a dedicated Web Worker for reliable timing. -// The worker posts noteOn/noteOff messages back to the main thread, which -// forwards them through the active SynthEngine. This keeps the arpeggiator -// decoupled from C15's SharedArrayBuffer ring buffer so any engine works. - -export class Arpeggiator { - /** - * @param {import('./engine-interface.js').SynthEngine} engine - */ - constructor(engine) { - this.engine = engine; - this._worker = null; - this._playing = false; - this._bpm = 120; - this._octaves = 2; - this._octaveOffset = 0; - this._progression = 'Cmin7-rand'; - this._direction = 'updown'; - } - - get progressionNames() { - return ['Cmin7-rand', 'I-vi-IV-V', 'I-IV-vi-V', 'i-VI-III-VII', 'I-V-vi-IV']; - } - - get directionNames() { - return ['up', 'updown']; - } - - get playing() { return this._playing; } - - get bpm() { return this._bpm; } - set bpm(v) { - this._bpm = v; - this._send('set', { bpm: v }); - } - - get octaves() { return this._octaves; } - set octaves(v) { - this._octaves = v; - this._send('set', { octaves: v }); - } - - get octaveOffset() { return this._octaveOffset; } - set octaveOffset(v) { - this._octaveOffset = v; - this._send('set', { octaveOffset: v }); - } - - get progression() { return this._progression; } - set progression(v) { - this._progression = v; - this._send('set', { progression: v }); - } - - get direction() { return this._direction; } - set direction(v) { - this._direction = v; - this._send('set', { direction: v }); - } - - _ensureWorker() { - if (this._worker) return; - - this._worker = new Worker( - new URL('./arpeggiator-worker.js', import.meta.url), - { type: 'module' } - ); - - this._worker.onmessage = (e) => { - const { type, data } = e.data; - switch (type) { - case 'state': - this._playing = data.playing; - break; - case 'noteOn': - this.engine.noteOn(data.note, data.velocity); - break; - case 'noteOff': - this.engine.noteOff(data.note); - break; - } - }; - - // Sync current settings to worker - this._send('set', { - bpm: this._bpm, - octaves: this._octaves, - octaveOffset: this._octaveOffset, - progression: this._progression, - direction: this._direction, - }); - } - - _send(type, data) { - if (this._worker) { - this._worker.postMessage({ type, data }); - } - } - - start() { - this._ensureWorker(); - this._send('start'); - this._playing = true; // optimistic, worker confirms - } - - stop() { - this._send('stop'); - this._playing = false; - } - - /** - * Hot-swap the engine without restarting the worker. - * Called by setActiveEngine() in a-app.js. - * @param {import('./engine-interface.js').SynthEngine} engine - */ - setEngine(engine) { - this.engine = engine; - } -} diff --git a/playground/js/synth/c15-adapter.js b/playground/js/synth/c15-adapter.js deleted file mode 100644 index 051abbe..0000000 --- a/playground/js/synth/c15-adapter.js +++ /dev/null @@ -1,175 +0,0 @@ -// C15Adapter — wraps C15Bridge to satisfy the SynthEngine interface. -// -// All C15-specific internals (SharedArrayBuffer, ring buffer, AudioWorklet) -// remain inside C15Bridge. This adapter is the only file outside the synth/ -// directory that needs to know about C15Bridge. - -import { SynthEngine } from './engine-interface.js'; -import { C15Bridge } from './c15-bridge.js'; -import { SYNTH_PARAM_MAP } from './param-map.js'; - -// --------------------------------------------------------------------------- -// Derive paramMeta from SYNTH_PARAM_MAP -// --------------------------------------------------------------------------- - -// Infer a human-readable group name from the parameter's machine name. -// e.g. 'Env_A_Att' → 'Envelope A', 'SV_Flt_Cut' → 'SVF', 'Cabinet_Drive' → 'Cabinet' -function _inferGroup(name) { - if (name.startsWith('Env_A')) return 'Envelope A'; - if (name.startsWith('Env_B')) return 'Envelope B'; - if (name.startsWith('Env_C')) return 'Envelope C'; - if (name.startsWith('Osc_A')) return 'Oscillator A'; - if (name.startsWith('Osc_B')) return 'Oscillator B'; - if (name.startsWith('Shp_A')) return 'Shaper A'; - if (name.startsWith('Shp_B')) return 'Shaper B'; - if (name.startsWith('Comb_Flt')) return 'Comb Filter'; - if (name.startsWith('SV_Flt')) return 'SVF'; - if (name.startsWith('Gap_Flt')) return 'Gap Filter'; - if (name.startsWith('FB_Mix')) return 'Feedback Mixer'; - if (name.startsWith('Out_Mix')) return 'Output Mixer'; - if (name.startsWith('Cabinet')) return 'Cabinet'; - if (name.startsWith('Flanger')) return 'Flanger'; - if (name.startsWith('Echo')) return 'Echo'; - if (name.startsWith('Reverb')) return 'Reverb'; - if (name.startsWith('Unison')) return 'Unison'; - if (name.startsWith('Mono')) return 'Mono'; - return 'Other'; -} - -// Build paramMeta once at module load time — no per-call allocation. -const C15_PARAM_META = SYNTH_PARAM_MAP.map(p => ({ - id: p.name, // stable machine ID (e.g. 'Env_A_Att') - name: p.label, // short display label (e.g. 'EnvA Att') - min: p.safeMin !== undefined ? p.safeMin : 0, - max: p.safeMax !== undefined ? p.safeMax : 1, - init: p.defaultValue, - curve: 0.5, // linear by default; presets can override - group: _inferGroup(p.name), -})); - -// --------------------------------------------------------------------------- -// C15Adapter -// --------------------------------------------------------------------------- - -export class C15Adapter extends SynthEngine { - constructor() { - super(); - this._bridge = new C15Bridge(); - this._running = false; - } - - // --- Identity --- - - get id() { return 'shaper-feedback'; } - get displayName() { return 'C15 Shaper-Feedback'; } - - // --- Parameter schema --- - - get paramMeta() { return C15_PARAM_META; } - // paramCount is derived from paramMeta.length via the base class - - // --- Lifecycle --- - - /** - * Initialise the C15 engine. - * The AudioContext is created internally by C15Bridge.start(), so the - * audioCtx parameter is accepted for interface compatibility but not used. - * - * @param {AudioContext|null} _audioCtx — ignored; C15Bridge creates its own - */ - async init(_audioCtx) { - // Forward status callbacks before starting so callers can observe progress - this._bridge.onStatusChange = (msg) => { - this._onStatusChange?.(msg); - // Mirror the running flag — C15Bridge sets this.running internally - this._running = this._bridge.running; - }; - - await this._bridge.loadParams(); - await this._bridge.start(); - this._running = this._bridge.running; - } - - /** Stop audio and release the AudioContext. */ - async stop() { - await this._bridge.stop(); - this._running = this._bridge.running; - } - - dispose() { - this._bridge.panic(); - // C15Bridge has no explicit destroy; AudioContext will be GC'd - this._running = false; - } - - // --- Status callback (optional, assigned by a-app.js) --- - - set onStatusChange(fn) { this._onStatusChange = fn; } - - // --- running passthrough --- - get running() { return this._running; } - - // --- Real-time control --- - - /** - * Set a parameter by MLP output index (0-based position in SYNTH_PARAM_MAP). - * Converts the index to the C15 hardware param ID before writing. - * - * @param {number} index — MLP output index [0, paramCount) - * @param {number} normalizedValue — [0, 1] - */ - setParam(index, normalizedValue) { - const entry = SYNTH_PARAM_MAP[index]; - if (!entry) return; - this._bridge.setParameter(entry.id, normalizedValue); - } - - /** - * Trigger a note. - * @param {number} note — MIDI note number 0–127 - * @param {number} velocity — [0, 1] - */ - noteOn(note, velocity = 0.7) { - this._bridge.noteOn(note, velocity); - } - - /** - * Release a note. - * @param {number} note — MIDI note number 0–127 - */ - noteOff(note) { - this._bridge.noteOff(note); - } - - // --- Audio graph --- - - /** - * Return the limiter node — the last AudioNode before destination. - * This is the correct insertion point for post-processing (e.g. EOCChain). - * Falls back to masterGain before start() is called. - * Only useful after init() completes. - * - * @returns {AudioNode} - */ - getOutputNode() { - return this._bridge.limiterNode ?? this._bridge.masterGain; - } - - // --- C15-specific passthrough (for MIDIInput and volume controls) --- - - /** Pass-through for MIDIInput which was constructed with the bridge */ - get bridge() { return this._bridge; } - - setMasterVolume(value) { this._bridge.setMasterVolume(value); } - panic() { this._bridge.panic(); } - - /** - * The SharedArrayBuffer from the ring buffer. Exposed so Arpeggiator can - * hand it off to its worker. After the engine-interface migration is complete - * the arpeggiator no longer needs direct SAB access — this getter exists only - * for backward compatibility during transition. - * - * @returns {SharedArrayBuffer|null} - */ - get sharedBuffer() { return this._bridge.sharedBuffer; } -} diff --git a/playground/js/synth/c15-bridge.js b/playground/js/synth/c15-bridge.js deleted file mode 100644 index c0e440a..0000000 --- a/playground/js/synth/c15-bridge.js +++ /dev/null @@ -1,245 +0,0 @@ -// C15 WASM synth engine bridge -// Handles WASM loading, AudioWorklet setup, ring buffer communication - -const CONFIG = { - WASM_PATH: 'c15/c15_engine.wasm', - WORKLET_PATH: 'c15/worklet-processor.js', - PARAMS_PATH: 'c15/parameters.json', - SAMPLE_RATE: 48000, - POLYPHONY: 24, -}; - -const MESSAGE_TYPE = { PARAMETER: 0, NOTE_ON: 1, NOTE_OFF: 2 }; -const HEADER_SIZE = 3; -const MESSAGE_SIZE = 4; -const RING_CAPACITY = 512; - -// Multi-producer-safe ring buffer writer using CAS (compare-and-swap). -// Safe for concurrent writes from main thread + arpeggiator Worker. -class RingBufferWriter { - constructor(sharedBuffer) { - this._sab = sharedBuffer; - this._buffer = new Float32Array(sharedBuffer); - this._int32 = new Int32Array(sharedBuffer); - } - - write(type, id, value) { - // CAS loop: claim a slot atomically - for (let attempt = 0; attempt < 4; attempt++) { - const writeIdx = Atomics.load(this._int32, 0); - const readIdx = Atomics.load(this._int32, 1); - const next = (writeIdx + 1) % RING_CAPACITY; - if (next === readIdx) return false; // full - - // Try to claim this slot - if (Atomics.compareExchange(this._int32, 0, writeIdx, next) === writeIdx) { - // Won the slot — write data - const off = HEADER_SIZE + writeIdx * MESSAGE_SIZE; - this._buffer[off] = type; - this._buffer[off + 1] = id; - this._buffer[off + 2] = value; - this._buffer[off + 3] = 0; - Atomics.add(this._int32, 2, 1); - return true; - } - // Another writer took it — retry - } - return false; // contention too high - } - - writeParameter(paramId, value) { - return this.write(MESSAGE_TYPE.PARAMETER, paramId, value); - } - writeNoteOn(note, velocity) { - return this.write(MESSAGE_TYPE.NOTE_ON, note, velocity); - } - writeNoteOff(note, velocity) { - return this.write(MESSAGE_TYPE.NOTE_OFF, note, velocity || 0); - } - - /** Expose the SharedArrayBuffer for passing to Workers */ - get sharedBuffer() { return this._sab; } -} - -export class C15Bridge { - constructor() { - this.audioContext = null; - this.workletNode = null; - this.masterGain = null; - this.ringWriter = null; - this.running = false; - this.ready = false; - this.allParams = null; - this.activeNotes = new Set(); - this._sab = null; // SharedArrayBuffer, exposed after start() - this._onStatusChange = null; - } - - set onStatusChange(fn) { this._onStatusChange = fn; } - - /** SharedArrayBuffer for the ring buffer — available after start() */ - get sharedBuffer() { return this._sab; } - - /** - * The limiter node — the last AudioNode before destination. - * Use this as the input to any post-processing chain (e.g. EOCChain). - * Only available after start(). - * @returns {DynamicsCompressorNode|null} - */ - get limiterNode() { return this.limiter ?? null; } - - _status(msg) { - console.log('[C15]', msg); - this._onStatusChange?.(msg); - } - - async loadParams() { - if (this.allParams) return this.allParams; - try { - const res = await fetch(CONFIG.PARAMS_PATH); - if (!res.ok) throw new Error(`${res.status}`); - const data = await res.json(); - this.allParams = data.parameters; - } catch (err) { - console.warn('[C15] Failed to load parameters.json:', err.message); - this.allParams = []; - } - return this.allParams; - } - - async start() { - if (this.running) return; - - try { - if (typeof SharedArrayBuffer === 'undefined') { - throw new Error('SharedArrayBuffer not available — serve with COOP/COEP headers (use serve.sh)'); - } - - this._status('Compiling WASM...'); - const resp = await fetch(CONFIG.WASM_PATH); - if (!resp.ok) throw new Error(`Failed to fetch WASM: ${resp.status}`); - const wasmBytes = await resp.arrayBuffer(); - const wasmModule = await WebAssembly.compile(wasmBytes); - - const AC = window.AudioContext || window.webkitAudioContext; - this.audioContext = new AC({ - sampleRate: CONFIG.SAMPLE_RATE, - latencyHint: 'interactive', - }); - - // Ring buffer (passed via processorOptions — worklet reads it immediately) - const sabSize = (HEADER_SIZE + RING_CAPACITY * MESSAGE_SIZE) * 4; - const sab = new SharedArrayBuffer(sabSize); - new Float32Array(sab).fill(0); - this._sab = sab; - this.ringWriter = new RingBufferWriter(sab); - - // Load worklet - this._status('Loading AudioWorklet...'); - await this.audioContext.audioWorklet.addModule(CONFIG.WORKLET_PATH); - - this.workletNode = new AudioWorkletNode(this.audioContext, 'c15-processor', { - processorOptions: { - sampleRate: CONFIG.SAMPLE_RATE, - polyphony: CONFIG.POLYPHONY, - wasmModule, - ringBuffer: sab, - }, - numberOfInputs: 0, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - - this.workletNode.port.onmessage = (e) => this._handleWorkletMsg(e.data); - - this.masterGain = this.audioContext.createGain(); - this.masterGain.gain.value = 0.5; - - // Limiter: always-on compressor configured as a brick-wall limiter - this.limiter = this.audioContext.createDynamicsCompressor(); - this.limiter.threshold.value = -6; // dB — start limiting at -6dB - this.limiter.knee.value = 3; // dB — soft knee for less pumping - this.limiter.ratio.value = 20; // near-infinite ratio = limiter - this.limiter.attack.value = 0.002; // 2ms — fast attack catches transients - this.limiter.release.value = 0.05; // 50ms — quick release, less pumping - - this.workletNode.connect(this.masterGain); - this.masterGain.connect(this.limiter); - this.limiter.connect(this.audioContext.destination); - - if (this.audioContext.state === 'suspended') { - await this.audioContext.resume(); - } - - this.running = true; - this._status('Running'); - } catch (err) { - this._status(`Error: ${err.message}`); - console.error('[C15] Start failed:', err); - } - } - - async stop() { - if (!this.running) return; - this.panic(); - if (this.audioContext) { - await this.audioContext.suspend(); - } - this.running = false; - this.ready = false; - this._status('Stopped'); - } - - _handleWorkletMsg(data) { - if (data.type === 'status' && data.status === 'ready') { - this.ready = true; - this._status('WASM ready'); - this._sendAllDefaults(); - } - } - - _sendAllDefaults() { - if (!this.allParams || !this.ringWriter) return; - for (const p of this.allParams) { - if (!p.valid) continue; - this.ringWriter.writeParameter(p.id, Math.max(0, Math.min(1, p.defaultValue))); - } - // Set some initial sound: Osc A output level up, envelope sustain - this.ringWriter.writeParameter(169, 0.75); // Out_Mix_A_Lvl - this.ringWriter.writeParameter(8, 0.4); // Env_A_Sus - this.ringWriter.writeParameter(0, 0.1); // Env_A_Att (quick) - this.ringWriter.writeParameter(10, 0.5); // Env_A_Rel - this.ringWriter.writeParameter(241, 0.15); // Reverb_Mix - this.ringWriter.writeParameter(233, 0.08); // Echo_Mix - } - - setParameter(paramId, value) { - if (!this.ringWriter || !this.running) return; - this.ringWriter.writeParameter(paramId, Math.max(0, Math.min(1, value))); - } - - noteOn(note, velocity = 0.7) { - if (!this.ringWriter || !this.running) return; - this.ringWriter.writeNoteOn(note, velocity); - this.activeNotes.add(note); - } - - noteOff(note) { - if (!this.ringWriter || !this.running) return; - this.ringWriter.writeNoteOff(note, 0); - this.activeNotes.delete(note); - } - - panic() { - for (const note of this.activeNotes) { - this.ringWriter?.writeNoteOff(note, 0); - } - this.activeNotes.clear(); - } - - setMasterVolume(value) { - if (this.masterGain) { - this.masterGain.gain.value = value; - } - } -} diff --git a/playground/js/synth/engine-interface.js b/playground/js/synth/engine-interface.js deleted file mode 100644 index 728b6d6..0000000 --- a/playground/js/synth/engine-interface.js +++ /dev/null @@ -1,145 +0,0 @@ -// SynthEngine — base interface that all synth engines implement. -// -// Engines are duck-typed in JavaScript, but this base class documents the -// contract and provides default no-op stubs so subclasses only override what -// they need. All engines must satisfy this interface for hot-swapping to work. -// -// Usage: -// import { SynthEngine } from './engine-interface.js'; -// class MyEngine extends SynthEngine { ... } - -export class SynthEngine { - // --------------------------------------------------------------------------- - // Identity (override in subclass) - // --------------------------------------------------------------------------- - - /** - * Stable machine ID, e.g. 'shaper-feedback' | 'additive' | 'fm'. - * Used for persistence keys and engine-switcher logic. - * @returns {string} - */ - get id() { - throw new Error(`${this.constructor.name}: id not implemented`); - } - - /** - * Human-readable label shown in the engine-switcher UI. - * @returns {string} - */ - get displayName() { - throw new Error(`${this.constructor.name}: displayName not implemented`); - } - - // --------------------------------------------------------------------------- - // Parameter schema - // --------------------------------------------------------------------------- - - /** - * Number of continuous parameters this engine exposes (= MLP output count). - * @returns {number} - */ - get paramCount() { - return this.paramMeta.length; - } - - /** - * Array of parameter descriptors, one per MLP output index. - * - * Each entry must have: - * id {string} — stable machine ID (used for presets) - * name {string} — short display name - * min {number} — normalised lower bound [0,1] - * max {number} — normalised upper bound [0,1] - * init {number} — default normalised value [0,1] - * curve {number} — power-curve bias: 0.5 = linear, <0.5 = log, >0.5 = exp - * group {string} — section label (for group drawer / colour coding) - * - * @returns {Array<{id:string, name:string, min:number, max:number, init:number, curve:number, group:string}>} - */ - get paramMeta() { - throw new Error(`${this.constructor.name}: paramMeta not implemented`); - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - /** - * Load WASM / worklet, connect the output node to audioCtx.destination. - * Must be idempotent — calling init() twice should be safe. - * - * @param {AudioContext} audioCtx - * @returns {Promise} - */ - async init(audioCtx) { // eslint-disable-line no-unused-vars - throw new Error(`${this.constructor.name}: init() not implemented`); - } - - /** - * Release all AudioNodes, Workers, and WASM memory. - * The engine should be unusable after dispose(). - */ - dispose() { - // default no-op — override when cleanup is needed - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - /** - * Set a single parameter by MLP output index. - * - * @param {number} index — 0-based index into paramMeta - * @param {number} normalizedValue — [0, 1] - */ - setParam(index, normalizedValue) { // eslint-disable-line no-unused-vars - // default no-op - } - - /** - * Trigger a note. - * - * @param {number} note — MIDI note number 0–127 - * @param {number} velocity — [0, 1] - */ - noteOn(note, velocity = 0.7) { // eslint-disable-line no-unused-vars - // default no-op - } - - /** - * Release a note. - * - * @param {number} note — MIDI note number 0–127 - */ - noteOff(note) { // eslint-disable-line no-unused-vars - // default no-op - } - - // --------------------------------------------------------------------------- - // Audio graph - // --------------------------------------------------------------------------- - - /** - * Return the AudioNode that should be connected downstream (e.g. to a - * compressor or the AudioContext destination). - * - * @returns {AudioNode} - */ - getOutputNode() { - throw new Error(`${this.constructor.name}: getOutputNode() not implemented`); - } - - // --------------------------------------------------------------------------- - // Status (mirrors C15Bridge.running for backward compat) - // --------------------------------------------------------------------------- - - /** - * True once init() has completed successfully and the engine is producing - * audio. Engines should set this themselves after init(). - * @type {boolean} - */ - get running() { - return this._running ?? false; - } -} diff --git a/playground/js/synth/faust-engine-base.js b/playground/js/synth/faust-engine-base.js deleted file mode 100644 index 788929e..0000000 --- a/playground/js/synth/faust-engine-base.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * faust-engine-base.js — Base class for Faust WASM synth engines - * - * Handles the common lifecycle for engines compiled from Faust .dsp files: - * 1. Fetch the .json descriptor → build paramMeta via faustJsonToParamMeta() - * 2. Load the .wasm binary as an AudioWorkletNode - * 3. Wire setParam / noteOn / noteOff through worklet messages - * - * Usage: - * class AdditiveEngine extends FaustEngineBase { - * constructor() { - * super({ - * id: 'additive', - * displayName: 'Additive', - * wasmUrl: 'faust/additive.wasm', - * jsonUrl: 'faust/additive.json', - * workletUrl: 'faust/additive-processor.js', - * processorName: 'additive-processor', - * }); - * } - * } - * - * const engine = new AdditiveEngine(); - * await engine.init(audioCtx); - * engine.noteOn(220, 0.8); - * engine.setParam(0, 0.5); - */ - -import { SynthEngine } from './engine-interface.js'; -import { loadFaustParamMeta } from './faust-param-meta.js'; - -export class FaustEngineBase extends SynthEngine { - /** - * @param {object} opts - * @param {string} opts.id Short unique id (e.g. 'additive') - * @param {string} opts.displayName Human-readable name - * @param {string} opts.wasmUrl URL to the .wasm binary (faust -lang wasm output) - * @param {string} opts.jsonUrl URL to the Faust .json descriptor - * @param {string} opts.workletUrl URL to the AudioWorklet processor JS file - * @param {string} opts.processorName Name passed to registerProcessor() in workletUrl - */ - constructor({ id, displayName, wasmUrl, jsonUrl, workletUrl, processorName } = {}) { - super(); - this._id = id ?? 'faust-engine'; - this._displayName = displayName ?? 'Faust Engine'; - this._wasmUrl = wasmUrl ?? null; - this._jsonUrl = jsonUrl ?? null; - this._workletUrl = workletUrl ?? null; - this._processorName = processorName ?? null; - - this._paramMeta = []; // populated in init() - this._audioCtx = null; - this._workletNode = null; - this._masterGain = null; - this._running = false; - this._onReady = null; // internal ready-wait callback - this._outputNode = null; - } - - // --------------------------------------------------------------------------- - // SynthEngine identity - // --------------------------------------------------------------------------- - - get id() { return this._id; } - get displayName() { return this._displayName; } - get paramMeta() { return this._paramMeta; } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - /** - * Eagerly load ONLY the JSON descriptor (no WASM, no AudioContext needed). - * Safe to call multiple times — no-ops if paramMeta is already populated. - * This ensures paramCount is available before init() for MLP sizing. - */ - async loadParamMeta() { - if (this._paramMeta.length > 0) return; - this._paramMeta = await loadFaustParamMeta(this._jsonUrl); - } - - /** - * Initialise the engine: fetch JSON → build paramMeta → load WASM worklet. - * - * @param {AudioContext} audioCtx A running (or suspended) AudioContext. - * @returns {Promise} - */ - async init(audioCtx) { - if (this._running) return; - if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - this._audioCtx = audioCtx; - - // Step 1: fetch and parse the Faust JSON descriptor → paramMeta - // (skip if already loaded eagerly via loadParamMeta()) - if (this._paramMeta.length === 0) { - this._paramMeta = await loadFaustParamMeta(this._jsonUrl); - } - - // Step 2: fetch the WASM binary - const wasmResp = await fetch(this._wasmUrl); - if (!wasmResp.ok) { - throw new Error(`[FaustEngineBase:${this._id}] Failed to fetch WASM: ${wasmResp.status}`); - } - const wasmBytes = await wasmResp.arrayBuffer(); - - // Step 3: register the AudioWorklet module (browser deduplicates) - await audioCtx.audioWorklet.addModule(this._workletUrl); - - // Step 4: create the AudioWorkletNode - this._workletNode = new AudioWorkletNode(audioCtx, this._processorName, { - numberOfInputs: 0, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - - // Step 5: listen for worklet → main thread messages - this._workletNode.port.onmessage = (e) => this._handleWorkletMsg(e.data); - - // Step 6: connect to audio graph through a master gain node - this._masterGain = audioCtx.createGain(); - this._masterGain.gain.value = 0.7; - this._workletNode.connect(this._masterGain); - this._masterGain.connect(audioCtx.destination); - this._outputNode = this._masterGain; - - // Step 7: send init message — transfer ownership of wasmBytes to avoid copy - this._workletNode.port.postMessage( - { type: 'init', wasmBytes, sampleRate: audioCtx.sampleRate }, - [wasmBytes] - ); - - // Wait for worklet to confirm readiness (10 s timeout) - await this._waitForReady(10_000); - } - - /** Return the output AudioNode (for downstream routing). */ - getOutputNode() { - return this._outputNode; - } - - /** Stop audio output (mute, but keep worklet alive for quick restart). */ - async stop() { - if (this._masterGain) { - this._masterGain.gain.setTargetAtTime(0, this._audioCtx.currentTime, 0.01); - } - this._running = false; - } - - /** Restart audio output after stop(). */ - async start() { - if (this._masterGain && this._audioCtx) { - this._masterGain.gain.setTargetAtTime(0.7, this._audioCtx.currentTime, 0.01); - } - this._running = true; - } - - /** Release all resources. */ - dispose() { - if (this._masterGain) { - this._masterGain.disconnect(); - this._masterGain = null; - } - if (this._workletNode) { - this._workletNode.disconnect(); - this._workletNode.port.onmessage = null; - this._workletNode = null; - } - this._outputNode = null; - this._running = false; - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - /** - * Set a parameter by index. - * normalizedValue is [0, 1] and is mapped to the param's [min, max] range. - * - * @param {number} index Index into paramMeta - * @param {number} normalizedValue 0–1 - */ - setParam(index, normalizedValue) { - if (!this._workletNode) return; - const meta = this._paramMeta[index]; - if (!meta) return; - const raw = meta.min + normalizedValue * (meta.max - meta.min); - this._workletNode.port.postMessage({ type: 'setParam', index, value: raw }); - } - - /** - * Trigger a note. - * @param {number} note MIDI note number 0–127 (converts to Hz internally) - * @param {number} vel Velocity 0–1 (default 0.7) - */ - noteOn(note, vel = 0.7) { - if (!this._workletNode) return; - const freq = 440 * Math.pow(2, (note - 69) / 12); - this._workletNode.port.postMessage({ type: 'noteOn', freq, vel }); - } - - /** - * Release a note. - * @param {number} note MIDI note number 0–127 - */ - noteOff(note) { - if (!this._workletNode) return; - const freq = 440 * Math.pow(2, (note - 69) / 12); - this._workletNode.port.postMessage({ type: 'noteOff', freq }); - } - - // --------------------------------------------------------------------------- - // Internal helpers - // --------------------------------------------------------------------------- - - _handleWorkletMsg(data) { - if (!data) return; - if (data.type === 'ready') { - this._running = true; - this._onReady?.(); - } else if (data.type === 'error') { - console.error(`[FaustEngineBase:${this._id}] Worklet error:`, data.message); - } - } - - _waitForReady(timeoutMs) { - if (this._running) return Promise.resolve(); - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this._onReady = null; - reject(new Error(`[FaustEngineBase:${this._id}] Timed out waiting for worklet ready`)); - }, timeoutMs); - - this._onReady = () => { - clearTimeout(timer); - this._onReady = null; - resolve(); - }; - }); - } -} diff --git a/playground/js/synth/faust-param-meta.js b/playground/js/synth/faust-param-meta.js deleted file mode 100644 index 9d77439..0000000 --- a/playground/js/synth/faust-param-meta.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * faust-param-meta.js — Faust JSON descriptor → playground paramMeta converter - * - * Faust's -json flag (or the JSON embedded in faust2wasm glue) emits a UI - * descriptor tree. This module parses that tree into the standard paramMeta - * format used throughout the playground: - * - * [{id, name, min, max, init, curve, group}] - * - * Usage: - * import { faustJsonToParamMeta } from './faust-param-meta.js'; - * const resp = await fetch('faust/additive.json'); - * const faustJson = await resp.json(); - * const paramMeta = faustJsonToParamMeta(faustJson); - * - * The returned array is ready to pass to SynthEngine._paramMeta. - */ - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -/** - * Convert a Faust address path (e.g. "/additive/freq") to a clean id string. - * Strips the leading slash + top-level DSP name segment, replaces remaining - * slashes and spaces with underscores, lower-cases. - * - * Examples: - * "/additive/freq" → "freq" - * "/fm-matrix/Osc/ratio" → "osc_ratio" - * "/MyDSP/Effects/wet mix" → "effects_wet_mix" - * - * @param {string} address Faust param address (e.g. from item.address) - * @returns {string} - */ -export function faustParamAddress(address) { - if (!address) return ''; - // Remove leading slash - let s = address.replace(/^\//, ''); - // Drop the first path segment (top-level DSP class name) - const slashIdx = s.indexOf('/'); - if (slashIdx !== -1) s = s.slice(slashIdx + 1); - // Normalise: lowercase, spaces → underscores, slashes → underscores - return s.toLowerCase().replace(/[\s/]+/g, '_'); -} - -/** - * Types of Faust UI items that represent continuous parameters. - * Buttons and checkboxes are intentionally excluded — they are not suitable - * for ML-driven continuous control. - */ -const CONTINUOUS_TYPES = new Set(['hslider', 'vslider', 'nentry']); - -/** - * Recursively walk a Faust UI tree and collect leaf parameter items. - * - * The Faust UI tree looks like: - * [ - * { - * type: "vgroup", - * label: "Oscillator", - * items: [ - * { type: "hslider", label: "freq", address: "/MyDSP/Oscillator/freq", - * min: 20, max: 4000, init: 220, step: 0.1, meta: [...] }, - * ... - * ] - * }, - * { type: "hslider", label: "amp", address: "/MyDSP/amp", ... } - * ] - * - * @param {Array} items Array of UI items at the current tree level - * @param {string} groupLabel Label of the nearest enclosing named group ("" at root) - * @param {Array} out Accumulator — push {item, group} objects here - */ -export function parseFaustUiTree(items, groupLabel = '', out = []) { - if (!Array.isArray(items)) return out; - - for (const item of items) { - if (!item || typeof item !== 'object') continue; - - if (CONTINUOUS_TYPES.has(item.type)) { - // Skip params marked [hidden:1] in Faust metadata — these are - // control inputs (freq, vel) set by noteOn, not by NISPS. - const isHidden = Array.isArray(item.meta) && - item.meta.some(m => m.hidden === '1' || m.hidden === 1); - if (isHidden) continue; - // Leaf param — collect it, tagged with the nearest enclosing group - out.push({ item, group: groupLabel }); - } else if (item.items) { - // Container node (vgroup, hgroup, tgroup) — recurse, propagating label - const childGroup = item.label || groupLabel; - parseFaustUiTree(item.items, childGroup, out); - } - // Buttons, checkboxes, bargraphs — ignored (non-continuous) - } - - return out; -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Convert a Faust JSON descriptor to a paramMeta array. - * - * @param {object} faustJson Parsed JSON from `faust -json` or embedded in WASM glue. - * Expected shape: { name, ui: [...], ...meta } - * @returns {Array<{id: string, name: string, min: number, max: number, - * init: number, curve: number, group: string}>} - * - * Each element: - * id — slug derived from the Faust address (unique within this engine) - * name — human-readable label (the Faust hslider label, possibly with metadata stripped) - * min — minimum value (raw Faust units) - * max — maximum value (raw Faust units) - * init — default value - * curve — 0.5 (linear default; override per-engine if needed) - * group — nearest enclosing group label (empty string for top-level params) - */ -export function faustJsonToParamMeta(faustJson) { - if (!faustJson || !Array.isArray(faustJson.ui)) { - console.warn('[faustJsonToParamMeta] Invalid or empty Faust JSON — no ui array found'); - return []; - } - - const collected = parseFaustUiTree(faustJson.ui); - - return collected.map(({ item, group }) => { - // Strip Faust metadata annotations from label: "freq[unit:Hz]" → "freq" - const cleanLabel = (item.label || '').replace(/\[.*?\]/g, '').trim(); - - return { - id: faustParamAddress(item.address), - name: cleanLabel, - min: typeof item.min === 'number' ? item.min : 0, - max: typeof item.max === 'number' ? item.max : 1, - init: typeof item.init === 'number' ? item.init : 0, - curve: 0.5, // linear — override per-engine if needed - group: group || '', - }; - }); -} - -/** - * Convenience: fetch a Faust JSON file by URL and return paramMeta. - * - * @param {string} jsonUrl URL of the .json descriptor (e.g. 'faust/additive.json') - * @returns {Promise} - */ -export async function loadFaustParamMeta(jsonUrl) { - const resp = await fetch(jsonUrl); - if (!resp.ok) throw new Error(`[faustJsonToParamMeta] Failed to fetch ${jsonUrl}: ${resp.status}`); - const json = await resp.json(); - return faustJsonToParamMeta(json); -} diff --git a/playground/js/synth/fm-engine.js b/playground/js/synth/fm-engine.js deleted file mode 100644 index 7c47762..0000000 --- a/playground/js/synth/fm-engine.js +++ /dev/null @@ -1,14 +0,0 @@ -import { FaustEngineBase } from './faust-engine-base.js'; - -export class FMEngine extends FaustEngineBase { - constructor() { - super({ - id: 'fm', - displayName: 'FM Matrix', - wasmUrl: 'faust/fm-matrix.wasm', - jsonUrl: 'faust/fm-matrix.json', - workletUrl: 'faust/fm-matrix-processor.js', - processorName: 'fm-matrix-processor', - }); - } -} diff --git a/playground/js/synth/fm-presets.js b/playground/js/synth/fm-presets.js deleted file mode 100644 index 4b75369..0000000 --- a/playground/js/synth/fm-presets.js +++ /dev/null @@ -1,213 +0,0 @@ -// FM synth presets for NISPS playground -// Each preset defines which params are active (ML-controlled) and which are -// muted at safe defaults. Param IDs match faustParamAddress() output from -// fm-matrix.json (e.g. 'operators_op_1_op1_ratio', 'matrix_m21'). -// -// 3 tiers of progressive complexity across 5 presets. - -export const FM_PRESETS = [ - // ====================================================================== - // TIER 1 — Beginner (~12-15 active params) - // ====================================================================== - { - id: 'fm-1', - name: '1.1', - tier: 1, - description: 'Simple FM — Op1/Op2 ratio+level, m21 mod index, op1 ADSR, master level', - active: [ - 'operators_op_1_op1_ratio', - 'operators_op_1_op1_level', - 'operators_op_2_op2_ratio', - 'operators_op_2_op2_level', - 'matrix_m21', - 'operators_op_1_op1_attack', - 'operators_op_1_op1_decay', - 'operators_op_1_op1_sustain', - 'operators_op_1_op1_release', - 'master_level', - 'master_stereo_spread', - 'master_waveform_blend', - ], - overrides: { - 'operators_op_1_op1_ratio': { min: 0.15, max: 0.65, curve: 0.5 }, - 'operators_op_2_op2_ratio': { min: 0.15, max: 0.65, curve: 0.5 }, - 'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 }, - 'operators_op_1_op1_attack': { min: 0.0, max: 0.6, curve: 0.35 }, - 'operators_op_1_op1_decay': { min: 0.05, max: 0.5, curve: 0.4 }, - 'operators_op_1_op1_sustain': { min: 0.2, max: 0.9, curve: 0.55 }, - 'operators_op_1_op1_release': { min: 0.1, max: 0.6, curve: 0.45 }, - 'master_level': { min: 0.3, max: 0.85, curve: 0.5 }, - 'master_waveform_blend': { min: 0.0, max: 0.5, curve: 0.4 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'fm-2', - name: '1.2', - tier: 1, - description: 'Bell Tones — Op1-3 ratios+levels, m21+m31 indices, op1 ADSR, fb1, level', - active: [ - 'operators_op_1_op1_ratio', - 'operators_op_1_op1_level', - 'operators_op_2_op2_ratio', - 'operators_op_2_op2_level', - 'operators_op_3_op3_ratio', - 'operators_op_3_op3_level', - 'matrix_m21', - 'matrix_m31', - 'feedback_fb1', - 'operators_op_1_op1_attack', - 'operators_op_1_op1_decay', - 'operators_op_1_op1_sustain', - 'operators_op_1_op1_release', - 'master_level', - ], - overrides: { - 'operators_op_1_op1_ratio': { min: 0.15, max: 0.55, curve: 0.5 }, - 'operators_op_2_op2_ratio': { min: 0.2, max: 0.7, curve: 0.55 }, - 'operators_op_3_op3_ratio': { min: 0.2, max: 0.7, curve: 0.55 }, - 'matrix_m21': { min: 0.0, max: 0.4, curve: 0.35 }, - 'matrix_m31': { min: 0.0, max: 0.35, curve: 0.3 }, - 'feedback_fb1': { min: 0.0, max: 0.4, curve: 0.3 }, - 'operators_op_1_op1_attack': { min: 0.0, max: 0.5, curve: 0.3 }, - 'operators_op_1_op1_decay': { min: 0.1, max: 0.6, curve: 0.45 }, - 'operators_op_1_op1_sustain': { min: 0.1, max: 0.7, curve: 0.45 }, - 'operators_op_1_op1_release': { min: 0.15, max: 0.7, curve: 0.5 }, - 'master_level': { min: 0.3, max: 0.8, curve: 0.5 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 2 — Intermediate (~25-30 active params) - // ====================================================================== - { - id: 'fm-3', - name: '2.1', - tier: 2, - description: 'Matrix Explorer — all 4 op ratios+levels, 6 cross-mod indices, all 4 ADSRs, LFO, level', - active: [ - // All 4 operator ratios + levels - 'operators_op_1_op1_ratio', - 'operators_op_1_op1_level', - 'operators_op_2_op2_ratio', - 'operators_op_2_op2_level', - 'operators_op_3_op3_ratio', - 'operators_op_3_op3_level', - 'operators_op_4_op4_ratio', - 'operators_op_4_op4_level', - // 6 cross-modulation indices (half the matrix) - 'matrix_m21', - 'matrix_m31', - 'matrix_m41', - 'matrix_m32', - 'matrix_m42', - 'matrix_m43', - // All 4 operator ADSRs (op1 only for now — full for intermediate) - 'operators_op_1_op1_attack', - 'operators_op_1_op1_decay', - 'operators_op_1_op1_sustain', - 'operators_op_1_op1_release', - 'operators_op_2_op2_attack', - 'operators_op_2_op2_decay', - 'operators_op_2_op2_sustain', - 'operators_op_2_op2_release', - // LFO - 'global_lfo_rate', - 'global_lfo_depth', - 'global_lfo_waveform', - // Master - 'master_level', - 'master_stereo_spread', - ], - overrides: { - 'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 }, - 'matrix_m31': { min: 0.0, max: 0.45, curve: 0.35 }, - 'matrix_m41': { min: 0.0, max: 0.4, curve: 0.35 }, - 'matrix_m32': { min: 0.0, max: 0.4, curve: 0.35 }, - 'matrix_m42': { min: 0.0, max: 0.35, curve: 0.3 }, - 'matrix_m43': { min: 0.0, max: 0.35, curve: 0.3 }, - 'global_lfo_depth': { min: 0.0, max: 0.6, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'fm-4', - name: '2.2', - tier: 2, - description: 'Feedback Machine — all op params, all 4 feedbacks, 4 key matrix indices, waveform blend, saturation, level', - active: [ - // All 4 operator ratios + levels - 'operators_op_1_op1_ratio', - 'operators_op_1_op1_level', - 'operators_op_2_op2_ratio', - 'operators_op_2_op2_level', - 'operators_op_3_op3_ratio', - 'operators_op_3_op3_level', - 'operators_op_4_op4_ratio', - 'operators_op_4_op4_level', - // All 4 feedbacks - 'feedback_fb1', - 'feedback_fb2', - 'feedback_fb3', - 'feedback_fb4', - // 4 key cross-mod indices - 'matrix_m21', - 'matrix_m31', - 'matrix_m12', - 'matrix_m34', - // Op1 + Op2 ADSR - 'operators_op_1_op1_attack', - 'operators_op_1_op1_decay', - 'operators_op_1_op1_sustain', - 'operators_op_1_op1_release', - 'operators_op_2_op2_attack', - 'operators_op_2_op2_decay', - 'operators_op_2_op2_sustain', - 'operators_op_2_op2_release', - // Waveform, saturation, master - 'master_waveform_blend', - 'master_output_saturation', - 'master_level', - 'master_stereo_spread', - ], - overrides: { - 'feedback_fb1': { min: 0.0, max: 0.6, curve: 0.35 }, - 'feedback_fb2': { min: 0.0, max: 0.6, curve: 0.35 }, - 'feedback_fb3': { min: 0.0, max: 0.5, curve: 0.3 }, - 'feedback_fb4': { min: 0.0, max: 0.5, curve: 0.3 }, - 'master_output_saturation': { min: 0.0, max: 0.5, curve: 0.35 }, - 'matrix_m21': { min: 0.0, max: 0.5, curve: 0.4 }, - 'matrix_m31': { min: 0.0, max: 0.45, curve: 0.35 }, - 'matrix_m12': { min: 0.0, max: 0.4, curve: 0.35 }, - 'matrix_m34': { min: 0.0, max: 0.4, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 3 — Advanced (all 55 params) - // ====================================================================== - { - id: 'fm-5', - name: '3.1', - tier: 3, - description: 'Full Matrix — all 55 params active, no restrictions', - active: null, // null = all params active - overrides: {}, - mutedOverrides: {}, - groupCurves: {}, - }, -]; - -export const FM_PRESET_TIERS = [ - { tier: 1, label: 'Beginner' }, - { tier: 2, label: 'Intermediate' }, - { tier: 3, label: 'Advanced' }, -]; diff --git a/playground/js/synth/midi-input.js b/playground/js/synth/midi-input.js deleted file mode 100644 index dfd0707..0000000 --- a/playground/js/synth/midi-input.js +++ /dev/null @@ -1,160 +0,0 @@ -// MIDI Input — receives notes and CCs from external MIDI controllers -// Uses Web MIDI API, routes note on/off through a SynthEngine interface - -export class MIDIInput { - constructor(engine) { - this.engine = engine; - this.enabled = false; - this.midiAccess = null; - this.selectedInputId = null; - this.activeInput = null; - this._onStatusChange = null; - this._onInputsChange = null; - this._onCC = null; // optional: (ccNumber, value01) => void - this._boundOnMessage = this._onMessage.bind(this); - this._boundOnStateChange = this._onStateChange.bind(this); - } - - set onStatusChange(fn) { this._onStatusChange = fn; } - set onInputsChange(fn) { this._onInputsChange = fn; } - set onCC(fn) { this._onCC = fn; } - - /** Hot-swap the engine (e.g. when the user switches synth engines). */ - setEngine(engine) { this.engine = engine; } - - _status(msg) { - console.log('[MIDI]', msg); - this._onStatusChange?.(msg); - } - - /** Request MIDI access. Returns true if available. */ - async init() { - if (this.midiAccess) return true; - - if (!navigator.requestMIDIAccess) { - this._status('Web MIDI not supported'); - return false; - } - - try { - this.midiAccess = await navigator.requestMIDIAccess({ sysex: false }); - this.midiAccess.onstatechange = this._boundOnStateChange; - this._status('MIDI available'); - return true; - } catch (err) { - this._status(`MIDI access denied: ${err.message}`); - return false; - } - } - - /** Get list of available MIDI inputs as [{id, name}] */ - getInputs() { - if (!this.midiAccess) return []; - const inputs = []; - for (const [id, input] of this.midiAccess.inputs) { - inputs.push({ id, name: input.name || `MIDI Input ${id}` }); - } - return inputs; - } - - /** Select a specific MIDI input by id, or null for all inputs */ - selectInput(inputId) { - // Disconnect previous - this._disconnectInput(); - - this.selectedInputId = inputId; - - if (!this.enabled) return; - this._connectInput(); - } - - /** Enable MIDI input routing to the synth */ - enable() { - if (this.enabled) return; - this.enabled = true; - this._connectInput(); - this._status('MIDI enabled'); - } - - /** Disable MIDI input routing */ - disable() { - if (!this.enabled) return; - this.enabled = false; - this._disconnectInput(); - this._status('MIDI disabled'); - } - - toggle() { - if (this.enabled) this.disable(); - else this.enable(); - } - - _connectInput() { - if (!this.midiAccess || !this.enabled) return; - - if (this.selectedInputId === null) { - // Listen on all inputs - for (const [, input] of this.midiAccess.inputs) { - input.onmidimessage = this._boundOnMessage; - } - this.activeInput = null; - } else { - const input = this.midiAccess.inputs.get(this.selectedInputId); - if (input) { - input.onmidimessage = this._boundOnMessage; - this.activeInput = input; - } - } - } - - _disconnectInput() { - if (!this.midiAccess) return; - for (const [, input] of this.midiAccess.inputs) { - input.onmidimessage = null; - } - this.activeInput = null; - } - - _onStateChange(e) { - // MIDI device connected/disconnected - this._onInputsChange?.(this.getInputs()); - - if (this.enabled) { - // Reconnect in case the selected device was re-plugged - this._disconnectInput(); - this._connectInput(); - } - } - - _onMessage(e) { - const [status, data1, data2] = e.data; - const type = status & 0xf0; - - switch (type) { - case 0x90: // Note On - if (data2 > 0) { - this.engine.noteOn(data1, data2 / 127); - } else { - // Velocity 0 = note off - this.engine.noteOff(data1); - } - break; - - case 0x80: // Note Off - this.engine.noteOff(data1); - break; - - case 0xb0: // Control Change - this._onCC?.(data1, data2 / 127); - break; - } - } - - destroy() { - this.disable(); - if (this.midiAccess) { - this.midiAccess.onstatechange = null; - } - this.midiAccess = null; - } -} diff --git a/playground/js/synth/modular-engine.js b/playground/js/synth/modular-engine.js deleted file mode 100644 index 376dde6..0000000 --- a/playground/js/synth/modular-engine.js +++ /dev/null @@ -1,946 +0,0 @@ -/** - * modular-engine.js — Phase B of the "Modular" audio mode. - * - * ModularEngine wraps one or more Faust sub-engines (subtractive, additive, - * fm — additive/fm are Phase D) that share a common mod pool (16 ADSRs, - * 32 LFOs) routed through a 48 × 10 modulation matrix. It presents a - * unified SynthEngine interface to the rest of the playground. - * - * Phase B implements the `modular-subtractive` sub-engine only. The other - * two slots are declared as stubs so Phase D can drop them in without - * touching ModularEngine or a-app.js. - * - * Differences from other Faust engines: - * - Does NOT extend FaustEngineBase — it has its own worklet lifecycle - * because the modular processors need `uiJson` threaded through the - * init message (Faust 2.79 wasm builds don't export getJSON), and - * because sub-engine swapping requires disposing and re-initialising - * the worklet node. - * - paramMeta is NOT built with `faustJsonToParamMeta()`. Instead we - * walk the JSON ourselves, collect non-hidden leaf params in the - * SAME order the worklet's `_buildZoneIndexFromJson` walks them - * (that walk order defines the "NISPS index" = position in the - * worklet's `_paramZones` array), then assemble paramMeta in a - * curated order: mod-source params first, matrix cells second, - * engine sound params last (hidden from MLP by default, exposable - * via Phase C UI). - * - * Each paramMeta entry carries a `faustIndex` field that is the worklet's - * NISPS index (position in `_paramZones`), not the raw memory-offset - * `index` field from the Faust JSON. `setParam(i, normValue)` sends - * `{ type:'setParam', index: faustIndex, value: raw }` to the worklet, - * and the processor's `_onSetParam` does the zone lookup. - */ - -import { SynthEngine } from './engine-interface.js'; - -// ----------------------------------------------------------------------------- -// Sub-engine registry -// ----------------------------------------------------------------------------- -// -// Each entry describes one Faust sub-engine that can back ModularEngine. -// Only `subtractive` is active in Phase B; `additive` and `fm` are stubs -// reserved for Phase D. -// -// destNames is the ordered list of modulation destinations, matching -// Matrix/s_d_ labels in the Faust JSON. Index into this -// array == the `d` suffix in the matrix labels. - -const SUB_ENGINES = { - subtractive: { - id: 'modular-subtractive', - displayName: 'Subtractive (3-osc)', - wasmUrl: 'faust/modular-subtractive.wasm', - jsonUrl: 'faust/modular-subtractive.json', - workletUrl: 'faust/modular-subtractive-processor.js', - processorName: 'modular-subtractive-processor', - destCount: 10, - destNames: [ - 'pitch', 'osc2_detune', 'osc3_detune', 'osc_mix_bal', 'noise_level', - 'cutoff', 'resonance', 'filter_env_amt', 'amp', 'pan', - ], - // Top-level group labels that hold the engine's sound params (the - // pieces that aren't mod pool / matrix / hidden). These map into - // paramMeta when setEngineParamExposed() opts them in. - soundGroupPrefixes: ['1_Oscillators', '2_Mixer', '3_Filter', '4_Master'], - }, - additive: { - id: 'modular-additive', - displayName: 'Additive (64-partial)', - wasmUrl: 'faust/modular-additive.wasm', - jsonUrl: 'faust/modular-additive.json', - workletUrl: 'faust/modular-additive-processor.js', - processorName: 'modular-additive-processor', - destCount: 10, - destNames: [ - 'pitch', 'bright', 'tilt', 'inharmonicity', 'odd_even', - 'formant_ctr', 'formant_depth', 'noise_mix', 'amp', 'pan', - ], - soundGroupPrefixes: ['1_Spectral', '2_Formants', '3_Master'], - }, - fm: { - id: 'modular-fm', - displayName: 'FM (4-op matrix)', - wasmUrl: 'faust/modular-fm.wasm', - jsonUrl: 'faust/modular-fm.json', - workletUrl: 'faust/modular-fm-processor.js', - processorName: 'modular-fm-processor', - destCount: 10, - destNames: [ - 'pitch', 'op1_level', 'op2_level', 'op3_level', 'op4_level', - 'cross_mod_global', 'feedback_global', 'global_ratio', 'amp', 'pan', - ], - soundGroupPrefixes: ['1_Operators', '2_CrossMod', '3_Feedback', '4_Master'], - }, -}; - -// Number of ADSR / LFO slots exposed to the MLP by default. The Faust DSP -// always compiles 16 ADSR + 32 LFO slots; the first N of each are what the -// MLP can drive, the rest are user-set-only (enable=0 by default). -const DEFAULT_ADSR_COUNT = 4; -const DEFAULT_LFO_COUNT = 8; - -// Per-source params driven by the MLP (enable is excluded — it's a -// discrete flag, poor target for continuous output). -const ADSR_MLP_PARAMS = ['attack', 'decay', 'sustain', 'release']; -const LFO_MLP_PARAMS = ['rate', 'morph']; - -// ----------------------------------------------------------------------------- - -export class ModularEngine extends SynthEngine { - // Processor names we've already registered via _registerCombinedWorklet. - // Shared across all ModularEngine instances since AudioWorklet deduplicates - // processor names per AudioContext; double-registering throws. - static _registeredProcessors = new Set(); - - constructor() { - super(); - - this._activeSubId = 'subtractive'; // keyword into SUB_ENGINES - this._subCfg = SUB_ENGINES[this._activeSubId]; - - // How many ADSR / LFO slots the MLP-facing paramMeta will contain. - // Can be changed at runtime via setModSourceCount(). - this._adsrCount = DEFAULT_ADSR_COUNT; - this._lfoCount = DEFAULT_LFO_COUNT; - - this._audioCtx = null; - this._workletNode = null; - this._masterGain = null; - this._outputNode = null; - this._running = false; - this._onReady = null; - - // Parsed Faust JSON of the active sub-engine. - this._faustJson = null; - - // Walk-order index maps. _walkEntries[i] is the ith non-hidden leaf - // param in the UI-tree walk — the same i that the worklet uses as - // its zone index. - this._walkEntries = []; // [{ label, path, min, max, init, nispsIndex }] - this._labelToWalk = new Map(); // label -> walk entry - - // MLP-facing paramMeta (ordered: ADSR → LFO → Matrix → optional sound). - this._paramMeta = []; - - // Exposure toggles (Phase C hooks). - this._exposedEngineParams = new Set(); // label strings - - // Matrix cell exposure: keys are "sXX_dYY" strings. Default-empty — - // matrix routing is a "patch" setting (user-configured once) rather - // than a "performance" surface (wiggled by the MLP). Users can opt in - // individual cells via the modular UI; each opt-in triggers a - // paramMeta:change event and an MLP resize. - this._exposedMatrixCells = new Set(); - - // Most-recent raw value written to each DSP label (by _setRawByLabel - // or setParam). Used by getState() to snapshot current DSP values - // without round-tripping through the worklet. Cleared on sub-engine - // swap since labels are sub-engine-specific. - this._lastRawByLabel = new Map(); // label -> rawValue - - // Event listeners. - this._listeners = new Map(); // type -> Set - } - - // --------------------------------------------------------------------------- - // SynthEngine identity - // --------------------------------------------------------------------------- - - get id() { return 'modular'; } - get displayName() { return 'Modular'; } - get paramMeta() { return this._paramMeta; } - - /** Id of the currently active Faust sub-engine (e.g. 'subtractive'). */ - get activeSubEngineId() { return this._activeSubId; } - - /** List of sub-engine ids that are currently implemented. */ - get availableSubEngines() { - return Object.keys(SUB_ENGINES); - } - - /** Destination names for the active sub-engine (matrix column headers). */ - get destNames() { - return this._subCfg?.destNames?.slice() ?? []; - } - - /** - * List of Faust labels for the active sub-engine's sound params — i.e. - * leaf params under any of `soundGroupPrefixes`. Used by the modular UI - * to populate the "expose to MLP" checkbox list. - */ - getEngineSoundParamLabels() { - const prefixes = this._subCfg?.soundGroupPrefixes ?? []; - if (prefixes.length === 0) return []; - const out = []; - for (const entry of this._walkEntries) { - const label = entry.label; - if (!label) continue; - if (prefixes.some(p => label.startsWith(p + '/'))) { - out.push({ - label, - min: entry.min, - max: entry.max, - init: entry.init, - }); - } - } - return out; - } - - /** True if a given engine sound param is currently exposed to the MLP. */ - isEngineParamExposed(label) { - return this._exposedEngineParams.has(label); - } - - /** Snapshot of the currently exposed engine sound param labels. */ - getExposedEngineParams() { - return [...this._exposedEngineParams]; - } - - // --------------------------------------------------------------------------- - // Phase E — full state snapshot / restore - // --------------------------------------------------------------------------- - - /** - * Capture a full raw-DSP snapshot suitable for `setState()`. Walks every - * non-hidden leaf param discovered by `_walkFaustJson()` and records its - * current value either from the live worklet (by label lookup) or from - * `_pendingByLabel` / init defaults if the worklet isn't running yet. - * - * Shape: - * { - * version: 1, - * subEngine: 'subtractive', - * adsrCount, lfoCount, - * exposedEngineParams: ['3_Filter/00_cutoff', ...], - * dsp: { [label]: rawValue, ... }, - * } - */ - getState() { - const dsp = {}; - for (const entry of this._walkEntries) { - const label = entry.label; - if (!label) continue; - let v; - if (this._lastRawByLabel.has(label)) { - v = this._lastRawByLabel.get(label); - } else { - v = entry.init; - } - dsp[label] = v; - } - return { - version: 1, - subEngine: this._activeSubId, - adsrCount: this._adsrCount, - lfoCount: this._lfoCount, - exposedEngineParams: [...this._exposedEngineParams], - dsp, - }; - } - - /** - * Restore a snapshot produced by `getState()`. - * - * Steps: - * 1. Validates `version` (currently only 1 is supported). - * 2. Swaps sub-engine if different (awaits the worklet swap; the swap - * reapplies the default patch first, which we then overwrite). - * 3. Sets adsrCount / lfoCount via `setModSourceCount`. - * 4. Restores `_exposedEngineParams`. - * 5. Writes every `dsp[label]` to the worklet via `_setRawByLabel`. - * Unknown labels (e.g. from a sub-engine snapshot that's been re-saved - * against a different sub-engine) are silently skipped. - * 6. Rebuilds paramMeta and emits `paramMeta:change` exactly once. - * - * Emits `paramMeta:change` at the end. - */ - async setState(state) { - if (!state || typeof state !== 'object') return; - if (state.version != null && state.version !== 1) { - console.warn(`[ModularEngine] setState: unknown version ${state.version}, skipping`); - return; - } - - // 1. Sub-engine swap (async — reloads JSON, reinstantiates worklet if running) - if (typeof state.subEngine === 'string' && - state.subEngine !== this._activeSubId && - SUB_ENGINES[state.subEngine]) { - try { - await this.setSubEngine(state.subEngine); - } catch (err) { - console.warn('[ModularEngine] setState: setSubEngine failed', err); - } - } - - // 2. Mod source counts. Don't call setModSourceCount() directly because - // it rebuilds paramMeta and fires paramMeta:change — we want exactly - // one rebuild at the very end. - if (typeof state.adsrCount === 'number') { - this._adsrCount = Math.max(1, Math.min(16, state.adsrCount | 0)); - } - if (typeof state.lfoCount === 'number') { - this._lfoCount = Math.max(1, Math.min(32, state.lfoCount | 0)); - } - - // 3. Exposed engine sound params - if (Array.isArray(state.exposedEngineParams)) { - this._exposedEngineParams = new Set( - state.exposedEngineParams.filter(l => typeof l === 'string' && this._labelToWalk.has(l)) - ); - } - - // 4. Raw DSP values. Only write labels the active sub-engine knows about. - // If the engine isn't running yet, _setRawByLabel is a no-op — but - // we still record the value in _lastRawByLabel so a later getState() - // sees it, and so that once init() runs _applyDefaultPatch we can - // replay through the pending path. - if (state.dsp && typeof state.dsp === 'object') { - for (const [label, rawVal] of Object.entries(state.dsp)) { - if (typeof rawVal !== 'number') continue; - if (!this._labelToWalk.has(label)) continue; - this._setRawByLabel(label, rawVal); - } - } - - // 5. Single paramMeta rebuild + event emission. - this._rebuildParamMeta(); - this._emit('paramMeta:change', { engine: this }); - } - - /** - * Re-apply the Phase B default patch. Exposed so preset code can reset - * to a known baseline before layering a preset on top. Safe to call - * before or after init() — pre-init it just updates the _lastRawByLabel - * bookkeeping. - */ - resetToDefaults() { - this._applyDefaultPatch(); - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - /** - * Load the default sub-engine's JSON so paramCount is known before init(). - * Safe to call multiple times. - * - * Also seeds `_lastRawByLabel` with the Phase B default patch values so - * that `getState()` returns a meaningful snapshot before audio has been - * started. Without this the snapshot reports raw DSP init values (0 for - * most matrix cells), which is technically correct pre-init but is - * surprising — the default patch is a property of the engine, not of - * the worklet lifecycle. - */ - async loadParamMeta() { - if (this._paramMeta.length > 0) return; - await this._loadSubEngineJson(this._activeSubId); - this._rebuildParamMeta(); - this._applyDefaultPatch(); - } - - /** - * Initialise the engine: load the active sub-engine, register the worklet, - * create the node, apply the default patch. - */ - async init(audioCtx) { - if (this._running) return; - if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - } - this._audioCtx = audioCtx; - - if (!this._faustJson) { - await this._loadSubEngineJson(this._activeSubId); - this._rebuildParamMeta(); - } - - await this._instantiateWorklet(); - this._applyDefaultPatch(); - this._running = true; - } - - /** Release all resources. */ - dispose() { - this._teardownWorklet(); - this._running = false; - this._audioCtx = null; - } - - getOutputNode() { return this._outputNode; } - - /** Convenience: set master gain (wired to the volume slider in a-app.js). */ - setMasterVolume(value) { - if (this._masterGain && this._audioCtx) { - this._masterGain.gain.setTargetAtTime(value, this._audioCtx.currentTime, 0.01); - } - } - - /** Mirror FaustEngineBase.stop()/start() for hot-pause on the audio button. */ - async stop() { - if (this._masterGain && this._audioCtx) { - this._masterGain.gain.setTargetAtTime(0, this._audioCtx.currentTime, 0.01); - } - this._running = false; - } - async start() { - if (this._masterGain && this._audioCtx) { - this._masterGain.gain.setTargetAtTime(0.7, this._audioCtx.currentTime, 0.01); - } - this._running = true; - } - - // --------------------------------------------------------------------------- - // Real-time control - // --------------------------------------------------------------------------- - - setParam(index, normalizedValue) { - const meta = this._paramMeta[index]; - if (!meta) return; - const raw = meta.min + normalizedValue * (meta.max - meta.min); - // Record the write so getState() can see it even if no worklet yet. - if (meta.label) this._lastRawByLabel.set(meta.label, raw); - if (!this._workletNode) return; - this._workletNode.port.postMessage({ - type: 'setParam', - index: meta.faustIndex, - value: raw, - }); - } - - noteOn(note, vel = 0.7) { - if (!this._workletNode) return; - const freq = 440 * Math.pow(2, (note - 69) / 12); - this._workletNode.port.postMessage({ type: 'noteOn', freq, vel }); - } - - noteOff(note) { - if (!this._workletNode) return; - const freq = 440 * Math.pow(2, (note - 69) / 12); - this._workletNode.port.postMessage({ type: 'noteOff', freq }); - } - - // --------------------------------------------------------------------------- - // Phase C hooks (public API the modular UI will call) - // --------------------------------------------------------------------------- - - /** - * Swap the active Faust sub-engine. Rebuilds paramMeta and emits - * 'paramMeta:change' so a-app.js can resize the MLP. - * - * @param {string} id Key into SUB_ENGINES (e.g. 'subtractive'). - */ - async setSubEngine(id) { - if (!SUB_ENGINES[id]) { - throw new Error(`[ModularEngine] Unknown sub-engine: ${id}`); - } - if (id === this._activeSubId && this._running) return; - - const wasRunning = this._running; - this._teardownWorklet(); - this._running = false; - - this._activeSubId = id; - this._subCfg = SUB_ENGINES[id]; - this._faustJson = null; - this._exposedEngineParams.clear(); - this._exposedMatrixCells.clear(); - this._lastRawByLabel.clear(); - - await this._loadSubEngineJson(id); - this._rebuildParamMeta(); - // Seed default-patch values even if the worklet isn't running — keeps - // getState() consistent with loadParamMeta()'s post-condition. - this._applyDefaultPatch(); - - if (wasRunning && this._audioCtx) { - await this._instantiateWorklet(); - this._applyDefaultPatch(); - this._running = true; - } - this._emit('paramMeta:change', { engine: this }); - } - - /** - * Opt a specific engine sound param into the MLP-controllable set. - * Rebuilds paramMeta and fires 'paramMeta:change'. - * - * @param {string} label Full Faust label, e.g. '3_Filter/00_cutoff'. - * @param {boolean} exposed true to expose, false to hide. - */ - setExposeEngineParam(label, exposed) { - if (!this._labelToWalk.has(label)) return; - const had = this._exposedEngineParams.has(label); - if (exposed && !had) this._exposedEngineParams.add(label); - else if (!exposed && had) this._exposedEngineParams.delete(label); - else return; - this._rebuildParamMeta(); - this._emit('paramMeta:change', { engine: this }); - } - - /** - * Opt a single matrix cell into (or out of) the MLP-driven paramMeta. - * When `exposed` is true, the cell joins the MLP output vector and its - * value is driven by inference every tick. When false, it stays at - * whatever raw value the worklet currently holds (e.g. from the default - * patch or the user's direct edits). - * - * @param {number} s 0..47 source index - * @param {number} d 0..9 destination index - * @param {boolean} exposed - */ - setExposeMatrixCell(s, d, exposed) { - const key = `s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}`; - const had = this._exposedMatrixCells.has(key); - if (exposed && !had) this._exposedMatrixCells.add(key); - else if (!exposed && had) this._exposedMatrixCells.delete(key); - else return; - this._rebuildParamMeta(); - this._emit('paramMeta:change', { engine: this }); - } - - /** Snapshot of currently-exposed matrix cell keys (e.g. "s00_d08"). */ - getExposedMatrixCells() { - return [...this._exposedMatrixCells]; - } - - /** - * Return a Float32Array of length paramCount holding the normalised - * [0,1] default value for each paramMeta entry — reading from - * _lastRawByLabel if the user has set a value, otherwise from the - * walk-entry init. Used by the app's cold-start bias shift so an - * untrained MLP output of 0.5 still reproduces the default patch. - */ - getDefaultNormalizedOutputs() { - const out = new Float32Array(this._paramMeta.length); - for (let i = 0; i < this._paramMeta.length; i++) { - const m = this._paramMeta[i]; - const range = (m.max - m.min) || 1; - const raw = this._lastRawByLabel.has(m.label) - ? this._lastRawByLabel.get(m.label) - : (this._labelToWalk.get(m.label)?.init ?? m.min); - out[i] = Math.max(0, Math.min(1, (raw - m.min) / range)); - } - return out; - } - - /** - * Change how many ADSR / LFO slots appear in paramMeta. Rebuilds paramMeta - * and emits 'paramMeta:change' so a-app.js resizes the MLP. Slots past the - * new counts remain alive in the Faust DSP (always 16 + 32 exist) but are - * no longer MLP-driven; the UI is expected to disable those slots via the - * per-slot enable flag if it wants them silent. - * - * @param {number} adsrCount 1..16 - * @param {number} lfoCount 1..32 - */ - setModSourceCount(adsrCount, lfoCount) { - adsrCount = Math.max(1, Math.min(16, adsrCount | 0)); - lfoCount = Math.max(1, Math.min(32, lfoCount | 0)); - if (adsrCount === this._adsrCount && lfoCount === this._lfoCount) return; - this._adsrCount = adsrCount; - this._lfoCount = lfoCount; - this._rebuildParamMeta(); - this._emit('paramMeta:change', { engine: this }); - } - - /** Current MLP-facing ADSR slot count. */ - get adsrCount() { return this._adsrCount; } - - /** Current MLP-facing LFO slot count. */ - get lfoCount() { return this._lfoCount; } - - /** Subscribe to a custom event ('paramMeta:change'). Returns unsubscribe fn. */ - on(type, handler) { - if (!this._listeners.has(type)) this._listeners.set(type, new Set()); - this._listeners.get(type).add(handler); - return () => this.off(type, handler); - } - - off(type, handler) { - this._listeners.get(type)?.delete(handler); - } - - _emit(type, detail) { - const set = this._listeners.get(type); - if (!set) return; - for (const fn of set) { - try { fn(detail); } catch (err) { console.error(`[ModularEngine:${type}]`, err); } - } - } - - // --------------------------------------------------------------------------- - // Internals — JSON walking + paramMeta construction - // --------------------------------------------------------------------------- - - async _loadSubEngineJson(subId) { - const cfg = SUB_ENGINES[subId]; - if (!cfg) throw new Error(`[ModularEngine] Unknown sub-engine: ${subId}`); - - const resp = await fetch(cfg.jsonUrl); - if (!resp.ok) { - throw new Error(`[ModularEngine] Failed to fetch ${cfg.jsonUrl}: ${resp.status}`); - } - this._faustJson = await resp.json(); - this._walkFaustJson(); - } - - /** - * Walk the Faust UI tree in the SAME order the worklet's - * _buildZoneIndexFromJson walks it, skipping hidden params. The position - * in the resulting list is the NISPS index = the zone argument the - * worklet passes to setParamValue. - */ - _walkFaustJson() { - const HIDDEN_TAIL = new Set(['freq', 'gate', '_vel']); - const entries = []; - const byLabel = new Map(); - - const walk = (items, path) => { - if (!Array.isArray(items)) return; - for (const item of items) { - const type = item?.type; - if (type === 'hslider' || type === 'vslider' || - type === 'nentry' || type === 'button' || - type === 'checkbox') { - const label = item.label ?? ''; - const idx = item.index; - if (typeof idx !== 'number') continue; - - const hasHiddenMeta = Array.isArray(item.meta) && - item.meta.some(m => m.hidden === '1' || m.hidden === 1); - const tail = label.includes('/') ? label.split('/').pop() : label; - if (hasHiddenMeta || HIDDEN_TAIL.has(tail)) continue; - - const entry = { - label, - path: path + '/' + label, - min: typeof item.min === 'number' ? item.min : 0, - max: typeof item.max === 'number' ? item.max : 1, - init: typeof item.init === 'number' ? item.init : 0, - nispsIndex: entries.length, // walk-order index - }; - entries.push(entry); - byLabel.set(label, entry); - } else if (item?.items) { - walk(item.items, path + '/' + (item.label ?? '')); - } - } - }; - - walk(this._faustJson?.ui ?? [], ''); - this._walkEntries = entries; - this._labelToWalk = byLabel; - } - - /** - * Build the MLP-facing paramMeta from the walk entries. Ordering: - * 1. Mod source params (ADSR A/D/S/R × DEFAULT_ADSR_COUNT, - * LFO rate/morph × DEFAULT_LFO_COUNT) - * 2. Matrix cells (all 48 × destCount, dest-major, - * source-major within each destination) - * 3. Opted-in engine sound params (empty by default) - */ - _rebuildParamMeta() { - const cfg = this._subCfg; - const meta = []; - - // ----- 1. Mod sources ----- - for (let i = 0; i < this._adsrCount; i++) { - const prefix = `MM_ADSR/${String(i).padStart(2, '0')}_adsr${String(i + 1).padStart(2, '0')}`; - for (const suffix of ADSR_MLP_PARAMS) { - const label = `${prefix}_${suffix}`; - const e = this._labelToWalk.get(label); - if (!e) continue; - meta.push(this._makeMetaEntry(e, { - id: `adsr${i + 1}_${suffix}`, - name: `${cap(suffix)}`, - group: `ADSR ${i + 1}`, - })); - } - } - for (let i = 0; i < this._lfoCount; i++) { - const prefix = `MM_LFO/${String(i).padStart(2, '0')}_lfo${String(i + 1).padStart(2, '0')}`; - for (const suffix of LFO_MLP_PARAMS) { - const label = `${prefix}_${suffix}`; - const e = this._labelToWalk.get(label); - if (!e) continue; - meta.push(this._makeMetaEntry(e, { - id: `lfo${i + 1}_${suffix}`, - name: `${cap(suffix)}`, - group: `LFO ${i + 1}`, - })); - } - } - - // ----- 2. Matrix cells — always in paramMeta (dest-major, source-major) ----- - // Every 48 × 10 cell lives in paramMeta so modular-ui.updateLive() can - // mirror live MLP outputs into the matrix DOM. This is required for - // the visual feedback loop in the modular drawer. The historical - // concern — that a denormalised matrix cell near 0 could silence the - // voice via the d08_amp route — is now handled at the DSP level: - // each sub-engine's amp_val is `clamp(base_amp + max(0, mod_amp))`, - // so matrix cells can only add to the amp floor, never cut it. - // (`_exposedMatrixCells` is kept for forward compatibility but the - // default is always-include.) - for (let d = 0; d < cfg.destCount; d++) { - const destName = cfg.destNames[d]; - for (let s = 0; s < 48; s++) { - const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`; - const e = this._labelToWalk.get(label); - if (!e) continue; - meta.push(this._makeMetaEntry(e, { - id: `mm_s${s}_d${d}_${destName}`, - name: `s${String(s).padStart(2, '0')} \u2192 ${destName}`, - group: `Matrix/${destName}`, - })); - } - } - - // ----- 3. Opted-in engine sound params ----- - for (const label of this._exposedEngineParams) { - const e = this._labelToWalk.get(label); - if (!e) continue; - const [group, leaf] = splitLabel(label); - meta.push(this._makeMetaEntry(e, { - id: slugify(label), - name: stripIndexPrefix(leaf), - group: group || 'Sound', - })); - } - - this._paramMeta = meta; - } - - _makeMetaEntry(walkEntry, over) { - const range = (walkEntry.max - walkEntry.min) || 1; - const initNorm = Math.max(0, Math.min(1, - (walkEntry.init - walkEntry.min) / range)); - return { - id: over.id, - name: over.name, - min: walkEntry.min, // raw units (FaustEngineBase convention) - max: walkEntry.max, - init: initNorm, // normalised 0..1 - curve: 0.5, - group: over.group, - faustIndex: walkEntry.nispsIndex, - label: walkEntry.label, // kept for debugging and Phase C wiring - }; - } - - // --------------------------------------------------------------------------- - // Worklet lifecycle - // --------------------------------------------------------------------------- - - async _instantiateWorklet() { - const cfg = this._subCfg; - const audioCtx = this._audioCtx; - - // Fetch WASM binary. - const wasmResp = await fetch(cfg.wasmUrl); - if (!wasmResp.ok) { - throw new Error(`[ModularEngine] Failed to fetch ${cfg.wasmUrl}: ${wasmResp.status}`); - } - const wasmBytes = await wasmResp.arrayBuffer(); - - // Register the worklet. In practice Chrome's AudioWorkletGlobalScope - // isolates classes declared at the top of separate addModule() scripts - // (contrary to the spec's "shared global" phrasing), so a base class - // defined in faust-worklet-processor.js is not visible when a subclass - // script is loaded via a second addModule() call. To sidestep this we - // fetch both files, concatenate them, and addModule() a single blob URL - // — the base class and subclass end up in one script evaluation. - await this._registerCombinedWorklet(audioCtx, cfg.workletUrl); - - this._workletNode = new AudioWorkletNode(audioCtx, cfg.processorName, { - numberOfInputs: 0, - numberOfOutputs: 1, - outputChannelCount: [2], - }); - this._workletNode.port.onmessage = (e) => this._handleWorkletMsg(e.data); - - // Build the audio graph. - this._masterGain = audioCtx.createGain(); - this._masterGain.gain.value = 0.7; - this._workletNode.connect(this._masterGain); - this._masterGain.connect(audioCtx.destination); - this._outputNode = this._masterGain; - - // Send the init message — uiJson is passed as a parsed object. - this._workletNode.port.postMessage({ - type: 'init', - wasmBytes, - sampleRate: audioCtx.sampleRate, - uiJson: this._faustJson, - }, [wasmBytes]); - - await this._waitForReady(10_000); - } - - /** - * Fetch faust-worklet-processor.js (base class) and the sub-engine's - * processor file, concatenate them, and addModule() the result as one - * blob. Cached per processor name so repeated sub-engine swaps don't - * re-register. - * - * Chrome has deduped addModule() by URL in the past, so a blob URL with - * a fresh identity each call is safe but slightly wasteful; we memoise - * on processorName to keep things tidy. - */ - async _registerCombinedWorklet(audioCtx, workletUrl) { - const name = this._subCfg.processorName; - if (ModularEngine._registeredProcessors.has(name)) return; - - const [baseSrc, subSrc] = await Promise.all([ - fetch('faust/faust-worklet-processor.js').then(r => { - if (!r.ok) throw new Error(`base worklet fetch failed: ${r.status}`); - return r.text(); - }), - fetch(workletUrl).then(r => { - if (!r.ok) throw new Error(`sub worklet fetch failed: ${r.status}`); - return r.text(); - }), - ]); - - const combined = baseSrc + '\n' + subSrc; - const blob = new Blob([combined], { type: 'application/javascript' }); - const blobUrl = URL.createObjectURL(blob); - try { - await audioCtx.audioWorklet.addModule(blobUrl); - ModularEngine._registeredProcessors.add(name); - } finally { - URL.revokeObjectURL(blobUrl); - } - } - - _teardownWorklet() { - if (this._masterGain) { - try { this._masterGain.disconnect(); } catch (_) { /* ignore */ } - this._masterGain = null; - } - if (this._workletNode) { - try { this._workletNode.disconnect(); } catch (_) { /* ignore */ } - this._workletNode.port.onmessage = null; - this._workletNode = null; - } - this._outputNode = null; - } - - _handleWorkletMsg(data) { - if (!data) return; - if (data.type === 'ready') { - this._onReady?.(); - } else if (data.type === 'error') { - console.error(`[ModularEngine:${this._activeSubId}] worklet error:`, data.message); - } - } - - _waitForReady(timeoutMs) { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - this._onReady = null; - reject(new Error(`[ModularEngine] Timed out waiting for worklet ready`)); - }, timeoutMs); - this._onReady = () => { - clearTimeout(timer); - this._onReady = null; - resolve(); - }; - }); - } - - // --------------------------------------------------------------------------- - // Default patch - // --------------------------------------------------------------------------- - - /** - * Write raw values directly to the worklet by Faust label, bypassing - * paramMeta. Used for the default patch, which includes params NOT in - * paramMeta (e.g. `enable` flags, engine sound defaults). - */ - _setRawByLabel(label, rawValue) { - // Always record the intent, even if the worklet isn't up yet — getState() - // reads from this map. The value will be re-applied when the worklet - // next runs _applyDefaultPatch() or when the caller explicitly replays - // state after init(). - this._lastRawByLabel.set(label, rawValue); - if (!this._workletNode) return; - this._workletNode.port.postMessage({ type: 'setByLabel', label, value: rawValue }); - } - - /** - * Apply the default patch so the first noteOn produces sound. - * - * Strategy: base_amp=1.0 keeps the voice fully open regardless of the - * mod matrix — users opt into ADSR/LFO→amp gating by lowering base_amp - * and routing a mod source to s##_d08_amp themselves. ADSR 1 is still - * pre-armed with a sensible envelope shape so a single matrix cell - * expose is all it takes to get classic VCA behaviour. - */ - _applyDefaultPatch() { - // ADSR 1 — pre-armed envelope (enabled, sensible shape). Not routed - // to amp by default; the user wires it up via the matrix UI. - this._setRawByLabel('MM_ADSR/00_adsr01_enable', 1.0); - this._setRawByLabel('MM_ADSR/00_adsr01_attack', 0.01); - this._setRawByLabel('MM_ADSR/00_adsr01_decay', 0.2); - this._setRawByLabel('MM_ADSR/00_adsr01_sustain', 0.7); - this._setRawByLabel('MM_ADSR/00_adsr01_release', 0.3); - - // Engine sound defaults (subtractive-specific). - if (this._activeSubId === 'subtractive') { - this._setRawByLabel('3_Filter/00_cutoff', 3000); - this._setRawByLabel('3_Filter/01_resonance', 0.2); - this._setRawByLabel('1_Oscillators/02_osc1_level', 0.8); - this._setRawByLabel('1_Oscillators/06_osc2_level', 0.0); - this._setRawByLabel('1_Oscillators/10_osc3_level', 0.0); - this._setRawByLabel('4_Master/00_master_level', 0.7); - } - } -} - -// ----------------------------------------------------------------------------- -// Small helpers -// ----------------------------------------------------------------------------- - -function cap(s) { - return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; -} - -function splitLabel(label) { - const i = label.lastIndexOf('/'); - if (i < 0) return ['', label]; - return [label.slice(0, i), label.slice(i + 1)]; -} - -function stripIndexPrefix(leaf) { - // Strip the leading "NN_" numeric prefix Faust uses to force UI ordering, - // e.g. "00_osc1_wave" → "osc1_wave". - return leaf.replace(/^\d+_/, ''); -} - -function slugify(s) { - return s.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); -} diff --git a/playground/js/synth/modular-presets.js b/playground/js/synth/modular-presets.js deleted file mode 100644 index 9c07cab..0000000 --- a/playground/js/synth/modular-presets.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - * modular-presets.js — Phase E presets for the Modular engine. - * - * Each preset is a curated snapshot shaped like `ModularEngine.getState()` - * output. `applyPreset(engine, preset)` first calls `engine.resetToDefaults()` - * to re-establish the Phase B baseline, then `engine.setState(preset.state)` - * to layer the preset's non-default values on top. Presets therefore only - * need to include the labels they actively change. - * - * Source ordering reminder (mirrors modular-engine.js comments): - * Matrix source 0..15 = ADSR slots 1..16 - * Matrix source 16..47 = LFO slots 1..32 - * - * Destinations (d00..d09) are sub-engine-specific; see - * faust/MODULAR_DESTINATIONS.md. Across all three sub-engines: - * d00 = pitch, d08 = amp, d09 = pan. - */ - -// Helper factories for building per-source label sets without macro soup. -// Only include labels that differ from the Phase B default patch. - -const pad2 = (n) => String(n).padStart(2, '0'); - -/** `MM_ADSR/NN_adsrNN_` label builder (NN zero-padded). */ -function adsrLabel(slot1, field) { - // slot1 = 1-based slot number - const nn = pad2(slot1 - 1); - const mm = pad2(slot1); - return `MM_ADSR/${nn}_adsr${mm}_${field}`; -} - -/** `MM_LFO/NN_lfoNN_` label builder. */ -function lfoLabel(slot1, field) { - const nn = pad2(slot1 - 1); - const mm = pad2(slot1); - return `MM_LFO/${nn}_lfo${mm}_${field}`; -} - -/** - * Matrix cell label: source `s` (0..47), destination `d` (0..9), name. - */ -function mxLabel(s, d, name) { - return `MM_Matrix/s${pad2(s)}_d${pad2(d)}_${name}`; -} - -// Sub-engine-specific base_amp labels. base_amp is a static floor on the -// voice amp — defaults to 1.0 (always audible). Presets that want classic -// ADSR-gated voices drop it to 0.0 and route a positive ADSR→amp matrix -// amount on top. -const BASE_AMP_SUB = '4_Master/04_base_amp'; // subtractive -const BASE_AMP_ADD = '3_Master/05_base_amp'; // additive -const BASE_AMP_FM = '4_Master/06_base_amp'; // fm - -// ----------------------------------------------------------------------------- -// Presets -// ----------------------------------------------------------------------------- - -/** 1. Default — Phase B out-of-the-box patch (empty diff). */ -const DEFAULT_PRESET = { - version: 1, - subEngine: 'subtractive', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: {}, -}; - -/** 2. Slow pad — long ADSR, subtle LFOs, filter open-ish. Subtractive. */ -const SLOW_PAD_PRESET = { - version: 1, - subEngine: 'subtractive', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: { - // ADSR 1 — long amp envelope - [adsrLabel(1, 'enable')]: 1.0, - [adsrLabel(1, 'attack')]: 2.0, - [adsrLabel(1, 'decay')]: 1.0, - [adsrLabel(1, 'sustain')]: 0.9, - [adsrLabel(1, 'release')]: 4.0, - - // LFO 1 — slow sine → cutoff - [lfoLabel(1, 'enable')]: 1.0, - [lfoLabel(1, 'rate')]: 0.3, - [lfoLabel(1, 'morph')]: 0.0, - - // LFO 2 — slow sine → pitch (subtle detune) - [lfoLabel(2, 'enable')]: 1.0, - [lfoLabel(2, 'rate')]: 0.5, - [lfoLabel(2, 'morph')]: 0.0, - - // Base amp: 0 → envelope-gated voice (ADSR1→amp drives the whole VCA). - [BASE_AMP_SUB]: 0.0, - - // Matrix routes - [mxLabel(0, 8, 'amp')]: 1.0, // ADSR1 → amp (full envelope gate) - [mxLabel(16, 5, 'cutoff')]: 0.4, // LFO1 → cutoff - [mxLabel(17, 0, 'pitch')]: 0.05, // LFO2 → pitch - - // Engine sound - '3_Filter/00_cutoff': 1000, - '3_Filter/01_resonance': 0.2, - '1_Oscillators/02_osc1_level': 0.7, - '1_Oscillators/06_osc2_level': 0.4, - }, -}; - -/** 3. Plucky bass — fast amp + filter envelopes, low cutoff, high res. */ -const PLUCKY_BASS_PRESET = { - version: 1, - subEngine: 'subtractive', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: { - // ADSR 1 — amp - [adsrLabel(1, 'enable')]: 1.0, - [adsrLabel(1, 'attack')]: 0.005, - [adsrLabel(1, 'decay')]: 0.15, - [adsrLabel(1, 'sustain')]: 0.0, - [adsrLabel(1, 'release')]: 0.1, - // ADSR 2 — filter - [adsrLabel(2, 'enable')]: 1.0, - [adsrLabel(2, 'attack')]: 0.005, - [adsrLabel(2, 'decay')]: 0.15, - [adsrLabel(2, 'sustain')]: 0.0, - [adsrLabel(2, 'release')]: 0.1, - - // Base amp 0 → ADSR1 fully gates the voice. - [BASE_AMP_SUB]: 0.0, - - [mxLabel(0, 8, 'amp')]: 1.0, // ADSR1 → amp - [mxLabel(1, 5, 'cutoff')]: 0.8, // ADSR2 → cutoff - - '3_Filter/00_cutoff': 400, - '3_Filter/01_resonance': 0.7, - '1_Oscillators/02_osc1_level': 0.9, - }, -}; - -/** 4. Crystal — additive, tremolo + slow formant sweep. */ -const CRYSTAL_PRESET = { - version: 1, - subEngine: 'additive', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: { - [adsrLabel(1, 'enable')]: 1.0, - [adsrLabel(1, 'attack')]: 0.02, - [adsrLabel(1, 'decay')]: 0.4, - [adsrLabel(1, 'sustain')]: 0.8, - [adsrLabel(1, 'release')]: 1.2, - - [lfoLabel(1, 'enable')]: 1.0, - [lfoLabel(1, 'rate')]: 4.0, - [lfoLabel(1, 'morph')]: 0.0, // sine - - [lfoLabel(2, 'enable')]: 1.0, - [lfoLabel(2, 'rate')]: 0.2, - [lfoLabel(2, 'morph')]: 0.33, // tri - - // Base amp 0 → ADSR1 shapes the voice envelope (0.8 route leaves some - // body even at the attack tail). - [BASE_AMP_ADD]: 0.0, - - [mxLabel(0, 8, 'amp')]: 0.8, // ADSR1 → amp - [mxLabel(16, 1, 'bright')]: 0.3, // LFO1 → bright - [mxLabel(17, 5, 'formant_ctr')]: 0.4, // LFO2 → formant_ctr - }, -}; - -/** 5. DX bell — 4 ADSRs with staggered decays driving FM operator levels. */ -const DX_BELL_PRESET = { - version: 1, - subEngine: 'fm', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: { - // All four ADSRs: fast attack, varying decay, zero sustain, equal release - [adsrLabel(1, 'enable')]: 1.0, - [adsrLabel(1, 'attack')]: 0.005, - [adsrLabel(1, 'decay')]: 0.3, - [adsrLabel(1, 'sustain')]: 0.0, - [adsrLabel(1, 'release')]: 0.5, - - [adsrLabel(2, 'enable')]: 1.0, - [adsrLabel(2, 'attack')]: 0.005, - [adsrLabel(2, 'decay')]: 0.6, - [adsrLabel(2, 'sustain')]: 0.0, - [adsrLabel(2, 'release')]: 0.5, - - [adsrLabel(3, 'enable')]: 1.0, - [adsrLabel(3, 'attack')]: 0.005, - [adsrLabel(3, 'decay')]: 1.2, - [adsrLabel(3, 'sustain')]: 0.0, - [adsrLabel(3, 'release')]: 0.5, - - [adsrLabel(4, 'enable')]: 1.0, - [adsrLabel(4, 'attack')]: 0.005, - [adsrLabel(4, 'decay')]: 2.0, - [adsrLabel(4, 'sustain')]: 0.0, - [adsrLabel(4, 'release')]: 0.5, - - // Matrix: each ADSR → the matching op level (d01..d04 in the fm sub-engine) - [mxLabel(0, 1, 'op1_level')]: 1.0, - [mxLabel(1, 2, 'op2_level')]: 1.0, - [mxLabel(2, 3, 'op3_level')]: 1.0, - [mxLabel(3, 4, 'op4_level')]: 1.0, - }, -}; - -/** 6. Morphing drone — additive, very slow amp, slow spectral LFOs. */ -const MORPHING_DRONE_PRESET = { - version: 1, - subEngine: 'additive', - adsrCount: 4, - lfoCount: 8, - exposedEngineParams: [], - dsp: { - [adsrLabel(1, 'enable')]: 1.0, - [adsrLabel(1, 'attack')]: 5.0, - [adsrLabel(1, 'decay')]: 1.0, - [adsrLabel(1, 'sustain')]: 1.0, - [adsrLabel(1, 'release')]: 6.0, - - [lfoLabel(1, 'enable')]: 1.0, - [lfoLabel(1, 'rate')]: 0.1, - [lfoLabel(1, 'morph')]: 0.5, // between tri and saw - - [lfoLabel(2, 'enable')]: 1.0, - [lfoLabel(2, 'rate')]: 0.15, - [lfoLabel(2, 'morph')]: 0.66, - - // Base amp 0 → very slow ADSR1 gates the voice. - [BASE_AMP_ADD]: 0.0, - - [mxLabel(0, 8, 'amp')]: 1.0, // ADSR1 → amp - [mxLabel(16, 3, 'inharmonicity')]: 0.3, // LFO1 → inharmonicity - [mxLabel(17, 4, 'odd_even')]: 0.3, // LFO2 → odd_even - }, -}; - -export const MODULAR_PRESETS = [ - { - id: 'modular-default', - name: 'Default', - description: 'Phase B out-of-the-box patch.', - state: DEFAULT_PRESET, - }, - { - id: 'modular-slow-pad', - name: 'Slow pad', - description: 'Long ADSR, slow LFO cutoff sweep, subtle pitch drift.', - state: SLOW_PAD_PRESET, - }, - { - id: 'modular-plucky-bass', - name: 'Plucky bass', - description: 'Fast amp + filter envelopes, resonant lowpass.', - state: PLUCKY_BASS_PRESET, - }, - { - id: 'modular-crystal', - name: 'Crystal', - description: 'Additive tremolo with slow formant sweep.', - state: CRYSTAL_PRESET, - }, - { - id: 'modular-dx-bell', - name: 'DX bell', - description: 'Four staggered ADSRs driving FM operator levels.', - state: DX_BELL_PRESET, - }, - { - id: 'modular-morphing-drone', - name: 'Morphing drone', - description: 'Additive drone with LFO-driven spectral morphing.', - state: MORPHING_DRONE_PRESET, - }, -]; - -/** - * Apply a preset to a ModularEngine instance. Resets to defaults first so - * each preset is independent of whatever state preceded it. - * - * @param {import('./modular-engine.js').ModularEngine} engine - * @param {{state: object}} preset - */ -export async function applyPreset(engine, preset) { - if (!engine || !preset || !preset.state) return; - // Baseline first — this clears user mutations and re-seeds the - // default amp env / matrix / filter values. - if (typeof engine.resetToDefaults === 'function') { - engine.resetToDefaults(); - } - // Layer the preset's non-default values. - await engine.setState({ version: 1, ...preset.state }); -} - -/** Look up a preset by id. */ -export function findPreset(id) { - return MODULAR_PRESETS.find(p => p.id === id) ?? null; -} diff --git a/playground/js/synth/osc-output.js b/playground/js/synth/osc-output.js deleted file mode 100644 index 42f6a19..0000000 --- a/playground/js/synth/osc-output.js +++ /dev/null @@ -1,118 +0,0 @@ -// OSC output via WebSocket bridge -// Sends NISPS parameter values to a local WebSocket→OSC bridge script. -// Same throttle/dead-zone pattern as C15 ring buffer output. - -import { SYNTH_PARAM_MAP } from './param-map.js'; - -const RECONNECT_INTERVAL = 3000; -const SEND_INTERVAL = 50; // ~20fps max -const DEAD_ZONE = 0.002; // ~0.2% change threshold - -export class OSCOutput { - constructor(url = 'ws://localhost:8765') { - this._url = url; - this._ws = null; - this._connected = false; - this._reconnectTimer = null; - this._lastSent = new Float32Array(SYNTH_PARAM_MAP.length); - this._lastSendTime = 0; - this._onStatusChange = null; - this._enabled = false; - } - - set onStatusChange(fn) { this._onStatusChange = fn; } - get connected() { return this._connected; } - get enabled() { return this._enabled; } - - _status(msg) { - console.log('[OSC]', msg); - this._onStatusChange?.(msg, this._connected); - } - - connect() { - this._enabled = true; - this._tryConnect(); - } - - disconnect() { - this._enabled = false; - clearTimeout(this._reconnectTimer); - this._reconnectTimer = null; - if (this._ws) { - this._ws.close(); - this._ws = null; - } - this._connected = false; - this._status('Disconnected'); - } - - _tryConnect() { - if (!this._enabled) return; - if (this._ws) { - this._ws.close(); - this._ws = null; - } - - try { - this._ws = new WebSocket(this._url); - } catch (e) { - this._scheduleReconnect(); - return; - } - - this._ws.onopen = () => { - this._connected = true; - this._lastSent.fill(0); - this._status('Connected'); - }; - - this._ws.onclose = () => { - this._connected = false; - this._status('Disconnected'); - this._scheduleReconnect(); - }; - - this._ws.onerror = () => { - // onclose will fire after this - }; - - this._ws.onmessage = (e) => { - // Bridge can send back info (e.g. target confirmation) - try { - const msg = JSON.parse(e.data); - if (msg.type === 'info') this._status(msg.message); - } catch {} - }; - } - - _scheduleReconnect() { - if (!this._enabled || this._reconnectTimer) return; - this._reconnectTimer = setTimeout(() => { - this._reconnectTimer = null; - this._tryConnect(); - }, RECONNECT_INTERVAL); - } - - /** Send parameter values — same signature/timing as C15 output path */ - sendParams(overriddenValues) { - if (!this._connected || !this._ws || this._ws.readyState !== WebSocket.OPEN) return; - - const now = performance.now(); - if (now - this._lastSendTime < SEND_INTERVAL) return; - this._lastSendTime = now; - - // Build batch of changed params - const batch = []; - for (let i = 0; i < overriddenValues.length && i < SYNTH_PARAM_MAP.length; i++) { - const v = overriddenValues[i]; - if (Math.abs(v - this._lastSent[i]) > DEAD_ZONE) { - batch.push([SYNTH_PARAM_MAP[i].name, v]); - this._lastSent[i] = v; - } - } - - if (batch.length > 0) { - this._ws.send(JSON.stringify(batch)); - } - } -} diff --git a/playground/js/synth/param-map.js b/playground/js/synth/param-map.js deleted file mode 100644 index b567695..0000000 --- a/playground/js/synth/param-map.js +++ /dev/null @@ -1,304 +0,0 @@ -// Maps NISPS outputs (0-1) to curated C15 synth parameters -// 126 params: all sonically meaningful continuous parameters -// Excludes: hardware routing, macros, scale/tuning, key tracking, velocity, -// envelope modulation depths, discrete switches, dangerous volume/pitch controls -// -// Grouped by synthesis section for readability: -// Envelopes (20) → Oscillators (10) → Shapers (12) → Filters (24) → -// Feedback Mixer (9) → Output Mixer (14) → Cabinet (8) → -// Flanger (13) → Echo (7) → Reverb (6) → Unison (3) → Mono (1) -// -// safeMin/safeMax: when the tame parameter > 0, the ML output range is -// interpolated from [0,1] toward [safeMin,safeMax]. This prevents: -// - Volume spikes (drive/level params capped) -// - Silence (output levels kept above minimum) -// - Voice buildup / wall of sound (envelope release/decay times capped, -// reverb size and echo feedback limited) -// - Feedback runaway (FB mixer sources, comb decay, flanger FB capped) -// - Harsh noise (PM self-mod and FB amounts constrained) -// - Filter self-oscillation (SVF/comb/gap resonance capped) -// - Effect energy buildup (echo/reverb mix and cross-FB limited) - -export const SYNTH_PARAM_MAP = [ - // --- Envelope A (7) --- - { id: 0, name: 'Env_A_Att', label: 'EnvA Att', defaultValue: 0, bipolar: false }, - { id: 2, name: 'Env_A_Dec_1', label: 'EnvA Dec1', defaultValue: 0.59, bipolar: false }, - { id: 4, name: 'Env_A_BP', label: 'EnvA BP', defaultValue: 0.5, bipolar: false }, - { id: 6, name: 'Env_A_Dec_2', label: 'EnvA Dec2', defaultValue: 0.79, bipolar: false, safeMax: 0.85 }, - { id: 8, name: 'Env_A_Sus', label: 'EnvA Sus', defaultValue: 0, bipolar: false }, - { id: 10, name: 'Env_A_Rel', label: 'EnvA Rel', defaultValue: 0.53, bipolar: false, safeMax: 0.7 }, - { id: 12, name: 'Env_A_Gain', label: 'EnvA Gain', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - - // --- Envelope B (7) --- - { id: 19, name: 'Env_B_Att', label: 'EnvB Att', defaultValue: 0, bipolar: false }, - { id: 21, name: 'Env_B_Dec_1', label: 'EnvB Dec1', defaultValue: 0.59, bipolar: false }, - { id: 23, name: 'Env_B_BP', label: 'EnvB BP', defaultValue: 0.5, bipolar: false }, - { id: 25, name: 'Env_B_Dec_2', label: 'EnvB Dec2', defaultValue: 0.79, bipolar: false, safeMax: 0.85 }, - { id: 27, name: 'Env_B_Sus', label: 'EnvB Sus', defaultValue: 0, bipolar: false }, - { id: 29, name: 'Env_B_Rel', label: 'EnvB Rel', defaultValue: 0.53, bipolar: false, safeMax: 0.7 }, - { id: 31, name: 'Env_B_Gain', label: 'EnvB Gain', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - - // --- Envelope C (6) --- - { id: 38, name: 'Env_C_Att', label: 'EnvC Att', defaultValue: 0, bipolar: false }, - { id: 40, name: 'Env_C_Dec_1', label: 'EnvC Dec1', defaultValue: 0.59, bipolar: false }, - { id: 42, name: 'Env_C_BP', label: 'EnvC BP', defaultValue: 0.5, bipolar: true }, - { id: 44, name: 'Env_C_Dec_2', label: 'EnvC Dec2', defaultValue: 0.79, bipolar: false, safeMax: 0.85 }, - { id: 46, name: 'Env_C_Rel', label: 'EnvC Rel', defaultValue: 0.53, bipolar: false, safeMax: 0.7 }, - { id: 297, name: 'Env_C_Sus', label: 'EnvC Sus', defaultValue: 0, bipolar: true }, - - // --- Oscillator A (5) --- - { id: 57, name: 'Osc_A_Fluct', label: 'OscA Fluct', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 60, name: 'Osc_A_PM_Self', label: 'OscA PM-Self', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - { id: 64, name: 'Osc_A_PM_B', label: 'OscA PM-B', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 68, name: 'Osc_A_PM_FB', label: 'OscA PM-FB', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - { id: 301, name: 'Osc_A_Phase', label: 'OscA Phase', defaultValue: 0, bipolar: true }, - - // --- Oscillator B (5) --- - { id: 87, name: 'Osc_B_Fluct', label: 'OscB Fluct', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 90, name: 'Osc_B_PM_Self', label: 'OscB PM-Self', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - { id: 94, name: 'Osc_B_PM_A', label: 'OscB PM-A', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 98, name: 'Osc_B_PM_FB', label: 'OscB PM-FB', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - { id: 302, name: 'Osc_B_Phase', label: 'OscB Phase', defaultValue: 0, bipolar: true }, - - // --- Shaper A (6) --- - { id: 71, name: 'Shp_A_Drive', label: 'ShpA Drive', defaultValue: 0.2, bipolar: false, safeMax: 0.6 }, - { id: 74, name: 'Shp_A_Fold', label: 'ShpA Fold', defaultValue: 0.5, bipolar: false }, - { id: 75, name: 'Shp_A_Asym', label: 'ShpA Asym', defaultValue: 0, bipolar: false }, - { id: 76, name: 'Shp_A_Mix', label: 'ShpA Mix', defaultValue: 0, bipolar: true }, - { id: 78, name: 'Shp_A_FB_Mix', label: 'ShpA FB Mix', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 81, name: 'Shp_A_Ring_Mod', label: 'ShpA Ring', defaultValue: 0, bipolar: false }, - - // --- Shaper B (6) --- - { id: 101, name: 'Shp_B_Drive', label: 'ShpB Drive', defaultValue: 0.2, bipolar: false, safeMax: 0.6 }, - { id: 104, name: 'Shp_B_Fold', label: 'ShpB Fold', defaultValue: 0.5, bipolar: false }, - { id: 105, name: 'Shp_B_Asym', label: 'ShpB Asym', defaultValue: 0, bipolar: false }, - { id: 106, name: 'Shp_B_Mix', label: 'ShpB Mix', defaultValue: 0, bipolar: true }, - { id: 108, name: 'Shp_B_FB_Mix', label: 'ShpB FB Mix', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 111, name: 'Shp_B_Ring_Mod', label: 'ShpB Ring', defaultValue: 0, bipolar: false }, - - // --- Comb Filter (8) --- - { id: 113, name: 'Comb_Flt_In_A_B', label: 'Comb In A/B', defaultValue: 0, bipolar: false }, - { id: 115, name: 'Comb_Flt_Pitch', label: 'Comb Pitch', defaultValue: 0.5, bipolar: false }, - { id: 119, name: 'Comb_Flt_Decay', label: 'Comb Decay', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - { id: 123, name: 'Comb_Flt_AP_Tune', label: 'Comb AP Tune', defaultValue: 1, bipolar: false }, - { id: 127, name: 'Comb_Flt_AP_Res', label: 'Comb AP Res', defaultValue: 0.5, bipolar: false, safeMax: 0.8 }, - { id: 129, name: 'Comb_Flt_LP_Tune', label: 'Comb LP Tune', defaultValue: 1, bipolar: false }, - { id: 133, name: 'Comb_Flt_PM', label: 'Comb PM', defaultValue: 0, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - { id: 135, name: 'Comb_Flt_PM_A_B', label: 'Comb PM A/B', defaultValue: 0, bipolar: false }, - - // --- State Variable Filter (9) --- - { id: 136, name: 'SV_Flt_In_A_B', label: 'SVF In A/B', defaultValue: 0, bipolar: false }, - { id: 138, name: 'SV_Flt_Comb_Mix', label: 'SVF CombMix', defaultValue: 0, bipolar: true }, - { id: 140, name: 'SV_Flt_Cut', label: 'SVF Cutoff', defaultValue: 0.5, bipolar: false }, - { id: 144, name: 'SV_Flt_Res', label: 'SVF Reso', defaultValue: 0.5, bipolar: false, safeMax: 0.8 }, - { id: 148, name: 'SV_Flt_Spread', label: 'SVF Spread', defaultValue: 0.2, bipolar: true }, - { id: 150, name: 'SV_Flt_LBH', label: 'SVF L/B/H', defaultValue: 0, bipolar: false }, - { id: 152, name: 'SV_Flt_Par', label: 'SVF Par', defaultValue: 0, bipolar: true }, - { id: 153, name: 'SV_Flt_FM', label: 'SVF FM', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 155, name: 'SV_Flt_FM_A_B', label: 'SVF FM A/B', defaultValue: 0, bipolar: false }, - - // --- Gap Filter (6) --- - { id: 201, name: 'Gap_Flt_Center', label: 'Gap Center', defaultValue: 0.5, bipolar: false }, - { id: 203, name: 'Gap_Flt_Stereo', label: 'Gap Stereo', defaultValue: 0, bipolar: true }, - { id: 204, name: 'Gap_Flt_Gap', label: 'Gap Width', defaultValue: 0.125, bipolar: false }, - { id: 206, name: 'Gap_Flt_Res', label: 'Gap Res', defaultValue: 0.5, bipolar: false, safeMax: 0.8 }, - { id: 207, name: 'Gap_Flt_Bal', label: 'Gap Bal', defaultValue: 0, bipolar: true }, - { id: 209, name: 'Gap_Flt_Mix', label: 'Gap Mix', defaultValue: 0, bipolar: true }, - - // --- Feedback Mixer (9) --- - { id: 156, name: 'FB_Mix_Comb', label: 'FB Comb', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - { id: 158, name: 'FB_Mix_SVF', label: 'FB SVF', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - { id: 160, name: 'FB_Mix_FX', label: 'FB FX', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - { id: 162, name: 'FB_Mix_Rvb', label: 'FB Reverb', defaultValue: 0.5, bipolar: false, safeMax: 0.75 }, - { id: 164, name: 'FB_Mix_Drive', label: 'FB Drive', defaultValue: 0.286, bipolar: false, safeMax: 0.4 }, - { id: 166, name: 'FB_Mix_Fold', label: 'FB Fold', defaultValue: 0.5, bipolar: false }, - { id: 167, name: 'FB_Mix_Asym', label: 'FB Asym', defaultValue: 0, bipolar: false }, - { id: 299, name: 'FB_Mix_Lvl', label: 'FB Level', defaultValue: 0.38, bipolar: false, safeMax: 0.55 }, - { id: 346, name: 'FB_Mix_Osc', label: 'FB Osc', defaultValue: 0, bipolar: true, safeMin: 0.25, safeMax: 0.75 }, - - // --- Output Mixer (14) --- - { id: 169, name: 'Out_Mix_A_Lvl', label: 'Out A Lvl', defaultValue: 0.75, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 171, name: 'Out_Mix_A_Pan', label: 'Out A Pan', defaultValue: 0, bipolar: true }, - { id: 172, name: 'Out_Mix_B_Lvl', label: 'Out B Lvl', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 174, name: 'Out_Mix_B_Pan', label: 'Out B Pan', defaultValue: 0, bipolar: true }, - { id: 175, name: 'Out_Mix_Comb_Lvl', label: 'Out Comb Lvl', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 177, name: 'Out_Mix_Comb_Pan', label: 'Out Comb Pan', defaultValue: 0, bipolar: true }, - { id: 178, name: 'Out_Mix_SVF_Lvl', label: 'Out SVF Lvl', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 180, name: 'Out_Mix_SVF_Pan', label: 'Out SVF Pan', defaultValue: 0, bipolar: true }, - { id: 181, name: 'Out_Mix_Drive', label: 'Out Drive', defaultValue: 0, bipolar: false, safeMax: 0.5 }, - { id: 183, name: 'Out_Mix_Fold', label: 'Out Fold', defaultValue: 0.5, bipolar: false, safeMax: 0.75 }, - { id: 184, name: 'Out_Mix_Asym', label: 'Out Asym', defaultValue: 0, bipolar: false }, - { id: 185, name: 'Out_Mix_Lvl', label: 'Out Level', defaultValue: 0.38, bipolar: false, safeMin: 0.15, safeMax: 0.75 }, - { id: 187, name: 'Out_Mix_Key_Pan', label: 'Out KeyPan', defaultValue: 0, bipolar: false }, - { id: 362, name: 'Out_Mix_To_FX', label: 'Out ToFX', defaultValue: 0, bipolar: false, safeMax: 0.8 }, - - // --- Cabinet (8) --- - { id: 188, name: 'Cabinet_Drive', label: 'Cab Drive', defaultValue: 0.4, bipolar: false, safeMax: 0.6 }, - { id: 190, name: 'Cabinet_Fold', label: 'Cab Fold', defaultValue: 0.25, bipolar: false }, - { id: 191, name: 'Cabinet_Asym', label: 'Cab Asym', defaultValue: 0.25, bipolar: false }, - { id: 192, name: 'Cabinet_Tilt', label: 'Cab Tilt', defaultValue: 0.5, bipolar: true }, - { id: 194, name: 'Cabinet_Hi_Cut', label: 'Cab HiCut', defaultValue: 0.625, bipolar: false }, - { id: 196, name: 'Cabinet_Lo_Cut', label: 'Cab LoCut', defaultValue: 0.125, bipolar: false }, - { id: 197, name: 'Cabinet_Cab_Lvl', label: 'Cab Level', defaultValue: 0.72, bipolar: false, safeMax: 0.85 }, - { id: 199, name: 'Cabinet_Mix', label: 'Cab Mix', defaultValue: 0, bipolar: false }, - - // --- Flanger (13) --- - { id: 211, name: 'Flanger_Time_Mod', label: 'Flng TimeMod', defaultValue: 0, bipolar: true }, - { id: 213, name: 'Flanger_Phase', label: 'Flng Phase', defaultValue: 0.5, bipolar: false }, - { id: 214, name: 'Flanger_Rate', label: 'Flng Rate', defaultValue: 0.317, bipolar: false }, - { id: 216, name: 'Flanger_Time', label: 'Flng Time', defaultValue: 0.317, bipolar: false }, - { id: 218, name: 'Flanger_Stereo', label: 'Flng Stereo', defaultValue: 0, bipolar: true }, - { id: 219, name: 'Flanger_Feedback', label: 'Flng FB', defaultValue: 0, bipolar: true, safeMin: 0.15, safeMax: 0.85 }, - { id: 221, name: 'Flanger_Cross_FB', label: 'Flng XFB', defaultValue: 0.5, bipolar: true, safeMin: 0.2, safeMax: 0.8 }, - { id: 222, name: 'Flanger_Hi_Cut', label: 'Flng HiCut', defaultValue: 0.75, bipolar: false }, - { id: 223, name: 'Flanger_Mix', label: 'Flng Mix', defaultValue: 0, bipolar: true }, - { id: 307, name: 'Flanger_Envelope', label: 'Flng Env', defaultValue: 0, bipolar: false }, - { id: 308, name: 'Flanger_AP_Mod', label: 'Flng AP Mod', defaultValue: 0, bipolar: true }, - { id: 310, name: 'Flanger_AP_Tune', label: 'Flng AP Tune', defaultValue: 1, bipolar: false }, - { id: 389, name: 'Flanger_Tremolo', label: 'Flng Trem', defaultValue: 0, bipolar: false }, - - // --- Echo (7) --- - { id: 225, name: 'Echo_Time', label: 'Echo Time', defaultValue: 0.433, bipolar: false }, - { id: 227, name: 'Echo_Stereo', label: 'Echo Stereo', defaultValue: 0, bipolar: true }, - { id: 229, name: 'Echo_Feedback', label: 'Echo FB', defaultValue: 0.5, bipolar: false, safeMax: 0.75 }, - { id: 231, name: 'Echo_Cross_FB', label: 'Echo XFB', defaultValue: 0.5, bipolar: false, safeMax: 0.7 }, - { id: 232, name: 'Echo_Hi_Cut', label: 'Echo HiCut', defaultValue: 0.75, bipolar: false }, - { id: 233, name: 'Echo_Mix', label: 'Echo Mix', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 342, name: 'Echo_Send', label: 'Echo Send', defaultValue: 1, bipolar: false }, - - // --- Reverb (6) --- - { id: 235, name: 'Reverb_Size', label: 'Verb Size', defaultValue: 0.33, bipolar: false, safeMax: 0.7 }, - { id: 237, name: 'Reverb_Pre_Dly', label: 'Verb PreDly', defaultValue: 0.33, bipolar: false }, - { id: 238, name: 'Reverb_Color', label: 'Verb Color', defaultValue: 0.5, bipolar: false }, - { id: 240, name: 'Reverb_Chorus', label: 'Verb Chorus', defaultValue: 0.25, bipolar: false }, - { id: 241, name: 'Reverb_Mix', label: 'Verb Mix', defaultValue: 0, bipolar: false, safeMax: 0.7 }, - { id: 344, name: 'Reverb_Send', label: 'Verb Send', defaultValue: 1, bipolar: false, safeMax: 0.85 }, - - // --- Unison (3) --- - { id: 250, name: 'Unison_Detune', label: 'Uni Detune', defaultValue: 0.004, bipolar: false, safeMax: 0.5 }, - { id: 252, name: 'Unison_Phase', label: 'Uni Phase', defaultValue: 0, bipolar: false }, - { id: 253, name: 'Unison_Pan', label: 'Uni Pan', defaultValue: 0, bipolar: false }, - - // --- Mono (1) --- - { id: 367, name: 'Mono_Grp_Glide', label: 'Mono Glide', defaultValue: 0, bipolar: false }, -]; - -// Parameter display names for the param bar UI -export const SYNTH_PARAM_NAMES = SYNTH_PARAM_MAP.map(p => p.label); - -// Generate colors by group using HSL for visual distinction -// Each synthesis section gets a hue range, params within get varying lightness -function generateParamColors() { - // Group hues: envelope=warm red, osc=orange, shaper=magenta, comb=cyan, - // svf=blue, gap=teal, fb=purple, out=green, cab=brown, flanger=pink, - // echo=sky, reverb=lavender, unison=lime, mono=gold - const groupColors = [ - // Envelope A (7) - warm red - ...hslRange(0, 75, 55, 7), - // Envelope B (7) - coral - ...hslRange(15, 75, 55, 7), - // Envelope C (6) - salmon - ...hslRange(30, 70, 55, 6), - // Oscillator A (5) - orange - ...hslRange(35, 85, 55, 5), - // Oscillator B (5) - amber - ...hslRange(45, 85, 55, 5), - // Shaper A (6) - magenta - ...hslRange(320, 75, 55, 6), - // Shaper B (6) - rose - ...hslRange(340, 70, 55, 6), - // Comb Filter (8) - cyan - ...hslRange(180, 70, 50, 8, true), - // State Variable Filter (9) - blue - ...hslRange(210, 75, 55, 9, true), - // Gap Filter (6) - teal - ...hslRange(165, 65, 50, 6), - // Feedback Mixer (9) - purple - ...hslRange(270, 65, 55, 9, true), - // Output Mixer (14) - green - ...hslRange(130, 65, 50, 14, true), - // Cabinet (8) - brown/earth - ...hslRange(25, 55, 50, 8), - // Flanger (13) - pink - ...hslRange(300, 60, 60, 13, true), - // Echo (7) - sky blue - ...hslRange(195, 70, 55, 7), - // Reverb (6) - lavender - ...hslRange(255, 55, 65, 6), - // Unison (3) - lime - ...hslRange(90, 70, 50, 3), - // Mono (1) - gold - ...hslRange(50, 80, 55, 1), - ]; - return groupColors; -} - -function hslRange(hue, sat, lightBase, count, varyHue = false) { - const colors = []; - for (let i = 0; i < count; i++) { - const t = count > 1 ? i / (count - 1) : 0; - const l = lightBase + (t - 0.5) * 16; - const h = varyHue ? hue + (t - 0.5) * 15 : hue; - const s = sat; - colors.push(`hsl(${Math.round(h)}, ${Math.round(s)}%, ${Math.round(l)}%)`); - } - return colors; -} - -export const SYNTH_PARAM_COLORS = generateParamColors(); - -// --- Tame parameter: constrain ML outputs to safe ranges --- -// tameLevel 0 = no constraint (full 0-1), 1 = maximum constraint (safeMin-safeMax) -// Params without safeMin/safeMax are unaffected. - -/** - * Apply tame-level range constraining to a raw ML output value. - * @param {number} rawValue - ML output in [0, 1] - * @param {object} paramEntry - entry from SYNTH_PARAM_MAP - * @param {number} tameLevel - 0 (no mitigation) to 1 (strongest) - * @returns {number} constrained value in [0, 1] - */ -export function applyTame(rawValue, paramEntry, tameLevel) { - if (tameLevel <= 0) return rawValue; - const lo = paramEntry.safeMin; - const hi = paramEntry.safeMax; - if (lo === undefined && hi === undefined) return rawValue; - - // Interpolate range bounds toward safe limits based on tameLevel - const effectiveMin = lo !== undefined ? lo * tameLevel : 0; - const effectiveMax = hi !== undefined ? 1 - (1 - hi) * tameLevel : 1; - - return effectiveMin + rawValue * (effectiveMax - effectiveMin); -} - -// --- Group overrides: per-parameter min/max and per-group curve --- - -/** - * Apply a power curve to remap a [0,1] value. - * curveFactor 0 = logarithmic (steep at start, flat at end) - * curveFactor 0.5 = linear (no change) - * curveFactor 1 = exponential (flat at start, steep at end) - * - * Internally maps curveFactor to an exponent: exp = 2^(4*(curve-0.5)) - * curve=0 -> exp=0.25 (log-ish) - * curve=0.5 -> exp=1 (linear) - * curve=1 -> exp=4 (exp-ish) - */ -export function applyCurve(value, curveFactor) { - if (curveFactor === 0.5) return value; - const exponent = Math.pow(2, 4 * (curveFactor - 0.5)); - return Math.pow(Math.max(0, Math.min(1, value)), exponent); -} - -/** - * Apply group override (curve + min/max) to a raw ML output. - * @param {number} rawValue - ML output in [0, 1] - * @param {number} curve - curve factor [0, 1], 0.5 = linear - * @param {number} min - output range minimum [0, 1] - * @param {number} max - output range maximum [0, 1] - * @returns {number} remapped value - */ -export function applyGroupOverride(rawValue, curve, min, max) { - const curved = applyCurve(rawValue, curve); - return min + curved * (max - min); -} diff --git a/playground/js/synth/presets.js b/playground/js/synth/presets.js deleted file mode 100644 index 8c5cc15..0000000 --- a/playground/js/synth/presets.js +++ /dev/null @@ -1,628 +0,0 @@ -// Synth presets for NISPS playground -// Each preset defines which of the 126 C15 params are active (ML-controlled) -// and which are muted at safe defaults. Organized in 4 tiers of complexity. - -export const SYNTH_PRESETS = [ - // ====================================================================== - // TIER 1 — Beginner (15 active params) - // ====================================================================== - { - id: 'beginner-1', - name: '1.1', - tier: 1, - description: 'Mellow filtered tones with slow attack and long reverb tail', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.2, max: 0.7, curve: 0.6 }, - 'Env_A_Dec_1': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Env_A_Sus': { min: 0.3, max: 0.8, curve: 0.6 }, - 'Env_A_Rel': { min: 0.3, max: 0.65, curve: 0.55 }, - 'Osc_A_Fluct': { min: 0.0, max: 0.3, curve: 0.35 }, - 'Osc_B_Fluct': { min: 0.0, max: 0.3, curve: 0.35 }, - 'Shp_A_Drive': { min: 0.05, max: 0.35, curve: 0.35 }, - 'Shp_A_Fold': { min: 0.3, max: 0.7, curve: 0.45 }, - 'SV_Flt_Cut': { min: 0.15, max: 0.55, curve: 0.35 }, - 'SV_Flt_Res': { min: 0.2, max: 0.6, curve: 0.4 }, - 'SV_Flt_LBH': { min: 0.0, max: 0.4, curve: 0.35 }, - 'Out_Mix_A_Lvl': { min: 0.4, max: 0.85, curve: 0.55 }, - 'Out_Mix_SVF_Lvl': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Out_Mix_Lvl': { min: 0.25, max: 0.6, curve: 0.45 }, - 'Reverb_Mix': { min: 0.15, max: 0.55, curve: 0.4 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'beginner-2', - name: '1.2', - tier: 1, - description: 'Snappy attack, crisp shaper, filter biased high', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.1, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.15, max: 0.5, curve: 0.35 }, - 'Env_A_Sus': { min: 0.0, max: 0.4, curve: 0.4 }, - 'Env_A_Rel': { min: 0.15, max: 0.45, curve: 0.4 }, - 'Osc_A_Fluct': { min: 0.0, max: 0.2, curve: 0.35 }, - 'Osc_B_Fluct': { min: 0.0, max: 0.2, curve: 0.35 }, - 'Shp_A_Drive': { min: 0.15, max: 0.55, curve: 0.6 }, - 'Shp_A_Fold': { min: 0.2, max: 0.8, curve: 0.55 }, - 'SV_Flt_Cut': { min: 0.5, max: 0.95, curve: 0.65 }, - 'SV_Flt_Res': { min: 0.3, max: 0.7, curve: 0.55 }, - 'SV_Flt_LBH': { min: 0.4, max: 1.0, curve: 0.6 }, - 'Out_Mix_A_Lvl': { min: 0.4, max: 0.85, curve: 0.55 }, - 'Out_Mix_SVF_Lvl': { min: 0.35, max: 0.8, curve: 0.55 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.25, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'beginner-3', - name: '1.3', - tier: 1, - description: 'Slow attack, high sustain, wide filter sweep, deep reverb', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.3, max: 0.85, curve: 0.6 }, - 'Env_A_Dec_1': { min: 0.4, max: 0.9, curve: 0.6 }, - 'Env_A_Sus': { min: 0.5, max: 1.0, curve: 0.6 }, - 'Env_A_Rel': { min: 0.35, max: 0.7, curve: 0.55 }, - 'Osc_A_Fluct': { min: 0.0, max: 0.4, curve: 0.35 }, - 'Osc_B_Fluct': { min: 0.0, max: 0.4, curve: 0.35 }, - 'Shp_A_Drive': { min: 0.0, max: 0.25, curve: 0.3 }, - 'Shp_A_Fold': { min: 0.4, max: 0.65, curve: 0.45 }, - 'SV_Flt_Cut': { min: 0.1, max: 0.85, curve: 0.45 }, - 'SV_Flt_Res': { min: 0.15, max: 0.55, curve: 0.4 }, - 'SV_Flt_LBH': { min: 0.0, max: 0.6, curve: 0.4 }, - 'Out_Mix_A_Lvl': { min: 0.45, max: 0.85, curve: 0.55 }, - 'Out_Mix_SVF_Lvl': { min: 0.4, max: 0.85, curve: 0.55 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Reverb_Mix': { min: 0.2, max: 0.65, curve: 0.45 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'beginner-4', - name: '1.4', - tier: 1, - description: 'Zero attack, fast decay, low sustain, bright filter with steep curve', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.02, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.1, max: 0.4, curve: 0.35 }, - 'Env_A_Sus': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Env_A_Rel': { min: 0.1, max: 0.35, curve: 0.4 }, - 'Osc_A_Fluct': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Osc_B_Fluct': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Shp_A_Drive': { min: 0.1, max: 0.45, curve: 0.55 }, - 'Shp_A_Fold': { min: 0.3, max: 0.75, curve: 0.55 }, - 'SV_Flt_Cut': { min: 0.55, max: 1.0, curve: 0.65 }, - 'SV_Flt_Res': { min: 0.25, max: 0.65, curve: 0.6 }, - 'SV_Flt_LBH': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Out_Mix_A_Lvl': { min: 0.4, max: 0.85, curve: 0.55 }, - 'Out_Mix_SVF_Lvl': { min: 0.35, max: 0.8, curve: 0.55 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.3, curve: 0.3 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 2 — Intermediate (adds 25 params → 40 total) - // ====================================================================== - { - id: 'intermediate-1', - name: '2.1', - tier: 2, - description: 'Lush reverb and echo, wide stereo spread, gentle cabinet warmth', - active: [ - // Tier 1 base - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - // Tier 2 additions - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.15, max: 0.6, curve: 0.55 }, - 'Env_A_Dec_1': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Env_A_Sus': { min: 0.3, max: 0.8, curve: 0.6 }, - 'Env_A_Rel': { min: 0.3, max: 0.65, curve: 0.55 }, - 'SV_Flt_Cut': { min: 0.2, max: 0.75, curve: 0.45 }, - 'SV_Flt_Spread': { min: 0.2, max: 0.8, curve: 0.6 }, - 'Out_Mix_A_Pan': { min: 0.2, max: 0.8, curve: 0.5 }, - 'Out_Mix_To_FX': { min: 0.2, max: 0.7, curve: 0.55 }, - 'Reverb_Size': { min: 0.25, max: 0.65, curve: 0.6 }, - 'Reverb_Color': { min: 0.3, max: 0.7, curve: 0.45 }, - 'Reverb_Mix': { min: 0.15, max: 0.6, curve: 0.45 }, - 'Echo_Mix': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Echo_Time': { min: 0.2, max: 0.7, curve: 0.55 }, - 'Flanger_Mix': { min: 0.0, max: 0.3, curve: 0.35 }, - 'Flanger_Time': { min: 0.2, max: 0.6, curve: 0.45 }, - 'Flanger_Rate': { min: 0.1, max: 0.4, curve: 0.4 }, - 'Cabinet_Drive': { min: 0.1, max: 0.35, curve: 0.35 }, - 'Cabinet_Tilt': { min: 0.3, max: 0.7, curve: 0.45 }, - 'Cabinet_Mix': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'intermediate-2', - name: '2.2', - tier: 2, - description: 'Short envelopes, echo with rhythmic timing, flanger movement', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.08, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.1, max: 0.4, curve: 0.35 }, - 'Env_A_Sus': { min: 0.0, max: 0.25, curve: 0.3 }, - 'Env_A_Rel': { min: 0.1, max: 0.35, curve: 0.35 }, - 'Env_B_Att': { min: 0.0, max: 0.05, curve: 0.3 }, - 'Env_B_Dec_1': { min: 0.1, max: 0.35, curve: 0.35 }, - 'SV_Flt_Cut': { min: 0.3, max: 0.9, curve: 0.55 }, - 'SV_Flt_FM': { min: 0.3, max: 0.7, curve: 0.45 }, - 'Echo_Mix': { min: 0.15, max: 0.55, curve: 0.45 }, - 'Echo_Time': { min: 0.15, max: 0.6, curve: 0.35 }, - 'Flanger_Mix': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Flanger_Time': { min: 0.1, max: 0.5, curve: 0.45 }, - 'Flanger_Rate': { min: 0.2, max: 0.7, curve: 0.45 }, - 'Cabinet_Drive': { min: 0.15, max: 0.45, curve: 0.55 }, - 'Out_Mix_To_FX': { min: 0.25, max: 0.7, curve: 0.55 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.2, curve: 0.3 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'intermediate-3', - name: '2.3', - tier: 2, - description: 'Long envelope times, filter FM, slow flanger, lush reverb', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.3, max: 0.85, curve: 0.6 }, - 'Env_A_Dec_1': { min: 0.5, max: 0.9, curve: 0.6 }, - 'Env_A_Dec_2': { min: 0.5, max: 0.85, curve: 0.6 }, - 'Env_A_Sus': { min: 0.4, max: 0.9, curve: 0.6 }, - 'Env_A_Rel': { min: 0.35, max: 0.65, curve: 0.55 }, - 'Env_B_Att': { min: 0.2, max: 0.7, curve: 0.6 }, - 'Env_B_Dec_1': { min: 0.4, max: 0.85, curve: 0.6 }, - 'Env_B_Sus': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Env_C_Att': { min: 0.2, max: 0.6, curve: 0.55 }, - 'SV_Flt_Cut': { min: 0.15, max: 0.8, curve: 0.4 }, - 'SV_Flt_FM': { min: 0.25, max: 0.75, curve: 0.45 }, - 'SV_Flt_Spread': { min: 0.2, max: 0.7, curve: 0.55 }, - 'Reverb_Size': { min: 0.3, max: 0.65, curve: 0.55 }, - 'Reverb_Color': { min: 0.3, max: 0.7, curve: 0.45 }, - 'Reverb_Mix': { min: 0.2, max: 0.6, curve: 0.45 }, - 'Echo_Mix': { min: 0.05, max: 0.35, curve: 0.35 }, - 'Flanger_Mix': { min: 0.05, max: 0.35, curve: 0.35 }, - 'Flanger_Rate': { min: 0.05, max: 0.25, curve: 0.35 }, - 'Flanger_Time': { min: 0.2, max: 0.6, curve: 0.45 }, - 'Out_Mix_To_FX': { min: 0.2, max: 0.6, curve: 0.5 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Cabinet_Mix': { min: 0.1, max: 0.4, curve: 0.35 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'intermediate-4', - name: '2.4', - tier: 2, - description: 'Cabinet drive pushed, asymmetric shaper, compressed envelope, dry', - active: [ - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.1, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.15, max: 0.5, curve: 0.4 }, - 'Env_A_Sus': { min: 0.1, max: 0.5, curve: 0.45 }, - 'Env_A_Rel': { min: 0.15, max: 0.4, curve: 0.4 }, - 'Shp_A_Drive': { min: 0.2, max: 0.55, curve: 0.6 }, - 'Shp_A_Fold': { min: 0.2, max: 0.8, curve: 0.55 }, - 'Shp_A_Asym': { min: 0.1, max: 0.7, curve: 0.6 }, - 'Shp_A_Mix': { min: 0.2, max: 0.8, curve: 0.55 }, - 'SV_Flt_Cut': { min: 0.25, max: 0.8, curve: 0.55 }, - 'SV_Flt_Res': { min: 0.3, max: 0.75, curve: 0.55 }, - 'Cabinet_Drive': { min: 0.3, max: 0.58, curve: 0.6 }, - 'Cabinet_Tilt': { min: 0.2, max: 0.65, curve: 0.55 }, - 'Cabinet_Mix': { min: 0.3, max: 0.8, curve: 0.6 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.5, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Echo_Mix': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Flanger_Mix': { min: 0.0, max: 0.1, curve: 0.3 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 3 — Advanced (adds 55 params → 95 total) - // ====================================================================== - { - id: 'advanced-1', - name: '3.1', - tier: 3, - description: 'Cross-oscillator PM, ring modulation, comb filter resonance, clean effects', - active: [ - // Tier 1 - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - // Tier 2 - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - // Tier 3 additions - 'Env_B_BP', 'Env_B_Dec_2', 'Env_B_Gain', - 'Env_C_BP', 'Env_C_Dec_2', 'Env_C_Rel', 'Env_C_Sus', - 'Osc_A_PM_Self', 'Osc_A_PM_B', 'Osc_A_PM_FB', 'Osc_A_Phase', - 'Osc_B_PM_Self', 'Osc_B_PM_A', 'Osc_B_PM_FB', 'Osc_B_Phase', - 'Shp_A_FB_Mix', 'Shp_A_Ring_Mod', - 'Shp_B_Drive', 'Shp_B_Fold', 'Shp_B_Asym', 'Shp_B_Mix', 'Shp_B_FB_Mix', 'Shp_B_Ring_Mod', - 'Comb_Flt_Pitch', 'Comb_Flt_Decay', 'Comb_Flt_In_A_B', 'Comb_Flt_AP_Res', 'Comb_Flt_PM', - 'SV_Flt_In_A_B', 'SV_Flt_Comb_Mix', 'SV_Flt_Par', 'SV_Flt_FM_A_B', - 'Gap_Flt_Center', 'Gap_Flt_Gap', 'Gap_Flt_Res', 'Gap_Flt_Mix', - 'FB_Mix_Comb', 'FB_Mix_SVF', 'FB_Mix_FX', 'FB_Mix_Osc', 'FB_Mix_Lvl', 'FB_Mix_Drive', - 'Out_Mix_B_Lvl', 'Out_Mix_B_Pan', 'Out_Mix_Comb_Lvl', - 'Flanger_Feedback', 'Flanger_Stereo', 'Flanger_Phase', - 'Echo_Feedback', 'Echo_Stereo', - 'Reverb_Pre_Dly', 'Reverb_Chorus', - 'Cabinet_Fold', 'Cabinet_Asym', 'Cabinet_Hi_Cut', 'Cabinet_Lo_Cut', 'Cabinet_Cab_Lvl', - 'Unison_Detune', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.05, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.2, max: 0.6, curve: 0.35 }, - 'Env_A_Sus': { min: 0.0, max: 0.2, curve: 0.3 }, - 'Env_A_Rel': { min: 0.2, max: 0.55, curve: 0.4 }, - 'Osc_A_PM_Self': { min: 0.3, max: 0.7, curve: 0.4 }, - 'Osc_A_PM_B': { min: 0.25, max: 0.7, curve: 0.4 }, - 'Osc_A_PM_FB': { min: 0.3, max: 0.65, curve: 0.4 }, - 'Osc_B_PM_Self': { min: 0.3, max: 0.7, curve: 0.4 }, - 'Osc_B_PM_A': { min: 0.25, max: 0.7, curve: 0.4 }, - 'Osc_B_PM_FB': { min: 0.3, max: 0.65, curve: 0.4 }, - 'Shp_A_Ring_Mod': { min: 0.0, max: 0.5, curve: 0.35 }, - 'Shp_B_Ring_Mod': { min: 0.0, max: 0.5, curve: 0.35 }, - 'Comb_Flt_Pitch': { min: 0.3, max: 0.7, curve: 0.45 }, - 'Comb_Flt_Decay': { min: 0.3, max: 0.7, curve: 0.4 }, - 'Comb_Flt_AP_Res': { min: 0.2, max: 0.65, curve: 0.4 }, - 'SV_Flt_Cut': { min: 0.3, max: 0.85, curve: 0.55 }, - 'Reverb_Mix': { min: 0.15, max: 0.5, curve: 0.4 }, - 'Reverb_Size': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Reverb_Chorus': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Echo_Mix': { min: 0.0, max: 0.25, curve: 0.35 }, - 'Flanger_Mix': { min: 0.0, max: 0.2, curve: 0.3 }, - 'FB_Mix_Lvl': { min: 0.1, max: 0.4, curve: 0.4 }, - 'FB_Mix_Drive': { min: 0.1, max: 0.3, curve: 0.35 }, - 'Out_Mix_Lvl': { min: 0.2, max: 0.55, curve: 0.45 }, - 'Out_Mix_B_Lvl': { min: 0.15, max: 0.6, curve: 0.45 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'advanced-2', - name: '3.2', - tier: 3, - description: 'Feedback mixer active, dual shapers, gap filter, gritty cabinet', - active: [ - // Tier 1 - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - // Tier 2 - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - // Tier 3 additions - 'Env_B_BP', 'Env_B_Dec_2', 'Env_B_Gain', - 'Env_C_BP', 'Env_C_Dec_2', 'Env_C_Rel', 'Env_C_Sus', - 'Osc_A_PM_Self', 'Osc_A_PM_B', 'Osc_A_PM_FB', 'Osc_A_Phase', - 'Osc_B_PM_Self', 'Osc_B_PM_A', 'Osc_B_PM_FB', 'Osc_B_Phase', - 'Shp_A_FB_Mix', 'Shp_A_Ring_Mod', - 'Shp_B_Drive', 'Shp_B_Fold', 'Shp_B_Asym', 'Shp_B_Mix', 'Shp_B_FB_Mix', 'Shp_B_Ring_Mod', - 'Comb_Flt_Pitch', 'Comb_Flt_Decay', 'Comb_Flt_In_A_B', 'Comb_Flt_AP_Res', 'Comb_Flt_PM', - 'SV_Flt_In_A_B', 'SV_Flt_Comb_Mix', 'SV_Flt_Par', 'SV_Flt_FM_A_B', - 'Gap_Flt_Center', 'Gap_Flt_Gap', 'Gap_Flt_Res', 'Gap_Flt_Mix', - 'FB_Mix_Comb', 'FB_Mix_SVF', 'FB_Mix_FX', 'FB_Mix_Osc', 'FB_Mix_Lvl', 'FB_Mix_Drive', - 'Out_Mix_B_Lvl', 'Out_Mix_B_Pan', 'Out_Mix_Comb_Lvl', - 'Flanger_Feedback', 'Flanger_Stereo', 'Flanger_Phase', - 'Echo_Feedback', 'Echo_Stereo', - 'Reverb_Pre_Dly', 'Reverb_Chorus', - 'Cabinet_Fold', 'Cabinet_Asym', 'Cabinet_Hi_Cut', 'Cabinet_Lo_Cut', 'Cabinet_Cab_Lvl', - 'Unison_Detune', - ], - overrides: { - 'Env_A_Att': { min: 0.0, max: 0.15, curve: 0.3 }, - 'Env_A_Dec_1': { min: 0.2, max: 0.6, curve: 0.4 }, - 'Env_A_Sus': { min: 0.1, max: 0.5, curve: 0.45 }, - 'Shp_A_Drive': { min: 0.2, max: 0.55, curve: 0.6 }, - 'Shp_A_Asym': { min: 0.1, max: 0.65, curve: 0.6 }, - 'Shp_B_Drive': { min: 0.15, max: 0.55, curve: 0.6 }, - 'Shp_B_Asym': { min: 0.1, max: 0.6, curve: 0.55 }, - 'Shp_B_FB_Mix': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Gap_Flt_Center': { min: 0.2, max: 0.8, curve: 0.45 }, - 'Gap_Flt_Gap': { min: 0.05, max: 0.4, curve: 0.35 }, - 'Gap_Flt_Res': { min: 0.2, max: 0.7, curve: 0.45 }, - 'Gap_Flt_Mix': { min: 0.2, max: 0.7, curve: 0.45 }, - 'FB_Mix_Comb': { min: 0.3, max: 0.65, curve: 0.45 }, - 'FB_Mix_SVF': { min: 0.3, max: 0.65, curve: 0.45 }, - 'FB_Mix_FX': { min: 0.3, max: 0.6, curve: 0.4 }, - 'FB_Mix_Osc': { min: 0.3, max: 0.6, curve: 0.4 }, - 'FB_Mix_Lvl': { min: 0.2, max: 0.5, curve: 0.4 }, - 'FB_Mix_Drive': { min: 0.15, max: 0.38, curve: 0.4 }, - 'Cabinet_Drive': { min: 0.3, max: 0.58, curve: 0.6 }, - 'Cabinet_Fold': { min: 0.15, max: 0.5, curve: 0.55 }, - 'Cabinet_Asym': { min: 0.1, max: 0.5, curve: 0.55 }, - 'Cabinet_Mix': { min: 0.3, max: 0.75, curve: 0.6 }, - 'Out_Mix_Lvl': { min: 0.18, max: 0.5, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.25, curve: 0.3 }, - 'Echo_Mix': { min: 0.0, max: 0.2, curve: 0.3 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'advanced-3', - name: '3.3', - tier: 3, - description: 'Both oscillators through both filters, stereo panning, full effects', - active: [ - // Tier 1 - 'Env_A_Att', 'Env_A_Dec_1', 'Env_A_Sus', 'Env_A_Rel', - 'Osc_A_Fluct', 'Osc_B_Fluct', - 'Shp_A_Drive', 'Shp_A_Fold', - 'SV_Flt_Cut', 'SV_Flt_Res', 'SV_Flt_LBH', - 'Out_Mix_A_Lvl', 'Out_Mix_SVF_Lvl', 'Out_Mix_Lvl', - 'Reverb_Mix', - // Tier 2 - 'Env_A_BP', 'Env_A_Dec_2', 'Env_A_Gain', - 'Env_B_Att', 'Env_B_Dec_1', 'Env_B_Sus', 'Env_B_Rel', - 'Env_C_Att', 'Env_C_Dec_1', - 'Shp_A_Asym', 'Shp_A_Mix', - 'SV_Flt_FM', 'SV_Flt_Spread', - 'Out_Mix_A_Pan', 'Out_Mix_To_FX', - 'Reverb_Size', 'Reverb_Color', - 'Echo_Mix', 'Echo_Time', - 'Flanger_Mix', 'Flanger_Time', 'Flanger_Rate', - 'Cabinet_Drive', 'Cabinet_Tilt', 'Cabinet_Mix', - // Tier 3 additions - 'Env_B_BP', 'Env_B_Dec_2', 'Env_B_Gain', - 'Env_C_BP', 'Env_C_Dec_2', 'Env_C_Rel', 'Env_C_Sus', - 'Osc_A_PM_Self', 'Osc_A_PM_B', 'Osc_A_PM_FB', 'Osc_A_Phase', - 'Osc_B_PM_Self', 'Osc_B_PM_A', 'Osc_B_PM_FB', 'Osc_B_Phase', - 'Shp_A_FB_Mix', 'Shp_A_Ring_Mod', - 'Shp_B_Drive', 'Shp_B_Fold', 'Shp_B_Asym', 'Shp_B_Mix', 'Shp_B_FB_Mix', 'Shp_B_Ring_Mod', - 'Comb_Flt_Pitch', 'Comb_Flt_Decay', 'Comb_Flt_In_A_B', 'Comb_Flt_AP_Res', 'Comb_Flt_PM', - 'SV_Flt_In_A_B', 'SV_Flt_Comb_Mix', 'SV_Flt_Par', 'SV_Flt_FM_A_B', - 'Gap_Flt_Center', 'Gap_Flt_Gap', 'Gap_Flt_Res', 'Gap_Flt_Mix', - 'FB_Mix_Comb', 'FB_Mix_SVF', 'FB_Mix_FX', 'FB_Mix_Osc', 'FB_Mix_Lvl', 'FB_Mix_Drive', - 'Out_Mix_B_Lvl', 'Out_Mix_B_Pan', 'Out_Mix_Comb_Lvl', - 'Flanger_Feedback', 'Flanger_Stereo', 'Flanger_Phase', - 'Echo_Feedback', 'Echo_Stereo', - 'Reverb_Pre_Dly', 'Reverb_Chorus', - 'Cabinet_Fold', 'Cabinet_Asym', 'Cabinet_Hi_Cut', 'Cabinet_Lo_Cut', 'Cabinet_Cab_Lvl', - 'Unison_Detune', - ], - overrides: { - 'Env_A_Att': { min: 0.05, max: 0.5, curve: 0.45 }, - 'Env_B_Att': { min: 0.05, max: 0.5, curve: 0.45 }, - 'Osc_A_PM_B': { min: 0.2, max: 0.65, curve: 0.45 }, - 'Osc_B_PM_A': { min: 0.2, max: 0.65, curve: 0.45 }, - 'SV_Flt_In_A_B': { min: 0.2, max: 0.8, curve: 0.5 }, - 'Comb_Flt_In_A_B': { min: 0.2, max: 0.8, curve: 0.5 }, - 'Out_Mix_A_Lvl': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Out_Mix_B_Lvl': { min: 0.3, max: 0.8, curve: 0.55 }, - 'Out_Mix_A_Pan': { min: 0.15, max: 0.45, curve: 0.4 }, - 'Out_Mix_B_Pan': { min: 0.55, max: 0.85, curve: 0.6 }, - 'Out_Mix_Comb_Lvl': { min: 0.15, max: 0.6, curve: 0.45 }, - 'Out_Mix_Lvl': { min: 0.18, max: 0.5, curve: 0.45 }, - 'SV_Flt_Cut': { min: 0.15, max: 0.85, curve: 0.45 }, - 'SV_Flt_Spread': { min: 0.15, max: 0.75, curve: 0.55 }, - 'Flanger_Stereo': { min: 0.2, max: 0.8, curve: 0.6 }, - 'Echo_Stereo': { min: 0.2, max: 0.8, curve: 0.6 }, - 'Reverb_Mix': { min: 0.1, max: 0.5, curve: 0.4 }, - 'Echo_Mix': { min: 0.05, max: 0.4, curve: 0.4 }, - 'Flanger_Mix': { min: 0.05, max: 0.35, curve: 0.35 }, - 'Unison_Detune': { min: 0.0, max: 0.3, curve: 0.3 }, - 'FB_Mix_Lvl': { min: 0.15, max: 0.45, curve: 0.4 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - // ====================================================================== - // TIER 4 — Expert (all 126 params active) - // ====================================================================== - { - id: 'expert-1', - name: '4.1', - tier: 4, - description: 'All parameters active with conservative tame-derived ranges', - active: null, // null means ALL params active - overrides: { - // Use tame-derived defaults for everything (no per-param overrides) - }, - mutedOverrides: {}, - groupCurves: {}, - }, - - { - id: 'expert-2', - name: '4.2', - tier: 4, - description: 'All parameters active with wider ranges for extreme exploration', - active: null, // all active - overrides: { - // Push ranges wider than tame defaults — halve the tame constraint - 'Osc_A_PM_Self': { min: 0.1, max: 0.9, curve: 0.45 }, - 'Osc_A_PM_B': { min: 0.1, max: 0.9, curve: 0.45 }, - 'Osc_A_PM_FB': { min: 0.15, max: 0.85, curve: 0.45 }, - 'Osc_B_PM_Self': { min: 0.1, max: 0.9, curve: 0.45 }, - 'Osc_B_PM_A': { min: 0.1, max: 0.9, curve: 0.45 }, - 'Osc_B_PM_FB': { min: 0.15, max: 0.85, curve: 0.45 }, - 'Shp_A_Drive': { min: 0.0, max: 0.8, curve: 0.55 }, - 'Shp_B_Drive': { min: 0.0, max: 0.8, curve: 0.55 }, - 'Shp_A_FB_Mix': { min: 0.0, max: 0.85, curve: 0.45 }, - 'Shp_B_FB_Mix': { min: 0.0, max: 0.85, curve: 0.45 }, - 'Comb_Flt_Decay': { min: 0.15, max: 0.85, curve: 0.45 }, - 'Comb_Flt_AP_Res': { min: 0.0, max: 0.9, curve: 0.4 }, - 'Comb_Flt_PM': { min: 0.15, max: 0.85, curve: 0.45 }, - 'SV_Flt_Res': { min: 0.0, max: 0.9, curve: 0.4 }, - 'SV_Flt_FM': { min: 0.1, max: 0.9, curve: 0.45 }, - 'Gap_Flt_Res': { min: 0.0, max: 0.9, curve: 0.4 }, - 'FB_Mix_Comb': { min: 0.15, max: 0.85, curve: 0.45 }, - 'FB_Mix_SVF': { min: 0.15, max: 0.85, curve: 0.45 }, - 'FB_Mix_FX': { min: 0.15, max: 0.85, curve: 0.45 }, - 'FB_Mix_Osc': { min: 0.15, max: 0.85, curve: 0.45 }, - 'FB_Mix_Lvl': { min: 0.0, max: 0.65, curve: 0.4 }, - 'FB_Mix_Drive': { min: 0.0, max: 0.5, curve: 0.4 }, - 'Out_Mix_Lvl': { min: 0.1, max: 0.8, curve: 0.45 }, - 'Out_Mix_Drive': { min: 0.0, max: 0.65, curve: 0.55 }, - 'Cabinet_Drive': { min: 0.0, max: 0.75, curve: 0.55 }, - 'Flanger_Feedback':{ min: 0.1, max: 0.9, curve: 0.45 }, - 'Flanger_Cross_FB':{ min: 0.15, max: 0.85, curve: 0.45 }, - 'Echo_Feedback': { min: 0.0, max: 0.85, curve: 0.4 }, - 'Echo_Mix': { min: 0.0, max: 0.8, curve: 0.4 }, - 'Reverb_Size': { min: 0.0, max: 0.8, curve: 0.45 }, - 'Reverb_Mix': { min: 0.0, max: 0.8, curve: 0.4 }, - 'Reverb_Send': { min: 0.0, max: 1.0, curve: 0.5 }, - }, - mutedOverrides: {}, - groupCurves: {}, - }, -]; - -// Tier metadata for UI grouping -export const PRESET_TIERS = [ - { tier: 1, label: 'Beginner' }, - { tier: 2, label: 'Intermediate' }, - { tier: 3, label: 'Advanced' }, - { tier: 4, label: 'Expert' }, -]; diff --git a/playground/js/ui/ab-compare.js b/playground/js/ui/ab-compare.js deleted file mode 100644 index 277acbd..0000000 --- a/playground/js/ui/ab-compare.js +++ /dev/null @@ -1,153 +0,0 @@ -// A/B Compare — Rapid toggle between two weight states -// -// Workflow: -// 1. User presses "A" to capture current state as the reference -// 2. User continues exploring (this becomes the live "B" state) -// 3. Toggle switches instantly between A and B -// 4. "Accept B" discards A and exits A/B mode -// 5. "Revert to A" restores the A snapshot and discards B -// -// The state object stored for each side includes weights, noiseLevel, and -// an optional controlState (for any control surface params worth preserving). -// -// Usage: -// import { ABCompare } from './ab-compare.js'; -// const ab = new ABCompare(); -// ab.captureA({ weights, noiseLevel, controlState }); -// // ... user explores ... -// const stateToRestore = ab.toggle(); // switches to A, returns A's state -// const stateToRestore2 = ab.toggle(); // switches back to B, returns B's state - -export class ABCompare { - constructor() { - this._a = null; // { weights, noiseLevel, controlState } - this._b = null; // { weights, noiseLevel, controlState } - this._current = null; // 'a' | 'b' | null (null = inactive) - } - - // ---- Public API ---- - - /** - * Capture the current state as side "A" and enter A/B mode. - * The live state after this call becomes "B". - * @param {{ weights: Array, noiseLevel: number, controlState?: any }} state - */ - captureA(state) { - this._a = { - weights: Array.isArray(state.weights) - ? [...state.weights] - : Array.from(state.weights), - noiseLevel: state.noiseLevel ?? 0, - controlState: state.controlState ?? null, - }; - this._b = null; // B is "live" — captured on first toggle away from B - this._current = 'b'; // We're currently hearing the B (live) side - this._dispatch('ab:activate', { side: 'b' }); - } - - /** - * Whether A/B mode is currently active. - * @returns {boolean} - */ - get active() { - return this._current !== null; - } - - /** - * Which side is currently active: 'a', 'b', or null if inactive. - * @returns {'a'|'b'|null} - */ - get current() { - return this._current; - } - - /** - * Toggle between A and B. Returns the state for whichever side we're - * switching TO, so the caller can restore weights/noise from it. - * - * The caller must pass the current live state so we can snapshot whichever - * side we're leaving. - * - * @param {{ weights: Array, noiseLevel: number, controlState?: any }} currentLiveState - * @returns {{ weights: Array, noiseLevel: number, controlState?: any }|null} - */ - toggle(currentLiveState) { - if (!this.active) return null; - - if (this._current === 'b') { - // Switching B → A: save current live state as B, return A - this._b = { - weights: Array.isArray(currentLiveState.weights) - ? [...currentLiveState.weights] - : Array.from(currentLiveState.weights), - noiseLevel: currentLiveState.noiseLevel ?? 0, - controlState: currentLiveState.controlState ?? null, - }; - this._current = 'a'; - this._dispatch('ab:toggle', { side: 'a' }); - return this._cloneState(this._a); - } else { - // Switching A → B: save current live state as A, return B - this._a = { - weights: Array.isArray(currentLiveState.weights) - ? [...currentLiveState.weights] - : Array.from(currentLiveState.weights), - noiseLevel: currentLiveState.noiseLevel ?? 0, - controlState: currentLiveState.controlState ?? null, - }; - this._current = 'b'; - this._dispatch('ab:toggle', { side: 'b' }); - return this._cloneState(this._b); - } - } - - /** - * Accept B: discard A, exit A/B mode. - * The current live state (B) is kept as-is — nothing to restore. - */ - acceptB() { - this._a = null; - this._b = null; - this._current = null; - this._dispatch('ab:deactivate', { accepted: 'b' }); - } - - /** - * Revert to A: restore A state and discard B, exit A/B mode. - * @returns {{ weights: Array, noiseLevel: number, controlState?: any }|null} - */ - revertToA() { - if (!this._a) return null; - const state = this._cloneState(this._a); - this._a = null; - this._b = null; - this._current = null; - this._dispatch('ab:deactivate', { accepted: 'a' }); - return state; - } - - /** - * Cancel A/B mode without choosing. Stays on current live state. - */ - cancel() { - this._a = null; - this._b = null; - this._current = null; - this._dispatch('ab:deactivate', { accepted: null }); - } - - // ---- Internal ---- - - _cloneState(s) { - if (!s) return null; - return { - weights: [...s.weights], - noiseLevel: s.noiseLevel, - controlState: s.controlState, - }; - } - - _dispatch(type, detail) { - document.dispatchEvent(new CustomEvent(type, { detail })); - } -} diff --git a/playground/js/ui/auto-explore.js b/playground/js/ui/auto-explore.js deleted file mode 100644 index 07b9f35..0000000 --- a/playground/js/ui/auto-explore.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Auto-Explore — automated thumbs-down at regular intervals. - * - * "Wander" mode for weight space: the system drifts continuously while - * the user only gives thumbs-up when it lands on something good. - * - * Respects noise cap, spread, zoom-aware feedback — same rules as manual - * thumbs-down. Intensity scales with zoom: zoomed in = gentler steps, - * zoomed out = bigger leaps. - * - * @module auto-explore - */ - -// ---- Constants ---- -const MIN_INTERVAL = 0.5; // seconds -const MAX_INTERVAL = 10; // seconds -const DEFAULT_INTERVAL = 2; // seconds -const MIN_INTENSITY = 0.1; -const MAX_INTENSITY = 1.0; -const DEFAULT_INTENSITY = 0.5; - -export class AutoExplore { - /** - * @param {object} [options] - * @param {number} [options.interval=2] - seconds between auto-steps - * @param {number} [options.intensity=0.5] - 0.1-1.0, scales noise per step - */ - constructor(options = {}) { - this._interval = clamp(options.interval ?? DEFAULT_INTERVAL, MIN_INTERVAL, MAX_INTERVAL); - this._intensity = clamp(options.intensity ?? DEFAULT_INTENSITY, MIN_INTENSITY, MAX_INTENSITY); - this._active = false; - this._elapsed = 0; // seconds since last auto-step - this._callback = null; - } - - // ---- Control ---- - - /** Start auto-exploration. */ - start() { - this._active = true; - this._elapsed = 0; - } - - /** Stop auto-exploration. */ - stop() { - this._active = false; - this._elapsed = 0; - } - - /** Toggle on/off. */ - toggle() { - if (this._active) this.stop(); - else this.start(); - } - - /** Whether auto-explore is currently running. */ - get active() { - return this._active; - } - - // ---- Configuration ---- - - /** - * Set interval between auto-steps. - * @param {number} seconds - 0.5 to 10 - */ - setInterval(seconds) { - this._interval = clamp(seconds, MIN_INTERVAL, MAX_INTERVAL); - } - - /** @returns {number} */ - getInterval() { return this._interval; } - - /** - * Set exploration intensity. - * @param {number} intensity - 0.1 to 1.0 - */ - setIntensity(intensity) { - this._intensity = clamp(intensity, MIN_INTENSITY, MAX_INTENSITY); - } - - /** @returns {number} */ - getIntensity() { return this._intensity; } - - // ---- Tick (call every frame) ---- - - /** - * Advance the auto-explore timer. When the interval elapses, fires - * the registered callback with { intensity, zoomLevel }. - * - * @param {number} deltaTime - seconds since last frame - * @param {number} [zoomLevel=1.0] - current zoom level (0-1) - */ - tick(deltaTime, zoomLevel = 1.0) { - if (!this._active) return; - if (deltaTime <= 0) return; - - this._elapsed += deltaTime; - - if (this._elapsed >= this._interval) { - this._elapsed -= this._interval; - // Prevent runaway accumulation if tab was backgrounded - if (this._elapsed > this._interval) this._elapsed = 0; - - this._fireExplore(zoomLevel); - } - } - - // ---- Callback ---- - - /** - * Register the explore callback. Called when auto-step fires. - * @param {function} callback - receives { intensity } - */ - onExplore(callback) { - this._callback = callback; - } - - // ---- Visual state ---- - - /** - * Progress toward next auto-step (0-1). - * Useful for a countdown/progress ring UI. - * @returns {number} - */ - getProgress() { - if (!this._active) return 0; - return Math.min(1, this._elapsed / this._interval); - } - - // ---- Serialization ---- - - getConfig() { - return { - interval: this._interval, - intensity: this._intensity, - active: this._active, - }; - } - - setConfig(config) { - if (config.interval != null) this.setInterval(config.interval); - if (config.intensity != null) this.setIntensity(config.intensity); - // Note: we don't auto-start from config — caller decides - } - - // ---- Internal ---- - - _fireExplore(zoomLevel) { - if (!this._callback) return; - // Scale intensity by zoom: zoomed in = gentler, zoomed out = bigger leaps - const scaledIntensity = this._intensity * Math.max(0.05, zoomLevel); - this._callback({ intensity: scaledIntensity }); - } -} - -// ---- Utility ---- -function clamp(v, lo, hi) { - return v < lo ? lo : v > hi ? hi : v; -} diff --git a/playground/js/ui/control-surface-ui.js b/playground/js/ui/control-surface-ui.js deleted file mode 100644 index 238137a..0000000 --- a/playground/js/ui/control-surface-ui.js +++ /dev/null @@ -1,884 +0,0 @@ -// Control Surface UI — DOM for compound axes + settings drawer -// Renders 3 compact sliders on the floating bar and a gear-icon settings drawer. -// -// Usage: -// import { initControlSurfaceUI } from './ui/control-surface-ui.js'; -// const { surface, destroy } = initControlSurfaceUI(); -// // surface is the ControlSurface instance - -import { ControlSurface, CONTROL_PRESETS, PARAM_DEFAULTS, PARAM_RANGES, LOG_SCALE_PARAMS } from './control-surface.js'; - -// ---- Parameter metadata for the drawer ---- - -const SECTIONS = [ - { - id: 'input', - label: 'Input', - params: [ - { name: 'zoom', label: 'Zoom', log: true }, - { name: 'deadzone', label: 'Deadzone' }, - { name: 'inputCurve', label: 'Curve' }, - { name: 'smoothing', label: 'Smoothing' }, - { name: 'momentumZoom', label: 'Momentum', type: 'select', options: ['off', 'gentle', 'strong'] }, - { name: 'invertX', label: 'Invert X', type: 'toggle' }, - { name: 'invertY', label: 'Invert Y', type: 'toggle' }, - ], - }, - { - id: 'training', - label: 'Training', - params: [ - { name: 'learningRate', label: 'Learning Rate', log: true }, - { name: 'maxIterations', label: 'Max Iterations' }, - { name: 'convergenceThreshold', label: 'Convergence', log: true }, - { name: 'rlTrainIntensity', label: 'RL Intensity' }, - { name: 'maxExamples', label: 'Max Examples' }, - { name: 'exampleDecay', label: 'Example Decay' }, - ], - }, - { - id: 'exploration', - label: 'Exploration', - params: [ - { name: 'spread', label: 'Spread' }, - { name: 'noiseFloor', label: 'Noise Floor' }, - { name: 'noiseCap', label: 'Noise Cap' }, - { name: 'noiseGrowth', label: 'Noise Growth' }, - { name: 'noiseDecay', label: 'Noise Decay' }, - { name: 'weightDecay', label: 'Weight Decay' }, - { name: 'noiseDistribution', label: 'Distribution', type: 'select', options: ['gaussian', 'cauchy'] }, - { name: 'layerAwareNoise', label: 'Layer-Aware', type: 'toggle' }, - { name: 'zoomAwareFeedback', label: 'Zoom-Aware FB', type: 'toggle' }, - ], - }, - { - id: 'output', - label: 'Output', - params: [ - { name: 'outputSmoothing', label: 'Smoothing' }, - { name: 'outputSlewRate', label: 'Slew Rate' }, - { name: 'tame', label: 'Tame' }, - { name: 'globalCurve', label: 'Global Curve' }, - ], - }, -]; - -// ---- Utility: log-scale slider helpers ---- - -function toLogSlider(value, min, max) { - // Map a log-scale value to 0-1 slider position - const logMin = Math.log(min); - const logMax = Math.log(max); - return (Math.log(Math.max(value, min)) - logMin) / (logMax - logMin); -} - -function fromLogSlider(sliderPos, min, max) { - const logMin = Math.log(min); - const logMax = Math.log(max); - return Math.exp(logMin + sliderPos * (logMax - logMin)); -} - -function formatValue(name, value) { - if (typeof value === 'boolean') return value ? 'On' : 'Off'; - if (typeof value === 'string') return value; - if (typeof value !== 'number') return String(value); - // Convergence threshold: scientific notation - if (name === 'convergenceThreshold') return value.toExponential(1); - // Integers - if (name === 'maxExamples' || name === 'maxIterations') return Math.round(value).toString(); - // Small decimals - if (Math.abs(value) < 0.01 && value !== 0) return value.toExponential(1); - if (Math.abs(value) < 1) return value.toFixed(3); - return value.toFixed(2); -} - -// ---- Build DOM ---- - -/** - * Initialize the control surface UI. - * Inserts compound axis sliders into the floating bar and creates the settings drawer. - * Returns { surface, destroy }. - */ -export function initControlSurfaceUI() { - const surface = new ControlSurface(); - - // Apply default preset - surface.applyPreset('default'); - - // --- Floating bar: compound axis sliders --- - const $barContent = document.querySelector('.floating-bar-content') || document.getElementById('floating-bar'); - const axisContainer = document.createElement('div'); - axisContainer.className = 'cs-axes'; - axisContainer.innerHTML = ` -
    - - -
    -
    - - -
    -
    - - -
    - `; - - $barContent.appendChild(axisContainer); - - // Wire axis sliders - const axisSliders = {}; - for (const axisEl of axisContainer.querySelectorAll('.cs-axis')) { - const axisName = axisEl.dataset.axis; - const slider = axisEl.querySelector('.cs-axis-slider'); - axisSliders[axisName] = slider; - - slider.addEventListener('input', () => { - const val = parseFloat(slider.value); - if (axisName === 'boldness') surface.setBoldness(val); - else if (axisName === 'memory') surface.setMemory(val); - else if (axisName === 'precision') surface.setPrecision(val); - }); - - // Double-tap to clear all overrides for this axis - let lastTap = 0; - slider.addEventListener('pointerdown', () => { - const now = Date.now(); - if (now - lastTap < 350) { - surface.clearAllOverrides(); - syncDrawerValues(); - } - lastTap = now; - }); - } - - // --- Gear button (settings drawer toggle) --- - const gearBtn = document.createElement('button'); - gearBtn.className = 'cs-gear-btn'; - gearBtn.title = 'Control surface settings'; - gearBtn.innerHTML = ` - - - `; - - // Place gear button near the help button (top-right area) - const $helpBtn = document.getElementById('help-btn'); - if ($helpBtn && $helpBtn.parentNode) { - $helpBtn.parentNode.insertBefore(gearBtn, $helpBtn); - } else { - document.body.appendChild(gearBtn); - } - - // --- Settings drawer --- - const overlay = document.createElement('div'); - overlay.className = 'cs-drawer-overlay hidden'; - - const drawer = document.createElement('div'); - drawer.className = 'cs-drawer'; - - // Build drawer content - drawer.innerHTML = buildDrawerHTML(); - overlay.appendChild(drawer); - document.body.appendChild(overlay); - - // --- Wire drawer interactions --- - - let drawerOpen = false; - - function openDrawer() { - drawerOpen = true; - overlay.classList.remove('hidden'); - requestAnimationFrame(() => { - overlay.classList.add('open'); - drawer.classList.add('open'); - }); - syncDrawerValues(); - } - - function closeDrawer() { - drawerOpen = false; - overlay.classList.remove('open'); - drawer.classList.remove('open'); - setTimeout(() => { - if (!drawerOpen) overlay.classList.add('hidden'); - }, 220); - } - - gearBtn.addEventListener('click', () => { - if (drawerOpen) closeDrawer(); - else openDrawer(); - }); - - // Close on overlay click (outside drawer) - overlay.addEventListener('click', (e) => { - if (e.target === overlay) closeDrawer(); - }); - - // Close button inside drawer - const closeBtn = drawer.querySelector('.cs-drawer-close'); - if (closeBtn) closeBtn.addEventListener('click', closeDrawer); - - // Preset dropdown - const presetSelect = drawer.querySelector('#cs-preset-select'); - if (presetSelect) { - presetSelect.addEventListener('change', () => { - const id = presetSelect.value; - if (id && CONTROL_PRESETS[id]) { - surface.applyPreset(id); - // Sync axis sliders - axisSliders.boldness.value = surface.getBoldness(); - axisSliders.memory.value = surface.getMemory(); - axisSliders.precision.value = surface.getPrecision(); - syncDrawerValues(); - } - }); - } - - // Reset button - const resetBtn = drawer.querySelector('#cs-reset-preset'); - if (resetBtn) { - resetBtn.addEventListener('click', () => { - const id = presetSelect?.value || 'default'; - surface.applyPreset(id); - axisSliders.boldness.value = surface.getBoldness(); - axisSliders.memory.value = surface.getMemory(); - axisSliders.precision.value = surface.getPrecision(); - syncDrawerValues(); - }); - } - - // Section expand/collapse - for (const header of drawer.querySelectorAll('.cs-section-header')) { - header.addEventListener('click', () => { - const section = header.parentElement; - section.classList.toggle('collapsed'); - }); - } - - // Wire individual parameter controls - wireParamControls(drawer, surface, () => syncDrawerValues()); - - // --- Sync drawer values from surface state --- - - function syncDrawerValues() { - const params = surface.getParams(); - for (const section of SECTIONS) { - for (const p of section.params) { - const val = params[p.name]; - const el = drawer.querySelector(`[data-param="${p.name}"]`); - if (!el) continue; - - const range = PARAM_RANGES[p.name]; - - if (p.type === 'toggle') { - const checkbox = el.querySelector('input[type="checkbox"]'); - if (checkbox) checkbox.checked = !!val; - } else if (p.type === 'select') { - const select = el.querySelector('select'); - if (select) select.value = val; - } else if (range) { - const slider = el.querySelector('input[type="range"]'); - const valueSpan = el.querySelector('.cs-param-value'); - if (slider) { - if (p.log || LOG_SCALE_PARAMS.has(p.name)) { - slider.value = toLogSlider(val, range[0], range[1]); - } else { - slider.value = val; - } - } - if (valueSpan) valueSpan.textContent = formatValue(p.name, val); - } - - // Show override indicator - const overrideIndicator = el.querySelector('.cs-override-dot'); - if (overrideIndicator) { - overrideIndicator.classList.toggle('active', surface.hasOverride(p.name)); - } - } - } - } - - // Listen for external changes to the surface (e.g. from app code) - surface.onChange(() => { - if (drawerOpen) syncDrawerValues(); - }); - - // --- Inject styles --- - injectStyles(); - - // --- Cleanup function --- - function destroy() { - axisContainer.remove(); - gearBtn.remove(); - overlay.remove(); - const styleEl = document.getElementById('cs-styles'); - if (styleEl) styleEl.remove(); - } - - return { surface, destroy }; -} - - -// ---- Build drawer HTML ---- - -function buildDrawerHTML() { - let html = ` -
    - Control Surface - -
    -
    -
    - - - -
    - `; - - for (const section of SECTIONS) { - html += ` -
    -
    - ${section.label} - -
    -
    - `; - - for (const p of section.params) { - html += buildParamRow(p); - } - - html += ` -
    -
    - `; - } - - html += `
    `; - return html; -} - -function buildParamRow(p) { - const range = PARAM_RANGES[p.name]; - - if (p.type === 'toggle') { - return ` -
    - - -
    - `; - } - - if (p.type === 'select') { - const options = p.options || []; - return ` -
    - - -
    - `; - } - - // Numeric slider - const isLog = p.log || LOG_SCALE_PARAMS.has(p.name); - const min = range ? range[0] : 0; - const max = range ? range[1] : 1; - const step = isLog ? 0.001 : (range ? (range[2] || 0.01) : 0.01); - const sliderMin = isLog ? 0 : min; - const sliderMax = isLog ? 1 : max; - const defaultVal = PARAM_DEFAULTS[p.name] ?? 0.5; - const sliderDefault = isLog ? toLogSlider(defaultVal, min, max) : defaultVal; - - return ` -
    - -
    - - ${formatValue(p.name, defaultVal)} -
    -
    - `; -} - - -// ---- Wire parameter controls ---- - -function wireParamControls(drawer, surface, syncFn) { - for (const section of SECTIONS) { - for (const p of section.params) { - const el = drawer.querySelector(`[data-param="${p.name}"]`); - if (!el) continue; - - const range = PARAM_RANGES[p.name]; - - if (p.type === 'toggle') { - const checkbox = el.querySelector('input[type="checkbox"]'); - if (checkbox) { - checkbox.addEventListener('change', () => { - surface.setOverride(p.name, checkbox.checked); - syncFn(); - }); - } - } else if (p.type === 'select') { - const select = el.querySelector('select'); - if (select) { - select.addEventListener('change', () => { - surface.setOverride(p.name, select.value); - syncFn(); - }); - } - } else if (range) { - const slider = el.querySelector('input[type="range"]'); - const valueSpan = el.querySelector('.cs-param-value'); - const isLog = p.log || LOG_SCALE_PARAMS.has(p.name); - - if (slider) { - slider.addEventListener('input', () => { - let val; - if (isLog) { - val = fromLogSlider(parseFloat(slider.value), range[0], range[1]); - } else { - val = parseFloat(slider.value); - } - surface.setOverride(p.name, val); - if (valueSpan) valueSpan.textContent = formatValue(p.name, val); - // Update override dot - const dot = el.querySelector('.cs-override-dot'); - if (dot) dot.classList.add('active'); - }); - - // Double-tap slider to clear override for this param - let lastTap = 0; - slider.addEventListener('pointerdown', () => { - const now = Date.now(); - if (now - lastTap < 350) { - surface.clearOverride(p.name); - syncFn(); - } - lastTap = now; - }); - } - } - } - } -} - - -// ---- CSS injection ---- - -function injectStyles() { - if (document.getElementById('cs-styles')) return; - - const style = document.createElement('style'); - style.id = 'cs-styles'; - style.textContent = ` -/* ---- Control Surface: Compound Axes (floating bar) ---- */ -.cs-axes { - display: flex; - align-items: center; - gap: 6px; - margin: 0 4px; -} - -.cs-axis { - display: flex; - align-items: center; - gap: 3px; -} - -.cs-axis-label { - font-size: 9px; - color: rgba(255, 255, 255, 0.45); - text-transform: uppercase; - letter-spacing: 0.5px; - white-space: nowrap; - cursor: default; - user-select: none; -} - -.cs-axis-slider { - width: 48px; - height: 3px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.12); - border-radius: 2px; - outline: none; - cursor: pointer; -} - -.cs-axis-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent, #ff6a00); - cursor: pointer; - border: none; -} - -.cs-axis-slider::-moz-range-thumb { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent, #ff6a00); - border: none; - cursor: pointer; -} - -/* ---- Gear button ---- */ -.cs-gear-btn { - position: fixed; - bottom: 96px; - right: 50px; - z-index: 45; - width: 32px; - height: 32px; - border-radius: 50%; - border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.08)); - background: var(--glass-bg, rgba(13, 13, 13, 0.65)); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - color: var(--text-dim, #888); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s; - padding: 0; -} - -.cs-gear-btn:hover, -.cs-gear-btn:active { - border-color: var(--accent, #ff6a00); - color: var(--accent, #ff6a00); -} - -/* ---- Drawer overlay ---- */ -.cs-drawer-overlay { - position: fixed; - inset: 0; - z-index: 90; - background: rgba(0, 0, 0, 0); - transition: background 0.2s ease; - pointer-events: auto; -} - -.cs-drawer-overlay.hidden { - display: none; -} - -.cs-drawer-overlay.open { - background: rgba(0, 0, 0, 0.4); -} - -/* ---- Drawer panel ---- */ -.cs-drawer { - position: fixed; - top: 0; - right: -320px; - width: 300px; - height: 100%; - z-index: 91; - background: rgba(0, 0, 0, 0.88); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border-left: 1px solid rgba(255, 255, 255, 0.08); - overflow-y: auto; - -webkit-overflow-scrolling: touch; - transition: right 0.2s ease; - font-family: 'JetBrains Mono', 'SF Mono', monospace; -} - -.cs-drawer.open { - right: 0; -} - -/* ---- Drawer header ---- */ -.cs-drawer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px 10px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.cs-drawer-title { - font-size: 12px; - font-weight: 600; - color: rgba(255, 255, 255, 0.8); - text-transform: uppercase; - letter-spacing: 0.8px; -} - -.cs-drawer-close { - width: 28px; - height: 28px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - color: rgba(255, 255, 255, 0.5); - font-size: 16px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s; - padding: 0; -} - -.cs-drawer-close:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -/* ---- Drawer body ---- */ -.cs-drawer-body { - padding: 0 0 24px; -} - -/* ---- Preset row ---- */ -.cs-preset-row { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); -} - -.cs-preset-label { - font-size: 10px; - color: rgba(255, 255, 255, 0.4); - text-transform: uppercase; - letter-spacing: 0.5px; - white-space: nowrap; -} - -.cs-preset-select { - flex: 1; - height: 28px; - padding: 0 8px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - color: var(--text, #e0e0e0); - font-family: inherit; - font-size: 11px; - cursor: pointer; - appearance: none; - -webkit-appearance: none; -} - -.cs-preset-select option { - background: #1a1a1a; - color: var(--text, #e0e0e0); -} - -.cs-reset-btn { - height: 28px; - padding: 0 10px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - color: rgba(255, 255, 255, 0.5); - font-family: inherit; - font-size: 10px; - cursor: pointer; - transition: all 0.15s; - white-space: nowrap; -} - -.cs-reset-btn:hover { - background: rgba(255, 255, 255, 0.1); - color: white; -} - -/* ---- Sections ---- */ -.cs-section { - border-bottom: 1px solid rgba(255, 255, 255, 0.04); -} - -.cs-section-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 16px; - cursor: pointer; - user-select: none; - transition: background 0.15s; -} - -.cs-section-header:hover { - background: rgba(255, 255, 255, 0.03); -} - -.cs-section-header span:first-child { - font-size: 10px; - font-weight: 500; - color: rgba(255, 255, 255, 0.5); - text-transform: uppercase; - letter-spacing: 1px; -} - -.cs-section-chevron { - font-size: 8px; - color: rgba(255, 255, 255, 0.3); - transition: transform 0.2s; -} - -.cs-section.collapsed .cs-section-chevron { - transform: rotate(-90deg); -} - -.cs-section-body { - padding: 0 16px 8px; - overflow: hidden; - transition: max-height 0.2s ease, padding 0.2s ease; - max-height: 600px; -} - -.cs-section.collapsed .cs-section-body { - max-height: 0; - padding-top: 0; - padding-bottom: 0; -} - -/* ---- Parameter rows ---- */ -.cs-param-row { - margin-bottom: 8px; -} - -.cs-param-label { - display: flex; - align-items: center; - gap: 4px; - font-size: 10px; - color: rgba(255, 255, 255, 0.45); - margin-bottom: 3px; - cursor: default; -} - -/* Override dot: shows when param has a manual offset */ -.cs-override-dot { - width: 5px; - height: 5px; - border-radius: 50%; - background: transparent; - flex-shrink: 0; - transition: background 0.15s; -} - -.cs-override-dot.active { - background: var(--accent, #ff6a00); -} - -/* ---- Slider row ---- */ -.cs-slider-row { - display: flex; - align-items: center; - gap: 6px; -} - -.cs-slider { - flex: 1; - height: 3px; - -webkit-appearance: none; - appearance: none; - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - outline: none; - cursor: pointer; -} - -.cs-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent, #ff6a00); - cursor: pointer; - border: none; -} - -.cs-slider::-moz-range-thumb { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--accent, #ff6a00); - border: none; - cursor: pointer; -} - -.cs-param-value { - font-family: 'JetBrains Mono', monospace; - font-size: 9px; - color: rgba(255, 255, 255, 0.5); - min-width: 36px; - text-align: right; - white-space: nowrap; -} - -/* ---- Toggle (checkbox) ---- */ -.cs-toggle { - accent-color: var(--accent, #ff6a00); - cursor: pointer; - width: 14px; - height: 14px; -} - -/* ---- Select ---- */ -.cs-select { - height: 24px; - padding: 0 8px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - color: var(--text, #e0e0e0); - font-family: inherit; - font-size: 10px; - cursor: pointer; - appearance: none; - -webkit-appearance: none; -} - -.cs-select option { - background: #1a1a1a; - color: var(--text, #e0e0e0); -} - -/* ---- Responsive: shrink axes on narrow screens ---- */ -@media (max-width: 600px) { - .cs-axes { - gap: 4px; - } - .cs-axis-slider { - width: 36px; - } - .cs-axis-label { - font-size: 8px; - } - .cs-drawer { - width: 280px; - right: -300px; - } -} -`; - document.head.appendChild(style); -} diff --git a/playground/js/ui/control-surface.js b/playground/js/ui/control-surface.js deleted file mode 100644 index 6bf8daf..0000000 --- a/playground/js/ui/control-surface.js +++ /dev/null @@ -1,405 +0,0 @@ -// Control Surface — Compound Axes + Parameter Override System -// Phase 1: Boldness, Memory, Precision axes with offset-based overrides. -// -// Each compound axis maps a single 0-1 slider to multiple underlying parameters. -// Users can override individual params; the offset from axis-derived value persists -// as the axis moves (trim-pot model). Double-tap an axis to re-link (clear offsets). - -// ---- Interpolation tables ---- -// Each row: [axisValue, { param: value, ... }] -// Linear interpolation between adjacent rows. - -const BOLDNESS_TABLE = [ - [0.0, { zoom: 0.1, noiseCap: 0.02, noiseGrowth: 1.1, learningRate: 0.1, weightDecay: 0.15, noiseDistribution: 'gaussian' }], - [0.5, { zoom: 0.5, noiseCap: 0.12, noiseGrowth: 1.5, learningRate: 1.0, weightDecay: 0.06, noiseDistribution: 'gaussian' }], - [1.0, { zoom: 1.0, noiseCap: 0.30, noiseGrowth: 2.5, learningRate: 3.0, weightDecay: 0.00, noiseDistribution: 'cauchy' }], -]; - -const MEMORY_TABLE = [ - [0.0, { maxExamples: 5, exampleDecay: 0.3, memoryWeightDecay: 0.20, noiseDecay: 0.85, convergenceThreshold: 1e-3 }], - [0.5, { maxExamples: 50, exampleDecay: 0.7, memoryWeightDecay: 0.06, noiseDecay: 0.97, convergenceThreshold: 1e-5 }], - [1.0, { maxExamples: 500, exampleDecay: 1.0, memoryWeightDecay: 0.00, noiseDecay: 0.995, convergenceThreshold: 1e-8 }], -]; - -const PRECISION_TABLE = [ - [0.0, { inputCurve: 1.0, deadzone: 0.0, smoothing: 0.0, slewRate: 1.0, momentumZoom: 'off' }], - [0.5, { inputCurve: 1.5, deadzone: 0.05, smoothing: 0.15, slewRate: 0.3, momentumZoom: 'off' }], - [1.0, { inputCurve: 3.0, deadzone: 0.15, smoothing: 0.40, slewRate: 0.1, momentumZoom: 'off' }], -]; - -// ---- Control Presets ---- - -const CONTROL_PRESETS = { - 'default': { boldness: 0.5, memory: 0.5, precision: 0.3 }, - 'first-touch': { boldness: 0.2, memory: 0.7, precision: 0.6 }, - 'jazz-hands': { boldness: 0.8, memory: 0.2, precision: 0.0 }, - 'sculptor': { boldness: 0.3, memory: 0.9, precision: 0.8 }, - 'improviser': { boldness: 0.6, memory: 0.3, precision: 0.2 }, - 'microscope': { boldness: 0.1, memory: 1.0, precision: 1.0 }, -}; - -// ---- Default values for all parameters ---- -// These are used when no axis or override controls a parameter. - -const PARAM_DEFAULTS = { - // Input pipeline - zoom: 1.0, - deadzone: 0.0, - inputCurve: 1.0, - smoothing: 0.0, - momentumZoom: 'off', - invertX: false, - invertY: false, - - // Training - learningRate: 1.0, - maxIterations: 1000, - convergenceThreshold: 1e-5, - rlTrainIntensity: 1.0, - maxExamples: 50, - exampleDecay: 0.7, - - // Noise / exploration - spread: 0.6, - noiseFloor: 0.005, - noiseCap: 0.12, - noiseGrowth: 1.5, - noiseDecay: 0.97, - weightDecay: 0.06, - noiseDistribution: 'gaussian', - layerAwareNoise: true, - zoomAwareFeedback: true, - - // Output pipeline - outputSmoothing: 0.0, - outputSlewRate: 1.0, - tame: 1.0, - globalCurve: 1.0, -}; - -// Valid ranges for clamping (min, max, step). Discrete params use null. -const PARAM_RANGES = { - zoom: [0.01, 1.0, 0.01], - deadzone: [0, 0.4, 0.01], - inputCurve: [0.2, 5.0, 0.1], - smoothing: [0, 1.0, 0.01], - momentumZoom: null, // discrete: 'off' | 'gentle' | 'strong' - invertX: null, // boolean - invertY: null, // boolean - learningRate: [0.01, 10.0, 0.01], - maxIterations: [10, 10000, 10], - convergenceThreshold: [1e-8, 1e-2, null], // log scale - rlTrainIntensity: [0.1, 5.0, 0.1], - maxExamples: [1, 500, 1], - exampleDecay: [0, 1.0, 0.01], - spread: [0, 1.0, 0.01], - noiseFloor: [0, 0.1, 0.001], - noiseCap: [0.01, 0.5, 0.01], - noiseGrowth: [1.0, 5.0, 0.1], - noiseDecay: [0.5, 1.0, 0.001], - weightDecay: [0, 0.5, 0.01], - noiseDistribution: null, // discrete: 'gaussian' | 'cauchy' - layerAwareNoise: null, // boolean - zoomAwareFeedback: null, // boolean - outputSmoothing: [0, 1.0, 0.01], - outputSlewRate: [0.01, 1.0, 0.01], - tame: [0, 1.0, 0.01], - globalCurve: [0.2, 5.0, 0.1], -}; - -// Which parameters are log-scale in the UI -const LOG_SCALE_PARAMS = new Set(['zoom', 'learningRate', 'convergenceThreshold']); - -// ---- Helpers ---- - -/** - * Linearly interpolate a value from a table. - * Table rows: [axisValue, { paramName: value, ... }] - * For discrete params (strings), snaps at 0.75 threshold toward the higher row. - */ -function interpolateTable(table, axisValue) { - const v = Math.max(0, Math.min(1, axisValue)); - const result = {}; - - // Find the two bracketing rows - let lo = 0; - let hi = table.length - 1; - for (let i = 0; i < table.length - 1; i++) { - if (v >= table[i][0] && v <= table[i + 1][0]) { - lo = i; - hi = i + 1; - break; - } - } - - const loVal = table[lo][0]; - const hiVal = table[hi][0]; - const t = hiVal === loVal ? 0 : (v - loVal) / (hiVal - loVal); - - const loParams = table[lo][1]; - const hiParams = table[hi][1]; - - for (const key of Object.keys(loParams)) { - const a = loParams[key]; - const b = hiParams[key]; - if (typeof a === 'number' && typeof b === 'number') { - result[key] = a + (b - a) * t; - } else { - // Discrete: snap at 75% toward the higher value - result[key] = t < 0.75 ? a : b; - } - } - - return result; -} - -/** - * Clamp a numeric value to its valid range. - */ -function clampParam(name, value) { - const range = PARAM_RANGES[name]; - if (!range) return value; // discrete, no clamping - return Math.max(range[0], Math.min(range[1], value)); -} - - -// ---- ControlSurface ---- - -export class ControlSurface { - constructor() { - // Axis values (0-1) - this._axes = { - boldness: 0.5, - memory: 0.5, - precision: 0.3, - }; - - // Per-parameter offsets (trim-pot overrides) - // key = paramName, value = offset from axis-derived value - this._offsets = {}; - - // Change listeners - this._listeners = []; - - // Compute initial derived params - this._derivedCache = null; - this._resolvedCache = null; - this._dirty = true; - } - - // ---- Compound axes ---- - - setBoldness(value) { - this._axes.boldness = Math.max(0, Math.min(1, value)); - this._dirty = true; - this._notify(); - } - - setMemory(value) { - this._axes.memory = Math.max(0, Math.min(1, value)); - this._dirty = true; - this._notify(); - } - - setPrecision(value) { - this._axes.precision = Math.max(0, Math.min(1, value)); - this._dirty = true; - this._notify(); - } - - getBoldness() { return this._axes.boldness; } - getMemory() { return this._axes.memory; } - getPrecision() { return this._axes.precision; } - - // ---- Individual overrides (offset model) ---- - - /** - * Set an override for a specific parameter. - * Computes offset = manualValue - axisDerivedValue, so moving the - * axis later shifts the base while the offset persists. - */ - setOverride(paramName, value) { - const derived = this._getDerived(); - const base = derived[paramName] ?? PARAM_DEFAULTS[paramName]; - if (typeof base === 'number' && typeof value === 'number') { - this._offsets[paramName] = value - base; - } else { - // For discrete params, store the literal value (not an offset) - this._offsets[paramName] = value; - } - this._dirty = true; - this._notify(); - } - - clearOverride(paramName) { - delete this._offsets[paramName]; - this._dirty = true; - this._notify(); - } - - clearAllOverrides() { - this._offsets = {}; - this._dirty = true; - this._notify(); - } - - hasOverride(paramName) { - return paramName in this._offsets; - } - - // ---- Resolved parameter values ---- - - /** - * Returns a flat object of all resolved parameter values. - * axis-derived + offsets, clamped to valid ranges. - */ - getParams() { - if (!this._dirty && this._resolvedCache) return this._resolvedCache; - - const derived = this._getDerived(); - const resolved = {}; - - for (const name of Object.keys(PARAM_DEFAULTS)) { - const base = derived[name] ?? PARAM_DEFAULTS[name]; - - if (name in this._offsets) { - const range = PARAM_RANGES[name]; - if (range && typeof base === 'number') { - // Numeric with offset - resolved[name] = clampParam(name, base + this._offsets[name]); - } else { - // Discrete override: stored as literal value - resolved[name] = this._offsets[name]; - } - } else { - resolved[name] = base; - } - } - - this._resolvedCache = resolved; - this._dirty = false; - return resolved; - } - - getParam(name) { - return this.getParams()[name]; - } - - // ---- Presets ---- - - applyPreset(presetId) { - const preset = CONTROL_PRESETS[presetId]; - if (!preset) { - console.warn(`[ControlSurface] Unknown preset: ${presetId}`); - return; - } - this._axes.boldness = preset.boldness; - this._axes.memory = preset.memory; - this._axes.precision = preset.precision; - this._offsets = {}; - this._dirty = true; - this._notify(); - } - - getPresetList() { - return Object.entries(CONTROL_PRESETS).map(([id, values]) => ({ - id, - ...values, - })); - } - - // ---- Events ---- - - onChange(callback) { - this._listeners.push(callback); - return () => { - const idx = this._listeners.indexOf(callback); - if (idx >= 0) this._listeners.splice(idx, 1); - }; - } - - // ---- Serialization ---- - - getState() { - return { - axes: { ...this._axes }, - offsets: { ...this._offsets }, - }; - } - - setState(state) { - if (state.axes) { - this._axes.boldness = state.axes.boldness ?? 0.5; - this._axes.memory = state.axes.memory ?? 0.5; - this._axes.precision = state.axes.precision ?? 0.3; - } - if (state.offsets) { - this._offsets = { ...state.offsets }; - } - this._dirty = true; - this._notify(); - } - - // ---- Internal ---- - - /** - * Compute axis-derived values (before offsets). - * Merges all three axis interpolation tables plus defaults. - */ - _getDerived() { - if (!this._dirty && this._derivedCache) return this._derivedCache; - - const boldness = interpolateTable(BOLDNESS_TABLE, this._axes.boldness); - const memory = interpolateTable(MEMORY_TABLE, this._axes.memory); - const precision = interpolateTable(PRECISION_TABLE, this._axes.precision); - - // Start from defaults, overlay axis-derived values. - // Memory table uses 'memoryWeightDecay' to avoid conflict with Boldness 'weightDecay'. - // Resolve: Boldness controls main weightDecay; Memory's memoryWeightDecay is additive context. - // For the resolved param, we take the max of the two weight decay influences. - const derived = { ...PARAM_DEFAULTS }; - - // Boldness params - derived.zoom = boldness.zoom; - derived.noiseCap = boldness.noiseCap; - derived.noiseGrowth = boldness.noiseGrowth; - derived.learningRate = boldness.learningRate; - derived.weightDecay = boldness.weightDecay; - derived.noiseDistribution = boldness.noiseDistribution; - - // Memory params - derived.maxExamples = Math.round(memory.maxExamples); - derived.exampleDecay = memory.exampleDecay; - derived.noiseDecay = memory.noiseDecay; - derived.convergenceThreshold = memory.convergenceThreshold; - // Memory weight decay: blend with boldness weight decay (take the larger influence) - derived.weightDecay = Math.max(boldness.weightDecay, memory.memoryWeightDecay); - - // Precision params - derived.inputCurve = precision.inputCurve; - derived.deadzone = precision.deadzone; - derived.smoothing = precision.smoothing; - derived.outputSlewRate = precision.slewRate; - derived.momentumZoom = precision.momentumZoom; - - this._derivedCache = derived; - return derived; - } - - _notify() { - const params = this.getParams(); - - // Dispatch DOM event for app-level wiring - document.dispatchEvent(new CustomEvent('controlsurface:change', { - detail: params, - })); - - // Direct listeners - for (const fn of this._listeners) { - try { fn(params); } catch (e) { console.error('[ControlSurface] listener error:', e); } - } - } -} - -// Re-export constants for UI module -export { CONTROL_PRESETS, PARAM_DEFAULTS, PARAM_RANGES, LOG_SCALE_PARAMS }; diff --git a/playground/js/ui/controls.js b/playground/js/ui/controls.js deleted file mode 100644 index 7eb7412..0000000 --- a/playground/js/ui/controls.js +++ /dev/null @@ -1,157 +0,0 @@ -// Control panel: unified action buttons, status display - -export class Controls { - constructor(container, callbacks) { - this.callbacks = callbacks; - this.el = container; - this.build(); - } - - build() { - this.el.innerHTML = ` -
    - - - - - - - -
    -
    - Examples: 0 - - Noise: 0.05 -
    -
    - Joystick - 0.50, 0.50 - Output Mean - 0.000 - Output Spread - 0.000 - Best Loss - - - Train Iters - 0 - Gamepad - Disconnected - Follow - Off -
    -
    - -
    - `; - - // Action buttons - this.el.querySelector('#btn-thumbsup').addEventListener('click', () => this.callbacks.onThumbsUp?.()); - this.el.querySelector('#btn-thumbsdown').addEventListener('click', () => this.callbacks.onThumbsDown?.()); - this.el.querySelector('#btn-add').addEventListener('click', () => this.callbacks.onAddExample?.()); - this.el.querySelector('#btn-train').addEventListener('click', () => this.callbacks.onTrain?.()); - this.el.querySelector('#btn-randomize').addEventListener('click', () => this.callbacks.onRandomize?.()); - this.el.querySelector('#btn-clear-examples').addEventListener('click', () => this.callbacks.onClearExamples?.()); - this.el.querySelector('#btn-clear').addEventListener('click', () => this.callbacks.onClear?.()); - } - - updateStatus(exampleCount, loss, noiseLevel) { - this.el.querySelector('#status-examples').textContent = `Examples: ${exampleCount}`; - if (loss !== null && loss !== undefined) { - this.el.querySelector('#status-loss').textContent = `Loss: ${loss.toFixed(5)}`; - } else { - this.el.querySelector('#status-loss').textContent = 'Loss: -'; - } - if (noiseLevel !== undefined) { - this.el.querySelector('#status-noise').textContent = `Noise: ${noiseLevel.toFixed(3)}`; - } - } - - updateMetrics(metrics = {}) { - const set = (id, value) => { - const el = this.el.querySelector(id); - if (el) el.textContent = value; - }; - - if (metrics.joystickX !== undefined && metrics.joystickY !== undefined) { - set('#metric-joystick', `${metrics.joystickX.toFixed(2)}, ${metrics.joystickY.toFixed(2)}`); - } - if (metrics.outputMean !== undefined) set('#metric-mean', metrics.outputMean.toFixed(3)); - if (metrics.outputSpread !== undefined) set('#metric-spread', metrics.outputSpread.toFixed(3)); - if (metrics.bestLoss !== undefined && metrics.bestLoss !== null) { - set('#metric-best-loss', metrics.bestLoss.toFixed(5)); - } - if (metrics.totalTrainingIterations !== undefined) { - set('#metric-train-iters', String(metrics.totalTrainingIterations)); - } - if (metrics.gamepadConnected !== undefined) { - set('#metric-gamepad', metrics.gamepadConnected ? 'Connected' : 'Disconnected'); - } - if (metrics.followMode !== undefined) { - set('#metric-follow', metrics.followMode ? 'On' : 'Off'); - } - } - - updateLossPlot(lossHistory = []) { - const canvas = this.el.querySelector('#loss-plot'); - if (!canvas) return; - const ctx = canvas.getContext('2d'); - const width = canvas.width; - const height = canvas.height; - - ctx.clearRect(0, 0, width, height); - ctx.fillStyle = '#121212'; - ctx.fillRect(0, 0, width, height); - ctx.strokeStyle = '#2b2b2b'; - ctx.strokeRect(0.5, 0.5, width - 1, height - 1); - - if (!lossHistory.length) { - ctx.fillStyle = '#666'; - ctx.font = '11px monospace'; - ctx.fillText('Loss history appears after training.', 10, 18); - return; - } - - const points = lossHistory.slice(-420); - let min = Infinity; - let max = -Infinity; - for (const v of points) { - if (v < min) min = v; - if (v > max) max = v; - } - const range = Math.max(max - min, 1e-9); - const leftPad = 6; - const rightPad = 6; - const topPad = 6; - const bottomPad = 14; - const plotW = width - leftPad - rightPad; - const plotH = height - topPad - bottomPad; - - ctx.strokeStyle = '#352418'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(leftPad, topPad + plotH / 2); - ctx.lineTo(width - rightPad, topPad + plotH / 2); - ctx.stroke(); - - ctx.strokeStyle = '#ff6a00'; - ctx.lineWidth = 1.6; - ctx.beginPath(); - for (let i = 0; i < points.length; i++) { - const x = leftPad + (i / Math.max(points.length - 1, 1)) * plotW; - const yNorm = (points[i] - min) / range; - const y = topPad + (1 - yNorm) * plotH; - if (i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - } - ctx.stroke(); - - ctx.fillStyle = '#8a8a8a'; - ctx.font = '10px monospace'; - ctx.fillText(`min ${min.toFixed(5)}`, leftPad, height - 3); - ctx.fillText(`max ${max.toFixed(5)}`, width - rightPad - 78, height - 3); - } -} diff --git a/playground/js/ui/dev-panel.js b/playground/js/ui/dev-panel.js deleted file mode 100644 index 3fb18ed..0000000 --- a/playground/js/ui/dev-panel.js +++ /dev/null @@ -1,222 +0,0 @@ -// Dev panel — draggable, collapsible floating panel for tuning hand tracking parameters -// Gated on ?devmode=true URL parameter - -import { HAND_TRACKER_DEFAULTS } from './hand-tracker.js'; - -const PARAMS = [ - { - key: 'minHandDetectionConfidence', - label: 'Detection confidence', - min: 0.1, max: 1.0, step: 0.05, - help: 'Palm detection threshold. Lower = more aggressive detection, more false positives.', - }, - { - key: 'minHandPresenceConfidence', - label: 'Presence confidence', - min: 0.1, max: 1.0, step: 0.05, - help: 'Hand presence threshold. Lower = holds onto tracked hands longer.', - }, - { - key: 'minTrackingConfidence', - label: 'Tracking confidence', - min: 0.1, max: 1.0, step: 0.05, - help: 'Frame-to-frame tracking IoU. Lower = tracks through fast movement.', - }, - { - key: 'smoothingFactor', - label: 'Smoothing', - min: 0.05, max: 1.0, step: 0.05, - help: 'Feature smoothing (EMA factor). Lower = smoother/laggier, higher = more responsive/jittery.', - }, - { - key: 'gestureHoldMs', - label: 'Gesture hold (ms)', - min: 100, max: 1500, step: 50, - help: 'How long a gesture must be held before firing.', - }, - { - key: 'useWorldLandmarks', - label: 'World landmarks', - type: 'toggle', - help: 'Use real-world meter coords (distance-independent) vs image-normalized coords.', - }, -]; - -export function createDevPanel(handTrackerGetter) { - if (new URLSearchParams(window.location.search).get('devmode') !== 'true') return null; - - const panel = document.createElement('div'); - panel.className = 'dev-panel'; - panel.innerHTML = ` -
    - Hand Tracking Dev - -
    -
    - `; - document.body.appendChild(panel); - - const $body = panel.querySelector('#dev-panel-body'); - const $header = panel.querySelector('#dev-panel-header'); - const $collapse = panel.querySelector('#dev-panel-collapse'); - const sliders = {}; - - // Build controls - for (const p of PARAMS) { - const row = document.createElement('div'); - row.className = 'dev-row'; - - if (p.type === 'toggle') { - row.innerHTML = ` - - `; - const input = row.querySelector('input'); - input.addEventListener('change', () => { - applyOption(p.key, input.checked); - }); - sliders[p.key] = input; - } else { - const defaultVal = HAND_TRACKER_DEFAULTS[p.key]; - row.innerHTML = ` - -
    - - ${formatVal(p.key, defaultVal)} -
    - `; - const input = row.querySelector('input[type=range]'); - const display = row.querySelector('.dev-value'); - input.addEventListener('input', () => { - const val = parseFloat(input.value); - display.textContent = formatVal(p.key, val); - applyOption(p.key, val); - }); - sliders[p.key] = input; - } - - $body.appendChild(row); - } - - // Reset button - const resetRow = document.createElement('div'); - resetRow.className = 'dev-row dev-row-actions'; - resetRow.innerHTML = ``; - resetRow.querySelector('button').addEventListener('click', () => { - for (const p of PARAMS) { - const def = HAND_TRACKER_DEFAULTS[p.key]; - if (p.type === 'toggle') { - sliders[p.key].checked = def; - } else { - sliders[p.key].value = def; - sliders[p.key].parentElement.querySelector('.dev-value').textContent = formatVal(p.key, def); - } - applyOption(p.key, def); - } - }); - $body.appendChild(resetRow); - - // Feature monitor (live feature values) - const monitorRow = document.createElement('div'); - monitorRow.className = 'dev-row'; - monitorRow.innerHTML = ` - - - `; - $body.appendChild(monitorRow); - const $featureBars = monitorRow.querySelector('canvas'); - const featureCtx = $featureBars.getContext('2d'); - - // --- Collapse --- - let collapsed = false; - $collapse.addEventListener('click', (e) => { - e.stopPropagation(); - collapsed = !collapsed; - $body.style.display = collapsed ? 'none' : ''; - $collapse.textContent = collapsed ? '+' : '_'; - panel.classList.toggle('collapsed', collapsed); - }); - - // --- Drag --- - let dragging = false, dragOffX = 0, dragOffY = 0; - $header.addEventListener('pointerdown', (e) => { - if (e.target === $collapse) return; - dragging = true; - dragOffX = e.clientX - panel.offsetLeft; - dragOffY = e.clientY - panel.offsetTop; - $header.setPointerCapture(e.pointerId); - e.preventDefault(); - }); - $header.addEventListener('pointermove', (e) => { - if (!dragging) return; - panel.style.left = (e.clientX - dragOffX) + 'px'; - panel.style.top = (e.clientY - dragOffY) + 'px'; - panel.style.right = 'auto'; - panel.style.bottom = 'auto'; - }); - $header.addEventListener('pointerup', () => { dragging = false; }); - - // --- Apply option to hand tracker --- - function applyOption(key, value) { - const ht = handTrackerGetter(); - if (ht) { - ht.setOptions({ [key]: value }); - } - } - - // --- Feature bar animation --- - const FEATURE_LABELS = [ - 'pX','pY','tC','iC','mC','rC','pnC','s01','s12','s23','s34','rol','pit','pnc' - ]; - - function drawFeatureBars() { - const ht = handTrackerGetter(); - const w = $featureBars.width; - const h = $featureBars.height; - featureCtx.clearRect(0, 0, w, h); - - if (!ht || !ht.active) { - featureCtx.fillStyle = 'rgba(255,255,255,0.1)'; - featureCtx.font = '9px monospace'; - featureCtx.fillText('No tracking', 60, h / 2 + 3); - requestAnimationFrame(drawFeatureBars); - return; - } - - const features = ht.features; - const barW = (w - 2) / 14; - - for (let i = 0; i < 14; i++) { - const v = features[i]; - const x = i * barW + 1; - const barH = v * (h - 12); - - // Bar background - featureCtx.fillStyle = 'rgba(255,255,255,0.05)'; - featureCtx.fillRect(x, 10, barW - 1, h - 12); - - // Bar fill - featureCtx.fillStyle = `hsl(${(i / 14) * 360}, 70%, 55%)`; - featureCtx.fillRect(x, h - barH, barW - 1, barH); - - // Label - featureCtx.fillStyle = 'rgba(255,255,255,0.4)'; - featureCtx.font = '6px monospace'; - featureCtx.fillText(FEATURE_LABELS[i], x, 8); - } - - requestAnimationFrame(drawFeatureBars); - } - drawFeatureBars(); - - return panel; -} - -function formatVal(key, val) { - if (key === 'gestureHoldMs') return `${val}`; - return val.toFixed(2); -} diff --git a/playground/js/ui/engine-switcher.js b/playground/js/ui/engine-switcher.js deleted file mode 100644 index e7aa6d6..0000000 --- a/playground/js/ui/engine-switcher.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * EngineSwitcher — engine selection UI for the Engine drawer. - * - * Usage: - * EngineSwitcher.init(containerEl, engines, onSwitch); - * - * @param {HTMLElement} containerEl — element to prepend the switcher into - * @param {Array<{id, displayName, paramCount, description, comingSoon?}>} engines - * @param {(engineId: string) => void} onSwitch — called when user confirms a switch - */ -export const EngineSwitcher = { - _containerEl: null, - _engines: [], - _activeId: null, - _onSwitch: null, - _hasTrainingData: null, // () => boolean - - /** - * @param {HTMLElement} containerEl - * @param {Array<{id:string, displayName:string, paramCount:number, description:string, comingSoon?:boolean}>} engines - * @param {(engineId:string) => void} onSwitch - * @param {{ hasTrainingData?: () => boolean }} [opts] - */ - init(containerEl, engines, onSwitch, opts = {}) { - this._containerEl = containerEl; - this._engines = engines; - this._onSwitch = onSwitch; - this._hasTrainingData = opts.hasTrainingData || (() => false); - - this._activeId = engines[0]?.id ?? null; - this._render(); - }, - - /** Update which card appears active (call after engine swap completes). */ - setActive(engineId) { - this._activeId = engineId; - const section = this._containerEl?.querySelector('.engine-switcher-section'); - if (!section) return; - section.querySelectorAll('.engine-card').forEach(card => { - card.classList.toggle('active', card.dataset.engineId === engineId); - }); - }, - - /** Show/hide the loading spinner on a card. */ - setLoading(engineId, loading) { - const section = this._containerEl?.querySelector('.engine-switcher-section'); - if (!section) return; - const card = section.querySelector(`[data-engine-id="${engineId}"]`); - if (card) card.classList.toggle('loading', loading); - }, - - _render() { - const container = this._containerEl; - if (!container) return; - - // Remove any previous section if re-initialised - const existing = container.querySelector('.engine-switcher-section'); - if (existing) existing.remove(); - - const section = document.createElement('div'); - section.className = 'engine-switcher-section'; - - const heading = document.createElement('div'); - heading.className = 'engine-switcher-heading'; - heading.textContent = 'Synthesis Engine'; - section.appendChild(heading); - - const grid = document.createElement('div'); - grid.className = 'engine-card-grid'; - - for (const eng of this._engines) { - const card = document.createElement('div'); - card.className = 'engine-card'; - card.dataset.engineId = eng.id; - if (eng.id === this._activeId) card.classList.add('active'); - if (eng.comingSoon) card.classList.add('coming-soon'); - - const nameRow = document.createElement('div'); - nameRow.className = 'engine-card-name-row'; - - const name = document.createElement('span'); - name.className = 'engine-card-name'; - name.textContent = eng.displayName; - - const badge = document.createElement('span'); - badge.className = 'engine-card-params'; - badge.textContent = eng.comingSoon ? 'soon' : `${eng.paramCount} params`; - - nameRow.appendChild(name); - nameRow.appendChild(badge); - - const desc = document.createElement('span'); - desc.className = 'engine-card-desc'; - desc.textContent = eng.description; - - card.appendChild(nameRow); - card.appendChild(desc); - grid.appendChild(card); - - if (!eng.comingSoon) { - card.addEventListener('click', () => this._handleClick(eng)); - } - } - - section.appendChild(grid); - - const divider = document.createElement('div'); - divider.className = 'engine-switcher-divider'; - section.appendChild(divider); - - // Prepend before any existing drawer content (e.g. engine-tuning sliders) - container.insertBefore(section, container.firstChild); - }, - - _handleClick(eng) { - if (eng.id === this._activeId) return; - - const proceed = () => { - this.setLoading(eng.id, true); - try { - this._onSwitch(eng.id); - } finally { - // Loading state cleared by caller via setLoading/setActive - } - }; - - if (this._hasTrainingData()) { - const msg = 'Switching engines will reset training examples. Network weights will be partially preserved. Continue?'; - if (window.confirm(msg)) proceed(); - } else { - proceed(); - } - }, -}; diff --git a/playground/js/ui/eoc-chain-ui.js b/playground/js/ui/eoc-chain-ui.js deleted file mode 100644 index be2912b..0000000 --- a/playground/js/ui/eoc-chain-ui.js +++ /dev/null @@ -1,396 +0,0 @@ -// eoc-chain-ui.js — Drawer UI for the End-of-Chain effects rack. -// -// Usage: -// import { EOCChainUI, moduleFactory } from './ui/eoc-chain-ui.js'; -// EOCChainUI.init(chain, document.getElementById('eoc-drawer-body')); -// -// The drawer renders: -// 1. NISPS mode selector (Bypass / Shared / Linked / Independent) -// 2. Ordered, draggable list of active modules (bypass toggle, remove, param count) -// 3. Add-module bar (one button per available but absent module) -// -// Reacts to 'eoc:change' events on window to stay in sync with external mutations. - -import { EOCModule } from '../eoc/eoc-module.js'; -import { EQModule } from '../eoc/modules/eq-module.js'; -import { CompressorModule } from '../eoc/modules/compressor-module.js'; -import { ReverbModule } from '../eoc/modules/reverb-module.js'; -import { DelayModule } from '../eoc/modules/delay-module.js'; -import { SaturationModule } from '../eoc/modules/saturation-module.js'; -import { MasterModule } from '../eoc/modules/master-module.js'; - -// --------------------------------------------------------------------------- -// Stub module factory -// --------------------------------------------------------------------------- - -const STUB_DEFS = { - saturation: { displayName: 'Saturation', paramMeta: [] }, - eq: { displayName: 'EQ', paramMeta: [] }, - compressor: { displayName: 'Compressor', paramMeta: [] }, - reverb: { displayName: 'Reverb', paramMeta: [] }, - delay: { displayName: 'Delay', paramMeta: [] }, - master: { displayName: 'Master Bus', paramMeta: [] }, -}; - -// Canonical slot order (matches EOCChain's DEFAULT_ORDER) -const MODULE_ORDER = ['saturation', 'eq', 'compressor', 'reverb', 'delay', 'master']; - -/** - * Build a minimal EOCModule stub for the given id. - * Exported so a-app.js can replace it with real implementations later. - * - * @param {string} id — one of saturation | eq | compressor | reverb | delay | master - * @returns {EOCModule} - */ -export function moduleFactory(id) { - // Real Faust WASM implementations - switch (id) { - case 'eq': return new EQModule(); - case 'compressor': return new CompressorModule(); - case 'reverb': return new ReverbModule(); - case 'delay': return new DelayModule(); - case 'saturation': return new SaturationModule(); - case 'master': return new MasterModule(); - default: break; - } - - const def = STUB_DEFS[id]; - if (!def) throw new Error(`moduleFactory: unknown module id '${id}'`); - - // Build stub class inline, scoped to the specific id/def - class StubModule extends EOCModule { - get id() { return id; } - get displayName() { return def.displayName; } - get paramMeta() { return def.paramMeta; } - - async init(audioCtx) { - // Minimal super call to satisfy the base class contract without - // requiring a real AudioContext in stub/test scenarios - if (audioCtx && typeof audioCtx.createGain === 'function') { - await super.init(audioCtx); - this._finishInit(); - } else { - this._initialized = true; - } - } - } - - return new StubModule(); -} - -// --------------------------------------------------------------------------- -// EOCChainUI -// --------------------------------------------------------------------------- - -export const EOCChainUI = { - /** - * @type {import('../eoc/eoc-chain.js').EOCChain|null} - */ - _chain: null, - /** @type {HTMLElement|null} */ - _container: null, - /** Dragged module id during HTML5 DnD */ - _dragId: null, - - /** - * Initialise the UI. - * - * @param {import('../eoc/eoc-chain.js').EOCChain} chain - * @param {HTMLElement} containerEl - */ - init(chain, containerEl) { - this._chain = chain; - this._container = containerEl; - - // Listen for external chain changes - window.addEventListener('eoc:change', () => this._render()); - - this._render(); - }, - - // --------------------------------------------------------------------------- - // Rendering - // --------------------------------------------------------------------------- - - _render() { - if (!this._chain || !this._container) return; - - const c = this._container; - c.innerHTML = ''; - - c.appendChild(this._buildNispsBar()); - c.appendChild(this._buildModuleList()); - c.appendChild(this._buildAddBar()); - }, - - // NISPS mode selector bar - _buildNispsBar() { - const modes = [ - { id: 'bypass', label: 'Bypass' }, - { id: 'shared', label: 'Shared' }, - { id: 'linked', label: 'Linked' }, - { id: 'independent', label: 'Indep' }, - ]; - - const bar = document.createElement('div'); - bar.className = 'eoc-nisps-bar'; - - const label = document.createElement('span'); - label.className = 'eoc-section-label'; - label.textContent = 'NISPS'; - bar.appendChild(label); - - const toggle = document.createElement('div'); - toggle.className = 'pill-toggle pill-toggle-sm eoc-nisps-toggle'; - - modes.forEach(({ id, label: lbl }) => { - const btn = document.createElement('button'); - btn.className = 'pill-opt' + (this._chain.nispsMode === id ? ' active' : ''); - btn.textContent = lbl; - btn.addEventListener('click', () => { - this._chain.nispsMode = id; - // re-render is triggered by eoc:change event from chain - }); - toggle.appendChild(btn); - }); - - bar.appendChild(toggle); - return bar; - }, - - // Ordered list of module rows - _buildModuleList() { - const list = document.createElement('div'); - list.className = 'eoc-module-list'; - - const modules = this._chain.modules; - - if (modules.length === 0) { - const empty = document.createElement('div'); - empty.className = 'eoc-empty-hint'; - empty.textContent = 'No modules — add one below'; - list.appendChild(empty); - return list; - } - - modules.forEach((mod, idx) => { - const row = this._buildModuleRow(mod, idx, modules.length, list); - list.appendChild(row); - }); - - return list; - }, - - _buildModuleRow(mod, idx, totalCount, listEl) { - const row = document.createElement('div'); - row.className = 'eoc-module-row'; - row.dataset.id = mod.id; - row.draggable = true; - - // --- Drag handle --- - const handle = document.createElement('span'); - handle.className = 'eoc-drag-handle'; - handle.title = 'Drag to reorder'; - handle.textContent = '⠿'; - row.appendChild(handle); - - // --- Module name --- - const name = document.createElement('span'); - name.className = 'eoc-module-name'; - name.textContent = mod.displayName; - row.appendChild(name); - - // --- Param count badge --- - const badge = document.createElement('span'); - badge.className = 'eoc-param-badge'; - badge.textContent = mod.paramCount === 1 - ? '1 param' - : `${mod.paramCount} params`; - row.appendChild(badge); - - // --- Bypass toggle --- - const toggleLabel = document.createElement('label'); - toggleLabel.className = 'eoc-bypass-toggle'; - toggleLabel.title = mod.enabled ? 'Active — click to bypass' : 'Bypassed — click to enable'; - - const toggleInput = document.createElement('input'); - toggleInput.type = 'checkbox'; - toggleInput.checked = mod.enabled; - toggleInput.addEventListener('change', () => { - mod.enabled = toggleInput.checked; - // _applyBypass wires audio graph internally; dispatch change so UI refreshes - window.dispatchEvent(new CustomEvent('eoc:change', { - detail: { reason: 'bypass-toggled', chain: this._chain } - })); - }); - - const toggleSlider = document.createElement('span'); - toggleSlider.className = 'eoc-bypass-slider'; - - toggleLabel.appendChild(toggleInput); - toggleLabel.appendChild(toggleSlider); - row.appendChild(toggleLabel); - - // --- Remove button --- - const removeBtn = document.createElement('button'); - removeBtn.className = 'eoc-remove-btn'; - removeBtn.title = `Remove ${mod.displayName}`; - removeBtn.textContent = '×'; - removeBtn.addEventListener('click', () => { - this._chain.removeModule(mod.id); - }); - row.appendChild(removeBtn); - - // --- Manual param sliders (shown in bypass mode, or when module has params) --- - if (mod.paramCount > 0) { - const paramsSection = this._buildParamSliders(mod); - row.appendChild(paramsSection); - } - - // --- HTML5 Drag-and-drop --- - row.addEventListener('dragstart', (e) => { - this._dragId = mod.id; - row.classList.add('dragging'); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', mod.id); - }); - - row.addEventListener('dragend', () => { - this._dragId = null; - row.classList.remove('dragging'); - // Clean up all drop indicators - listEl.querySelectorAll('.eoc-drop-indicator').forEach(el => el.remove()); - }); - - row.addEventListener('dragover', (e) => { - if (!this._dragId || this._dragId === mod.id) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - - // Show drop-indicator above or below based on cursor position - const rect = row.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - const insertBefore = e.clientY < midY; - - // Remove all existing indicators then place one - listEl.querySelectorAll('.eoc-drop-indicator').forEach(el => el.remove()); - const indicator = document.createElement('div'); - indicator.className = 'eoc-drop-indicator'; - if (insertBefore) { - listEl.insertBefore(indicator, row); - } else { - row.insertAdjacentElement('afterend', indicator); - } - }); - - row.addEventListener('drop', (e) => { - if (!this._dragId || this._dragId === mod.id) return; - e.preventDefault(); - - // Determine new position - const rect = row.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - const insertBefore = e.clientY < midY; - - const allRows = [...listEl.querySelectorAll('.eoc-module-row')]; - const targetIdx = allRows.findIndex(r => r.dataset.id === mod.id); - // After moving the dragged element out, the target index needs adjustment - const dragIdx = allRows.findIndex(r => r.dataset.id === this._dragId); - let newPos = insertBefore ? targetIdx : targetIdx + 1; - if (dragIdx < targetIdx) newPos -= 1; // account for removal shifting indices - - this._chain.moveModule(this._dragId, Math.max(0, newPos)); - // eoc:change will re-render - }); - - return row; - }, - - // Per-module param sliders (collapsible, shown only when paramCount > 0) - _buildParamSliders(mod) { - const section = document.createElement('div'); - section.className = 'eoc-params-section'; - - // Toggle button (chevron + label) - const toggleBtn = document.createElement('button'); - toggleBtn.className = 'eoc-params-toggle'; - const isManual = this._chain.nispsMode === 'bypass'; - toggleBtn.textContent = isManual ? '▶ params (manual)' : '▶ params'; - toggleBtn.title = 'Expand to show parameter sliders'; - - const sliderList = document.createElement('div'); - sliderList.className = 'eoc-params-list eoc-params-collapsed'; - - toggleBtn.addEventListener('click', () => { - const collapsed = sliderList.classList.toggle('eoc-params-collapsed'); - toggleBtn.textContent = collapsed - ? (isManual ? '▶ params (manual)' : '▶ params') - : (isManual ? '▼ params (manual)' : '▼ params'); - }); - - // One slider per param - mod.paramMeta.forEach((meta, i) => { - const row = document.createElement('div'); - row.className = 'eoc-param-row'; - - const label = document.createElement('label'); - label.className = 'eoc-param-label'; - label.textContent = meta.name ?? meta.id ?? `Param ${i}`; - - const slider = document.createElement('input'); - slider.type = 'range'; - slider.min = '0'; - slider.max = '1'; - slider.step = '0.001'; - slider.value = String(mod.getCurrentParamValue(i)); - slider.className = 'eoc-param-slider'; - - slider.addEventListener('input', () => { - mod.setParam(i, parseFloat(slider.value)); - }); - - row.appendChild(label); - row.appendChild(slider); - sliderList.appendChild(row); - }); - - section.appendChild(toggleBtn); - section.appendChild(sliderList); - return section; - }, - - // Add-module bar - _buildAddBar() { - const bar = document.createElement('div'); - bar.className = 'eoc-add-bar'; - - const activeIds = new Set(this._chain.modules.map(m => m.id)); - - const available = MODULE_ORDER.filter(id => !activeIds.has(id)); - - if (available.length === 0) { - const hint = document.createElement('span'); - hint.className = 'eoc-add-hint'; - hint.textContent = 'All modules added'; - bar.appendChild(hint); - return bar; - } - - available.forEach(id => { - const def = STUB_DEFS[id]; - if (!def) return; - - const btn = document.createElement('button'); - btn.className = 'action-btn eoc-add-btn'; - btn.textContent = `+ ${def.displayName}`; - btn.title = `Add ${def.displayName} to chain`; - btn.addEventListener('click', () => { - this._chain.addModule(moduleFactory(id)); - // eoc:change re-renders - }); - bar.appendChild(btn); - }); - - return bar; - }, -}; diff --git a/playground/js/ui/eoc-joystick.js b/playground/js/ui/eoc-joystick.js deleted file mode 100644 index c7844ba..0000000 --- a/playground/js/ui/eoc-joystick.js +++ /dev/null @@ -1,73 +0,0 @@ -// EOC Independent mode joystick — minimal 2D touch/mouse input -// No zoom, no momentum, no input pipeline — just raw normalized X/Y - -export class EOCJoystick { - constructor(canvas) { - this._canvas = canvas; - this._ctx = canvas.getContext('2d'); - this._x = 0.5; - this._y = 0.5; - this._dragging = false; - this._onChange = null; - this._bindEvents(); - this._draw(); - } - - get x() { return this._x; } - get y() { return this._y; } - set onChange(fn) { this._onChange = fn; } - - _bindEvents() { - this._canvas.addEventListener('pointerdown', e => { - e.preventDefault(); - this._dragging = true; - this._canvas.setPointerCapture(e.pointerId); - this._update(e); - }); - this._canvas.addEventListener('pointermove', e => { - if (this._dragging) this._update(e); - }); - this._canvas.addEventListener('pointerup', e => { - this._dragging = false; - this._canvas.releasePointerCapture(e.pointerId); - }); - this._canvas.addEventListener('pointercancel', e => { - this._dragging = false; - }); - } - - _update(e) { - const rect = this._canvas.getBoundingClientRect(); - this._x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - this._y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); - this._draw(); - this._onChange?.(this._x, this._y); - } - - _draw() { - const ctx = this._ctx; - const w = this._canvas.width; - const h = this._canvas.height; - ctx.clearRect(0, 0, w, h); - // Background - ctx.fillStyle = 'rgba(255,255,255,0.05)'; - ctx.fillRect(0, 0, w, h); - // Crosshairs - ctx.strokeStyle = 'rgba(255,255,255,0.15)'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(w / 2, 0); ctx.lineTo(w / 2, h); - ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2); - ctx.stroke(); - // Dot - ctx.fillStyle = this._dragging ? '#5ff5ea' : '#4ecdc4'; - ctx.beginPath(); - ctx.arc(this._x * w, this._y * h, 6, 0, Math.PI * 2); - ctx.fill(); - // Position label - ctx.fillStyle = 'rgba(78,205,196,0.5)'; - ctx.font = '9px monospace'; - ctx.textAlign = 'right'; - ctx.fillText(`${this._x.toFixed(2)}, ${this._y.toFixed(2)}`, w - 4, h - 4); - } -} diff --git a/playground/js/ui/gamepad.js b/playground/js/ui/gamepad.js deleted file mode 100644 index 9856d12..0000000 --- a/playground/js/ui/gamepad.js +++ /dev/null @@ -1,168 +0,0 @@ -// GamepadInput — reusable gamepad polling module for NISPS playground -// Maps first stick axes to [0,1] with deadzone, polls via rAF, fires callbacks - -export class GamepadInput { - /** - * @param {Object} options - * @param {function(number, number)} options.onMove - called with (x, y) in [0,1] when stick moves - * @param {function(string)} [options.onButton] - called with button name ('lb'|'rb') on press - * @param {function(boolean)} [options.onConnectionChange] - called when gamepad connects/disconnects - * @param {number} [options.deadzone=0.08] - axis deadzone threshold - * @param {number} [options.moveThreshold=0.002] - minimum mapped change to trigger onMove - * @param {number} [options.axisX=0] - gamepad axis index for X - * @param {number} [options.axisY=1] - gamepad axis index for Y - * @param {boolean} [options.invertY=false] - if true, invert Y axis (up = 0) - */ - constructor(options = {}) { - this.onMove = options.onMove || (() => {}); - this.onButton = options.onButton || null; - this.onConnectionChange = options.onConnectionChange || null; - this.deadzone = options.deadzone ?? 0.08; - this.moveThreshold = options.moveThreshold ?? 0.002; - this.axisX = options.axisX ?? 0; - this.axisY = options.axisY ?? 1; - this.invertY = options.invertY ?? false; - - this.connected = false; - this._index = -1; - this._lastAxes = [0.5, 0.5]; - this._buttonsPrev = []; - this._polling = false; - - this._onConnected = this._onConnected.bind(this); - this._onDisconnected = this._onDisconnected.bind(this); - - window.addEventListener('gamepadconnected', this._onConnected); - window.addEventListener('gamepaddisconnected', this._onDisconnected); - - // Check if a gamepad is already connected - this._findGamepad(); - - // Periodic fallback: some environments (Steam Deck, some Linux browsers) - // don't fire gamepadconnected reliably. Poll every 500ms until found. - this._probeInterval = setInterval(() => { - if (!this.connected) { - this._findGamepad(); - } else { - clearInterval(this._probeInterval); - this._probeInterval = null; - } - }, 500); - } - - _onConnected(e) { - if (this._index < 0) { - this._index = e.gamepad.index; - } - this._setConnected(true); - } - - _onDisconnected(e) { - if (e.gamepad.index === this._index) { - this._index = -1; - // Try to find another connected gamepad - this._findGamepad(); - if (this._index < 0) { - this._setConnected(false); - } - } - } - - _setConnected(value) { - if (this.connected !== value) { - this.connected = value; - if (this.onConnectionChange) this.onConnectionChange(value); - } - } - - _findGamepad() { - if (!navigator.getGamepads) return; - const gamepads = navigator.getGamepads(); - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i] && gamepads[i].connected) { - this._index = i; - this._setConnected(true); - return; - } - } - } - - /** - * Poll the gamepad. Call this once per frame (e.g. inside requestAnimationFrame). - * Returns true if stick moved. - */ - poll() { - if (!navigator.getGamepads) return false; - const gamepads = navigator.getGamepads(); - let gp = null; - - if (this._index >= 0 && gamepads[this._index]?.connected) { - gp = gamepads[this._index]; - } else { - this._index = -1; - for (let i = 0; i < gamepads.length; i++) { - if (gamepads[i]?.connected) { - gp = gamepads[i]; - this._index = i; - break; - } - } - } - - const wasConnected = this.connected; - this.connected = !!gp; - if (wasConnected !== this.connected) { - if (this.onConnectionChange) this.onConnectionChange(this.connected); - } - - if (!gp) { - this._buttonsPrev = []; - return false; - } - - // Axes - const rawX = gp.axes[this.axisX] || 0; - const rawY = gp.axes[this.axisY] || 0; - const axisX = Math.abs(rawX) < this.deadzone ? 0 : rawX; - const axisY = Math.abs(rawY) < this.deadzone ? 0 : rawY; - let mappedX = (axisX + 1) * 0.5; - let mappedY = (axisY + 1) * 0.5; - if (this.invertY) mappedY = 1 - mappedY; - - const moved = Math.abs(mappedX - this._lastAxes[0]) > this.moveThreshold || - Math.abs(mappedY - this._lastAxes[1]) > this.moveThreshold; - - if (moved) { - this._lastAxes = [mappedX, mappedY]; - this.onMove(mappedX, mappedY); - } - - // Buttons: A=0, B=1, X=2, Y=3, LB=4, RB=5 - if (this.onButton) { - const watched = [0, 1, 2, 3, 4, 5]; - const names = ['a', 'b', 'x', 'y', 'lb', 'rb']; - for (let i = 0; i < watched.length; i++) { - const idx = watched[i]; - const pressed = !!gp.buttons[idx]?.pressed; - const prev = !!this._buttonsPrev[idx]; - if (pressed && !prev) this.onButton(names[i]); - this._buttonsPrev[idx] = pressed; - } - } - - return moved; - } - - /** Current mapped X position [0,1] */ - get x() { return this._lastAxes[0]; } - - /** Current mapped Y position [0,1] */ - get y() { return this._lastAxes[1]; } - - /** Clean up event listeners */ - destroy() { - window.removeEventListener('gamepadconnected', this._onConnected); - window.removeEventListener('gamepaddisconnected', this._onDisconnected); - if (this._probeInterval) clearInterval(this._probeInterval); - } -} diff --git a/playground/js/ui/gradient-flow.js b/playground/js/ui/gradient-flow.js deleted file mode 100644 index be7dfd2..0000000 --- a/playground/js/ui/gradient-flow.js +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Gradient Flow Indicator — per-layer gradient magnitude visualization. - * - * Uses the weight-delta approach: snapshot weights before training, snapshot after, - * compute per-layer L2 norm of the delta. No WASM changes needed. - * - * @module gradient-flow - */ - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -/** Ratio threshold: if next layer's gradient < VANISHING_RATIO * previous, it's vanishing */ -const VANISHING_RATIO = 0.5; - -/** Ratio threshold: if next layer's gradient > EXPLODING_RATIO * previous, it's exploding */ -const EXPLODING_RATIO = 2.0; - -/** Absolute threshold: if all gradient norms below this, network has converged */ -const CONVERGED_THRESHOLD = 1e-6; - -// --------------------------------------------------------------------------- -// Colors -// --------------------------------------------------------------------------- - -const COLOR_HEALTHY = { r: 100, g: 200, b: 120 }; -const COLOR_WARNING = { r: 230, g: 200, b: 60 }; -const COLOR_DANGER = { r: 240, g: 80, b: 60 }; -const COLOR_CONVERGED = { r: 100, g: 140, b: 200 }; - -// --------------------------------------------------------------------------- -// GradientFlowIndicator -// --------------------------------------------------------------------------- - -export class GradientFlowIndicator { - /** - * @param {number[]} layerSizes — e.g. [3, 32, 48, 64, 126] - */ - constructor(layerSizes) { - this._layerSizes = layerSizes; - this._numLayers = layerSizes.length - 1; // number of weight matrices - - // Compute per-layer weight counts: layer i has layerSizes[i] * layerSizes[i+1] weights + layerSizes[i+1] biases - this._layerWeightCounts = []; - this._layerOffsets = []; - let offset = 0; - for (let i = 0; i < this._numLayers; i++) { - const count = layerSizes[i] * layerSizes[i + 1] + layerSizes[i + 1]; - this._layerWeightCounts.push(count); - this._layerOffsets.push(offset); - offset += count; - } - this._totalWeights = offset; - - // Build layer labels - this._layerLabels = []; - for (let i = 0; i < this._numLayers; i++) { - if (i === this._numLayers - 1) { - this._layerLabels.push('Out'); - } else { - this._layerLabels.push(`L${i + 1}`); - } - } - - // Weight snapshots - this._beforeWeights = null; - this._afterWeights = null; - this._flow = null; - } - - /** - * Capture weight snapshot before training. - * @param {number[]|Float32Array} weightsArray — flat array of all weights - */ - captureBeforeTrain(weightsArray) { - if (!weightsArray) return; - this._beforeWeights = weightsArray instanceof Float32Array - ? new Float32Array(weightsArray) - : new Float32Array(weightsArray); - } - - /** - * Capture weight snapshot after training and compute gradient flow. - * @param {number[]|Float32Array} weightsArray — flat array of all weights - */ - captureAfterTrain(weightsArray) { - if (!weightsArray || !this._beforeWeights) return; - - this._afterWeights = weightsArray instanceof Float32Array - ? weightsArray - : new Float32Array(weightsArray); - - this._computeFlow(); - } - - /** - * Get per-layer gradient flow info. - * @returns {object|null} - */ - getFlow() { - return this._flow; - } - - /** - * Draw per-layer gradient flow bars. - * - * @param {CanvasRenderingContext2D} ctx - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - */ - draw(ctx, x, y, width, height) { - if (!this._flow) { - ctx.fillStyle = 'rgba(60, 60, 60, 0.4)'; - ctx.fillRect(x, y, width, height); - ctx.font = '8px monospace'; - ctx.fillStyle = 'rgba(120, 120, 120, 0.6)'; - ctx.textBaseline = 'middle'; - ctx.textAlign = 'center'; - ctx.fillText('no grad data', x + width / 2, y + height / 2); - return; - } - - const { layers, status } = this._flow; - const n = layers.length; - if (n === 0) return; - - // Background - ctx.fillStyle = 'rgba(20, 20, 20, 0.6)'; - ctx.fillRect(x, y, width, height); - - const labelHeight = 12; - const barAreaHeight = height - labelHeight - 2; - const barWidth = Math.floor((width - 4) / n); - const startX = x + 2 + (width - 4 - barWidth * n) / 2; - - for (let i = 0; i < n; i++) { - const layer = layers[i]; - const barH = Math.max(1, layer.relativeFlow * barAreaHeight); - const bx = startX + i * barWidth; - const by = y + barAreaHeight - barH + 1; - - // Color based on per-layer health - let color; - if (status === 'converged') { - color = COLOR_CONVERGED; - } else if (layer.relativeFlow < 0.15 && i > 0) { - color = COLOR_DANGER; // vanishing at this layer - } else if (layer.relativeFlow > 0.85 && i === n - 1 && n > 1) { - color = COLOR_WARNING; // potential explosion - } else { - color = COLOR_HEALTHY; - } - - ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 0.75)`; - ctx.fillRect(bx + 1, by, barWidth - 2, barH); - - // Layer label - ctx.font = '7px monospace'; - ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, 0.7)`; - ctx.textBaseline = 'top'; - ctx.textAlign = 'center'; - ctx.fillText(this._layerLabels[i], bx + barWidth / 2, y + barAreaHeight + 2); - } - - // Status label at top - ctx.font = '8px monospace'; - ctx.textBaseline = 'top'; - ctx.textAlign = 'left'; - - let statusColor; - if (status === 'healthy') statusColor = COLOR_HEALTHY; - else if (status === 'vanishing') statusColor = COLOR_DANGER; - else if (status === 'exploding') statusColor = COLOR_WARNING; - else statusColor = COLOR_CONVERGED; - - ctx.fillStyle = `rgba(${statusColor.r}, ${statusColor.g}, ${statusColor.b}, 0.8)`; - ctx.fillText(`G: ${status}`, x + 2, y + 1); - } - - // ----------------------------------------------------------------------- - // Internal - // ----------------------------------------------------------------------- - - _computeFlow() { - const before = this._beforeWeights; - const after = this._afterWeights; - - if (!before || !after || before.length !== after.length) { - this._flow = null; - return; - } - - const layers = []; - const norms = []; - - for (let li = 0; li < this._numLayers; li++) { - const offset = this._layerOffsets[li]; - const count = this._layerWeightCounts[li]; - - // Compute L2 norm of weight delta for this layer - let sumSq = 0; - for (let j = 0; j < count; j++) { - const idx = offset + j; - if (idx < before.length && idx < after.length) { - const delta = after[idx] - before[idx]; - sumSq += delta * delta; - } - } - const norm = Math.sqrt(sumSq); - norms.push(norm); - - layers.push({ - name: `${this._layerLabels[li]} (${this._layerSizes[li]}\u2192${this._layerSizes[li + 1]})`, - gradientNorm: norm, - relativeFlow: 0, // computed below - }); - } - - // Compute relative flow (normalize to max) - const maxNorm = Math.max(...norms, 1e-12); - for (let i = 0; i < layers.length; i++) { - layers[i].relativeFlow = norms[i] / maxNorm; - } - - // Detect status - let status = 'healthy'; - - // Check convergence: all norms very small - if (norms.every(n => n < CONVERGED_THRESHOLD)) { - status = 'converged'; - } else if (norms.length >= 2) { - // Check vanishing: each successive layer < VANISHING_RATIO of previous - let vanishing = true; - let exploding = true; - - for (let i = 1; i < norms.length; i++) { - const prev = norms[i - 1]; - const curr = norms[i]; - - if (prev <= CONVERGED_THRESHOLD) { - // Can't assess ratio with near-zero denominator - vanishing = false; - exploding = false; - break; - } - - const ratio = curr / prev; - if (ratio >= VANISHING_RATIO) vanishing = false; - if (ratio <= EXPLODING_RATIO) exploding = false; - } - - if (vanishing) status = 'vanishing'; - else if (exploding) status = 'exploding'; - } - - this._flow = { layers, status }; - } -} diff --git a/playground/js/ui/hand-tracker.js b/playground/js/ui/hand-tracker.js deleted file mode 100644 index f0fa81e..0000000 --- a/playground/js/ui/hand-tracker.js +++ /dev/null @@ -1,530 +0,0 @@ -// HandTracker — MediaPipe hand tracking input for NISPS playground -// Extracts 14 derived features from right hand, gesture recognition from left hand - -// MediaPipe landmark indices -const WRIST = 0; -const THUMB_CMC = 1, THUMB_MCP = 2, THUMB_IP = 3, THUMB_TIP = 4; -const INDEX_MCP = 5, INDEX_PIP = 6, INDEX_DIP = 7, INDEX_TIP = 8; -const MIDDLE_MCP = 9, MIDDLE_PIP = 10, MIDDLE_DIP = 11, MIDDLE_TIP = 12; -const RING_MCP = 13, RING_PIP = 14, RING_DIP = 15, RING_TIP = 16; -const PINKY_MCP = 17, PINKY_PIP = 18, PINKY_DIP = 19, PINKY_TIP = 20; - -const FINGER_LANDMARKS = [ - [THUMB_CMC, THUMB_MCP, THUMB_IP, THUMB_TIP], - [INDEX_MCP, INDEX_PIP, INDEX_DIP, INDEX_TIP], - [MIDDLE_MCP, MIDDLE_PIP, MIDDLE_DIP, MIDDLE_TIP], - [RING_MCP, RING_PIP, RING_DIP, RING_TIP], - [PINKY_MCP, PINKY_PIP, PINKY_DIP, PINKY_TIP], -]; - -const FINGER_TIPS = [THUMB_TIP, INDEX_TIP, MIDDLE_TIP, RING_TIP, PINKY_TIP]; -const FINGER_MCPS = [THUMB_MCP, INDEX_MCP, MIDDLE_MCP, RING_MCP, PINKY_MCP]; - -// Hand connections for skeleton drawing -const HAND_CONNECTIONS = [ - [0,1],[1,2],[2,3],[3,4], - [0,5],[5,6],[6,7],[7,8], - [0,9],[9,10],[10,11],[11,12], - [0,13],[13,14],[14,15],[15,16], - [0,17],[17,18],[18,19],[19,20], - [5,9],[9,13],[13,17], -]; - -// Default tuning parameters (exported for dev panel) -export const HAND_TRACKER_DEFAULTS = { - minHandDetectionConfidence: 0.5, - minHandPresenceConfidence: 0.5, - minTrackingConfidence: 0.5, - smoothingFactor: 0.4, - gestureHoldMs: 400, - useWorldLandmarks: false, -}; - -export class HandTracker { - /** - * @param {Object} options - * @param {function(number[])} options.onTrackingInput - called with 14 derived features [0,1] - * @param {function('thumbsup'|'thumbsdown')} options.onGesture - called when gesture confirmed - * @param {function(boolean)} [options.onConnectionChange] - called when tracking starts/stops - * @param {HTMLVideoElement} options.videoElement - video element for camera feed - * @param {HTMLCanvasElement} options.overlayCanvas - canvas for skeleton drawing - */ - constructor(options = {}) { - this.onTrackingInput = options.onTrackingInput || (() => {}); - this.onGesture = options.onGesture || (() => {}); - this.onConnectionChange = options.onConnectionChange || null; - this.videoElement = options.videoElement; - this.overlayCanvas = options.overlayCanvas; - this.overlayCtx = this.overlayCanvas?.getContext('2d'); - - this.active = false; - this.features = new Array(14).fill(0.5); - this._handLandmarker = null; - this._stream = null; - this._rafId = null; - this._lastDetectTime = 0; - this._minDetectInterval = 33; // ~30fps, will increase if slow - - // Tuning parameters (runtime-adjustable via setOptions) - this.opts = { ...HAND_TRACKER_DEFAULTS }; - - // Gesture state - this._gestureCandidate = null; // 'thumbsup' | 'thumbsdown' | null - this._gestureStartTime = 0; - this._gestureProgress = 0; // 0-1 for UI - this._lastGestureFired = 0; - - // Smoothing - this._smoothedFeatures = new Array(14).fill(0.5); - - // Status - this._trackingRight = false; - this._trackingLeft = false; - } - - /** - * Update tuning parameters at runtime. - * Confidence changes require re-creating the HandLandmarker (async). - */ - async setOptions(patch) { - const prev = { ...this.opts }; - Object.assign(this.opts, patch); - - // Check if MediaPipe confidence thresholds changed — requires re-init - const confidenceChanged = - prev.minHandDetectionConfidence !== this.opts.minHandDetectionConfidence || - prev.minHandPresenceConfidence !== this.opts.minHandPresenceConfidence || - prev.minTrackingConfidence !== this.opts.minTrackingConfidence; - - if (confidenceChanged && this._handLandmarker) { - await this._handLandmarker.setOptions({ - minHandDetectionConfidence: this.opts.minHandDetectionConfidence, - minHandPresenceConfidence: this.opts.minHandPresenceConfidence, - minTrackingConfidence: this.opts.minTrackingConfidence, - }); - console.log('[HandTracker] Updated confidence thresholds:', this.opts); - } - } - - get gestureProgress() { return this._gestureProgress; } - get gestureCandidate() { return this._gestureCandidate; } - get trackingRight() { return this._trackingRight; } - get trackingLeft() { return this._trackingLeft; } - - async start() { - if (this.active) return; - - try { - // Request camera - this._stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } } - }); - this.videoElement.srcObject = this._stream; - await this.videoElement.play(); - - // Load MediaPipe (only once) - if (!this._handLandmarker) { - await this._initHandLandmarker(); - } - - this.active = true; - if (this.onConnectionChange) this.onConnectionChange(true); - this._detectLoop(); - } catch (e) { - console.error('[HandTracker] Failed to start:', e); - this.stop(); - throw e; - } - } - - stop() { - this.active = false; - if (this._rafId) { - cancelAnimationFrame(this._rafId); - this._rafId = null; - } - if (this._stream) { - for (const track of this._stream.getTracks()) track.stop(); - this._stream = null; - } - this.videoElement.srcObject = null; - this._trackingRight = false; - this._trackingLeft = false; - if (this.onConnectionChange) this.onConnectionChange(false); - } - - destroy() { - this.stop(); - if (this._handLandmarker) { - this._handLandmarker.close(); - this._handLandmarker = null; - } - } - - async _initHandLandmarker() { - // Dynamic import of MediaPipe vision tasks - const vision = await import('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/vision_bundle.mjs'); - const { HandLandmarker, FilesetResolver } = vision; - - const wasmFileset = await FilesetResolver.forVisionTasks( - 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm' - ); - - this._handLandmarker = await HandLandmarker.createFromOptions(wasmFileset, { - baseOptions: { - modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task', - delegate: 'GPU', - }, - runningMode: 'VIDEO', - numHands: 2, - minHandDetectionConfidence: this.opts.minHandDetectionConfidence, - minHandPresenceConfidence: this.opts.minHandPresenceConfidence, - minTrackingConfidence: this.opts.minTrackingConfidence, - }); - } - - _detectLoop() { - if (!this.active) return; - - const now = performance.now(); - if (now - this._lastDetectTime >= this._minDetectInterval) { - const frameStart = now; - - if (this.videoElement.readyState >= 2 && this._handLandmarker) { - const results = this._handLandmarker.detectForVideo(this.videoElement, now); - this._processResults(results, now); - } - - // Adaptive frame rate: slow down if detection is heavy, recover gradually - const elapsed = performance.now() - frameStart; - if (elapsed > 25) { - this._minDetectInterval = 66; // drop to 15fps - } else if (this._minDetectInterval > 33) { - this._minDetectInterval = 33; // recover to 30fps - } - } - - this._rafId = requestAnimationFrame(() => this._detectLoop()); - } - - _processResults(results, now) { - // Set canvas to a fixed size matching the PIP aspect ratio - if (this.overlayCtx) { - this.overlayCanvas.width = 360; - this.overlayCanvas.height = 270; - this._drawBackground(); - } - - let rightHand = null; - let leftHand = null; - let rightHandNorm = null; // always normalized (for drawing) - let leftHandNorm = null; - - // Classify hands - if (results.handednesses && results.landmarks) { - // Choose landmark source: world (meters, hand-centric) or normalized (image-relative) - const useWorld = this.opts.useWorldLandmarks && results.worldLandmarks; - const lmSource = useWorld ? results.worldLandmarks : results.landmarks; - - for (let i = 0; i < results.handednesses.length; i++) { - const handedness = results.handednesses[i][0]; - const landmarks = lmSource[i]; - const normLandmarks = results.landmarks[i]; // always keep normalized for drawing - if (handedness.categoryName === 'Right') { - rightHand = landmarks; - rightHandNorm = normLandmarks; - } else { - leftHand = landmarks; - leftHandNorm = normLandmarks; - } - } - } - - // If only one hand detected, use it as tracking hand - if (!rightHand && leftHand) { - rightHand = leftHand; - rightHandNorm = leftHandNorm; - leftHand = null; - leftHandNorm = null; - } - - this._trackingRight = !!rightHand; - this._trackingLeft = !!leftHand; - - // Extract features from tracking hand (right) - if (rightHand) { - const raw = this._extractFeatures(rightHand); - // Smooth features - const sf = this.opts.smoothingFactor; - for (let i = 0; i < 14; i++) { - this._smoothedFeatures[i] += (raw[i] - this._smoothedFeatures[i]) * sf; - this.features[i] = this._smoothedFeatures[i]; - } - this.onTrackingInput(this.features); - } - - // Gesture recognition from left hand (uses normalized landmarks for finger counting) - if (leftHandNorm) { - this._processGesture(leftHandNorm, now); - } else { - this._gestureCandidate = null; - this._gestureProgress = 0; - } - - // Draw skeletons with zone awareness (always use normalized landmarks for drawing) - // In canvas (pre-CSS-mirror) coords: right 1/3 = gesture zone, left 2/3 = tracking zone - // After CSS scaleX(-1): left 1/3 = gesture, right 2/3 = tracking - if (this.overlayCtx) { - const w = this.overlayCanvas.width; - const dividerX = w * (2 / 3); - - if (rightHandNorm) { - const avgX = rightHandNorm[WRIST].x * w; - const crossingZone = avgX > dividerX; - this._drawSkeleton(rightHandNorm, '#ff6a00', crossingZone ? 0.25 : 0.9); - } - if (leftHandNorm) { - const avgX = leftHandNorm[WRIST].x * w; - const crossingZone = avgX < dividerX; - this._drawSkeleton(leftHandNorm, '#00ccff', crossingZone ? 0.25 : 0.9); - } - } - } - - _extractFeatures(landmarks) { - const f = new Array(14); - const isWorld = this.opts.useWorldLandmarks; - - // 0-1: Palm position X, Y - // For world landmarks, x/y are in meters centered on hand — normalize differently - if (isWorld) { - // World coords: origin at hand center, range roughly ±0.1m - f[0] = clamp01((landmarks[WRIST].x + 0.1) / 0.2); - f[1] = clamp01((landmarks[WRIST].y + 0.1) / 0.2); - } else { - f[0] = 1.0 - landmarks[WRIST].x; // mirror X - f[1] = landmarks[WRIST].y; - } - - // 2-6: Finger curl (thumb through pinky) - for (let fi = 0; fi < 5; fi++) { - f[2 + fi] = this._fingerCurl(landmarks, fi); - } - - // 7-10: Finger spread (4 adjacent pairs) - for (let fi = 0; fi < 4; fi++) { - f[7 + fi] = this._fingerSpread(landmarks, fi); - } - - // 11: Hand roll (rotation around forward axis) - const wrist = landmarks[WRIST]; - const middleMcp = landmarks[MIDDLE_MCP]; - const dx = middleMcp.x - wrist.x; - const dy = middleMcp.y - wrist.y; - const roll = (Math.atan2(dx, -dy) / Math.PI + 1) * 0.5; - f[11] = clamp01(roll); - - // 12: Hand pitch (tilt forward/back from z-depth difference) - const avgTipZ = (landmarks[INDEX_TIP].z + landmarks[MIDDLE_TIP].z + landmarks[RING_TIP].z) / 3; - if (isWorld) { - // World z is in meters — typical pitch range ~±0.05m - f[12] = clamp01((wrist.z - avgTipZ + 0.05) / 0.1); - } else { - f[12] = clamp01((wrist.z - avgTipZ + 0.15) / 0.3); - } - - // 13: Pinch distance (thumb tip to index tip) - const pinch = dist3d(landmarks[THUMB_TIP], landmarks[INDEX_TIP]); - if (isWorld) { - // World pinch: range 0–0.15m typically - f[13] = clamp01(1.0 - pinch / 0.15); - } else { - f[13] = clamp01(1.0 - pinch / 0.3); - } - - return f; - } - - _fingerCurl(landmarks, fingerIndex) { - const joints = FINGER_LANDMARKS[fingerIndex]; - // Angle at PIP joint (middle joint) - const a = landmarks[joints[0]]; // MCP/CMC - const b = landmarks[joints[1]]; // MCP/PIP - const c = landmarks[joints[2]]; // PIP/DIP - const d = landmarks[joints[3]]; // DIP/TIP - - // Use angle between base→mid and mid→tip vectors - const v1x = b.x - a.x, v1y = b.y - a.y, v1z = b.z - a.z; - const v2x = d.x - b.x, v2y = d.y - b.y, v2z = d.z - b.z; - - const dot = v1x * v2x + v1y * v2y + v1z * v2z; - const mag1 = Math.sqrt(v1x * v1x + v1y * v1y + v1z * v1z) || 0.001; - const mag2 = Math.sqrt(v2x * v2x + v2y * v2y + v2z * v2z) || 0.001; - - const cosAngle = clamp(dot / (mag1 * mag2), -1, 1); - const angle = Math.acos(cosAngle); // 0 = straight, PI = fully bent - - // Also consider distance from tip to MCP (more robust) - const tipDist = dist3d(landmarks[joints[0]], landmarks[joints[3]]); - const baseDist = dist3d(landmarks[joints[0]], landmarks[joints[2]]); - const ratio = baseDist > 0.001 ? tipDist / (baseDist * 1.8) : 1; - - // Blend angle-based and distance-based curl - const angleCurl = clamp01(1.0 - angle / Math.PI); - const distCurl = clamp01(1.0 - ratio); - - return clamp01(angleCurl * 0.4 + distCurl * 0.6); - } - - _fingerSpread(landmarks, pairIndex) { - // Spread between adjacent finger tips - const tip1 = landmarks[FINGER_TIPS[pairIndex]]; - const tip2 = landmarks[FINGER_TIPS[pairIndex + 1]]; - const mcp1 = landmarks[FINGER_MCPS[pairIndex]]; - const mcp2 = landmarks[FINGER_MCPS[pairIndex + 1]]; - - // Direction vectors from MCP to tip - const v1x = tip1.x - mcp1.x, v1y = tip1.y - mcp1.y; - const v2x = tip2.x - mcp2.x, v2y = tip2.y - mcp2.y; - - const dot = v1x * v2x + v1y * v2y; - const mag1 = Math.sqrt(v1x * v1x + v1y * v1y) || 0.001; - const mag2 = Math.sqrt(v2x * v2x + v2y * v2y) || 0.001; - - const cosAngle = clamp(dot / (mag1 * mag2), -1, 1); - const angle = Math.acos(cosAngle); // 0 = parallel, larger = more spread - - // Normalize: typical spread is 0-0.5 radians - return clamp01(angle / 0.6); - } - - _processGesture(landmarks, now) { - const extended = this._countExtendedFingers(landmarks); - - let candidate = null; - if (extended === 1) candidate = 'thumbsup'; - else if (extended === 2) candidate = 'thumbsdown'; - - if (candidate !== this._gestureCandidate) { - // New gesture or cleared - this._gestureCandidate = candidate; - this._gestureStartTime = now; - this._gestureProgress = 0; - } else if (candidate) { - // Same gesture continuing - const elapsed = now - this._gestureStartTime; - this._gestureProgress = Math.min(elapsed / this.opts.gestureHoldMs, 1); - - if (this._gestureProgress >= 1 && now - this._lastGestureFired > 800) { - // Fire gesture - this.onGesture(candidate); - this._lastGestureFired = now; - this._gestureCandidate = null; - this._gestureProgress = 0; - } - } - } - - _countExtendedFingers(landmarks) { - let count = 0; - - // Thumb: check if tip is far from palm center (different axis) - const thumbExtended = dist3d(landmarks[THUMB_TIP], landmarks[THUMB_MCP]) > - dist3d(landmarks[THUMB_IP], landmarks[THUMB_MCP]) * 1.2; - - // Other fingers: tip should be farther from wrist than PIP - for (let fi = 1; fi < 5; fi++) { - const joints = FINGER_LANDMARKS[fi]; - const tipToWrist = dist3d(landmarks[joints[3]], landmarks[WRIST]); - const pipToWrist = dist3d(landmarks[joints[1]], landmarks[WRIST]); - if (tipToWrist > pipToWrist * 1.05) count++; - } - - // Don't count thumb for gesture (only counting index, middle, ring, pinky) - return count; - } - - _drawBackground() { - const ctx = this.overlayCtx; - const w = this.overlayCanvas.width; - const h = this.overlayCanvas.height; - - // Dark background - ctx.fillStyle = '#0a0a0a'; - ctx.fillRect(0, 0, w, h); - - // Zone backgrounds (subtle tint) - // In canvas coords (pre-CSS-mirror): left 2/3 = tracking (right hand), right 1/3 = gesture (left hand) - const dividerX = w * (2 / 3); - - // Tracking zone — very subtle warm tint - ctx.fillStyle = 'rgba(255, 106, 0, 0.03)'; - ctx.fillRect(0, 0, dividerX, h); - - // Gesture zone — very subtle cool tint - ctx.fillStyle = 'rgba(0, 204, 255, 0.03)'; - ctx.fillRect(dividerX, 0, w - dividerX, h); - - // Dashed divider line - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.lineWidth = 1; - ctx.setLineDash([4, 4]); - ctx.beginPath(); - ctx.moveTo(dividerX, 0); - ctx.lineTo(dividerX, h); - ctx.stroke(); - ctx.setLineDash([]); - - // Zone labels (drawn in canvas coords, CSS mirror flips them) - ctx.font = '9px monospace'; - ctx.textAlign = 'center'; - - // Tracking label (left 2/3 of canvas → right 2/3 of display) - ctx.fillStyle = 'rgba(255, 106, 0, 0.3)'; - ctx.fillText('TRACKING', dividerX / 2, 12); - - // Gesture label (right 1/3 of canvas → left 1/3 of display) - ctx.fillStyle = 'rgba(0, 204, 255, 0.3)'; - ctx.fillText('GESTURE', dividerX + (w - dividerX) / 2, 12); - - ctx.textAlign = 'start'; // reset - } - - _drawSkeleton(landmarks, color, opacity) { - const ctx = this.overlayCtx; - const w = this.overlayCanvas.width; - const h = this.overlayCanvas.height; - - // Draw connections - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.globalAlpha = opacity * 0.8; - - for (const [a, b] of HAND_CONNECTIONS) { - const la = landmarks[a], lb = landmarks[b]; - ctx.beginPath(); - ctx.moveTo(la.x * w, la.y * h); - ctx.lineTo(lb.x * w, lb.y * h); - ctx.stroke(); - } - - // Draw landmarks - ctx.fillStyle = color; - ctx.globalAlpha = opacity; - for (const lm of landmarks) { - ctx.beginPath(); - ctx.arc(lm.x * w, lm.y * h, 3, 0, Math.PI * 2); - ctx.fill(); - } - - ctx.globalAlpha = 1; - } -} - -// --- Utility --- -function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } -function clamp01(v) { return clamp(v, 0, 1); } -function dist3d(a, b) { - const dx = a.x - b.x, dy = a.y - b.y, dz = (a.z || 0) - (b.z || 0); - return Math.sqrt(dx * dx + dy * dy + dz * dz); -} diff --git a/playground/js/ui/input-heatmap.js b/playground/js/ui/input-heatmap.js deleted file mode 100644 index 13d2bbf..0000000 --- a/playground/js/ui/input-heatmap.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Input Space Heatmap — 2D color field on the joy-map showing what the - * network produces across the entire input space. - * - * Samples the MLP at a grid of input points, reduces the output vector - * to a color, and renders as a background layer on the joy-map canvas. - * - * Three color modes: - * - luminance: mean output -> brightness (shows "loud" vs "quiet" regions) - * - variance: output variance -> saturation (shows "interesting" vs "flat") - * - divergence: difference from center point output (how each region diverges) - * - * Performance: 16x16 = 256 inferences. When inferBatchFn is provided, - * all points are evaluated in a single WASM call. Falls back to per-point - * inferFn (~20us each = ~5ms). Throttled to max 5 updates/sec by default. - * - * @module input-heatmap - */ - -// ---- Constants ---- -const DEFAULT_RESOLUTION = 16; -const MIN_RESOLUTION = 4; -const MAX_RESOLUTION = 32; -const DEFAULT_THROTTLE = 200; // ms -const COLOR_MODES = ['luminance', 'variance', 'divergence']; - -// ---- Color palette ---- -// Dark-to-warm gradient: dark blue/purple -> amber -> white -// Pre-computed as HSL stops for fast interpolation -const PALETTE = [ - { h: 260, s: 60, l: 8 }, // 0.0 — very dark purple - { h: 250, s: 65, l: 18 }, // 0.2 — deep blue-purple - { h: 220, s: 55, l: 30 }, // 0.4 — medium blue - { h: 35, s: 80, l: 45 }, // 0.6 — warm amber - { h: 38, s: 90, l: 60 }, // 0.8 — bright amber - { h: 42, s: 95, l: 85 }, // 1.0 — near-white warm -]; - -function samplePalette(t) { - const clamped = Math.max(0, Math.min(1, t)); - const idx = clamped * (PALETTE.length - 1); - const lo = Math.floor(idx); - const hi = Math.min(lo + 1, PALETTE.length - 1); - const frac = idx - lo; - - const a = PALETTE[lo]; - const b = PALETTE[hi]; - const h = a.h + (b.h - a.h) * frac; - const s = a.s + (b.s - a.s) * frac; - const l = a.l + (b.l - a.l) * frac; - return { h, s, l }; -} - -function hslToRGB(h, s, l) { - s /= 100; - l /= 100; - const c = (1 - Math.abs(2 * l - 1)) * s; - const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); - const m = l - c / 2; - let r, g, b; - if (h < 60) { r = c; g = x; b = 0; } - else if (h < 120) { r = x; g = c; b = 0; } - else if (h < 180) { r = 0; g = c; b = x; } - else if (h < 240) { r = 0; g = x; b = c; } - else if (h < 300) { r = x; g = 0; b = c; } - else { r = c; g = 0; b = x; } - return { - r: Math.round((r + m) * 255), - g: Math.round((g + m) * 255), - b: Math.round((b + m) * 255), - }; -} - -export class InputHeatmap { - /** - * @param {object} [options] - * @param {number} [options.resolution=16] - grid points per axis - * @param {string} [options.colorMode='luminance'] - 'luminance'|'variance'|'divergence' - * @param {number} [options.throttle=200] - min ms between recomputes - * @param {number} [options.opacity=0.55] - heatmap alpha - */ - constructor(options = {}) { - this._resolution = clamp(options.resolution ?? DEFAULT_RESOLUTION, MIN_RESOLUTION, MAX_RESOLUTION); - this._colorMode = COLOR_MODES.includes(options.colorMode) ? options.colorMode : 'luminance'; - this._throttle = Math.max(50, options.throttle ?? DEFAULT_THROTTLE); - this._opacity = Math.max(0, Math.min(1, options.opacity ?? 0.55)); - this._enabled = false; - this._lastUpdate = 0; - - // Cached heatmap data: Float32Array of reduced values (resolution x resolution) - this._grid = null; // raw reduced values per cell - this._gridWindow = null; // zoom window used when computing this grid - this._imageData = null; // cached ImageData for rendering - this._offscreen = null; // offscreen canvas for compositing - this._offscreenCtx = null; - - // Center-point output cache (for divergence mode) - this._centerOutput = null; - } - - // ---- Public API ---- - - /** - * Recompute the heatmap. Call on weight changes (train, randomize, moveWeights). - * - * @param {function} inferFn - (inputArray: number[]) => number[] - * Runs inference for a given 2D input. Must NOT corrupt the main inference state. - * @param {object} [options] - * @param {object} [options.zoomWindow] - { x1, y1, x2, y2 } in [0,1] space - * @param {number} [options.resolution] - override resolution for this update - * @param {function} [options.inferBatchFn] - (inputPoints: number[][]) => number[][] - * Batch inference: takes array of [x,y] pairs, returns array of output arrays. - * When provided, used instead of per-point inferFn for better performance. - */ - update(inferFn, options = {}) { - if (!this._enabled) return; - - const now = performance.now(); - if (now - this._lastUpdate < this._throttle) return; - this._lastUpdate = now; - - const res = clamp(options.resolution ?? this._resolution, MIN_RESOLUTION, MAX_RESOLUTION); - const zw = options.zoomWindow || { x1: 0, y1: 0, x2: 1, y2: 1 }; - const inferBatchFn = options.inferBatchFn || null; - - // Sample grid - const grid = new Float32Array(res * res); - let outputs; - - if (inferBatchFn) { - // ---- Batch path: build all input points, call once ---- - const needsCenter = this._colorMode === 'divergence'; - const points = []; - - // If divergence mode, first point is the center - if (needsCenter) { - const cx = (zw.x1 + zw.x2) / 2; - const cy = (zw.y1 + zw.y2) / 2; - points.push([cx, cy]); - } - - // Grid points - for (let gy = 0; gy < res; gy++) { - for (let gx = 0; gx < res; gx++) { - const inputX = zw.x1 + (gx + 0.5) / res * (zw.x2 - zw.x1); - const inputY = zw.y1 + (gy + 0.5) / res * (zw.y2 - zw.y1); - points.push([inputX, inputY]); - } - } - - const allOutputs = inferBatchFn(points); - - if (needsCenter) { - this._centerOutput = allOutputs[0]; - outputs = allOutputs.slice(1); - } else { - outputs = allOutputs; - } - } else { - // ---- Per-point fallback path ---- - outputs = []; - - // Pre-compute center output for divergence mode - if (this._colorMode === 'divergence') { - const cx = (zw.x1 + zw.x2) / 2; - const cy = (zw.y1 + zw.y2) / 2; - this._centerOutput = inferFn([cx, cy]); - } - - // Collect all outputs for normalization - for (let gy = 0; gy < res; gy++) { - for (let gx = 0; gx < res; gx++) { - const inputX = zw.x1 + (gx + 0.5) / res * (zw.x2 - zw.x1); - const inputY = zw.y1 + (gy + 0.5) / res * (zw.y2 - zw.y1); - const out = inferFn([inputX, inputY]); - outputs.push(out); - } - } - } - - // Reduce outputs to scalar values based on color mode - let minVal = Infinity; - let maxVal = -Infinity; - - for (let i = 0; i < outputs.length; i++) { - const val = this._reduceOutput(outputs[i]); - grid[i] = val; - if (val < minVal) minVal = val; - if (val > maxVal) maxVal = val; - } - - // Normalize to [0,1] - const range = maxVal - minVal; - if (range > 1e-8) { - for (let i = 0; i < grid.length; i++) { - grid[i] = (grid[i] - minVal) / range; - } - } else { - grid.fill(0.5); - } - - this._grid = grid; - this._gridWindow = { ...zw }; - this._gridRes = res; - - // Build ImageData - this._buildImageData(res); - } - - /** - * Draw the heatmap onto a canvas context as a background layer. - * Should be called before other joy-map layers. - * - * @param {CanvasRenderingContext2D} ctx - * @param {number} canvasWidth - CSS pixel width - * @param {number} canvasHeight - CSS pixel height - * @param {object} [zoomWindow] - current zoom window { x1, y1, x2, y2 } - */ - draw(ctx, canvasWidth, canvasHeight, zoomWindow) { - if (!this._enabled || !this._imageData) return; - - const res = this._gridRes; - if (!this._offscreen || this._offscreen.width !== res || this._offscreen.height !== res) { - this._offscreen = new OffscreenCanvas(res, res); - this._offscreenCtx = this._offscreen.getContext('2d'); - } - - this._offscreenCtx.putImageData(this._imageData, 0, 0); - - // Determine draw rect: if the heatmap was computed for a zoom window, - // draw it into that region of the canvas - const gw = this._gridWindow || { x1: 0, y1: 0, x2: 1, y2: 1 }; - - // Canvas Y is inverted (y=0 is top, but our y1 is bottom of input space) - const dx = gw.x1 * canvasWidth; - const dy = (1 - gw.y2) * canvasHeight; - const dw = (gw.x2 - gw.x1) * canvasWidth; - const dh = (gw.y2 - gw.y1) * canvasHeight; - - ctx.save(); - ctx.globalAlpha = this._opacity; - // Use bilinear interpolation for smooth gradients - ctx.imageSmoothingEnabled = true; - ctx.imageSmoothingQuality = 'low'; - ctx.drawImage(this._offscreen, dx, dy, dw, dh); - ctx.restore(); - } - - // ---- Color mode ---- - - /** - * Set the color reduction mode. - * @param {string} mode - 'luminance' | 'variance' | 'divergence' - */ - setColorMode(mode) { - if (!COLOR_MODES.includes(mode)) return; - this._colorMode = mode; - // Invalidate cache so next update recomputes - this._grid = null; - this._imageData = null; - } - - /** @returns {string} */ - getColorMode() { return this._colorMode; } - - /** - * Cycle to next color mode. - * @returns {string} the new mode - */ - cycleColorMode() { - const idx = COLOR_MODES.indexOf(this._colorMode); - const next = COLOR_MODES[(idx + 1) % COLOR_MODES.length]; - this.setColorMode(next); - return next; - } - - // ---- Toggle ---- - - /** @param {boolean} enabled */ - setEnabled(enabled) { - this._enabled = !!enabled; - if (!this._enabled) { - this._grid = null; - this._imageData = null; - } - } - - /** @returns {boolean} */ - get enabled() { return this._enabled; } - - // ---- Configuration ---- - - /** @param {number} ms - minimum time between recomputes */ - setThrottle(ms) { - this._throttle = Math.max(50, ms); - } - - /** @param {number} res - grid points per axis (4-32) */ - setResolution(res) { - this._resolution = clamp(res, MIN_RESOLUTION, MAX_RESOLUTION); - } - - /** @returns {number} */ - getResolution() { return this._resolution; } - - /** @param {number} alpha - 0-1 */ - setOpacity(alpha) { - this._opacity = Math.max(0, Math.min(1, alpha)); - } - - /** - * Force a recompute on next update() call (clears throttle timer). - */ - invalidate() { - this._lastUpdate = 0; - } - - // ---- Serialization ---- - - getConfig() { - return { - enabled: this._enabled, - resolution: this._resolution, - colorMode: this._colorMode, - throttle: this._throttle, - opacity: this._opacity, - }; - } - - setConfig(config) { - if (config.enabled != null) this.setEnabled(config.enabled); - if (config.resolution != null) this.setResolution(config.resolution); - if (config.colorMode != null) this.setColorMode(config.colorMode); - if (config.throttle != null) this.setThrottle(config.throttle); - if (config.opacity != null) this.setOpacity(config.opacity); - } - - // ---- Internal ---- - - /** - * Reduce an output vector to a single scalar based on color mode. - */ - _reduceOutput(output) { - switch (this._colorMode) { - case 'luminance': - return this._meanOutput(output); - case 'variance': - return this._varianceOutput(output); - case 'divergence': - return this._divergenceOutput(output); - default: - return this._meanOutput(output); - } - } - - /** Mean of all outputs. */ - _meanOutput(output) { - let sum = 0; - for (let i = 0; i < output.length; i++) sum += output[i]; - return sum / output.length; - } - - /** Variance of outputs (how "interesting" / spread out the values are). */ - _varianceOutput(output) { - const mean = this._meanOutput(output); - let sumSq = 0; - for (let i = 0; i < output.length; i++) { - const d = output[i] - mean; - sumSq += d * d; - } - return sumSq / output.length; - } - - /** Euclidean distance from center-point output (normalized by dimension). */ - _divergenceOutput(output) { - if (!this._centerOutput || this._centerOutput.length !== output.length) { - return this._meanOutput(output); - } - let sumSq = 0; - for (let i = 0; i < output.length; i++) { - const d = output[i] - this._centerOutput[i]; - sumSq += d * d; - } - // Normalize: max possible distance for [0,1] outputs = sqrt(N) - return Math.sqrt(sumSq / output.length); - } - - /** - * Build an ImageData from the normalized grid values. - */ - _buildImageData(res) { - if (!this._grid) return; - - this._imageData = new ImageData(res, res); - const data = this._imageData.data; - - for (let gy = 0; gy < res; gy++) { - for (let gx = 0; gx < res; gx++) { - // Grid is stored bottom-to-top (y=0 is bottom of input space) - // ImageData is top-to-bottom, so flip Y - const gridIdx = (res - 1 - gy) * res + gx; - const val = this._grid[gridIdx]; - - const { h, s, l } = samplePalette(val); - const { r, g, b } = hslToRGB(h, s, l); - - const pixIdx = (gy * res + gx) * 4; - data[pixIdx + 0] = r; - data[pixIdx + 1] = g; - data[pixIdx + 2] = b; - data[pixIdx + 3] = 255; - } - } - } -} - -// ---- Utility ---- -function clamp(v, lo, hi) { - return v < lo ? lo : v > hi ? hi : v; -} diff --git a/playground/js/ui/input-pipeline.js b/playground/js/ui/input-pipeline.js deleted file mode 100644 index fc8d689..0000000 --- a/playground/js/ui/input-pipeline.js +++ /dev/null @@ -1,633 +0,0 @@ -/** - * Input Pipeline — data processing layer between physical input and the MLP. - * - * Pipeline stages (in order): - * 1. Deadzone — suppress jitter near center, remap to full [0,1] - * 2. Zoom — narrow effective input window around an anchor point - * 3. Input Curve — centered power curve (exponent) - * 4. Smoothing — exponential moving average (frame-rate-independent) - * 5. Momentum-as-zoom — movement speed modulates effective zoom - * - * Pure math module — no DOM dependencies. - * - * @module input-pipeline - */ - -// --------------------------------------------------------------------------- -// Default constants -// --------------------------------------------------------------------------- - -/** @type {number} Default zoom level (1.0 = full range, no zoom) */ -export const DEFAULT_ZOOM = 1.0; -/** @type {number} Minimum zoom before input is frozen */ -export const ZOOM_MIN = 0.01; -/** @type {number} Maximum zoom level */ -export const ZOOM_MAX = 1.0; -/** @type {number} Default deadzone (0 = off) */ -export const DEFAULT_DEADZONE = 0; -/** @type {number} Maximum allowed deadzone */ -export const DEADZONE_MAX = 0.4; -/** @type {number} Default input curve exponent (1.0 = linear) */ -export const DEFAULT_INPUT_CURVE = 1.0; -/** @type {number} Minimum input curve exponent */ -export const INPUT_CURVE_MIN = 0.2; -/** @type {number} Maximum input curve exponent */ -export const INPUT_CURVE_MAX = 5.0; -/** @type {number} Default smoothing factor (0 = no smoothing) */ -export const DEFAULT_SMOOTHING = 0; -/** @type {number} Maximum smoothing factor */ -export const SMOOTHING_MAX = 0.95; -/** @type {string} Default momentum-zoom mode */ -export const DEFAULT_MOMENTUM_ZOOM = 'off'; -/** @type {string} Default anchor mode */ -export const DEFAULT_ANCHOR_MODE = 'center'; -/** @type {number} Default velocity estimation window in ms */ -export const DEFAULT_VELOCITY_WINDOW = 150; - -/** @type {number} Freeze threshold — zoom at or below this value freezes input */ -const FREEZE_THRESHOLD = ZOOM_MIN; - -// Momentum-zoom presets: { factor, minZoomMul, maxZoomMul } -// factor controls how much velocity scales zoom (higher = more effect) -const MOMENTUM_PRESETS = { - off: null, - gentle: { factor: 0.6, minZoomMul: 0.3, maxZoomMul: 1.0 }, - strong: { factor: 1.5, minZoomMul: 0.15, maxZoomMul: 1.0 }, -}; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** Clamp value to [lo, hi]. */ -function clamp(v, lo, hi) { - return v < lo ? lo : v > hi ? hi : v; -} - -/** Sign-preserving centered power curve (spec 2.3). Input and output in [0,1]. */ -function centeredPowerCurve(input, exponent) { - if (exponent === 1) return input; - const offset = input - 0.5; - const sign = offset < 0 ? -1 : 1; - const shaped = sign * Math.pow(Math.abs(offset) * 2, exponent) / 2; - return shaped + 0.5; -} - -/** - * Apply deadzone then remap live zone to [0,1]. - * Deadzone is a fraction of half-travel from center (0.5). - * Returns value in [0,1]. - */ -function applyDeadzone(input, deadzone) { - if (deadzone <= 0) return input; - const offset = input - 0.5; // [-0.5, 0.5] - const absOff = Math.abs(offset); - const halfDz = deadzone * 0.5; // deadzone fraction of half-travel (0.5) - if (absOff <= halfDz) return 0.5; - const sign = offset < 0 ? -1 : 1; - // Remap [halfDz, 0.5] -> [0, 0.5] - const remapped = (absOff - halfDz) / (0.5 - halfDz) * 0.5; - return 0.5 + sign * remapped; -} - -/** - * Apply zoom around anchor for a single axis. - * effective = anchor + (raw - 0.5) * zoomLevel, clamped [0,1] - */ -function applyZoom(input, anchor, zoomLevel) { - return clamp(anchor + (input - 0.5) * zoomLevel, 0, 1); -} - -/** - * Frame-rate-independent EMA. - * Converts a per-frame smoothing factor into a time-domain factor so - * the perceived smoothing is consistent regardless of frame rate. - * - * Reference frame rate is 60 fps (≈16.67 ms). - */ -function emaSmooth(prev, raw, smoothing, dt) { - if (smoothing <= 0) return raw; - // Convert from "per-frame at 60fps" to time-constant-based - const refDt = 1 / 60; - const effectiveDt = dt > 0 ? dt : refDt; - // alpha is the fraction of the new value to blend in per reference frame - // For frame-rate independence: alpha_eff = 1 - (1 - alpha)^(dt/refDt) - const alpha = 1 - smoothing; // per-reference-frame new-value weight - const alphaEff = 1 - Math.pow(1 - alpha, effectiveDt / refDt); - return prev + alphaEff * (raw - prev); -} - -// --------------------------------------------------------------------------- -// InputPipeline -// --------------------------------------------------------------------------- - -/** - * Configurable input processing pipeline. - * - * Transforms raw 2D input (e.g. joystick X/Y in [0,1]) through deadzone, - * zoom, curve shaping, smoothing, and optional momentum-zoom, producing a - * processed {x, y, frozen} result suitable for feeding into the MLP. - */ -export class InputPipeline { - /** - * @param {object} [config] — initial configuration (all optional) - * @param {number} [config.zoom=1.0] - * @param {number} [config.zoomX] per-axis zoom override - * @param {number} [config.zoomY] per-axis zoom override - * @param {number} [config.deadzone=0] - * @param {number} [config.inputCurve=1.0] - * @param {number} [config.inputCurveX] per-axis curve override - * @param {number} [config.inputCurveY] per-axis curve override - * @param {number} [config.smoothing=0] - * @param {string} [config.momentumZoom='off'] 'off' | 'gentle' | 'strong' - * @param {number} [config.velocityWindow=150] velocity estimation window in ms - * @param {string} [config.anchorMode='auto'] 'auto' | 'sticky' | 'center' - * @param {number} [config.anchorX] explicit anchor X (for sticky mode) - * @param {number} [config.anchorY] explicit anchor Y (for sticky mode) - * @param {boolean} [config.invertX=false] - * @param {boolean} [config.invertY=false] - */ - constructor(config = {}) { - // --- Configuration --- - this._zoom = DEFAULT_ZOOM; - this._zoomX = null; // null = use global zoom - this._zoomY = null; - this._deadzone = DEFAULT_DEADZONE; - this._inputCurve = DEFAULT_INPUT_CURVE; - this._inputCurveX = null; - this._inputCurveY = null; - this._smoothing = DEFAULT_SMOOTHING; - this._momentumZoom = DEFAULT_MOMENTUM_ZOOM; - this._velocityWindow = DEFAULT_VELOCITY_WINDOW; - this._anchorMode = DEFAULT_ANCHOR_MODE; - this._anchorX = 0.5; - this._anchorY = 0.5; - this._invertX = false; - this._invertY = false; - - // --- Internal state --- - this._smoothedX = 0.5; - this._smoothedY = 0.5; - this._frozen = false; - - // Velocity estimation ring buffer: { x, y, t } - this._velocityHistory = []; - this._currentVelocity = 0; // magnitude, [0,1]-space units per second - this._momentumZoomMultiplier = 1; // current momentum zoom factor - - // Apply any initial config overrides - if (config && typeof config === 'object') { - this.setConfig(config); - } - } - - // ----------------------------------------------------------------------- - // Main processing - // ----------------------------------------------------------------------- - - /** - * Process raw input through the full pipeline. - * - * @param {number} rawX — raw input X in [0,1] - * @param {number} rawY — raw input Y in [0,1] - * @param {number} deltaTime — time since last call in seconds (e.g. 0.016) - * @returns {{ x: number, y: number, frozen: boolean }} - */ - process(rawX, rawY, deltaTime) { - const dt = Math.max(0, deltaTime || 0); - - // Resolve effective zoom per-axis - const baseZoomX = this._zoomX != null ? this._zoomX : this._zoom; - const baseZoomY = this._zoomY != null ? this._zoomY : this._zoom; - - // Check freeze *before* momentum modulation - const frozenX = baseZoomX <= FREEZE_THRESHOLD; - const frozenY = baseZoomY <= FREEZE_THRESHOLD; - this._frozen = frozenX && frozenY; - - if (this._frozen) { - // Both axes frozen — return last smoothed output, skip everything - return { x: this._smoothedX, y: this._smoothedY, frozen: true }; - } - - // --- 0. Invert --- - let x = this._invertX ? 1 - rawX : rawX; - let y = this._invertY ? 1 - rawY : rawY; - - // --- 1. Deadzone --- - x = applyDeadzone(x, this._deadzone); - y = applyDeadzone(y, this._deadzone); - - // --- 1.5. Circular clamp --- - // Constrain input to a unit circle (radius 0.5 centered at 0.5,0.5) - // so the reachable input space matches the circular joystick UI. - const cx = x - 0.5; - const cy = y - 0.5; - const dist = Math.sqrt(cx * cx + cy * cy); - if (dist > 0.5 && dist > 1e-12) { - const scale = 0.5 / dist; - x = 0.5 + cx * scale; - y = 0.5 + cy * scale; - } - - // --- 2. Zoom --- - const anchorX = this._resolveAnchorX(); - const anchorY = this._resolveAnchorY(); - - // Apply momentum-zoom multiplier to base zoom - const effZoomX = frozenX ? FREEZE_THRESHOLD : clamp(baseZoomX * this._momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX); - const effZoomY = frozenY ? FREEZE_THRESHOLD : clamp(baseZoomY * this._momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX); - - x = frozenX ? this._smoothedX : applyZoom(x, anchorX, effZoomX); - y = frozenY ? this._smoothedY : applyZoom(y, anchorY, effZoomY); - - // --- 3. Input Curve --- - const curveX = this._inputCurveX != null ? this._inputCurveX : this._inputCurve; - const curveY = this._inputCurveY != null ? this._inputCurveY : this._inputCurve; - if (!frozenX) x = centeredPowerCurve(x, curveX); - if (!frozenY) y = centeredPowerCurve(y, curveY); - - // --- 4. Smoothing --- - if (!frozenX) this._smoothedX = emaSmooth(this._smoothedX, x, this._smoothing, dt); - if (!frozenY) this._smoothedY = emaSmooth(this._smoothedY, y, this._smoothing, dt); - - // --- 5. Momentum-as-zoom (update for *next* frame) --- - this._updateMomentum(rawX, rawY, dt); - - return { - x: this._smoothedX, - y: this._smoothedY, - frozen: false, - }; - } - - // ----------------------------------------------------------------------- - // Configuration setters - // ----------------------------------------------------------------------- - - /** - * Set global zoom level. - * In 'auto' anchor mode, updates the anchor to the current smoothed position - * when zoom changes. - * @param {number} level — 0.01 to 1.0 - */ - setZoom(level) { - const prev = this._zoom; - this._zoom = clamp(level, ZOOM_MIN, ZOOM_MAX); - if (this._anchorMode === 'auto' && prev !== this._zoom) { - this._anchorX = this._smoothedX; - this._anchorY = this._smoothedY; - } - } - - /** - * Set per-axis zoom overrides. Pass null to clear an axis override. - * @param {number|null} x — zoom for X axis (0.01-1.0), or null - * @param {number|null} y — zoom for Y axis (0.01-1.0), or null - */ - setZoomPerAxis(x, y) { - this._zoomX = x != null ? clamp(x, ZOOM_MIN, ZOOM_MAX) : null; - this._zoomY = y != null ? clamp(y, ZOOM_MIN, ZOOM_MAX) : null; - if (this._anchorMode === 'auto') { - this._anchorX = this._smoothedX; - this._anchorY = this._smoothedY; - } - } - - /** - * Set explicit anchor point (used in sticky mode, also sets anchor for auto). - * @param {number} x — anchor X in [0,1] - * @param {number} y — anchor Y in [0,1] - */ - setAnchor(x, y) { - this._anchorX = clamp(x, 0, 1); - this._anchorY = clamp(y, 0, 1); - } - - /** - * Set anchor mode. - * @param {'auto'|'sticky'|'center'} mode - */ - setAnchorMode(mode) { - if (mode !== 'auto' && mode !== 'sticky' && mode !== 'center') return; - this._anchorMode = mode; - if (mode === 'center') { - this._anchorX = 0.5; - this._anchorY = 0.5; - } else if (mode === 'auto') { - // Snap anchor to current position - this._anchorX = this._smoothedX; - this._anchorY = this._smoothedY; - } - } - - /** - * Set deadzone as a fraction of half-travel. - * @param {number} pct — 0 to 0.4 - */ - setDeadzone(pct) { - this._deadzone = clamp(pct, 0, DEADZONE_MAX); - } - - /** - * Set input curve exponent (applies to both axes unless per-axis is set). - * @param {number} exp — 0.2 to 5.0 (1.0 = linear) - */ - setInputCurve(exp) { - this._inputCurve = clamp(exp, INPUT_CURVE_MIN, INPUT_CURVE_MAX); - } - - /** - * Set per-axis input curve overrides. Pass null to clear. - * @param {number|null} expX — curve exponent for X, or null - * @param {number|null} expY — curve exponent for Y, or null - */ - setInputCurvePerAxis(expX, expY) { - this._inputCurveX = expX != null ? clamp(expX, INPUT_CURVE_MIN, INPUT_CURVE_MAX) : null; - this._inputCurveY = expY != null ? clamp(expY, INPUT_CURVE_MIN, INPUT_CURVE_MAX) : null; - } - - /** - * Set EMA smoothing factor. - * @param {number} factor — 0 (off) to 0.95 - */ - setSmoothing(factor) { - this._smoothing = clamp(factor, 0, SMOOTHING_MAX); - } - - /** - * Set momentum-zoom mode. - * @param {'off'|'gentle'|'strong'} mode - */ - setMomentumZoom(mode) { - if (!MOMENTUM_PRESETS.hasOwnProperty(mode)) return; - this._momentumZoom = mode; - if (mode === 'off') { - this._momentumZoomMultiplier = 1; - this._velocityHistory = []; - this._currentVelocity = 0; - } - } - - /** - * Set axis inversion. - * @param {boolean} invertX - * @param {boolean} invertY - */ - setInvert(invertX, invertY) { - this._invertX = !!invertX; - this._invertY = !!invertY; - } - - /** - * Set velocity estimation window for momentum-zoom. - * @param {number} ms — window in milliseconds (50-500) - */ - setVelocityWindow(ms) { - this._velocityWindow = clamp(ms, 50, 500); - } - - // ----------------------------------------------------------------------- - // State queries - // ----------------------------------------------------------------------- - - /** - * Get current effective zoom level (after momentum modulation). - * Returns the *minimum* of X and Y effective zoom if they differ. - * @returns {number} - */ - getZoomLevel() { - const baseX = this._zoomX != null ? this._zoomX : this._zoom; - const baseY = this._zoomY != null ? this._zoomY : this._zoom; - const effX = clamp(baseX * this._momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX); - const effY = clamp(baseY * this._momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX); - return Math.min(effX, effY); - } - - /** - * Get current anchor position. - * @returns {{ x: number, y: number }} - */ - getAnchor() { - return { x: this._resolveAnchorX(), y: this._resolveAnchorY() }; - } - - /** - * Get the zoom window as a circle in [0,1] space — for minimap rendering. - * Returns center + radius of the input space the joystick currently covers. - * @returns {{ cx: number, cy: number, r: number }} - */ - getZoomWindow() { - const anchorX = this._resolveAnchorX(); - const anchorY = this._resolveAnchorY(); - const baseZoom = this._zoomX != null ? this._zoomX : this._zoom; - const effZoom = clamp(baseZoom * this._momentumZoomMultiplier, ZOOM_MIN, ZOOM_MAX); - return { - cx: anchorX, - cy: anchorY, - r: effZoom / 2, - }; - } - - /** - * Get the zoom window as an axis-aligned bounding rect in [0,1] space. - * For consumers that need {x1, y1, x2, y2} (heatmap, region pinning). - * @returns {{ x1: number, y1: number, x2: number, y2: number }} - */ - getZoomWindowRect() { - const { cx, cy, r } = this.getZoomWindow(); - return { - x1: clamp(cx - r, 0, 1), - y1: clamp(cy - r, 0, 1), - x2: clamp(cx + r, 0, 1), - y2: clamp(cy + r, 0, 1), - }; - } - - /** - * Whether input is effectively frozen (zoom at or below minimum). - * @returns {boolean} - */ - isFrozen() { - return this._frozen; - } - - // ----------------------------------------------------------------------- - // Serialization - // ----------------------------------------------------------------------- - - /** - * Export all settings as a plain object (no internal state). - * @returns {object} - */ - getConfig() { - return { - zoom: this._zoom, - zoomX: this._zoomX, - zoomY: this._zoomY, - deadzone: this._deadzone, - inputCurve: this._inputCurve, - inputCurveX: this._inputCurveX, - inputCurveY: this._inputCurveY, - smoothing: this._smoothing, - momentumZoom: this._momentumZoom, - velocityWindow: this._velocityWindow, - anchorMode: this._anchorMode, - anchorX: this._anchorX, - anchorY: this._anchorY, - invertX: this._invertX, - invertY: this._invertY, - }; - } - - /** - * Restore configuration from a plain object. Unknown keys are ignored. - * Only updates settings that are present in the object. - * @param {object} config - */ - setConfig(config) { - if (config.zoom != null) this.setZoom(config.zoom); - if (config.deadzone != null) this.setDeadzone(config.deadzone); - if (config.inputCurve != null) this.setInputCurve(config.inputCurve); - if (config.smoothing != null) this.setSmoothing(config.smoothing); - if (config.momentumZoom != null) this.setMomentumZoom(config.momentumZoom); - if (config.velocityWindow != null) this.setVelocityWindow(config.velocityWindow); - if (config.anchorMode != null) this.setAnchorMode(config.anchorMode); - if (config.invertX != null || config.invertY != null) { - this.setInvert( - config.invertX != null ? config.invertX : this._invertX, - config.invertY != null ? config.invertY : this._invertY, - ); - } - // Per-axis overrides (allow explicit null to clear) - if ('zoomX' in config || 'zoomY' in config) { - this.setZoomPerAxis( - 'zoomX' in config ? config.zoomX : this._zoomX, - 'zoomY' in config ? config.zoomY : this._zoomY, - ); - } - if ('inputCurveX' in config || 'inputCurveY' in config) { - this.setInputCurvePerAxis( - 'inputCurveX' in config ? config.inputCurveX : this._inputCurveX, - 'inputCurveY' in config ? config.inputCurveY : this._inputCurveY, - ); - } - // Explicit anchor (set after anchorMode so sticky mode is already active) - if (config.anchorX != null && config.anchorY != null) { - this.setAnchor(config.anchorX, config.anchorY); - } - } - - // ----------------------------------------------------------------------- - // Reset - // ----------------------------------------------------------------------- - - /** - * Reset all settings to defaults and clear internal state. - */ - reset() { - this._zoom = DEFAULT_ZOOM; - this._zoomX = null; - this._zoomY = null; - this._deadzone = DEFAULT_DEADZONE; - this._inputCurve = DEFAULT_INPUT_CURVE; - this._inputCurveX = null; - this._inputCurveY = null; - this._smoothing = DEFAULT_SMOOTHING; - this._momentumZoom = DEFAULT_MOMENTUM_ZOOM; - this._velocityWindow = DEFAULT_VELOCITY_WINDOW; - this._anchorMode = DEFAULT_ANCHOR_MODE; - this._anchorX = 0.5; - this._anchorY = 0.5; - this._invertX = false; - this._invertY = false; - - this._smoothedX = 0.5; - this._smoothedY = 0.5; - this._frozen = false; - this._velocityHistory = []; - this._currentVelocity = 0; - this._momentumZoomMultiplier = 1; - } - - // ----------------------------------------------------------------------- - // Private helpers - // ----------------------------------------------------------------------- - - /** Resolve anchor X based on current mode. */ - _resolveAnchorX() { - if (this._anchorMode === 'center') return 0.5; - return this._anchorX; - } - - /** Resolve anchor Y based on current mode. */ - _resolveAnchorY() { - if (this._anchorMode === 'center') return 0.5; - return this._anchorY; - } - - /** - * Update velocity estimation and momentum-zoom multiplier. - * Called at the end of each process() with the *raw* (pre-pipeline) input - * so velocity reflects actual physical movement, not processed output. - */ - _updateMomentum(rawX, rawY, dt) { - const preset = MOMENTUM_PRESETS[this._momentumZoom]; - if (!preset) { - this._momentumZoomMultiplier = 1; - return; - } - - const now = performance.now(); - - // Push current sample - this._velocityHistory.push({ x: rawX, y: rawY, t: now }); - - // Prune samples outside the velocity window - const windowStart = now - this._velocityWindow; - while (this._velocityHistory.length > 1 && this._velocityHistory[0].t < windowStart) { - this._velocityHistory.shift(); - } - - // Estimate velocity (distance in [0,1] space per second) - if (this._velocityHistory.length >= 2) { - const first = this._velocityHistory[0]; - const last = this._velocityHistory[this._velocityHistory.length - 1]; - const elapsed = (last.t - first.t) / 1000; // seconds - if (elapsed > 0.001) { - const dx = last.x - first.x; - const dy = last.y - first.y; - const dist = Math.sqrt(dx * dx + dy * dy); - this._currentVelocity = dist / elapsed; - } - } - - // Map velocity to zoom multiplier. - // Slow movement (velocity ~ 0) → low multiplier (zoomed in, fine control) - // Fast movement (velocity high) → high multiplier (zoomed out, broad traversal) - // - // Velocity is in [0,1]-space units per second. - // Typical slow movement: 0.1–0.5 u/s - // Typical fast sweep: 2–6 u/s - // - // We use a sigmoid-like mapping so the transition feels smooth. - const v = this._currentVelocity; - const { factor, minZoomMul, maxZoomMul } = preset; - - // Normalised speed: 0 at rest, approaches 1 at high velocity - // Using 1 - exp(-factor * v) which is smooth and bounded - const normalised = 1 - Math.exp(-factor * v); - - // Interpolate between minZoomMul (slow) and maxZoomMul (fast) - const target = minZoomMul + normalised * (maxZoomMul - minZoomMul); - - // Smooth the multiplier itself to avoid jitter - // Use a fast-attack, slow-release envelope so zoom-out is instant - // but zoom-in (slowing down) ramps gently - const attackRate = 12; // per second — fast response to speed increase - const releaseRate = 3; // per second — gentle return when slowing down - const rate = target > this._momentumZoomMultiplier ? attackRate : releaseRate; - const blend = 1 - Math.exp(-rate * dt); - this._momentumZoomMultiplier += blend * (target - this._momentumZoomMultiplier); - } -} diff --git a/playground/js/ui/joy-map-enhanced.js b/playground/js/ui/joy-map-enhanced.js deleted file mode 100644 index cf57739..0000000 --- a/playground/js/ui/joy-map-enhanced.js +++ /dev/null @@ -1,745 +0,0 @@ -// Enhanced Joy-Map Visualization -// Zoom minimap (7.1), vanishing trail (7.2), noise ring (7.3) -// Replaces/augments the basic drawJoyMap() in a-app.js - -// ---- Catmull-Rom spline helper ---- - -function catmullRomPoint(p0, p1, p2, p3, t) { - // Centripetal Catmull-Rom (alpha=0.5 simplified to uniform for speed) - const t2 = t * t; - const t3 = t2 * t; - return { - x: 0.5 * ( - (2 * p1.x) + - (-p0.x + p2.x) * t + - (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + - (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3 - ), - y: 0.5 * ( - (2 * p1.y) + - (-p0.y + p2.y) * t + - (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + - (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3 - ) - }; -} - -// ---- Ring buffer ---- - -class RingBuffer { - constructor(capacity) { - this.capacity = capacity; - this.buffer = new Array(capacity); - this.head = 0; // next write index - this.count = 0; - } - - push(item) { - this.buffer[this.head] = item; - this.head = (this.head + 1) % this.capacity; - if (this.count < this.capacity) this.count++; - } - - // Iterate oldest to newest - forEach(fn) { - if (this.count === 0) return; - const start = (this.head - this.count + this.capacity) % this.capacity; - for (let i = 0; i < this.count; i++) { - const idx = (start + i) % this.capacity; - fn(this.buffer[idx], i, this.count); - } - } - - // Get item by age-ordered index (0 = oldest) - get(i) { - if (i < 0 || i >= this.count) return null; - const start = (this.head - this.count + this.capacity) % this.capacity; - return this.buffer[(start + i) % this.capacity]; - } - - clear() { - this.head = 0; - this.count = 0; - } -} - -// ---- Constants ---- - -const TRAIL_COLORS = { - visual: '#ff6432', - synth: '#00d4aa' -}; - -const FROZEN_COLOR = 'rgba(100, 180, 255, 0.15)'; -const ZOOM_WINDOW_FILL = 'rgba(255, 200, 100, 0.06)'; -const ZOOM_WINDOW_BORDER = 'rgba(255, 200, 100, 0.6)'; -const DIM_OVERLAY = 'rgba(0, 0, 0, 0.35)'; -const GRID_MINOR = 'rgba(255, 255, 255, 0.08)'; -const GRID_MAJOR = 'rgba(255, 255, 255, 0.15)'; -const CURSOR_GLOW = 'rgba(255, 255, 255, 0.35)'; - -const DEFAULT_TRAIL_DURATION = 5000; // ms -const MAX_TRAIL_POINTS = 300; -const TRAIL_MIN_WIDTH = 1.5; -const TRAIL_MAX_WIDTH = 4; -const CURSOR_RADIUS = 6; -const TAP_HIT_RADIUS = 12; -const SPLINE_SEGMENTS = 4; // subdivisions per trail segment - -// ---- Main class ---- - -export class JoyMapEnhanced { - /** - * @param {HTMLCanvasElement} canvas - The joy-map canvas element - * @param {Object} options - * @param {Function} options.onTrailTap - Callback({x, y}) when a trail point is tapped - * @param {Function} options.getTrainingData - Returns {features, labels} for example dots - */ - constructor(canvas, options = {}) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.onTrailTap = options.onTrailTap || null; - this.getTrainingData = options.getTrainingData || null; - - this.trail = new RingBuffer(MAX_TRAIL_POINTS); - this.trailDuration = DEFAULT_TRAIL_DURATION; - this.trailColor = TRAIL_COLORS.visual; - - this.pinnedRegions = []; - - // Optional heatmap layer (InputHeatmap instance) - this._heatmap = null; - - // Flash state for tapped trail point - this._flashPoint = null; - this._flashTime = 0; - - // Pulse phase for noise ring - this._pulsePhase = 0; - - // HiDPI tracking - this._dpr = 1; - this._cssW = 0; - this._cssH = 0; - - // Bind event handlers - this._onPointerDown = this._handlePointerDown.bind(this); - this._onTouchStart = this._handleTouchStart.bind(this); - - canvas.addEventListener('pointerdown', this._onPointerDown); - canvas.addEventListener('touchstart', this._onTouchStart, { passive: false }); - } - - // ---- Public API ---- - - /** - * Main draw call. Invoke once per rAF frame. - */ - draw(state) { - const { - joyX = 0.5, - joyY = 0.5, - effectiveX, - effectiveY, - zoomWindow = null, - zoomLevel = 1.0, - noiseLevel = 0, - outputMode = 'visual', - frozen = false - } = state; - - this._ensureHiDPI(); - const ctx = this.ctx; - const w = this._cssW; - const h = this._cssH; - const dpr = this._dpr; - - ctx.save(); - ctx.scale(dpr, dpr); - - // Clip to circle - const cx = w / 2; - const cy = h / 2; - const r = w / 2 - 1; - - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.clip(); - - // Background - ctx.fillStyle = 'rgba(13, 13, 13, 0.85)'; - ctx.fillRect(0, 0, w, h); - - // Layers (back to front): - // 0. Input heatmap (background) - // 1. Dim area outside zoom window - // 2. Grid (adapts to zoom) - // 3. Zoom window border - // 4. Pinned regions - // 5. Training examples - // 6. Trail - // 7. Noise ring (canvas-drawn) - // 8. Zoom ring (canvas-drawn) - // 9. Cursor - // 10. Frozen overlay - - const zw = this._normalizeZoomWindow(zoomWindow, zoomLevel); - - // Heatmap background layer - if (this._heatmap && this._heatmap.enabled) { - this._heatmap.draw(ctx, w, h, zw); - } - - this._drawDimOverlay(ctx, w, h, zw); - this._drawGrid(ctx, w, h, zw, zoomLevel); - this._drawZoomWindowBorder(ctx, w, h, zw); - this._drawPinnedRegions(ctx, w, h); - this._drawTrainingExamples(ctx, w, h); - this._drawTrail(ctx, w, h, outputMode); - this._drawNoiseRings(ctx, cx, cy, r, zoomLevel, noiseLevel); - this._drawCursor(ctx, w, h, joyX, joyY, effectiveX, effectiveY, frozen); - this._drawFlash(ctx, w, h); - - if (frozen) { - this._drawFrozenOverlay(ctx, w, h, cx, cy, r); - } - - // Circle border (outermost) - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; - ctx.lineWidth = 1.5; - ctx.stroke(); - - ctx.restore(); - - // Advance pulse - this._pulsePhase += 0.04; - } - - /** - * Record a trail point. Call when joystick moves. - */ - addTrailPoint(x, y, zoomLevel = 1.0) { - const now = performance.now(); - // Deduplicate: skip if very close to last point and recent - if (this.trail.count > 0) { - const last = this.trail.get(this.trail.count - 1); - const dx = x - last.x; - const dy = y - last.y; - if (dx * dx + dy * dy < 0.0001 && now - last.t < 50) return; - } - this.trail.push({ x, y, z: zoomLevel, t: now }); - } - - /** - * Handle tap events. Returns {x, y} if a trail point was tapped, null otherwise. - */ - handleTap(canvasX, canvasY) { - const now = performance.now(); - const w = this._cssW; - const h = this._cssH; - const hitR = TAP_HIT_RADIUS; - let bestDist = hitR * hitR; - let bestPoint = null; - - this.trail.forEach((pt, i, count) => { - const age = now - pt.t; - if (age > this.trailDuration) return; - const px = pt.x * w; - const py = (1 - pt.y) * h; - const dx = canvasX - px; - const dy = canvasY - py; - const d2 = dx * dx + dy * dy; - if (d2 < bestDist) { - bestDist = d2; - bestPoint = pt; - } - }); - - if (bestPoint) { - this._flashPoint = { x: bestPoint.x, y: bestPoint.y }; - this._flashTime = now; - return { x: bestPoint.x, y: bestPoint.y }; - } - return null; - } - - /** - * Set a heatmap instance to draw as the background layer. - * @param {InputHeatmap|null} heatmap - */ - setHeatmap(heatmap) { - this._heatmap = heatmap || null; - } - - setPinnedRegions(regions) { - this.pinnedRegions = regions || []; - } - - setTrailDuration(seconds) { - this.trailDuration = seconds * 1000; - } - - setTrailColor(color) { - this.trailColor = color; - } - - destroy() { - this.canvas.removeEventListener('pointerdown', this._onPointerDown); - this.canvas.removeEventListener('touchstart', this._onTouchStart); - this.trail.clear(); - this.pinnedRegions = []; - } - - // ---- HiDPI ---- - - _ensureHiDPI() { - const dpr = window.devicePixelRatio || 1; - const rect = this.canvas.getBoundingClientRect(); - const cssW = Math.round(rect.width) || this.canvas.clientWidth || 160; - const cssH = Math.round(rect.height) || this.canvas.clientHeight || 160; - - if (this._dpr !== dpr || this._cssW !== cssW || this._cssH !== cssH) { - this._dpr = dpr; - this._cssW = cssW; - this._cssH = cssH; - this.canvas.width = cssW * dpr; - this.canvas.height = cssH * dpr; - } - } - - // ---- Zoom window ---- - - /** - * Normalize zoom window to { cx, cy, r } circle format. - */ - _normalizeZoomWindow(zw, zoomLevel) { - if (zw && typeof zw.cx === 'number') return zw; - // Synthesize from zoomLevel centered on 0.5 - return { cx: 0.5, cy: 0.5, r: zoomLevel / 2 }; - } - - // ---- Drawing layers ---- - - _drawDimOverlay(ctx, w, h, zw) { - if (zw.r >= 0.5) return; // no zoom — circle covers entire space - - // Draw dim overlay over the entire area, cut out the zoom circle - ctx.save(); - ctx.fillStyle = DIM_OVERLAY; - - const zcx = zw.cx * w; - const zcy = (1 - zw.cy) * h; - const zr = zw.r * Math.min(w, h); - - // Even-odd: outer rect minus inner circle - ctx.beginPath(); - ctx.rect(0, 0, w, h); - ctx.arc(zcx, zcy, zr, 0, Math.PI * 2, true); // counter-clockwise for cutout - ctx.fill('evenodd'); - ctx.restore(); - } - - _drawGrid(ctx, w, h, zw, zoomLevel) { - // Determine grid density based on zoom - const zoomScale = zw.r * 2; - - // We draw multiple grid levels with fading - // Level 0: 4x4 (always) - // Level 1: 8x8 (fades in as zoom < 0.75) - // Level 2: 16x16 (fades in as zoom < 0.375) - // Level 3: 32x32 (fades in as zoom < 0.1875) - const levels = [ - { divs: 4, fadeStart: 2.0, fadeFull: 1.0 }, - { divs: 8, fadeStart: 0.75, fadeFull: 0.5 }, - { divs: 16, fadeStart: 0.375, fadeFull: 0.25 }, - { divs: 32, fadeStart: 0.1875, fadeFull: 0.1 } - ]; - - for (const level of levels) { - let alpha; - if (zoomScale >= level.fadeStart) { - alpha = 0; - } else if (zoomScale <= level.fadeFull) { - alpha = 1; - } else { - alpha = 1 - (zoomScale - level.fadeFull) / (level.fadeStart - level.fadeFull); - } - if (alpha < 0.01) continue; - - // Is this a "major" grid (4x4)? - const isMajor = level.divs === 4; - const baseAlpha = isMajor ? 0.15 : 0.08; - const finalAlpha = baseAlpha * alpha; - - ctx.strokeStyle = `rgba(255, 255, 255, ${finalAlpha})`; - ctx.lineWidth = isMajor ? 0.8 : 0.5; - - // Draw grid lines within the zoom window region - // Lines at intervals of 1/divs in input space - const step = 1 / level.divs; - - ctx.beginPath(); - - // Vertical lines - for (let i = 0; i <= level.divs; i++) { - const inputX = i * step; - // Only draw if visible in the full map - const canvasX = inputX * w; - ctx.moveTo(canvasX, 0); - ctx.lineTo(canvasX, h); - } - - // Horizontal lines - for (let i = 0; i <= level.divs; i++) { - const inputY = i * step; - const canvasY = (1 - inputY) * h; - ctx.moveTo(0, canvasY); - ctx.lineTo(w, canvasY); - } - - ctx.stroke(); - } - } - - _drawZoomWindowBorder(ctx, w, h, zw) { - if (zw.r >= 0.5) return; // no zoom - - const zcx = zw.cx * w; - const zcy = (1 - zw.cy) * h; - const zr = zw.r * Math.min(w, h); - - // Fill - ctx.beginPath(); - ctx.arc(zcx, zcy, zr, 0, Math.PI * 2); - ctx.fillStyle = ZOOM_WINDOW_FILL; - ctx.fill(); - - // Border - ctx.beginPath(); - ctx.arc(zcx, zcy, zr, 0, Math.PI * 2); - ctx.strokeStyle = ZOOM_WINDOW_BORDER; - ctx.lineWidth = 1.5; - ctx.setLineDash([4, 3]); - ctx.stroke(); - ctx.setLineDash([]); - } - - _drawPinnedRegions(ctx, w, h) { - for (const region of this.pinnedRegions) { - const x1 = region.x1 * w; - const y1 = (1 - region.y2) * h; - const rw = (region.x2 - region.x1) * w; - const rh = (region.y2 - region.y1) * h; - const color = region.color || 'rgba(100, 200, 255, 0.15)'; - - ctx.fillStyle = color; - ctx.fillRect(x1, y1, rw, rh); - - ctx.strokeStyle = color.replace(/[\d.]+\)$/, '0.5)'); - ctx.lineWidth = 1; - ctx.strokeRect(x1, y1, rw, rh); - } - } - - _drawTrainingExamples(ctx, w, h) { - if (!this.getTrainingData) return; - const data = this.getTrainingData(); - if (!data) return; - - const { features, labels } = data; - for (let i = 0; i < features.length; i++) { - const fx = features[i][0] * w; - const fy = (1 - features[i][1]) * h; - let hue = 0; - if (labels[i]) { - hue = (labels[i][3] || 0) * 360; - } - ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.85)`; - ctx.beginPath(); - ctx.arc(fx, fy, 3, 0, Math.PI * 2); - ctx.fill(); - } - } - - _drawTrail(ctx, w, h, outputMode) { - const now = performance.now(); - const duration = this.trailDuration; - const baseColor = TRAIL_COLORS[outputMode] || this.trailColor; - - // Collect alive points - const alive = []; - this.trail.forEach((pt) => { - const age = now - pt.t; - if (age <= duration) { - alive.push(pt); - } - }); - - if (alive.length < 2) return; - - // Parse base color to RGB - const rgb = this._parseColor(baseColor); - - // Draw spline segments using Catmull-Rom interpolation - // Pre-convert control points to canvas space - const canvasPts = alive.map(pt => ({ - x: pt.x * w, - y: (1 - pt.y) * h, - z: pt.z, - t: pt.t - })); - - for (let i = 0; i < canvasPts.length - 1; i++) { - const cp0 = canvasPts[Math.max(0, i - 1)]; - const cp1 = canvasPts[i]; - const cp2 = canvasPts[i + 1]; - const cp3 = canvasPts[Math.min(canvasPts.length - 1, i + 2)]; - - let prev = catmullRomPoint(cp0, cp1, cp2, cp3, 0); - - for (let s = 1; s <= SPLINE_SEGMENTS; s++) { - const t = s / SPLINE_SEGMENTS; - const curr = catmullRomPoint(cp0, cp1, cp2, cp3, t); - - // Interpolate age and zoom along the segment - const segAge = now - (cp1.t + (cp2.t - cp1.t) * (t - 0.5 / SPLINE_SEGMENTS)); - const segZoom = cp1.z + (cp2.z - cp1.z) * t; - - const alpha = Math.max(0, 1 - segAge / duration); - const lineW = TRAIL_MIN_WIDTH + (TRAIL_MAX_WIDTH - TRAIL_MIN_WIDTH) * segZoom; - - ctx.beginPath(); - ctx.moveTo(prev.x, prev.y); - ctx.lineTo(curr.x, curr.y); - ctx.strokeStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha * 0.8})`; - ctx.lineWidth = lineW; - ctx.lineCap = 'round'; - ctx.stroke(); - - prev = curr; - } - } - - // Draw trail point dots (for tap targets) — only recent, visible ones - for (const pt of alive) { - const age = now - pt.t; - const alpha = Math.max(0, 1 - age / duration); - if (alpha < 0.1) continue; - - const px = pt.x * w; - const py = (1 - pt.y) * h; - const dotR = 2 + alpha * 2; - - ctx.beginPath(); - ctx.arc(px, py, dotR, 0, Math.PI * 2); - ctx.fillStyle = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha * 0.5})`; - ctx.fill(); - } - } - - _drawFlash(ctx, w, h) { - if (!this._flashPoint) return; - const now = performance.now(); - const elapsed = now - this._flashTime; - const flashDuration = 300; - if (elapsed > flashDuration) { - this._flashPoint = null; - return; - } - - const alpha = 1 - elapsed / flashDuration; - const radius = 6 + (elapsed / flashDuration) * 14; - const px = this._flashPoint.x * w; - const py = (1 - this._flashPoint.y) * h; - - ctx.beginPath(); - ctx.arc(px, py, radius, 0, Math.PI * 2); - ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.6})`; - ctx.fill(); - } - - _drawNoiseRings(ctx, cx, cy, maxR, zoomLevel, noiseLevel) { - // Inner ring: zoom level (shrinks as zoom decreases) - const zoomR = maxR * Math.max(0.1, zoomLevel); - ctx.beginPath(); - ctx.arc(cx, cy, zoomR, 0, Math.PI * 2); - ctx.strokeStyle = `rgba(255, 200, 100, ${0.15 + 0.15 * (1 - zoomLevel)})`; - ctx.lineWidth = 1.5; - ctx.stroke(); - - // Outer ring: noise magnitude - if (noiseLevel > 0.001) { - const noiseR = zoomR + 4 + noiseLevel * (maxR - zoomR - 4) * 3; - const clampedR = Math.min(noiseR, maxR - 1); - - // Pulse when noise is high - const isHigh = noiseLevel > 0.1; - const pulse = isHigh ? Math.sin(this._pulsePhase) * 0.15 : 0; - const noiseAlpha = 0.2 + noiseLevel * 2 + pulse; - - ctx.beginPath(); - ctx.arc(cx, cy, clampedR, 0, Math.PI * 2); - ctx.strokeStyle = `rgba(255, 80, 80, ${Math.min(0.8, noiseAlpha)})`; - ctx.lineWidth = 2 + noiseLevel * 4; - ctx.stroke(); - - // Glow on high noise - if (isHigh) { - ctx.beginPath(); - ctx.arc(cx, cy, clampedR, 0, Math.PI * 2); - ctx.strokeStyle = `rgba(255, 80, 80, ${Math.min(0.3, noiseAlpha * 0.3)})`; - ctx.lineWidth = 6 + noiseLevel * 8; - ctx.stroke(); - } - } - } - - _drawCursor(ctx, w, h, joyX, joyY, effectiveX, effectiveY, frozen) { - const px = joyX * w; - const py = (1 - joyY) * h; - - // If we have effective coords, draw a ghost line from physical to effective - if (effectiveX !== undefined && effectiveY !== undefined) { - const ex = effectiveX * w; - const ey = (1 - effectiveY) * h; - const dx = ex - px; - const dy = ey - py; - if (dx * dx + dy * dy > 4) { - ctx.beginPath(); - ctx.moveTo(px, py); - ctx.lineTo(ex, ey); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.lineWidth = 0.5; - ctx.setLineDash([2, 2]); - ctx.stroke(); - ctx.setLineDash([]); - - // Effective position indicator (small ring) - ctx.beginPath(); - ctx.arc(ex, ey, 3, 0, Math.PI * 2); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)'; - ctx.lineWidth = 1; - ctx.stroke(); - } - } - - // Crosshair (subtle) - ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)'; - ctx.lineWidth = 0.5; - ctx.beginPath(); - ctx.moveTo(px, 0); - ctx.lineTo(px, h); - ctx.moveTo(0, py); - ctx.lineTo(w, py); - ctx.stroke(); - - // Glow - ctx.save(); - ctx.shadowColor = frozen ? 'rgba(100, 180, 255, 0.7)' : CURSOR_GLOW; - ctx.shadowBlur = 12; - - // Outer dot - ctx.beginPath(); - ctx.arc(px, py, CURSOR_RADIUS, 0, Math.PI * 2); - ctx.fillStyle = frozen ? 'rgba(100, 180, 255, 0.7)' : 'rgba(255, 255, 255, 0.6)'; - ctx.fill(); - - // Inner bright dot - ctx.shadowBlur = 0; - ctx.beginPath(); - ctx.arc(px, py, 2.5, 0, Math.PI * 2); - ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; - ctx.fill(); - - ctx.restore(); - } - - _drawFrozenOverlay(ctx, w, h, cx, cy, r) { - // Subtle blue tint - ctx.fillStyle = FROZEN_COLOR; - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.fill(); - - // Snowflake icon (simple asterisk-like) - ctx.save(); - ctx.translate(cx, cy); - ctx.strokeStyle = 'rgba(150, 210, 255, 0.4)'; - ctx.lineWidth = 1.5; - ctx.lineCap = 'round'; - - const armLen = 12; - const tickLen = 4; - for (let i = 0; i < 6; i++) { - const angle = (Math.PI / 3) * i; - ctx.save(); - ctx.rotate(angle); - - // Main arm - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(0, -armLen); - ctx.stroke(); - - // Ticks - ctx.beginPath(); - ctx.moveTo(-tickLen * 0.5, -armLen * 0.55); - ctx.lineTo(0, -armLen * 0.7); - ctx.lineTo(tickLen * 0.5, -armLen * 0.55); - ctx.stroke(); - - ctx.restore(); - } - - ctx.restore(); - - // "FROZEN" text - ctx.fillStyle = 'rgba(150, 210, 255, 0.35)'; - ctx.font = `bold ${Math.round(w * 0.065)}px system-ui, sans-serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText('FROZEN', cx, cy + armLen + 14); - } - - // ---- Event handling ---- - - _handlePointerDown(e) { - // Only respond to primary button (not right-click) and not if dragging - if (e.button !== 0) return; - this._tryTap(e.offsetX, e.offsetY); - } - - _handleTouchStart(e) { - if (e.touches.length !== 1) return; - const rect = this.canvas.getBoundingClientRect(); - const touch = e.touches[0]; - const x = touch.clientX - rect.left; - const y = touch.clientY - rect.top; - this._tryTap(x, y); - } - - _tryTap(cssX, cssY) { - const result = this.handleTap(cssX, cssY); - if (result && this.onTrailTap) { - this.onTrailTap(result); - } - } - - // ---- Utilities ---- - - _parseColor(hex) { - // Parse #rrggbb or rgb(r,g,b) - if (hex.startsWith('#')) { - const bigint = parseInt(hex.slice(1), 16); - return { - r: (bigint >> 16) & 255, - g: (bigint >> 8) & 255, - b: bigint & 255 - }; - } - // Fallback - return { r: 255, g: 100, b: 50 }; - } -} diff --git a/playground/js/ui/joystick.js b/playground/js/ui/joystick.js deleted file mode 100644 index 1129540..0000000 --- a/playground/js/ui/joystick.js +++ /dev/null @@ -1,183 +0,0 @@ -// Virtual touch joystick (Canvas-based) -// Outputs normalized X, Y in [0, 1] - -export class VirtualJoystick { - constructor(container, options = {}) { - this.canvas = document.createElement('canvas'); - this.size = options.size || 180; - this.canvas.width = this.size; - this.canvas.height = this.size; - this.canvas.style.width = this.size + 'px'; - this.canvas.style.height = this.size + 'px'; - this.canvas.style.touchAction = 'none'; - container.appendChild(this.canvas); - this.ctx = this.canvas.getContext('2d'); - - this.x = 0.5; - this.y = 0.5; - this.touching = false; - this.followMode = false; - this.springBack = options.springBack ?? false; - this.trackpadScale = Math.max(1, options.trackpadScale || 1); - this.onChange = options.onChange || (() => {}); - this.onFollowModeChange = options.onFollowModeChange || (() => {}); - - // Touch events - this.canvas.addEventListener('touchstart', this._onTouch.bind(this), { passive: false }); - this.canvas.addEventListener('touchmove', this._onTouch.bind(this), { passive: false }); - this.canvas.addEventListener('touchend', this._onRelease.bind(this)); - this.canvas.addEventListener('touchcancel', this._onRelease.bind(this)); - - // Mouse fallback - this.canvas.addEventListener('mousedown', (e) => { - if (this.followMode) { - this._updateFromEvent(e); - return; - } - this.touching = true; - this._updateFromEvent(e); - }); - window.addEventListener('mousemove', (e) => { - if (this.followMode) { - this._updateFromEvent(e, true); - } else if (this.touching) { - this._updateFromEvent(e); - } - }); - window.addEventListener('mouseup', () => { - if (this.touching && !this.followMode) this._onRelease(); - }); - this.canvas.addEventListener('dblclick', (e) => { - e.preventDefault(); - this.setFollowMode(!this.followMode); - this._updateFromEvent(e, true); - }); - - this.draw(); - } - - _onTouch(e) { - e.preventDefault(); - const touch = e.touches[0]; - this.touching = true; - this._updateFromClient(touch.clientX, touch.clientY); - } - - _getTrackingRect() { - const rect = this.canvas.getBoundingClientRect(); - const w = rect.width * this.trackpadScale; - const h = rect.height * this.trackpadScale; - return { - left: rect.left - (w - rect.width) * 0.5, - top: rect.top - (h - rect.height) * 0.5, - width: w, - height: h, - }; - } - - _updateFromClient(clientX, clientY) { - const rect = this._getTrackingRect(); - this.x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); - this.y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height)); - this.onChange(this.x, this.y); - this.draw(); - } - - _isInsideTrackingRect(clientX, clientY) { - const rect = this._getTrackingRect(); - return clientX >= rect.left && clientX <= rect.left + rect.width && - clientY >= rect.top && clientY <= rect.top + rect.height; - } - - _updateFromEvent(e, requireInside = false) { - if (requireInside && !this._isInsideTrackingRect(e.clientX, e.clientY)) return; - this._updateFromClient(e.clientX, e.clientY); - } - - _onRelease() { - if (this.followMode) return; - this.touching = false; - if (this.springBack) { - this.x = 0.5; - this.y = 0.5; - this.onChange(this.x, this.y); - } - this.draw(); - } - - setPosition(x, y, options = {}) { - const { emit = true, touching = false } = options; - this.x = Math.max(0, Math.min(1, x)); - this.y = Math.max(0, Math.min(1, y)); - this.touching = touching; - if (emit) this.onChange(this.x, this.y); - this.draw(); - } - - setFollowMode(enabled) { - this.followMode = !!enabled; - this.touching = this.followMode || this.touching; - this.onFollowModeChange(this.followMode); - this.draw(); - } - - draw() { - const { ctx, size: s } = this; - const r = s / 2; - ctx.clearRect(0, 0, s, s); - - // Background circle - ctx.beginPath(); - ctx.arc(r, r, r - 4, 0, Math.PI * 2); - ctx.strokeStyle = this.followMode ? '#cc5500' : '#333'; - ctx.lineWidth = 1.5; - ctx.stroke(); - - // Crosshairs - ctx.strokeStyle = '#222'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(r, 6); ctx.lineTo(r, s - 6); - ctx.moveTo(6, r); ctx.lineTo(s - 6, r); - ctx.stroke(); - - // Grid - ctx.strokeStyle = '#1a1a1a'; - ctx.setLineDash([2, 4]); - ctx.beginPath(); - ctx.moveTo(s * 0.25, 6); ctx.lineTo(s * 0.25, s - 6); - ctx.moveTo(s * 0.75, 6); ctx.lineTo(s * 0.75, s - 6); - ctx.moveTo(6, s * 0.25); ctx.lineTo(s - 6, s * 0.25); - ctx.moveTo(6, s * 0.75); ctx.lineTo(s - 6, s * 0.75); - ctx.stroke(); - ctx.setLineDash([]); - - // Thumb - const tx = this.x * s; - const ty = this.y * s; - - // Glow - if (this.touching) { - const grad = ctx.createRadialGradient(tx, ty, 0, tx, ty, 24); - grad.addColorStop(0, 'rgba(255, 106, 0, 0.3)'); - grad.addColorStop(1, 'rgba(255, 106, 0, 0)'); - ctx.fillStyle = grad; - ctx.beginPath(); - ctx.arc(tx, ty, 24, 0, Math.PI * 2); - ctx.fill(); - } - - ctx.fillStyle = this.touching ? '#ff6a00' : '#666'; - ctx.beginPath(); - ctx.arc(tx, ty, 14, 0, Math.PI * 2); - ctx.fill(); - - // Position text - ctx.fillStyle = this.followMode ? '#cc5500' : '#555'; - ctx.font = '10px monospace'; - ctx.textAlign = 'left'; - ctx.fillText(this.followMode ? 'FOLLOW' : 'HOLD', 8, s - 6); - ctx.textAlign = 'right'; - ctx.fillText(`${this.x.toFixed(2)}, ${this.y.toFixed(2)}`, s - 8, s - 6); - } -} diff --git a/playground/js/ui/modular-ui.js b/playground/js/ui/modular-ui.js deleted file mode 100644 index 2cc0057..0000000 --- a/playground/js/ui/modular-ui.js +++ /dev/null @@ -1,1531 +0,0 @@ -import { MODULAR_PRESETS, applyPreset } from '../synth/modular-presets.js'; - -/** - * modular-ui.js — Phase C of the "Modular" audio mode. - * - * User-facing control panel for ModularEngine: - * - Sub-engine toggle (Subtractive / Additive / FM) - * - Mod pool count steppers (ADSR 1..16, LFO 1..32) - * - Per-slot enable toggles (independent of count) - * - Matrix grid editor (sources × destinations, signed [-1, 1] amounts) - * - Expose engine sound params to the MLP - * - * The panel is mounted as a drawer in the existing `#drawer-stack`, with a - * matching dock icon injected into `#dock`. Both are hidden until the - * modular engine becomes active. - * - * Matrix paramMeta layout (from Phase B, VERIFIED at runtime via label - * prefix `MM_Matrix/` on rebuild): mod sources first (ADSR + LFO), matrix - * cells second (dest-major, source-major within each destination), opted-in - * engine sound params last. We discover matrix cells by group prefix, not - * by hardcoded offsets, so that count changes / sub-engine swaps are safe. - * - * Raw matrix cell range is [-1, 1]; paramMeta normalises to [0, 1] via - * `(raw - min) / (max - min)`. UI uses signed values directly and converts - * `(v + 1) / 2` when calling `engine.setParam(i, norm01)`. - * - * @module modular-ui - */ - -// --------------------------------------------------------------------------- -// Module-level constants -// --------------------------------------------------------------------------- - -const STORAGE_KEY = 'nisps-modular-state'; - -const MAX_ADSR = 16; -const MAX_LFO = 32; - -// Matrix cell cycle on left-click (positive only). Shift-click → 0. -// Right-click opens a menu with ±1 / 0 / random. -const CLICK_CYCLE = [0, 0.25, 0.5, 0.75, 1.0]; - -const SUB_ENGINE_OPTIONS = [ - { id: 'subtractive', label: 'Subtractive' }, - { id: 'additive', label: 'Additive' }, - { id: 'fm', label: 'FM' }, -]; - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** - * Initialise the Modular UI. Creates the drawer and dock icon (hidden), and - * wires them to the engine returned by `getEngine()`. - * - * @param {object} opts - * @param {() => object} opts.getEngine — returns current activeEngine - * @param {() => void} [opts.onStateChange] — optional hook when UI state - * persists to localStorage - * @returns {{ teardown, refresh, show, hide, isVisible }} - */ -export function initModularUI({ getEngine, onStateChange } = {}) { - injectStyles(); - - const dockIcon = createDockIcon(); - const drawer = createDrawer(); - - const uiState = loadUIState(); - - // Subscription handle for the active engine's paramMeta:change event. - let unsubParamMeta = null; - let lastWiredEngine = null; - - // DOM refs — populated by the section builders. - const refs = { - subEngineBtns: [], - adsrCountValue: null, - lfoCountValue: null, - adsrEnables: [], - lfoEnables: [], - matrixGrid: null, - matrixEmpty: null, - soundParamList: null, - }; - - // ---- Build panel sections ---- - buildSubEngineSection(drawer.body, refs, applySubEngineChange); - buildPresetsSection(drawer.body, openPresetOverlay); - buildCountSection(drawer.body, refs, applyCountChange); - buildEnableSection(drawer.body, refs, applyEnableChange); - buildMatrixSection(drawer.body, refs, { - onCellCycle: (src, dst) => cycleCell(src, dst), - onCellInvert: (src, dst) => setCell(src, dst, -getCell(src, dst)), - onCellZero: (src, dst) => setCell(src, dst, 0), - onCellPrecise: (src, dst, el) => openPreciseEditor(src, dst, el), - onCellMenu: (src, dst, el, e) => openCellMenu(src, dst, el, e), - }); - buildSoundParamSection(drawer.body, refs, applyExposeChange); - - // ---- Apply saved UI state to engine as soon as we can ---- - // We defer engine mutations until refresh() is called with a live engine. - let pendingRestore = true; - - // --------------------------------------------------------------------------- - // Engine binding - // --------------------------------------------------------------------------- - - function bindEngine(engine) { - if (engine === lastWiredEngine) return; - if (unsubParamMeta) { try { unsubParamMeta(); } catch (_) { /* ignore */ } } - unsubParamMeta = null; - lastWiredEngine = engine; - if (engine && typeof engine.on === 'function') { - unsubParamMeta = engine.on('paramMeta:change', () => { - rebuildMatrixGrid(); - rebuildSoundParamList(); - refreshSubEngineButtons(); - }); - } - } - - // --------------------------------------------------------------------------- - // Build: sub-engine section - // --------------------------------------------------------------------------- - - function refreshSubEngineButtons() { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - const available = new Set(engine.availableSubEngines || []); - const active = engine.activeSubEngineId; - for (const btn of refs.subEngineBtns) { - const id = btn.dataset.subEngine; - btn.classList.toggle('active', id === active); - btn.disabled = !available.has(id); - btn.title = available.has(id) - ? `Use ${id} sub-engine` - : `${id} not available yet`; - } - } - - async function applySubEngineChange(id) { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - if (id === engine.activeSubEngineId) return; - refs.subEngineBtns.forEach(b => b.classList.toggle('busy', b.dataset.subEngine === id)); - try { - await engine.setSubEngine(id); - uiState.subEngine = id; - saveUIState(); - // paramMeta:change listener rebuilds the grid - } catch (err) { - console.error('[modular-ui] setSubEngine failed:', err); - } finally { - refs.subEngineBtns.forEach(b => b.classList.remove('busy')); - refreshSubEngineButtons(); - } - } - - // --------------------------------------------------------------------------- - // Build: count section - // --------------------------------------------------------------------------- - - function applyCountChange(kind, newCount) { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - const adsr = kind === 'adsr' ? newCount : engine.adsrCount; - const lfo = kind === 'lfo' ? newCount : engine.lfoCount; - engine.setModSourceCount?.(adsr, lfo); - uiState.adsrCount = adsr; - uiState.lfoCount = lfo; - // Ensure enable arrays are long enough - while (uiState.adsrEnables.length < MAX_ADSR) uiState.adsrEnables.push(false); - while (uiState.lfoEnables.length < MAX_LFO) uiState.lfoEnables.push(false); - // Newly-in-count slots default to enabled (if previously disabled). - for (let i = 0; i < adsr; i++) if (!uiState.adsrEnables[i]) uiState.adsrEnables[i] = true; - for (let i = 0; i < lfo; i++) if (!uiState.lfoEnables[i]) uiState.lfoEnables[i] = true; - applyAllEnablesToEngine(); - saveUIState(); - refreshCountValues(); - refreshEnableRow(); - rebuildMatrixGrid(); - } - - function refreshCountValues() { - if (refs.adsrCountValue) refs.adsrCountValue.textContent = String(uiState.adsrCount); - if (refs.lfoCountValue) refs.lfoCountValue.textContent = String(uiState.lfoCount); - } - - // --------------------------------------------------------------------------- - // Build: enable switches - // --------------------------------------------------------------------------- - - function refreshEnableRow() { - const active = getEngine?.(); - for (let i = 0; i < MAX_ADSR; i++) { - const btn = refs.adsrEnables[i]; - if (!btn) continue; - const inCount = i < uiState.adsrCount; - btn.classList.toggle('active', !!uiState.adsrEnables[i]); - btn.classList.toggle('out-of-count', !inCount); - btn.title = inCount - ? `ADSR ${i + 1}: ${uiState.adsrEnables[i] ? 'enabled' : 'disabled'}` - : `ADSR ${i + 1}: slot past count (not MLP-driven)`; - } - for (let i = 0; i < MAX_LFO; i++) { - const btn = refs.lfoEnables[i]; - if (!btn) continue; - const inCount = i < uiState.lfoCount; - btn.classList.toggle('active', !!uiState.lfoEnables[i]); - btn.classList.toggle('out-of-count', !inCount); - btn.title = inCount - ? `LFO ${i + 1}: ${uiState.lfoEnables[i] ? 'enabled' : 'disabled'}` - : `LFO ${i + 1}: slot past count (not MLP-driven)`; - } - } - - function applyEnableChange(kind, i, enabled) { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - if (kind === 'adsr') uiState.adsrEnables[i] = enabled; - else uiState.lfoEnables[i] = enabled; - writeEnableToEngine(engine, kind, i, enabled); - saveUIState(); - refreshEnableRow(); - } - - function writeEnableToEngine(engine, kind, i, enabled) { - if (typeof engine._setRawByLabel !== 'function') return; - const label = (kind === 'adsr') - ? `MM_ADSR/${pad2(i)}_adsr${pad2(i + 1)}_enable` - : `MM_LFO/${pad2(i)}_lfo${pad2(i + 1)}_enable`; - engine._setRawByLabel(label, enabled ? 1.0 : 0.0); - } - - function applyAllEnablesToEngine() { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - for (let i = 0; i < MAX_ADSR; i++) { - writeEnableToEngine(engine, 'adsr', i, !!uiState.adsrEnables[i]); - } - for (let i = 0; i < MAX_LFO; i++) { - writeEnableToEngine(engine, 'lfo', i, !!uiState.lfoEnables[i]); - } - } - - // --------------------------------------------------------------------------- - // Matrix grid - // --------------------------------------------------------------------------- - - /** - * Given engine paramMeta, discover matrix cells by group prefix. - * Returns a Map: `${s}|${d}` -> paramMeta index. - */ - function buildMatrixIndex(engine) { - const out = new Map(); - const meta = engine?.paramMeta ?? []; - for (let i = 0; i < meta.length; i++) { - const m = meta[i]; - if (!m || !m.group || !m.group.startsWith('Matrix/')) continue; - // Label is like "MM_Matrix/s03_d08_amp" — parse out s and d. - const lbl = m.label || ''; - const match = lbl.match(/s(\d{2})_d(\d{2})_/); - if (!match) continue; - const s = parseInt(match[1], 10); - const d = parseInt(match[2], 10); - out.set(`${s}|${d}`, i); - } - return out; - } - - // Current cell values (signed [-1, 1]), keyed by "s|d". Populated from - // paramMeta entries on rebuild; mutated by user interaction. - const cellValues = new Map(); - - function getCell(s, d) { return cellValues.get(`${s}|${d}`) ?? 0; } - - function setCell(s, d, value, { persistToEngine = true } = {}) { - const v = Math.max(-1, Math.min(1, value)); - cellValues.set(`${s}|${d}`, v); - updateCellDOM(s, d, v); - if (!persistToEngine) return; - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - - // Prefer routing through paramMeta (engine.setParam) so the write - // is visible to getDefaultNormalizedOutputs() and to the a-app's - // inference routing path. The cell exists in paramMeta by default - // now that matrix cells are MLP-driven. Fall back to _setRawByLabel - // if the index lookup fails (e.g. sub-engine swap in flight). - const idx = matrixIndexCache.get(`${s}|${d}`); - if (idx != null) { - const meta = engine.paramMeta[idx]; - if (meta) { - const range = (meta.max - meta.min) || 1; - const norm = Math.max(0, Math.min(1, (v - meta.min) / range)); - engine.setParam(idx, norm); - return; - } - } - const destName = (engine.destNames || [])[d]; - if (!destName) return; - const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`; - engine._setRawByLabel?.(label, v); - } - - function cycleCell(s, d) { - const cur = getCell(s, d); - // Cycle positive steps only, wrapping back to 0. - // Negatives reached via right-click menu or precise editor. - let nextIdx = 0; - // Match to nearest current step, then advance. - if (cur > 0) { - let best = 0, bestD = Infinity; - for (let i = 0; i < CLICK_CYCLE.length; i++) { - const diff = Math.abs(CLICK_CYCLE[i] - cur); - if (diff < bestD) { bestD = diff; best = i; } - } - nextIdx = (best + 1) % CLICK_CYCLE.length; - } else { - // From 0 or negative: jump up to 0.25 to start the cycle. - nextIdx = 1; - } - setCell(s, d, CLICK_CYCLE[nextIdx]); - } - - let matrixIndexCache = new Map(); - - function rebuildMatrixGrid() { - const engine = getEngine?.(); - const gridEl = refs.matrixGrid; - if (!gridEl) return; - - gridEl.innerHTML = ''; - matrixIndexCache = new Map(); - cellValues.clear(); - - if (!engine || engine.id !== 'modular') { - if (refs.matrixEmpty) refs.matrixEmpty.textContent = 'Modular engine not active.'; - return; - } - - const destNames = engine.destNames || []; - if (destNames.length === 0) { - if (refs.matrixEmpty) refs.matrixEmpty.textContent = 'Waiting for sub-engine…'; - return; - } - - // Matrix cells may or may not be in paramMeta (they're opt-in for MLP - // control; default is that the matrix is a direct-DSP patch editor). - // Build the paramMeta index for cells that ARE exposed, so updateLive - // can mirror MLP outputs into them; other cells are direct-edit only. - matrixIndexCache = buildMatrixIndex(engine); - if (refs.matrixEmpty) refs.matrixEmpty.textContent = ''; - - // Seed current values from the engine's _lastRawByLabel map (which - // tracks every write via setParam / _setRawByLabel / default patch), - // falling back to the walk-entry init value for cells the user has - // never touched. (destNames is already in scope from the early guard.) - const lastRaw = engine._lastRawByLabel || new Map(); - for (let d = 0; d < destNames.length; d++) { - const destName = destNames[d]; - for (let s = 0; s < 48; s++) { - const label = `MM_Matrix/s${String(s).padStart(2, '0')}_d${String(d).padStart(2, '0')}_${destName}`; - const walk = engine._labelToWalk?.get?.(label); - if (!walk) continue; - const raw = lastRaw.has(label) ? lastRaw.get(label) : walk.init; - cellValues.set(`${s}|${d}`, raw); - } - } - - // Determine visible source range from current counts. - const adsrN = uiState.adsrCount; - const lfoN = uiState.lfoCount; - const visibleSources = []; - for (let i = 0; i < adsrN; i++) visibleSources.push({ s: i, label: `A${i + 1}`, kind: 'adsr' }); - for (let i = 0; i < lfoN; i++) visibleSources.push({ s: 16 + i, label: `L${i + 1}`, kind: 'lfo' }); - // NOTE: source index layout — sources 0..15 are ADSRs, 16..47 are LFOs. - // This mirrors the Faust .dsp's MM_* declaration order, VERIFIED by the - // walk-entry labels: matrix column s00..s15 are ADSRs, s16..s47 are LFOs. - - const cols = destNames.length; - const rows = visibleSources.length; - - // CSS grid: 1 header col + N dest cols × 1 header row + M source rows - gridEl.style.gridTemplateColumns = `auto repeat(${cols}, minmax(28px, 1fr))`; - gridEl.style.gridTemplateRows = `auto repeat(${rows}, 32px)`; - - // Top-left corner - const corner = document.createElement('div'); - corner.className = 'mm-corner'; - gridEl.appendChild(corner); - - // Column headers - destNames.forEach((name, d) => { - const h = document.createElement('div'); - h.className = 'mm-col-header'; - h.textContent = shortDest(name); - h.title = name; - gridEl.appendChild(h); - }); - - // Rows - visibleSources.forEach((src) => { - const rh = document.createElement('div'); - rh.className = `mm-row-header mm-src-${src.kind}`; - rh.textContent = src.label; - rh.title = src.kind === 'adsr' ? `ADSR ${src.s + 1}` : `LFO ${src.s - 15}`; - gridEl.appendChild(rh); - - for (let d = 0; d < cols; d++) { - const cell = document.createElement('div'); - cell.className = 'mm-cell'; - cell.dataset.s = src.s; - cell.dataset.d = d; - const has = matrixIndexCache.has(`${src.s}|${d}`); - if (!has) cell.classList.add('unavailable'); - attachCellHandlers(cell, src.s, d); - gridEl.appendChild(cell); - updateCellDOM(src.s, d, getCell(src.s, d)); - } - }); - } - - function updateCellDOM(s, d, v) { - const cell = refs.matrixGrid?.querySelector(`.mm-cell[data-s="${s}"][data-d="${d}"]`); - if (!cell) return; - const abs = Math.abs(v); - const label = v === 0 - ? '' - : (v > 0 ? '' : '−') + abs.toFixed(2).replace(/^0/, ''); - cell.textContent = label; - const alpha = Math.min(1, abs); - const color = v >= 0 - ? `rgba(255, 106, 0, ${alpha * 0.85})` // accent (positive) - : `rgba(80, 160, 255, ${alpha * 0.85})`; // blue (negative / inverted) - cell.style.background = alpha > 0.01 - ? color - : 'rgba(255, 255, 255, 0.04)'; - cell.classList.toggle('nonzero', alpha > 0.01); - } - - function attachCellHandlers(cell, s, d) { - let longPressTimer = null; - let didLongPress = false; - - cell.addEventListener('click', (e) => { - if (cell.classList.contains('unavailable')) return; - if (didLongPress) { didLongPress = false; return; } - if (e.shiftKey) { setCell(s, d, 0); return; } - cycleCell(s, d); - }); - - cell.addEventListener('contextmenu', (e) => { - e.preventDefault(); - if (cell.classList.contains('unavailable')) return; - openCellMenu(s, d, cell, e); - }); - - // Long-press → precise editor (mobile-friendly). - const startLong = () => { - longPressTimer = setTimeout(() => { - didLongPress = true; - openPreciseEditor(s, d, cell); - }, 500); - }; - const cancelLong = () => { - if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } - }; - cell.addEventListener('pointerdown', startLong); - cell.addEventListener('pointerup', cancelLong); - cell.addEventListener('pointerleave', cancelLong); - cell.addEventListener('pointercancel', cancelLong); - } - - function openPreciseEditor(s, d, cellEl) { - const cur = getCell(s, d); - const input = prompt(`Matrix s${pad2(s)} → d${pad2(d)} (range -1..1)`, String(cur)); - if (input == null) return; - const v = parseFloat(input); - if (!Number.isFinite(v)) return; - setCell(s, d, v); - } - - function openCellMenu(s, d, cellEl, ev) { - const existing = document.getElementById('mm-cell-menu'); - if (existing) existing.remove(); - - const menu = document.createElement('div'); - menu.id = 'mm-cell-menu'; - menu.className = 'mm-cell-menu'; - const actions = [ - { label: 'Set +1', fn: () => setCell(s, d, 1) }, - { label: 'Set +0.5', fn: () => setCell(s, d, 0.5) }, - { label: 'Set 0', fn: () => setCell(s, d, 0) }, - { label: 'Set -0.5', fn: () => setCell(s, d, -0.5) }, - { label: 'Set -1', fn: () => setCell(s, d, -1) }, - { label: 'Invert', fn: () => setCell(s, d, -getCell(s, d)) }, - { label: 'Random', fn: () => setCell(s, d, (Math.random() * 2) - 1) }, - { label: 'Precise…', fn: () => openPreciseEditor(s, d, cellEl) }, - ]; - for (const a of actions) { - const b = document.createElement('button'); - b.textContent = a.label; - b.addEventListener('click', () => { a.fn(); menu.remove(); }); - menu.appendChild(b); - } - - document.body.appendChild(menu); - const rect = cellEl.getBoundingClientRect(); - const x = Math.min(window.innerWidth - 180, rect.left); - const y = Math.min(window.innerHeight - menu.offsetHeight - 10, rect.bottom + 4); - menu.style.left = `${x}px`; - menu.style.top = `${y}px`; - - // Dismiss on outside click. - const dismiss = (e) => { - if (!menu.contains(e.target)) { - menu.remove(); - document.removeEventListener('pointerdown', dismiss, true); - } - }; - setTimeout(() => document.addEventListener('pointerdown', dismiss, true), 0); - } - - // --------------------------------------------------------------------------- - // Sound param exposure - // --------------------------------------------------------------------------- - - function rebuildSoundParamList() { - const list = refs.soundParamList; - if (!list) return; - list.innerHTML = ''; - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular' || - typeof engine.getEngineSoundParamLabels !== 'function') { - const note = document.createElement('div'); - note.className = 'mm-hint'; - note.textContent = 'Modular engine not active.'; - list.appendChild(note); - return; - } - const entries = engine.getEngineSoundParamLabels(); - if (entries.length === 0) { - const note = document.createElement('div'); - note.className = 'mm-hint'; - note.textContent = 'No engine sound params exposed by this sub-engine.'; - list.appendChild(note); - return; - } - - // Group by top-level prefix for readability. - const groups = new Map(); - for (const e of entries) { - const [group] = splitLabel(e.label); - if (!groups.has(group)) groups.set(group, []); - groups.get(group).push(e); - } - - for (const [group, params] of groups.entries()) { - const h = document.createElement('div'); - h.className = 'mm-sound-group'; - h.textContent = prettyGroup(group); - list.appendChild(h); - - for (const p of params) { - const row = document.createElement('label'); - row.className = 'mm-sound-row'; - const cb = document.createElement('input'); - cb.type = 'checkbox'; - cb.checked = engine.isEngineParamExposed?.(p.label) ?? false; - cb.addEventListener('change', () => applyExposeChange(p.label, cb.checked)); - const name = document.createElement('span'); - name.textContent = stripIndexPrefix(splitLabel(p.label)[1]); - name.title = p.label; - row.appendChild(cb); - row.appendChild(name); - list.appendChild(row); - } - } - - const warning = document.createElement('div'); - warning.className = 'mm-hint warn'; - warning.textContent = - 'Toggling exposes/unexposes a param to the MLP. The network is resized ' + - 'in place (training examples are preserved; output layer is warm-started).'; - list.appendChild(warning); - } - - // --------------------------------------------------------------------------- - // Preset overlay (Phase E) - // --------------------------------------------------------------------------- - - async function openPresetOverlay() { - const existing = document.getElementById('mm-preset-overlay'); - if (existing) existing.remove(); - - const overlay = document.createElement('div'); - overlay.id = 'mm-preset-overlay'; - overlay.className = 'mm-preset-overlay'; - - const panel = document.createElement('div'); - panel.className = 'mm-preset-panel'; - - const header = document.createElement('div'); - header.className = 'mm-preset-header'; - const title = document.createElement('div'); - title.className = 'mm-preset-title'; - title.textContent = 'Modular presets'; - const closeBtn = document.createElement('button'); - closeBtn.className = 'mm-preset-close'; - closeBtn.innerHTML = '×'; - closeBtn.addEventListener('click', () => overlay.remove()); - header.appendChild(title); - header.appendChild(closeBtn); - panel.appendChild(header); - - const list = document.createElement('div'); - list.className = 'mm-preset-list'; - for (const preset of MODULAR_PRESETS) { - const item = document.createElement('button'); - item.className = 'mm-preset-item'; - item.dataset.presetId = preset.id; - const name = document.createElement('div'); - name.className = 'mm-preset-name'; - name.textContent = preset.name; - const desc = document.createElement('div'); - desc.className = 'mm-preset-desc'; - desc.textContent = preset.description; - const sub = document.createElement('div'); - sub.className = 'mm-preset-sub'; - sub.textContent = preset.state?.subEngine ?? ''; - item.appendChild(name); - item.appendChild(desc); - item.appendChild(sub); - item.addEventListener('click', async () => { - item.classList.add('busy'); - try { - const engine = getEngine?.(); - if (engine && engine.id === 'modular') { - await applyPreset(engine, preset); - // Sync Phase C UI state to reflect the preset's sub-engine - // and counts so the rebuild that follows matches the engine. - if (preset.state?.subEngine) uiState.subEngine = preset.state.subEngine; - if (typeof preset.state?.adsrCount === 'number') { - uiState.adsrCount = preset.state.adsrCount; - } - if (typeof preset.state?.lfoCount === 'number') { - uiState.lfoCount = preset.state.lfoCount; - } - saveUIState(); - onStateChange?.(); // trigger a-app saveState -> persists DSP snapshot - refresh(); - } - } catch (err) { - console.error('[modular-ui] preset apply failed:', err); - } finally { - item.classList.remove('busy'); - overlay.remove(); - } - }); - list.appendChild(item); - } - panel.appendChild(list); - overlay.appendChild(panel); - - // Dismiss on backdrop click - overlay.addEventListener('click', (e) => { - if (e.target === overlay) overlay.remove(); - }); - - document.body.appendChild(overlay); - } - - function applyExposeChange(label, exposed) { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - engine.setExposeEngineParam?.(label, exposed); - if (exposed) { - if (!uiState.exposedEngineParams.includes(label)) { - uiState.exposedEngineParams.push(label); - } - } else { - uiState.exposedEngineParams = uiState.exposedEngineParams.filter(l => l !== label); - } - saveUIState(); - // paramMeta:change listener rebuilds the matrix & list. - } - - // --------------------------------------------------------------------------- - // Lifecycle - // --------------------------------------------------------------------------- - - function show() { - dockIcon.classList.remove('hidden'); - const engine = getEngine?.(); - if (engine && engine.id === 'modular') bindEngine(engine); - refresh(); - } - - function hide() { - dockIcon.classList.add('hidden'); - dockIcon.classList.remove('active'); - drawer.root.classList.add('hidden'); - } - - function isVisible() { - return !dockIcon.classList.contains('hidden'); - } - - function refresh() { - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - bindEngine(engine); - - // One-time apply of persisted state to the engine. - if (pendingRestore) { - pendingRestore = false; - try { - if (uiState.subEngine && uiState.subEngine !== engine.activeSubEngineId - && engine.availableSubEngines?.includes(uiState.subEngine)) { - engine.setSubEngine?.(uiState.subEngine); - } - // Sync counts (UI persisted → engine). Guard if method missing. - if (typeof engine.setModSourceCount === 'function') { - engine.setModSourceCount(uiState.adsrCount, uiState.lfoCount); - } - // Re-expose saved sound params. - for (const label of uiState.exposedEngineParams) { - engine.setExposeEngineParam?.(label, true); - } - applyAllEnablesToEngine(); - } catch (err) { - console.warn('[modular-ui] restore failed:', err); - } - } - - // Keep UI widgets in sync with engine state (in case engine was - // mutated outside of our control — e.g. preset load). - if (engine.adsrCount != null) uiState.adsrCount = engine.adsrCount; - if (engine.lfoCount != null) uiState.lfoCount = engine.lfoCount; - - refreshSubEngineButtons(); - refreshCountValues(); - refreshEnableRow(); - rebuildMatrixGrid(); - rebuildSoundParamList(); - } - - function teardown() { - if (unsubParamMeta) { try { unsubParamMeta(); } catch (_) { /* ignore */ } } - unsubParamMeta = null; - dockIcon.remove(); - drawer.root.remove(); - const style = document.getElementById('modular-ui-styles'); - if (style) style.remove(); - } - - // Start hidden (engine-gated). - hide(); - - // Listen for close button on our drawer so the dock icon state stays synced. - drawer.closeBtn.addEventListener('click', () => { - drawer.root.classList.add('hidden'); - dockIcon.classList.remove('active'); - }); - - // Dock icon opens/closes our drawer. We piggy-back on the existing - // wireDock() event delegation — but since our drawer's id is `drawer-modular` - // and our icon uses `data-drawer="modular"`, wireDock() already handles it. - // However, wireDock was wired at init time — any new dock-click delegation - // works because it's attached to #dock via event delegation. - - // Live-update throttle: the MLP can push ~60 Hz of new output vectors, but - // rewriting ~120 visible DOM cells that often is wasteful. Cap to ~20 fps. - const LIVE_UPDATE_INTERVAL_MS = 50; - let _lastLiveUpdate = 0; - - /** - * Called from the inference loop with the MLP output vector (normalized - * [0,1]). Updates visible matrix cell DOM to reflect live values without - * touching the engine (engine.setParam is already being called on the - * same tick by routeOutputs). - */ - function updateLive(outputs) { - if (!isVisible()) return; - if (!outputs || outputs.length === 0) return; - const engine = getEngine?.(); - if (!engine || engine.id !== 'modular') return; - if (matrixIndexCache.size === 0) return; - - const now = performance.now(); - if (now - _lastLiveUpdate < LIVE_UPDATE_INTERVAL_MS) return; - _lastLiveUpdate = now; - - const meta = engine.paramMeta; - const adsrN = uiState.adsrCount; - const lfoN = uiState.lfoCount; - const nVisibleSources = adsrN + lfoN; - - for (const [key, idx] of matrixIndexCache.entries()) { - // Only update cells whose source is currently visible. - const barIdx = key.indexOf('|'); - const s = +key.slice(0, barIdx); - if (s >= 16) { - // LFO: sources 16..47 map to LFO slots 0..31 - if ((s - 16) >= lfoN) continue; - } else if (s >= adsrN) { - continue; - } - const m = meta[idx]; - if (!m) continue; - const norm = outputs[idx]; - if (norm == null) continue; - const raw = m.min + norm * (m.max - m.min); - cellValues.set(key, raw); - updateCellDOM(+key.slice(0, barIdx), +key.slice(barIdx + 1), raw); - } - } - - return { - teardown, - refresh, - show, - hide, - isVisible, - updateLive, - /** Exposed for testability. */ - _debug: { - getCell, - setCell, - cycleCell, - uiState: () => ({ ...uiState }), - }, - }; - - // =========================================================================== - // Section builders - // =========================================================================== - - function buildSubEngineSection(body, refs, onChange) { - const section = el('div', 'mm-section'); - section.appendChild(el('div', 'mm-section-header', 'Sub-engine')); - - const row = el('div', 'mm-row mm-sub-row'); - for (const opt of SUB_ENGINE_OPTIONS) { - const b = el('button', 'mm-sub-btn', opt.label); - b.dataset.subEngine = opt.id; - b.addEventListener('click', () => onChange(opt.id)); - refs.subEngineBtns.push(b); - row.appendChild(b); - } - section.appendChild(row); - body.appendChild(section); - } - - function buildPresetsSection(body, onOpen) { - const section = el('div', 'mm-section'); - const header = el('div', 'mm-section-header', 'Presets'); - section.appendChild(header); - const btn = el('button', 'mm-preset-open-btn', 'Load preset…'); - btn.addEventListener('click', onOpen); - section.appendChild(btn); - body.appendChild(section); - } - - function buildCountSection(body, refs, onCountChange) { - const section = el('div', 'mm-section'); - section.appendChild(el('div', 'mm-section-header', 'Mod Pool')); - - const hint = el('div', 'mm-hint', - 'Slots past the count stay alive in the DSP but are not MLP-driven. ' + - 'Changing counts reinitialises the MLP.'); - section.appendChild(hint); - - const mkStepper = (labelText, kind, max) => { - const row = el('div', 'mm-row mm-stepper-row'); - row.appendChild(el('label', 'mm-label', labelText)); - const dec = el('button', 'mm-step-btn', '−'); - const val = el('span', 'mm-step-val', String(uiState[kind === 'adsr' ? 'adsrCount' : 'lfoCount'])); - const inc = el('button', 'mm-step-btn', '+'); - dec.addEventListener('click', () => { - const cur = parseInt(val.textContent, 10); - if (cur > 1) onCountChange(kind, cur - 1); - }); - inc.addEventListener('click', () => { - const cur = parseInt(val.textContent, 10); - if (cur < max) onCountChange(kind, cur + 1); - }); - row.appendChild(dec); - row.appendChild(val); - row.appendChild(inc); - row.appendChild(el('span', 'mm-step-range', `(1..${max})`)); - if (kind === 'adsr') refs.adsrCountValue = val; - else refs.lfoCountValue = val; - return row; - }; - - section.appendChild(mkStepper('ADSR count', 'adsr', MAX_ADSR)); - section.appendChild(mkStepper('LFO count', 'lfo', MAX_LFO)); - body.appendChild(section); - } - - function buildEnableSection(body, refs, onEnableChange) { - const section = el('div', 'mm-section'); - section.appendChild(el('div', 'mm-section-header', 'Slot enables')); - - const mkRow = (kind, max) => { - const wrap = el('div', 'mm-enable-wrap'); - wrap.appendChild(el('div', 'mm-enable-label', kind.toUpperCase())); - const row = el('div', 'mm-enable-row'); - for (let i = 0; i < max; i++) { - const b = el('button', `mm-enable-btn mm-enable-${kind}`, String(i + 1)); - b.addEventListener('click', () => { - const enabled = !b.classList.contains('active'); - onEnableChange(kind, i, enabled); - }); - if (kind === 'adsr') refs.adsrEnables.push(b); - else refs.lfoEnables.push(b); - row.appendChild(b); - } - wrap.appendChild(row); - return wrap; - }; - - section.appendChild(mkRow('adsr', MAX_ADSR)); - section.appendChild(mkRow('lfo', MAX_LFO)); - body.appendChild(section); - } - - function buildMatrixSection(body, refs, handlers) { - const section = el('div', 'mm-section mm-matrix-section'); - const header = el('div', 'mm-section-header', 'Matrix'); - // Phase E: the tap-vs-drag mode toggle stub was deleted — drag-to-paint - // is non-goal for Phase E and the button did nothing functional. - section.appendChild(header); - - const legend = el('div', 'mm-hint', - 'Click to cycle 0 → 0.25 → 0.5 → 0.75 → 1 → 0. ' + - 'Shift-click: zero. Long-press: precise. Right-click: menu (negatives).'); - section.appendChild(legend); - - const scrollWrap = el('div', 'mm-scroll'); - const grid = el('div', 'mm-grid'); - refs.matrixGrid = grid; - scrollWrap.appendChild(grid); - section.appendChild(scrollWrap); - - const empty = el('div', 'mm-hint warn', ''); - refs.matrixEmpty = empty; - section.appendChild(empty); - - body.appendChild(section); - } - - function buildSoundParamSection(body, refs, onExposeChange) { - const section = el('div', 'mm-section'); - section.appendChild(el('div', 'mm-section-header', 'Engine sound params')); - const list = el('div', 'mm-sound-list'); - refs.soundParamList = list; - section.appendChild(list); - body.appendChild(section); - } - - // =========================================================================== - // localStorage - // =========================================================================== - - function loadUIState() { - const defaults = { - subEngine: 'subtractive', - adsrCount: 4, - lfoCount: 8, - adsrEnables: Array.from({ length: MAX_ADSR }, (_, i) => i < 4), - lfoEnables: Array.from({ length: MAX_LFO }, (_, i) => i < 8), - exposedEngineParams: [], - }; - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return defaults; - const s = JSON.parse(raw); - return { - subEngine: typeof s.subEngine === 'string' ? s.subEngine : defaults.subEngine, - adsrCount: clamp(s.adsrCount, 1, MAX_ADSR, defaults.adsrCount), - lfoCount: clamp(s.lfoCount, 1, MAX_LFO, defaults.lfoCount), - adsrEnables: padBool(s.adsrEnables, MAX_ADSR, defaults.adsrEnables), - lfoEnables: padBool(s.lfoEnables, MAX_LFO, defaults.lfoEnables), - exposedEngineParams: Array.isArray(s.exposedEngineParams) ? s.exposedEngineParams.slice() : [], - }; - } catch (_) { - return defaults; - } - } - - function saveUIState() { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(uiState)); - onStateChange?.(); - } catch (err) { - console.warn('[modular-ui] saveUIState failed:', err); - } - } -} - -// --------------------------------------------------------------------------- -// DOM scaffolding -// --------------------------------------------------------------------------- - -function createDockIcon() { - const dock = document.getElementById('dock'); - if (!dock) throw new Error('[modular-ui] #dock not found'); - - // Insert before the help icon if present, else append. - const helpIcon = dock.querySelector('.dock-icon[data-drawer="help"]'); - const btn = document.createElement('button'); - btn.className = 'dock-icon hidden'; - btn.dataset.drawer = 'modular'; - btn.title = 'Modular mod pool & matrix'; - btn.innerHTML = ` - - - - - - - - - - - - Mod - `; - if (helpIcon) dock.insertBefore(btn, helpIcon); - else dock.appendChild(btn); - return btn; -} - -function createDrawer() { - const stack = document.getElementById('drawer-stack'); - if (!stack) throw new Error('[modular-ui] #drawer-stack not found'); - - const root = document.createElement('div'); - root.className = 'drawer hidden mm-drawer'; - root.id = 'drawer-modular'; - root.dataset.drawer = 'modular'; - - const header = document.createElement('div'); - header.className = 'drawer-header'; - const title = document.createElement('span'); - title.textContent = 'Modular'; - const closeBtn = document.createElement('button'); - closeBtn.className = 'drawer-close'; - closeBtn.dataset.drawer = 'modular'; - closeBtn.innerHTML = '×'; - header.appendChild(title); - header.appendChild(closeBtn); - - const body = document.createElement('div'); - body.className = 'drawer-body mm-body'; - - root.appendChild(header); - root.appendChild(body); - stack.appendChild(root); - - return { root, header, body, closeBtn }; -} - -// --------------------------------------------------------------------------- -// Styles -// --------------------------------------------------------------------------- - -function injectStyles() { - if (document.getElementById('modular-ui-styles')) return; - const s = document.createElement('style'); - s.id = 'modular-ui-styles'; - s.textContent = ` - .mm-drawer .mm-body { - display: flex; - flex-direction: column; - gap: 12px; - overflow-y: auto; - overflow-x: hidden; - padding: 8px 10px 16px; - max-height: 85vh; - } - .mm-section { - display: flex; - flex-direction: column; - gap: 6px; - padding: 8px; - border: 1px solid var(--glass-border, rgba(255,255,255,0.08)); - border-radius: var(--radius-sm, 8px); - background: rgba(255,255,255,0.02); - } - .mm-section-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-dim, #888); - margin-bottom: 2px; - } - .mm-row { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; - } - .mm-label { - flex: 0 0 auto; - font-size: 0.75rem; - color: var(--text-dim, #888); - } - .mm-hint { - font-size: 0.65rem; - color: var(--text-dim, #888); - line-height: 1.3; - } - .mm-hint.warn { - color: #ffb070; - } - - /* Sub-engine buttons */ - .mm-sub-row { gap: 4px; } - .mm-sub-btn { - flex: 1; - padding: 8px 10px; - background: rgba(255,255,255,0.04); - border: 1px solid var(--glass-border, rgba(255,255,255,0.1)); - border-radius: 6px; - color: var(--text, #e0e0e0); - font-size: 0.75rem; - cursor: pointer; - min-height: 34px; - touch-action: manipulation; - } - .mm-sub-btn:hover:not(:disabled) { border-color: var(--accent, #ff6a00); } - .mm-sub-btn.active { - background: var(--accent-dim, rgba(255,106,0,0.25)); - border-color: var(--accent, #ff6a00); - color: var(--accent, #ff6a00); - } - .mm-sub-btn:disabled { - opacity: 0.35; - cursor: not-allowed; - } - .mm-sub-btn.busy { opacity: 0.6; } - - /* Steppers */ - .mm-stepper-row { - display: flex; - align-items: center; - gap: 6px; - } - .mm-stepper-row .mm-label { flex: 1; } - .mm-step-btn { - width: 32px; - height: 32px; - background: rgba(255,255,255,0.04); - border: 1px solid var(--glass-border, rgba(255,255,255,0.1)); - border-radius: 6px; - color: var(--text, #e0e0e0); - cursor: pointer; - font-size: 1rem; - touch-action: manipulation; - } - .mm-step-btn:hover { border-color: var(--accent, #ff6a00); } - .mm-step-val { - display: inline-block; - min-width: 28px; - text-align: center; - font-variant-numeric: tabular-nums; - font-weight: 500; - } - .mm-step-range { - font-size: 0.65rem; - color: var(--text-dim, #888); - margin-left: 4px; - } - - /* Enable rows */ - .mm-enable-wrap { - display: flex; - flex-direction: column; - gap: 3px; - } - .mm-enable-label { - font-size: 0.6rem; - color: var(--text-dim, #888); - text-transform: uppercase; - letter-spacing: 0.08em; - } - .mm-enable-row { - display: flex; - flex-wrap: wrap; - gap: 3px; - } - .mm-enable-btn { - width: 24px; - height: 24px; - font-size: 0.6rem; - background: rgba(255,255,255,0.04); - border: 1px solid var(--glass-border, rgba(255,255,255,0.1)); - border-radius: 4px; - color: var(--text-dim, #888); - cursor: pointer; - padding: 0; - touch-action: manipulation; - } - .mm-enable-btn.active { - background: var(--accent-dim, rgba(255,106,0,0.25)); - border-color: var(--accent, #ff6a00); - color: var(--accent, #ff6a00); - } - .mm-enable-btn.out-of-count { - opacity: 0.35; - } - - /* Matrix grid */ - .mm-matrix-section { - /* Allow this section to take extra vertical space */ - } - .mm-mode-toggle { - font-size: 0.6rem; - padding: 2px 6px; - background: rgba(255,255,255,0.04); - border: 1px solid var(--glass-border, rgba(255,255,255,0.1)); - border-radius: 4px; - color: var(--text-dim, #888); - cursor: pointer; - touch-action: manipulation; - } - .mm-scroll { - overflow-x: auto; - overflow-y: auto; - max-height: 360px; - border-radius: 4px; - border: 1px solid var(--glass-border, rgba(255,255,255,0.06)); - } - .mm-grid { - display: grid; - gap: 1px; - background: rgba(255,255,255,0.05); - padding: 1px; - min-width: max-content; - touch-action: manipulation; - } - .mm-corner, - .mm-col-header, - .mm-row-header { - background: #0d0d0d; - color: var(--text-dim, #888); - font-size: 0.55rem; - display: flex; - align-items: center; - justify-content: center; - padding: 2px 4px; - text-transform: uppercase; - letter-spacing: 0.05em; - position: sticky; - z-index: 2; - } - .mm-col-header { - top: 0; - writing-mode: vertical-rl; - transform: rotate(180deg); - min-height: 48px; - min-width: 28px; - } - .mm-row-header { - left: 0; - min-width: 32px; - font-weight: 500; - } - .mm-row-header.mm-src-adsr { color: #ffb070; } - .mm-row-header.mm-src-lfo { color: #6bb6ff; } - .mm-corner { - top: 0; - left: 0; - z-index: 3; - } - .mm-cell { - background: rgba(255,255,255,0.04); - cursor: pointer; - font-size: 0.55rem; - font-variant-numeric: tabular-nums; - display: flex; - align-items: center; - justify-content: center; - min-height: 32px; - min-width: 28px; - user-select: none; - color: #fff; - transition: background 120ms ease; - } - .mm-cell:hover:not(.unavailable) { - outline: 1px solid rgba(255,255,255,0.25); - } - .mm-cell.unavailable { - cursor: not-allowed; - background: repeating-linear-gradient( - 45deg, - rgba(255,255,255,0.02), - rgba(255,255,255,0.02) 4px, - rgba(255,255,255,0.05) 4px, - rgba(255,255,255,0.05) 8px - ); - } - - /* Context menu */ - .mm-cell-menu { - position: fixed; - background: #0d0d0d; - border: 1px solid var(--glass-border, rgba(255,255,255,0.15)); - border-radius: 6px; - padding: 4px; - display: flex; - flex-direction: column; - gap: 2px; - box-shadow: 0 4px 16px rgba(0,0,0,0.5); - z-index: 9999; - min-width: 140px; - } - .mm-cell-menu button { - background: transparent; - border: none; - color: var(--text, #e0e0e0); - text-align: left; - padding: 6px 10px; - font-size: 0.7rem; - border-radius: 4px; - cursor: pointer; - touch-action: manipulation; - } - .mm-cell-menu button:hover { - background: var(--accent-dim, rgba(255,106,0,0.25)); - color: var(--accent, #ff6a00); - } - - /* Sound params list */ - .mm-sound-list { - display: flex; - flex-direction: column; - gap: 4px; - max-height: 260px; - overflow-y: auto; - } - .mm-sound-group { - font-size: 0.6rem; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-dim, #888); - margin-top: 6px; - padding-bottom: 2px; - border-bottom: 1px solid var(--glass-border, rgba(255,255,255,0.06)); - } - .mm-sound-row { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.72rem; - color: var(--text, #e0e0e0); - padding: 3px 2px; - cursor: pointer; - } - .mm-sound-row input[type="checkbox"] { - accent-color: var(--accent, #ff6a00); - width: 16px; - height: 16px; - } - - /* Preset overlay (Phase E) */ - .mm-preset-open-btn { - padding: 8px 12px; - background: rgba(255,255,255,0.04); - border: 1px solid var(--glass-border, rgba(255,255,255,0.1)); - border-radius: 6px; - color: var(--text, #e0e0e0); - cursor: pointer; - font-size: 0.75rem; - text-align: center; - touch-action: manipulation; - } - .mm-preset-open-btn:hover { - border-color: var(--accent, #ff6a00); - color: var(--accent, #ff6a00); - } - .mm-preset-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - padding: 16px; - } - .mm-preset-panel { - background: #0d0d0d; - border: 1px solid var(--glass-border, rgba(255,255,255,0.15)); - border-radius: 10px; - width: min(420px, 100%); - max-height: 85vh; - display: flex; - flex-direction: column; - overflow: hidden; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7); - } - .mm-preset-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid var(--glass-border, rgba(255,255,255,0.08)); - } - .mm-preset-title { - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text, #e0e0e0); - } - .mm-preset-close { - background: transparent; - border: none; - color: var(--text-dim, #888); - font-size: 1.3rem; - cursor: pointer; - padding: 0 6px; - } - .mm-preset-close:hover { color: var(--accent, #ff6a00); } - .mm-preset-list { - display: flex; - flex-direction: column; - gap: 1px; - background: rgba(255,255,255,0.04); - overflow-y: auto; - } - .mm-preset-item { - background: #0d0d0d; - border: none; - color: var(--text, #e0e0e0); - text-align: left; - padding: 10px 16px; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 2px; - touch-action: manipulation; - } - .mm-preset-item:hover { - background: var(--accent-dim, rgba(255,106,0,0.15)); - } - .mm-preset-item.busy { opacity: 0.5; } - .mm-preset-name { - font-size: 0.85rem; - font-weight: 500; - color: var(--accent, #ff6a00); - } - .mm-preset-desc { - font-size: 0.7rem; - color: var(--text-dim, #888); - line-height: 1.3; - } - .mm-preset-sub { - font-size: 0.6rem; - color: var(--text-dim, #666); - text-transform: uppercase; - letter-spacing: 0.08em; - } - - /* Mobile tweaks */ - @media (max-width: 640px) { - .mm-drawer .mm-body { padding: 6px 8px 12px; gap: 10px; } - .mm-cell { min-width: 26px; min-height: 28px; font-size: 0.5rem; } - .mm-scroll { max-height: 300px; } - } - `; - document.head.appendChild(s); -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function el(tag, className, text) { - const e = document.createElement(tag); - if (className) e.className = className; - if (text != null) e.textContent = text; - return e; -} - -function pad2(n) { - return String(n).padStart(2, '0'); -} - -function clamp(v, lo, hi, fallback) { - const n = typeof v === 'number' ? v : parseInt(v, 10); - if (!Number.isFinite(n)) return fallback; - return Math.max(lo, Math.min(hi, n)); -} - -function padBool(arr, len, fallback) { - const src = Array.isArray(arr) ? arr : fallback; - const out = new Array(len); - for (let i = 0; i < len; i++) out[i] = !!src[i]; - return out; -} - -function splitLabel(label) { - const i = label.lastIndexOf('/'); - if (i < 0) return ['', label]; - return [label.slice(0, i), label.slice(i + 1)]; -} - -function stripIndexPrefix(leaf) { - return leaf.replace(/^\d+_/, ''); -} - -function prettyGroup(group) { - // "1_Oscillators" → "Oscillators" - return group.replace(/^\d+_/, '').replace(/_/g, ' '); -} - -function shortDest(name) { - // Truncate long destination names for column headers. - if (name.length <= 10) return name; - return name.slice(0, 9) + '…'; -} diff --git a/playground/js/ui/output-pipeline.js b/playground/js/ui/output-pipeline.js deleted file mode 100644 index 14a44fc..0000000 --- a/playground/js/ui/output-pipeline.js +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Output Pipeline — processes MLP outputs before they reach the synth/visualizer. - * - * Pipeline stages (in order): - * 1. Global Curve — power curve applied to all outputs - * 2. Output Smoothing — per-output EMA (frame-rate-independent) - * 3. Slew Rate Limiting — max change per second per output - * 4. Freeze Gate — when frozen, outputs don't update - * - * Pure math module — no DOM dependencies. - * - * @module output-pipeline - */ - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const DEFAULT_GLOBAL_CURVE = 1.0; -const GLOBAL_CURVE_MIN = 0.2; -const GLOBAL_CURVE_MAX = 5.0; - -const DEFAULT_SMOOTHING = 0; -const SMOOTHING_MAX = 0.95; - -const DEFAULT_SLEW_RATE = Infinity; // unlimited -const SLEW_RATE_MIN = 0.005; - -const REFERENCE_DT = 1 / 60; // 60fps reference for frame-rate-independent EMA - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -function clamp(v, lo, hi) { - return v < lo ? lo : v > hi ? hi : v; -} - -/** - * Frame-rate-independent EMA. - * Same approach as input pipeline: converts per-frame smoothing factor into - * a time-domain factor so perceived smoothing is consistent regardless of frame rate. - */ -function emaSmooth(prev, raw, smoothing, dt) { - if (smoothing <= 0) return raw; - const effectiveDt = dt > 0 ? dt : REFERENCE_DT; - const alpha = 1 - smoothing; - const alphaEff = 1 - Math.pow(1 - alpha, effectiveDt / REFERENCE_DT); - return prev + alphaEff * (raw - prev); -} - -// --------------------------------------------------------------------------- -// OutputPipeline -// --------------------------------------------------------------------------- - -export class OutputPipeline { - /** - * @param {number} [numOutputs=126] — number of MLP outputs - */ - constructor(numOutputs = 126) { - this._numOutputs = numOutputs; - - // Configuration - this._globalCurve = DEFAULT_GLOBAL_CURVE; - this._smoothing = DEFAULT_SMOOTHING; - this._slewRate = DEFAULT_SLEW_RATE; - this._frozen = false; - - // Per-output state - this._smoothed = new Float32Array(numOutputs); - this._lastOutput = new Float32Array(numOutputs); - this._initialized = false; - - // Per-output freeze mask (for parameter pinning) - this._outputFrozen = new Uint8Array(numOutputs); // 0 = not frozen, 1 = frozen - - // Working buffer for processed output (avoid allocation per frame) - this._processed = new Float32Array(numOutputs); - - // Store raw outputs separately (for heatmap preview while globally frozen) - this._lastRawAfterCurve = new Float32Array(numOutputs); - } - - // ----------------------------------------------------------------------- - // Main processing - // ----------------------------------------------------------------------- - - /** - * Process raw MLP outputs through the pipeline. - * - * @param {number[]|Float32Array} rawOutputs — raw MLP output values in [0,1] - * @param {number} deltaTime — time since last call in seconds (e.g. 0.016) - * @returns {Float32Array} — processed outputs - */ - process(rawOutputs, deltaTime) { - const dt = Math.max(0, deltaTime || 0); - const n = Math.min(rawOutputs.length, this._numOutputs); - - // On first call, initialize smoothed state to raw values - if (!this._initialized) { - for (let i = 0; i < n; i++) { - this._smoothed[i] = rawOutputs[i]; - this._lastOutput[i] = rawOutputs[i]; - } - this._initialized = true; - } - - // Stage 1: Global Curve - for (let i = 0; i < n; i++) { - const raw = clamp(rawOutputs[i], 0, 1); - this._lastRawAfterCurve[i] = this._globalCurve === 1.0 - ? raw - : Math.pow(raw, this._globalCurve); - } - - // If globally frozen, return the last non-frozen output - // (raw-after-curve is still updated for preview access) - if (this._frozen) { - this._processed.set(this._lastOutput); - return this._processed; - } - - for (let i = 0; i < n; i++) { - // Per-output freeze: skip this output if individually pinned - if (this._outputFrozen[i]) { - this._processed[i] = this._lastOutput[i]; - continue; - } - - let value = this._lastRawAfterCurve[i]; - - // Stage 2: Output Smoothing (EMA) - value = emaSmooth(this._smoothed[i], value, this._smoothing, dt); - this._smoothed[i] = value; - - // Stage 3: Slew Rate Limiting - if (this._slewRate !== Infinity && isFinite(this._slewRate)) { - const maxDelta = this._slewRate * dt; - if (maxDelta > 0) { - const delta = value - this._lastOutput[i]; - if (Math.abs(delta) > maxDelta) { - value = this._lastOutput[i] + Math.sign(delta) * maxDelta; - } - } - } - - value = clamp(value, 0, 1); - this._processed[i] = value; - this._lastOutput[i] = value; - } - - return this._processed; - } - - // ----------------------------------------------------------------------- - // Configuration - // ----------------------------------------------------------------------- - - /** - * Set global curve exponent. - * <1 pushes outputs toward extremes (0 and 1), >1 pushes toward center. - * @param {number} exponent — 0.2 to 5.0, default 1.0 (linear) - */ - setGlobalCurve(exponent) { - this._globalCurve = clamp(exponent, GLOBAL_CURVE_MIN, GLOBAL_CURVE_MAX); - } - - /** @returns {number} current global curve exponent */ - getGlobalCurve() { - return this._globalCurve; - } - - /** - * Set output smoothing factor (EMA). - * @param {number} factor — 0 (off) to 0.95 - */ - setSmoothing(factor) { - this._smoothing = clamp(factor, 0, SMOOTHING_MAX); - } - - /** @returns {number} current smoothing factor */ - getSmoothing() { - return this._smoothing; - } - - /** - * Set slew rate limit (max change per second per output). - * @param {number} maxChangePerSec — 0.005 to Infinity, default Infinity (unlimited) - */ - setSlewRate(maxChangePerSec) { - if (maxChangePerSec >= 1.0 || !isFinite(maxChangePerSec)) { - this._slewRate = Infinity; - } else { - this._slewRate = Math.max(SLEW_RATE_MIN, maxChangePerSec); - } - } - - /** @returns {number} current slew rate */ - getSlewRate() { - return this._slewRate; - } - - /** - * Set global freeze state. - * When frozen, process() returns the last non-frozen output. - * @param {boolean} frozen - */ - setFrozen(frozen) { - this._frozen = !!frozen; - } - - // ----------------------------------------------------------------------- - // State queries - // ----------------------------------------------------------------------- - - /** @returns {boolean} whether output is globally frozen */ - isFrozen() { - return this._frozen; - } - - /** @returns {Float32Array} last processed (post-pipeline) output */ - getLastOutput() { - return new Float32Array(this._lastOutput); - } - - /** - * Get the last raw-after-curve output (useful for heatmap preview while frozen). - * @returns {Float32Array} - */ - getRawPreview() { - return new Float32Array(this._lastRawAfterCurve); - } - - // ----------------------------------------------------------------------- - // Per-output freeze (parameter pinning) - // ----------------------------------------------------------------------- - - /** - * Freeze a specific output index (its value will not update). - * @param {number} index - */ - freezeOutput(index) { - if (index >= 0 && index < this._numOutputs) { - this._outputFrozen[index] = 1; - } - } - - /** - * Unfreeze a specific output index. - * @param {number} index - */ - unfreezeOutput(index) { - if (index >= 0 && index < this._numOutputs) { - this._outputFrozen[index] = 0; - } - } - - /** - * Check if a specific output is frozen. - * @param {number} index - * @returns {boolean} - */ - isOutputFrozen(index) { - return index >= 0 && index < this._numOutputs && this._outputFrozen[index] === 1; - } - - // ----------------------------------------------------------------------- - // Serialization - // ----------------------------------------------------------------------- - - /** Export configuration as a plain object (no internal state). */ - getConfig() { - return { - globalCurve: this._globalCurve, - smoothing: this._smoothing, - slewRate: this._slewRate === Infinity ? 1.0 : this._slewRate, - frozen: this._frozen, - frozenOutputs: Array.from(this._outputFrozen), - }; - } - - /** Restore configuration from a plain object. */ - setConfig(config) { - if (config.globalCurve != null) this.setGlobalCurve(config.globalCurve); - if (config.smoothing != null) this.setSmoothing(config.smoothing); - if (config.slewRate != null) this.setSlewRate(config.slewRate); - if (config.frozen != null) this.setFrozen(config.frozen); - if (Array.isArray(config.frozenOutputs)) { - for (let i = 0; i < config.frozenOutputs.length && i < this._numOutputs; i++) { - this._outputFrozen[i] = config.frozenOutputs[i] ? 1 : 0; - } - } - } - - // ----------------------------------------------------------------------- - // Reset - // ----------------------------------------------------------------------- - - /** Reset all settings to defaults and clear internal state. */ - reset() { - this._globalCurve = DEFAULT_GLOBAL_CURVE; - this._smoothing = DEFAULT_SMOOTHING; - this._slewRate = DEFAULT_SLEW_RATE; - this._frozen = false; - this._smoothed.fill(0); - this._lastOutput.fill(0); - this._lastRawAfterCurve.fill(0); - this._outputFrozen.fill(0); - this._processed.fill(0); - this._initialized = false; - } -} diff --git a/playground/js/ui/param-display.js b/playground/js/ui/param-display.js deleted file mode 100644 index bb79123..0000000 --- a/playground/js/ui/param-display.js +++ /dev/null @@ -1,133 +0,0 @@ -// Parameter bar display -// Shows output parameters as horizontal bars, draggable in examples mode - -const DEFAULT_PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate']; -const DEFAULT_PARAM_COLORS = ['#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', '#ffc15f', '#ff8a3d']; - -export class ParamDisplay { - constructor(container, numParams = 20) { - this.container = container; - this.numParams = numParams; - this.values = new Array(numParams).fill(0.5); - this.draggable = false; - this.onChange = null; - this.activeBar = -1; - this.paramNames = [...DEFAULT_PARAM_NAMES]; - this.paramColors = [...DEFAULT_PARAM_COLORS]; - this.build(); - } - - build() { - this.container.innerHTML = ''; - this.bars = []; - - for (let i = 0; i < this.numParams; i++) { - const row = document.createElement('div'); - row.className = 'param-row'; - - const label = document.createElement('span'); - label.className = 'param-label'; - label.textContent = this.paramNames[i] || `p${i}`; - - const track = document.createElement('div'); - track.className = 'param-track'; - track.dataset.index = i; - - const fill = document.createElement('div'); - fill.className = 'param-fill'; - fill.style.background = this.paramColors[i] || '#888'; - fill.style.width = '50%'; - - const val = document.createElement('span'); - val.className = 'param-value'; - val.textContent = '0.50'; - - track.appendChild(fill); - row.appendChild(label); - row.appendChild(track); - row.appendChild(val); - this.container.appendChild(row); - - this.bars.push({ fill, val, track }); - } - - // Touch/mouse drag events on the container - const onStart = (e) => { - if (!this.draggable) return; - const target = e.target.closest('.param-track'); - if (!target) return; - e.preventDefault(); - this.activeBar = parseInt(target.dataset.index); - this._updateFromEvent(e); - }; - - const onMove = (e) => { - if (this.activeBar < 0 || !this.draggable) return; - e.preventDefault(); - this._updateFromEvent(e); - }; - - const onEnd = () => { - this.activeBar = -1; - }; - - this.container.addEventListener('mousedown', onStart); - window.addEventListener('mousemove', onMove); - window.addEventListener('mouseup', onEnd); - this.container.addEventListener('touchstart', onStart, { passive: false }); - this.container.addEventListener('touchmove', onMove, { passive: false }); - this.container.addEventListener('touchend', onEnd); - } - - _updateFromEvent(e) { - const i = this.activeBar; - if (i < 0) return; - const track = this.bars[i].track; - const rect = track.getBoundingClientRect(); - const clientX = e.touches ? e.touches[0].clientX : e.clientX; - const value = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); - this.values[i] = value; - this._renderBar(i); - if (this.onChange) this.onChange(i, value, this.values); - } - - update(values) { - for (let i = 0; i < this.numParams && i < values.length; i++) { - this.values[i] = values[i]; - this._renderBar(i); - } - } - - _renderBar(i) { - const v = this.values[i]; - this.bars[i].fill.style.width = (v * 100) + '%'; - this.bars[i].val.textContent = v.toFixed(2); - } - - setDraggable(draggable) { - this.draggable = draggable; - this.container.classList.toggle('draggable', draggable); - } - - setNamesAndColors(names, colors) { - this.paramNames = names || DEFAULT_PARAM_NAMES; - this.paramColors = colors || DEFAULT_PARAM_COLORS; - // Update existing bars in-place - for (let i = 0; i < this.numParams; i++) { - if (!this.bars[i]) continue; - const row = this.bars[i].fill.closest('.param-row'); - const label = row?.querySelector('.param-label'); - if (label) label.textContent = this.paramNames[i] || `p${i}`; - this.bars[i].fill.style.background = this.paramColors[i] || '#888'; - } - } - - rebuild(numParams, names, colors) { - this.numParams = numParams; - this.values = new Array(numParams).fill(0.5); - this.paramNames = names || DEFAULT_PARAM_NAMES; - this.paramColors = colors || DEFAULT_PARAM_COLORS; - this.activeBar = -1; - this.build(); - } -} diff --git a/playground/js/ui/param-pin.js b/playground/js/ui/param-pin.js deleted file mode 100644 index 3210c63..0000000 --- a/playground/js/ui/param-pin.js +++ /dev/null @@ -1,151 +0,0 @@ -// Parameter Pin Manager — Per-output pin flags -// -// Pinned parameters keep their current mapping during moveWeights and training. -// During moveWeights, weights in the final layer connecting to pinned output nodes -// are skipped. During training, pinned outputs have their labels frozen to current -// inferred values so the network maintains the learned mapping. -// -// For the WASM path (which doesn't support pin masks natively), the approach is: -// 1. Before moveWeights: snapshot weights for pinned output nodes' final-layer connections -// 2. After moveWeights: restore those weights -// This is handled by the app integration, not inside this module. -// -// Usage: -// import { ParamPinManager } from './param-pin.js'; -// const paramPins = new ParamPinManager(126); -// paramPins.pin(42); // pin output #42 -// paramPins.toggle(42); // unpin -// const mask = paramPins.getPinMask(); // Uint8Array[126], 1 = pinned - -export class ParamPinManager { - /** - * @param {number} numOutputs Total number of output parameters - */ - constructor(numOutputs) { - this._numOutputs = numOutputs; - this._pinned = new Uint8Array(numOutputs); // 0 = unpinned, 1 = pinned - } - - // ---- Public API ---- - - /** - * Pin a specific output index. - * @param {number} outputIndex - */ - pin(outputIndex) { - if (outputIndex < 0 || outputIndex >= this._numOutputs) return; - if (this._pinned[outputIndex] === 1) return; // already pinned - this._pinned[outputIndex] = 1; - this._dispatch('parampin:change', { - index: outputIndex, - pinned: true, - totalPinned: this.pinnedCount, - }); - } - - /** - * Unpin a specific output index. - * @param {number} outputIndex - */ - unpin(outputIndex) { - if (outputIndex < 0 || outputIndex >= this._numOutputs) return; - if (this._pinned[outputIndex] === 0) return; // already unpinned - this._pinned[outputIndex] = 0; - this._dispatch('parampin:change', { - index: outputIndex, - pinned: false, - totalPinned: this.pinnedCount, - }); - } - - /** - * Toggle pin state for a specific output index. - * @param {number} outputIndex - * @returns {boolean} New pin state (true = pinned). - */ - toggle(outputIndex) { - if (outputIndex < 0 || outputIndex >= this._numOutputs) return false; - if (this._pinned[outputIndex]) { - this.unpin(outputIndex); - return false; - } else { - this.pin(outputIndex); - return true; - } - } - - /** - * Check if a specific output is pinned. - * @param {number} outputIndex - * @returns {boolean} - */ - isPinned(outputIndex) { - if (outputIndex < 0 || outputIndex >= this._numOutputs) return false; - return this._pinned[outputIndex] === 1; - } - - /** - * Get the full pin mask. - * @returns {Uint8Array} Length = numOutputs. 1 = pinned, 0 = unpinned. - */ - getPinMask() { - return this._pinned; - } - - /** - * Get array of pinned output indices. - * @returns {number[]} - */ - getPinnedIndices() { - const indices = []; - for (let i = 0; i < this._numOutputs; i++) { - if (this._pinned[i]) indices.push(i); - } - return indices; - } - - /** - * Number of currently pinned outputs. - * @returns {number} - */ - get pinnedCount() { - let count = 0; - for (let i = 0; i < this._numOutputs; i++) { - if (this._pinned[i]) count++; - } - return count; - } - - /** - * Unpin all outputs. - */ - clearAll() { - this._pinned.fill(0); - this._dispatch('parampin:clearall', { totalPinned: 0 }); - } - - // ---- Serialization ---- - - getState() { - return { - numOutputs: this._numOutputs, - pinned: Array.from(this._pinned), - }; - } - - setState(saved) { - if (!saved) return; - if (Array.isArray(saved.pinned)) { - const len = Math.min(saved.pinned.length, this._numOutputs); - for (let i = 0; i < len; i++) { - this._pinned[i] = saved.pinned[i] ? 1 : 0; - } - } - } - - // ---- Internal ---- - - _dispatch(type, detail) { - document.dispatchEvent(new CustomEvent(type, { detail })); - } -} diff --git a/playground/js/ui/phase2-ui.js b/playground/js/ui/phase2-ui.js deleted file mode 100644 index e1b41f3..0000000 --- a/playground/js/ui/phase2-ui.js +++ /dev/null @@ -1,626 +0,0 @@ -// Phase 2 UI — Undo button, A/B Compare toggle, Region Pin, Parameter Pin icons -// -// Adds Phase 2 controls to the immersive app: -// - Undo button (near RL thumbs) with badge showing snapshot depth + long-press list -// - A/B toggle button pair near RL buttons -// - Region pin: long-press on joy-map (>500ms) pins the current zoom window -// - Parameter pin: pin icon overlay on synth visualizer bars -// -// All state management is delegated to the data modules (SnapshotStack, ABCompare, -// RegionPinManager, ParamPinManager). This module only builds DOM and dispatches events. -// -// Usage: -// import { initPhase2UI } from './phase2-ui.js'; -// const phase2 = initPhase2UI({ -// snapshotStack, abCompare, regionPins, paramPins, -// getWeights, setWeights, getNoiseLevel, setNoiseLevel, -// getZoomWindow, onRegionPin, onParamPinToggle, -// }); - -export function initPhase2UI(options) { - const { - snapshotStack, - abCompare, - regionPins, - paramPins, - // Callbacks the app provides so we can read/write state - getWeights, // () => Array (flat weights) - setWeights, // (weights: Array) => void - getNoiseLevel, // () => number - setNoiseLevel, // (level: number) => void - getZoomLevel, // () => number - getZoomWindow, // () => { x1, y1, x2, y2 } | null - getTrainingData, // () => { features, labels } - runInference, // () => void (re-run inference after weight change) - joyMapCanvas, // the joy-map canvas element (for long-press detection) - synthVisualizer, // SynthVisualizer instance (for param pin overlay) - } = options; - - // ---- Inject styles ---- - injectPhase2Styles(); - - // ==== 1. UNDO BUTTON ==== - - const $rlButtons = document.getElementById('rl-buttons'); - - const $undoBtn = document.createElement('button'); - $undoBtn.className = 'rl-btn phase2-undo-btn'; - $undoBtn.title = 'Undo (long-press for history)'; - $undoBtn.innerHTML = ` - - 0 - `; - - // Insert undo button at the start of the RL buttons container - if ($rlButtons) { - $rlButtons.insertBefore($undoBtn, $rlButtons.firstChild); - } - - const $undoBadge = $undoBtn.querySelector('#phase2-undo-badge'); - - function updateUndoBadge() { - const depth = snapshotStack.depth; - $undoBadge.textContent = depth; - $undoBadge.classList.toggle('hidden', depth === 0); - $undoBtn.disabled = depth === 0; - } - updateUndoBadge(); - - // Tap: single undo - $undoBtn.addEventListener('click', () => { - const state = snapshotStack.pop(); - if (state) { - setWeights(state.weights); - setNoiseLevel(state.noiseLevel); - runInference(); - } - updateUndoBadge(); - }); - - // Long-press: show snapshot list popup - let _undoLongPressTimer = null; - let _undoLongPressActive = false; - - $undoBtn.addEventListener('pointerdown', (e) => { - _undoLongPressActive = false; - _undoLongPressTimer = setTimeout(() => { - _undoLongPressActive = true; - showSnapshotListPopup(); - }, 500); - }); - - $undoBtn.addEventListener('pointerup', () => { - clearTimeout(_undoLongPressTimer); - }); - - $undoBtn.addEventListener('pointerleave', () => { - clearTimeout(_undoLongPressTimer); - }); - - // Prevent click from firing after long-press - $undoBtn.addEventListener('click', (e) => { - if (_undoLongPressActive) { - e.stopImmediatePropagation(); - _undoLongPressActive = false; - } - }, true); - - // Snapshot list popup - let $snapshotPopup = null; - - function showSnapshotListPopup() { - removeSnapshotPopup(); - - const snapshots = snapshotStack.list(); - if (snapshots.length === 0) return; - - $snapshotPopup = document.createElement('div'); - $snapshotPopup.className = 'phase2-snapshot-popup'; - - const header = document.createElement('div'); - header.className = 'phase2-snapshot-popup-header'; - header.textContent = 'Snapshot History'; - $snapshotPopup.appendChild(header); - - const list = document.createElement('div'); - list.className = 'phase2-snapshot-list'; - - // Show newest first - for (let i = snapshots.length - 1; i >= 0; i--) { - const snap = snapshots[i]; - const item = document.createElement('button'); - item.className = 'phase2-snapshot-item'; - const age = formatAge(snap.timestamp); - item.innerHTML = ` - ${escapeHtml(snap.tag)} - ${age} - `; - item.addEventListener('click', () => { - const state = snapshotStack.jumpTo(snap.index); - if (state) { - setWeights(state.weights); - setNoiseLevel(state.noiseLevel); - runInference(); - } - updateUndoBadge(); - removeSnapshotPopup(); - }); - list.appendChild(item); - } - - $snapshotPopup.appendChild(list); - document.body.appendChild($snapshotPopup); - - // Position near the undo button - const rect = $undoBtn.getBoundingClientRect(); - $snapshotPopup.style.left = `${rect.left}px`; - $snapshotPopup.style.bottom = `${window.innerHeight - rect.top + 8}px`; - - // Close on click outside - setTimeout(() => { - document.addEventListener('pointerdown', handlePopupOutsideClick); - }, 50); - } - - function handlePopupOutsideClick(e) { - if ($snapshotPopup && !$snapshotPopup.contains(e.target)) { - removeSnapshotPopup(); - } - } - - function removeSnapshotPopup() { - if ($snapshotPopup) { - $snapshotPopup.remove(); - $snapshotPopup = null; - document.removeEventListener('pointerdown', handlePopupOutsideClick); - } - } - - // Listen for snapshot events to update badge - document.addEventListener('snapshot:push', updateUndoBadge); - document.addEventListener('snapshot:pop', updateUndoBadge); - document.addEventListener('snapshot:jump', updateUndoBadge); - document.addEventListener('snapshot:clear', updateUndoBadge); - - // ==== 2. A/B COMPARE TOGGLE ==== - - const $abContainer = document.createElement('div'); - $abContainer.className = 'phase2-ab-container'; - $abContainer.innerHTML = ` - - - - - `; - - if ($rlButtons) { - $rlButtons.appendChild($abContainer); - } - - const $abCapture = $abContainer.querySelector('#phase2-ab-capture'); - const $abToggle = $abContainer.querySelector('#phase2-ab-toggle'); - const $abAccept = $abContainer.querySelector('#phase2-ab-accept'); - const $abRevert = $abContainer.querySelector('#phase2-ab-revert'); - - function updateABUI() { - const active = abCompare.active; - const side = abCompare.current; - - $abCapture.classList.toggle('hidden', active); - $abToggle.classList.toggle('hidden', !active); - $abAccept.classList.toggle('hidden', !active); - $abRevert.classList.toggle('hidden', !active); - - if (active) { - $abToggle.textContent = side === 'a' ? 'A' : 'B'; - $abToggle.classList.toggle('phase2-ab-side-a', side === 'a'); - $abToggle.classList.toggle('phase2-ab-side-b', side === 'b'); - } - } - - $abCapture.addEventListener('click', () => { - abCompare.captureA({ - weights: getWeights(), - noiseLevel: getNoiseLevel(), - }); - updateABUI(); - }); - - $abToggle.addEventListener('click', () => { - const state = abCompare.toggle({ - weights: getWeights(), - noiseLevel: getNoiseLevel(), - }); - if (state) { - setWeights(state.weights); - setNoiseLevel(state.noiseLevel); - runInference(); - } - updateABUI(); - }); - - $abAccept.addEventListener('click', () => { - abCompare.acceptB(); - updateABUI(); - }); - - $abRevert.addEventListener('click', () => { - const state = abCompare.revertToA(); - if (state) { - setWeights(state.weights); - setNoiseLevel(state.noiseLevel); - runInference(); - } - updateABUI(); - }); - - // Listen for AB events - document.addEventListener('ab:activate', updateABUI); - document.addEventListener('ab:toggle', updateABUI); - document.addEventListener('ab:deactivate', updateABUI); - - // ==== 3. REGION PIN (long-press on joy-map) ==== - - let _regionLongPressTimer = null; - - if (joyMapCanvas) { - joyMapCanvas.addEventListener('pointerdown', (e) => { - _regionLongPressTimer = setTimeout(() => { - // Pin the current zoom window as a region - const zoomWindow = getZoomWindow ? getZoomWindow() : null; - let region; - if (zoomWindow) { - region = { - x1: zoomWindow.x1, - y1: zoomWindow.y1, - x2: zoomWindow.x2, - y2: zoomWindow.y2, - }; - } else { - // No zoom — pin the full space (not very useful, but valid) - region = { x1: 0, y1: 0, x2: 1, y2: 1 }; - } - - // Snapshot before pinning - snapshotStack.push('before region pin', { - weights: getWeights(), - noiseLevel: getNoiseLevel(), - zoomLevel: getZoomLevel ? getZoomLevel() : 1.0, - }); - updateUndoBadge(); - - const data = getTrainingData ? getTrainingData() : { features: [], labels: [] }; - const pinId = regionPins.pin(region, data); - - if (pinId >= 0) { - // Flash feedback - joyMapCanvas.classList.add('phase2-pin-flash'); - setTimeout(() => joyMapCanvas.classList.remove('phase2-pin-flash'), 300); - - // Update joy-map overlay - syncJoyMapPins(); - } - }, 500); - }); - - joyMapCanvas.addEventListener('pointerup', () => { - clearTimeout(_regionLongPressTimer); - }); - - joyMapCanvas.addEventListener('pointerleave', () => { - clearTimeout(_regionLongPressTimer); - }); - - // Prevent context menu on long-press - joyMapCanvas.addEventListener('contextmenu', (e) => e.preventDefault()); - } - - // Keep joy-map overlay in sync when pins change - function syncJoyMapPins() { - document.dispatchEvent(new CustomEvent('regionpin:sync', { - detail: { regions: regionPins.getRegions() }, - })); - } - - document.addEventListener('regionpin:add', syncJoyMapPins); - document.addEventListener('regionpin:remove', syncJoyMapPins); - - // ==== 4. PARAMETER PIN (overlay on synth visualizer) ==== - - // Param pin state is tracked by paramPins (ParamPinManager). - // We add a click handler to the synth visualizer canvas to toggle pins. - // The SynthVisualizer's draw loop can check paramPins.isPinned(i) for rendering. - - if (synthVisualizer && synthVisualizer.canvas) { - // Double-tap on a param bar to toggle pin - let _lastParamTapTime = 0; - let _lastParamTapIndex = -1; - - synthVisualizer.canvas.addEventListener('pointerdown', (e) => { - const idx = synthVisualizer.hitTest(e.clientX, e.clientY); - if (idx < 0) return; - - const now = Date.now(); - if (now - _lastParamTapTime < 400 && idx === _lastParamTapIndex) { - // Double-tap: toggle pin - const newState = paramPins.toggle(idx); - // Visual feedback is handled by the draw loop checking paramPins - _lastParamTapTime = 0; - _lastParamTapIndex = -1; - } else { - _lastParamTapTime = now; - _lastParamTapIndex = idx; - } - }); - } - - // ==== PUBLIC API ==== - - /** - * Push a snapshot (called by app before destructive operations). - * @param {string} tag - */ - function pushSnapshot(tag) { - snapshotStack.push(tag, { - weights: getWeights(), - noiseLevel: getNoiseLevel(), - zoomLevel: getZoomLevel ? getZoomLevel() : 1.0, - }); - updateUndoBadge(); - } - - /** - * Cleanup all Phase 2 UI elements. - */ - function destroy() { - $undoBtn.remove(); - $abContainer.remove(); - removeSnapshotPopup(); - document.removeEventListener('snapshot:push', updateUndoBadge); - document.removeEventListener('snapshot:pop', updateUndoBadge); - document.removeEventListener('snapshot:jump', updateUndoBadge); - document.removeEventListener('snapshot:clear', updateUndoBadge); - document.removeEventListener('ab:activate', updateABUI); - document.removeEventListener('ab:toggle', updateABUI); - document.removeEventListener('ab:deactivate', updateABUI); - document.removeEventListener('regionpin:add', syncJoyMapPins); - document.removeEventListener('regionpin:remove', syncJoyMapPins); - const styleEl = document.getElementById('phase2-styles'); - if (styleEl) styleEl.remove(); - } - - return { - pushSnapshot, - updateUndoBadge, - syncJoyMapPins, - destroy, - }; -} - -// ---- Helpers ---- - -function formatAge(timestamp) { - const seconds = Math.floor((Date.now() - timestamp) / 1000); - if (seconds < 5) return 'just now'; - if (seconds < 60) return `${seconds}s ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; - return `${Math.floor(minutes / 60)}h ago`; -} - -function escapeHtml(str) { - return str.replace(/&/g, '&').replace(//g, '>'); -} - -// ---- CSS ---- - -function injectPhase2Styles() { - if (document.getElementById('phase2-styles')) return; - - const style = document.createElement('style'); - style.id = 'phase2-styles'; - style.textContent = ` -/* ==== Phase 2: Undo Button ==== */ -.phase2-undo-btn { - position: relative; -} - -.phase2-undo-icon { - font-size: 18px; - line-height: 1; -} - -.phase2-undo-badge { - position: absolute; - top: -4px; - right: -4px; - min-width: 16px; - height: 16px; - border-radius: 8px; - background: var(--accent, #ff6a00); - color: #000; - font-size: 9px; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - padding: 0 3px; - pointer-events: none; - transition: opacity 0.15s; -} - -.phase2-undo-badge.hidden { - opacity: 0; -} - -.phase2-undo-btn:disabled { - opacity: 0.3; - pointer-events: none; -} - -/* ==== Phase 2: Snapshot Popup ==== */ -.phase2-snapshot-popup { - position: fixed; - z-index: 100; - width: 240px; - max-height: 300px; - background: rgba(0, 0, 0, 0.92); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - overflow: hidden; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - font-family: 'JetBrains Mono', 'SF Mono', monospace; -} - -.phase2-snapshot-popup-header { - padding: 10px 12px 8px; - font-size: 10px; - font-weight: 600; - color: rgba(255, 255, 255, 0.6); - text-transform: uppercase; - letter-spacing: 0.8px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.phase2-snapshot-list { - overflow-y: auto; - max-height: 250px; - -webkit-overflow-scrolling: touch; -} - -.phase2-snapshot-item { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 8px 12px; - border: none; - background: transparent; - color: rgba(255, 255, 255, 0.8); - font-family: inherit; - font-size: 11px; - cursor: pointer; - transition: background 0.1s; - text-align: left; -} - -.phase2-snapshot-item:hover { - background: rgba(255, 255, 255, 0.06); -} - -.phase2-snap-tag { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: 8px; -} - -.phase2-snap-age { - font-size: 9px; - color: rgba(255, 255, 255, 0.35); - white-space: nowrap; -} - -/* ==== Phase 2: A/B Compare ==== */ -.phase2-ab-container { - display: flex; - gap: 4px; - align-items: center; - margin-left: 6px; -} - -.phase2-ab-btn { - height: 36px; - min-width: 36px; - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: var(--glass-bg, rgba(13, 13, 13, 0.65)); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - color: rgba(255, 255, 255, 0.7); - font-family: 'JetBrains Mono', 'SF Mono', monospace; - font-size: 11px; - font-weight: 600; - cursor: pointer; - padding: 0 10px; - transition: all 0.15s; - white-space: nowrap; -} - -.phase2-ab-btn:hover { - border-color: rgba(255, 255, 255, 0.25); - color: white; -} - -.phase2-ab-btn.hidden { - display: none; -} - -/* A/B side indicators */ -.phase2-ab-side-a { - border-color: #4488ff; - color: #4488ff; - background: rgba(68, 136, 255, 0.12); -} - -.phase2-ab-side-b { - border-color: #ff8844; - color: #ff8844; - background: rgba(255, 136, 68, 0.12); -} - -.phase2-ab-capture { - border-color: rgba(68, 136, 255, 0.4); - color: rgba(68, 136, 255, 0.8); -} - -.phase2-ab-accept { - border-color: rgba(139, 195, 74, 0.4); - color: rgba(139, 195, 74, 0.9); - font-size: 10px; -} - -.phase2-ab-revert { - border-color: rgba(255, 152, 0, 0.4); - color: rgba(255, 152, 0, 0.9); - font-size: 10px; -} - -/* ==== Phase 2: Region Pin Flash ==== */ -.phase2-pin-flash { - animation: phase2-pin-flash-anim 0.3s ease-out; -} - -@keyframes phase2-pin-flash-anim { - 0% { box-shadow: 0 0 0 0 rgba(0, 188, 212, 0.5); } - 100% { box-shadow: 0 0 0 12px rgba(0, 188, 212, 0); } -} - -/* ==== Phase 2: Param Pin overlay (rendered on synth visualizer) ==== */ -/* Lock icon styling — drawn by SynthVisualizer, these are for any DOM overlays */ -.phase2-param-pinned-bar { - opacity: 0.5; -} - -/* ==== Responsive ==== */ -@media (max-width: 600px) { - .phase2-ab-container { - gap: 3px; - margin-left: 4px; - } - .phase2-ab-btn { - height: 32px; - min-width: 32px; - font-size: 10px; - padding: 0 8px; - } - .phase2-snapshot-popup { - width: 200px; - } -} -`; - document.head.appendChild(style); -} diff --git a/playground/js/ui/phase3-ui.js b/playground/js/ui/phase3-ui.js deleted file mode 100644 index 1dcebf5..0000000 --- a/playground/js/ui/phase3-ui.js +++ /dev/null @@ -1,588 +0,0 @@ -/** - * Phase 3 UI Integration — Auto-Explore button, Heatmap toggle, - * Pressure indicator, and settings drawer additions. - * - * Wires PressureFeedback, AutoExplore, and InputHeatmap into the - * immersive app's DOM and event flow. - * - * Usage: - * import { initPhase3UI } from './ui/phase3-ui.js'; - * const p3 = initPhase3UI({ - * onAutoExplore: ({ intensity }) => { ... }, - * getZoomLevel: () => pipeline.getZoomLevel(), - * }); - * // In animation loop: - * p3.tick(deltaTime); - * - * @module phase3-ui - */ - -import { PressureFeedback } from './pressure-feedback.js'; -import { AutoExplore } from './auto-explore.js'; -import { InputHeatmap } from './input-heatmap.js'; - -// ---- Style injection ---- - -const PHASE3_STYLES = ` -/* ---- Auto-Explore button ---- */ -.auto-explore-btn { - width: 56px; - height: 56px; - border-radius: 50%; - border: 2px solid rgba(52, 211, 153, 0.4); - background: rgba(13, 13, 13, 0.65); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - color: rgba(52, 211, 153, 0.7); - font-family: inherit; - font-size: 10px; - font-weight: 600; - letter-spacing: 0.3px; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - transition: all 0.2s ease; - position: relative; - overflow: visible; - -webkit-tap-highlight-color: transparent; - touch-action: manipulation; -} - -.auto-explore-btn:active { - transform: scale(0.9); -} - -.auto-explore-btn.active { - border-color: rgba(52, 211, 153, 0.7); - background: rgba(52, 211, 153, 0.15); - color: rgba(52, 211, 153, 1); - box-shadow: 0 0 16px rgba(52, 211, 153, 0.3); -} - -.auto-explore-btn.active .ae-icon { - animation: ae-wander 2s ease-in-out infinite; -} - -@keyframes ae-wander { - 0%, 100% { transform: translate(0, 0); } - 25% { transform: translate(2px, -1px); } - 50% { transform: translate(-1px, 2px); } - 75% { transform: translate(-2px, -1px); } -} - -/* Progress ring (SVG overlay) */ -.ae-progress-ring { - position: absolute; - inset: -3px; - width: calc(100% + 6px); - height: calc(100% + 6px); - pointer-events: none; - transform: rotate(-90deg); -} - -.ae-progress-ring circle { - fill: none; - stroke: rgba(52, 211, 153, 0.6); - stroke-width: 2; - stroke-dasharray: 175; - stroke-dashoffset: 175; - stroke-linecap: round; - transition: stroke-dashoffset 0.1s linear; -} - -/* ---- Heatmap toggle ---- */ -.heatmap-toggle { - position: absolute; - top: 4px; - right: 4px; - width: 24px; - height: 24px; - border-radius: 50%; - border: 1px solid rgba(255, 255, 255, 0.15); - background: rgba(13, 13, 13, 0.5); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - color: rgba(255, 255, 255, 0.4); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - z-index: 5; - transition: all 0.15s ease; - padding: 0; - -webkit-tap-highlight-color: transparent; - touch-action: manipulation; -} - -.heatmap-toggle:active { - transform: scale(0.85); -} - -.heatmap-toggle.active { - border-color: rgba(255, 200, 100, 0.5); - background: rgba(255, 200, 100, 0.15); - color: rgba(255, 200, 100, 0.9); -} - -.heatmap-toggle svg { - width: 14px; - height: 14px; -} - -/* Color mode label (appears briefly on mode switch) */ -.heatmap-mode-label { - position: absolute; - top: 30px; - right: 0; - background: rgba(13, 13, 13, 0.8); - color: rgba(255, 200, 100, 0.8); - font-size: 9px; - padding: 2px 6px; - border-radius: 4px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s ease; - z-index: 5; -} - -.heatmap-mode-label.visible { - opacity: 1; -} - -/* ---- Pressure indicator ---- */ -.pressure-indicator { - position: absolute; - bottom: -10px; - left: 50%; - transform: translateX(-50%); - width: 40px; - height: 4px; - border-radius: 2px; - background: rgba(255, 255, 255, 0.08); - overflow: hidden; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s ease; -} - -.pressure-indicator.visible { - opacity: 1; -} - -.pressure-indicator-fill { - height: 100%; - width: 0%; - border-radius: 2px; - background: linear-gradient(90deg, rgba(255, 200, 100, 0.6), rgba(255, 106, 0, 0.8)); - transition: width 0.1s ease; -} - -/* ---- Settings drawer additions ---- */ -.cs-section[data-section="phase3"] .cs-section-header { - color: rgba(52, 211, 153, 0.8); -} -`; - -// ---- Eye icon SVG ---- -const EYE_SVG = ` - - -`; - -// ---- Wander icon SVG ---- -const WANDER_SVG = ` - - -`; - -/** - * Initialize Phase 3 UI. - * - * @param {object} options - * @param {function} options.onAutoExplore - callback({ intensity }) on auto-step - * @param {function} options.getZoomLevel - () => number (current zoom 0-1) - * @param {function} [options.inferFn] - (inputs) => outputs for heatmap - * @param {function} [options.getZoomWindow] - () => { x1, y1, x2, y2 } - * @returns {{ pressure, autoExplore, heatmap, tick, updateHeatmap, destroy, getConfig, setConfig }} - */ -export function initPhase3UI(options = {}) { - const { onAutoExplore, getZoomLevel, inferFn, getZoomWindow } = options; - - // ---- Instances ---- - const pressure = new PressureFeedback({ holdEnabled: true, pressureEnabled: false }); - const autoExplore = new AutoExplore(); - const heatmap = new InputHeatmap(); - - // Wire auto-explore callback - autoExplore.onExplore((data) => { - if (onAutoExplore) onAutoExplore(data); - }); - - // ---- Inject styles ---- - if (!document.getElementById('phase3-styles')) { - const style = document.createElement('style'); - style.id = 'phase3-styles'; - style.textContent = PHASE3_STYLES; - document.head.appendChild(style); - } - - // ---- Auto-Explore button ---- - const $rlButtons = document.getElementById('rl-buttons'); - const $autoBtn = document.createElement('button'); - $autoBtn.className = 'auto-explore-btn'; - $autoBtn.title = 'Auto-Explore: automated wandering'; - $autoBtn.innerHTML = ` - ${WANDER_SVG} - Auto - - - - `; - - if ($rlButtons) { - $rlButtons.appendChild($autoBtn); - } - - const $progressCircle = $autoBtn.querySelector('.ae-progress-ring circle'); - const circumference = 2 * Math.PI * 28; // r=28 - if ($progressCircle) { - $progressCircle.style.strokeDasharray = circumference; - $progressCircle.style.strokeDashoffset = circumference; - } - - $autoBtn.addEventListener('click', () => { - autoExplore.toggle(); - $autoBtn.classList.toggle('active', autoExplore.active); - }); - - // ---- Heatmap toggle (on joy-map) ---- - const $joystickContainer = document.querySelector('.joystick-container'); - let $heatmapToggle = null; - let $hmModeLabel = null; - let _hmLabelTimeout = null; - - if ($joystickContainer) { - // Ensure position context - if (getComputedStyle($joystickContainer).position === 'static') { - $joystickContainer.style.position = 'relative'; - } - - $heatmapToggle = document.createElement('button'); - $heatmapToggle.className = 'heatmap-toggle'; - $heatmapToggle.title = 'Toggle input heatmap'; - $heatmapToggle.innerHTML = EYE_SVG; - $joystickContainer.appendChild($heatmapToggle); - - $hmModeLabel = document.createElement('div'); - $hmModeLabel.className = 'heatmap-mode-label'; - $joystickContainer.appendChild($hmModeLabel); - - // Tap to toggle, long-press to cycle color mode - let _pressTimer = null; - let _didLongPress = false; - - $heatmapToggle.addEventListener('pointerdown', (e) => { - e.stopPropagation(); - _didLongPress = false; - _pressTimer = setTimeout(() => { - _didLongPress = true; - if (heatmap.enabled) { - const mode = heatmap.cycleColorMode(); - showModeLabel(mode); - // Trigger recompute - if (inferFn) { - heatmap.invalidate(); - const zw = getZoomWindow ? getZoomWindow() : undefined; - heatmap.update(inferFn, { zoomWindow: zw }); - } - } - }, 500); - }); - - $heatmapToggle.addEventListener('pointerup', (e) => { - e.stopPropagation(); - clearTimeout(_pressTimer); - if (!_didLongPress) { - heatmap.setEnabled(!heatmap.enabled); - $heatmapToggle.classList.toggle('active', heatmap.enabled); - if (heatmap.enabled && inferFn) { - heatmap.invalidate(); - const zw = getZoomWindow ? getZoomWindow() : undefined; - heatmap.update(inferFn, { zoomWindow: zw }); - } - } - }); - - $heatmapToggle.addEventListener('pointercancel', () => { - clearTimeout(_pressTimer); - }); - - // Prevent joy-map from capturing these events - $heatmapToggle.addEventListener('touchstart', (e) => e.stopPropagation(), { passive: true }); - } - - function showModeLabel(mode) { - if (!$hmModeLabel) return; - $hmModeLabel.textContent = mode; - $hmModeLabel.classList.add('visible'); - clearTimeout(_hmLabelTimeout); - _hmLabelTimeout = setTimeout(() => { - $hmModeLabel.classList.remove('visible'); - }, 1500); - } - - // ---- Pressure indicators (on RL buttons) ---- - const $thumbsUp = document.getElementById('btn-thumbsup'); - const $thumbsDown = document.getElementById('btn-thumbsdown'); - const pressureIndicators = []; - - for (const $btn of [$thumbsUp, $thumbsDown]) { - if (!$btn) continue; - // Ensure position context - if (getComputedStyle($btn).position === 'static') { - $btn.style.position = 'relative'; - } - - const $indicator = document.createElement('div'); - $indicator.className = 'pressure-indicator'; - $indicator.innerHTML = '
    '; - $btn.appendChild($indicator); - pressureIndicators.push({ el: $indicator, fill: $indicator.querySelector('.pressure-indicator-fill'), btn: $btn }); - - // Wire pointer events for pressure tracking - $btn.addEventListener('pointerdown', (e) => pressure.onPointerDown(e)); - $btn.addEventListener('pointermove', (e) => pressure.onPointerMove(e)); - $btn.addEventListener('pointerup', (e) => { - pressure.onPointerUp(e); - $indicator.classList.remove('visible'); - }); - $btn.addEventListener('pointercancel', (e) => { - pressure.onPointerUp(e); - $indicator.classList.remove('visible'); - }); - } - - // ---- Settings drawer additions ---- - addDrawerSection(); - - // ---- Frame tick ---- - - /** - * Call once per animation frame. - * @param {number} deltaTime - seconds since last frame - */ - function tick(deltaTime) { - const zoomLevel = getZoomLevel ? getZoomLevel() : 1.0; - - // Auto-explore - autoExplore.tick(deltaTime, zoomLevel); - - // Update progress ring - if ($progressCircle && autoExplore.active) { - const progress = autoExplore.getProgress(); - const offset = circumference * (1 - progress); - $progressCircle.style.strokeDashoffset = offset; - } else if ($progressCircle) { - $progressCircle.style.strokeDashoffset = circumference; - } - - // Update pressure indicators - if (pressure.active && pressure.enabled) { - const intensity = pressure.getIntensity(); - for (const { el, fill } of pressureIndicators) { - el.classList.add('visible'); - fill.style.width = `${intensity * 100}%`; - } - } - } - - /** - * Trigger a heatmap update. Call after weight changes (train, randomize, moveWeights). - */ - function updateHeatmap() { - if (!heatmap.enabled || !inferFn) return; - const zw = getZoomWindow ? getZoomWindow() : undefined; - heatmap.update(inferFn, { zoomWindow: zw }); - } - - // ---- Settings drawer: Phase 3 section ---- - - function addDrawerSection() { - // Find the drawer body - const $drawerBody = document.querySelector('.cs-drawer-body'); - if (!$drawerBody) return; - - const section = document.createElement('div'); - section.className = 'cs-section'; - section.dataset.section = 'phase3'; - section.innerHTML = ` -
    - Exploration & Viz - -
    -
    -
    - -
    - - 2.0s -
    -
    -
    - -
    - - 0.50 -
    -
    -
    - - -
    -
    - - -
    -
    - -
    - - 2.0s -
    -
    -
    - -
    - - 16 -
    -
    -
    - -
    - - 0.55 -
    -
    -
    - `; - - $drawerBody.appendChild(section); - - // Wire collapse - const header = section.querySelector('.cs-section-header'); - if (header) { - header.addEventListener('click', () => section.classList.toggle('collapsed')); - } - - // Wire sliders - wireSlider(section, 'autoExploreInterval', (val) => { - autoExplore.setInterval(val); - return `${val.toFixed(1)}s`; - }); - - wireSlider(section, 'autoExploreIntensity', (val) => { - autoExplore.setIntensity(val); - return val.toFixed(2); - }); - - wireSlider(section, 'holdCurve', (val) => { - pressure.setHoldCurve(val); - return `${val.toFixed(1)}s`; - }); - - wireSlider(section, 'heatmapResolution', (val) => { - heatmap.setResolution(val); - return String(Math.round(val)); - }); - - wireSlider(section, 'heatmapOpacity', (val) => { - heatmap.setOpacity(val); - return val.toFixed(2); - }); - - // Wire toggles - wireToggle(section, 'pressureHold', (checked) => { - pressure.setEnabled(pressure.getPressureEnabled(), checked); - }); - - wireToggle(section, 'pressureFeedback', (checked) => { - pressure.setEnabled(checked, pressure.getHoldEnabled()); - }); - } - - function wireSlider(container, paramName, onChange) { - const row = container.querySelector(`[data-param="${paramName}"]`); - if (!row) return; - const slider = row.querySelector('input[type="range"]'); - const valueSpan = row.querySelector('.cs-param-value'); - if (!slider) return; - slider.addEventListener('input', () => { - const val = parseFloat(slider.value); - const display = onChange(val); - if (valueSpan && display) valueSpan.textContent = display; - // Dispatch controlsurface:change for consistency - document.dispatchEvent(new CustomEvent('controlsurface:change', { - detail: { _phase3: true, [paramName]: val }, - })); - }); - } - - function wireToggle(container, paramName, onChange) { - const row = container.querySelector(`[data-param="${paramName}"]`); - if (!row) return; - const checkbox = row.querySelector('input[type="checkbox"]'); - if (!checkbox) return; - checkbox.addEventListener('change', () => { - onChange(checkbox.checked); - }); - } - - // ---- Cleanup ---- - - function destroy() { - $autoBtn.remove(); - if ($heatmapToggle) $heatmapToggle.remove(); - if ($hmModeLabel) $hmModeLabel.remove(); - for (const { el } of pressureIndicators) el.remove(); - const $styles = document.getElementById('phase3-styles'); - if ($styles) $styles.remove(); - const $section = document.querySelector('.cs-section[data-section="phase3"]'); - if ($section) $section.remove(); - clearTimeout(_hmLabelTimeout); - } - - // ---- Serialization ---- - - function getConfig() { - return { - pressure: pressure.getConfig(), - autoExplore: autoExplore.getConfig(), - heatmap: heatmap.getConfig(), - }; - } - - function setConfig(config) { - if (config.pressure) pressure.setConfig(config.pressure); - if (config.autoExplore) autoExplore.setConfig(config.autoExplore); - if (config.heatmap) heatmap.setConfig(config.heatmap); - // Sync UI - $autoBtn.classList.toggle('active', autoExplore.active); - if ($heatmapToggle) $heatmapToggle.classList.toggle('active', heatmap.enabled); - } - - return { - pressure, - autoExplore, - heatmap, - tick, - updateHeatmap, - destroy, - getConfig, - setConfig, - }; -} diff --git a/playground/js/ui/phase4-ui.js b/playground/js/ui/phase4-ui.js deleted file mode 100644 index 5fab772..0000000 --- a/playground/js/ui/phase4-ui.js +++ /dev/null @@ -1,577 +0,0 @@ -/** - * Phase 4 UI — Output Pipeline + Visualization + Polish - * - * Integrates: - * - Output freeze button on floating bar - * - Weight health + gradient flow panel in settings drawer - * - Session preset UI in settings drawer - * - Wires output pipeline sliders to actual OutputPipeline instance - * - * Usage: - * import { initPhase4UI } from './ui/phase4-ui.js'; - * const phase4 = initPhase4UI({ outputPipeline, iml, controlSurface, ... }); - * - * @module phase4-ui - */ - -import { WeightHealthIndicator } from './weight-health.js'; -import { GradientFlowIndicator } from './gradient-flow.js'; -import { SessionPresetManager } from './session-presets.js'; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -const HEALTH_UPDATE_INTERVAL = 500; // ms between weight health updates - -// Snowflake SVG icon (for output freeze button) -const SNOWFLAKE_SVG = ` - - - - - - - - -`; - -// --------------------------------------------------------------------------- -// Main initialization -// --------------------------------------------------------------------------- - -/** - * Initialize Phase 4 UI components. - * - * @param {object} opts - * @param {import('./output-pipeline.js').OutputPipeline} opts.outputPipeline - * @param {object} opts.iml — WasmIML or IML instance - * @param {import('./control-surface.js').ControlSurface} opts.controlSurface - * @param {function} opts.getGroupOverrides — returns current groupOverrides array - * @param {function} opts.getActiveSynthPresetId — returns active synth preset id - * @param {import('./input-pipeline.js').InputPipeline} opts.inputPipeline - * @param {function} opts.onSessionLoad — callback(preset) when a session is loaded - * @returns {object} { weightHealth, gradientFlow, sessionManager, destroy } - */ -export function initPhase4UI(opts) { - const { - outputPipeline, - iml, - controlSurface, - getGroupOverrides, - getActiveSynthPresetId, - inputPipeline, - onSessionLoad, - } = opts; - - // ---- Weight Health ---- - const weightHealth = new WeightHealthIndicator(); - - // ---- Gradient Flow ---- - // layerSizes from the IML instance - const layerSizes = iml.layerSizes || [3, 32, 48, 64, 126]; - const gradientFlow = new GradientFlowIndicator(layerSizes); - - // ---- Session Presets ---- - const sessionManager = new SessionPresetManager(); - - // ---- 1. Output Freeze Button ---- - const freezeBtn = createFreezeButton(outputPipeline); - - // ---- 2. Network Health Panel (in settings drawer) ---- - const healthPanel = createHealthPanel(weightHealth, gradientFlow); - - // ---- 3. Session Preset UI (in settings drawer) ---- - const sessionUI = createSessionUI(sessionManager, { - controlSurface, - outputPipeline, - inputPipeline, - getGroupOverrides, - getActiveSynthPresetId, - onSessionLoad, - }); - - // ---- 4. Wire output pipeline sliders ---- - wireOutputPipelineSliders(outputPipeline, controlSurface); - - // ---- Periodic weight health updates ---- - let healthInterval = setInterval(() => { - updateWeightHealth(iml, weightHealth, healthPanel.canvas, healthPanel.ctx, gradientFlow, healthPanel.gradCanvas, healthPanel.gradCtx); - }, HEALTH_UPDATE_INTERVAL); - - // ---- Inject styles ---- - injectPhase4Styles(); - - // ---- Cleanup ---- - function destroy() { - clearInterval(healthInterval); - freezeBtn.remove(); - healthPanel.container.remove(); - sessionUI.container.remove(); - const styleEl = document.getElementById('phase4-styles'); - if (styleEl) styleEl.remove(); - } - - return { - weightHealth, - gradientFlow, - sessionManager, - freezeBtn, - /** - * Call before iml.train() / iml.trainAsync() to capture weight snapshot. - */ - captureBeforeTrain() { - const weights = getFlatWeights(iml); - if (weights) gradientFlow.captureBeforeTrain(weights); - }, - /** - * Call after training completes to capture weight snapshot and update gradient flow. - */ - captureAfterTrain() { - const weights = getFlatWeights(iml); - if (weights) { - gradientFlow.captureAfterTrain(weights); - // Immediately refresh the health panel - updateWeightHealth(iml, weightHealth, healthPanel.canvas, healthPanel.ctx, gradientFlow, healthPanel.gradCanvas, healthPanel.gradCtx); - } - }, - destroy, - }; -} - -// --------------------------------------------------------------------------- -// Freeze Button -// --------------------------------------------------------------------------- - -function createFreezeButton(outputPipeline) { - const $barContent = document.querySelector('.floating-bar-content') || document.getElementById('floating-bar'); - if (!$barContent) { - console.warn('[Phase4] floating-bar not found'); - return document.createElement('button'); - } - - const btn = document.createElement('button'); - btn.className = 'float-btn p4-freeze-btn'; - btn.id = 'btn-freeze-output'; - btn.title = 'Freeze output (synth/visual stays locked)'; - btn.innerHTML = SNOWFLAKE_SVG; - - btn.addEventListener('click', () => { - const frozen = !outputPipeline.isFrozen(); - outputPipeline.setFrozen(frozen); - btn.classList.toggle('active', frozen); - btn.title = frozen ? 'Unfreeze output' : 'Freeze output'; - }); - - $barContent.appendChild(btn); - - return btn; -} - -// --------------------------------------------------------------------------- -// Network Health Panel -// --------------------------------------------------------------------------- - -function createHealthPanel(weightHealth, gradientFlow) { - // Find the settings drawer body - const drawerBody = document.querySelector('.cs-drawer-body'); - if (!drawerBody) { - // Drawer not yet created; defer by creating a detached element - const container = document.createElement('div'); - return { - container, - canvas: document.createElement('canvas'), - ctx: null, - gradCanvas: document.createElement('canvas'), - gradCtx: null, - }; - } - - const container = document.createElement('div'); - container.className = 'cs-section p4-health-section'; - container.innerHTML = ` -
    - Network Health - -
    -
    -
    -
    Weight Health
    - -
    -
    -
    Gradient Flow
    - -
    -
    - `; - - // Collapse toggle - const header = container.querySelector('.cs-section-header'); - header.addEventListener('click', () => { - container.classList.toggle('collapsed'); - }); - - // Append to drawer body (at the bottom) - drawerBody.appendChild(container); - - const canvas = container.querySelector('.p4-health-canvas'); - const gradCanvas = container.querySelector('.p4-grad-canvas'); - - return { - container, - canvas, - ctx: canvas.getContext('2d'), - gradCanvas, - gradCtx: gradCanvas.getContext('2d'), - }; -} - -// --------------------------------------------------------------------------- -// Session Preset UI -// --------------------------------------------------------------------------- - -function createSessionUI(sessionManager, deps) { - const drawerBody = document.querySelector('.cs-drawer-body'); - if (!drawerBody) { - return { container: document.createElement('div') }; - } - - const container = document.createElement('div'); - container.className = 'p4-session-section'; - - const presets = sessionManager.list(); - - container.innerHTML = ` -
    Session Presets
    -
    - - - -
    -
    - - - -
    - `; - - // Insert at the top of the drawer body (before the preset row) - const firstChild = drawerBody.firstChild; - drawerBody.insertBefore(container, firstChild); - - // Wire interactions - const $select = container.querySelector('#p4-session-select'); - const $loadBtn = container.querySelector('#p4-session-load'); - const $deleteBtn = container.querySelector('#p4-session-delete'); - const $nameInput = container.querySelector('#p4-session-name'); - const $saveBtn = container.querySelector('#p4-session-save'); - const $shareBtn = container.querySelector('#p4-session-share'); - - function refreshDropdown() { - const list = sessionManager.list(); - $select.innerHTML = `` + - list.map(p => ``).join(''); - } - - function getCurrentState() { - return { - controlSurface: deps.controlSurface ? deps.controlSurface.getState() : null, - synthPresetId: deps.getActiveSynthPresetId ? deps.getActiveSynthPresetId() : null, - groupOverrides: deps.getGroupOverrides ? deps.getGroupOverrides() : null, - inputPipeline: deps.inputPipeline ? deps.inputPipeline.getConfig() : null, - outputPipeline: deps.outputPipeline ? deps.outputPipeline.getConfig() : null, - }; - } - - $saveBtn.addEventListener('click', () => { - const name = $nameInput.value.trim(); - if (!name) return; - sessionManager.save(name, getCurrentState()); - $nameInput.value = ''; - refreshDropdown(); - $select.value = name; - }); - - $loadBtn.addEventListener('click', () => { - const name = $select.value; - if (!name) return; - const preset = sessionManager.load(name); - if (preset && deps.onSessionLoad) { - deps.onSessionLoad(preset); - } - }); - - $deleteBtn.addEventListener('click', () => { - const name = $select.value; - if (!name) return; - sessionManager.delete(name); - refreshDropdown(); - }); - - $shareBtn.addEventListener('click', () => { - const state = getCurrentState(); - const preset = sessionManager.capture('share', state); - const params = sessionManager.toURL(preset); - const url = new URL(window.location.href); - // Clear existing session params - for (const key of ['sp', 'cs', 'op', 'co']) { - url.searchParams.delete(key); - } - // Append session params - const sessionParams = new URLSearchParams(params); - for (const [k, v] of sessionParams) { - url.searchParams.set(k, v); - } - navigator.clipboard.writeText(url.toString()).then(() => { - $shareBtn.textContent = 'Copied!'; - setTimeout(() => { $shareBtn.textContent = 'URL'; }, 1500); - }).catch(() => { - $shareBtn.textContent = 'Error'; - setTimeout(() => { $shareBtn.textContent = 'URL'; }, 1500); - }); - }); - - return { container, refreshDropdown }; -} - -// --------------------------------------------------------------------------- -// Wire Output Pipeline Sliders -// --------------------------------------------------------------------------- - -/** - * Listen for controlsurface:change events and pipe output-related params - * into the OutputPipeline instance. - */ -function wireOutputPipelineSliders(outputPipeline, controlSurface) { - document.addEventListener('controlsurface:change', (e) => { - const p = e.detail; - - if (p.outputSmoothing != null) { - outputPipeline.setSmoothing(p.outputSmoothing); - } - if (p.outputSlewRate != null) { - outputPipeline.setSlewRate(p.outputSlewRate); - } - if (p.globalCurve != null) { - outputPipeline.setGlobalCurve(p.globalCurve); - } - // Note: 'tame' is handled at groupOverrides init time (baked into min/max), - // not as a runtime pipeline stage. - }); -} - -// --------------------------------------------------------------------------- -// Weight Health Update -// --------------------------------------------------------------------------- - -function getFlatWeights(iml) { - // WasmIML has _getFlatWeights() - if (typeof iml._getFlatWeights === 'function') { - return iml._getFlatWeights(); - } - // JS IML: flatten from mlp.getWeights() structure - if (iml.mlp && typeof iml.mlp.getWeights === 'function') { - const structured = iml.mlp.getWeights(); - const flat = []; - for (const layer of structured) { - for (const node of layer) { - flat.push(...node.weights, node.bias); - } - } - return new Float32Array(flat); - } - return null; -} - -function updateWeightHealth(iml, weightHealth, canvas, ctx, gradientFlow, gradCanvas, gradCtx) { - const weights = getFlatWeights(iml); - if (weights) { - weightHealth.update(weights); - } - - if (ctx && canvas) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - weightHealth.draw(ctx, 0, 0, canvas.width, canvas.height); - } - - if (gradCtx && gradCanvas) { - gradCtx.clearRect(0, 0, gradCanvas.width, gradCanvas.height); - gradientFlow.draw(gradCtx, 0, 0, gradCanvas.width, gradCanvas.height); - } -} - -// --------------------------------------------------------------------------- -// CSS injection -// --------------------------------------------------------------------------- - -function injectPhase4Styles() { - if (document.getElementById('phase4-styles')) return; - - const style = document.createElement('style'); - style.id = 'phase4-styles'; - style.textContent = ` -/* ---- Phase 4: Freeze Button ---- */ -.p4-freeze-btn { - display: flex; - align-items: center; - justify-content: center; - padding: 0 6px; - min-width: 28px; - color: rgba(255, 255, 255, 0.5); - transition: color 0.15s, background 0.15s; -} - -.p4-freeze-btn.active { - color: #5bcefa; - background: rgba(91, 206, 250, 0.12); - border-color: rgba(91, 206, 250, 0.3); -} - -.p4-freeze-btn:hover { - color: #5bcefa; -} - -/* ---- Phase 4: Network Health Section ---- */ -.p4-health-section { - border-top: 1px solid rgba(255, 255, 255, 0.06); - margin-top: 4px; -} - -.p4-health-row { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 0; -} - -.p4-health-label { - font-size: 10px; - color: rgba(255, 255, 255, 0.45); - min-width: 70px; - flex-shrink: 0; -} - -.p4-health-canvas { - border-radius: 3px; - flex: 1; - max-width: 140px; - height: 32px; -} - -.p4-grad-canvas { - border-radius: 3px; - flex: 1; - max-width: 140px; - height: 48px; -} - -/* ---- Phase 4: Session Presets ---- */ -.p4-session-section { - padding: 10px 16px 8px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); -} - -.p4-session-header { - font-size: 10px; - font-weight: 600; - color: rgba(255, 255, 255, 0.6); - text-transform: uppercase; - letter-spacing: 0.7px; - margin-bottom: 6px; -} - -.p4-session-row { - display: flex; - align-items: center; - gap: 4px; - margin-bottom: 4px; -} - -.p4-session-select { - flex: 1; - min-width: 0; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - color: rgba(255, 255, 255, 0.8); - font-size: 11px; - padding: 4px 6px; - font-family: inherit; -} - -.p4-session-name { - flex: 1; - min-width: 0; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - color: rgba(255, 255, 255, 0.8); - font-size: 11px; - padding: 4px 6px; - font-family: inherit; - outline: none; -} - -.p4-session-name::placeholder { - color: rgba(255, 255, 255, 0.25); -} - -.p4-session-name:focus { - border-color: var(--accent, #ff6a00); -} - -.p4-session-btn { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 4px; - color: rgba(255, 255, 255, 0.6); - font-size: 10px; - padding: 4px 8px; - cursor: pointer; - font-family: inherit; - white-space: nowrap; - transition: all 0.12s; -} - -.p4-session-btn:hover { - background: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.9); -} - -.p4-session-save { - color: var(--accent, #ff6a00); - border-color: rgba(255, 106, 0, 0.2); -} - -.p4-session-save:hover { - background: rgba(255, 106, 0, 0.12); -} - -.p4-session-del { - color: rgba(255, 80, 80, 0.7); - font-size: 14px; - padding: 2px 6px; - line-height: 1; -} - -.p4-session-del:hover { - color: rgba(255, 80, 80, 1); - background: rgba(255, 80, 80, 0.1); -} - -.p4-session-share { - color: rgba(150, 180, 255, 0.7); - border-color: rgba(150, 180, 255, 0.15); -} - -.p4-session-share:hover { - background: rgba(150, 180, 255, 0.1); - color: rgba(150, 180, 255, 1); -} -`; - document.head.appendChild(style); -} diff --git a/playground/js/ui/pressure-feedback.js b/playground/js/ui/pressure-feedback.js deleted file mode 100644 index c9b6978..0000000 --- a/playground/js/ui/pressure-feedback.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Pressure / Hold-Duration Feedback - * - * Combines touch pressure (Force Touch, stylus) and hold duration into a - * single intensity multiplier (0-1). Applied to noise growth on thumbs-down - * and train intensity on thumbs-up. - * - * When disabled, getIntensity() returns 1.0 (passthrough). - * - * @module pressure-feedback - */ - -export class PressureFeedback { - /** - * @param {object} [options] - * @param {boolean} [options.pressureEnabled=false] - use event.pressure - * @param {boolean} [options.holdEnabled=true] - ramp intensity by hold duration - * @param {number} [options.holdCurve=2] - seconds to reach max intensity - * @param {number} [options.minIntensity=0.15] - floor when active (quick tap) - */ - constructor(options = {}) { - this._pressureEnabled = options.pressureEnabled ?? false; - this._holdEnabled = options.holdEnabled ?? true; - this._holdCurve = Math.max(0.1, options.holdCurve ?? 2); - this._minIntensity = Math.max(0, Math.min(1, options.minIntensity ?? 0.15)); - - // Runtime state - this._active = false; // pointer is currently down - this._startTime = 0; // timestamp of pointerdown (ms) - this._currentPressure = 0; // latest event.pressure (0-1) - this._holdIntensity = 0; // computed hold intensity (0-1) - } - - // ---- Pointer event handlers ---- - - /** - * Call on pointerdown. Starts tracking hold duration and pressure. - * @param {PointerEvent} event - */ - onPointerDown(event) { - this._active = true; - this._startTime = performance.now(); - this._currentPressure = this._extractPressure(event); - this._holdIntensity = 0; - } - - /** - * Call on pointermove while pointer is down. - * Updates pressure reading. - * @param {PointerEvent} event - */ - onPointerMove(event) { - if (!this._active) return; - this._currentPressure = this._extractPressure(event); - this._updateHold(); - } - - /** - * Call on pointerup / pointercancel. - */ - onPointerUp(_event) { - this.reset(); - } - - // ---- Queries ---- - - /** - * Get current intensity multiplier. - * - * - If both pressure and hold are disabled, returns 1.0 (passthrough). - * - If active and at least one source is enabled, returns max(pressure, holdIntensity). - * - If not active (no pointer down), returns the minIntensity floor. - * - * @returns {number} 0-1 - */ - getIntensity() { - if (!this._pressureEnabled && !this._holdEnabled) return 1.0; - if (!this._active) return this._minIntensity; - - this._updateHold(); - - let intensity = 0; - - if (this._pressureEnabled) { - intensity = Math.max(intensity, this._currentPressure); - } - - if (this._holdEnabled) { - intensity = Math.max(intensity, this._holdIntensity); - } - - // Ensure at least minIntensity when active - return Math.max(this._minIntensity, intensity); - } - - /** - * Whether a pointer is currently held down. - * @returns {boolean} - */ - get active() { - return this._active; - } - - /** - * Whether any source is enabled. - * @returns {boolean} - */ - get enabled() { - return this._pressureEnabled || this._holdEnabled; - } - - // ---- Configuration ---- - - /** - * Toggle pressure and hold-duration sources independently. - * @param {boolean} pressure - * @param {boolean} holdDuration - */ - setEnabled(pressure, holdDuration) { - this._pressureEnabled = !!pressure; - this._holdEnabled = !!holdDuration; - } - - /** - * Set the hold-duration ramp time. - * @param {number} seconds - time to reach max intensity (0.1-10) - */ - setHoldCurve(seconds) { - this._holdCurve = Math.max(0.1, Math.min(10, seconds)); - } - - /** @returns {number} */ - getHoldCurve() { return this._holdCurve; } - /** @returns {boolean} */ - getPressureEnabled() { return this._pressureEnabled; } - /** @returns {boolean} */ - getHoldEnabled() { return this._holdEnabled; } - - /** - * Reset state (called on pointer up). - */ - reset() { - this._active = false; - this._startTime = 0; - this._currentPressure = 0; - this._holdIntensity = 0; - } - - // ---- Serialization ---- - - getConfig() { - return { - pressureEnabled: this._pressureEnabled, - holdEnabled: this._holdEnabled, - holdCurve: this._holdCurve, - minIntensity: this._minIntensity, - }; - } - - setConfig(config) { - if (config.pressureEnabled != null) this._pressureEnabled = !!config.pressureEnabled; - if (config.holdEnabled != null) this._holdEnabled = !!config.holdEnabled; - if (config.holdCurve != null) this.setHoldCurve(config.holdCurve); - if (config.minIntensity != null) this._minIntensity = Math.max(0, Math.min(1, config.minIntensity)); - } - - // ---- Internal ---- - - /** - * Extract pressure from a pointer event. - * Falls back to 1.0 on devices without pressure support. - * A pressure of 0 on non-touch events means "no pressure data" — treat as 1.0. - */ - _extractPressure(event) { - if (!event) return 1.0; - const p = event.pressure; - // Most mouse/touch events report 0.5 for button-down without pressure sensing. - // A true 0 means "no pressure data" (mouse hover, etc.) — fall back to 1.0. - if (typeof p !== 'number' || p === 0) return 1.0; - return Math.max(0, Math.min(1, p)); - } - - /** - * Update hold intensity based on elapsed time since pointer down. - * Uses an ease-in curve (quadratic). - */ - _updateHold() { - if (!this._active || !this._holdEnabled) return; - const elapsed = (performance.now() - this._startTime) / 1000; // seconds - const t = Math.min(1, elapsed / this._holdCurve); - // Ease-in (quadratic) — slow start, fast finish - this._holdIntensity = t * t; - } -} diff --git a/playground/js/ui/region-pin.js b/playground/js/ui/region-pin.js deleted file mode 100644 index b57d164..0000000 --- a/playground/js/ui/region-pin.js +++ /dev/null @@ -1,205 +0,0 @@ -// Region Pin Manager — Pin rectangular input-space regions (Approach A: Example Pinning) -// -// Pinned regions capture training examples whose inputs fall within a rectangle -// in the 2D input space. These pinned examples: -// - Are always included in training with high weight -// - Cannot be evicted by FIFO example rotation -// - Anchor behavior in that region while the user explores elsewhere -// -// Up to 5 pinned regions, each with a distinct color from the palette. -// -// Usage: -// import { RegionPinManager } from './region-pin.js'; -// const regionPins = new RegionPinManager(); -// const id = regionPins.pin({ x1: 0.2, y1: 0.3, x2: 0.5, y2: 0.7 }, examples); -// const pinned = regionPins.getPinnedExamples(); // { features: [...], labels: [...] } - -const MAX_PINS = 5; - -// Distinct colors for pinned region overlays (used for joy-map rendering) -const PIN_PALETTE = [ - 'rgba(0, 188, 212, 0.15)', // teal - 'rgba(156, 39, 176, 0.15)', // purple - 'rgba(255, 193, 7, 0.15)', // amber - 'rgba(233, 30, 99, 0.15)', // rose - 'rgba(139, 195, 74, 0.15)', // lime -]; - -let _nextId = 1; - -export class RegionPinManager { - constructor() { - // Array of { id, region: {x1,y1,x2,y2}, color, examples: { features: [[]], labels: [[]] } } - this._pins = []; - } - - // ---- Public API ---- - - /** - * Pin a region. Captures examples whose inputs fall within the region. - * @param {{ x1: number, y1: number, x2: number, y2: number }} region - * Normalized [0,1] coordinates. x1 < x2, y1 < y2. - * @param {{ features: Array>, labels: Array> }} examples - * The current full training dataset to filter from. - * @returns {number} Pin ID, or -1 if at capacity. - */ - pin(region, examples) { - if (this._pins.length >= MAX_PINS) { - console.warn('[RegionPin] Max pins reached (' + MAX_PINS + ')'); - return -1; - } - - // Normalize region bounds - const r = { - x1: Math.min(region.x1, region.x2), - y1: Math.min(region.y1, region.y2), - x2: Math.max(region.x1, region.x2), - y2: Math.max(region.y1, region.y2), - }; - - // Filter examples whose inputs fall within the region - const pinnedFeatures = []; - const pinnedLabels = []; - if (examples && examples.features) { - for (let i = 0; i < examples.features.length; i++) { - const f = examples.features[i]; - // Inputs are the first 2 elements (joystick X, Y) — without bias - const x = f[0]; - const y = f.length > 1 ? f[1] : 0.5; - if (x >= r.x1 && x <= r.x2 && y >= r.y1 && y <= r.y2) { - pinnedFeatures.push([...f]); - pinnedLabels.push([...(examples.labels[i] || [])]); - } - } - } - - const id = _nextId++; - const colorIndex = this._pins.length % PIN_PALETTE.length; - - this._pins.push({ - id, - region: r, - color: PIN_PALETTE[colorIndex], - examples: { - features: pinnedFeatures, - labels: pinnedLabels, - }, - }); - - this._dispatch('regionpin:add', { - id, - region: r, - exampleCount: pinnedFeatures.length, - total: this._pins.length, - }); - - return id; - } - - /** - * Remove a pinned region by ID. - * @param {number} pinId - * @returns {boolean} True if removed. - */ - unpin(pinId) { - const idx = this._pins.findIndex(p => p.id === pinId); - if (idx < 0) return false; - this._pins.splice(idx, 1); - this._dispatch('regionpin:remove', { id: pinId, total: this._pins.length }); - return true; - } - - /** - * Get all pinned regions for joy-map overlay rendering. - * @returns {Array<{ id: number, region: {x1,y1,x2,y2}, color: string, exampleCount: number }>} - */ - getRegions() { - return this._pins.map(p => ({ - id: p.id, - region: { ...p.region }, - color: p.color, - exampleCount: p.examples.features.length, - })); - } - - /** - * Get all pinned examples merged together (for training). - * These should always be included in the training set alongside regular examples. - * @returns {{ features: Array>, labels: Array> }} - */ - getPinnedExamples() { - const features = []; - const labels = []; - for (const pin of this._pins) { - for (let i = 0; i < pin.examples.features.length; i++) { - features.push(pin.examples.features[i]); - labels.push(pin.examples.labels[i]); - } - } - return { features, labels }; - } - - /** - * Check if a 2D input point falls within any pinned region. - * @param {Array} inputs [x, y, ...] - * @returns {boolean} - */ - isInPinnedRegion(inputs) { - const x = inputs[0]; - const y = inputs.length > 1 ? inputs[1] : 0.5; - for (const pin of this._pins) { - const r = pin.region; - if (x >= r.x1 && x <= r.x2 && y >= r.y1 && y <= r.y2) { - return true; - } - } - return false; - } - - /** - * Current number of pinned regions. - * @returns {number} - */ - get count() { - return this._pins.length; - } - - // ---- Serialization ---- - - getState() { - return { - pins: this._pins.map(p => ({ - id: p.id, - region: { ...p.region }, - color: p.color, - examples: { - features: p.examples.features.map(f => [...f]), - labels: p.examples.labels.map(l => [...l]), - }, - })), - }; - } - - setState(saved) { - if (!saved || !Array.isArray(saved.pins)) return; - this._pins = saved.pins.map(p => ({ - id: p.id || _nextId++, - region: p.region || { x1: 0, y1: 0, x2: 1, y2: 1 }, - color: p.color || PIN_PALETTE[0], - examples: { - features: (p.examples?.features || []).map(f => [...f]), - labels: (p.examples?.labels || []).map(l => [...l]), - }, - })); - // Advance ID counter past any restored IDs - for (const p of this._pins) { - if (p.id >= _nextId) _nextId = p.id + 1; - } - } - - // ---- Internal ---- - - _dispatch(type, detail) { - document.dispatchEvent(new CustomEvent(type, { detail })); - } -} diff --git a/playground/js/ui/session-presets.js b/playground/js/ui/session-presets.js deleted file mode 100644 index e50c531..0000000 --- a/playground/js/ui/session-presets.js +++ /dev/null @@ -1,291 +0,0 @@ -/** - * 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(); - - /** - * Optional reference to an active ShapeSeq engine. - * When set, save/load will include ShapeSeq state. - * @private @type {import('../shapeseq/sequencer.js').ShapeSeqEngine|null} - */ - this._shapeSeqEngine = null; - - /** - * Lazy-loaded ShapeSeq session helpers. - * @private @type {{ serializeShapeSeqState: Function, restoreShapeSeqState: Function }|null} - */ - this._shapeSeqSession = null; - } - - /** - * Register a ShapeSeq engine so that save/load includes its state. - * Call with null to unregister. - * - * @param {import('../shapeseq/sequencer.js').ShapeSeqEngine|null} engine - */ - setShapeSeqEngine(engine) { - this._shapeSeqEngine = engine; - if (engine && !this._shapeSeqSession) { - // Lazy-load the session helpers to avoid hard dependency - import('../shapeseq/session.js').then(mod => { - this._shapeSeqSession = mod; - }).catch(() => { - // ShapeSeq module not available — ignore silently - this._shapeSeqSession = null; - }); - } - } - - /** - * 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) { - const preset = { - name, - controlSurface: state.controlSurface || null, - synthPresetId: state.synthPresetId || null, - groupOverrides: state.groupOverrides || null, - inputPipeline: state.inputPipeline || null, - outputPipeline: state.outputPipeline || null, - timestamp: Date.now(), - }; - - // Include ShapeSeq state when engine is registered - if (this._shapeSeqEngine && this._shapeSeqSession) { - preset.shapeseq = this._shapeSeqSession.serializeShapeSeqState(this._shapeSeqEngine); - } - - return preset; - } - - /** - * 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. - * - * If the preset contains a `shapeseq` key and a ShapeSeq engine is - * registered, the ShapeSeq state is automatically restored. Presets - * without a `shapeseq` key load fine (backward compatible). - * - * @param {string} name - * @returns {object|null} - */ - load(name) { - const preset = this._presets[name] || null; - if (preset && preset.shapeseq && this._shapeSeqEngine && this._shapeSeqSession) { - this._shapeSeqSession.restoreShapeSeqState(this._shapeSeqEngine, preset.shapeseq); - } - return preset; - } - - /** - * 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); - } - } -} diff --git a/playground/js/ui/snapshot-stack.js b/playground/js/ui/snapshot-stack.js deleted file mode 100644 index 9d2e83e..0000000 --- a/playground/js/ui/snapshot-stack.js +++ /dev/null @@ -1,169 +0,0 @@ -// Snapshot Stack — Multi-level undo for weight states -// -// A ring buffer of weight snapshots tagged with context (e.g. "before thumbs-down", -// "before train"). Auto-snapshot triggers in the app push snapshots before destructive -// operations. Users can undo (pop), peek, list, or jump to any snapshot. -// -// Each snapshot stores: -// - weights: the full flat weight array (Array or Float32Array) -// - noiseLevel: the RL noise level at time of snapshot -// - zoomLevel: the input pipeline zoom level (optional) -// - tag: human-readable label for UI display -// - timestamp: Date.now() when captured -// -// Usage: -// import { SnapshotStack } from './snapshot-stack.js'; -// const stack = new SnapshotStack(20); -// stack.push('before thumbs-down', { weights, noiseLevel, zoomLevel }); -// const prev = stack.pop(); // undo - -export class SnapshotStack { - /** - * @param {number} maxSnapshots Ring buffer capacity. Oldest evicted when full. - */ - constructor(maxSnapshots = 20) { - this._max = maxSnapshots; - this._stack = []; // Array of { tag, timestamp, state: { weights, noiseLevel, zoomLevel } } - } - - // ---- Core operations ---- - - /** - * Push a snapshot onto the stack. - * @param {string} tag Human-readable label (e.g. "before thumbs-down") - * @param {{ weights: Array|Float32Array, noiseLevel: number, zoomLevel?: number }} state - */ - push(tag, state) { - const snapshot = { - tag, - timestamp: Date.now(), - state: { - weights: Array.isArray(state.weights) - ? [...state.weights] - : Array.from(state.weights), - noiseLevel: state.noiseLevel ?? 0, - zoomLevel: state.zoomLevel ?? 1.0, - }, - }; - - this._stack.push(snapshot); - - // Ring buffer eviction: drop oldest if over capacity - if (this._stack.length > this._max) { - this._stack.shift(); - } - - this._dispatch('snapshot:push', { tag, depth: this.depth }); - } - - /** - * Pop the most recent snapshot (undo). - * @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null} - */ - pop() { - if (this._stack.length === 0) return null; - const snapshot = this._stack.pop(); - this._dispatch('snapshot:pop', { tag: snapshot.tag, depth: this.depth }); - return snapshot.state; - } - - /** - * Peek at the most recent snapshot without removing it. - * @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null} - */ - peek() { - if (this._stack.length === 0) return null; - return this._stack[this._stack.length - 1].state; - } - - /** - * List all snapshots (newest last) for UI display. - * @returns {Array<{ tag: string, timestamp: number, index: number }>} - */ - list() { - return this._stack.map((snap, i) => ({ - tag: snap.tag, - timestamp: snap.timestamp, - index: i, - })); - } - - /** - * Jump to a specific snapshot by index. Removes everything above it. - * @param {number} index - * @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null} - */ - jumpTo(index) { - if (index < 0 || index >= this._stack.length) return null; - const snapshot = this._stack[index]; - // Truncate: keep entries 0..index (inclusive), remove the rest - this._stack = this._stack.slice(0, index + 1); - this._dispatch('snapshot:jump', { tag: snapshot.tag, depth: this.depth }); - return snapshot.state; - } - - /** - * Current stack depth. - * @returns {number} - */ - get depth() { - return this._stack.length; - } - - /** - * Clear all snapshots. - */ - clear() { - this._stack = []; - this._dispatch('snapshot:clear', { depth: 0 }); - } - - // ---- Serialization ---- - - /** - * Serialize for localStorage persistence. - */ - getState() { - return { - max: this._max, - snapshots: this._stack.map(s => ({ - tag: s.tag, - timestamp: s.timestamp, - state: { - weights: s.state.weights, - noiseLevel: s.state.noiseLevel, - zoomLevel: s.state.zoomLevel, - }, - })), - }; - } - - /** - * Restore from serialized state. - */ - setState(saved) { - if (!saved) return; - if (typeof saved.max === 'number') this._max = saved.max; - if (Array.isArray(saved.snapshots)) { - this._stack = saved.snapshots.map(s => ({ - tag: s.tag || 'restored', - timestamp: s.timestamp || Date.now(), - state: { - weights: s.state?.weights || [], - noiseLevel: s.state?.noiseLevel ?? 0, - zoomLevel: s.state?.zoomLevel ?? 1.0, - }, - })); - // Trim to capacity - while (this._stack.length > this._max) { - this._stack.shift(); - } - } - } - - // ---- Internal ---- - - _dispatch(type, detail) { - document.dispatchEvent(new CustomEvent(type, { detail })); - } -} diff --git a/playground/js/ui/visualizer.js b/playground/js/ui/visualizer.js deleted file mode 100644 index 904a41b..0000000 --- a/playground/js/ui/visualizer.js +++ /dev/null @@ -1,288 +0,0 @@ -// Flow field particle system with Canvas2D -// Controlled by 20 output parameters from the IML network - -// Simple value noise (no dependencies) -const PERM = new Uint8Array(512); -{ - const p = new Uint8Array(256); - for (let i = 0; i < 256; i++) p[i] = i; - for (let i = 255; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [p[i], p[j]] = [p[j], p[i]]; - } - for (let i = 0; i < 512; i++) PERM[i] = p[i & 255]; -} - -function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } -function lerp(a, b, t) { return a + t * (b - a); } - -function grad(hash, x, y) { - const h = hash & 3; - const u = h < 2 ? x : y; - const v = h < 2 ? y : x; - return ((h & 1) ? -u : u) + ((h & 2) ? -v : v); -} - -function noise2D(x, y) { - const X = Math.floor(x) & 255; - const Y = Math.floor(y) & 255; - const xf = x - Math.floor(x); - const yf = y - Math.floor(y); - const u = fade(xf); - const v = fade(yf); - - const aa = PERM[PERM[X] + Y]; - const ab = PERM[PERM[X] + Y + 1]; - const ba = PERM[PERM[X + 1] + Y]; - const bb = PERM[PERM[X + 1] + Y + 1]; - - return lerp( - lerp(grad(aa, xf, yf), grad(ba, xf - 1, yf), u), - lerp(grad(ab, xf, yf - 1), grad(bb, xf - 1, yf - 1), u), - v - ); -} - -const TWO_PI = Math.PI * 2; - -export class FlowFieldVisualizer { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.particles = []; - this.numParticles = 400; - this.time = 0; - - // Parameters (all 0-1 from IML, mapped to visual ranges) - this.params = { - angleOffset: 0, // p0: flow direction - scale: 0.005, // p1: pattern size - speed: 2, // p2: particle speed - hueBase: 180, // p3: base color - hueSpread: 60, // p4: color variation - particleSize: 3, // p5: dot radius - fadeRate: 0.05, // p6: trail length - turbulence: 1, // p7: chaos - attractStrength: 0.8, // p8: pull toward screen center - attractRadius: 200, // p9: radius where attraction is strongest - dispersionRate: 2.0, // p10: speed of outward dispersion pulses - dispersionAmount: 1.0, // p11: strength of outward dispersion - particleLifetime: 220, // p12: average frames before respawn - respawnStyle: 0.0, // p13: 0=random, 1=edge, 2=center-burst - advectionMode: 0.0, // p14: flow->orbit->radial blend - inertia: 0.2, // p15: velocity memory - drag: 0.02, // p16: velocity damping - repulsorStrength: 0.0, // p17: repulsor force amount - repulsorCount: 0, // p18: number of active repulsors - repulsorOrbitRate: 0.8, // p19: repulsor orbital speed - }; - - this.resize(); - this.initParticles(); - } - - resize() { - const rect = this.canvas.getBoundingClientRect(); - const dpr = window.devicePixelRatio || 1; - this.canvas.width = rect.width * dpr; - this.canvas.height = rect.height * dpr; - this.ctx.scale(dpr, dpr); - this.width = rect.width; - this.height = rect.height; - } - - initParticles() { - this.particles = []; - for (let i = 0; i < this.numParticles; i++) { - this.particles.push(this.makeParticle(i)); - } - // Clear canvas to black - this.ctx.fillStyle = '#0d0d0d'; - this.ctx.fillRect(0, 0, this.width, this.height); - } - - makeParticle(id) { - return { - x: Math.random() * this.width, - y: Math.random() * this.height, - id, - age: Math.floor(Math.random() * this.params.particleLifetime), - life: this.computeLifetime(), - vx: 0, - vy: 0, - }; - } - - computeLifetime() { - const variance = 0.65 + Math.random() * 0.7; - return Math.max(10, Math.floor(this.params.particleLifetime * variance)); - } - - respawnParticle(p) { - const mode = Math.min(2, Math.floor(this.params.respawnStyle * 2.999)); - const { width, height } = this; - - if (mode === 1) { - // Edge respawn - const side = Math.floor(Math.random() * 4); - if (side === 0) { p.x = Math.random() * width; p.y = 0; } - if (side === 1) { p.x = width; p.y = Math.random() * height; } - if (side === 2) { p.x = Math.random() * width; p.y = height; } - if (side === 3) { p.x = 0; p.y = Math.random() * height; } - // Give edge spawns an inward impulse. - const towardCenterX = width * 0.5 - p.x; - const towardCenterY = height * 0.5 - p.y; - const inwardDist = Math.hypot(towardCenterX, towardCenterY) + 1e-6; - p.vx = (towardCenterX / inwardDist) * 2.0; - p.vy = (towardCenterY / inwardDist) * 2.0; - } else if (mode === 2) { - // Center-burst respawn - const angle = Math.random() * TWO_PI; - const r = Math.random() * Math.min(width, height) * 0.08; - p.x = width * 0.5 + Math.cos(angle) * r; - p.y = height * 0.5 + Math.sin(angle) * r; - p.vx = Math.cos(angle) * 2.5; - p.vy = Math.sin(angle) * 2.5; - } else { - // Random respawn - p.x = Math.random() * width; - p.y = Math.random() * height; - p.vx = (Math.random() * 2 - 1) * 0.5; - p.vy = (Math.random() * 2 - 1) * 0.5; - } - - p.age = 0; - p.life = this.computeLifetime(); - } - - // Set parameters from IML output (all values 0-1) - setParams(outputs) { - if (!outputs || outputs.length < 20) return; - this.params.angleOffset = outputs[0] * TWO_PI; - this.params.scale = 0.001 + outputs[1] * 0.009; - this.params.speed = 0.5 + outputs[2] * 4.5; - this.params.hueBase = outputs[3] * 360; - this.params.hueSpread = outputs[4] * 120; - this.params.particleSize = 1 + outputs[5] * 5; - this.params.fadeRate = 0.01 + outputs[6] * 0.14; - this.params.turbulence = outputs[7] * 2; - this.params.attractStrength = 0.1 + outputs[8] * 2.9; - this.params.attractRadius = 40 + outputs[9] * 420; - this.params.dispersionRate = 0.2 + outputs[10] * 8; - this.params.dispersionAmount = outputs[11] * 3; - this.params.particleLifetime = 30 + outputs[12] * 470; - this.params.respawnStyle = outputs[13]; - this.params.advectionMode = outputs[14]; - this.params.inertia = outputs[15] * 0.98; - this.params.drag = outputs[16] * 0.35; - this.params.repulsorStrength = outputs[17] * 4.5; - this.params.repulsorCount = Math.floor(outputs[18] * 4.999); - this.params.repulsorOrbitRate = 0.1 + outputs[19] * 2.9; - } - - draw() { - const { ctx, width, height, params } = this; - this.time += 0.003; - - // Fade existing content (creates trails) - ctx.fillStyle = `rgba(13, 13, 13, ${params.fadeRate})`; - ctx.fillRect(0, 0, width, height); - - for (const p of this.particles) { - const cx = width * 0.5; - const cy = height * 0.5; - - // Sample flow field - const nx = p.x * params.scale; - const ny = p.y * params.scale; - const angle = noise2D(nx + this.time, ny) * TWO_PI + params.angleOffset; - const curl = noise2D(nx + 100, ny + 100 + this.time * 0.5) * params.turbulence; - - // Mix between three advection fields for larger visual mode changes. - const flowVx = Math.cos(angle + curl) * params.speed; - const flowVy = Math.sin(angle + curl) * params.speed; - const fromCenterX = p.x - cx; - const fromCenterY = p.y - cy; - const centerDist = Math.hypot(fromCenterX, fromCenterY) + 1e-6; - const radialX = fromCenterX / centerDist; - const radialY = fromCenterY / centerDist; - const orbitX = -radialY; - const orbitY = radialX; - const orbitVx = orbitX * params.speed; - const orbitVy = orbitY * params.speed; - const radialVx = radialX * params.speed; - const radialVy = radialY * params.speed; - - const modeBlend = params.advectionMode * 2; - let targetVx; - let targetVy; - if (modeBlend < 1) { - targetVx = lerp(flowVx, orbitVx, modeBlend); - targetVy = lerp(flowVy, orbitVy, modeBlend); - } else { - targetVx = lerp(orbitVx, radialVx, modeBlend - 1); - targetVy = lerp(orbitVy, radialVy, modeBlend - 1); - } - p.vx = p.vx * params.inertia + targetVx * (1 - params.inertia); - p.vy = p.vy * params.inertia + targetVy * (1 - params.inertia); - p.vx *= (1 - params.drag); - p.vy *= (1 - params.drag); - let nextX = p.x + p.vx; - let nextY = p.y + p.vy; - - // Central attractor keeps trajectories from sticking to the outer edges. - const dx = cx - nextX; - const dy = cy - nextY; - const dist = Math.hypot(dx, dy) + 1e-6; - const nxCenter = dx / dist; - const nyCenter = dy / dist; - const normalizedDist = Math.min(dist / params.attractRadius, 2); - const falloff = 1 / (1 + normalizedDist * normalizedDist); - nextX += nxCenter * params.attractStrength * falloff; - nextY += nyCenter * params.attractStrength * falloff; - - // Time-varying dispersion pushes particles outward near the center. - const dispersionPulse = 0.5 + 0.5 * Math.sin(this.time * params.dispersionRate + p.id * 0.07); - const dispersionForce = params.dispersionAmount * dispersionPulse * falloff; - nextX -= nxCenter * dispersionForce; - nextY -= nyCenter * dispersionForce; - - // Orbiting repulsor points carve dynamic voids and bursts. - const repulsorRadius = Math.min(width, height) * 0.28; - for (let r = 0; r < params.repulsorCount; r++) { - const phase = this.time * params.repulsorOrbitRate + (r / 4) * TWO_PI; - const wobble = 0.6 + 0.15 * r; - const rx = cx + Math.cos(phase * (1.0 + wobble)) * repulsorRadius; - const ry = cy + Math.sin(phase * (1.3 + wobble)) * repulsorRadius; - const repulseDx = nextX - rx; - const repulseDy = nextY - ry; - const distSq = repulseDx * repulseDx + repulseDy * repulseDy + 160; - const distInv = 1 / Math.sqrt(distSq); - const force = params.repulsorStrength * (650 / distSq); - nextX += repulseDx * distInv * force; - nextY += repulseDy * distInv * force; - } - - p.x = nextX; - p.y = nextY; - - // Wrap around edges - if (p.x < 0) p.x += width; - if (p.x > width) p.x -= width; - if (p.y < 0) p.y += height; - if (p.y > height) p.y -= height; - - p.age += 1; - if (p.age >= p.life) this.respawnParticle(p); - - // Color based on particle id + hue params - const hue = (params.hueBase + (p.id / this.numParticles) * params.hueSpread) % 360; - const lightness = 50 + Math.sin(p.id * 0.1 + this.time) * 15; - - ctx.fillStyle = `hsl(${hue}, 75%, ${lightness}%)`; - ctx.beginPath(); - ctx.arc(p.x, p.y, params.particleSize, 0, TWO_PI); - ctx.fill(); - } - } -} diff --git a/playground/js/ui/weight-health.js b/playground/js/ui/weight-health.js deleted file mode 100644 index 0426555..0000000 --- a/playground/js/ui/weight-health.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Weight Health Indicator — ambient indicator showing network weight statistics. - * - * Analyzes a flat weight array and produces health status + compact visualization. - * Call update() periodically (not every frame) with the current flat weights. - * - * @module weight-health - */ - -// --------------------------------------------------------------------------- -// Thresholds -// --------------------------------------------------------------------------- - -/** Weight magnitude below this is considered "dead" */ -const DEAD_THRESHOLD = 0.01; - -/** Weight magnitude above this drives sigmoid >99% saturated */ -const SATURATING_THRESHOLD = 3.0; - -/** If more than this fraction of weights are dead, status = 'dead' */ -const DEAD_FRACTION_LIMIT = 0.30; - -/** If more than this fraction of weights are saturating, status = 'saturating' */ -const SATURATED_FRACTION_LIMIT = 0.40; - -/** Number of histogram bins for weight magnitude distribution */ -const HISTOGRAM_BINS = 10; - -/** Histogram bin range: [0, MAX_HISTOGRAM_MAG] */ -const MAX_HISTOGRAM_MAG = 5.0; - -// --------------------------------------------------------------------------- -// Colors -// --------------------------------------------------------------------------- - -const COLOR_HEALTHY = { r: 100, g: 200, b: 120 }; // calm green -const COLOR_SATURATING = { r: 255, g: 120, b: 40 }; // hot orange -const COLOR_DEAD = { r: 120, g: 120, b: 120 }; // dim gray - -// --------------------------------------------------------------------------- -// WeightHealthIndicator -// --------------------------------------------------------------------------- - -export class WeightHealthIndicator { - constructor() { - this._status = null; - this._pulsePhase = 0; - } - - /** - * Update from current weights. Call periodically (e.g. every 500ms). - * - * @param {number[]|Float32Array} weightsArray — flat array of all network weights - */ - update(weightsArray) { - if (!weightsArray || weightsArray.length === 0) { - this._status = null; - return; - } - - const n = weightsArray.length; - let sumMag = 0; - let maxMag = 0; - let deadCount = 0; - let saturatedCount = 0; - - const histogram = new Array(HISTOGRAM_BINS).fill(0); - const binWidth = MAX_HISTOGRAM_MAG / HISTOGRAM_BINS; - - for (let i = 0; i < n; i++) { - const mag = Math.abs(weightsArray[i]); - sumMag += mag; - if (mag > maxMag) maxMag = mag; - - if (mag < DEAD_THRESHOLD) deadCount++; - if (mag > SATURATING_THRESHOLD) saturatedCount++; - - const bin = Math.min(Math.floor(mag / binWidth), HISTOGRAM_BINS - 1); - histogram[bin]++; - } - - const meanMagnitude = sumMag / n; - const deadFraction = deadCount / n; - const saturatedFraction = saturatedCount / n; - - // Normalize histogram to fractions - for (let i = 0; i < HISTOGRAM_BINS; i++) { - histogram[i] /= n; - } - - let health; - if (deadFraction > DEAD_FRACTION_LIMIT) { - health = 'dead'; - } else if (saturatedFraction > SATURATED_FRACTION_LIMIT) { - health = 'saturating'; - } else { - health = 'healthy'; - } - - this._status = { - health, - meanMagnitude, - maxMagnitude: maxMag, - deadFraction, - saturatedFraction, - histogram, - }; - } - - /** - * Get the current health status. - * @returns {object|null} Status object or null if never updated. - */ - getStatus() { - return this._status; - } - - /** - * Draw a compact visual indicator (ambient glow bar). - * - * @param {CanvasRenderingContext2D} ctx - * @param {number} x — left edge - * @param {number} y — top edge - * @param {number} width — total width - * @param {number} height — total height - */ - draw(ctx, x, y, width, height) { - if (!this._status) { - // No data — draw a dim placeholder - ctx.fillStyle = 'rgba(60, 60, 60, 0.4)'; - ctx.fillRect(x, y, width, height); - return; - } - - const { health, deadFraction, saturatedFraction, histogram } = this._status; - - // Advance pulse phase - this._pulsePhase += 0.04; - - // Determine base color + glow intensity - let color; - let glowAlpha; - let pulseAmount = 0; - - if (health === 'healthy') { - color = COLOR_HEALTHY; - glowAlpha = 0.6 + 0.15 * Math.sin(this._pulsePhase * 0.3); // gentle breathing - pulseAmount = 0; - } else if (health === 'saturating') { - color = COLOR_SATURATING; - glowAlpha = 0.7; - pulseAmount = 0.25 * (0.5 + 0.5 * Math.sin(this._pulsePhase * 1.5)); // pulsing - } else { - // dead - color = COLOR_DEAD; - glowAlpha = 0.3; - pulseAmount = 0; - } - - const alpha = glowAlpha + pulseAmount; - - // Background - ctx.fillStyle = 'rgba(20, 20, 20, 0.6)'; - ctx.fillRect(x, y, width, height); - - // Draw mini histogram bars - const barWidth = width / HISTOGRAM_BINS; - const maxBinVal = Math.max(...histogram, 0.01); - - for (let i = 0; i < HISTOGRAM_BINS; i++) { - const barH = (histogram[i] / maxBinVal) * height; - const bx = x + i * barWidth; - const by = y + height - barH; - - // Color gradient: first bins (low magnitude) lean toward dead color, - // last bins (high magnitude) lean toward saturating color - const t = i / (HISTOGRAM_BINS - 1); - let r, g, b; - if (t < 0.3) { - // Low magnitude — blend dead/healthy - const lt = t / 0.3; - r = COLOR_DEAD.r + (color.r - COLOR_DEAD.r) * lt; - g = COLOR_DEAD.g + (color.g - COLOR_DEAD.g) * lt; - b = COLOR_DEAD.b + (color.b - COLOR_DEAD.b) * lt; - } else if (t > 0.7) { - // High magnitude — blend toward saturating - const lt = (t - 0.7) / 0.3; - r = color.r + (COLOR_SATURATING.r - color.r) * lt; - g = color.g + (COLOR_SATURATING.g - color.g) * lt; - b = color.b + (COLOR_SATURATING.b - color.b) * lt; - } else { - r = color.r; - g = color.g; - b = color.b; - } - - ctx.fillStyle = `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${alpha.toFixed(2)})`; - ctx.fillRect(bx, by, barWidth - 1, barH); - } - - // Top glow line - ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${(alpha * 0.8).toFixed(2)})`; - ctx.fillRect(x, y, width, 1); - - // Status label - ctx.font = '8px monospace'; - ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${(alpha * 0.9).toFixed(2)})`; - ctx.textBaseline = 'top'; - ctx.textAlign = 'left'; - - const label = health === 'healthy' - ? `W: ok` - : health === 'saturating' - ? `W: sat ${(saturatedFraction * 100).toFixed(0)}%` - : `W: dead ${(deadFraction * 100).toFixed(0)}%`; - ctx.fillText(label, x + 2, y + 1); - } -} diff --git a/playground/osc-bridge/bridge.mjs b/playground/osc-bridge/bridge.mjs deleted file mode 100644 index 2439f8e..0000000 --- a/playground/osc-bridge/bridge.mjs +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env node - -// NISPS <-> OSC Bridge (bidirectional) -// WebSocket server that bridges the browser webapp and OSC-capable software -// (VCV Rack MEMLNaut module, SuperCollider, etc.). -// -// Usage: -// node bridge.mjs # defaults -// node bridge.mjs --osc-host 192.168.1.5 # send to another machine -// node bridge.mjs --osc-port 9000 # target port -// node bridge.mjs --ws-port 8765 # WebSocket listen port -// node bridge.mjs --listen-port 9001 # UDP listen port for incoming OSC -// node bridge.mjs --osc-prefix /nisps # OSC address prefix -// node bridge.mjs --bundle # send OSC bundles -// -// Webapp -> Bridge -> OSC target (param updates, state, weights) -// OSC target -> Bridge -> Webapp (output values, input values) - -import { createSocket } from 'node:dgram'; -import { WebSocketServer } from 'ws'; - -// ---- CLI args ---- -const args = process.argv.slice(2); -function flag(name, fallback) { - const idx = args.indexOf(`--${name}`); - if (idx === -1) return fallback; - return args[idx + 1] ?? fallback; -} -const hasFlag = (name) => args.includes(`--${name}`); - -const WS_PORT = parseInt(flag('ws-port', '8765'), 10); -const OSC_HOST = flag('osc-host', '127.0.0.1'); -const OSC_PORT = parseInt(flag('osc-port', '9000'), 10); -const OSC_PREFIX = flag('osc-prefix', '/nisps'); -const LISTEN_PORT = parseInt(flag('listen-port', '9001'), 10); -const USE_BUNDLES = hasFlag('bundle'); - -// ---- OSC encoding (minimal, no dependencies) ---- - -function oscPadded(len) { - return len + (4 - (len % 4)) % 4; -} - -function oscString(str) { - const len = str.length + 1; // null terminator - const padded = oscPadded(len); - const buf = Buffer.alloc(padded); - buf.write(str, 'ascii'); - return buf; -} - -function oscFloat(val) { - const buf = Buffer.alloc(4); - buf.writeFloatBE(val, 0); - return buf; -} - -function oscMessage(address, value) { - return Buffer.concat([ - oscString(address), - oscString(',f'), - oscFloat(value), - ]); -} - -function oscMessageString(address, value) { - return Buffer.concat([ - oscString(address), - oscString(',s'), - oscString(value), - ]); -} - -function oscBundle(messages) { - const header = oscString('#bundle'); - const timetag = Buffer.alloc(8); - timetag.writeUInt32BE(1, 0); - - const parts = [header, timetag]; - for (const msg of messages) { - const size = Buffer.alloc(4); - size.writeUInt32BE(msg.length, 0); - parts.push(size, msg); - } - return Buffer.concat(parts); -} - -// ---- OSC decoding ---- - -function readOscString(buf, offset) { - let end = offset; - while (end < buf.length && buf[end] !== 0) end++; - const str = buf.toString('ascii', offset, end); - const nextOffset = offset + oscPadded(end - offset + 1); - return [str, nextOffset]; -} - -function readOscFloat(buf, offset) { - if (offset + 4 > buf.length) return [0, offset + 4]; - return [buf.readFloatBE(offset), offset + 4]; -} - -function parseOscMessage(buf) { - if (buf.length < 4) return null; - let offset = 0; - - const [address, off1] = readOscString(buf, offset); - if (!address.startsWith('/')) return null; - offset = off1; - - const [tags, off2] = readOscString(buf, offset); - if (!tags.startsWith(',')) return null; - offset = off2; - - const types = tags.slice(1); - const args = []; - - for (const t of types) { - if (t === 'f') { - const [val, off] = readOscFloat(buf, offset); - args.push(val); - offset = off; - } else if (t === 's') { - const [val, off] = readOscString(buf, offset); - args.push(val); - offset = off; - } - } - - return { address, types, args }; -} - -// ---- UDP sockets ---- - -// Outgoing: sends OSC to target -const udpSend = createSocket('udp4'); - -function sendOSC(address, value) { - const msg = oscMessage(address, value); - udpSend.send(msg, OSC_PORT, OSC_HOST); -} - -function sendOSCString(address, value) { - const msg = oscMessageString(address, value); - udpSend.send(msg, OSC_PORT, OSC_HOST); -} - -function sendOSCBundle(params) { - const messages = params.map(([name, value]) => - oscMessage(`${OSC_PREFIX}/${name}`, value) - ); - const bundle = oscBundle(messages); - udpSend.send(bundle, OSC_PORT, OSC_HOST); -} - -// Incoming: listens for OSC from target -const udpRecv = createSocket('udp4'); -udpRecv.bind(LISTEN_PORT, '0.0.0.0'); - -udpRecv.on('message', (buf, _rinfo) => { - const msg = parseOscMessage(buf); - if (!msg) return; - - const wsMsg = { type: 'osc', address: msg.address }; - - if (msg.address === `${OSC_PREFIX}/output` || msg.address === '/nisps/output') { - wsMsg.type = 'outputs'; - wsMsg.values = msg.args.filter(a => typeof a === 'number'); - } else if (msg.address === `${OSC_PREFIX}/input` || msg.address === '/nisps/input') { - wsMsg.type = 'inputs'; - wsMsg.values = msg.args.filter(a => typeof a === 'number'); - } else { - wsMsg.args = msg.args; - } - - broadcastToWs(JSON.stringify(wsMsg)); -}); - -udpRecv.on('error', (err) => { - console.error('[udp] Listen error:', err.message); -}); - -// ---- WebSocket server ---- -const wss = new WebSocketServer({ port: WS_PORT }); -const wsClients = new Set(); - -function broadcastToWs(data) { - for (const ws of wsClients) { - try { - if (ws.readyState === 1) { // OPEN - ws.send(data); - } - } catch { - // ignore - } - } -} - -wss.on('connection', (ws) => { - wsClients.add(ws); - console.log(`[ws] Client connected (${wsClients.size} total)`); - - ws.send(JSON.stringify({ - type: 'info', - message: `OSC <-> ${OSC_HOST}:${OSC_PORT} (prefix: ${OSC_PREFIX}, listen: ${LISTEN_PORT})`, - })); - - ws.on('message', (raw) => { - try { - const data = JSON.parse(raw); - - // Structured message format: { type, payload } - if (data && typeof data === 'object' && data.type) { - switch (data.type) { - case 'state': - sendOSCString(`${OSC_PREFIX}/state`, JSON.stringify(data.payload)); - return; - case 'weights': - sendOSCString(`${OSC_PREFIX}/weights`, JSON.stringify(data.payload)); - return; - case 'params': - if (Array.isArray(data.payload)) { - if (USE_BUNDLES) { - sendOSCBundle(data.payload); - } else { - for (const [name, value] of data.payload) { - sendOSC(`${OSC_PREFIX}/${name}`, value); - } - } - } - return; - } - } - - // Legacy format: [[paramName, value], ...] - if (Array.isArray(data)) { - if (USE_BUNDLES) { - sendOSCBundle(data); - } else { - for (const [name, value] of data) { - sendOSC(`${OSC_PREFIX}/${name}`, value); - } - } - } - } catch (e) { - console.error('[ws] Bad message:', e.message); - } - }); - - ws.on('close', () => { - wsClients.delete(ws); - console.log(`[ws] Client disconnected (${wsClients.size} remaining)`); - }); -}); - -console.log(` -NISPS <-> OSC Bridge (bidirectional) -──────────────────────────────────── - WebSocket: ws://localhost:${WS_PORT} - OSC target: ${OSC_HOST}:${OSC_PORT} - OSC listen: 0.0.0.0:${LISTEN_PORT} - Prefix: ${OSC_PREFIX} - Mode: ${USE_BUNDLES ? 'bundles' : 'individual messages'} - - Webapp -> VCV: - params: [[name, value], ...] or { type: "params", payload: [...] } - state: { type: "state", payload: } - weights: { type: "weights", payload: } - - VCV -> Webapp: - /nisps/output -> { type: "outputs", values: [...] } - /nisps/input -> { type: "inputs", values: [...] } - - Waiting for connections... -`); diff --git a/playground/osc-bridge/bridge.ts b/playground/osc-bridge/bridge.ts deleted file mode 100644 index a7cf565..0000000 --- a/playground/osc-bridge/bridge.ts +++ /dev/null @@ -1,350 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --unstable-net - -// NISPS <-> OSC Bridge (bidirectional) -// WebSocket server that bridges the browser webapp and OSC-capable software -// (VCV Rack MEMLNaut module, SuperCollider, etc.). -// -// Webapp -> Bridge -> OSC target (param updates, state, weights) -// OSC target -> Bridge -> Webapp (output values, input values) -// -// Run with Deno: -// deno run --allow-net bridge.ts -// -// Options: -// --osc-host 192.168.1.5 Target IP (default: 127.0.0.1) -// --osc-port 9000 Target port (default: 9000 / VCV MEMLNaut) -// --osc-prefix /my Address prefix (default: /nisps) -// --ws-port 8765 WebSocket listen port (default: 8765) -// --listen-port 9001 UDP port for incoming OSC (default: 9001) -// --bundle Send OSC bundles instead of individual messages -// -// OSC address format: -// /nisps/ (webapp -> target) -// /nisps/state (webapp -> target: full JSON state) -// /nisps/weights (webapp -> target: weights JSON) -// /nisps/output (target -> webapp: output float array) -// /nisps/input (target -> webapp: input float array) - -import { parseArgs } from "jsr:@std/cli@1/parse-args"; - -// ---- CLI args ---- -const args = parseArgs(Deno.args, { - string: ["osc-host", "osc-port", "osc-prefix", "ws-port", "listen-port"], - boolean: ["bundle", "help"], - default: { - "osc-host": "127.0.0.1", - "osc-port": "9000", - "osc-prefix": "/nisps", - "ws-port": "8765", - "listen-port": "9001", - "bundle": false, - "help": false, - }, -}); - -if (args.help) { - console.log(` -NISPS <-> OSC Bridge (bidirectional) - -Usage: nisps-osc-bridge [options] - -Options: - --osc-host Target IP address (default: 127.0.0.1) - --osc-port Target UDP port (default: 9000) - --osc-prefix OSC address prefix (default: /nisps) - --ws-port WebSocket listen port (default: 8765) - --listen-port UDP listen port for incoming OSC (default: 9001) - --bundle Send OSC bundles instead of individual messages - --help Show this help -`); - Deno.exit(0); -} - -const WS_PORT = parseInt(args["ws-port"]); -const OSC_HOST = args["osc-host"]; -const OSC_PORT = parseInt(args["osc-port"]); -const OSC_PREFIX = args["osc-prefix"]; -const LISTEN_PORT = parseInt(args["listen-port"]); -const USE_BUNDLES = args.bundle; - -// ---- OSC encoding (zero dependencies) ---- - -function oscPadded(len: number): number { - return len + (4 - (len % 4)) % 4; -} - -function oscString(str: string): Uint8Array { - const encoder = new TextEncoder(); - const strBytes = encoder.encode(str); - const len = strBytes.length + 1; // null terminator - const padded = oscPadded(len); - const buf = new Uint8Array(padded); - buf.set(strBytes); - return buf; -} - -function oscFloat(val: number): Uint8Array { - const buf = new ArrayBuffer(4); - new DataView(buf).setFloat32(0, val, false); // big-endian - return new Uint8Array(buf); -} - -function concat(...arrays: Uint8Array[]): Uint8Array { - const total = arrays.reduce((s, a) => s + a.length, 0); - const result = new Uint8Array(total); - let offset = 0; - for (const a of arrays) { - result.set(a, offset); - offset += a.length; - } - return result; -} - -function oscMessage(address: string, value: number): Uint8Array { - return concat(oscString(address), oscString(",f"), oscFloat(value)); -} - -function oscMessageString(address: string, value: string): Uint8Array { - return concat(oscString(address), oscString(",s"), oscString(value)); -} - -function u32be(val: number): Uint8Array { - const buf = new ArrayBuffer(4); - new DataView(buf).setUint32(0, val, false); - return new Uint8Array(buf); -} - -function oscBundle(messages: Uint8Array[]): Uint8Array { - const header = oscString("#bundle"); - // NTP timestamp: immediately (1 in upper 32 bits) - const timetag = new Uint8Array(8); - new DataView(timetag.buffer).setUint32(0, 1, false); - - const parts: Uint8Array[] = [header, timetag]; - for (const msg of messages) { - parts.push(u32be(msg.length), msg); - } - return concat(...parts); -} - -// ---- OSC decoding ---- - -function readOscString(buf: Uint8Array, offset: number): [string, number] { - let end = offset; - while (end < buf.length && buf[end] !== 0) end++; - const decoder = new TextDecoder(); - const str = decoder.decode(buf.slice(offset, end)); - const nextOffset = offset + oscPadded(end - offset + 1); - return [str, nextOffset]; -} - -function readOscFloat(buf: Uint8Array, offset: number): [number, number] { - if (offset + 4 > buf.length) return [0, offset + 4]; - const view = new DataView(buf.buffer, buf.byteOffset + offset, 4); - return [view.getFloat32(0, false), offset + 4]; -} - -interface ParsedOscMessage { - address: string; - types: string; - args: (number | string)[]; -} - -function parseOscMessage(buf: Uint8Array): ParsedOscMessage | null { - if (buf.length < 4) return null; - let offset = 0; - - const [address, off1] = readOscString(buf, offset); - if (!address.startsWith("/")) return null; - offset = off1; - - const [tags, off2] = readOscString(buf, offset); - if (!tags.startsWith(",")) return null; - offset = off2; - - const types = tags.slice(1); - const args: (number | string)[] = []; - - for (const t of types) { - if (t === "f") { - const [val, off] = readOscFloat(buf, offset); - args.push(val); - offset = off; - } else if (t === "s") { - const [val, off] = readOscString(buf, offset); - args.push(val); - offset = off; - } - // skip unknown types - } - - return { address, types, args }; -} - -// ---- UDP sockets ---- - -// Outgoing: sends OSC to the target (VCV module) -const udpSend = Deno.listenDatagram({ port: 0, transport: "udp", hostname: "0.0.0.0" }); -const oscAddr: Deno.NetAddr = { transport: "udp", hostname: OSC_HOST, port: OSC_PORT }; - -function sendOSC(address: string, value: number): void { - const msg = oscMessage(address, value); - udpSend.send(msg, oscAddr); -} - -function sendOSCString(address: string, value: string): void { - const msg = oscMessageString(address, value); - udpSend.send(msg, oscAddr); -} - -function sendOSCBundle(params: [string, number][]): void { - const messages = params.map(([name, value]) => - oscMessage(`${OSC_PREFIX}/${name}`, value) - ); - const bundle = oscBundle(messages); - udpSend.send(bundle, oscAddr); -} - -// Incoming: listens for OSC from the target (VCV module) -const udpRecv = Deno.listenDatagram({ port: LISTEN_PORT, transport: "udp", hostname: "0.0.0.0" }); - -// ---- WebSocket server ---- -const wsClients: Set = new Set(); - -function broadcastToWs(data: string): void { - for (const ws of wsClients) { - try { - if (ws.readyState === WebSocket.OPEN) { - ws.send(data); - } - } catch { - // ignore send errors - } - } -} - -function handleWs(ws: WebSocket): void { - wsClients.add(ws); - console.log(`[ws] Client connected (${wsClients.size} total)`); - - ws.onopen = () => { - ws.send(JSON.stringify({ - type: "info", - message: `OSC <-> ${OSC_HOST}:${OSC_PORT} (prefix: ${OSC_PREFIX}, listen: ${LISTEN_PORT})`, - })); - }; - - ws.onmessage = (e) => { - try { - const data = JSON.parse(e.data as string); - - // New structured message format: { type, payload } - if (data && typeof data === "object" && data.type) { - switch (data.type) { - case "state": - // Send full state JSON as OSC string to /nisps/state - sendOSCString(`${OSC_PREFIX}/state`, JSON.stringify(data.payload)); - return; - case "weights": - // Send weights JSON as OSC string to /nisps/weights - sendOSCString(`${OSC_PREFIX}/weights`, JSON.stringify(data.payload)); - return; - case "params": - // Legacy batch format embedded in structured message - if (Array.isArray(data.payload)) { - if (USE_BUNDLES) { - sendOSCBundle(data.payload); - } else { - for (const [name, value] of data.payload) { - sendOSC(`${OSC_PREFIX}/${name}`, value); - } - } - } - return; - } - } - - // Legacy format: [[paramName, value], ...] - if (Array.isArray(data)) { - if (USE_BUNDLES) { - sendOSCBundle(data); - } else { - for (const [name, value] of data) { - sendOSC(`${OSC_PREFIX}/${name}`, value); - } - } - } - } catch (err) { - console.error("[ws] Bad message:", (err as Error).message); - } - }; - - ws.onclose = () => { - wsClients.delete(ws); - console.log(`[ws] Client disconnected (${wsClients.size} remaining)`); - }; -} - -// ---- UDP receive loop (OSC from VCV -> relay to WebSocket clients) ---- - -async function udpReceiveLoop(): Promise { - for await (const [data, _addr] of udpRecv) { - const msg = parseOscMessage(data); - if (!msg) continue; - - // Relay parsed OSC messages to all connected WebSocket clients - const wsMsg: Record = { type: "osc", address: msg.address }; - - if (msg.address === `${OSC_PREFIX}/output` || msg.address === "/nisps/output") { - // Float array of outputs - wsMsg.type = "outputs"; - wsMsg.values = msg.args.filter((a): a is number => typeof a === "number"); - } else if (msg.address === `${OSC_PREFIX}/input` || msg.address === "/nisps/input") { - // Float array of inputs - wsMsg.type = "inputs"; - wsMsg.values = msg.args.filter((a): a is number => typeof a === "number"); - } else { - // Generic OSC message - wsMsg.args = msg.args; - } - - broadcastToWs(JSON.stringify(wsMsg)); - } -} - -// Start UDP receive loop -udpReceiveLoop().catch((err) => { - console.error("[udp] Receive loop error:", err); -}); - -// Start WebSocket server -Deno.serve({ port: WS_PORT }, (req) => { - const upgrade = req.headers.get("upgrade") || ""; - if (upgrade.toLowerCase() !== "websocket") { - return new Response("NISPS OSC Bridge — connect via WebSocket", { status: 200 }); - } - const { socket, response } = Deno.upgradeWebSocket(req); - handleWs(socket); - return response; -}); - -console.log(` -NISPS <-> OSC Bridge (bidirectional) -──────────────────────────────────── - WebSocket: ws://localhost:${WS_PORT} - OSC target: ${OSC_HOST}:${OSC_PORT} - OSC listen: 0.0.0.0:${LISTEN_PORT} - Prefix: ${OSC_PREFIX} - Mode: ${USE_BUNDLES ? "bundles" : "individual messages"} - - Webapp -> VCV: - params: [[name, value], ...] or { type: "params", payload: [...] } - state: { type: "state", payload: } - weights: { type: "weights", payload: } - - VCV -> Webapp: - /nisps/output -> { type: "outputs", values: [...] } - /nisps/input -> { type: "inputs", values: [...] } - - Waiting for connections... -`); diff --git a/playground/osc-bridge/compile.sh b/playground/osc-bridge/compile.sh deleted file mode 100755 index 3c37b97..0000000 --- a/playground/osc-bridge/compile.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Compile NISPS OSC Bridge for all platforms -# Requires: deno 2.x -# Outputs go to dist/ -# -# Note: macOS cross-compilation from Linux has a known Deno bug. -# macOS binaries must be built on macOS (or via GitHub Actions CI). - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -DIST="$SCRIPT_DIR/dist" -SRC="$SCRIPT_DIR/bridge.ts" -NAME="nisps-osc-bridge" - -mkdir -p "$DIST" - -# Detect host OS for cross-compile compatibility -HOST_OS="$(uname -s)" - -TARGETS=( - "x86_64-unknown-linux-gnu:linux-x86_64" - "aarch64-unknown-linux-gnu:linux-arm64" - "x86_64-apple-darwin:macos-x86_64" - "aarch64-apple-darwin:macos-arm64" - "x86_64-pc-windows-msvc:windows-x86_64" -) - -FAILED=() - -for entry in "${TARGETS[@]}"; do - target="${entry%%:*}" - suffix="${entry##*:}" - - outname="$NAME-$suffix" - if [[ "$target" == *windows* ]]; then - outname="$outname.exe" - fi - - echo "Compiling $outname ($target)..." - if deno compile \ - --allow-net \ - --unstable-net \ - --target "$target" \ - --output "$DIST/$outname" \ - "$SRC" 2>&1; then - echo " OK" - else - echo " FAILED (skipping — cross-compile to this target may not work on $HOST_OS)" - FAILED+=("$outname") - fi - echo "" -done - -echo "Binaries in $DIST/:" -ls -lh "$DIST/" 2>/dev/null || echo " (none)" - -if [[ ${#FAILED[@]} -gt 0 ]]; then - echo "" - echo "Failed targets: ${FAILED[*]}" - echo "These may need to be built natively or via CI." -fi diff --git a/playground/osc-bridge/deno.lock b/playground/osc-bridge/deno.lock deleted file mode 100644 index 5275890..0000000 --- a/playground/osc-bridge/deno.lock +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/cli@1": "1.0.28" - }, - "jsr": { - "@std/cli@1.0.28": { - "integrity": "74ef9b976db59ca6b23a5283469c9072be6276853807a83ec6c7ce412135c70a" - } - }, - "workspace": { - "packageJson": { - "dependencies": [ - "npm:ws@^8.18.0" - ] - } - } -} diff --git a/playground/osc-bridge/package-lock.json b/playground/osc-bridge/package-lock.json deleted file mode 100644 index 34e917e..0000000 --- a/playground/osc-bridge/package-lock.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "nisps-osc-bridge", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "nisps-osc-bridge", - "version": "0.1.0", - "dependencies": { - "ws": "^8.18.0" - } - }, - "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/playground/osc-bridge/test-receive.ts b/playground/osc-bridge/test-receive.ts deleted file mode 100644 index 7d47cb7..0000000 --- a/playground/osc-bridge/test-receive.ts +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --unstable-net - -// Quick OSC receiver — prints incoming NISPS parameters to the terminal. -// Usage: deno run --allow-net test-receive.ts [--port 57120] - -import { parseArgs } from "jsr:@std/cli@1/parse-args"; - -const args = parseArgs(Deno.args, { - string: ["port"], - default: { port: "57120" }, -}); - -const port = parseInt(args.port); -const udp = Deno.listenDatagram({ port, transport: "udp", hostname: "0.0.0.0" }); - -console.log(`Listening for OSC on UDP port ${port}...\n`); - -// Minimal OSC parser -function readOscString(buf: Uint8Array, offset: number): [string, number] { - let end = offset; - while (end < buf.length && buf[end] !== 0) end++; - const str = new TextDecoder().decode(buf.slice(offset, end)); - // Advance past null + padding to 4-byte boundary - const padded = end + 1; - return [str, padded + (4 - (padded % 4)) % 4]; -} - -function parseOscMessage(buf: Uint8Array): { address: string; args: number[] } | null { - if (buf.length < 8) return null; - const [address, typeOffset] = readOscString(buf, 0); - if (buf[typeOffset] !== 0x2C) return null; // ',' - const [typeTag, dataOffset] = readOscString(buf, typeOffset); - - const oscArgs: number[] = []; - let pos = dataOffset; - for (let i = 1; i < typeTag.length; i++) { - if (typeTag[i] === "f") { - const view = new DataView(buf.buffer, buf.byteOffset + pos, 4); - oscArgs.push(view.getFloat32(0, false)); - pos += 4; - } else if (typeTag[i] === "i") { - const view = new DataView(buf.buffer, buf.byteOffset + pos, 4); - oscArgs.push(view.getInt32(0, false)); - pos += 4; - } - } - return { address, args: oscArgs }; -} - -function parseOscBundle(buf: Uint8Array): { address: string; args: number[] }[] { - // Check for #bundle header - const header = new TextDecoder().decode(buf.slice(0, 7)); - if (header !== "#bundle") return []; - - const messages: { address: string; args: number[] }[] = []; - let pos = 16; // skip header (8) + timetag (8) - while (pos + 4 < buf.length) { - const size = new DataView(buf.buffer, buf.byteOffset + pos, 4).getUint32(0, false); - pos += 4; - if (pos + size > buf.length) break; - const msg = parseOscMessage(buf.slice(pos, pos + size)); - if (msg) messages.push(msg); - pos += size; - } - return messages; -} - -// Bar chart helper -function bar(value: number, width = 30): string { - const filled = Math.round(value * width); - return "\x1b[36m" + "█".repeat(filled) + "\x1b[90m" + "░".repeat(width - filled) + "\x1b[0m"; -} - -let msgCount = 0; -let lastPrint = 0; -const latest = new Map(); - -for await (const [data] of udp) { - const buf = new Uint8Array(data); - let messages: { address: string; args: number[] }[]; - - // Try bundle first, fall back to single message - messages = parseOscBundle(buf); - if (messages.length === 0) { - const single = parseOscMessage(buf); - if (single) messages = [single]; - } - - for (const msg of messages) { - msgCount++; - const name = msg.address.replace(/^\/nisps\//, ""); - latest.set(name, msg.args[0]); - } - - // Throttle display to ~10fps - const now = Date.now(); - if (now - lastPrint < 100) continue; - lastPrint = now; - - // Clear and redraw - const lines: string[] = []; - lines.push(`\x1b[2J\x1b[H\x1b[1mNISPS OSC Receiver\x1b[0m (${msgCount} messages received)\n`); - - const sorted = [...latest.entries()].sort((a, b) => a[0].localeCompare(b[0])); - for (const [name, value] of sorted) { - const v = value.toFixed(3).padStart(5); - lines.push(` ${name.padEnd(24)} ${v} ${bar(value)}`); - } - - lines.push(`\n\x1b[90mCtrl+C to quit\x1b[0m`); - console.log(lines.join("\n")); -} diff --git a/playground/serve-coop.py b/playground/serve-coop.py deleted file mode 100755 index 0573f10..0000000 --- a/playground/serve-coop.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -"""HTTP server with COOP/COEP headers for SharedArrayBuffer support.""" -import http.server -import sys -import os - -PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 - -class COOPHandler(http.server.SimpleHTTPRequestHandler): - def end_headers(self): - self.send_header('Cross-Origin-Opener-Policy', 'same-origin') - self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') - super().end_headers() - -os.chdir(os.path.dirname(os.path.abspath(__file__))) -with http.server.HTTPServer(('', PORT), COOPHandler) as httpd: - print(f'Serving playground at http://localhost:{PORT} (COOP/COEP enabled)') - httpd.serve_forever() diff --git a/playground/serve.sh b/playground/serve.sh deleted file mode 100755 index e72ffa9..0000000 --- a/playground/serve.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# Serve the NISPS playground with COOP/COEP headers (required for SharedArrayBuffer/synth mode). - -PORT="${1:-8000}" -MAX_ATTEMPTS=20 -DIR="$(cd "$(dirname "$0")" && pwd)" - -for ((i = 0; i < MAX_ATTEMPTS; i++)); do - candidate=$((PORT + i)) - if ! ss -tlnp 2>/dev/null | grep -q ":${candidate} "; then - echo "Serving playground at http://localhost:${candidate} (COOP/COEP enabled)" - exec python3 "$DIR/serve-coop.py" "$candidate" - fi - echo "Port ${candidate} in use, trying next..." -done - -echo "No open port found in range ${PORT}–$((PORT + MAX_ATTEMPTS - 1))" >&2 -exit 1 diff --git a/playground/test-midi-receiver.py b/playground/test-midi-receiver.py deleted file mode 100644 index aedb1ee..0000000 --- a/playground/test-midi-receiver.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""MIDI CC receiver — listens on all ALSA ports and prints CC messages.""" -import mido -import sys - -def main(): - ports = mido.get_input_names() - print(f"Available MIDI inputs: {ports}") - - if not ports: - print("No MIDI input ports found!") - sys.exit(1) - - # Open the Midi Through port (Chrome sends to this) - port_name = ports[0] - for p in ports: - if 'Through' in p: - port_name = p - break - - print(f"Listening on: {port_name}") - print("Waiting for MIDI CC messages...\n") - - with mido.open_input(port_name) as inport: - for msg in inport: - if msg.type == 'control_change': - print(f" CC #{msg.control:3d} val={msg.value:3d} ch={msg.channel + 1:2d}") - else: - print(f" {msg}") - -if __name__ == '__main__': - main() diff --git a/playground/wasm/build.sh b/playground/wasm/build.sh deleted file mode 100755 index 4e1f3e2..0000000 --- a/playground/wasm/build.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# Build nisps-core WASM module for the playground -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -NISPS_CORE="$SCRIPT_DIR/../../nisps-core/include" -OUT_DIR="$SCRIPT_DIR" - -echo "Building nisps WASM module..." - -emcc "$SCRIPT_DIR/nisps_bindings.cpp" \ - -I"$NISPS_CORE" \ - -std=c++20 \ - -O2 \ - -s EXPORTED_FUNCTIONS='[ - "_nisps_mlp_create", - "_nisps_mlp_destroy", - "_nisps_mlp_weight_count", - "_nisps_mlp_get_weights", - "_nisps_mlp_set_weights", - "_nisps_mlp_inference", - "_nisps_mlp_train", - "_nisps_mlp_draw_weights_spread", - "_nisps_mlp_move_weights_spread", - "_nisps_mlp_infer_batch", - "_nisps_mlp_train_ex", - "_nisps_mlp_move_weights_ex", - "_nisps_mlp_eval_loss", - "_nisps_mlp_get_layer_stats", - "_nisps_alloc", - "_nisps_free", - "_nisps_alloc_int", - "_nisps_free_int", - "_malloc", - "_free" - ]' \ - -s EXPORTED_RUNTIME_METHODS='["cwrap","HEAPF32","HEAP32"]' \ - -s MODULARIZE=1 \ - -s EXPORT_NAME=NispsModule \ - -s EXPORT_ES6=1 \ - -s ENVIRONMENT='web,worker' \ - -s ALLOW_MEMORY_GROWTH=1 \ - -s INITIAL_MEMORY=16777216 \ - -o "$OUT_DIR/nisps.js" - -echo "Built: $OUT_DIR/nisps.js + $OUT_DIR/nisps.wasm" -echo "Size: $(du -h "$OUT_DIR/nisps.wasm" | cut -f1)" diff --git a/playground/wasm/nisps.js b/playground/wasm/nisps.js deleted file mode 100644 index 52aad07..0000000 --- a/playground/wasm/nisps.js +++ /dev/null @@ -1,2 +0,0 @@ -async function NispsModule(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("nisps.wasm")}return new URL("nisps.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var _random_get=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["cwrap"]=cwrap;var _nisps_mlp_create,_nisps_mlp_destroy,_nisps_mlp_weight_count,_nisps_mlp_get_weights,_nisps_mlp_set_weights,_nisps_mlp_inference,_nisps_mlp_train,_nisps_mlp_draw_weights_spread,_nisps_mlp_move_weights_spread,_nisps_mlp_infer_batch,_nisps_mlp_train_ex,_nisps_mlp_move_weights_ex,_nisps_mlp_eval_loss,_nisps_mlp_get_layer_stats,_nisps_alloc,_malloc,_nisps_free,_free,_nisps_alloc_int,_nisps_free_int,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_nisps_mlp_create=Module["_nisps_mlp_create"]=wasmExports["nisps_mlp_create"];_nisps_mlp_destroy=Module["_nisps_mlp_destroy"]=wasmExports["nisps_mlp_destroy"];_nisps_mlp_weight_count=Module["_nisps_mlp_weight_count"]=wasmExports["nisps_mlp_weight_count"];_nisps_mlp_get_weights=Module["_nisps_mlp_get_weights"]=wasmExports["nisps_mlp_get_weights"];_nisps_mlp_set_weights=Module["_nisps_mlp_set_weights"]=wasmExports["nisps_mlp_set_weights"];_nisps_mlp_inference=Module["_nisps_mlp_inference"]=wasmExports["nisps_mlp_inference"];_nisps_mlp_train=Module["_nisps_mlp_train"]=wasmExports["nisps_mlp_train"];_nisps_mlp_draw_weights_spread=Module["_nisps_mlp_draw_weights_spread"]=wasmExports["nisps_mlp_draw_weights_spread"];_nisps_mlp_move_weights_spread=Module["_nisps_mlp_move_weights_spread"]=wasmExports["nisps_mlp_move_weights_spread"];_nisps_mlp_infer_batch=Module["_nisps_mlp_infer_batch"]=wasmExports["nisps_mlp_infer_batch"];_nisps_mlp_train_ex=Module["_nisps_mlp_train_ex"]=wasmExports["nisps_mlp_train_ex"];_nisps_mlp_move_weights_ex=Module["_nisps_mlp_move_weights_ex"]=wasmExports["nisps_mlp_move_weights_ex"];_nisps_mlp_eval_loss=Module["_nisps_mlp_eval_loss"]=wasmExports["nisps_mlp_eval_loss"];_nisps_mlp_get_layer_stats=Module["_nisps_mlp_get_layer_stats"]=wasmExports["nisps_mlp_get_layer_stats"];_nisps_alloc=Module["_nisps_alloc"]=wasmExports["nisps_alloc"];_malloc=Module["_malloc"]=wasmExports["malloc"];_nisps_free=Module["_nisps_free"]=wasmExports["nisps_free"];_free=Module["_free"]=wasmExports["free"];_nisps_alloc_int=Module["_nisps_alloc_int"]=wasmExports["nisps_alloc_int"];_nisps_free_int=Module["_nisps_free_int"]=wasmExports["nisps_free_int"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap,random_get:_random_get};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} -;return moduleRtn}export default NispsModule; diff --git a/playground/wasm/nisps.wasm b/playground/wasm/nisps.wasm deleted file mode 100755 index 324156207dd0332c4c1604a382227cb4f489b069..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36699 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≈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 PV$~)nz#E-#2rB%)lTFoq diff --git a/playground/wasm/nisps_bindings.cpp b/playground/wasm/nisps_bindings.cpp deleted file mode 100644 index dee76ce..0000000 --- a/playground/wasm/nisps_bindings.cpp +++ /dev/null @@ -1,370 +0,0 @@ -// WASM bindings for nisps-core MLP engine -// Provides a flat C API for use from JavaScript via Emscripten - -#include -#include "nisps/mlp.hpp" -#include -#include - -// Helper subclass to access protected members for extended training -struct NispsMLPAccessor : public nisps::MLP { - using nisps::MLP::loss_fn_; - using nisps::MLP::UpdateWeights; -}; - -extern "C" { - -// ---- Lifecycle ---- - -EMSCRIPTEN_KEEPALIVE -void* nisps_mlp_create(int* layer_sizes, int n_layers, int* activation_ids, int n_activations) { - std::vector layers(layer_sizes, layer_sizes + n_layers); - std::vector activations; - for (int i = 0; i < n_activations; i++) { - activations.push_back(static_cast(activation_ids[i])); - } - auto* mlp = new nisps::MLP(layers, activations, nisps::loss::LOSS_MSE, false, 0.0f); - return mlp; -} - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_destroy(void* ptr) { - delete static_cast*>(ptr); -} - -// ---- Weight serialization ---- -// Flat format: for each layer, for each node: [w0, w1, ..., wN, bias] - -EMSCRIPTEN_KEEPALIVE -int nisps_mlp_weight_count(void* ptr) { - auto* mlp = static_cast*>(ptr); - int count = 0; - for (auto& layer : mlp->m_layers) { - for (auto& node : layer.m_nodes) { - count += node.m_weights.size() + 1; // weights + bias - } - } - return count; -} - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_get_weights(void* ptr, float* out) { - auto* mlp = static_cast*>(ptr); - int idx = 0; - for (auto& layer : mlp->m_layers) { - for (auto& node : layer.m_nodes) { - for (size_t j = 0; j < node.m_weights.size(); j++) { - out[idx++] = node.m_weights[j]; - } - out[idx++] = node.m_bias; - } - } -} - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_set_weights(void* ptr, float* weights) { - auto* mlp = static_cast*>(ptr); - int idx = 0; - for (auto& layer : mlp->m_layers) { - for (auto& node : layer.m_nodes) { - for (size_t j = 0; j < node.m_weights.size(); j++) { - node.m_weights[j] = weights[idx++]; - } - node.m_bias = weights[idx++]; - } - } -} - -// ---- Inference ---- - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_inference(void* ptr, float* input, int input_dim, float* output, int output_dim) { - auto* mlp = static_cast*>(ptr); - std::vector in_vec(input, input + input_dim); - std::vector out_vec; - mlp->GetOutput(in_vec, &out_vec, nullptr, true); - int n = output_dim < (int)out_vec.size() ? output_dim : (int)out_vec.size(); - for (int i = 0; i < n; i++) { - output[i] = out_vec[i]; - } -} - -// ---- Training ---- -// Takes flat arrays, builds training pairs, returns final loss - -EMSCRIPTEN_KEEPALIVE -float nisps_mlp_train(void* ptr, - float* features_flat, int n_samples, int feature_dim, - float* labels_flat, int label_dim, - float* sample_weights, - float learning_rate, int max_iterations, float min_error) { - - auto* mlp = static_cast*>(ptr); - - std::vector> features(n_samples); - std::vector> labels(n_samples); - - for (int i = 0; i < n_samples; i++) { - features[i].assign(features_flat + i * feature_dim, - features_flat + (i + 1) * feature_dim); - labels[i].assign(labels_flat + i * label_dim, - labels_flat + (i + 1) * label_dim); - } - - // Build weights vector if provided (non-null pointer) - std::vector weights_vec; - const std::vector* weights_ptr = nullptr; - if (sample_weights) { - weights_vec.assign(sample_weights, sample_weights + n_samples); - weights_ptr = &weights_vec; - } - - nisps::MLP::training_pair_t data(features, labels); - return mlp->Train(data, learning_rate, max_iterations, min_error, false, weights_ptr); -} - -// ---- Weight manipulation with spread ---- -// These mirror the JS playground's spread-aware versions which the C++ core -// doesn't have natively. - -// DrawWeights: randomize with per-layer Xavier scaling controlled by spread -// spread=0: uniform [-1,1], spread=1: Xavier-scaled per layer -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_draw_weights_spread(void* ptr, float spread) { - auto* mlp = static_cast*>(ptr); - for (size_t l = 0; l < mlp->m_layers.size(); l++) { - int fan_in = mlp->m_layers[l].GetInputSize(); - float xavier_scale = 1.0f / std::sqrt((float)fan_in); - float scale = 1.0f * (1.0f - spread) + xavier_scale * spread; - for (auto& node : mlp->m_layers[l].GetNodesChangeable()) { - for (size_t j = 0; j < node.m_weights.size(); j++) { - node.m_weights[j] = ((float)rand() / RAND_MAX * 2.0f - 1.0f) * scale; - } - node.m_bias = 0.0f; - } - } -} - -// MoveWeights: add per-layer noise with weight decay, controlled by spread -// spread=0: flat noise, no decay. spread=1: Xavier-scaled noise, 10% decay. -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_move_weights_spread(void* ptr, float speed, float spread) { - auto* mlp = static_cast*>(ptr); - float decay = 1.0f - 0.1f * spread; - for (size_t l = 0; l < mlp->m_layers.size(); l++) { - int fan_in = mlp->m_layers[l].GetInputSize(); - float xavier_scale = 1.0f / std::sqrt((float)fan_in); - float layer_scale = 1.0f * (1.0f - spread) + xavier_scale * spread; - for (auto& node : mlp->m_layers[l].GetNodesChangeable()) { - for (size_t j = 0; j < node.m_weights.size(); j++) { - node.m_weights[j] *= decay; - // gen_randn: sum of 3 uniform randoms - float accum = 0; - for (int n = 0; n < 3; n++) { - accum += (float)rand() / RAND_MAX * 2.0f - 1.0f; - } - node.m_weights[j] += 3.0f * accum * speed * layer_scale; - } - } - } -} - -// ---- Batch inference ---- - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_infer_batch(void* ptr, float* inputs_flat, int n_points, int input_dim, float* outputs_flat, int output_dim) { - auto* mlp = static_cast*>(ptr); - std::vector in_vec(input_dim); - std::vector out_vec; - for (int i = 0; i < n_points; i++) { - in_vec.assign(inputs_flat + i * input_dim, inputs_flat + (i + 1) * input_dim); - out_vec.clear(); - mlp->GetOutput(in_vec, &out_vec, nullptr, true); - int n = output_dim < (int)out_vec.size() ? output_dim : (int)out_vec.size(); - for (int j = 0; j < n; j++) { - outputs_flat[i * output_dim + j] = out_vec[j]; - } - } -} - -// ---- Extended training with per-iteration loss history ---- - -EMSCRIPTEN_KEEPALIVE -int nisps_mlp_train_ex(void* ptr, - float* features_flat, int n_samples, int feature_dim, - float* labels_flat, int label_dim, - float* sample_weights, - float learning_rate, int max_iterations, float min_error, - float* loss_history_out) { - - auto* mlp = static_cast(static_cast*>(ptr)); - - std::vector> features(n_samples); - std::vector> labels(n_samples); - - for (int i = 0; i < n_samples; i++) { - features[i].assign(features_flat + i * feature_dim, - features_flat + (i + 1) * feature_dim); - labels[i].assign(labels_flat + i * label_dim, - labels_flat + (i + 1) * label_dim); - } - - float sample_size_recip = 1.0f / n_samples; - - int iter = 0; - for (iter = 0; iter < max_iterations; iter++) { - float iteration_loss = 0.0f; - - for (int s = 0; s < n_samples; s++) { - float w = sample_weights ? sample_weights[s] : sample_size_recip; - - std::vector predicted_output; - std::vector> all_layers_activations; - - mlp->GetOutput(features[s], &predicted_output, &all_layers_activations, false); - - std::vector deriv_error_output(predicted_output.size()); - float loss = mlp->loss_fn_(labels[s], predicted_output, deriv_error_output, w); - - iteration_loss += loss; - - mlp->UpdateWeights(all_layers_activations, deriv_error_output, learning_rate); - } - - if (!sample_weights) { - iteration_loss *= sample_size_recip; - } - - loss_history_out[iter] = iteration_loss; - - if (iteration_loss < min_error) { - iter++; - break; - } - } - - return iter; -} - -// ---- moveWeights with output pin mask ---- - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_move_weights_ex(void* ptr, float speed, float spread, int* pin_mask, int n_outputs) { - auto* mlp = static_cast*>(ptr); - float decay = 1.0f - 0.1f * spread; - size_t n_layers = mlp->m_layers.size(); - for (size_t l = 0; l < n_layers; l++) { - int fan_in = mlp->m_layers[l].GetInputSize(); - float xavier_scale = 1.0f / std::sqrt((float)fan_in); - float layer_scale = 1.0f * (1.0f - spread) + xavier_scale * spread; - bool is_output_layer = (l == n_layers - 1); - int node_idx = 0; - for (auto& node : mlp->m_layers[l].GetNodesChangeable()) { - // Skip pinned output nodes - if (is_output_layer && pin_mask && node_idx < n_outputs && pin_mask[node_idx] == 1) { - node_idx++; - continue; - } - for (size_t j = 0; j < node.m_weights.size(); j++) { - node.m_weights[j] *= decay; - float accum = 0; - for (int n = 0; n < 3; n++) { - accum += (float)rand() / RAND_MAX * 2.0f - 1.0f; - } - node.m_weights[j] += 3.0f * accum * speed * layer_scale; - } - node_idx++; - } - } -} - -// ---- Evaluate loss without updating weights ---- - -EMSCRIPTEN_KEEPALIVE -float nisps_mlp_eval_loss(void* ptr, - float* features_flat, int n_samples, int feature_dim, - float* labels_flat, int label_dim, - float* sample_weights) { - - auto* mlp = static_cast(static_cast*>(ptr)); - float total_loss = 0.0f; - float sample_size_recip = 1.0f / n_samples; - - for (int s = 0; s < n_samples; s++) { - float w = sample_weights ? sample_weights[s] : sample_size_recip; - - std::vector in_vec(features_flat + s * feature_dim, - features_flat + (s + 1) * feature_dim); - std::vector label_vec(labels_flat + s * label_dim, - labels_flat + (s + 1) * label_dim); - - std::vector predicted_output; - mlp->GetOutput(in_vec, &predicted_output, nullptr, true); - - std::vector deriv_error_output(predicted_output.size()); - float loss = mlp->loss_fn_(label_vec, predicted_output, deriv_error_output, w); - total_loss += loss; - } - - if (!sample_weights) { - total_loss *= sample_size_recip; - } - - return total_loss; -} - -// ---- Per-layer weight statistics ---- - -EMSCRIPTEN_KEEPALIVE -void nisps_mlp_get_layer_stats(void* ptr, float* stats_out, int n_layers) { - auto* mlp = static_cast*>(ptr); - int layers_to_process = n_layers < (int)mlp->m_layers.size() ? n_layers : (int)mlp->m_layers.size(); - for (int l = 0; l < layers_to_process; l++) { - float sum_abs = 0.0f; - float max_abs = 0.0f; - int dead_count = 0; - int saturating_count = 0; - int total_weights = 0; - - for (auto& node : mlp->m_layers[l].m_nodes) { - for (size_t j = 0; j < node.m_weights.size(); j++) { - float aw = std::fabs(node.m_weights[j]); - sum_abs += aw; - if (aw > max_abs) max_abs = aw; - if (aw < 0.01f) dead_count++; - if (aw > 3.0f) saturating_count++; - total_weights++; - } - } - - float inv_total = total_weights > 0 ? 1.0f / total_weights : 0.0f; - stats_out[l * 4 + 0] = sum_abs * inv_total; // mean absolute weight - stats_out[l * 4 + 1] = max_abs; // max absolute weight - stats_out[l * 4 + 2] = dead_count * inv_total; // fraction dead - stats_out[l * 4 + 3] = saturating_count * inv_total; // fraction saturating - } -} - -// ---- Memory helpers ---- - -EMSCRIPTEN_KEEPALIVE -float* nisps_alloc(int n) { - return (float*)malloc(n * sizeof(float)); -} - -EMSCRIPTEN_KEEPALIVE -void nisps_free(float* ptr) { - free(ptr); -} - -EMSCRIPTEN_KEEPALIVE -int* nisps_alloc_int(int n) { - return (int*)malloc(n * sizeof(int)); -} - -EMSCRIPTEN_KEEPALIVE -void nisps_free_int(int* ptr) { - free(ptr); -} - -} // extern "C"