@@ -60,7 +60,7 @@
-
+
-
-
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
+
@@ -286,7 +299,7 @@
Mute (M) — removes the parameter from NISPS control; its bar disappears and you get a manual value slider instead
MIDI input — connect a MIDI controller and it will be detected automatically; MIDI notes trigger the synth directly
- ShapeSeq — in synth mode, the sequencer UI appears: a circular step visualizer shows the current pattern; the chain builder below lets you add/remove/reorder algorithmic primitives; press Play to start the generative sequencer
+ Arpeggiator — expand the bottom bar to access the built-in arpeggiator with tempo, octave range, offset, and chord progression controls; press Pause to stop it
diff --git a/playground/css/a-immersive.css b/playground/css/a-immersive.css
index c6f2475..61a27b4 100644
--- a/playground/css/a-immersive.css
+++ b/playground/css/a-immersive.css
@@ -98,7 +98,7 @@ html, body {
display: none;
}
-/* ---- ShapeSeq container (synth mode) ---- */
+/* ---- ShapeSeq container (synth mode, feature-flagged) ---- */
.shapeseq-container {
position: fixed;
inset: 0;
@@ -108,17 +108,8 @@ html, body {
background: #0d0d0d;
overflow: hidden;
}
-
-.shapeseq-container.hidden {
- display: none;
-}
-
-#shapeseq-viz {
- width: 100%;
- flex: 0 0 45%;
- min-height: 0;
-}
-
+.shapeseq-container.hidden { display: none; }
+#shapeseq-viz { width: 100%; flex: 0 0 45%; min-height: 0; }
.shapeseq-chain-wrap {
flex: 1 1 auto;
overflow-y: auto;
@@ -1035,7 +1026,6 @@ html, body {
transition: background 0.15s, border-color 0.15s;
padding: 0;
flex-shrink: 0;
- align-self: flex-start;
}
.play-btn:active {
@@ -1101,8 +1091,7 @@ html, body {
color: var(--text-dim);
}
-.play-btn:hover ~ .play-drawer,
-.play-drawer:hover,
+.synth-quick-controls:hover .play-drawer,
.play-drawer.show {
display: flex;
}
@@ -1131,7 +1120,6 @@ html, body {
min-width: 0;
max-width: 160px;
text-overflow: ellipsis;
- align-self: flex-start;
}
.preset-select:hover,
diff --git a/playground/js/a-app.js b/playground/js/a-app.js
index 6806546..ba399d4 100644
--- a/playground/js/a-app.js
+++ b/playground/js/a-app.js
@@ -4,11 +4,31 @@
import { WasmIML } from './nisps/nisps-wasm.js';
import { FlowFieldVisualizer } from './ui/visualizer.js';
import { C15Bridge } from './synth/c15-bridge.js';
-import { ShapeSeqEngine } from './shapeseq/sequencer.js';
-import { StepVisualizer } from './shapeseq/step-viz.js';
-import { ChainBuilderUI } from './shapeseq/chain-ui.js';
-import { getDefaultBus } from './shapeseq/event-bus.js';
+import { Arpeggiator } from './synth/arpeggiator.js';
import { MIDIInput } from './synth/midi-input.js';
+
+// ---- ShapeSeq (feature-flagged, enable with ?shapeseq=1) ----
+const ENABLE_SHAPESEQ = new URLSearchParams(window.location.search).get('shapeseq') === '1';
+let _shapeSeqImports = null;
+async function getShapeSeqModules() {
+ if (_shapeSeqImports) return _shapeSeqImports;
+ const [seqMod, vizMod, uiMod, busMod] = await Promise.all([
+ import('./shapeseq/sequencer.js'),
+ import('./shapeseq/step-viz.js'),
+ import('./shapeseq/chain-ui.js'),
+ import('./shapeseq/event-bus.js'),
+ ]);
+ _shapeSeqImports = {
+ ShapeSeqEngine: seqMod.ShapeSeqEngine,
+ StepVisualizer: vizMod.StepVisualizer,
+ ChainBuilderUI: uiMod.ChainBuilderUI,
+ getDefaultBus: busMod.getDefaultBus,
+ };
+ return _shapeSeqImports;
+}
+let shapeSeq = null;
+let stepViz = null;
+let chainUI = null;
import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyCurve, applyGroupOverride } from './synth/param-map.js';
import { GamepadInput } from './ui/gamepad.js';
import { HandTracker } from './ui/hand-tracker.js';
@@ -76,9 +96,7 @@ let handTracker = null;
let visualizer;
let synthVisualizer;
let c15 = null;
-let shapeSeq = null; // ShapeSeqEngine instance
-let stepViz = null; // StepVisualizer
-let chainUI = null; // ChainBuilderUI
+let arpeggiator = null;
let midiInput = null;
let outputMode = 'visual';
@@ -708,9 +726,7 @@ async function init() {
if (el) el.textContent = msg;
};
c15.loadParams();
-
- // ShapeSeq is initialized lazily when audio starts (needs AudioContext)
-
+ arpeggiator = new Arpeggiator(c15);
midiInput = new MIDIInput(c15);
initMIDIControls();
@@ -759,7 +775,6 @@ async function init() {
wireQuickPlayControls();
wireGroupDrawer();
wireHelp();
- wireShapeSeqControls();
// Resize
window.addEventListener('resize', onResize);
@@ -1309,6 +1324,7 @@ function setOutputMode(mode) {
const heatmapStrip = document.getElementById('heatmap-strip');
const synthQuickControls = document.getElementById('synth-quick-controls');
+
const shapeseqContainer = document.getElementById('shapeseq-container');
if (mode === 'synth') {
@@ -1318,19 +1334,20 @@ function setOutputMode(mode) {
heatmapStrip.classList.add('hidden');
synthQuickControls.classList.remove('hidden');
synthVisualizer.enableInteraction(true);
- // Show ShapeSeq UI
- if (shapeseqContainer) shapeseqContainer.classList.remove('hidden');
- // Resize step viz canvas to fit its container
- if (stepViz) {
- const vizCanvas = document.getElementById('shapeseq-viz');
- if (vizCanvas) {
- const rect = vizCanvas.getBoundingClientRect();
- stepViz.resize(rect.width, rect.height);
- }
- }
// Pulse play button if audio not yet started
const qp = document.getElementById('quick-play');
if (qp) qp.classList.toggle('audio-needs-init', !(c15 && c15.running));
+ // Show ShapeSeq UI if feature-flagged
+ if (ENABLE_SHAPESEQ && shapeseqContainer) {
+ shapeseqContainer.classList.remove('hidden');
+ if (stepViz) {
+ const vizCanvas = document.getElementById('shapeseq-viz');
+ if (vizCanvas) {
+ const rect = vizCanvas.getBoundingClientRect();
+ stepViz.resize(rect.width, rect.height);
+ }
+ }
+ }
} else {
$synthPanel.classList.add('hidden');
$canvas.classList.remove('hidden-canvas');
@@ -1588,41 +1605,47 @@ function syncRawParamsFromOutputs(outputs) {
});
}
-// ---- ShapeSeq lazy init (needs AudioContext from C15) ----
+// ---- ShapeSeq lazy init (feature-flagged) ----
async function ensureShapeSeqInit() {
- if (shapeSeq) return; // already initialized
- if (!c15 || !c15.audioContext) return; // no audio context yet
-
- const eventBus = getDefaultBus(c15.audioContext);
- shapeSeq = new ShapeSeqEngine({ audioContext: c15.audioContext, eventBus, c15Bridge: c15 });
- await shapeSeq.init();
-
- // Wire up StepVisualizer and ChainBuilderUI now that engine exists
- const seqVizCanvas = document.getElementById('shapeseq-viz');
- const seqChainContainer = document.getElementById('shapeseq-chain');
- if (seqVizCanvas && !stepViz) {
- stepViz = new StepVisualizer({ canvas: seqVizCanvas, eventBus });
- const rect = seqVizCanvas.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) stepViz.resize(rect.width, rect.height);
+ if (!ENABLE_SHAPESEQ) return;
+ if (shapeSeq) return;
+ if (!c15 || !c15.audioContext) return;
+ try {
+ const mods = await getShapeSeqModules();
+ const eventBus = mods.getDefaultBus(c15.audioContext);
+ shapeSeq = new mods.ShapeSeqEngine({ audioContext: c15.audioContext, eventBus, c15Bridge: c15 });
+ await shapeSeq.init();
+ const seqVizCanvas = document.getElementById('shapeseq-viz');
+ const seqChainContainer = document.getElementById('shapeseq-chain');
+ if (seqVizCanvas && !stepViz) {
+ stepViz = new mods.StepVisualizer({ canvas: seqVizCanvas, eventBus });
+ const rect = seqVizCanvas.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) stepViz.resize(rect.width, rect.height);
+ }
+ if (seqChainContainer && !chainUI) {
+ chainUI = new mods.ChainBuilderUI({ container: seqChainContainer, chain: shapeSeq.getChain(), eventBus });
+ }
+ console.log('[NISPS] ShapeSeq initialized');
+ } catch (err) {
+ console.error('[NISPS] ShapeSeq init failed:', err);
}
- if (seqChainContainer && !chainUI) {
- chainUI = new ChainBuilderUI({ container: seqChainContainer, chain: shapeSeq.getChain(), eventBus });
- }
-
- console.log('[NISPS] ShapeSeq initialized');
}
// ---- 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');
startBtn.addEventListener('click', async () => {
const quickPlay = document.getElementById('quick-play');
if (c15.running) {
- if (shapeSeq) shapeSeq.stop();
- const seqToggle = document.getElementById('seq-toggle');
- if (seqToggle) seqToggle.textContent = 'Play';
+ arpeggiator.stop();
+ arpToggle.textContent = 'Play';
await c15.stop();
startBtn.textContent = 'Start Audio';
if (quickPlay) quickPlay.classList.add('audio-needs-init');
@@ -1641,48 +1664,42 @@ function wireSynthControls() {
if (quickVol) quickVol.value = e.target.value;
});
-}
+ arpToggle.addEventListener('click', () => {
+ if (!c15.running) return;
+ if (arpeggiator.playing) {
+ arpeggiator.stop();
+ arpToggle.textContent = 'Play';
+ } else {
+ arpeggiator.start();
+ arpToggle.textContent = 'Stop';
+ }
+ });
-// ---- ShapeSeq controls (bottom sheet) ----
-function wireShapeSeqControls() {
- const seqToggle = document.getElementById('seq-toggle');
- const seqTempo = document.getElementById('seq-tempo');
- const seqSteps = document.getElementById('seq-steps');
+ arpProgression.addEventListener('change', (e) => {
+ arpeggiator.progression = e.target.value;
+ });
- if (seqToggle) {
- seqToggle.addEventListener('click', async () => {
- if (!c15.running) return;
- await ensureShapeSeqInit();
- if (!shapeSeq) return;
- if (shapeSeq.isPlaying) {
- if (shapeSeq) shapeSeq.stop();
- seqToggle.textContent = 'Play';
- } else {
- shapeSeq.start();
- seqToggle.textContent = 'Stop';
- }
- });
- }
+ 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;
+ });
- if (seqTempo) {
- seqTempo.addEventListener('input', (e) => {
- const val = parseInt(e.target.value);
- if (shapeSeq) shapeSeq.setTempo(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;
+ });
- if (seqSteps) {
- seqSteps.addEventListener('input', (e) => {
- const val = parseInt(e.target.value);
- if (shapeSeq) shapeSeq.setStepCount(val);
- document.getElementById('steps-val').textContent = val;
- });
- }
+ arpOffset.addEventListener('input', (e) => {
+ const val = parseInt(e.target.value);
+ arpeggiator.octaveOffset = val;
+ document.getElementById('offset-val').textContent = val;
+ });
}
// ---- MIDI Input ----
@@ -1826,23 +1843,23 @@ function wireQuickPlayControls() {
quickPlay.addEventListener('click', async () => {
if (c15.running) {
- if (shapeSeq) shapeSeq.stop();
+ arpeggiator.stop();
await c15.stop();
// Also update the bottom sheet controls
const startBtn = document.getElementById('synth-start');
- const seqToggle = document.getElementById('seq-toggle');
+ const arpToggle = document.getElementById('arp-toggle');
if (startBtn) startBtn.textContent = 'Start Audio';
- if (seqToggle) seqToggle.textContent = 'Play';
+ if (arpToggle) arpToggle.textContent = 'Play';
} else {
await c15.start();
await ensureShapeSeqInit();
- if (shapeSeq) shapeSeq.start();
+ arpeggiator.start();
routeOutputs(iml.getOutputs());
// Also update the bottom sheet controls
const startBtn = document.getElementById('synth-start');
- const seqToggle = document.getElementById('seq-toggle');
+ const arpToggle = document.getElementById('arp-toggle');
if (startBtn) startBtn.textContent = 'Stop Audio';
- if (seqToggle) seqToggle.textContent = 'Stop';
+ if (arpToggle) arpToggle.textContent = 'Stop';
}
updatePlayIcon();
});
@@ -1856,10 +1873,10 @@ function wireQuickPlayControls() {
quickBpm.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
- if (shapeSeq) shapeSeq.setTempo(val);
+ arpeggiator.bpm = val;
quickBpmVal.textContent = val;
// Sync with bottom sheet tempo slider
- const sheetTempo = document.getElementById('seq-tempo');
+ const sheetTempo = document.getElementById('arp-tempo');
const sheetTempoVal = document.getElementById('tempo-val');
if (sheetTempo) sheetTempo.value = val;
if (sheetTempoVal) sheetTempoVal.textContent = val;
@@ -2192,13 +2209,11 @@ function onResize() {
synthVisualizer.resize();
// Resize ShapeSeq step viz if visible
- if (stepViz && outputMode === 'synth') {
+ if (ENABLE_SHAPESEQ && stepViz && outputMode === 'synth') {
const vizCanvas = document.getElementById('shapeseq-viz');
if (vizCanvas) {
const rect = vizCanvas.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) {
- stepViz.resize(rect.width, rect.height);
- }
+ if (rect.width > 0 && rect.height > 0) stepViz.resize(rect.width, rect.height);
}
}
@@ -2235,15 +2250,15 @@ function animate() {
if (gamepad) gamepad.poll();
if (outputMode === 'synth') {
synthVisualizer.draw();
- // Route inputs to ShapeSeq and render step viz
- if (shapeSeq && shapeSeq.isPlaying) {
+ // ShapeSeq: route inputs and render step viz (feature-flagged)
+ if (ENABLE_SHAPESEQ && shapeSeq && shapeSeq.isPlaying) {
if (inputMode === 'hands' && handTracker && handTracker.active && handTracker.features) {
shapeSeq.setSequenceInputs([handTracker.features[0], handTracker.features[1]]);
} else {
shapeSeq.setSequenceInputs([joyX, joyY]);
}
}
- if (stepViz) stepViz.render();
+ if (ENABLE_SHAPESEQ && stepViz) stepViz.render();
} else {
visualizer.draw();
}