diff --git a/components/editor/AudioEditor.tsx b/components/editor/AudioEditor.tsx index 428b73e..3de29bb 100644 --- a/components/editor/AudioEditor.tsx +++ b/components/editor/AudioEditor.tsx @@ -15,6 +15,7 @@ import { TrackList } from '@/components/tracks/TrackList'; import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog'; import { formatDuration } from '@/lib/audio/decoder'; import { useHistory } from '@/lib/hooks/useHistory'; +import { useRecording } from '@/lib/hooks/useRecording'; import { createMultiTrackCutCommand, createMultiTrackCopyCommand, @@ -30,6 +31,7 @@ export function AudioEditor() { const [zoom, setZoom] = React.useState(1); const [masterVolume, setMasterVolume] = React.useState(0.8); const [clipboard, setClipboard] = React.useState(null); + const [recordingTrackId, setRecordingTrackId] = React.useState(null); const { addToast } = useToast(); @@ -38,6 +40,14 @@ export function AudioEditor() { const canUndo = historyState.canUndo; const canRedo = historyState.canRedo; + // Recording hook + const { + state: recordingState, + startRecording, + stopRecording, + requestPermission, + } = useRecording(); + // Multi-track hooks const { tracks, @@ -155,6 +165,91 @@ export function AudioEditor() { updateTrack(trackId, { selection }); }; + // Recording handlers + const handleToggleRecordEnable = React.useCallback((trackId: string) => { + const track = tracks.find((t) => t.id === trackId); + if (!track) return; + + // Toggle record enable + updateTrack(trackId, { recordEnabled: !track.recordEnabled }); + }, [tracks, updateTrack]); + + const handleStartRecording = React.useCallback(async () => { + // Find first armed track + const armedTrack = tracks.find((t) => t.recordEnabled); + if (!armedTrack) { + addToast({ + title: 'No Track Armed', + description: 'Please arm a track for recording first', + variant: 'warning', + duration: 3000, + }); + return; + } + + // Request permission if needed + const hasPermission = await requestPermission(); + if (!hasPermission) { + addToast({ + title: 'Microphone Access Denied', + description: 'Please allow microphone access to record', + variant: 'error', + duration: 3000, + }); + return; + } + + try { + await startRecording(); + setRecordingTrackId(armedTrack.id); + addToast({ + title: 'Recording Started', + description: `Recording to ${armedTrack.name}`, + variant: 'success', + duration: 2000, + }); + } catch (error) { + console.error('Failed to start recording:', error); + addToast({ + title: 'Recording Failed', + description: 'Failed to start recording', + variant: 'error', + duration: 3000, + }); + } + }, [tracks, startRecording, requestPermission, addToast]); + + const handleStopRecording = React.useCallback(async () => { + if (!recordingTrackId) return; + + try { + const audioBuffer = await stopRecording(); + + if (audioBuffer) { + // Update the track with recorded audio + updateTrack(recordingTrackId, { audioBuffer }); + + addToast({ + title: 'Recording Complete', + description: `Recorded ${audioBuffer.duration.toFixed(2)}s of audio`, + variant: 'success', + duration: 3000, + }); + } + + setRecordingTrackId(null); + } catch (error) { + console.error('Failed to stop recording:', error); + addToast({ + title: 'Recording Error', + description: 'Failed to save recording', + variant: 'error', + duration: 3000, + }); + setRecordingTrackId(null); + } + }, [recordingTrackId, stopRecording, updateTrack, addToast]); + // Edit handlers const handleCut = React.useCallback(() => { const track = tracks.find((t) => t.selection); @@ -540,6 +635,9 @@ export function AudioEditor() { onUpdateTrack={updateTrack} onSeek={seek} onSelectionChange={handleSelectionChange} + onToggleRecordEnable={handleToggleRecordEnable} + recordingTrackId={recordingTrackId} + recordingLevel={recordingState.inputLevel} /> @@ -561,6 +659,9 @@ export function AudioEditor() { onVolumeChange={setMasterVolume} currentTimeFormatted={formatDuration(currentTime)} durationFormatted={formatDuration(duration)} + isRecording={recordingState.isRecording} + onStartRecording={handleStartRecording} + onStopRecording={handleStopRecording} /> diff --git a/components/editor/PlaybackControls.tsx b/components/editor/PlaybackControls.tsx index aff9da8..1667d3f 100644 --- a/components/editor/PlaybackControls.tsx +++ b/components/editor/PlaybackControls.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { Play, Pause, Square, SkipBack, Volume2, VolumeX } from 'lucide-react'; +import { Play, Pause, Square, SkipBack, Volume2, VolumeX, Circle } from 'lucide-react'; import { Button } from '@/components/ui/Button'; import { Slider } from '@/components/ui/Slider'; import { cn } from '@/lib/utils/cn'; @@ -21,6 +21,9 @@ export interface PlaybackControlsProps { className?: string; currentTimeFormatted?: string; durationFormatted?: string; + isRecording?: boolean; + onStartRecording?: () => void; + onStopRecording?: () => void; } export function PlaybackControls({ @@ -38,6 +41,9 @@ export function PlaybackControls({ className, currentTimeFormatted, durationFormatted, + isRecording = false, + onStartRecording, + onStopRecording, }: PlaybackControlsProps) { const [isMuted, setIsMuted] = React.useState(false); const [previousVolume, setPreviousVolume] = React.useState(volume); @@ -144,6 +150,23 @@ export function PlaybackControls({ > + + {/* Record Button */} + {(onStartRecording || onStopRecording) && ( + + )} {/* Volume Control */} diff --git a/components/tracks/TrackList.tsx b/components/tracks/TrackList.tsx index 7f9115a..80bbeee 100644 --- a/components/tracks/TrackList.tsx +++ b/components/tracks/TrackList.tsx @@ -21,6 +21,9 @@ export interface TrackListProps { onUpdateTrack: (trackId: string, updates: Partial) => void; onSeek?: (time: number) => void; onSelectionChange?: (trackId: string, selection: { start: number; end: number } | null) => void; + onToggleRecordEnable?: (trackId: string) => void; + recordingTrackId?: string | null; + recordingLevel?: number; } export function TrackList({ @@ -36,6 +39,9 @@ export function TrackList({ onUpdateTrack, onSeek, onSelectionChange, + onToggleRecordEnable, + recordingTrackId, + recordingLevel = 0, }: TrackListProps) { const [importDialogOpen, setImportDialogOpen] = React.useState(false); @@ -151,6 +157,13 @@ export function TrackList({ ? (selection) => onSelectionChange(track.id, selection) : undefined } + onToggleRecordEnable={ + onToggleRecordEnable + ? () => onToggleRecordEnable(track.id) + : undefined + } + isRecording={recordingTrackId === track.id} + recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} /> ))}