Files
DS5Dongle-OLED-Edition-stea…/src/audio.cpp
T
MarcelineVPQandClaude Opus 4.7 20b41d80a1 feat: DualSense BT microphone + USB 3.0 connection watchdog
BT microphone over Bluetooth: the DS5 mic now works over the dongle's BT
pairing — decoded from the controller's Opus stream to the USB capture
endpoint. Hinges on pkt[4] bit 0 (mic-enable) in the outbound 0x36 audio
report; credit to awalol (upstream) for identifying it. Mic-tagged 0x31
frames ((data[2]>>1)&1) are ALWAYS diverted out of the input path (decoded
when on, dropped when off) so Opus payload can never corrupt sticks/buttons.
Always-on via a sticky-latch keep-alive that only runs post-enumeration
(tud_mounted) so it never floods the fresh-pair handshake (which otherwise
delayed controller detection past the watchdog and tore the link down).
Toggle: bt_mic_enable config field (default on) — OLED Settings + web config.
README gains a "DualSense Microphone over Bluetooth" section;
BLUETOOTH_AUDIO_NOTES.md rewritten from "dead end" to the working mechanism.

USB 3.0 connection watchdog: auto-recovers a stalled connection (re-inquiry)
instead of hanging on the amber lightbar, for USB 3.0 ~2.4 GHz RF interference
that desensitizes the CYW43 BT radio. Re-enabled the ACL-fail / auth-fail /
create-connection-reject recovery paths. README "USB 3.0 ports & Bluetooth
interference" section with mitigations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:30:37 -06:00

426 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Created by awalol on 2026/3/5.
//
#include "audio.h"
#include "bt.h"
#include "resample.h"
#include "tusb.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include "opus.h"
#include "utils.h"
#include "pico/multicore.h"
#include "pico/util/queue.h"
#include "pico/time.h"
#include "config.h"
#include "state_mgr.h"
#include "usb.h"
#define INPUT_CHANNELS 4
#define OUTPUT_CHANNELS 2
#define SAMPLE_SIZE 64
#define REPORT_SIZE 398
#define REPORT_ID 0x36
// #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;
static WDL_Resampler resampler;
static uint8_t reportSeqCounter = 0;
static uint8_t packetCounter = 0;
static bool plug_headset = false;
alignas(8) static uint32_t audio_core1_stack[8192];
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];
};
void set_headset(bool state) {
plug_headset = state;
}
// Stubs kept for OLED diag-screen compatibility. Upstream removed the opus
// queue and audio FIFO drop tracking isn't wired here; OLED shows 0.
uint32_t audio_fifo_drops() { return 0; }
uint32_t opus_fifo_drops() { return 0; }
// Monotonic byte-flow counters for the OLED Diagnostics screen and the web
// emulator's USB / BT rate display. Updated below.
static volatile uint32_t g_usb_frames = 0;
static volatile uint32_t g_bt_packets = 0;
uint32_t audio_usb_frames() { return g_usb_frames; }
uint32_t audio_bt_packets() { return g_bt_packets; }
// Rolling-peak meters for the OLED VU screen. Updated during audio_loop's
// per-sample iteration, decayed 12.5 % on each read (so the bar falls back
// over a few frames if the signal goes quiet).
static volatile uint16_t g_peak_spk = 0;
static volatile uint16_t g_peak_hap = 0;
uint8_t audio_peak_speaker() {
const uint16_t v = g_peak_spk;
g_peak_spk = (uint16_t)((v * 7) / 8);
return (uint8_t)(v >> 7);
}
uint8_t audio_peak_haptic() {
const uint16_t v = g_peak_hap;
g_peak_hap = (uint16_t)((v * 7) / 8);
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);
}
// Re-assert the DS5 mic-enable (pkt[4] bit 0) so the controller streams its mic
// even when no audio is being output to it. Normally the enable only rides the
// 0x36 audio frames, which are gated on active USB audio — so without this, mic
// only works while a game plays sound. The enable is sticky (the DS5 keeps
// streaming once it starts), so we send a control-only 0x36 (enable + the
// load-bearing SetStateData sub-report + a silent haptic block, no speaker
// payload → makes no sound) at ~4 Hz ONLY until mic frames start arriving, then
// stop — minimizing BT traffic and DS5 battery. Resumes if the stream stalls.
static void mic_enable_keepalive() {
if (!bt_is_connected() || !get_config().bt_mic_enable) return;
const uint64_t now = time_us_64();
static uint32_t last_frames = 0;
static uint64_t last_frame_us = 0;
static uint64_t last_send_us = 0;
const uint32_t frames = g_mic_frames;
if (frames != last_frames) { last_frames = frames; last_frame_us = now; }
if (last_frame_us != 0 && (now - last_frame_us) < 1000000ULL) return; // streaming → sticky, no resend
if (last_send_us != 0 && (now - last_send_us) < 250000ULL) return; // throttle to ~4 Hz while arming
last_send_us = now;
uint8_t pkt[REPORT_SIZE]{};
pkt[0] = REPORT_ID;
pkt[1] = reportSeqCounter << 4;
reportSeqCounter = (reportSeqCounter + 1) & 0x0F;
pkt[2] = 0x11 | 1 << 7;
pkt[3] = 7;
pkt[4] = 0b11111111; // mic-enable (bit 0)
const auto buf_len = get_config().audio_buffer_length;
pkt[5] = pkt[6] = pkt[7] = pkt[8] = pkt[9] = buf_len;
pkt[10] = packetCounter++;
pkt[11] = 0x10 | 1 << 7; // SetStateData sub-report (load-bearing — keeps actuators alive)
pkt[12] = 63;
state_set(pkt + 13, 63);
pkt[76] = 0x12 | 1 << 7; // haptic sub-report; samples left zero = silent
pkt[77] = SAMPLE_SIZE;
// no speaker sub-report (pkt[142..] stays zero) → control-only, no audio out
bt_write(pkt, sizeof(pkt));
g_bt_packets++;
}
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()) {
// Keep the DS5 mic streaming even without output audio — but ONLY once
// the host has enumerated us (tud_mounted). Running it during the
// fresh-pair feature handshake floods BT TX and delays controller-type
// detection past the connection watchdog's timeout, which then tears the
// link down (~10-15s "shutdown" on fresh pair). After enumeration the
// handshake is done, so it's safe — and always-on mic still works.
if (tud_mounted()) mic_enable_keepalive();
return;
}
int16_t raw[192];
uint32_t bytes_read = tud_audio_read(raw, sizeof(raw)); // 每次读入 384 bytes
int frames = bytes_read / (INPUT_CHANNELS * sizeof(int16_t));
if (frames == 0) {
return;
}
g_usb_frames += (uint32_t)frames;
static float audio_buf[512 * 2];
static uint audio_buf_pos = 0;
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
WDL_ResampleSample *in_buf;
int nframes = resampler.ResamplePrepare(frames, OUTPUT_CHANNELS, &in_buf);
const float audio_gain = mute[0] ? 0.0f : powf(10.0f, get_config().speaker_volume / 20.0f);
const float haptics_gain = get_config().haptics_gain;
uint16_t spk_max = g_peak_spk;
uint16_t hap_max = g_peak_hap;
// ---- Audio Auto Haptics (borrowed from loteran/DS5Dongle 5d6bc2f) ----
// Derives a haptic-feedback waveform from the speaker audio so games that
// never write haptic data (e.g. Ghost of Tsushima on Linux+Steam) still
// produce rumble. Mode 1 (Fallback, default) fires only when native is
// silent → preserves native HD haptics in games that do send them.
const uint8_t auto_mode = get_config().auto_haptics_enable;
const float auto_gain = (auto_mode > 0) ? (get_config().auto_haptics_gain / 100.0f) * haptics_gain : 0.0f;
static const float LP_COEFF[4] = { 0.01039f, 0.02074f, 0.03095f, 0.05123f };
const float lp_a = LP_COEFF[get_config().auto_haptics_lowpass & 3];
static float lp_l = 0.0f, lp_r = 0.0f;
static float env_l = 0.0f, env_r = 0.0f;
constexpr float ENV_ATK = 0.40f;
constexpr float ENV_REL = 0.025f;
constexpr int NATIVE_SILENT_TIMEOUT = 100;
constexpr uint16_t NATIVE_THRESHOLD = 256;
static int native_silent_count = NATIVE_SILENT_TIMEOUT * 2;
const bool fallback_active = (auto_mode == 1) && (native_silent_count >= NATIVE_SILENT_TIMEOUT);
for (int i = 0; i < nframes; i++) {
// VU peak tracking
{
int16_t sl = raw[i * INPUT_CHANNELS];
int16_t sr = raw[i * INPUT_CHANNELS + 1];
int16_t hl = raw[i * INPUT_CHANNELS + 2];
int16_t hr = raw[i * INPUT_CHANNELS + 3];
uint16_t a = (uint16_t)(sl < 0 ? -sl : sl);
uint16_t b = (uint16_t)(sr < 0 ? -sr : sr);
if (a > spk_max) spk_max = a;
if (b > spk_max) spk_max = b;
a = (uint16_t)(hl < 0 ? -hl : hl);
b = (uint16_t)(hr < 0 ? -hr : hr);
if (a > hap_max) hap_max = a;
if (b > hap_max) hap_max = b;
}
#if !DISABLE_SPEAKER_PROC
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS] / 32768.0f * audio_gain;
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
if (audio_buf_pos == 512 * 2) {
static audio_raw_element element{};
memcpy(element.data, audio_buf, 512 * 2 * 4);
if (queue_is_full(&audio_fifo)) {
queue_try_remove(&audio_fifo,NULL);
}
if (!queue_try_add(&audio_fifo, &element)) {
printf("[Audio] Warning: audio_fifo add failed\n");
}
audio_buf_pos = 0;
}
#endif
float h_l = raw[i * INPUT_CHANNELS + 2] / 32768.0f * haptics_gain;
float h_r = raw[i * INPUT_CHANNELS + 3] / 32768.0f * haptics_gain;
if (auto_mode > 0) {
const float spk_l = raw[i * INPUT_CHANNELS ] / 32768.0f;
const float spk_r = raw[i * INPUT_CHANNELS + 1] / 32768.0f;
lp_l += lp_a * (spk_l - lp_l);
lp_r += lp_a * (spk_r - lp_r);
const float abs_l = lp_l < 0.0f ? -lp_l : lp_l;
const float abs_r = lp_r < 0.0f ? -lp_r : lp_r;
env_l = (abs_l > env_l) ? env_l + ENV_ATK * (abs_l - env_l)
: env_l + ENV_REL * (abs_l - env_l);
env_r = (abs_r > env_r) ? env_r + ENV_ATK * (abs_r - env_r)
: env_r + ENV_REL * (abs_r - env_r);
float al = lp_l * (1.0f + 3.0f * env_l) * auto_gain;
float ar = lp_r * (1.0f + 3.0f * env_r) * auto_gain;
al = al / (1.0f + (al < 0.0f ? -al : al));
ar = ar / (1.0f + (ar < 0.0f ? -ar : ar));
if (auto_mode == 3) { // Replace
h_l = al; h_r = ar;
} else if (auto_mode == 2) { // Mix
float m_l = h_l + al, m_r = h_r + ar;
h_l = m_l / (1.0f + (m_l < 0.0f ? -m_l : m_l));
h_r = m_r / (1.0f + (m_r < 0.0f ? -m_r : m_r));
} else if (auto_mode == 1 && fallback_active) { // Fallback (default)
h_l = al; h_r = ar;
}
}
in_buf[i * 2] = static_cast<WDL_ResampleSample>(clamp(h_l, -1.0f, 1.0f));
in_buf[i * 2 + 1] = static_cast<WDL_ResampleSample>(clamp(h_r, -1.0f, 1.0f));
}
g_peak_spk = spk_max;
g_peak_hap = hap_max;
if (hap_max > NATIVE_THRESHOLD) {
native_silent_count = 0;
} else if (native_silent_count < NATIVE_SILENT_TIMEOUT * 2) {
native_silent_count++;
}
// 3. 48kHz -> 3kHz 重采样
static WDL_ResampleSample out_buf[SAMPLE_SIZE]; // 64 floats = 32帧 × 2ch
const int out_frames = resampler.ResampleOut(out_buf, nframes, nframes / 4, OUTPUT_CHANNELS);
static int8_t haptic_buf[SAMPLE_SIZE];
static int haptic_buf_pos = 0;
// 4. 转换为int8并缓冲,满64字节即组包发送
for (int i = 0; i < out_frames; i++) {
int val_l = static_cast<int>(out_buf[i * 2] * 127.0f);
int val_r = static_cast<int>(out_buf[i * 2 + 1] * 127.0f);
haptic_buf[haptic_buf_pos++] = (int8_t) clamp(val_l, -128, 127); // 似乎clamp有点多余?还是以防万一吧
haptic_buf[haptic_buf_pos++] = (int8_t) clamp(val_r, -128, 127);
if (haptic_buf_pos != SAMPLE_SIZE) {
continue;
}
uint8_t pkt[REPORT_SIZE]{};
pkt[0] = REPORT_ID;
pkt[1] = reportSeqCounter << 4;
reportSeqCounter = (reportSeqCounter + 1) & 0x0F;
pkt[2] = 0x11 | 0 << 6 | 1 << 7;
pkt[3] = 7;
// bit 0 = mic-enable: tells the DS5 to stream its mic over BT (awalol
// confirmed this is the key). Bits 1-7 are the pre-existing speaker/
// haptic audio-enable flags. Gated on the bt_mic_enable config toggle.
pkt[4] = get_config().bt_mic_enable ? 0b11111111 : 0b11111110;
const auto buf_len = get_config().audio_buffer_length;
pkt[5] = buf_len;
pkt[6] = buf_len;
pkt[7] = buf_len;
pkt[8] = buf_len; // 这 4 个字节的作用未知,调整没有效果
pkt[9] = buf_len; // audio buffer length 只有调整这个字节生效。
pkt[10] = packetCounter++;
// SetStateData
pkt[11] = 0x10 | 0 << 6 | 1 << 7;
pkt[12] = 63;
state_set(pkt + 13,63);
// Haptics Audio Data
pkt[76] = 0x12 | 0 << 6 | 1 << 7;
pkt[77] = SAMPLE_SIZE;
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
#if !DISABLE_SPEAKER_PROC
// Speaker Audio Data
pkt[142] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
// L Headset Mono: 0x14
// L Headset R Speaker: 0x15
// Headset: 0x16
pkt[143] = 200;
critical_section_enter_blocking(&opus_cs);
memcpy(pkt + 144, opus_buf, 200);
critical_section_exit(&opus_cs);
#endif
bt_write(pkt, sizeof(pkt));
g_bt_packets++;
haptic_buf_pos = 0;
}
}
void audio_init() {
resampler.SetMode(true, 0, false);
resampler.SetRates(48000, 3000);
resampler.SetFeedMode(true);
resampler.Prealloc(2, 24, 6);
#if !DISABLE_SPEAKER_PROC
queue_init(&audio_fifo, sizeof(audio_raw_element), 2);
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;
static WDL_Resampler resampler_audio;
void core1_entry() {
int error = 0;
encoder = opus_encoder_create(48000, 2,OPUS_APPLICATION_AUDIO, &error);
if (error != 0) {
printf("[Audio] OpusEncoder create failed\n");
return;
}
opus_encoder_ctl(encoder,OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS));
opus_encoder_ctl(encoder,OPUS_SET_BITRATE(200 * 8 * 100));
opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // max 4
resampler_audio.SetMode(true, 0, false);
resampler_audio.SetRates(51200, 48000);
resampler_audio.SetFeedMode(true);
resampler_audio.Prealloc(2, 512, 480);
while (true) {
static audio_raw_element audio_element{};
queue_remove_blocking(&audio_fifo, &audio_element);
// 将 512 frames 重采样成 480 frames 以解决噪音问题。感谢 @Junhoo
WDL_ResampleSample *in_buf;
int nframes = resampler_audio.ResamplePrepare(512, 2, &in_buf);
for (int i = 0; i < nframes * 2; i++) {
in_buf[i] = audio_element.data[i];
}
static WDL_ResampleSample out_buf[480 * 2];
resampler_audio.ResampleOut(out_buf, nframes, 480, 2);
static uint8_t out[200];
(void) opus_encode_float(encoder, out_buf, 480, out, 200);
critical_section_enter_blocking(&opus_cs);
memcpy(opus_buf, out, 200);
critical_section_exit(&opus_cs);
}
}