diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f44ae80e3..74b390411 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -177,6 +177,15 @@ if(BUILD_TESTING) set_tests_properties(guest_buffer_reuse PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(audio_out2_pad_mix_tests prx/libSceAudioOut/tests/AudioOut2PadMix.cpp prx/libSceAudioOut/src/AudioOut2PadMix.cpp) + target_include_directories(audio_out2_pad_mix_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(audio_out2_pad_mix_tests PRIVATE libc) + configure_windows_unwind(audio_out2_pad_mix_tests) + add_test(NAME audio_out2_pad_mix COMMAND audio_out2_pad_mix_tests) + if(WIN32) + set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_indexed_draw_tests tests/GuestIndexedDraw.cpp) add_executable(shader_buffer_address_tests tests/ShaderBufferAddress.cpp) target_link_libraries(shader_buffer_address_tests PRIVATE shader_recompiler_agc_driver libc) diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index d56668ce9..f9a479870 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Context.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2PadMix.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Port.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2SpeakerArray.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 85de18a57..fc6408d62 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -40,6 +41,8 @@ static constexpr std::uint16_t DEVICE_SAMPLES = 512; static constexpr float MASTER_GAIN = 0.5f; static constexpr std::size_t OUTPUT_FRAME_BYTES = AUDIO_OUT2_OUTPUT_CHANNELS * sizeof(float); static constexpr std::uint32_t OUTPUT_BYTES_PER_MS = AUDIO_OUT2_SAMPLE_RATE * OUTPUT_FRAME_BYTES / 1000; +static constexpr std::chrono::seconds PAD_PROBE_INTERVAL{2}; +static constexpr std::uint32_t PAD_SLACK_GRAINS = 2; bool AudioOut2TraceEnabled() { static const bool enabled = std::getenv("APS5_TRACE_AUDIOOUT2") != nullptr; @@ -120,14 +123,88 @@ static void CloseDevice(AudioOut2Context& context) { context.device = 0; } -// Mixes the ports' current grain and queues it on the SDL device. The caller holds the lock. -static std::uint32_t Render(AudioOut2Context& context) { +static void OpenPadDevice(AudioOut2Context& context) { + if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return; + const int count = SDL_GetNumAudioDevices(0); + for (int index = 0; index < count; index++) { + const char* listed = SDL_GetAudioDeviceName(index, 0); + if (!AudioOut2IsPadAudioDevice(listed)) continue; + const std::string name(listed); + SDL_AudioSpec native{}; + if (SDL_GetAudioDeviceSpec(index, 0, &native) == 0 && native.channels != 0 && native.channels != AUDIO_OUT2_PAD_CHANNELS) continue; + const auto layout = AudioOut2PadLayoutForDriver(SDL_GetCurrentAudioDriver()); + SDL_AudioSpec desired{}; + desired.freq = static_cast(AUDIO_OUT2_SAMPLE_RATE); + desired.format = AUDIO_F32SYS; + desired.channels = static_cast(layout.channels); + desired.samples = DEVICE_SAMPLES; + desired.callback = nullptr; + SDL_AudioSpec obtained{}; + context.padDevice = SDL_OpenAudioDevice(name.c_str(), 0, &desired, &obtained, 0); + if (context.padDevice == 0) { + APS5_LOG_ERR("AudioOut2: could not open the controller sound card '%s': %s", name.c_str(), SDL_GetError()); + return; + } + context.padLayout = layout; + context.padMix.assign(static_cast(context.grain) * AUDIO_OUT2_PAD_CHANNELS, 0.0f); + context.padFrames.assign(static_cast(context.grain) * layout.channels, 0.0f); + SDL_PauseAudioDevice(context.padDevice, 0); + APS5_LOG_OUT("AudioOut2: controller speaker and vibration ports play on '%s'", name.c_str()); + return; + } +} + +static void ClosePadDevice(AudioOut2Context& context) { + if (context.padDevice != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { + SDL_ClearQueuedAudio(context.padDevice); + SDL_CloseAudioDevice(context.padDevice); + } + context.padDevice = 0; +} + +static void UpdatePadDevice(AudioOut2Context& context, Clock::time_point now) { + if (context.padDevice != 0 && SDL_GetAudioDeviceStatus(context.padDevice) == SDL_AUDIO_STOPPED) { + APS5_LOG_CHARS_OUT("AudioOut2: the controller sound card went away; its ports play in the main mix"); + ClosePadDevice(context); + context.nextPadProbe = now + PAD_PROBE_INTERVAL; + } + if (context.padDevice != 0 || now < context.nextPadProbe) return; + context.nextPadProbe = now + PAD_PROBE_INTERVAL; + if (AudioOut2HasPadPorts(context)) OpenPadDevice(context); +} + +static void QueuePadGrain(AudioOut2Context& context) { + AudioOut2WritePadFrames(context.padMix.data(), context.padLayout, context.padFrames.data(), context.grain); + const auto frameBytes = static_cast(context.padLayout.channels * sizeof(float)); + const auto grainBytes = context.grain * frameBytes; + const auto cushionBytes = CUSHION_MS * (AUDIO_OUT2_SAMPLE_RATE / 1000) * frameBytes; + const auto mainQueuedBytes = context.device != 0 ? SDL_GetQueuedAudioSize(context.device) / OUTPUT_FRAME_BYTES * frameBytes : cushionBytes + context.queueDepth * grainBytes; + const auto queuedBytes = SDL_GetQueuedAudioSize(context.padDevice); + if (queuedBytes > mainQueuedBytes + PAD_SLACK_GRAINS * grainBytes) return; + if (queuedBytes == 0) { + static const std::vector silence(static_cast(CUSHION_MS) * AUDIO_OUT2_SAMPLE_RATE / 1000 * AUDIO_OUT2_PAD_DEVICE_CHANNELS_MAX, 0.0f); + SDL_QueueAudio(context.padDevice, silence.data(), cushionBytes); + } + SDL_QueueAudio(context.padDevice, context.padFrames.data(), grainBytes); +} + +static std::uint32_t Render(AudioOut2Context& context, Clock::time_point now) { + UpdatePadDevice(context, now); + float* pad = nullptr; + if (context.padDevice != 0) { + std::fill(context.padMix.begin(), context.padMix.end(), 0.0f); + pad = context.padMix.data(); + } std::fill(context.mix.begin(), context.mix.end(), 0.0f); - const auto mixed = AudioOut2MixPorts(context, context.mix.data(), context.grain); + const auto mixed = AudioOut2MixPorts(context, context.mix.data(), pad, context.grain); for (float& sample : context.mix) { sample = std::clamp(sample * MASTER_GAIN, -1.0f, 1.0f); if (AudioOut2TraceEnabled()) context.summaryPeak = std::max(context.summaryPeak, std::abs(sample)); } + if (pad != nullptr) { + AudioOut2FinishPadMix(pad, context.grain); + QueuePadGrain(context); + } if (context.device == 0) return mixed; const auto queuedBytes = SDL_GetQueuedAudioSize(context.device); if (queuedBytes > MAX_QUEUED_MS * OUTPUT_BYTES_PER_MS) { @@ -199,6 +276,7 @@ int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { { std::lock_guard lock(context->lock); CloseDevice(*context); + ClosePadDevice(*context); } AudioOut2ReleasePorts(*context); delete context; @@ -238,7 +316,7 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block } context->queued++; context->playHead += GrainDuration(*context); - const auto mixed = Render(*context); + const auto mixed = Render(*context, now); context->pushes++; context->summaryPushes++; if (blocking) context->blockingPushes++; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 0ee974536..6c4f37e55 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -10,6 +10,7 @@ #include "SDL.h" #include "SceTypes.hpp" +#include "AudioOut2PadMix.hpp" // Shared by the AudioOut2 context and port files. // @@ -66,6 +67,11 @@ struct AudioOut2Context { SDL_AudioDeviceID device = 0; // Stereo float mix of the ports for one push. std::vector mix; + SDL_AudioDeviceID padDevice = 0; + std::chrono::steady_clock::time_point nextPadProbe; + AudioOut2PadLayout padLayout; + std::vector padMix; + std::vector padFrames; // Trace counters. std::uint64_t pushes = 0; std::uint64_t blockingPushes = 0; @@ -84,7 +90,8 @@ struct AudioOut2Context { // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing // onto the zeroed buffer. Returns the number of ports mixed. -std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames); +std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, float* padOut, std::uint32_t frames); +bool AudioOut2HasPadPorts(const AudioOut2Context& context); // Forgets the ports of a context being destroyed. void AudioOut2ReleasePorts(const AudioOut2Context& context); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp new file mode 100644 index 000000000..e1c7c049e --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.cpp @@ -0,0 +1,57 @@ +#include "AudioOut2PadMix.hpp" + +#include +#include +#include +#include + +AudioOut2Route AudioOut2RouteForPort(std::uint16_t type, std::uint32_t channels) { + if (type == AUDIO_OUT2_PORT_TYPE_PAD_SPEAKER && (channels == 1 || channels == 2)) return AudioOut2Route::PadSpeaker; + if (type == AUDIO_OUT2_PORT_TYPE_PAD_VIBRATION && (channels == 1 || channels == 2)) return AudioOut2Route::PadVibration; + return AudioOut2Route::Main; +} + +bool AudioOut2IsPadAudioDevice(const char* name) { + if (name == nullptr) return false; + constexpr char needle[] = "dualsense"; + constexpr std::size_t length = sizeof(needle) - 1; + for (const char* start = name; *start != '\0'; start++) { + std::size_t matched = 0; + while (matched < length && start[matched] != '\0' && (start[matched] | 0x20) == needle[matched]) matched++; + if (matched == length) return true; + } + return false; +} + +void AudioOut2AccumulatePadFrame(AudioOut2Route route, const float* in, std::uint32_t channels, const float* volume, float* pad) { + if (route == AudioOut2Route::Main || (channels != 1 && channels != 2)) throw std::invalid_argument("AudioOut2AccumulatePadFrame: the port does not play on the controller"); + const auto left = route == AudioOut2Route::PadSpeaker ? AUDIO_OUT2_PAD_SPEAKER_LEFT : AUDIO_OUT2_PAD_VIBRATION_LEFT; + const auto right = route == AudioOut2Route::PadSpeaker ? AUDIO_OUT2_PAD_SPEAKER_RIGHT : AUDIO_OUT2_PAD_VIBRATION_RIGHT; + const auto rightSource = channels == 2 ? 1u : 0u; + pad[left] += in[0] * volume[0]; + pad[right] += in[rightSource] * volume[rightSource]; +} + +void AudioOut2FinishPadMix(float* out, std::uint32_t frames) { + const auto samples = static_cast(frames) * AUDIO_OUT2_PAD_CHANNELS; + for (std::size_t index = 0; index < samples; index++) out[index] = std::clamp(out[index], -1.0f, 1.0f); +} + +AudioOut2PadLayout AudioOut2PadLayoutForDriver(const char* driver) { + AudioOut2PadLayout layout; + if (driver != nullptr && std::strcmp(driver, "pulseaudio") == 0) { + layout.channels = AUDIO_OUT2_PAD_DEVICE_CHANNELS_MAX; + layout.position[AUDIO_OUT2_PAD_VIBRATION_LEFT] = 4; + layout.position[AUDIO_OUT2_PAD_VIBRATION_RIGHT] = 5; + } + return layout; +} + +void AudioOut2WritePadFrames(const float* pad, const AudioOut2PadLayout& layout, float* out, std::uint32_t frames) { + std::fill(out, out + static_cast(frames) * layout.channels, 0.0f); + for (std::uint32_t frame = 0; frame < frames; frame++) { + for (std::uint32_t channel = 0; channel < AUDIO_OUT2_PAD_CHANNELS; channel++) { + out[static_cast(frame) * layout.channels + layout.position[channel]] = pad[static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS + channel]; + } + } +} diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp new file mode 100644 index 000000000..501a1af8e --- /dev/null +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2PadMix.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2PADMIX_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2PADMIX_HPP + +#include + +static constexpr std::uint16_t AUDIO_OUT2_PORT_TYPE_PAD_SPEAKER = 0x3; +static constexpr std::uint16_t AUDIO_OUT2_PORT_TYPE_PAD_VIBRATION = 0x6; + +static constexpr std::uint32_t AUDIO_OUT2_PAD_CHANNELS = 4; +static constexpr std::uint32_t AUDIO_OUT2_PAD_SPEAKER_LEFT = 0; +static constexpr std::uint32_t AUDIO_OUT2_PAD_SPEAKER_RIGHT = 1; +static constexpr std::uint32_t AUDIO_OUT2_PAD_VIBRATION_LEFT = 2; +static constexpr std::uint32_t AUDIO_OUT2_PAD_VIBRATION_RIGHT = 3; +static constexpr std::uint32_t AUDIO_OUT2_PAD_DEVICE_CHANNELS_MAX = 6; + +enum class AudioOut2Route { + Main, + PadSpeaker, + PadVibration, +}; + +struct AudioOut2PadLayout { + std::uint32_t channels = AUDIO_OUT2_PAD_CHANNELS; + std::uint32_t position[AUDIO_OUT2_PAD_CHANNELS] = {0, 1, 2, 3}; +}; + +AudioOut2Route AudioOut2RouteForPort(std::uint16_t type, std::uint32_t channels); +bool AudioOut2IsPadAudioDevice(const char* name); +void AudioOut2AccumulatePadFrame(AudioOut2Route route, const float* in, std::uint32_t channels, const float* volume, float* pad); +void AudioOut2FinishPadMix(float* out, std::uint32_t frames); +AudioOut2PadLayout AudioOut2PadLayoutForDriver(const char* driver); +void AudioOut2WritePadFrames(const float* pad, const AudioOut2PadLayout& layout, float* out, std::uint32_t frames); + +#endif diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 2cc479fe1..8b6b16732 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -9,6 +9,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "AudioOut2Internal.hpp" +#include "AudioOut2PadMix.hpp" static constexpr std::uint16_t OUTPUT_MAIN = 1; static constexpr std::int16_t VOLUME_MAX = 127; @@ -40,15 +41,27 @@ static AudioOut2Port* FromHandle(AudioOut2PortHandle handle) { // and side pairs fold into the front at -3 dB, the centre into both sides, and the LFE is dropped. static constexpr float DOWNMIX_GAIN = 0.7071f; +static void ReadFrame(const AudioOut2Port& port, std::uint32_t frame, float* in) { + const auto first = static_cast(frame) * port.channels; + for (std::uint32_t c = 0; c < port.channels; c++) { + in[c] = port.int16 ? static_cast(port.data)[first + c] / 32768.0f : static_cast(port.data)[first + c]; + } +} + +static void AccumulatePadPort(const AudioOut2Port& port, AudioOut2Route route, float* out, std::uint32_t frames) { + float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; + for (std::uint32_t frame = 0; frame < frames; frame++) { + ReadFrame(port, frame, in); + AudioOut2AccumulatePadFrame(route, in, port.channels, port.volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); + } +} + static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) { const auto ch = port.channels; const float* volume = port.volume; float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { - const auto first = static_cast(frame) * ch; - for (std::uint32_t c = 0; c < ch; c++) { - in[c] = port.int16 ? static_cast(port.data)[first + c] / 32768.0f : static_cast(port.data)[first + c]; - } + ReadFrame(port, frame, in); float left = 0.0f; float right = 0.0f; if (ch == 1) { @@ -66,17 +79,26 @@ static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t } } -std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames) { +std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, float* padOut, std::uint32_t frames) { std::lock_guard lock(g_portsLock); std::uint32_t mixed = 0; for (const auto& port : g_ports) { if (!port.used || port.context != &context || port.data == nullptr || port.channels == 0) continue; - AccumulatePort(port, out, frames); + const auto route = padOut != nullptr ? AudioOut2RouteForPort(port.type, port.channels) : AudioOut2Route::Main; + if (route == AudioOut2Route::Main) AccumulatePort(port, out, frames); + else AccumulatePadPort(port, route, padOut, frames); mixed++; } return mixed; } +bool AudioOut2HasPadPorts(const AudioOut2Context& context) { + std::lock_guard lock(g_portsLock); + return std::any_of(g_ports.begin(), g_ports.end(), [&context](const AudioOut2Port& port) { + return port.used && port.context == &context && AudioOut2RouteForPort(port.type, port.channels) != AudioOut2Route::Main; + }); +} + void AudioOut2ReleasePorts(const AudioOut2Context& context) { std::lock_guard lock(g_portsLock); for (auto& port : g_ports) { diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp new file mode 100644 index 000000000..a7e6f3252 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PadMix.cpp @@ -0,0 +1,116 @@ +#include "prx/libSceAudioOut/src/AudioOut2PadMix.hpp" + +#include +#include +#include +#include + +static void Require(bool value, const char*) { + if (!value) std::abort(); +} + +template +static bool Throws(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +template +static std::array Silence() { + return {}; +} + +static void Accumulate(AudioOut2Route route, const float* data, std::uint32_t channels, const float* volume, float* out, std::uint32_t frames) { + for (std::uint32_t frame = 0; frame < frames; frame++) AudioOut2AccumulatePadFrame(route, data + static_cast(frame) * channels, channels, volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); +} + +int main() { + Require(AudioOut2RouteForPort(0x3, 1) == AudioOut2Route::PadSpeaker, "mono type 0x3 is the pad speaker"); + Require(AudioOut2RouteForPort(0x3, 2) == AudioOut2Route::PadSpeaker, "stereo type 0x3 is the pad speaker"); + Require(AudioOut2RouteForPort(0x6, 2) == AudioOut2Route::PadVibration, "stereo type 0x6 is the vibration"); + Require(AudioOut2RouteForPort(0x6, 1) == AudioOut2Route::PadVibration, "mono type 0x6 is the vibration"); + Require(AudioOut2RouteForPort(0x6, 8) == AudioOut2Route::Main, "an 8-channel type 0x6 stays in the main mix"); + Require(AudioOut2RouteForPort(0x3, 0) == AudioOut2Route::Main, "an undecoded format stays in the main mix"); + for (std::uint16_t type : {0x0, 0x1, 0x2, 0x4, 0x5, 0x7, 0x100}) { + Require(AudioOut2RouteForPort(type, 1) == AudioOut2Route::Main, "other port types stay in the main mix"); + Require(AudioOut2RouteForPort(type, 2) == AudioOut2Route::Main, "other port types stay in the main mix"); + } + + Require(AudioOut2IsPadAudioDevice("DualSense wireless controller (PS5) Direct DualSense Wireless Controller"), "pulse name"); + Require(AudioOut2IsPadAudioDevice("DualSense Edge Wireless Controller, USB Audio"), "edge alsa name"); + Require(!AudioOut2IsPadAudioDevice("AD102 High Definition Audio Controller Digital Stereo (HDMI)"), "hdmi name"); + Require(!AudioOut2IsPadAudioDevice(nullptr), "null name"); + + constexpr std::uint32_t frames = 3; + const float unity[2] = {1.0f, 1.0f}; + + { + auto out = Silence(); + const float speaker[frames] = {0.5f, -0.25f, 0.125f}; + const float half[1] = {0.5f}; + Accumulate(AudioOut2Route::PadSpeaker, speaker, 1, half, out.data(), frames); + for (std::uint32_t frame = 0; frame < frames; frame++) { + const float* pad = &out[frame * AUDIO_OUT2_PAD_CHANNELS]; + Require(pad[0] == speaker[frame] * 0.5f && pad[1] == speaker[frame] * 0.5f, "mono speaker on channels 1-2"); + Require(pad[2] == 0.0f && pad[3] == 0.0f, "speaker leaves the actuators alone"); + } + } + + { + auto out = Silence(); + const float vibration[frames * 2] = {0.1f, 0.2f, 0.3f, 0.4f, -0.5f, -0.6f}; + const float gains[2] = {1.0f, 0.5f}; + Accumulate(AudioOut2Route::PadVibration, vibration, 2, gains, out.data(), frames); + for (std::uint32_t frame = 0; frame < frames; frame++) { + const float* pad = &out[frame * AUDIO_OUT2_PAD_CHANNELS]; + Require(pad[0] == 0.0f && pad[1] == 0.0f, "vibration leaves the speaker alone"); + Require(pad[2] == vibration[frame * 2] && pad[3] == vibration[frame * 2 + 1] * 0.5f, "vibration on channels 3-4"); + } + } + + { + auto out = Silence(); + const float speaker[frames] = {0.75f, 0.75f, 0.0f}; + const float vibration[frames * 2] = {0.9f, -0.9f, 0.0f, 0.0f, 0.0f, 0.0f}; + Accumulate(AudioOut2Route::PadSpeaker, speaker, 1, unity, out.data(), frames); + Accumulate(AudioOut2Route::PadSpeaker, speaker, 1, unity, out.data(), frames); + Accumulate(AudioOut2Route::PadVibration, vibration, 2, unity, out.data(), frames); + Accumulate(AudioOut2Route::PadVibration, vibration, 2, unity, out.data(), frames); + AudioOut2FinishPadMix(out.data(), frames); + Require(out[0] == 1.0f && out[1] == 1.0f, "speaker sum clamps at full scale"); + Require(out[2] == 1.0f && out[3] == -1.0f, "vibration sum clamps at full scale"); + Require(out[4] == 1.0f && out[6] == 0.0f, "second frame"); + for (std::uint32_t index = 8; index < frames * AUDIO_OUT2_PAD_CHANNELS; index++) Require(out[index] == 0.0f, "silent frame stays silent"); + } + + { + auto out = Silence<1>(); + const float data[8] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + Require(Throws([&] { AudioOut2AccumulatePadFrame(AudioOut2Route::Main, data, 2, unity, out.data()); }), "a main-mix port is refused"); + Require(Throws([&] { AudioOut2AccumulatePadFrame(AudioOut2Route::PadVibration, data, 8, unity, out.data()); }), "an 8-channel pad port is refused"); + for (float sample : out) Require(sample == 0.0f, "a refused port mixes nothing"); + } + + { + const auto quad = AudioOut2PadLayoutForDriver("pipewire"); + Require(quad.channels == 4 && quad.position[0] == 0 && quad.position[1] == 1 && quad.position[2] == 2 && quad.position[3] == 3, "quad layout"); + Require(AudioOut2PadLayoutForDriver("alsa").channels == 4 && AudioOut2PadLayoutForDriver(nullptr).channels == 4, "alsa and unknown use quad"); + const auto pulse = AudioOut2PadLayoutForDriver("pulseaudio"); + Require(pulse.channels == 6 && pulse.position[0] == 0 && pulse.position[1] == 1 && pulse.position[2] == 4 && pulse.position[3] == 5, "pulse layout"); + + const float pad[2 * AUDIO_OUT2_PAD_CHANNELS] = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f}; + float out[2 * AUDIO_OUT2_PAD_DEVICE_CHANNELS_MAX]; + for (float& sample : out) sample = 9.0f; + AudioOut2WritePadFrames(pad, pulse, out, 2); + const float expected[2 * 6] = {0.1f, 0.2f, 0.0f, 0.0f, 0.3f, 0.4f, 0.5f, 0.6f, 0.0f, 0.0f, 0.7f, 0.8f}; + for (int index = 0; index < 12; index++) Require(out[index] == expected[index], "pulse frames: speaker front, centre and LFE silent, actuators back"); + AudioOut2WritePadFrames(pad, quad, out, 2); + for (int index = 0; index < 8; index++) Require(out[index] == pad[index], "quad frames are the pad mix"); + } + + return 0; +}