Files
KGStudio/src/hooks/useRegionOperations.ts
T
Xiaohan-Tian fc84edfc59 feat: add audio track support with WAV/MP3 playback and looping
Introduce KGAudioTrack and KGAudioRegion as new track/region types
alongside existing MIDI tracks. Users can import WAV, MP3, OGG, FLAC,
and AAC files into audio tracks with full playback, looping, and
solo/mute/volume support.

New core models:
- KGAudioTrack (extends KGTrack with TrackType.Wave)
- KGAudioRegion (extends KGRegion with audio file reference)
- KGAudioPlayerBus (Tone.Gain + ToneBufferSource-based playback)
- KGAudioFileStorage (OPFS media/ directory for binary audio files)

New commands:
- AddAudioTrackCommand (create audio track with player bus)
- ImportAudioCommand (import audio file, create region at playhead,
auto-expand maxBars)

Playback features:
- Audio regions scheduled via Tone.Transport alongside MIDI events
- Loop support with duration capping at loop boundaries
- Resume safety offset to prevent ToneBufferSource timing misses
- Cross-track buffer copy when moving audio regions between tracks
- Proper buffer cleanup on track deletion and project switch

UI changes:
- Separate "+ MIDI" and "+ Audio" buttons in top-left spacer
- Audio track shows upload button instead of instrument selector
- FileImportModal reused for audio file drag-and-drop upload
- Real waveform rendering on audio region canvas
- Green color scheme for audio regions (vs blue for MIDI)
- Pencil icon and resize handles hidden for audio regions
- Cross-type region moves blocked (MIDI ↔ Audio)
- Piano roll blocked for audio regions
- Audio region deletion support

Infrastructure:
- Project structure version bumped to 4 (no-op migration)
- class-transformer discriminators updated for new subtypes
- RemoveTrackCommand updated to clean up audio player buses
2026-04-09 21:54:54 -07:00

116 lines
3.9 KiB
TypeScript

import { useCallback } from 'react';
import { DEBUG_MODE } from '../constants';
import { KGMidiRegion } from '../core/region/KGMidiRegion';
import { KGAudioRegion } from '../core/region/KGAudioRegion';
import { KGTrack } from '../core/track/KGTrack';
import { KGCore } from '../core/KGCore';
import type { RegionUI } from '../components/interfaces';
import { DeleteMultipleRegionsCommand } from '../core/commands';
interface UseRegionOperationsProps {
tracks: KGTrack[];
updateTrack: (track: KGTrack) => void;
setRegions: React.Dispatch<React.SetStateAction<RegionUI[]>>;
selectedRegionId: string | null;
setSelectedRegionId: React.Dispatch<React.SetStateAction<string | null>>;
showPianoRoll: boolean;
setShowPianoRoll: (show: boolean) => void;
activeRegionId: string | null;
setActiveRegionId: (regionId: string | null) => void;
}
export const useRegionOperations = ({
tracks,
updateTrack,
setRegions,
selectedRegionId,
setSelectedRegionId,
showPianoRoll,
setShowPianoRoll,
activeRegionId,
setActiveRegionId
}: UseRegionOperationsProps) => {
// Get KGCore instance for accessing selected items
const core = KGCore.instance();
/**
* Utility function to delete selected regions using commands
* This function:
* 1. Gets all selected regions from KGCore
* 2. Creates a DeleteMultipleRegionsCommand with the region IDs
* 3. Executes the command (which handles the model updates)
* 4. Updates the UI state to reflect the deletion
* 5. Updates tracks in the store and manages UI state
*
* @returns {boolean} True if regions were deleted, false if no regions were selected
*/
const deleteSelectedRegions = useCallback(() => {
// Get all selected regions from KGCore
const selectedItems = core.getSelectedItems();
const selectedRegions = selectedItems.filter(item =>
item instanceof KGMidiRegion || item instanceof KGAudioRegion
);
if (selectedRegions.length === 0) {
if (DEBUG_MODE.MAIN_CONTENT) {
console.log('No regions selected for deletion');
}
return false;
}
if (DEBUG_MODE.MAIN_CONTENT) {
console.log(`Deleting ${selectedRegions.length} selected regions`);
}
// Get the region IDs for the command
const regionIds = selectedRegions.map(region => region.getId());
// Create and execute the delete command
const command = new DeleteMultipleRegionsCommand(regionIds);
core.executeCommand(command);
// Update tracks in the store to reflect the changes
const deletedRegionData = command.getDeletedRegionData();
const uniqueTrackIds = new Set(deletedRegionData.map(data => data.trackId));
uniqueTrackIds.forEach(trackId => {
const track = tracks.find(t => t.getId().toString() === trackId);
if (track) {
updateTrack(track);
}
});
// Update regions state to reflect the deletion
setRegions(prevRegions => {
const selectedRegionIds = new Set(regionIds);
const filteredRegions = prevRegions.filter(region => !selectedRegionIds.has(region.id));
if (DEBUG_MODE.MAIN_CONTENT) {
console.log(`Updated regions state: ${prevRegions.length} -> ${filteredRegions.length} regions`);
}
return filteredRegions;
});
// Clear selected region ID if it was deleted
if (selectedRegionId && regionIds.includes(selectedRegionId)) {
setSelectedRegionId(null);
}
// Close piano roll if the active region was deleted
if (activeRegionId && regionIds.includes(activeRegionId)) {
setShowPianoRoll(false);
setActiveRegionId(null);
}
if (DEBUG_MODE.MAIN_CONTENT) {
console.log(`Deleted ${selectedRegions.length} regions using command`);
}
return true;
}, [tracks, selectedRegionId, activeRegionId, updateTrack, setRegions, setSelectedRegionId, setShowPianoRoll, setActiveRegionId, core]);
return {
deleteSelectedRegions
};
};