feat: audio - add concurrent mixing, output switching, and device-aware UI
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user