UNPKG

murmuraba

Version:

Real-time audio noise reduction with advanced chunked processing for web applications

338 lines (337 loc) 11.2 kB
import { vi } from 'vitest'; export class VADDataFactory { static create(time, vad) { return { time, vad }; } static createSequence(count, baseVad = 0.5) { return Array.from({ length: count }, (_, i) => ({ time: i, vad: baseVad + (Math.sin(i * 0.5) * 0.3), // Simulate variation })); } static createSpeechPattern() { // Simulates typical speech pattern with pauses return [ { time: 0, vad: 0.1 }, // Silence { time: 1, vad: 0.7 }, // Speech starts { time: 2, vad: 0.9 }, // Peak speech { time: 3, vad: 0.8 }, // Continued speech { time: 4, vad: 0.3 }, // Trailing off { time: 5, vad: 0.1 }, // Pause { time: 6, vad: 0.6 }, // Speech resumes { time: 7, vad: 0.85 }, // Strong speech { time: 8, vad: 0.4 }, // Ending { time: 9, vad: 0.05 }, // Silence ]; } } // Metrics Factory export class MetricsFactory { static create(overrides = {}) { return { processingLatency: 45.2, frameCount: 100, inputLevel: 0.8, outputLevel: 0.7, noiseReductionLevel: 0.755, timestamp: Date.now(), droppedFrames: 0, ...overrides, }; } static createHighPerformance() { return this.create({ processingLatency: 10.5, frameCount: 1000, droppedFrames: 0, inputLevel: 0.9, outputLevel: 0.85, noiseReductionLevel: 0.95, }); } static createLowPerformance() { return this.create({ processingLatency: 150.8, frameCount: 50, droppedFrames: 25, inputLevel: 0.3, outputLevel: 0.2, noiseReductionLevel: 0.4, }); } } // Chunk Factory export class ChunkFactory { static reset() { this.idCounter = 0; } static create(overrides = {}) { const now = Date.now(); const id = overrides.id || `chunk-${++this.idCounter}`; const duration = overrides.duration || 10000; return { id, index: 0, startTime: now - duration, endTime: now, duration, originalSize: 1024000, processedSize: 950000, noiseRemoved: 74000, averageVad: 0.5, vadData: VADDataFactory.createSequence(5), metrics: MetricsFactory.create(), isPlaying: false, isExpanded: false, isValid: true, ...overrides, }; } static createWithVAD(averageVad, id) { return this.create({ id: id || `chunk-vad-${averageVad}`, averageVad, vadData: VADDataFactory.createSequence(10, averageVad), }); } static createWithSpeech() { const vadData = VADDataFactory.createSpeechPattern(); const averageVad = vadData.reduce((sum, d) => sum + d.vad, 0) / vadData.length; return this.create({ id: 'chunk-speech', averageVad, vadData, }); } static createInvalid() { return this.create({ id: 'chunk-invalid', isValid: false, noiseRemoved: 0, averageVad: 0, vadData: [], }); } static createPlaying() { return this.create({ id: 'chunk-playing', isPlaying: true, }); } static createExpanded() { return this.create({ id: 'chunk-expanded', isExpanded: true, }); } static createBatch(count) { return Array.from({ length: count }, (_, i) => this.create({ id: `chunk-batch-${i}` })); } static createSequence(options) { const { count, startTime = Date.now() - 100000, duration = 10000 } = options; const chunks = []; for (let i = 0; i < count; i++) { const chunkStart = startTime + (i * duration); chunks.push(this.create({ id: `chunk-seq-${i}`, startTime: chunkStart, endTime: chunkStart + duration, duration, })); } return chunks; } } ChunkFactory.idCounter = 0; // MediaStream Factory export class MediaStreamFactory { static create(options = { audio: true }) { const tracks = []; if (options.audio !== false) { tracks.push(this.createAudioTrack()); } if (options.video) { tracks.push(this.createVideoTrack()); } return { id: options.id || `stream-${Math.random().toString(36).substr(2, 9)}`, active: true, getTracks: vi.fn(() => tracks), getAudioTracks: vi.fn(() => tracks.filter(t => t.kind === 'audio')), getVideoTracks: vi.fn(() => tracks.filter(t => t.kind === 'video')), addTrack: vi.fn(), removeTrack: vi.fn(), clone: vi.fn().mockReturnThis(), getTrackById: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), onaddtrack: null, onremovetrack: null, }; } static createAudioTrack(options = {}) { return { kind: 'audio', id: `track-${Math.random().toString(36).substr(2, 9)}`, label: 'Mock Audio Track', enabled: true, muted: false, readyState: 'live', stop: vi.fn(), clone: vi.fn().mockReturnThis(), getCapabilities: vi.fn(() => ({ channelCount: { min: 1, max: 2 }, echoCancellation: [true, false], noiseSuppression: [true, false], sampleRate: { min: 8000, max: 48000 }, sampleSize: { min: 8, max: 32 }, })), getConstraints: vi.fn(() => ({})), getSettings: vi.fn(() => ({ deviceId: 'default', groupId: 'default-group', echoCancellation: false, noiseSuppression: false, autoGainControl: false, channelCount: 2, sampleRate: 48000, sampleSize: 16, })), applyConstraints: vi.fn().mockResolvedValue(undefined), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), ...options, }; } static createVideoTrack(options = {}) { return { ...this.createAudioTrack(options), kind: 'video', label: 'Mock Video Track', getCapabilities: vi.fn(() => ({ width: { min: 320, max: 1920 }, height: { min: 240, max: 1080 }, frameRate: { min: 1, max: 60 }, facingMode: ['user', 'environment'], })), getSettings: vi.fn(() => ({ deviceId: 'default-camera', groupId: 'default-group', width: 1280, height: 720, frameRate: 30, facingMode: 'user', })), }; } static createInactiveStream() { const stream = this.create(); stream.active = false; stream.getTracks().forEach((track) => { track.readyState = 'ended'; }); return stream; } } // Audio Buffer Factory export class AudioBufferFactory { static create(options = {}) { const { numberOfChannels = 2, length = 48000, sampleRate = 48000, fillWith = 'silence', customData, } = options; const channelData = this.generateChannelData(length, fillWith, customData); return { numberOfChannels, length, sampleRate, duration: length / sampleRate, getChannelData: vi.fn((channel) => { if (channel >= numberOfChannels) { throw new Error(`Channel ${channel} does not exist`); } return channelData; }), copyFromChannel: vi.fn(), copyToChannel: vi.fn(), }; } static generateChannelData(length, fillWith, customData) { if (customData) { return customData; } const data = new Float32Array(length); switch (fillWith) { case 'sine': for (let i = 0; i < length; i++) { data[i] = Math.sin(2 * Math.PI * 440 * i / 48000); } break; case 'noise': for (let i = 0; i < length; i++) { data[i] = Math.random() * 2 - 1; } break; case 'silence': default: // Already filled with zeros break; } return data; } } // WASM Module Factory export class WASMModuleFactory { static create(options = {}) { const { rnnoiseState = 12345, vadProbability = 0.7, memorySize = 10000, } = options; return { _rnnoise_create: vi.fn().mockReturnValue(rnnoiseState), _rnnoise_destroy: vi.fn(), _rnnoise_process_frame: vi.fn().mockReturnValue(vadProbability), _malloc: vi.fn((size) => size), _free: vi.fn(), HEAPF32: new Float32Array(memorySize), HEAP32: new Int32Array(memorySize), HEAPU8: new Uint8Array(memorySize), HEAPU32: new Uint32Array(memorySize), }; } static createWithError() { return { _rnnoise_create: vi.fn().mockReturnValue(0), // Return 0 to indicate error _rnnoise_destroy: vi.fn(), _rnnoise_process_frame: vi.fn().mockImplementation(() => { throw new Error('WASM processing error'); }), _malloc: vi.fn().mockReturnValue(0), _free: vi.fn(), HEAPF32: new Float32Array(0), HEAP32: new Int32Array(0), HEAPU8: new Uint8Array(0), HEAPU32: new Uint32Array(0), }; } } // Blob Factory export class BlobFactory { static createAudioBlob(sizeInKB = 10, mimeType = 'audio/webm') { const size = sizeInKB * 1024; const data = new Uint8Array(size); // Fill with some pattern to simulate audio data for (let i = 0; i < size; i++) { data[i] = Math.floor(Math.sin(i * 0.01) * 127 + 128); } return new Blob([data], { type: mimeType }); } static createVideoBlob(sizeInKB = 100, mimeType = 'video/webm') { const size = sizeInKB * 1024; const data = new Uint8Array(size); // Fill with pattern for (let i = 0; i < size; i++) { data[i] = i % 256; } return new Blob([data], { type: mimeType }); } static createTextBlob(content) { return new Blob([content], { type: 'text/plain' }); } }