feat: audio - add concurrent mixing, output switching, and device-aware UI

This commit is contained in:
daniel committed 2026-07-29 20:03:35 +01:00
1 parent d99dab45e5
commit 86de3400a9
25 files changed
+688 -209

No files matched your search

+96 -49
View File
@@ -65,9 +65,9 @@ namespace Drivers::USB::Bluetooth::A2dp {
// State
// =========================================================================
static State g_state = State::Idle;
static std::atomic<State> g_state{State::Idle};
static uint16_t g_sigCid = 0; // L2CAP CID for AVDTP signaling
static uint16_t g_mediaCid = 0; // L2CAP CID for AVDTP media transport
static std::atomic<uint16_t> g_mediaCid{0}; // L2CAP CID for AVDTP media transport
static uint8_t g_txLabel = 1;
static uint8_t g_remoteSeid = 0; // Remote stream endpoint ID
static uint8_t g_localSeid = 1; // Our local SEID
@@ -92,7 +92,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
// SBC encoder
static Sbc::SbcEncoder g_sbcEncoder = {};
static bool g_sbcInitialized = false;
static std::atomic<bool> g_sbcInitialized{false};
static std::atomic<bool> g_routeChanged{false};
// SBC capability negotiation. An A2DP source must SetConfiguration with a
// subset of what the sink advertised in GetCapabilities -- asserting a fixed
@@ -120,6 +121,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
static std::atomic<uint32_t> g_ringHead{0}; // producer: WriteAudio
static std::atomic<uint32_t> g_ringTail{0}; // consumer: PumpMedia
static std::atomic<bool> g_pumpActive{false}; // single pumper at a time
static std::atomic<bool> g_serviceActive{false}; // serialize USB event reap too
static uint32_t g_pcmRate = 48000;
static uint64_t g_clockBase = 0; // ms timestamp of the media clock zero
static uint64_t g_sentSamples = 0; // per-channel samples sent since reset
@@ -132,12 +134,8 @@ 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<int> g_outputOwnerPid{-1};
static std::atomic<bool> g_muted{false};
static std::atomic<int> g_requestedVolume{-1};
// AVDTP response tracking
static volatile bool g_avdtpResponseReady = false;
@@ -1313,6 +1311,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
// with no kernel log output at all).
case AVDTP_CLOSE: {
g_state = State::Idle;
g_routeChanged.store(true, std::memory_order_release);
SendAvdtpResponse(txLabel, AVDTP_CLOSE, nullptr, 0);
KernelLogStream(WARNING, "BT-A2DP") << "Remote CLOSED stream";
break;
@@ -1327,6 +1326,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
case AVDTP_ABORT: {
g_state = State::Idle;
g_routeChanged.store(true, std::memory_order_release);
SendAvdtpResponse(txLabel, AVDTP_ABORT, nullptr, 0);
KernelLogStream(WARNING, "BT-A2DP") << "Remote ABORTED stream";
break;
@@ -1353,6 +1353,13 @@ namespace Drivers::USB::Bluetooth::A2dp {
// =========================================================================
bool ConfigureStream(uint32_t sampleRate, uint8_t channels, uint8_t bitsPerSample) {
// Encoder configuration and PumpMedia both mutate the SBC encoder.
// Device switching normally configures an Open stream, but explicitly
// exclude a pumper that was already in flight.
bool expected = false;
if (!g_pumpActive.compare_exchange_strong(expected, true,
std::memory_order_acquire))
return false;
Sbc::Init(&g_sbcEncoder, sampleRate, channels, bitsPerSample);
// Override with the SBC parameters actually negotiated in
// SetConfiguration so the encoded frame headers match what the sink
@@ -1373,6 +1380,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
<< (uint64_t)sampleRate << "Hz " << (uint64_t)bitsPerSample << "-bit "
<< (uint64_t)channels << "ch";
g_pumpActive.store(false, std::memory_order_release);
return true;
}
@@ -1380,19 +1388,40 @@ namespace Drivers::USB::Bluetooth::A2dp {
// StartStream / StopStream
// =========================================================================
static bool AcquireMediaService() {
for (int spin = 0; spin < 100000; spin++) {
bool expected = false;
if (g_serviceActive.compare_exchange_weak(expected, true,
std::memory_order_acquire))
return true;
asm volatile("pause" ::: "memory");
}
return false;
}
bool StartStream() {
if (!AcquireMediaService()) return false;
bool result = false;
if (g_state == State::Open || g_state == State::Configured) {
if (g_state == State::Configured) {
if (!AvdtpOpen()) return false;
if (!AvdtpOpen()) {
g_serviceActive.store(false, std::memory_order_release);
return false;
}
}
if (!AvdtpStart()) return false;
ResetMediaClock();
return true;
if (AvdtpStart()) {
ResetMediaClock();
result = true;
}
} else {
result = (g_state == State::Streaming);
}
return (g_state == State::Streaming);
g_serviceActive.store(false, std::memory_order_release);
return result;
}
bool StopStream(bool flushQueued) {
if (!AcquireMediaService()) return false;
if (g_state == State::Streaming) {
uint8_t payload[1] = {(uint8_t)(g_remoteSeid << 2)};
SendAvdtpCommand(AVDTP_SUSPEND, payload, 1);
@@ -1406,6 +1435,7 @@ namespace Drivers::USB::Bluetooth::A2dp {
g_ringTail.store(g_ringHead.load(std::memory_order_relaxed),
std::memory_order_release);
}
g_serviceActive.store(false, std::memory_order_release);
return true;
}
@@ -1521,9 +1551,8 @@ namespace Drivers::USB::Bluetooth::A2dp {
bytesPerFrame - firstPart);
g_ringTail.store(tail + bytesPerFrame, std::memory_order_release);
uint32_t numSamples = samplesPerFrame * g_sbcEncoder.Channels;
for (uint32_t i = 0; i < numSamples; i++) {
framePcm[i] = (int16_t)(((int32_t)framePcm[i] * g_volume) / 100);
if (g_muted.load(std::memory_order_acquire)) {
memset(framePcm, 0, bytesPerFrame);
}
frameLen = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[off]);
@@ -1556,10 +1585,17 @@ namespace Drivers::USB::Bluetooth::A2dp {
static uint32_t rejCount = 0;
rejCount++;
if (rejCount <= 2 || (rejCount & 0x3FF) == 0) {
bool sbcInitialized =
g_sbcInitialized.load(std::memory_order_acquire);
State state = g_state.load(std::memory_order_acquire);
uint16_t mediaCid =
g_mediaCid.load(std::memory_order_acquire);
KernelLogStream(WARNING, "BT-A2DP") << "WriteAudio rejected #"
<< (uint64_t)rejCount << ": sbc=" << (uint64_t)(g_sbcInitialized ? 1 : 0)
<< " state=" << (uint64_t)(int)g_state
<< " mediaCid=" << base::hex << (uint64_t)g_mediaCid << base::dec;
<< (uint64_t)rejCount << ": sbc="
<< (uint64_t)(sbcInitialized ? 1 : 0)
<< " state=" << (uint64_t)(int)state
<< " mediaCid=" << base::hex << (uint64_t)mediaCid
<< base::dec;
}
return -1;
}
@@ -1579,14 +1615,39 @@ namespace Drivers::USB::Bluetooth::A2dp {
memcpy(&g_pcmRing[0], pcmData + firstPart, n - firstPart);
g_ringHead.store(head + n, std::memory_order_release);
// Reap events (NOCP credits, inbound traffic) and feed the link from
// syscall context too, so streaming keeps moving even when no core
// is idle.
// Event processing and SBC encoding deliberately happen in
// ServiceMedia(), after the mixer releases its lock.
return (int)n;
}
void ServiceMedia() {
if (!AcquireMediaService()) return;
Xhci::PollEvents();
Hci::DrainEvents();
PumpMedia();
g_serviceActive.store(false, std::memory_order_release);
}
return (int)n;
uint32_t GetWriteSpace() {
if (!g_sbcInitialized || g_state != State::Streaming || g_mediaCid == 0)
return 0;
uint32_t head = g_ringHead.load(std::memory_order_relaxed);
uint32_t tail = g_ringTail.load(std::memory_order_acquire);
return (PCM_RING_SIZE - (head - tail)) & ~3u;
}
void OnDisconnected(uint16_t aclHandle) {
if (aclHandle != L2cap::GetAclHandle()) return;
g_state.store(State::Idle, std::memory_order_release);
g_mediaCid.store(0, std::memory_order_release);
g_sbcInitialized.store(false, std::memory_order_release);
g_ringTail.store(g_ringHead.load(std::memory_order_relaxed),
std::memory_order_release);
g_routeChanged.store(true, std::memory_order_release);
}
bool ConsumeRouteChange() {
return g_routeChanged.exchange(false, std::memory_order_acq_rel);
}
// =========================================================================
@@ -1594,42 +1655,28 @@ namespace Drivers::USB::Bluetooth::A2dp {
// =========================================================================
State GetState() {
return g_state;
return g_state.load(std::memory_order_acquire);
}
bool IsStreaming() {
return (g_state == State::Streaming);
return g_state.load(std::memory_order_acquire) == State::Streaming;
}
int GetVolume() {
return g_volume;
}
void SetVolume(int percent) {
void RequestMasterVolume(int percent) {
if (percent < 0) percent = 0;
if (percent > 100) percent = 100;
g_volume = percent;
g_requestedVolume.store(percent, std::memory_order_release);
}
// =========================================================================
// 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 SetMuted(bool muted) {
g_muted.store(muted, std::memory_order_release);
}
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);
bool ConsumeVolumeRequest(int* percent) {
int value = g_requestedVolume.exchange(-1, std::memory_order_acq_rel);
if (value < 0) return false;
if (percent) *percent = value;
return true;
}
}
+18 -18
View File
@@ -59,36 +59,36 @@ 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);
// Free bytes in the PCM queue, aligned to complete stereo frames.
uint32_t GetWriteSpace();
// Tear down local media state after the ACL link disappears. The actual
// mixer notification is deferred out of the nested HCI receive path.
void OnDisconnected(uint16_t aclHandle);
bool ConsumeRouteChange();
// 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.
void PumpMedia();
// Reap controller events and pump media from process context. Kept
// separate from WriteAudio so the mixer never holds its spinlock across
// USB event processing or SBC encoding.
void ServiceMedia();
// Get current state
State GetState();
// Check if currently streaming
bool IsStreaming();
// Get volume (0-100)
int GetVolume();
// Queue a headset AVRCP absolute-volume request for the system mixer.
void RequestMasterVolume(int percent);
void SetMuted(bool muted);
// Set volume (0-100)
void SetVolume(int percent);
// AVRCP receive runs nested inside the transport event pump. Defer its
// master-volume request until the top-level Bluetooth service context.
bool ConsumeVolumeRequest(int* percent);
}
+3 -2
View File
@@ -7,6 +7,7 @@
#include "Avrcp.hpp"
#include "A2dp.hpp"
#include "L2cap.hpp"
#include <Drivers/Audio/Mixer.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
@@ -183,7 +184,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
// follow-up on actual change is a later feature.)
if (p[0] == EVT_VOLUME_CHANGED) {
uint8_t rp[2] = {EVT_VOLUME_CHANGED,
(uint8_t)((A2dp::GetVolume() * 127) / 100)};
(uint8_t)((Drivers::Audio::Mixer::GetMasterVolume() * 127) / 100)};
SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
pdu, rp, sizeof(rp));
} else if (p[0] == EVT_PLAYBACK_STATUS) {
@@ -203,7 +204,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
}
} else if (pdu == PDU_SET_ABS_VOLUME && ctype == AVC_CTYPE_CONTROL && plen >= 1) {
uint8_t vol = p[0] & 0x7F;
A2dp::SetVolume(((int)vol * 100) / 127);
A2dp::RequestMasterVolume(((int)vol * 100) / 127);
SendVendorRsp(localCid, transaction, AVC_RSP_ACCEPTED,
pdu, &vol, 1);
KernelLogStream(INFO, "BT-AVRCP") << "absolute volume -> "
@@ -8,6 +8,7 @@
#include "Hci.hpp"
#include "A2dp.hpp"
#include "IntelFirmware.hpp"
#include <Drivers/Audio/Mixer.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Fs/Vfs.hpp>
@@ -419,10 +420,14 @@ namespace Drivers::USB::Bluetooth {
void ServiceEvents() {
if (!g_initialized) return;
if (Xhci::InPollContext()) return; // never nest under PollEvents
Xhci::PollEvents();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
A2dp::PumpMedia(); // feed queued media to the link (no-op when idle)
A2dp::ServiceMedia(); // reap events and feed queued media
Drivers::Audio::Mixer::OnBluetoothWritable();
int requestedVolume;
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
Drivers::Audio::Mixer::SetMasterVolume(requestedVolume);
if (A2dp::ConsumeRouteChange())
Drivers::Audio::Mixer::OnBluetoothStateChanged();
}
// =========================================================================
@@ -605,6 +610,7 @@ namespace Drivers::USB::Bluetooth {
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
}
A2dp::StartSource();
Drivers::Audio::Mixer::OnBluetoothStateChanged();
}
// Persist any new link key now (process context), even if
// the link later dropped, so the disk write never stalls
+2
View File
@@ -6,6 +6,7 @@
#include "Hci.hpp"
#include "L2cap.hpp"
#include "A2dp.hpp"
#include <atomic>
#include <Fs/Vfs.hpp>
#include <Drivers/USB/Xhci.hpp>
@@ -795,6 +796,7 @@ namespace Drivers::USB::Bluetooth::Hci {
KernelLogStream(INFO, "BT-HCI") << "Disconnection: handle="
<< (uint64_t)handle << " reason=" << (uint64_t)reason;
A2dp::OnDisconnected(handle);
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active && g_connections[i].Handle == handle) {