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:
Jose Luis
2026-03-21 04:28:36 +01:00
parent 58d567c671
commit 36eb31a652
5 changed files with 173 additions and 24 deletions

View File

@@ -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) {