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
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 12 #define MONTAUK_BUILD_NUMBER 13
+61 -26
View File
@@ -1387,6 +1387,21 @@ namespace Drivers::USB::Bluetooth::A2dp {
uint32_t bytesPerFrame = samplesPerFrame * g_sbcEncoder.Channels * 2; uint32_t bytesPerFrame = samplesPerFrame * g_sbcEncoder.Channels * 2;
int16_t framePcm[512]; 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) { while (bytesPerFrame <= sizeof(framePcm) && g_pcmRate != 0) {
if (g_state != State::Streaming) break; // torn down mid-pump if (g_state != State::Streaming) break; // torn down mid-pump
@@ -1426,25 +1441,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
elapsed = audioMs; 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 // Build media packet: RTP-like header (12 bytes) + optional SCMS-T
// content-protection header (1 byte) + SBC payload header (1 byte) // content-protection header (1 byte) + SBC payload header (1 byte)
// + SBC frame // + up to 15 SBC frames, MTU permitting
uint8_t mediaPkt[256] = {}; uint8_t mediaPkt[768] = {};
mediaPkt[0] = 0x80; // V=2, P=0, X=0, CC=0 mediaPkt[0] = 0x80; // V=2, P=0, X=0, CC=0
mediaPkt[1] = 0x60; // M=0, PT=96 mediaPkt[1] = 0x60; // M=0, PT=96
mediaPkt[2] = (uint8_t)(g_seqNum >> 8); 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). // and the SBC payload header (0x00 = copy permitted, as BlueZ).
uint32_t hdr = 12; uint32_t hdr = 12;
if (g_sinkContentProtection) mediaPkt[hdr++] = 0x00; 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]); // Pull frames from the ring (may wrap), apply volume, encode.
L2cap::SendData(g_mediaCid, mediaPkt, (uint16_t)(hdr + encodedSize)); // 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_seqNum++;
g_timestamp += samplesPerFrame; g_timestamp += samplesPerFrame * nFrames;
g_sentSamples += samplesPerFrame; g_sentSamples += samplesPerFrame * nFrames;
g_lastSendMs = now; g_lastSendMs = now;
g_inUnderrun = false; g_inUnderrun = false;
// Bring-up diagnostic: heartbeat every 2048 frames (~5.5 s of // Bring-up diagnostic: heartbeat every 512 packets (~6.8 s of
// audio) proving the pipeline is healthy. // audio at 5 frames/packet) proving the pipeline is healthy.
if ((g_seqNum & 0x7FF) == 0) { 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 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)) / ((uint64_t)g_pcmRate * g_sbcEncoder.Channels * 2))
: 0; : 0;
KernelLogStream(INFO, "BT-A2DP") << "media: seq=" KernelLogStream(INFO, "BT-A2DP") << "media: seq="
<< (uint64_t)g_seqNum << (uint64_t)g_seqNum
<< " bundle=" << (uint64_t)nFrames
<< " ring=" << (uint64_t)ringMs << "ms" << " ring=" << (uint64_t)ringMs << "ms"
<< " lead=" << (uint64_t)(audioMs > elapsed ? audioMs - elapsed : 0) << "ms" << " lead=" << (uint64_t)(audioMs > elapsed ? audioMs - elapsed : 0) << "ms"
<< " inflight=" << (uint64_t)Hci::AclTxInFlight() << " inflight=" << (uint64_t)Hci::AclTxInFlight()
+20
View File
@@ -12,6 +12,7 @@
#include <Drivers/USB/UsbDevice.hpp> #include <Drivers/USB/UsbDevice.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <CppLib/Spinlock.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
@@ -54,6 +55,13 @@ namespace Drivers::USB::Bluetooth::Hci {
static uint8_t* g_aclTxRing[ACL_TX_SLOTS] = {}; static uint8_t* g_aclTxRing[ACL_TX_SLOTS] = {};
static uint64_t g_aclTxRingPhys[ACL_TX_SLOTS] = {}; static uint64_t g_aclTxRingPhys[ACL_TX_SLOTS] = {};
static uint8_t g_aclTxSlot = 0; 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) // HCI command DMA buffer (separate from ACL to avoid conflicts)
static uint8_t* g_cmdDmaBuf = nullptr; static uint8_t* g_cmdDmaBuf = nullptr;
@@ -549,6 +557,17 @@ namespace Drivers::USB::Bluetooth::Hci {
if (!g_initialized || !g_aclTxRing[0]) return false; if (!g_initialized || !g_aclTxRing[0]) return false;
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer 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 // 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. // packet before its bulk OUT transfer DMAs it to the wire.
uint8_t* txBuf = g_aclTxRing[g_aclTxSlot]; 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); g_aclTxCount.fetch_add(1, std::memory_order_relaxed);
Xhci::QueueBulkOutTransfer(g_slotId, txBuf, txPhys, totalLen); Xhci::QueueBulkOutTransfer(g_slotId, txBuf, txPhys, totalLen);
g_aclTxLock.Release();
return true; return true;
} }