feat: audio - add concurrent mixing, output switching, and device-aware UI
This commit is contained in:
1 parent
d99dab45e5
commit
86de3400a9
25 files changed
+688
-209
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user