mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 04:40:40 -07:00
Implement audio format conversion and enhance recording completion handling
- Added a new utility function to convert audio blobs to WAV format, ensuring compatibility without requiring ffmpeg on the backend. - Updated the useAudioRecording hook to convert recorded audio from WebM to WAV upon completion, with error handling for conversion failures. - Improved the organization of imports in useAudioRecording for better readability.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { convertToWav } from '@/lib/utils/audio';
|
||||
|
||||
interface UseAudioRecordingOptions {
|
||||
maxDurationSeconds?: number;
|
||||
@@ -85,13 +86,26 @@ export function useAudioRecording({
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
const blob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
||||
// Pass the actual recorded duration
|
||||
const recordedDuration = startTimeRef.current
|
||||
? (Date.now() - startTimeRef.current) / 1000
|
||||
: undefined;
|
||||
onRecordingComplete?.(blob, recordedDuration);
|
||||
mediaRecorder.onstop = async () => {
|
||||
const webmBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
|
||||
|
||||
// Convert to WAV format to avoid needing ffmpeg on backend
|
||||
try {
|
||||
const wavBlob = await convertToWav(webmBlob);
|
||||
|
||||
// Pass the actual recorded duration
|
||||
const recordedDuration = startTimeRef.current
|
||||
? (Date.now() - startTimeRef.current) / 1000
|
||||
: undefined;
|
||||
onRecordingComplete?.(wavBlob, recordedDuration);
|
||||
} catch (err) {
|
||||
console.error('Error converting audio to WAV:', err);
|
||||
// Fallback to original blob if conversion fails
|
||||
const recordedDuration = startTimeRef.current
|
||||
? (Date.now() - startTimeRef.current) / 1000
|
||||
: undefined;
|
||||
onRecordingComplete?.(webmBlob, recordedDuration);
|
||||
}
|
||||
|
||||
// Stop all tracks
|
||||
streamRef.current?.getTracks().forEach((track) => {
|
||||
|
||||
@@ -16,3 +16,104 @@ export function formatAudioDuration(seconds: number): string {
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any audio blob to WAV format using Web Audio API.
|
||||
* This ensures compatibility without requiring ffmpeg on the backend.
|
||||
*/
|
||||
export async function convertToWav(audioBlob: Blob): Promise<Blob> {
|
||||
// Create audio context
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
// Read blob as array buffer
|
||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
||||
|
||||
// Decode audio data
|
||||
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||
|
||||
// Convert to WAV
|
||||
const wavBlob = audioBufferToWav(audioBuffer);
|
||||
|
||||
// Close audio context to free resources
|
||||
await audioContext.close();
|
||||
|
||||
return wavBlob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AudioBuffer to WAV blob.
|
||||
*/
|
||||
function audioBufferToWav(buffer: AudioBuffer): Blob {
|
||||
const numberOfChannels = buffer.numberOfChannels;
|
||||
const sampleRate = buffer.sampleRate;
|
||||
const format = 1; // PCM
|
||||
const bitDepth = 16;
|
||||
|
||||
const bytesPerSample = bitDepth / 8;
|
||||
const blockAlign = numberOfChannels * bytesPerSample;
|
||||
|
||||
// Interleave channels
|
||||
const interleaved = interleaveChannels(buffer);
|
||||
|
||||
// Create WAV file
|
||||
const dataLength = interleaved.length * bytesPerSample;
|
||||
const buffer2 = new ArrayBuffer(44 + dataLength);
|
||||
const view = new DataView(buffer2);
|
||||
|
||||
// Write WAV header
|
||||
writeString(view, 0, 'RIFF');
|
||||
view.setUint32(4, 36 + dataLength, true);
|
||||
writeString(view, 8, 'WAVE');
|
||||
writeString(view, 12, 'fmt ');
|
||||
view.setUint32(16, 16, true); // fmt chunk size
|
||||
view.setUint16(20, format, true); // audio format (PCM)
|
||||
view.setUint16(22, numberOfChannels, true);
|
||||
view.setUint32(24, sampleRate, true);
|
||||
view.setUint32(28, sampleRate * blockAlign, true); // byte rate
|
||||
view.setUint16(32, blockAlign, true);
|
||||
view.setUint16(34, bitDepth, true);
|
||||
writeString(view, 36, 'data');
|
||||
view.setUint32(40, dataLength, true);
|
||||
|
||||
// Write audio data
|
||||
floatTo16BitPCM(view, 44, interleaved);
|
||||
|
||||
return new Blob([buffer2], { type: 'audio/wav' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Interleave multiple channels into a single array.
|
||||
*/
|
||||
function interleaveChannels(buffer: AudioBuffer): Float32Array {
|
||||
const numberOfChannels = buffer.numberOfChannels;
|
||||
const length = buffer.length;
|
||||
const interleaved = new Float32Array(length * numberOfChannels);
|
||||
|
||||
for (let channel = 0; channel < numberOfChannels; channel++) {
|
||||
const channelData = buffer.getChannelData(channel);
|
||||
for (let i = 0; i < length; i++) {
|
||||
interleaved[i * numberOfChannels + channel] = channelData[i];
|
||||
}
|
||||
}
|
||||
|
||||
return interleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write string to DataView.
|
||||
*/
|
||||
function writeString(view: DataView, offset: number, string: string): void {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert float32 audio data to 16-bit PCM.
|
||||
*/
|
||||
function floatTo16BitPCM(view: DataView, offset: number, input: Float32Array): void {
|
||||
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||
const s = Math.max(-1, Math.min(1, input[i]));
|
||||
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user