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.
This commit is contained in:
Jamie Pine
2026-01-26 16:22:31 -08:00
parent 446182e16c
commit 8cd868d33f
5 changed files with 63 additions and 19 deletions
@@ -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<number> {
async function getAudioDuration(file: File & { recordedDuration?: number }): Promise<number> {
// 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',
@@ -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',
+6 -2
View File
@@ -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) => {
+6 -2
View File
@@ -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
Binary file not shown.