Adds a secondary Synth mode (toggled via collapsible left side panel) where the NISPS ML engine's 20 outputs control curated C15 synthesizer parameters (oscillator PM, shapers, filters, reverb, echo, flanger, output mixer) through a WASM AudioWorklet bridge. Includes a chord progression arpeggiator with controls for tempo (BPM), octave range, octave offset, and 4 selectable progressions. The C15 engine runs in an AudioWorklet with lock-free SharedArrayBuffer ring buffer communication. New files: c15/ (WASM assets), js/synth/ (bridge, arpeggiator, param map), serve-coop.py (COOP/COEP headers for SharedArrayBuffer support).
124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
// 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 = ['#00ff88', '#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';
|
|
}
|
|
}
|
|
}
|