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
+18 -4
View File
@@ -428,6 +428,10 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
}
Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format) {
if (freq == 0) {
return Id::Invalid();
}
Common::LockGuard lock(m_mutex);
for (int id = 0; id < OUT_PORTS_MAX; id++) {
@@ -552,12 +556,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
auto& port = m_out_ports[params[i].handle.GetId()];
uint64_t controller_queued_us = 0;
bool controller_uses_bluetooth = false;
if (port.haptics != nullptr) {
// Keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
controller_queued_us = Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
controller_uses_bluetooth =
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
@@ -568,12 +575,16 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
}
} else {
port.queue_primed = false;
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK) {
// Bluetooth HID has its own sender. Let the sample clock pace a batch
// without another audio device, instead of waiting on the HID queue.
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK && !controller_uses_bluetooth) {
any_device = true;
if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
deadline =
std::max(deadline, LibKernel::KernelGetProcessTime() +
controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
// Limit a stalled USB endpoint to one audio block of waiting.
const uint64_t block_us = 1000000ULL * port.samples_num / port.freq;
deadline = std::max(deadline, LibKernel::KernelGetProcessTime() +
std::min(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US,
block_us));
}
}
}
@@ -842,6 +853,9 @@ int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, u
if (!audio_out_port_type_is_valid(type)) {
return AUDIO_OUT_ERROR_INVALID_PORT_TYPE;
}
if (freq == 0) {
return AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ;
}
EXIT_NOT_IMPLEMENTED(index != 0);
Audio::Format format = Audio::Format::Unknown;
+467
View File
@@ -0,0 +1,467 @@
#include "libs/dualSenseBluetooth.h"
#include "common/logging/log.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_hidapi.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <map>
#include <memory>
#include <mutex>
#include <opus.h>
#include <string>
#include <vector>
namespace Libs::Controller::DualSenseBluetooth {
namespace {
// Report 0x39 and its sub-packets are described by awalol/DS5Dongle (MIT):
// https://github.com/awalol/DS5Dongle
// Each report plays two 32-frame stereo haptic blocks at 3 kHz (21.333 ms).
constexpr uint64_t PERIOD_NS = 64000000000ULL / 3000;
constexpr int HAPTIC_RATE = 3000;
constexpr int HAPTIC_REPORT_FLOATS = 2 * 32 * 2;
// Two 480-frame Opus packets occupy the same 21.333 ms slot. Resampling
// 48 kHz input to 45 kHz before encoding at 48 kHz compensates for playback.
constexpr int SPEAKER_RATE = 45000;
constexpr int OPUS_FRAMES = 480;
constexpr int OPUS_BYTES = 200;
constexpr size_t REPORT_BYTES = 547;
constexpr int MAX_QUEUE_MS = 80;
constexpr uint64_t COALESCE_NS = 1000000;
struct Session;
std::mutex g_mutex;
std::condition_variable g_wake;
std::map<int, std::shared_ptr<Session>> g_sessions;
SDL_Thread* g_thread = nullptr;
bool g_stopping = false;
void (*g_tick)() = nullptr;
template <typename Char>
std::string Address(const Char* serial) {
std::string address;
if (serial == nullptr) {
return address;
}
for (; *serial != 0; ++serial) {
if (*serial >= '0' && *serial <= '9') {
address.push_back(static_cast<char>(*serial));
} else if (*serial >= 'a' && *serial <= 'f') {
address.push_back(static_cast<char>(*serial));
} else if (*serial >= 'A' && *serial <= 'F') {
address.push_back(static_cast<char>(*serial - 'A' + 'a'));
} else if (*serial != ':' && *serial != '-') {
return {};
}
}
return address.size() == 12 ? address : std::string {};
}
SDL_hid_device* OpenDevice(int controller) {
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
const bool wireless = pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5 &&
SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
const auto vendor = wireless ? SDL_GetGamepadVendor(pad) : 0;
const auto product = wireless ? SDL_GetGamepadProduct(pad) : 0;
const auto address = wireless ? Address(SDL_GetGamepadSerial(pad)) : std::string {};
SDL_UnlockJoysticks();
if (vendor != 0x054c || (product != 0x0ce6 && product != 0x0df2) || address.empty()) {
return nullptr;
}
auto* devices = SDL_hid_enumerate(vendor, product);
std::string path;
bool ambiguous = false;
for (auto* device = devices; device != nullptr; device = device->next) {
if (device->bus_type == SDL_HID_API_BUS_BLUETOOTH && device->path != nullptr &&
Address(device->serial_number) == address &&
(device->usage_page == 0 || (device->usage_page == 1 && device->usage == 5))) {
if (!path.empty()) {
ambiguous = true;
break;
}
path = device->path;
}
}
SDL_hid_free_enumeration(devices);
return !ambiguous && !path.empty() ? SDL_hid_open_path(path.c_str()) : nullptr;
}
void FinishReport(std::array<uint8_t, REPORT_BYTES>& report) {
const uint8_t prefix = 0xa2;
auto crc = SDL_crc32(0, &prefix, 1);
crc = SDL_crc32(crc, report.data(), report.size() - 4);
for (size_t i = 0; i < 4; i++) {
report[report.size() - 4 + i] = static_cast<uint8_t>(crc >> (8 * i));
}
}
struct Session {
SDL_hid_device* hid = nullptr;
OpusEncoder* encoder = nullptr;
std::vector<Stream*> streams;
std::mutex io_mutex;
std::atomic<bool> active {true};
std::atomic<bool> failed {false};
std::atomic<uint64_t> generation {0};
uint64_t next_send = 0;
uint64_t pending_deadline = 0;
uint8_t sequence = 0;
uint8_t counter = 0;
~Session() {
if (encoder != nullptr) {
opus_encoder_destroy(encoder);
}
if (hid != nullptr) {
SDL_hid_close(hid);
}
}
bool CreateEncoder() {
if (encoder != nullptr) {
return true;
}
int error = OPUS_OK;
encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &error);
if (encoder == nullptr || error != OPUS_OK) {
return false;
}
if (opus_encoder_ctl(encoder, OPUS_SET_BITRATE(OPUS_BYTES * 8 * 100)) == OPUS_OK &&
opus_encoder_ctl(encoder, OPUS_SET_VBR(0)) == OPUS_OK &&
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(0)) == OPUS_OK) {
return true;
}
opus_encoder_destroy(encoder);
encoder = nullptr;
return false;
}
};
} // namespace
struct Stream {
std::shared_ptr<Session> session;
SDL_AudioStream* audio = nullptr;
uint32_t freq = 0;
bool speaker = false;
};
namespace {
template <size_t N>
void Mix(SDL_AudioStream* stream, std::array<float, N>& mixed) {
std::array<float, N> pcm {};
const int bytes = SDL_GetAudioStreamData(stream, pcm.data(), sizeof(pcm));
for (int i = 0; i < std::max(0, bytes) / static_cast<int>(sizeof(float)); i++) {
mixed[i] += pcm[i];
}
}
struct PendingReport {
std::shared_ptr<Session> session;
uint64_t generation;
std::array<uint8_t, REPORT_BYTES> data {};
std::array<float, 2 * OPUS_FRAMES * 2> audio {};
bool speaker = false;
};
bool PrepareReport(Session& session, PendingReport& pending, uint64_t now) {
bool has_data = false;
bool ready = false;
bool speaker_ready = false;
for (const auto* stream: session.streams) {
const bool available = SDL_GetAudioStreamAvailable(stream->audio) > 0;
// Both payloads cover 64 / 3000 seconds. Measure input duration so SDL's
// resampler lookahead does not make a complete block appear partial.
const auto frames =
(static_cast<uint64_t>(stream->freq) * (HAPTIC_REPORT_FLOATS / 2) + HAPTIC_RATE - 1) /
HAPTIC_RATE;
const bool full = available && SDL_GetAudioStreamQueued(stream->audio) >=
static_cast<int>(frames * 2 * sizeof(float));
has_data |= available;
ready |= full;
speaker_ready |= stream->speaker && full;
pending.speaker |= stream->speaker;
}
if (!has_data) {
session.pending_deadline = 0;
return false;
}
if (!ready || (!speaker_ready && (pending.speaker || session.next_send == 0))) {
// Accumulate short blocks for at most one report period. A full stream
// must not wait on a partial peer; only briefly coalesce haptics with
// speaker audio arriving in the same emulated batch.
const auto deadline = now + (ready ? COALESCE_NS : PERIOD_NS);
session.pending_deadline =
session.pending_deadline == 0 ? deadline : std::min(session.pending_deadline, deadline);
if (now < session.pending_deadline) {
return false;
}
}
session.pending_deadline = 0;
std::array<float, HAPTIC_REPORT_FLOATS> haptics {};
for (auto* stream: session.streams) {
if (stream->speaker) {
Mix(stream->audio, pending.audio);
} else {
Mix(stream->audio, haptics);
}
}
auto& report = pending.data;
report[0] = 0x39;
report[1] = static_cast<uint8_t>(session.sequence << 4);
session.sequence = (session.sequence + 1) & 0x0f;
report[2] = 0x91;
report[3] = 6;
report[4] = 0x7e;
std::fill_n(report.begin() + 5, 4, 48);
session.counter += 2;
report[9] = session.counter;
report[10] = 0xd2;
report[11] = 64;
for (size_t i = 0; i < haptics.size(); i++) {
const auto value = std::isfinite(haptics[i]) ? haptics[i] : 0.0f;
report[12 + i] = static_cast<uint8_t>(
static_cast<int>(std::lround(std::clamp(value, -1.0f, 1.0f) * 127)));
}
if (pending.speaker) {
report[140] = 0xd3;
report[141] = OPUS_BYTES;
}
return true;
}
bool EncodeReport(PendingReport& pending) {
if (!pending.session->active || pending.session->failed) {
return false;
}
if (pending.speaker) {
for (auto& sample: pending.audio) {
sample = std::isfinite(sample) ? std::clamp(sample, -1.0f, 1.0f) : 0.0f;
}
for (int packet = 0; packet < 2; packet++) {
if (opus_encode_float(pending.session->encoder,
pending.audio.data() + packet * OPUS_FRAMES * 2, OPUS_FRAMES,
pending.data.data() + 142 + packet * OPUS_BYTES,
OPUS_BYTES) != OPUS_BYTES) {
pending.session->failed = true;
return false;
}
}
}
FinishReport(pending.data);
return true;
}
std::vector<PendingReport> Prepare(uint64_t now) {
std::vector<PendingReport> pending;
{
std::lock_guard lock(g_mutex);
for (auto& [id, session]: g_sessions) {
if (session->failed || now < session->next_send) {
continue;
}
PendingReport report {session, session->generation.load()};
if (PrepareReport(*session, report, now)) {
pending.push_back(std::move(report));
// Regain timing after a late HID write without sending reports in a
// burst. Reset the cadence only on the first report.
session->next_send = session->next_send == 0
? now + PERIOD_NS
: std::max(session->next_send + PERIOD_NS,
now + PERIOD_NS / 2);
}
}
}
// Opus encoding can take milliseconds; the emulation audio thread must be able
// to queue its next block while this work runs.
pending.erase(std::remove_if(pending.begin(), pending.end(),
[](PendingReport& report) { return !EncodeReport(report); }),
pending.end());
return pending;
}
void Send(const std::vector<PendingReport>& pending) {
for (const auto& report: pending) {
auto& session = *report.session;
std::lock_guard io_lock(session.io_mutex);
if (!session.active || session.failed || report.generation != session.generation) {
continue;
}
if (SDL_hid_write(session.hid, report.data.data(), report.data.size()) !=
static_cast<int>(report.data.size())) {
session.failed = true;
LOGF("DualSenseBluetooth: audio HID write failed: %s\n", SDL_GetError());
}
}
}
int SDLCALL Sender(void*) {
std::unique_lock lock(g_mutex);
while (!g_stopping) {
auto tick = g_tick;
lock.unlock();
auto pending = Prepare(SDL_GetTicksNS());
Send(pending);
if (tick != nullptr) {
tick();
}
lock.lock();
if (g_sessions.empty()) {
g_wake.wait(lock, [] { return g_stopping || !g_sessions.empty(); });
} else {
const auto now = SDL_GetTicksNS();
uint64_t wait_ns = 10000000; // Check rumble expiry at most 10 ms late.
for (const auto& [id, session]: g_sessions) {
if (session->next_send > now) {
wait_ns = std::min(wait_ns, session->next_send - now);
}
if (session->pending_deadline > now) {
wait_ns = std::min(wait_ns, session->pending_deadline - now);
}
}
g_wake.wait_for(lock, std::chrono::nanoseconds(wait_ns));
}
}
return 0;
}
} // namespace
Stream* Open(uint32_t freq, bool speaker, int controller, void (*tick)()) {
if (freq == 0 || freq > 192000) {
return nullptr;
}
std::lock_guard lock(g_mutex);
if (g_stopping) {
return nullptr;
}
auto it = g_sessions.find(controller);
std::shared_ptr<Session> session;
if (it != g_sessions.end() && !it->second->failed) {
session = it->second;
} else {
session = std::make_shared<Session>();
session->hid = OpenDevice(controller);
if (session->hid == nullptr) {
return nullptr;
}
}
if (speaker && !session->CreateEncoder()) {
return nullptr;
}
const SDL_AudioSpec source {SDL_AUDIO_F32, 2, static_cast<int>(freq)};
const SDL_AudioSpec target {SDL_AUDIO_F32, 2, speaker ? SPEAKER_RATE : HAPTIC_RATE};
auto* audio = SDL_CreateAudioStream(&source, &target);
if (audio == nullptr) {
return nullptr;
}
if (g_thread == nullptr) {
g_thread = SDL_CreateThread(Sender, "DualSense Bluetooth audio", nullptr);
if (g_thread == nullptr) {
SDL_DestroyAudioStream(audio);
return nullptr;
}
}
g_tick = tick;
auto* stream = new Stream {session, audio, freq, speaker};
session->streams.push_back(stream);
session->pending_deadline = 0;
g_sessions[controller] = session;
g_wake.notify_one();
return stream;
}
uint64_t Queue(Stream* stream, const float* stereo, uint32_t frames) {
if (stream == nullptr || stereo == nullptr || frames == 0) {
return 0;
}
std::lock_guard lock(g_mutex);
if (!stream->session->active || stream->session->failed) {
return 0;
}
const uint32_t max_frames = stream->freq * MAX_QUEUE_MS / 1000;
if (frames > max_frames) {
stereo += static_cast<size_t>(frames - max_frames) * 2;
frames = max_frames;
}
int queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
const int bytes = static_cast<int>(frames * 2 * sizeof(float));
if (queued + bytes > static_cast<int>(max_frames * 2 * sizeof(float))) {
// Discard complete old report slots, preserving the newest queued audio.
// Clearing the entire stream here creates an audible gap after a brief
// Windows Bluetooth write stall.
std::array<float, 2 * OPUS_FRAMES * 2> discarded {};
const int discard_bytes = stream->speaker
? static_cast<int>(sizeof(discarded))
: static_cast<int>(HAPTIC_REPORT_FLOATS * sizeof(float));
const int target = static_cast<int>(max_frames * 2 * sizeof(float)) - bytes;
while (queued > target) {
if (SDL_GetAudioStreamData(stream->audio, discarded.data(), discard_bytes) <= 0) {
SDL_ClearAudioStream(stream->audio);
break;
}
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
}
}
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
if (!SDL_PutAudioStreamData(stream->audio, stereo, bytes)) {
return 0;
}
g_wake.notify_one();
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * 2 * sizeof(float));
}
void Close(Stream* stream) {
if (stream == nullptr) {
return;
}
auto session = stream->session;
{
std::lock_guard lock(g_mutex);
auto& streams = session->streams;
streams.erase(std::remove(streams.begin(), streams.end(), stream), streams.end());
session->generation++;
session->pending_deadline = 0;
if (streams.empty()) {
session->active = false;
for (auto it = g_sessions.begin(); it != g_sessions.end(); ++it) {
if (it->second == session) {
g_sessions.erase(it);
break;
}
}
}
SDL_DestroyAudioStream(stream->audio);
delete stream;
}
// Drain a write already in flight before the caller changes speaker routing.
std::lock_guard io_lock(session->io_mutex);
}
void Shutdown() {
SDL_Thread* thread;
{
std::lock_guard lock(g_mutex);
g_stopping = true;
for (auto& [id, session]: g_sessions) {
session->active = false;
}
thread = g_thread;
g_wake.notify_one();
}
if (thread != nullptr) {
SDL_WaitThread(thread, nullptr);
}
std::lock_guard lock(g_mutex);
g_sessions.clear();
g_thread = nullptr;
g_tick = nullptr;
g_stopping = false;
}
} // namespace Libs::Controller::DualSenseBluetooth
+14
View File
@@ -0,0 +1,14 @@
#ifndef KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
#define KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
#include <cstdint>
namespace Libs::Controller::DualSenseBluetooth {
struct Stream;
Stream* Open(uint32_t frequency, bool speaker, int controller, void (*tick)());
uint64_t Queue(Stream* stream, const float* samples, uint32_t frames);
void Close(Stream* stream);
void Shutdown();
} // namespace Libs::Controller::DualSenseBluetooth
#endif
+140 -58
View File
@@ -2,6 +2,7 @@
#include "common/logging/log.h"
#include "common/threads.h"
#include "libs/dualSenseBluetooth.h"
#include <SDL3/SDL.h>
#include <algorithm>
@@ -13,18 +14,20 @@
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
bool speaker = false;
int controller = -1;
SDL_AudioStream* sdl = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
uint32_t freq = 0;
bool speaker = false;
int controller = -1;
SDL_AudioStream* sdl = nullptr;
DualSenseBluetooth::Stream* bluetooth = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
};
namespace {
constexpr int FRAME_BYTES = 4 * sizeof(float);
constexpr int FRAME_BYTES = 4 * sizeof(float);
constexpr uint32_t MAX_QUEUE_MS = 80;
Common::Mutex g_mutex;
int g_controller = -1;
int g_haptics_streams = 0;
@@ -107,11 +110,22 @@ void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
}
}
void UpdateBluetoothRumble() {
UpdateRumble(nullptr, nullptr, 0, 0);
}
void CloseDevice(Stream* stream) {
const bool had_device = stream->sdl != nullptr || stream->bluetooth != nullptr;
if (stream->bluetooth != nullptr) {
DualSenseBluetooth::Close(stream->bluetooth);
stream->bluetooth = nullptr;
}
if (stream->sdl != nullptr) {
// Destroy outside g_mutex: SDL waits for any running get callback.
SDL_DestroyAudioStream(stream->sdl);
stream->sdl = nullptr;
}
if (had_device) {
Common::LockGuard lock(g_mutex);
if (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
@@ -127,26 +141,34 @@ void CloseDevice(Stream* stream) {
}
}
bool IsWireless(int controller) {
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
const bool wireless =
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
SDL_UnlockJoysticks();
return wireless;
}
bool CanUseDevice(int controller) {
// SDL exposes no portable association between a gamepad and its USB audio endpoint.
// Require one wired DualSense; guessing with multiple pads could play on the wrong one.
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
bool usable =
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED;
int count = 0;
auto* pads = SDL_GetGamepads(&count);
int dualsense_count = 0;
bool found = false;
int count = 0;
auto* pads = SDL_GetGamepads(&count);
int wired_dualsense_count = 0;
bool found = false;
for (int i = 0; i < count; i++) {
if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) {
dualsense_count++;
auto* candidate = SDL_GetGamepadFromID(pads[i]);
if (candidate != nullptr && SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5 &&
SDL_GetGamepadConnectionState(candidate) == SDL_JOYSTICK_CONNECTION_WIRED) {
wired_dualsense_count++;
found |= pads[i] == static_cast<SDL_JoystickID>(controller);
}
}
SDL_free(pads);
SDL_UnlockJoysticks();
return usable && found && dualsense_count == 1;
return found && wired_dualsense_count == 1;
}
SDL_AudioDeviceID FindDevice() {
@@ -169,32 +191,42 @@ SDL_AudioDeviceID FindDevice() {
return device;
}
void RefreshDevice(Stream* stream, int controller) {
if (stream->controller != controller) {
void RefreshDevice(Stream* stream, int controller, bool wireless) {
if (stream->controller != controller || (wireless && stream->sdl != nullptr) ||
(!wireless && stream->bluetooth != nullptr)) {
CloseDevice(stream);
stream->controller = controller;
stream->next_check = 0;
}
const auto now = SDL_GetTicks();
if (now < stream->next_check) {
if (stream->bluetooth != nullptr || now < stream->next_check) {
return;
}
stream->next_check = now + 2000;
const auto device = FindDevice();
const auto device = wireless ? 0 : FindDevice();
if (stream->device == device && stream->sdl != nullptr) {
return;
}
CloseDevice(stream);
stream->device = device;
if (device == 0) {
if (!wireless && device == 0) {
return;
}
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
SDL_AudioSpec actual {};
bool ready = sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
SDL_AudioStream* sdl = nullptr;
DualSenseBluetooth::Stream* bluetooth = nullptr;
bool ready;
if (wireless) {
bluetooth = DualSenseBluetooth::Open(stream->freq, stream->speaker, controller,
UpdateBluetoothRumble);
ready = bluetooth != nullptr;
} else {
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
SDL_AudioSpec actual {};
sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
ready = sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
}
if (ready) {
Common::LockGuard lock(g_mutex);
if (stream->speaker) {
@@ -207,14 +239,56 @@ void RefreshDevice(Stream* stream, int controller) {
}
}
if (ready) {
stream->sdl = sdl;
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
stream->sdl = sdl;
stream->bluetooth = bluetooth;
if (!wireless) {
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
}
} else {
DualSenseBluetooth::Close(bluetooth);
SDL_DestroyAudioStream(sdl);
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
if (!wireless) {
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
}
}
}
uint64_t QueueUsbAudio(Stream* stream, uint32_t frames) {
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
const int bytes = static_cast<int>(frames * FRAME_BYTES);
const int max_bytes =
static_cast<int>(static_cast<uint64_t>(stream->freq) * FRAME_BYTES * MAX_QUEUE_MS / 1000);
if (queued + bytes > max_bytes) {
// Keep the newest sound after a brief device stall. Clearing the entire
// stream produces a gap that is much longer than one audio block.
SDL_AudioSpec target {};
std::array<uint8_t, 16384> discarded {};
if (SDL_GetAudioStreamFormat(stream->sdl, nullptr, &target)) {
const int frame_bytes = SDL_AUDIO_FRAMESIZE(target);
if (frame_bytes > 0) {
const int chunk_frames =
std::min<int>(static_cast<int>(discarded.size()) / frame_bytes,
static_cast<int>((static_cast<uint64_t>(frames) * target.freq +
stream->freq - 1) /
stream->freq));
while (queued + bytes > max_bytes && chunk_frames > 0 &&
SDL_GetAudioStreamData(stream->sdl, discarded.data(),
chunk_frames * frame_bytes) > 0) {
queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
}
}
}
if (queued + bytes > max_bytes) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
return 0;
}
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
}
} // namespace
Stream* Open(uint32_t freq, bool speaker) {
@@ -232,18 +306,30 @@ void Close(Stream* stream) {
}
}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && stream->bluetooth != nullptr;
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
if (!CanUseDevice(controller)) {
const auto max_frames =
static_cast<uint32_t>(static_cast<uint64_t>(stream->freq) * MAX_QUEUE_MS / 1000);
const auto first_frame = frames > max_frames ? frames - max_frames : 0;
frames -= first_frame;
if (frames == 0) {
return 0;
}
const bool wireless = IsWireless(controller);
if (!wireless && !CanUseDevice(controller)) {
CloseDevice(stream);
stream->next_check = 0; // Look for the device as soon as a DualSense is active again.
return 0;
}
RefreshDevice(stream, controller);
if (stream->sdl == nullptr) {
RefreshDevice(stream, controller, wireless);
if (stream->sdl == nullptr && stream->bluetooth == nullptr) {
return 0;
}
if (stream->speaker) {
@@ -257,47 +343,42 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
return 0;
}
}
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
const uint32_t output_channels = wireless ? 2 : 4;
const uint32_t channel_offset = wireless || stream->speaker ? 0 : 2;
stream->buffer.assign(static_cast<size_t>(frames) * output_channels, 0.0f);
bool audible = false;
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < 2; ch++) {
const auto src_ch = channels == 1 ? 0 : ch;
const auto index = static_cast<size_t>(frame) * channels + src_ch;
const auto index = (static_cast<size_t>(first_frame) + frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f;
// The speaker plays the right front channel; the actuators play the back ones.
stream->buffer[static_cast<size_t>(frame) * 4 + ch + (stream->speaker ? 0 : 2)] = value;
audible |= std::fabs(value) > 1.0f / 1024;
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
value;
audible |= std::isfinite(value) && std::fabs(value) > 1.0f / 1024;
}
}
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
if (queued > static_cast<int>(stream->freq * FRAME_BYTES * 80 / 1000)) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
const int bytes = static_cast<int>(frames * FRAME_BYTES);
const auto queued_us =
static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
wireless ? DualSenseBluetooth::Queue(stream->bluetooth, stream->buffer.data(), frames)
: QueueUsbAudio(stream, frames);
if (queued_us == 0) {
CloseDevice(stream);
stream->next_check = SDL_GetTicks() + 2000;
return 0;
}
if (audible && !stream->speaker) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return 0;
}
SelectController(controller);
const auto playback_now = SDL_GetTicks();
const bool starting = g_haptics_until <= playback_now;
const auto now = SDL_GetTicks();
const bool starting = g_haptics_until <= now;
g_haptics_until =
std::max(g_haptics_until, playback_now + std::max<uint64_t>(queued_us / 1000, 250));
std::max(g_haptics_until, now + std::max<uint64_t>(queued_us / 1000, 250));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
return 0;
}
return queued_us;
}
@@ -315,6 +396,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
}
void Shutdown() {
DualSenseBluetooth::Shutdown();
Common::LockGuard lock(g_mutex);
SelectController(-1);
SelectSpeaker(-1);
+4 -2
View File
@@ -11,9 +11,11 @@ struct Stream;
// ports on the actuators.
Stream* Open(uint32_t freq, bool speaker);
void Close(Stream* stream);
// True while this stream is routed through the asynchronous Bluetooth HID sender.
bool UsesBluetooth(const Stream* stream);
// Returns how long the queued audio still plays, in microseconds, or 0 when no DualSense took it
// (the active pad has no usable USB audio device or its speaker cannot be routed).
// Multiple DualSenses or matching audio endpoints are ambiguous and are not selected.
// (the active pad has no usable controller audio device or its speaker cannot be routed).
// Ambiguous USB or Bluetooth endpoints are not selected.
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume);
// Returns false for other gamepad types, which retain the normal rumble path.