mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
audio: mix AudioOut2 objects and apply port gains
Co-authored-by: chenxiao07 <chenxiao07@gmail.com>
This commit is contained in:
@@ -3,12 +3,15 @@
|
||||
#include "libs/errno.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -24,6 +27,7 @@ std::vector<int> g_live_devices;
|
||||
std::vector<int> g_device_backed_handles;
|
||||
std::vector<bool> g_output_blocking;
|
||||
std::vector<std::vector<uint8_t>> g_output_pcm;
|
||||
std::vector<std::array<float, 2>> 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<int32_t>(0x80268001u);
|
||||
constexpr int busy = static_cast<int32_t>(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 <typename T>
|
||||
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<std::vector<uint8_t>> OutputPcm() {
|
||||
return g_output_pcm;
|
||||
}
|
||||
|
||||
std::vector<std::vector<uint8_t>> 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<uint8_t>& pcm, std::initializer_list<float> 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<float> pcm(512 * 2, 0.25f);
|
||||
SetPcm(port, pcm.data());
|
||||
auto check_output = [&](const std::array<float, 2>& 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<float, 2> 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<float>::infinity(),
|
||||
std::numeric_limits<float>::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<int32_t>(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<int32_t>(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<int32_t>(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<float> floating(512, 0.5f);
|
||||
std::vector<int16_t> integer(512, 16384);
|
||||
const void* pcm = format == 0x100 ? static_cast<const void*>(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<float> bed(512 * 2, 0.125f);
|
||||
SetPcm(first, bed.data());
|
||||
SetPcm(second, bed.data());
|
||||
auto param = MakeParam(0x100);
|
||||
param.port_type = 0x100;
|
||||
std::vector<float> 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<const uint8_t*>(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;
|
||||
|
||||
Reference in New Issue
Block a user