@whereby.com/audio-denoiser 0.0.0-canary-20260522073902

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/LICENCE.md ADDED
@@ -0,0 +1,23 @@
1
+ ## MIT License
2
+
3
+ Copyright (c) 2024 Whereby AS (https://www.whereby.com)
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # `@whereby.com/audio-denoiser`
2
+
3
+ Audio denoiser (noise suppression) for microphone streams.
4
+
5
+ Wraps the input `MediaStream` in an `AudioWorklet` that runs an RNNoise-based
6
+ WebAssembly model, returning a new `MediaStream` with the cleaned audio.
7
+ Static assets (the WASM model and worklet script) are hosted on a CDN and
8
+ loaded at runtime.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @whereby.com/audio-denoiser
14
+ ```
15
+
16
+ or
17
+
18
+ ```bash
19
+ yarn add @whereby.com/audio-denoiser
20
+ ```
21
+
22
+ or
23
+
24
+ ```bash
25
+ pnpm add @whereby.com/audio-denoiser
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```typescript
31
+ import { applyAudioDenoiser, canUse } from "@whereby.com/audio-denoiser";
32
+
33
+ if (canUse()) {
34
+ const { outputStream, stop } = await applyAudioDenoiser({
35
+ inputStream: micStream,
36
+ doCaptureException: (err, ctx) => reportError(err, ctx),
37
+ });
38
+
39
+ // hand `outputStream` to your RTC pipeline; call `stop()` when done
40
+ }
41
+ ```
42
+
43
+ `applyAudioDenoiser` also returns `audioContext` and `denoiserNode` for
44
+ consumers that need to share the underlying `AudioContext` (e.g. wiring an
45
+ audio analyzer onto the same node without creating a second source).
46
+
47
+ ## Development
48
+
49
+ The static assets (WASM model + worklet script) are hosted on a CDN in
50
+ production builds. To exercise the local copies during development, set the
51
+ environment variable `REACT_APP_IS_DEV=true` before building. When unset (or
52
+ `false`), the build references the CDN as in production.
@@ -0,0 +1,70 @@
1
+ let instance;
2
+ let heapFloat32;
3
+
4
+ class DenoiserProcessor extends AudioWorkletProcessor {
5
+ constructor(options) {
6
+ super({
7
+ ...options,
8
+ numberOfInputs: 1,
9
+ numberOfOutputs: 1,
10
+ outputChannelCount: [1],
11
+ });
12
+
13
+ this.alive = true;
14
+
15
+ (async () => {
16
+ try {
17
+ if (!instance) {
18
+ const wasmModule = (await WebAssembly.instantiate(options.processorOptions.wasmBuffer)).module;
19
+ instance = new WebAssembly.Instance(wasmModule).exports;
20
+ heapFloat32 = new Float32Array(instance.memory.buffer);
21
+ }
22
+ this.state = instance.newState();
23
+ this.active = true;
24
+ this.port.onmessage = ({ data: keepalive }) => {
25
+ if (this.alive) {
26
+ if (!keepalive) {
27
+ this.active = false;
28
+ this.alive = false;
29
+ instance.deleteState(this.state);
30
+ this.state = undefined;
31
+ }
32
+ }
33
+ };
34
+ } catch (ex) {
35
+ this.port.postMessage({ error: ex.toString() });
36
+ }
37
+ })();
38
+ }
39
+
40
+ process([input], [output]) {
41
+ if (this.active) {
42
+ if (!input.length) {
43
+ return true;
44
+ }
45
+ // ensure the state is truthy before proceeding, otherwise just passthrough audio
46
+ if (!this.state) {
47
+ try {
48
+ output[0].set(input[0]);
49
+ } catch (_) {}
50
+ return true;
51
+ }
52
+
53
+ heapFloat32.set(input[0], instance.getInput(this.state) / 4);
54
+ const o = output[0];
55
+ const ptr4 = instance.pipe(this.state, o.length) / 4;
56
+ if (ptr4) o.set(heapFloat32.subarray(ptr4, ptr4 + o.length));
57
+ return true;
58
+ }
59
+ if (this.alive) {
60
+ // not yet loaded, or error initalizing wasm, so try to passthrough audio
61
+ try {
62
+ output[0].set(input[0]);
63
+ } catch (_) {}
64
+ return true;
65
+ }
66
+ return false; // we signal it is ok to destroy the processor
67
+ }
68
+ }
69
+
70
+ registerProcessor("denoiser", DenoiserProcessor);
package/dist/index.cjs ADDED
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ var media = require('@whereby.com/media');
4
+
5
+ /******************************************************************************
6
+ Copyright (c) Microsoft Corporation.
7
+
8
+ Permission to use, copy, modify, and/or distribute this software for any
9
+ purpose with or without fee is hereby granted.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
+ PERFORMANCE OF THIS SOFTWARE.
18
+ ***************************************************************************** */
19
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
20
+
21
+
22
+ function __awaiter(thisArg, _arguments, P, generator) {
23
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
24
+ return new (P || (P = Promise))(function (resolve, reject) {
25
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
26
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
27
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
28
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
29
+ });
30
+ }
31
+
32
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
33
+ var e = new Error(message);
34
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
35
+ };
36
+
37
+ const CDN_BASE_URL = "https://cdn.srv.whereby.com/audio-denoiser/v0-0-0-canary-20260522073902-canary";
38
+ const getAssetUrl = (path) => {
39
+ {
40
+ return `${CDN_BASE_URL}/${path}`;
41
+ }
42
+ };
43
+ const assetUrls = {
44
+ denoiser: {
45
+ wasm: getAssetUrl("assets/denoiser/model.ext.wasm") ,
46
+ processor: getAssetUrl("assets/denoiser/processor.ext.js") ,
47
+ },
48
+ };
49
+
50
+ const audioContexts = new Map();
51
+ const AudioContextCtor = typeof AudioContext !== "undefined"
52
+ ? AudioContext
53
+ : globalThis.webkitAudioContext;
54
+ function getAudioContext(name, options) {
55
+ var _a;
56
+ if (!AudioContextCtor) {
57
+ throw new Error("AudioContext is not supported in this environment");
58
+ }
59
+ const key = `${name}:${(_a = options === null || options === void 0 ? void 0 : options.sampleRate) !== null && _a !== void 0 ? _a : "default"}`;
60
+ let ctx = audioContexts.get(key);
61
+ if (!ctx) {
62
+ ctx = new AudioContextCtor(options);
63
+ audioContexts.set(key, ctx);
64
+ }
65
+ return ctx;
66
+ }
67
+
68
+ const getWasmUrl = () => __awaiter(void 0, void 0, void 0, function* () {
69
+ {
70
+ return assetUrls.denoiser.wasm;
71
+ }
72
+ });
73
+ const getProcessorUrl = () => __awaiter(void 0, void 0, void 0, function* () {
74
+ {
75
+ return assetUrls.denoiser.processor;
76
+ }
77
+ });
78
+ let wasmBufferPromise = null;
79
+ const loadWasmBuffer = (doCaptureException) => __awaiter(void 0, void 0, void 0, function* () {
80
+ if (!wasmBufferPromise) {
81
+ wasmBufferPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
82
+ const url = yield getWasmUrl();
83
+ const response = yield fetch(url);
84
+ if (!response.ok) {
85
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(`Failed to fetch denoiser model: ${response.status} ${response.statusText}`), { tags: { from: "audioDenoiser.loadWasmBuffer" } });
86
+ }
87
+ return response.arrayBuffer();
88
+ }))();
89
+ }
90
+ return wasmBufferPromise;
91
+ });
92
+ let workletRegistered = null;
93
+ const ensureWorkletRegistered = (context) => __awaiter(void 0, void 0, void 0, function* () {
94
+ if (!workletRegistered)
95
+ workletRegistered = new WeakSet();
96
+ if (workletRegistered.has(context))
97
+ return;
98
+ const processorUrl = yield getProcessorUrl();
99
+ yield context.audioWorklet.addModule(processorUrl);
100
+ workletRegistered.add(context);
101
+ });
102
+ class Denoiser extends AudioWorkletNode {
103
+ constructor(context, wasmBuffer) {
104
+ super(context, "denoiser", {
105
+ channelCountMode: "explicit",
106
+ channelCount: 1,
107
+ channelInterpretation: "speakers",
108
+ numberOfInputs: 1,
109
+ numberOfOutputs: 1,
110
+ outputChannelCount: [1],
111
+ processorOptions: {
112
+ wasmBuffer,
113
+ },
114
+ });
115
+ }
116
+ }
117
+ const canUse = () => typeof AudioWorkletNode !== "undefined" && typeof AudioContext !== "undefined";
118
+ const warmup = () => __awaiter(void 0, void 0, void 0, function* () {
119
+ yield loadWasmBuffer();
120
+ });
121
+ const applyAudioDenoiser = (_a) => __awaiter(void 0, [_a], void 0, function* ({ inputStream, doCaptureException, }) {
122
+ const inputTrack = inputStream.getAudioTracks()[0];
123
+ const sampleRate = inputTrack === null || inputTrack === void 0 ? void 0 : inputTrack.getSettings().sampleRate;
124
+ const audioContext = getAudioContext("audiodenoiser", sampleRate ? { sampleRate } : undefined);
125
+ const destination = audioContext.createMediaStreamDestination();
126
+ const [wasmBuffer] = yield Promise.all([loadWasmBuffer(doCaptureException), ensureWorkletRegistered(audioContext)]);
127
+ const source = audioContext.createMediaStreamSource(inputStream);
128
+ const denoiserNode = new Denoiser(audioContext, wasmBuffer);
129
+ denoiserNode.port.onmessage = (event) => {
130
+ var _a;
131
+ if ((_a = event.data) === null || _a === void 0 ? void 0 : _a.error) {
132
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(event.data.error), {
133
+ tags: { from: "audioDenoiser.onmessage" },
134
+ extra: { sampleRate },
135
+ });
136
+ }
137
+ };
138
+ denoiserNode.onprocessorerror = (errorEvent) => {
139
+ const event = errorEvent;
140
+ const error = new Error(`Denoiser processor error: ${event.error} - ${event.message}`);
141
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(error, {
142
+ tags: { from: "audioDenoiser.onprocessorerror" },
143
+ extra: { sampleRate },
144
+ });
145
+ };
146
+ denoiserNode.connect(destination);
147
+ source.connect(denoiserNode);
148
+ const outputTrack = destination.stream.getAudioTracks()[0];
149
+ if (outputTrack && inputTrack) {
150
+ Object.defineProperty(outputTrack, "enabled", {
151
+ get() {
152
+ return inputTrack.enabled;
153
+ },
154
+ set(value) {
155
+ inputTrack.enabled = value;
156
+ },
157
+ });
158
+ }
159
+ if (outputTrack) {
160
+ media.trackAnnotations(outputTrack).isEffectTrack = true;
161
+ }
162
+ let stopped = false;
163
+ const stop = () => {
164
+ if (stopped)
165
+ return;
166
+ stopped = true;
167
+ try {
168
+ denoiserNode.port.postMessage(false);
169
+ source.disconnect();
170
+ denoiserNode.disconnect();
171
+ }
172
+ catch (error) {
173
+ console.error("Error stopping audio denoiser", error);
174
+ }
175
+ };
176
+ return { outputStream: destination.stream, audioContext, denoiserNode, stop };
177
+ });
178
+
179
+ exports.applyAudioDenoiser = applyAudioDenoiser;
180
+ exports.canUse = canUse;
181
+ exports.warmup = warmup;
@@ -0,0 +1,21 @@
1
+ type CaptureExceptionContext = {
2
+ tags?: Record<string, string>;
3
+ extra?: Record<string, unknown>;
4
+ };
5
+ type CaptureExceptionFn = (err: Error, ctx?: CaptureExceptionContext) => void;
6
+ type ApplyAudioDenoiserParams = {
7
+ inputStream: MediaStream;
8
+ doCaptureException?: CaptureExceptionFn;
9
+ };
10
+ type AudioDenoiserHandle = {
11
+ outputStream: MediaStream;
12
+ audioContext: AudioContext;
13
+ denoiserNode: AudioWorkletNode;
14
+ stop: () => void;
15
+ };
16
+ declare const canUse: () => boolean;
17
+ declare const warmup: () => Promise<void>;
18
+ declare const applyAudioDenoiser: ({ inputStream, doCaptureException, }: ApplyAudioDenoiserParams) => Promise<AudioDenoiserHandle>;
19
+
20
+ export { applyAudioDenoiser, canUse, warmup };
21
+ export type { ApplyAudioDenoiserParams, AudioDenoiserHandle, CaptureExceptionContext, CaptureExceptionFn };
@@ -0,0 +1,21 @@
1
+ type CaptureExceptionContext = {
2
+ tags?: Record<string, string>;
3
+ extra?: Record<string, unknown>;
4
+ };
5
+ type CaptureExceptionFn = (err: Error, ctx?: CaptureExceptionContext) => void;
6
+ type ApplyAudioDenoiserParams = {
7
+ inputStream: MediaStream;
8
+ doCaptureException?: CaptureExceptionFn;
9
+ };
10
+ type AudioDenoiserHandle = {
11
+ outputStream: MediaStream;
12
+ audioContext: AudioContext;
13
+ denoiserNode: AudioWorkletNode;
14
+ stop: () => void;
15
+ };
16
+ declare const canUse: () => boolean;
17
+ declare const warmup: () => Promise<void>;
18
+ declare const applyAudioDenoiser: ({ inputStream, doCaptureException, }: ApplyAudioDenoiserParams) => Promise<AudioDenoiserHandle>;
19
+
20
+ export { applyAudioDenoiser, canUse, warmup };
21
+ export type { ApplyAudioDenoiserParams, AudioDenoiserHandle, CaptureExceptionContext, CaptureExceptionFn };
@@ -0,0 +1,21 @@
1
+ type CaptureExceptionContext = {
2
+ tags?: Record<string, string>;
3
+ extra?: Record<string, unknown>;
4
+ };
5
+ type CaptureExceptionFn = (err: Error, ctx?: CaptureExceptionContext) => void;
6
+ type ApplyAudioDenoiserParams = {
7
+ inputStream: MediaStream;
8
+ doCaptureException?: CaptureExceptionFn;
9
+ };
10
+ type AudioDenoiserHandle = {
11
+ outputStream: MediaStream;
12
+ audioContext: AudioContext;
13
+ denoiserNode: AudioWorkletNode;
14
+ stop: () => void;
15
+ };
16
+ declare const canUse: () => boolean;
17
+ declare const warmup: () => Promise<void>;
18
+ declare const applyAudioDenoiser: ({ inputStream, doCaptureException, }: ApplyAudioDenoiserParams) => Promise<AudioDenoiserHandle>;
19
+
20
+ export { applyAudioDenoiser, canUse, warmup };
21
+ export type { ApplyAudioDenoiserParams, AudioDenoiserHandle, CaptureExceptionContext, CaptureExceptionFn };
package/dist/index.mjs ADDED
@@ -0,0 +1,177 @@
1
+ import { trackAnnotations } from '@whereby.com/media';
2
+
3
+ /******************************************************************************
4
+ Copyright (c) Microsoft Corporation.
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any
7
+ purpose with or without fee is hereby granted.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
16
+ ***************************************************************************** */
17
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
18
+
19
+
20
+ function __awaiter(thisArg, _arguments, P, generator) {
21
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
22
+ return new (P || (P = Promise))(function (resolve, reject) {
23
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
24
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
25
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
26
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
27
+ });
28
+ }
29
+
30
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
31
+ var e = new Error(message);
32
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
33
+ };
34
+
35
+ const CDN_BASE_URL = "https://cdn.srv.whereby.com/audio-denoiser/v0-0-0-canary-20260522073902-canary";
36
+ const getAssetUrl = (path) => {
37
+ {
38
+ return `${CDN_BASE_URL}/${path}`;
39
+ }
40
+ };
41
+ const assetUrls = {
42
+ denoiser: {
43
+ wasm: getAssetUrl("assets/denoiser/model.ext.wasm") ,
44
+ processor: getAssetUrl("assets/denoiser/processor.ext.js") ,
45
+ },
46
+ };
47
+
48
+ const audioContexts = new Map();
49
+ const AudioContextCtor = typeof AudioContext !== "undefined"
50
+ ? AudioContext
51
+ : globalThis.webkitAudioContext;
52
+ function getAudioContext(name, options) {
53
+ var _a;
54
+ if (!AudioContextCtor) {
55
+ throw new Error("AudioContext is not supported in this environment");
56
+ }
57
+ const key = `${name}:${(_a = options === null || options === void 0 ? void 0 : options.sampleRate) !== null && _a !== void 0 ? _a : "default"}`;
58
+ let ctx = audioContexts.get(key);
59
+ if (!ctx) {
60
+ ctx = new AudioContextCtor(options);
61
+ audioContexts.set(key, ctx);
62
+ }
63
+ return ctx;
64
+ }
65
+
66
+ const getWasmUrl = () => __awaiter(void 0, void 0, void 0, function* () {
67
+ {
68
+ return assetUrls.denoiser.wasm;
69
+ }
70
+ });
71
+ const getProcessorUrl = () => __awaiter(void 0, void 0, void 0, function* () {
72
+ {
73
+ return assetUrls.denoiser.processor;
74
+ }
75
+ });
76
+ let wasmBufferPromise = null;
77
+ const loadWasmBuffer = (doCaptureException) => __awaiter(void 0, void 0, void 0, function* () {
78
+ if (!wasmBufferPromise) {
79
+ wasmBufferPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
80
+ const url = yield getWasmUrl();
81
+ const response = yield fetch(url);
82
+ if (!response.ok) {
83
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(`Failed to fetch denoiser model: ${response.status} ${response.statusText}`), { tags: { from: "audioDenoiser.loadWasmBuffer" } });
84
+ }
85
+ return response.arrayBuffer();
86
+ }))();
87
+ }
88
+ return wasmBufferPromise;
89
+ });
90
+ let workletRegistered = null;
91
+ const ensureWorkletRegistered = (context) => __awaiter(void 0, void 0, void 0, function* () {
92
+ if (!workletRegistered)
93
+ workletRegistered = new WeakSet();
94
+ if (workletRegistered.has(context))
95
+ return;
96
+ const processorUrl = yield getProcessorUrl();
97
+ yield context.audioWorklet.addModule(processorUrl);
98
+ workletRegistered.add(context);
99
+ });
100
+ class Denoiser extends AudioWorkletNode {
101
+ constructor(context, wasmBuffer) {
102
+ super(context, "denoiser", {
103
+ channelCountMode: "explicit",
104
+ channelCount: 1,
105
+ channelInterpretation: "speakers",
106
+ numberOfInputs: 1,
107
+ numberOfOutputs: 1,
108
+ outputChannelCount: [1],
109
+ processorOptions: {
110
+ wasmBuffer,
111
+ },
112
+ });
113
+ }
114
+ }
115
+ const canUse = () => typeof AudioWorkletNode !== "undefined" && typeof AudioContext !== "undefined";
116
+ const warmup = () => __awaiter(void 0, void 0, void 0, function* () {
117
+ yield loadWasmBuffer();
118
+ });
119
+ const applyAudioDenoiser = (_a) => __awaiter(void 0, [_a], void 0, function* ({ inputStream, doCaptureException, }) {
120
+ const inputTrack = inputStream.getAudioTracks()[0];
121
+ const sampleRate = inputTrack === null || inputTrack === void 0 ? void 0 : inputTrack.getSettings().sampleRate;
122
+ const audioContext = getAudioContext("audiodenoiser", sampleRate ? { sampleRate } : undefined);
123
+ const destination = audioContext.createMediaStreamDestination();
124
+ const [wasmBuffer] = yield Promise.all([loadWasmBuffer(doCaptureException), ensureWorkletRegistered(audioContext)]);
125
+ const source = audioContext.createMediaStreamSource(inputStream);
126
+ const denoiserNode = new Denoiser(audioContext, wasmBuffer);
127
+ denoiserNode.port.onmessage = (event) => {
128
+ var _a;
129
+ if ((_a = event.data) === null || _a === void 0 ? void 0 : _a.error) {
130
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(event.data.error), {
131
+ tags: { from: "audioDenoiser.onmessage" },
132
+ extra: { sampleRate },
133
+ });
134
+ }
135
+ };
136
+ denoiserNode.onprocessorerror = (errorEvent) => {
137
+ const event = errorEvent;
138
+ const error = new Error(`Denoiser processor error: ${event.error} - ${event.message}`);
139
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(error, {
140
+ tags: { from: "audioDenoiser.onprocessorerror" },
141
+ extra: { sampleRate },
142
+ });
143
+ };
144
+ denoiserNode.connect(destination);
145
+ source.connect(denoiserNode);
146
+ const outputTrack = destination.stream.getAudioTracks()[0];
147
+ if (outputTrack && inputTrack) {
148
+ Object.defineProperty(outputTrack, "enabled", {
149
+ get() {
150
+ return inputTrack.enabled;
151
+ },
152
+ set(value) {
153
+ inputTrack.enabled = value;
154
+ },
155
+ });
156
+ }
157
+ if (outputTrack) {
158
+ trackAnnotations(outputTrack).isEffectTrack = true;
159
+ }
160
+ let stopped = false;
161
+ const stop = () => {
162
+ if (stopped)
163
+ return;
164
+ stopped = true;
165
+ try {
166
+ denoiserNode.port.postMessage(false);
167
+ source.disconnect();
168
+ denoiserNode.disconnect();
169
+ }
170
+ catch (error) {
171
+ console.error("Error stopping audio denoiser", error);
172
+ }
173
+ };
174
+ return { outputStream: destination.stream, audioContext, denoiserNode, stop };
175
+ });
176
+
177
+ export { applyAudioDenoiser, canUse, warmup };
@@ -0,0 +1,177 @@
1
+ import { trackAnnotations } from '@whereby.com/media';
2
+
3
+ /******************************************************************************
4
+ Copyright (c) Microsoft Corporation.
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any
7
+ purpose with or without fee is hereby granted.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
16
+ ***************************************************************************** */
17
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
18
+
19
+
20
+ function __awaiter(thisArg, _arguments, P, generator) {
21
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
22
+ return new (P || (P = Promise))(function (resolve, reject) {
23
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
24
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
25
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
26
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
27
+ });
28
+ }
29
+
30
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
31
+ var e = new Error(message);
32
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
33
+ };
34
+
35
+ const CDN_BASE_URL = "https://cdn.srv.whereby.com/audio-denoiser/v0-0-0-canary-20260522073902-canary";
36
+ const getAssetUrl = (path) => {
37
+ {
38
+ return `${CDN_BASE_URL}/${path}`;
39
+ }
40
+ };
41
+ const assetUrls = {
42
+ denoiser: {
43
+ wasm: getAssetUrl("assets/denoiser/model.ext.wasm") ,
44
+ processor: getAssetUrl("assets/denoiser/processor.ext.js") ,
45
+ },
46
+ };
47
+
48
+ const audioContexts = new Map();
49
+ const AudioContextCtor = typeof AudioContext !== "undefined"
50
+ ? AudioContext
51
+ : globalThis.webkitAudioContext;
52
+ function getAudioContext(name, options) {
53
+ var _a;
54
+ if (!AudioContextCtor) {
55
+ throw new Error("AudioContext is not supported in this environment");
56
+ }
57
+ const key = `${name}:${(_a = options === null || options === void 0 ? void 0 : options.sampleRate) !== null && _a !== void 0 ? _a : "default"}`;
58
+ let ctx = audioContexts.get(key);
59
+ if (!ctx) {
60
+ ctx = new AudioContextCtor(options);
61
+ audioContexts.set(key, ctx);
62
+ }
63
+ return ctx;
64
+ }
65
+
66
+ const getWasmUrl = () => __awaiter(void 0, void 0, void 0, function* () {
67
+ {
68
+ return assetUrls.denoiser.wasm;
69
+ }
70
+ });
71
+ const getProcessorUrl = () => __awaiter(void 0, void 0, void 0, function* () {
72
+ {
73
+ return assetUrls.denoiser.processor;
74
+ }
75
+ });
76
+ let wasmBufferPromise = null;
77
+ const loadWasmBuffer = (doCaptureException) => __awaiter(void 0, void 0, void 0, function* () {
78
+ if (!wasmBufferPromise) {
79
+ wasmBufferPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
80
+ const url = yield getWasmUrl();
81
+ const response = yield fetch(url);
82
+ if (!response.ok) {
83
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(`Failed to fetch denoiser model: ${response.status} ${response.statusText}`), { tags: { from: "audioDenoiser.loadWasmBuffer" } });
84
+ }
85
+ return response.arrayBuffer();
86
+ }))();
87
+ }
88
+ return wasmBufferPromise;
89
+ });
90
+ let workletRegistered = null;
91
+ const ensureWorkletRegistered = (context) => __awaiter(void 0, void 0, void 0, function* () {
92
+ if (!workletRegistered)
93
+ workletRegistered = new WeakSet();
94
+ if (workletRegistered.has(context))
95
+ return;
96
+ const processorUrl = yield getProcessorUrl();
97
+ yield context.audioWorklet.addModule(processorUrl);
98
+ workletRegistered.add(context);
99
+ });
100
+ class Denoiser extends AudioWorkletNode {
101
+ constructor(context, wasmBuffer) {
102
+ super(context, "denoiser", {
103
+ channelCountMode: "explicit",
104
+ channelCount: 1,
105
+ channelInterpretation: "speakers",
106
+ numberOfInputs: 1,
107
+ numberOfOutputs: 1,
108
+ outputChannelCount: [1],
109
+ processorOptions: {
110
+ wasmBuffer,
111
+ },
112
+ });
113
+ }
114
+ }
115
+ const canUse = () => typeof AudioWorkletNode !== "undefined" && typeof AudioContext !== "undefined";
116
+ const warmup = () => __awaiter(void 0, void 0, void 0, function* () {
117
+ yield loadWasmBuffer();
118
+ });
119
+ const applyAudioDenoiser = (_a) => __awaiter(void 0, [_a], void 0, function* ({ inputStream, doCaptureException, }) {
120
+ const inputTrack = inputStream.getAudioTracks()[0];
121
+ const sampleRate = inputTrack === null || inputTrack === void 0 ? void 0 : inputTrack.getSettings().sampleRate;
122
+ const audioContext = getAudioContext("audiodenoiser", sampleRate ? { sampleRate } : undefined);
123
+ const destination = audioContext.createMediaStreamDestination();
124
+ const [wasmBuffer] = yield Promise.all([loadWasmBuffer(doCaptureException), ensureWorkletRegistered(audioContext)]);
125
+ const source = audioContext.createMediaStreamSource(inputStream);
126
+ const denoiserNode = new Denoiser(audioContext, wasmBuffer);
127
+ denoiserNode.port.onmessage = (event) => {
128
+ var _a;
129
+ if ((_a = event.data) === null || _a === void 0 ? void 0 : _a.error) {
130
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(new Error(event.data.error), {
131
+ tags: { from: "audioDenoiser.onmessage" },
132
+ extra: { sampleRate },
133
+ });
134
+ }
135
+ };
136
+ denoiserNode.onprocessorerror = (errorEvent) => {
137
+ const event = errorEvent;
138
+ const error = new Error(`Denoiser processor error: ${event.error} - ${event.message}`);
139
+ doCaptureException === null || doCaptureException === void 0 ? void 0 : doCaptureException(error, {
140
+ tags: { from: "audioDenoiser.onprocessorerror" },
141
+ extra: { sampleRate },
142
+ });
143
+ };
144
+ denoiserNode.connect(destination);
145
+ source.connect(denoiserNode);
146
+ const outputTrack = destination.stream.getAudioTracks()[0];
147
+ if (outputTrack && inputTrack) {
148
+ Object.defineProperty(outputTrack, "enabled", {
149
+ get() {
150
+ return inputTrack.enabled;
151
+ },
152
+ set(value) {
153
+ inputTrack.enabled = value;
154
+ },
155
+ });
156
+ }
157
+ if (outputTrack) {
158
+ trackAnnotations(outputTrack).isEffectTrack = true;
159
+ }
160
+ let stopped = false;
161
+ const stop = () => {
162
+ if (stopped)
163
+ return;
164
+ stopped = true;
165
+ try {
166
+ denoiserNode.port.postMessage(false);
167
+ source.disconnect();
168
+ denoiserNode.disconnect();
169
+ }
170
+ catch (error) {
171
+ console.error("Error stopping audio denoiser", error);
172
+ }
173
+ };
174
+ return { outputStream: destination.stream, audioContext, denoiserNode, stop };
175
+ });
176
+
177
+ export { applyAudioDenoiser, canUse, warmup };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@whereby.com/audio-denoiser",
3
+ "version": "0.0.0-canary-20260522073902",
4
+ "description": "Audio denoiser (noise suppression) for microphone streams",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs",
16
+ "default": "./dist/index.mjs"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist/**",
21
+ "!dist/cdn",
22
+ "!dist/assets"
23
+ ],
24
+ "devDependencies": {
25
+ "@rollup/plugin-url": "^8.0.2",
26
+ "rimraf": "^5.0.5",
27
+ "rollup": "^4.22.4",
28
+ "@whereby.com/eslint-config": "0.1.0",
29
+ "@whereby.com/jest-config": "0.1.0",
30
+ "@whereby.com/prettier-config": "0.1.0",
31
+ "@whereby.com/rollup-config": "0.1.1",
32
+ "@whereby.com/tsconfig": "0.1.0"
33
+ },
34
+ "dependencies": {
35
+ "@whereby.com/media": "8.3.2"
36
+ },
37
+ "prettier": "@whereby.com/prettier-config",
38
+ "scripts": {
39
+ "build": "rollup -c",
40
+ "build:dev": "REACT_APP_IS_DEV=true rollup -c",
41
+ "build:prod": "REACT_APP_IS_DEV=false rollup -c",
42
+ "clean": "rimraf dist",
43
+ "lint": "eslint .",
44
+ "lint:fix": "eslint --fix ."
45
+ }
46
+ }