From 34215fbd32054e101a1add65bb888df531a5a18f Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Sun, 12 Jul 2026 01:46:03 +0200 Subject: [PATCH] fix(static): request raw stereo audio for the loopback capture track MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chromium treats a getDisplayMedia audio track as a voice call by default: echo cancellation, noise suppression, auto gain control and mono downmix. Music through that pipeline is the tester-reported "tin can" sound on ASIO/exclusive outputs. Request the raw path explicitly (EC/NS/AGC off, stereo, 48 kHz) — all constraints are best-effort so unsupported ones degrade silently instead of failing the capture. The [asio-diag] loopback line now dumps track.getSettings() so logs prove which processing actually applied. Co-Authored-By: Claude Fable 5 --- static/js/juce-audio.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/static/js/juce-audio.js b/static/js/juce-audio.js index 8122089..bf279a3 100644 --- a/static/js/juce-audio.js +++ b/static/js/juce-audio.js @@ -614,9 +614,22 @@ import { S } from './player-state.js'; let _lbStream = null, _lbCtx = null, _lbTap = null, _lbPageMuted = false; let _loopbackUnavailable = false; // sticky: probe once, then fall back async function _engageLoopback() { + // Chromium's default capture pipeline treats the audio track as a + // VOICE call: echo cancellation + noise suppression + AGC + mono + // downmix. On music that is the "tin can" sound (tester 2026-07-12). + // Request the raw stereo path explicitly — every constraint here is + // best-effort (ideal-valued), so unsupported ones degrade silently + // rather than failing the capture. const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, - audio: { suppressLocalAudioPlayback: true }, + audio: { + suppressLocalAudioPlayback: true, + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false, + channelCount: 2, + sampleRate: 48000, + }, }); for (const t of stream.getVideoTracks()) t.stop(); // required, unused const track = stream.getAudioTracks()[0]; @@ -637,8 +650,19 @@ import { S } from './player-state.js'; _lbPageMuted = (await api.setPageMuted(true)) === true; } if (window._asioDiagEnabled?.()) { + // Full track settings: shows whether the raw-audio constraints + // (no EC/NS/AGC, stereo) actually took — a track that still + // reports echoCancellation=true or channelCount=1 explains + // "tin can" quality reports directly from the log. + const s = track.getSettings?.() || {}; console.log('[asio-diag] loopback: suppressed=', suppressed, - 'pageMuted=', _lbPageMuted, 'rate=', _lbCtx.sampleRate); + 'pageMuted=', _lbPageMuted, 'rate=', _lbCtx.sampleRate, + 'track=', JSON.stringify({ + sampleRate: s.sampleRate, channelCount: s.channelCount, + echoCancellation: s.echoCancellation, + noiseSuppression: s.noiseSuppression, + autoGainControl: s.autoGainControl, + })); } await api.setRendererBus(true, 1.0); tap.active = true;