@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.51 â 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/MLNoiseSuppressor.d.ts +3 -1
- package/dist/MLNoiseSuppressor.js +75 -21
- package/dist/index.js +11 -3
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ export declare class MLNoiseSuppressor {
|
|
|
11
11
|
private processingQueue;
|
|
12
12
|
private outputQueue;
|
|
13
13
|
private isProcessing;
|
|
14
|
+
private highPassFilter;
|
|
14
15
|
/**
|
|
15
16
|
* Initialize the ML noise suppressor
|
|
16
17
|
* @param modelUrl URL to the model.json file
|
|
@@ -52,7 +53,8 @@ export declare class MLNoiseSuppressor {
|
|
|
52
53
|
*/
|
|
53
54
|
private startBackgroundProcessing;
|
|
54
55
|
/**
|
|
55
|
-
* Fast audio processing
|
|
56
|
+
* Fast audio processing optimized for voice quality
|
|
57
|
+
* Preserves voice fundamentals (80-250Hz) while reducing noise
|
|
56
58
|
*/
|
|
57
59
|
private processAudioFast;
|
|
58
60
|
/**
|
|
@@ -50,6 +50,8 @@ class MLNoiseSuppressor {
|
|
|
50
50
|
this.processingQueue = [];
|
|
51
51
|
this.outputQueue = [];
|
|
52
52
|
this.isProcessing = false;
|
|
53
|
+
// High-pass filter state for voice optimization (remove <80Hz rumble)
|
|
54
|
+
this.highPassFilter = null;
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
57
|
* Initialize the ML noise suppressor
|
|
@@ -220,22 +222,36 @@ class MLNoiseSuppressor {
|
|
|
220
222
|
return inputStream;
|
|
221
223
|
}
|
|
222
224
|
try {
|
|
223
|
-
console.log('đ¤ Setting up
|
|
225
|
+
console.log('đ¤ [ML] Setting up noise suppression pipeline...');
|
|
226
|
+
console.log('đ¤ [ML] Input stream tracks:', inputStream.getTracks().length);
|
|
224
227
|
// Create MediaStreamSource from input
|
|
225
228
|
const source = this.audioContext.createMediaStreamSource(inputStream);
|
|
229
|
+
console.log('đ¤ [ML] Created MediaStreamSource');
|
|
230
|
+
// Create high-pass filter to remove rumble (<80Hz)
|
|
231
|
+
// This improves voice clarity and matches Google Meet quality
|
|
232
|
+
this.highPassFilter = this.audioContext.createBiquadFilter();
|
|
233
|
+
this.highPassFilter.type = 'highpass';
|
|
234
|
+
this.highPassFilter.frequency.value = 80; // Remove frequencies below 80Hz
|
|
235
|
+
this.highPassFilter.Q.value = 0.7; // Gentle slope
|
|
236
|
+
console.log('đ¤ [ML] Created high-pass filter (80Hz cutoff)');
|
|
226
237
|
// Create destination for output
|
|
227
238
|
const destination = this.audioContext.createMediaStreamDestination();
|
|
239
|
+
console.log('đ¤ [ML] Created destination stream');
|
|
228
240
|
// Create ScriptProcessor for real-time processing
|
|
229
241
|
const bufferSize = 4096;
|
|
230
242
|
const processor = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
|
|
243
|
+
console.log('đ¤ [ML] Created ScriptProcessor with buffer size:', bufferSize);
|
|
231
244
|
// Keep reference to prevent garbage collection
|
|
232
245
|
processor.keepAlive = true;
|
|
233
246
|
// Start background processing worker
|
|
234
247
|
this.startBackgroundProcessing();
|
|
248
|
+
console.log('đ¤ [ML] Background processing started');
|
|
249
|
+
let processedFrames = 0;
|
|
235
250
|
// Process audio with buffering strategy
|
|
236
251
|
processor.onaudioprocess = (event) => {
|
|
237
252
|
const inputBuffer = event.inputBuffer.getChannelData(0);
|
|
238
253
|
const outputBuffer = event.outputBuffer.getChannelData(0);
|
|
254
|
+
processedFrames++;
|
|
239
255
|
// Copy input buffer for processing
|
|
240
256
|
const bufferCopy = new Float32Array(inputBuffer);
|
|
241
257
|
this.processingQueue.push(bufferCopy);
|
|
@@ -247,20 +263,30 @@ class MLNoiseSuppressor {
|
|
|
247
263
|
if (this.outputQueue.length > 0) {
|
|
248
264
|
const processed = this.outputQueue.shift();
|
|
249
265
|
outputBuffer.set(processed);
|
|
266
|
+
// Log occasionally
|
|
267
|
+
if (processedFrames % 100 === 0) {
|
|
268
|
+
console.log(`đ¤ [ML] Processed ${processedFrames} frames, queue: ${this.processingQueue.length}/${this.outputQueue.length}`);
|
|
269
|
+
}
|
|
250
270
|
}
|
|
251
271
|
else {
|
|
252
272
|
// Pass through original audio if processing is behind
|
|
253
273
|
outputBuffer.set(inputBuffer);
|
|
274
|
+
// Log when behind
|
|
275
|
+
if (processedFrames % 100 === 0) {
|
|
276
|
+
console.log(`â ī¸ [ML] Processing behind, passing through (frame ${processedFrames})`);
|
|
277
|
+
}
|
|
254
278
|
}
|
|
255
279
|
};
|
|
256
|
-
// Connect: source -> processor -> destination
|
|
257
|
-
source.connect(
|
|
280
|
+
// Connect: source -> highpass -> processor -> destination
|
|
281
|
+
source.connect(this.highPassFilter);
|
|
282
|
+
this.highPassFilter.connect(processor);
|
|
258
283
|
processor.connect(destination);
|
|
259
|
-
console.log('â
ML
|
|
284
|
+
console.log('â
[ML] Pipeline connected: source -> highpass(80Hz) -> processor -> destination');
|
|
285
|
+
console.log('â
[ML] Output stream tracks:', destination.stream.getTracks().length);
|
|
260
286
|
return destination.stream;
|
|
261
287
|
}
|
|
262
288
|
catch (error) {
|
|
263
|
-
console.error('â Failed to process MediaStream:', error);
|
|
289
|
+
console.error('â [ML] Failed to process MediaStream:', error);
|
|
264
290
|
return inputStream;
|
|
265
291
|
}
|
|
266
292
|
}
|
|
@@ -283,9 +309,6 @@ class MLNoiseSuppressor {
|
|
|
283
309
|
if (this.outputQueue.length > 5) {
|
|
284
310
|
this.outputQueue.shift();
|
|
285
311
|
}
|
|
286
|
-
this.isProcessing = false;
|
|
287
|
-
this.processingQueue = [];
|
|
288
|
-
this.outputQueue = [];
|
|
289
312
|
}
|
|
290
313
|
catch (error) {
|
|
291
314
|
// On error, pass through original
|
|
@@ -301,32 +324,56 @@ class MLNoiseSuppressor {
|
|
|
301
324
|
processLoop();
|
|
302
325
|
}
|
|
303
326
|
/**
|
|
304
|
-
* Fast audio processing
|
|
327
|
+
* Fast audio processing optimized for voice quality
|
|
328
|
+
* Preserves voice fundamentals (80-250Hz) while reducing noise
|
|
305
329
|
*/
|
|
306
330
|
async processAudioFast(inputBuffer) {
|
|
307
331
|
if (!this.model || !this.config || !this.normStats) {
|
|
308
332
|
return inputBuffer;
|
|
309
333
|
}
|
|
310
334
|
try {
|
|
311
|
-
// Simplified fast processing - just apply a learned mask pattern
|
|
312
|
-
// This is much faster than full LSTM inference
|
|
313
335
|
const output = new Float32Array(inputBuffer.length);
|
|
314
|
-
//
|
|
315
|
-
const windowSize =
|
|
316
|
-
|
|
336
|
+
// Use smaller windows for better voice quality
|
|
337
|
+
const windowSize = 128;
|
|
338
|
+
const overlapFactor = 0.5;
|
|
339
|
+
const hopSize = Math.floor(windowSize * (1 - overlapFactor));
|
|
340
|
+
// Apply gentle noise reduction that preserves voice
|
|
341
|
+
for (let i = 0; i < inputBuffer.length; i += hopSize) {
|
|
317
342
|
const end = Math.min(i + windowSize, inputBuffer.length);
|
|
318
343
|
const window = inputBuffer.slice(i, end);
|
|
319
|
-
// Calculate energy
|
|
344
|
+
// Calculate RMS energy
|
|
320
345
|
let energy = 0;
|
|
321
346
|
for (let j = 0; j < window.length; j++) {
|
|
322
347
|
energy += window[j] * window[j];
|
|
323
348
|
}
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
349
|
+
const rms = Math.sqrt(energy / window.length);
|
|
350
|
+
// Voice-optimized noise gate
|
|
351
|
+
// Lower threshold to preserve quiet speech
|
|
352
|
+
// Softer transition to avoid artifacts
|
|
353
|
+
const threshold = 0.005; // More sensitive for voice
|
|
354
|
+
const ratio = 0.5; // Gentler reduction
|
|
355
|
+
let gain;
|
|
356
|
+
if (rms > threshold * 2) {
|
|
357
|
+
// Clear voice - pass through
|
|
358
|
+
gain = 1.0;
|
|
359
|
+
}
|
|
360
|
+
else if (rms > threshold) {
|
|
361
|
+
// Transition zone - smooth interpolation
|
|
362
|
+
const t = (rms - threshold) / threshold;
|
|
363
|
+
gain = 0.7 + (0.3 * t); // 0.7 to 1.0
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// Likely noise - reduce gently
|
|
367
|
+
gain = 0.7; // Much less aggressive than before (was 0.3)
|
|
368
|
+
}
|
|
369
|
+
// Apply gain with smoothing to reduce artifacts
|
|
370
|
+
for (let j = i; j < end && j < inputBuffer.length; j++) {
|
|
371
|
+
// Blend with previous sample for smoothness
|
|
372
|
+
const blendFactor = (j - i) / windowSize;
|
|
373
|
+
const smoothGain = output[j - 1] !== undefined
|
|
374
|
+
? gain * blendFactor + (1 - blendFactor) * (output[j - 1] / (inputBuffer[j - 1] || 1))
|
|
375
|
+
: gain;
|
|
376
|
+
output[j] = inputBuffer[j] * smoothGain;
|
|
330
377
|
}
|
|
331
378
|
}
|
|
332
379
|
return output;
|
|
@@ -363,6 +410,13 @@ class MLNoiseSuppressor {
|
|
|
363
410
|
* Cleanup resources
|
|
364
411
|
*/
|
|
365
412
|
dispose() {
|
|
413
|
+
this.isProcessing = false;
|
|
414
|
+
this.processingQueue = [];
|
|
415
|
+
this.outputQueue = [];
|
|
416
|
+
if (this.highPassFilter) {
|
|
417
|
+
this.highPassFilter.disconnect();
|
|
418
|
+
this.highPassFilter = null;
|
|
419
|
+
}
|
|
366
420
|
if (this.model) {
|
|
367
421
|
this.model.dispose();
|
|
368
422
|
this.model = null;
|
package/dist/index.js
CHANGED
|
@@ -145,22 +145,30 @@ class OdysseySpatialComms extends EventManager_1.EventManager {
|
|
|
145
145
|
return this.mlNoiseSuppressionEnabled && this.mlNoiseSuppressor !== null;
|
|
146
146
|
}
|
|
147
147
|
async produceTrack(track, appData) {
|
|
148
|
+
console.log(`đŦ [SDK] produceTrack called - kind: ${track.kind}, enabled: ${track.enabled}, readyState: ${track.readyState}`);
|
|
148
149
|
let processedTrack = track;
|
|
149
150
|
// Apply ML noise suppression to audio BEFORE sending to MediaSoup
|
|
150
151
|
if (track.kind === 'audio' && this.mlNoiseSuppressionEnabled && this.mlNoiseSuppressor) {
|
|
151
152
|
try {
|
|
152
|
-
console.log('đ¤ Applying ML noise suppression to audio...');
|
|
153
|
+
console.log('đ¤ [SDK] Applying ML noise suppression to audio...');
|
|
153
154
|
const inputStream = new MediaStream([track]);
|
|
155
|
+
console.log('đ¤ [SDK] Created input stream with track');
|
|
154
156
|
const cleanedStream = await this.mlNoiseSuppressor.processMediaStream(inputStream);
|
|
157
|
+
console.log('đ¤ [SDK] Got cleaned stream from ML');
|
|
155
158
|
processedTrack = cleanedStream.getAudioTracks()[0];
|
|
156
|
-
console.log(
|
|
159
|
+
console.log(`â
[SDK] ML noise suppression applied - processed track state: ${processedTrack.readyState}`);
|
|
157
160
|
}
|
|
158
161
|
catch (error) {
|
|
159
|
-
console.error('â ML noise suppression failed, using original track:', error);
|
|
162
|
+
console.error('â [SDK] ML noise suppression failed, using original track:', error);
|
|
160
163
|
processedTrack = track; // Fallback to original track
|
|
161
164
|
}
|
|
162
165
|
}
|
|
166
|
+
else {
|
|
167
|
+
console.log(`âšī¸ [SDK] Skipping ML - kind: ${track.kind}, ML enabled: ${this.mlNoiseSuppressionEnabled}`);
|
|
168
|
+
}
|
|
169
|
+
console.log(`đ¤ [SDK] Producing track to MediaSoup - kind: ${processedTrack.kind}, state: ${processedTrack.readyState}`);
|
|
163
170
|
const producer = await this.mediasoupManager.produce(processedTrack, appData);
|
|
171
|
+
console.log(`â
[SDK] Producer created - id: ${producer.id}, kind: ${producer.kind}`);
|
|
164
172
|
if (this.localParticipant) {
|
|
165
173
|
const isFirstProducer = this.localParticipant.producers.size === 0;
|
|
166
174
|
this.localParticipant.producers.set(producer.id, producer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.53",
|
|
4
4
|
"description": "Odyssey Spatial Audio & Video SDK using MediaSoup for real-time communication with AI-powered noise suppression",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|