UNPKG

@siteed/expo-audio-studio

Version:

Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web

285 lines 11.4 kB
// src/ExpoAudioStreamModule.web.ts import { LegacyEventEmitter } from 'expo-modules-core'; import { WebRecorder } from './WebRecorder.web'; import { encodingToBitDepth } from './utils/encodingToBitDepth'; export class ExpoAudioStreamWeb extends LegacyEventEmitter { customRecorder; audioChunks; isRecording; isPaused; recordingStartTime; pausedTime; currentDurationMs; currentSize; currentInterval; currentIntervalAnalysis; lastEmittedSize; lastEmittedTime; lastEmittedCompressionSize; lastEmittedAnalysisTime; streamUuid; extension = 'wav'; // Default extension is 'wav' recordingConfig; bitDepth; // Bit depth of the audio audioWorkletUrl; featuresExtratorUrl; logger; latestPosition = 0; totalCompressedSize = 0; maxBufferSize; constructor({ audioWorkletUrl, featuresExtratorUrl, logger, maxBufferSize = 100, // Default to storing last 100 chunks (1 chunk = 0.5 seconds) }) { const mockNativeModule = { addListener: () => { // Not used on web }, removeListeners: () => { // Not used on web }, }; super(mockNativeModule); // Pass the mock native module to the parent class this.logger = logger; this.customRecorder = null; this.audioChunks = []; this.isRecording = false; this.isPaused = false; this.recordingStartTime = 0; this.pausedTime = 0; this.currentDurationMs = 0; this.currentSize = 0; this.bitDepth = 32; // Default this.currentInterval = 1000; // Default interval in ms this.currentIntervalAnalysis = 500; // Default analysis interval in ms this.lastEmittedSize = 0; this.lastEmittedTime = 0; this.latestPosition = 0; this.lastEmittedCompressionSize = 0; this.lastEmittedAnalysisTime = 0; this.streamUuid = null; // Initialize UUID on first recording start this.audioWorkletUrl = audioWorkletUrl; this.featuresExtratorUrl = featuresExtratorUrl; this.maxBufferSize = maxBufferSize; } // Utility to handle user media stream async getMediaStream() { try { return await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (error) { this.logger?.error('Failed to get media stream:', error); throw error; } } // Start recording with options async startRecording(recordingConfig = {}) { if (this.isRecording) { throw new Error('Recording is already in progress'); } this.bitDepth = encodingToBitDepth({ encoding: recordingConfig.encoding ?? 'pcm_32bit', }); const audioContext = new (window.AudioContext || // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - Allow webkitAudioContext for Safari window.webkitAudioContext)(); const stream = await this.getMediaStream(); const source = audioContext.createMediaStreamSource(stream); this.customRecorder = new WebRecorder({ logger: this.logger, audioContext, source, recordingConfig, emitAudioEventCallback: ({ data, position, compression, }) => { // Keep only the latest chunks based on maxBufferSize this.audioChunks.push(new Float32Array(data)); if (this.audioChunks.length > this.maxBufferSize) { this.audioChunks.shift(); // Remove oldest chunk } this.currentSize += data.byteLength; this.emitAudioEvent({ data, position, compression }); this.lastEmittedTime = Date.now(); this.lastEmittedSize = this.currentSize; this.lastEmittedCompressionSize = compression?.size ?? 0; }, emitAudioAnalysisCallback: (audioAnalysisData) => { this.logger?.log(`Emitted AudioAnalysis:`, audioAnalysisData); this.emit('AudioAnalysis', audioAnalysisData); }, }); await this.customRecorder.init(); this.customRecorder.start(); // // Set a timer to stop recording after 5 seconds // setTimeout(() => { // logger.log("AUTO Stopping recording"); // this.customRecorder?.stopAndPlay(); // this.isRecording = false; // }, 3000); this.isRecording = true; this.recordingConfig = recordingConfig; this.recordingStartTime = Date.now(); this.pausedTime = 0; this.isPaused = false; this.lastEmittedSize = 0; this.lastEmittedTime = 0; this.lastEmittedCompressionSize = 0; this.currentInterval = recordingConfig.interval ?? 1000; this.currentIntervalAnalysis = recordingConfig.intervalAnalysis ?? 500; this.lastEmittedAnalysisTime = Date.now(); // Use custom filename if provided, otherwise fallback to timestamp if (recordingConfig.filename) { // Remove any existing extension from the filename this.streamUuid = recordingConfig.filename.replace(/\.[^/.]+$/, ''); } else { this.streamUuid = Date.now().toString(); } const fileUri = `${this.streamUuid}.${this.extension}`; const streamConfig = { fileUri, mimeType: `audio/${this.extension}`, bitDepth: this.bitDepth, channels: recordingConfig.channels ?? 1, sampleRate: recordingConfig.sampleRate ?? 44100, compression: recordingConfig.compression ? { ...recordingConfig.compression, bitrate: recordingConfig.compression?.bitrate ?? 128000, size: 0, mimeType: 'audio/webm', format: recordingConfig.compression?.format ?? 'opus', compressedFileUri: '', } : undefined, }; return streamConfig; } emitAudioEvent({ data, position, compression }) { const fileUri = `${this.streamUuid}.${this.extension}`; if (compression?.size) { this.lastEmittedCompressionSize = compression.size; this.totalCompressedSize = compression.totalSize; } this.latestPosition = position; this.currentDurationMs = position * 1000; // Convert position (in seconds) to ms const audioEventPayload = { fileUri, mimeType: `audio/${this.extension}`, lastEmittedSize: this.lastEmittedSize, deltaSize: data.byteLength, position, totalSize: this.currentSize, buffer: data, streamUuid: this.streamUuid ?? '', compression: compression ? { data: compression?.data, totalSize: this.totalCompressedSize, eventDataSize: compression?.size ?? 0, position, } : undefined, }; this.emit('AudioData', audioEventPayload); } // Stop recording async stopRecording() { if (!this.customRecorder) { throw new Error('Recorder is not initialized'); } this.logger?.debug('[Stop] Starting stop process'); const startTime = performance.now(); try { this.logger?.debug('[Stop] Stopping recorder'); const { compressedBlob } = await this.customRecorder.stop(); this.isRecording = false; this.isPaused = false; this.currentDurationMs = Date.now() - this.recordingStartTime; let compression; let fileUri = `${this.streamUuid}.${this.extension}`; let mimeType = `audio/${this.extension}`; if (compressedBlob && this.recordingConfig?.compression?.enabled) { const compressedUri = URL.createObjectURL(compressedBlob); compression = { compressedFileUri: compressedUri, size: compressedBlob.size, mimeType: 'audio/webm', format: 'opus', bitrate: this.recordingConfig.compression.bitrate ?? 128000, }; // Use compressed values when compression is enabled fileUri = compressedUri; mimeType = 'audio/webm'; } this.logger?.debug(`[Stop] Completed stop process in ${performance.now() - startTime}ms`, { durationMs: this.currentDurationMs, compressedSize: compression?.size, }); // Use the stored streamUuid (which contains our custom filename) for the final filename const filename = `${this.streamUuid}.${this.extension}`; const result = { fileUri, filename, // This will now use our custom filename bitDepth: this.bitDepth, createdAt: this.recordingStartTime, channels: this.recordingConfig?.channels ?? 1, sampleRate: this.recordingConfig?.sampleRate ?? 44100, durationMs: this.currentDurationMs, size: this.currentSize, mimeType, compression, }; // Reset after creating the result this.streamUuid = null; return result; } catch (error) { this.logger?.error('[Stop] Error stopping recording:', error); throw error; } } // Pause recording async pauseRecording() { if (!this.isRecording || this.isPaused) { throw new Error('Recording is not active or already paused'); } if (this.customRecorder) { this.customRecorder.pause(); } this.isPaused = true; this.pausedTime = Date.now(); } // Resume recording async resumeRecording() { if (!this.isPaused) { throw new Error('Recording is not paused'); } if (this.customRecorder) { this.customRecorder.resume(); } this.isPaused = false; this.recordingStartTime += Date.now() - this.pausedTime; } // Get current status status() { const status = { isRecording: this.isRecording, isPaused: this.isPaused, durationMs: this.currentDurationMs, size: this.currentSize, interval: this.currentInterval, intervalAnalysis: this.currentIntervalAnalysis, mimeType: `audio/${this.extension}`, compression: this.recordingConfig?.compression?.enabled ? { size: this.totalCompressedSize, mimeType: 'audio/webm', format: this.recordingConfig.compression.format ?? 'opus', bitrate: this.recordingConfig.compression.bitrate ?? 128000, compressedFileUri: `${this.streamUuid}.webm`, } : undefined, }; return status; } } //# sourceMappingURL=ExpoAudioStream.web.js.map