mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 10:56:58 +02:00
audio: play pad speaker ports on the DualSense speaker (#868)
* audio: play pad speaker ports on the DualSense speaker Route the right channel to the speaker with the audio-control values the Linux hid-playstation driver uses without headphones, and fall back to the main output while no DualSense audio device is active. Also stop the previous DualSense's rumble when another DualSense becomes active. * audio: harden DualSense speaker routing and fallback --------- Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
committed by
GitHub
parent
a107d1e333
commit
22ff4693c5
+22
-10
@@ -32,6 +32,7 @@ constexpr int AUDIO_OUT_PORT_TYPE_AUDIO3D = 126;
|
||||
constexpr int AUDIO_OUT_PORT_TYPE_AUX = 127;
|
||||
|
||||
constexpr uint32_t AUDIO_OUT_PARAM_FORMAT_MASK = 0x000000ffu;
|
||||
constexpr uint64_t AUDIO_OUT_TARGET_LATENCY_US = 40000;
|
||||
|
||||
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
|
||||
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
|
||||
@@ -368,11 +369,11 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
||||
|
||||
uint32_t min_queued_size = 0;
|
||||
if (blocking) {
|
||||
constexpr uint64_t target_latency_us = 40000;
|
||||
const auto buffer_us = port->freq != 0 ? (1000000ULL * port->samples_num) / port->freq : 0;
|
||||
const auto buffers =
|
||||
buffer_us != 0 ? static_cast<uint32_t>((target_latency_us + buffer_us - 1) / buffer_us)
|
||||
: 2u;
|
||||
buffer_us != 0
|
||||
? static_cast<uint32_t>((AUDIO_OUT_TARGET_LATENCY_US + buffer_us - 1) / buffer_us)
|
||||
: 2u;
|
||||
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
|
||||
const auto wait_start = LibKernel::KernelGetProcessTime();
|
||||
auto queued = SDL_GetAudioStreamQueued(port->stream);
|
||||
@@ -440,11 +441,14 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For
|
||||
port.volume[i] = 32768;
|
||||
}
|
||||
|
||||
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
|
||||
port.haptics = Controller::DualSenseHaptics::Open(freq);
|
||||
} else {
|
||||
// Pad speaker ports keep the main output too; it plays them whenever no DualSense does.
|
||||
if (type != AUDIO_OUT_PORT_TYPE_VIBRATION) {
|
||||
OpenSdlDevice(&port);
|
||||
}
|
||||
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_PADSPK) {
|
||||
port.haptics =
|
||||
Controller::DualSenseHaptics::Open(freq, type == AUDIO_OUT_PORT_TYPE_PADSPK);
|
||||
}
|
||||
|
||||
return Id::Create(id);
|
||||
}
|
||||
@@ -554,14 +558,22 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
|
||||
for (uint32_t i = 0; i < num; i++) {
|
||||
auto& port = m_out_ports[params[i].handle.GetId()];
|
||||
|
||||
if (port.type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
|
||||
// Haptics never pace output; keep the stream alive against close and volume changes.
|
||||
uint64_t controller_queued_us = 0;
|
||||
if (port.haptics != nullptr) {
|
||||
// Keep the stream alive against close and volume changes.
|
||||
Common::LockGuard lock(m_mutex);
|
||||
Controller::DualSenseHaptics::Queue(
|
||||
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);
|
||||
} else {
|
||||
}
|
||||
if (controller_queued_us == 0) {
|
||||
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
|
||||
// output instead, and a vibration port has none.
|
||||
QueueSdlAudio(&port, params[i].data, blocking);
|
||||
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
|
||||
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
|
||||
// Vibration never paces output; the speaker is audible, so pace it like other ports.
|
||||
Common::Thread::SleepMicro(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+171
-53
@@ -5,13 +5,17 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
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;
|
||||
@@ -20,19 +24,26 @@ struct Stream {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int FRAME_BYTES = 4 * sizeof(float);
|
||||
Common::Mutex g_mutex;
|
||||
int g_controller = -1;
|
||||
int g_playback_streams = 0;
|
||||
uint8_t g_large_motor = 0, g_small_motor = 0;
|
||||
uint64_t g_rumble_until = 0, g_haptics_until = 0;
|
||||
constexpr int FRAME_BYTES = 4 * sizeof(float);
|
||||
Common::Mutex g_mutex;
|
||||
int g_controller = -1;
|
||||
int g_haptics_streams = 0;
|
||||
uint8_t g_large_motor = 0, g_small_motor = 0;
|
||||
uint64_t g_rumble_until = 0, g_haptics_until = 0;
|
||||
int g_speaker_controller = -1;
|
||||
std::map<int, int> g_speaker_streams;
|
||||
|
||||
void ApplyRumble() { // Caller holds g_mutex; no calls into Audio or Controller.
|
||||
void ApplyRumble(bool force = false) { // Caller holds g_mutex; no calls into Audio or Controller.
|
||||
SDL_LockJoysticks();
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(g_controller));
|
||||
pad != nullptr) {
|
||||
const auto now = SDL_GetTicks();
|
||||
const bool muted = g_haptics_until > now || g_rumble_until <= now;
|
||||
if (force && !muted) {
|
||||
// SDL caches motor strengths. Force a report after raw speaker routing changed the
|
||||
// controller's motor mode without changing that cache.
|
||||
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
|
||||
}
|
||||
(void)SDL_RumbleGamepad(
|
||||
pad, muted ? 0 : g_large_motor * 0x101U, muted ? 0 : g_small_motor * 0x101U,
|
||||
g_rumble_until > now ? static_cast<uint32_t>(g_rumble_until - now) : 0);
|
||||
@@ -42,14 +53,52 @@ void ApplyRumble() { // Caller holds g_mutex; no calls into Audio or Controller.
|
||||
|
||||
void SelectController(int controller) {
|
||||
if (g_controller != controller) {
|
||||
g_haptics_until = 0;
|
||||
g_haptics_until = g_rumble_until = 0;
|
||||
ApplyRumble();
|
||||
g_controller = controller;
|
||||
g_large_motor = g_small_motor = 0;
|
||||
g_rumble_until = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool RouteSpeaker(int controller, bool speaker) { // Caller holds g_mutex.
|
||||
// Route the right channel to the speaker and mute the headphones with the values Linux uses
|
||||
// without headphones (output report 0x02). Plugged-in headphones are not checked.
|
||||
std::array<uint8_t, 38> report {};
|
||||
report[0] = 0x80; // Audio control valid
|
||||
if (speaker) {
|
||||
report[0] |= 0x20; // Speaker volume valid
|
||||
report[1] = 0x80; // Audio control 2 valid
|
||||
report[5] = 0x64; // Speaker volume
|
||||
report[7] = 0x30; // Output path: right channel to the speaker, headphones muted
|
||||
report[37] = 0x02; // Speaker preamp +6 dB
|
||||
}
|
||||
bool routed = false;
|
||||
SDL_LockJoysticks();
|
||||
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller)); pad != nullptr) {
|
||||
routed = SDL_SendGamepadEffect(pad, report.data(), static_cast<int>(report.size()));
|
||||
}
|
||||
SDL_UnlockJoysticks();
|
||||
if (routed && controller == g_controller) {
|
||||
ApplyRumble(true);
|
||||
}
|
||||
return routed;
|
||||
}
|
||||
|
||||
bool SelectSpeaker(int controller) { // Caller holds g_mutex; -1 restores headphone routing.
|
||||
if (g_speaker_controller == controller) {
|
||||
return true;
|
||||
}
|
||||
if (g_speaker_controller != -1) {
|
||||
RouteSpeaker(g_speaker_controller, false);
|
||||
}
|
||||
g_speaker_controller = -1;
|
||||
if (controller != -1 && !RouteSpeaker(controller, true)) {
|
||||
return false;
|
||||
}
|
||||
g_speaker_controller = controller;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (g_haptics_until != 0 && SDL_GetTicks() >= g_haptics_until) {
|
||||
@@ -64,22 +113,55 @@ void CloseDevice(Stream* stream) {
|
||||
SDL_DestroyAudioStream(stream->sdl);
|
||||
stream->sdl = nullptr;
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (--g_playback_streams == 0 && g_haptics_until != 0) {
|
||||
if (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) {
|
||||
g_haptics_until = 0;
|
||||
ApplyRumble();
|
||||
}
|
||||
// Route back to the headphones once no speaker port plays on the controller.
|
||||
if (stream->speaker && --g_speaker_streams.at(stream->controller) == 0) {
|
||||
g_speaker_streams.erase(stream->controller);
|
||||
if (g_speaker_controller == stream->controller) {
|
||||
SelectSpeaker(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) {
|
||||
dualsense_count++;
|
||||
found |= pads[i] == static_cast<SDL_JoystickID>(controller);
|
||||
}
|
||||
}
|
||||
SDL_free(pads);
|
||||
SDL_UnlockJoysticks();
|
||||
return usable && found && dualsense_count == 1;
|
||||
}
|
||||
|
||||
SDL_AudioDeviceID FindDevice() {
|
||||
int count = 0;
|
||||
auto* devices = SDL_GetAudioPlaybackDevices(&count);
|
||||
SDL_AudioDeviceID device = 0;
|
||||
for (int i = 0; i < count && device == 0; i++) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* name = SDL_GetAudioDeviceName(devices[i]);
|
||||
SDL_AudioSpec spec {};
|
||||
if (name != nullptr && SDL_strcasestr(name, "DualSense") != nullptr &&
|
||||
SDL_GetAudioDeviceFormat(devices[i], &spec, nullptr) && spec.channels == 4) {
|
||||
if (device != 0) {
|
||||
device = 0; // Multiple matching endpoints cannot be associated reliably either.
|
||||
break;
|
||||
}
|
||||
device = devices[i];
|
||||
}
|
||||
}
|
||||
@@ -87,13 +169,59 @@ SDL_AudioDeviceID FindDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
void RefreshDevice(Stream* stream, int controller) {
|
||||
if (stream->controller != controller) {
|
||||
CloseDevice(stream);
|
||||
stream->controller = controller;
|
||||
stream->next_check = 0;
|
||||
}
|
||||
const auto now = SDL_GetTicks();
|
||||
if (now < stream->next_check) {
|
||||
return;
|
||||
}
|
||||
stream->next_check = now + 2000;
|
||||
const auto device = FindDevice();
|
||||
if (stream->device == device && stream->sdl != nullptr) {
|
||||
return;
|
||||
}
|
||||
CloseDevice(stream);
|
||||
stream->device = device;
|
||||
if (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);
|
||||
if (ready) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (stream->speaker) {
|
||||
ready = SelectSpeaker(controller);
|
||||
if (ready) {
|
||||
g_speaker_streams[controller]++;
|
||||
}
|
||||
} else {
|
||||
g_haptics_streams++;
|
||||
}
|
||||
}
|
||||
if (ready) {
|
||||
stream->sdl = sdl;
|
||||
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
|
||||
} else {
|
||||
SDL_DestroyAudioStream(sdl);
|
||||
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Stream* Open(uint32_t freq) {
|
||||
Stream* Open(uint32_t freq, bool speaker) {
|
||||
if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
return nullptr;
|
||||
}
|
||||
return new Stream {.freq = freq};
|
||||
return new Stream {.freq = freq, .speaker = speaker};
|
||||
}
|
||||
|
||||
void Close(Stream* stream) {
|
||||
@@ -104,44 +232,30 @@ void Close(Stream* stream) {
|
||||
}
|
||||
}
|
||||
|
||||
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
|
||||
bool is_float, const int* volume) {
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
|
||||
if (!CanUseDevice(controller)) {
|
||||
CloseDevice(stream);
|
||||
return;
|
||||
}
|
||||
const auto now = SDL_GetTicks();
|
||||
if (now >= stream->next_check) {
|
||||
stream->next_check = now + 2000;
|
||||
const auto device = FindDevice();
|
||||
if (stream->device != device || stream->sdl == nullptr) {
|
||||
CloseDevice(stream);
|
||||
stream->device = device;
|
||||
if (device != 0) {
|
||||
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
|
||||
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
|
||||
SDL_AudioSpec actual {};
|
||||
if (sdl != nullptr &&
|
||||
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
|
||||
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl)) {
|
||||
stream->sdl = sdl;
|
||||
{
|
||||
Common::LockGuard lock(g_mutex);
|
||||
g_playback_streams++;
|
||||
}
|
||||
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
|
||||
} else {
|
||||
SDL_DestroyAudioStream(sdl);
|
||||
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
if (stream->speaker) {
|
||||
bool routed;
|
||||
{
|
||||
Common::LockGuard lock(g_mutex);
|
||||
routed = SelectSpeaker(controller);
|
||||
}
|
||||
if (!routed) {
|
||||
CloseDevice(stream);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
|
||||
bool audible = false;
|
||||
@@ -152,7 +266,8 @@ void Queue(Stream* stream, int controller, const void* data, uint32_t frames, ui
|
||||
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;
|
||||
stream->buffer[static_cast<size_t>(frame) * 4 + ch + 2] = value;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -161,26 +276,29 @@ void Queue(Stream* stream, int controller, const void* data, uint32_t frames, ui
|
||||
SDL_ClearAudioStream(stream->sdl);
|
||||
queued = 0;
|
||||
}
|
||||
const int bytes = static_cast<int>(frames * FRAME_BYTES);
|
||||
if (audible) {
|
||||
const int bytes = static_cast<int>(frames * FRAME_BYTES);
|
||||
const auto queued_us =
|
||||
static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
|
||||
if (audible && !stream->speaker) {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
|
||||
SDL_GAMEPAD_TYPE_PS5) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
SelectController(controller);
|
||||
const auto playback_now = SDL_GetTicks();
|
||||
const bool starting = g_haptics_until <= playback_now;
|
||||
const auto duration = (queued + bytes) * 1000ULL / (stream->freq * FRAME_BYTES);
|
||||
g_haptics_until =
|
||||
std::max(g_haptics_until, playback_now + std::max<uint64_t>(duration, 250));
|
||||
std::max(g_haptics_until, playback_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;
|
||||
}
|
||||
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
|
||||
@@ -198,8 +316,8 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
|
||||
|
||||
void Shutdown() {
|
||||
Common::LockGuard lock(g_mutex);
|
||||
g_rumble_until = 0;
|
||||
SelectController(-1);
|
||||
SelectSpeaker(-1);
|
||||
}
|
||||
|
||||
} // namespace Libs::Controller::DualSenseHaptics
|
||||
|
||||
@@ -7,12 +7,18 @@ namespace Libs::Controller::DualSenseHaptics {
|
||||
|
||||
struct Stream;
|
||||
|
||||
Stream* Open(uint32_t freq);
|
||||
// A speaker stream plays a pad speaker port and routes the speaker to it; the others play vibration
|
||||
// ports on the actuators.
|
||||
Stream* Open(uint32_t freq, bool speaker);
|
||||
void Close(Stream* stream);
|
||||
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
|
||||
bool is_float, const int* volume);
|
||||
// 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.
|
||||
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.
|
||||
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
|
||||
// Also restores the headphone routing.
|
||||
void Shutdown();
|
||||
|
||||
} // namespace Libs::Controller::DualSenseHaptics
|
||||
|
||||
Reference in New Issue
Block a user