mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 00:53:02 -05:00
291 lines
16 KiB
C++
291 lines
16 KiB
C++
#include "SDL.h"
|
|
#include "prx/libSceAudioOut/include/AudioOutSdl.hpp"
|
|
#include "prx/libScePad/include/PadState.hpp"
|
|
#include "prx/libScePad/include/PadAudioHapticQueue.hpp"
|
|
#include "prx/libkernel/Time/include/Time.hpp"
|
|
#include "prx/libc/include/general/VabiMacros.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <initializer_list>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
extern "C" {
|
|
int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t);
|
|
int APS5_VABI sceAudioOutClose(int);
|
|
int APS5_VABI sceAudioOutOutput(int, const void*);
|
|
int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam*, std::uint32_t);
|
|
int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*);
|
|
int APS5_VABI sceAudioOutGetLastOutputTime(int, std::uint64_t*);
|
|
}
|
|
|
|
namespace {
|
|
constexpr std::uint32_t frames = 256;
|
|
bool available = true;
|
|
bool failOpen = false;
|
|
bool failInitialize = false;
|
|
bool failEnumeration = false;
|
|
bool pulse = false;
|
|
std::uint32_t frequency = 48000;
|
|
int opened = 0;
|
|
int closed = 0;
|
|
int queued = 0;
|
|
std::vector<float> played;
|
|
struct Block {
|
|
int source;
|
|
std::uint64_t begin;
|
|
std::uint64_t end;
|
|
std::array<std::uint8_t, 2> rumble;
|
|
};
|
|
std::vector<Block> blocks;
|
|
std::vector<int> clears;
|
|
PadAudioHapticQueue timeline;
|
|
|
|
void Require(bool value, const char* message) {
|
|
if (!value) throw std::runtime_error(message);
|
|
}
|
|
|
|
float Expected(std::int16_t sample, int volume, bool floating) {
|
|
const float value = sample / 32768.0f;
|
|
if (floating) return std::clamp(value * (volume / 32768.0f), -1.0f, 1.0f);
|
|
return static_cast<float>(std::clamp<std::int64_t>(static_cast<std::int64_t>(sample) * volume / 32768, -32768, 32767)) / 32768.0f;
|
|
}
|
|
|
|
void Run(std::uint32_t format, bool batch, int volume) {
|
|
const bool floating = format >= 3;
|
|
const std::uint32_t channels = format == 0 || format == 3 ? 1u : 2u;
|
|
const int handle = sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, format);
|
|
Require(handle == 1, "a closed or failed vibration port remained allocated");
|
|
std::array<int, 2> levels{volume, volume / 2};
|
|
Require(sceAudioOutSetVolume(handle, 3, levels.data()) == 0, "vibration gains failed");
|
|
std::array<std::int16_t, frames * 2> integers{};
|
|
std::array<float, frames * 2> floats{};
|
|
for (std::uint32_t frame = 0; frame < frames; ++frame) {
|
|
integers[frame * channels] = 8192;
|
|
floats[frame * channels] = 0.25f;
|
|
if (channels == 2) {
|
|
integers[frame * channels + 1] = -16384;
|
|
floats[frame * channels + 1] = -0.5f;
|
|
}
|
|
}
|
|
const void* data = floating ? static_cast<const void*>(floats.data()) : static_cast<const void*>(integers.data());
|
|
AudioOutOutputParam outputs[2]{};
|
|
outputs[0].handle = handle;
|
|
outputs[0].ptr = data;
|
|
outputs[1].handle = 9999;
|
|
outputs[1].ptr = data;
|
|
const int before = queued;
|
|
Require(sceAudioOutOutputs(outputs, 2) < 0 && queued == before, "an invalid batch queued a valid prefix");
|
|
Require((batch ? sceAudioOutOutputs(outputs, 1) : sceAudioOutOutput(handle, data)) == static_cast<int>(frames), "vibration frame count changed");
|
|
Require(queued == before + 1, "native vibration did not reach the named controller device");
|
|
const std::uint32_t deviceChannels = pulse ? 6u : 4u;
|
|
Require(played.size() == frames * deviceChannels, "native vibration frame layout changed");
|
|
const auto left = Expected(8192, levels[0], floating);
|
|
const auto right = channels == 1 ? left : Expected(-16384, levels[1], floating);
|
|
for (std::uint32_t frame = 0; frame < frames; ++frame) {
|
|
for (std::uint32_t channel = 0; channel < deviceChannels; ++channel) {
|
|
const auto expected = channel == deviceChannels - 2 ? left : channel == deviceChannels - 1 ? right : 0.0f;
|
|
Require(played[frame * deviceChannels + channel] == expected, "vibration PCM or gain leaked into a speaker channel");
|
|
}
|
|
}
|
|
std::uint64_t time = 0;
|
|
Require(sceAudioOutGetLastOutputTime(handle, &time) == 0 && time != 0, "vibration acceptance time was not recorded");
|
|
Require(sceAudioOutOutput(handle, nullptr) == static_cast<int>(frames) && queued == before + 1, "draining vibration queued samples");
|
|
std::uint64_t after = 0;
|
|
Require(sceAudioOutGetLastOutputTime(handle, &after) == 0 && after == time, "a drain changed the last data time");
|
|
Require(sceAudioOutClose(handle) == 0, "native vibration close failed");
|
|
}
|
|
|
|
int Submit(std::uint32_t count, std::uint32_t rate, bool split = false) {
|
|
const int handle = sceAudioOutOpen(0x10000000, 10, 0, count, rate, 1);
|
|
Require(handle > 0, "timed vibration port did not open");
|
|
std::vector<std::int16_t> samples(static_cast<std::size_t>(count) * 2);
|
|
for (std::uint32_t frame = 0; frame < count; ++frame) {
|
|
samples[frame * 2] = split && frame >= 256 ? 16384 : 8192;
|
|
samples[frame * 2 + 1] = split && frame >= 256 ? -8192 : -16384;
|
|
}
|
|
Require(sceAudioOutOutput(handle, samples.data()) == static_cast<int>(count), "timed vibration output failed");
|
|
return handle;
|
|
}
|
|
|
|
void RunTiming() {
|
|
available = false;
|
|
const int shortPort = Submit(256, 48000);
|
|
Require(blocks.size() == 1 && blocks[0].end - blocks[0].begin == 5333 && blocks[0].rumble == std::array<std::uint8_t, 2>{128, 255}, "short block timing changed");
|
|
const int longPort = Submit(4800, 24000);
|
|
Require(blocks.size() == 20 && blocks.back().end - blocks[1].begin == 200000, "long block duration did not follow frame count/rate");
|
|
for (std::size_t index = 2; index < blocks.size(); ++index) Require(blocks[index].begin == blocks[index - 1].end, "successive grains overlap or leave a gap");
|
|
auto model = timeline;
|
|
Require(model.Read(blocks[1].begin + 81000) == std::array<std::uint8_t, 2>{128, 255}, "a long block inherited the AudioOut2 timeout");
|
|
Require(sceAudioOutOutput(shortPort, nullptr) == 256 && clears == std::vector<int>{shortPort}, "a drain cleared another port");
|
|
Require(sceKernelGetProcessTime() >= blocks[0].end, "a drain returned before the submitted block ended");
|
|
Require(sceAudioOutClose(shortPort) == 0 && sceAudioOutClose(longPort) == 0, "timed port close failed");
|
|
Require(clears == std::vector<int>{shortPort, shortPort, longPort}, "close did not clear its own source");
|
|
Require(timeline.Read(sceKernelGetProcessTime()) == std::array<std::uint8_t, 2>{}, "close retained a queued source");
|
|
|
|
const auto firstLong = blocks.size();
|
|
const int drained = Submit(4800, 24000);
|
|
const auto end = blocks.back().end;
|
|
Require(blocks.size() == firstLong + 19, "long PCM was not divided into grains");
|
|
Require(sceAudioOutOutput(drained, nullptr) == 4800 && sceKernelGetProcessTime() >= end, "long drain did not wait for its frame duration");
|
|
Require(sceAudioOutClose(drained) == 0, "drained port close failed");
|
|
|
|
const auto firstSplit = blocks.size();
|
|
const int split = Submit(512, 48000, true);
|
|
Require(blocks.size() == firstSplit + 2, "two-grain PCM was reduced to one value");
|
|
Require(blocks[firstSplit].rumble == std::array<std::uint8_t, 2>{128, 255} && blocks.back().rumble == std::array<std::uint8_t, 2>{255, 128}, "grain-local energy was replaced by whole-block RMS");
|
|
Require(sceAudioOutClose(split) == 0, "split port close failed");
|
|
Require(opened == 0 && queued == 0, "virtual vibration opened a speaker device");
|
|
}
|
|
|
|
void RunCadence() {
|
|
available = false;
|
|
const std::array<int, 2> ports{
|
|
sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1),
|
|
sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1)
|
|
};
|
|
Require(ports[0] > 0 && ports[1] > 0 && ports[0] != ports[1], "repeated vibration ports did not open independently");
|
|
constexpr std::array<std::array<std::int16_t, 2>, 4> patterns{{{8192, -16384}, {0, 0}, {-16384, 8192}, {16384, -8192}}};
|
|
constexpr std::array<std::array<std::uint8_t, 2>, 4> expected{{{128, 255}, {0, 0}, {255, 128}, {255, 128}}};
|
|
std::array<std::uint64_t, 2> ends{};
|
|
std::array<std::int16_t, frames * 2> samples{};
|
|
std::vector<std::uint64_t> boundaries;
|
|
for (std::size_t call = 0; call < 32; ++call) {
|
|
for (std::size_t port = 0; port < ports.size(); ++port) {
|
|
const auto pattern = (call + port) % patterns.size();
|
|
for (std::size_t frame = 0; frame < frames; ++frame) {
|
|
samples[frame * 2] = patterns[pattern][0];
|
|
samples[frame * 2 + 1] = patterns[pattern][1];
|
|
}
|
|
const auto count = blocks.size();
|
|
Require(sceAudioOutOutput(ports[port], samples.data()) == static_cast<int>(frames), "repeated vibration output rejected 256 frames");
|
|
Require(blocks.size() == count + 1, "repeated vibration output did not queue one grain per call");
|
|
const auto& block = blocks.back();
|
|
Require(block.source == ports[port] && block.rumble == expected[pattern], "repeated vibration output used the wrong source or PCM energy");
|
|
Require(block.end - block.begin == 5333 && block.begin >= ends[port], "repeated vibration interval has the wrong duration or overlaps its preceding call");
|
|
ends[port] = block.end;
|
|
boundaries.insert(boundaries.end(), {block.begin, block.end - 1, block.end});
|
|
}
|
|
}
|
|
Require(blocks.size() == 64 && clears.empty() && opened == 0 && queued == 0, "repeated virtual output changed routes or cleared a live port");
|
|
std::sort(boundaries.begin(), boundaries.end());
|
|
boundaries.erase(std::unique(boundaries.begin(), boundaries.end()), boundaries.end());
|
|
const auto check = [&](PadAudioHapticQueue model, int removed) {
|
|
for (const auto time : boundaries) {
|
|
std::array<std::uint8_t, 2> value{};
|
|
for (const auto& block : blocks) {
|
|
if (block.source == removed || time < block.begin || time >= block.end) continue;
|
|
for (std::size_t channel = 0; channel < value.size(); ++channel) value[channel] = std::max(value[channel], block.rumble[channel]);
|
|
}
|
|
Require(model.Read(time) == value, "repeated vibration timeline lost a grain, silence, boundary or independent source");
|
|
}
|
|
};
|
|
check(timeline, 0);
|
|
Require(sceAudioOutOutput(ports[0], nullptr) == static_cast<int>(frames) && clears == std::vector<int>{ports[0]}, "repeated vibration drain cleared the wrong source");
|
|
check(timeline, ports[0]);
|
|
Require(sceAudioOutClose(ports[0]) == 0 && sceAudioOutClose(ports[1]) == 0, "repeated vibration port close failed");
|
|
Require(clears == std::vector<int>{ports[0], ports[0], ports[1]}, "repeated vibration close did not clear only its source");
|
|
Require(timeline.Read(ends[0]) == std::array<std::uint8_t, 2>{}, "repeated vibration close retained queued output");
|
|
std::puts("64 repeated legacy stereo S16/48 kHz outputs: 256 frames per call, 32 calls per port; PCM, interval boundaries, source mixing and drain isolation passed");
|
|
}
|
|
}
|
|
|
|
int InitializeSdlAudio_nid_no_patch() { return failInitialize ? -1 : 0; }
|
|
extern "C" void PadQueueLegacyAudioHaptics_nid_postfix(int source, std::uint64_t begin, std::uint64_t end, std::uint8_t large, std::uint8_t small) {
|
|
blocks.push_back({source, begin, end, {large, small}});
|
|
timeline.Queue(source, begin, end, large, small);
|
|
}
|
|
extern "C" void PadClearLegacyAudioHaptics_nid_postfix(int source) {
|
|
clears.push_back(source);
|
|
timeline.Clear(source);
|
|
}
|
|
extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; }
|
|
extern "C" int SDLCALL SDL_GetNumAudioDevices(int) { return failEnumeration ? -1 : available ? 2 : 1; }
|
|
extern "C" const char* SDLCALL SDL_GetAudioDeviceName(int index, int) { return index == 0 ? "Main speakers" : "DualSense Wireless Controller"; }
|
|
extern "C" int SDLCALL SDL_GetAudioDeviceSpec(int index, int, SDL_AudioSpec* spec) { *spec = {}; spec->channels = index == 0 ? 2 : 4; return 0; }
|
|
extern "C" const char* SDLCALL SDL_GetCurrentAudioDriver() { return pulse ? "pulseaudio" : "dummy"; }
|
|
extern "C" const char* SDLCALL SDL_GetError() { return "test controller open failure"; }
|
|
extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char* name, int capture, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int flags) {
|
|
Require(name != nullptr && std::string_view(name) == "DualSense Wireless Controller", "vibration opened the default speakers");
|
|
Require(capture == 0 && flags == 0 && desired->callback == nullptr && desired->freq == static_cast<int>(frequency) && desired->format == AUDIO_F32SYS && desired->channels == (pulse ? 6 : 4) && desired->samples == frames, "native controller request changed");
|
|
if (failOpen) return 0;
|
|
++opened;
|
|
*obtained = *desired;
|
|
return 42;
|
|
}
|
|
extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID device, int) { Require(device == 42, "wrong native device paused"); }
|
|
extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID device) { Require(device == 42, "wrong native queue queried"); return 0; }
|
|
extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID device, const void* data, Uint32 bytes) {
|
|
Require(device == 42 && bytes % sizeof(float) == 0, "wrong vibration queue");
|
|
const auto* samples = static_cast<const float*>(data);
|
|
const std::size_t sampleCount = bytes / sizeof(float);
|
|
played.assign(samples, samples + sampleCount);
|
|
++queued;
|
|
return 0;
|
|
}
|
|
extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID device) { Require(device == 42, "wrong native queue cleared"); }
|
|
extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID device) { Require(device == 42, "wrong native device closed"); ++closed; }
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
Require(argc == 2, "invalid test arguments");
|
|
const std::string_view mode = argv[1];
|
|
Require(mode == "quad" || mode == "pulse" || mode == "open_failure" || mode == "timing" || mode == "cadence" || mode == "discovery_failure", "invalid device mode");
|
|
pulse = mode == "pulse";
|
|
Pad::Initialize();
|
|
Pad::SetVibrationMode(1);
|
|
std::uint32_t seen = 0;
|
|
PadOutputState state{};
|
|
static_cast<void>(PadFetchOutput_nid_postfix(&seen, &state));
|
|
if (mode == "cadence") {
|
|
RunCadence();
|
|
return 0;
|
|
}
|
|
if (mode == "timing") {
|
|
RunTiming();
|
|
std::puts("legacy vibration device tests passed");
|
|
return 0;
|
|
}
|
|
if (mode == "open_failure") {
|
|
failOpen = true;
|
|
bool threw = false;
|
|
try { static_cast<void>(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1)); }
|
|
catch (const std::runtime_error&) { threw = true; }
|
|
Require(threw && opened == 0 && queued == 0, "a failed native open silently changed routes");
|
|
failOpen = false;
|
|
}
|
|
if (mode == "discovery_failure") {
|
|
for (bool* failure : {&failInitialize, &failEnumeration}) {
|
|
*failure = true;
|
|
bool threw = false;
|
|
try { static_cast<void>(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, 1)); }
|
|
catch (const std::runtime_error&) { threw = true; }
|
|
Require(threw && opened == 0 && queued == 0, "a discovery failure silently changed routes");
|
|
*failure = false;
|
|
}
|
|
}
|
|
for (auto rate : {24000u, 48000u}) {
|
|
frequency = rate;
|
|
for (auto format : {0u, 1u, 3u, 4u})
|
|
for (bool batch : {false, true})
|
|
for (int volume : {0, 16384, 32768, 131072}) Run(format, batch, volume);
|
|
}
|
|
for (auto format : {2u, 5u, 6u, 7u}) {
|
|
bool threw = false;
|
|
try { static_cast<void>(sceAudioOutOpen(0x10000000, 10, 0, frames, frequency, format)); }
|
|
catch (const std::runtime_error&) { threw = true; }
|
|
Require(threw && opened == 64, "an unknown multichannel vibration layout was accepted");
|
|
}
|
|
Require(opened == closed && opened == 64, "native vibration device ownership changed");
|
|
Require(!PadFetchOutput_nid_postfix(&seen, &state), "native waveform output also sent motor approximation");
|
|
std::puts("legacy vibration device tests passed");
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "%s\n", error.what());
|
|
return 1;
|
|
}
|
|
}
|