feat(audio-canvas): per-file loop count, beat-synced lengths, param range groups, pitch lock

- Per-file loop count (0-4) with stepper UI on each waveform cell;
  dynamic MLP output sizing (8 range + N_loops × 4 position outputs)
- Beat-synced loop length: global bars/beats selector (2/4/8/16/32/64)
  with per-loop overrides via long-press dropdown, persistent badges
- Global param range groups: dual-thumb range sliders (volume, pitch,
  position, filter) with ML-driven min/max and per-loop position mapping
- Pitch lock toggle (1×) to freeze playback rate across all submodes
- Fix: _resizeOutputs now syncs _loopOutputs to prevent stale reads
- Fix: granular visuals consolidated per-clip to prevent multi-loop flicker
This commit is contained in:
w1n5t0n 2026-04-03 20:36:24 +01:00
parent 960676acf3
commit 41106f22c0
2 changed files with 925 additions and 124 deletions

View file

@ -3,7 +3,7 @@
import { WasmIML } from './nisps/nisps-wasm.js'; import { WasmIML } from './nisps/nisps-wasm.js';
import { FlowFieldVisualizer } from './ui/visualizer.js'; import { FlowFieldVisualizer } from './ui/visualizer.js';
import { AudioCanvas, N_AUDIO_OUTPUTS, AUDIO_PARAM_NAMES, AUDIO_PARAM_COLORS } from './audio/audio-canvas.js'; import { AudioCanvas } from './audio/audio-canvas.js';
import { C15Adapter } from './synth/c15-adapter.js'; import { C15Adapter } from './synth/c15-adapter.js';
import { Arpeggiator } from './synth/arpeggiator.js'; import { Arpeggiator } from './synth/arpeggiator.js';
import { MIDIInput } from './synth/midi-input.js'; import { MIDIInput } from './synth/midi-input.js';
@ -30,7 +30,7 @@ const N_HAND_INPUTS = 14;
const N_INPUTS = N_JOY_INPUTS; // default (joystick) const N_INPUTS = N_JOY_INPUTS; // default (joystick)
const N_VISUAL_OUTPUTS = 20; const N_VISUAL_OUTPUTS = 20;
const N_SYNTH_OUTPUTS = SYNTH_PARAM_MAP.length; // 126 const N_SYNTH_OUTPUTS = SYNTH_PARAM_MAP.length; // 126
// N_AUDIO_OUTPUTS = 36 — imported from audio-canvas.js // Audio canvas output count is dynamic — queried from audioCanvas.getOutputCount()
let N_OUTPUTS = N_SYNTH_OUTPUTS; // Dynamic — changes with output mode let N_OUTPUTS = N_SYNTH_OUTPUTS; // Dynamic — changes with output mode
let audioCanvas = null; // lazily created on first switch to audio-canvas mode let audioCanvas = null; // lazily created on first switch to audio-canvas mode
@ -193,9 +193,14 @@ const visualOverrides = VISUAL_PARAM_NAMES.map(() => ({
})); }));
// ---- Audio Canvas Overrides ---- // ---- Audio Canvas Overrides ----
const audioCanvasOverrides = Array.from({ length: N_AUDIO_OUTPUTS }, () => ({ // Dynamic array — resized when AudioCanvas output count changes
min: 0, max: 1, curve: 0.5, frozen: false, fixedValue: 0.5, 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) ---- // ---- Synth Sections (for SynthVisualizer) ----
const SYNTH_SECTIONS = [ const SYNTH_SECTIONS = [
@ -958,7 +963,7 @@ function outputCountForMode(mode) {
if (mode === 'visual') return N_VISUAL_OUTPUTS; if (mode === 'visual') return N_VISUAL_OUTPUTS;
if (mode === 'synth') return N_SYNTH_OUTPUTS; if (mode === 'synth') return N_SYNTH_OUTPUTS;
if (mode === 'midi-cc') return midiCCMap.length; if (mode === 'midi-cc') return midiCCMap.length;
if (mode === 'audio-canvas') return N_AUDIO_OUTPUTS; if (mode === 'audio-canvas') return audioCanvas ? audioCanvas.getOutputCount() : 12;
return N_SYNTH_OUTPUTS; return N_SYNTH_OUTPUTS;
} }
@ -1576,9 +1581,20 @@ function buildHeatmap() {
names = meta.map(p => p.name); names = meta.map(p => p.name);
colors = useCuratedColors ? SYNTH_PARAM_COLORS : meta.map(p => _colorFromGroup(p.group)); colors = useCuratedColors ? SYNTH_PARAM_COLORS : meta.map(p => _colorFromGroup(p.group));
} else { } else {
count = isMidiCC ? midiCCMap.length : isAudioCanvas ? N_AUDIO_OUTPUTS : N_VISUAL_OUTPUTS; if (isAudioCanvas && audioCanvas) {
names = isMidiCC ? midiCCMap.map(p => p.name) : isAudioCanvas ? AUDIO_PARAM_NAMES : VISUAL_PARAM_NAMES; count = audioCanvas.getOutputCount();
colors = isMidiCC ? MIDI_CC_PARAM_COLORS : isAudioCanvas ? AUDIO_PARAM_COLORS : VISUAL_PARAM_COLORS; 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++) { for (let i = 0; i < count; i++) {
@ -1670,8 +1686,9 @@ function setHeatmapValue(paramIndex, e, cell) {
paramName = activeEngine.paramMeta[paramIndex].name; paramName = activeEngine.paramMeta[paramIndex].name;
} else if (isMidiCC) { } else if (isMidiCC) {
paramName = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`; paramName = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`;
} else if (isAudioCanvas) { } else if (isAudioCanvas && audioCanvas) {
paramName = AUDIO_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; const acNames = audioCanvas.getAudioParamNames();
paramName = acNames[paramIndex] ?? `p${paramIndex}`;
} else { } else {
paramName = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; paramName = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
} }
@ -1766,9 +1783,11 @@ function showParamPopup(paramIndex, cell) {
} else if (isMidiCC) { } else if (isMidiCC) {
name = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`; name = midiCCMap[paramIndex]?.name ?? `p${paramIndex}`;
color = MIDI_CC_PARAM_COLORS[paramIndex] ?? '#888'; color = MIDI_CC_PARAM_COLORS[paramIndex] ?? '#888';
} else if (isAudioCanvas) { } else if (isAudioCanvas && audioCanvas) {
name = AUDIO_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; const acNames = audioCanvas.getAudioParamNames();
color = AUDIO_PARAM_COLORS[paramIndex] ?? '#888'; const acColors = audioCanvas.getAudioParamColors();
name = acNames[paramIndex] ?? `p${paramIndex}`;
color = acColors[paramIndex] ?? '#888';
} else { } else {
name = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`; name = VISUAL_PARAM_NAMES[paramIndex] ?? `p${paramIndex}`;
color = VISUAL_PARAM_COLORS[paramIndex] ?? '#888'; color = VISUAL_PARAM_COLORS[paramIndex] ?? '#888';
@ -2612,6 +2631,18 @@ async function setOutputMode(mode, { skipConfirm = false } = {}) {
// Lazily create AudioCanvas on first switch // Lazily create AudioCanvas on first switch
if (mode === 'audio-canvas' && !audioCanvas) { if (mode === 'audio-canvas' && !audioCanvas) {
audioCanvas = new AudioCanvas(audioCanvasWrap); 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());
});
} }
// Hide/show audio-canvas wrap // Hide/show audio-canvas wrap

File diff suppressed because it is too large Load diff