diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index ef27e98..1003b19 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -44,6 +44,8 @@ const MainContent: React.FC = ({ pianoRollMode, openMidiPianoRoll, openSpectrogramViewer, + openHybridMode, + hybridAudioRegionId, addTrack, addAudioTrack, projectName, @@ -552,6 +554,19 @@ const MainContent: React.FC = ({ openSpectrogramViewer(regionId); }; + // Handle hybrid mode open (+ button clicked on opposite-type region) + const handleOpenHybrid = (regionId: string) => { + if (pianoRollMode === 'midi-edit' && activeRegionId) { + openHybridMode(activeRegionId, regionId); + } else if (pianoRollMode === 'spectrogram' && activeRegionId) { + openHybridMode(regionId, activeRegionId); + } + }; + + // + button is visible only when piano roll is open and mode is not hybrid + const showHybridButtonForAudio = showPianoRoll && pianoRollMode === 'midi-edit'; + const showHybridButtonForMidi = showPianoRoll && pianoRollMode === 'spectrogram'; + // Handle piano roll close const handlePianoRollClose = () => { setShowPianoRoll(false); @@ -844,6 +859,9 @@ const MainContent: React.FC = ({ onRegionClick={handleRegionClick} onOpenPianoRoll={handleOpenPianoRoll} onOpenSpectrogram={handleOpenSpectrogram} + showHybridButtonForAudio={showHybridButtonForAudio} + showHybridButtonForMidi={showHybridButtonForMidi} + onOpenHybrid={handleOpenHybrid} onExternalDropComplete={handleExternalDropComplete} /> @@ -855,26 +873,33 @@ const MainContent: React.FC = ({ onClose={handlePianoRollClose} regionId={activeRegionId} mode={pianoRollMode} - audioRegion={pianoRollMode === 'spectrogram' && activeRegionId - ? (() => { - for (const track of tracks) { - const region = track.getRegions().find(r => r.getId() === activeRegionId); - if (region && region.getCurrentType() === 'KGAudioRegion') { - return region as unknown as KGAudioRegion; - } - } - return undefined; - })() - : undefined} - trackId={pianoRollMode === 'spectrogram' && activeRegionId - ? (() => { - for (const track of tracks) { - const region = track.getRegions().find(r => r.getId() === activeRegionId); - if (region) return track.getId().toString(); - } - return undefined; - })() - : undefined} + audioRegion={(() => { + // spectrogram mode: audio region IS the activeRegionId + // hybrid mode: audio region is hybridAudioRegionId + const audioId = pianoRollMode === 'spectrogram' ? activeRegionId + : pianoRollMode === 'hybrid' ? hybridAudioRegionId + : null; + if (!audioId) return undefined; + for (const track of tracks) { + const region = track.getRegions().find(r => r.getId() === audioId); + if (region && region.getCurrentType() === 'KGAudioRegion') { + return region as unknown as KGAudioRegion; + } + } + return undefined; + })()} + trackId={(() => { + const audioId = pianoRollMode === 'spectrogram' ? activeRegionId + : pianoRollMode === 'hybrid' ? hybridAudioRegionId + : null; + if (!audioId) return undefined; + for (const track of tracks) { + if (track.getRegions().some(r => r.getId() === audioId)) { + return track.getId().toString(); + } + } + return undefined; + })()} projectName={savedProjectName} />, document.body diff --git a/src/components/piano-roll/PianoGrid.tsx b/src/components/piano-roll/PianoGrid.tsx index b21d2c3..e49d197 100644 --- a/src/components/piano-roll/PianoGrid.tsx +++ b/src/components/piano-roll/PianoGrid.tsx @@ -32,6 +32,7 @@ interface PianoGridProps { bpm?: number; spectrogramThresholdDb?: number; spectrogramPower?: number; + mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; } interface CursorPosition { diff --git a/src/components/piano-roll/PianoRoll.css b/src/components/piano-roll/PianoRoll.css index d4929b7..58c8fdb 100644 --- a/src/components/piano-roll/PianoRoll.css +++ b/src/components/piano-roll/PianoRoll.css @@ -77,10 +77,17 @@ /* Ensure toolbar and its dropdowns appear above piano roll content */ } +.piano-roll-toolbar .tool-button { + width: 20px; + height: 20px; +} + /* Override pointer-events for piano roll toolbar sections */ .piano-roll-toolbar .toolbar-left, .piano-roll-toolbar .toolbar-right { pointer-events: auto; + /* max-width: 70%; */ + width: auto; } .piano-roll-toolbar .quant-button { @@ -101,6 +108,7 @@ display: flex; align-items: center; gap: 6px; + margin-left: 5px; } .spectrogram-control-label { @@ -121,7 +129,7 @@ .spectrogram-threshold-value { font-size: 10px; color: #e0e0e0; - min-width: 44px; + min-width: 30px; text-align: right; white-space: nowrap; } @@ -131,7 +139,7 @@ text-align: center; font-size: 14px; color: #e0e0e0; - text-transform: uppercase; + /* text-transform: uppercase; */ cursor: pointer; padding: 5px; border-radius: 3px; diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index 990aed1..a63af15 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -23,7 +23,7 @@ interface PianoRollProps { regionId: string | null; initialPosition?: { x: number; y: number }; initialSize?: { width: number; height: number }; - mode?: 'midi-edit' | 'spectrogram'; + mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; audioRegion?: KGAudioRegion; trackId?: string; projectName?: string; @@ -40,6 +40,7 @@ const PianoRoll: React.FC = ({ projectName, }) => { const isSpectrogram = mode === 'spectrogram'; + const isHybrid = mode === 'hybrid'; const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm } = useProjectStore(); // Tool state for piano roll @@ -870,6 +871,11 @@ const PianoRoll: React.FC = ({ // Get the title for the piano roll based on the active region const getPianoRollTitle = () => { if (isSpectrogram) return audioRegion ? `SPECTROGRAM — ${audioRegion.getName()}` : 'SPECTROGRAM'; + if (isHybrid) { + const midiName = activeRegion?.getName() ?? 'MIDI'; + const audioName = audioRegion?.getName() ?? 'Audio'; + return `${midiName} + ${audioName}`; + } if (!activeRegion) return "EDIT NOTE CLIP"; // Calculate the bar and beat position of the region diff --git a/src/components/piano-roll/PianoRollContent.tsx b/src/components/piano-roll/PianoRollContent.tsx index 5f47117..24f7c56 100644 --- a/src/components/piano-roll/PianoRollContent.tsx +++ b/src/components/piano-roll/PianoRollContent.tsx @@ -27,7 +27,7 @@ interface PianoRollContentProps { selectedMode: string; keySignature: KeySignature; chordGuide: string; - mode?: 'midi-edit' | 'spectrogram'; + mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; audioRegion?: KGAudioRegion; trackId?: string; projectName?: string; diff --git a/src/components/piano-roll/PianoRollToolbar.tsx b/src/components/piano-roll/PianoRollToolbar.tsx index 4df21b6..d0746e1 100644 --- a/src/components/piano-roll/PianoRollToolbar.tsx +++ b/src/components/piano-roll/PianoRollToolbar.tsx @@ -24,7 +24,7 @@ interface PianoRollToolbarProps { chordGuide: string; onChordGuideChange: (value: string) => void; blinkButton?: string | null; - mode?: 'midi-edit' | 'spectrogram'; + mode?: 'midi-edit' | 'spectrogram' | 'hybrid'; thresholdDb?: number; onThresholdChange?: (db: number) => void; power?: number; @@ -51,11 +51,27 @@ const PianoRollToolbar: React.FC = ({ onPowerChange, }) => { const isSpectrogram = mode === 'spectrogram'; + const showMidiControls = mode !== 'spectrogram'; // midi-edit and hybrid + const showSpecControls = mode === 'spectrogram' || mode === 'hybrid'; return (
- {!isSpectrogram && ( + {showMidiControls && (
+ + ({ label: data.name, value: id }))} value={selectedMode} @@ -80,27 +96,8 @@ const PianoRollToolbar: React.FC = ({
)} - {!isSpectrogram && ( -
- - -
- )} -
- {!isSpectrogram && ( + {showMidiControls && ( <> = ({ )} - {isSpectrogram && ( + {showSpecControls && (
Floor 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) + showHybridButton?: boolean; + onOpenHybrid?: (regionId: string) => void; // MIDI region data for rendering notes midiRegion?: KGMidiRegion; // Audio region data for rendering waveform @@ -53,6 +56,8 @@ const RegionItem: React.FC = ({ onClick, onOpenPianoRoll, onOpenSpectrogram, + showHybridButton, + onOpenHybrid, midiRegion, audioRegion, audioBuffer @@ -64,7 +69,7 @@ const RegionItem: React.FC = ({ const [resizeEdge, setResizeEdge] = useState('none'); const [isResizing, setIsResizing] = useState(false); const [isDragging, setIsDragging] = useState(false); - const initialMousePosRef = useRef<{x: number, y: number}>({x: 0, y: 0}); + const initialMousePosRef = useRef<{ x: number, y: number }>({ x: 0, y: 0 }); // Use refs to track states for immediate access const isResizingRef = useRef(false); const isDraggingRef = useRef(false); @@ -600,6 +605,26 @@ const RegionItem: React.FC = ({ )} + {showHybridButton && ( + + )}
diff --git a/src/components/track/TrackGridItem.tsx b/src/components/track/TrackGridItem.tsx index 8f322d3..20e891c 100644 --- a/src/components/track/TrackGridItem.tsx +++ b/src/components/track/TrackGridItem.tsx @@ -27,6 +27,9 @@ interface TrackGridItemProps { onRegionClick?: (regionId: string) => void; onOpenPianoRoll?: (regionId: string) => void; onOpenSpectrogram?: (regionId: string) => void; + showHybridButtonForAudio?: boolean; + showHybridButtonForMidi?: boolean; + onOpenHybrid?: (regionId: string) => void; allTracks?: KGTrack[]; // Added to access all tracks for drag operations onKGOneClipDrop?: (e: React.DragEvent, trackIndex: number) => void; } @@ -49,6 +52,9 @@ const TrackGridItem: React.FC = ({ onRegionClick, onOpenPianoRoll, onOpenSpectrogram, + showHybridButtonForAudio, + showHybridButtonForMidi, + onOpenHybrid, allTracks, onKGOneClipDrop, }) => { @@ -571,6 +577,8 @@ const TrackGridItem: React.FC = ({ onOpenSpectrogram={audioRegion ? (regionId) => { onOpenSpectrogram?.(regionId); } : undefined} + showHybridButton={audioRegion ? showHybridButtonForAudio : showHybridButtonForMidi} + onOpenHybrid={onOpenHybrid} midiRegion={midiRegion} audioRegion={audioRegion} audioBuffer={audioBuffer} diff --git a/src/components/track/TrackGridPanel.tsx b/src/components/track/TrackGridPanel.tsx index 875895e..43718bc 100644 --- a/src/components/track/TrackGridPanel.tsx +++ b/src/components/track/TrackGridPanel.tsx @@ -31,6 +31,9 @@ interface TrackGridPanelProps { onRegionClick?: (regionId: string) => void; onOpenPianoRoll?: (regionId: string) => void; onOpenSpectrogram?: (regionId: string) => void; + showHybridButtonForAudio?: boolean; + showHybridButtonForMidi?: boolean; + onOpenHybrid?: (regionId: string) => void; onExternalDropComplete?: (trackIndex: number, regionUI: RegionUI) => void; } @@ -48,6 +51,9 @@ const TrackGridPanel: React.FC = ({ onRegionClick, onOpenPianoRoll, onOpenSpectrogram, + showHybridButtonForAudio, + showHybridButtonForMidi, + onOpenHybrid, onExternalDropComplete, }) => { const gridContainerRef = useRef(null); @@ -654,6 +660,9 @@ const TrackGridPanel: React.FC = ({ onRegionClick={handleRegionClick} onOpenPianoRoll={onOpenPianoRoll} onOpenSpectrogram={onOpenSpectrogram} + showHybridButtonForAudio={showHybridButtonForAudio} + showHybridButtonForMidi={showHybridButtonForMidi} + onOpenHybrid={onOpenHybrid} allTracks={tracks} onKGOneClipDrop={handleExternalDrop} /> diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index 0e2f50d..836f557 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -78,7 +78,8 @@ interface ProjectState { // Piano roll state showPianoRoll: boolean; activeRegionId: string | null; - pianoRollMode: 'midi-edit' | 'spectrogram'; + pianoRollMode: 'midi-edit' | 'spectrogram' | 'hybrid'; + hybridAudioRegionId: string | null; // ChatBox state showChatBox: boolean; @@ -149,6 +150,7 @@ interface ProjectState { setActiveRegionId: (regionId: string | null) => void; openMidiPianoRoll: (regionId: string) => void; openSpectrogramViewer: (regionId: string) => void; + openHybridMode: (midiRegionId: string, audioRegionId: string) => void; // Project state cleanup cleanupProjectState: () => void; @@ -308,6 +310,7 @@ export const useProjectStore = create((set, get) => { showPianoRoll: false, activeRegionId: null, pianoRollMode: 'midi-edit' as const, + hybridAudioRegionId: null, // Initial ChatBox state showChatBox: initialChatBoxState, @@ -1038,20 +1041,24 @@ export const useProjectStore = create((set, get) => { }, openMidiPianoRoll: (regionId: string) => { - set({ showPianoRoll: true, activeRegionId: regionId, pianoRollMode: 'midi-edit' }); + set({ showPianoRoll: true, activeRegionId: regionId, pianoRollMode: 'midi-edit', hybridAudioRegionId: null }); }, openSpectrogramViewer: (regionId: string) => { - set({ showPianoRoll: true, activeRegionId: regionId, pianoRollMode: 'spectrogram' }); + set({ showPianoRoll: true, activeRegionId: regionId, pianoRollMode: 'spectrogram', hybridAudioRegionId: null }); + }, + + openHybridMode: (midiRegionId: string, audioRegionId: string) => { + set({ showPianoRoll: true, activeRegionId: midiRegionId, hybridAudioRegionId: audioRegionId, pianoRollMode: 'hybrid' }); }, // Project state cleanup - used when starting new/loading projects cleanupProjectState: () => { // Close piano roll if it's visible set({ showPianoRoll: false }); - - // Clear active region - set({ activeRegionId: null }); + + // Clear active region and hybrid state + set({ activeRegionId: null, hybridAudioRegionId: null, pianoRollMode: 'midi-edit' }); // Clear any selected items KGCore.instance().clearSelectedItems();