JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q56118F
  • License MIT

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

Package Exports

  • murmuraba
  • murmuraba/package.json

Readme

🎵 Murmuraba - Real-time Neural Noise Reduction with Chunked Streaming

npm version React TypeScript License: MIT

Professional-grade real-time audio streaming with neural noise reduction using RNNoise WASM. Process long recordings efficiently with automatic chunking - no need to wait for recording completion. Now with advanced Voice Activity Detection (VAD) capabilities!

🚀 Why Murmuraba?

Traditional audio recording libraries make you wait until the user stops recording to process the entire audio file. This approach fails for:

  • Long recordings (podcasts, meetings, interviews)
  • Real-time transcription needs
  • Live streaming applications
  • Memory-constrained environments

Murmuraba solves this with automatic chunked streaming: Audio is processed in real-time segments while recording continues, giving you instant access to processed chunks with neural noise reduction applied.

📦 Installation

npm install murmuraba

🎯 Core Concept: The Hook-First Approach

Murmuraba is built around the powerful useMurmubaraEngine hook that handles everything:

import { useMurmubaraEngine } from 'murmuraba';

function YourAudioApp() {
  const {
    recordingState,      // Real-time state with chunks array
    startRecording,      // Start chunked recording
    stopRecording,       // Stop and cleanup
    // ... more controls
  } = useMurmubaraEngine();
  
  // Your custom UI here
}

🔄 The Chunked Streaming Flow

How It Works

Microphone Input (Continuous Stream)
         ↓
    [8 seconds] → Chunk 1 → Process → Noise Reduced → Available Immediately
         ↓
    [8 seconds] → Chunk 2 → Process → Noise Reduced → Available Immediately
         ↓
    [8 seconds] → Chunk 3 → Process → Noise Reduced → Available Immediately
         ↓
    ... continues until stop

Each chunk is processed independently with:

  • Neural noise reduction via RNNoise
  • Dual audio URLs (original + processed)
  • Real-time metrics (VAD, noise levels)
  • Instant availability for playback/export/upload

Basic Implementation

import { useMurmubaraEngine } from 'murmuraba';
import { useEffect } from 'react';

function StreamingRecorder() {
  const {
    recordingState,
    startRecording,
    stopRecording,
    exportChunkAsWav,
  } = useMurmubaraEngine({
    defaultChunkDuration: 8  // 8-second chunks
  });

  // Watch for new chunks in real-time
  useEffect(() => {
    const latestChunk = recordingState.chunks[recordingState.chunks.length - 1];
    
    if (latestChunk) {
      console.log('New chunk available!', {
        id: latestChunk.id,
        duration: latestChunk.duration,
        processedUrl: latestChunk.processedAudioUrl,  // Noise-reduced audio
        originalUrl: latestChunk.originalAudioUrl,    // Raw microphone input
        noiseReduction: latestChunk.noiseRemoved      // Percentage reduced
      });
      
      // You can immediately:
      // - Play it back
      // - Send to server
      // - Transcribe it
      // - Export it
      // No need to wait for recording to finish!
    }
  }, [recordingState.chunks.length]);

  return (
    <div>
      <button onClick={() => startRecording(10)}>
        Start Recording (10s chunks)
      </button>
      <button onClick={stopRecording}>Stop</button>
      
      {/* Real-time chunk list */}
      {recordingState.chunks.map(chunk => (
        <div key={chunk.id}>
          Chunk {chunk.index}: {chunk.duration}ms
          <audio src={chunk.processedAudioUrl} controls />
          <button onClick={() => exportChunkAsWav(chunk.id)}>
            Export WAV
          </button>
        </div>
      ))}
    </div>
  );
}

🧠 Neural Noise Reduction with RNNoise

Every audio chunk passes through RNNoise, a recurrent neural network trained on thousands of hours of speech data. This happens in real-time using WebAssembly for native performance.

The Processing Pipeline

// What happens inside each chunk:
Raw Audio → RNNoise Neural Network → Clean Audio
           ↓
    - Removes background noise
    - Preserves voice clarity  
    - Maintains natural sound
    - ~85% noise reduction

Comparing Original vs Processed

function NoiseComparison() {
  const { recordingState, toggleChunkPlayback } = useMurmubaraEngine();
  
  return (
    <div>
      {recordingState.chunks.map(chunk => (
        <div key={chunk.id}>
          {/* Play original (noisy) */}
          <button onClick={() => toggleChunkPlayback(chunk.id, 'original')}>
            Play Original
          </button>
          
          {/* Play processed (clean) */}
          <button onClick={() => toggleChunkPlayback(chunk.id, 'processed')}>
            Play Processed
          </button>
          
          <span>Noise Reduced: {chunk.noiseRemoved}%</span>
        </div>
      ))}
    </div>
  );
}

📊 Real-time Chunk Data Structure

Each chunk in recordingState.chunks contains:

interface ProcessedChunk {
  // Identifiers
  id: string;                      // Unique chunk ID
  index: number;                   // Sequential index (0, 1, 2...)
  
  // Audio URLs (Blob URLs ready for immediate use)
  processedAudioUrl?: string;      // blob:http://... (noise-reduced)
  originalAudioUrl?: string;       // blob:http://... (raw input)
  
  // Timing
  duration: number;                // Duration in milliseconds
  startTime: number;               // Recording start timestamp
  endTime: number;                 // Recording end timestamp
  
  // Processing Metrics
  noiseRemoved: number;            // Noise reduction % (0-100)
  averageVad: number;              // Voice activity average (0-1)
  vadData: VadPoint[];             // Voice activity timeline
  
  // Quality Metrics
  metrics: {
    processingLatency: number;     // Processing time in ms
    frameCount: number;            // Audio frames processed
    inputLevel: number;            // Input volume (0-1)
    outputLevel: number;           // Output volume (0-1)
    noiseReductionLevel: number;  // Reduction applied (0-1)
  };
  
  // File Information
  originalSize: number;            // Original blob size in bytes
  processedSize: number;           // Processed blob size in bytes
  
  // State
  isPlaying: boolean;              // Currently playing
  isExpanded: boolean;             // UI expanded state
  isValid: boolean;                // Processing succeeded
  errorMessage?: string;           // Error details if failed
}

🎮 Complete Hook API

const {
  // 📊 State
  recordingState: {
    isRecording: boolean,          // Currently recording
    isPaused: boolean,              // Recording paused
    chunks: ProcessedChunk[],       // All processed chunks
    recordingTime: number,          // Total recording duration
    currentChunkTime: number,       // Current chunk progress
    playingChunks: Set<string>,     // Currently playing chunk IDs
    expandedChunk: string | null,   // Expanded chunk ID
  },
  
  // 🎙️ Recording Controls
  startRecording: (chunkDuration?: number) => Promise<void>,
  stopRecording: () => void,
  pauseRecording: () => void,
  resumeRecording: () => void,
  clearRecordings: () => void,
  
  // 🔊 Playback Controls
  toggleChunkPlayback: (chunkId: string, type?: 'processed' | 'original') => Promise<void>,
  stopAllPlayback: () => void,
  
  // 💾 Export Functions
  exportChunkAsWav: (chunkId: string, type?: 'processed' | 'original') => Promise<void>,
  exportChunkAsMp3: (chunkId: string, type?: 'processed' | 'original') => Promise<void>,
  downloadChunk: (chunkId: string, format: 'wav' | 'mp3', type?: 'processed' | 'original') => Promise<void>,
  exportAllChunks: () => Promise<void>,
  
  // 🎚️ Audio Controls
  inputGain: number,                 // Current gain (0.5-3.0)
  setInputGain: (gain: number) => void,
  agcEnabled: boolean,               // Auto gain control
  setAgcEnabled: (enabled: boolean) => Promise<void>,
  
  // 🔧 Engine Management
  isInitialized: boolean,
  isLoading: boolean,
  error: string | null,
  initialize: () => Promise<void>,
  reinitialize: () => Promise<void>,
  metrics: ProcessingMetrics | null,
  diagnostics: EngineDiagnostics | null,
  
} = useMurmubaraEngine(options);

🚀 Advanced Usage Patterns

Pattern 1: Auto-Upload Chunks to Server

function AutoUploadRecorder() {
  const { recordingState, startRecording } = useMurmubaraEngine();
  const uploadedRef = useRef(new Set());
  
  useEffect(() => {
    recordingState.chunks.forEach(async chunk => {
      if (!uploadedRef.current.has(chunk.id)) {
        uploadedRef.current.add(chunk.id);
        
        // Convert blob URL to blob
        const response = await fetch(chunk.processedAudioUrl!);
        const blob = await response.blob();
        
        // Upload to your server
        const formData = new FormData();
        formData.append('audio', blob, `chunk-${chunk.id}.wav`);
        formData.append('metadata', JSON.stringify({
          duration: chunk.duration,
          noiseReduction: chunk.noiseRemoved,
          vad: chunk.averageVad
        }));
        
        await fetch('/api/upload-chunk', {
          method: 'POST',
          body: formData
        });
        
        console.log(`Uploaded chunk ${chunk.id}`);
      }
    });
  }, [recordingState.chunks]);
  
  return <button onClick={() => startRecording(5)}>Start 5s Chunks</button>;
}

Pattern 2: Real-time Transcription

function LiveTranscription() {
  const { recordingState } = useMurmubaraEngine();
  const [transcripts, setTranscripts] = useState<Record<string, string>>({});
  
  useEffect(() => {
    const latestChunk = recordingState.chunks[recordingState.chunks.length - 1];
    
    if (latestChunk && !transcripts[latestChunk.id]) {
      // Send to transcription service
      transcribeChunk(latestChunk).then(text => {
        setTranscripts(prev => ({
          ...prev,
          [latestChunk.id]: text
        }));
      });
    }
  }, [recordingState.chunks.length]);
  
  return (
    <div>
      {recordingState.chunks.map(chunk => (
        <p key={chunk.id}>
          [{chunk.index}]: {transcripts[chunk.id] || 'Transcribing...'}
        </p>
      ))}
    </div>
  );
}

Pattern 3: Voice Activity Detection (VAD)

function VoiceDetector() {
  const { recordingState, metrics } = useMurmubaraEngine();
  
  return (
    <div>
      {/* Real-time VAD */}
      {metrics && (
        <div>
          Voice Active: {metrics.vadLevel > 0.5 ? '🎤 Speaking' : '🔇 Silent'}
          Level: {(metrics.vadLevel * 100).toFixed(0)}%
        </div>
      )}
      
      {/* Historical VAD per chunk */}
      {recordingState.chunks.map(chunk => (
        <div key={chunk.id}>
          Chunk {chunk.index}: {(chunk.averageVad * 100).toFixed(0)}% voice activity
        </div>
      ))}
    </div>
  );
}

Pattern 4: Advanced VAD Analysis (NEW in v3.0.3)

import { murmubaraVAD, extractAudioMetadata } from 'murmuraba';

function AdvancedVADAnalysis() {
  const analyzeAudio = async (audioBuffer: ArrayBuffer) => {
    // Get accurate audio metadata
    const metadata = extractAudioMetadata(audioBuffer);
    console.log(`Duration: ${metadata.duration}s, Format: ${metadata.format}`);
    
    // Perform detailed VAD analysis
    const vadResult = await murmubaraVAD(audioBuffer);
    console.log(`Voice Activity: ${(vadResult.average * 100).toFixed(1)}%`);
    console.log(`Voice Segments: ${vadResult.voiceSegments?.length || 0}`);
    
    // Analyze voice segments
    vadResult.voiceSegments?.forEach((segment, i) => {
      console.log(`Segment ${i + 1}: ${segment.startTime}s - ${segment.endTime}s (confidence: ${segment.confidence})`);
    });
    
    return vadResult;
  };
  
  return (
    <div>
      {/* Use with recorded chunks */}
      <button onClick={async () => {
        const response = await fetch(chunk.processedAudioUrl!);
        const arrayBuffer = await response.arrayBuffer();
        const analysis = await analyzeAudio(arrayBuffer);
        // Display results...
      }}>
        Analyze VAD
      </button>
    </div>
  );
}

🎯 New in v3.0.3: Advanced Voice Activity Detection

Murmuraba now includes powerful VAD analysis functions for detailed audio inspection:

murmubaraVAD(buffer: ArrayBuffer): Promise<VADResult>

Analyzes audio for voice activity using multiple algorithms:

  • Energy-based detection with adaptive noise floor
  • Zero-crossing rate analysis for voiced/unvoiced classification
  • RNNoise integration when available
  • Temporal smoothing for stable results

Returns:

{
  average: number;              // Average VAD score (0.0-1.0)
  scores: number[];             // Frame-by-frame VAD scores
  metrics: VADMetric[];         // Detailed metrics per frame
  voiceSegments: VoiceSegment[]; // Detected voice segments
}

extractAudioMetadata(buffer: ArrayBuffer): AudioMetadata

Extracts accurate metadata from audio files:

  • Supports WAV, MP3, WebM/Opus formats
  • Returns precise duration, sample rate, channels, bit depth
  • No more guessing audio duration!

Example:

const metadata = extractAudioMetadata(audioBuffer);
console.log(`Duration: ${metadata.duration}s`);
console.log(`Format: ${metadata.format}`);
console.log(`Sample Rate: ${metadata.sampleRate}Hz`);

⚙️ Configuration Options

interface UseMurmubaraEngineOptions {
  // Chunking
  defaultChunkDuration?: number;    // Seconds per chunk (default: 8)
  
  // Audio Processing
  bufferSize?: number;              // Audio buffer size (default: 16384)
  sampleRate?: number;              // Sample rate Hz (default: 48000)
  denoiseStrength?: number;         // Noise reduction 0-1 (default: 0.85)
  
  // Gain Control
  inputGain?: number;               // Initial gain 0.5-3.0 (default: 1.0)
  enableAGC?: boolean;              // Auto gain control (default: true)
  
  // Voice Detection
  spectralFloorDb?: number;         // Noise floor dB (default: -80)
  noiseFloorDb?: number;            // VAD threshold dB (default: -60)
  
  // Performance
  enableMetrics?: boolean;          // Real-time metrics (default: true)
  metricsUpdateInterval?: number;   // Update interval ms (default: 100)
  
  // Initialization
  autoInitialize?: boolean;         // Auto-init on mount (default: false)
  allowDegraded?: boolean;          // Allow fallback mode (default: true)
  
  // Debugging
  logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
}

🔧 Memory Management

Murmuraba automatically manages blob URLs to prevent memory leaks:

// URLs are automatically created and tracked
const processedUrl = URL.createObjectURL(processedBlob);  // ✅ Tracked

// URLs are automatically revoked when:
clearRecordings();  // All URLs revoked
// or on component unmount

📈 Performance Considerations

  • Chunk Duration: 5-10 seconds optimal for most use cases
  • Buffer Size: Larger = better quality, more latency
  • Sample Rate: 48kHz for quality, 16kHz for size
  • Memory: Each chunk ~100-500KB depending on duration

🎨 Building Custom UIs

Since you have full control via the hook, build any UI you want:

function MinimalRecorder() {
  const { recordingState, startRecording, stopRecording } = useMurmubaraEngine();
  
  if (!recordingState.isRecording) {
    return <button onClick={() => startRecording()}>🎤 Record</button>;
  }
  
  return (
    <div>
      <button onClick={stopRecording}>⏹ Stop</button>
      <div>
        Recording: {recordingState.recordingTime}s
        Chunks: {recordingState.chunks.length}
      </div>
    </div>
  );
}

🚨 Error Handling

function RobustRecorder() {
  const { 
    error, 
    isInitialized, 
    initialize, 
    startRecording 
  } = useMurmubaraEngine();
  
  const handleStart = async () => {
    try {
      if (!isInitialized) {
        await initialize();
      }
      await startRecording();
    } catch (err) {
      console.error('Recording failed:', err);
      // Handle specific errors
      if (err.message.includes('microphone')) {
        alert('Please allow microphone access');
      }
    }
  };
  
  if (error) {
    return <div>Error: {error}</div>;
  }
  
  return <button onClick={handleStart}>Start</button>;
}

🤝 Contributing

Contributions welcome! Please check our GitHub repository.

📄 License

MIT © Murmuraba Team


Built with ❤️ for developers who need professional audio streaming with neural noise reduction.