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).
121 lines
3 KiB
JavaScript
121 lines
3 KiB
JavaScript
// Arpeggiator — plays chord progressions through the C15 engine
|
|
// Supports tempo, octave range, octave offset, and multiple chord progressions
|
|
|
|
// Chord progressions as arrays of arrays of intervals (semitones from root)
|
|
const PROGRESSIONS = {
|
|
'I-vi-IV-V': [
|
|
[0, 4, 7], // C major
|
|
[9, 12, 16], // A minor
|
|
[5, 9, 12], // F major
|
|
[7, 11, 14], // G major
|
|
],
|
|
'I-IV-vi-V': [
|
|
[0, 4, 7],
|
|
[5, 9, 12],
|
|
[9, 12, 16],
|
|
[7, 11, 14],
|
|
],
|
|
'i-VI-III-VII': [
|
|
[0, 3, 7], // C minor
|
|
[8, 12, 15], // Ab major
|
|
[3, 7, 10], // Eb major
|
|
[10, 14, 17], // Bb major
|
|
],
|
|
'I-V-vi-IV': [
|
|
[0, 4, 7],
|
|
[7, 11, 14],
|
|
[9, 12, 16],
|
|
[5, 9, 12],
|
|
],
|
|
};
|
|
|
|
export class Arpeggiator {
|
|
constructor(bridge) {
|
|
this.bridge = bridge;
|
|
this.bpm = 120;
|
|
this.octaves = 2; // how many octaves to span
|
|
this.octaveOffset = 0; // base octave shift (-2 to +2)
|
|
this.progression = 'I-vi-IV-V';
|
|
this.playing = false;
|
|
this._timer = null;
|
|
this._chordIndex = 0;
|
|
this._noteIndex = 0;
|
|
this._currentNotes = [];
|
|
this._lastNote = -1;
|
|
}
|
|
|
|
get progressionNames() {
|
|
return Object.keys(PROGRESSIONS);
|
|
}
|
|
|
|
start() {
|
|
if (this.playing) return;
|
|
this.playing = true;
|
|
this._chordIndex = 0;
|
|
this._noteIndex = 0;
|
|
this._scheduleNext();
|
|
}
|
|
|
|
stop() {
|
|
this.playing = false;
|
|
if (this._timer) {
|
|
clearTimeout(this._timer);
|
|
this._timer = null;
|
|
}
|
|
// Release current note
|
|
if (this._lastNote >= 0) {
|
|
this.bridge.noteOff(this._lastNote);
|
|
this._lastNote = -1;
|
|
}
|
|
}
|
|
|
|
_scheduleNext() {
|
|
if (!this.playing) return;
|
|
|
|
const msPerBeat = 60000 / this.bpm;
|
|
// Each note gets a 16th-note duration, chord changes every bar (4 beats)
|
|
const noteDuration = msPerBeat / 4;
|
|
|
|
this._playNextNote();
|
|
|
|
this._timer = setTimeout(() => this._scheduleNext(), noteDuration);
|
|
}
|
|
|
|
_playNextNote() {
|
|
const chords = PROGRESSIONS[this.progression] || PROGRESSIONS['I-vi-IV-V'];
|
|
|
|
// Release previous note
|
|
if (this._lastNote >= 0) {
|
|
this.bridge.noteOff(this._lastNote);
|
|
}
|
|
|
|
// Build arpeggiated note sequence from current chord across octaves
|
|
const chord = chords[this._chordIndex];
|
|
const baseNote = 48 + (this.octaveOffset * 12); // C3 as base + offset
|
|
|
|
// Build notes across octave range
|
|
const notes = [];
|
|
for (let oct = 0; oct < this.octaves; oct++) {
|
|
for (const interval of chord) {
|
|
const note = baseNote + interval + (oct * 12);
|
|
if (note >= 0 && note <= 127) {
|
|
notes.push(note);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (notes.length === 0) return;
|
|
|
|
// Play the next note in sequence
|
|
const note = notes[this._noteIndex % notes.length];
|
|
this.bridge.noteOn(note, 0.6 + Math.random() * 0.2);
|
|
this._lastNote = note;
|
|
|
|
// Advance
|
|
this._noteIndex++;
|
|
if (this._noteIndex >= notes.length) {
|
|
this._noteIndex = 0;
|
|
this._chordIndex = (this._chordIndex + 1) % chords.length;
|
|
}
|
|
}
|
|
}
|