Skip to content

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

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

research: aptX Adaptive/Lossless status on a non-Qualcomm Linux host#15
baizhu945 wants to merge 47 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 R2.2 packet shape (768-byte packets,
    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.

Remaining gap

Everything observable from the host has been aligned with a working source, so
the remaining difference is either the controller/over-the-air behaviour (the
phone offloads aptX to a Qualcomm controller and the FiiO BT11 runs a QCC5181;
this host sends plain L2CAP ACL packets over an Intel AX210) or a dependence of
the sink firmware on a licensed Qualcomm source. The sink sends no reverse
ACL data at all during a 30 s stream, so it exposes no feedback or error path.

I would be very grateful for any pointer on the last point, and of course for
any correction to the protocol conclusions above. The negative results are
included deliberately: they should save anyone else from repeating them.

Thanks again for the project.

Codex and others added 30 commits September 5, 2026 22:05
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).
Codex added 5 commits September 9, 2026 22:50
…nalysed

- Faking the adapter Class of Device as a smartphone does not help (reverted).
- The live stream decodes to a real 440 Hz tone (RMS -31 dBFS), so we are not
  sending silence; there is a ~24 dB gain difference vs the source to chase.
- New phone logs (pulled over adb) show the phone's own aptX Adaptive source
  capability (f0 3e ... 0x0f000017) and that it RECONFIGURES the headphone
  stream to 48 kHz stereo mid-session.
- The module's own TTP is effectively frozen (4 units per 25 ms); the helper's
  host-clock override is correct.
- The phone's A2DP sink profile is disabled and cannot be enabled without root,
  so the planned 'phone as sink' validation is not available.
The branch carried its own 'Fix backend destroy guards' and 'Fix pkg-config
paths' work, which upstream has since merged as PR arkq#13 and arkq#14.  The merge
resolves test/aptx-lifecycle.c in favour of the upstream version (SPDX header,
no assert) and removes the duplicated aptx-lifecycle-test target that the
auto-merge produced in test/CMakeLists.txt.
The previous report pointed at the encoder payload as the likely cause.  That
is now disproven: the host's SET_CONFIG, RTP header, OTA header and codec
frames are byte-identical to a working Android source, and replaying the
source's own captured frames is silent as well.

STATUS.md now records:

- the OTA header layout, the payload-size table and the module's period/PCM
  tables (extracted from the module image);
- that the OTA version byte is driven by the encoder level (>= 6 -> 0xaf) and
  that the bitrate-level map values are kbit/s, not bit/s;
- the R2.2 wrapper pinning the process period to 25 ms / 656 bytes while the
  inner codec is configured for 14 ms / 364 kbps;
- the eight host-side bugs found and fixed;
- the twelve hypotheses that have been ruled out by measurement, each with the
  experiment that ruled it out;
- the remaining hypothesis (controller/link layer or sink firmware) and the
  cheapest experiment that would discriminate between them;
- the tooling and a reproduction recipe.

Also add SPDX headers to the new sources, matching the upstream style, and
refresh the two README statements that no longer matched the measurements.
- Add a .markdownlintignore for the two raw working logs (HANDOFF.md and
  gen-cntr-process-loop.md); STATUS.md, README.md and TOOLS.md are now
  lint-clean under the repository's default markdownlint rules (line length,
  fence languages, blank lines around headings/lists/fences).
- Add the research directory to REUSE.toml so every file carries copyright and
  license information; 'reuse lint' now reports 95/95.
@baizhu945

Copy link
Copy Markdown
Contributor Author

Updated with a further round of measurements. Two things changed materially
since the first revision of this PR:

  1. The merge conflict is resolved. The branch now contains a merge of
    current master; test/aptx-lifecycle.c is taken from upstream (this
    branch had carried an earlier version of the same fix). All CI checks
    (build/test matrix, markdown-lint, REUSE) pass.

  2. The earlier conclusion about the payload was wrong, and the report is
    corrected.
    The first revision suggested that the fixed 14-byte prefix and
    the ~210 kbps payload were the problem. That is now disproven by
    measurement:

    • 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 path
      reproduces them 266/266 byte for byte;
    • the reference decoder decodes the frames the host actually transmits
      (48 kHz stereo, 440.0 Hz FFT peak for a 440 Hz sine);
    • the phone's own 44.1 kHz aptX Adaptive stream is also 656 B / 25 ms
      (210 kbps), so that bitrate is normal for this codec;
    • re-stamping the phone's frames to a 16 ms period (338 kbps), 96 kHz
      end-to-end, the R2.2 0xaf version byte, full 768-byte R2.2 packets, a
      "smartphone" Class of Device and a 30 s gapless stream all stay silent.

    STATUS.md now lists the twelve hypotheses that have been ruled out, each
    with the experiment that ruled it out, plus the protocol findings that came
    out of disassembling the module (the OTA header layout, the payload-size
    table, the period/PCM tables, the fact that the bitrate-level map takes
    kbit/s, and that the OTA version byte is level-driven).

The remaining gap is outside what the host can observe: the sink accepts the
stream at L2CAP level, sends no reverse traffic at all, and produces no audio,
while it plays aptX HD from the same host and aptX Adaptive from a phone and a
FiiO BT11. Any pointer on that last point would be very welcome.

… kHz

The early sections of the handoff report read the A2DP sampling-frequency
field through Qualcomm's bthost_ipc.h DSP payload numbering and concluded
that 0x40 meant 192 kHz.  That is wrong: 0x40 is 44.1 kHz and 0x10 is
48 kHz, as the user's own measurements of the phone confirm.

Add an errata banner to the affected sections, mark the withdrawn
reasoning, and record the fresh capture where the phone reconfigures an
open aptX Adaptive stream to 48 kHz for 48 kHz/24-bit content and back to
44.1 kHz for 44.1 kHz content.  Also record that the phone offloads the
media path entirely (no AD frames in its HCI log) and that the sink never
queries the source capabilities, which rules our advertised capability
blob out as a cause of the silence.
…n chain

The module exports a mode-2 CAPI entry point that the helper never used;
driving it instead of the R2/R2.2 wrapper produces exactly the same
25 ms / 656 byte packets, so the fixed period is not a property of the
wrapper.  The module logs now show that the inner encoder selects 12 ms
and 437 kbps (576 sample PCM interval) while PROCESS writes period 0x64,
and 437 kbps x 12 ms / 8 = 656 bytes explains the payload size.  Record
that, together with the conclusion that the remaining difference is at
the source-device level.
…packet shape

The FIIO BT11 exposes HID plus UAC audio only, with no Wireless class
interface, so it cannot serve as an HCI controller for this host.  While
verifying that, the probe runs showed that the earlier lossless attempts
never actually reached the 2.2 shape: the helper silently downgrades a
forced lossless mode unless APTX_ADAPTIVE_ALLOW_UNSTABLE_LOSSLESS is set,
and the 768 byte / packet type 5 shape additionally needs a 16 bit source
word size hint.  With both fixed the bridge produces genuine 768 byte
packets with the R3 frame header, and version 0xad or 0xaf via a new
override.  Record that, the two static-properties / profile bugs found in
the R3 entry, and the live test plan.
Record that only the PipeWire plugin and the openaptx library are Nix
built while the Hexagon helper is a runtime file, which is why transport
schemes can be switched with a helper rebuild plus a user systemd
drop-in.  Also record the plain (non-Lossless) sweep: config bytes, RTP
header, frame format, cadence/rate (217 kbps, matching the phone) and the
OTA header all match the working source, and moving TTP into the audio
clock domain still leaves the sink silent, so the remaining difference is
at the controller/air interface.
Timeline the HCI commands around the phone's aptX Adaptive stream setup:
Qualcomm vendor command 0xFC0A carries the complete A2DP codec
configuration (vendor id, codec id, sampling frequency, channel mode and
the whole parameter block, byte for byte the AVDTP SetConfiguration value)
to the controller at the same millisecond as Start, together with 0xFD57 /
0xFD59 and the link policy / sniff changes.  That is the A2DP offload
handoff: the controller encodes and schedules the stream, which is also
why the phone's own log never shows media.  Record it as the direct
evidence for why a host-side bridge cannot reproduce the working setup,
and correct the hardware advice accordingly.
Replaying Android's A2DP link settings (BR-only packet type 0xcc18 plus
link policy 0x0005) on our link leaves the stream healthy but the sink
silent, so the link-layer configuration is not the gate either.  Add
air_analyse.py, which summarises an Ubertooth BR/EDR capture into the
comparison the HCI log cannot provide (LMP opcodes, air packet types,
direction split), and document the three-capture plan together with the
LAP/UAP values needed to follow each piconet.
…code review

The deployed helper was built from a /tmp source that no longer existed, so the
runtime was not reproducible.  Port the env-gated switches (APTX_FORCE_BITS,
APTX_TTP_MODE/OFFSET, APTX_OTA_VERSION[_FILE]) into the tracked helper, fix the
R3 static-properties lookup that made aptx_adaptive3_enc_init() fail with EIO,
reject unknown OTA version bytes, read the version file only from a configured
path, and default TTP to the audio-clock domain.  Add CODE-REVIEW.md with the
full bug list, the fixes, the residual risks and the verification performed,
plus link_mode.sh for the Android link-setting comparison.
…mode tool

The previous commit was created with -am, which does not stage untracked
files; add the three new documents/tools that belong with it.
The plugin fix is deployed from fork rev b97eae8 (verified through the
loaded .so path), the helper is rebuilt from the tracked source, the NixOS
module no longer injects the three harmful leftovers and no longer exports
the encoder environment system-wide, and the stale values had to be
cleared from the user manager explicitly.  End-to-end the stream is back
to the reference shape with the OTA header intact.
CI runs markdownlint-cli 0.43.0, whose defaults differ from the newer local
version: a leading HTML comment and wide tables are rejected there.  Rewrite
the document with an H1 as the first line and no tables, keeping every line at
or below 80 columns.
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