wip: park audio-crackle investigation + haptic/OLED defaults + debug tooling
Preserves in-progress work that had been sitting uncommitted in the working tree (predates this session) so it is protected in git history. This is NOT a verified/shipped fix — it is parked. Audio path is unverified on hardware and still carries debug instrumentation. - src/audio.cpp: drop the 512->480 resampler and retime the 0x36 frame to a true 10ms/100Hz grid (SAMPLE_SIZE 64->60, 480-sample buffer) to chase the ~45kHz-vs-48kHz speaker underrun theory behind the crackle; dynamic speaker sub-report offset; debug printf + Opus-frame-dump instrumentation. - src/config.cpp: default audio_buffer_length 64->16; auto_haptics_enable default 1 (Fallback) -> 0 (Off). - src/oled.cpp: chunked non-blocking SPI flush (issue #7 OLED-stall angle) and a battery-% midpoint display tweak. - scripts/: audio debug helpers (opus-dump decode, pi audio test, sine ch1/2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
71cead401d
commit
84393c00e8
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Decode raw Opus frames captured from firmware serial output.
|
||||||
|
|
||||||
|
Reads [OPUS_FRAME_N] hex lines from stdin or a file, decodes each frame
|
||||||
|
with libopus, and writes the result as a WAV file + prints a summary
|
||||||
|
(peak amplitude, zero-crossing rate) to diagnose encoder output quality.
|
||||||
|
"""
|
||||||
|
import ctypes
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import wave
|
||||||
|
import re
|
||||||
|
|
||||||
|
lib = ctypes.cdll.LoadLibrary("libopus.so.0")
|
||||||
|
|
||||||
|
SAMPLE_RATE = 48000
|
||||||
|
CHANNELS = 2
|
||||||
|
FRAME_SIZE = 480 # 10ms at 48kHz
|
||||||
|
|
||||||
|
# Create decoder
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
decoder = lib.opus_decoder_create(SAMPLE_RATE, CHANNELS, ctypes.byref(err))
|
||||||
|
if err.value != 0:
|
||||||
|
print(f"opus_decoder_create failed: {err.value}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
infile = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ds5_opus.log"
|
||||||
|
with open(infile) as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
frames = []
|
||||||
|
for line in lines:
|
||||||
|
m = re.match(r'\[OPUS_FRAME_\d+\]\s+([0-9a-fA-F]+)', line.strip())
|
||||||
|
if m:
|
||||||
|
frames.append(bytes.fromhex(m.group(1)))
|
||||||
|
|
||||||
|
if not frames:
|
||||||
|
print("No [OPUS_FRAME_N] lines found in input.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Found {len(frames)} Opus frames, decoding...")
|
||||||
|
|
||||||
|
all_pcm = b""
|
||||||
|
for i, frame_data in enumerate(frames):
|
||||||
|
pcm = (ctypes.c_int16 * (FRAME_SIZE * CHANNELS))()
|
||||||
|
ret = lib.opus_decode(
|
||||||
|
decoder,
|
||||||
|
frame_data, len(frame_data),
|
||||||
|
pcm, FRAME_SIZE,
|
||||||
|
0 # no FEC
|
||||||
|
)
|
||||||
|
if ret < 0:
|
||||||
|
errstr = lib.opus_strerror(ret)
|
||||||
|
print(f" Frame {i+1}: DECODE ERROR {ret} ({ctypes.string_at(errstr).decode()})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
samples = list(pcm)
|
||||||
|
peak = max(abs(s) for s in samples)
|
||||||
|
nonzero = sum(1 for s in samples if s != 0)
|
||||||
|
print(f" Frame {i+1}: {ret} samples decoded, peak={peak}, nonzero={nonzero}/{len(samples)}")
|
||||||
|
print(f" TOC=0x{frame_data[0]:02x} first 8 bytes: {frame_data[:8].hex()}")
|
||||||
|
all_pcm += struct.pack(f"<{ret * CHANNELS}h", *samples[:ret * CHANNELS])
|
||||||
|
|
||||||
|
outpath = "/tmp/ds5_opus_decoded.wav"
|
||||||
|
with wave.open(outpath, "w") as wf:
|
||||||
|
wf.setnchannels(CHANNELS)
|
||||||
|
wf.setsampwidth(2)
|
||||||
|
wf.setframerate(SAMPLE_RATE)
|
||||||
|
wf.writeframes(all_pcm)
|
||||||
|
|
||||||
|
print(f"\nDecoded audio written to {outpath}")
|
||||||
|
print(f"Play with: aplay {outpath}")
|
||||||
|
print(f"View spectrogram: sox {outpath} -n spectrogram -o /tmp/ds5_opus_spectrogram.png")
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send a 440 Hz sine to the DualSense speaker over Bluetooth from a Linux host
|
||||||
|
(Raspberry Pi), via raw /dev/hidraw writes — replicating the DS5Dongle firmware's
|
||||||
|
0x36 audio report BYTE-FOR-BYTE (same Opus settings, same SetStateData, same CRC).
|
||||||
|
|
||||||
|
Purpose: test whether a NON-tunneled BT path (Pi's BlueZ over a real UART) delivers
|
||||||
|
clean audio to the DS5 speaker, vs the Pico 2 W's BT-over-gSPI tunnel. If clean here,
|
||||||
|
the Pico's tunnel is the crackle culprit.
|
||||||
|
|
||||||
|
Pair the DS5 first (bluetoothctl), then run as root (hidraw needs RW):
|
||||||
|
sudo python3 pi_ds5_audio_test.py [seconds] [--headset] [--audbuf N]
|
||||||
|
"""
|
||||||
|
import ctypes, struct, math, sys, os, glob, time
|
||||||
|
|
||||||
|
# ---- libopus via ctypes (same loader style as decode_opus_dump.py) ----
|
||||||
|
opus = ctypes.cdll.LoadLibrary("libopus.so.0")
|
||||||
|
opus.opus_encoder_create.restype = ctypes.c_void_p
|
||||||
|
opus.opus_encoder_create.argtypes = [ctypes.c_int32, ctypes.c_int, ctypes.c_int,
|
||||||
|
ctypes.POINTER(ctypes.c_int)]
|
||||||
|
opus.opus_encode_float.restype = ctypes.c_int32
|
||||||
|
opus.opus_encode_float.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float),
|
||||||
|
ctypes.c_int, ctypes.POINTER(ctypes.c_ubyte),
|
||||||
|
ctypes.c_int32]
|
||||||
|
# opus_encoder_ctl is variadic; leave argtypes unset and pass c_int values.
|
||||||
|
OPUS_APPLICATION_AUDIO = 2049
|
||||||
|
OPUS_SET_BITRATE_REQUEST = 4002
|
||||||
|
OPUS_SET_VBR_REQUEST = 4006
|
||||||
|
OPUS_SET_COMPLEXITY_REQUEST = 4010
|
||||||
|
|
||||||
|
RATE, CH, FRAME, OPUS_BYTES = 48000, 2, 480, 200 # 480 = 10 ms; 200 B = 160 kbps CBR
|
||||||
|
|
||||||
|
def make_encoder():
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
enc = opus.opus_encoder_create(RATE, CH, OPUS_APPLICATION_AUDIO, ctypes.byref(err))
|
||||||
|
if err.value != 0 or not enc:
|
||||||
|
sys.exit(f"opus_encoder_create failed: {err.value}")
|
||||||
|
enc_p = ctypes.c_void_p(enc)
|
||||||
|
# match firmware core1_entry(): 160 kbps, CBR, complexity 0
|
||||||
|
opus.opus_encoder_ctl(enc_p, OPUS_SET_BITRATE_REQUEST, ctypes.c_int(OPUS_BYTES*8*100))
|
||||||
|
opus.opus_encoder_ctl(enc_p, OPUS_SET_VBR_REQUEST, ctypes.c_int(0))
|
||||||
|
opus.opus_encoder_ctl(enc_p, OPUS_SET_COMPLEXITY_REQUEST, ctypes.c_int(0))
|
||||||
|
return enc_p
|
||||||
|
|
||||||
|
# ---- 63-byte SetStateData, verbatim from firmware src/state_mgr.cpp ----
|
||||||
|
STATE_DATA = bytes([
|
||||||
|
0xfd, 0xf7, 0x00, 0x00,
|
||||||
|
0x7f, 0x64, # VolHeadphonesMax, VolSpeaker
|
||||||
|
0x40, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
|
||||||
|
0x07, 0x00, 0x00, 0x02, 0x01,
|
||||||
|
0x00,
|
||||||
|
0xff, 0xd7, 0x00, # RGB
|
||||||
|
]) + bytes(16) # trailing zeros -> 63 total
|
||||||
|
assert len(STATE_DATA) == 63
|
||||||
|
|
||||||
|
# ---- CRC-32, 0xA2-seeded, verbatim from firmware src/utils.h ----
|
||||||
|
def ds_crc32(data):
|
||||||
|
crc = (~0xEADA2D49) & 0xFFFFFFFF
|
||||||
|
for b in data:
|
||||||
|
crc ^= b
|
||||||
|
for _ in range(8):
|
||||||
|
if crc & 1: crc = (crc >> 1) ^ 0xEDB88320
|
||||||
|
else: crc >>= 1
|
||||||
|
return (~crc) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
REPORT_SIZE, SAMPLE_SIZE = 398, 64 # original firmware layout
|
||||||
|
|
||||||
|
def build_packet(payload, seq, counter, headset, audbuf):
|
||||||
|
pkt = bytearray(REPORT_SIZE)
|
||||||
|
pkt[0] = 0x36
|
||||||
|
pkt[1] = (seq & 0x0F) << 4
|
||||||
|
pkt[2] = 0x11 | 0x80 # 0x91
|
||||||
|
pkt[3] = 7
|
||||||
|
pkt[4] = 0xFE # audio enable, mic OFF
|
||||||
|
for i in range(5, 10): pkt[i] = audbuf & 0xFF
|
||||||
|
pkt[10] = counter & 0xFF
|
||||||
|
pkt[11] = 0x10 | 0x80 # 0x90 SetStateData
|
||||||
|
pkt[12] = 63
|
||||||
|
pkt[13:76] = STATE_DATA
|
||||||
|
pkt[76] = 0x12 | 0x80 # 0x92 haptic
|
||||||
|
pkt[77] = SAMPLE_SIZE # haptic data pkt[78..141] = 0 (silent)
|
||||||
|
pkt[142] = (0x16 if headset else 0x13) | 0x80 # 0x96 headset / 0x93 speaker
|
||||||
|
pkt[143] = OPUS_BYTES
|
||||||
|
pkt[144:144+OPUS_BYTES] = payload
|
||||||
|
struct.pack_into("<I", pkt, REPORT_SIZE-4, ds_crc32(pkt[:REPORT_SIZE-4]))
|
||||||
|
return bytes(pkt)
|
||||||
|
|
||||||
|
def find_ds5_hidraw():
|
||||||
|
for ue in glob.glob("/sys/class/hidraw/hidraw*/device/uevent"):
|
||||||
|
txt = open(ue).read().upper()
|
||||||
|
if "054C" in txt and ("0CE6" in txt or "0DF2" in txt):
|
||||||
|
return "/dev/" + ue.split("/")[4]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
headset = "--headset" in sys.argv
|
||||||
|
audbuf = int(sys.argv[sys.argv.index("--audbuf")+1]) if "--audbuf" in sys.argv else 64
|
||||||
|
pos = [a for a in sys.argv[1:] if not a.startswith("--") and a.isdigit()]
|
||||||
|
secs = int(pos[0]) if pos else 15
|
||||||
|
|
||||||
|
node = find_ds5_hidraw()
|
||||||
|
if not node:
|
||||||
|
sys.exit("DualSense hidraw node not found — paired & connected? (bluetoothctl)")
|
||||||
|
print(f"DS5 hidraw: {node} | output: {'HEADSET' if headset else 'SPEAKER'} | "
|
||||||
|
f"AudBuf={audbuf} | {secs}s")
|
||||||
|
|
||||||
|
enc = make_encoder()
|
||||||
|
pcm = (ctypes.c_float * (FRAME*CH))()
|
||||||
|
out = (ctypes.c_ubyte * OPUS_BYTES)()
|
||||||
|
|
||||||
|
# Pre-build every packet so the timed send loop is pure I/O (no compute jitter).
|
||||||
|
n_frames = secs * 100
|
||||||
|
packets, seq, counter, samp = [], 0, 0, 0
|
||||||
|
first_nb = None
|
||||||
|
for _ in range(n_frames):
|
||||||
|
for i in range(FRAME):
|
||||||
|
v = 0.5 * math.sin(2*math.pi*440.0*samp/RATE)
|
||||||
|
pcm[i*2] = v; pcm[i*2+1] = v; samp += 1
|
||||||
|
nb = opus.opus_encode_float(enc, pcm, FRAME, out, OPUS_BYTES)
|
||||||
|
if nb < 0: sys.exit(f"opus_encode_float error {nb}")
|
||||||
|
if first_nb is None: first_nb = nb
|
||||||
|
payload = bytes(out[:nb]) + bytes(OPUS_BYTES - nb) if nb < OPUS_BYTES else bytes(out[:OPUS_BYTES])
|
||||||
|
packets.append(build_packet(payload, seq, counter, headset, audbuf))
|
||||||
|
seq = (seq+1) & 0x0F; counter = (counter+1) & 0xFF
|
||||||
|
print(f"Encoded {len(packets)} frames (first frame {first_nb} B — expect ~200 for CBR). Streaming...")
|
||||||
|
|
||||||
|
fd = os.open(node, os.O_RDWR)
|
||||||
|
short = 0
|
||||||
|
t0 = time.monotonic()
|
||||||
|
for n, pkt in enumerate(packets):
|
||||||
|
try:
|
||||||
|
w = os.write(fd, pkt)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"write failed at frame {n}: {e}"); break
|
||||||
|
if w != REPORT_SIZE: short += 1
|
||||||
|
target = t0 + (n+1)*0.01 # 10 ms grid -> 100 pkt/s
|
||||||
|
dt = target - time.monotonic()
|
||||||
|
if dt > 0: time.sleep(dt)
|
||||||
|
os.close(fd)
|
||||||
|
print(f"Done — {n+1} frames sent (~{(n+1)/100:.1f}s).", end="")
|
||||||
|
print(f" WARNING: {short} short writes (hidraw truncating the 398 B report!)" if short else " All writes full-length.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send a 440 Hz sine wave on channels 1+2 only (speaker), silence on 3+4 (haptic).
|
||||||
|
|
||||||
|
Usage: python3 scripts/sine_ch12.py [seconds]
|
||||||
|
"""
|
||||||
|
import subprocess, struct, math, sys, re
|
||||||
|
|
||||||
|
RATE = 48000
|
||||||
|
FREQ = 440
|
||||||
|
DURATION = int(sys.argv[1]) if len(sys.argv) > 1 else 5
|
||||||
|
CHANNELS = 4
|
||||||
|
SAMPLES = RATE * DURATION
|
||||||
|
|
||||||
|
# Generate 4-channel S16_LE: sine on ch1+ch2, silence on ch3+ch4
|
||||||
|
data = bytearray()
|
||||||
|
for i in range(SAMPLES):
|
||||||
|
val = int(6000 * math.sin(2 * math.pi * FREQ * i / RATE)) # ~-15 dBFS (was 32767 / 0 dBFS) — clipping test
|
||||||
|
s = struct.pack('<h', val)
|
||||||
|
data += s + s + b'\x00\x00' + b'\x00\x00' # L, R, hapL=0, hapR=0
|
||||||
|
|
||||||
|
# Auto-detect the dongle's ALSA card — it enumerates as "DualSense Wireless
|
||||||
|
# Controller", and the card number shifts across reboots, so never hardcode it.
|
||||||
|
def find_dualsense_card():
|
||||||
|
out = subprocess.check_output(['aplay', '-l'], text=True)
|
||||||
|
for line in out.splitlines():
|
||||||
|
m = re.match(r'card (\d+):', line)
|
||||||
|
if m and 'DualSense' in line:
|
||||||
|
return int(m.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
card = find_dualsense_card()
|
||||||
|
if card is None:
|
||||||
|
print("DualSense dongle not found in `aplay -l` — paired and enumerated?", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
device = f'hw:{card},0'
|
||||||
|
print(f"Playing to {device} (DualSense dongle)")
|
||||||
|
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
['aplay', '-D', device, '-f', 'S16_LE', '-c', '4', '-r', '48000', '-'],
|
||||||
|
stdin=subprocess.PIPE
|
||||||
|
)
|
||||||
|
proc.stdin.write(data)
|
||||||
|
proc.stdin.close()
|
||||||
|
proc.wait()
|
||||||
|
print(f"Played {DURATION}s of {FREQ} Hz sine on ch1+ch2 (speaker only)")
|
||||||
+51
-25
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
#define INPUT_CHANNELS 4
|
#define INPUT_CHANNELS 4
|
||||||
#define OUTPUT_CHANNELS 2
|
#define OUTPUT_CHANNELS 2
|
||||||
#define SAMPLE_SIZE 64
|
#define SAMPLE_SIZE 60 // 60 B = 30 haptic frames @ 3 kHz = 10.00 ms/packet → true 100 Hz cadence
|
||||||
#define REPORT_SIZE 398
|
#define REPORT_SIZE 398
|
||||||
#define REPORT_ID 0x36
|
#define REPORT_ID 0x36
|
||||||
// #define VOLUME_GAIN 2
|
// #define VOLUME_GAIN 2
|
||||||
@@ -80,7 +80,7 @@ static volatile uint32_t g_mic_plc_frames = 0; // concealed frames genera
|
|||||||
uint32_t audio_mic_plc_frames() { return g_mic_plc_frames; }
|
uint32_t audio_mic_plc_frames() { return g_mic_plc_frames; }
|
||||||
|
|
||||||
struct audio_raw_element {
|
struct audio_raw_element {
|
||||||
float data[512 * 2];
|
float data[480 * 2]; // exactly one 10 ms Opus frame (480 stereo samples)
|
||||||
};
|
};
|
||||||
|
|
||||||
void set_headset(bool state) {
|
void set_headset(bool state) {
|
||||||
@@ -96,6 +96,10 @@ uint32_t opus_fifo_drops() { return 0; }
|
|||||||
// emulator's USB / BT rate display. Updated below.
|
// emulator's USB / BT rate display. Updated below.
|
||||||
static volatile uint32_t g_usb_frames = 0;
|
static volatile uint32_t g_usb_frames = 0;
|
||||||
static volatile uint32_t g_bt_packets = 0;
|
static volatile uint32_t g_bt_packets = 0;
|
||||||
|
static volatile int32_t g_opus_last_ret = 0;
|
||||||
|
static volatile uint32_t g_fifo_drops = 0;
|
||||||
|
static volatile uint32_t g_opus_encodes = 0;
|
||||||
|
static volatile bool g_opus_ready = false;
|
||||||
uint32_t audio_usb_frames() { return g_usb_frames; }
|
uint32_t audio_usb_frames() { return g_usb_frames; }
|
||||||
uint32_t audio_bt_packets() { return g_bt_packets; }
|
uint32_t audio_bt_packets() { return g_bt_packets; }
|
||||||
|
|
||||||
@@ -258,7 +262,7 @@ void audio_loop() {
|
|||||||
}
|
}
|
||||||
g_usb_frames += (uint32_t)frames;
|
g_usb_frames += (uint32_t)frames;
|
||||||
|
|
||||||
static float audio_buf[512 * 2];
|
static float audio_buf[480 * 2];
|
||||||
static uint audio_buf_pos = 0;
|
static uint audio_buf_pos = 0;
|
||||||
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
// 2. 从4ch中提取ch3/ch4,转换为float输入重采样器
|
||||||
WDL_ResampleSample *in_buf;
|
WDL_ResampleSample *in_buf;
|
||||||
@@ -306,11 +310,12 @@ void audio_loop() {
|
|||||||
#if !DISABLE_SPEAKER_PROC
|
#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] / 32768.0f * audio_gain;
|
||||||
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
|
audio_buf[audio_buf_pos++] = raw[i * INPUT_CHANNELS + 1] / 32768.0f * audio_gain;
|
||||||
if (audio_buf_pos == 512 * 2) {
|
if (audio_buf_pos == 480 * 2) {
|
||||||
static audio_raw_element element{};
|
static audio_raw_element element{};
|
||||||
memcpy(element.data, audio_buf, 512 * 2 * 4);
|
memcpy(element.data, audio_buf, 480 * 2 * 4);
|
||||||
if (queue_is_full(&audio_fifo)) {
|
if (queue_is_full(&audio_fifo)) {
|
||||||
queue_try_remove(&audio_fifo,NULL);
|
queue_try_remove(&audio_fifo,NULL);
|
||||||
|
g_fifo_drops++;
|
||||||
}
|
}
|
||||||
if (!queue_try_add(&audio_fifo, &element)) {
|
if (!queue_try_add(&audio_fifo, &element)) {
|
||||||
printf("[Audio] Warning: audio_fifo add failed\n");
|
printf("[Audio] Warning: audio_fifo add failed\n");
|
||||||
@@ -402,20 +407,45 @@ void audio_loop() {
|
|||||||
pkt[77] = SAMPLE_SIZE;
|
pkt[77] = SAMPLE_SIZE;
|
||||||
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
|
memcpy(pkt + 78, haptic_buf, SAMPLE_SIZE);
|
||||||
#if !DISABLE_SPEAKER_PROC
|
#if !DISABLE_SPEAKER_PROC
|
||||||
// Speaker Audio Data
|
// Speaker Audio Data — MUST immediately follow the haptic block. The DS5
|
||||||
pkt[142] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
|
// parses sub-reports sequentially (header + len + data), so this offset is
|
||||||
|
// 78 + SAMPLE_SIZE, NOT a fixed 142. At SAMPLE_SIZE 64 that worked out to
|
||||||
|
// 142; shrinking the haptic block to 60 without moving this is what
|
||||||
|
// silenced the speaker (controller couldn't locate the speaker sub-report).
|
||||||
|
constexpr int kSpkOff = 78 + SAMPLE_SIZE; // = 138 at SAMPLE_SIZE 60
|
||||||
|
pkt[kSpkOff] = (plug_headset ? 0x16 : 0x13) | 0 << 6 | 1 << 7; // Speaker: 0x13
|
||||||
// L Headset Mono: 0x14
|
// L Headset Mono: 0x14
|
||||||
// L Headset R Speaker: 0x15
|
// L Headset R Speaker: 0x15
|
||||||
// Headset: 0x16
|
// Headset: 0x16
|
||||||
pkt[143] = 200;
|
pkt[kSpkOff + 1] = 200;
|
||||||
critical_section_enter_blocking(&opus_cs);
|
critical_section_enter_blocking(&opus_cs);
|
||||||
memcpy(pkt + 144, opus_buf, 200);
|
memcpy(pkt + kSpkOff + 2, opus_buf, 200);
|
||||||
critical_section_exit(&opus_cs);
|
critical_section_exit(&opus_cs);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bt_write(pkt, sizeof(pkt));
|
bt_write(pkt, sizeof(pkt));
|
||||||
g_bt_packets++;
|
g_bt_packets++;
|
||||||
haptic_buf_pos = 0;
|
haptic_buf_pos = 0;
|
||||||
|
|
||||||
|
// Debug: dump 5 consecutive Opus frames (skip first 10 to let encoder settle)
|
||||||
|
{
|
||||||
|
static int dump_count = 0;
|
||||||
|
if (g_bt_packets > 10 && dump_count < 5) {
|
||||||
|
dump_count++;
|
||||||
|
printf("[OPUS_FRAME_%d] ", dump_count);
|
||||||
|
critical_section_enter_blocking(&opus_cs);
|
||||||
|
for (int di = 0; di < 200; di++) printf("%02x", opus_buf[di]);
|
||||||
|
critical_section_exit(&opus_cs);
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
if ((g_bt_packets % 94) == 0) {
|
||||||
|
printf("[AUD] usb=%lu enc=%lu bt=%lu opus_ret=%ld fifo_drop=%lu hs=%d\n",
|
||||||
|
(unsigned long)g_usb_frames, (unsigned long)g_opus_encodes,
|
||||||
|
(unsigned long)g_bt_packets, (long)g_opus_last_ret,
|
||||||
|
(unsigned long)g_fifo_drops,
|
||||||
|
plug_headset ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +473,6 @@ void audio_init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static OpusEncoder *encoder;
|
static OpusEncoder *encoder;
|
||||||
static WDL_Resampler resampler_audio;
|
|
||||||
|
|
||||||
void core1_entry() {
|
void core1_entry() {
|
||||||
int error = 0;
|
int error = 0;
|
||||||
@@ -455,28 +484,25 @@ void core1_entry() {
|
|||||||
opus_encoder_ctl(encoder,OPUS_SET_EXPERT_FRAME_DURATION(OPUS_FRAMESIZE_10_MS));
|
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_BITRATE(200 * 8 * 100));
|
||||||
opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
|
opus_encoder_ctl(encoder,OPUS_SET_VBR(false));
|
||||||
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // max 4
|
opus_encoder_ctl(encoder,OPUS_SET_COMPLEXITY(0)); // 5 overloaded core1 (stale frames -> worse); 0 keeps up
|
||||||
resampler_audio.SetMode(true, 0, false);
|
|
||||||
resampler_audio.SetRates(51200, 48000);
|
|
||||||
resampler_audio.SetFeedMode(true);
|
|
||||||
resampler_audio.Prealloc(2, 512, 480);
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
static audio_raw_element audio_element{};
|
static audio_raw_element audio_element{};
|
||||||
queue_remove_blocking(&audio_fifo, &audio_element);
|
queue_remove_blocking(&audio_fifo, &audio_element);
|
||||||
// 将 512 frames 重采样成 480 frames 以解决噪音问题。感谢 @Junhoo
|
// audio_element is exactly 480 stereo frames (10 ms @ 48 kHz) = one native
|
||||||
WDL_ResampleSample *in_buf;
|
// Opus frame, so encode it directly. The old 512→480 (51200→48000) resample
|
||||||
int nframes = resampler_audio.ResamplePrepare(512, 2, &in_buf);
|
// only existed to coerce a 512-sample buffer into a legal Opus frame size; it
|
||||||
for (int i = 0; i < nframes * 2; i++) {
|
// shipped 480 samples every 10.667 ms (haptic-gated cadence) = 45 kHz into the
|
||||||
in_buf[i] = audio_element.data[i];
|
// DS5's free-running 48 kHz DAC → ~6.25% underrun = the periodic gaps/crackle.
|
||||||
}
|
// SAMPLE_SIZE 60 + a 480-sample buffer put the whole 0x36 frame on a true
|
||||||
static WDL_ResampleSample out_buf[480 * 2];
|
// 10 ms / 100 Hz grid: 100 × 480 = 48000 samples/s, matched, no gaps.
|
||||||
resampler_audio.ResampleOut(out_buf, nframes, 480, 2);
|
|
||||||
|
|
||||||
static uint8_t out[200];
|
static uint8_t out[200];
|
||||||
(void) opus_encode_float(encoder, out_buf, 480, out, 200);
|
int enc_ret = opus_encode_float(encoder, audio_element.data, 480, out, 200);
|
||||||
|
g_opus_last_ret = enc_ret;
|
||||||
|
g_opus_encodes++;
|
||||||
critical_section_enter_blocking(&opus_cs);
|
critical_section_enter_blocking(&opus_cs);
|
||||||
memcpy(opus_buf, out, 200);
|
memcpy(opus_buf, out, 200);
|
||||||
critical_section_exit(&opus_cs);
|
critical_section_exit(&opus_cs);
|
||||||
|
g_opus_ready = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -72,7 +72,7 @@ void config_valid() {
|
|||||||
printf("[Config] polling_rate_mode is invalid\n");
|
printf("[Config] polling_rate_mode is invalid\n");
|
||||||
}
|
}
|
||||||
if (body->audio_buffer_length < 16 || body->audio_buffer_length > 128) {
|
if (body->audio_buffer_length < 16 || body->audio_buffer_length > 128) {
|
||||||
body->audio_buffer_length = 64;
|
body->audio_buffer_length = 16; // low buffer avoids the DS5's periodic re-buffer gap
|
||||||
printf("[Config] haptics_buffer_length is invalid\n");
|
printf("[Config] haptics_buffer_length is invalid\n");
|
||||||
}
|
}
|
||||||
if (body->controller_mode > 2) {
|
if (body->controller_mode > 2) {
|
||||||
@@ -84,8 +84,8 @@ void config_valid() {
|
|||||||
printf("[Config] current_slot is invalid\n");
|
printf("[Config] current_slot is invalid\n");
|
||||||
}
|
}
|
||||||
if (body->auto_haptics_enable > 3) {
|
if (body->auto_haptics_enable > 3) {
|
||||||
body->auto_haptics_enable = 1; // Fallback default
|
body->auto_haptics_enable = 0; // default OFF (was 1/Fallback) — it derives erratic rumble from the speaker
|
||||||
printf("[Config] auto_haptics_enable invalid, defaulting to 1 (Fallback)\n");
|
printf("[Config] auto_haptics_enable invalid, defaulting to 0 (Off)\n");
|
||||||
}
|
}
|
||||||
if (body->auto_haptics_gain > 200) {
|
if (body->auto_haptics_gain > 200) {
|
||||||
body->auto_haptics_gain = 100;
|
body->auto_haptics_gain = 100;
|
||||||
|
|||||||
+57
-9
@@ -52,6 +52,10 @@ constexpr int kRowBytes = kW / 8;
|
|||||||
constexpr int kFbBytes = kRowBytes * kH;
|
constexpr int kFbBytes = kRowBytes * kH;
|
||||||
|
|
||||||
uint8_t fb[kFbBytes];
|
uint8_t fb[kFbBytes];
|
||||||
|
uint8_t fb_tx[kFbBytes];
|
||||||
|
uint8_t reverse_lut[256];
|
||||||
|
int flush_progress = -1;
|
||||||
|
constexpr int kFlushChunkRows = 8;
|
||||||
|
|
||||||
uint32_t last_render_us = 0;
|
uint32_t last_render_us = 0;
|
||||||
constexpr uint32_t kFrameUs = 100000;
|
constexpr uint32_t kFrameUs = 100000;
|
||||||
@@ -237,7 +241,9 @@ void sh1107_init() {
|
|||||||
// lives near the other text-drawing helpers below.
|
// lives near the other text-drawing helpers below.
|
||||||
void draw_button_chrome();
|
void draw_button_chrome();
|
||||||
|
|
||||||
void flush_fb_raw() {
|
// Blocking SPI flush — used only during boot splash before the main loop
|
||||||
|
// starts (no audio/BT to service yet, so blocking is fine).
|
||||||
|
void flush_fb_raw_blocking() {
|
||||||
cmd(0xB0);
|
cmd(0xB0);
|
||||||
for (int j = 0; j < kH; j++) {
|
for (int j = 0; j < kH; j++) {
|
||||||
const uint8_t col = kH - 1 - j;
|
const uint8_t col = kH - 1 - j;
|
||||||
@@ -249,11 +255,45 @@ void flush_fb_raw() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void flush_fb() {
|
// Prepare a chunked (non-blocking) SPI flush. Pre-reverses the framebuffer
|
||||||
draw_button_chrome();
|
// into fb_tx via the LUT and sets flush_progress = 0. The actual SPI
|
||||||
flush_fb_raw();
|
// transfer is driven by flush_chunk(), called from oled_loop() on
|
||||||
|
// subsequent iterations — each chunk flushes kFlushChunkRows rows (~130 µs)
|
||||||
|
// then yields back to the main loop so audio_loop / tud_task /
|
||||||
|
// cyw43_arch_poll stay serviced. This eliminates the ~1.1 ms blocking
|
||||||
|
// window that caused audio distortion when the OLED was active (issue #7).
|
||||||
|
void flush_prepare(bool chrome) {
|
||||||
|
if (chrome) draw_button_chrome();
|
||||||
|
for (int i = 0; i < kFbBytes; i++) fb_tx[i] = reverse_lut[fb[i]];
|
||||||
|
flush_progress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool flush_chunk() {
|
||||||
|
if (flush_progress < 0) return true;
|
||||||
|
if (flush_progress == 0) cmd(0xB0);
|
||||||
|
const int end = (flush_progress + kFlushChunkRows < kH)
|
||||||
|
? flush_progress + kFlushChunkRows : kH;
|
||||||
|
for (int j = flush_progress; j < end; j++) {
|
||||||
|
const uint8_t col = kH - 1 - j;
|
||||||
|
cmd(0x00 + (col & 0x0F));
|
||||||
|
cmd(0x10 + (col >> 4));
|
||||||
|
gpio_put(kPinDC, 1);
|
||||||
|
gpio_put(kPinCS, 0);
|
||||||
|
spi_write_blocking(spi1, &fb_tx[j * kRowBytes], kRowBytes);
|
||||||
|
gpio_put(kPinCS, 1);
|
||||||
|
}
|
||||||
|
flush_progress = end;
|
||||||
|
if (flush_progress >= kH) {
|
||||||
|
flush_progress = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void flush_fb_raw() { flush_prepare(false); }
|
||||||
|
|
||||||
|
void flush_fb() { flush_prepare(true); }
|
||||||
|
|
||||||
void fb_clear() { memset(fb, 0, sizeof(fb)); }
|
void fb_clear() { memset(fb, 0, sizeof(fb)); }
|
||||||
|
|
||||||
void px(int x, int y, bool on) {
|
void px(int x, int y, bool on) {
|
||||||
@@ -542,8 +582,10 @@ void handle_buttons() {
|
|||||||
|
|
||||||
// --- Charge ETA tracker --------------------------------------------------
|
// --- Charge ETA tracker --------------------------------------------------
|
||||||
// The DS5 only reports battery in 10% steps (interrupt_in_data[52] low
|
// The DS5 only reports battery in 10% steps (interrupt_in_data[52] low
|
||||||
// nibble, 0..10; high nibble is power-state, 1 == charging). We can't read a
|
// nibble, 0..10; high nibble is power-state, 1 == charging). We display the
|
||||||
// finer percentage over BT, so a smooth countdown is impossible. Instead we
|
// midpoint of each band (+5), matching the kernel hid-playstation driver and
|
||||||
|
// Steam: 5, 15, 25, … 95, 100%. We can't read a finer percentage over BT,
|
||||||
|
// so a smooth countdown is impossible. Instead we
|
||||||
// time how long each 10% step takes while charging and extrapolate the
|
// time how long each 10% step takes while charging and extrapolate the
|
||||||
// remaining steps. Sampled once per frame from oled_loop (continuously, so
|
// remaining steps. Sampled once per frame from oled_loop (continuously, so
|
||||||
// the estimate stays current even while the panel is dimmed/off and even when
|
// the estimate stays current even while the panel is dimmed/off and even when
|
||||||
@@ -701,8 +743,8 @@ __attribute__((noinline)) void render_screen() {
|
|||||||
draw_text(kContentX, 9, buf);
|
draw_text(kContentX, 9, buf);
|
||||||
|
|
||||||
const uint8_t pwr = interrupt_in_data[52];
|
const uint8_t pwr = interrupt_in_data[52];
|
||||||
int pct = (pwr & 0x0F) * 10;
|
int raw = pwr & 0x0F;
|
||||||
if (pct > 100) pct = 100;
|
int pct = (raw >= 10) ? 100 : raw * 10 + 5;
|
||||||
const uint8_t pstate = pwr >> 4;
|
const uint8_t pstate = pwr >> 4;
|
||||||
char marker = ' ';
|
char marker = ' ';
|
||||||
if (pstate == 1) marker = '+'; // Charging
|
if (pstate == 1) marker = '+'; // Charging
|
||||||
@@ -1807,13 +1849,15 @@ void boot_splash() {
|
|||||||
draw_text(cx_for(l1), 16, l1);
|
draw_text(cx_for(l1), 16, l1);
|
||||||
draw_text(cx_for(l2), 30, l2);
|
draw_text(cx_for(l2), 30, l2);
|
||||||
draw_text(cx_for(l3), 44, l3);
|
draw_text(cx_for(l3), 44, l3);
|
||||||
flush_fb();
|
draw_button_chrome();
|
||||||
|
flush_fb_raw_blocking();
|
||||||
sleep_ms(1500);
|
sleep_ms(1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void oled_init() {
|
void oled_init() {
|
||||||
|
for (int i = 0; i < 256; i++) reverse_lut[i] = reverse_byte((uint8_t)i);
|
||||||
spi_init(spi1, 10 * 1000 * 1000);
|
spi_init(spi1, 10 * 1000 * 1000);
|
||||||
gpio_set_function(kPinCLK, GPIO_FUNC_SPI);
|
gpio_set_function(kPinCLK, GPIO_FUNC_SPI);
|
||||||
gpio_set_function(kPinMOSI, GPIO_FUNC_SPI);
|
gpio_set_function(kPinMOSI, GPIO_FUNC_SPI);
|
||||||
@@ -1874,6 +1918,10 @@ void oled_loop() {
|
|||||||
handle_buttons();
|
handle_buttons();
|
||||||
const uint32_t now = time_us_32();
|
const uint32_t now = time_us_32();
|
||||||
rumble_burst_tick(now);
|
rumble_burst_tick(now);
|
||||||
|
if (flush_progress >= 0) {
|
||||||
|
flush_chunk();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if ((now - last_render_us) < kFrameUs) return;
|
if ((now - last_render_us) < kFrameUs) return;
|
||||||
last_render_us = now;
|
last_render_us = now;
|
||||||
// Track charge progress every frame — before the power-ladder early-returns
|
// Track charge progress every frame — before the power-ladder early-returns
|
||||||
|
|||||||
Reference in New Issue
Block a user