longpipe 0.0.14 → 0.0.16

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/README.md CHANGED
@@ -1,286 +1,65 @@
1
1
  # Longpipe 🐉
2
2
 
3
- Fast, high quality virtual effects in the browser
3
+ Fast, high-quality video effects in the browser — virtual backgrounds, face touch-up, auto-reframe, and mic noise removal in one open-source SDK.
4
4
 
5
+ <img width="960" height="540" alt="video-effects-sdk" src="https://github.com/user-attachments/assets/5dbe1ccb-09dd-4f22-b4be-e3122767dbcc" />
5
6
 
6
- <img width="640" height="360" alt="0087" src="https://github.com/user-attachments/assets/525f983c-6b3d-43ff-aedf-b1203e2955c1" />
7
-
8
-
9
-
10
- Try the live demo: [longpipe.dev/demo](https://longpipe.dev/demo)
7
+ **Try the live demo → [longpipe.dev/demo](https://longpipe.dev/demo)**
11
8
 
12
9
  > **Warning** — This project is very new and still under active development. Expect API changes between versions and bugs.
13
10
 
14
- ## Features
15
-
16
- - **Support across browsers:** Works on every browser, simplifying dozens of browser inconsistencies in one simple API.
17
- - **Performance:** Longpipe was built from the ground up to work as well on 10-year-old netbooks as it does on the latest MacBook Pro.
18
- - **Adaptivity:** Longpipe has several model variants (xl to xs), auto-selecting to provide the best quality while maintaining 30 fps.
19
- - **Effects that compose:** virtual backgrounds, skin touch-up, and auto-reframe all run together off a single shared encoder pass — enabling one doesn't cost you another inference.
20
- - Built by the founder of [Vectorly](https://www.crunchbase.com/organization/vectorly), a commercial effects SDK acquired in 2021. A ground-up redesign for the WebGPU era, with years of production lessons baked in.
21
-
22
- ## Install
11
+ ## Quick start
23
12
 
24
13
  ```
25
14
  npm install longpipe
26
15
  ```
27
16
 
28
- ## Quick start
29
-
30
17
  ```ts
31
18
  import { EffectsPipeline } from 'longpipe'
32
19
 
33
- const inputStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
20
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
34
21
 
35
- const pipeline = new EffectsPipeline(inputStream, {
36
- background: 'blur',
37
- audio: 'denoise'
22
+ const pipeline = new EffectsPipeline(stream, {
23
+ background: 'blur', // or an image, video, or solid color
24
+ touchup: true, // skin smoothing
25
+ reframe: true, // auto-frame the subject
26
+ audio: 'denoise', // mic noise removal
38
27
  })
39
28
 
40
29
  videoEl.srcObject = pipeline.stream // available immediately
41
30
  await pipeline.ready // optional — resolves once the effect is live
42
31
  ```
43
32
 
44
- `pipeline.stream` is wired synchronously and emits the unprocessed input until the model is ready (~1–3s on cold start, depending on hardware), so the consumer sees live video the whole time.
45
-
46
-
47
- ## Performance
48
-
49
- Longpipe uses custom-trained models built with an EfficientNet-Lite encoder and a U-Net style decoder, broken into five performance presets (`xs` through `xl`) that vary in encoder size, decoder width, and input resolution. Across all variants Longpipe has much higher segmentation accuracy than alternative open-source models like MediaPipe and BodyPix, while also delivering much better real-world performance — the model runs as pure WebGPU/WebGL shaders in a zero-copy, fully-GPU pipeline.
50
-
51
- ### Quality
52
- Using average alpha pixel error on the P3M-10K valdation dataset (499 landscape images), all variants of Longpipe surpassed mediapipe in both MAE and IoU.
53
-
54
- <img width="640" alt="lp-quality" src="https://github.com/user-attachments/assets/c7044937-95af-496f-893a-a05a50d7c914" />
55
-
56
- ### Speed
57
- With the pure GPU zero copy pipeline, Longpipe achieves better real world performance than mediapipe using much larger models.
58
-
59
- <img width="640" alt="lp-speed" src="https://github.com/user-attachments/assets/ce658b8d-dc62-4d6b-90f8-cd62dfef2a59" />
60
-
61
-
62
-
63
-
64
-
65
- ## Backgrounds
66
-
67
- The `background` option takes a wide set of inputs and normalizes them internally:
68
-
69
- ```ts
70
- new EffectsPipeline(stream, { background: 'none' }) // passthrough
71
- new EffectsPipeline(stream, { background: 'blur' }) // default sigma
72
- new EffectsPipeline(stream, { background: { blur: { sigma: 12 } } })
73
-
74
- // images: URL, <img>, ImageBitmap, or { image: ... }
75
- new EffectsPipeline(stream, { background: 'https://example.com/bg.jpg' })
76
- new EffectsPipeline(stream, { background: imgElement })
77
- new EffectsPipeline(stream, { background: imageBitmap })
78
-
79
- // video: URL, Blob, or <video> — looped, muted, decoded on the main thread
80
- new EffectsPipeline(stream, { background: { video: 'https://example.com/bg.mp4' } })
81
-
82
- // solid color — hex string or [r, g, b] floats in [0, 1]
83
- new EffectsPipeline(stream, { background: { color: '#00b050' } }) // greenscreen
84
- new EffectsPipeline(stream, { background: { color: [0, 0.7, 0.3] } })
85
- ```
86
-
87
- Swap at runtime:
33
+ `pipeline.stream` is wired synchronously and emits the unprocessed input until the model is ready (~1–3 s on cold start), so users see live video the whole time. Model weights stream from `cdn.longpipe.dev` by default — no extra setup, and [self-hostable](https://longpipe.dev/docs/self-hosting).
88
34
 
89
- ```ts
90
- await pipeline.setBackground({ blur: { sigma: 20 } })
91
- await pipeline.setBackground('https://example.com/other-bg.jpg')
92
- ```
35
+ ## Why Longpipe
93
36
 
94
- ## Performance presets
37
+ - **One model, many effects.** Backgrounds, touch-up, and auto-reframe all run off a single shared encoder pass — enabling another effect doesn't cost another inference. Audio denoise runs in a parallel `AudioWorklet`, off the GPU entirely. ([Architecture](https://longpipe.dev/docs/architecture))
38
+ - **More accurate *and* faster than the open-source alternatives.** Custom-trained models running as pure WebGPU/WebGL shaders in a zero-copy, fully-GPU pipeline — no general-purpose runtime, no CPU↔GPU round trips.
39
+ - **Works everywhere.** Chromium, Firefox, and Safari (desktop + iOS). WebGPU when available, WebGL2 fallback, per-browser frame transport handled internally.
40
+ - **Adapts to the device.** Five model presets (`xs`–`xl`); autotune benchmarks the actual device at init and an adaptive controller swaps presets at runtime to hold 30 fps — on 10-year-old netbooks and the latest MacBook Pro alike.
41
+ - **Production pedigree.** Built by the founder of [Vectorly](https://www.crunchbase.com/organization/vectorly), a commercial effects SDK acquired in 2021 — a ground-up redesign for the WebGPU era.
95
42
 
96
- ```ts
97
- new EffectsPipeline(stream, { preset: 'auto' }) // benchmarks at init (default)
98
- new EffectsPipeline(stream, { preset: 'fast' }) // small model
99
- new EffectsPipeline(stream, { preset: 'balanced' }) // medium model
100
- new EffectsPipeline(stream, { preset: 'quality' }) // xl model
101
- ```
43
+ ### Best models on the web
102
44
 
103
- `'auto'` runs a microbenchmark at init and picks the largest preset that fits the per-frame budget on the current device, **topping out at `large`** — `xl` is the explicit opt-in flagship (`'quality'` or a manual config). While `'auto'` is in effect an adaptive controller polls FPS / model time and swaps preset up or down as conditions change. Explicit preset choices (`'fast'`, `'balanced'`, `'quality'`, or a manual config) are always respected and never auto-overridden.
45
+ Longpipe's hydranet architecture, custom model-specific shaders, zero-memory-copy pipeline, developer friendly interface and 100% MIT open source license mean that 🐲 Longpipe pareto dominates on performance, model quality and ease of use.
104
46
 
105
- You can also pass a manual config:
47
+ <img width="958" height="652" alt="lp-performance" src="https://github.com/user-attachments/assets/792b9f63-e8fd-4d30-aa90-9c92b6a0430c" />
106
48
 
107
- ```ts
108
- new EffectsPipeline(stream, {
109
- preset: { model: 'large', dtype: 'f32', resolution: { w: 640, h: 400 }, skipFrames: 0 },
110
- })
111
- ```
112
49
 
113
- ## Audio denoise
50
+ ## Documentation
114
51
 
115
- Real-time speech denoising runs as a **separate AudioWorklet pipeline** on the audio render thread — independent of the video/GPU pipeline, in parallel. Pass `audio: 'denoise'` and the input stream's mic is cleaned in place; `pipeline.stream` carries the denoised audio track.
52
+ Full docs live at **[longpipe.dev/docs](https://longpipe.dev/docs)**:
116
53
 
117
- ```ts
118
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
119
- new EffectsPipeline(stream, { audio: 'denoise' }) // auto-picks a model for the device
120
- ```
121
-
122
- Three models back three tiers. `auto` (the default) runs a tiny **weight-free probe** at init — it times the real network on this device *without* downloading the multi-MB weights — and picks the best fit:
123
-
124
- | `model` | tier | what it is |
125
- |-----------|------|------------|
126
- | `dfn` | high | DeepFilterNet3, full f32 — best quality |
127
- | `dfnint8` | mid | DeepFilterNet3 with int8 GRUs — smaller download, faster on weak hardware |
128
- | `rnnoise` | low | classic RNNoise — tiny, and the fallback where wasm SIMD is unavailable |
129
-
130
- ```ts
131
- // force a specific model or tier instead of probing
132
- new EffectsPipeline(stream, { audio: { denoise: { model: 'dfn' } } })
133
-
134
- interface DenoiseOptions {
135
- model?: 'auto' | 'high' | 'mid' | 'low' | 'rnnoise' | 'dfn' | 'dfnint8' // default 'auto'
136
- postFilterBeta?: number // DFN post-filter, suppresses residual noise (default 0.03)
137
- gruLeak?: number // DFN recurrent-drift bound (default 0.995)
138
- enabled?: boolean // start denoising vs. passthrough (default true)
139
- }
140
- ```
141
-
142
- Toggle, reconfigure, and inspect at runtime:
143
-
144
- ```ts
145
- pipeline.setDenoise(false) // passthrough (cheap to re-enable)
146
- pipeline.setDenoise({ postFilterBeta: 0.05 }) // tweak DFN params live
147
- pipeline.getAudioStats() // { model, p50Ms, p95Ms, latencyMs, sampleRate, active }
148
- ```
149
-
150
- The denoiser handles sample-rate conversion internally (it runs at 48 kHz and resamples when the device can't). `pipeline.ready` resolves on the video first frame and does **not** wait on audio — denoising joins asynchronously, with the mic passing through until it's live.
151
-
152
- ## Other options
153
-
154
- ```ts
155
- new EffectsPipeline(stream, {
156
- background: 'blur',
157
- touchup: { strength: 0.6 }, // skin smoothing; omit to disable
158
- reframe: true, // auto-frame the subject; omit to disable
159
- preset: 'auto',
160
- adaptive: true, // default; only applies when preset is 'auto'
161
- audio: 'denoise', // 'passthrough' | 'drop' | 'denoise' | { denoise: {...} }
162
- outputResolution: { w: 1280, h: 720 }, // default: matches the input video track
163
- weightsBaseUrl: 'https://your-cdn/longpipe/', // default: cdn.longpipe.dev
164
- enabled: true, // false = pass input through unchanged
165
- onReady: () => console.log('live'),
166
- onError: (err) => console.error(err),
167
- })
168
- ```
169
-
170
- Toggle the effect on/off without tearing anything down (cheap to re-enable):
171
-
172
- ```ts
173
- pipeline.setEnabled(false)
174
- ```
175
-
176
- Tear it all down:
177
-
178
- ```ts
179
- pipeline.destroy()
180
- ```
181
-
182
- ## Self-hosting weights
183
-
184
- By default Longpipe fetches model weights from `https://cdn.longpipe.dev/models/v/0.0.5/`. You can browse the available files, sizes, and SHA-256 hashes at [cdn.longpipe.dev/models/v/0.0.5/index.html](https://cdn.longpipe.dev/models/v/0.0.5/index.html) (machine-readable list at [manifest.json](https://cdn.longpipe.dev/models/v/0.0.5/manifest.json)).
185
-
186
- To serve them yourself, mirror the files under any base URL with the same `model_${name}.bin` naming convention and pass it via `weightsBaseUrl`:
187
-
188
- ```ts
189
- new EffectsPipeline(stream, {
190
- weightsBaseUrl: 'https://your-cdn.example.com/longpipe-weights/',
191
- })
192
- ```
193
-
194
- If you use touch-up, three more files must sit alongside the tier weights — they're fetched from the same base URL the first time the effect is enabled: `model_landmark_mesh.bin` (and `.f16.bin`), `face_topology.json`, and `weight_mask.png`. Miss these and matting still works, but touch-up fails to enable. Auto-reframe needs nothing extra.
195
-
196
- ## Browser support
197
-
198
- Works on Chromium (Chrome, Edge), Firefox, and Safari (desktop and iOS). WebGPU is used when available; WebGL2 is the fallback. Longpipe picks the optimal video frame transport for each browser internally — `MediaStreamTrackProcessor`, `transferControlToOffscreen` + `captureStream`, or an `ImageBitmap` shuttle as universal fallback — all invisible to the caller.
199
-
200
-
201
-
202
- ## Face touch-up
203
-
204
- UV-space skin smoothing (the "beautify" filter). It runs **in parallel with any background effect** — both features share one encoder pass per frame, and the retouched frame feeds the background compositor.
205
-
206
- ```ts
207
- new EffectsPipeline(stream, {
208
- background: 'blur',
209
- touchup: { strength: 0.6 }, // presence enables it
210
- })
211
-
212
- await pipeline.setTouchup({ strength: 0.8, style: 'bilateral' }) // live update
213
- await pipeline.setTouchup(null) // disable
214
- ```
215
-
216
- ```ts
217
- interface TouchupOptions {
218
- strength?: number // 0..1 — blend of smoothed skin over original (default 0.6)
219
- amount?: number // smoothing radius (default 8)
220
- detail?: number // high-frequency (pore texture) keep — freq-sep only (default 0.35)
221
- style?: 'freq-sep' | 'bilateral' // default 'freq-sep'
222
- }
223
- ```
224
-
225
- How it works: a 5-point face-keypoint head rides the matting encoder (no second encoder pass); its heatmaps are decoded on the GPU into a face box; a 478-point landmark model runs on the face crop; the face is unwrapped into a pose-independent UV atlas, smoothed there, and composited back through a per-region weight mask. Eyes, brows, and lips are protected and stay pixel-exact; `'freq-sep'` smooths while keeping pore-level texture, `'bilateral'` is a stronger edge-preserving filter. The whole chain is GPU-resident — no per-frame readback.
226
-
227
- On first enable the landmark weights (`model_landmark_mesh.bin`, ~2.6 MB) and two small static assets are fetched from `weightsBaseUrl`, then cached; parameter updates are instant. With no face in frame the effect passes through cleanly.
228
-
229
- Everyone in frame gets smoothed — up to **four faces** on the `medium`, `large` and `xl` presets. (`small` and `xs` decode keypoints on a coarser grid that can't reliably separate a second face, so they smooth one.) There's no per-face API and nothing to configure; the faces share a single atlas, so the smoothing passes run once regardless of how many people are in shot.
230
-
231
- ## Auto-reframe
232
-
233
- Crop and zoom the output so the subject is framed — the "centre stage" behaviour from Meet, Zoom and Teams. It rides the same face keypoints as touch-up, so it needs **no extra model and no extra assets**: turning it on costs a crop, not an inference.
234
-
235
- ```ts
236
- new EffectsPipeline(stream, { reframe: true }) // auto-follow, all defaults
237
- new EffectsPipeline(stream, { reframe: { auto: false } }) // solve once, then freeze
238
-
239
- await pipeline.setReframe({ zoom: 1.6 }) // enable / update live
240
- await pipeline.reframe() // manual mode: re-solve now
241
- await pipeline.setReframe(null) // disable
242
- ```
243
-
244
- ```ts
245
- type ReframeConfig = boolean | ReframeOptions
246
-
247
- interface ReframeOptions {
248
- zoom?: number // crop = frame / zoom (default 1.35; relaxes toward 1 as needed)
249
- gravity?: number // 0..1 pull toward the subject (default 0.5; 1 would centre it exactly)
250
- margin?: number // keep-out space around the face (default 0.04)
251
- auto?: boolean | { deadband?: number; ease?: number } // default true
252
- }
253
- ```
254
-
255
- Two modes, because apps ship both. **auto** (default) follows the subject with a deadband — it holds still, makes a deliberate move when they've actually gone somewhere, then holds again; continuous tracking reads as swimming, because a head moves constantly even when a person doesn't. **manual** (`auto: false`) frames once and freezes until you call `pipeline.reframe()` — the mode for a reframe button in your own UI.
256
-
257
- The crop must stay inside the frame *and* contain the subject, which has a useful consequence: if you sit hard in a corner, auto-reframe does nothing at all, because containing you would need a crop the size of the whole frame. That falls out of the constraints rather than being a special case, and it matches what the commercial implementations do.
258
-
259
- It reframes **you**, not your background: with a virtual background the image stays put while you zoom (it's a backdrop behind you, not part of the shot), while a *blurred* background reframes with you, since that is your real room. Neither needs configuring. Auto-reframe frames one subject — the largest face — and the choice is sticky, so two people side by side won't make the camera flip back and forth. That's the opposite of touch-up, which smooths everyone: framing is a choice about whose shot it is; smoothing isn't.
260
-
261
- ## How it works
262
-
263
- Two layers:
264
-
265
- - **Model** (`src/model/`) — a shared EfficientNet-Lite encoder (lite0; lite3 on the `xl` tier) feeding multiple heads: a U-Net matting decoder (sharpened at higher resolution by a lightweight U-Net *wrapper*), a 5-point face-keypoint head (drives touch-up and auto-reframe), and a small optical-flow head that provides temporal stability — the renderer warps the previous outputs along the predicted flow and blends them through a flow-gated stabilizer, so the matte is steady without any recurrent state in the network. A separate 478-point landmark model runs on the detected face crop. Written as TypeScript op classes — each layer is a class; weights load as binary tensors at init; the backend (WebGPU or WebGL2) is injected at construction. BatchNorm is fused into conv weights at export — there is no BN op at inference.
266
- - **Pipeline** (`src/pipeline/`) — capability detection, per-browser frame transport selection, worker spawn, audio passthrough, autotune, and the adaptive controller. Designed to absorb the browser/codec/canvas plumbing complexity so consumers don't have to.
267
-
268
- Five trained presets cover the hardware range. "Resolution" is the model's working (canvas) resolution; the encoder runs at a lower internal resolution and the U-Net wrapper refines back up to this.
269
-
270
- | Preset | Resolution | Encoder | Decoder | Skip frames |
271
- |--------|------------|--------------|----------|-------------|
272
- | xl | 1280×768 | full (lite3) | 2× ch | 0 |
273
- | large | 640×400 | full (lite0) | standard | 0 |
274
- | medium | 512×320 | full (lite0) | standard | 1 |
275
- | small | 384×224 | small | standard | 1 |
276
- | xs | 384×240 | small | standard | 2 |
277
-
278
- `Skip frames` is how many input frames the model sits out between runs — the compositor still renders every frame using the most recent alpha matte. Autotune picks one of these at init based on a microbenchmark of the actual network on the actual device.
279
-
280
- ## Development
281
-
282
- Training scripts, fixture generation, and the weight export pipeline are not yet documented here — coming soon.
54
+ - [Getting started](https://longpipe.dev/docs/getting-started)
55
+ - [API reference](https://longpipe.dev/docs/api) — all `EffectsPipeline` options and methods
56
+ - [Architecture](https://longpipe.dev/docs/architecture) — the hydranet: one encoder, many heads
57
+ - [Backgrounds](https://longpipe.dev/docs/backgrounds) · [Touch-up](https://longpipe.dev/docs/touchup) · [Auto-reframe](https://longpipe.dev/docs/reframe) · [Audio denoise](https://longpipe.dev/docs/audio)
58
+ - [Presets & autotune](https://longpipe.dev/docs/presets)
59
+ - [Model layer](https://longpipe.dev/docs/model-layer) — drive the model directly, without the pipeline
60
+ - [Self-hosting weights](https://longpipe.dev/docs/self-hosting)
283
61
 
62
+ Machine-readable for LLMs and agents: [`llms.txt`](https://longpipe.dev/llms.txt) · [`llms-full.txt`](https://longpipe.dev/llms-full.txt)
284
63
 
285
64
  ## Roadmap
286
65
 
@@ -292,25 +71,8 @@ Training scripts, fixture generation, and the weight export pipeline are not yet
292
71
  - [ ] AR effects
293
72
  - [ ] Lighting correction
294
73
 
295
-
296
74
  ## License
297
75
 
298
- SDK source code is MIT-licensed — see [LICENSE](LICENSE).
299
-
300
- The pre-trained model weights distributed via `cdn.longpipe.dev` (and any mirrored copies) are also MIT-licensed — see [WEIGHTS_LICENSE](WEIGHTS_LICENSE). You're free to self-host them inside other open-source or commercial projects under MIT-compatible terms.
301
-
302
- ### Training data and pretrained weights
303
-
304
- Longpipe's pre-trained weights are released under MIT (see [WEIGHTS_LICENSE](WEIGHTS_LICENSE)). They were trained on:
305
-
306
- - [P3M-10k](https://github.com/JizhiziLi/P3M) — MIT
307
- - [AISegment Matting Human Datasets](https://www.kaggle.com/datasets/laurentmih/aisegmentcom-matting-human-datasets) — MIT ([upstream license](https://github.com/aisegmentcn/matting_human_datasets/blob/master/LICENSE))
308
- - [COCO](https://cocodataset.org) CC BY 4.0
309
- - [OpenImages](https://storage.googleapis.com/openimages/web/index.html) CC by 4.0
310
- - Synthetic images generated with [Z-Image](https://github.com/Tongyi-MAI/Z-Image) (text-to-image) — Apache 2.0
311
- - A custom dataset of short webcam videos collected via [Prolific](https://www.prolific.com/), from participants who gave explicit, informed consent for their footage to be used to train an open-source virtual-background model. Faces are blurred before training; the raw videos are stored privately (EU), never distributed, and deleted after the training window.
312
- - Pseudo-labels generated by [BiRefNet](https://github.com/ZhengPeng7/BiRefNet) (MIT), used as a teacher model
313
-
314
- The encoder is initialized from a pre-trained [EfficientNet-Lite](https://github.com/RangiLyu/EfficientNet-Lite) backbone — Apache 2.0.
76
+ SDK source code **and** pre-trained model weights are MIT-licensed — see [LICENSE](LICENSE) and [WEIGHTS_LICENSE](WEIGHTS_LICENSE). You're free to self-host the weights inside other open-source or commercial projects under MIT-compatible terms.
315
77
 
316
- As is standard for trained models, the released weights are not a redistribution of any training image or video — they are published under MIT.
78
+ The weights were trained on permissively-licensed public datasets (P3M-10k, AISegment, COCO, OpenImages), Z-Image synthetics, and a custom webcam dataset collected with participants' explicit, informed consent. The full dataset list and provenance details: [longpipe.dev/docs/licensing](https://longpipe.dev/docs/licensing).
package/dist/audio.cjs ADDED
@@ -0,0 +1,33 @@
1
+ 'use strict';var a={rnnoise:{wasm:"rnnoise.wasm",weights:null,scaleIn:32768,scaleOut:30517578125e-15,needsSimd:false,exports:{malloc:"malloc",create:"rn_create",process:"rn_process"}},dfn:{wasm:"dfn.wasm",weights:"dfn_weights.pack",scaleIn:1,scaleOut:1,needsSimd:true,exports:{malloc:"df_lite_malloc",create:"df_lite_create",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}},dfnint8:{wasm:"dfn.wasm",weights:"dfn_weights_int8.pack",scaleIn:1,scaleOut:1,needsSimd:true,exports:{malloc:"df_lite_malloc",create:"df_lite_create_i8",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}}},_={high:"dfn",mid:"dfnint8",low:"rnnoise"},b=["high","mid","low"];function x(t){return b.includes(t)}function p(t){return x(t)?_[t]:t}function m(t,e){return `${t.replace(/\/$/,"")}/${e}`}async function f(t){let e=await fetch(t);if(!e.ok)throw new Error(`audio asset fetch failed: ${e.status} ${t}`);return e.arrayBuffer()}async function g(t,e){let r=a[t],[s,o]=await Promise.all([f(m(e,r.wasm)),r.weights?f(m(e,r.weights)):Promise.resolve(null)]);return {wasmBytes:s,weights:o}}function l(){try{return WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,22,1,20,0,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11]))}catch{return false}}var M=1.3,k=64,S=3,A=`
2
+ self.onmessage = async (e) => {
3
+ const { wasmUrl, iters, rounds, budget } = e.data
4
+ try {
5
+ const res = await fetch(wasmUrl)
6
+ if (!res.ok) throw new Error(res.status + ' ' + wasmUrl)
7
+ const module = await WebAssembly.compile(await res.arrayBuffer())
8
+ const imports = {}
9
+ for (const im of WebAssembly.Module.imports(module)) {
10
+ ;(imports[im.module] = imports[im.module] || {})
11
+ if (im.kind === 'function') imports[im.module][im.name] = () => 0
12
+ }
13
+ const ex = new WebAssembly.Instance(module, imports).exports
14
+ const best = (fn) => {
15
+ fn(iters) // warm up
16
+ let b = Infinity
17
+ for (let r = 0; r < rounds; r++) {
18
+ const t = performance.now()
19
+ fn(iters)
20
+ b = Math.min(b, (performance.now() - t) / iters)
21
+ }
22
+ return b
23
+ }
24
+ const dfnMs = best(ex.calibrate_f32)
25
+ const int8Ms = ex.calibrate_i8 ? best(ex.calibrate_i8) : Infinity
26
+ const model = dfnMs <= budget ? 'dfn' : int8Ms <= budget ? 'dfnint8' : 'rnnoise'
27
+ self.postMessage({ model, dfnMs, int8Ms })
28
+ } catch (err) {
29
+ self.postMessage({ error: (err && err.message) || String(err) })
30
+ }
31
+ }
32
+ `;async function w(t,e){if(!l())return "rnnoise";let r=`${t.replace(/\/$/,"")}/${a.dfn.wasm}`,s=null,o=null;try{s=URL.createObjectURL(new Blob([A],{type:"application/javascript"})),o=new Worker(s);let i=await new Promise((u,n)=>{o.onmessage=d=>u(d.data),o.onerror=d=>n(new Error(d.message||"probe worker error")),o.postMessage({wasmUrl:r,iters:k,rounds:S,budget:M});});if(i.error)throw new Error(i.error);return i.model??"rnnoise"}catch(i){return e?.(`audio tier probe failed (${i.message??String(i)}); using rnnoise`),"rnnoise"}finally{o?.terminate(),s&&URL.revokeObjectURL(s);}}var h='var b=Object.defineProperty;var y=(u,a,i)=>a in u?b(u,a,{enumerable:!0,configurable:!0,writable:!0,value:i}):u[a]=i;var r=(u,a,i)=>y(u,typeof a!="symbol"?a+"":a,i);var f={rnnoise:{wasm:"rnnoise.wasm",weights:null,scaleIn:32768,scaleOut:30517578125e-15,needsSimd:!1,exports:{malloc:"malloc",create:"rn_create",process:"rn_process"}},dfn:{wasm:"dfn.wasm",weights:"dfn_weights.pack",scaleIn:1,scaleOut:1,needsSimd:!0,exports:{malloc:"df_lite_malloc",create:"df_lite_create",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}},dfnint8:{wasm:"dfn.wasm",weights:"dfn_weights_int8.pack",scaleIn:1,scaleOut:1,needsSimd:!0,exports:{malloc:"df_lite_malloc",create:"df_lite_create_i8",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}}};var c=4096,m=class extends AudioWorkletProcessor{constructor(i){super();r(this,"cfg");r(this,"mem");r(this,"run");r(this,"state",0);r(this,"inPtr",0);r(this,"outPtr",0);r(this,"setBeta",null);r(this,"setGruLeak",null);r(this,"resample",!1);r(this,"rsPush",null);r(this,"inRs",0);r(this,"outRs",0);r(this,"rsDevInPtr",0);r(this,"rsOutPtr",0);r(this,"enabled");r(this,"model");r(this,"inAcc",new Float32Array(480));r(this,"inFill",0);r(this,"outRing",new Float32Array(4096));r(this,"outRead",0);r(this,"outWrite",0);r(this,"outCount",0);r(this,"primed",!1);r(this,"now");r(this,"times",new Float32Array(256));r(this,"tIdx",0);r(this,"hops",0);r(this,"_f32",null);let o=i.processorOptions;this.model=o.model,this.enabled=o.enabled,this.cfg=f[o.model];try{let t=this.instantiate(o.wasmBytes),e=n=>n?t[n]:void 0;this.mem=t.memory;let s=t[this.cfg.exports.malloc];if(this.run=t[this.cfg.exports.process],this.inPtr=s(480*4),this.outPtr=s(480*4),o.weights?this.state=t[this.cfg.exports.create](this.upload(s,o.weights),o.weights.byteLength):this.state=t[this.cfg.exports.create](),this.setBeta=e(this.cfg.exports.setBeta)??null,this.setGruLeak=e(this.cfg.exports.setGruLeak)??null,this.setBeta&&(o.postFilterBeta??0)>0&&this.setBeta(this.state,o.postFilterBeta),this.setGruLeak&&(o.gruLeak??1)<1&&this.setGruLeak(this.state,o.gruLeak),sampleRate!==48e3){let n=t.df_resampler_create;this.rsPush=t.df_resampler_push??null,n&&this.rsPush?(this.resample=!0,this.inRs=n(sampleRate,48e3),this.outRs=n(48e3,sampleRate),this.rsDevInPtr=s(256*4),this.rsOutPtr=s(c*4)):this.post({type:"error",message:`${o.model} can\'t resample ${sampleRate}\\u219248000 Hz; needs a 48 kHz AudioContext`})}}catch(t){let e=t;this.post({type:"error",message:`denoise init failed: ${e.message??String(t)}${e.stack?`\n`+e.stack:""}`})}this.now=typeof performance<"u"&&performance.now?()=>performance.now():null,this.port.onmessage=t=>{let e=t.data;e.type==="enabled"?this.enabled=e.value:e.type==="config"&&(this.setBeta&&e.postFilterBeta!=null&&this.setBeta(this.state,e.postFilterBeta),this.setGruLeak&&e.gruLeak!=null&&this.setGruLeak(this.state,e.gruLeak))},this.post({type:"ready"})}instantiate(i){var e;let o=new WebAssembly.Module(i),t={};for(let s of WebAssembly.Module.imports(o))t[e=s.module]??(t[e]={}),s.kind==="function"?t[s.module][s.name]=()=>0:s.kind==="memory"?t[s.module][s.name]=new WebAssembly.Memory({initial:256}):s.kind==="table"?t[s.module][s.name]=new WebAssembly.Table({initial:0,element:"anyfunc"}):s.kind==="global"&&(t[s.module][s.name]=new WebAssembly.Global({value:"i32",mutable:!1},0));return new WebAssembly.Instance(o,t).exports}upload(i,o){let t=new Uint8Array(o),e=i(t.length);return new Uint8Array(this.mem.buffer).set(t,e),e}heap(){return(!this._f32||this._f32.buffer!==this.mem.buffer)&&(this._f32=new Float32Array(this.mem.buffer)),this._f32}post(i){this.port.postMessage(i)}pushOut(i){this.outRing[this.outWrite]=i,this.outWrite=(this.outWrite+1)%this.outRing.length,this.outCount++}feed48k(i,o){for(let t=0;t<o;t++)this.inAcc[this.inFill++]=i[t],this.inFill===480&&(this.runHop(),this.inFill=0)}runHop(){let i=this.heap(),o=this.inPtr>>2;for(let e=0;e<480;e++)i[o+e]=this.inAcc[e]*this.cfg.scaleIn;let t=this.now?this.now():0;if(this.run(this.state,this.inPtr,this.outPtr),this.now&&(this.times[this.tIdx=this.tIdx+1&255]=this.now()-t),this.resample&&this.rsPush){let e=this.rsPush(this.outRs,this.outPtr,480,this.rsOutPtr,c);i=this.heap();let s=this.rsOutPtr>>2;for(let n=0;n<e;n++)this.pushOut(i[s+n])}else{let e=this.outPtr>>2;for(let s=0;s<480;s++)this.pushOut(i[e+s]*this.cfg.scaleOut)}++this.hops>=50&&(this.reportStats(),this.hops=0)}reportStats(){let i=Array.from(this.times).filter(e=>e>0).sort((e,s)=>e-s),o=e=>i.length?i[Math.min(i.length-1,Math.floor(e*i.length))]:null,t={model:this.model,p50Ms:this.now?o(.5):null,p95Ms:this.now?o(.95):null,latencyMs:this.outCount/sampleRate*1e3,active:this.enabled&&this.state!==0,sampleRate};this.post({type:"stats",stats:t})}process(i,o){let t=i[0]?.[0],e=o[0]?.[0];if(!e)return!0;let s=e.length;if(!t||!this.enabled||this.state===0)return t&&!this.enabled?e.set(t):e.fill(0),!0;if(this.resample&&this.rsPush){let n=this.heap(),g=this.rsDevInPtr>>2;for(let h=0;h<s;h++)n[g+h]=t[h];let d=this.rsPush(this.inRs,this.rsDevInPtr,s,this.rsOutPtr,c);n=this.heap(),this.feed48k(n.subarray(this.rsOutPtr>>2,(this.rsOutPtr>>2)+d),d)}else this.feed48k(t,s);if(!this.primed)if(this.outCount>=480)this.primed=!0;else return e.fill(0),!0;if(this.outCount>=s)for(let n=0;n<s;n++)e[n]=this.outRing[this.outRead],this.outRead=(this.outRead+1)%this.outRing.length,this.outCount--;else e.fill(0);return!0}};registerProcessor("denoise",m);\n';var y="https://cdn.longpipe.dev/models/v/0.0.5/";var E=.03,v=.995,c=class{constructor(e,r={}){this.opts=r;this.ctx=new AudioContext({sampleRate:48e3}),this.source=this.ctx.createMediaStreamSource(new MediaStream([e])),this.dest=this.ctx.createMediaStreamDestination(),this.outputTrack=this.dest.stream.getAudioTracks()[0],this.resume().catch(()=>{}),this.source.connect(this.dest),this.ready=this.init().then(()=>this.node!==null,s=>(this.opts.onError?.(`audio denoise init failed: ${s.message??String(s)}`),false));}opts;outputTrack;ready;ctx;source;dest;node=null;model=null;latestStats=null;destroyed=false;resolveExplicit(){let e=p(this.opts.model);return a[e].needsSimd&&!l()?(this.opts.onError?.(`${e} needs wasm SIMD; falling back to rnnoise`),"rnnoise"):e}async init(){let e=this.opts.weightsBaseUrl??y,r=(this.opts.model??"auto")==="auto"?await w(e,this.opts.onError):this.resolveExplicit();this.model=r;let s=await g(r,e);if(await this.ctx.audioWorklet.addModule(D()),this.destroyed)return;let o={model:r,wasmBytes:s.wasmBytes,weights:s.weights,enabled:this.opts.enabled??true,postFilterBeta:this.opts.postFilterBeta??E,gruLeak:this.opts.gruLeak??v},i=new AudioWorkletNode(this.ctx,"denoise",{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[1],processorOptions:o});i.port.onmessage=u=>{let n=u.data;n.type==="stats"?this.latestStats=n.stats:n.type==="error"?this.opts.onError?.(n.message):n.type==="ready"&&this.opts.onReady?.();},this.node=i,this.source.disconnect(this.dest),this.source.connect(i).connect(this.dest);}setEnabled(e){this.node?.port.postMessage({type:"enabled",value:e});}setConfig(e){this.node?.port.postMessage({type:"config",...e});}getStats(){return this.latestStats}async resume(){this.destroyed||this.ctx.state!=="suspended"||await this.ctx.resume();}destroy(){this.destroyed=true;try{this.node?.disconnect();}catch{}try{this.source.disconnect();}catch{}this.ctx.close();}};function D(){return URL.createObjectURL(new Blob([h],{type:"application/javascript"}))}
33
+ exports.AudioDenoiser=c;
@@ -0,0 +1,37 @@
1
+ import { D as DenoiseModelOption, A as AudioStats } from './messages-DjW-szBq.cjs';
2
+ export { a as DenoiseModel, b as DenoiseTier } from './messages-DjW-szBq.cjs';
3
+
4
+ interface AudioDenoiserOptions {
5
+ model?: DenoiseModelOption;
6
+ weightsBaseUrl?: string;
7
+ postFilterBeta?: number;
8
+ gruLeak?: number;
9
+ enabled?: boolean;
10
+ onReady?: () => void;
11
+ onError?: (message: string) => void;
12
+ }
13
+ declare class AudioDenoiser {
14
+ private opts;
15
+ readonly outputTrack: MediaStreamTrack;
16
+ readonly ready: Promise<boolean>;
17
+ private ctx;
18
+ private source;
19
+ private dest;
20
+ private node;
21
+ private model;
22
+ private latestStats;
23
+ private destroyed;
24
+ constructor(inputTrack: MediaStreamTrack, opts?: AudioDenoiserOptions);
25
+ private resolveExplicit;
26
+ private init;
27
+ setEnabled(on: boolean): void;
28
+ setConfig(cfg: {
29
+ postFilterBeta?: number;
30
+ gruLeak?: number;
31
+ }): void;
32
+ getStats(): AudioStats | null;
33
+ resume(): Promise<void>;
34
+ destroy(): void;
35
+ }
36
+
37
+ export { AudioDenoiser, type AudioDenoiserOptions, AudioStats, DenoiseModelOption };
@@ -0,0 +1,37 @@
1
+ import { D as DenoiseModelOption, A as AudioStats } from './messages-DjW-szBq.js';
2
+ export { a as DenoiseModel, b as DenoiseTier } from './messages-DjW-szBq.js';
3
+
4
+ interface AudioDenoiserOptions {
5
+ model?: DenoiseModelOption;
6
+ weightsBaseUrl?: string;
7
+ postFilterBeta?: number;
8
+ gruLeak?: number;
9
+ enabled?: boolean;
10
+ onReady?: () => void;
11
+ onError?: (message: string) => void;
12
+ }
13
+ declare class AudioDenoiser {
14
+ private opts;
15
+ readonly outputTrack: MediaStreamTrack;
16
+ readonly ready: Promise<boolean>;
17
+ private ctx;
18
+ private source;
19
+ private dest;
20
+ private node;
21
+ private model;
22
+ private latestStats;
23
+ private destroyed;
24
+ constructor(inputTrack: MediaStreamTrack, opts?: AudioDenoiserOptions);
25
+ private resolveExplicit;
26
+ private init;
27
+ setEnabled(on: boolean): void;
28
+ setConfig(cfg: {
29
+ postFilterBeta?: number;
30
+ gruLeak?: number;
31
+ }): void;
32
+ getStats(): AudioStats | null;
33
+ resume(): Promise<void>;
34
+ destroy(): void;
35
+ }
36
+
37
+ export { AudioDenoiser, type AudioDenoiserOptions, AudioStats, DenoiseModelOption };
package/dist/audio.js ADDED
@@ -0,0 +1 @@
1
+ export{b as AudioDenoiser}from'./chunk-O4OF2WRO.js';
@@ -0,0 +1,33 @@
1
+ var a={rnnoise:{wasm:"rnnoise.wasm",weights:null,scaleIn:32768,scaleOut:30517578125e-15,needsSimd:false,exports:{malloc:"malloc",create:"rn_create",process:"rn_process"}},dfn:{wasm:"dfn.wasm",weights:"dfn_weights.pack",scaleIn:1,scaleOut:1,needsSimd:true,exports:{malloc:"df_lite_malloc",create:"df_lite_create",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}},dfnint8:{wasm:"dfn.wasm",weights:"dfn_weights_int8.pack",scaleIn:1,scaleOut:1,needsSimd:true,exports:{malloc:"df_lite_malloc",create:"df_lite_create_i8",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}}},_={high:"dfn",mid:"dfnint8",low:"rnnoise"},b=["high","mid","low"];function M(t){return b.includes(t)}function c(t){return M(t)?_[t]:t}function m(t,e){return `${t.replace(/\/$/,"")}/${e}`}async function p(t){let e=await fetch(t);if(!e.ok)throw new Error(`audio asset fetch failed: ${e.status} ${t}`);return e.arrayBuffer()}async function f(t,e){let r=a[t],[s,o]=await Promise.all([p(m(e,r.wasm)),r.weights?p(m(e,r.weights)):Promise.resolve(null)]);return {wasmBytes:s,weights:o}}function l(){try{return WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,22,1,20,0,253,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11]))}catch{return false}}var k=1.3,x=64,S=3,R=`
2
+ self.onmessage = async (e) => {
3
+ const { wasmUrl, iters, rounds, budget } = e.data
4
+ try {
5
+ const res = await fetch(wasmUrl)
6
+ if (!res.ok) throw new Error(res.status + ' ' + wasmUrl)
7
+ const module = await WebAssembly.compile(await res.arrayBuffer())
8
+ const imports = {}
9
+ for (const im of WebAssembly.Module.imports(module)) {
10
+ ;(imports[im.module] = imports[im.module] || {})
11
+ if (im.kind === 'function') imports[im.module][im.name] = () => 0
12
+ }
13
+ const ex = new WebAssembly.Instance(module, imports).exports
14
+ const best = (fn) => {
15
+ fn(iters) // warm up
16
+ let b = Infinity
17
+ for (let r = 0; r < rounds; r++) {
18
+ const t = performance.now()
19
+ fn(iters)
20
+ b = Math.min(b, (performance.now() - t) / iters)
21
+ }
22
+ return b
23
+ }
24
+ const dfnMs = best(ex.calibrate_f32)
25
+ const int8Ms = ex.calibrate_i8 ? best(ex.calibrate_i8) : Infinity
26
+ const model = dfnMs <= budget ? 'dfn' : int8Ms <= budget ? 'dfnint8' : 'rnnoise'
27
+ self.postMessage({ model, dfnMs, int8Ms })
28
+ } catch (err) {
29
+ self.postMessage({ error: (err && err.message) || String(err) })
30
+ }
31
+ }
32
+ `;async function g(t,e){if(!l())return "rnnoise";let r=`${t.replace(/\/$/,"")}/${a.dfn.wasm}`,s=null,o=null;try{s=URL.createObjectURL(new Blob([R],{type:"application/javascript"})),o=new Worker(s);let i=await new Promise((h,n)=>{o.onmessage=u=>h(u.data),o.onerror=u=>n(new Error(u.message||"probe worker error")),o.postMessage({wasmUrl:r,iters:x,rounds:S,budget:k});});if(i.error)throw new Error(i.error);return i.model??"rnnoise"}catch(i){return e?.(`audio tier probe failed (${i.message??String(i)}); using rnnoise`),"rnnoise"}finally{o?.terminate(),s&&URL.revokeObjectURL(s);}}var d='var b=Object.defineProperty;var y=(u,a,i)=>a in u?b(u,a,{enumerable:!0,configurable:!0,writable:!0,value:i}):u[a]=i;var r=(u,a,i)=>y(u,typeof a!="symbol"?a+"":a,i);var f={rnnoise:{wasm:"rnnoise.wasm",weights:null,scaleIn:32768,scaleOut:30517578125e-15,needsSimd:!1,exports:{malloc:"malloc",create:"rn_create",process:"rn_process"}},dfn:{wasm:"dfn.wasm",weights:"dfn_weights.pack",scaleIn:1,scaleOut:1,needsSimd:!0,exports:{malloc:"df_lite_malloc",create:"df_lite_create",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}},dfnint8:{wasm:"dfn.wasm",weights:"dfn_weights_int8.pack",scaleIn:1,scaleOut:1,needsSimd:!0,exports:{malloc:"df_lite_malloc",create:"df_lite_create_i8",process:"df_lite_process",setBeta:"df_lite_set_beta",setGruLeak:"df_lite_set_gru_leak"}}};var c=4096,m=class extends AudioWorkletProcessor{constructor(i){super();r(this,"cfg");r(this,"mem");r(this,"run");r(this,"state",0);r(this,"inPtr",0);r(this,"outPtr",0);r(this,"setBeta",null);r(this,"setGruLeak",null);r(this,"resample",!1);r(this,"rsPush",null);r(this,"inRs",0);r(this,"outRs",0);r(this,"rsDevInPtr",0);r(this,"rsOutPtr",0);r(this,"enabled");r(this,"model");r(this,"inAcc",new Float32Array(480));r(this,"inFill",0);r(this,"outRing",new Float32Array(4096));r(this,"outRead",0);r(this,"outWrite",0);r(this,"outCount",0);r(this,"primed",!1);r(this,"now");r(this,"times",new Float32Array(256));r(this,"tIdx",0);r(this,"hops",0);r(this,"_f32",null);let o=i.processorOptions;this.model=o.model,this.enabled=o.enabled,this.cfg=f[o.model];try{let t=this.instantiate(o.wasmBytes),e=n=>n?t[n]:void 0;this.mem=t.memory;let s=t[this.cfg.exports.malloc];if(this.run=t[this.cfg.exports.process],this.inPtr=s(480*4),this.outPtr=s(480*4),o.weights?this.state=t[this.cfg.exports.create](this.upload(s,o.weights),o.weights.byteLength):this.state=t[this.cfg.exports.create](),this.setBeta=e(this.cfg.exports.setBeta)??null,this.setGruLeak=e(this.cfg.exports.setGruLeak)??null,this.setBeta&&(o.postFilterBeta??0)>0&&this.setBeta(this.state,o.postFilterBeta),this.setGruLeak&&(o.gruLeak??1)<1&&this.setGruLeak(this.state,o.gruLeak),sampleRate!==48e3){let n=t.df_resampler_create;this.rsPush=t.df_resampler_push??null,n&&this.rsPush?(this.resample=!0,this.inRs=n(sampleRate,48e3),this.outRs=n(48e3,sampleRate),this.rsDevInPtr=s(256*4),this.rsOutPtr=s(c*4)):this.post({type:"error",message:`${o.model} can\'t resample ${sampleRate}\\u219248000 Hz; needs a 48 kHz AudioContext`})}}catch(t){let e=t;this.post({type:"error",message:`denoise init failed: ${e.message??String(t)}${e.stack?`\n`+e.stack:""}`})}this.now=typeof performance<"u"&&performance.now?()=>performance.now():null,this.port.onmessage=t=>{let e=t.data;e.type==="enabled"?this.enabled=e.value:e.type==="config"&&(this.setBeta&&e.postFilterBeta!=null&&this.setBeta(this.state,e.postFilterBeta),this.setGruLeak&&e.gruLeak!=null&&this.setGruLeak(this.state,e.gruLeak))},this.post({type:"ready"})}instantiate(i){var e;let o=new WebAssembly.Module(i),t={};for(let s of WebAssembly.Module.imports(o))t[e=s.module]??(t[e]={}),s.kind==="function"?t[s.module][s.name]=()=>0:s.kind==="memory"?t[s.module][s.name]=new WebAssembly.Memory({initial:256}):s.kind==="table"?t[s.module][s.name]=new WebAssembly.Table({initial:0,element:"anyfunc"}):s.kind==="global"&&(t[s.module][s.name]=new WebAssembly.Global({value:"i32",mutable:!1},0));return new WebAssembly.Instance(o,t).exports}upload(i,o){let t=new Uint8Array(o),e=i(t.length);return new Uint8Array(this.mem.buffer).set(t,e),e}heap(){return(!this._f32||this._f32.buffer!==this.mem.buffer)&&(this._f32=new Float32Array(this.mem.buffer)),this._f32}post(i){this.port.postMessage(i)}pushOut(i){this.outRing[this.outWrite]=i,this.outWrite=(this.outWrite+1)%this.outRing.length,this.outCount++}feed48k(i,o){for(let t=0;t<o;t++)this.inAcc[this.inFill++]=i[t],this.inFill===480&&(this.runHop(),this.inFill=0)}runHop(){let i=this.heap(),o=this.inPtr>>2;for(let e=0;e<480;e++)i[o+e]=this.inAcc[e]*this.cfg.scaleIn;let t=this.now?this.now():0;if(this.run(this.state,this.inPtr,this.outPtr),this.now&&(this.times[this.tIdx=this.tIdx+1&255]=this.now()-t),this.resample&&this.rsPush){let e=this.rsPush(this.outRs,this.outPtr,480,this.rsOutPtr,c);i=this.heap();let s=this.rsOutPtr>>2;for(let n=0;n<e;n++)this.pushOut(i[s+n])}else{let e=this.outPtr>>2;for(let s=0;s<480;s++)this.pushOut(i[e+s]*this.cfg.scaleOut)}++this.hops>=50&&(this.reportStats(),this.hops=0)}reportStats(){let i=Array.from(this.times).filter(e=>e>0).sort((e,s)=>e-s),o=e=>i.length?i[Math.min(i.length-1,Math.floor(e*i.length))]:null,t={model:this.model,p50Ms:this.now?o(.5):null,p95Ms:this.now?o(.95):null,latencyMs:this.outCount/sampleRate*1e3,active:this.enabled&&this.state!==0,sampleRate};this.post({type:"stats",stats:t})}process(i,o){let t=i[0]?.[0],e=o[0]?.[0];if(!e)return!0;let s=e.length;if(!t||!this.enabled||this.state===0)return t&&!this.enabled?e.set(t):e.fill(0),!0;if(this.resample&&this.rsPush){let n=this.heap(),g=this.rsDevInPtr>>2;for(let h=0;h<s;h++)n[g+h]=t[h];let d=this.rsPush(this.inRs,this.rsDevInPtr,s,this.rsOutPtr,c);n=this.heap(),this.feed48k(n.subarray(this.rsOutPtr>>2,(this.rsOutPtr>>2)+d),d)}else this.feed48k(t,s);if(!this.primed)if(this.outCount>=480)this.primed=!0;else return e.fill(0),!0;if(this.outCount>=s)for(let n=0;n<s;n++)e[n]=this.outRing[this.outRead],this.outRead=(this.outRead+1)%this.outRing.length,this.outCount--;else e.fill(0);return!0}};registerProcessor("denoise",m);\n';var w="https://cdn.longpipe.dev/models/v/0.0.5/";var E=.03,v=.995,y=class{constructor(e,r={}){this.opts=r;this.ctx=new AudioContext({sampleRate:48e3}),this.source=this.ctx.createMediaStreamSource(new MediaStream([e])),this.dest=this.ctx.createMediaStreamDestination(),this.outputTrack=this.dest.stream.getAudioTracks()[0],this.source.connect(this.dest),this.ready=this.init().then(()=>this.node!==null,s=>(this.opts.onError?.(`audio denoise init failed: ${s.message??String(s)}`),false));}opts;outputTrack;ready;ctx;source;dest;node=null;model=null;latestStats=null;destroyed=false;resolveExplicit(){let e=c(this.opts.model);return a[e].needsSimd&&!l()?(this.opts.onError?.(`${e} needs wasm SIMD; falling back to rnnoise`),"rnnoise"):e}async init(){let e=this.opts.weightsBaseUrl??w,r=(this.opts.model??"auto")==="auto"?await g(e,this.opts.onError):this.resolveExplicit();this.model=r;let s=await f(r,e);if(await this.ctx.audioWorklet.addModule(L()),this.destroyed)return;let o={model:r,wasmBytes:s.wasmBytes,weights:s.weights,enabled:this.opts.enabled??true,postFilterBeta:this.opts.postFilterBeta??E,gruLeak:this.opts.gruLeak??v},i=new AudioWorkletNode(this.ctx,"denoise",{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[1],processorOptions:o});i.port.onmessage=h=>{let n=h.data;n.type==="stats"?this.latestStats=n.stats:n.type==="error"?this.opts.onError?.(n.message):n.type==="ready"&&this.opts.onReady?.();},this.node=i,this.source.disconnect(this.dest),this.source.connect(i).connect(this.dest);}setEnabled(e){this.node?.port.postMessage({type:"enabled",value:e});}setConfig(e){this.node?.port.postMessage({type:"config",...e});}getStats(){return this.latestStats}destroy(){this.destroyed=true;try{this.node?.disconnect();}catch{}try{this.source.disconnect();}catch{}this.ctx.close();}};function L(){return URL.createObjectURL(new Blob([d],{type:"application/javascript"}))}
33
+ export{w as a,y as b};