AnyPS5/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp

396 lines
19 KiB
C++

#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "AudioOut2Internal.hpp"
// An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples)
// to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so
// the level must follow the output clock. With an SDL device open, a grain's port buffers are read at
// the next push, the device callback plays the mixed grains behind a cushion and the level counts the
// grains beyond that cushion; otherwise each push is mixed at once and a wall-clock model of the
// queue stands in.
using Clock = std::chrono::steady_clock;
static constexpr std::size_t CONTEXT_MEMORY = 0x10000;
static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16;
// Pushes, polls and advances traced individually before the trace falls back to the per-second summary.
static constexpr std::uint64_t CALL_TRACE_FULL = 16;
// Grains the device waits for before it starts playing (and again whenever it ran dry), so the
// pushing thread's scheduling jitter does not starve it; the pad device queues as much silence ahead
// of its first grain.
static constexpr std::uint32_t CUSHION_MS = 40;
// Grains the device has not played are not allowed to run further ahead than this; newer ones are dropped.
static constexpr std::uint32_t MAX_QUEUED_MS = 250;
// A blocking push on a full queue gives up after this long.
static constexpr std::chrono::milliseconds FULL_WAIT_TIMEOUT{200};
static constexpr std::chrono::milliseconds FULL_WAIT_STEP{1};
static constexpr std::chrono::microseconds READ_DELAY{100};
static constexpr std::uint16_t DEVICE_SAMPLES = 512;
// The summed ports (a 7.1 bed folded to stereo plus the object ports) peak above full scale in the
// intro cutscene (2.1 measured); the device takes float and clips hard, so the mix is attenuated
// and clamped.
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;
return enabled;
}
double AudioOut2TraceSeconds() {
static const auto start = Clock::now();
return std::chrono::duration<double>(Clock::now() - start).count();
}
static AudioOut2Context* FromHandle(AudioOut2ContextHandle ctx) {
return reinterpret_cast<AudioOut2Context*>(ctx);
}
static Clock::duration GrainDuration(const AudioOut2Context& context) {
return std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double>(static_cast<double>(context.grain) / AUDIO_OUT2_SAMPLE_RATE));
}
static std::uint32_t GrainBytes(const AudioOut2Context& context) {
return static_cast<std::uint32_t>(context.grain * OUTPUT_FRAME_BYTES);
}
static std::uint32_t CushionGrains(const AudioOut2Context& context) {
return (CUSHION_MS * AUDIO_OUT2_SAMPLE_RATE / 1000 + context.grain - 1) / context.grain;
}
static std::uint32_t UnplayedGrains(const AudioOut2Context& context) {
const auto frames = context.output.size() / AUDIO_OUT2_OUTPUT_CHANNELS;
return static_cast<std::uint32_t>(context.pendingGrains.size() + (frames + context.grain - 1) / context.grain);
}
static std::uint32_t PendingMs(const AudioOut2Context& context) {
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(UnplayedGrains(context)) * context.grain * 1000 / AUDIO_OUT2_SAMPLE_RATE);
}
// Retires the modelled grains whose playback finished by now. The caller holds the context lock.
static void Drain(AudioOut2Context& context, Clock::time_point now) {
const auto duration = GrainDuration(context);
while (context.queued > 0) {
const auto oldestEnd = context.playHead - duration * (context.queued - 1);
if (oldestEnd > now) break;
context.queued--;
}
if (context.queued == 0 && context.playHead < now) context.playHead = now;
}
// Grains queued and not yet played, as the title sees them. The caller holds the context lock.
static std::uint32_t QueueLevel(AudioOut2Context& context, Clock::time_point now) {
Drain(context, now);
if (context.device == 0) return context.queued;
const auto unplayed = UnplayedGrains(context);
const auto cushion = CushionGrains(context);
return unplayed > cushion ? std::min(context.queueDepth, unplayed - cushion) : 0;
}
static void DeviceCallback(void* userdata, Uint8* stream, int length);
static void OpenDevice(AudioOut2Context& context) {
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
AUDIOOUT2_TRACE("SDL audio init failed: %s\n", SDL_GetError());
return;
}
SDL_AudioSpec desired{};
desired.freq = static_cast<int>(AUDIO_OUT2_SAMPLE_RATE);
desired.format = AUDIO_F32SYS;
desired.channels = AUDIO_OUT2_OUTPUT_CHANNELS;
desired.samples = DEVICE_SAMPLES;
desired.callback = DeviceCallback;
desired.userdata = &context;
SDL_AudioSpec obtained{};
// No format change is allowed: SDL converts to the device's native format itself, so the callback
// always writes stereo float at 48 kHz.
context.device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0);
if (context.device == 0) {
AUDIOOUT2_TRACE("SDL_OpenAudioDevice failed: %s\n", SDL_GetError());
return;
}
SDL_PauseAudioDevice(context.device, 0);
AUDIOOUT2_TRACE("SDL device %u opened: %d Hz, format 0x%x, %u channels, %u samples\n", context.device, obtained.freq, obtained.format, obtained.channels, obtained.samples);
}
static void CloseDevice(SDL_AudioDeviceID device) {
if (device != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) SDL_CloseAudioDevice(device);
}
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 ? UnplayedGrains(context) * grainBytes : 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 void Render(AudioOut2Context& context, const AudioOut2Grain& grain) {
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);
AudioOut2MixPorts(context, grain, 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);
}
}
static void DeviceCallback(void* userdata, Uint8* stream, int length) {
auto& context = *static_cast<AudioOut2Context*>(userdata);
std::lock_guard lock(context.lock);
auto* out = reinterpret_cast<float*>(stream);
const auto samples = static_cast<std::size_t>(length) / sizeof(float);
const auto cushionSamples = static_cast<std::size_t>(CushionGrains(context)) * context.grain * AUDIO_OUT2_OUTPUT_CHANNELS;
if (context.priming && context.output.size() >= cushionSamples) context.priming = false;
const auto count = context.priming ? 0 : std::min(samples, context.output.size());
std::copy_n(context.output.begin(), count, out);
context.output.erase(context.output.begin(), context.output.begin() + static_cast<std::ptrdiff_t>(count));
std::fill(out + count, out + samples, 0.0f);
if (count < samples && !context.priming) {
context.priming = true;
context.primes++;
}
}
static void TraceSummary(AudioOut2Context& context, Clock::time_point now) {
if (!AudioOut2TraceEnabled()) return;
if (context.summaryStart == Clock::time_point{}) {
context.summaryStart = now;
return;
}
const auto elapsed = std::chrono::duration<double>(now - context.summaryStart).count();
if (elapsed < 1.0) return;
std::fprintf(stderr, "[audioout2] t=%.3f ctx %p: %.1f pushes/s, %.1f advances/s, %.1f queue polls/s, hw queue %u/%u, unread %u ms, mix peak %.3f; totals: pushes %llu (%llu blocking, %llu queue-full rejects), primes %llu, dropped %llu\n",
AudioOut2TraceSeconds(), static_cast<void*>(&context), static_cast<double>(context.summaryPushes) / elapsed, static_cast<double>(context.summaryAdvances) / elapsed,
static_cast<double>(context.summaryPolls) / elapsed, QueueLevel(context, now), context.queueDepth, PendingMs(context), static_cast<double>(context.summaryPeak),
static_cast<unsigned long long>(context.pushes), static_cast<unsigned long long>(context.blockingPushes), static_cast<unsigned long long>(context.fullRejects),
static_cast<unsigned long long>(context.primes), static_cast<unsigned long long>(context.dropped));
context.summaryStart = now;
context.summaryPushes = 0;
context.summaryAdvances = 0;
context.summaryPolls = 0;
context.summaryPeak = 0.0f;
}
extern "C" {
// The title's per-tick step between setting the ports' data and pushing. A push records the buffers the
// ports point at and the next push reads them, and the push paces the clock, so nothing is due here.
int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) {
auto* context = FromHandle(ctx);
if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
std::lock_guard lock(context->lock);
context->advances++;
context->summaryAdvances++;
if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast<void*>(context), static_cast<unsigned long long>(context->pushes));
return 0;
}
int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam* params, void* buffer, size_t buffer_size, AudioOut2ContextHandle* ctx) {
if (!params || !ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
auto* context = new AudioOut2Context();
context->grain = params->num_grains ? params->num_grains : AUDIO_OUT2_DEFAULT_GRAIN;
context->queueDepth = params->queue_depth ? params->queue_depth : 1;
context->playHead = Clock::now();
context->mix.assign(static_cast<std::size_t>(context->grain) * AUDIO_OUT2_OUTPUT_CHANNELS, 0.0f);
AUDIOOUT2_TRACE("t=%.3f ContextCreate: max_ports=%u max_object_ports=%u guarantee_object_ports=%u queue_depth=%u num_grains=%u flags=0x%x buffer=%p size=%zu -> ctx %p: %u-sample grains (%.2f ms), %u queued, %u Hz stereo float output\n",
AudioOut2TraceSeconds(), params->max_ports, params->max_object_ports, params->guarantee_object_ports, params->queue_depth, params->num_grains, params->flags, buffer, buffer_size,
static_cast<void*>(context), context->grain, 1000.0 * context->grain / AUDIO_OUT2_SAMPLE_RATE, context->queueDepth, AUDIO_OUT2_SAMPLE_RATE);
OpenDevice(*context);
*ctx = reinterpret_cast<AudioOut2ContextHandle>(context);
return 0;
}
int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle ctx) {
auto* context = FromHandle(ctx);
if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
AUDIOOUT2_TRACE("t=%.3f ContextDestroy ctx %p after %llu pushes\n", AudioOut2TraceSeconds(), static_cast<void*>(context), static_cast<unsigned long long>(context->pushes));
SDL_AudioDeviceID device = 0;
{
std::lock_guard lock(context->lock);
device = std::exchange(context->device, 0);
ClosePadDevice(*context);
}
CloseDevice(device);
AudioOut2ReleasePorts(*context);
delete context;
return 0;
}
int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queue) {
auto* context = FromHandle(ctx);
if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
std::lock_guard lock(context->lock);
const auto level = QueueLevel(*context, Clock::now());
if (queue_level) *queue_level = level;
if (available_queue) *available_queue = context->queueDepth - level;
context->queueLevelPolls++;
context->summaryPolls++;
if (context->queueLevelPolls <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f GetQueueLevel ctx %p -> level %u, available %u\n", AudioOut2TraceSeconds(), static_cast<void*>(context), level, context->queueDepth - level);
return 0;
}
int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t blocking) {
auto* context = FromHandle(ctx);
if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
std::unique_lock lock(context->lock);
auto now = Clock::now();
const auto waitStart = now;
while (QueueLevel(*context, now) >= context->queueDepth) {
if (!blocking) {
context->fullRejects++;
if (context->fullRejects <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Push ctx %p non-blocking on a full queue (%u): rejected\n", AudioOut2TraceSeconds(), static_cast<void*>(context), context->queueDepth);
return SCE_AUDIO_OUT2_ERROR_QUEUE_FULL;
}
if (now - waitStart > FULL_WAIT_TIMEOUT) break;
lock.unlock();
std::this_thread::sleep_for(FULL_WAIT_STEP);
lock.lock();
now = Clock::now();
}
context->queued++;
context->playHead += GrainDuration(*context);
UpdatePadDevice(*context, now);
auto grain = AudioOut2CaptureGrain(*context);
const auto mixed = static_cast<std::uint32_t>(grain.size());
if (context->device == 0) {
Render(*context, grain);
} else {
context->pendingGrains.push_back(std::move(grain));
context->pendingSince.push_back(Clock::now());
while (context->pendingGrains.size() >= 2) {
const auto readAt = context->pendingSince.front() + READ_DELAY;
if (Clock::now() < readAt) {
lock.unlock();
while (Clock::now() < readAt) std::this_thread::yield();
lock.lock();
continue;
}
Render(*context, context->pendingGrains.front());
context->pendingGrains.pop_front();
context->pendingSince.pop_front();
if (PendingMs(*context) > MAX_QUEUED_MS) {
context->dropped++;
continue;
}
context->output.insert(context->output.end(), context->mix.begin(), context->mix.end());
}
}
context->pushes++;
context->summaryPushes++;
if (blocking) context->blockingPushes++;
if (context->pushes <= CALL_TRACE_FULL) {
AUDIOOUT2_TRACE("t=%.3f Push ctx %p blocking=%u: grain from %u ports, hw queue %u/%u, unread %u ms\n", AudioOut2TraceSeconds(), static_cast<void*>(context), blocking,
mixed, QueueLevel(*context, now), context->queueDepth, PendingMs(*context));
}
TraceSummary(*context, now);
return 0;
}
int APS5_VABI sceAudioOut2ContextQueryMemory(const AudioOut2ContextParam* params, size_t* memory_size) {
if (!params || !memory_size) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
*memory_size = CONTEXT_MEMORY;
return 0;
}
int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam* params) {
if (!params) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
std::memset(params, 0, sizeof(*params));
params->max_ports = DEFAULT_MAX_PORTS;
params->queue_depth = 1;
params->num_grains = AUDIO_OUT2_DEFAULT_GRAIN;
return 0;
}
int APS5_VABI sceAudioOut2ContextSetAttributes(AudioOut2ContextHandle ctx, const AudioOut2Attribute* attributes, uint32_t num) {
auto* context = FromHandle(ctx);
if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
if (!attributes && num != 0) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
for (uint32_t index = 0; index < num; index++) {
AUDIOOUT2_TRACE("t=%.3f ContextSetAttributes ctx %p: id=0x%x size=%zu value=%p (ignored)\n", AudioOut2TraceSeconds(), static_cast<void*>(context), attributes[index].attribute_id, attributes[index].value_size, attributes[index].value);
}
return 0;
}
}