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:
parent
89792a3bca
commit
e1be2ead54
|
|
@ -2,10 +2,31 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
|||
|
||||
add_subdirectory(cpuinfo)
|
||||
|
||||
# The Windows guest red-zone patcher needs a decoder/encoder and a small
|
||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||
include(FetchContent)
|
||||
|
||||
# The Bluetooth DualSense transports speaker audio as fixed-size Opus packets.
|
||||
set(kyty_build_testing_saved "${BUILD_TESTING}")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE)
|
||||
if(MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
# Opus assumes MSVC accepts SSE4.1 intrinsics without per-file flags, but
|
||||
# clang-cl rejects them in its baseline x64 files. Its AVX2 dispatch also
|
||||
# depends on the SSE4.1 declarations, so retain the x64 SSE2 baseline only.
|
||||
set(OPUS_X86_MAY_HAVE_SSE4_1 OFF CACHE BOOL "" FORCE)
|
||||
set(OPUS_X86_MAY_HAVE_AVX2 OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
FetchContent_Declare(opus
|
||||
GIT_REPOSITORY https://github.com/xiph/opus.git
|
||||
GIT_TAG ddbe48383984d56acd9e1ab6a090c54ca6b735a6
|
||||
GIT_SHALLOW FALSE)
|
||||
FetchContent_MakeAvailable(opus)
|
||||
set(BUILD_TESTING "${kyty_build_testing_saved}")
|
||||
install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus")
|
||||
|
||||
# The Windows guest red-zone patcher needs a decoder/encoder and a small
|
||||
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
|
||||
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
xbyak
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ if (KYTY_CLANG_CL)
|
|||
set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED")
|
||||
endif()
|
||||
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak)
|
||||
set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus)
|
||||
|
||||
# Linux system libraries.
|
||||
if(LINUX)
|
||||
|
|
@ -495,7 +495,7 @@ target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
|
|||
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
|
||||
)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
|
||||
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
|
||||
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
|
|
|
|||
|
|
@ -428,6 +428,10 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
|
|||
}
|
||||
|
||||
Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format) {
|
||||
if (freq == 0) {
|
||||
return Id::Invalid();
|
||||
}
|
||||
|
||||
Common::LockGuard lock(m_mutex);
|
||||
|
||||
for (int id = 0; id < OUT_PORTS_MAX; id++) {
|
||||
|
|
@ -552,12 +556,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
|
|||
auto& port = m_out_ports[params[i].handle.GetId()];
|
||||
|
||||
uint64_t controller_queued_us = 0;
|
||||
bool controller_uses_bluetooth = false;
|
||||
if (port.haptics != nullptr) {
|
||||
// Keep the stream alive against close and volume changes.
|
||||
Common::LockGuard lock(m_mutex);
|
||||
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);
|
||||
controller_uses_bluetooth =
|
||||
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
|
||||
}
|
||||
if (controller_queued_us == 0) {
|
||||
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
|
||||
|
|
@ -568,12 +575,16 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
|
|||
}
|
||||
} else {
|
||||
port.queue_primed = false;
|
||||
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK) {
|
||||
// Bluetooth HID has its own sender. Let the sample clock pace a batch
|
||||
// without another audio device, instead of waiting on the HID queue.
|
||||
if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK && !controller_uses_bluetooth) {
|
||||
any_device = true;
|
||||
if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
|
||||
deadline =
|
||||
std::max(deadline, LibKernel::KernelGetProcessTime() +
|
||||
controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
|
||||
// Limit a stalled USB endpoint to one audio block of waiting.
|
||||
const uint64_t block_us = 1000000ULL * port.samples_num / port.freq;
|
||||
deadline = std::max(deadline, LibKernel::KernelGetProcessTime() +
|
||||
std::min(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US,
|
||||
block_us));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -842,6 +853,9 @@ int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, u
|
|||
if (!audio_out_port_type_is_valid(type)) {
|
||||
return AUDIO_OUT_ERROR_INVALID_PORT_TYPE;
|
||||
}
|
||||
if (freq == 0) {
|
||||
return AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(index != 0);
|
||||
|
||||
Audio::Format format = Audio::Format::Unknown;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,467 @@
|
|||
#include "libs/dualSenseBluetooth.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <opus.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs::Controller::DualSenseBluetooth {
|
||||
|
||||
namespace {
|
||||
// Report 0x39 and its sub-packets are described by awalol/DS5Dongle (MIT):
|
||||
// https://github.com/awalol/DS5Dongle
|
||||
// Each report plays two 32-frame stereo haptic blocks at 3 kHz (21.333 ms).
|
||||
constexpr uint64_t PERIOD_NS = 64000000000ULL / 3000;
|
||||
constexpr int HAPTIC_RATE = 3000;
|
||||
constexpr int HAPTIC_REPORT_FLOATS = 2 * 32 * 2;
|
||||
// Two 480-frame Opus packets occupy the same 21.333 ms slot. Resampling
|
||||
// 48 kHz input to 45 kHz before encoding at 48 kHz compensates for playback.
|
||||
constexpr int SPEAKER_RATE = 45000;
|
||||
constexpr int OPUS_FRAMES = 480;
|
||||
constexpr int OPUS_BYTES = 200;
|
||||
constexpr size_t REPORT_BYTES = 547;
|
||||
constexpr int MAX_QUEUE_MS = 80;
|
||||
constexpr uint64_t COALESCE_NS = 1000000;
|
||||
|
||||
struct Session;
|
||||
std::mutex g_mutex;
|
||||
std::condition_variable g_wake;
|
||||
std::map<int, std::shared_ptr<Session>> g_sessions;
|
||||
SDL_Thread* g_thread = nullptr;
|
||||
bool g_stopping = false;
|
||||
void (*g_tick)() = nullptr;
|
||||
|
||||
template <typename Char>
|
||||
std::string Address(const Char* serial) {
|
||||
std::string address;
|
||||
if (serial == nullptr) {
|
||||
return address;
|
||||
}
|
||||
for (; *serial != 0; ++serial) {
|
||||
if (*serial >= '0' && *serial <= '9') {
|
||||
address.push_back(static_cast<char>(*serial));
|
||||
} else if (*serial >= 'a' && *serial <= 'f') {
|
||||
address.push_back(static_cast<char>(*serial));
|
||||
} else if (*serial >= 'A' && *serial <= 'F') {
|
||||
address.push_back(static_cast<char>(*serial - 'A' + 'a'));
|
||||
} else if (*serial != ':' && *serial != '-') {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return address.size() == 12 ? address : std::string {};
|
||||
}
|
||||
|
||||
SDL_hid_device* OpenDevice(int controller) {
|
||||
SDL_LockJoysticks();
|
||||
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
|
||||
const bool wireless = pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5 &&
|
||||
SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS;
|
||||
const auto vendor = wireless ? SDL_GetGamepadVendor(pad) : 0;
|
||||
const auto product = wireless ? SDL_GetGamepadProduct(pad) : 0;
|
||||
const auto address = wireless ? Address(SDL_GetGamepadSerial(pad)) : std::string {};
|
||||
SDL_UnlockJoysticks();
|
||||
if (vendor != 0x054c || (product != 0x0ce6 && product != 0x0df2) || address.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* devices = SDL_hid_enumerate(vendor, product);
|
||||
std::string path;
|
||||
bool ambiguous = false;
|
||||
for (auto* device = devices; device != nullptr; device = device->next) {
|
||||
if (device->bus_type == SDL_HID_API_BUS_BLUETOOTH && device->path != nullptr &&
|
||||
Address(device->serial_number) == address &&
|
||||
(device->usage_page == 0 || (device->usage_page == 1 && device->usage == 5))) {
|
||||
if (!path.empty()) {
|
||||
ambiguous = true;
|
||||
break;
|
||||
}
|
||||
path = device->path;
|
||||
}
|
||||
}
|
||||
SDL_hid_free_enumeration(devices);
|
||||
return !ambiguous && !path.empty() ? SDL_hid_open_path(path.c_str()) : nullptr;
|
||||
}
|
||||
|
||||
void FinishReport(std::array<uint8_t, REPORT_BYTES>& report) {
|
||||
const uint8_t prefix = 0xa2;
|
||||
auto crc = SDL_crc32(0, &prefix, 1);
|
||||
crc = SDL_crc32(crc, report.data(), report.size() - 4);
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
report[report.size() - 4 + i] = static_cast<uint8_t>(crc >> (8 * i));
|
||||
}
|
||||
}
|
||||
|
||||
struct Session {
|
||||
SDL_hid_device* hid = nullptr;
|
||||
OpusEncoder* encoder = nullptr;
|
||||
std::vector<Stream*> streams;
|
||||
std::mutex io_mutex;
|
||||
std::atomic<bool> active {true};
|
||||
std::atomic<bool> failed {false};
|
||||
std::atomic<uint64_t> generation {0};
|
||||
uint64_t next_send = 0;
|
||||
uint64_t pending_deadline = 0;
|
||||
uint8_t sequence = 0;
|
||||
uint8_t counter = 0;
|
||||
|
||||
~Session() {
|
||||
if (encoder != nullptr) {
|
||||
opus_encoder_destroy(encoder);
|
||||
}
|
||||
if (hid != nullptr) {
|
||||
SDL_hid_close(hid);
|
||||
}
|
||||
}
|
||||
|
||||
bool CreateEncoder() {
|
||||
if (encoder != nullptr) {
|
||||
return true;
|
||||
}
|
||||
int error = OPUS_OK;
|
||||
encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &error);
|
||||
if (encoder == nullptr || error != OPUS_OK) {
|
||||
return false;
|
||||
}
|
||||
if (opus_encoder_ctl(encoder, OPUS_SET_BITRATE(OPUS_BYTES * 8 * 100)) == OPUS_OK &&
|
||||
opus_encoder_ctl(encoder, OPUS_SET_VBR(0)) == OPUS_OK &&
|
||||
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(0)) == OPUS_OK) {
|
||||
return true;
|
||||
}
|
||||
opus_encoder_destroy(encoder);
|
||||
encoder = nullptr;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
struct Stream {
|
||||
std::shared_ptr<Session> session;
|
||||
SDL_AudioStream* audio = nullptr;
|
||||
uint32_t freq = 0;
|
||||
bool speaker = false;
|
||||
};
|
||||
|
||||
namespace {
|
||||
template <size_t N>
|
||||
void Mix(SDL_AudioStream* stream, std::array<float, N>& mixed) {
|
||||
std::array<float, N> pcm {};
|
||||
const int bytes = SDL_GetAudioStreamData(stream, pcm.data(), sizeof(pcm));
|
||||
for (int i = 0; i < std::max(0, bytes) / static_cast<int>(sizeof(float)); i++) {
|
||||
mixed[i] += pcm[i];
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingReport {
|
||||
std::shared_ptr<Session> session;
|
||||
uint64_t generation;
|
||||
std::array<uint8_t, REPORT_BYTES> data {};
|
||||
std::array<float, 2 * OPUS_FRAMES * 2> audio {};
|
||||
bool speaker = false;
|
||||
};
|
||||
|
||||
bool PrepareReport(Session& session, PendingReport& pending, uint64_t now) {
|
||||
bool has_data = false;
|
||||
bool ready = false;
|
||||
bool speaker_ready = false;
|
||||
for (const auto* stream: session.streams) {
|
||||
const bool available = SDL_GetAudioStreamAvailable(stream->audio) > 0;
|
||||
// Both payloads cover 64 / 3000 seconds. Measure input duration so SDL's
|
||||
// resampler lookahead does not make a complete block appear partial.
|
||||
const auto frames =
|
||||
(static_cast<uint64_t>(stream->freq) * (HAPTIC_REPORT_FLOATS / 2) + HAPTIC_RATE - 1) /
|
||||
HAPTIC_RATE;
|
||||
const bool full = available && SDL_GetAudioStreamQueued(stream->audio) >=
|
||||
static_cast<int>(frames * 2 * sizeof(float));
|
||||
has_data |= available;
|
||||
ready |= full;
|
||||
speaker_ready |= stream->speaker && full;
|
||||
pending.speaker |= stream->speaker;
|
||||
}
|
||||
if (!has_data) {
|
||||
session.pending_deadline = 0;
|
||||
return false;
|
||||
}
|
||||
if (!ready || (!speaker_ready && (pending.speaker || session.next_send == 0))) {
|
||||
// Accumulate short blocks for at most one report period. A full stream
|
||||
// must not wait on a partial peer; only briefly coalesce haptics with
|
||||
// speaker audio arriving in the same emulated batch.
|
||||
const auto deadline = now + (ready ? COALESCE_NS : PERIOD_NS);
|
||||
session.pending_deadline =
|
||||
session.pending_deadline == 0 ? deadline : std::min(session.pending_deadline, deadline);
|
||||
if (now < session.pending_deadline) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
session.pending_deadline = 0;
|
||||
std::array<float, HAPTIC_REPORT_FLOATS> haptics {};
|
||||
for (auto* stream: session.streams) {
|
||||
if (stream->speaker) {
|
||||
Mix(stream->audio, pending.audio);
|
||||
} else {
|
||||
Mix(stream->audio, haptics);
|
||||
}
|
||||
}
|
||||
auto& report = pending.data;
|
||||
report[0] = 0x39;
|
||||
report[1] = static_cast<uint8_t>(session.sequence << 4);
|
||||
session.sequence = (session.sequence + 1) & 0x0f;
|
||||
report[2] = 0x91;
|
||||
report[3] = 6;
|
||||
report[4] = 0x7e;
|
||||
std::fill_n(report.begin() + 5, 4, 48);
|
||||
session.counter += 2;
|
||||
report[9] = session.counter;
|
||||
report[10] = 0xd2;
|
||||
report[11] = 64;
|
||||
for (size_t i = 0; i < haptics.size(); i++) {
|
||||
const auto value = std::isfinite(haptics[i]) ? haptics[i] : 0.0f;
|
||||
report[12 + i] = static_cast<uint8_t>(
|
||||
static_cast<int>(std::lround(std::clamp(value, -1.0f, 1.0f) * 127)));
|
||||
}
|
||||
if (pending.speaker) {
|
||||
report[140] = 0xd3;
|
||||
report[141] = OPUS_BYTES;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EncodeReport(PendingReport& pending) {
|
||||
if (!pending.session->active || pending.session->failed) {
|
||||
return false;
|
||||
}
|
||||
if (pending.speaker) {
|
||||
for (auto& sample: pending.audio) {
|
||||
sample = std::isfinite(sample) ? std::clamp(sample, -1.0f, 1.0f) : 0.0f;
|
||||
}
|
||||
for (int packet = 0; packet < 2; packet++) {
|
||||
if (opus_encode_float(pending.session->encoder,
|
||||
pending.audio.data() + packet * OPUS_FRAMES * 2, OPUS_FRAMES,
|
||||
pending.data.data() + 142 + packet * OPUS_BYTES,
|
||||
OPUS_BYTES) != OPUS_BYTES) {
|
||||
pending.session->failed = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
FinishReport(pending.data);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<PendingReport> Prepare(uint64_t now) {
|
||||
std::vector<PendingReport> pending;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
for (auto& [id, session]: g_sessions) {
|
||||
if (session->failed || now < session->next_send) {
|
||||
continue;
|
||||
}
|
||||
PendingReport report {session, session->generation.load()};
|
||||
if (PrepareReport(*session, report, now)) {
|
||||
pending.push_back(std::move(report));
|
||||
// Regain timing after a late HID write without sending reports in a
|
||||
// burst. Reset the cadence only on the first report.
|
||||
session->next_send = session->next_send == 0
|
||||
? now + PERIOD_NS
|
||||
: std::max(session->next_send + PERIOD_NS,
|
||||
now + PERIOD_NS / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Opus encoding can take milliseconds; the emulation audio thread must be able
|
||||
// to queue its next block while this work runs.
|
||||
pending.erase(std::remove_if(pending.begin(), pending.end(),
|
||||
[](PendingReport& report) { return !EncodeReport(report); }),
|
||||
pending.end());
|
||||
return pending;
|
||||
}
|
||||
|
||||
void Send(const std::vector<PendingReport>& pending) {
|
||||
for (const auto& report: pending) {
|
||||
auto& session = *report.session;
|
||||
std::lock_guard io_lock(session.io_mutex);
|
||||
if (!session.active || session.failed || report.generation != session.generation) {
|
||||
continue;
|
||||
}
|
||||
if (SDL_hid_write(session.hid, report.data.data(), report.data.size()) !=
|
||||
static_cast<int>(report.data.size())) {
|
||||
session.failed = true;
|
||||
LOGF("DualSenseBluetooth: audio HID write failed: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int SDLCALL Sender(void*) {
|
||||
std::unique_lock lock(g_mutex);
|
||||
while (!g_stopping) {
|
||||
auto tick = g_tick;
|
||||
lock.unlock();
|
||||
auto pending = Prepare(SDL_GetTicksNS());
|
||||
Send(pending);
|
||||
if (tick != nullptr) {
|
||||
tick();
|
||||
}
|
||||
lock.lock();
|
||||
if (g_sessions.empty()) {
|
||||
g_wake.wait(lock, [] { return g_stopping || !g_sessions.empty(); });
|
||||
} else {
|
||||
const auto now = SDL_GetTicksNS();
|
||||
uint64_t wait_ns = 10000000; // Check rumble expiry at most 10 ms late.
|
||||
for (const auto& [id, session]: g_sessions) {
|
||||
if (session->next_send > now) {
|
||||
wait_ns = std::min(wait_ns, session->next_send - now);
|
||||
}
|
||||
if (session->pending_deadline > now) {
|
||||
wait_ns = std::min(wait_ns, session->pending_deadline - now);
|
||||
}
|
||||
}
|
||||
g_wake.wait_for(lock, std::chrono::nanoseconds(wait_ns));
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Stream* Open(uint32_t freq, bool speaker, int controller, void (*tick)()) {
|
||||
if (freq == 0 || freq > 192000) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_stopping) {
|
||||
return nullptr;
|
||||
}
|
||||
auto it = g_sessions.find(controller);
|
||||
std::shared_ptr<Session> session;
|
||||
if (it != g_sessions.end() && !it->second->failed) {
|
||||
session = it->second;
|
||||
} else {
|
||||
session = std::make_shared<Session>();
|
||||
session->hid = OpenDevice(controller);
|
||||
if (session->hid == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (speaker && !session->CreateEncoder()) {
|
||||
return nullptr;
|
||||
}
|
||||
const SDL_AudioSpec source {SDL_AUDIO_F32, 2, static_cast<int>(freq)};
|
||||
const SDL_AudioSpec target {SDL_AUDIO_F32, 2, speaker ? SPEAKER_RATE : HAPTIC_RATE};
|
||||
auto* audio = SDL_CreateAudioStream(&source, &target);
|
||||
if (audio == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (g_thread == nullptr) {
|
||||
g_thread = SDL_CreateThread(Sender, "DualSense Bluetooth audio", nullptr);
|
||||
if (g_thread == nullptr) {
|
||||
SDL_DestroyAudioStream(audio);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
g_tick = tick;
|
||||
auto* stream = new Stream {session, audio, freq, speaker};
|
||||
session->streams.push_back(stream);
|
||||
session->pending_deadline = 0;
|
||||
g_sessions[controller] = session;
|
||||
g_wake.notify_one();
|
||||
return stream;
|
||||
}
|
||||
|
||||
uint64_t Queue(Stream* stream, const float* stereo, uint32_t frames) {
|
||||
if (stream == nullptr || stereo == nullptr || frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (!stream->session->active || stream->session->failed) {
|
||||
return 0;
|
||||
}
|
||||
const uint32_t max_frames = stream->freq * MAX_QUEUE_MS / 1000;
|
||||
if (frames > max_frames) {
|
||||
stereo += static_cast<size_t>(frames - max_frames) * 2;
|
||||
frames = max_frames;
|
||||
}
|
||||
int queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
const int bytes = static_cast<int>(frames * 2 * sizeof(float));
|
||||
if (queued + bytes > static_cast<int>(max_frames * 2 * sizeof(float))) {
|
||||
// Discard complete old report slots, preserving the newest queued audio.
|
||||
// Clearing the entire stream here creates an audible gap after a brief
|
||||
// Windows Bluetooth write stall.
|
||||
std::array<float, 2 * OPUS_FRAMES * 2> discarded {};
|
||||
const int discard_bytes = stream->speaker
|
||||
? static_cast<int>(sizeof(discarded))
|
||||
: static_cast<int>(HAPTIC_REPORT_FLOATS * sizeof(float));
|
||||
const int target = static_cast<int>(max_frames * 2 * sizeof(float)) - bytes;
|
||||
while (queued > target) {
|
||||
if (SDL_GetAudioStreamData(stream->audio, discarded.data(), discard_bytes) <= 0) {
|
||||
SDL_ClearAudioStream(stream->audio);
|
||||
break;
|
||||
}
|
||||
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
}
|
||||
}
|
||||
queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio));
|
||||
if (!SDL_PutAudioStreamData(stream->audio, stereo, bytes)) {
|
||||
return 0;
|
||||
}
|
||||
g_wake.notify_one();
|
||||
return static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * 2 * sizeof(float));
|
||||
}
|
||||
|
||||
void Close(Stream* stream) {
|
||||
if (stream == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto session = stream->session;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
auto& streams = session->streams;
|
||||
streams.erase(std::remove(streams.begin(), streams.end(), stream), streams.end());
|
||||
session->generation++;
|
||||
session->pending_deadline = 0;
|
||||
if (streams.empty()) {
|
||||
session->active = false;
|
||||
for (auto it = g_sessions.begin(); it != g_sessions.end(); ++it) {
|
||||
if (it->second == session) {
|
||||
g_sessions.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
SDL_DestroyAudioStream(stream->audio);
|
||||
delete stream;
|
||||
}
|
||||
// Drain a write already in flight before the caller changes speaker routing.
|
||||
std::lock_guard io_lock(session->io_mutex);
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
SDL_Thread* thread;
|
||||
{
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_stopping = true;
|
||||
for (auto& [id, session]: g_sessions) {
|
||||
session->active = false;
|
||||
}
|
||||
thread = g_thread;
|
||||
g_wake.notify_one();
|
||||
}
|
||||
if (thread != nullptr) {
|
||||
SDL_WaitThread(thread, nullptr);
|
||||
}
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_sessions.clear();
|
||||
g_thread = nullptr;
|
||||
g_tick = nullptr;
|
||||
g_stopping = false;
|
||||
}
|
||||
|
||||
} // namespace Libs::Controller::DualSenseBluetooth
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
|
||||
#define KYTY_LIBS_DUALSENSE_BLUETOOTH_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Libs::Controller::DualSenseBluetooth {
|
||||
struct Stream;
|
||||
Stream* Open(uint32_t frequency, bool speaker, int controller, void (*tick)());
|
||||
uint64_t Queue(Stream* stream, const float* samples, uint32_t frames);
|
||||
void Close(Stream* stream);
|
||||
void Shutdown();
|
||||
} // namespace Libs::Controller::DualSenseBluetooth
|
||||
|
||||
#endif
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ struct Stream;
|
|||
// ports on the actuators.
|
||||
Stream* Open(uint32_t freq, bool speaker);
|
||||
void Close(Stream* stream);
|
||||
// True while this stream is routed through the asynchronous Bluetooth HID sender.
|
||||
bool UsesBluetooth(const Stream* stream);
|
||||
// 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.
|
||||
// (the active pad has no usable controller audio device or its speaker cannot be routed).
|
||||
// Ambiguous USB or Bluetooth endpoints 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.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ uint32_t oversleep = 0, processing = 0;
|
|||
std::vector<uint32_t> sleeps;
|
||||
std::vector<std::unique_ptr<Stream>> streams;
|
||||
bool fail_open = false, fail_put = false, stalled = false;
|
||||
bool pad_connected = false, pad_bluetooth = false;
|
||||
uint64_t pad_queue_us = 0;
|
||||
int clears = 0;
|
||||
|
||||
Stream& GetStream(SDL_AudioStream* stream) {
|
||||
|
|
@ -136,11 +138,15 @@ int GetActiveControllerId() {
|
|||
|
||||
namespace DualSenseHaptics {
|
||||
Stream* Open(uint32_t, bool) {
|
||||
return nullptr;
|
||||
static int pad_stream;
|
||||
return pad_connected ? reinterpret_cast<Stream*>(&pad_stream) : nullptr;
|
||||
}
|
||||
void Close(Stream*) {}
|
||||
uint64_t Queue(Stream*, int, const void*, uint32_t, uint32_t, bool, const int*) {
|
||||
return 0;
|
||||
bool UsesBluetooth(const Stream* stream) {
|
||||
return stream != nullptr && pad_bluetooth;
|
||||
}
|
||||
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) {
|
||||
return stream != nullptr ? pad_queue_us : 0;
|
||||
}
|
||||
} // namespace DualSenseHaptics
|
||||
} // namespace Libs::Controller
|
||||
|
|
@ -170,6 +176,8 @@ struct Fixture {
|
|||
processing = oversleep = 0;
|
||||
clears = 0;
|
||||
fail_open = fail_put = stalled = false;
|
||||
pad_connected = pad_bluetooth = false;
|
||||
pad_queue_us = 0;
|
||||
sleeps.clear();
|
||||
}
|
||||
|
||||
|
|
@ -336,6 +344,27 @@ void TestFailedQueueUsesFallbackClock() {
|
|||
Check(now - start == 16000, "repeated queue failures reset fallback pacing");
|
||||
}
|
||||
|
||||
void TestControllerSpeakerPacing() {
|
||||
Fixture f;
|
||||
fail_open = true; // No PC audio device paces the pad speaker.
|
||||
pad_connected = true;
|
||||
pad_queue_us = 80000;
|
||||
const auto speaker = f.Open(256, 4);
|
||||
|
||||
pad_bluetooth = true;
|
||||
f.Output(speaker);
|
||||
const auto bluetooth_start = now;
|
||||
f.Output(speaker);
|
||||
Check(now - bluetooth_start >= 5333 && now - bluetooth_start <= 5334,
|
||||
"Bluetooth speaker waited on its HID queue instead of the sample clock");
|
||||
|
||||
pad_bluetooth = false;
|
||||
const auto usb_start = now;
|
||||
f.Output(speaker);
|
||||
Check(now - usb_start >= 5333 && now - usb_start <= 5334,
|
||||
"stalled USB speaker waited longer than one audio block");
|
||||
}
|
||||
|
||||
void TestInvalidBatchSize() {
|
||||
Libs::Audio::AudioOut::AudioOutOutputParam param {};
|
||||
for (const auto count: {0u, 33u}) {
|
||||
|
|
@ -344,6 +373,16 @@ void TestInvalidBatchSize() {
|
|||
"invalid batch size was not rejected before accessing ports");
|
||||
}
|
||||
}
|
||||
|
||||
void TestZeroOutputFrequency() {
|
||||
Fixture f;
|
||||
Check(Libs::Audio::AudioOut::AudioOutOpen(0, 0, 0, 256, 0, 4) ==
|
||||
Libs::Audio::AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ,
|
||||
"public output open accepted zero frequency");
|
||||
Check(!f.audio.AudioOutOpen(10, 256, 0, Audio::Format::FloatStereo).IsValid(),
|
||||
"internal output open accepted zero frequency");
|
||||
Check(f.Open().ToInt() == 1, "rejected output port occupied a handle");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
|
|
@ -354,7 +393,9 @@ int main() {
|
|||
TestSynchronizedBatchAndInactivePorts();
|
||||
TestFallbackUsesEachPortsPeriod();
|
||||
TestFailedQueueUsesFallbackClock();
|
||||
TestControllerSpeakerPacing();
|
||||
TestInvalidBatchSize();
|
||||
TestZeroOutputFrequency();
|
||||
Check(streams.empty(), "output stream leaked");
|
||||
std::puts("AudioOutTimingTests: all cases passed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,14 +238,31 @@ struct TrackerHarness {
|
|||
MemoryTracker tracker;
|
||||
};
|
||||
|
||||
uint8_t *AllocateFixedGuestRange(uint64_t size, uintptr_t offset) {
|
||||
// Keep the mapping inside the tracker's guest address space and preserve the
|
||||
// requested region alignment. A fixed address can be occupied by the host
|
||||
// process (notably by the macOS runner's ASLR layout).
|
||||
constexpr uintptr_t first_base = 0x0000000200000000ull;
|
||||
constexpr uintptr_t stride = 0x0000000100000000ull;
|
||||
for (uintptr_t attempt = 0; attempt < 256; attempt++) {
|
||||
auto *wanted = reinterpret_cast<void *>(first_base + offset + attempt * stride);
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(wanted, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
if (memory == wanted) {
|
||||
return memory;
|
||||
}
|
||||
if (memory != nullptr) {
|
||||
Check(VirtualFree(memory, 0, MEM_RELEASE) != 0,
|
||||
"release unexpected fixed allocation failed");
|
||||
}
|
||||
}
|
||||
Check(false, "no free fixed guest address found");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint8_t *Allocate(PageManager &manager, uint64_t pages) {
|
||||
constexpr uintptr_t base = 0x0000000200010000ull;
|
||||
const auto size = manager.GetPageSize() * pages;
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), size,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
|
||||
return memory;
|
||||
return AllocateFixedGuestRange(size, 0x10000);
|
||||
}
|
||||
|
||||
void Release(uint8_t *memory) {
|
||||
|
|
@ -386,16 +403,10 @@ void TestCpuDirtyUpload() {
|
|||
}
|
||||
|
||||
void TestRangeInvalidation() {
|
||||
constexpr uintptr_t base = 0x0000000201000000ull;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto &page_manager = harness.page_manager;
|
||||
constexpr uint64_t size = Libs::Graphics::TRACKER_REGION_SIZE * 2;
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), size,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base),
|
||||
"range invalidation allocation failed");
|
||||
auto *memory = AllocateFixedGuestRange(size, 0x1000000);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
|
||||
tracker.ForEachUploadRange(
|
||||
|
|
@ -614,16 +625,12 @@ void TestGpuDownloadProtectionMirrors() {
|
|||
}
|
||||
|
||||
void TestCrossRegionUpload() {
|
||||
constexpr uintptr_t base = 0x0000000200010000ull;
|
||||
constexpr uint64_t region_size = 4ull * 1024ull * 1024ull;
|
||||
TrackerHarness harness;
|
||||
auto &tracker = harness.tracker;
|
||||
auto &page_manager = harness.page_manager;
|
||||
const auto page_size = page_manager.GetPageSize();
|
||||
auto *memory = static_cast<uint8_t *>(
|
||||
VirtualAlloc(reinterpret_cast<void *>(base), region_size * 2,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Check(memory == reinterpret_cast<void *>(base), "fixed VirtualAlloc failed");
|
||||
auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000);
|
||||
const auto address = reinterpret_cast<uint64_t>(memory);
|
||||
const auto boundary = (address + region_size - 1) & ~(region_size - 1);
|
||||
uint32_t ranges = 0;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
// Exercise the production module with real SDL streams and fake USB endpoints.
|
||||
#include <SDL3/SDL.h>
|
||||
#include <opus.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
#include <semaphore>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
|
@ -24,23 +30,32 @@ 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::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;
|
||||
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, wireless_pad3 = false;
|
||||
bool hid_available = false, fail_hid_write = false;
|
||||
bool duplicate_hid = false;
|
||||
int hid_opens = 0;
|
||||
int opened_hid_pad = 0;
|
||||
std::vector<std::array<uint8_t, 547>> hid_reports;
|
||||
std::atomic_bool block_opus_encode {false}, block_hid_write {false};
|
||||
std::atomic_int hid_closes {0};
|
||||
std::binary_semaphore encode_started {0}, allow_encode {0};
|
||||
std::binary_semaphore write_started {0}, allow_write {0};
|
||||
SDL_AudioStream* default_stream = nullptr;
|
||||
bool fail_default_resume = false;
|
||||
int active_controller = 1;
|
||||
} // namespace
|
||||
|
||||
namespace Fake {
|
||||
|
|
@ -116,8 +131,9 @@ void DestroyAudioStream(SDL_AudioStream* stream) {
|
|||
return;
|
||||
}
|
||||
const auto it = std::find(streams.begin(), streams.end(), stream);
|
||||
Check(it != streams.end(), "stream destroyed twice");
|
||||
streams.erase(it);
|
||||
if (it != streams.end()) {
|
||||
streams.erase(it);
|
||||
}
|
||||
if (stream == default_stream) {
|
||||
default_stream = nullptr;
|
||||
}
|
||||
|
|
@ -134,6 +150,73 @@ SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
|
|||
return id == 1 || id == 3 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
|
||||
: nullptr;
|
||||
}
|
||||
SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) {
|
||||
return GetGamepadTypeForID(static_cast<SDL_JoystickID>(reinterpret_cast<uintptr_t>(pad)));
|
||||
}
|
||||
Uint16 GetGamepadVendor(SDL_Gamepad*) {
|
||||
return 0x054c;
|
||||
}
|
||||
Uint16 GetGamepadProduct(SDL_Gamepad*) {
|
||||
return 0x0ce6;
|
||||
}
|
||||
const char* GetGamepadSerial(SDL_Gamepad* pad) {
|
||||
return reinterpret_cast<uintptr_t>(pad) == 3 ? "aa-bb-cc-dd-ee-03" : "aa-bb-cc-dd-ee-01";
|
||||
}
|
||||
Uint64 GetTicksNS() {
|
||||
return now * 1000000;
|
||||
}
|
||||
SDL_Thread* CreateThread(SDL_ThreadFunction, const char*, void*) {
|
||||
return reinterpret_cast<SDL_Thread*>(1);
|
||||
}
|
||||
void WaitThread(SDL_Thread*, int*) {}
|
||||
SDL_hid_device_info* HidEnumerate(unsigned short, unsigned short) {
|
||||
if (!hid_available) {
|
||||
return nullptr;
|
||||
}
|
||||
static wchar_t serial1[] = L"AA:BB:CC:DD:EE:01";
|
||||
static wchar_t serial3[] = L"AA:BB:CC:DD:EE:03";
|
||||
static char path1[] = "pad-1";
|
||||
static char path3[] = "pad-3";
|
||||
static SDL_hid_device_info pad3 {}, pad1 {};
|
||||
pad3.path = path3;
|
||||
pad3.serial_number = duplicate_hid ? serial1 : serial3;
|
||||
pad3.bus_type = SDL_HID_API_BUS_BLUETOOTH;
|
||||
pad1.path = path1;
|
||||
pad1.serial_number = serial1;
|
||||
pad1.bus_type = SDL_HID_API_BUS_BLUETOOTH;
|
||||
pad1.next = &pad3;
|
||||
return &pad1;
|
||||
}
|
||||
void HidFreeEnumeration(SDL_hid_device_info*) {}
|
||||
SDL_hid_device* HidOpenPath(const char* path) {
|
||||
hid_opens++;
|
||||
opened_hid_pad = path[4] == '1' ? 1 : 3;
|
||||
return reinterpret_cast<SDL_hid_device*>(static_cast<uintptr_t>(opened_hid_pad));
|
||||
}
|
||||
void HidClose(SDL_hid_device*) {
|
||||
hid_closes++;
|
||||
}
|
||||
int HidWrite(SDL_hid_device*, const unsigned char* data, size_t size) {
|
||||
if (block_hid_write.exchange(false)) {
|
||||
write_started.release();
|
||||
allow_write.acquire();
|
||||
}
|
||||
if (fail_hid_write || size != 547) {
|
||||
return -1;
|
||||
}
|
||||
std::array<uint8_t, 547> report {};
|
||||
std::copy_n(data, report.size(), report.begin());
|
||||
hid_reports.push_back(report);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
opus_int32 OpusEncodeFloat(OpusEncoder* encoder, const float* pcm, int frames,
|
||||
unsigned char* packet, opus_int32 bytes) {
|
||||
if (block_opus_encode.exchange(false)) {
|
||||
encode_started.release();
|
||||
allow_encode.acquire();
|
||||
}
|
||||
return ::opus_encode_float(encoder, pcm, frames, packet, bytes);
|
||||
}
|
||||
SDL_JoystickID* GetGamepads(int* count) {
|
||||
*count = static_cast<int>(connected_pads.size());
|
||||
auto* ids = static_cast<SDL_JoystickID*>(
|
||||
|
|
@ -142,8 +225,10 @@ SDL_JoystickID* GetGamepads(int* count) {
|
|||
ids[connected_pads.size()] = 0;
|
||||
return ids;
|
||||
}
|
||||
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad*) {
|
||||
return wireless ? SDL_JOYSTICK_CONNECTION_WIRELESS : SDL_JOYSTICK_CONNECTION_WIRED;
|
||||
SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad* pad) {
|
||||
return wireless || (wireless_pad3 && reinterpret_cast<uintptr_t>(pad) == 3)
|
||||
? 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);
|
||||
|
|
@ -173,22 +258,37 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
|
|||
}
|
||||
} // 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_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_GetGamepadType Fake::GetGamepadType
|
||||
#define SDL_GetGamepadVendor Fake::GetGamepadVendor
|
||||
#define SDL_GetGamepadProduct Fake::GetGamepadProduct
|
||||
#define SDL_GetGamepadSerial Fake::GetGamepadSerial
|
||||
#define SDL_GetTicksNS Fake::GetTicksNS
|
||||
#undef SDL_CreateThread
|
||||
#define SDL_CreateThread Fake::CreateThread
|
||||
#define SDL_WaitThread Fake::WaitThread
|
||||
#define SDL_hid_enumerate Fake::HidEnumerate
|
||||
#define SDL_hid_free_enumeration Fake::HidFreeEnumeration
|
||||
#define SDL_hid_open_path Fake::HidOpenPath
|
||||
#define SDL_hid_close Fake::HidClose
|
||||
#define SDL_hid_write Fake::HidWrite
|
||||
#define opus_encode_float Fake::OpusEncodeFloat
|
||||
#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/dualSenseBluetooth.cpp"
|
||||
#include "libs/dualSenseHaptics.cpp"
|
||||
#include "libs/audio.cpp"
|
||||
#undef SDL_GetTicks
|
||||
|
|
@ -202,6 +302,19 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) {
|
|||
#undef SDL_DestroyAudioStream
|
||||
#undef SDL_GetAudioStreamDevice
|
||||
#undef SDL_GetGamepadTypeForID
|
||||
#undef SDL_GetGamepadType
|
||||
#undef SDL_GetGamepadVendor
|
||||
#undef SDL_GetGamepadProduct
|
||||
#undef SDL_GetGamepadSerial
|
||||
#undef SDL_GetTicksNS
|
||||
#undef SDL_CreateThread
|
||||
#undef SDL_WaitThread
|
||||
#undef SDL_hid_enumerate
|
||||
#undef SDL_hid_free_enumeration
|
||||
#undef SDL_hid_open_path
|
||||
#undef SDL_hid_close
|
||||
#undef SDL_hid_write
|
||||
#undef opus_encode_float
|
||||
#undef SDL_GetGamepadFromID
|
||||
#undef SDL_RumbleGamepad
|
||||
#undef SDL_SendGamepadEffect
|
||||
|
|
@ -228,6 +341,7 @@ double GetTimeMs() {
|
|||
|
||||
namespace {
|
||||
namespace Haptics = Libs::Controller::DualSenseHaptics;
|
||||
namespace Bluetooth = Libs::Controller::DualSenseBluetooth;
|
||||
using Port = std::unique_ptr<Haptics::Stream, decltype(&Haptics::Close)>;
|
||||
constexpr std::array<int, 2> unity {32768, 32768};
|
||||
constexpr std::array<float, 4> pcm {0.5f, 0.5f, 0.5f, 0.5f};
|
||||
|
|
@ -240,11 +354,17 @@ struct Fixture {
|
|||
cached_rumble = {};
|
||||
effects = {};
|
||||
connected_pads = {1};
|
||||
fail_effect = wireless = false;
|
||||
default_stream = nullptr;
|
||||
fail_effect = wireless = wireless_pad3 = false;
|
||||
hid_available = fail_hid_write = false;
|
||||
duplicate_hid = false;
|
||||
hid_opens = 0;
|
||||
hid_closes = 0;
|
||||
opened_hid_pad = 0;
|
||||
hid_reports.clear();
|
||||
default_stream = nullptr;
|
||||
fail_default_resume = false;
|
||||
active_controller = 1;
|
||||
now = 1000;
|
||||
active_controller = 1;
|
||||
now = 1000;
|
||||
opens = rumble_calls = 0;
|
||||
fail_open = fail_resume = false;
|
||||
actual_channels = 4;
|
||||
|
|
@ -255,8 +375,8 @@ struct Fixture {
|
|||
}
|
||||
};
|
||||
|
||||
Port Open(bool speaker = false) {
|
||||
auto* stream = Haptics::Open(48000, speaker);
|
||||
Port Open(bool speaker = false, uint32_t freq = 48000) {
|
||||
auto* stream = Haptics::Open(freq, speaker);
|
||||
Check(stream != nullptr, "port open failed");
|
||||
return Port(stream, Haptics::Close);
|
||||
}
|
||||
|
|
@ -516,6 +636,446 @@ void TestSpeakerRequiresUnambiguousUsbDevice() {
|
|||
"USB to Bluetooth change retained the USB speaker");
|
||||
}
|
||||
|
||||
void TestBluetoothAudioAndIdentity() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
connected_pads = {1, 3};
|
||||
devices.clear(); // Bluetooth does not enumerate a quad USB audio endpoint.
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> haptic(2048, 0.5f);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::SetVibration(1, 100, 80) && pad_rumble[1].large == 100 * 0x101,
|
||||
"Bluetooth controller did not receive normal rumble");
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth haptics did not open");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not open");
|
||||
Check(hid_opens == 1 && opened_hid_pad == 1 && effects[1][7] == 0x30,
|
||||
"Bluetooth audio was not matched to the active controller");
|
||||
Check(pad_rumble[1].large == 0, "Bluetooth haptics did not suppress motor rumble");
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1, "Bluetooth streams were not combined in one report");
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(hid_reports.size() == 1, "Bluetooth audio report was not sent");
|
||||
const auto& report = hid_reports.back();
|
||||
Check(report[0] == 0x39 && report[2] == 0x91 && report[3] == 6 && report[4] == 0x7e &&
|
||||
report[10] == 0xd2 && report[11] == 64 && report[140] == 0xd3 && report[141] == 200,
|
||||
"Bluetooth audio sub-packets are malformed");
|
||||
Check(report[12] > 0 && report[13] > 0,
|
||||
"haptic waveform was not downsampled into the actuator channels");
|
||||
uint8_t prefix = 0xa2;
|
||||
auto crc = SDL_crc32(0, &prefix, 1);
|
||||
crc = SDL_crc32(crc, report.data(), report.size() - 4);
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
Check(report[report.size() - 4 + i] == static_cast<uint8_t>(crc >> (8 * i)),
|
||||
"Bluetooth audio CRC is invalid");
|
||||
}
|
||||
int opus_error = OPUS_OK;
|
||||
auto* decoder = opus_decoder_create(48000, 2, &opus_error);
|
||||
Check(decoder != nullptr && opus_error == OPUS_OK, "Opus decoder unavailable in test");
|
||||
std::array<float, 960> decoded {};
|
||||
Check(opus_decode_float(decoder, report.data() + 142, 200, decoded.data(), 480, 0) == 480,
|
||||
"Bluetooth speaker packet could not be decoded");
|
||||
opus_decoder_destroy(decoder);
|
||||
const auto mean = std::accumulate(decoded.begin(), decoded.end(), 0.0f) / decoded.size();
|
||||
Check(mean > 0.05f && mean < 0.35f, "Bluetooth speaker audio was not preserved");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"Bluetooth sender ignored report pacing");
|
||||
now += 300;
|
||||
Haptics::UpdateBluetoothRumble();
|
||||
Check(pad_rumble[1].large == 100 * 0x101,
|
||||
"Bluetooth haptics did not restore normal rumble afterward");
|
||||
|
||||
// A different active DualSense has a different HID address, even if both are
|
||||
// paired.
|
||||
Check(Haptics::Queue(vibration.get(), 3, haptic.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_hid_pad == 3,
|
||||
"Bluetooth stream followed the wrong controller");
|
||||
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not follow the selected controller");
|
||||
Check(effects[1][7] == 0, "old Bluetooth controller remained speaker-routed");
|
||||
wireless = false;
|
||||
connected_pads = {3};
|
||||
devices = {{20, "Speakers (DualSense Wireless Controller)", 4}};
|
||||
Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_device == 20 && effects[3][7] == 0x30,
|
||||
"Bluetooth to USB change did not reopen the USB speaker immediately");
|
||||
speaker.reset();
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothFailureFallsBack() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth port did not initially open");
|
||||
fail_hid_write = true;
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) == 0 &&
|
||||
effects[1][7] == 0,
|
||||
"failed HID write did not restore the normal audio fallback");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothFormatsAndRoutingFailure() {
|
||||
Fixture f;
|
||||
wireless = hid_available = fail_effect = true;
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
!Haptics::UsesBluetooth(speaker.get()) && hid_closes == 1 && effects[1].empty(),
|
||||
"failed Bluetooth speaker routing leaked a session or suppressed fallback");
|
||||
fail_effect = false;
|
||||
now += 2001;
|
||||
std::vector<int16_t> mono(1024, 16384);
|
||||
const int volume = 16384;
|
||||
Queue(speaker, mono.data(), 1024, 1, false, &volume);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 &&
|
||||
std::abs(pending[0].audio[200] - 0.25f) < 1e-4f &&
|
||||
std::abs(pending[0].audio[201] - 0.25f) < 1e-4f,
|
||||
"Bluetooth mono int16 volume mapping or speaker routing recovery failed");
|
||||
}
|
||||
|
||||
void TestBluetoothEncodingDoesNotBlockAudioQueue() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not open for concurrent encoding test");
|
||||
block_opus_encode = true;
|
||||
std::thread encoder([&] {
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
});
|
||||
encode_started.acquire();
|
||||
std::binary_semaphore queue_finished {0};
|
||||
std::atomic_bool queue_ok {false};
|
||||
std::thread audio([&] {
|
||||
queue_ok = Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0;
|
||||
queue_finished.release();
|
||||
});
|
||||
const bool queued_without_encoder =
|
||||
queue_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
allow_encode.release();
|
||||
audio.join();
|
||||
encoder.join();
|
||||
Check(queued_without_encoder && queue_ok,
|
||||
"Bluetooth audio queue waited for Opus encoding");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothCloseCancelsPreparedReport() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
const auto base = now * 1000000;
|
||||
auto pending = Bluetooth::Prepare(base);
|
||||
Check(pending.size() == 1, "Bluetooth report was not prepared before stream close");
|
||||
speaker.reset();
|
||||
Bluetooth::Send(pending);
|
||||
Check(hid_reports.empty(), "closed speaker's prepared report was sent");
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
Bluetooth::Send(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS));
|
||||
Check(hid_reports.size() == 1 && hid_reports.back()[140] == 0,
|
||||
"closing the speaker prevented the remaining haptic stream from sending");
|
||||
}
|
||||
|
||||
void TestBluetoothCloseDuringEncoding() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
block_opus_encode = true;
|
||||
std::thread encoder([&] { Bluetooth::Send(Bluetooth::Prepare(now * 1000000)); });
|
||||
encode_started.acquire();
|
||||
std::binary_semaphore close_finished {0};
|
||||
std::thread closer([&] {
|
||||
speaker.reset();
|
||||
close_finished.release();
|
||||
});
|
||||
const bool closed_without_encoder = close_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
const bool hid_alive_during_encode = hid_closes == 0;
|
||||
allow_encode.release();
|
||||
closer.join();
|
||||
encoder.join();
|
||||
Check(closed_without_encoder && hid_alive_during_encode && hid_closes == 1 &&
|
||||
hid_reports.empty(),
|
||||
"closing during encoding blocked, destroyed a live session, or sent stale audio");
|
||||
}
|
||||
|
||||
void TestBluetoothCloseWaitsForWriteWithoutBlockingQueue() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(speaker, sound.data(), 1024);
|
||||
Queue(vibration, sound.data(), 1024);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1, "Bluetooth report was not prepared before blocked write");
|
||||
block_hid_write = true;
|
||||
std::thread sender([&] { Bluetooth::Send(pending); });
|
||||
write_started.acquire();
|
||||
std::binary_semaphore close_started {0}, close_finished {0}, queue_finished {0};
|
||||
std::thread closer([&] {
|
||||
close_started.release();
|
||||
speaker.reset();
|
||||
close_finished.release();
|
||||
});
|
||||
close_started.acquire();
|
||||
const bool closed_before_write = close_finished.try_acquire_for(std::chrono::milliseconds(50));
|
||||
std::atomic_bool queue_ok {false};
|
||||
std::thread audio([&] {
|
||||
queue_ok =
|
||||
Haptics::Queue(vibration.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0;
|
||||
queue_finished.release();
|
||||
});
|
||||
const bool queued_without_write = queue_finished.try_acquire_for(std::chrono::seconds(2));
|
||||
allow_write.release();
|
||||
audio.join();
|
||||
closer.join();
|
||||
sender.join();
|
||||
Check(!closed_before_write && queued_without_write && queue_ok && hid_reports.size() == 1 &&
|
||||
effects[1][7] == 0,
|
||||
"close failed to drain the HID write independently of the audio queue");
|
||||
}
|
||||
|
||||
void TestBluetoothSpeakerWaitsForItsAudioBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> haptic(2048, 0.5f);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0,
|
||||
"Bluetooth vibration stream did not open");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"haptics sent a silent speaker packet before the audio batch arrived");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 256, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not accept a short block");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(),
|
||||
"short Bluetooth speaker block was padded with silence immediately");
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 768, 2, true, unity.data()) != 0,
|
||||
"Bluetooth speaker did not accept the remainder of its block");
|
||||
auto reports = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(reports.size() == 1 && reports[0].audio[100] > 0.2f && reports[0].data[12] > 0,
|
||||
"Bluetooth speaker and haptics were not combined into a complete report");
|
||||
speaker.reset();
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothShortHapticBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(vibration, sound.data(), 256);
|
||||
const auto base = now * 1000000;
|
||||
Check(Bluetooth::Prepare(base).empty() && Bluetooth::Prepare(base + 1000000).empty(),
|
||||
"short haptic block was padded with silence before its remainder arrived");
|
||||
Queue(vibration, sound.data(), 768);
|
||||
Check(Bluetooth::Prepare(base + 1000000).empty(),
|
||||
"completed haptic block skipped its speaker coalescing window");
|
||||
auto pending = Bluetooth::Prepare(base + 2000000);
|
||||
Check(pending.size() == 1 && pending[0].data[12 + 100] > 0,
|
||||
"complete haptic block did not play after its remainder arrived");
|
||||
}
|
||||
|
||||
void TestBluetoothIncrementalBatchCoalesces() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
auto speaker = Open(true);
|
||||
std::array<float, 512> sound;
|
||||
sound.fill(0.25f);
|
||||
for (int block = 0; block < 3; ++block) {
|
||||
Queue(vibration, sound.data(), 256);
|
||||
Queue(speaker, sound.data(), 256);
|
||||
Check(Bluetooth::Prepare(now * 1000000).empty(),
|
||||
"incremental Bluetooth batch was padded before it was complete");
|
||||
now += 5;
|
||||
}
|
||||
Queue(vibration, sound.data(), 256);
|
||||
Check(Bluetooth::Prepare(now * 1000000).empty(),
|
||||
"completed haptics sent before the same batch's final speaker block arrived");
|
||||
Queue(speaker, sound.data(), 256);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[1800] > 0.2f && pending[0].data[12 + 100] > 0,
|
||||
"completed incremental speaker and haptics batch did not play immediately");
|
||||
}
|
||||
|
||||
void TestBluetoothLowRateCompleteBlock() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true, 8000);
|
||||
std::array<float, 171 * 2> sound;
|
||||
sound.fill(0.25f);
|
||||
Queue(speaker, sound.data(), 171);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[1000] > 0.2f,
|
||||
"resampler lookahead delayed a complete low-rate speaker block");
|
||||
}
|
||||
|
||||
void TestBluetoothCompleteStreamDoesNotWaitForPartialPeer() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto complete = Open(true);
|
||||
auto partial = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(complete, sound.data(), 1024);
|
||||
Queue(partial, sound.data(), 256);
|
||||
auto pending = Bluetooth::Prepare(now * 1000000);
|
||||
Check(pending.size() == 1 && pending[0].audio[100] > 0.4f && pending[0].audio[1000] > 0.2f,
|
||||
"complete speaker stream waited for a partial peer instead of mixing available audio");
|
||||
}
|
||||
|
||||
void TestBluetoothShortFinalBlock() {
|
||||
for (bool speaker: {false, true}) {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto port = Open(speaker);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Queue(port, sound.data(), 256);
|
||||
const auto base = now * 1000000;
|
||||
Check(Bluetooth::Prepare(base).empty(),
|
||||
"short final Bluetooth block sent before more audio could arrive");
|
||||
Check(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS).size() == 1,
|
||||
"short final Bluetooth block was never played");
|
||||
}
|
||||
}
|
||||
|
||||
void TestBluetoothOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(sound.begin(), sound.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth speaker overflow queue failed");
|
||||
}
|
||||
auto first = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Check(first.size() == 1 && first[0].audio[200] > 0.15f && first[0].audio[200] < 0.25f,
|
||||
"Bluetooth queue overflow lost all buffered audio instead of the oldest block");
|
||||
auto second = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000 +
|
||||
Libs::Controller::DualSenseBluetooth::PERIOD_NS);
|
||||
Check(second.size() == 1 && second[0].audio[200] > 0.25f && second[0].audio[200] < 0.35f,
|
||||
"Bluetooth queue overflow did not preserve the next audio block");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothHapticOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto vibration = Open();
|
||||
std::vector<float> haptic(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(haptic.begin(), haptic.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth haptic overflow queue failed");
|
||||
}
|
||||
const auto base = now * 1000000;
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).empty(),
|
||||
"haptic-only report skipped its coalescing window");
|
||||
auto first = Libs::Controller::DualSenseBluetooth::Prepare(base + 1000000);
|
||||
Check(first.size() == 1 && first[0].data[12 + 32] > 18 && first[0].data[12 + 32] < 32,
|
||||
"Bluetooth haptic overflow discarded buffered waveform instead of one report");
|
||||
vibration.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothRecoversAfterLateWrite() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"Bluetooth speaker could not buffer three reports");
|
||||
}
|
||||
const auto base = now * 1000000;
|
||||
const auto period = Libs::Controller::DualSenseBluetooth::PERIOD_NS;
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base).size() == 1,
|
||||
"first Bluetooth report was not prepared");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + period + 10000000).size() == 1,
|
||||
"late Bluetooth report was not prepared");
|
||||
Check(Libs::Controller::DualSenseBluetooth::Prepare(base + 2 * period).size() == 1,
|
||||
"Bluetooth sender did not recover its schedule after a late write");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestUsbOverflowKeepsRecentAudio() {
|
||||
Fixture f;
|
||||
auto speaker = Open(true);
|
||||
std::vector<float> sound(2048);
|
||||
for (int block = 1; block <= 4; ++block) {
|
||||
std::fill(sound.begin(), sound.end(), block * 0.1f);
|
||||
Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true,
|
||||
unity.data()) != 0,
|
||||
"USB speaker overflow queue failed");
|
||||
}
|
||||
std::array<float, 4> first {};
|
||||
Check(!streams.empty() &&
|
||||
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
|
||||
sizeof(first) &&
|
||||
first[0] > 0.15f && first[0] < 0.25f,
|
||||
"USB queue overflow did not preserve the newest buffered audio");
|
||||
// A single oversized input must also preserve only its newest 80 ms.
|
||||
sound.assign(4096 * 2, 0.1f);
|
||||
std::fill(sound.begin() + 256 * 2, sound.end(), 0.5f);
|
||||
const auto queued = Haptics::Queue(speaker.get(), 1, sound.data(), 4096, 2, true, unity.data());
|
||||
Check(queued == 80000 && SDL_GetAudioStreamQueued(streams.back()) == 3840 * 4 * sizeof(float) &&
|
||||
SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) ==
|
||||
sizeof(first) &&
|
||||
first[0] == 0.5f && first[1] == 0.5f,
|
||||
"oversized USB block did not retain only the newest 80 ms");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestBluetoothAmbiguousDevice() {
|
||||
Fixture f;
|
||||
wireless = hid_available = duplicate_hid = true;
|
||||
auto speaker = Open(true);
|
||||
Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 &&
|
||||
hid_opens == 0 && effects[1].empty(),
|
||||
"ambiguous Bluetooth devices were routed arbitrarily");
|
||||
speaker.reset();
|
||||
}
|
||||
|
||||
void TestUsbAndBluetoothFeatureParity() {
|
||||
Fixture f;
|
||||
connected_pads = {1, 3};
|
||||
wireless_pad3 = hid_available = true;
|
||||
auto wired_speaker = Open(true);
|
||||
auto wireless_speaker = Open(true);
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
opened_device == 10,
|
||||
"Bluetooth pad disabled the wired pad speaker");
|
||||
Check(Haptics::Queue(wireless_speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) !=
|
||||
0 &&
|
||||
opened_hid_pad == 3,
|
||||
"wired pad disabled the Bluetooth speaker");
|
||||
Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 &&
|
||||
effects[1][7] == 0x30 && effects[3][7] == 0,
|
||||
"returning to the wired speaker left Bluetooth routing active");
|
||||
wireless_speaker.reset();
|
||||
wired_speaker.reset();
|
||||
}
|
||||
|
||||
void TestAudioSpeakerFallback() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
|
|
@ -545,6 +1105,26 @@ void TestAudioSpeakerRouting() {
|
|||
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
|
||||
}
|
||||
|
||||
void TestBluetoothAudioSpeakerRouting() {
|
||||
Fixture f;
|
||||
wireless = hid_available = true;
|
||||
devices.clear();
|
||||
Libs::Audio::Audio audio;
|
||||
const auto port = audio.AudioOutOpen(4, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
|
||||
Check(port.IsValid() && default_stream != nullptr, "Bluetooth pad speaker port did not open");
|
||||
std::vector<float> sound(2048, 0.25f);
|
||||
Libs::Audio::Audio::OutputParam output {port, sound.data()};
|
||||
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && hid_opens == 1 &&
|
||||
SDL_GetAudioStreamQueued(default_stream) == 0,
|
||||
"Bluetooth pad speaker also played on the host output");
|
||||
fail_hid_write = true;
|
||||
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
|
||||
Libs::Controller::DualSenseBluetooth::Send(pending);
|
||||
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 &&
|
||||
SDL_GetAudioStreamQueued(default_stream) > 0,
|
||||
"Bluetooth write failure did not use the normal speaker output");
|
||||
}
|
||||
|
||||
void TestAudioDefaultResumeFailure() {
|
||||
Fixture f;
|
||||
Libs::Audio::Audio audio;
|
||||
|
|
@ -564,6 +1144,7 @@ int main() {
|
|||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
TestAudioSpeakerFallback();
|
||||
TestAudioSpeakerRouting();
|
||||
TestBluetoothAudioSpeakerRouting();
|
||||
TestAudioDefaultResumeFailure();
|
||||
TestFormatsAndVolume();
|
||||
TestDiscoveryAndHotplug();
|
||||
|
|
@ -571,6 +1152,25 @@ int main() {
|
|||
TestSpeakerUnplug();
|
||||
TestSpeakerSwitchAndFailures();
|
||||
TestSpeakerRequiresUnambiguousUsbDevice();
|
||||
TestBluetoothAudioAndIdentity();
|
||||
TestBluetoothFailureFallsBack();
|
||||
TestBluetoothFormatsAndRoutingFailure();
|
||||
TestBluetoothEncodingDoesNotBlockAudioQueue();
|
||||
TestBluetoothCloseCancelsPreparedReport();
|
||||
TestBluetoothCloseDuringEncoding();
|
||||
TestBluetoothCloseWaitsForWriteWithoutBlockingQueue();
|
||||
TestBluetoothSpeakerWaitsForItsAudioBlock();
|
||||
TestBluetoothShortHapticBlock();
|
||||
TestBluetoothIncrementalBatchCoalesces();
|
||||
TestBluetoothLowRateCompleteBlock();
|
||||
TestBluetoothCompleteStreamDoesNotWaitForPartialPeer();
|
||||
TestBluetoothShortFinalBlock();
|
||||
TestBluetoothOverflowKeepsRecentAudio();
|
||||
TestBluetoothHapticOverflowKeepsRecentAudio();
|
||||
TestBluetoothRecoversAfterLateWrite();
|
||||
TestUsbOverflowKeepsRecentAudio();
|
||||
TestBluetoothAmbiguousDevice();
|
||||
TestUsbAndBluetoothFeatureParity();
|
||||
TestFailuresAndBoundedQueue();
|
||||
TestRumbleLeaseAndDuration();
|
||||
TestSwitchStopsOldRumble();
|
||||
|
|
|
|||
|
|
@ -192,13 +192,25 @@ uint8_t *Allocate(uint64_t size, uint32_t protection = PAGE_READWRITE,
|
|||
Check(memory == reinterpret_cast<void *>(test_address),
|
||||
"fixed low VirtualAlloc failed");
|
||||
#elif defined(__APPLE__)
|
||||
mach_vm_address_t raw = test_address;
|
||||
Check(mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) ==
|
||||
KERN_SUCCESS &&
|
||||
mach_vm_protect(mach_task_self(), raw, size, false,
|
||||
static_cast<vm_prot_t>(ToHostProt(protection))) ==
|
||||
KERN_SUCCESS,
|
||||
"fixed low mach_vm_allocate failed");
|
||||
mach_vm_address_t raw = 0;
|
||||
bool allocated = false;
|
||||
constexpr uintptr_t stride = 0x0000000100000000ull;
|
||||
for (uintptr_t candidate = test_address;
|
||||
Libs::Graphics::GuestRange{candidate, size}.Valid(); candidate += stride) {
|
||||
raw = candidate;
|
||||
if (mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) !=
|
||||
KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
if (mach_vm_protect(mach_task_self(), raw, size, false,
|
||||
static_cast<vm_prot_t>(ToHostProt(protection))) ==
|
||||
KERN_SUCCESS) {
|
||||
allocated = true;
|
||||
break;
|
||||
}
|
||||
mach_vm_deallocate(mach_task_self(), raw, size);
|
||||
}
|
||||
Check(allocated, "no free fixed guest address found");
|
||||
auto *memory = reinterpret_cast<uint8_t *>(raw);
|
||||
AllocationSizes()[memory] = static_cast<size_t>(size);
|
||||
#else
|
||||
|
|
|
|||
Loading…
Reference in New Issue