#include #include #include #include #include #include #include #include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "AudioOut2Internal.hpp" // An AudioOut2 context is the hardware output queue: every push mixes the ports' current grain // (num_grains samples) and appends it to a queue of queue_depth grains that plays in real time. The // title paces its mixer on that queue (Demon's Souls polls GetQueueLevel and pushes non-blocking // whenever a slot is free), so the level must follow the output clock: with an SDL device open the // device's own queue is the hardware queue, otherwise a wall-clock model of it 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; // Silence queued ahead of the first grain (and again whenever the SDL queue ran dry) so scheduling // jitter of the pushing thread does not starve the device; the queue level reported to the title // counts only what lies beyond it. static constexpr std::uint32_t CUSHION_MS = 40; // The SDL queue is not allowed to run further ahead than this; grains beyond it 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::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(Clock::now() - start).count(); } static AudioOut2Context* FromHandle(AudioOut2ContextHandle ctx) { return reinterpret_cast(ctx); } static Clock::duration GrainDuration(const AudioOut2Context& context) { return std::chrono::duration_cast(std::chrono::duration(static_cast(context.grain) / AUDIO_OUT2_SAMPLE_RATE)); } static std::uint32_t GrainBytes(const AudioOut2Context& context) { return static_cast(context.grain * OUTPUT_FRAME_BYTES); } static std::uint32_t SdlQueuedMs(const AudioOut2Context& context) { return context.device ? SDL_GetQueuedAudioSize(context.device) / OUTPUT_BYTES_PER_MS : 0; } // 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 queuedBytes = SDL_GetQueuedAudioSize(context.device); const auto cushionBytes = CUSHION_MS * OUTPUT_BYTES_PER_MS; const auto pending = queuedBytes > cushionBytes ? queuedBytes - cushionBytes : 0; const auto grainBytes = GrainBytes(context); return std::min(context.queueDepth, (pending + grainBytes - 1) / grainBytes); } 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(AUDIO_OUT2_SAMPLE_RATE); desired.format = AUDIO_F32SYS; desired.channels = AUDIO_OUT2_OUTPUT_CHANNELS; desired.samples = DEVICE_SAMPLES; desired.callback = nullptr; SDL_AudioSpec obtained{}; // No format change is allowed: SDL converts to the device's native format itself, so the queue // always takes 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(AudioOut2Context& context) { if (context.device != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { SDL_ClearQueuedAudio(context.device); SDL_CloseAudioDevice(context.device); } context.device = 0; } static void OpenPadDevice(AudioOut2Context& context) { if (SDL_WasInit(SDL_INIT_AUDIO) == 0 && SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) return; const int count = SDL_GetNumAudioDevices(0); for (int index = 0; index < count; index++) { const char* listed = SDL_GetAudioDeviceName(index, 0); if (!AudioOut2IsPadAudioDevice(listed)) continue; const std::string name(listed); SDL_AudioSpec native{}; if (SDL_GetAudioDeviceSpec(index, 0, &native) == 0 && native.channels != 0 && native.channels != AUDIO_OUT2_PAD_CHANNELS) continue; const auto layout = AudioOut2PadLayoutForDriver(SDL_GetCurrentAudioDriver()); SDL_AudioSpec desired{}; desired.freq = static_cast(AUDIO_OUT2_SAMPLE_RATE); desired.format = AUDIO_F32SYS; desired.channels = static_cast(layout.channels); desired.samples = DEVICE_SAMPLES; desired.callback = nullptr; SDL_AudioSpec obtained{}; context.padDevice = SDL_OpenAudioDevice(name.c_str(), 0, &desired, &obtained, 0); if (context.padDevice == 0) { APS5_LOG_ERR("AudioOut2: could not open the controller sound card '%s': %s", name.c_str(), SDL_GetError()); return; } context.padLayout = layout; context.padMix.assign(static_cast(context.grain) * AUDIO_OUT2_PAD_CHANNELS, 0.0f); context.padFrames.assign(static_cast(context.grain) * layout.channels, 0.0f); SDL_PauseAudioDevice(context.padDevice, 0); APS5_LOG_OUT("AudioOut2: controller speaker and vibration ports play on '%s'", name.c_str()); return; } } static void ClosePadDevice(AudioOut2Context& context) { if (context.padDevice != 0 && SDL_WasInit(SDL_INIT_AUDIO) != 0) { SDL_ClearQueuedAudio(context.padDevice); SDL_CloseAudioDevice(context.padDevice); } context.padDevice = 0; } static void UpdatePadDevice(AudioOut2Context& context, Clock::time_point now) { if (context.padDevice != 0 && SDL_GetAudioDeviceStatus(context.padDevice) == SDL_AUDIO_STOPPED) { APS5_LOG_CHARS_OUT("AudioOut2: the controller sound card went away; its ports play in the main mix"); ClosePadDevice(context); context.nextPadProbe = now + PAD_PROBE_INTERVAL; } if (context.padDevice != 0 || now < context.nextPadProbe) return; context.nextPadProbe = now + PAD_PROBE_INTERVAL; if (AudioOut2HasPadPorts(context)) OpenPadDevice(context); } static void QueuePadGrain(AudioOut2Context& context) { AudioOut2WritePadFrames(context.padMix.data(), context.padLayout, context.padFrames.data(), context.grain); const auto frameBytes = static_cast(context.padLayout.channels * sizeof(float)); const auto grainBytes = context.grain * frameBytes; const auto cushionBytes = CUSHION_MS * (AUDIO_OUT2_SAMPLE_RATE / 1000) * frameBytes; const auto mainQueuedBytes = context.device != 0 ? SDL_GetQueuedAudioSize(context.device) / OUTPUT_FRAME_BYTES * frameBytes : cushionBytes + context.queueDepth * grainBytes; const auto queuedBytes = SDL_GetQueuedAudioSize(context.padDevice); if (queuedBytes > mainQueuedBytes + PAD_SLACK_GRAINS * grainBytes) return; if (queuedBytes == 0) { static const std::vector silence(static_cast(CUSHION_MS) * AUDIO_OUT2_SAMPLE_RATE / 1000 * AUDIO_OUT2_PAD_DEVICE_CHANNELS_MAX, 0.0f); SDL_QueueAudio(context.padDevice, silence.data(), cushionBytes); } SDL_QueueAudio(context.padDevice, context.padFrames.data(), grainBytes); } static std::uint32_t Render(AudioOut2Context& context, Clock::time_point now) { UpdatePadDevice(context, now); float* pad = nullptr; if (context.padDevice != 0) { std::fill(context.padMix.begin(), context.padMix.end(), 0.0f); pad = context.padMix.data(); } std::fill(context.mix.begin(), context.mix.end(), 0.0f); const auto mixed = AudioOut2MixPorts(context, context.mix.data(), 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) { context.dropped++; return mixed; } if (queuedBytes == 0) { static const std::vector silence(static_cast(CUSHION_MS) * AUDIO_OUT2_SAMPLE_RATE / 1000 * AUDIO_OUT2_OUTPUT_CHANNELS, 0.0f); context.primes++; SDL_QueueAudio(context.device, silence.data(), static_cast(silence.size() * sizeof(float))); } SDL_QueueAudio(context.device, context.mix.data(), GrainBytes(context)); return mixed; } 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(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, sdl queue %u ms, mix peak %.3f; totals: pushes %llu (%llu blocking, %llu queue-full rejects), primes %llu, dropped %llu\n", AudioOut2TraceSeconds(), static_cast(&context), static_cast(context.summaryPushes) / elapsed, static_cast(context.summaryAdvances) / elapsed, static_cast(context.summaryPolls) / elapsed, QueueLevel(context, now), context.queueDepth, SdlQueuedMs(context), static_cast(context.summaryPeak), static_cast(context.pushes), static_cast(context.blockingPushes), static_cast(context.fullRejects), static_cast(context.primes), static_cast(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. The ports are mixed at push // time from the buffers they currently point at, 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(context), static_cast(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(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(context), context->grain, 1000.0 * context->grain / AUDIO_OUT2_SAMPLE_RATE, context->queueDepth, AUDIO_OUT2_SAMPLE_RATE); OpenDevice(*context); *ctx = reinterpret_cast(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(context), static_cast(context->pushes)); { std::lock_guard lock(context->lock); CloseDevice(*context); ClosePadDevice(*context); } 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(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(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); const auto mixed = Render(*context, now); 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, sdl queue %u ms\n", AudioOut2TraceSeconds(), static_cast(context), blocking, mixed, QueueLevel(*context, now), context->queueDepth, SdlQueuedMs(*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(context), attributes[index].attribute_id, attributes[index].value_size, attributes[index].value); } return 0; } }