feat: added audio track spectrogram visualization feature

This commit is contained in:
Xiaohan-Tian
2026-05-01 17:44:29 -07:00
parent cd90dc1c99
commit eafb3a4937
15 changed files with 696 additions and 88 deletions
+116 -69
View File
@@ -4,6 +4,13 @@ import { KGDropdown } from '../common';
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
import { KGCore } from '../../core/KGCore';
const POWER_OPTIONS = [
{ label: 'Linear', value: '1.0' },
{ label: '√ (default)', value: '0.5' },
{ label: 'Mild', value: '0.4' },
{ label: 'Strong', value: '0.3' },
];
interface PianoRollToolbarProps {
activeTool: 'pointer' | 'pencil';
onToolSelect: (tool: 'pointer' | 'pencil') => void;
@@ -17,6 +24,11 @@ interface PianoRollToolbarProps {
chordGuide: string;
onChordGuideChange: (value: string) => void;
blinkButton?: string | null;
mode?: 'midi-edit' | 'spectrogram';
thresholdDb?: number;
onThresholdChange?: (db: number) => void;
power?: number;
onPowerChange?: (power: number) => void;
}
const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
@@ -31,82 +43,117 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
onModeChange,
chordGuide,
onChordGuideChange,
blinkButton = null
blinkButton = null,
mode = 'midi-edit',
thresholdDb = -25,
onThresholdChange,
power = 0.5,
onPowerChange,
}) => {
const isSpectrogram = mode === 'spectrogram';
return (
<div className="piano-roll-toolbar">
<div className="toolbar-left">
{/* Left section with mode and chord guide dropdowns */}
<KGDropdown
options={Object.entries(KGCore.FUNCTIONAL_CHORDS_DATA).map(([id, data]) => ({ label: data.name, value: id }))}
value={selectedMode}
onChange={(value) => onModeChange(value)}
label="Mode"
buttonClassName="mode-dropdown"
showValueAsLabel={true}
/>
<KGDropdown
options={[
{ label: 'Guide: Disabled', value: 'N' },
{ label: 'Chord Guide: T', value: 'T' },
{ label: 'Chord Guide: S', value: 'S' },
{ label: 'Chord Guide: D', value: 'D' }
]}
value={chordGuide}
onChange={(value) => onChordGuideChange(value)}
label="Chord"
buttonClassName="chord-guide-dropdown"
showValueAsLabel={true}
/>
</div>
<div className="toolbar-center">
{/* Center section with pointer and pencil tools */}
<button
className={`tool-button ${activeTool === 'pointer' ? 'active' : ''}`}
onClick={() => onToolSelect('pointer')}
title="Pointer Tool"
>
<FaMousePointer />
</button>
<button
className={`tool-button ${activeTool === 'pencil' ? 'active' : ''}`}
onClick={() => onToolSelect('pencil')}
title="Pencil Tool"
>
<FaPencilAlt />
</button>
</div>
{!isSpectrogram && (
<div className="toolbar-left">
<KGDropdown
options={Object.entries(KGCore.FUNCTIONAL_CHORDS_DATA).map(([id, data]) => ({ label: data.name, value: id }))}
value={selectedMode}
onChange={(value) => onModeChange(value)}
label="Mode"
buttonClassName="mode-dropdown"
showValueAsLabel={true}
/>
<KGDropdown
options={[
{ label: 'Guide: Disabled', value: 'N' },
{ label: 'Chord Guide: T', value: 'T' },
{ label: 'Chord Guide: S', value: 'S' },
{ label: 'Chord Guide: D', value: 'D' }
]}
value={chordGuide}
onChange={(value) => onChordGuideChange(value)}
label="Chord"
buttonClassName="chord-guide-dropdown"
showValueAsLabel={true}
/>
</div>
)}
{!isSpectrogram && (
<div className="toolbar-center">
<button
className={`tool-button ${activeTool === 'pointer' ? 'active' : ''}`}
onClick={() => onToolSelect('pointer')}
title="Pointer Tool"
>
<FaMousePointer />
</button>
<button
className={`tool-button ${activeTool === 'pencil' ? 'active' : ''}`}
onClick={() => onToolSelect('pencil')}
title="Pencil Tool"
>
<FaPencilAlt />
</button>
</div>
)}
<div className="toolbar-right">
{/* Right section with quantization options */}
<KGDropdown
options={KGPianoRollState.SNAP_OPTIONS}
value={snapping}
onChange={(value) => onSnappingSelect(value)}
label="Snap"
buttonClassName="snapping"
showValueAsLabel={true}
/>
{!isSpectrogram && (
<>
<KGDropdown
options={KGPianoRollState.SNAP_OPTIONS}
value={snapping}
onChange={(value) => onSnappingSelect(value)}
label="Snap"
buttonClassName="snapping"
showValueAsLabel={true}
/>
<KGDropdown
options={KGPianoRollState.QUANT_POS_OPTIONS}
value={quantPosition}
onChange={(value) => onQuantSelect('position', value)}
label="Qua. Pos."
buttonClassName={`quant-position ${blinkButton === 'quant-position' ? 'button-blink' : ''}`}
/>
<KGDropdown
options={KGPianoRollState.QUANT_LEN_OPTIONS}
value={quantLength}
onChange={(value) => onQuantSelect('length', value)}
label="Qua. Len."
buttonClassName={`quant-length ${blinkButton === 'quant-length' ? 'button-blink' : ''}`}
/>
</>
)}
<KGDropdown
options={KGPianoRollState.QUANT_POS_OPTIONS}
value={quantPosition}
onChange={(value) => onQuantSelect('position', value)}
label="Qua. Pos."
buttonClassName={`quant-position ${blinkButton === 'quant-position' ? 'button-blink' : ''}`}
/>
<KGDropdown
options={KGPianoRollState.QUANT_LEN_OPTIONS}
value={quantLength}
onChange={(value) => onQuantSelect('length', value)}
label="Qua. Len."
buttonClassName={`quant-length ${blinkButton === 'quant-length' ? 'button-blink' : ''}`}
/>
{isSpectrogram && (
<div className="spectrogram-toolbar-controls">
<span className="spectrogram-control-label">Floor</span>
<input
type="range"
className="spectrogram-threshold-slider"
min={-50}
max={-5}
step={1}
value={thresholdDb}
onChange={e => onThresholdChange?.(parseInt(e.target.value))}
title={`Noise floor: ${thresholdDb} dB`}
/>
<span className="spectrogram-threshold-value">{thresholdDb} dB</span>
<KGDropdown
options={POWER_OPTIONS}
value={power.toString()}
onChange={v => onPowerChange?.(parseFloat(v))}
label="Curve"
buttonClassName="curve-dropdown"
showValueAsLabel={true}
/>
</div>
)}
</div>
</div>
);
};
export default PianoRollToolbar;
export default PianoRollToolbar;