From fc729b1f4afe08e82bbfd1ca3e1d6fdf60f8e20f Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:25:55 +0000 Subject: [PATCH] test(libSceAudioOut): cover the 16-bit host gain on mono, stereo and 8 channels Register audio_out_host_gain. Covers the byte-for-byte bypass at unity, the exact first sample and landing frame of a ramp, no step larger than the ramp slope, a ramp split across buffers equal to one buffer, saturation above unity, one gain per frame on every channel, a silent gain, and the return to bypass. --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 11 ++ .../prx/libSceAudioOut/tests/HostGain.cpp | 150 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/tests/HostGain.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 2182e06944..dde2f49570 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -44,6 +44,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_host_gain_tests tests/HostGain.cpp) + target_include_directories(audio_out_host_gain_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_host_gain_tests PRIVATE libc) + configure_windows_unwind(audio_out_host_gain_tests) + add_test(NAME audio_out_host_gain COMMAND audio_out_host_gain_tests) + set_tests_properties(audio_out_host_gain PROPERTIES TIMEOUT 5) + if(WIN32) + set_tests_properties(audio_out_host_gain PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out_vibration_device_tests tests/AudioOutVibrationDevice.cpp src/AudioOut.cpp src/AudioOut2PadMix.cpp src/AudioOutShutdown.cpp) target_compile_definitions(audio_out_vibration_device_tests PRIVATE SDL_MAIN_HANDLED) target_include_directories(audio_out_vibration_device_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceAudioOut/tests/HostGain.cpp b/core/libs/prx/libSceAudioOut/tests/HostGain.cpp new file mode 100644 index 0000000000..c3417c0783 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/HostGain.cpp @@ -0,0 +1,150 @@ +#include "prx/libSceAudioOut/include/HostGain.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector constantS16(std::size_t frames, unsigned channels, std::int16_t value) { + return std::vector(frames * channels, value); +} + +void s16BypassAtUnity() { + for (const unsigned channels : {1u, 2u, 8u}) { + std::vector samples(256 * channels); + for (std::size_t index = 0; index < samples.size(); ++index) samples[index] = static_cast(static_cast(index * 7919u % 65536u) - 32768); + const auto before = samples; + HostGainRamp ramp; + require(ramp.Bypass(HostGainRamp::Unity), "a ramp at unity with a unity target bypasses"); + ramp.Apply(samples.data(), 256, channels, HostGainRamp::Unity); + require(std::memcmp(before.data(), samples.data(), samples.size() * sizeof(std::int16_t)) == 0, "unity leaves 16-bit samples bit-identical"); + require(ramp.Current() == HostGainRamp::Unity, "bypass keeps the gain at unity"); + } +} + +void s16FirstSampleOfRamp() { + HostGainRamp ramp; + require(ramp.Step() == 136, "a 480 frame ramp steps 136 per frame"); + auto samples = constantS16(1, 1, 10000); + ramp.Apply(samples.data(), 1, 1, 32768); + require(ramp.Current() == 65536 - 136, "the first frame moves one step"); + require(samples[0] == 9979, "the first frame is scaled by the first step"); +} + +void s16RampReachesTargetExactly() { + HostGainRamp ramp; + auto samples = constantS16(2000, 1, 10000); + ramp.Apply(samples.data(), 2000, 1, 32768); + require(ramp.Current() == 32768, "the ramp lands exactly on the target"); + std::size_t firstAtTarget = 0; + while (firstAtTarget < samples.size() && samples[firstAtTarget] != 5000) ++firstAtTarget; + require(firstAtTarget == 240, "the ramp takes the 241 frames its step needs"); + for (std::size_t index = firstAtTarget; index < samples.size(); ++index) require(samples[index] == 5000, "samples stay at the target after the ramp"); +} + +void s16RampHasNoDiscontinuity() { + HostGainRamp ramp; + auto samples = constantS16(2000, 1, 32000); + ramp.Apply(samples.data(), 2000, 1, 0); + int largest = 0; + int previous = 32000; + for (const auto sample : samples) { + require(sample <= previous, "a ramp down never rises"); + largest = std::max(largest, previous - sample); + previous = sample; + } + require(largest <= 67, "no step of a full-scale ramp exceeds its per-frame slope"); + require(samples.back() == 0, "the ramp ends in silence"); +} + +void s16RampContinuesAcrossBuffers() { + auto whole = constantS16(3000, 2, 20000); + HostGainRamp wholeRamp; + wholeRamp.Apply(whole.data(), 3000, 2, 16384); + auto split = constantS16(3000, 2, 20000); + HostGainRamp splitRamp; + std::size_t done = 0; + for (const std::size_t chunk : {1u, 7u, 100u, 333u, 1u, 900u, 1658u}) { + splitRamp.Apply(split.data() + done * 2, chunk, 2, 16384); + done += chunk; + } + require(done == 3000, "the chunks cover the buffer"); + require(std::memcmp(whole.data(), split.data(), whole.size() * sizeof(std::int16_t)) == 0, "a ramp split across buffers equals the same ramp in one buffer"); + require(wholeRamp.Current() == splitRamp.Current(), "both ramps end at the same gain"); +} + +void s16SaturatesAboveUnity() { + HostGainRamp ramp(1); + auto samples = constantS16(4, 1, 30000); + samples[1] = -30000; + samples[2] = 8000; + ramp.Apply(samples.data(), 4, 1, 4 * HostGainRamp::Unity); + require(samples[0] == 32767, "a positive overshoot saturates at 32767"); + require(samples[1] == -32768, "a negative overshoot saturates at -32768"); + require(samples[2] == 32000, "a sample inside the range is scaled"); +} + +void s16ChannelsShareTheFrameGain() { + for (const unsigned channels : {1u, 2u, 8u}) { + HostGainRamp ramp; + auto samples = constantS16(600, channels, 12345); + ramp.Apply(samples.data(), 600, channels, 0); + for (std::size_t frame = 0; frame < 600; ++frame) { + for (unsigned channel = 1; channel < channels; ++channel) require(samples[frame * channels + channel] == samples[frame * channels], "every channel of a frame gets the same gain"); + } + require(samples[599 * channels] == 0, "the last frame is silent"); + } +} + +void s16SilentTargetSilencesAfterRamp() { + HostGainRamp ramp(1); + auto samples = constantS16(8, 2, -32768); + ramp.Apply(samples.data(), 8, 2, 0); + for (const auto sample : samples) require(sample == 0, "a zero gain writes zeros"); + require(!ramp.Bypass(0), "a silent target is never bypassed"); + auto again = constantS16(8, 2, 1000); + ramp.Apply(again.data(), 8, 2, 0); + for (const auto sample : again) require(sample == 0, "a settled silent gain stays silent"); +} + +void s16ReturnToUnityBypassesAgain() { + HostGainRamp ramp; + auto samples = constantS16(2000, 2, 9000); + ramp.Apply(samples.data(), 2000, 2, 0); + auto back = constantS16(2000, 2, 9000); + ramp.Apply(back.data(), 2000, 2, HostGainRamp::Unity); + require(ramp.Current() == HostGainRamp::Unity, "the ramp returns to exactly unity"); + require(ramp.Bypass(HostGainRamp::Unity), "once back at unity the stream bypasses again"); + auto steady = constantS16(64, 2, 9000); + const auto before = steady; + ramp.Apply(steady.data(), 64, 2, HostGainRamp::Unity); + require(steady == before, "samples after the return are bit-identical"); +} + +} + +int main() try { + s16BypassAtUnity(); + s16FirstSampleOfRamp(); + s16RampReachesTargetExactly(); + s16RampHasNoDiscontinuity(); + s16RampContinuesAcrossBuffers(); + s16SaturatesAboveUnity(); + s16ChannelsShareTheFrameGain(); + s16SilentTargetSilencesAfterRamp(); + s16ReturnToUnityBypassesAgain(); + std::puts("host gain tests passed"); + return 0; +} catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; +}