fix: move computing spectrogram text to the center of current viewport of piano roll window

This commit is contained in:
Xiaohan-Tian
2026-05-02 00:09:33 -07:00
parent 5890a6d0b9
commit 6b8a470022
4 changed files with 89 additions and 62 deletions
+3
View File
@@ -34,6 +34,7 @@ interface PianoGridProps {
spectrogramPower?: number;
pianoRollZoom?: number;
mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
onSpectrogramLoadingChange?: (loading: boolean) => void;
}
interface CursorPosition {
@@ -62,6 +63,7 @@ const PianoGrid: React.FC<PianoGridProps> = ({
spectrogramThresholdDb = -25,
spectrogramPower = 0.5,
pianoRollZoom = 1,
onSpectrogramLoadingChange,
}) => {
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
const [isModifierPressed, setIsModifierPressed] = useState(false);
@@ -243,6 +245,7 @@ const PianoGrid: React.FC<PianoGridProps> = ({
thresholdDb={spectrogramThresholdDb}
power={spectrogramPower}
zoom={pianoRollZoom}
onLoadingChange={onSpectrogramLoadingChange}
/>
)}
+25 -1
View File
@@ -159,13 +159,37 @@
color: #ff4444;
}
.piano-roll-content-outer {
position: relative;
flex: 1;
overflow: hidden;
}
.piano-roll-content {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: auto;
}
.spectrogram-loading-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 100;
}
.spectrogram-loading-label {
padding: 6px 10px;
background: rgba(0, 0, 0, 0.6);
color: #aaa;
font-size: 11px;
border-radius: 3px;
}
.piano-grid-header {
display: grid;
grid-template-columns: repeat(var(--max-number-of-bars), var(--region-grid-bar-width));
+45 -30
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useState, useRef, useEffect } from 'react';
import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
import { DEBUG_MODE } from '../../constants';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { KGMidiNote } from '../../core/midi/KGMidiNote';
@@ -60,6 +60,11 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
pianoRollZoom = 1,
}) => {
const isSpectrogram = mode === 'spectrogram';
const [spectrogramLoading, setSpectrogramLoading] = useState(false);
const handleSpectrogramLoadingChange = useCallback((loading: boolean) => {
setSpectrogramLoading(loading);
}, []);
// Get KGCore instance
const core = KGCore.instance();
@@ -257,38 +262,48 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
}, [isRecording, recordingNotes, activeRegion]);
return (
<div
className="piano-roll-content"
ref={contentRef}
>
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
<div className="piano-roll-content-outer">
<div
className="piano-roll-content"
ref={contentRef}
>
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
<div className="piano-roll-body">
<PianoKeys activeRegion={activeRegion} />
<div className="piano-roll-body">
<PianoKeys activeRegion={activeRegion} />
<PianoGrid
gridRef={pianoGridRef}
onDoubleClick={isSpectrogram ? () => {} : handleGridDoubleClick}
onClick={isSpectrogram ? () => {} : handleCombinedClick}
onMouseDown={isSpectrogram ? () => {} : handleBackgroundMouseDown}
isBoxSelecting={isSpectrogram ? false : isBoxSelectingRef.current}
selectionBox={isSpectrogram ? { startX: 0, startY: 0, endX: 0, endY: 0 } : selectionBoxRef.current}
regionStartBeat={activeRegion?.getStartFromBeat() || 0}
selectedMode={selectedMode}
keySignature={keySignature}
chordGuide={chordGuide}
audioRegion={audioRegion}
trackId={trackId}
projectName={projectName}
bpm={bpm}
spectrogramThresholdDb={spectrogramThresholdDb}
spectrogramPower={spectrogramPower}
pianoRollZoom={pianoRollZoom}
>
{memoizedNotes}
{!isSpectrogram && recordingNoteOverlays}
</PianoGrid>
<PianoGrid
gridRef={pianoGridRef}
onDoubleClick={isSpectrogram ? () => {} : handleGridDoubleClick}
onClick={isSpectrogram ? () => {} : handleCombinedClick}
onMouseDown={isSpectrogram ? () => {} : handleBackgroundMouseDown}
isBoxSelecting={isSpectrogram ? false : isBoxSelectingRef.current}
selectionBox={isSpectrogram ? { startX: 0, startY: 0, endX: 0, endY: 0 } : selectionBoxRef.current}
regionStartBeat={activeRegion?.getStartFromBeat() || 0}
selectedMode={selectedMode}
keySignature={keySignature}
chordGuide={chordGuide}
audioRegion={audioRegion}
trackId={trackId}
projectName={projectName}
bpm={bpm}
spectrogramThresholdDb={spectrogramThresholdDb}
spectrogramPower={spectrogramPower}
pianoRollZoom={pianoRollZoom}
onSpectrogramLoadingChange={handleSpectrogramLoadingChange}
>
{memoizedNotes}
{!isSpectrogram && recordingNoteOverlays}
</PianoGrid>
</div>
</div>
{spectrogramLoading && (
<div className="spectrogram-loading-overlay">
<div className="spectrogram-loading-label">
Computing spectrogram
</div>
</div>
)}
</div>
);
};
+14 -29
View File
@@ -13,6 +13,7 @@ interface SpectrogramCanvasProps {
thresholdDb: number;
power: number;
zoom: number;
onLoadingChange?: (loading: boolean) => void;
}
const PITCH_BINS = 128;
@@ -55,6 +56,7 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
thresholdDb,
power,
zoom,
onLoadingChange,
}) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [loading, setLoading] = useState(true);
@@ -70,6 +72,8 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
const zoomRef = useRef(zoom);
useEffect(() => { zoomRef.current = zoom; }, [zoom]);
useEffect(() => { onLoadingChange?.(loading); }, [loading, onLoadingChange]);
const renderSpectrogram = useCallback((
result: SpectrogramResult,
sampleRate: number,
@@ -240,35 +244,16 @@ const SpectrogramCanvas: React.FC<SpectrogramCanvasProps> = ({
}, [audioRegion, trackId, projectName, bpm]);
return (
<>
<canvas
ref={canvasRef}
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: 0,
pointerEvents: 'none',
}}
/>
{loading && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: 1,
padding: '6px 10px',
background: 'rgba(0,0,0,0.6)',
color: '#aaa',
fontSize: '11px',
pointerEvents: 'none',
}}
>
Computing spectrogram
</div>
)}
</>
<canvas
ref={canvasRef}
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: 0,
pointerEvents: 'none',
}}
/>
);
};