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

@@ -1,9 +1,19 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { getAnalyserData } from '../engine/audioEngine.js';
// Zoom levels: how many samples to display (from a 2048-sample buffer)
// Fewer samples = zoomed in (more detail), more samples = zoomed out (more time visible)
const ZOOM_LEVELS = [64, 128, 256, 512, 1024, 2048];
const DEFAULT_ZOOM = 2; // index → 256 samples
export default function ScopeDisplay({ moduleId }) {
const canvasRef = useRef(null);
const rafRef = useRef(null);
const [zoomIdx, setZoomIdx] = useState(DEFAULT_ZOOM);
const zoomRef = useRef(ZOOM_LEVELS[DEFAULT_ZOOM]);
// Keep ref in sync so the draw loop picks it up without re-creating the effect
useEffect(() => { zoomRef.current = ZOOM_LEVELS[zoomIdx]; }, [zoomIdx]);
useEffect(() => {
const canvas = canvasRef.current;
@@ -23,16 +33,25 @@ export default function ScopeDisplay({ moduleId }) {
ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2);
ctx.moveTo(0, h / 4); ctx.lineTo(w, h / 4);
ctx.moveTo(0, h * 3 / 4); ctx.lineTo(w, h * 3 / 4);
for (let x = w / 4; x < w; x += w / 4) {
ctx.moveTo(x, 0); ctx.lineTo(x, h);
}
ctx.stroke();
const data = getAnalyserData(moduleId);
if (data && data.length > 0) {
const samplesToShow = zoomRef.current;
// Center the window in the buffer
const offset = Math.max(0, Math.floor((data.length - samplesToShow) / 2));
const end = Math.min(data.length, offset + samplesToShow);
ctx.strokeStyle = '#00e5ff';
ctx.lineWidth = 1.5;
ctx.beginPath();
const step = w / data.length;
for (let i = 0; i < data.length; i++) {
const y = h / 2 + data[i] * h / 2 * -1;
const count = end - offset;
const step = w / count;
for (let i = 0; i < count; i++) {
const y = h / 2 + data[offset + i] * h / 2 * -1;
if (i === 0) ctx.moveTo(0, y);
else ctx.lineTo(i * step, y);
}
@@ -46,5 +65,43 @@ export default function ScopeDisplay({ moduleId }) {
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
}, [moduleId]);
return <canvas ref={canvasRef} className="scope-canvas" />;
const canZoomIn = zoomIdx > 0;
const canZoomOut = zoomIdx < ZOOM_LEVELS.length - 1;
return (
<div style={{ position: 'relative' }}>
<canvas ref={canvasRef} className="scope-canvas" />
<div style={{
position: 'absolute', bottom: 2, right: 2,
display: 'flex', gap: 2,
}}>
<button
onClick={() => canZoomOut && setZoomIdx(i => i + 1)}
disabled={!canZoomOut}
title="Zoom out (más tiempo)"
style={{
width: 18, height: 18, padding: 0,
background: canZoomOut ? '#1a1a3a' : '#0a0a15',
border: '1px solid #333', borderRadius: 3,
color: canZoomOut ? '#00e5ff' : '#333',
cursor: canZoomOut ? 'pointer' : 'default',
fontSize: 12, lineHeight: '16px', fontWeight: 'bold',
}}
></button>
<button
onClick={() => canZoomIn && setZoomIdx(i => i - 1)}
disabled={!canZoomIn}
title="Zoom in (más detalle)"
style={{
width: 18, height: 18, padding: 0,
background: canZoomIn ? '#1a1a3a' : '#0a0a15',
border: '1px solid #333', borderRadius: 3,
color: canZoomIn ? '#00e5ff' : '#333',
cursor: canZoomIn ? 'pointer' : 'default',
fontSize: 12, lineHeight: '16px', fontWeight: 'bold',
}}
>+</button>
</div>
</div>
);
}