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

This commit is contained in:
2026-07-29 20:03:35 +01:00
parent d99dab45e5
commit 86de3400a9
25 changed files with 688 additions and 209 deletions
+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;
}
}