mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 13:20:39 -07:00
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:
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user