Client: our own product. Role: everything: product, audio engine, machine learning, four native hosts, site. Live: agrohi.com/nomus · try the demo in your browser
nomus takes the background music out of live audio and keeps the voices, speech and singing. It runs in a Chrome tab, system-wide on macOS and Linux, and on Android. It is free, needs no account, and the audio never leaves the device.
We built it to answer a hard product question with engineering, and to show what we build for clients.
The problem
People want to hear what is being said without the music under it: a lecture with a soundtrack, a podcast with a music bed, a short video with a trending track over someone talking. Some people avoid music for personal or religious reasons. Others find it tiring or distracting.
The only other browser extension we found that filters live audio on the device uses a speech detector: it mutes everything it does not recognise as speech. That removes the music, and it also removes singing and cuts into the voice. We wanted to keep every voice, sung or spoken, and still run on the device.
Constraints
| Constraint | What it forced |
|---|---|
| Works on anything that plays, live | A streaming model with no look-ahead beyond one 10 ms hop |
| Not noticeably late | 20 ms of algorithmic delay, the same in every mode, so switching never shifts the sound |
| On the device, no server | The model ships inside each app and must be small enough for a real-time audio thread |
| Private by construction | No network path for audio, enforced by the platform, not promised |
| Browser, phone and desktop | One engine in portable Rust; thin native shells per platform |
| Keep the voice intact | Evaluation that punishes voice loss, not only rewards music removal |
The last row is the one that is easy to get wrong. A model that halves the voice while cutting music by 20 dB scores well on a naive metric and is useless to the listener.
Architecture: one Rust core, four hosts
Everything that decides what you hear lives in one crate. The hosts contain no audio logic; each one is a few hundred lines that turn one platform’s audio into calls on the engine. Every host loads the same model file, so one fix or one new model reaches all four at once.
The engine’s timing is a constant, and every mode is padded to it:
// crates/nomus-core/src/engine.rs
/// Processor latency every mode is padded to, so switching modes never
/// shifts time. Raise this when adding a processor with more lookahead.
pub const PROCESSOR_LATENCY_BUDGET: usize = FRAME_SIZE;
/// Total engine latency in samples (20 ms at 48 kHz).
pub const LATENCY: usize = FRAME_SIZE + Self::PROCESSOR_LATENCY_BUDGET;
The browser. An AudioWorklet has no fetch and no JavaScript glue, so the engine is compiled to WebAssembly with zero imports and exposed as a flat C ABI: one call per block per channel. It cannot reach the network or the disk, and the extension’s content security policy (connect-src 'self') blocks any request to another server.
// crates/nomus-wasm/src/lib.rs
pub unsafe extern "C" fn nomus_process(
engine: *mut Engine,
channel: u32,
input: *const f32,
output: *mut f32,
n: u32,
) -> u32 {
Android. Android lets an app capture other apps’ media audio, but does not mute the originals, so a naive filter plays both. nomus captures the audio, attaches a silencing effect to each captured app, and plays the filtered copy through its own track, which is excluded from capture. Audio crosses into Rust through direct buffers, with no copy. Android keeps a mute effect alive after the process that created it dies, so the app has three layers of crash safety to make sure no other app is ever left muted. The app has no INTERNET permission.
macOS. A Core Audio process tap hears every other process and mutes it at the source, with no driver. nomus filters the tap and writes the result to the real output in the same callback. If nomus crashes, the OS removes the tap and sound returns to normal.
// crates/nomus-system/src/macos.rs
let d = CATapDescription::initStereoGlobalTapButExcludeProcesses(CATapDescription::alloc(), &exclude);
d.setName(&NSString::from_str("nomus"));
d.setPrivate(true);
d.setMuteBehavior(CATapMuteBehavior::Muted);
Linux. nomus creates a PipeWire virtual sink, makes it the default output, and plays its filtered contents to the real device. No root, no kernel module.
Windows is not supported. Its loopback capture cannot silence the original sound, so a system-wide filter there needs a signed kernel audio driver. We said no rather than ship something that plays both.
The model and how we trained it
The network does not generate audio. Every 10 ms it predicts one gain between 0 and 1 for each of 481 frequency bins, and the engine multiplies the spectrum by it. The worst a bad prediction can do is turn something down.
| Part | Detail |
|---|---|
| Network | LayerNorm, Linear 481→256, two GRU layers of 256, Linear 256→481, sigmoid |
| Parameters | 1,037,475 (4.15 MB as 32-bit floats) |
| Inference | hand-written in Rust, no ML runtime; the WebAssembly engine is 815,166 bytes |
| Parity | the same input through PyTorch and through the real WebAssembly ABI differs by at most 8.6e-7 |
Every step of the training pipeline has a Rust twin: the same window, the same framing, the same GRU gate order. The model is trained in the exact form it runs in, and every exported model must pass a parity test natively and through the WebAssembly ABI before it can ship.
Training data: read speech (LibriSpeech), studio singing in nine languages (GTSinger), music stems (MUSDB18), thousands of instrumental tracks (Free Music Archive), and solo instruments that sound like voices, such as trumpet, flute and saxophone (Medley-solos-DB). Mixtures are built on the fly and degraded to sound like real uploads: EQ, reverb, compression and quantisation.
The lesson we would pass to any client: data moved the numbers, architecture did not. Four model generations share one architecture. A more complex frequency-local network was built, tested and not shipped; it gained at most 0.09 dB. Adding a multilingual singing corpus moved unseen singers from +2.58 to +5.85 dB.
Evaluation, including where it fails
No script promotes a model. A checkpoint is rejected if its output mask collapses to a constant, and the best one is chosen on held-out audio with penalties for voice lost. We test on sets built for specific failures: degraded real-world audio, singers the model never heard, music with nobody singing, and singing buried under loud music.
Held-out results for the current model (vocal8), mean over each set:
| Test set | Separation gain (SI-SDR) | Music removed | Voice level |
|---|---|---|---|
| MUSDB18 test songs | +6.50 dB | 14.4 dB | -0.91 dB |
| Unseen singers | +6.50 dB | 19.5 dB | -0.66 dB |
| Unseen singers, degraded | +5.88 dB | 14.7 dB | -1.41 dB |
| Free Music Archive music | +5.92 dB | 16.3 dB | -0.35 dB |
| Free Music Archive, degraded | +3.98 dB | 13.1 dB | -0.83 dB |
Against NoMusic, the only other extension that filters live tab audio on-device, on the same 193 held-out clips (this benchmark ran on our earlier vocal4 model):
| nomus (vocal4) | NoMusic Standard | |
|---|---|---|
| Mean separation gain | +5.34 dB | -0.98 dB |
| Clips made worse than the input | 8 of 193 | 82 of 193 |
| Singing clips losing more than 10 dB of voice | 0% | 24% |
| Music removed when nobody is talking | 17.3 dB | 40.5 dB |
Where it still falls short, in our own measurements:
- Silence between words. NoMusic mutes everything that is not speech, so it is much quieter in intros and pauses (about 40 dB against our 17 dB). Our choice keeps singing; it also means music is reduced, not silenced.
- Clean speech. The speech-enhancement model inside NoMusic, run on its own, beats ours by about 3.5 dB on clean speech mixtures.
- Saxophone. Solo tenor saxophone is only reduced by 3.8 dB. We need real saxophone recordings, not more training steps.
- Singing buried under loud music. On degraded audio with music 5 dB louder than an unseen singer, vocal8 keeps +1.9 dB of gain, against +3.7 dB for the previous model. We accepted that trade for 4 to 16 dB more removal of voice-like solo instruments.
- Leakage the model thinks is voice. 96% of the music that leaks through is in frames the model scores as voice, so no output gate can fix it. Only training can.
- No listening panel yet. All numbers are objective metrics on held-out audio.
Results
| Platform | Status (2026-09-27) | Evidence |
|---|---|---|
| Chrome extension | Live on the Chrome Web Store: version 0.4.0, with the vocal8 model | Store listing |
| macOS 14.2+ | Verified end to end on an arm64 Mac. Version 0.9.0 coming soon; notarisation waits on an Apple Developer ID | Live output equals the offline engine to -76.0 dB; music in pauses -18.2 dB; estimated delay about 44 ms; killing the app undid every mute, 3 of 3 |
| Linux (PipeWire) | Verified end to end in a headless PipeWire graph. Version 0.9.0 coming soon | Live output equals the offline engine to -77.7 dB; speakers bit-exact with nomus’s output; music in pauses -20.0 dB; estimated delay about 31 ms. A crash used to leave the default-output setting pointing at nomus; that is fixed, and 7 of 7 kill-test scenarios in Docker restored the user’s output. Not yet tested on real Linux hardware |
| Android | Verified on the emulator only. Version 0.9.0 coming soon, after tests on real phones | Device output equals the offline engine to -78 dB; 0.4 to 0.7 ms of compute per 10 ms block; capture to speaker 180 to 200 ms on the emulator, mostly the platform’s own audio paths |
Cost on the audio thread: about 48 µs per 128-sample stereo block in WebAssembly, 1.8% of the real-time budget (measured in Node/V8 on an Apple M4 Pro, not in a live Chrome tab).
What it proves for clients
- Real-time systems. We can hit a fixed latency budget on a real-time thread, with no allocation, no locks and no garbage collector, and prove it with tests.
- One core, many platforms. A single Rust engine behind a browser, a JVM, Core Audio and PipeWire, with every host checked against the same offline reference.
- Machine learning that ships. Data work, training, evaluation that looks for failure, and inference we wrote ourselves, small enough for a phone and an AudioWorklet.
- Privacy as a property of the system. No server to trust. The browser and the OS enforce it.
- Honest reporting. We publish the weaknesses with the wins, because that is what you need to decide.
A note on licences
nomus is free. Its model is trained partly on research datasets, and its weights are licensed CC BY-NC-SA 4.0. Agrohi does not sell nomus or its model; this page describes the work.