diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da2675f2..91f46e93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,7 +113,7 @@ jobs: shell: cmd run: | cmake --build _Build/windows --target launcher ^ - audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^ + audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^ shader_recompiler_compute_tests ^ resource_tracking_tests scalar_provenance_tests ^ resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel @@ -121,7 +121,7 @@ jobs: - name: Test shell: cmd run: | - ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$" + ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$" - name: Install shell: cmd @@ -202,7 +202,7 @@ jobs: shell: bash run: | cmake --build _Build/macos \ - --target launcher audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ + --target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \ resource_tracking_tests scalar_provenance_tests \ resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \ @@ -212,7 +212,7 @@ jobs: shell: bash run: | ctest --test-dir _Build/macos --output-on-failure \ - -R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$' + -R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$' - name: Install shell: bash @@ -386,7 +386,7 @@ jobs: run: | cmake --build _Build/linux \ --target launcher page_manager_tests memory_tracker_tests \ - audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ + audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \ resource_tracking_tests scalar_provenance_tests \ resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \ --parallel @@ -395,7 +395,7 @@ jobs: shell: bash run: | ctest --test-dir _Build/linux --output-on-failure \ - -R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$' + -R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$' - name: Install shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 97da78dd..799099f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -498,6 +498,12 @@ add_executable(pad_haptics_tests EXCLUDE_FROM_ALL target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus) target_include_directories(pad_haptics_tests PRIVATE ${inc_headers}) +add_executable(controller_settings_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/ControllerSettingsTests.cpp" +) +target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static) +target_include_directories(controller_settings_tests PRIVATE ${inc_headers}) + add_executable(save_data_memory_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp" ) @@ -613,6 +619,7 @@ if(BUILD_TESTING) add_test(NAME audio_out_timing COMMAND $) add_test(NAME ngs2_sampler COMMAND $) add_test(NAME pad_haptics COMMAND $) + add_test(NAME controller_settings COMMAND $) add_test(NAME save_data_memory COMMAND $) add_test(NAME png_enc COMMAND $) add_test(NAME ces COMMAND $) @@ -690,6 +697,7 @@ if(BUILD_TESTING) audio_out_timing_tests ngs2_sampler_tests pad_haptics_tests + controller_settings_tests save_data_memory_tests png_enc_tests ces_tests diff --git a/src/graphics/presentation/window/hostInput.cpp b/src/graphics/presentation/window/hostInput.cpp index c8b37547..425e0168 100644 --- a/src/graphics/presentation/window/hostInput.cpp +++ b/src/graphics/presentation/window/hostInput.cpp @@ -25,6 +25,8 @@ struct ControlInfo { Controller::Axis axis = Controller::Axis::AxisMax; bool positive = false; float touch_x = 0.0f; + + Controller::Setting setting {}; }; static constexpr std::array CONTROL_INFO = { @@ -55,6 +57,10 @@ static constexpr std::array CONTROL_INFO = { ControlInfo {"RightStickRight", 0, Controller::Axis::RightX, true}, ControlInfo {"RightStickUp", 0, Controller::Axis::RightY, false}, ControlInfo {"RightStickDown", 0, Controller::Axis::RightY, true}, + ControlInfo {.name = "SpeakerVolume", .setting = Controller::Setting::SpeakerVolume}, + ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity}, + ControlInfo {.name = "TriggerEffectIntensity", + .setting = Controller::Setting::TriggerEffectIntensity}, }; constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size(); @@ -242,6 +248,12 @@ void SetStickAxis(Controller::Axis axis, bool negative, bool positive) { Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value); } +void CycleSettingOnPress(Controller::Setting setting, bool down) { + if (down) { + Controller::CycleSetting(setting); + } +} + void SetControl(std::size_t control, bool down) { if (control == INVALID_CONTROL) { return; @@ -256,6 +268,10 @@ void SetControl(std::size_t control, bool down) { SetButton(info.button, down); return; } + if (info.axis == Controller::Axis::AxisMax) { + CycleSettingOnPress(info.setting, down); + return; + } struct AxisKeys { bool negative = false; @@ -280,6 +296,9 @@ void DefaultKeyboardInput(int key_code, bool down) { switch (NormalizeKey(static_cast(key_code))) { case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return; case SDLK_TAB: SetTouchPad(0.75f, down); return; + case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return; + case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return; + case SDLK_3: CycleSettingOnPress(Controller::Setting::TriggerEffectIntensity, down); return; case SDLK_A: left.left = down; SetStickAxis(Controller::Axis::LeftX, left.left, left.right); diff --git a/src/launcher/src/inputMappingDialog.cpp b/src/launcher/src/inputMappingDialog.cpp index 1821de3d..9dbc932f 100644 --- a/src/launcher/src/inputMappingDialog.cpp +++ b/src/launcher/src/inputMappingDialog.cpp @@ -53,6 +53,9 @@ constexpr PadControl PAD_CONTROLS[] = { {"Options", "Options", "Return"}, {"TouchPad", "Touch pad left (SELECT)", "Backspace"}, {"TouchPadRight", "Touch pad right (START)", "Tab"}, + {"SpeakerVolume", "Speaker volume (cycle)", "1"}, + {"VibrationIntensity", "Vibration intensity (cycle)", "2"}, + {"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"}, }; QString KeypadName(int key) { diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 216084b9..7c7bdc2c 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -159,8 +159,8 @@ private: static void OpenSdlDevice(PortIn* port, Format format); static void CloseSdlDevice(PortIn* port); static const void* PrepareOutputBuffer(const PortOut& port, const void* data, - std::vector* buffer); - static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking); + std::vector* buffer, float gain); + static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain); }; static Audio* g_audio = nullptr; @@ -302,7 +302,7 @@ void Audio::CloseSdlDevice(PortOut* port) { } const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, - std::vector* buffer) { + std::vector* buffer, float gain) { EXIT_IF(data == nullptr); EXIT_IF(buffer == nullptr); @@ -312,7 +312,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, const auto bytes_per_sample = BytesPerSample(port.format); const bool reorder = channels >= 8 && !FormatIsStd(port.format); - bool volume_changed = false; + bool volume_changed = gain != 1.0f; for (uint32_t ch = 0; ch < channels; ch++) { if (port.volume[ch] != 32768) { volume_changed = true; @@ -338,7 +338,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch; dst[frame * output_channels + ch] = src[frame * channels + src_ch] * - (static_cast(port.volume[src_ch]) / 32768.0f); + (static_cast(port.volume[src_ch]) / 32768.0f) * gain; } if (channels == 12) { // Add the four top channels to their front/back channels, turning 12 into 8. @@ -347,7 +347,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, for (uint32_t ch = 0; ch < 4; ch++) { dst[frame * output_channels + HEIGHT_DST[ch]] += src[frame * channels + 8 + ch] * - (static_cast(port.volume[8 + ch]) / 32768.0f); + (static_cast(port.volume[8 + ch]) / 32768.0f) * gain; } } } @@ -360,6 +360,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch; int64_t sample = static_cast(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768; + sample = static_cast(sample * static_cast(gain)); if (sample > std::numeric_limits::max()) { sample = std::numeric_limits::max(); } else if (sample < std::numeric_limits::min()) { @@ -373,7 +374,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, return buffer->data(); } -bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) { +bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) { EXIT_IF(port == nullptr); if (port->stream == nullptr || data == nullptr) { @@ -381,7 +382,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) { } std::vector prepared_buffer; - const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer); + const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain); const auto output_channels = OutputChannels(*port); const auto prepared_size = BytesPerSample(port->format) * output_channels * port->samples_num; @@ -554,6 +555,12 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking for (uint32_t i = 0; i < num; i++) { auto& port = m_out_ports[params[i].handle.GetId()]; + const float gain = + port.type == AUDIO_OUT_PORT_TYPE_PADSPK + ? Controller::GetSettingScale(Controller::Setting::SpeakerVolume) + : port.type == AUDIO_OUT_PORT_TYPE_VIBRATION + ? Controller::GetSettingScale(Controller::Setting::VibrationIntensity) + : 1.0f; uint64_t controller_queued_us = 0; bool controller_uses_bluetooth = false; @@ -562,14 +569,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking Common::LockGuard lock(m_mutex); controller_queued_us = Controller::DualSenseHaptics::Queue( port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num, - static_cast(port.channels_num), FormatIsFloat(port.format), port.volume); + static_cast(port.channels_num), FormatIsFloat(port.format), port.volume, + gain); 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 // output instead, and a vibration port has none. - if (QueueSdlAudio(&port, params[i].data, blocking)) { + if (QueueSdlAudio(&port, params[i].data, blocking, gain)) { any_device = true; paced[i] = port.queue_primed; } diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index 901cf72b..2492a4ae 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -117,6 +118,8 @@ public: int GetActiveControllerId(); void SetLightBar(uint8_t r, uint8_t g, uint8_t b); bool SetTriggerEffect(const PadTriggerEffectParam& param); + void CycleSetting(Setting setting); + float GetSettingScale(Setting setting) const; void ReadState(ControllerState* state, bool* flag, int* count); int ReadStates(ControllerState* states, int states_num, bool* flag, int* count); @@ -125,6 +128,9 @@ private: void CheckActive(); void AddState(); + // Apply cached output without changing its game-requested lifetime; caller holds m_mutex. + void ApplyVibration(); + bool SendTriggerEffect(const PadTriggerEffectParam& param); Common::Mutex m_mutex; std::vector m_connected_ids; @@ -142,6 +148,12 @@ private: uint32_t m_states_num = 0; uint32_t m_first_state = 0; uint8_t m_next_touch_id = 1; + // The game's last requests, re-sent when their intensity setting changes. + std::array m_vibration {}; + uint64_t m_vibration_until = 0; + PadTriggerEffectParam m_trigger_effect {}; + // Setting changes share the output lock; audio only needs an atomic scale snapshot. + std::array, 3> m_setting_steps {}; }; static GameController* g_controller = nullptr; @@ -183,17 +195,56 @@ static void pad_fill_data(PadData* data, const ControllerState& state, bool conn data->device_unique_data_len = 0; } -static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint8_t type, - uint8_t frequency = 0) { +// In cycle order, starting at the PS5 default. +constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f}; +constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f}; + +void CycleSetting(Setting setting) { + if (g_controller != nullptr) { + g_controller->CycleSetting(setting); + } +} + +float GetSettingScale(Setting setting) { + return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f; +} + +void GameController::CycleSetting(Setting setting) { + Common::LockGuard lock(m_mutex); + auto& step = m_setting_steps[static_cast(setting)]; + const auto count = setting == Setting::SpeakerVolume ? SPEAKER_VOLUME.size() : INTENSITY.size(); + step.store((step.load(std::memory_order_relaxed) + 1) % count, std::memory_order_relaxed); + if (setting == Setting::VibrationIntensity) { + ApplyVibration(); + } else if (setting == Setting::TriggerEffectIntensity) { + (void)SendTriggerEffect(m_trigger_effect); + } +} + +float GameController::GetSettingScale(Setting setting) const { + const auto step = m_setting_steps[static_cast(setting)].load(std::memory_order_relaxed); + return setting == Setting::SpeakerVolume ? SPEAKER_VOLUME[step] : INTENSITY[step]; +} + +static uint8_t Scale(uint8_t value, float scale) { + if (value == 0 || scale == 0.0f) { + return 0; + } + return static_cast(std::max(1L, std::lround(value * scale))); +} + +static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, float scale, + uint8_t type, uint8_t frequency = 0) { uint16_t active = 0; uint32_t packed = 0; for (int i = 0; i < 10; i++) { if (strengths[i] > 8) { return false; } - if (strengths[i] != 0) { + const auto strength = Scale(strengths[i], scale); + if (strength != 0) { active |= static_cast(1u << i); - packed |= static_cast(strengths[i] - 1u) << (3 * i); + packed |= static_cast(strength - 1u) << (3 * i); } } @@ -208,7 +259,8 @@ static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint return true; } -static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect) { +static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect, + float scale) { std::memset(effect, 0, 11); effect[0] = 0x05; @@ -220,7 +272,7 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, return false; } std::fill(strengths + command.data[0], strengths + 10, command.data[1]); - return trigger_effect_zones(effect, strengths, 0x21); + return trigger_effect_zones(effect, strengths, scale, 0x21); case 2: { const auto start = command.data[0]; const auto end = command.data[1]; @@ -228,14 +280,15 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, if (start < 2 || start > 7 || end <= start || end > 8 || strength > 8) { return false; } - if (strength == 0) { + const auto scaled = Scale(strength, scale); + if (scaled == 0) { return true; } const uint16_t zones = static_cast((1u << start) | (1u << end)); effect[0] = 0x25; effect[1] = static_cast(zones); effect[2] = static_cast(zones >> 8u); - effect[3] = strength - 1u; + effect[3] = scaled - 1u; return true; } case 3: @@ -243,8 +296,8 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, return false; } std::fill(strengths + command.data[0], strengths + 10, command.data[1]); - return trigger_effect_zones(effect, strengths, 0x26, command.data[2]); - case 4: return trigger_effect_zones(effect, command.data, 0x21); + return trigger_effect_zones(effect, strengths, scale, 0x26, command.data[2]); + case 4: return trigger_effect_zones(effect, command.data, scale, 0x21); case 5: { const int start = command.data[0]; const int end = command.data[1]; @@ -262,9 +315,9 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, : start_strength + (delta + (delta < 0 ? -length / 2 : length / 2)) / length); } - return trigger_effect_zones(effect, strengths, 0x21); + return trigger_effect_zones(effect, strengths, scale, 0x21); } - case 6: return trigger_effect_zones(effect, command.data + 1, 0x26, command.data[0]); + case 6: return trigger_effect_zones(effect, command.data + 1, scale, 0x26, command.data[0]); default: return false; } } @@ -355,6 +408,9 @@ void GameController::CheckActive() { m_states_num = 0; m_first_state = 0; m_next_touch_id = 1; + m_vibration = {}; + m_vibration_until = 0; + m_trigger_effect = {}; } void GameController::AddState() { @@ -600,11 +656,24 @@ void GameController::ReleaseHostPads() { } m_connected_ids.clear(); + CheckActive(); } void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) { Common::LockGuard lock(m_mutex); - if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) { + m_vibration = {large_motor, small_motor}; + m_vibration_until = SDL_GetTicks() + RUMBLE_DURATION_MS; + ApplyVibration(); +} + +void GameController::ApplyVibration() { + const auto now = SDL_GetTicks(); + const auto duration = + m_vibration_until > now ? static_cast(m_vibration_until - now) : 0; + const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f; + const auto large_motor = Scale(m_vibration[0], scale); + const auto small_motor = Scale(m_vibration[1], scale); + if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) { return; } @@ -619,7 +688,7 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) { const auto large = static_cast(large_motor * 0x101U); const auto small = static_cast(small_motor * 0x101U); - if (!SDL_RumbleGamepad(pad, large, small, RUMBLE_DURATION_MS)) { + if (!SDL_RumbleGamepad(pad, large, small, duration)) { LOGF("\t rumble failed: %s\n", SDL_GetError()); } } @@ -643,20 +712,35 @@ void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) { } bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) { + Common::LockGuard lock(m_mutex); + if (!SendTriggerEffect(param)) { + return false; + } + for (int i = 0; i < 2; i++) { + if ((param.trigger_mask & (1u << i)) != 0) { + m_trigger_effect.trigger_mask |= static_cast(1u << i); + m_trigger_effect.command[i] = param.command[i]; + } + } + return true; +} + +bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) { if ((param.trigger_mask & ~0x03u) != 0) { return false; } DualSenseEffects effect {}; + const auto scale = GetSettingScale(Setting::TriggerEffectIntensity); if ((param.trigger_mask & 0x01u) != 0) { effect.enable_bits |= 0x08; - if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger)) { + if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger, scale)) { return false; } } if ((param.trigger_mask & 0x02u) != 0) { effect.enable_bits |= 0x04; - if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger)) { + if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger, scale)) { return false; } } @@ -664,7 +748,6 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) { return true; } - Common::LockGuard lock(m_mutex); auto* pad = SDL_GetGamepadFromID(static_cast(m_active_id)); if (pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) { (void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect)); diff --git a/src/libs/controller.h b/src/libs/controller.h index 217e23cd..6c83c5ae 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -75,6 +75,11 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us); void ResetInputState(); int GetActiveControllerId(); +enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity }; + +void CycleSetting(Setting setting); +float GetSettingScale(Setting setting); + int KYTY_SYSV_ABI PadInit(); int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param); int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index); diff --git a/src/libs/dualSenseHaptics.cpp b/src/libs/dualSenseHaptics.cpp index 7e89626b..a698ed5e 100644 --- a/src/libs/dualSenseHaptics.cpp +++ b/src/libs/dualSenseHaptics.cpp @@ -311,7 +311,7 @@ bool UsesBluetooth(const Stream* stream) { } uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels, - bool is_float, const int* volume) { + bool is_float, const int* volume, float gain) { if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) { return 0; } @@ -353,7 +353,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames const auto index = (static_cast(first_frame) + frame) * channels + src_ch; float value = is_float ? static_cast(data)[index] : static_cast(data)[index] / 32768.0f; - value *= volume[src_ch] / 32768.0f; + value *= volume[src_ch] / 32768.0f * gain; // USB places the speaker in front and the actuators in back; Bluetooth takes stereo. stream->buffer[static_cast(frame) * output_channels + ch + channel_offset] = value; @@ -382,7 +382,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames return queued_us; } -bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) { +bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) { if (SDL_GetGamepadTypeForID(static_cast(controller)) != SDL_GAMEPAD_TYPE_PS5) { return false; } @@ -390,7 +390,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) { SelectController(controller); g_large_motor = large_motor; g_small_motor = small_motor; - g_rumble_until = SDL_GetTicks() + 0xffff; + g_rumble_until = SDL_GetTicks() + duration_ms; ApplyRumble(); return true; } diff --git a/src/libs/dualSenseHaptics.h b/src/libs/dualSenseHaptics.h index 6a2016b2..e5475ace 100644 --- a/src/libs/dualSenseHaptics.h +++ b/src/libs/dualSenseHaptics.h @@ -17,9 +17,10 @@ bool UsesBluetooth(const Stream* stream); // (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); + bool is_float, const int* volume, float gain = 1.0f); // Returns false for other gamepad types, which retain the normal rumble path. -bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor); +bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor, + uint32_t duration_ms = 0xffff); // Also restores the headphone routing. void Shutdown(); diff --git a/tests/AudioOutTimingTests.cpp b/tests/AudioOutTimingTests.cpp index bcf65521..c0d4818f 100644 --- a/tests/AudioOutTimingTests.cpp +++ b/tests/AudioOutTimingTests.cpp @@ -135,6 +135,9 @@ namespace Libs::Controller { int GetActiveControllerId() { return 0; } +float GetSettingScale(Setting) { + return 1.0f; +} namespace DualSenseHaptics { Stream* Open(uint32_t, bool) { @@ -145,7 +148,7 @@ void Close(Stream*) {} 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*) { +uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float) { return stream != nullptr ? pad_queue_us : 0; } } // namespace DualSenseHaptics diff --git a/tests/ControllerSettingsTests.cpp b/tests/ControllerSettingsTests.cpp new file mode 100644 index 00000000..513bf22c --- /dev/null +++ b/tests/ControllerSettingsTests.cpp @@ -0,0 +1,296 @@ +// Exercise the production controller with deterministic host output and time. +#include +#include +#include +#include +#include +#include + +namespace { +void Check(bool condition, const char* text) { + if (!condition) { + std::fprintf(stderr, "ControllerSettingsTests: %s\n", text); + std::abort(); + } +} + +struct Rumble { + Uint16 large, small; + Uint32 duration; +}; +Uint64 now = 1000; +std::vector rumble; +std::vector haptics; +std::vector> effects; +bool haptics_handles_rumble = false; +} // namespace + +namespace Fake { +Uint64 GetTicks() { + return now; +} +SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) { + return id == 1 || id == 2 ? reinterpret_cast(static_cast(id)) + : nullptr; +} +SDL_GamepadType GetGamepadType(SDL_Gamepad*) { + return SDL_GAMEPAD_TYPE_PS5; +} +bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) { + rumble.push_back({large, small, duration}); + return true; +} +bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) { + Check(size == 32, "unexpected DualSense effect size"); + std::array effect {}; + std::memcpy(effect.data(), data, effect.size()); + effects.push_back(effect); + return true; +} +bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) { + return true; +} +bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) { + return false; +} +bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) { + return true; +} +void CloseGamepad(SDL_Gamepad*) {} +void Delay(Uint32) {} +} // namespace Fake + +#define SDL_GetTicks Fake::GetTicks +#define SDL_GetGamepadFromID Fake::GetGamepadFromID +#define SDL_GetGamepadType Fake::GetGamepadType +#define SDL_RumbleGamepad Fake::RumbleGamepad +#define SDL_SendGamepadEffect Fake::SendGamepadEffect +#define SDL_SetGamepadLED Fake::SetGamepadLED +#define SDL_GamepadHasSensor Fake::GamepadHasSensor +#define SDL_SetGamepadSensorEnabled Fake::SetGamepadSensorEnabled +#define SDL_CloseGamepad Fake::CloseGamepad +#define SDL_Delay Fake::Delay +#include "libs/controller.cpp" +#undef SDL_GetTicks +#undef SDL_GetGamepadFromID +#undef SDL_GetGamepadType +#undef SDL_RumbleGamepad +#undef SDL_SendGamepadEffect +#undef SDL_SetGamepadLED +#undef SDL_GamepadHasSensor +#undef SDL_SetGamepadSensorEnabled +#undef SDL_CloseGamepad +#undef SDL_Delay + +namespace Libs::Controller::DualSenseHaptics { +bool SetVibration(int, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) { + haptics.push_back({large_motor, small_motor, duration_ms}); + return haptics_handles_rumble; +} +void Shutdown() {} +} // namespace Libs::Controller::DualSenseHaptics + +namespace Libs::LibKernel { +uint64_t KYTY_SYSV_ABI KernelGetProcessTime() { + return now * 1000; +} +} // namespace Libs::LibKernel + +namespace Loader::Timer { +double GetTimeMs() { + return static_cast(now); +} +} // namespace Loader::Timer + +namespace { +using namespace Libs::Controller; + +struct Controller { + Controller() { + now = 1000; + haptics_handles_rumble = false; + Initialize(); + Connect(1); + Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f && + GetSettingScale(Setting::VibrationIntensity) == 1.0f && + GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f, + "controller initialization retained old settings"); + rumble.clear(); + haptics.clear(); + effects.clear(); + } + ~Controller() { Shutdown(); } +}; + +DualSenseEffects LastEffect() { + Check(!effects.empty(), "missing trigger output"); + DualSenseEffects effect {}; + std::memcpy(&effect, effects.back().data(), sizeof(effect)); + return effect; +} + +int ZoneStrength(const uint8_t* effect, int zone) { + const unsigned active = effect[1] | (effect[2] << 8u); + const uint32_t packed = effect[3] | (effect[4] << 8u) | (effect[5] << 16u) | + (static_cast(effect[6]) << 24u); + return (active & (1u << zone)) != 0 ? 1 + ((packed >> (3 * zone)) & 7u) : 0; +} + +void SetRumble(uint8_t large, uint8_t small) { + const PadVibrationParam param {large, small}; + Check(PadSetVibration(1, ¶m) == 0, "vibration request failed"); +} + +void TestSettingCycles() { + Controller controller; + for (auto setting: + {Setting::SpeakerVolume, Setting::VibrationIntensity, Setting::TriggerEffectIntensity}) { + Check(GetSettingScale(setting) == 1.0f, "initial setting is not strong"); + const std::vector levels = setting == Setting::SpeakerVolume + ? std::vector {0.0f, 0.02f, 0.12f, 0.42f, 1.0f} + : std::vector {0.0f, 0.33f, 0.66f, 1.0f}; + for (int cycle = 0; cycle < 2; ++cycle) { + for (float level: levels) { + CycleSetting(setting); + Check(GetSettingScale(setting) == level, "setting did not cycle or wrap"); + } + } + } + CycleSetting(Setting::SpeakerVolume); + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::TriggerEffectIntensity); +} + +void TestVibrationLifetime() { + Controller controller; + SetRumble(255, 1); + Check(rumble.size() == 1 && rumble.back().large == 65535 && rumble.back().small == 257 && + rumble.back().duration == 65535, + "full vibration changed"); + now += 100; + CycleSetting(Setting::VibrationIntensity); + Check(rumble.back().large == 0 && rumble.back().small == 0, "muting did not stop vibration"); + now += 100; + CycleSetting(Setting::VibrationIntensity); + Check(rumble.back().large == 84 * 257 && rumble.back().small == 257 && + rumble.back().duration == 65335, + "changing intensity lost a small motor or extended the vibration deadline"); + Check(effects.empty(), "vibration intensity resent triggers"); + now = 1000 + 65535; + const auto calls = rumble.size(); + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::VibrationIntensity); + Check(rumble.size() == calls || (rumble.back().large == 0 && rumble.back().small == 0), + "expired vibration was resurrected"); + SetRumble(0, 0); + const auto stopped = rumble.size(); + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::VibrationIntensity); + Check(rumble.size() == stopped || (rumble.back().large == 0 && rumble.back().small == 0), + "explicitly stopped vibration was resurrected"); +} + +void TestMaskedTriggersAndValidation() { + Controller controller; + PadTriggerEffectParam param {}; + param.trigger_mask = 3; + param.command[0].mode = 1; + param.command[0].data[0] = 2; + param.command[0].data[1] = 8; + param.command[1].mode = 2; + param.command[1].data[0] = 2; + param.command[1].data[1] = 7; + param.command[1].data[2] = 6; + Check(PadSetTriggerEffect(1, ¶m) == 0, "initial trigger request failed"); + CycleSetting(Setting::TriggerEffectIntensity); + Check(LastEffect().left_trigger[0] == 5 && LastEffect().right_trigger[0] == 5, + "muting did not disable both triggers"); + for (uint32_t mode = 1; mode <= 7; ++mode) { + auto invalid = param; + invalid.command[1] = {}; + invalid.command[1].mode = mode; + std::memset(invalid.command[1].data, 255, sizeof(invalid.command[1].data)); + const auto calls = effects.size(); + Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG && effects.size() == calls, + "muting bypassed trigger validation or sent a partial invalid request"); + } + param.trigger_mask = 4; + Check(PadSetTriggerEffect(1, ¶m) == PAD_ERROR_INVALID_ARG, "invalid trigger mask accepted"); + param.trigger_mask = 1; + param.command[0].data[1] = 6; + Check(PadSetTriggerEffect(1, ¶m) == 0 && LastEffect().enable_bits == 8, + "left-only update touched the right trigger"); + CycleSetting(Setting::TriggerEffectIntensity); + auto effect = LastEffect(); + Check(effect.enable_bits == 12 && effect.left_trigger[0] == 0x21 && + ZoneStrength(effect.left_trigger, 1) == 0 && + ZoneStrength(effect.left_trigger, 2) == 2 && effect.right_trigger[0] == 0x25 && + effect.right_trigger[1] == 0x84 && effect.right_trigger[3] == 1, + "masked updates lost a cached trigger or failed to scale its strength"); + CycleSetting(Setting::TriggerEffectIntensity); + CycleSetting(Setting::TriggerEffectIntensity); + effect = LastEffect(); + Check(ZoneStrength(effect.left_trigger, 2) == 6 && effect.right_trigger[3] == 5, + "restoring strong intensity did not recover original trigger strengths"); + Check(rumble.empty() && haptics.empty(), "trigger intensity resent vibration"); +} + +void TestIndependentOutputsAndPadSwitch() { + Controller controller; + haptics_handles_rumble = true; + SetRumble(200, 100); + now += 100; + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::VibrationIntensity); + Check(rumble.empty() && haptics.back().large == 66 && haptics.back().small == 33 && + haptics.back().duration == 65435, + "haptics rumble did not receive scaled motors and remaining duration"); + PadTriggerEffectParam param {}; + param.trigger_mask = 1; + param.command[0].mode = 1; + param.command[0].data[1] = 8; + Check(PadSetTriggerEffect(1, ¶m) == 0, "trigger request failed"); + const auto vibration_calls = haptics.size(); + const auto trigger_calls = effects.size(); + CycleSetting(Setting::SpeakerVolume); + Check(haptics.size() == vibration_calls && effects.size() == trigger_calls, + "speaker volume resent controller effects"); + Connect(2); + Disconnect(1); + Check(GetActiveControllerId() == 2, "active controller did not change"); + haptics.clear(); + effects.clear(); + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::TriggerEffectIntensity); + CycleSetting(Setting::TriggerEffectIntensity); + Check(effects.empty(), "previous controller's trigger effects leaked to new pad"); + for (const auto& request: haptics) { + Check(request.large == 0 && request.small == 0, + "previous controller's vibration leaked to new pad"); + } + SetRumble(200, 100); + Check(PadSetTriggerEffect(1, ¶m) == 0, "replacement pad trigger request failed"); + EmergencyShutdown(); + Check(GetActiveControllerId() == -1, "released controller remained active"); + haptics.clear(); + effects.clear(); + CycleSetting(Setting::VibrationIntensity); + CycleSetting(Setting::TriggerEffectIntensity); + Check(effects.empty(), "released controller received a trigger request"); + for (const auto& request: haptics) { + Check(request.large == 0 && request.small == 0, + "released controller retained a cached vibration"); + } +} +} // namespace + +int main() { + Config::Initialize(); + TestSettingCycles(); + // Each following test initializes another controller and requires strong defaults. + TestVibrationLifetime(); + TestMaskedTriggersAndValidation(); + TestIndependentOutputsAndPadSwitch(); + Config::Shutdown(); + return 0; +} diff --git a/tests/PadHapticsTests.cpp b/tests/PadHapticsTests.cpp index d40060b6..da3f1973 100644 --- a/tests/PadHapticsTests.cpp +++ b/tests/PadHapticsTests.cpp @@ -56,6 +56,7 @@ std::binary_semaphore write_started {0}, allow_write {0}; SDL_AudioStream* default_stream = nullptr; bool fail_default_resume = false; int active_controller = 1; +float speaker_scale = 1.0f, vibration_scale = 1.0f; } // namespace namespace Fake { @@ -325,6 +326,9 @@ namespace Libs::Controller { int GetActiveControllerId() { return active_controller; } +float GetSettingScale(Setting setting) { + return setting == Setting::SpeakerVolume ? speaker_scale : vibration_scale; +} } // namespace Libs::Controller namespace Libs::LibKernel { @@ -364,6 +368,7 @@ struct Fixture { default_stream = nullptr; fail_default_resume = false; active_controller = 1; + speaker_scale = vibration_scale = 1.0f; now = 1000; opens = rumble_calls = 0; fail_open = fail_resume = false; @@ -381,8 +386,8 @@ Port Open(bool speaker = false, uint32_t freq = 48000) { return Port(stream, Haptics::Close); } void Queue(const Port& port, const void* data, uint32_t frames = 2, uint32_t channels = 2, - bool is_float = true, const int* volume = unity.data()) { - Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume); + bool is_float = true, const int* volume = unity.data(), float gain = 1.0f) { + Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume, gain); } void Pull() { std::array buffer {}; @@ -412,6 +417,8 @@ void TestFormatsAndVolume() { const std::array mono {16384, -32768}; Queue(port, mono.data(), 2, 1, false, volume.data() + 1); ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f}); + Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f); + ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f}); std::array multichannel {}; multichannel.fill(0.9f); multichannel[0] = 0.1f; @@ -561,6 +568,33 @@ void TestRumbleLeaseAndDuration() { Check(rumble_calls == calls, "audio pull touched rumble after shutdown"); } +void TestScaledRumbleDuration() { + for (bool bluetooth: {false, true}) { + Fixture f; + wireless = hid_available = bluetooth; + auto port = Open(); + std::vector sound(2048, 0.5f); + Haptics::SetVibration(1, 100, 50, 1000); + Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.0f); + Check(rumble.large == 100 * 0x101, "muted haptics suppressed motor rumble"); + Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.5f); + Check(rumble.large == 0, "scaled haptics did not suppress motor rumble"); + now += 100; + Haptics::SetVibration(1, 50, 25, 900); + now += 200; + if (bluetooth) { + Haptics::UpdateBluetoothRumble(); + } else { + Pull(); + } + Check(rumble.large == 50 * 0x101 && rumble.small == 25 * 0x101 && rumble.duration == 700, + "scaled rumble lost its strength or extended its original expiry"); + Haptics::SetVibration(1, 50, 25, 0); + Check(rumble.large == 0 && rumble.small == 0 && rumble.duration == 0, + "expired rumble was restarted"); + } +} + void TestSwitchStopsOldRumble() { Fixture f; auto port = Open(); @@ -732,12 +766,12 @@ void TestBluetoothFormatsAndRoutingFailure() { now += 2001; std::vector mono(1024, 16384); const int volume = 16384; - Queue(speaker, mono.data(), 1024, 1, false, &volume); + Queue(speaker, mono.data(), 1024, 1, false, &volume, 0.5f); 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"); + std::abs(pending[0].audio[200] - 0.125f) < 1e-4f && + std::abs(pending[0].audio[201] - 0.125f) < 1e-4f, + "Bluetooth mono int16 volume/gain mapping or speaker routing recovery failed"); } void TestBluetoothEncodingDoesNotBlockAudioQueue() { @@ -1103,6 +1137,82 @@ void TestAudioSpeakerRouting() { SDL_GetAudioStreamQueued(default_stream) == 0, "successful DualSense playback also queued audio on the main output"); ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0}); + speaker_scale = 0.5f; + audio.AudioOutOutputs(&output, 1, false); + ExpectPcm({0.25f, 0.25f, 0, 0, 0.25f, 0.25f, 0, 0}); + speaker_scale = 0.0f; + audio.AudioOutOutputs(&output, 1, false); + ExpectPcm({}); + active_controller = 2; + audio.AudioOutOutputs(&output, 1, false); + std::array fallback {}; + Check(SDL_GetAudioStreamData(default_stream, fallback.data(), sizeof(fallback)) == + sizeof(fallback) && + fallback == std::array {}, + "unplugging the controller unmuted its speaker fallback"); +} + +void TestAudioFallbackGain() { + for (bool is_float: {false, true}) { + Fixture f; + Libs::Audio::Audio audio; + active_controller = 2; + speaker_scale = 0.5f; + const std::array integer_pcm {16384, 16384, 16384, 16384}; + const auto format = is_float ? Libs::Audio::Audio::Format::FloatStereo + : Libs::Audio::Audio::Format::Signed16bitStereo; + const void* data = is_float ? static_cast(pcm.data()) : integer_pcm.data(); + for (int type: {0, 4}) { + const auto port = audio.AudioOutOpen(type, 2, 48000, format); + const std::array volume {16384, 16384}; + audio.AudioOutSetVolume(port, 3, volume.data()); + Libs::Audio::Audio::OutputParam output {port, data}; + audio.AudioOutOutputs(&output, 1, false); + const float expected = type == 4 ? 0.125f : 0.25f; + std::array actual {}; + if (is_float) { + Check(SDL_GetAudioStreamData(default_stream, actual.data(), sizeof(actual)) == + sizeof(actual), + "float fallback output is missing"); + } else { + std::array integers {}; + Check(SDL_GetAudioStreamData(default_stream, integers.data(), sizeof(integers)) == + sizeof(integers), + "integer fallback output is missing"); + std::transform(integers.begin(), integers.end(), actual.begin(), + [](int16_t value) { return value / 32768.0f; }); + } + Check(std::all_of(actual.begin(), actual.end(), + [expected](float value) { return value == expected; }), + "speaker setting gain was lost or applied to ordinary audio"); + audio.AudioOutClose(port); + } + } +} + +void TestAudioVibrationGain() { + for (bool bluetooth: {false, true}) { + Fixture f; + wireless = hid_available = bluetooth; + Libs::Audio::Audio audio; + vibration_scale = 0.5f; + speaker_scale = 0.0f; + const auto port = + audio.AudioOutOpen(10, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo); + std::vector sound(2048, 0.5f); + Libs::Audio::Audio::OutputParam output {port, sound.data()}; + audio.AudioOutOutputs(&output, 1, false); + Check(default_stream == nullptr, "vibration port opened ordinary audio"); + if (bluetooth) { + Check(Bluetooth::Prepare(now * 1000000).empty(), + "Bluetooth vibration skipped its coalescing window"); + const auto pending = Bluetooth::Prepare((now + 1) * 1000000); + Check(pending.size() == 1 && pending[0].data[76] == 32, + "Bluetooth vibration did not apply its own setting gain"); + } else { + ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, 0.25f, 0.25f}); + } + } } void TestBluetoothAudioSpeakerRouting() { @@ -1113,12 +1223,15 @@ void TestBluetoothAudioSpeakerRouting() { 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 sound(2048, 0.25f); + speaker_scale = 0.5f; 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); + Check(pending.size() == 1 && std::abs(pending[0].audio[200] - 0.125f) < 1e-4f, + "Bluetooth speaker did not apply its setting gain"); Libs::Controller::DualSenseBluetooth::Send(pending); Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && SDL_GetAudioStreamQueued(default_stream) > 0, @@ -1144,6 +1257,8 @@ int main() { SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); TestAudioSpeakerFallback(); TestAudioSpeakerRouting(); + TestAudioFallbackGain(); + TestAudioVibrationGain(); TestBluetoothAudioSpeakerRouting(); TestAudioDefaultResumeFailure(); TestFormatsAndVolume(); @@ -1173,6 +1288,7 @@ int main() { TestUsbAndBluetoothFeatureParity(); TestFailuresAndBoundedQueue(); TestRumbleLeaseAndDuration(); + TestScaledRumbleDuration(); TestSwitchStopsOldRumble(); TestCloseRestoresRumble(); std::printf("PadHapticsTests: all cases passed\n");