fc84edfc59
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
116 lines
3.9 KiB
TypeScript
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
|
|
};
|
|
};
|