dcrey7's picture
Upload 9 files
e30257d verified
raw
history blame
10.4 kB
class AudioManager {
constructor() {
// Core audio configuration
this.config = {
sampleRate: 44100,
channels: 1,
bitDepth: 16,
maxRecordingTime: 30000, // 30 seconds in milliseconds
minRecordingTime: 15000 // 15 seconds in milliseconds
};
// Recording state management
this.state = {
isRecording: false,
startTime: null,
recorder: null,
stream: null,
audioChunks: [],
audioContext: null,
analyser: null
};
// Audio visualization settings
this.visualizer = {
canvasContext: null,
dataArray: null,
bufferLength: null,
width: 0,
height: 0
};
// Initialize audio context with error handling
try {
this.state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (error) {
console.error('AudioContext not supported in this browser');
}
// Bind methods to maintain context
this.startRecording = this.startRecording.bind(this);
this.stopRecording = this.stopRecording.bind(this);
this.processAudio = this.processAudio.bind(this);
}
/**
* Initialize audio visualization on a canvas element
* @param {HTMLCanvasElement} canvas - The canvas element for visualization
*/
initializeVisualizer(canvas) {
if (!canvas) return;
this.visualizer.canvasContext = canvas.getContext('2d');
this.visualizer.width = canvas.width;
this.visualizer.height = canvas.height;
// Set up audio analyser for visualization
this.state.analyser = this.state.audioContext.createAnalyser();
this.state.analyser.fftSize = 2048;
this.visualizer.bufferLength = this.state.analyser.frequencyBinCount;
this.visualizer.dataArray = new Uint8Array(this.visualizer.bufferLength);
}
/**
* Start recording audio with visualization
* @returns {Promise<void>}
*/
async startRecording() {
try {
// Request microphone access
this.state.stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: this.config.channels,
sampleRate: this.config.sampleRate
}
});
// Create and configure MediaRecorder
this.state.recorder = new MediaRecorder(this.state.stream, {
mimeType: 'audio/webm;codecs=opus'
});
// Set up recording event handlers
this.state.recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.state.audioChunks.push(event.data);
}
};
// Connect audio nodes for visualization
const source = this.state.audioContext.createMediaStreamSource(this.state.stream);
source.connect(this.state.analyser);
// Start recording
this.state.recorder.start(100); // Collect data every 100ms
this.state.isRecording = true;
this.state.startTime = Date.now();
// Start visualization if canvas is set up
if (this.visualizer.canvasContext) {
this.drawVisualization();
}
// Set up automatic recording stop
setTimeout(() => {
if (this.state.isRecording) {
this.stopRecording();
}
}, this.config.maxRecordingTime);
} catch (error) {
console.error('Error starting recording:', error);
throw new Error('Failed to start recording');
}
}
/**
* Stop recording and process the audio
* @returns {Promise<Blob>} The processed audio blob
*/
async stopRecording() {
return new Promise((resolve, reject) => {
try {
const recordingDuration = Date.now() - this.state.startTime;
// Check if recording meets minimum duration
if (recordingDuration < this.config.minRecordingTime) {
throw new Error('Recording too short');
}
this.state.recorder.onstop = async () => {
try {
const audioBlob = await this.processAudio();
resolve(audioBlob);
} catch (error) {
reject(error);
}
};
// Stop recording and clean up
this.state.recorder.stop();
this.state.stream.getTracks().forEach(track => track.stop());
this.state.isRecording = false;
} catch (error) {
reject(error);
}
});
}
/**
* Process recorded audio chunks into a single blob
* @returns {Promise<Blob>}
*/
async processAudio() {
try {
// Combine audio chunks into a single blob
const audioBlob = new Blob(this.state.audioChunks, { type: 'audio/webm;codecs=opus' });
// Convert to proper format for ElevenLabs API
const arrayBuffer = await audioBlob.arrayBuffer();
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// Create WAV format buffer
const wavBuffer = this.createWAVBuffer(audioBuffer);
return new Blob([wavBuffer], { type: 'audio/wav' });
} catch (error) {
console.error('Error processing audio:', error);
throw new Error('Failed to process audio');
}
}
/**
* Create WAV buffer from audio buffer
* @param {AudioBuffer} audioBuffer
* @returns {ArrayBuffer}
*/
createWAVBuffer(audioBuffer) {
const numChannels = audioBuffer.numberOfChannels;
const length = audioBuffer.length * numChannels * 2;
const buffer = new ArrayBuffer(44 + length);
const view = new DataView(buffer);
// Write WAV header
this.writeWAVHeader(view, length, numChannels, audioBuffer.sampleRate);
// Write audio data
const channels = [];
for (let i = 0; i < numChannels; i++) {
channels.push(audioBuffer.getChannelData(i));
}
let offset = 44;
for (let i = 0; i < audioBuffer.length; i++) {
for (let channel = 0; channel < numChannels; channel++) {
const sample = Math.max(-1, Math.min(1, channels[channel][i]));
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
offset += 2;
}
}
return buffer;
}
/**
* Write WAV header to DataView
* @param {DataView} view
* @param {number} length
* @param {number} numChannels
* @param {number} sampleRate
*/
writeWAVHeader(view, length, numChannels, sampleRate) {
// RIFF identifier
this.writeString(view, 0, 'RIFF');
// RIFF chunk length
view.setUint32(4, 36 + length, true);
// RIFF type
this.writeString(view, 8, 'WAVE');
// Format chunk identifier
this.writeString(view, 12, 'fmt ');
// Format chunk length
view.setUint32(16, 16, true);
// Sample format (raw)
view.setUint16(20, 1, true);
// Channel count
view.setUint16(22, numChannels, true);
// Sample rate
view.setUint32(24, sampleRate, true);
// Byte rate (sample rate * block align)
view.setUint32(28, sampleRate * numChannels * 2, true);
// Block align (channel count * bytes per sample)
view.setUint16(32, numChannels * 2, true);
// Bits per sample
view.setUint16(34, 16, true);
// Data chunk identifier
this.writeString(view, 36, 'data');
// Data chunk length
view.setUint32(40, length, true);
}
/**
* Write string to DataView
* @param {DataView} view
* @param {number} offset
* @param {string} string
*/
writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
/**
* Draw audio visualization
*/
drawVisualization() {
if (!this.state.isRecording || !this.visualizer.canvasContext) return;
requestAnimationFrame(() => this.drawVisualization());
// Get frequency data
this.state.analyser.getByteFrequencyData(this.visualizer.dataArray);
// Clear canvas
this.visualizer.canvasContext.fillStyle = 'rgb(10, 10, 10)';
this.visualizer.canvasContext.fillRect(0, 0, this.visualizer.width, this.visualizer.height);
// Draw frequency bars
const barWidth = (this.visualizer.width / this.visualizer.bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < this.visualizer.bufferLength; i++) {
barHeight = (this.visualizer.dataArray[i] / 255) * this.visualizer.height;
this.visualizer.canvasContext.fillStyle = `rgb(${barHeight + 100},50,50)`;
this.visualizer.canvasContext.fillRect(
x,
this.visualizer.height - barHeight,
barWidth,
barHeight
);
x += barWidth + 1;
}
}
/**
* Clean up audio resources
*/
cleanup() {
if (this.state.stream) {
this.state.stream.getTracks().forEach(track => track.stop());
}
this.state.audioChunks = [];
this.state.isRecording = false;
}
}
// Export the AudioManager class
export default AudioManager;