From 5890a6d0b93adbb3c742b387a100b0cc798159e5 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Fri, 1 May 2026 23:56:12 -0700 Subject: [PATCH] fix: piano roll spectrogram is not being zoomed correctly; switching from MIDI region to audio region with piano roll open might cause residual notes. --- src/components/piano-roll/PianoGrid.tsx | 3 +++ src/components/piano-roll/PianoRoll.tsx | 25 ++++++++++--------- .../piano-roll/PianoRollContent.tsx | 5 +++- .../piano-roll/SpectrogramCanvas.tsx | 25 ++++++++++++++++--- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/components/piano-roll/PianoGrid.tsx b/src/components/piano-roll/PianoGrid.tsx index e49d197..7f181fc 100644 --- a/src/components/piano-roll/PianoGrid.tsx +++ b/src/components/piano-roll/PianoGrid.tsx @@ -32,6 +32,7 @@ interface PianoGridProps { bpm?: number; spectrogramThresholdDb?: number; spectrogramPower?: number; + pianoRollZoom?: number; mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; } @@ -60,6 +61,7 @@ const PianoGrid: React.FC = ({ bpm = 120, spectrogramThresholdDb = -25, spectrogramPower = 0.5, + pianoRollZoom = 1, }) => { const [cursorPosition, setCursorPosition] = useState(null); const [isModifierPressed, setIsModifierPressed] = useState(false); @@ -240,6 +242,7 @@ const PianoGrid: React.FC = ({ bpm={bpm} thresholdDb={spectrogramThresholdDb} power={spectrogramPower} + zoom={pianoRollZoom} /> )} diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index 18383a9..4306aaa 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -157,23 +157,23 @@ const PianoRoll: React.FC = ({ setActiveRegion(null); return; } - - // Find the region in the tracks + + let found: KGMidiRegion | null = null; for (const track of tracks) { - const regions = track.getRegions(); - const region = regions.find(r => r.getId() === regionId); - + const region = track.getRegions().find(r => r.getId() === regionId); if (region && region instanceof KGMidiRegion) { - setActiveRegion(region); - - if (DEBUG_MODE.PIANO_ROLL) { - console.log(`Active region set in PianoRoll: ${region.getId()}`); - console.log(`Region details: name=${region.getName()}, trackId=${region.getTrackId()}, trackIndex=${region.getTrackIndex()}`); - } - + found = region; break; } } + + // Always update — clears stale MIDI region when switching to an audio region + setActiveRegion(found); + + if (found && DEBUG_MODE.PIANO_ROLL) { + console.log(`Active region set in PianoRoll: ${found.getId()}`); + console.log(`Region details: name=${found.getName()}, trackId=${found.getTrackId()}, trackIndex=${found.getTrackIndex()}`); + } }, [regionId, tracks]); // Sync local state with KGPianoRollState on mount @@ -965,6 +965,7 @@ const PianoRoll: React.FC = ({ bpm={bpm} spectrogramThresholdDb={spectrogramThresholdDb} spectrogramPower={spectrogramPower} + pianoRollZoom={pianoRollZoom} />
= ({ @@ -56,6 +57,7 @@ const PianoRollContent: React.FC = ({ bpm = 120, spectrogramThresholdDb = -25, spectrogramPower = 0.5, + pianoRollZoom = 1, }) => { const isSpectrogram = mode === 'spectrogram'; // Get KGCore instance @@ -234,7 +236,7 @@ const PianoRollContent: React.FC = ({ /> ); }); - }, [activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]); + }, [mode, activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]); const recordingNoteOverlays = useMemo(() => { if (!isRecording || !activeRegion || recordingNotes.length === 0) return null; @@ -281,6 +283,7 @@ const PianoRollContent: React.FC = ({ bpm={bpm} spectrogramThresholdDb={spectrogramThresholdDb} spectrogramPower={spectrogramPower} + pianoRollZoom={pianoRollZoom} > {memoizedNotes} {!isSpectrogram && recordingNoteOverlays} diff --git a/src/components/piano-roll/SpectrogramCanvas.tsx b/src/components/piano-roll/SpectrogramCanvas.tsx index 6b4b90f..9aa3ac0 100644 --- a/src/components/piano-roll/SpectrogramCanvas.tsx +++ b/src/components/piano-roll/SpectrogramCanvas.tsx @@ -12,6 +12,7 @@ interface SpectrogramCanvasProps { bpm: number; thresholdDb: number; power: number; + zoom: number; } const PITCH_BINS = 128; @@ -53,6 +54,7 @@ const SpectrogramCanvas: React.FC = ({ bpm, thresholdDb, power, + zoom, }) => { const canvasRef = useRef(null); const [loading, setLoading] = useState(true); @@ -62,6 +64,11 @@ const SpectrogramCanvas: React.FC = ({ const rawResultRef = useRef(null); const sampleRateRef = useRef(44100); const regionDurationRef = useRef(0); + // Natural (1x) canvas pixel width — set after each draw, used to apply zoom as CSS stretch + const naturalWidthRef = useRef(0); + // Always-current zoom without making it a renderSpectrogram dependency + const zoomRef = useRef(zoom); + useEffect(() => { zoomRef.current = zoom; }, [zoom]); const renderSpectrogram = useCallback(( result: SpectrogramResult, @@ -75,13 +82,12 @@ const SpectrogramCanvas: React.FC = ({ const ctx = canvas.getContext('2d'); if (!ctx) return; - const beatWidth = - parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40; const noteHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20; const totalBeats = (regionDurationSeconds * bpm) / 60; - const canvasWidth = Math.ceil(totalBeats * beatWidth); + // Always draw at 1x resolution; zoom is applied as CSS width stretch + const canvasWidth = Math.ceil(totalBeats * 40); const canvasHeight = PITCH_BINS * noteHeight; // Convert dB threshold to linear: values below this → black @@ -131,6 +137,11 @@ const SpectrogramCanvas: React.FC = ({ ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(offscreen, 0, 0, canvasWidth, canvasHeight); + + // Store natural width and apply current zoom as CSS stretch (no pixel recompute on zoom) + naturalWidthRef.current = canvasWidth; + canvas.style.width = `${canvasWidth * zoomRef.current}px`; + canvas.style.height = `${canvasHeight}px`; }, [bpm]); // Re-render without re-running the worker when threshold or power changes @@ -146,6 +157,14 @@ const SpectrogramCanvas: React.FC = ({ } }, [thresholdDb, power, renderSpectrogram]); + // Zoom changes: stretch width only, pin height to canvas pixel height + useEffect(() => { + if (canvasRef.current && naturalWidthRef.current > 0) { + canvasRef.current.style.width = `${naturalWidthRef.current * zoom}px`; + canvasRef.current.style.height = `${canvasRef.current.height}px`; + } + }, [zoom]); + // Load audio + run worker when the audio region itself changes useEffect(() => { let cancelled = false;