diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 7c7bdc2c..1aa13b2c 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -90,8 +89,9 @@ public: }; struct OutputParam { - Id handle; - const void* data = nullptr; + Id handle; + const void* data = nullptr; + const float* gains = nullptr; }; Audio() = default; @@ -159,8 +159,10 @@ 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, float gain); - static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain); + std::vector* buffer, float gain, + const float* gains); + static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain, + const float* gains); }; static Audio* g_audio = nullptr; @@ -196,7 +198,7 @@ uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) for (uint32_t i = 0; i < num; i++) { if (params[i].handle > 0 && params[i].data != nullptr) { output_params.push_back( - Audio::OutputParam {Audio::Id(params[i].handle), params[i].data}); + Audio::OutputParam {Audio::Id(params[i].handle), params[i].data, params[i].gains}); } } @@ -302,7 +304,8 @@ void Audio::CloseSdlDevice(PortOut* port) { } const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, - std::vector* buffer, float gain) { + std::vector* buffer, float gain, + const float* gains) { EXIT_IF(data == nullptr); EXIT_IF(buffer == nullptr); @@ -312,12 +315,12 @@ 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 = gain != 1.0f; + std::array scales {}; + bool volume_changed = false; for (uint32_t ch = 0; ch < channels; ch++) { - if (port.volume[ch] != 32768) { - volume_changed = true; - break; - } + scales[ch] = (static_cast(port.volume[ch]) / 32768.0) * gain * + (gains != nullptr ? gains[ch] : 1.0f); + volume_changed |= scales[ch] != 1.0; } if (!volume_changed && !reorder) { @@ -337,8 +340,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, for (uint32_t ch = 0; ch < output_channels; ch++) { 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) * gain; + static_cast(src[frame * channels + src_ch] * scales[src_ch]); } if (channels == 12) { // Add the four top channels to their front/back channels, turning 12 into 8. @@ -346,8 +348,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, static constexpr uint32_t HEIGHT_DST[4] = {0, 1, 4, 5}; 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) * gain; + static_cast(src[frame * channels + 8 + ch] * scales[8 + ch]); } } } @@ -358,15 +359,9 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, for (uint32_t frame = 0; frame < frames; frame++) { for (uint32_t ch = 0; ch < output_channels; ch++) { 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()) { - sample = std::numeric_limits::min(); - } - dst[frame * output_channels + ch] = static_cast(sample); + const double sample = src[frame * channels + src_ch] * scales[src_ch]; + dst[frame * output_channels + ch] = static_cast( + std::clamp(sample, -32768.0, 32767.0)); } } } @@ -374,7 +369,8 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data, return buffer->data(); } -bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) { +bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain, + const float* gains) { EXIT_IF(port == nullptr); if (port->stream == nullptr || data == nullptr) { @@ -382,7 +378,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float } std::vector prepared_buffer; - const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain); + const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain, gains); const auto output_channels = OutputChannels(*port); const auto prepared_size = BytesPerSample(port->format) * output_channels * port->samples_num; @@ -570,14 +566,14 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking 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, - gain); + gain, params[i].gains); 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, gain)) { + if (QueueSdlAudio(&port, params[i].data, blocking, gain, params[i].gains)) { any_device = true; paced[i] = port.queue_primed; } diff --git a/src/libs/audio_internal.h b/src/libs/audio_internal.h index ad596ee5..ca2a430e 100644 --- a/src/libs/audio_internal.h +++ b/src/libs/audio_internal.h @@ -19,8 +19,9 @@ enum class Format { }; struct OutputParam { - int handle = 0; - const void* data = nullptr; + int handle = 0; + const void* data = nullptr; + const float* gains = nullptr; }; static constexpr int OUT_PORTS_MAX = 32; diff --git a/src/libs/dualSenseHaptics.cpp b/src/libs/dualSenseHaptics.cpp index aa79f853..9fe4af61 100644 --- a/src/libs/dualSenseHaptics.cpp +++ b/src/libs/dualSenseHaptics.cpp @@ -315,7 +315,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, float gain) { + bool is_float, const int* volume, float gain, const float* gains) { if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) { return 0; } @@ -357,7 +357,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 * gain; + value *= volume[src_ch] / 32768.0f * gain * (gains != nullptr ? gains[src_ch] : 1.0f); // 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; diff --git a/src/libs/dualSenseHaptics.h b/src/libs/dualSenseHaptics.h index e5475ace..05be7af2 100644 --- a/src/libs/dualSenseHaptics.h +++ b/src/libs/dualSenseHaptics.h @@ -17,7 +17,8 @@ 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, float gain = 1.0f); + bool is_float, const int* volume, float gain = 1.0f, + const float* gains = nullptr); // Returns false for other gamepad types, which retain the normal rumble path. bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms = 0xffff); diff --git a/src/libs/libAudio2.cpp b/src/libs/libAudio2.cpp index 35f5e82a..91fb8367 100644 --- a/src/libs/libAudio2.cpp +++ b/src/libs/libAudio2.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -170,12 +171,14 @@ static std::atomic_uint64_t g_audioout2_next_port {1}; static AudioOut2UserHandle g_audioout2_next_user = 1; struct AudioOut2ContextState { - bool used = false; - AudioOut2ContextHandle handle = 0; - uint32_t queue_depth = 4; - uint32_t queued = 0; - uint32_t num_grains = 512; - uint64_t last_update = 0; + bool used = false; + AudioOut2ContextHandle handle = 0; + uint32_t queue_depth = 4; + uint32_t queued = 0; + uint32_t num_grains = 512; + uint64_t last_update = 0; + int object_audio_handle = 0; + std::vector object_mix; }; struct AudioOut2PortStateEntry { @@ -189,9 +192,11 @@ struct AudioOut2PortStateEntry { uint32_t samples_num = 512; AudioInternal::Format audio_format = AudioInternal::Format::Unknown; int audio_handle = 0; + std::array gains {}; + uint32_t ambisonics = UINT32_MAX; + bool attributes_locked = false; std::vector pcm_data; }; - struct AudioOut2SpeakerArrayState { bool used = false; uint32_t num_speakers = 0; @@ -207,9 +212,8 @@ struct AudioOut2LatencyState { uint32_t latency_us = 0; }; -static Common::Mutex g_audioout2_context_mutex; +static Common::Mutex g_audioout2_state_mutex; static std::array g_audioout2_contexts; -static Common::Mutex g_audioout2_port_mutex; static std::array g_audioout2_ports; static std::vector g_audioout2_users; static Common::Mutex g_audioout2_speaker_array_mutex; @@ -223,7 +227,11 @@ static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */ static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */ static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */ +static constexpr int AUDIO_OUT2_ERROR_OUT_OF_RESOURCE = -2144960510; /* 0x80268002 */ +static constexpr int AUDIO_OUT2_ERROR_INVALID_PORT = -2144960503; /* 0x80268009 */ static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0; +static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN = 1; +static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS = 8; static constexpr uint32_t AUDIO_OUT2_MASTERING_OUTPUT_RECORDING = 2; static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_DEFAULT = 1; static constexpr uint32_t AUDIO_OUT2_MASTERING_STATES_ID_V2 = 2; @@ -350,37 +358,80 @@ static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) { audioout2_data_format_channels(state.data_format) * bytes_per_sample; } -static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) { - Common::LockGuard lock(g_audioout2_port_mutex); +static bool audioout2_context_has_queueable_device(const AudioOut2ContextState& context) { for (const auto& state: g_audioout2_ports) { - if (state.used && state.context == ctx && state.audio_handle > 0 && - !state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) { - return true; + if (state.used && state.context == context.handle && !state.pcm_data.empty()) { + const int handle = audioout2_port_type_is_object(state.port_type) + ? context.object_audio_handle + : state.audio_handle; + if (AudioInternal::AudioOutHasDevice(handle)) { + return true; + } } } return false; } +static void audioout2_mix_object(AudioOut2ContextState& context, + const AudioOut2PortStateEntry& object) { + auto& mix = context.object_mix; + if (mix.empty()) { + mix.resize(static_cast(context.num_grains) * 2, 0.0f); + } + + // Center ordinary objects. First-order stereo ambisonics uses left/right-facing + // cardioids; FuMa W has a different normalization from ACN/SN3D W. + constexpr float half_power = 0.70710678f; + float left = half_power, right = half_power; + switch (object.ambisonics) { + case UINT32_MAX: + case 64: break; + case 0: left = right = 1.0f; break; + case 2: + case 65: right = -half_power; break; + default: return; + } + left *= object.gains[0]; + right *= object.gains[0]; + for (uint32_t frame = 0; frame < context.num_grains; frame++) { + const float sample = + object.audio_format == AudioInternal::Format::FloatMono + ? reinterpret_cast(object.pcm_data.data())[frame] + : reinterpret_cast(object.pcm_data.data())[frame] / 32768.0f; + mix[frame * 2] += sample * left; + mix[frame * 2 + 1] += sample * right; + } +} + static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) { // The backend consumes the buffers synchronously, but can block while pacing SDL's queue. // Keep both PCM storage and port handles alive until it returns. + Common::LockGuard lock(g_audioout2_state_mutex); + auto* context = audioout2_find_context_locked(ctx); + if (context == nullptr) { + return; + } + context->object_mix.clear(); std::vector params; params.reserve(AudioInternal::OUT_PORTS_MAX); - - Common::LockGuard lock(g_audioout2_port_mutex); for (const auto& state: g_audioout2_ports) { - if (state.used && state.context == ctx && state.audio_handle > 0 && - !state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) { - params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()}); + if (!state.used || state.context != ctx || state.pcm_data.empty()) { + continue; + } + if (audioout2_port_type_is_object(state.port_type)) { + audioout2_mix_object(*context, state); + } else if (state.audio_handle > 0 && params.size() < AudioInternal::OUT_PORTS_MAX) { + params.push_back({state.audio_handle, state.pcm_data.data(), state.gains.data()}); } } - + if (!context->object_mix.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) { + params.push_back({context->object_audio_handle, context->object_mix.data()}); + } if (!params.empty()) { (void)AudioInternal::AudioOutOutputs(params.data(), static_cast(params.size()), blocking); } } - static void audioout2_close_audio_handle(int audio_handle) { if (audio_handle > 0) { AudioInternal::AudioOutClose(audio_handle); @@ -431,52 +482,58 @@ int KYTY_SYSV_ABI AudioOut2ContextCreate(const AudioOut2ContextParam* params, vo EXIT_NOT_IMPLEMENTED(params == nullptr); EXIT_NOT_IMPLEMENTED(ctx == nullptr); - *ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed); - - g_audioout2_context_mutex.Lock(); - auto* state = audioout2_find_context_locked(0); - if (state == nullptr) { - for (auto& candidate: g_audioout2_contexts) { - if (!candidate.used) { - state = &candidate; - break; - } - } + const auto num_grains = params->num_grains == 0 ? 512u : params->num_grains; + const int object_audio_handle = + params->max_object_ports == 0 + ? 0 + : AudioInternal::AudioOutOpen(AUDIO_OUT_PORT_TYPE_MAIN, num_grains, 48000, + AudioInternal::Format::FloatStereo); + if (params->max_object_ports != 0 && object_audio_handle == 0) { + return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE; } - EXIT_NOT_IMPLEMENTED(state == nullptr); - *state = AudioOut2ContextState {}; - state->used = true; - state->handle = *ctx; - state->queue_depth = (params->queue_depth == 0 ? 4u : params->queue_depth); - state->queued = 0; - state->num_grains = (params->num_grains == 0 ? 512u : params->num_grains); - state->last_update = LibKernel::KernelGetProcessTime(); - g_audioout2_context_mutex.Unlock(); + + g_audioout2_state_mutex.Lock(); + auto it = std::find_if(g_audioout2_contexts.begin(), g_audioout2_contexts.end(), + [](const auto& state) { return !state.used; }); + if (it == g_audioout2_contexts.end()) { + g_audioout2_state_mutex.Unlock(); + audioout2_close_audio_handle(object_audio_handle); + return AUDIO_OUT2_ERROR_OUT_OF_RESOURCE; + } + *ctx = g_audioout2_next_context.fetch_add(1, std::memory_order_relaxed); + auto* state = &*it; + *state = AudioOut2ContextState {}; + state->used = true; + state->handle = *ctx; + state->queue_depth = params->queue_depth == 0 ? 4u : params->queue_depth; + state->num_grains = num_grains; + state->last_update = LibKernel::KernelGetProcessTime(); + state->object_audio_handle = object_audio_handle; + g_audioout2_state_mutex.Unlock(); LOGF("\t buffer = 0x%016" PRIx64 "\n" "\t buffer_size = 0x%016" PRIx64 "\n" "\t ctx = 0x%016" PRIx64 "\n" "\t queue_depth = %" PRIu32 ", num_grains = %" PRIu32 "\n", reinterpret_cast(buffer), static_cast(buffer_size), *ctx, - state->queue_depth, state->num_grains); + (params->queue_depth == 0 ? 4u : params->queue_depth), num_grains); return OK; } - int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { PRINT_NAME(); LOGF("\t ctx = 0x%016" PRIx64 "\n", ctx); - g_audioout2_context_mutex.Lock(); + std::array audio_handles {}; + size_t audio_handles_num = 0; + + g_audioout2_state_mutex.Lock(); if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { + if (state->object_audio_handle > 0) { + audio_handles[audio_handles_num++] = state->object_audio_handle; + } *state = AudioOut2ContextState {}; } - g_audioout2_context_mutex.Unlock(); - - std::array audio_handles {}; - size_t audio_handles_num = 0; - - g_audioout2_port_mutex.Lock(); for (auto& port_state: g_audioout2_ports) { if (port_state.used && port_state.context == ctx) { if (port_state.audio_handle > 0 && audio_handles_num < audio_handles.size()) { @@ -485,7 +542,7 @@ int KYTY_SYSV_ABI AudioOut2ContextDestroy(AudioOut2ContextHandle ctx) { port_state = AudioOut2PortStateEntry {}; } } - g_audioout2_port_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); for (size_t i = 0; i < audio_handles_num; i++) { audioout2_close_audio_handle(audio_handles[i]); @@ -504,11 +561,16 @@ int KYTY_SYSV_ABI AudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx, } int KYTY_SYSV_ABI AudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { - g_audioout2_context_mutex.Lock(); + g_audioout2_state_mutex.Lock(); if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { audioout2_update_context_locked(state); } - g_audioout2_context_mutex.Unlock(); + for (auto& port: g_audioout2_ports) { + if (port.used && port.context == ctx) { + port.attributes_locked = false; + } + } + g_audioout2_state_mutex.Unlock(); return OK; } @@ -520,11 +582,10 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc // Only a synchronous submission carrying PCM to a real device can rely on the SDL queue for // pacing. Async pushes must retain queue-depth backpressure, and a handle without PCM (or a // vibration/failed-open handle) has no downstream operation that can block this call. - const bool use_device_clock = - blocking != 0 && audioout2_context_has_queueable_device(ctx); - - g_audioout2_context_mutex.Lock(); + g_audioout2_state_mutex.Lock(); if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { + const bool use_device_clock = + blocking != 0 && audioout2_context_has_queueable_device(*state); audioout2_update_context_locked(state); sleep_micros = audioout2_grain_micros(state->num_grains); if (state->queued < state->queue_depth || use_device_clock) { @@ -534,12 +595,12 @@ int KYTY_SYSV_ABI AudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t bloc if (state->queued < state->queue_depth) { state->queued++; } - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); audioout2_queue_context_audio(ctx, blocking != 0); return OK; } } - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); if (blocking == 0) { return AUDIO_OUT2_ERROR_NOT_READY; @@ -558,7 +619,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint *available_queues = 4; } - g_audioout2_context_mutex.Lock(); + g_audioout2_state_mutex.Lock(); if (auto* state = audioout2_find_context_locked(ctx); state != nullptr) { audioout2_update_context_locked(state); if (queue_level != nullptr) { @@ -569,7 +630,7 @@ int KYTY_SYSV_ABI AudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint (state->queued < state->queue_depth ? state->queue_depth - state->queued : 0); } } - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); return OK; } @@ -583,19 +644,25 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut const auto audio_format = audioout2_data_format_to_audio_format(params->data_format); const auto audio_type = audioout2_port_type_to_audio_out_type(params->port_type); - g_audioout2_context_mutex.Lock(); + g_audioout2_state_mutex.Lock(); const auto* context_state = audioout2_find_context_locked(ctx); if (context_state == nullptr) { - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); return AUDIO_OUT2_ERROR_INVALID_PARAM; } - const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains; + if (audioout2_port_type_is_object(params->port_type) && + (params->port_type != 0x100 || context_state->object_audio_handle == 0 || + params->sampling_freq != 48000 || + (audio_format != AudioInternal::Format::FloatMono && + audio_format != AudioInternal::Format::Signed16bitMono))) { + g_audioout2_state_mutex.Unlock(); + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + const auto samples_num = context_state->num_grains; - g_audioout2_port_mutex.Lock(); if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) == g_audioout2_users.end()) { - g_audioout2_port_mutex.Unlock(); - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); return AUDIO_OUT2_ERROR_INVALID_PARAM; } AudioOut2PortStateEntry* port_state = nullptr; @@ -616,9 +683,9 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut port_state->sampling_freq = params->sampling_freq; port_state->samples_num = samples_num; port_state->audio_format = audio_format; + port_state->gains.fill(1.0f); } - g_audioout2_port_mutex.Unlock(); - g_audioout2_context_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); if (port_state == nullptr) { return AUDIO_OUT2_ERROR_PORT_FULL; @@ -632,12 +699,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut audio_format); } - g_audioout2_port_mutex.Lock(); + g_audioout2_state_mutex.Lock(); const bool reserved = port_state->used && port_state->handle == next_port; if (reserved) { port_state->audio_handle = audio_handle; } - g_audioout2_port_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); if (!reserved) { audioout2_close_audio_handle(audio_handle); return AUDIO_OUT2_ERROR_INVALID_PARAM; @@ -663,12 +730,12 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) { LOGF("\t port = 0x%016" PRIx64 "\n", port); int audio_handle = 0; - g_audioout2_port_mutex.Lock(); + g_audioout2_state_mutex.Lock(); if (auto* state = audioout2_find_port_locked(port); state != nullptr) { audio_handle = state->audio_handle; *state = AudioOut2PortStateEntry {}; } - g_audioout2_port_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); audioout2_close_audio_handle(audio_handle); @@ -677,36 +744,86 @@ int KYTY_SYSV_ABI AudioOut2PortDestroy(AudioOut2PortHandle port) { int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) { - EXIT_NOT_IMPLEMENTED(num != 0 && attributes == nullptr); + if (num != 0 && attributes == nullptr) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + Common::LockGuard lock(g_audioout2_state_mutex); + auto* state = audioout2_find_port_locked(port); + if (state == nullptr) { + return AUDIO_OUT2_ERROR_INVALID_PORT; + } - const void* pcm_data = nullptr; - bool has_pcm = false; + const void* pcm_data = nullptr; + auto gains = state->gains; + auto ambisonics = state->ambisonics; + uint32_t seen = 0; for (uint32_t i = 0; i < num; i++) { - if (attributes[i].attribute_id == AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && - attributes[i].value != nullptr && attributes[i].value_size >= sizeof(AudioOut2Pcm)) { - AudioOut2Pcm pcm {}; - std::memcpy(&pcm, attributes[i].value, sizeof(AudioOut2Pcm)); - pcm_data = pcm.data; - has_pcm = true; + const auto& attribute = attributes[i]; + const auto id = attribute.attribute_id; + if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN && + id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS) { + continue; } - } - - if (has_pcm) { - g_audioout2_port_mutex.Lock(); - if (auto* state = audioout2_find_port_locked(port); state != nullptr) { - if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) { - const auto* bytes = static_cast(pcm_data); - state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state)); - } else { - state->pcm_data.clear(); + if ((seen & (1u << id)) != 0) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + seen |= 1u << id; + if (id != AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM && state->attributes_locked) { + continue; + } + if (attribute.value == nullptr) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + switch (id) { + case AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM: { + if (attribute.value_size < sizeof(AudioOut2Pcm)) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + AudioOut2Pcm pcm {}; + std::memcpy(&pcm, attribute.value, sizeof(pcm)); + pcm_data = pcm.data; + break; } + case AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN: { + const auto channels = audioout2_data_format_channels(state->data_format); + if (attribute.value_size != channels * sizeof(float)) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + std::memcpy(gains.data(), attribute.value, attribute.value_size); + for (uint32_t ch = 0; ch < channels; ch++) { + if (!std::isfinite(gains[ch]) || gains[ch] < 0.0f) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + } + break; + } + case AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS: + if (!audioout2_port_type_is_object(state->port_type) || + attribute.value_size != sizeof(ambisonics)) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + std::memcpy(&ambisonics, attribute.value, sizeof(ambisonics)); + if (ambisonics != UINT32_MAX && ambisonics > 15 && + (ambisonics < 64 || ambisonics > 99)) { + return AUDIO_OUT2_ERROR_INVALID_PARAM; + } + break; } - g_audioout2_port_mutex.Unlock(); } + state->gains = gains; + state->ambisonics = ambisonics; + if ((seen & (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM)) != 0) { + if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) { + const auto* bytes = static_cast(pcm_data); + state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state)); + } else { + state->pcm_data.clear(); + } + state->attributes_locked = audioout2_port_type_is_object(state->port_type); + } return OK; } - int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { PRINT_NAME(); @@ -719,11 +836,11 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS state->reroute_counter = 0; state->flags = 0; - g_audioout2_port_mutex.Lock(); + g_audioout2_state_mutex.Lock(); if (auto* port_state = audioout2_find_port_locked(port); port_state != nullptr) { state->num_channels = audioout2_data_format_channels(port_state->data_format); } - g_audioout2_port_mutex.Unlock(); + g_audioout2_state_mutex.Unlock(); // LOGF("\t port = 0x%016" PRIx64 "\n", port); // LOGF("\t num_channels = %" PRIu8 "\n", state->num_channels); @@ -743,7 +860,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han if (handle == nullptr) { return AUDIO_OUT2_ERROR_INVALID_PARAM; } - Common::LockGuard lock(g_audioout2_port_mutex); + Common::LockGuard lock(g_audioout2_state_mutex); *handle = g_audioout2_next_user++; g_audioout2_users.push_back(*handle); LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id, @@ -754,7 +871,7 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) { PRINT_NAME(); LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast(handle)); - Common::LockGuard lock(g_audioout2_port_mutex); + Common::LockGuard lock(g_audioout2_state_mutex); const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle); if (it == g_audioout2_users.end()) { return AUDIO_OUT2_ERROR_INVALID_PARAM; @@ -773,13 +890,15 @@ int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle if (context_attributes == nullptr || port_attributes == nullptr) { return AUDIO_OUT2_ERROR_INVALID_PARAM; } - Common::LockGuard lock(g_audioout2_port_mutex); + Common::LockGuard lock(g_audioout2_state_mutex); if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) == g_audioout2_users.end()) { return AUDIO_OUT2_ERROR_INVALID_PARAM; } *context_attributes = 0; - *port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM; + *port_attributes = (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM) | + (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_GAIN) | + (1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_AMBISONICS); return OK; } diff --git a/tests/AudioOut2PortTests.cpp b/tests/AudioOut2PortTests.cpp index 95e204d3..7802832b 100644 --- a/tests/AudioOut2PortTests.cpp +++ b/tests/AudioOut2PortTests.cpp @@ -3,12 +3,15 @@ #include "libs/errno.h" #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -24,6 +27,7 @@ std::vector g_live_devices; std::vector g_device_backed_handles; std::vector g_output_blocking; std::vector> g_output_pcm; +std::vector> g_output_gains; size_t g_capture_bytes = 0; int g_next_device = 1; int g_open_waiters = 0; @@ -102,8 +106,9 @@ PortParam MakeParam(uint32_t data_format = 0x200) { return param; } -AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) { +AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4, uint32_t max_object_ports = 0) { ContextParam param {}; + param.max_object_ports = max_object_ports; param.queue_depth = queue_depth; param.num_grains = 512; AudioOut2::AudioOut2ContextHandle context = 0; @@ -112,6 +117,12 @@ AudioOut2::AudioOut2ContextHandle CreateContext(uint32_t queue_depth = 4) { return context; } +AudioOut2::AudioOut2PortHandle CreatePort(AudioOut2::AudioOut2ContextHandle context, const PortParam& param) { + AudioOut2::AudioOut2PortHandle port = 0; + Check(AudioOut2::AudioOut2PortCreate(context, AsParam(¶m), &port) == OK, "port create failed"); + return port; +} + void TestUserSupportedAttributes() { constexpr int invalid_param = static_cast(0x80268001u); constexpr int busy = static_cast(0x80268007u); @@ -123,8 +134,8 @@ void TestUserSupportedAttributes() { uint32_t port_attributes = UINT32_MAX; Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, &context_attributes, &port_attributes) == OK && - context_attributes == 0 && port_attributes == 1, - "user capabilities do not match implemented PCM support"); + context_attributes == 0 && port_attributes == 0x103, + "user capabilities do not match implemented attributes"); context_attributes = port_attributes = UINT32_MAX; Check(AudioOut2::AudioOut2UserGetSupportedAttributes(user, nullptr, &port_attributes) == @@ -191,6 +202,13 @@ void SetPcm(AudioOut2::AudioOut2PortHandle port, const void* data) { "setting PCM failed"); } +template +void SetAttribute(AudioOut2::AudioOut2PortHandle port, uint32_t id, const T& value) { + const Attribute attribute {id, 0, &value, sizeof(value)}; + Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == OK, + "setting attribute failed"); +} + void ResetOutputCalls() { std::lock_guard lock(g_device_mutex); g_output_blocking.clear(); @@ -212,6 +230,23 @@ std::vector> OutputPcm() { return g_output_pcm; } +std::vector> PushPcm(AudioOut2::AudioOut2ContextHandle context, size_t bytes) { + CaptureOutputPcm(bytes); + Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "PCM push failed"); + auto output = OutputPcm(); + CaptureOutputPcm(0); + return output; +} + +void CheckSamples(const std::vector& pcm, std::initializer_list frame, const char* message) { + Check(!pcm.empty() && pcm.size() % (frame.size() * sizeof(float)) == 0, "incorrect PCM size"); + for (size_t i = 0; i < pcm.size() / sizeof(float); i++) { + float sample; + std::memcpy(&sample, pcm.data() + i * sizeof(float), sizeof(float)); + Check(std::abs(sample - *(frame.begin() + i % frame.size())) < 0.000001f, message); + } +} + void TestSlotReuse() { const auto context = CreateContext(); const auto param = MakeParam(); @@ -428,6 +463,148 @@ void TestPcmCopiedBeforeScratchBufferReuse() { AudioOut2::AudioOut2ContextDestroy(context); } +void TestPortGainAndValidation() { + const auto context = CreateContext(); + const auto port = CreatePort(context, MakeParam()); + std::vector pcm(512 * 2, 0.25f); + SetPcm(port, pcm.data()); + auto check_output = [&](const std::array& gain, float expected_pcm) { + CheckSamples(PushPcm(context, pcm.size() * sizeof(float)).at(0), {expected_pcm}, + "gain modified stored PCM"); + std::lock_guard lock(g_device_mutex); + Check(g_output_gains.size() == 1 && g_output_gains[0] == gain, + "incorrect per-channel gain forwarded to backend"); + }; + check_output({1.0f, 1.0f}, 0.25f); + for (const std::array gain: {std::array {2.0f, 0.5f}, {0.0f, 0.0f}, {1.0f, 1.0f}}) { + SetAttribute(port, 1, gain); + check_output(gain, 0.25f); + check_output(gain, 0.25f); + } + + const std::array gain {0.5f, 0.25f}; + SetAttribute(port, 1, gain); + std::fill(pcm.begin(), pcm.end(), 0.75f); + SetPcm(port, pcm.data()); + check_output(gain, 0.75f); + + std::fill(pcm.begin(), pcm.end(), -0.25f); + const Pcm replacement {pcm.data()}; + for (const float invalid: {-1.0f, std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + const std::array invalid_gain {1.0f, invalid}; + const Attribute attributes[] {{0, 0, &replacement, sizeof(replacement)}, + {1, 0, &invalid_gain, sizeof(invalid_gain)}}; + Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(attributes), 2) == + static_cast(0x80268001u), "invalid gain was accepted"); + check_output(gain, 0.75f); + } + for (const Attribute attribute: {Attribute {1, 0, nullptr, sizeof(gain)}, + Attribute {1, 0, &gain, sizeof(float)}}) { + Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(&attribute), 1) == + static_cast(0x80268001u), "malformed gain was accepted"); + } + const Attribute duplicates[] {{0, 0, &replacement, sizeof(replacement)}, + {1, 0, &gain, sizeof(gain)}, {1, 0, &gain, sizeof(gain)}}; + Check(AudioOut2::AudioOut2PortSetAttributes(port, AsAttribute(duplicates), 3) == + static_cast(0x80268001u), "duplicate gain attributes were accepted"); + check_output(gain, 0.75f); + AudioOut2::AudioOut2ContextDestroy(context); +} + +void TestObjectGainAndAmbisonics() { + constexpr float half_power = 0.70710678f; + struct Coefficients { uint32_t channel; float left; float right; }; + constexpr Coefficients cases[] {{UINT32_MAX, half_power, half_power}, {0, 1.0f, 1.0f}, + {1, 0.0f, 0.0f}, {2, half_power, -half_power}, {3, 0.0f, 0.0f}, + {64, half_power, half_power}, {65, half_power, -half_power}, + {66, 0.0f, 0.0f}, {99, 0.0f, 0.0f}}; + for (const auto format: {0x100u, 0x101u}) { + const auto context = CreateContext(1, 1); + auto param = MakeParam(format); + param.port_type = 0x100; + const auto object = CreatePort(context, param); + Check(LiveDeviceCount() == 1, "object-only context did not create one output device"); + std::vector floating(512, 0.5f); + std::vector integer(512, 16384); + const void* pcm = format == 0x100 ? static_cast(floating.data()) : integer.data(); + constexpr auto output_bytes = 512 * 2 * sizeof(float); + SetPcm(object, pcm); + std::fill(floating.begin(), floating.end(), 0.0f); + std::fill(integer.begin(), integer.end(), 0); + SetAttribute(object, 1, 0.0f); + SetAttribute(object, 8, 65u); + for (int push = 0; push < 2; push++) { + const auto output = PushPcm(context, output_bytes); + Check(output.size() == 1, "object-only push did not output exactly once"); + CheckSamples(output[0], {0.5f * half_power, 0.5f * half_power}, + "object PCM ownership, default gain, or attribute freeze failed"); + } + std::fill(floating.begin(), floating.end(), 0.5f); + std::fill(integer.begin(), integer.end(), 16384); + for (const auto& test: cases) { + Check(AudioOut2::AudioOut2ContextAdvance(context) == OK, "context advance failed"); + const Pcm input {pcm}; + const float gain = 0.5f; + const Attribute attributes[] {{0, 0, &input, sizeof(input)}, {1, 0, &gain, sizeof(gain)}, + {8, 0, &test.channel, sizeof(test.channel)}}; + Check(AudioOut2::AudioOut2PortSetAttributes(object, AsAttribute(attributes), 3) == OK, + "object attributes could not be set atomically with PCM first"); + for (int push = 0; push < 2; push++) { + CheckSamples(PushPcm(context, output_bytes).at(0), {0.25f * test.left, 0.25f * test.right}, + "object gain or ambisonics decoded incorrectly or accumulated across pushes"); + } + } + AudioOut2::AudioOut2ContextAdvance(context); + SetAttribute(object, 8, UINT32_MAX); + SetAttribute(object, 1, 0.0f); + SetPcm(object, pcm); + CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "zero gain did not mute object"); + AudioOut2::AudioOut2ContextAdvance(context); + SetPcm(object, pcm); + CheckSamples(PushPcm(context, output_bytes).at(0), {0.0f}, "object gain did not persist"); + AudioOut2::AudioOut2ContextDestroy(context); + Check(LiveDeviceCount() == 0, "object context leaked its output device"); + } +} + +void TestObjectBusRouting() { + const auto context = CreateContext(1, 2); + const auto other_context = CreateContext(1, 1); + AudioOut2::AudioOut2UserHandle other_user = 0; + Check(AudioOut2::AudioOut2UserCreate(2000, &other_user) == OK, "second user create failed"); + const auto first = CreatePort(context, MakeParam()); + const auto second = CreatePort(context, MakeParam()); + std::vector bed(512 * 2, 0.125f); + SetPcm(first, bed.data()); + SetPcm(second, bed.data()); + auto param = MakeParam(0x100); + param.port_type = 0x100; + std::vector object_pcm(512, 0.25f); + SetPcm(CreatePort(context, param), object_pcm.data()); + param.user_handle = other_user; + SetPcm(CreatePort(context, param), object_pcm.data()); + std::fill(object_pcm.begin(), object_pcm.end(), 1.0f); + SetPcm(CreatePort(other_context, param), object_pcm.data()); + constexpr auto bytes = 512 * 2 * sizeof(float); + for (int push = 0; push < 2; push++) { + const auto output = PushPcm(context, bytes); + Check(output.size() == 3, "object bus was missing or queued more than once"); + CheckSamples(output[0], {0.125f}, "objects modified first bed"); + CheckSamples(output[1], {0.125f}, "objects modified second bed"); + CheckSamples(output[2], {0.35355339f}, "MAIN objects failed to mix across users or leaked contexts"); + } + SetPcm(first, nullptr); + SetPcm(second, nullptr); + CheckSamples(PushPcm(context, bytes).at(0), {0.35355339f}, "object output depended on bed PCM"); + AudioOut2::AudioOut2ContextDestroy(context); + Check(LiveDeviceCount() == 1, "context destroy closed another context's object device"); + CheckSamples(PushPcm(other_context, bytes).at(0), {0.70710678f}, "other context lost its object PCM"); + AudioOut2::AudioOut2ContextDestroy(other_context); + Check(AudioOut2::AudioOut2UserDestroy(other_user) == OK, "second user destroy failed"); + Check(LiveDeviceCount() == 0, "object bus device leaked"); +} + } // namespace namespace Libs::Audio::AudioInternal { @@ -467,10 +644,14 @@ bool AudioOutHasDevice(int handle) { uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) { std::lock_guard lock(g_device_mutex); g_output_blocking.push_back(blocking); + g_output_gains.clear(); if (g_capture_bytes != 0) { for (uint32_t i = 0; i < num; i++) { const auto* bytes = static_cast(params[i].data); g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes); + g_output_gains.push_back(params[i].gains != nullptr + ? std::array {params[i].gains[0], params[i].gains[1]} + : std::array {1.0f, 1.0f}); } } return 0; @@ -499,6 +680,9 @@ int main() { TestAsynchronousDevicePushKeepsQueueBounded(); TestHandleWithoutPcmDoesNotBypassQueue(); TestPcmCopiedBeforeScratchBufferReuse(); + TestPortGainAndValidation(); + TestObjectGainAndAmbisonics(); + TestObjectBusRouting(); Check(AudioOut2::AudioOut2UserDestroy(g_user_handle) == OK, "test user destroy failed"); std::printf("AudioOut2PortTests: all cases passed\n"); return 0; diff --git a/tests/AudioOutTimingTests.cpp b/tests/AudioOutTimingTests.cpp index c0d4818f..120d6c54 100644 --- a/tests/AudioOutTimingTests.cpp +++ b/tests/AudioOutTimingTests.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -22,6 +24,7 @@ struct Stream { SDL_AudioSpec spec; uint64_t frames = 0; // Millionths of a frame retain exact simulated consumption. uint64_t updated; + std::vector pcm; }; uint64_t now = 1000000; @@ -31,6 +34,7 @@ std::vector> streams; bool fail_open = false, fail_put = false, stalled = false; bool pad_connected = false, pad_bluetooth = false; uint64_t pad_queue_us = 0; +const float* queued_pad_gains = nullptr; int clears = 0; Stream& GetStream(SDL_AudioStream* stream) { @@ -61,7 +65,7 @@ SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID, const SDL_AudioSpec* s return nullptr; } - streams.push_back(std::make_unique(Stream {*spec, 0, now})); + streams.push_back(std::make_unique(Stream {*spec, 0, now, {}})); return reinterpret_cast(streams.back().get()); } @@ -80,12 +84,14 @@ int GetAudioStreamQueued(SDL_AudioStream* stream) { state.spec.channels); } -bool PutAudioStreamData(SDL_AudioStream* stream, const void*, int bytes) { +bool PutAudioStreamData(SDL_AudioStream* stream, const void* data, int bytes) { now += processing; if (fail_put) { return false; } auto& state = GetStream(stream); + const auto* pcm = static_cast(data); + state.pcm.assign(pcm, pcm + bytes); Drain(state); state.frames += static_cast(bytes) * 1000000 / (SDL_AUDIO_BYTESIZE(state.spec.format) * state.spec.channels); @@ -148,7 +154,8 @@ 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*, float) { +uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float, const float* gains) { + queued_pad_gains = gains; return stream != nullptr ? pad_queue_us : 0; } } // namespace DualSenseHaptics @@ -181,6 +188,7 @@ struct Fixture { fail_open = fail_put = stalled = false; pad_connected = pad_bluetooth = false; pad_queue_us = 0; + queued_pad_gains = nullptr; sleeps.clear(); } @@ -386,6 +394,68 @@ void TestZeroOutputFrequency() { "internal output open accepted zero frequency"); Check(f.Open().ToInt() == 1, "rejected output port occupied a handle"); } + +template +void CheckOutputSamples(std::initializer_list expected, const char* message) { + const auto& pcm = streams.back()->pcm; + Check(pcm.size() == expected.size() * sizeof(T) && + std::memcmp(pcm.data(), expected.begin(), pcm.size()) == 0, message); +} + +void TestChannelGains() { + Fixture f; + const std::array pcm {0.25f, -0.5f, 0.75f, -1.0f}; + const auto stereo = f.audio.AudioOutOpen(0, 2, 48000, Audio::Format::FloatStereo); + std::array gains {2.0f, 0.5f}; + Audio::OutputParam output {stereo, pcm.data(), gains.data()}; + for (int push = 0; push < 2; push++) { + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({0.5f, -0.25f, 1.5f, -0.5f}, "float channel gains were not applied independently"); + } + const int volume[] {16384, 32768}; + f.audio.AudioOutSetVolume(stereo, 3, volume); + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({0.25f, -0.25f, 0.75f, -0.5f}, "channel gain did not combine with port volume"); + gains = {0.0f, 0.0f}; + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({0.0f, -0.0f, 0.0f, -0.0f}, "zero channel gains did not mute"); + output.gains = nullptr; + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({0.125f, -0.5f, 0.375f, -1.0f}, "channel scaling changed original float PCM"); + + const std::array integer_pcm {20000, -20000}; + const auto integer = f.audio.AudioOutOpen(0, 1, 48000, Audio::Format::Signed16bitStereo); + output = {integer, integer_pcm.data(), gains.data()}; + for (const auto gain: {2.0f, std::numeric_limits::max()}) { + gains = {gain, gain}; + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({32767, -32768}, "integer gain overflowed instead of saturating"); + } + gains = {0.5f, 0.25f}; + f.audio.AudioOutOutputs(&output, 1, false); + CheckOutputSamples({10000, -5000}, "integer channel gains changed original PCM"); + + const std::array surround_pcm {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f}; + for (const auto format: {Audio::Format::Float8Ch, Audio::Format::Float12Ch}) { + const auto surround = f.audio.AudioOutOpen(0, 1, 48000, format); + output = {surround, surround_pcm.data(), surround_pcm.data()}; + f.audio.AudioOutOutputs(&output, 1, false); + if (format == Audio::Format::Float8Ch) { + CheckOutputSamples({1.0f, 4.0f, 9.0f, 16.0f, 49.0f, 64.0f, 25.0f, 36.0f}, + "channel reordering used destination channel gains"); + } else { + CheckOutputSamples({82.0f, 104.0f, 9.0f, 16.0f, 170.0f, 208.0f, 25.0f, 36.0f}, + "height downmix lost per-channel gains"); + } + } + pad_connected = true; + const auto speaker = f.Open(2, 4); + gains = {0.75f, 0.25f}; + output = {speaker, pcm.data(), gains.data()}; + f.audio.AudioOutOutputs(&output, 1, false); + Check(queued_pad_gains != nullptr && queued_pad_gains[0] == gains[0] && queued_pad_gains[1] == gains[1], + "connected controller did not receive channel gains"); +} } // namespace int main() { @@ -399,6 +469,7 @@ int main() { TestControllerSpeakerPacing(); TestInvalidBatchSize(); TestZeroOutputFrequency(); + TestChannelGains(); Check(streams.empty(), "output stream leaked"); std::puts("AudioOutTimingTests: all cases passed"); } diff --git a/tests/PadHapticsTests.cpp b/tests/PadHapticsTests.cpp index db5dbf65..47234959 100644 --- a/tests/PadHapticsTests.cpp +++ b/tests/PadHapticsTests.cpp @@ -414,11 +414,16 @@ void TestFormatsAndVolume() { Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32, "did not select quad DualSense PCM"); ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0}); + const std::array gains {0.25f, 2.0f}; + Haptics::Queue(port.get(), 1, stereo.data(), 2, 2, true, volume.data(), 1.0f, gains.data()); + ExpectPcm({0, 0, 0.125f, -0.25f, 0, 0, 0.25f, 0}); 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}); + Haptics::Queue(port.get(), 1, mono.data(), 2, 1, false, volume.data() + 1, 1.0f, gains.data()); + ExpectPcm({0, 0, 0.0625f, 0.0625f, 0, 0, -0.125f, -0.125f}); std::array multichannel {}; multichannel.fill(0.9f); multichannel[0] = 0.1f;