From 8cd868d33fa51cae891c781e1dad8e2523a692c8 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Mon, 26 Jan 2026 16:22:31 -0800 Subject: [PATCH] Enhance audio recording functionality to improve duration handling - Updated getAudioDuration function to utilize recordedDuration property for files, addressing metadata issues on Windows. - Modified onRecordingComplete callbacks in audio recording hooks to pass the actual recorded duration. - Adjusted error handling in ProfileForm and SampleUpload components to clear validation errors for recorded files. - Ensured consistent handling of audio file duration across components. --- .../components/VoiceProfiles/ProfileForm.tsx | 50 ++++++++++++++---- .../components/VoiceProfiles/SampleUpload.tsx | 16 ++++-- app/src/lib/hooks/useAudioRecording.ts | 8 ++- app/src/lib/hooks/useSystemAudioCapture.ts | 8 ++- tauri/src-tauri/gen/Assets.car | Bin 3847048 -> 3847048 bytes 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/app/src/components/VoiceProfiles/ProfileForm.tsx b/app/src/components/VoiceProfiles/ProfileForm.tsx index f67c163f..68a42bf8 100644 --- a/app/src/components/VoiceProfiles/ProfileForm.tsx +++ b/app/src/components/VoiceProfiles/ProfileForm.tsx @@ -46,14 +46,26 @@ import { formatAudioDuration } from '@/lib/utils/audio'; import { isTauri } from '@/lib/tauri'; // Helper function to get audio duration from File -async function getAudioDuration(file: File): Promise { +async function getAudioDuration(file: File & { recordedDuration?: number }): Promise { + // If the file has a recordedDuration property (from our recording hooks), + // use that instead of trying to read metadata. This fixes issues on Windows + // where WebM files from MediaRecorder don't have proper duration metadata. + if (file.recordedDuration !== undefined && Number.isFinite(file.recordedDuration)) { + return file.recordedDuration; + } + return new Promise((resolve, reject) => { const audio = new Audio(); const url = URL.createObjectURL(file); audio.addEventListener('loadedmetadata', () => { URL.revokeObjectURL(url); - resolve(audio.duration); + // Check if duration is valid (not Infinity or NaN) + if (Number.isFinite(audio.duration) && audio.duration > 0) { + resolve(audio.duration); + } else { + reject(new Error('Audio file has invalid duration metadata')); + } }); audio.addEventListener('error', () => { @@ -123,7 +135,7 @@ export function ProfileForm() { useEffect(() => { if (selectedFile && selectedFile instanceof File) { setIsValidatingAudio(true); - getAudioDuration(selectedFile) + getAudioDuration(selectedFile as File & { recordedDuration?: number }) .then((duration) => { setAudioDuration(duration); if (duration > MAX_AUDIO_DURATION_SECONDS) { @@ -138,10 +150,18 @@ export function ProfileForm() { .catch((error) => { console.error('Failed to get audio duration:', error); setAudioDuration(null); - form.setError('sampleFile', { - type: 'manual', - message: 'Failed to validate audio file. Please try a different file.', - }); + // For recordings, we auto-stop at max duration, so we can skip validation errors + const isRecordedFile = selectedFile.name.startsWith('recording-') || + selectedFile.name.startsWith('system-audio-'); + if (!isRecordedFile) { + form.setError('sampleFile', { + type: 'manual', + message: 'Failed to validate audio file. Please try a different file.', + }); + } else { + // Clear any existing errors for recorded files + form.clearErrors('sampleFile'); + } }) .finally(() => { setIsValidatingAudio(false); @@ -161,10 +181,14 @@ export function ProfileForm() { cancelRecording, } = useAudioRecording({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { const file = new File([blob], `recording-${Date.now()}.webm`, { type: blob.type || 'audio/webm', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('sampleFile', file, { shouldValidate: true }); toast({ title: 'Recording complete', @@ -183,10 +207,14 @@ export function ProfileForm() { cancelRecording: cancelSystemRecording, } = useSystemAudioCapture({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { const file = new File([blob], `system-audio-${Date.now()}.wav`, { type: blob.type || 'audio/wav', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('sampleFile', file, { shouldValidate: true }); toast({ title: 'System audio captured', diff --git a/app/src/components/VoiceProfiles/SampleUpload.tsx b/app/src/components/VoiceProfiles/SampleUpload.tsx index 3cc3afa6..26ca4598 100644 --- a/app/src/components/VoiceProfiles/SampleUpload.tsx +++ b/app/src/components/VoiceProfiles/SampleUpload.tsx @@ -72,11 +72,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp cancelRecording, } = useAudioRecording({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { // Convert blob to File object const file = new File([blob], `recording-${Date.now()}.webm`, { type: blob.type || 'audio/webm', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('file', file, { shouldValidate: true }); toast({ title: 'Recording complete', @@ -95,11 +99,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp cancelRecording: cancelSystemRecording, } = useSystemAudioCapture({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { // Convert blob to File object const file = new File([blob], `system-audio-${Date.now()}.wav`, { type: blob.type || 'audio/wav', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('file', file, { shouldValidate: true }); toast({ title: 'System audio captured', diff --git a/app/src/lib/hooks/useAudioRecording.ts b/app/src/lib/hooks/useAudioRecording.ts index 91d25801..cf6c2c79 100644 --- a/app/src/lib/hooks/useAudioRecording.ts +++ b/app/src/lib/hooks/useAudioRecording.ts @@ -3,7 +3,7 @@ import { isTauri } from '@/lib/tauri'; interface UseAudioRecordingOptions { maxDurationSeconds?: number; - onRecordingComplete?: (blob: Blob) => void; + onRecordingComplete?: (blob: Blob, duration?: number) => void; } export function useAudioRecording({ @@ -87,7 +87,11 @@ export function useAudioRecording({ mediaRecorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); - onRecordingComplete?.(blob); + // Pass the actual recorded duration + const recordedDuration = startTimeRef.current + ? (Date.now() - startTimeRef.current) / 1000 + : undefined; + onRecordingComplete?.(blob, recordedDuration); // Stop all tracks streamRef.current?.getTracks().forEach((track) => { diff --git a/app/src/lib/hooks/useSystemAudioCapture.ts b/app/src/lib/hooks/useSystemAudioCapture.ts index 13a22b55..2c498e2f 100644 --- a/app/src/lib/hooks/useSystemAudioCapture.ts +++ b/app/src/lib/hooks/useSystemAudioCapture.ts @@ -4,7 +4,7 @@ import { isTauri } from '@/lib/tauri'; interface UseSystemAudioCaptureOptions { maxDurationSeconds?: number; - onRecordingComplete?: (blob: Blob) => void; + onRecordingComplete?: (blob: Blob, duration?: number) => void; } /** @@ -110,7 +110,11 @@ export function useSystemAudioCapture({ } const blob = new Blob([bytes], { type: 'audio/wav' }); - onRecordingComplete?.(blob); + // Pass the actual recorded duration + const recordedDuration = startTimeRef.current + ? (Date.now() - startTimeRef.current) / 1000 + : undefined; + onRecordingComplete?.(blob, recordedDuration); } catch (err) { const errorMessage = err instanceof Error diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index 9db1fbf04877a8b8b113fde64a5c34bff3f18998..c0a2c1f2faa34468fedbc2a7a21bedd1e00909b3 100644 GIT binary patch delta 832 zcmZwGJx>%t90u?^PULt!Q9!^$L_OaJW@cyRKSSX%FT0_%CF*e$h z(3nsxr60hAFJNKk#TcVDv_3?xIyd=ck|~~lo|)O+x9{~Hd_3u_&DLtQ8EC;AOhOZ; zVe062=cKjI$NNtV?|;&7`map8{^jU{`7@RqUD!auvfx!>N{Pf=X(EeBhb#yuBPB*w z3C;M~r%KY^W>RUr`TW__{f*Vv1J@n6I*dUBF2Fckgo$e4n#Fi-GDFd7&a@}Tl-C&x zQh8PeDhi`4Wx|in5mE{NZ%(`F<;}N^+*sn?7{@FjO@*>#HfqBV76ti|Ta}FlNw0kv z2EB;Q#d?lPA^1QjU6f35qY;@BE4hi-(u#=xCrHX+^L8~z&6P&3NlK@!V7m0ErZC?n zU$8{ng@RKVGCdOHG8e@85iWQ4cAGE>Q!ouPFbi|gf=e(Dm*EO5z#=TcRXEF7fmOH$ zZCHcrunsq118%}CxD6e+19#yb+=mC%-tNQG)7&Rh8kqzml_bNhbLL4zmWrbE!f~a^ zjFhqzl{~+%)$YO0BVe!zTfjkpgbWnu>R`tl|9q@d7KJ4ygT}lV@ji$X3%67zW^^AvCl=DYT_c1EoOt|5_jaI|HXaKDK%Zy#qGH$iRTakc?CjVu8I< z7o-l!$OS~=7TAaoLKOma<&8?07E6!3y`TMB?tS}S?ZKxn?fPt`Qkm(G+N%e@+oQQ% zR_`CGxBbtG^R%I@@f=bI0N$))snUfT}usX&}ClSap%?z>0k}(FI zd`MbsymcfI*$?7CxDg(;}PIjF;Vm>!H>Bi3{7nbuxK3I!`j7EF*$!ja{LFhOx8 z0zW&)>XPZ>Ijuo2Z@jDJMJmgb3QpX5NzzJA-Y`#`WzKk+qW9`-kW0$2lc1NOu~^NM zkc@Gm$!m_FkV-0|iKL=n0*6Y*{^vwa#*j{e#${urmfOrmhVfK#V@W!c>8;7AC^g!3 zj>)q@CetFgcpS9c-Pvxy1(<Yt`k!kNwVv>A%7~=D`2}