feat: implemented piano roll hybrid mode to allow user edit MIDI notes while observing spectrogram as reference

This commit is contained in:
Xiaohan-Tian
2026-05-01 18:28:45 -07:00
parent ba0e27e50a
commit 83034455cc
11 changed files with 162 additions and 55 deletions
+21
View File
@@ -140,6 +140,27 @@
background: rgba(0, 0, 0, 0.35);
}
.region-hybrid-btn {
position: absolute;
top: 4px;
left: 24px;
background: rgba(0, 0, 0, 0.25);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
padding: 2px;
margin: 0;
cursor: pointer;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
}
.region-hybrid-btn:hover {
background: rgba(0, 0, 0, 0.35);
}
/* Instrument dropdown specific styles */
.instrument-dropdown .quant-dropdown {
min-width: 80px;
+27 -2
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import './Region.css';
import { FaPencilAlt } from 'react-icons/fa';
import { FaPencilAlt, FaPlus } from 'react-icons/fa';
import { MdGraphicEq } from 'react-icons/md';
import type { ResizeAction } from '../interfaces';
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
@@ -30,6 +30,9 @@ interface RegionItemProps {
onOpenPianoRoll?: (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)
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<RegionItemProps> = ({
onClick,
onOpenPianoRoll,
onOpenSpectrogram,
showHybridButton,
onOpenHybrid,
midiRegion,
audioRegion,
audioBuffer
@@ -64,7 +69,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
const [resizeEdge, setResizeEdge] = useState<ResizeAction>('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<boolean>(false);
const isDraggingRef = useRef<boolean>(false);
@@ -600,6 +605,26 @@ const RegionItem: React.FC<RegionItemProps> = ({
<MdGraphicEq size={10} />
</button>
)}
{showHybridButton && (
<button
className="region-hybrid-btn"
title="Open in hybrid mode"
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (onOpenHybrid) {
onOpenHybrid(id);
}
}}
aria-label="Open hybrid mode"
>
<FaPlus size={10} />
</button>
)}
<canvas ref={canvasRef} />
</div>
</div>
+8
View File
@@ -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<HTMLDivElement>, trackIndex: number) => void;
}
@@ -49,6 +52,9 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
onRegionClick,
onOpenPianoRoll,
onOpenSpectrogram,
showHybridButtonForAudio,
showHybridButtonForMidi,
onOpenHybrid,
allTracks,
onKGOneClipDrop,
}) => {
@@ -571,6 +577,8 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
onOpenSpectrogram={audioRegion ? (regionId) => {
onOpenSpectrogram?.(regionId);
} : undefined}
showHybridButton={audioRegion ? showHybridButtonForAudio : showHybridButtonForMidi}
onOpenHybrid={onOpenHybrid}
midiRegion={midiRegion}
audioRegion={audioRegion}
audioBuffer={audioBuffer}
+9
View File
@@ -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<TrackGridPanelProps> = ({
onRegionClick,
onOpenPianoRoll,
onOpenSpectrogram,
showHybridButtonForAudio,
showHybridButtonForMidi,
onOpenHybrid,
onExternalDropComplete,
}) => {
const gridContainerRef = useRef<HTMLDivElement>(null);
@@ -654,6 +660,9 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
onRegionClick={handleRegionClick}
onOpenPianoRoll={onOpenPianoRoll}
onOpenSpectrogram={onOpenSpectrogram}
showHybridButtonForAudio={showHybridButtonForAudio}
showHybridButtonForMidi={showHybridButtonForMidi}
onOpenHybrid={onOpenHybrid}
allTracks={tracks}
onKGOneClipDrop={handleExternalDrop}
/>