feat: add sequencer, piano roll modules with pre-composed chiptune melody

Add step sequencer (16-step with note/gate editing) and piano roll
(canvas-based MIDI editor with draw/erase tools). Includes a Megaman-style
melody in C minor. Chiptune preset now uses piano roll instead of keyboard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 01:30:03 +01:00
parent 4a003f2af2
commit 65a89e2b59
7 changed files with 647 additions and 23 deletions

View File

@@ -146,7 +146,6 @@ function createNode(mod) {
};
}
case 'keyboard': {
// Keyboard outputs frequency as a Signal and gate as a Signal
const freqSig = new Tone.Signal(440);
const gateSig = new Tone.Signal(0);
return {
@@ -158,6 +157,37 @@ function createNode(mod) {
dispose: () => { freqSig.dispose(); gateSig.dispose(); },
};
}
case 'sequencer': {
const freqSig = new Tone.Signal(440);
const gateSig = new Tone.Signal(0);
// Sequencer loop managed externally by SequencerWidget
return {
node: null,
inputs: {},
outputs: { freq: freqSig, gate: gateSig },
_freqSig: freqSig,
_gateSig: gateSig,
_seq: null, // Tone.Sequence set by widget
dispose: () => {
freqSig.dispose(); gateSig.dispose();
},
};
}
case 'pianoroll': {
const freqSig = new Tone.Signal(440);
const gateSig = new Tone.Signal(0);
return {
node: null,
inputs: {},
outputs: { freq: freqSig, gate: gateSig },
_freqSig: freqSig,
_gateSig: gateSig,
_part: null, // Tone.Part set by widget
dispose: () => {
freqSig.dispose(); gateSig.dispose();
},
};
}
default:
return null;
}
@@ -280,11 +310,31 @@ export function updateParam(moduleId, paramName, value) {
if (paramName === 'volume') entry.node.gain.value = Tone.dbToGain(value);
break;
case 'keyboard':
if (paramName === 'octave') { /* stored in state only */ }
case 'sequencer':
case 'pianoroll':
// All params stored in state, managed by widgets
break;
}
}
export function setSequencerSignals(moduleId, freq, gate) {
const entry = audioNodes[moduleId];
if (!entry) return;
if (entry._freqSig) entry._freqSig.value = freq;
if (entry._gateSig) entry._gateSig.value = gate ? 1 : 0;
// Trigger connected envelopes
for (const conn of state.connections) {
if (conn.from.moduleId === moduleId && conn.from.port === 'gate') {
const envEntry = audioNodes[conn.to.moduleId];
if (envEntry && envEntry.node instanceof Tone.Envelope) {
if (gate) envEntry.node.triggerAttack();
else envEntry.node.triggerRelease();
}
}
}
}
export function triggerKeyboard(moduleId, freq, gate) {
const entry = audioNodes[moduleId];
if (!entry) return;