murmuraba
Version:
Real-time audio noise reduction with advanced chunked processing for web applications
166 lines (165 loc) • 6.85 kB
JavaScript
import { processStream } from '../../api';
import { logger } from './logger';
import { DEFAULT_CHUNK_DURATION } from './constants';
export function createRecordingFunctions({ isInitialized, recordingState, recordingStateHook, currentStream, originalStream, setCurrentStream, setOriginalStream, setStreamController, setError, chunkManager, recordingManager, initialize }) {
const { startRecording: startRecordingState, stopRecording: stopRecordingState, pauseRecording: pauseRecordingState, resumeRecording: resumeRecordingState, addChunk, clearRecordings: clearRecordingsState, updateRecordingTime } = recordingStateHook;
/**
* Start recording with concatenated streaming
*/
const startRecording = async (chunkDuration = DEFAULT_CHUNK_DURATION) => {
try {
if (!isInitialized) {
logger.info('Engine not initialized, attempting initialization...');
try {
await initialize();
}
catch (initError) {
logger.error('Initialization failed during recording start', { error: initError });
throw new Error(`Failed to initialize audio engine: ${initError instanceof Error ? initError.message : 'Unknown error'}`);
}
}
logger.info('Requesting microphone access...');
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false, // REGLA 10: Desactivar TODO
noiseSuppression: false, // We're doing our own
autoGainControl: false // We have our own AGC
}
});
logger.info('Microphone access granted', {
streamId: stream.id,
tracks: stream.getTracks().map(t => ({
kind: t.kind,
enabled: t.enabled,
readyState: t.readyState,
label: t.label
}))
});
setOriginalStream(stream);
const controller = await processStream(stream);
setStreamController(controller);
// Debug: Verificar ambos streams
console.log('recordingFunctions: Stream comparison:', {
originalStream: {
id: stream.id,
audioTracks: stream.getAudioTracks().length
},
processedStream: {
id: controller.stream.id,
audioTracks: controller.stream.getAudioTracks().length
}
});
// Use processed stream for Live Waveform Analysis to get real-time metrics
setCurrentStream(controller.stream);
// Use hook's state management
startRecordingState();
// Start the chunk processing
const onChunkProcessed = (chunk) => {
// Add index to chunk for ChunkData compatibility
const chunkWithIndex = {
...chunk,
index: recordingStateHook.recordingState.chunks.length
};
addChunk(chunkWithIndex);
logger.info('Chunk processed', {
id: chunk.id,
duration: chunk.duration,
noiseReduction: chunk.noiseRemoved
});
};
await recordingManager.startCycle(controller.stream, stream, chunkDuration, onChunkProcessed);
logger.info('Recording started', { chunkDuration });
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to start recording';
logger.error('Failed to start recording', { error: errorMessage });
setError(errorMessage);
throw err;
}
};
/**
* Stop recording and cleanup (but keep engine for chunk playback)
*/
const stopRecording = () => {
logger.info('Stopping recording');
// First stop the recording manager
recordingManager.stopRecording();
// Stop all tracks in the processed stream
if (currentStream) {
currentStream.getTracks().forEach(track => {
track.stop();
logger.debug('Stopped processed track', { kind: track.kind, id: track.id });
});
setCurrentStream(null);
}
// CRITICAL: Stop all tracks in the original microphone stream
// This is what releases the microphone and removes the recording indicator
if (originalStream) {
originalStream.getTracks().forEach(track => {
track.stop();
logger.debug('Stopped original track', { kind: track.kind, id: track.id });
});
setOriginalStream(null);
}
// Clean up the stream controller if it exists
if (setStreamController) {
setStreamController(prevController => {
if (prevController) {
// Call the stop method on the controller if it exists
if (typeof prevController.stop === 'function') {
try {
prevController.stop();
logger.debug('Stopped stream controller');
}
catch (error) {
logger.warn('Error stopping stream controller', { error });
}
}
}
return null;
});
}
// Use hook's state management
stopRecordingState();
// NOTE: We don't destroy the engine here to keep chunks playable
logger.info('Recording stopped and audio resources released');
};
/**
* Pause recording
*/
const pauseRecording = () => {
logger.info('Pausing recording');
recordingManager.pauseRecording();
pauseRecordingState();
};
/**
* Resume recording
*/
const resumeRecording = () => {
logger.info('Resuming recording');
recordingManager.resumeRecording();
resumeRecordingState();
};
/**
* Clear all recordings
*/
const clearRecordings = () => {
logger.info('Clearing all recordings');
// Stop any ongoing recording
if (recordingState.isRecording) {
stopRecording();
}
// Revoke all chunk URLs
chunkManager.clearChunks(recordingState.chunks);
// Clear state
clearRecordingsState();
logger.info('All recordings cleared');
};
return {
startRecording,
stopRecording,
pauseRecording,
resumeRecording,
clearRecordings
};
}