wip(mic): BT-side mic capture infrastructure + host-side diag

In-progress work on DualSense microphone capture over BT. Mic-add tap
itself is disabled (was decoding standard input bytes as Opus and
producing INT16_MIN garbage on the USB IN endpoint) but everything
around it is wired and ready to re-enable once we identify the actual
mic transport.

Firmware:
- src/audio.cpp: Opus decoder on core0, mic_fifo queue, audio_loop
  mic-in path with decode + mono->stereo + tud_audio_write. Decoder
  init in audio_init() (creates 48kHz mono OpusDecoder).
- src/audio.h: exports mic_add_queue() + per-frame diagnostic
  accessors (audio_mic_frames, last_decoded, last_want, last_wrote,
  last_toc).
- src/main.cpp on_bt_data(): BT-side instrumentation — counts every
  INTERRUPT input report, tracks min/max length, OR mask of byte[2],
  most recent non-0x31 report ID, hex prefix of last 0x31/other/any
  frame, full content of the longest 0x31 frame seen. Mic-tap call
  itself stubbed behind `if (false)` pending the real detector.
- src/state_mgr.cpp: state_init_data byte 6 (VolumeMic) 0xFF→0x40
  (was out of range), byte 9 (MuteControl) 0x0F→0x00 (clear all
  PowerSave bits — AudioPowerSave was muting DSP).
- src/cmd.cpp: two new vendor feature reports — 0xFD returns 32-byte
  diagnostic state (counters + prefixes), 0xFE returns the longest
  0x31 frame in full (up to 80 bytes). Both queryable via
  /dev/hidraw on Linux from the host script.
- src/oled.cpp: Diagnostics screen shows TOC + decode result + USB
  wrote/want bytes for live BT-side visibility.

Host-side:
- scripts/mic_diag.sh: subcommands `status`, `capture [secs]`,
  `watch`, `bt-trace`. The bt-trace subcommand reads the 0xFD
  feature report via hidraw ioctl, decodes counters + recent
  prefixes, computes per-second rates. Drastically cuts iteration
  time — no OLED relay or per-test flash cycle needed.

Findings to date:
- Upstream/mic's mic-flag bit ((data[2] >> 1) & 1) does NOT match
  this DS5 firmware; bit 1 of byte[2] is NEVER set. Bit 0 is the
  standard input report type indicator, not a mic tag — confirmed
  by stick-bytes appearing as our supposed "Opus prefix".
- DS5 sends both report ID 0x01 and 0x31 over BT; the longest frame
  is a standard 79-byte 0x31 input report with sticks/IMU/touchpad
  but no audio bytes appended.
- Conclusion in progress: the DS5 firmware on this controller is
  not currently streaming mic over BT at all, even with
  AllowAudioControl=1, VolumeMic=0x40, AudioPowerSave=0,
  MicMute=0. Next investigation step: compare against a USB-mode
  DS5 to see what a real mic stream looks like at the UAC1 layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MarcelineVPQ
2026-05-19 17:35:08 -06:00
co-authored by Claude Opus 4.7
parent 2209f9b8c7
commit 72f163ca50
7 changed files with 480 additions and 17 deletions
+77
View File
@@ -25,6 +25,14 @@
// #define VOLUME_GAIN 2
// #define BUFFER_LENGTH 48
// DualSense microphone, ported from awalol/DS5Dongle's `mic` branch.
// The DS5 sends mic audio as Opus packets embedded in BT input report
// 0x31 when bit 1 of byte 2 is set; payload is 71 bytes of Opus at
// offset 4, decoded to mono 48 kHz 10 ms frames (480 samples).
#define MIC_CHANNELS 1
#define MIC_FRAMES 480
#define MIC_OPUS_SIZE 71
using std::clamp;
using std::max;
@@ -37,6 +45,21 @@ queue_t audio_fifo;
static uint8_t opus_buf[200];
critical_section_t opus_cs;
// Mic ingress queue — filled from on_bt_data() (BT poll, core0), drained
// at the top of audio_loop() on core0. The decoder is single-threaded
// (core0 only), so no critical section is needed around it.
queue_t mic_fifo;
struct mic_element { uint8_t data[MIC_OPUS_SIZE]; };
static OpusDecoder *mic_decoder = nullptr;
static volatile uint32_t g_mic_frames = 0;
static volatile int32_t g_mic_last_decoded = 0; // opus_decode return value
static volatile uint16_t g_mic_last_want = 0; // bytes we asked TinyUSB to send
static volatile uint16_t g_mic_last_wrote = 0; // bytes TinyUSB accepted
uint32_t audio_mic_frames() { return g_mic_frames; }
int32_t audio_mic_last_decoded() { return g_mic_last_decoded; }
uint16_t audio_mic_last_want() { return g_mic_last_want; }
uint16_t audio_mic_last_wrote() { return g_mic_last_wrote; }
struct audio_raw_element {
float data[512 * 2];
};
@@ -73,7 +96,51 @@ uint8_t audio_peak_haptic() {
return (uint8_t)(v >> 7);
}
// Most-recent Opus TOC byte (first byte of the packet). Used by the OLED
// Diagnostics screen to decode the frame's bandwidth + duration config
// without serial.
static volatile uint8_t g_mic_toc = 0;
uint8_t audio_mic_last_toc() { return g_mic_toc; }
// Push a 71-byte Opus mic packet from the BT handler into the mic_fifo.
// Called from src/main.cpp's on_bt_data() when the DS5 sends a mic-tagged
// 0x31 input report. Drops the oldest queued packet if the FIFO is full —
// preferring fresh audio over backlog on overload.
void mic_add_queue(const uint8_t *data) {
static mic_element packet{};
memcpy(packet.data, data, MIC_OPUS_SIZE);
g_mic_toc = data[0]; // first byte of the Opus packet
if (queue_is_full(&mic_fifo)) queue_try_remove(&mic_fifo, NULL);
queue_try_add(&mic_fifo, &packet);
}
void audio_loop() {
// Mic-in path: pull one Opus packet from the BT-side FIFO, decode to
// mono PCM, duplicate to stereo (our UAC1 endpoint declares 2 channels),
// push to the host via tud_audio_write. Runs once per loop iteration so
// it keeps up with the ~100 Hz arrival rate of mic-tagged BT frames.
if (mic_decoder != nullptr) {
static mic_element packet{};
if (queue_try_remove(&mic_fifo, &packet)) {
static int16_t mono[MIC_FRAMES];
const int decoded = opus_decode(mic_decoder, packet.data,
MIC_OPUS_SIZE, mono, MIC_FRAMES, 0);
g_mic_last_decoded = decoded; // observed in OLED Diag
if (decoded > 0) {
static int16_t stereo[MIC_FRAMES * 2];
for (int i = 0; i < decoded; i++) {
stereo[i * 2] = mono[i];
stereo[i * 2 + 1] = mono[i];
}
const uint16_t want = (uint16_t)(decoded * 2 * sizeof(int16_t));
const uint16_t wrote = tud_audio_write(stereo, want);
g_mic_last_want = want;
g_mic_last_wrote = wrote;
g_mic_frames++;
}
}
}
// 1. 读取 USB 音频数据
if (!tud_audio_available()) return;
@@ -253,6 +320,16 @@ void audio_init() {
critical_section_init(&opus_cs);
multicore_launch_core1_with_stack(core1_entry, audio_core1_stack, sizeof(audio_core1_stack));
#endif
// Mic path: queue + decoder live on core0 (audio_loop), separate from
// the core1 speaker encoder. Mic Opus is mono / 48 kHz / 10 ms frames.
queue_init(&mic_fifo, sizeof(mic_element), 2);
int dec_error = 0;
mic_decoder = opus_decoder_create(48000, MIC_CHANNELS, &dec_error);
if (dec_error != 0 || mic_decoder == nullptr) {
printf("[Audio] OpusDecoder create failed (err=%d)\n", dec_error);
mic_decoder = nullptr; // ensure audio_loop's null-guard short-circuits
}
}
static OpusEncoder *encoder;