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:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -157,23 +157,23 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
setActiveRegion(null);
|
setActiveRegion(null);
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user