feat(audio): play the DualSense speaker and vibration ports on its USB sound card

A title can open AudioOut2 ports of type 0x3 (the controller's speaker)
and 0x6 (its two voice-coil actuators) next to its bed and object ports.
Both were folded into the TV mix, so the haptics came out of the PC
speakers as low thumps and the controller got nothing.

A USB DualSense is also a 4-channel 48 kHz sound card: channels 1-2 feed
the speaker and headset jack, channels 3-4 the left and right actuators.
When SDL lists such a card, a context with these ports opens it and
queues a grain on it with every push: the speaker port on channels 1-2
(a mono port duplicated) and the vibration port's left and right
channels on 3-4, at the port volumes and clamped. Both ports then leave
the main mix. The card's queue gets the main queue's silence cushion when
it runs dry and drops grains that run more than two grains beyond the
main queue, so the controller keeps pace with the TV. An unplugged card
is released (its ports return to the main mix) and looked for again
every 2 s. Without a card nothing changes, and other port types and
channel counts keep their routing.

SDL2's PulseAudio backend labels a 4-channel stream FL FR FC LFE instead
of quad, and the sound server then folds channels 3-4 into the speaker.
Under that backend the card is opened as 5.1 with FC and LFE silent and
the actuators on the back pair, which lands on the card's RL/RR.
This commit is contained in:
Dean Galvin 2026-10-01 00:38:07 +13:00
parent fc0596240d
commit dee927ea76
8 changed files with 335 additions and 11 deletions

View File

@ -177,6 +177,15 @@ if(BUILD_TESTING)
set_tests_properties(guest_buffer_reuse PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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)

View File

@ -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

View File

@ -6,6 +6,7 @@
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#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<int>(AUDIO_OUT2_SAMPLE_RATE);
desired.format = AUDIO_F32SYS;
desired.channels = static_cast<Uint8>(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<std::size_t>(context.grain) * AUDIO_OUT2_PAD_CHANNELS, 0.0f);
context.padFrames.assign(static_cast<std::size_t>(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<std::uint32_t>(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<float> silence(static_cast<std::size_t>(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++;

View File

@ -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<float> mix;
SDL_AudioDeviceID padDevice = 0;
std::chrono::steady_clock::time_point nextPadProbe;
AudioOut2PadLayout padLayout;
std::vector<float> padMix;
std::vector<float> 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);

View File

@ -0,0 +1,57 @@
#include "AudioOut2PadMix.hpp"
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <stdexcept>
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<std::size_t>(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<std::size_t>(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<std::size_t>(frame) * layout.channels + layout.position[channel]] = pad[static_cast<std::size_t>(frame) * AUDIO_OUT2_PAD_CHANNELS + channel];
}
}
}

View File

@ -0,0 +1,34 @@
#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2PADMIX_HPP
#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2PADMIX_HPP
#include <cstdint>
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

View File

@ -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<std::size_t>(frame) * port.channels;
for (std::uint32_t c = 0; c < port.channels; c++) {
in[c] = port.int16 ? static_cast<const std::int16_t*>(port.data)[first + c] / 32768.0f : static_cast<const float*>(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<std::size_t>(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<std::size_t>(frame) * ch;
for (std::uint32_t c = 0; c < ch; c++) {
in[c] = port.int16 ? static_cast<const std::int16_t*>(port.data)[first + c] / 32768.0f : static_cast<const float*>(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) {

View File

@ -0,0 +1,116 @@
#include "prx/libSceAudioOut/src/AudioOut2PadMix.hpp"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
static void Require(bool value, const char*) {
if (!value) std::abort();
}
template<typename TAction>
static bool Throws(TAction action) {
try {
action();
} catch (const std::invalid_argument&) {
return true;
}
return false;
}
template<std::uint32_t Frames>
static std::array<float, Frames * AUDIO_OUT2_PAD_CHANNELS> 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<std::size_t>(frame) * channels, channels, volume, out + static_cast<std::size_t>(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<frames>();
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<frames>();
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<frames>();
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;
}