feat: A2DP reliability improvements

This commit is contained in:
2026-06-12 11:48:24 +02:00
parent ca79678823
commit 22691c5c17
3 changed files with 82 additions and 27 deletions
+61 -26
View File
@@ -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()