mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 03:16:54 +02:00
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:
+140
-58
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user