From 928fe0bbedd5d5a3011ca2e5a6c3969d3cf9e9f8 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Fri, 10 Jul 2026 12:41:17 +0200 Subject: [PATCH] fix: make BT A2DP output exclusive to one process The A2DP output is a single unmixed PCM stream. A second process opening audio while a stream was active would reconfigure the SBC encoder and media clock under the owner and interleave both apps' PCM into one ring, garbling playback (e.g. launching DOOM destabilized Music). Add ClaimOutput/ReleaseOutput pid ownership: the first opener gets the BT sink, later openers fall back to the HDA mixer, and only the owner can tear the stream down. The scheduler releases ownership on process exit so a killed app cannot leak the claim. Co-Authored-By: Claude Fable 5 --- kernel/src/Api/Audio.hpp | 40 +++++++++++++++-------- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Drivers/USB/Bluetooth/A2dp.cpp | 26 +++++++++++++++ kernel/src/Drivers/USB/Bluetooth/A2dp.hpp | 15 +++++++++ kernel/src/Sched/Scheduler.cpp | 6 ++++ 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/kernel/src/Api/Audio.hpp b/kernel/src/Api/Audio.hpp index 01212d4..2638b2d 100644 --- a/kernel/src/Api/Audio.hpp +++ b/kernel/src/Api/Audio.hpp @@ -26,6 +26,10 @@ namespace montauk::abi { static constexpr int AUDIO_HANDLE_BT = 0x100; static int64_t Sys_AudioOpen(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) { + auto* proc = Sched::GetCurrentProcessPtr(); + int pid = proc ? proc->pid : -1; + const char* name = proc ? proc->name : "?"; + // Auto-switch: when a Bluetooth A2DP sink is connected and its stream is // set up (StartSource left it Configured/Open), route audio to the // headphones -- like a phone does when you plug in BT. Falls back to @@ -35,16 +39,25 @@ namespace montauk::abi { if (state == Drivers::USB::Bluetooth::A2dp::State::Open || state == Drivers::USB::Bluetooth::A2dp::State::Streaming || state == Drivers::USB::Bluetooth::A2dp::State::Configured) { - Drivers::USB::Bluetooth::A2dp::ConfigureStream(sampleRate, channels, bitsPerSample); - if (Drivers::USB::Bluetooth::A2dp::StartStream()) { - return AUDIO_HANDLE_BT; + // The BT output is a single unmixed stream, so only the first + // opener gets it. Configuring it again while another stream + // owns it would reset the SBC encoder and media clock under + // that stream and interleave both apps' PCM into one ring + // (garbled playback, dropouts persisting until the owner + // reopens). Later openers fall through to the HDA mixer. + if (Drivers::USB::Bluetooth::A2dp::ClaimOutput(pid)) { + Drivers::USB::Bluetooth::A2dp::ConfigureStream(sampleRate, channels, bitsPerSample); + if (Drivers::USB::Bluetooth::A2dp::StartStream()) { + return AUDIO_HANDLE_BT; + } + // Stream would not start (sink unresponsive / state desync): + // returning the BT handle anyway would make every write fail + // with the app stuck reporting "playing" at 0:00. Fall through + // to the speakers instead. + Drivers::USB::Bluetooth::A2dp::ReleaseOutput(pid); + Kt::KernelLogStream(Kt::WARNING, "Audio") + << "BT A2DP stream failed to start; falling back to HDA"; } - // Stream would not start (sink unresponsive / state desync): - // returning the BT handle anyway would make every write fail - // with the app stuck reporting "playing" at 0:00. Fall through - // to the speakers instead. - Kt::KernelLogStream(Kt::WARNING, "Audio") - << "BT A2DP stream failed to start; falling back to HDA"; } } @@ -52,9 +65,6 @@ namespace montauk::abi { // hardware stream open across virtual streams, so multiple apps can // play simultaneously. if (Drivers::Audio::IntelHda::IsInitialized()) { - auto* proc = Sched::GetCurrentProcessPtr(); - int pid = proc ? proc->pid : -1; - const char* name = proc ? proc->name : "?"; return (int64_t)Drivers::Audio::Mixer::Open(sampleRate, channels, bitsPerSample, pid, name); } @@ -64,7 +74,11 @@ namespace montauk::abi { static int64_t Sys_AudioClose(int handle) { if (handle == AUDIO_HANDLE_BT) { - Drivers::USB::Bluetooth::A2dp::StopStream(true); // drop queued tail + // Stops the stream (dropping the queued tail) only when the caller + // owns the BT output, so a stale handle held by another process + // cannot tear down the owner's stream. + auto* proc = Sched::GetCurrentProcessPtr(); + Drivers::USB::Bluetooth::A2dp::ReleaseOutput(proc ? proc->pid : -1); return 0; } Drivers::Audio::Mixer::Close(handle); diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 9aa0514..34ee96d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 3 +#define MONTAUK_BUILD_NUMBER 4 diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp b/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp index 1963a82..39e5384 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp +++ b/kernel/src/Drivers/USB/Bluetooth/A2dp.cpp @@ -134,6 +134,11 @@ namespace Drivers::USB::Bluetooth::A2dp { // Volume static int g_volume = 80; + // Exclusive owner (pid) of the A2DP audio output, -1 = free. See + // ClaimOutput/ReleaseOutput in the header: the output is one unmixed + // stream, so a second process sharing the handle would corrupt it. + static std::atomic g_outputOwnerPid{-1}; + // AVDTP response tracking static volatile bool g_avdtpResponseReady = false; static uint8_t g_avdtpResponseBuf[128] = {}; @@ -1606,4 +1611,25 @@ namespace Drivers::USB::Bluetooth::A2dp { g_volume = percent; } + // ========================================================================= + // Output ownership (one process at a time; see header) + // ========================================================================= + + bool ClaimOutput(int pid) { + if (pid < 0) return false; + int expected = -1; + return g_outputOwnerPid.compare_exchange_strong(expected, pid, + std::memory_order_acq_rel); + } + + void ReleaseOutput(int pid) { + if (pid < 0) return; + if (g_outputOwnerPid.load(std::memory_order_acquire) != pid) return; + // Stop (suspend + flush queued PCM) BEFORE freeing ownership, so a + // concurrent Open cannot configure the stream while it is being + // torn down. + StopStream(true); + g_outputOwnerPid.store(-1, std::memory_order_release); + } + } diff --git a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp b/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp index 6870fec..165b3da 100644 --- a/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp +++ b/kernel/src/Drivers/USB/Bluetooth/A2dp.hpp @@ -59,6 +59,21 @@ namespace Drivers::USB::Bluetooth::A2dp { // of bytes accepted (0 = ring full, retry later). int WriteAudio(const uint8_t* pcmData, uint32_t pcmLen); + // The A2DP output is a single unmixed PCM stream, so at most one process + // may own the Bluetooth audio handle at a time. A second opener sharing + // it would reconfigure the SBC encoder and media clock under the first + // stream and interleave its raw PCM into the same ring (audible garble + // and dropouts), and its close would suspend the owner's stream. + // + // ClaimOutput returns true if `pid` now owns the output; false if it is + // already owned (the caller should fall back to the HDA mixer). + // ReleaseOutput stops the stream (dropping queued PCM) and frees the + // output when `pid` is the current owner; no-op otherwise. The + // scheduler also calls it on process exit so a killed app cannot leak + // ownership. + bool ClaimOutput(int pid); + void ReleaseOutput(int pid); + // Encode + send queued PCM, paced to the audio clock and gated on ACL TX // readiness. Called from the idle-loop event pump and from WriteAudio; // self-serializing, cheap no-op when not streaming. diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 8dfad62..d8d3361 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include // Assembly: context switch with CR3 and FPU state parameters @@ -1104,6 +1105,11 @@ namespace Sched { // and its ring buffer don't leak when an app forgets to audio_close. Drivers::Audio::Mixer::CleanupProcess(exitingPid); + // Release the Bluetooth A2DP output if this process owned it, so a + // killed app cannot leave the output claimed forever (no-op when the + // process was not the owner). + Drivers::USB::Bluetooth::A2dp::ReleaseOutput(exitingPid); + // Release process-scoped IPC handles/mappings before tearing down the address space. Ipc::CleanupProcessSlot(slot, exitingPid, proc.pml4Phys); montauk::abi::CleanupHeapForSlot(slot, proc.pml4Phys);