@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 +23 -0
- package/README.md +52 -0
- package/dist/cdn/v0-0-0-canary-20260522073902-canary/assets/denoiser/model.ext.wasm +0 -0
- package/dist/cdn/v0-0-0-canary-20260522073902-canary/assets/denoiser/processor.ext.js +70 -0
- package/dist/index.cjs +181 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +177 -0
- package/dist/legacy-esm.js +177 -0
- package/package.json +46 -0
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.
|
|
Binary file
|
|
@@ -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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|