mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 07:05:14 -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';
|
import { isTauri } from '@/lib/tauri';
|
||||||
|
|
||||||
// Helper function to get audio duration from File
|
// 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
audio.addEventListener('loadedmetadata', () => {
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
URL.revokeObjectURL(url);
|
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', () => {
|
audio.addEventListener('error', () => {
|
||||||
@@ -123,7 +135,7 @@ export function ProfileForm() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFile && selectedFile instanceof File) {
|
if (selectedFile && selectedFile instanceof File) {
|
||||||
setIsValidatingAudio(true);
|
setIsValidatingAudio(true);
|
||||||
getAudioDuration(selectedFile)
|
getAudioDuration(selectedFile as File & { recordedDuration?: number })
|
||||||
.then((duration) => {
|
.then((duration) => {
|
||||||
setAudioDuration(duration);
|
setAudioDuration(duration);
|
||||||
if (duration > MAX_AUDIO_DURATION_SECONDS) {
|
if (duration > MAX_AUDIO_DURATION_SECONDS) {
|
||||||
@@ -138,10 +150,18 @@ export function ProfileForm() {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to get audio duration:', error);
|
console.error('Failed to get audio duration:', error);
|
||||||
setAudioDuration(null);
|
setAudioDuration(null);
|
||||||
form.setError('sampleFile', {
|
// For recordings, we auto-stop at max duration, so we can skip validation errors
|
||||||
type: 'manual',
|
const isRecordedFile = selectedFile.name.startsWith('recording-') ||
|
||||||
message: 'Failed to validate audio file. Please try a different file.',
|
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(() => {
|
.finally(() => {
|
||||||
setIsValidatingAudio(false);
|
setIsValidatingAudio(false);
|
||||||
@@ -161,10 +181,14 @@ export function ProfileForm() {
|
|||||||
cancelRecording,
|
cancelRecording,
|
||||||
} = useAudioRecording({
|
} = useAudioRecording({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 30,
|
||||||
onRecordingComplete: (blob) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||||
type: blob.type || 'audio/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 });
|
form.setValue('sampleFile', file, { shouldValidate: true });
|
||||||
toast({
|
toast({
|
||||||
title: 'Recording complete',
|
title: 'Recording complete',
|
||||||
@@ -183,10 +207,14 @@ export function ProfileForm() {
|
|||||||
cancelRecording: cancelSystemRecording,
|
cancelRecording: cancelSystemRecording,
|
||||||
} = useSystemAudioCapture({
|
} = useSystemAudioCapture({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 30,
|
||||||
onRecordingComplete: (blob) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||||
type: blob.type || 'audio/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 });
|
form.setValue('sampleFile', file, { shouldValidate: true });
|
||||||
toast({
|
toast({
|
||||||
title: 'System audio captured',
|
title: 'System audio captured',
|
||||||
|
|||||||
@@ -72,11 +72,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
|||||||
cancelRecording,
|
cancelRecording,
|
||||||
} = useAudioRecording({
|
} = useAudioRecording({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 30,
|
||||||
onRecordingComplete: (blob) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
// Convert blob to File object
|
// Convert blob to File object
|
||||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||||
type: blob.type || 'audio/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 });
|
form.setValue('file', file, { shouldValidate: true });
|
||||||
toast({
|
toast({
|
||||||
title: 'Recording complete',
|
title: 'Recording complete',
|
||||||
@@ -95,11 +99,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
|||||||
cancelRecording: cancelSystemRecording,
|
cancelRecording: cancelSystemRecording,
|
||||||
} = useSystemAudioCapture({
|
} = useSystemAudioCapture({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 30,
|
||||||
onRecordingComplete: (blob) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
// Convert blob to File object
|
// Convert blob to File object
|
||||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||||
type: blob.type || 'audio/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 });
|
form.setValue('file', file, { shouldValidate: true });
|
||||||
toast({
|
toast({
|
||||||
title: 'System audio captured',
|
title: 'System audio captured',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { isTauri } from '@/lib/tauri';
|
|||||||
|
|
||||||
interface UseAudioRecordingOptions {
|
interface UseAudioRecordingOptions {
|
||||||
maxDurationSeconds?: number;
|
maxDurationSeconds?: number;
|
||||||
onRecordingComplete?: (blob: Blob) => void;
|
onRecordingComplete?: (blob: Blob, duration?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAudioRecording({
|
export function useAudioRecording({
|
||||||
@@ -87,7 +87,11 @@ export function useAudioRecording({
|
|||||||
|
|
||||||
mediaRecorder.onstop = () => {
|
mediaRecorder.onstop = () => {
|
||||||
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
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
|
// Stop all tracks
|
||||||
streamRef.current?.getTracks().forEach((track) => {
|
streamRef.current?.getTracks().forEach((track) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isTauri } from '@/lib/tauri';
|
|||||||
|
|
||||||
interface UseSystemAudioCaptureOptions {
|
interface UseSystemAudioCaptureOptions {
|
||||||
maxDurationSeconds?: number;
|
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' });
|
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) {
|
} catch (err) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err instanceof Error
|
err instanceof Error
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user