UNPKG

murmuraba

Version:

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

180 lines (179 loc) 6.86 kB
// RNNoise Worker Manager - Manages Web Worker pool for audio processing // 2025 best practices: Worker pooling, load balancing, zero-copy transfers export class RNNoiseWorkerManager { constructor(maxWorkers = navigator.hardwareConcurrency || 4, initTimeout = 30000) { this.workers = []; this.taskQueue = []; this.busyWorkers = new Set(); this.workerTasks = new Map(); this.initialized = false; this.maxWorkers = Math.min(maxWorkers, 8); // Cap at 8 workers this.initTimeout = initTimeout; console.log(`[RNNoiseWorkerManager] Initializing with ${this.maxWorkers} workers, timeout: ${initTimeout}ms`); } async initialize() { if (this.initialized) return; const initPromises = []; // Create worker pool for (let i = 0; i < this.maxWorkers; i++) { const worker = new Worker(new URL('../workers/rnnoise.worker.ts', import.meta.url), { type: 'module' }); const initPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Worker ${i} initialization timeout after ${this.initTimeout}ms`)); }, this.initTimeout); const messageHandler = (event) => { if (event.data.type === 'initialized') { clearTimeout(timeout); worker.removeEventListener('message', messageHandler); if (event.data.success) { resolve(); } else { reject(new Error(event.data.error || 'Worker initialization failed')); } } }; worker.addEventListener('message', messageHandler); worker.addEventListener('error', (error) => { clearTimeout(timeout); reject(error); }); }); // Set up permanent message handler worker.addEventListener('message', (event) => this.handleWorkerMessage(worker, event)); worker.addEventListener('error', (error) => this.handleWorkerError(worker, error)); // Initialize worker worker.postMessage({ type: 'init' }); this.workers.push(worker); initPromises.push(initPromise); } // Wait for all workers to initialize await Promise.all(initPromises); this.initialized = true; console.log(`[RNNoiseWorkerManager] All ${this.maxWorkers} workers initialized`); } async processBuffer(buffer) { if (!this.initialized) { await this.initialize(); } return new Promise((resolve, reject) => { const task = { id: crypto.randomUUID(), buffer, resolve, reject }; // Try to find an idle worker const idleWorker = this.getIdleWorker(); if (idleWorker) { this.assignTask(idleWorker, task); } else { // Queue task if all workers are busy this.taskQueue.push(task); } }); } getIdleWorker() { for (const worker of this.workers) { if (!this.busyWorkers.has(worker)) { return worker; } } return null; } assignTask(worker, task) { this.busyWorkers.add(worker); this.workerTasks.set(worker, task); // Send task to worker with zero-copy transfer worker.postMessage({ type: 'process', id: task.id, data: { buffer: task.buffer, sampleRate: 48000 } }, [task.buffer.buffer]); // Transfer ownership } handleWorkerMessage(worker, event) { const { type, id, data, error } = event.data; if (type === 'processed') { const task = this.workerTasks.get(worker); if (task && task.id === id) { task.resolve(data.buffer); this.completeTask(worker); } } else if (type === 'error') { const task = this.workerTasks.get(worker); if (task && task.id === id) { task.reject(new Error(error)); this.completeTask(worker); } } } handleWorkerError(worker, error) { console.error('[RNNoiseWorkerManager] Worker error:', error); const task = this.workerTasks.get(worker); if (task) { task.reject(new Error(`Worker error: ${error.message}`)); this.completeTask(worker); } // Replace failed worker this.replaceWorker(worker); } completeTask(worker) { this.busyWorkers.delete(worker); this.workerTasks.delete(worker); // Process next task in queue if (this.taskQueue.length > 0) { const nextTask = this.taskQueue.shift(); this.assignTask(worker, nextTask); } } async replaceWorker(failedWorker) { const index = this.workers.indexOf(failedWorker); if (index === -1) return; // Terminate failed worker failedWorker.terminate(); // Create new worker const newWorker = new Worker(new URL('../workers/rnnoise.worker.ts', import.meta.url), { type: 'module' }); // Initialize new worker newWorker.addEventListener('message', (event) => this.handleWorkerMessage(newWorker, event)); newWorker.addEventListener('error', (error) => this.handleWorkerError(newWorker, error)); newWorker.postMessage({ type: 'init' }); // Replace in array this.workers[index] = newWorker; } getStats() { return { totalWorkers: this.workers.length, busyWorkers: this.busyWorkers.size, queueLength: this.taskQueue.length }; } async destroy() { // Clear queue for (const task of this.taskQueue) { task.reject(new Error('Worker manager destroyed')); } this.taskQueue = []; // Terminate all workers await Promise.all(this.workers.map(worker => { worker.postMessage({ type: 'destroy' }); return new Promise(resolve => { setTimeout(() => { worker.terminate(); resolve(); }, 100); }); })); this.workers = []; this.busyWorkers.clear(); this.workerTasks.clear(); this.initialized = false; console.log('[RNNoiseWorkerManager] Destroyed'); } }