Stop asking for voice isolation on top of the voice pipeline
Since the mid-call device work, every call has asked for voiceIsolation as an ideal whenever studio audio was off — which is to say, for everyone, by default. Before that the two pages asked for `audio: true` and took the browser's own pipeline. On a Mac that name reaches Apple's system Voice Isolation, and it runs after the browser's noiseSuppression rather than instead of it. Two suppressors on one voice is not twice as clean: it is a growl with the keystrokes still coming through, because transients survive what a model takes a held vowel to be. A call on 2026-09-17 came back exactly that way, and the person on the other end was inaudible while his typing was not. So voiceIsolation is now only ever asked for *off*, and never on. Off it still belongs here: someone may have switched Voice Isolation on for the whole machine, and studio audio — an interface in a room that has been treated — is precisely the case that has to be able to say no to it. The three flags above it are unchanged and are every browser's own default. The two copies stay byte-identical, so the mirror test still holds them together, and the test now says out loud that the default path names three flags and leaves the fourth alone. This does not explain the call it was found by: both people were on the Mac app, which is native WebRTC and never loads room.js. That one is still open. Gates: node --test test/browser/audio.test.mjs, and make browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3 files changed,
+44
−9
internal/calls/static/devices.js+13 −3internal/calls/static/room.js+13 −3test/browser/audio.test.mjs+18 −3
diff --git a/internal/calls/static/devices.js b/internal/calls/static/devices.js| index 79fc997..73ca423 100644 |
| --- a/internal/calls/static/devices.js |
| +++ b/internal/calls/static/devices.js |
| @@ -90,8 +90,18 @@ |
| // the holding screen checks afterwards what it was actually given |
| // (rawHonoured) instead of trusting that it got what it asked for. |
| // |
| - // voiceIsolation is the newer, heavier filter in Safari and Chrome, |
| - // and it is not everywhere. An unknown constraint name in this basic, |
| + // voiceIsolation is the newer, heavier filter in Safari and Chrome — |
| + // on a Mac it is Apple's own system Voice Isolation, reached through |
| + // the browser. It is only ever asked for *off*, and never on. Asked |
| + // on, it runs on top of the noise suppression above rather than |
| + // instead of it, and two suppressors chewing one voice is not twice |
| + // as clean: it is a growl with the keystrokes still coming through, |
| + // which is what a call on 2026-09-17 came back as. The pipeline above |
| + // is every browser's own default and is the one this app asks for. |
| + // |
| + // Off it still belongs here, because someone may have turned Voice |
| + // Isolation on for the whole machine and studio audio has to be able |
| + // to say no to it. An unknown constraint name in this basic, |
| // non-exact form is dropped rather than refused, so naming it costs |
| // nothing in a browser that has never heard of it. |
| function audioConstraints(prefs) { |
| @@ -100,8 +110,8 @@ |
| echoCancellation: processed, |
| noiseSuppression: processed, |
| autoGainControl: processed, |
| - voiceIsolation: processed, |
| }; |
| + if (!processed) constraints.voiceIsolation = false; |
| if (prefs.mic) constraints.deviceId = { exact: prefs.mic }; |
| return constraints; |
| } |
diff --git a/internal/calls/static/room.js b/internal/calls/static/room.js| index ba339f1..8b3b909 100644 |
| --- a/internal/calls/static/room.js |
| +++ b/internal/calls/static/room.js |
| @@ -359,8 +359,18 @@ |
| // the holding screen checks afterwards what it was actually given |
| // (rawHonoured) instead of trusting that it got what it asked for. |
| // |
| - // voiceIsolation is the newer, heavier filter in Safari and Chrome, |
| - // and it is not everywhere. An unknown constraint name in this basic, |
| + // voiceIsolation is the newer, heavier filter in Safari and Chrome — |
| + // on a Mac it is Apple's own system Voice Isolation, reached through |
| + // the browser. It is only ever asked for *off*, and never on. Asked |
| + // on, it runs on top of the noise suppression above rather than |
| + // instead of it, and two suppressors chewing one voice is not twice |
| + // as clean: it is a growl with the keystrokes still coming through, |
| + // which is what a call on 2026-09-17 came back as. The pipeline above |
| + // is every browser's own default and is the one this app asks for. |
| + // |
| + // Off it still belongs here, because someone may have turned Voice |
| + // Isolation on for the whole machine and studio audio has to be able |
| + // to say no to it. An unknown constraint name in this basic, |
| // non-exact form is dropped rather than refused, so naming it costs |
| // nothing in a browser that has never heard of it. |
| function audioConstraints(prefs) { |
| @@ -369,8 +379,8 @@ |
| echoCancellation: processed, |
| noiseSuppression: processed, |
| autoGainControl: processed, |
| - voiceIsolation: processed, |
| }; |
| + if (!processed) constraints.voiceIsolation = false; |
| if (prefs.mic) constraints.deviceId = { exact: prefs.mic }; |
| return constraints; |
| } |
diff --git a/test/browser/audio.test.mjs b/test/browser/audio.test.mjs| index 328a5a5..70b96f9 100644 |
| --- a/test/browser/audio.test.mjs |
| +++ b/test/browser/audio.test.mjs |
| @@ -63,10 +63,22 @@ test('the voice pipeline is on unless it is turned off', () => { |
| assert.equal(c.echoCancellation, true); |
| assert.equal(c.noiseSuppression, true); |
| assert.equal(c.autoGainControl, true); |
| - assert.equal(c.voiceIsolation, true); |
| }); |
| -test('studio audio turns all of it off', () => { |
| +test('voice isolation is never asked for on top of the pipeline', () => { |
| + // The second suppressor is the one that does the damage: Apple's |
| + // system Voice Isolation running after the browser's own noise |
| + // suppression returns a growl with the keystrokes still in it. The |
| + // default path names three flags and leaves the fourth alone, so |
| + // whatever the machine is set to is what a person gets. |
| + assert.equal('voiceIsolation' in audioConstraints({}), false); |
| + assert.equal('voiceIsolation' in audioConstraints({ rawAudio: false }), false); |
| +}); |
| + |
| +test('studio audio turns all of it off, system voice isolation included', () => { |
| + // The one place voiceIsolation is named: someone may have switched it |
| + // on for the whole machine, and an interface in a treated room is |
| + // exactly the case that has to be able to say no to it. |
| const c = audioConstraints({ rawAudio: true }); |
| assert.equal(c.echoCancellation, false); |
| assert.equal(c.noiseSuppression, false); |
| @@ -79,10 +91,13 @@ test('the processing is asked for as an ideal and never as exact', () => { |
| // would mean a browser that cannot turn its filters off refuses |
| // getUserMedia outright — costing someone their microphone over a |
| // preference, which is the opposite of what the ladder in either file |
| - // is built to do. Booleans are ideals; objects are not. |
| + // is built to do. Booleans are ideals; objects are not. A name the |
| + // constraints leave out entirely is not under test here: that is the |
| + // test above. |
| for (const prefs of [{}, { rawAudio: true }, { rawAudio: false }]) { |
| const c = audioConstraints(prefs); |
| for (const key of ['echoCancellation', 'noiseSuppression', 'autoGainControl', 'voiceIsolation']) { |
| + if (!(key in c)) continue; |
| assert.equal(typeof c[key], 'boolean', key + ' must be a bare boolean, not ' + JSON.stringify(c[key])); |
| } |
| } |