@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
JavaScript
// 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