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:
@@ -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;
|
||||
|
||||
@@ -257,3 +257,41 @@ defineModule('keyboard', {
|
||||
octave: { type: 'knob', min: 1, max: 8, default: 4, unit: '', label: 'Octave' },
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== SEQUENCER ====================
|
||||
|
||||
defineModule('sequencer', {
|
||||
name: 'Sequencer',
|
||||
icon: '▦',
|
||||
category: 'Source',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'freq', type: PORT_TYPE.AUDIO, label: 'Freq' },
|
||||
{ name: 'gate', type: PORT_TYPE.TRIGGER, label: 'Gate' },
|
||||
],
|
||||
params: {
|
||||
bpm: { type: 'knob', min: 40, max: 300, default: 140, unit: 'bpm', label: 'BPM' },
|
||||
steps: { type: 'select', options: ['8', '16', '32'], default: '16', label: 'Steps' },
|
||||
swing: { type: 'knob', min: 0, max: 0.5, default: 0, unit: '', label: 'Swing' },
|
||||
},
|
||||
// Custom data: step notes/gates stored in module.params._steps
|
||||
});
|
||||
|
||||
// ==================== PIANO ROLL ====================
|
||||
|
||||
defineModule('pianoroll', {
|
||||
name: 'Piano Roll',
|
||||
icon: '🎼',
|
||||
category: 'Source',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{ name: 'freq', type: PORT_TYPE.AUDIO, label: 'Freq' },
|
||||
{ name: 'gate', type: PORT_TYPE.TRIGGER, label: 'Gate' },
|
||||
],
|
||||
params: {
|
||||
bpm: { type: 'knob', min: 40, max: 300, default: 140, unit: 'bpm', label: 'BPM' },
|
||||
loop: { type: 'select', options: ['on', 'off'], default: 'on', label: 'Loop' },
|
||||
bars: { type: 'select', options: ['1', '2', '4', '8'], default: '4', label: 'Bars' },
|
||||
},
|
||||
// Custom data: notes stored in module.params._notes = [{note, start, duration}, ...]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user