feat: added waveform view for audio regions
This commit is contained in:
@@ -50,6 +50,7 @@ const storeState = {
|
||||
requestedSheetMusicViewEnabled: false,
|
||||
pianoRollViewRequestVersion: 0,
|
||||
openMidiPianoRoll: vi.fn(),
|
||||
openAudioWaveformViewer: vi.fn(),
|
||||
openSpectrogramViewer: vi.fn(),
|
||||
openHybridMode: vi.fn(),
|
||||
hybridAudioRegionId: null as string | null,
|
||||
@@ -166,6 +167,7 @@ describe('MainContent', () => {
|
||||
storeState.setShowPianoRoll.mockClear();
|
||||
storeState.setActiveRegionId.mockClear();
|
||||
storeState.openMidiPianoRoll.mockClear();
|
||||
storeState.openAudioWaveformViewer.mockClear();
|
||||
storeState.openSpectrogramViewer.mockClear();
|
||||
storeState.addTrack.mockClear();
|
||||
storeState.addAudioTrack.mockClear();
|
||||
@@ -220,7 +222,8 @@ describe('MainContent', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select-audio-region' }));
|
||||
|
||||
expect(storeState.activeRegionId).toBe('audio-1');
|
||||
expect(storeState.openSpectrogramViewer).toHaveBeenCalledWith('audio-1');
|
||||
expect(storeState.openAudioWaveformViewer).toHaveBeenCalledWith('audio-1');
|
||||
expect(storeState.openSpectrogramViewer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cmd-click adds a second regular region without range fill', () => {
|
||||
|
||||
@@ -58,6 +58,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
requestedSheetMusicViewEnabled,
|
||||
pianoRollViewRequestVersion,
|
||||
openMidiPianoRoll,
|
||||
openAudioWaveformViewer,
|
||||
openSpectrogramViewer,
|
||||
openHybridMode,
|
||||
hybridAudioRegionId,
|
||||
@@ -95,6 +96,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
setShowPianoRoll,
|
||||
setActiveRegionId,
|
||||
openMidiPianoRoll,
|
||||
openAudioWaveformViewer,
|
||||
openSpectrogramViewer,
|
||||
openHybridMode,
|
||||
pianoRollMode,
|
||||
@@ -321,7 +323,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
}, [globalTracks, mainContentRegions, refreshProjectState, selectedRegionIds, timeSignature.numerator, tracks]);
|
||||
|
||||
const showHybridButtonForAudio = showPianoRoll && pianoRollMode === 'midi-edit';
|
||||
const showHybridButtonForMidi = showPianoRoll && pianoRollMode === 'spectrogram';
|
||||
const showHybridButtonForMidi = showPianoRoll && (pianoRollMode === 'audio-waveform' || pianoRollMode === 'spectrogram');
|
||||
const beatTicksPerBar = Math.max(0, timeSignature.numerator - 1);
|
||||
|
||||
return (
|
||||
@@ -412,6 +414,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
onRegionLassoSelection={mainContentRegions.handleRegionLassoSelection}
|
||||
onRegionLassoCommit={mainContentRegions.handleRegionLassoCommit}
|
||||
onOpenPianoRoll={mainContentRegions.handleOpenPianoRoll}
|
||||
onOpenWaveform={mainContentRegions.handleOpenWaveform}
|
||||
onOpenSpectrogram={mainContentRegions.handleOpenSpectrogram}
|
||||
showHybridButtonForAudio={showHybridButtonForAudio}
|
||||
showHybridButtonForMidi={showHybridButtonForMidi}
|
||||
@@ -441,7 +444,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
requestedSheetMusicViewEnabled={requestedSheetMusicViewEnabled}
|
||||
pianoRollViewRequestVersion={pianoRollViewRequestVersion}
|
||||
audioRegion={(() => {
|
||||
const audioId = pianoRollMode === 'spectrogram'
|
||||
const audioId = pianoRollMode === 'audio-waveform' || pianoRollMode === 'spectrogram'
|
||||
? activeRegionId
|
||||
: pianoRollMode === 'hybrid'
|
||||
? hybridAudioRegionId
|
||||
@@ -459,7 +462,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
||||
return undefined;
|
||||
})()}
|
||||
trackId={(() => {
|
||||
const audioId = pianoRollMode === 'spectrogram'
|
||||
const audioId = pianoRollMode === 'audio-waveform' || pianoRollMode === 'spectrogram'
|
||||
? activeRegionId
|
||||
: pianoRollMode === 'hybrid'
|
||||
? hybridAudioRegionId
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import * as Tone from 'tone';
|
||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||
import { KGAudioFileStorage } from '../../core/io/KGAudioFileStorage';
|
||||
import { KGCore } from '../../core/KGCore';
|
||||
import { beatRangeToSeconds } from '../../util/globalTrackUtil';
|
||||
|
||||
interface AudioWaveformCanvasProps {
|
||||
audioRegion: KGAudioRegion;
|
||||
trackId: string;
|
||||
projectName: string;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
const LIGHT_ROW_COLOR = '#4a4a4a';
|
||||
const DARK_ROW_COLOR = '#282828';
|
||||
const BASE_BEAT_WIDTH = 40;
|
||||
|
||||
const AudioWaveformCanvas: React.FC<AudioWaveformCanvasProps> = ({
|
||||
audioRegion,
|
||||
trackId,
|
||||
projectName,
|
||||
zoom,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const audioBufferRef = useRef<AudioBuffer | null>(null);
|
||||
|
||||
const drawWaveform = useCallback((audioBuffer: AudioBuffer) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const regionStartBeat = audioRegion.getStartFromBeat();
|
||||
const regionEndBeat = regionStartBeat + audioRegion.getLength();
|
||||
const clipStartOffsetSeconds = audioRegion.getClipStartOffsetSeconds();
|
||||
const visibleDurationSeconds = Math.min(
|
||||
beatRangeToSeconds(project, regionStartBeat, regionEndBeat),
|
||||
Math.max(0, audioRegion.getAudioDurationSeconds() - clipStartOffsetSeconds),
|
||||
);
|
||||
|
||||
const zoomedBeatWidth = BASE_BEAT_WIDTH * zoom;
|
||||
const renderWidth = Math.max(1, Math.ceil(audioRegion.getLength() * zoomedBeatWidth));
|
||||
const parentHeight = canvas.parentElement?.clientHeight ?? 0;
|
||||
const canvasHeight = Math.max(160, parentHeight || canvas.clientHeight || 320);
|
||||
const centerY = canvasHeight / 2;
|
||||
const amplitudeScale = canvasHeight * 0.42;
|
||||
|
||||
canvas.width = renderWidth;
|
||||
canvas.height = canvasHeight;
|
||||
canvas.style.width = `${renderWidth}px`;
|
||||
canvas.style.height = `${canvasHeight}px`;
|
||||
|
||||
context.clearRect(0, 0, renderWidth, canvasHeight);
|
||||
context.fillStyle = DARK_ROW_COLOR;
|
||||
context.fillRect(0, 0, renderWidth, canvasHeight);
|
||||
|
||||
const channelData = audioBuffer.getChannelData(0);
|
||||
const totalSamples = channelData.length;
|
||||
const sampleRate = audioBuffer.sampleRate;
|
||||
const renderStartSample = Math.max(0, Math.min(totalSamples, Math.floor(clipStartOffsetSeconds * sampleRate)));
|
||||
const renderEndSample = Math.max(
|
||||
renderStartSample,
|
||||
Math.min(totalSamples, renderStartSample + Math.floor(visibleDurationSeconds * sampleRate)),
|
||||
);
|
||||
const renderSampleCount = renderEndSample - renderStartSample;
|
||||
|
||||
if (renderSampleCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const samplesPerPixel = Math.max(1, Math.ceil(renderSampleCount / renderWidth));
|
||||
|
||||
for (let x = 0; x < renderWidth; x++) {
|
||||
const startSample = renderStartSample + (x * samplesPerPixel);
|
||||
const endSample = Math.min(renderEndSample, startSample + samplesPerPixel);
|
||||
|
||||
let min = 1;
|
||||
let max = -1;
|
||||
for (let sampleIndex = startSample; sampleIndex < endSample; sampleIndex++) {
|
||||
const sample = channelData[sampleIndex];
|
||||
if (sample < min) min = sample;
|
||||
if (sample > max) max = sample;
|
||||
}
|
||||
|
||||
const topHeight = Math.max(1, Math.abs(max) * amplitudeScale);
|
||||
const bottomHeight = Math.max(1, Math.abs(min) * amplitudeScale);
|
||||
|
||||
context.fillStyle = LIGHT_ROW_COLOR;
|
||||
context.fillRect(x, centerY - topHeight, 1, topHeight);
|
||||
context.fillRect(x, centerY, 1, bottomHeight);
|
||||
}
|
||||
}, [audioRegion]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadAndDraw = async () => {
|
||||
try {
|
||||
let audioBuffer = KGAudioInterface.instance().getAudioBuffer(trackId, audioRegion.getAudioFileId());
|
||||
if (!audioBuffer) {
|
||||
const arrayBuffer = await KGAudioFileStorage.loadAudioFile(projectName, audioRegion.getAudioFileId());
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const audioContext = Tone.getContext().rawContext as AudioContext;
|
||||
audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
}
|
||||
|
||||
if (cancelled || !audioBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
audioBufferRef.current = audioBuffer;
|
||||
drawWaveform(audioBuffer);
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.error('AudioWaveformCanvas: failed to render waveform', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadAndDraw();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [audioRegion, drawWaveform, projectName, trackId]);
|
||||
|
||||
useEffect(() => {
|
||||
const parent = canvasRef.current?.parentElement;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (audioBufferRef.current) {
|
||||
drawWaveform(audioBufferRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(parent);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [drawWaveform]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
data-testid="audio-waveform-canvas"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: `${audioRegion.getStartFromBeat() * BASE_BEAT_WIDTH * zoom}px`,
|
||||
zIndex: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioWaveformCanvas;
|
||||
@@ -7,6 +7,7 @@ import { generatePianoGridBackground, getMatchingChordsForPitch } from '../../ut
|
||||
import type { KeySignature } from '../../core/KGProject';
|
||||
import { KGPianoRollState } from '../../core/state/KGPianoRollState';
|
||||
import SpectrogramCanvas from './SpectrogramCanvas';
|
||||
import AudioWaveformCanvas from './AudioWaveformCanvas';
|
||||
import type { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
import type { SpectrogramHeightResolution } from '../../util/spectrogramUtil';
|
||||
|
||||
@@ -35,7 +36,7 @@ interface PianoGridProps {
|
||||
spectrogramPower?: number;
|
||||
spectrogramHeightResolution?: SpectrogramHeightResolution;
|
||||
pianoRollZoom?: number;
|
||||
mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
|
||||
mode?: 'midi-edit' | 'audio-waveform' | 'spectrogram' | 'hybrid';
|
||||
onSpectrogramLoadingChange?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ const PianoGrid: React.FC<PianoGridProps> = ({
|
||||
spectrogramPower = 0.5,
|
||||
spectrogramHeightResolution = 3,
|
||||
pianoRollZoom = 1,
|
||||
mode = 'midi-edit',
|
||||
onSpectrogramLoadingChange,
|
||||
}) => {
|
||||
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
|
||||
@@ -239,7 +241,16 @@ const PianoGrid: React.FC<PianoGridProps> = ({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{/* Spectrogram layer — rendered at z-index 0, behind all highlights and notes */}
|
||||
{audioRegion && trackId && projectName && (
|
||||
{audioRegion && mode === 'audio-waveform' && trackId && projectName && (
|
||||
<AudioWaveformCanvas
|
||||
audioRegion={audioRegion}
|
||||
trackId={trackId}
|
||||
projectName={projectName}
|
||||
zoom={pianoRollZoom}
|
||||
/>
|
||||
)}
|
||||
|
||||
{audioRegion && (mode === 'spectrogram' || mode === 'hybrid') && trackId && projectName && (
|
||||
<SpectrogramCanvas
|
||||
audioRegion={audioRegion}
|
||||
trackId={trackId}
|
||||
|
||||
@@ -8,11 +8,13 @@ import { getSnappedBeatPosition } from './pianoRollSnap';
|
||||
interface PianoGridHeaderProps {
|
||||
maxBars: number;
|
||||
timeSignature?: { numerator: number; denominator: number };
|
||||
hasPianoKeys?: boolean;
|
||||
}
|
||||
|
||||
const PianoGridHeader: React.FC<PianoGridHeaderProps> = ({
|
||||
maxBars,
|
||||
timeSignature = { numerator: 4, denominator: 4 } // Default to 4/4 if not provided
|
||||
timeSignature = { numerator: 4, denominator: 4 }, // Default to 4/4 if not provided
|
||||
hasPianoKeys = true,
|
||||
}) => {
|
||||
// Get store access for playhead position updates
|
||||
const { setPlayheadPosition, requestMainContentScroll } = useProjectStore();
|
||||
@@ -29,9 +31,11 @@ const PianoGridHeader: React.FC<PianoGridHeaderProps> = ({
|
||||
const relativeX = clientX - rect.left;
|
||||
|
||||
// Account for the piano keys width offset
|
||||
const pianoKeysWidth = parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
|
||||
) || 60;
|
||||
const pianoKeysWidth = hasPianoKeys
|
||||
? (parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
|
||||
) || 60)
|
||||
: 0;
|
||||
|
||||
const adjustedX = relativeX - pianoKeysWidth;
|
||||
|
||||
@@ -138,7 +142,7 @@ const PianoGridHeader: React.FC<PianoGridHeaderProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="piano-grid-header"
|
||||
className={`piano-grid-header${hasPianoKeys ? '' : ' no-piano-keys'}`}
|
||||
ref={headerElementRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handlePianoGridHeaderClick}
|
||||
|
||||
@@ -181,6 +181,13 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.piano-roll-toolbar .spectrogram-view-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.piano-roll-toolbar .tool-button:hover {
|
||||
--toolbar-button-bg: #3a3a3a;
|
||||
--toolbar-button-fg: #e0e0e0;
|
||||
@@ -467,6 +474,11 @@
|
||||
cursor: url("data:image/svg+xml,%3csvg width='16' height='16' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M4 6h8l-4 4-4-4z' fill='%23e0e0e0'/%3e%3c/svg%3e") 8 8, pointer;
|
||||
}
|
||||
|
||||
.piano-grid-header.no-piano-keys {
|
||||
width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width));
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.piano-bar-number {
|
||||
border-left: 1px solid #3a3a3a;
|
||||
padding-left: 10px;
|
||||
@@ -484,6 +496,11 @@
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.piano-roll-body.audio-waveform-body {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sheet-music-view {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -616,6 +633,16 @@
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.piano-roll-body.audio-waveform-body .piano-grid-container {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.piano-roll-body.audio-waveform-body .piano-grid {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.piano-octave {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -67,7 +67,7 @@ interface PianoRollProps {
|
||||
regionId: string | null;
|
||||
initialPosition?: { x: number; y: number };
|
||||
initialSize?: { width: number; height: number };
|
||||
mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
|
||||
mode?: 'midi-edit' | 'audio-waveform' | 'spectrogram' | 'hybrid';
|
||||
requestedSheetMusicViewEnabled?: boolean;
|
||||
pianoRollViewRequestVersion?: number;
|
||||
audioRegion?: KGAudioRegion;
|
||||
@@ -87,8 +87,11 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
trackId,
|
||||
projectName,
|
||||
}) => {
|
||||
const isSpectrogram = mode === 'spectrogram';
|
||||
const isHybrid = mode === 'hybrid';
|
||||
const [currentMode, setCurrentMode] = useState<'midi-edit' | 'audio-waveform' | 'spectrogram' | 'hybrid'>(mode);
|
||||
const isSpectrogram = currentMode === 'spectrogram';
|
||||
const isAudioWaveform = currentMode === 'audio-waveform';
|
||||
const isAudioOnly = isAudioWaveform || isSpectrogram;
|
||||
const isHybrid = currentMode === 'hybrid';
|
||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showEventListPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, automationRedrawVersion, refreshProjectState, setBpm } = useProjectStore();
|
||||
|
||||
// Tool state for piano roll
|
||||
@@ -136,6 +139,10 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [activeRegion, setActiveRegion] = useState<KGMidiRegion | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentMode(mode);
|
||||
}, [mode]);
|
||||
|
||||
const selectedNotes = useMemo(
|
||||
() => activeRegion?.getNotes().filter(n => selectedNoteIds.includes(n.getId())) ?? [],
|
||||
[activeRegion, selectedNoteIds]
|
||||
@@ -285,7 +292,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
}, []); // Empty dependency array means this runs once on mount
|
||||
|
||||
useEffect(() => {
|
||||
if (isSpectrogram) {
|
||||
if (isAudioOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,7 +319,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
KGPianoRollState.instance().setSheetMusicViewEnabled(requestedSheetMusicViewEnabled);
|
||||
}, [
|
||||
activeRegion,
|
||||
isSpectrogram,
|
||||
isAudioOnly,
|
||||
pianoRollViewRequestVersion,
|
||||
playheadPosition,
|
||||
requestedSheetMusicViewEnabled,
|
||||
@@ -508,7 +515,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
let detectedChords: DetectedAudioChord[] | DetectedMidiChord[];
|
||||
if (audioRegion) {
|
||||
if (!projectName || !trackId) {
|
||||
await showAlert('Open an audio region in spectrogram mode before detecting chords.');
|
||||
await showAlert('Open an audio region in spectrogram mode before detecting chords.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1081,6 +1088,14 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
});
|
||||
}, [activeRegion, playheadPosition, sheetMusicTrackScopeEnabled, sheetMusicViewEnabled]);
|
||||
|
||||
const handleAudioSpectrogramToggle = useCallback(() => {
|
||||
if (isHybrid || !audioRegion) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentMode(current => current === 'spectrogram' ? 'audio-waveform' : 'spectrogram');
|
||||
}, [audioRegion, isHybrid]);
|
||||
|
||||
const handleSheetMeasureMetricsChange = useCallback((metrics: SheetMeasureMetric[]) => {
|
||||
setSheetMeasureMetrics((current) => {
|
||||
if (
|
||||
@@ -1106,6 +1121,11 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAudioWaveform) {
|
||||
container.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const keyHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
|
||||
const c4Position = 4 * 12 * keyHeight;
|
||||
const totalHeight = 8 * 12 * keyHeight;
|
||||
@@ -1113,7 +1133,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const scrollPosition = (totalHeight - c4Position) - (viewportHeight / 2);
|
||||
|
||||
container.scrollTop = Math.max(0, scrollPosition);
|
||||
}, []);
|
||||
}, [isAudioWaveform]);
|
||||
|
||||
// Calculate C4 position and scroll to it when piano roll opens
|
||||
useEffect(() => {
|
||||
@@ -1470,7 +1490,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
|
||||
// Get the title for the piano roll based on the active region
|
||||
const getPianoRollTitle = () => {
|
||||
if (isSpectrogram) return audioRegion ? `SPECTROGRAM — ${audioRegion.getName()}` : 'SPECTROGRAM';
|
||||
if (currentMode === 'spectrogram') return audioRegion ? `SPECTROGRAM — ${audioRegion.getName()}` : 'SPECTROGRAM';
|
||||
if (currentMode === 'audio-waveform') return audioRegion ? `WAVEFORM — ${audioRegion.getName()}` : 'WAVEFORM';
|
||||
if (isHybrid) {
|
||||
const midiName = activeRegion?.getName() ?? 'MIDI';
|
||||
const audioName = audioRegion?.getName() ?? 'Audio';
|
||||
@@ -1523,6 +1544,10 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
sheetQuantization={sheetQuantization}
|
||||
onSheetQuantizationChange={handleSheetQuantizationChange}
|
||||
sheetQuantizationOptions={getSheetQuantizationOptions()}
|
||||
showAudioSpectrogramToggle={!!audioRegion && !isHybrid}
|
||||
audioSpectrogramEnabled={currentMode === 'spectrogram'}
|
||||
onAudioSpectrogramToggle={handleAudioSpectrogramToggle}
|
||||
sheetMusicToggleDisabled={!activeRegion}
|
||||
activeTool={activeTool}
|
||||
onToolSelect={handleToolSelect}
|
||||
quantPosition={quantPosition}
|
||||
@@ -1535,14 +1560,14 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
chordGuide={chordGuide}
|
||||
onChordGuideChange={handleChordGuideSelect}
|
||||
blinkButton={blinkButton}
|
||||
mode={mode}
|
||||
mode={currentMode}
|
||||
thresholdDb={spectrogramThresholdDb}
|
||||
onThresholdChange={setSpectrogramThresholdDb}
|
||||
power={spectrogramPower}
|
||||
onPowerChange={setSpectrogramPower}
|
||||
zoom={pianoRollZoom}
|
||||
onZoomChange={handleZoomChange}
|
||||
showAutomationControls={!isSpectrogram}
|
||||
showAutomationControls={!isAudioOnly}
|
||||
automationEnabled={automationEnabled}
|
||||
automationType={automationType}
|
||||
onAutomationToggle={handleAutomationToggle}
|
||||
@@ -1553,7 +1578,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
detectingTempo={isDetectingTempo}
|
||||
/>
|
||||
|
||||
<NoteAttributeBar selectedNotes={selectedNotes} isSpectrogram={isSpectrogram} activeRegion={activeRegion} />
|
||||
<NoteAttributeBar selectedNotes={selectedNotes} isSpectrogram={isAudioOnly} activeRegion={activeRegion} />
|
||||
|
||||
<PianoRollContent
|
||||
contentRef={pianoRollContentRef}
|
||||
@@ -1569,7 +1594,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
selectedMode={selectedMode}
|
||||
keySignature={keySignature}
|
||||
chordGuide={chordGuide}
|
||||
mode={mode}
|
||||
mode={currentMode}
|
||||
audioRegion={audioRegion}
|
||||
trackId={trackId}
|
||||
projectName={projectName}
|
||||
|
||||
@@ -46,7 +46,13 @@ vi.mock('../../hooks/useNoteSelection', () => ({
|
||||
|
||||
vi.mock('./PianoGridHeader', () => ({ default: () => <div data-testid="piano-grid-header" /> }));
|
||||
vi.mock('./PianoKeys', () => ({ default: () => <div data-testid="piano-keys" /> }));
|
||||
vi.mock('./PianoGrid', () => ({ default: ({ children }: { children?: React.ReactNode }) => <div data-testid="piano-grid">{children}</div> }));
|
||||
const pianoGridSpy = vi.fn();
|
||||
vi.mock('./PianoGrid', () => ({
|
||||
default: (props: { children?: React.ReactNode; mode?: string }) => {
|
||||
pianoGridSpy(props);
|
||||
return <div data-testid="piano-grid">{props.children}</div>;
|
||||
},
|
||||
}));
|
||||
vi.mock('./PianoNote', () => ({ default: () => <div data-testid="piano-note" /> }));
|
||||
vi.mock('./PianoRollAutomationLane', () => ({ default: () => <div data-testid="automation-lane" /> }));
|
||||
const sheetMusicViewSpy = vi.fn();
|
||||
@@ -76,6 +82,7 @@ describe('PianoRollContent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
sheetMusicViewSpy.mockClear();
|
||||
pianoGridSpy.mockClear();
|
||||
});
|
||||
|
||||
it('keeps the single-pane layout when automation is disabled', () => {
|
||||
@@ -120,6 +127,21 @@ describe('PianoRollContent', () => {
|
||||
expect(screen.queryByTestId('automation-lane')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('suppresses the automation lane in waveform mode and forwards the explicit mode', () => {
|
||||
render(
|
||||
<PianoRollContent
|
||||
{...baseProps}
|
||||
mode="audio-waveform"
|
||||
automationEnabled={true}
|
||||
automationType="cc-7"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('piano-roll-content-single')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('automation-lane')).not.toBeInTheDocument();
|
||||
expect(pianoGridSpy).toHaveBeenCalledWith(expect.objectContaining({ mode: 'audio-waveform' }));
|
||||
});
|
||||
|
||||
it('shows an overlay message when one is supplied by the parent panel', () => {
|
||||
render(
|
||||
<PianoRollContent
|
||||
|
||||
@@ -37,7 +37,7 @@ interface PianoRollContentProps {
|
||||
selectedMode: string;
|
||||
keySignature: KeySignature;
|
||||
chordGuide: string;
|
||||
mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
|
||||
mode?: 'midi-edit' | 'audio-waveform' | 'spectrogram' | 'hybrid';
|
||||
audioRegion?: KGAudioRegion;
|
||||
trackId?: string;
|
||||
projectName?: string;
|
||||
@@ -94,8 +94,9 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
overlayMessage = null,
|
||||
overlayProgressPercent = null,
|
||||
}) => {
|
||||
const isAudioView = mode === 'audio-waveform' || mode === 'spectrogram';
|
||||
const isSpectrogram = mode === 'spectrogram';
|
||||
const showAutomationLane = automationEnabled && !isSpectrogram && !sheetMusicViewEnabled;
|
||||
const showAutomationLane = automationEnabled && !isAudioView && !sheetMusicViewEnabled;
|
||||
const [spectrogramLoading, setSpectrogramLoading] = useState(false);
|
||||
const [noteScrollLeft, setNoteScrollLeft] = useState(0);
|
||||
const handleSpectrogramLoadingChange = useCallback((loading: boolean) => {
|
||||
@@ -174,7 +175,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
|
||||
// Combined click handler for both pointer and pencil modes
|
||||
const handleCombinedClick = (e: React.MouseEvent) => {
|
||||
if (isSpectrogram) return;
|
||||
if (isAudioView) return;
|
||||
handleBackgroundClick(e);
|
||||
handleGridClick(e);
|
||||
};
|
||||
@@ -212,7 +213,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
|
||||
// Memoize the notes rendering to prevent unnecessary recalculations
|
||||
const memoizedNotes = useMemo(() => {
|
||||
if (isSpectrogram || sheetMusicViewEnabled || !activeRegion) return null;
|
||||
if (isAudioView || sheetMusicViewEnabled || !activeRegion) return null;
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Rendering notes for region: ${activeRegion.getId()}`);
|
||||
@@ -289,7 +290,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [mode, activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks, sheetMusicViewEnabled]);
|
||||
}, [isAudioView, activeRegion, noteUpdateCounter, resizingNoteId, draggingNoteId, tempNoteStyles, selectedNoteIds, selectionBoxRender, tracks, sheetMusicViewEnabled]);
|
||||
|
||||
const recordingNoteOverlays = useMemo(() => {
|
||||
if (!isRecording || !activeRegion || recordingNotes.length === 0) return null;
|
||||
@@ -341,10 +342,14 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
onScroll={(event) => setNoteScrollLeft(event.currentTarget.scrollLeft)}
|
||||
>
|
||||
{!sheetMusicViewEnabled && (
|
||||
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
|
||||
<PianoGridHeader
|
||||
maxBars={maxBars}
|
||||
timeSignature={timeSignature}
|
||||
hasPianoKeys={mode !== 'audio-waveform'}
|
||||
/>
|
||||
)}
|
||||
<div className={`piano-roll-body ${sheetMusicViewEnabled ? 'sheet-music-body' : ''}`}>
|
||||
{!sheetMusicViewEnabled && <PianoKeys activeRegion={activeRegion} />}
|
||||
<div className={`piano-roll-body ${sheetMusicViewEnabled ? 'sheet-music-body' : ''} ${mode === 'audio-waveform' ? 'audio-waveform-body' : ''}`}>
|
||||
{!sheetMusicViewEnabled && mode !== 'audio-waveform' && <PianoKeys activeRegion={activeRegion} />}
|
||||
{sheetMusicViewEnabled && activeRegion && sheetQuantization ? (
|
||||
<SheetMusicView
|
||||
activeRegion={activeRegion}
|
||||
@@ -363,10 +368,10 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
<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}
|
||||
onClick={isAudioView ? () => {} : handleCombinedClick}
|
||||
onMouseDown={isAudioView ? () => {} : handleBackgroundMouseDown}
|
||||
isBoxSelecting={isAudioView ? false : isBoxSelectingRef.current}
|
||||
selectionBox={isAudioView ? { startX: 0, startY: 0, endX: 0, endY: 0 } : selectionBoxRef.current}
|
||||
regionStartBeat={activeRegion?.getStartFromBeat() || 0}
|
||||
selectedMode={selectedMode}
|
||||
keySignature={keySignature}
|
||||
@@ -379,10 +384,11 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
spectrogramPower={spectrogramPower}
|
||||
spectrogramHeightResolution={spectrogramHeightResolution}
|
||||
pianoRollZoom={pianoRollZoom}
|
||||
mode={mode}
|
||||
onSpectrogramLoadingChange={handleSpectrogramLoadingChange}
|
||||
>
|
||||
{memoizedNotes}
|
||||
{!isSpectrogram && recordingNoteOverlays}
|
||||
{!isAudioView && recordingNoteOverlays}
|
||||
</PianoGrid>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,37 @@ describe('PianoRollToolbar', () => {
|
||||
expect(screen.queryByRole('button', { name: /Pitch Bend/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the spectrogram toggle for pure audio waveform mode and toggles it', () => {
|
||||
const onAudioSpectrogramToggle = vi.fn();
|
||||
|
||||
render(
|
||||
<PianoRollToolbar
|
||||
{...baseProps}
|
||||
mode="audio-waveform"
|
||||
showAudioSpectrogramToggle={true}
|
||||
audioSpectrogramEnabled={false}
|
||||
onAudioSpectrogramToggle={onAudioSpectrogramToggle}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Spectrogram View' }));
|
||||
|
||||
expect(onAudioSpectrogramToggle).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole('button', { name: 'Spectrogram View' }).className).not.toContain('active');
|
||||
});
|
||||
|
||||
it('hides the spectrogram toggle in hybrid mode', () => {
|
||||
render(
|
||||
<PianoRollToolbar
|
||||
{...baseProps}
|
||||
mode="hybrid"
|
||||
showAudioSpectrogramToggle={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Spectrogram View' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the detect chords action in spectrogram mode and triggers it', () => {
|
||||
const onDetectChords = vi.fn();
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ const POWER_OPTIONS = [
|
||||
];
|
||||
|
||||
interface PianoRollToolbarProps {
|
||||
showAudioSpectrogramToggle?: boolean;
|
||||
audioSpectrogramEnabled?: boolean;
|
||||
onAudioSpectrogramToggle?: () => void;
|
||||
sheetMusicToggleDisabled?: boolean;
|
||||
sheetMusicViewEnabled?: boolean;
|
||||
onSheetMusicViewToggle?: () => void;
|
||||
sheetMusicTrackScopeEnabled?: boolean;
|
||||
@@ -36,7 +40,7 @@ interface PianoRollToolbarProps {
|
||||
chordGuide: string;
|
||||
onChordGuideChange: (value: string) => void;
|
||||
blinkButton?: string | null;
|
||||
mode?: 'midi-edit' | 'spectrogram' | 'hybrid';
|
||||
mode?: 'midi-edit' | 'audio-waveform' | 'spectrogram' | 'hybrid';
|
||||
thresholdDb?: number;
|
||||
onThresholdChange?: (db: number) => void;
|
||||
power?: number;
|
||||
@@ -55,6 +59,10 @@ interface PianoRollToolbarProps {
|
||||
}
|
||||
|
||||
const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
showAudioSpectrogramToggle = false,
|
||||
audioSpectrogramEnabled = false,
|
||||
onAudioSpectrogramToggle,
|
||||
sheetMusicToggleDisabled = false,
|
||||
sheetMusicViewEnabled = false,
|
||||
onSheetMusicViewToggle,
|
||||
sheetMusicTrackScopeEnabled = false,
|
||||
@@ -91,7 +99,9 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
onDetectTempo,
|
||||
detectingTempo = false,
|
||||
}) => {
|
||||
const showMidiControls = mode !== 'spectrogram' && !sheetMusicViewEnabled; // midi-edit and hybrid
|
||||
const showMidiControls = mode !== 'spectrogram' && mode !== 'audio-waveform' && !sheetMusicViewEnabled;
|
||||
const showAudioOnlyControls = mode === 'audio-waveform' && !sheetMusicViewEnabled;
|
||||
const showSpectrogramOnlyControls = mode === 'spectrogram' && !sheetMusicViewEnabled;
|
||||
const showSpecControls = !sheetMusicViewEnabled && (mode === 'spectrogram' || mode === 'hybrid');
|
||||
const showSpecMenu = !sheetMusicViewEnabled && (!!onDetectChords || !!onDetectTempo);
|
||||
|
||||
@@ -123,18 +133,39 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showMoreMenu]);
|
||||
|
||||
const spectrogramToggleButton = showAudioSpectrogramToggle ? (
|
||||
<button
|
||||
className={`tool-button icon-only sheet-mode-toggle ${audioSpectrogramEnabled ? 'active' : ''}`}
|
||||
onClick={() => onAudioSpectrogramToggle?.()}
|
||||
title="Spectrogram View"
|
||||
aria-label="Spectrogram View"
|
||||
>
|
||||
<svg className="spectrogram-view-icon" width="12" height="12" viewBox="0 0 10 10" fill="currentColor">
|
||||
<rect x="3" y="0.5" width="6.5" height="2.5" rx="0.4" />
|
||||
<rect x="1.5" y="3.75" width="6.5" height="2.5" rx="0.4" />
|
||||
<rect x="0" y="7" width="6.5" height="2.5" rx="0.4" />
|
||||
</svg>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const sheetMusicToggleButton = (
|
||||
<button
|
||||
className={`tool-button sheet-mode-toggle ${sheetMusicViewEnabled ? 'active' : ''}`}
|
||||
onClick={() => onSheetMusicViewToggle?.()}
|
||||
title="Sheet Music View"
|
||||
aria-label="Sheet Music View"
|
||||
disabled={sheetMusicToggleDisabled}
|
||||
>
|
||||
♬
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="piano-roll-toolbar">
|
||||
{showMidiControls && (
|
||||
<div className="toolbar-left">
|
||||
<button
|
||||
className={`tool-button sheet-mode-toggle ${sheetMusicViewEnabled ? 'active' : ''}`}
|
||||
onClick={() => onSheetMusicViewToggle?.()}
|
||||
title="Sheet Music View"
|
||||
aria-label="Sheet Music View"
|
||||
>
|
||||
♬
|
||||
</button>
|
||||
{spectrogramToggleButton}
|
||||
{sheetMusicToggleButton}
|
||||
<button
|
||||
className={`tool-button ${activeTool === 'pointer' ? 'active' : ''}`}
|
||||
onClick={() => onToolSelect('pointer')}
|
||||
@@ -193,16 +224,22 @@ const PianoRollToolbar: React.FC<PianoRollToolbarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAudioOnlyControls && (
|
||||
<div className="toolbar-left">
|
||||
{spectrogramToggleButton}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSpectrogramOnlyControls && (
|
||||
<div className="toolbar-left">
|
||||
{spectrogramToggleButton}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sheetMusicViewEnabled && (
|
||||
<div className="toolbar-left">
|
||||
<button
|
||||
className={`tool-button sheet-mode-toggle ${sheetMusicViewEnabled ? 'active' : ''}`}
|
||||
onClick={() => onSheetMusicViewToggle?.()}
|
||||
title="Sheet Music View"
|
||||
aria-label="Sheet Music View"
|
||||
>
|
||||
♬
|
||||
</button>
|
||||
{spectrogramToggleButton}
|
||||
{sheetMusicToggleButton}
|
||||
{mode !== 'spectrogram' && (
|
||||
<button
|
||||
className={`tool-button icon-only sheet-track-scope-toggle ${sheetMusicTrackScopeEnabled ? 'active' : ''}`}
|
||||
|
||||
@@ -151,10 +151,6 @@
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.region-waveform-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.region-waveform-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import RegionItem from './RegionItem';
|
||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
||||
|
||||
vi.mock('../../stores/projectStore', () => ({
|
||||
@@ -191,4 +192,24 @@ describe('RegionItem', () => {
|
||||
width: '120px',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the waveform button for audio regions and triggers the waveform open callback', () => {
|
||||
const onOpenWaveform = vi.fn();
|
||||
const audioRegion = new KGAudioRegion('audio-1', 'track-2', 1, 'Audio Region', 0, 4);
|
||||
|
||||
render(
|
||||
<RegionItem
|
||||
id="audio-1"
|
||||
name="Audio Region"
|
||||
style={{ left: '0px', width: '120px', position: 'absolute' }}
|
||||
onClick={vi.fn()}
|
||||
onOpenWaveform={onOpenWaveform}
|
||||
audioRegion={audioRegion}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View waveform' }));
|
||||
|
||||
expect(onOpenWaveform).toHaveBeenCalledWith('audio-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ interface RegionItemProps {
|
||||
onClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||
// Explicit open piano roll action from header pencil icon
|
||||
onOpenPianoRoll?: (regionId: string) => void;
|
||||
onOpenWaveform?: (regionId: string) => void;
|
||||
// Open spectrogram viewer for audio regions
|
||||
onOpenSpectrogram?: (regionId: string) => void;
|
||||
// Enter hybrid mode (show + when piano roll is open with the opposite region type selected)
|
||||
@@ -73,6 +74,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
onDragEnd,
|
||||
onClick,
|
||||
onOpenPianoRoll,
|
||||
onOpenWaveform,
|
||||
onOpenSpectrogram,
|
||||
showHybridButton,
|
||||
onOpenHybrid,
|
||||
@@ -732,6 +734,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenWaveform?.(id);
|
||||
}}
|
||||
aria-label="View waveform"
|
||||
>
|
||||
|
||||
@@ -47,6 +47,7 @@ interface TrackGridItemProps {
|
||||
onRegionFineMoveEnd?: (regionId: string, deltaInBars: number) => void;
|
||||
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||
onOpenPianoRoll?: (regionId: string) => void;
|
||||
onOpenWaveform?: (regionId: string) => void;
|
||||
onOpenSpectrogram?: (regionId: string) => void;
|
||||
showHybridButtonForAudio?: boolean;
|
||||
showHybridButtonForMidi?: boolean;
|
||||
@@ -78,6 +79,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
onRegionFineMoveEnd,
|
||||
onRegionClick,
|
||||
onOpenPianoRoll,
|
||||
onOpenWaveform,
|
||||
onOpenSpectrogram,
|
||||
showHybridButtonForAudio,
|
||||
showHybridButtonForMidi,
|
||||
@@ -802,6 +804,9 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
onRegionClick(regionId, { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||
}
|
||||
}}
|
||||
onOpenWaveform={audioRegion ? (regionId) => {
|
||||
onOpenWaveform?.(regionId);
|
||||
} : undefined}
|
||||
onOpenSpectrogram={audioRegion ? (regionId) => {
|
||||
onOpenSpectrogram?.(regionId);
|
||||
} : undefined}
|
||||
|
||||
@@ -48,6 +48,7 @@ interface TrackGridPanelProps {
|
||||
onRegionLassoSelection?: (regionIds: string[], options: RegionClickOptions) => void;
|
||||
onRegionLassoCommit?: () => void;
|
||||
onOpenPianoRoll?: (regionId: string) => void;
|
||||
onOpenWaveform?: (regionId: string) => void;
|
||||
onOpenSpectrogram?: (regionId: string) => void;
|
||||
showHybridButtonForAudio?: boolean;
|
||||
showHybridButtonForMidi?: boolean;
|
||||
@@ -70,6 +71,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
onRegionLassoSelection,
|
||||
onRegionLassoCommit,
|
||||
onOpenPianoRoll,
|
||||
onOpenWaveform,
|
||||
onOpenSpectrogram,
|
||||
showHybridButtonForAudio,
|
||||
showHybridButtonForMidi,
|
||||
@@ -1038,6 +1040,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
onRegionFineMoveEnd={handleRegionFineMoveEnd}
|
||||
onRegionClick={handleRegionClick}
|
||||
onOpenPianoRoll={onOpenPianoRoll}
|
||||
onOpenWaveform={onOpenWaveform}
|
||||
onOpenSpectrogram={onOpenSpectrogram}
|
||||
showHybridButtonForAudio={showHybridButtonForAudio}
|
||||
showHybridButtonForMidi={showHybridButtonForMidi}
|
||||
|
||||
Reference in New Issue
Block a user