mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 02:47:02 +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
+292
-34
@@ -24,15 +24,23 @@ struct Rumble {
|
||||
Uint16 large, small;
|
||||
Uint32 duration;
|
||||
};
|
||||
std::vector<Device> devices;
|
||||
std::vector<SDL_AudioStream*> streams;
|
||||
Rumble rumble {};
|
||||
Uint64 now = 1000;
|
||||
int audio_refs = 0, opens = 0, rumble_calls = 0;
|
||||
bool fail_open = false, fail_resume = false;
|
||||
int actual_channels = 4;
|
||||
SDL_AudioDeviceID opened_device = 0;
|
||||
SDL_AudioSpec opened_spec {};
|
||||
std::vector<Device> devices;
|
||||
std::vector<SDL_AudioStream*> streams;
|
||||
Rumble rumble {};
|
||||
Uint64 now = 1000;
|
||||
int audio_refs = 0, opens = 0, rumble_calls = 0;
|
||||
bool fail_open = false, fail_resume = false;
|
||||
int actual_channels = 4;
|
||||
SDL_AudioDeviceID opened_device = 0;
|
||||
SDL_AudioSpec opened_spec {};
|
||||
std::array<Rumble, 4> pad_rumble {};
|
||||
std::array<std::vector<uint8_t>, 4> effects;
|
||||
std::vector<SDL_JoystickID> connected_pads {1};
|
||||
std::array<Rumble, 4> cached_rumble {};
|
||||
bool fail_effect = false, wireless = false;
|
||||
SDL_AudioStream* default_stream = nullptr;
|
||||
bool fail_default_resume = false;
|
||||
int active_controller = 1;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
@@ -95,10 +103,13 @@ SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID id, const SDL_AudioSpec
|
||||
Check(stream != nullptr && SDL_SetAudioStreamGetCallback(stream, callback, userdata),
|
||||
"real SDL stream/callback creation failed");
|
||||
streams.push_back(stream);
|
||||
if (id == SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK) {
|
||||
default_stream = stream;
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
bool ResumeAudioStreamDevice(SDL_AudioStream*) {
|
||||
return !fail_resume;
|
||||
bool ResumeAudioStreamDevice(SDL_AudioStream* stream) {
|
||||
return !fail_resume && !(stream == default_stream && fail_default_resume);
|
||||
}
|
||||
void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||
if (stream == nullptr) {
|
||||
@@ -107,38 +118,79 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
|
||||
const auto it = std::find(streams.begin(), streams.end(), stream);
|
||||
Check(it != streams.end(), "stream destroyed twice");
|
||||
streams.erase(it);
|
||||
if (stream == default_stream) {
|
||||
default_stream = nullptr;
|
||||
}
|
||||
SDL_DestroyAudioStream(stream);
|
||||
}
|
||||
SDL_AudioDeviceID GetAudioStreamDevice(SDL_AudioStream*) {
|
||||
return 999;
|
||||
}
|
||||
// Pads 1 and 3 are DualSenses.
|
||||
SDL_GamepadType GetGamepadTypeForID(SDL_JoystickID id) {
|
||||
return id == 1 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN;
|
||||
return id == 1 || id == 3 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN;
|
||||
}
|
||||
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
||||
return id == 1 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id)) : nullptr;
|
||||
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
rumble = {large, small, duration};
|
||||
SDL_JoystickID* GetGamepads(int* count) {
|
||||
*count = static_cast<int>(connected_pads.size());
|
||||
auto* ids = static_cast<SDL_JoystickID*>(
|
||||
SDL_malloc((connected_pads.size() + 1) * sizeof(SDL_JoystickID)));
|
||||
std::copy(connected_pads.begin(), connected_pads.end(), ids);
|
||||
ids[connected_pads.size()] = 0;
|
||||
return ids;
|
||||
}
|
||||
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad*) {
|
||||
return wireless ? SDL_JOYSTICK_CONNECTION_WIRELESS : SDL_JOYSTICK_CONNECTION_WIRED;
|
||||
}
|
||||
bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
const auto id = reinterpret_cast<uintptr_t>(pad);
|
||||
// SDL only sends a new report when the strengths change.
|
||||
if (large != cached_rumble[id].large || small != cached_rumble[id].small) {
|
||||
pad_rumble[id] = {large, small, duration};
|
||||
}
|
||||
cached_rumble[id] = {large, small, duration};
|
||||
rumble = pad_rumble[id];
|
||||
rumble.duration = duration;
|
||||
rumble_calls++;
|
||||
return true;
|
||||
}
|
||||
bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
|
||||
if (fail_effect) {
|
||||
return false;
|
||||
}
|
||||
effects[reinterpret_cast<uintptr_t>(pad)].assign(static_cast<const uint8_t*>(data),
|
||||
static_cast<const uint8_t*>(data) + size);
|
||||
// Raw audio-routing reports clear the emulated-rumble mode on the controller,
|
||||
// without updating SDL's cached motor strengths.
|
||||
if ((static_cast<const uint8_t*>(data)[0] & 3) == 0) {
|
||||
pad_rumble[reinterpret_cast<uintptr_t>(pad)] = {};
|
||||
rumble = {};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace Fake
|
||||
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
|
||||
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
|
||||
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
|
||||
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
|
||||
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_RumbleGamepad Fake::RumbleGamepad
|
||||
#define SDL_GetTicks Fake::GetTicks
|
||||
#define SDL_InitSubSystem Fake::InitSubSystem
|
||||
#define SDL_QuitSubSystem Fake::QuitSubSystem
|
||||
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
|
||||
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
|
||||
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
|
||||
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
|
||||
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
|
||||
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
|
||||
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
|
||||
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
|
||||
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
|
||||
#define SDL_RumbleGamepad Fake::RumbleGamepad
|
||||
#define SDL_SendGamepadEffect Fake::SendGamepadEffect
|
||||
#define SDL_GetGamepads Fake::GetGamepads
|
||||
#define SDL_GetGamepadConnectionState Fake::GetGamepadConnectionState
|
||||
#include "libs/dualSenseHaptics.cpp"
|
||||
#include "libs/audio.cpp"
|
||||
#undef SDL_GetTicks
|
||||
#undef SDL_InitSubSystem
|
||||
#undef SDL_QuitSubSystem
|
||||
@@ -152,6 +204,27 @@ bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
|
||||
#undef SDL_GetGamepadTypeForID
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_RumbleGamepad
|
||||
#undef SDL_SendGamepadEffect
|
||||
#undef SDL_GetGamepads
|
||||
#undef SDL_GetGamepadConnectionState
|
||||
|
||||
namespace Libs::Controller {
|
||||
int GetActiveControllerId() {
|
||||
return active_controller;
|
||||
}
|
||||
} // namespace Libs::Controller
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
|
||||
return now * 1000;
|
||||
}
|
||||
} // namespace Libs::LibKernel
|
||||
|
||||
namespace Loader::Timer {
|
||||
double GetTimeMs() {
|
||||
return static_cast<double>(now);
|
||||
}
|
||||
} // namespace Loader::Timer
|
||||
|
||||
namespace {
|
||||
namespace Haptics = Libs::Controller::DualSenseHaptics;
|
||||
@@ -161,9 +234,17 @@ constexpr std::array<float, 4> pcm {0.5f, 0.5f, 0.5f, 0.5f};
|
||||
|
||||
struct Fixture {
|
||||
Fixture() {
|
||||
devices = {{10, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
rumble = {};
|
||||
now = 1000;
|
||||
devices = {{10, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
rumble = {};
|
||||
pad_rumble = {};
|
||||
cached_rumble = {};
|
||||
effects = {};
|
||||
connected_pads = {1};
|
||||
fail_effect = wireless = false;
|
||||
default_stream = nullptr;
|
||||
fail_default_resume = false;
|
||||
active_controller = 1;
|
||||
now = 1000;
|
||||
opens = rumble_calls = 0;
|
||||
fail_open = fail_resume = false;
|
||||
actual_channels = 4;
|
||||
@@ -174,8 +255,8 @@ struct Fixture {
|
||||
}
|
||||
};
|
||||
|
||||
Port Open() {
|
||||
auto* stream = Haptics::Open(48000);
|
||||
Port Open(bool speaker = false) {
|
||||
auto* stream = Haptics::Open(48000, speaker);
|
||||
Check(stream != nullptr, "port open failed");
|
||||
return Port(stream, Haptics::Close);
|
||||
}
|
||||
@@ -237,6 +318,71 @@ void TestDiscoveryAndHotplug() {
|
||||
Check(streams.empty(), "disconnected endpoint retained its stream");
|
||||
}
|
||||
|
||||
void TestSpeaker() {
|
||||
Fixture f;
|
||||
devices = {{10, "DualSense Wireless Controller", 2}};
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
streams.empty(),
|
||||
"speaker played without a quad DualSense device");
|
||||
devices = {{10, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
now += 2001;
|
||||
Check(Haptics::SetVibration(1, 100, 50), "set rumble failed");
|
||||
const std::array<float, 4> stereo {0.5f, -0.25f, 1.0f, 0.0f};
|
||||
Queue(speaker, stereo.data());
|
||||
ExpectPcm({0.5f, -0.25f, 0, 0, 1.0f, 0, 0, 0});
|
||||
Check(rumble.large == 100 * 0x101, "speaker audio stopped the rumble");
|
||||
const auto routed = effects[1];
|
||||
Check(routed.size() == 38 && routed[0] == 0xa0 && routed[1] == 0x80 && routed[5] == 0x64 &&
|
||||
routed[7] == 0x30 && routed[37] == 0x02,
|
||||
"speaker not routed like the Linux driver");
|
||||
Check(Haptics::Queue(speaker.get(), 1, stereo.data(), 2, 2, true, unity.data()) ==
|
||||
2 * 1000000 / 48000,
|
||||
"queued speaker playback time is wrong");
|
||||
auto other = Open(true);
|
||||
connected_pads = {3};
|
||||
Haptics::Queue(other.get(), 3, pcm.data(), 2, 2, true, unity.data());
|
||||
Check(effects[1][0] == 0x80 && effects[1][1] == 0 && effects[1][7] == 0 && effects[3] == routed,
|
||||
"switching pads left the old speaker routed");
|
||||
speaker.reset();
|
||||
Check(effects[3] == routed, "closing one speaker port unrouted the other");
|
||||
other.reset();
|
||||
Check(effects[3][0] == 0x80 && effects[3][7] == 0,
|
||||
"closing the last speaker port left it routed");
|
||||
auto last = Open(true);
|
||||
Haptics::Queue(last.get(), 3, pcm.data(), 2, 2, true, unity.data());
|
||||
Check(effects[3] == routed, "reopened speaker port was not routed");
|
||||
Haptics::Shutdown();
|
||||
Check(effects[3][0] == 0x80 && effects[3][7] == 0, "shutdown left the speaker routed");
|
||||
}
|
||||
|
||||
void TestSpeakerUnplug() {
|
||||
Fixture f;
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) != 0,
|
||||
"speaker did not play on the DualSense");
|
||||
const auto routed = effects[1];
|
||||
// Unplugged: another controller becomes active, and the port must fall back
|
||||
// to the main output.
|
||||
Check(Haptics::Queue(speaker.get(), 2, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
streams.empty(),
|
||||
"unplugged pad kept the speaker port");
|
||||
Check(effects[1][0] == 0x80 && effects[1][7] == 0, "unplugging left the speaker routed");
|
||||
// Plugged back in: the port returns to the controller at once.
|
||||
effects = {};
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) != 0 &&
|
||||
streams.size() == 1 && effects[1] == routed,
|
||||
"replugged pad did not take the speaker back");
|
||||
// The audio endpoint vanishes while a DualSense stays active.
|
||||
devices.clear();
|
||||
now += 2001;
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
streams.empty(),
|
||||
"lost audio endpoint kept the speaker port");
|
||||
Check(effects[1][0] == 0x80 && effects[1][7] == 0,
|
||||
"lost audio endpoint left the speaker routed");
|
||||
}
|
||||
|
||||
void TestFailuresAndBoundedQueue() {
|
||||
Fixture f;
|
||||
auto port = Open();
|
||||
@@ -295,9 +441,23 @@ void TestRumbleLeaseAndDuration() {
|
||||
Check(rumble_calls == calls, "audio pull touched rumble after shutdown");
|
||||
}
|
||||
|
||||
void TestSwitchStopsOldRumble() {
|
||||
Fixture f;
|
||||
auto port = Open();
|
||||
Haptics::SetVibration(1, 100, 50);
|
||||
Queue(port, pcm.data());
|
||||
Check(pad_rumble[1].large == 0, "haptics did not suppress rumble");
|
||||
Haptics::SetVibration(3, 10, 10);
|
||||
Check(pad_rumble[1].large == 0 && pad_rumble[1].small == 0,
|
||||
"switching controllers restarted the old pad's rumble");
|
||||
Check(pad_rumble[3].large == 10 * 0x101, "new pad did not rumble");
|
||||
}
|
||||
|
||||
void TestCloseRestoresRumble() {
|
||||
Fixture f;
|
||||
auto first = Open(), second = Open();
|
||||
auto speaker = Open(true);
|
||||
Queue(speaker, pcm.data());
|
||||
auto first = Open(), second = Open();
|
||||
Haptics::SetVibration(1, 100, 50);
|
||||
Queue(first, pcm.data());
|
||||
Queue(second, pcm.data());
|
||||
@@ -308,14 +468,112 @@ void TestCloseRestoresRumble() {
|
||||
Check(rumble.large == 100 * 0x101 && rumble.small == 50 * 0x101,
|
||||
"closing the final stream left rumble suppressed");
|
||||
}
|
||||
|
||||
void TestSpeakerSwitchAndFailures() {
|
||||
Fixture f;
|
||||
auto speaker = Open(true);
|
||||
auto queue = [&](int controller) {
|
||||
return Haptics::Queue(speaker.get(), controller, pcm.data(), 2, 2, true, unity.data());
|
||||
};
|
||||
fail_effect = true;
|
||||
Check(queue(1) == 0 && streams.empty(), "failed speaker routing suppressed fallback");
|
||||
fail_effect = false;
|
||||
now += 2001;
|
||||
Check(queue(1) != 0, "speaker did not recover after routing failed");
|
||||
connected_pads = {3};
|
||||
devices = {{20, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
Check(queue(3) != 0 && opened_device == 20 && effects[1][7] == 0 && effects[3][7] == 0x30,
|
||||
"persistent speaker port did not follow the new controller immediately");
|
||||
// An idle stream on the old pad must not keep the new pad routed after its
|
||||
// last port closes.
|
||||
connected_pads = {1};
|
||||
auto other = Open(true);
|
||||
Check(Haptics::Queue(other.get(), 1, pcm.data(), 2, 2, true, unity.data()) != 0,
|
||||
"second speaker port failed");
|
||||
other.reset();
|
||||
Check(effects[1][7] == 0, "old controller stream kept the new speaker routed");
|
||||
}
|
||||
|
||||
void TestSpeakerRequiresUnambiguousUsbDevice() {
|
||||
Fixture f;
|
||||
auto speaker = Open(true);
|
||||
auto queue = [&] {
|
||||
return Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data());
|
||||
};
|
||||
wireless = true;
|
||||
Check(queue() == 0 && streams.empty(), "Bluetooth controller stole another pad's USB audio");
|
||||
wireless = false;
|
||||
connected_pads = {1, 3};
|
||||
Check(queue() == 0 && streams.empty(), "multiple DualSenses chose an arbitrary controller");
|
||||
connected_pads = {1};
|
||||
devices.push_back({20, "DualSense Wireless Controller", 4});
|
||||
Check(queue() == 0 && streams.empty(), "multiple audio endpoints chose an arbitrary device");
|
||||
devices.pop_back();
|
||||
now += 2001;
|
||||
Check(queue() != 0, "unambiguous USB device was not recovered");
|
||||
wireless = true;
|
||||
Check(queue() == 0 && streams.empty() && effects[1][7] == 0,
|
||||
"USB to Bluetooth change retained the USB speaker");
|
||||
}
|
||||
|
||||
void TestAudioSpeakerFallback() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
active_controller = 2; // A different controller is active while a USB DualSense is attached.
|
||||
const auto port = audio.AudioOutOpen(4, 2, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
Check(port.IsValid() && default_stream != nullptr, "pad speaker fallback did not open");
|
||||
Libs::Audio::Audio::OutputParam output {port, pcm.data()};
|
||||
Check(audio.AudioOutOutputs(&output, 1, false) == 2, "pad speaker output count is wrong");
|
||||
Check(streams.size() == 1 && opens == 1 && effects[1].empty(),
|
||||
"another controller's pad speaker opened or routed the DualSense");
|
||||
std::array<float, 4> actual {};
|
||||
Check(SDL_GetAudioStreamData(default_stream, actual.data(), sizeof(actual)) == sizeof(actual) &&
|
||||
actual == pcm,
|
||||
"pad speaker did not fall back to the main output");
|
||||
}
|
||||
|
||||
void TestAudioSpeakerRouting() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
const auto port = audio.AudioOutOpen(4, 2, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
Check(port.IsValid() && default_stream != nullptr, "pad speaker port did not open");
|
||||
Libs::Audio::Audio::OutputParam output {port, pcm.data()};
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
Check(streams.size() == 2 && opened_device == 10 &&
|
||||
SDL_GetAudioStreamQueued(default_stream) == 0,
|
||||
"successful DualSense playback also queued audio on the main output");
|
||||
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
|
||||
}
|
||||
|
||||
void TestAudioDefaultResumeFailure() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
fail_default_resume = true;
|
||||
const auto port = audio.AudioOutOpen(4, 2, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
Check(port.IsValid() && opens == 1 && default_stream == nullptr,
|
||||
"default-output resume failure was not exercised");
|
||||
Libs::Audio::Audio::OutputParam output {port, pcm.data()};
|
||||
audio.AudioOutOutputs(&output, 1, false);
|
||||
Check(streams.size() == 1 && opened_device == 10,
|
||||
"default-output resume failure disabled the DualSense speaker");
|
||||
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
TestAudioSpeakerFallback();
|
||||
TestAudioSpeakerRouting();
|
||||
TestAudioDefaultResumeFailure();
|
||||
TestFormatsAndVolume();
|
||||
TestDiscoveryAndHotplug();
|
||||
TestSpeaker();
|
||||
TestSpeakerUnplug();
|
||||
TestSpeakerSwitchAndFailures();
|
||||
TestSpeakerRequiresUnambiguousUsbDevice();
|
||||
TestFailuresAndBoundedQueue();
|
||||
TestRumbleLeaseAndDuration();
|
||||
TestSwitchStopsOldRumble();
|
||||
TestCloseRestoresRumble();
|
||||
std::printf("PadHapticsTests: all cases passed\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user