feat: A2DP reliability improvements
This commit is contained in:
@@ -1387,6 +1387,21 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
uint32_t bytesPerFrame = samplesPerFrame * g_sbcEncoder.Channels * 2;
|
||||
int16_t framePcm[512];
|
||||
|
||||
// Bundle as many SBC frames as fit in the media channel's MTU into
|
||||
// each RTP packet. One frame per packet means 375 packets/s at
|
||||
// 48 kHz -- each costing a 2-DH3 + ACK (~2.5 ms) on air, ~94% of the
|
||||
// radio's airtime. With zero headroom, any retransmission burst,
|
||||
// WiFi coexistence window (combo chip), or multipoint service to the
|
||||
// phone delays frames past the sink's deadline -> concealment static
|
||||
// and dropouts. Bundling to the MTU (5 frames at bitpool 53) cuts
|
||||
// this to ~75 packets/s, ~28% airtime, like every stock stack does.
|
||||
uint16_t maxPayload = 672; // default L2CAP MTU; never exceed buffer
|
||||
if (auto* mch = L2cap::GetChannel(g_mediaCid)) {
|
||||
if (mch->RemoteMtu >= 48 && mch->RemoteMtu < maxPayload) {
|
||||
maxPayload = mch->RemoteMtu;
|
||||
}
|
||||
}
|
||||
|
||||
while (bytesPerFrame <= sizeof(framePcm) && g_pcmRate != 0) {
|
||||
if (g_state != State::Streaming) break; // torn down mid-pump
|
||||
|
||||
@@ -1426,25 +1441,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
elapsed = audioMs;
|
||||
}
|
||||
|
||||
// Pull one frame from the ring (may wrap) and apply volume.
|
||||
uint32_t tail = g_ringTail.load(std::memory_order_relaxed);
|
||||
uint32_t idx = tail & (PCM_RING_SIZE - 1);
|
||||
uint32_t firstPart = PCM_RING_SIZE - idx;
|
||||
if (firstPart > bytesPerFrame) firstPart = bytesPerFrame;
|
||||
memcpy(framePcm, &g_pcmRing[idx], firstPart);
|
||||
memcpy((uint8_t*)framePcm + firstPart, &g_pcmRing[0],
|
||||
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);
|
||||
}
|
||||
|
||||
// Build media packet: RTP-like header (12 bytes) + optional SCMS-T
|
||||
// content-protection header (1 byte) + SBC payload header (1 byte)
|
||||
// + SBC frame
|
||||
uint8_t mediaPkt[256] = {};
|
||||
// + up to 15 SBC frames, MTU permitting
|
||||
uint8_t mediaPkt[768] = {};
|
||||
mediaPkt[0] = 0x80; // V=2, P=0, X=0, CC=0
|
||||
mediaPkt[1] = 0x60; // M=0, PT=96
|
||||
mediaPkt[2] = (uint8_t)(g_seqNum >> 8);
|
||||
@@ -1460,26 +1460,61 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// and the SBC payload header (0x00 = copy permitted, as BlueZ).
|
||||
uint32_t hdr = 12;
|
||||
if (g_sinkContentProtection) mediaPkt[hdr++] = 0x00;
|
||||
mediaPkt[hdr++] = 1; // SBC payload header: number of SBC frames
|
||||
uint32_t sbcHdrPos = hdr++; // SBC payload header: frame count
|
||||
|
||||
uint32_t encodedSize = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[hdr]);
|
||||
L2cap::SendData(g_mediaCid, mediaPkt, (uint16_t)(hdr + encodedSize));
|
||||
// Pull frames from the ring (may wrap), apply volume, encode.
|
||||
// Frame size is constant for a fixed config; the size of the
|
||||
// first encode bounds whether the next one still fits.
|
||||
uint32_t off = hdr;
|
||||
uint32_t nFrames = 0;
|
||||
uint32_t frameLen = 0;
|
||||
while (nFrames < 15) {
|
||||
if (frameLen != 0 && off + frameLen > maxPayload) break;
|
||||
uint32_t avail = g_ringHead.load(std::memory_order_acquire)
|
||||
- g_ringTail.load(std::memory_order_relaxed);
|
||||
if (avail < bytesPerFrame) break;
|
||||
|
||||
uint32_t tail = g_ringTail.load(std::memory_order_relaxed);
|
||||
uint32_t idx = tail & (PCM_RING_SIZE - 1);
|
||||
uint32_t firstPart = PCM_RING_SIZE - idx;
|
||||
if (firstPart > bytesPerFrame) firstPart = bytesPerFrame;
|
||||
memcpy(framePcm, &g_pcmRing[idx], firstPart);
|
||||
memcpy((uint8_t*)framePcm + firstPart, &g_pcmRing[0],
|
||||
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);
|
||||
}
|
||||
|
||||
frameLen = Sbc::Encode(&g_sbcEncoder, framePcm, &mediaPkt[off]);
|
||||
off += frameLen;
|
||||
nFrames++;
|
||||
}
|
||||
if (nFrames == 0) break;
|
||||
mediaPkt[sbcHdrPos] = (uint8_t)nFrames;
|
||||
|
||||
L2cap::SendData(g_mediaCid, mediaPkt, (uint16_t)off);
|
||||
|
||||
g_seqNum++;
|
||||
g_timestamp += samplesPerFrame;
|
||||
g_sentSamples += samplesPerFrame;
|
||||
g_timestamp += samplesPerFrame * nFrames;
|
||||
g_sentSamples += samplesPerFrame * nFrames;
|
||||
g_lastSendMs = now;
|
||||
g_inUnderrun = false;
|
||||
|
||||
// Bring-up diagnostic: heartbeat every 2048 frames (~5.5 s of
|
||||
// audio) proving the pipeline is healthy.
|
||||
if ((g_seqNum & 0x7FF) == 0) {
|
||||
// Bring-up diagnostic: heartbeat every 512 packets (~6.8 s of
|
||||
// audio at 5 frames/packet) proving the pipeline is healthy.
|
||||
if ((g_seqNum & 0x1FF) == 0) {
|
||||
uint32_t left = g_ringHead.load(std::memory_order_acquire)
|
||||
- g_ringTail.load(std::memory_order_relaxed);
|
||||
uint32_t ringMs = (g_pcmRate && g_sbcEncoder.Channels)
|
||||
? (uint32_t)((uint64_t)(fill - bytesPerFrame) * 1000
|
||||
? (uint32_t)((uint64_t)left * 1000
|
||||
/ ((uint64_t)g_pcmRate * g_sbcEncoder.Channels * 2))
|
||||
: 0;
|
||||
KernelLogStream(INFO, "BT-A2DP") << "media: seq="
|
||||
<< (uint64_t)g_seqNum
|
||||
<< " bundle=" << (uint64_t)nFrames
|
||||
<< " ring=" << (uint64_t)ringMs << "ms"
|
||||
<< " lead=" << (uint64_t)(audioMs > elapsed ? audioMs - elapsed : 0) << "ms"
|
||||
<< " inflight=" << (uint64_t)Hci::AclTxInFlight()
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Drivers/USB/UsbDevice.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -54,6 +55,13 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
static uint8_t* g_aclTxRing[ACL_TX_SLOTS] = {};
|
||||
static uint64_t g_aclTxRingPhys[ACL_TX_SLOTS] = {};
|
||||
static uint8_t g_aclTxSlot = 0;
|
||||
// Serializes the whole send (slot grab + packet build + xHCI bulk OUT
|
||||
// enqueue). PumpMedia and DrainEvents have SEPARATE single-instance
|
||||
// guards, so one core can be sending a media packet while another sends a
|
||||
// signaling reply; without this lock the two race on g_aclTxSlot and on
|
||||
// the bulk OUT TRB ring's enqueue/cycle state, garbling packets on air.
|
||||
// IRQ-disabling spinlock so the holder also can't be preempted mid-send.
|
||||
static kcp::Spinlock g_aclTxLock;
|
||||
|
||||
// HCI command DMA buffer (separate from ACL to avoid conflicts)
|
||||
static uint8_t* g_cmdDmaBuf = nullptr;
|
||||
@@ -549,6 +557,17 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
if (!g_initialized || !g_aclTxRing[0]) return false;
|
||||
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
|
||||
|
||||
g_aclTxLock.Acquire();
|
||||
|
||||
// Every slot still owned by the xHCI: reusing one would let this
|
||||
// packet's memcpy race the in-flight DMA. Dropping is recoverable
|
||||
// (L2CAP peers retransmit signaling; media just skips a frame),
|
||||
// corruption on air is not.
|
||||
if (AclTxInFlight() >= (uint32_t)ACL_TX_SLOTS) {
|
||||
g_aclTxLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the next TX ring slot so a rapid second send can't overwrite this
|
||||
// packet before its bulk OUT transfer DMAs it to the wire.
|
||||
uint8_t* txBuf = g_aclTxRing[g_aclTxSlot];
|
||||
@@ -569,6 +588,7 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
g_aclTxCount.fetch_add(1, std::memory_order_relaxed);
|
||||
Xhci::QueueBulkOutTransfer(g_slotId, txBuf, txPhys, totalLen);
|
||||
|
||||
g_aclTxLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user