fix: piano roll spectrogram is not being zoomed correctly; switching from MIDI region to audio region with piano roll open might cause residual notes.

This commit is contained in:
Xiaohan-Tian
2026-05-01 23:56:12 -07:00
parent 811b3a0936
commit 5890a6d0b9
4 changed files with 42 additions and 16 deletions
+3
View File
@@ -32,6 +32,7 @@ interface PianoGridProps {
bpm?: number; bpm?: number;
spectrogramThresholdDb?: number; spectrogramThresholdDb?: number;
spectrogramPower?: number; spectrogramPower?: number;
pianoRollZoom?: number;
mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
} }
@@ -60,6 +61,7 @@ const PianoGrid: React.FC<PianoGridProps> = ({
bpm = 120, bpm = 120,
spectrogramThresholdDb = -25, spectrogramThresholdDb = -25,
spectrogramPower = 0.5, spectrogramPower = 0.5,
pianoRollZoom = 1,
}) => { }) => {
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null); const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
const [isModifierPressed, setIsModifierPressed] = useState(false); const [isModifierPressed, setIsModifierPressed] = useState(false);
@@ -240,6 +242,7 @@ const PianoGrid: React.FC<PianoGridProps> = ({
bpm={bpm} bpm={bpm}
thresholdDb={spectrogramThresholdDb} thresholdDb={spectrogramThresholdDb}
power={spectrogramPower} power={spectrogramPower}
zoom={pianoRollZoom}
/> />
)} )}
+12 -11
View File
@@ -158,22 +158,22 @@ const PianoRoll: React.FC<PianoRollProps> = ({
return; return;
} }
// Find the region in the tracks let found: KGMidiRegion | null = null;
for (const track of tracks) { for (const track of tracks) {
const regions = track.getRegions(); const region = track.getRegions().find(r => r.getId() === regionId);
const region = regions.find(r => r.getId() === regionId);
if (region && region instanceof KGMidiRegion) { if (region && region instanceof KGMidiRegion) {
setActiveRegion(region); found = 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()}`);
}
break; 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]); }, [regionId, tracks]);
// Sync local state with KGPianoRollState on mount // Sync local state with KGPianoRollState on mount
@@ -965,6 +965,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
bpm={bpm} bpm={bpm}
spectrogramThresholdDb={spectrogramThresholdDb} spectrogramThresholdDb={spectrogramThresholdDb}
spectrogramPower={spectrogramPower} spectrogramPower={spectrogramPower}
pianoRollZoom={pianoRollZoom}
/> />
<div <div
@@ -34,6 +34,7 @@ interface PianoRollContentProps {
bpm?: number; bpm?: number;
spectrogramThresholdDb?: number; spectrogramThresholdDb?: number;
spectrogramPower?: number; spectrogramPower?: number;
pianoRollZoom?: number;
} }
const PianoRollContent: React.FC<PianoRollContentProps> = ({ const PianoRollContent: React.FC<PianoRollContentProps> = ({
@@ -56,6 +57,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
bpm = 120, bpm = 120,
spectrogramThresholdDb = -25, spectrogramThresholdDb = -25,
spectrogramPower = 0.5, spectrogramPower = 0.5,
pianoRollZoom = 1,
}) => { }) => {
const isSpectrogram = mode === 'spectrogram'; const isSpectrogram = mode === 'spectrogram';
// Get KGCore instance // Get KGCore instance
@@ -234,7 +236,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
/> />
); );
}); });
}, [activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]); }, [mode, activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks]);
const recordingNoteOverlays = useMemo(() => { const recordingNoteOverlays = useMemo(() => {
if (!isRecording || !activeRegion || recordingNotes.length === 0) return null; if (!isRecording || !activeRegion || recordingNotes.length === 0) return null;
@@ -281,6 +283,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
bpm={bpm} bpm={bpm}
spectrogramThresholdDb={spectrogramThresholdDb} spectrogramThresholdDb={spectrogramThresholdDb}
spectrogramPower={spectrogramPower} spectrogramPower={spectrogramPower}
pianoRollZoom={pianoRollZoom}
> >
{memoizedNotes} {memoizedNotes}
{!isSpectrogram && recordingNoteOverlays} {!isSpectrogram && recordingNoteOverlays}
@@ -12,6 +12,7 @@ interface SpectrogramCanvasProps {
bpm: number; bpm: number;
thresholdDb: number; thresholdDb: number;
power: number; power: number;
zoom: number;
} }
const PITCH_BINS = 128; const PITCH_BINS = 128;
@@ -53,6 +54,7 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
bpm, bpm,
thresholdDb, thresholdDb,
power, power,
zoom,
}) => { }) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null); const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -62,6 +64,11 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
const rawResultRef = useRef<SpectrogramResult | null>(null); const rawResultRef = useRef<SpectrogramResult | null>(null);
const sampleRateRef = useRef<number>(44100); const sampleRateRef = useRef<number>(44100);
const regionDurationRef = useRef<number>(0); const regionDurationRef = useRef<number>(0);
// Natural (1x) canvas pixel width — set after each draw, used to apply zoom as CSS stretch
const naturalWidthRef = useRef<number>(0);
// Always-current zoom without making it a renderSpectrogram dependency
const zoomRef = useRef(zoom);
useEffect(() => { zoomRef.current = zoom; }, [zoom]);
const renderSpectrogram = useCallback(( const renderSpectrogram = useCallback((
result: SpectrogramResult, result: SpectrogramResult,
@@ -75,13 +82,12 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
const beatWidth =
parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40;
const noteHeight = const noteHeight =
parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20; parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
const totalBeats = (regionDurationSeconds * bpm) / 60; 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; const canvasHeight = PITCH_BINS * noteHeight;
// Convert dB threshold to linear: values below this → black // Convert dB threshold to linear: values below this → black
@@ -131,6 +137,11 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high'; ctx.imageSmoothingQuality = 'high';
ctx.drawImage(offscreen, 0, 0, canvasWidth, canvasHeight); 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]); }, [bpm]);
// Re-render without re-running the worker when threshold or power changes // Re-render without re-running the worker when threshold or power changes
@@ -146,6 +157,14 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
} }
}, [thresholdDb, power, renderSpectrogram]); }, [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 // Load audio + run worker when the audio region itself changes
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;