{"_id":"@workadventure/noise-suppression","_rev":"5-1595d2588368c24bf4fff6977ee47145","name":"@workadventure/noise-suppression","dist-tags":{"latest":"0.1.1"},"versions":{"0.0.1":{"name":"@workadventure/noise-suppression","version":"0.0.1","keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"license":"MIT","_id":"@workadventure/noise-suppression@0.0.1","maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"homepage":"https://github.com/workAdventure/noise-suppression#readme","bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"dist":{"shasum":"925e6457c60e0b336e8964c30c4f60261b678af2","tarball":"https://registry.npmjs.org/@workadventure/noise-suppression/-/noise-suppression-0.0.1.tgz","fileCount":38,"integrity":"sha512-FvwlTTPOPJzCPwM396RxyAJxBoXByxPhHDnN/kuyQ0YAa91Z4KM2zj1sFnR+7zpFny6BykRIsgV2XzG8Y7WAHw==","signatures":[{"sig":"MEUCIBU8A3cTPKY9Eb738nNLIcRBv1WI+iTB+XPUqkiY74F0AiEAjJacpvLDe46pPs0WWh/+vG8eqTC2qrpiw9wtaPNK8Ig=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":59436548},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json","./audio-worklet":{"types":"./dist/audio-worklet.d.ts","import":"./dist/audio-worklet.js","default":"./dist/audio-worklet.js"}},"gitHead":"68eda1ec2b7d99d62784e91cdf9a297349afec85","scripts":{"dev":"vite","build":"npm run typecheck && vite build && tsc -p tsconfig.build.json","preview":"vite preview","typecheck":"tsc -p tsconfig.json","test:browser":"vitest --config vitest.config.ts --browser --run"},"_npmUser":{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},"repository":{"url":"git+https://github.com/workadventure/noise-suppression.git","type":"git"},"_npmVersion":"10.9.4","description":"Browser noise suppression powered by LiteRT.js and DTLN models","directories":{},"_nodeVersion":"22.21.1","dependencies":{"fft.js":"^4.0.4"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.14","vitest":"^4.1.0","playwright":"^1.58.2","typescript":"^5.8.2","@litertjs/core":"^2.0.0","@vitest/browser":"^4.1.0","vite-plugin-static-copy":"^1.0.6","@vitest/browser-playwright":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/noise-suppression_0.0.1_1778747250429_0.2713806815259221","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@workadventure/noise-suppression","version":"0.0.3","keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"license":"MIT","_id":"@workadventure/noise-suppression@0.0.3","maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"homepage":"https://github.com/workAdventure/noise-suppression#readme","bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"dist":{"shasum":"883822f1f1ea9e14cc6f7e0d3c7638e965e14548","tarball":"https://registry.npmjs.org/@workadventure/noise-suppression/-/noise-suppression-0.0.3.tgz","fileCount":38,"integrity":"sha512-Wao1tcTh7Fdg8J3FaCqC5u/pBls/GmfLTjyF9gn3KUGQWsGNc6HjyFvypptrt5wVeJ9Q7PsT0wNMlsMcf5U7uw==","signatures":[{"sig":"MEYCIQDPd5MW4MewBrTfdee7cKGakmVPuOm0v5K4FfKecMJyXgIhAM+IorUr53axQYRZTv67hAzLyMnbp4nNeG6/8Bc5e2pI","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@workadventure%2fnoise-suppression@0.0.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59366057},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json","./audio-worklet":{"types":"./dist/audio-worklet.d.ts","import":"./dist/audio-worklet.js","default":"./dist/audio-worklet.js"}},"gitHead":"29d91bad0313667e6cdefefb2794c65e300af73e","scripts":{"dev":"vite","build":"npm run typecheck && vite build && tsc -p tsconfig.build.json","preview":"vite preview","typecheck":"tsc -p tsconfig.json","test:browser":"vitest --config vitest.config.ts --browser --run"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5f874adf-cca8-43f8-8b53-a2792bb83a20"}},"repository":{"url":"git+https://github.com/workadventure/noise-suppression.git","type":"git"},"_npmVersion":"11.13.0","description":"Browser noise suppression powered by LiteRT.js and DTLN models","directories":{},"_nodeVersion":"24.16.0","dependencies":{"fft.js":"^4.0.4"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","vitest":"^4.1.0","playwright":"^1.58.2","typescript":"^5.8.2","@litertjs/core":"^2.0.0","@vitest/browser":"^4.1.0","vite-plugin-static-copy":"^4.1.1","@vitest/browser-playwright":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/noise-suppression_0.0.3_1781097954177_0.4166253890480629","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@workadventure/noise-suppression","version":"0.0.4","keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"license":"MIT","_id":"@workadventure/noise-suppression@0.0.4","maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"homepage":"https://github.com/workAdventure/noise-suppression#readme","bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"dist":{"shasum":"e8eed90ab0b34d1931305d38f6037ba3afe41bbf","tarball":"https://registry.npmjs.org/@workadventure/noise-suppression/-/noise-suppression-0.0.4.tgz","fileCount":46,"integrity":"sha512-v8DQgV2TQAWh7YLo7bZ1grV3iDNltRuvPaIYTcaBWoOjUaxDp/j5zrFLz4ZuijPGxzqcQxeW7ql/HJltMuLDtA==","signatures":[{"sig":"MEUCIQDbdOKCDHNItEOLV30L1T5AsbSKcHn02PqIexoqNETyXAIgF4XeZ2lOxjWRqs70JOL4ixMaLDEXux/zEyeyNCpWEkc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@workadventure%2fnoise-suppression@0.0.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59373759},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./vite":{"types":"./dist/vite.d.ts","import":"./dist/vite.js","default":"./dist/vite.js"},"./package.json":"./package.json","./audio-worklet":{"types":"./dist/audio-worklet.d.ts","import":"./dist/audio-worklet.js","default":"./dist/audio-worklet.js"}},"gitHead":"248ce9f4fe16a2ca9be183a2e1d38654121f2e6d","scripts":{"dev":"vite","build":"npm run typecheck && vite build && tsc -p tsconfig.build.json","preview":"vite preview","typecheck":"tsc -p tsconfig.json","test:browser":"vitest --config vitest.config.ts --browser --run"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5f874adf-cca8-43f8-8b53-a2792bb83a20"}},"repository":{"url":"git+https://github.com/workadventure/noise-suppression.git","type":"git"},"_npmVersion":"11.13.0","description":"Browser noise suppression powered by LiteRT.js and DTLN models","directories":{},"_nodeVersion":"24.16.0","dependencies":{"fft.js":"^4.0.4"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","vitest":"^4.1.0","playwright":"^1.58.2","typescript":"^5.8.2","@types/node":"^25.9.2","@litertjs/core":"^2.0.0","@vitest/browser":"^4.1.0","vite-plugin-static-copy":"^4.1.1","@vitest/browser-playwright":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/noise-suppression_0.0.4_1781104831195_0.3475859767847431","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@workadventure/noise-suppression","version":"0.1.0","keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"license":"MIT","_id":"@workadventure/noise-suppression@0.1.0","maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"homepage":"https://github.com/workAdventure/noise-suppression#readme","bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"dist":{"shasum":"a6eafb2213acf0eef2819c052ddc0a4327fda14f","tarball":"https://registry.npmjs.org/@workadventure/noise-suppression/-/noise-suppression-0.1.0.tgz","fileCount":63,"integrity":"sha512-MX4QMtBp0iX41cvl2soVWw7ge+27xIosN3bLdTU1czFoWDkHBQtyykvFzWYT6NKuPz34+E0sN9tFnjP8O1ZZKw==","signatures":[{"sig":"MEQCIDlUR0bB9K/Njq853WklTiZZFPmpAz0iUQ7KcTgllB9gAiAXB3u1KGxFf+WRAcZOmnCvP3NX8mfYCUq4wTg44C509w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@workadventure%2fnoise-suppression@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":129742741},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./vite":{"types":"./dist/vite.d.ts","import":"./dist/vite.js","default":"./dist/vite.js"},"./package.json":"./package.json","./audio-worklet":{"types":"./dist/audio-worklet.d.ts","import":"./dist/audio-worklet.js","default":"./dist/audio-worklet.js"},"./background-noise":{"types":"./dist/background-noise.d.ts","import":"./dist/background-noise.js","default":"./dist/background-noise.js"}},"gitHead":"e93907a89fae9f0c30f8ce8af0b26ad2d947650b","scripts":{"dev":"vite","build":"npm run typecheck && vite build && tsc -p tsconfig.build.json","preview":"vite preview","typecheck":"tsc -p tsconfig.json","build:pages":"vite build --mode pages","test:browser":"vitest --config vitest.config.ts --browser --run"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5f874adf-cca8-43f8-8b53-a2792bb83a20"}},"repository":{"url":"git+https://github.com/workadventure/noise-suppression.git","type":"git"},"_npmVersion":"11.13.0","description":"Browser noise suppression powered by LiteRT.js and DTLN models","directories":{},"_nodeVersion":"24.16.0","dependencies":{"fft.js":"^4.0.4","@ricky0123/vad-web":"^0.0.30"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.0.16","vitest":"^4.1.0","playwright":"^1.58.2","typescript":"^5.8.2","@types/node":"^25.9.2","@litertjs/core":"^2.0.0","@vitest/browser":"^4.1.0","vite-plugin-static-copy":"^4.1.1","@vitest/browser-playwright":"^4.1.0"},"_npmOperationalInternal":{"tmp":"tmp/noise-suppression_0.1.0_1781862778353_0.1170895721194194","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@workadventure/noise-suppression","version":"0.1.1","description":"Browser noise suppression powered by LiteRT.js and DTLN models","type":"module","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./audio-worklet":{"types":"./dist/audio-worklet.d.ts","import":"./dist/audio-worklet.js","default":"./dist/audio-worklet.js"},"./background-noise":{"types":"./dist/background-noise.d.ts","import":"./dist/background-noise.js","default":"./dist/background-noise.js"},"./vite":{"types":"./dist/vite.d.ts","import":"./dist/vite.js","default":"./dist/vite.js"},"./package.json":"./package.json"},"scripts":{"dev":"vite","build":"npm run typecheck && vite build && tsc -p tsconfig.build.json","build:pages":"vite build --mode pages","test:browser":"vitest --config vitest.config.ts --browser --run","typecheck":"tsc -p tsconfig.json","preview":"vite preview"},"keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"repository":{"type":"git","url":"git+https://github.com/workadventure/noise-suppression.git"},"bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"homepage":"https://github.com/workAdventure/noise-suppression#readme","license":"MIT","dependencies":{"@ricky0123/vad-web":"^0.0.30","fft.js":"^4.0.4"},"devDependencies":{"@litertjs/core":"^2.0.0","@types/node":"^25.9.2","@vitest/browser":"^4.1.0","@vitest/browser-playwright":"^4.1.0","playwright":"^1.58.2","typescript":"^5.8.2","vite":"^8.0.16","vite-plugin-static-copy":"^4.1.1","vitest":"^4.1.0"},"gitHead":"0f119b7e1ec753e6e42a5c314c95d7fca9a0d8db","_id":"@workadventure/noise-suppression@0.1.1","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-G2RmkYpLgHy44mFasv+XNQUqQv27r0XqO9lJ2FSr9fnK2QWP97gXPrgRRPtsoqChb5849Cdk0AhbeW1Zc1tKPw==","shasum":"dc21c6d055e71bc4427fb0910ce1ad1b543e6054","tarball":"https://registry.npmjs.org/@workadventure/noise-suppression/-/noise-suppression-0.1.1.tgz","fileCount":63,"unpackedSize":129742811,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@workadventure%2fnoise-suppression@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCj2BU7dFAFiD3FrH/i0ybWiLZBJV1In9e6k0ISIHYBjAIhAODFhHqf58pTQhariMXyf5lDj9IuPjG4fhH5I3Gc2z7H"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:5f874adf-cca8-43f8-8b53-a2792bb83a20"}},"directories":{},"maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/noise-suppression_0.1.1_1781877535075_0.10992675943465469"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-14T08:27:30.347Z","modified":"2026-06-19T13:58:56.148Z","0.0.1":"2026-05-14T08:27:31.119Z","0.0.3":"2026-06-10T13:25:56.890Z","0.0.4":"2026-06-10T15:20:31.699Z","0.1.0":"2026-06-19T09:52:59.028Z","0.1.1":"2026-06-19T13:58:55.869Z"},"bugs":{"url":"https://github.com/workadventure/noise-suppression/issues"},"license":"MIT","homepage":"https://github.com/workAdventure/noise-suppression#readme","keywords":["noise-suppression","audio","dtln","litert","litertjs","browser","webrtc","denoise","real-time","audio-processing"],"repository":{"type":"git","url":"git+https://github.com/workadventure/noise-suppression.git"},"description":"Browser noise suppression powered by LiteRT.js and DTLN models","maintainers":[{"name":"moufmouf","email":"d.negrier@thecodingmachine.com"},{"name":"gregoire-workadventure","email":"gregoire@workadventu.re"}],"readme":"# @workadventure/noise-suppression\n\n[![npm version](https://img.shields.io/npm/v/@workadventure/noise-suppression)](https://www.npmjs.com/package/@workadventure/noise-suppression)\n[![CI](https://github.com/workadventure/noise-suppression/actions/workflows/ci.yml/badge.svg)](https://github.com/workadventure/noise-suppression/actions/workflows/ci.yml)\n[![npm downloads](https://img.shields.io/npm/dm/@workadventure/noise-suppression)](https://www.npmjs.com/package/@workadventure/noise-suppression)\n[![License](https://img.shields.io/github/license/workadventure/noise-suppression)](./LICENSE)\n[![Test site](https://img.shields.io/badge/test_site-live-0f766e)](https://workadventure.github.io/noise-suppression/)\n\nBrowser-side noise suppression and noise-detection for realtime voice applications.\n\n#### 👉 [Try noise suppression and noise detection in your browser](https://workadventure.github.io/noise-suppression/) 👈\n\nThis package provides two complementary tools for handling noisy microphone\ninput directly in the browser:\n\n- **Noise suppression** runs the DTLN speech-denoising models with LiteRT.js.\n  Its primary integration path is an `AudioWorklet` that can sit between a\n  microphone track and a WebRTC peer connection.\n- **[Background noise detection](#detect-sustained-background-noise)** identifies\n  sustained noise that is unlikely to contain speech, so an application can\n  warn the user or suggest enabling noise suppression.\n\nUse it when you want to:\n\n- clean microphone audio before sending it to a WebRTC call\n- detect when a user's microphone is picking up sustained background noise\n- keep processing local to the browser\n\nThe package pre-bundles the assets required by both features and exposes\nhigh-level browser APIs for adding them to an application.\n\nThe package is browser-only. It does not ship a native addon, Rust runtime, or\nNode backend. If you are looking for server-side variants, take a look at\n[hayatialikeles/dtln-rs](https://github.com/hayatialikeles/dtln-rs), which\nthis package was originally forked from.\n\n## Installation\n\n```bash\nnpm install @workadventure/noise-suppression\n```\n\n## Add Noise Suppression To A WebRTC Track\n\nThe most common WebRTC integration is:\n\n1. capture the microphone with `getUserMedia`\n2. route it through the noise suppression `AudioWorklet`\n3. create a new processed `MediaStreamTrack`\n4. pass that processed track to your `RTCPeerConnection`\n\n```ts\nimport {\n  createNoiseSuppressionAudioWorklet,\n} from \"@workadventure/noise-suppression/audio-worklet\";\n\nconst microphoneStream = await navigator.mediaDevices.getUserMedia({\n  audio: {\n    channelCount: 1,\n    echoCancellation: true,\n    noiseSuppression: false,\n    autoGainControl: true,\n  },\n});\n\nconst context = new AudioContext({ sampleRate: 16000 });\nawait context.resume();\n\nconst source = context.createMediaStreamSource(microphoneStream);\nconst destination = context.createMediaStreamDestination();\n\nconst worklet = await createNoiseSuppressionAudioWorklet(context, {\n  bypassUntilReady: true,\n});\n\nsource.connect(worklet.node).connect(destination);\nawait worklet.ready;\n\nconst [processedTrack] = destination.stream.getAudioTracks();\n\nif (!processedTrack) {\n  throw new Error(\"Noise suppression did not create an audio track.\");\n}\n\npeerConnection.addTrack(processedTrack, destination.stream);\n\n// When the call ends or when you switch back to the raw microphone:\n// worklet.dispose();\n// source.disconnect();\n// microphoneStream.getTracks().forEach((track) => track.stop());\n// destination.stream.getTracks().forEach((track) => track.stop());\n// await context.close();\n```\n\nFor an existing call, replace the current microphone track instead:\n\n```ts\nconst sender = peerConnection\n  .getSenders()\n  .find((candidate) => candidate.track?.kind === \"audio\");\n\nif (!sender) {\n  throw new Error(\"No audio sender found.\");\n}\n\nawait sender.replaceTrack(processedTrack);\n```\n\n## AudioWorklet API\n\n```ts\nimport {\n  createNoiseSuppressionAudioWorklet,\n  observeNoiseSuppressionAudioWorkletMessages,\n  isNoiseSuppressionProcessingStartedMessage,\n} from \"@workadventure/noise-suppression/audio-worklet\";\n\nconst context = new AudioContext({ sampleRate: 16000 });\nconst worklet = await createNoiseSuppressionAudioWorklet(context);\n\nconst stopObserving = observeNoiseSuppressionAudioWorkletMessages(\n  worklet,\n  (message) => {\n    if (isNoiseSuppressionProcessingStartedMessage(message)) {\n      console.log(\"Noise suppression started.\");\n    }\n  }\n);\n\nawait worklet.ready;\nsourceNode.connect(worklet.node).connect(destinationNode);\n\n// Later:\nstopObserving();\nworklet.dispose();\n```\n\n`createNoiseSuppressionAudioWorklet(context, options?)` returns:\n\n- `node`: the `AudioWorkletNode` to insert in your Web Audio graph\n- `ready`: resolves after LiteRT.js and the DTLN models are initialized\n- `moduleUrl`: the processor module URL that was loaded\n- `processorName`: the registered processor name\n- `dispose()`: disconnects the node and stops the denoiser instance\n\nOptions:\n\n```ts\ninterface NoiseSuppressionAudioWorkletOptions {\n  moduleUrl?: string;\n  threads?: boolean;\n  numThreads?: number;\n  bypassUntilReady?: boolean;\n  readyTimeoutMs?: number;\n}\n```\n\nDefaults:\n\n- `moduleUrl`: the bundled worklet processor from this package\n- `threads`: `false`\n- `numThreads`: based on browser CPU count when available\n- `bypassUntilReady`: `true`\n- `readyTimeoutMs`: `30000`\n\nWith `bypassUntilReady: true`, microphone audio passes through while the worklet\ninitializes. With `false`, the worklet outputs silence until the denoiser is\nready.\n\nThe bundled worklet path currently targets single-threaded LiteRT execution.\nKeep `threads` unset or `false` unless you are testing a custom worklet bundle\nthat supports threaded Wasm loading.\n\n## Runtime Requirements\n\n- Use an `AudioContext` at `16000` Hz for DTLN processing.\n- Use one input and one output channel.\n- Create or resume the `AudioContext` after a user gesture when the browser\n  requires it.\n- For microphone capture, disable the browser's built-in `noiseSuppression` if\n  you want this package to be the only denoiser in the chain.\n- The default worklet bundle includes the LiteRT Wasm bytes and the two DTLN\n  model files, so the worklet path does not need the application to host those\n  files separately.\n\nThe processor buffers four 128-sample render quanta into one 512-sample DTLN\nframe, then writes the denoised samples back to an output ring buffer.\n\n## Bundlers\n\nThe package is ESM-only and is intended for browser bundlers.\n\n```ts\nimport { createNoiseSuppressionAudioWorklet } from \"@workadventure/noise-suppression/audio-worklet\";\n```\n\nIn the normal worklet path, consumers should not need to configure model URLs,\nWasm URLs, or worklet processor URLs. The distributed `audio-worklet` entrypoint\nloads the packaged processor bundle.\n\nIf your application serves assets from a constrained location, you can override\nthe worklet processor URL:\n\n```ts\nconst worklet = await createNoiseSuppressionAudioWorklet(context, {\n  moduleUrl: \"/assets/noise-suppression/audio-worklet-processor.js\",\n});\n```\n\n### Vite Dev Server\n\nVite can transform JavaScript loaded through `audioWorklet.addModule()` in dev\nmode. The transformed module may import Vite's client runtime, which is not\navailable inside an `AudioWorkletGlobalScope`.\n\nAdd the package Vite plugin:\n\n```ts\n// vite.config.ts\nimport { noiseSuppressionAudioWorkletVitePlugin } from \"@workadventure/noise-suppression/vite\";\n\nexport default defineConfig({\n  plugins: [noiseSuppressionAudioWorkletVitePlugin()],\n});\n```\n\nThe plugin serves the packaged worklet processor as raw JavaScript in dev and\nrewrites the package's default AudioWorklet URL to that raw endpoint. Application\ncode can keep calling `createNoiseSuppressionAudioWorklet()` without a\ndev-specific `moduleUrl` override.\n\n## Detect Sustained Background Noise\n\nThe background-noise detector identifies sustained input that is loud but\nunlikely to contain speech. It can be used to suggest enabling noise suppression\nwhen a user has a noisy microphone.\n\nThe detector uses Silero VAD through `@ricky0123/vad-web`. It analyzes a supplied\n`MediaStream` but does not modify the stream, play it, or enable DTLN noise\nsuppression.\n\n```ts\nimport {\n  createBackgroundNoiseDetector,\n  isBackgroundNoiseDetectedMessage,\n  observeBackgroundNoiseDetectorMessages,\n} from \"@workadventure/noise-suppression/background-noise\";\n\nconst microphoneStream = await navigator.mediaDevices.getUserMedia({\n  audio: {\n    channelCount: 1,\n    echoCancellation: true,\n    noiseSuppression: false,\n    autoGainControl: true,\n  },\n});\n\nconst context = new AudioContext({ sampleRate: 16000 });\nawait context.resume();\n\nconst detector = await createBackgroundNoiseDetector(\n  context,\n  microphoneStream\n);\n\nconst stopObserving = observeBackgroundNoiseDetectorMessages(\n  detector,\n  (message) => {\n    if (isBackgroundNoiseDetectedMessage(message)) {\n      console.log(\"Sustained background noise detected\", message);\n      // Offer to enable noise suppression here.\n    }\n  }\n);\n\nawait detector.ready;\n\n// Later:\nstopObserving();\ndetector.dispose();\nmicrophoneStream.getTracks().forEach((track) => track.stop());\nawait context.close();\n```\n\n`createBackgroundNoiseDetector(context, stream, options?)` returns a promise for\na detector handle:\n\n- `ready`: resolves with the Silero model, sample rate, frame size, and frame\n  duration\n- `dispose()`: stops VAD processing and releases its internal resources\n\nThe creation promise rejects if the Silero model, helper worklet, or ONNX Runtime\ncannot be initialized.\n\nThe caller retains ownership of the supplied stream. Calling `dispose()` does\nnot stop its tracks or close the `AudioContext`.\n\n### Detection Rules\n\nThe detector starts a candidate window when a frame exceeds `triggerRms` and is\nnot classified as speech. It emits `background-noise-detected` only when the\ncomplete window remains loud enough and stays below both configured speech\nlimits.\n\nDetector options and defaults:\n\n| Option | Default | Meaning |\n| --- | ---: | --- |\n| `triggerRms` | `0.01` | Minimum frame RMS needed to start a candidate window |\n| `noisyRms` | `0.02` | Minimum average RMS required to emit an event |\n| `analysisWindowMs` | `1500` | Sustained-noise window duration |\n| `speechProbabilityThreshold` | `0.3` | Probability at which a frame counts as speech |\n| `maxSpeechFrameRatio` | `0.75` | Maximum ratio of speech frames in the window |\n| `maxAverageSpeechProbability` | `0.5` | Maximum average speech probability in the window |\n| `cooldownMs` | `15000` | Minimum delay between emitted events |\n| `sileroModel` | `\"v5\"` | Silero model; `\"legacy\"` is also available |\n| `processorType` | `\"AudioWorklet\"` | Frame-capture mechanism used internally by `vad-web` |\n\nThe Silero integration also forwards `positiveSpeechThreshold`,\n`negativeSpeechThreshold`, `redemptionMs`, `preSpeechPadMs`, and `minSpeechMs`\nto `@ricky0123/vad-web`. In most integrations, tune the detector-level rules\nfirst and leave these VAD-specific options unchanged.\n\nA `background-noise-detected` message contains:\n\n```ts\ninterface BackgroundNoiseDetectedMessage {\n  type: \"background-noise-detected\";\n  rms: number;\n  rmsDb: number;\n  speechFrameRatio: number;\n  voiceFrameRatio: number;\n  averageSpeechProbability: number;\n  maxSpeechProbability: number;\n  activeFrameRatio: number;\n  windowMs: number;\n  timestampMs: number;\n}\n```\n\n`voiceFrameRatio` is currently an alias of `speechFrameRatio`.\n\n### Analyze Another Audio Source\n\nThe detector accepts any `MediaStream`, not only a microphone stream. To analyze\nan existing Web Audio graph, mirror its source into a\n`MediaStreamAudioDestinationNode`:\n\n```ts\nconst detectorInput = context.createMediaStreamDestination();\nsourceNode.connect(detectorInput);\n\nconst detector = await createBackgroundNoiseDetector(\n  context,\n  detectorInput.stream\n);\n```\n\nConnecting a node to `detectorInput` does not play it through the speakers. Add a\nseparate connection to `context.destination` only when playback is intended.\n\n### Silero And ONNX Assets\n\nThe background-noise detector is a separate package entrypoint. Applications\nthat only import the noise-suppression APIs do not initialize Silero or ONNX\nRuntime Web.\n\nThe package includes the Silero model, the VAD helper worklet, and ONNX Runtime\nWeb assets under `dist/vendor/`. Their default URLs are resolved relative to the\n`background-noise.js` module. A deployment must preserve those files and serve\n`.js`, `.mjs`, `.wasm`, and `.onnx` files with appropriate MIME types and CORS\nheaders.\n\nFor deployments that copy these assets elsewhere, override both base paths:\n\n```ts\nconst detector = await createBackgroundNoiseDetector(context, stream, {\n  baseAssetPath: \"/assets/noise-detector/silero/\",\n  onnxWASMBasePath: \"/assets/noise-detector/onnxruntime/\",\n});\n```\n\nThe package does not expose a dedicated background-noise `AudioWorkletNode`.\nWith the default `processorType`, `@ricky0123/vad-web` still uses its own small\nhelper worklet for audio capture and framing; Silero inference runs outside the\naudio render callback.\n\n## Advanced: Synchronous Frame API\n\nThe package also exposes the lower-level runtime API. This is useful for tests,\nbenchmarks, offline processing, or custom pipelines where you already manage\n512-sample mono frames.\n\n```ts\nimport createNoiseSuppressionModule from \"@workadventure/noise-suppression\";\n\nconst noiseSuppression = await createNoiseSuppressionModule();\nawait noiseSuppression.ready;\n\nconst handle = noiseSuppression.dtln_create();\nconst input = new Float32Array(512);\nconst output = new Float32Array(512);\n\nnoiseSuppression.dtln_denoise(handle, input, output);\nnoiseSuppression.dtln_stop(handle);\n```\n\nAudio contract:\n\n- sample rate: `16000`\n- channels: `1`\n- frame size: `512`\n- frame duration: `32 ms`\n- sample format: `Float32Array`\n\n`dtln_denoise` accepts input lengths that are multiples of `128`, but the\nrealtime target is the standard 512-sample frame.\n\nFrame API options:\n\n```ts\ninterface NoiseSuppressionModuleOptions {\n  liteRtWasmRoot?: string;\n  model1Url?: string;\n  model2Url?: string;\n  threads?: boolean;\n  numThreads?: number;\n  logModelDetails?: boolean;\n  enableProfiling?: boolean;\n}\n```\n\nThe frame API uses packaged LiteRT.js Wasm and model assets by default. It\nenables LiteRT.js threads automatically when `crossOriginIsolated === true`,\nunless you pass `threads: false`.\n\n## Local Development\n\n```bash\nnpm install\nnpm run dev\n```\n\nUseful local pages:\n\n- `/`: landing page linking to all local test pages\n- `/runtime.html`: runtime initialization and single-frame smoke test\n- `/listen-test.html`: microphone, sample clip, or local file playback with a\n  worklet/bypass switch\n- `/audio-worklet.html`: minimal AudioWorklet initialization demo\n- `/background-noise.html`: microphone or sample clip background-noise\n  detector tuning demo\n- `/audio-worklet-validation.html`: validation page for the worklet runtime\n- `/audio-worklet-benchmark.html`: real-time AudioWorklet benchmark\n- `/browser-benchmark-litert.html`: LiteRT benchmark page\n- `/browser-benchmark-compare.html`: single-threaded vs threaded comparison\n- `/browser-benchmark-litert-manual.html`: DevTools benchmark helper harness\n\nThe Vite dev server is configured with COOP and COEP headers so\ncross-origin-isolated runtime experiments are possible during local development.\n\nThe same pages are deployed from `main` to\n[GitHub Pages](https://workadventure.github.io/noise-suppression/). GitHub Pages\ndoes not provide the COOP and COEP headers required by threaded LiteRT, so the\nhosted runtime comparison is limited to the single-threaded path.\n\n## Build And Test\n\n```bash\nnpm run typecheck\nnpm run build\nnpm run build:pages\nnpm run test:browser\n```\n\nThe Pages build writes the compiled multi-page test site to `pages-dist/`.\n\nThe library build writes:\n\n- `dist/index.js`\n- `dist/index.d.ts`\n- `dist/audio-worklet.js`\n- `dist/audio-worklet.d.ts`\n- `dist/background-noise.js`\n- `dist/background-noise.d.ts`\n- `dist/assets/audio-worklet-processor.js`\n- `dist/assets/*.tflite`\n- `dist/vendor/litert/*`\n- `dist/vendor/silero/*`\n- `dist/vendor/onnxruntime/*`\n\n## Architecture Notes\n\n- The worklet path uses the repository-local [LiteRT ESM fork](https://github.com/moufmouf/LiteRT/tree/esm-module) and passes bundled\n  Wasm bytes to the Emscripten module factory.\n- The bundled worklet path currently runs LiteRT single-threaded.\n- The lower-level frame API currently depends on LiteRT.js internal synchronous\n  runner APIs to keep `dtln_denoise()` synchronous.\n- Threaded LiteRT experiments require cross-origin isolation in production.\n- The background-noise detector uses Silero VAD and is independent from the DTLN\n  denoiser.\n\nSee [Architecture Decision Records](./docs/adr/README.md) for more background.\n","readmeFilename":"README.md"}