rebase: Phase E — VU peaks, frame counters, Audio Auto Haptics

Build now compiles. audio.cpp on top of upstream's state_mgr-based
audio_loop adds three additive pieces:

1. VU peak meters for ch0/1 (spk_max) and ch2/3 (hap_max) tracked
   per-sample in the existing loop; written back to g_peak_spk /
   g_peak_hap after. Accessors decay 12.5% per read so the OLED
   VU bars fall back naturally over a few frames.

2. Monotonic byte-flow counters: g_usb_frames++ after tud_audio_read
   (the OLED Diagnostics screen + web emulator compute USB-aud/s
   and BT-0x32/s as delta over time). g_bt_packets++ after each
   0x36 bt_write.

3. Audio Auto Haptics DSP — 1-pole LP + envelope follower +
   modulation + soft-clip per loteran/DS5Dongle 5d6bc2f, with our
   added 4th "Fallback" mode that fires only after the game's
   native haptic path has been silent for ~1 s (NATIVE_SILENT_TIMEOUT
   = 100 audio_loop calls). Preserves native HD haptics (Spider-Man
   Remastered) while filling in for games that send no haptic data
   (Ghost of Tsushima). Modes: 0=Off, 1=Fallback (default), 2=Mix,
   3=Replace.

Crucially the state_mgr-based state_set(pkt+13, 63) call is left
untouched — that's upstream's correct replacement for our prior
state_data restore hack, and it's load-bearing for speaker output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MarcelineVPQ
2026-05-17 07:59:58 -06:00
co-authored by Claude Opus 4.7
parent 0593d79f5b
commit 8217d3a414
2 changed files with 118 additions and 5 deletions
+106 -5
View File
@@ -45,6 +45,34 @@ 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);
}
void audio_loop() {
// 1. 读取 USB 音频数据
if (!tud_audio_available()) return;
@@ -55,6 +83,7 @@ void audio_loop() {
if (frames == 0) {
return;
}
g_usb_frames += (uint32_t)frames;
static float audio_buf[512 * 2];
static uint audio_buf_pos = 0;
@@ -64,8 +93,44 @@ void audio_loop() {
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++) {
#if !DISABLE_SPEAKER_PROC
// 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) {
@@ -80,10 +145,45 @@ void audio_loop() {
audio_buf_pos = 0;
}
#endif
in_buf[i * 2] = static_cast<WDL_ResampleSample>(clamp(raw[i * INPUT_CHANNELS + 2] / 32768.0f * haptics_gain,
-1.0f, 1.0f));
in_buf[i * 2 + 1] = static_cast<WDL_ResampleSample>(clamp(raw[i * INPUT_CHANNELS + 3] / 32768.0f * haptics_gain,
-1.0f, 1.0f));
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 重采样
@@ -138,6 +238,7 @@ void audio_loop() {
#endif
bt_write(pkt, sizeof(pkt));
g_bt_packets++;
haptic_buf_pos = 0;
}
}
+12
View File
@@ -5,9 +5,21 @@
#ifndef DS5_BRIDGE_AUDIO_H
#define DS5_BRIDGE_AUDIO_H
#include <cstdint>
void audio_init();
void audio_loop();
void core1_entry();
void set_headset(bool state);
// Accessors used by the optional OLED add-on (diag + VU meter screens).
uint32_t audio_fifo_drops();
uint32_t opus_fifo_drops();
uint8_t audio_peak_speaker(); // 0..255, decays on read
uint8_t audio_peak_haptic(); // 0..255, decays on read
// Byte-flow counters for the Diagnostics screen + web emulator.
uint32_t audio_usb_frames();
uint32_t audio_bt_packets();
#endif //DS5_BRIDGE_AUDIO_H