memlnaut-nisps/playground/js/synth/arpeggiator.js

121 lines
2.8 KiB
JavaScript
Raw Normal View History

// 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;
2026-03-30 16:05:45 +02:00
this._progression = 'Cmin7-rand';
this._direction = 'updown';
}
get progressionNames() {
2026-03-30 16:05:45 +02:00
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 });
}
2026-03-30 16:05:45 +02:00
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,
2026-03-30 16:05:45 +02:00
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;
}
}