audio: play DualSense haptics from vibration audio ports (#843)

* audio: play DualSense haptics from vibration audio ports

Games drive the DualSense's actuators through a vibration audio port, one channel per actuator.
These ports had no device, so their samples were dropped. Over USB the DualSense is a 4-channel
audio device whose back channels feed the actuators; play the port's samples there through SDL.

The device is looked up while the port plays, so the controller can be connected at any time. The
queue keeps about the latency of the regular audio path, with the playback rate nudged by up to 3%
when the port is fed off real time.

While haptic audio plays, hold a DualSense's rumble at zero: SDL disables audio haptics whenever
rumble is non-zero.

Add pad_haptics tests for the sample conversion, device-name matching and rate correction.

* Simplify DualSense audio haptics and restore idle rumble

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Devran Cosmo Uenal
2026-09-26 22:37:27 +02:00
committed by GitHub
parent 81f312638f
commit a22a9d9348
7 changed files with 588 additions and 3 deletions
+18 -3
View File
@@ -7,6 +7,8 @@
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/audio_internal.h"
#include "libs/controller.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -105,7 +107,8 @@ private:
int channels_num = 0;
int volume[12] = {};
SDL_AudioStream* stream = nullptr;
SDL_AudioStream* stream = nullptr;
Controller::DualSenseHaptics::Stream* haptics = nullptr;
};
struct PortIn {
@@ -268,6 +271,8 @@ bool Audio::OpenSdlDevice(PortOut* port) {
void Audio::CloseSdlDevice(PortOut* port) {
EXIT_IF(port == nullptr);
Controller::DualSenseHaptics::Close(port->haptics);
port->haptics = nullptr;
if (port->stream != nullptr) {
SDL_DestroyAudioStream(port->stream);
@@ -435,7 +440,9 @@ 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) {
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
port.haptics = Controller::DualSenseHaptics::Open(freq);
} else {
OpenSdlDevice(&port);
}
@@ -547,7 +554,15 @@ 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()];
QueueSdlAudio(&port, params[i].data, blocking);
if (port.type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
// Haptics never pace output; keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
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 {
QueueSdlAudio(&port, params[i].data, blocking);
}
}
for (uint32_t i = 0; i < num; i++) {
+15
View File
@@ -8,6 +8,7 @@
#include "common/stringUtils.h"
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/padData.h"
@@ -113,6 +114,7 @@ public:
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void ReadState(ControllerState* state, bool* flag, int* count);
@@ -518,6 +520,7 @@ void GameController::ResetInputState() {
void GameController::ReleaseHostPads() {
Common::LockGuard lock(m_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto id: m_connected_ids) {
@@ -551,6 +554,9 @@ void GameController::ReleaseHostPads() {
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
Common::LockGuard lock(m_mutex);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) {
return;
}
if (m_active_id == HOST_INPUT_CONTROLLER_ID) {
return;
@@ -568,6 +574,11 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
}
}
int GameController::GetActiveControllerId() {
Common::LockGuard lock(m_mutex);
return m_active_id;
}
void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
Common::LockGuard lock(m_mutex);
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
@@ -692,6 +703,10 @@ void ResetInputState() {
g_controller->ResetInputState();
}
int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
+1
View File
@@ -73,6 +73,7 @@ void SetRightStick(int id, int x, int y);
void SetTouchPad(int id, int finger, bool down, float x, float y);
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ResetInputState();
int GetActiveControllerId();
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
+205
View File
@@ -0,0 +1,205 @@
#include "libs/dualSenseHaptics.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <vector>
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
SDL_AudioStream* sdl = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
};
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;
void ApplyRumble() { // 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;
(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);
}
SDL_UnlockJoysticks();
}
void SelectController(int controller) {
if (g_controller != controller) {
g_haptics_until = 0;
ApplyRumble();
g_controller = controller;
g_large_motor = g_small_motor = 0;
g_rumble_until = 0;
}
}
void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
Common::LockGuard lock(g_mutex);
if (g_haptics_until != 0 && SDL_GetTicks() >= g_haptics_until) {
g_haptics_until = 0;
ApplyRumble();
}
}
void CloseDevice(Stream* stream) {
if (stream->sdl != nullptr) {
// Destroy outside g_mutex: SDL waits for any running get callback.
SDL_DestroyAudioStream(stream->sdl);
stream->sdl = nullptr;
Common::LockGuard lock(g_mutex);
if (--g_playback_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
ApplyRumble();
}
}
}
SDL_AudioDeviceID FindDevice() {
int count = 0;
auto* devices = SDL_GetAudioPlaybackDevices(&count);
SDL_AudioDeviceID device = 0;
for (int i = 0; i < count && device == 0; 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) {
device = devices[i];
}
}
SDL_free(devices);
return device;
}
} // namespace
Stream* Open(uint32_t freq) {
if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
return nullptr;
}
return new Stream {.freq = freq};
}
void Close(Stream* stream) {
if (stream != nullptr) {
CloseDevice(stream);
delete stream;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
void 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;
}
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
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());
}
}
}
}
if (stream->sdl == nullptr) {
return;
}
stream->buffer.assign(static_cast<size_t>(frames) * 4, 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;
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;
audible |= 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);
if (audible) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return;
}
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));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
}
}
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
return false;
}
Common::LockGuard lock(g_mutex);
SelectController(controller);
g_large_motor = large_motor;
g_small_motor = small_motor;
g_rumble_until = SDL_GetTicks() + 0xffff;
ApplyRumble();
return true;
}
void Shutdown() {
Common::LockGuard lock(g_mutex);
g_rumble_until = 0;
SelectController(-1);
}
} // namespace Libs::Controller::DualSenseHaptics
+20
View File
@@ -0,0 +1,20 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#define EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#include <cstdint>
namespace Libs::Controller::DualSenseHaptics {
struct Stream;
Stream* Open(uint32_t freq);
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 false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
void Shutdown();
} // namespace Libs::Controller::DualSenseHaptics
#endif // EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_