Skip to content

research: aptX Adaptive/Lossless status on a non-Qualcomm Linux host - #15

Closed
baizhu945 wants to merge 64 commits into
arkq:masterfrom
baizhu945:research/aptx-adaptive
Closed

baizhu945 wants to merge 64 commits into
arkq:masterfrom
baizhu945:research/aptx-adaptive

Conversation

@baizhu945

@baizhu945 baizhu945 commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

aptX Adaptive / Lossless research on a non-Qualcomm Linux host

First of all, thank you for openaptx and for keeping the reverse-engineered
apt-X documentation and tooling public. The protocol notes in this repository
were the starting point for the work below, and being able to compare against a
readable implementation of the classic aptX framing saved a lot of guesswork.

This PR is a research report plus the diagnostic helper that drives the Qualcomm
aptX Adaptive encoder module (extracted from a phone firmware image) under
QEMU/Hexagon on a host without a Qualcomm Bluetooth controller.

What is in the PR

Path Content
research/aptx-adaptive-qemu/STATUS.md the status report (rewritten, see below)
research/aptx-adaptive-qemu/README.md how to build and run the helper
research/aptx-adaptive-qemu/TOOLS.md the diagnostic tooling
research/aptx-adaptive-qemu/aptx-lossless-helper.c the QEMU adapter (R2 + R3)
research/aptx-adaptive-qemu/compat.c compatibility symbols for the module
src/aptx-adaptive-stream.c, include/aptxadaptive.h OTA transport-header parser
test/aptx-adaptive-stream.c its unit test

The PR also contains a merge of the current master, which resolves the previous
conflict in test/aptx-lifecycle.c in favour of the upstream version (this
branch had carried an earlier version of the same fix, which upstream merged as
PR #14).

Current status

The encoder path is solved and verified; the sink still does not decode.

Verified by measurement:

  • the Qualcomm reference decoder decodes the frames the host actually transmits
    over Bluetooth (48 kHz stereo, 440.0 Hz FFT peak for a 440 Hz sine input, and
    44.1 kHz stereo with a 440.1 Hz peak for a live capture);
  • the host's SET_CONFIG, RTP header, OTA header and codec frames are
    byte-identical to a working Android source's towards the same headphone;
  • replaying the Android source's own captured records through the host's
    PipeWire/BlueZ path reproduces them 266/266 byte for byte in an HCI
    capture, and the stream is continuous (30.8 s, no gap above 60 ms).

Ruled out — every one of these was tested and the headphone stayed silent:

  • missing OTA header (the wire payload is RTP(12) + OTA(8) + frame(656), the
    same 676 bytes the phone sends);
  • wrong AVDTP configuration (the phone-identical 40 02 … 92 string);
  • invalid bitstream (replay of the phone's own records);
  • low bitrate / long frames (phone frames re-stamped to a 16 ms period,
    62.5 fps, 338 kbps);
  • wrong sample rate (96 kHz end-to-end);
  • missing R2.2 version byte (0xaf) and the R2.2 packet shape (760-byte frames in
    780-byte payloads, channel_mode 0xa0);
  • discontinuous stream, source device class (adapter Class of Device set to
    "smartphone"), AVRCP playback state, encoder silence.

Protocol findings

  • The 8-byte OTA transport header is [TTP:2][period:1][packet_type:1][channel_mode:1][00 00][version:1],
    with the period in 0.25 ms units and packet_type indexing the payload-size
    table {348, 656, 140, 152, 560, 760, 960, 348, 980}. This is implemented in
    src/aptx-adaptive-stream.c and covered by a test.
  • The module's own period/PCM tables are [20, 19, 18, 17, 16, 15, 14, 13, 12] ms
    and [960, 912, 864, 816, 768, 720, 672, 624, 576] samples at 48 kHz.
  • AVS_ENCODER_PARAM_ID_BIT_RATE_LEVEL_MAP (0x000132e1) takes kbit/s
    values, not bit/s; the thresholds are 262/275/290/307/327/348/373/402/436.
  • The OTA version byte is level-driven: encLevelHqStateMachine emits 0xaf
    (R2.2) for levels 6..15 and 0xae (R2) below.
  • The frame-type byte encodes the sample rate and the channel count
    (b0 = 96 kHz mono, f0 = 96 kHz stereo, c0 = 44.1 kHz, d0 = 48 kHz).
  • The R2.2 wrapper pins the process-time period to 25 ms / 656 bytes / ~210 kbps
    even though the inner codec is configured for 14 ms / 364 kbps; the CAPI
    profile, MTU, sink buffers, IMCL bitrate level and input block size do not
    change that.

The module's own diagnostics (HAP_debug_v2) are what made this visible; they
are hidden behind an environment check in compat.c, and TOOLS.md documents
the one-line change that re-enables them.

Update 2026-09-12: what the air capture and the R2.2 work established

The sections above are kept as the record of the earlier rounds. Later rounds
changed the conclusions; they are reported in full in the PR comments, with the
raw chronology in HANDOFF.md section 21.

The host side is exhausted. Every host-controlled variable has now been put
on the air behind a mandatory gate (the card must really be on
aptx_adaptive and the headset must be the default sink, and the wire shape is
verified from an HCI capture afterwards):

variable values tried result
sample rate 48 kHz and 44.1 kHz (the phone's own value) silent
OTA version byte 0xae (R2) and 0xaf (R2.2) silent
packet type 0x01 (ordinary R2, 656-byte frame) and 0x05 (R2.2, 760-byte frame) silent
AVDTP element byte-identical to the phone's, R2.2 bit included silent
stream content the phone's own captured records replayed silent
time base wall clock, audio clock, frozen silent

The R2.2/Lossless path is rate-bound on this module build. With
lossless=force, a 16-bit source word and ABR off, the path produces a
continuous stream of 780-byte records -- version 0xaf, packet type 0x05,
760-byte frames, channel_mode 0xa0, i.e. the Snapdragon Sound form itself
(1237 packets over 61.8 s in the live run; 938 over 46.8 s in the capture that
was re-measured) -- and it is still silent. The encoder consumes
2204 input samples per frame, about 50 ms at 44.1 kHz (46.5 ms measured on
the wire, ~125 kbps), while both sources that do play run at roughly a 10 ms
cadence. Neither the encoder profile (nine values), the bitrate level (0..8),
the MTU (700..3000) nor the PCM block size moves it, and the one alternative
period recorded in the notes (cap_ext_ver_num = 0, "16 ms") kills the helper
process outright: the OTA fields change as documented and the stream dies after
24 packets.

A real bug was found on the way: configure_bitrate_map() sent the map in
bit/s while the module snaps those values to the kbit/s thresholds
262/275/290/307/327/348/373/402/436, which collapses all five levels into one.
The failure mode is documented in STATUS.md 4.3 but the fix had never landed
in the tracked source; it is applied now. It does not change the observable
stream, so the Lossless path evidently does not consult that map -- reported for
completeness rather than as the answer.

A correction to our own instrumentation, found while preparing this update.
research/aptx-adaptive-qemu/stream_check.py unpacked the OTA header with
period as a 16-bit field. The header is
[TTP:2][period:1][packet_type:1][channel_mode:1][pad:2][version:1], so that
folded the packet-type byte into the period (0x64 0x01 read as 356) and
shifted every following field by one byte: what the tool printed as ptype was
really channel_mode, and its declared-period figure was meaningless. Raw bytes
settle it -- the host's own ordinary stream is 3c 0f 64 01 00 00 00 ae
against the phone's ... 64 01 00 00 00 ae, i.e. period = 100 (25.00 ms) and
packet_type = 1 (656-byte frame) on both sides, and the measured interval is
25.00 ms. Two earlier statements of ours are therefore withdrawn: there is no
helper unit-conversion bug, and the ordinary stream's declared cadence matches
the wire exactly. The tool is fixed and now cross-checks the frame size against
the packet-type table, which is what would have caught it. HANDOFF.md 21.17
and STATUS.md 5.12 record it in full.

What the air capture adds. An Ubertooth One was used as a passive receiver
while the phone played to the same headset:

  • this host's own aptX Adaptive link is plainly visible (saturated RSSI, and
    hop-following locks onto its clock), so "aptX Adaptive cannot be sniffed" is
    not true;
  • the phone's audio never appears as standard BR/EDR traffic: its piconet is
    seen only while the link is being set up, and a follower locked onto it then
    hears nothing for three minutes. The phone's own HCI log rules out LE Audio
    (zero ISO packets, no LE connection or CIS events, while the Qualcomm A2DP
    offload commands are all present), so its audio really does travel over
    BR/EDR -- on a link a standard receiver cannot see;
  • the FiiO BT11 (QCC5181) settles the question from the other side, because it
    plays in both modes and can be power-cycled on demand -- and it corrects us
    twice. Its piconet is not 0x08064f/0xb7163b at all (neither ever appears
    in either mode) but 0x6a8fcc: it is plainly visible while the link is
    being established
    -- 259 hits in 31 s in Adaptive and 246 in 31 s in
    Lossless, 8.4 and 7.7 hits/s over 45 and 39 channels, up to 0 dBm, with the
    headset's own address 0xb7163b appearing for two seconds just before, which
    is the paging phase -- and then invisible while it streams: 0 hits over
    the following 118 s of the same capture and over further captures, with audio
    audibly playing. Four 130 s captures around an operator-anchored power cycle
    pin the identity down (0 hits in both dongle-absent runs; 74.3 and 59.1
    hits/min in both streaming runs), and a 304 s Lossless capture backs the
    steady-state negative with a liveness control: 535 packets from 32 piconets,
    several at 40-80 hits, and zero from the BT11, 0x08064f or the headset.
    Cumulatively, 551 s of Lossless and 488 s of Adaptive playback went by without
    the BT11's link ever appearing. The earlier "Lossless stays visible in steady
    state (0x08064f, 103 hits/min)" is therefore withdrawn: with the operator
    confirming the dongle really ran Lossless, that anchor was a mis-attribution,
    and both modes leave standard BR/EDR once media flows.

Two measurement limits are worth recording for anyone repeating this: the
sniffer cannot measure throughput (single-channel counting recovers 1 to 2
percent of the real packets, calibrated against this host's own link at a known
40 packets/s), and a survey detects a low-traffic piconet only about once a
minute, so such LAPs show spurious 25 to 78 s gaps. What settled the link
questions was an operator-anchored power-cycle test together with the fact that
a piconet LAP is the master's address, plus the paged device's own address
showing up during the paging phase.

Where that leaves the question. The five-source table is the crux:

source mode link audible
FiiO BT11 (QCC5181) aptX Lossless visible only while connecting yes
FiiO BT11 ordinary Adaptive visible only while connecting yes
phone (Snapdragon) ordinary Adaptive visible only while connecting yes
this host (AX210) ordinary Adaptive standard EDR, visible no
this host (AX210) aptX HD standard EDR, visible yes

Host-side configuration is no longer the difference: the same host, on the same
link, plays aptX HD normally. Every audible source measured here -- the
phone's Adaptive, the BT11's Adaptive and the BT11's Lossless -- keeps its
steady-state link off standard BR/EDR, and the one source whose Adaptive link is
visible while it plays (this host) is silent.
That leaves the proprietary-PHY
requirement as the reading the evidence supports, and it removes the
observation the rival reading rested on: the claim that Lossless runs over
standard EDR.

The negative is not merely a missing observation. The Snapdragon Sound form was
emitted continuously on standard EDR (packet type 5, 760-byte frames, 0xaf,
channel_mode 0xa0) and the headset stayed silent, so producing the right
stream is not sufficient on this controller. What is left is a difference this
host cannot remove:

  1. the link: the audible sources carry their media over a PHY a standard
    receiver cannot see, which is exactly what an Intel controller provides no
    way to join;
  2. the version byte: the audible Lossless source negotiates R3 (0xad)
    while this module emits R2.2 (0xaf) -- APTX_OTA_VERSION can force the
    byte, but the state machine's acceptance of R3 without Qualcomm sideband
    feedback is untested, and by point 1 it would not be sufficient on its own;
  3. the cadence: the encoder is locked to 2204 input samples per frame
    (~46 ms here), ABR on stalls the pipeline after 55 packets, and the one
    alternative period in the notes kills the helper outright.

Any pointer on a controller that can drive the proprietary high-speed link, on
a module build that emits short (5-10 ms) adaptive frames, or on whether R3
(0xad) is reachable without Qualcomm sideband feedback, would be welcome. We
would rather be told the last one is a dead end than keep guessing.

Add the eight-byte R3 OTA framing parser, packet-size and channel-mode mappings, and focused CTest coverage. This is transport framing only; it does not implement the proprietary aptX Adaptive codec.

The observations were cross-checked against the binary and strings notes in https://github.com/AndyCruz08/aptX-lossless-info. No proprietary library or generated binary is included.
Add opt-in stdin/stdout adapters for exercising user-supplied Qualcomm CAPI modules under qemu-hexagon, including the R3 profile-6 path. The adapters are research scaffolding, not an open-source codec implementation or a production PipeWire backend.

No proprietary libraries, firmware, sysroot, or generated streams are included. The framing and observations were cross-checked with https://github.com/AndyCruz08/aptX-lossless-info; see also openaptx issue arkq#8 for the original research context.
Use CMake full include and library directory variables so openaptx.pc remains valid when the install prefix is an absolute Nix store path. This is needed by fixed-output/package builds and does not change codec behavior.
The FFmpeg and libfreeaptx cleanup helpers had inverted NULL checks, so valid contexts were not released while a NULL argument could be dereferenced. Add a CTest regression covering NULL-safe destruction and normal decoder lifecycles.\n\nThis is independent of the research-only Adaptive work in PR arkq#12 and contains no proprietary codec files.
Three fixes measured on the standalone container:

- Clear the peer-only R2.2 capability bit when Lossless is disabled.
  With the bit set and no Lossless feedback the R2.2 wrapper stalled or
  faulted on the 44.1 kHz path; sanitising it makes 44.1 kHz work.
- Downgrade a forced Lossless request to ordinary aptX Adaptive unless
  APTX_ADAPTIVE_ALLOW_UNSTABLE_LOSSLESS=1 is set, and warn loudly that
  the QHS/Wi-Fi feedback is an assertion, not a measurement.
- Detect the R3 kernel's unwrapping per-channel cursors before they run
  past the module allocation.  The kernel previously died with SIGSEGV
  at call ~94; it now returns EOVERFLOW with an explanation.
- Call capi_aptx_adaptive_enc_get_static_properties() with the same init
  property list used for init(), size the module memory from
  CAPI_INIT_MEMORY_REQUIREMENT (116144 bytes for this build, kept at the
  1 MiB floor), and query CAPI_PORT_DATA_THRESHOLD after init.
- Report the module's framework extension (FWK_EXTN_BT_CODEC 0x132e4) and
  the DISABLE_PREBUFFER / KPPS_SCALE_FACTOR events under
  APTX_ADAPTIVE_VERBOSE=1.
- Document that the reference libgcc.so must export the 64-bit integer
  builtins: libaptXAdaptiveEnc3.so references __hexagon_{div,mod,udiv,
  umod}di3 and faults when they are missing.
- Record the measured R3 cursor behaviour in the research notes.
The R3 kernel's per-channel descriptors are {base, data_start, data_end,
limit, limit2}.  The adapter treated 0x6428 as the right read cursor and
set it to 0x6430, but 0x6428 is the ring BASE.  That advanced the base by
one frame per call until it reached the 32 KiB limit, after which the
kernel stopped producing packets (the 7-packet Lossless stall and the
R3 SIGSEGV/EOVERFLOW at call 89).

Mark only the data window empty (0x6418 = 0x641c, 0x642c = 0x6430) and
let the kernel compact back to the base itself, as
aptx_adaptive3_enc_process() does at 0xd1b0/0xd200.

Measured after the fix: R3 @48 kHz runs 2000 packets in 2000 calls and
R2.2 Lossless @44.1 kHz runs 1945 packets in 2000 calls, with no stall or
crash.  Remaining limitation documented: the R3/Lossless output settles
into a small set of repeated frames; the reference stream from the earlier
standalone R3 probe shows the same pattern.
Extract the process loop from Qualcomm's open-source AudioReach container
(gen_cntr, BSD-3-Clause) and migrate it into the standalone helper:

- loop module->process() until the external output buffer holds a complete
  frame, retrying on media-format / port-threshold / process-state events
  and stopping when nothing changed (1000-iteration runaway guard)
- handle the BT codec framework extension events (DISABLE_PREBUFFER 0x132e5,
  KPPS_SCALE_FACTOR 0x132e7) exactly as gen_cntr_bt_codec_fwk_ext.c does
- add per-port state (max_frames_per_buffer, num_frames_in_buf, buffer size,
  release flag) mirroring gen_cntr_ext_out_port_t
- document the extraction, gap analysis and migration in
  research/aptx-adaptive-qemu/gen-cntr-process-loop.md

All five verification suites pass with no regression; the lossy R2 path is
unchanged (168 packets, all unique, monotonic TTP).
…onvention

The proprietary module calls
    aptX3Encode(ctx, input_descriptor, output_descriptor)
(decompiled from deferred_rhs_process @0x9a20 -> 0x9bc0).  Earlier probes
passed NULL as the input descriptor and the ring as the output descriptor,
which is exactly why the encoder emitted a 4-byte stub.

direct-probe2.c drives it correctly and the encoder returns 0 and produces
real, input-dependent bitstreams:

  sine : 81 cc d0 7f fc 0f ff d1 8c 01 0e 7d fe f4 32 93 b5 ...  (696 nz)
  noise: 81 cc d0 7f c8 05 e0 08 4b 5f 13 83 a8 1f a4 1f e2 0f ...  (707 nz)

Requirements found empirically:
  - input window must hold >= 1344 samples (2 frames), otherwise 0xF015
  - output descriptor needs limit2 - limit > 63, otherwise 0xF014
  - output descriptor needs limit - end > 11, otherwise 0xF016
  - with a 328-byte output buffer (matching the module) it emits a full
    328-byte frame with 325/327/291 non-zero bytes

Also dump the module's live descriptors: they are well formed, so the
remaining module-side stub has another cause.  Document the next steps for
a direct encoding pipeline in the helper.
…streams)

Drive aptX3Encode() with the convention the proprietary module uses:

    aptX3Encode(ctx, input_descriptor, output_descriptor)

- r3_direct_setup(): allocate per-channel rings, build the input descriptor
  (limit == limit2) and the output descriptor (limit = base+328,
  limit2 = limit+64) for the R3 mode
- process_audio_direct_r3(): deinterleave and >>8 the CAPI input (matching
  the module's internal scaling), wait for two frames, call aptX3Encode,
  then build the OTA header (TTP step 375, period from me+0x248, type from
  me+0x230, version from me+0x231) with 328 bytes per channel
- advance/compact the input window and free the rings on reset

Measured on 48 kHz R3: 40 packets, 40 distinct payloads, dense bitstreams
(652/600/486 non-zero of 656), OTA version 0xad and monotonic TTP.
The lossy R2/R2.2 path is unchanged (168/182 packets, all suites pass).
…ple frame

- do not advance the R3 input descriptor's start manually: the encoder already
  does (measured 720 samples per call), and double-counting made the window
  drain
- call the encoder on every block and treat a non-zero result as 'still
  buffering' (EAGAIN) instead of a hard error
- document that the R3 frame is 720 samples (15 ms) at 44.1/48/96 kHz and that
  720-sample blocks give exactly one packet per call with a zero-length window
The R2 path advances 375 TTP units per 25 ms packet, i.e. 15 units/ms
(TTP unit = 1/15000 s).  The direct R3 pipeline reused 375 for a 15 ms frame,
so the sink's buffer drained and playback stopped.  Derive the step from the
real frame duration and the encoder rate instead.

Also document the MOMENTUM 5 bring-up: the three real-link fixes
(WirePlumber SPA path, MemoryDenyWriteExecute for the qemu JIT, headset
autoswitch), the peer's Lossless capability bit appearing after the app
switch, and the remaining A2DP transport instability.
Controlled experiment on the same link: aptX HD and SBC write fine, aptX
Adaptive with STEREO (0x02) fails with EAGAIN and stalls at 17.8 kB/s, while
JOINT_STEREO (0x08) writes fine and reaches 384 kB/s PCM / 44.8 kB/s codec.
The btmon capture showed the stream starts perfectly (61 packets at 15 ms
intervals, TTP +225/packet) and then degrades; the sink buffers ~1 s of audio
and stops pulling.  A fixed TTP base cannot align with the sink's playout
scheduler, so seed the TTP from CLOCK_MONOTONIC (unit 1/15000 s) and keep
advancing it by the real frame duration.
…host

Records the hardware, the verified CAPI/R3 behaviour, the five host-side bugs
fixed (source type, channel mode, PipeWire quantum, TTP, 44.1 kHz for
Lossless), the AVDTP SET_CONFIG captured from a working Android source, and
the remaining gap: the sink accepts the stream at L2CAP level but does not
decode it, with a constant 14-byte payload prefix and a bitrate below the
lowest documented Adaptive level.
Adds a detailed continuation document covering the current configuration, the
eight verified fixes, the capture-only sink used to obtain a reference
bitstream, the confirmed frame layout, and the single remaining difference
(frame header byte 2) together with the tooling needed to investigate it.
When APTX_ADAPTIVE_REPLAY points at a file of 664-byte OTA records
(eight-byte transport header + 656-byte codec frame), return those
records verbatim instead of encoding.  This pushes a captured reference
bitstream through the exact same PipeWire/BlueZ path, which separates an
encoder defect from a transport or negotiation defect.

Verified: the replay helper returns the captured frames byte-for-byte
and PipeWire transmits them on the wire unchanged.
…oot cause fixed

- The encoder output is now proven correct: the Qualcomm reference decoder
  (openaptx PR arkq#9 tools under Wine) decodes the stream actually sent over
  Bluetooth to 440.0 Hz at 48 kHz.
- Replaying the phone's captured frames through the bridge is byte-exact on
  the wire, so the transport layer is proven correct too.
- Root cause found: the Adaptive sampling-frequency bitmask was wrong
  (44100 was 0x40 = 192 kHz, 96000 was 0xa0).  Fixed in the PipeWire fork.
- Tested 48k / 96k / R2.2 combinations after the fix; still silent.  The
  headphone capability record only advertises 48k/88.2k/192k, and the phone
  selects 0x40, so a real 192 kHz encode path is the next step.
Measured against the CPH2749 module: the CAPI sampling-rate selector is
1=48 kHz, 2=44.1 kHz, 3=96 kHz; any other value falls back to 48 kHz.
The previous 96000 -> 0 mapping therefore produced 48 kHz frames while
the A2DP config claimed 96 kHz.  Verified with the reference decoder:
selector 3 output decodes as 96000 Hz with frame header 83 00 b0 a1,
matching the captured phone 96 kHz section.

Also forward HAP_debug/HAP_debug_v2 to stderr under APTX_HAP_LOG so the
module's own bitrate/period reports are no longer discarded.

HANDOFF section 12 records the round-2 findings: the Adaptive
sampling-rate bits are 0x40=44.1k / 0x10=48k / 0x20=96k, our SET_CONFIG
and RTP header now match the phone byte for byte, and the remaining
suspicion is the bitrate (our 48 kHz stream is 212 kbps, below the
279 kbps floor).
Eleven profile values (0..7, 0x1000, 0x2000, 0x4000) all produce 1200
samples per frame and 664-byte packets at 48 kHz, so the R2 wrapper's
frame length is not profile-controlled.  The 212 kbps wire rate therefore
matches the captured phone 48 kHz stream (RTP ts +1200, 656-byte
payload) and the bitrate is not the cause of the silence; the user's
50 kB/s observation corresponds to the 96 kHz section.
…es, HAP logs

- The deployed APTX_ADAPTIVE_STRIP_OTA=1 was a regression: the Android
  reference source sends RTP(12) + OTA(8) + frame(656) = 676 bytes.  With
  the strip removed our wire payload is byte-identical to the phone's
  (266/266 replay records).
- Decoded the OTA header layout (TTP/period/packet_type/channel_mode/
  version) and the payload-size table.
- Extracted the module's period table [20..12 ms] and PCM interval table
  [960..576] and the kbps thresholds of the bitrate-level map; the helper
  was sending bps, which collapsed every level.
- Found why the module logs never appeared: the HAP_debug_v2 shim's
  APTX_HAP_LOG gate.  With it removed the whole decision chain is visible:
  the codec wants 14 ms / 364 kbps but the R2.2 wrapper pins the process
  period to 100 (25 ms) / 656 B / 204 kbps.
- Ruled out (still silent): OTA strip, phone-exact SET_CONFIG, replaying
  the phone's own 664-byte records, and a 16 ms / 338 kbps stream.
- Reference measurement on the same link: aptX HD 55 kB/s (works) vs
  aptX Adaptive 27 kB/s (silent); the headphone sends no reverse traffic.
- Added the probe/analysis tooling and TOOLS.md.
…xaf above)

Disassembling encLevelHqStateMachine shows the R2.2 wrapper picks the OTA
version byte from the encoder level: levels 6..15 emit 0xaf (R2.2) and
everything below emits 0xae (R2).  The R2.2 level tables also use
packet_type 5 (760-byte payloads, 768-byte packets) and channel_mode 0xa0,
which is exactly the shape the lossless=AUTO run already produces - only
the version byte is wrong.

The link evidence lines up: the phone uses features 0x0f000092 (R2.2 bit)
towards the headphone and 0x0f000017 towards this host, and we advertise
0x92 while sending 0xae frames.  That is now the leading silence
hypothesis, ahead of the frame-rate one (a 16 ms / 338 kbps replay is also
silent).

Also recorded: the IMCL br_level is clamped to 3 inside the module even
when level 7 is sent.
…rt 260ms

- Reference decoder confirms both 0xb0 and 0xf0 frame types decode as
  96 kHz stereo, so the difference is not the sample rate.
- Selector map measured at encoder_rate=96000: 0/1->48k, 2->44.1k, 3->96k,
  4..7->48k.  The 96k wrapper emits one packet per two 1920-sample blocks
  (3840 samples, 40 ms, period 0xa0, ~133 kbps).
- The headphone reports a 260 ms delay for aptX Adaptive and 235 ms for
  aptX HD (25 ms = one AD frame); both are accepted.
- AVRCP playback status stays Stopped on our side while the phone reports
  Playing, but aptX HD plays in the same Stopped state, so it is not a gate.
…port/session

Replaying the phone's own frames is silent in four shapes (48k original OTA,
44.1k original TTP, 44.1k clean TTP, full R2.2 768-byte 0xaf/0xa0 packets).
Together with the earlier negatives (OTA header, SET_CONFIG, 16 ms/338 kbps,
96 kHz, version byte alone) this rules out the codec payload and format.

New evidence: the reference decoder shows frame-type byte 2 encodes rate AND
channel count (b0=96k mono, f0=96k stereo, c0=44.1k, d0=48k), so our 96k
frame type is not a bug; and the phone's own 44.1k stream is 656 B / 25 ms =
210 kbps, i.e. our rate is normal and the ~50 kB/s figure belongs to aptX HD
(measured 55 kB/s here).

The remaining differences are the RTP seq/ts base and the controller/link
layer (the phone offloads aptX to its Qualcomm controller; we see zero
reverse ACL traffic over a 30 s stream).
Setting APTX_ADAPTIVE_LOSSLESS=auto to restore the R2.2 capability bit does
not only change the advertisement: the encoder switches to the R2.2 form, the
packet interval doubles to 50 ms and the bitrate halves to 13.5 kB/s (the
user-visible 13.37 kB/s).  With that setting our SET_CONFIG becomes
byte-identical to the phone's, the headphone still stays silent, so the
capability bit and the CIE are ruled out as the cause.
Add preflight.sh (adapter up, headset connected, A2DP/aptX Adaptive actually
negotiated, MOMENTUM 5 is the default sink) and stream_check.py (measure the
real packet size, interval, throughput and OTA header from a btmon capture and
compare them with the 48 kHz R2 expectation).  The Bluetooth card silently
falls back to headset-head-unit/CVSD when the A2DP profile is not selectable,
and an encoder mode switch can halve the bitrate without changing any host
property, so both checks are required before a result means anything.
… field

Every configuration was put on the air behind the new gate: the ordinary R2
form with version 0xae, the same form with 0xaf, and the R2.2 form.  The
stream checker now also compares the measured packet interval with the period
the OTA header itself declares, which is what caught the halved cadence.

The R2.2 experiment shows the live pipeline stalls after 55 packets (the
module waits for sideband feedback that never arrives), so that path is
incomplete rather than merely wrong, and at 48 kHz/24 bit -- what the phone
actually streams -- every host-side variant is silent with a verified correct
wire format.  The remaining difference is the link layer: the phone runs
Qualcomm QHS, the AX210 can only do standard EDR.
…periment

The phone's own SetConfiguration carries sampling frequency 0x40, which the
plugin header defines as 44.1 kHz (0x10 is 48 kHz), so every test before this
one ran at a rate the working source never uses.  At 44.1 kHz the ordinary R2
form is 676 B / 25 ms / 217 kbps with frame header 83 00 c0 a1, and with the
R2.2 capability advertised the codec element becomes byte-identical to the
phone's while the encoder stays on the ordinary path.

Adds preflight.sh (codec and sink gate), stream_check.py (wire shape plus a
self-consistency check against the OTA period field), cie_check.py (AVDTP
element comparison) and run_ad_test.sh (the whole sequence), and records the
44.1 kHz matrix in HANDOFF section 21.11.
The byte-identical configuration -- 44.1 kHz like the phone, the R2.2
capability advertised, the ordinary R2 stream, every element matching the
phone's SetConfiguration -- is still silent while the measured rate matches
(26.73 kB/s observed against 27.07 kB/s on the wire).  Every host-controlled
variable has now been tested and each one is silent, so the remaining
difference is the link layer: the phone runs Qualcomm QHS, the AX210 can only
do standard EDR.

Recorded with the next options (a Qualcomm M.2 controller, the external FiiO
BT11 that already works, or dropping AD for the codecs that do play) and the
restored baseline.
STATUS.md now records the Ubertooth investigation (the phone's aptX Adaptive
audio never appears as standard BR/EDR traffic; the piconet is visible only
while the link is being set up, and a locked hop-follower then hears nothing
for three minutes, which points at Qualcomm's proprietary 6 Mbps QHS PHY), the
three operator-anchored experiments that establish it, the survey caveat that
cost time, the completed 44.1 kHz / 48 kHz matrix that ends with a byte
identical codec element and still no audio, the two methodology corrections
(rate slip, and LOSSLESS=auto changing the encoder form), the new test gate and
its tooling, and the next experiment.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Round summary: air capture, and the host side is now exhausted

Hardware used

  • Host: Intel Core Ultra 7 255HX / NixOS, Intel AX210 (FC:B3:AA:C5:01:42).
    No Qualcomm controller anywhere in the machine.
  • Sink: Sennheiser MOMENTUM 5 (80:C3:BA:B7:16:3B), SEP 9, vendor 0x00d7.
  • Reference sources: HONOR 90 GT (Snapdragon, Android 16), FiiO BT11 (QCC5181).
  • Sniffer: Ubertooth One (1d50:6002), firmware and host tools 2020-12-R1
    (API 1.07).

Tooling added (all under research/aptx-adaptive-qemu/)

  • preflight.sh refuses to run a test unless the card really is on
    a2dp-sink with api.bluez5.codec = aptx_adaptive and the MOMENTUM 5 is the
    default sink. The card silently falls back to headset-head-unit (CVSD) when
    the A2DP profile is not selectable, and every host-side property then still
    looks correct while the test measures nothing; it happened three times and
    --fix repaired it each time.
  • stream_check.py measures the real wire shape from a btmon capture and,
    importantly, cross-checks the measured packet interval against the period the
    OTA header itself declares. That check is what caught an encoder-mode change
    that halved the bitrate while every host property stayed correct.
  • cie_check.py reads both AVDTP codec elements (direction taken from the
    btsnoop flags) and can assert that ours equals the phone's.
  • run_ad_test.sh runs gate, playback and both checks in one command.
  • air_analyse.py no longer hard-codes a tshark store path; a garbage
    collected path made tshark die with SIGBUS instead of reporting the problem.

The air capture: the phone's audio is not standard BR/EDR

The instrument is sound. The host's own aptX Adaptive link to the same
headset is plainly visible on the Ubertooth (saturated RSSI, and hop-following
locks onto its clock), so "aptX Adaptive cannot be sniffed" is not true.

The phone's piconet (LAP 0x40FDDD, UAP 0x46) is different. Three
experiments, each anchored by a deliberate action:

# action observation
A phone Bluetooth off for 40 s 0x40fddd appears only as it returns
B headset power-cycled 0x40fddd appears only at reconnection
C hop-follow across a reconnect follower locks (clock trim), hears 3 packets, then 180 s of silence

During steady-state playback three further follow attempts (100 s, 60 s, 90 s,
one with a relaxed access-code tolerance) hear zero packets, following the
headset's own LAP hears zero, and a 180 s survey does not find the phone at all
while the same capture finds other piconets 57 to 68 times.

One caveat cost real time and is recorded in the report: in survey mode the
detection of any given piconet recurs about every 60 seconds, so absence from a
survey is not evidence of absence. The load-bearing observation is experiment C.

Qualcomm High Speed (QHS) is a proprietary PHY rated up to 6 Mbps where
standard BR/EDR offers 1, 2 or 3 Mbps, enabled only when both ends support it,
and it must not disturb the LMP state machine. A link that has switched to QHS
is not demodulable by an Ubertooth. That fits every observation: the
standard-mode exchange happens while the link is being set up, and the audio
then rides a PHY the receiver cannot see.

The host side is exhausted

Every host-controlled variable has now been put on the air behind the gate
above, and each one is silent:

variable values tested
sample rate 48 kHz and 44.1 kHz (the rate the phone actually negotiates)
OTA version byte 0xae (R2) and 0xaf (R2.2)
OTA packet type 0x00 (ordinary R2 form)
AVDTP codec element byte-identical to the phone's, R2.2 capability included
stream content the phone's own captured PDUs replayed (section 6 of the report)
TTP time base wall clock, audio clock, frozen

With the R2.2 capability advertised and the rate pinned to 44.1 kHz, our
element towards the headset is d7 00 00 00 ad 00 40 02 50 64 64 64 ff ff 00 01 92 00 00 0f ... — the same 26 bytes the phone sends — and the stream is
the ordinary R2 form measured at 676 B / 25 ms / 217 kbps with packet type
0x00. The operator's own link monitor read 26.73 kB/s against the 27.07 kB/s
measured on the wire, so the data is genuinely transmitted. The sink stays
silent.

Two corrections worth recording

  1. Several earlier runs negotiated 48 kHz although the phone negotiates
    44.1 kHz with this headset (0x40 in its SET_CONFIG; the plugin header
    defines APTX_ADAPTIVE_SAMPLING_FREQ_44100 = 0x40, _48000 = 0x10). The
    "48 kHz" in the phone's developer options is a preference; the stream
    follows the content.
  2. An attempt to test only the capability bit via APTX_ADAPTIVE_LOSSLESS=auto
    also pushed the encoder into its R2.2 form (packet type 0xa0, 50 ms
    interval, half the bitrate), so that run was inconclusive. The bridge now
    prepares the element sent to the peer and the element handed to the encoder
    from separate copies (APTX_ADAPTIVE_ADVERTISE_R2_2 in
    baizhu945/pipewire 8976dcf).

Hypothesis and next experiment

The two sources that do play are Qualcomm devices; the one that does not is
not, and the air evidence says the phone's audio does not travel as standard
BR/EDR. The remaining difference is therefore the link mode, not anything the
host can put into the stream.

The discriminating experiment is a Qualcomm controller in the machine (an M.2
module, so still internal hardware). If the headset then plays, the link was
the gate and the encoder work here stands as it is; if it stays silent, the
decoder depends on something else again. Caveat: BlueZ would still not drive
Qualcomm's aptX offload, so the host would keep sending ordinary A2DP payload
over that link — which is exactly the variable under test.

Full details: research/aptx-adaptive-qemu/STATUS.md, sections 6 and 7.
Raw chronological log: research/aptx-adaptive-qemu/HANDOFF.md, section 21.

Unplugging the FiiO BT11 for 40 s during a 300 s survey identifies its link:
piconet 0x08064F has 103 hits (peak -30 dBm) before the unplug, is absent for
the whole window and returns afterwards.  That is the link which plays audio on
this headset, and it is ordinary BR/EDR, so a proprietary PHY is not a
prerequisite.  QHS is an optional Qualcomm-to-Qualcomm optimisation, used by
the phone and apparently not by the BT11.

What remains is the frame shape: both working sources send the Snapdragon Sound
shape (packet type 5, 760-byte frames) while this host has never put a type-5
stream on the air continuously.  Making the R2.2/Lossless path survive the live
pipeline is therefore now the critical path rather than an optional feature.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Correction: the headset does not require Qualcomm QHS

My previous comment concluded that the MOMENTUM 5 only plays aptX Adaptive on
a Qualcomm QHS link. That conclusion is withdrawn; a direct experiment
refutes it.

The experiment

The FiiO BT11 (QCC5181) is a source that plays on this headset. I identified
its link by asking the operator to unplug it for 40 s while an Ubertooth
survey ran for 300 s, then aligning by timestamp:

08064f: 3s(-40)   [40 s unplugged: no hits at all]   65s(-48) 66(-66) 97(-63) 102(-63)

Before the unplug the same piconet had 103 hits with a peak RSSI of -30 dBm.
It disappears with the dongle and returns with it, so 0x08064F is the
BT11-to-headset link -- and it is ordinary BR/EDR that the sniffer reads
without difficulty.

What that means

The headset plays on a standard EDR link, so a proprietary PHY is not a
prerequisite. QHS is best read as an optional Qualcomm-to-Qualcomm
optimisation: the phone's FastConnect supports it (hence the phone's link being
invisible to the sniffer), the BT11's QCC5181 apparently does not, and the
headset plays either way. My earlier "the phone's audio is not standard BR/EDR,
therefore the headset requires QHS" inference skipped that step.

A methodological note, since it cost time: hop-following is far too sparse to
decide whether a piconet exists (0.45 packets/s on this host's own link, 0.08
packets/s on the BT11's). The reliable test is a high survey hit count combined
with a disappearance and return triggered by a deliberate operator action.

What is left

source link frame form audible
phone (Snapdragon) likely QHS, invisible only its AVDTP layer was captured yes
FiiO BT11 (QCC5181) standard EDR, visible Lossless: type 5, 760 B yes
this host standard EDR, visible ordinary R2: type 0, 656 B no

Both working sources send the Snapdragon Sound frame shape (packet type 5,
760-byte records). This host has never put a type-5 stream on the air
continuously -- its only attempt stopped after 55 packets -- so the leading
hypothesis is now that the headset decodes the R2.2/R3 shape but not the plain
R2 shape. One older row in the report ("wrong R2.2 shape, 768-byte 0xaf,
silent") argues against it, but whether that stream was continuous was never
verified and it probably stalled the same way.

Next steps, in order:

  1. Make the R2.2/Lossless path survive the live pipeline. This moved from an
    optional feature to the critical path: without a continuous type-5 stream
    the hypothesis cannot be tested.
  2. If the BT11 can be switched to ordinary aptX Adaptive, have it send plain
    Adaptive once. If the headset still plays, the shape hypothesis is wrong.
  3. Gate any type-5 stream with stream_check.py --expect-ptype 5 --expect-version 0xaf and listen.

STATUS.md sections 7.2 and 7.3 have been rewritten accordingly, and the raw
chronology is in HANDOFF.md section 21.13.

The 'the BT11 link carries only ~5 packets/s, so its audio is elsewhere' claim
was an uncalibrated extrapolation.  Calibrating the single-channel counting
method against this host's own link -- whose rate is known from its HCI capture
to be 40 packets/s -- shows it recovers 1 to 2 percent of the real packets and
that the figure depends on RSSI, so no link's throughput can be measured this
way, and EDR frame lengths are unavailable to the sniffer as well.

Whether the BT11's Lossless is real therefore remains unverified; the headset's
own codec report through Sennheiser's app is the practical check.

Also recorded: the LE Audio alternative is ruled out for the phone from its own
btsnoop logs (zero HCI ISO packets, no LE connection or CIS events, but the
Qualcomm A2DP offload commands are present), so its audio really does travel
over BR/EDR.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Follow-up: the sniffer cannot measure a link's throughput

A direct question came up that I could not answer from the air, and it is worth
recording because it limits what the previous two comments can claim.

Question: is the BT11's aptX Lossless real, or is it sending a lower rate
while its app says Lossless? If it were nominal, "both working sources use the
Snapdragon Sound shape" would lose its support.

Method and calibration. I camped the Ubertooth on a single channel and
counted the packets of one piconet, then calibrated that against this host's
own link, whose true rate is known exactly from its HCI capture (676 bytes
every 25.00 ms, i.e. 40 packets/s). Camping on three of that link's AFH
channels for 45 s each recovered:

channel counted true rate capture ratio
47 0.62 pkt/s 40 pkt/s 1.6 %
60 0.20 pkt/s 40 pkt/s 0.5 %
64 0.29 pkt/s 40 pkt/s 0.7 %

So the method recovers 1 to 2 percent of the real packets, and the figure
depends on RSSI (this host's link is saturated at 0 dBm, the BT11's sits at
-30 to -72 dBm). Frame lengths do not help either: the Ubertooth cannot decode
EDR headers, and our own reference capture shows packet_header = 0x00000000
with frame.len being the raw buffer size.

Consequences. My earlier statement that the BT11's visible link carries only
about 5 packets/s, and therefore not its audio, is withdrawn -- it was an
uncalibrated extrapolation. Whether the BT11's Lossless is real stays
unverified for now; the headset's own codec report (Sennheiser's app, read over
BLE) is the practical check rather than anything on 2.4 GHz.

One thing the same round did settle. I also explored the opposite
possibility -- that both working sources were actually using LE Audio, which
would have explained the phone's invisible link without QHS. The phone's own
btsnoop logs rule that out: zero HCI ISO packets, no LE connection or CIS
events (only extended advertising reports), while the Qualcomm A2DP offload
commands are all present (0xFC0A, 0xFD57, 0xFD59). The phone's audio
really does travel over BR/EDR, so for the phone the link-visibility mystery
stands.

STATUS.md sections 7.2 and 7.3 were updated accordingly, and HANDOFF.md
section 21.14 holds the raw numbers.

The BT11 can be switched between Lossless and ordinary Adaptive and plays in
both, which turns it into an experiment the fixed sources cannot provide.  A
piconet LAP is the master's address, so the BT11-to-headset link can only be
0x08064F (BT11 master) or 0xB7163B (headset master).  In Lossless mode 0x08064F
carries 103 hits/min and disappears exactly for a 40 s unplug; in ordinary
Adaptive mode both LAPs are at zero across two 300 s surveys while the audio
plays.  The whole piconet is therefore on a proprietary PHY in Adaptive mode,
not merely its media.

That restores the earlier reading for Adaptive and adds the useful half: the
BT11's Lossless runs over standard EDR and is decoded, so the headset does not
need a Qualcomm controller, it needs a stream shape it recognises.  The
R2.2/R3 path is consequently the critical one, and the project's original
non-Lossless scope conflicts with what this headset decodes on an Intel
controller.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Final result: ordinary Adaptive is decoded only over a proprietary link

This supersedes my comment of a few hours ago ("the BT11 plays over standard
EDR, so the QHS conclusion is withdrawn"). The experiment that prompted the
withdrawal was incomplete: it identified the BT11's link while the BT11 was
streaming Lossless, and I assumed the same link carried its ordinary Adaptive.
It does not.

The cleaner experiment. The operator can switch the BT11 between Lossless
and ordinary Adaptive, and it plays in both modes. A piconet's LAP is the
master's address, so the BT11-to-headset link can only ever be 0x08064F (BT11
as master) or 0xB7163B (headset as master) -- there is no third identity.

observation result
Lossless, unplug test 0x08064F at 103 hits/min, gone for the whole 40 s, back on replug
ordinary Adaptive, 300 s survey 0x08064F 0 hits, 0xB7163B 0 hits, audio playing
ordinary Adaptive, a second 300 s survey with another unplug test both LAPs 0 hits

In Adaptive mode the piconet is invisible as a whole -- the ACL signalling of a
visible piconet would still show up, so this is not just the media moving to a
sub-channel. The same hardware pair in Lossless mode runs a plainly visible
standard EDR link.

source mode link audible
FiiO BT11 aptX Lossless standard EDR, visible yes
FiiO BT11 ordinary Adaptive invisible (proprietary PHY) yes
phone (Snapdragon) ordinary Adaptive invisible yes
this host ordinary Adaptive standard EDR, visible no
this host aptX HD standard EDR, visible yes

What follows. Ordinary aptX Adaptive appears to be decoded only when it
arrives over a proprietary link, and the one host that cannot offer such a link
is the one that stays silent. But Lossless/R3 over plain EDR is decoded -- the
BT11 proves it -- so the headset does not require a Qualcomm controller, it
requires a stream shape it recognises. The R2.2/R3 path is therefore the
critical one; it currently stalls after 55 packets in the live pipeline, and
until that is fixed the shape cannot be tested at all. It also means the
project's original scope (ordinary, non-Lossless Adaptive) conflicts with what
this headset decodes on an Intel controller.

Two measurement limits worth knowing, both recorded in STATUS.md 7.3: the
sniffer cannot measure throughput (single-channel counting recovers 1 to 2
percent of the real packets, verified against this host's own link at a known
40 packets/s), and a survey detects a low-traffic piconet only about once a
minute, so such LAPs show spurious 25 to 78 s gaps. What settled the question
was the operator-anchored unplug test plus the fact that a LAP is the master's
address. The LE Audio alternative was ruled out separately from the phone's own
btsnoop logs (zero HCI ISO packets, no LE connection or CIS events, while the
Qualcomm A2DP offload commands are all present).

The helper sent the encoder's bitrate-level map in bit/s while the module snaps
those values to kbit/s thresholds, so all five levels collapsed into one.  That
failure mode is already documented in STATUS.md 4.3 but the fix had never
landed in the tracked source; it is applied now.  It does not change the
observable stream, so the lossless path evidently does not consult that map.

The round also established that ABR=0 is what makes the R2.2 path continuous
(1237 packets over 61.8 s, 780-byte records, type 0xa0, version 0xaf) while
that same stream is still silent and runs at 46.5 ms per packet -- the encoder
consumes 2204 samples per frame, and neither the profile, the bitrate level,
the MTU nor the PCM block size moves it.  cap_ext_ver_num = 0, the one
alternative period in the notes, kills the helper outright.

Also records the build environment repair (the toolchain's host libraries had
been garbage collected) and the probe extension that made the offline sweeps
possible.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Round report: R2.2 / Lossless path on the reference module, and the air capture

This comment closes out the most recent rounds. The PR description has been
updated with the current conclusions; the raw chronology is in HANDOFF.md
sections 21.1-21.16 and the user-facing summary in STATUS.md 6-7.

Everything below was collected on one machine:

  • Host: NixOS, PipeWire 1.6.0, the a2dp-codec-aptx-adaptive.c plugin in
    this PR, Intel AX210, Sennheiser MOMENTUM 5 (LMP 0x0D, manufacturer 0x001D).
  • Encoder: aptx_adaptive_enc_module.so.1,
    APTX.ADAPTIVE.2.2-00001-SPF-HEX-V73-SDK-4-5-0, run under
    qemu-hexagon -cpu v68.
  • Gate: every measurement below was taken behind preflight.sh, which
    refuses to run unless the card is really on aptx_adaptive, the headset is
    connected and it is the default sink -- the failure mode this guards against
    (silent CVSD fallback) was hit more than once during the work.
  • Verification: the wire shape is read back from an HCI capture
    (stream_check.py) and the negotiated AVDTP element from the same capture
    (cie_check.py), never from what the host intended to send.

1. The host-side matrix, all silent

variable values tried measured shape
sample rate 48 kHz; 44.1 kHz (the phone's value) 676 B / 25.00 ms / 216 kbps; 626 B / 25.00 ms / 200 kbps
OTA version 0xae (R2); 0xaf (R2.2) ptype 0x00 both
packet type 0x00; the R2.2 0xa0 shape 780 B records, 46.5 ms
AVDTP element ours; byte-identical to the phone's (40 02 ... 92) accepted, still silent
stream content generated; the phone's own records replayed silent
time base wall clock; audio clock; frozen silent

The 44.1 kHz run is worth singling out: the AVDTP element the headset accepted
was byte-identical to the one the phone negotiates, and the host's own rate
readout (26.73 kB/s) matched the measured wire rate (27.07 kB/s) -- so the
headset was receiving exactly the rate and configuration it receives from a
working source, and stayed silent.

2. The R2.2 / Lossless path is rate-bound in this build

lossless=force + a 16-bit source word + ABR off produces a continuous stream
instead of a dead one: 1237 packets / 61.8 s, 780 B each, packet_type 0xa0,
version 0xaf, ~15.6 kB/s. It is silent, and it is self-inconsistent: the OTA
period field (1424) does not match the measured 46.48 ms interval.

The reason is visible in the sample accounting: the encoder consumes 2204
input samples per frame
(confirmed by feeding a known number of samples and
counting the frames that come out; 2204 / 44.1 kHz = 50.0 ms, and the same
count fed at 48 kHz reproduces the 45.9 ms wall-clock interval that shows up as
46.48 ms on the wire). Nothing moves it: encoder profile (1..7, 0x1000, 0),
bitrate level (0..8), MTU (700..3000), PCM block size (672 and 1102 samples).

The one alternative period in the notes -- cap_ext_ver_num = 0, "16 ms" --
does change the OTA fields (period 356 -> 320, frame header 83 00 d0 a1 ->
81 00 2d a1, i.e. exactly the fields the R2.2 work predicted) but kills the
helper: broken pipe after 24 packets, reproducibly.

Both sources that actually play use roughly a 10 ms cadence. That is the
remaining host-side difference.

3. A real bug found here (fixed in this PR)

configure_bitrate_map() was sending the map in bit/s while the module
snaps those values to the kbit/s thresholds
262/275/290/307/327/348/373/402/436. All five levels therefore collapsed onto
one. The failure mode was already documented in STATUS.md 4.3 but the fix had
never landed in the tracked source; it is in this PR now
(279/320/352/384/420) with a comment. Honest caveat: it changes nothing
observable in the stream, so the Lossless path evidently does not consult that
map. It is reported because it is a real defect, not because it is the answer.

4. What a passive receiver adds

An Ubertooth One was used while the phone played to the same headset.

  • This host's Adaptive link is plainly visible to a standard receiver:
    hop-following locks onto its clock and RSSI saturates. "aptX Adaptive cannot
    be sniffed" is not true.
  • The phone's audio never appears. Its piconet is seen only while the link
    is being established; a follower locked onto it then hears nothing for three
    minutes (three independent attempts, plus a 180 s survey that saw zero from
    it while other piconets produced 57-68 hits). The phone's own HCI log rules
    out LE Audio -- zero ISO packets, no LE connection or CIS events, while all
    the Qualcomm A2DP offload commands are present -- so its audio does travel
    over BR/EDR, on a link a standard receiver cannot see.
  • The FiiO BT11 is the counter-example on the host side: once it sits next
    to the sniffer, its piconet is clearly visible in ordinary Adaptive mode too.
    A hidden link is therefore not a general requirement of the codec family;
    the phone's invisibility is vendor-specific behaviour, not something the
    headset demands.

5. Corrections to earlier statements in this thread

For the record, since some of these were asserted before being tested:

  1. I claimed a proprietary high-speed (QHS) link was required. Withdrawn when
    the BT11's link looked visible, then re-established for the phone after its
    Adaptive-mode piconet proved invisible. The BT11's own visibility is now
    confirmed with the dongle next to the sniffer, so QHS is the phone's choice,
    not the headset's requirement.
  2. I identified piconet 08064f as the BT11 and later as the phone based on a
    presence/absence pattern that was really a survey phase artefact (a survey
    detects a low-traffic piconet only about once a minute, producing spurious
    25-78 s gaps). That identification is retracted; what settled it was an
    operator-anchored unplug test plus the fact that a piconet LAP is the
    master's address.
  3. Earlier "13.37 kB/s" reports were an artifact of LOSSLESS=auto switching
    the encoder to ptype 0xa0 / 50 ms / 13.5 kB/s. The experiment was
    invalidated and later redone properly with a CIE split instead of a flag.
  4. The sniffer cannot be used for throughput: single-channel counting
    recovers 1-2 percent of real packets. It was calibrated against this host's
    own link at a known 40 packets/s. Any absolute rate read from the sniffer is
    meaningless; only presence/absence and relative comparison are usable.

6. Current status

The code in this PR is correct as far as it is measurable: it negotiates the
same AVDTP element a working source negotiates, and it emits well-formed R2
records at the right rate, on the air, verified from captures. AptX HD and the
other codecs in the plugin play normally on the same link, and the gate scripts
in research/aptx-adaptive-qemu/ make the verification reproducible.

What is missing is a frame cadence this module build will not produce (2204
samples/frame, immovable by every documented control). The remaining candidates
are: a different build of the same module (controller-side or DSP variant), a
Qualcomm controller driving its own offload, or a documented way to set the
adaptive frame period below 2204 samples. Any pointer on any of those three
would be very welcome -- everything else here is reproduced by
research/aptx-adaptive-qemu/run_ad_test.sh.

@baizhu945

Copy link
Copy Markdown
Contributor Author

Correction to the previous comment: the BT11's ordinary-Adaptive link is not visible

In the comment above I wrote that the FiiO BT11's piconet "is clearly visible in
ordinary Adaptive mode too", and drew the conclusion that ordinary Adaptive
therefore does not need a proprietary link and that the phone's invisibility is
merely vendor-specific. That is wrong, and it contradicts our own measurement
record
(STATUS.md 7.2, HANDOFF.md 21.15). It came from an earlier
mis-identification of a piconet LAP that had not been confirmed by an
operator-anchored unplug test. The corrected result is below, and the PR
description has been fixed to match it.

A piconet LAP is the master's address, so the BT11 -> headset link can only be
0x08064f (BT11 as master) or 0xb7163b (headset as master). There is no third
possibility:

observation result
BT11 in Lossless, unplug test 0x08064f 103 hits/min, gone for the 40 s of the unplug, back on replug
BT11 in ordinary Adaptive, 300 s survey 0x08064f 0 hits, 0xb7163b 0 hits, music playing
BT11 in ordinary Adaptive again, 300 s with unplug both LAPs 0 hits

So the BT11 switches PHY with the mode: Lossless runs on a visible standard
EDR link and is audible, ordinary Adaptive is invisible even down to its ACL
signalling.
The phone behaves like the BT11's ordinary-Adaptive mode, not like
an exception to it.

That makes the summary table the crux of the whole investigation:

source mode link audible
FiiO BT11 (QCC5181) aptX Lossless standard EDR, visible yes
FiiO BT11 ordinary Adaptive invisible (proprietary PHY) yes
phone (Snapdragon) ordinary Adaptive invisible yes
this host (AX210) ordinary Adaptive standard EDR, visible no
this host (AX210) aptX HD standard EDR, visible yes

Two readings fit this table and the data does not yet separate them: either the
headset decodes ordinary R2 only over the proprietary PHY, or it needs a
stream shape (frame cadence) that this module build cannot produce. The second
reading stays live precisely because Lossless is decoded over standard EDR --
so the headset does not require a Qualcomm controller, only a stream it
recognises.

I also want to retract the framing in item 5.1 of that comment: the QHS
hypothesis should not have been described as settled either way for the host
side. What the record supports is narrower and is exactly the table above.

Everything else in the previous comment stands: the host-side matrix (all
silent, gate-verified), the 2204-samples-per-frame bound on the R2.2 path, the
cap_ext_ver_num = 0 helper death, the bitrate-map unit defect (fixed, no
observable change), and the sniffer's inability to measure throughput. Apologies
for the noise -- the corrected statements are now in the description as well.

stream_check.py unpacked the OTA header with period as a 16-bit field, so the
packet-type byte was folded into it (0x64 0x01 read as 356) and every following
field was reported one byte out of step: the value the tool printed as ptype was
really channel_mode, and its declared-period figure was meaningless.

Raw bytes from the captures settle it.  The host's ordinary stream is
3c 0f 64 01 00 00 00 ae, i.e. period = 100 (25.00 ms) and packet type 1
(656-byte frame) -- byte-identical to the phone's header apart from TTP -- and
the measured interval is 25.00 ms.  Two earlier statements are therefore
withdrawn: there is no helper unit-conversion bug, and the ordinary stream's
declared cadence does match the wire.  The R2.2 form's declared-versus-measured
mismatch is real but different from what was reported (36.00 ms declared against
46.48 ms measured; the first attempt declared 28.00 ms).

The tool now parses [TTP:2][period:1][ptype:1][channel:1][pad:2][version:1],
reads the period in 0.25 ms units, defaults --expect-ptype to 1, and
cross-checks the frame size against the packet-type table, which is the check
that would have caught the original error.

That also sharpens the R2.2 result: the Snapdragon Sound form really is emitted
on the air continuously (version 0xaf, packet type 5, 760-byte frames,
channel_mode 0xa0, 938 packets over 46.8 s) and the headset stays silent, so
"only the stream shape is missing" is weaker than it looked.  What remains is
the version byte (R3 0xad against our R2.2 0xaf), the ~46 ms cadence this build
is pinned to, and the link itself.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Correction: our own wire-shape tool misread the OTA header; two claims withdrawn

The commit that just landed (7432663) fixes an error in our instrumentation,
found while preparing the previous comment. Reporting it in full, because it
invalidates two numbers we published and sharpens one conclusion.

The bug. research/aptx-adaptive-qemu/stream_check.py unpacked the 8-byte
OTA header with struct.unpack('<HHBBBB', ...), i.e. period as a 16-bit field.
The header really is [TTP:2][period:1][packet_type:1][channel_mode:1][pad:2] [version:1] -- our own STATUS.md 4.1 had it right from the start -- so that
read folded the packet-type byte into the period and shifted every following
field by one byte: what the tool printed as ptype was actually channel_mode.

Raw bytes, dumped without the tool:

capture OTA bytes correct reading tool (buggy) reading
our ordinary R2, 48 kHz 3c 0f 64 01 00 00 00 ae period 100 = 25.00 ms, ptype 1 period 356, ptype 0x00
our ordinary R2, 44.1 kHz 3c 0f 64 01 00 00 00 ae same same
phone (reference) ... 64 01 00 00 00 ae period 100 = 25 ms, ptype 1 --
our first R2.2 attempt 3c 0f 70 05 a0 00 00 af period 112 = 28.00 ms, ptype 5 period 1392, ptype 0xa0
our continuous R2.2 run 3c 0f 90 05 a0 00 00 af period 144 = 36.00 ms, ptype 5 period 1424, ptype 0xa0

Two facts confirm the byte reading with no unit assumption at all: the module's
payload-size table maps packet type 1 to 656 bytes and packet type 5 to 760
bytes, our captures carry 676-byte (12+8+656) and 780-byte (12+8+760) L2CAP
payloads, and the buggy reading gave values (0x00, and 0xa0 = 160) that are
not valid table indices.

Withdrawn:

  1. "the ordinary stream declares 23.73 ms while the packets are 25.00 ms
    apart, because the helper converts the period with a << 2 that should be
    x 3.75"
    -- there is no unit-conversion bug. The module's field is in
    milliseconds (25), x 4 yields exactly 100 = 0x64, and our OTA header is
    byte-identical to the phone's apart from TTP. Declared and measured cadence
    agree exactly.
  2. "the R2.2 stream's OTA period field (1424) contradicts the measured
    46.48 ms interval"
    -- that reading was wrong. An inconsistency does exist,
    but it is 36.00 ms declared (28.00 ms on the first attempt) against ~46.5 ms
    measured, which matches the 2204 samples per frame the encoder consumes.

What this changes at the receiving end. The R2.2 stream is not merely close
to the Snapdragon Sound form -- it is that form on the wire: version 0xaf,
packet type 0x05, 760-byte frames, channel_mode 0xa0, 938 packets over 46.8 s
at 15.63 kB/s (and 1237 packets over 61.8 s in the live run). The headset stays
silent. So "the headset only needs the right stream shape" is weaker than it
looked, and what is left between our stream and the audible standard-EDR source
(the BT11's Lossless) is:

  1. the version byte -- R3 (0xad) versus our R2.2 (0xaf);
  2. the cadence -- roughly 10 ms for the audible sources against ~46 ms here;
  3. the link itself.

The tool now parses the header correctly, reads the period in 0.25 ms units
(what the wire actually uses), defaults --expect-ptype to 0x01, and
cross-checks the frame size against the packet-type table -- precisely the check
that would have caught this at once. Re-verified afterwards: both ordinary-R2
captures are fully self-consistent (declared 25.00 ms, measured 25.00/24.99 ms,
656-byte frames matching packet type 1), and the tool no longer fails a correct
R2.2 stream on the ptype gate.

One methodological lesson, for anyone writing similar tooling: an off-by-one
field parse still yields plausible-looking numbers (356 and 1424 are not obvious
garbage), so a header parser needs a physical cross-check that does not depend
on the parse -- here, payload length against packet type. HANDOFF.md 21.17 and
STATUS.md 5.12 record the whole thing.

The earlier "ordinary Adaptive is invisible" result was measured against
0x08064F and 0xB7163B.  Neither appears in Adaptive mode at all: the BT11's
Adaptive piconet is 0x6A8FCC, established with four 130 s captures (0 hits in
both BT11-absent runs, 74.3 and 59.1 hits/min at up to 0 dBm in both streaming
runs) and then confirmed in a single capture around an operator power-cycle,
where the headset's own address 0xB7163B shows up during paging and 0x6A8FCC
bursts for 31 s (259 hits, 8.4/s, 45 channels) before going quiet for 118 s
while the music keeps playing.

So the old verdict holds for steady state but not for its reason or its scope:
the connection setup is plainly visible on standard BR/EDR, and only the
streaming link disappears.  The phone behaves the same way, so this is a
property of ordinary Adaptive rather than of the BT11 or the headset.

The same round also records the noise floor of the method: between two captures
in an identical state, 0x73BB13 went from 21.2 hits/min to 0, so a single
contrast proves nothing.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Re-verification round: the BT11's ordinary-Adaptive link, with an anchored capture

We re-tested our own claim that "in ordinary Adaptive mode the BT11's piconet is
invisible to a standard BR/EDR receiver", because the evidence behind it had a
hole: it only ever looked for 0x08064F and 0xB7163B, on the assumption that
those were the only two LAPs the link could have. They are not.

Setup. Ubertooth One (firmware 2020-12-R1), a FiiO BT11 (QCC5181) streaming
to the Sennheiser MOMENTUM 5 in ordinary aptX Adaptive, with audio from this
host over USB. The dongle's USB audio node was running throughout, and plug
state was verified from lsusb, so "playing" and "present" are not assumptions.

Four 130 s captures, identity anchored by the operator unplugging the dongle.

BT11 state 0x6A8FCC 0x08064F 0xB7163B noise-floor samples
unplugged 0 hits 0 0 0x1C8CB9 119.5/min, 0x73BB13 21.2/min
unplugged (repeat) 0 hits 0 0 0x1C8CB9 120.4/min, 0x73BB13 0
streaming 74.3/min, 0 dBm 0 0 0x73BB13 13.8/min
streaming (repeat) 59.1/min, 0 dBm 0 0 0x73BB13 0

So the BT11's Adaptive piconet is 0x6A8FCC, and the two LAPs the earlier test
watched never appear in this mode at all. Note the noise floor: between the two
identical "unplugged" captures 0x73BB13 went from 21.2 hits/min to zero, which
is why a single contrast would have proved nothing. 0x6A8FCC is the only LAP
that is 0 in both absent runs and 59-74 hits/min in both present ones.

One capture around an operator power-cycle shows the mechanism directly.

t (s)       0 ....... 47 48 49 ................... 79 ....... 197
0x6A8FCC    0          0  0  9 93 92 65  <- 259 hits / 31 s = 8.4 hits/s, 45 channels, 0 dBm
                                     then 0 hits for 118 s (music still playing)
0xB7163B    0          4 hits (0 dBm)   0 ...

0xB7163B is the headset's own address, and it appears for two seconds
immediately before the burst: that is the paging phase, where the access code
belongs to the paged device. Once the connection exists, the piconet's access
code belongs to the master, so 0x6A8FCC is the BT11.

Revised conclusion. The old verdict was right about steady state and wrong
about its reason and its scope:

  • steady state really is invisible -- 0 hits over 118 s in the same capture, and
    over 180 s and 190 s in separate captures, with audio audibly playing;
  • but the connection setup is plainly visible on standard BR/EDR (259 hits
    in 31 s), so the stronger claim that the whole piconet lives on the proprietary
    PHY, "even the ACL signalling", does not hold;
  • and the phone behaves exactly the same way (its piconet appears only at
    reconnect), so this is a property of ordinary Adaptive rather than of the BT11
    or of this headset.

One loose end, stated plainly: the earlier Lossless-mode anchor was 0x08064F
(103 hits/min, unplug-anchored), while the Adaptive-mode anchor is now
0x6A8FCC. A dongle has one BD_ADDR, so one of the two anchors must be wrong.
Re-running the power-cycle method in Lossless mode would settle it, and we will
report the result either way.

STATUS.md 7.2/7.3 and HANDOFF.md 21.18 record the measurements, the noise
floor, and the three methodology lessons: anchor actions must be sequenced by
the coordinator rather than asked concurrently, any "this LAP is not there"
claim needs the same state measured twice, and the paged device's address
appearing during paging is a reliable way to tie a LAP to a device.

…rotocol

Same protocol as the Adaptive re-check: a 129 s steady-state capture and a
199 s capture around an operator power-cycle, with the host confirming that the
dongle's UAC sink was running and streaming 44.1 kHz.

The two modes are indistinguishable on the air.  The master LAP is 0x6A8FCC in
both, the piconet is visible for the ~31 s after reconnection (246 hits, 7.7/s,
39 channels, up to 0 dBm, with the headset's own address during paging two
seconds earlier), steady state is silent in both (118 s in the same capture
plus 129 s), and 0x08064F never appears.

That makes the earlier Lossless anchor (0x08064F, 103 hits/min in steady state)
unreproducible.  Either it was a mis-attribution, or the dongle did not really
run Lossless today; the air cannot tell which, and the host feeds it 24-bit
audio while Lossless may want 16-bit input.  If the first reading holds, then
every audible source measured today keeps its steady-state link off standard
BR/EDR, which strengthens the proprietary-PHY reading and weakens "only the
stream shape is missing".
@baizhu945

Copy link
Copy Markdown
Contributor Author

Lossless re-check: the two modes are indistinguishable on the air

We ran the same anchored protocol with the dongle switched to Lossless, to
settle the contradiction left open in the previous comment (the old Lossless
anchor was 0x08064F, the Adaptive anchor is 0x6A8FCC, and one dongle has one
BD_ADDR). Host-side preconditions were verified: the BT11's USB audio node was
running, and ALSA reports the real stream as 44100 Hz / S24_3LE.

Steady state, 129 s, audio playing: 0x08064F 0 hits, 0x6A8FCC 0 hits,
0xB7163B 0 hits.

Around an operator power-cycle, 199 s, one capture:

t (s)      0 ........ 49 50 51 ................... 81 ........ 199
0x6A8FCC   0           0  0  0  78 76 80 12  <- 246 hits / 31 s = 7.7/s, 39 channels, 0 dBm
                                    then 0 hits for 118 s (music still playing)
0xB7163B   0           14 hits (0 dBm)  0 ...
0x08064F   0 hits throughout

Side by side with the Adaptive round, same operator, same protocol:

mode master LAP burst after reconnect steady state 0x08064F paging
ordinary Adaptive 0x6A8FCC 259 hits / 31 s (8.4/s, 45 ch) 0 hits absent 47-48 s, 4 hits
Lossless 0x6A8FCC 246 hits / 31 s (7.7/s, 39 ch) 0 hits absent 49-50 s, 14 hits

What this settles and what it does not.

  • The old Lossless anchor 0x08064F (103 hits/min in steady state) does not
    reproduce: it appears in neither mode today. Either it was a mis-attribution,
    or the dongle did not actually run Lossless today.
  • The air cannot tell which codec the dongle is running -- that is exactly the
    measurement limit we hit earlier -- so the honest reading is that the two
    modes are indistinguishable to this instrument. A fallback is possible and
    worth naming: this host feeds the dongle 44.1 kHz 24-bit, and Lossless may
    want 16-bit input.
  • If the first reading holds, the consequence is uncomfortable for our earlier
    argument: every audible source measured today (phone Adaptive, BT11 Adaptive,
    BT11 Lossless) keeps its steady-state link off standard BR/EDR. The
    "the headset needs the proprietary PHY" reading is then strengthened, and
    "the headset only needs the right stream shape" is weakened -- the Lossless
    link being visible on standard EDR was the one observation that supported the
    latter, and it no longer reproduces.

Next, in order of value: (1) an external indication of the codec the dongle
actually negotiates (its own mode display, or the headset's codec readout via
the Sennheiser app); (2) if Lossless is confirmed, re-run with a 44.1 kHz
16-bit host stream to rule out a bit-depth fallback; (3) if both hold,
retire 0x08064F as "the Lossless anchor" in the notes.

STATUS.md 7.2/7.3 and HANDOFF.md 21.19 record the numbers and the two
readings.

Sections 21.13 and 21.15 still presented 0x08064F as the BT11's link and
"Lossless runs on standard EDR" as settled.  Both rest on an anchor that the
controlled power-cycle re-tests could not reproduce, so each section now opens
with a pointer to 21.18/21.19 and names what changed: the Adaptive piconet is
0x6A8FCC, the connection setup is visible on standard BR/EDR while steady state
is not, and 0x6A8FCC was never a survey artefact.
…changes

The operator confirms the BT11 was running Lossless during the re-check, which
picks the first reading of the previous round: 0x08064F was a mis-attribution,
0x6A8FCC is the BT11 in both modes, and Lossless leaves standard BR/EDR once
the media flows just as Adaptive does.

A 304 s capture in Lossless with audio playing backs the negative with a
liveness control: 535 packets from 32 piconets, several at 40-80 hits, and zero
from 0x6A8FCC, 0x08064F or the headset.  Cumulatively that is 551 s of Lossless
and 488 s of Adaptive playback with the BT11's link absent.

So every audible source measured here keeps its steady-state link off standard
BR/EDR, while the one visible-link source -- this host -- is silent.  That makes
the proprietary-PHY requirement the only reading the evidence supports, and
removes the observation the rival "it only needs the right stream shape" reading
rested on (Lossless running on standard EDR).  The R2.2 experiment points the
same way independently: the Snapdragon Sound form went out continuously and the
headset stayed silent.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Follow-up: the dongle was confirmed to be in Lossless, so the conclusion changes

In the previous comment I left two readings open. The operator has now confirmed
that the BT11 really was running Lossless, which selects the first one:
0x08064F was a mis-attribution, 0x6A8FCC is the BT11 in both modes, and
Lossless leaves standard BR/EDR once the media flows, exactly as Adaptive
does
.

The steady-state negative now has a liveness control. A 304 s capture in
Lossless with audio playing, on a working receiver:

measurement value
packets / piconets seen 535 / 32 (so the instrument was working)
busiest piconets 0x17F122 78, 0xEE02F8 78, 0xC1B781 65, 0x6F18BB 57
0x6A8FCC (BT11) 0
0x08064F (old anchor) 0
0xB7163B (headset) 0

Cumulatively that is 551 s of Lossless and 488 s of Adaptive playback during
which the BT11's link never appeared, all with the host confirming the dongle's
USB audio node was running.

Why this matters for the conclusion. Three audible sources have now been
measured -- the phone's Adaptive, the BT11's Adaptive and the BT11's Lossless --
and none of them carries its steady-state media on standard BR/EDR, while
the one source whose Adaptive link is visible while it plays (this host) is
silent. So:

  • the proprietary-PHY requirement becomes the only reading the evidence
    supports. The original QHS hypothesis, which an earlier round of ours
    "refuted" on the strength of the 0x08064F anchor, is back -- now on evidence
    that survives a controlled power-cycle test;
  • the rival reading, "the headset only needs a stream shape it recognises",
    loses the observation it rested on (Lossless over standard EDR);
  • the R2.2 experiment points the same way independently: the Snapdragon Sound
    form went out continuously on standard EDR and the headset stayed silent,
    so a correct stream is not sufficient on this controller.

The honest summary for anyone reading this PR for the codec work: the host-side
implementation is correct and verified as far as measurement reaches, but the
headset decodes these codecs only from sources whose media never appears on the
standard BR/EDR link this host can provide. Reaching the original goal on this
hardware would need a controller able to drive that proprietary link; the
untested branches left on the module side (forcing R3 0xad, and the frame
cadence) cannot close a link-layer gap.

The two methodology lessons from this round are in the docs and worth repeating:
a negative result needs a liveness control in the same capture (535 packets from
32 piconets is what makes the zero meaningful), and an instrument's blind spot
belongs in the conclusion -- this sniffer cannot distinguish Adaptive from
Lossless, which is exactly why the operator's mode confirmation is what decided
the question. STATUS.md 7.2/7.3 and HANDOFF.md 21.19/21.20 record it.

@baizhu945

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #16.

The work here is unchanged -- the branch content is the same, with the commits
re-authored to the fork owner's identity -- but this pull request's description
and comment thread had turned into a running log: several conclusions were
stated, measured again, corrected and in one case reversed (the BT11's LAP
identity, and with it whether ordinary Adaptive is visible on standard BR/EDR).
That is honest research, but it is a bad thing to ask a reviewer to read.

#16 restates the whole thing once, in its final form: what the code is, what is
verified, the protocol findings, the bug we found in our own tooling, the
cross-source evidence, and the conclusion that the remaining gap is at the link
layer rather than in the codec stream. The corrections are preserved in
HANDOFF.md sections 21.17-21.20 rather than in the discussion, so the trail is
not lost.

Thanks to anyone who read through this thread.

@baizhu945 baizhu945 closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant