// Arpeggiator Worker — runs note scheduling on a dedicated thread // for reliable timing independent of main thread load. // Writes noteOn/noteOff directly to the C15 SharedArrayBuffer ring buffer. const MESSAGE_TYPE = { PARAMETER: 0, NOTE_ON: 1, NOTE_OFF: 2 }; const HEADER_SIZE = 3; const MESSAGE_SIZE = 4; const RING_CAPACITY = 512; const PROGRESSIONS = { 'I-vi-IV-V': [[0,4,7],[9,12,16],[5,9,12],[7,11,14]], 'I-IV-vi-V': [[0,4,7],[5,9,12],[9,12,16],[7,11,14]], 'i-VI-III-VII': [[0,3,7],[8,12,15],[3,7,10],[10,14,17]], 'I-V-vi-IV': [[0,4,7],[7,11,14],[9,12,16],[5,9,12]], }; // Ring buffer writer (CAS-safe, matches c15-bridge.js) let ringBuffer = null; let ringF32 = null; let ringI32 = null; function ringWrite(type, id, value) { if (!ringI32) return false; for (let attempt = 0; attempt < 4; attempt++) { const writeIdx = Atomics.load(ringI32, 0); const readIdx = Atomics.load(ringI32, 1); const next = (writeIdx + 1) % RING_CAPACITY; if (next === readIdx) return false; if (Atomics.compareExchange(ringI32, 0, writeIdx, next) === writeIdx) { const off = HEADER_SIZE + writeIdx * MESSAGE_SIZE; ringF32[off] = type; ringF32[off + 1] = id; ringF32[off + 2] = value; ringF32[off + 3] = 0; Atomics.add(ringI32, 2, 1); return true; } } return false; } // Arpeggiator state let playing = false; let bpm = 120; let octaves = 2; let octaveOffset = 0; let progression = 'I-vi-IV-V'; let chordIndex = 0; let noteIndex = 0; let lastNote = -1; let timer = null; function scheduleNext() { if (!playing) return; const msPerBeat = 60000 / bpm; const noteDuration = msPerBeat / 4; // 16th notes playNextNote(); timer = setTimeout(scheduleNext, noteDuration); } function playNextNote() { const chords = PROGRESSIONS[progression] || PROGRESSIONS['I-vi-IV-V']; // Release previous note if (lastNote >= 0) { ringWrite(MESSAGE_TYPE.NOTE_OFF, lastNote, 0); } const chord = chords[chordIndex]; const baseNote = 48 + (octaveOffset * 12); const notes = []; for (let oct = 0; oct < 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; const note = notes[noteIndex % notes.length]; const velocity = 0.6 + Math.random() * 0.2; ringWrite(MESSAGE_TYPE.NOTE_ON, note, velocity); lastNote = note; noteIndex++; if (noteIndex >= notes.length) { noteIndex = 0; chordIndex = (chordIndex + 1) % chords.length; } } function start() { if (playing) return; playing = true; chordIndex = 0; noteIndex = 0; scheduleNext(); postMessage({ type: 'state', playing: true }); } function stop() { playing = false; if (timer) { clearTimeout(timer); timer = null; } if (lastNote >= 0) { ringWrite(MESSAGE_TYPE.NOTE_OFF, lastNote, 0); lastNote = -1; } postMessage({ type: 'state', playing: false }); } // Handle messages from main thread self.onmessage = (e) => { const { type, data } = e.data; switch (type) { case 'init': ringBuffer = data.sharedBuffer; ringF32 = new Float32Array(ringBuffer); ringI32 = new Int32Array(ringBuffer); break; case 'start': start(); break; case 'stop': stop(); break; case 'set': if ('bpm' in data) bpm = data.bpm; if ('octaves' in data) octaves = data.octaves; if ('octaveOffset' in data) octaveOffset = data.octaveOffset; if ('progression' in data) progression = data.progression; break; } };