UNPKG

murmuraba

Version:

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

162 lines (161 loc) 5.93 kB
/** * Audio Test Helpers * REAL audio data generation for REAL testing * No more bullshit mocks that don't test anything */ /** * Generate a sine wave at specified frequency */ export function generateSineWave(frequency, sampleRate, duration, amplitude = 1.0) { const samples = Math.floor(sampleRate * duration); const data = new Float32Array(samples); for (let i = 0; i < samples; i++) { data[i] = amplitude * Math.sin(2 * Math.PI * frequency * i / sampleRate); } return data; } /** * Generate realistic speech-like signal using formants */ export function generateSpeechLikeSignal(sampleRate, duration) { const samples = Math.floor(sampleRate * duration); const data = new Float32Array(samples); // Typical formant frequencies for vowel 'a' const formants = [700, 1220, 2600]; const bandwidths = [130, 70, 160]; for (let i = 0; i < samples; i++) { let sample = 0; // Add each formant formants.forEach((freq, idx) => { const envelope = Math.exp(-bandwidths[idx] * i / sampleRate); sample += envelope * Math.sin(2 * Math.PI * freq * i / sampleRate) / formants.length; }); // Add fundamental frequency (pitch) sample += 0.3 * Math.sin(2 * Math.PI * 120 * i / sampleRate); // Apply envelope for more natural speech const globalEnvelope = Math.sin(Math.PI * i / samples); data[i] = sample * globalEnvelope * 0.5; } return data; } /** * Add realistic noise to clean audio */ export function addNoise(cleanAudio, noiseProfile) { const noisyAudio = new Float32Array(cleanAudio); // Add white noise if (noiseProfile.whiteNoise > 0) { for (let i = 0; i < noisyAudio.length; i++) { noisyAudio[i] += (Math.random() - 0.5) * 2 * noiseProfile.whiteNoise; } } // Add pink noise (1/f noise) if (noiseProfile.pinkNoise) { let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; for (let i = 0; i < noisyAudio.length; i++) { const white = Math.random() - 0.5; b0 = 0.99886 * b0 + white * 0.0555179; b1 = 0.99332 * b1 + white * 0.0750759; b2 = 0.96900 * b2 + white * 0.1538520; b3 = 0.86650 * b3 + white * 0.3104856; b4 = 0.55000 * b4 + white * 0.5329522; b5 = -0.7616 * b5 - white * 0.0168980; const pink = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; b6 = white * 0.115926; noisyAudio[i] += pink * noiseProfile.pinkNoise * 0.11; } } // Add brown noise (1/f² noise) if (noiseProfile.brownNoise) { let lastOut = 0; for (let i = 0; i < noisyAudio.length; i++) { const white = Math.random() - 0.5; const brown = (lastOut + (0.02 * white)) / 1.02; lastOut = brown; noisyAudio[i] += brown * noiseProfile.brownNoise * 3.5; } } // Add AC hum if (noiseProfile.hum) { const { freq, level } = noiseProfile.hum; for (let i = 0; i < noisyAudio.length; i++) { noisyAudio[i] += level * Math.sin(2 * Math.PI * freq * i / 48000); } } // Add random crackle/pops if (noiseProfile.crackle) { for (let i = 0; i < noisyAudio.length; i++) { if (Math.random() < noiseProfile.crackle * 0.001) { noisyAudio[i] += (Math.random() - 0.5) * 2; } } } return noisyAudio; } /** * Generate test audio chunks for RNNoise (480 samples each) */ export function generateTestChunks(signalType, noiseProfile, numChunks, frequency = 440) { const chunks = []; const samplesPerChunk = 480; // RNNoise requirement const sampleRate = 48000; // RNNoise requirement // Generate base signal let baseSignal; const totalSamples = samplesPerChunk * numChunks; const duration = totalSamples / sampleRate; switch (signalType) { case 'sine': baseSignal = generateSineWave(frequency, sampleRate, duration); break; case 'speech': baseSignal = generateSpeechLikeSignal(sampleRate, duration); break; case 'silence': baseSignal = new Float32Array(totalSamples); break; } // Add noise const noisySignal = addNoise(baseSignal, noiseProfile); // Split into chunks for (let i = 0; i < numChunks; i++) { const chunk = noisySignal.slice(i * samplesPerChunk, (i + 1) * samplesPerChunk); chunks.push(chunk); } return chunks; } /** * Calculate Signal-to-Noise Ratio (SNR) in dB */ export function calculateSNR(clean, noisy) { if (clean.length !== noisy.length) { throw new Error('Arrays must have same length'); } let signalPower = 0; let noisePower = 0; for (let i = 0; i < clean.length; i++) { signalPower += clean[i] * clean[i]; const noise = noisy[i] - clean[i]; noisePower += noise * noise; } signalPower /= clean.length; noisePower /= clean.length; if (noisePower === 0) return Infinity; return 10 * Math.log10(signalPower / noisePower); } /** * Measure noise reduction effectiveness */ export function measureNoiseReduction(original, processed, expectedReduction = 0.7 // 70% noise reduction expected ) { // Calculate RMS of both signals const originalRMS = Math.sqrt(original.reduce((sum, val) => sum + val * val, 0) / original.length); const processedRMS = Math.sqrt(processed.reduce((sum, val) => sum + val * val, 0) / processed.length); const reduction = 1 - (processedRMS / originalRMS); const passed = reduction >= expectedReduction * 0.9; // 90% of expected return { passed, reduction, message: `Noise reduction: ${(reduction * 100).toFixed(1)}% (expected: ${(expectedReduction * 100).toFixed(1)}%)` }; }