refactor(shapeseq): restore arpeggiator, gate ShapeSeq behind ?shapeseq=1
Restore the original arpeggiator as the default sequencer. ShapeSeq code is preserved but gated behind a URL feature flag (?shapeseq=1): - Arpeggiator import, state, and all wiring fully restored - ShapeSeq modules loaded via dynamic import() only when flag is on - ensureShapeSeqInit() creates engine/viz/UI lazily on first audio start - animate loop routes inputs to ShapeSeq only when flag + playing - setOutputMode shows/hides ShapeSeq container only when flag is on - HTML: ShapeSeq container added but hidden by default - CSS: ShapeSeq styles added (inert when container is hidden) All 13 ShapeSeq modules in playground/js/shapeseq/ are untouched. To test ShapeSeq: add ?shapeseq=1 to the URL.
This commit is contained in:
parent
d40711eb77
commit
d0bc34d8c9
3 changed files with 137 additions and 121 deletions
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<!-- Synth quick controls (play button + drawer, next to back button) -->
|
||||
<div class="synth-quick-controls hidden" id="synth-quick-controls">
|
||||
<button class="play-btn" id="quick-play" title="Start audio + sequencer">
|
||||
<button class="play-btn" id="quick-play" title="Start audio + arpeggiator">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" id="quick-play-icon"><path d="M4 2l10 6-10 6z"/></svg>
|
||||
</button>
|
||||
<div class="play-drawer" id="play-drawer">
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<!-- Synth visualization canvas -->
|
||||
<canvas id="synth-vis-canvas"></canvas>
|
||||
|
||||
<!-- ShapeSeq UI (synth mode only) -->
|
||||
<!-- ShapeSeq UI (hidden by default, enabled with ?shapeseq=1) -->
|
||||
<div class="shapeseq-container hidden" id="shapeseq-container">
|
||||
<canvas id="shapeseq-viz"></canvas>
|
||||
<div class="shapeseq-chain-wrap" id="shapeseq-chain"></div>
|
||||
|
|
@ -157,16 +157,29 @@
|
|||
<input type="range" id="synth-volume" min="0" max="1" step="0.01" value="0.5">
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<label>Sequencer</label>
|
||||
<button class="action-btn" id="seq-toggle">Play</button>
|
||||
<label>Arpeggiator</label>
|
||||
<button class="action-btn" id="arp-toggle">Play</button>
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<label>Progression</label>
|
||||
<select id="arp-progression">
|
||||
<option value="I-vi-IV-V">I-vi-IV-V</option>
|
||||
<option value="I-IV-vi-V">I-IV-vi-V</option>
|
||||
<option value="i-VI-III-VII">i-VI-III-VII</option>
|
||||
<option value="I-V-vi-IV">I-V-vi-IV</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<label>Tempo <span id="tempo-val">120</span></label>
|
||||
<input type="range" id="seq-tempo" min="40" max="240" step="1" value="120">
|
||||
<input type="range" id="arp-tempo" min="40" max="240" step="1" value="120">
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<label>Steps <span id="steps-val">8</span></label>
|
||||
<input type="range" id="seq-steps" min="1" max="32" step="1" value="8">
|
||||
<label>Octaves <span id="octaves-val">2</span></label>
|
||||
<input type="range" id="arp-octaves" min="1" max="5" step="1" value="2">
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<label>Oct Offset <span id="offset-val">0</span></label>
|
||||
<input type="range" id="arp-offset" min="-2" max="3" step="1" value="0">
|
||||
</div>
|
||||
<div class="synth-row" id="midi-row" style="display:none">
|
||||
<label>MIDI Input</label>
|
||||
|
|
@ -286,7 +299,7 @@
|
|||
<li><strong>Mute (M)</strong> — removes the parameter from NISPS control; its bar disappears and you get a manual value slider instead</li>
|
||||
</ul>
|
||||
<li><strong>MIDI input</strong> — connect a MIDI controller and it will be detected automatically; MIDI notes trigger the synth directly</li>
|
||||
<li><strong>ShapeSeq</strong> — 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</li>
|
||||
<li><strong>Arpeggiator</strong> — expand the bottom bar to access the built-in arpeggiator with tempo, octave range, offset, and chord progression controls; press Pause to stop it</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue