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);