fix: Transport lifecycle, scope zoom, clear button, and freq routing
- Fix pianoroll/sequencer Transport not resetting on stop/restart (notes were scheduled in the past and never fired) - Stop and cancel Transport in stopAudio() to prevent stale events - Add zoom +/- buttons to scope widget (6 levels, 64–2048 samples) - Increase scope analyser buffer from 256 to 2048 for wider time view - Add vertical grid lines to scope display - Add "Limpiar" clear canvas button to PuzzleView - Skip audio-graph connection for keyboard/seq/pianoroll freq→osc freq (direct frequency setting prevents inaudible ultrasonic values) - Auto-trigger envelopes without gate connections for noise/ambient levels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,7 @@ function createNode(mod) {
|
||||
};
|
||||
}
|
||||
case 'scope': {
|
||||
const analyser = new Tone.Analyser('waveform', 256);
|
||||
const analyser = new Tone.Analyser('waveform', 2048);
|
||||
return {
|
||||
node: analyser,
|
||||
inputs: { in: analyser },
|
||||
@@ -245,6 +245,17 @@ export function connectWire(conn) {
|
||||
const toEntry = ensureNode(conn.to.moduleId);
|
||||
if (!fromEntry || !toEntry) return;
|
||||
|
||||
// Skip audio-graph connection for keyboard/sequencer/pianoroll freq → oscillator freq.
|
||||
// These signals carry absolute Hz values that would be mangled by the oscillator's
|
||||
// frequency-modulation Gain scaler. Instead, triggerKeyboard / setSequencerSignals
|
||||
// set the oscillator frequency directly when notes are played.
|
||||
const fromMod = state.modules.find(m => m.id === conn.from.moduleId);
|
||||
const toMod = state.modules.find(m => m.id === conn.to.moduleId);
|
||||
if (fromMod && ['keyboard', 'sequencer', 'pianoroll'].includes(fromMod.type) &&
|
||||
conn.from.port === 'freq' && toMod?.type === 'oscillator' && conn.to.port === 'freq') {
|
||||
return; // handled imperatively in triggerKeyboard / setSequencerSignals
|
||||
}
|
||||
|
||||
const output = fromEntry.outputs[conn.from.port];
|
||||
const input = toEntry.inputs[conn.to.port];
|
||||
if (!output || input === undefined || input === null) return;
|
||||
@@ -356,6 +367,17 @@ export function setSequencerSignals(moduleId, freq, gate) {
|
||||
if (entry._freqSig) entry._freqSig.value = freq;
|
||||
if (entry._gateSig) entry._gateSig.value = gate ? 1 : 0;
|
||||
|
||||
// Directly set connected oscillator frequencies (bypasses the modulation Gain)
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'freq') {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
const oscMod = state.modules.find(m => m.id === conn.to.moduleId);
|
||||
if (oscEntry?.node && oscMod?.type === 'oscillator') {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger connected envelopes
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'gate') {
|
||||
@@ -374,6 +396,17 @@ export function triggerKeyboard(moduleId, freq, gate) {
|
||||
if (entry._freqSig) entry._freqSig.value = freq;
|
||||
if (entry._gateSig) entry._gateSig.value = gate ? 1 : 0;
|
||||
|
||||
// Directly set connected oscillator frequencies (bypasses the modulation Gain)
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'freq') {
|
||||
const oscEntry = audioNodes[conn.to.moduleId];
|
||||
const oscMod = state.modules.find(m => m.id === conn.to.moduleId);
|
||||
if (oscEntry?.node && oscMod?.type === 'oscillator') {
|
||||
oscEntry.node.frequency.value = freq;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also trigger any connected envelopes
|
||||
for (const conn of state.connections) {
|
||||
if (conn.from.moduleId === moduleId && conn.from.port === 'gate') {
|
||||
@@ -395,6 +428,13 @@ export async function startAudio() {
|
||||
}
|
||||
|
||||
export function stopAudio() {
|
||||
// Stop and reset Transport so pianoroll/sequencer Parts don't get stranded
|
||||
try {
|
||||
Tone.getTransport().stop();
|
||||
Tone.getTransport().cancel(); // Remove all scheduled events
|
||||
Tone.getTransport().position = 0;
|
||||
} catch (e) { /* ignore if Transport not started */ }
|
||||
|
||||
// Destroy all nodes
|
||||
for (const id of Object.keys(audioNodes)) {
|
||||
destroyNode(parseInt(id));
|
||||
@@ -417,6 +457,21 @@ export function rebuildGraph() {
|
||||
for (const conn of state.connections) {
|
||||
connectWire(conn);
|
||||
}
|
||||
|
||||
// Auto-trigger envelopes that have no gate connection (free-running mode).
|
||||
// This allows noise/ambient patches to work without a keyboard/sequencer.
|
||||
for (const mod of state.modules) {
|
||||
if (mod.type !== 'envelope') continue;
|
||||
const hasGateInput = state.connections.some(
|
||||
c => c.to.moduleId === mod.id && c.to.port === 'gate'
|
||||
);
|
||||
if (!hasGateInput) {
|
||||
const entry = audioNodes[mod.id];
|
||||
if (entry && entry.node && typeof entry.node.triggerAttack === 'function') {
|
||||
entry.node.triggerAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getAnalyserData(moduleId) {
|
||||
|
||||
Reference in New Issue
Block a user