DualSense haptics and controller speaker sound bluethooth (#969)

* pad: send DualSense speaker and haptic audio over Bluetooth

* ci: fix Opus clang-cl build and macOS memory tracker test

* docs: remove Bluetooth notes from project README

* pad: keep Bluetooth audio work off emulation timing path

* pad: smooth DualSense speaker audio on USB and Bluetooth

* tests: find available macOS guest range for page manager

* pad: keep haptic waveform when Bluetooth queue overflows

* audio: reject zero output sample frequency

* Simplify DualSense audio routing and fix Bluetooth batching

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Pyxellator
2026-10-02 12:55:00 +02:00
committed by GitHub
parent 89792a3bca
commit e1be2ead54
11 changed files with 1394 additions and 134 deletions
+44 -3
View File
@@ -29,6 +29,8 @@ uint32_t oversleep = 0, processing = 0;
std::vector<uint32_t> sleeps;
std::vector<std::unique_ptr<Stream>> streams;
bool fail_open = false, fail_put = false, stalled = false;
bool pad_connected = false, pad_bluetooth = false;
uint64_t pad_queue_us = 0;
int clears = 0;
Stream& GetStream(SDL_AudioStream* stream) {
@@ -136,11 +138,15 @@ int GetActiveControllerId() {
namespace DualSenseHaptics {
Stream* Open(uint32_t, bool) {
return nullptr;
static int pad_stream;
return pad_connected ? reinterpret_cast<Stream*>(&pad_stream) : nullptr;
}
void Close(Stream*) {}
uint64_t Queue(Stream*, int, const void*, uint32_t, uint32_t, bool, const int*) {
return 0;
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) {
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
} // namespace Libs::Controller
@@ -170,6 +176,8 @@ struct Fixture {
processing = oversleep = 0;
clears = 0;
fail_open = fail_put = stalled = false;
pad_connected = pad_bluetooth = false;
pad_queue_us = 0;
sleeps.clear();
}
@@ -336,6 +344,27 @@ void TestFailedQueueUsesFallbackClock() {
Check(now - start == 16000, "repeated queue failures reset fallback pacing");
}
void TestControllerSpeakerPacing() {
Fixture f;
fail_open = true; // No PC audio device paces the pad speaker.
pad_connected = true;
pad_queue_us = 80000;
const auto speaker = f.Open(256, 4);
pad_bluetooth = true;
f.Output(speaker);
const auto bluetooth_start = now;
f.Output(speaker);
Check(now - bluetooth_start >= 5333 && now - bluetooth_start <= 5334,
"Bluetooth speaker waited on its HID queue instead of the sample clock");
pad_bluetooth = false;
const auto usb_start = now;
f.Output(speaker);
Check(now - usb_start >= 5333 && now - usb_start <= 5334,
"stalled USB speaker waited longer than one audio block");
}
void TestInvalidBatchSize() {
Libs::Audio::AudioOut::AudioOutOutputParam param {};
for (const auto count: {0u, 33u}) {
@@ -344,6 +373,16 @@ void TestInvalidBatchSize() {
"invalid batch size was not rejected before accessing ports");
}
}
void TestZeroOutputFrequency() {
Fixture f;
Check(Libs::Audio::AudioOut::AudioOutOpen(0, 0, 0, 256, 0, 4) ==
Libs::Audio::AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ,
"public output open accepted zero frequency");
Check(!f.audio.AudioOutOpen(10, 256, 0, Audio::Format::FloatStereo).IsValid(),
"internal output open accepted zero frequency");
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
}
} // namespace
int main() {
@@ -354,7 +393,9 @@ int main() {
TestSynchronizedBatchAndInactivePorts();
TestFallbackUsesEachPortsPeriod();
TestFailedQueueUsesFallbackClock();
TestControllerSpeakerPacing();
TestInvalidBatchSize();
TestZeroOutputFrequency();
Check(streams.empty(), "output stream leaked");
std::puts("AudioOutTimingTests: all cases passed");
}