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

122 lines
3 KiB
JavaScript
Raw Normal View History

// 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;
}
}
}