feat(audio): AudioOut2 SDL output queue, ATRAC9 decoder in libSceAjm.native

- AudioOut2: one SDL device per context; the hardware grain queue level
  is derived from the SDL queued bytes so the title's poll-and-push loop
  runs at real time; ports are mixed per push with 7.1 folded to stereo.
- libSceAjm.native: instance and batch APIs; ATRAC9 decoding with
  variable-size frames inside superframes, padding, gapless parameters
  and handle re-creation on re-initialisation.
- AudioOut v1: sceAudioOutOutput(handle, NULL) drains instead of
  throwing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
This commit is contained in:
devilismyfriend 2026-09-25 16:27:49 -07:00
parent 8a69fefe13
commit 91557ebade
10 changed files with 1152 additions and 216 deletions

View File

@ -0,0 +1,31 @@
enable_language(C)
set(LIBATRAC9_DIR ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
add_library(atrac9 STATIC EXCLUDE_FROM_ALL
${LIBATRAC9_DIR}/band_extension.c
${LIBATRAC9_DIR}/bit_allocation.c
${LIBATRAC9_DIR}/bit_reader.c
${LIBATRAC9_DIR}/decinit.c
${LIBATRAC9_DIR}/decoder.c
${LIBATRAC9_DIR}/huffCodes.c
${LIBATRAC9_DIR}/imdct.c
${LIBATRAC9_DIR}/libatrac9.c
${LIBATRAC9_DIR}/quantization.c
${LIBATRAC9_DIR}/scale_factors.c
${LIBATRAC9_DIR}/tables.c
${LIBATRAC9_DIR}/unpack.c
${LIBATRAC9_DIR}/utility.c
)
set_target_properties(atrac9 PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp
)
target_include_directories(libSceAjm.native PRIVATE
${LIBS_INCLUDE_DIR}
${LIBATRAC9_DIR}
)
target_link_libraries(libSceAjm.native PRIVATE atrac9 libc libkernel)
configure_windows_unwind(libSceAjm.native)

View File

@ -2,36 +2,22 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
#include <cstdio>
// Entry points this library does not implement: reported before the unimplemented-call exception so an
// APS5_TRACE_AJM run shows which one a title reached.
static void AjmStub(const char* name) {
std::fprintf(stderr, "[ajm] unimplemented %s called\n", name);
NotImplemented_nid_no_patch(name);
}
extern "C" {
int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) {
(void)context;
(void)batch;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) {
(void)info;
(void)error;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) {
(void)buffer;
(void)size;
(void)info;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo* info, uint32_t instance, void* result) {
(void)info;
(void)instance;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -43,7 +29,7 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6
(void)sideband_input_size;
(void)sideband_output;
(void)sideband_output_size;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -55,7 +41,7 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const
(void)pcm_output;
(void)pcm_output_size;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -67,7 +53,7 @@ int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance,
(void)pcm_output;
(void)pcm_output_size;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -79,7 +65,7 @@ int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, c
(void)output_buffers;
(void)output_buffers_num;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -91,7 +77,7 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const
(void)bitstream_output;
(void)bitstream_output_size;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -100,7 +86,7 @@ int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance,
(void)instance;
(void)result;
(void)result_size;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -108,7 +94,7 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instan
(void)info;
(void)instance;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -116,7 +102,7 @@ int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void*
(void)info;
(void)instance;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -124,25 +110,7 @@ int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instanc
(void)info;
(void)instance;
(void)result;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) {
(void)info;
(void)interval;
(void)result;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo* info, uint32_t instance, const void* codec_parameters, size_t codec_parameters_size, void* result) {
(void)info;
(void)instance;
(void)codec_parameters;
(void)codec_parameters_size;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -156,31 +124,7 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t
(void)data_output_size;
(void)sideband_output;
(void)sideband_output_size;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) {
(void)info;
(void)instance;
(void)flags;
(void)input_buffers;
(void)input_buffers_num;
(void)output_buffers;
(void)output_buffers_num;
(void)sideband_output;
(void)sideband_output_size;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instance, const void* gapless_decode, int reset, void* result) {
(void)info;
(void)instance;
(void)gapless_decode;
(void)reset;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -190,7 +134,7 @@ int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t i
(void)ratio;
(void)flags;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
@ -201,99 +145,19 @@ int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t
(void)ratio_change_per_sample;
(void)flags;
(void)result;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return 0;
}
int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) {
(void)context;
(void)info;
(void)priority;
const char* APS5_VABI sceAjmStrError(int error) {
(void)error;
(void)batch;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) {
(void)context;
(void)batch;
(void)timeout;
(void)error;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9ConfigDataInfo* config_info) {
(void)config_data;
(void)config_info;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmFinalize(uint32_t context) {
(void)context;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmInitialize(int64_t reserved, uint32_t* context) {
(void)reserved;
(void)context;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) {
(void)context;
(void)codec;
(void)flags;
(void)instance;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) {
(void)context;
(void)instance;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmMemoryRegister(uint32_t context, void* ptr, size_t pages) {
(void)context;
(void)ptr;
(void)pages;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) {
(void)context;
(void)ptr;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmModuleRegister(uint32_t context, uint32_t codec, int64_t reserved) {
(void)context;
(void)codec;
(void)reserved;
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAjmModuleUnregister(uint32_t context, uint32_t codec) {
(void)context;
(void)codec;
NotImplemented_nid_no_patch(__func__);
return 0;
}
const char* sceAjmStrError(int error) {
(void)error;
NotImplemented_nid_no_patch(__func__);
AjmStub(__func__);
return nullptr;
}
int APS5_VABI sceAjmDecMp3ParseFrame() {
AjmStub(__func__);
return 0;
}
}

View File

@ -0,0 +1,573 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "libatrac9.h"
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
// The batch buffer and its job records are private to this library: the guest only reserves the
// memory and reads the "used bytes" field of AjmBatchInfo. Sideband results written back to guest
// memory follow the SDK layouts.
namespace {
constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast<int>(0x80930003);
constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80930005);
constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast<int>(0x80930007);
constexpr std::int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
constexpr std::int32_t AJM_RESULT_INVALID_DATA = 0x00000002;
constexpr std::int32_t AJM_RESULT_INVALID_PARAMETER = 0x00000004;
constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008;
constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010;
constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000;
constexpr std::uint32_t CODEC_AT9 = 1;
// APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back.
bool TraceEnabled() {
static const bool enabled = std::getenv("APS5_TRACE_AJM") != nullptr;
return enabled;
}
#define AJM_TRACE(...) \
do { \
if (TraceEnabled()) std::fprintf(stderr, __VA_ARGS__); \
} while (0)
constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12;
constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45;
constexpr std::uint64_t SIDEBAND_FORMAT = 1ull << 46;
constexpr std::uint64_t SIDEBAND_STREAM = 1ull << 47;
struct SidebandResult {
std::int32_t result;
std::int32_t internalResult;
};
struct SidebandStream {
std::int32_t inputConsumed;
std::int32_t outputWritten;
std::uint64_t totalDecodedSamples;
};
struct SidebandFormat {
std::uint32_t numChannels;
std::uint32_t channelMask;
std::uint32_t sampleRate;
std::uint32_t sampleFormat;
std::uint32_t bitrate;
std::uint32_t reserved;
};
struct SidebandGaplessDecode {
std::uint32_t totalSamples;
std::uint16_t skipSamples;
std::uint16_t skippedSamples;
};
struct SidebandMultipleFrames {
std::uint32_t numFrames;
std::uint32_t reserved;
};
struct Instance {
std::uint32_t codec = 0;
std::uint64_t flags = 0;
void* decoder = nullptr;
Atrac9CodecInfo info{};
bool initialized = false;
std::uint32_t superframeRemaining = 0;
std::uint32_t frameInSuperframe = 0;
std::uint64_t totalDecodedSamples = 0;
SidebandGaplessDecode gapless{};
bool flagsReported = false;
~Instance() {
if (decoder) Atrac9ReleaseHandle(decoder);
}
};
std::mutex g_lock;
std::map<std::uint32_t, std::unique_ptr<Instance>> g_instances;
std::atomic<std::uint32_t> g_nextContext{1};
std::atomic<std::uint32_t> g_nextInstance{1};
std::atomic<std::uint32_t> g_nextBatch{1};
enum class JobKind : std::uint32_t {
Initialize = 1,
ClearContext = 2,
SetGaplessDecode = 3,
Run = 4,
GetStatistics = 5,
};
struct JobHeader {
JobKind kind;
std::uint32_t bytes;
std::uint32_t instance;
std::uint32_t reserved;
std::uint64_t flags;
void* sideband;
std::uint64_t sidebandSize;
std::uint64_t parameterSize;
std::uint32_t inputCount;
std::uint32_t outputCount;
std::uint8_t parameters[16];
};
int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) {
if (!info || !info->p_buffer) return SCE_AJM_ERROR_INVALID_PARAMETER;
const std::size_t bytes = sizeof(JobHeader) + (header.inputCount + header.outputCount) * sizeof(AjmBuffer);
if (bytes > info->size - info->offset) return SCE_AJM_ERROR_OUT_OF_RESOURCES;
auto* cursor = static_cast<std::uint8_t*>(info->p_buffer) + info->offset;
JobHeader record = header;
record.bytes = static_cast<std::uint32_t>(bytes);
std::memcpy(cursor, &record, sizeof(record));
cursor += sizeof(record);
if (header.inputCount) std::memcpy(cursor, inputs, header.inputCount * sizeof(AjmBuffer));
cursor += header.inputCount * sizeof(AjmBuffer);
if (header.outputCount) std::memcpy(cursor, outputs, header.outputCount * sizeof(AjmBuffer));
info->offset += bytes;
return 0;
}
JobHeader MakeHeader(JobKind kind, std::uint32_t instance, void* sideband, std::uint64_t sidebandSize) {
JobHeader header{};
header.kind = kind;
header.instance = instance;
header.sideband = sideband;
header.sidebandSize = sidebandSize;
return header;
}
// The SDK's speaker masks (SCE_AJM_CHANNELMASK_*) for a decoded channel count.
std::uint32_t ChannelMask(std::size_t channels) {
switch (channels) {
case 1: return 0x4;
case 2: return 0x3;
case 4: return 0x33;
case 6: return 0x3F;
case 8: return 0x63F;
default: return 0;
}
}
void WriteResult(void* sideband, std::uint64_t size, std::int32_t result) {
if (!sideband || size < sizeof(SidebandResult)) return;
const SidebandResult value{result, 0};
std::memcpy(sideband, &value, sizeof(value));
}
Instance* Find(std::uint32_t id) {
const auto found = g_instances.find(id);
return found == g_instances.end() ? nullptr : found->second.get();
}
// Replaces the instance's decoder with a fresh one for its configuration, at a superframe boundary. The
// decoder handle keeps its position within the superframe, which a re-initialization does not clear.
bool ResetDecoder(Instance& instance) {
if (instance.decoder) Atrac9ReleaseHandle(instance.decoder);
instance.decoder = Atrac9GetHandle();
instance.superframeRemaining = 0;
instance.frameInSuperframe = 0;
unsigned char config[ATRAC9_CONFIG_DATA_SIZE];
std::memcpy(config, instance.info.configData, sizeof(config));
return Atrac9InitDecoder(instance.decoder, config) == 0;
}
std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) {
if (instance.codec != CODEC_AT9) {
instance.initialized = true;
return 0;
}
if (size < ATRAC9_CONFIG_DATA_SIZE) return AJM_RESULT_INVALID_PARAMETER;
std::memcpy(instance.info.configData, parameters, ATRAC9_CONFIG_DATA_SIZE);
if (!ResetDecoder(instance)) {
instance.initialized = false;
return AJM_RESULT_INVALID_PARAMETER;
}
Atrac9GetCodecInfo(instance.decoder, &instance.info);
instance.initialized = true;
instance.totalDecodedSamples = 0;
instance.gapless = {};
return 0;
}
// Titles may hand the decoder a whole .at9 file. Like the console's decoder, a RIFF/WAVE header at a
// frame boundary is skipped up to the data chunk's payload and counted as consumed input. Returns the
// payload offset, or 0 when the bytes do not start a complete RIFF header.
std::size_t RiffDataOffset(const std::uint8_t* data, std::size_t size) {
if (size < 12 || std::memcmp(data, "RIFF", 4) != 0 || std::memcmp(data + 8, "WAVE", 4) != 0) return 0;
std::size_t cursor = 12;
while (cursor + 8 <= size) {
std::uint32_t chunkBytes = 0;
std::memcpy(&chunkBytes, data + cursor + 4, sizeof(chunkBytes));
if (std::memcmp(data + cursor, "data", 4) == 0) return cursor + 8;
cursor += 8 + static_cast<std::size_t>(chunkBytes) + (chunkBytes & 1u);
}
return 0;
}
// Whether this is the instance's first run since it was (re)initialized; used to report its flags once.
bool consumedFirstRun(Instance& instance) {
if (instance.flagsReported) return false;
instance.flagsReported = true;
return true;
}
// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM
// encoding.
void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
std::vector<std::uint8_t> input;
for (std::uint32_t index = 0; index < job.inputCount; ++index) {
const auto* data = static_cast<const std::uint8_t*>(inputs[index].ptr);
input.insert(input.end(), data, data + inputs[index].size);
}
std::size_t outputCapacity = 0;
for (std::uint32_t index = 0; index < job.outputCount; ++index) outputCapacity += outputs[index].size;
const auto channels = static_cast<std::size_t>(instance.info.channels);
const auto frameSamples = static_cast<std::size_t>(instance.info.frameSamples);
// Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float.
const auto encoding = static_cast<std::uint32_t>((instance.flags >> 7u) & 7u);
const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t);
const std::size_t frameBytes = frameSamples * channels * sampleBytes;
std::vector<std::uint8_t> pcm(frameBytes);
static const bool traceFlags = std::getenv("APS5_TRACE_AJM") != nullptr;
if (traceFlags && instance.totalDecodedSamples == 0 && consumedFirstRun(instance)) std::fprintf(stderr, "[ajm] instance %u flags 0x%llx (encoding %u)\n", job.instance, static_cast<unsigned long long>(instance.flags), encoding);
std::int32_t result = 0;
int decodeStatus = 0;
std::size_t consumed = 0;
std::size_t produced = 0;
std::uint32_t frames = 0;
std::uint32_t outputIndex = 0;
std::size_t outputOffset = 0;
const auto emit = [&](const std::uint8_t* source, std::size_t bytes) {
while (bytes > 0 && outputIndex < job.outputCount) {
const std::size_t room = outputs[outputIndex].size - outputOffset;
const std::size_t chunk = std::min(room, bytes);
std::memcpy(static_cast<std::uint8_t*>(outputs[outputIndex].ptr) + outputOffset, source, chunk);
source += chunk;
bytes -= chunk;
produced += chunk;
outputOffset += chunk;
if (outputOffset == outputs[outputIndex].size) {
++outputIndex;
outputOffset = 0;
}
}
};
// A superframe is a fixed superframeSize bytes holding framesInSuperframe frames packed back to back,
// each byte aligned but of its own size (the encoder moves bits between the frames of a superframe),
// with padding after the last one. The decoder reports each frame's size, so frames are walked with
// it and the padding is skipped after the last frame. Like the console's decoder, a superframe is
// decoded only once it is wholly present: a trailing partial superframe stays unconsumed and is
// reported as partial input, and the title resubmits it from the consumed offset.
const auto superframeSize = static_cast<std::size_t>(instance.info.superframeSize);
const auto framesInSuperframe = static_cast<std::uint32_t>(std::max(1, instance.info.framesInSuperframe));
for (;;) {
if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed);
const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining;
if (input.size() - consumed < needed) {
if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT;
break;
}
if (outputCapacity - produced < frameBytes) {
if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
if (instance.superframeRemaining == 0) {
instance.superframeRemaining = static_cast<std::uint32_t>(superframeSize);
instance.frameInSuperframe = 0;
}
int used = 0;
switch (encoding) {
case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, reinterpret_cast<short*>(pcm.data()), &used, 0); break;
case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, reinterpret_cast<int*>(pcm.data()), &used, 0); break;
default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, reinterpret_cast<float*>(pcm.data()), &used, 0); break;
}
if (decodeStatus != 0 || used <= 0 || static_cast<std::uint32_t>(used) > instance.superframeRemaining) {
result |= AJM_RESULT_INVALID_DATA;
// The decoder tracks its position in the superframe; a fresh one starts the next superframe.
ResetDecoder(instance);
break;
}
consumed += static_cast<std::size_t>(used);
instance.superframeRemaining -= static_cast<std::uint32_t>(used);
if (++instance.frameInSuperframe == framesInSuperframe) {
consumed += instance.superframeRemaining;
instance.superframeRemaining = 0;
}
++frames;
std::size_t first = 0;
std::size_t count = frameSamples;
if (instance.gapless.skippedSamples < instance.gapless.skipSamples) {
const std::size_t skip = std::min<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(instance.gapless.skippedSamples + skip);
first += skip;
count -= skip;
}
if (instance.gapless.totalSamples != 0) {
const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0;
count = static_cast<std::size_t>(std::min<std::uint64_t>(count, remaining));
}
emit(pcm.data() + first * channels * sampleBytes, count * channels * sampleBytes);
instance.totalDecodedSamples += count;
if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break;
}
if (TraceEnabled()) {
std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input",
job.instance, static_cast<unsigned long long>(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast<unsigned long long>(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate,
instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast<unsigned>(result), static_cast<unsigned>(decodeStatus), frames, consumed, produced, static_cast<unsigned long long>(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples);
for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]);
std::fprintf(stderr, "\n");
}
auto* sideband = static_cast<std::uint8_t*>(job.sideband);
std::size_t offset = 0;
const auto write = [&](const void* value, std::size_t size) {
if (!sideband || offset + size > job.sidebandSize) return;
std::memcpy(sideband + offset, value, size);
offset += size;
};
const SidebandResult status{result, 0};
write(&status, sizeof(status));
if (job.flags & SIDEBAND_STREAM) {
const SidebandStream stream{static_cast<std::int32_t>(consumed), static_cast<std::int32_t>(produced), instance.totalDecodedSamples};
write(&stream, sizeof(stream));
}
if (job.flags & SIDEBAND_FORMAT) {
const SidebandFormat format{static_cast<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(instance.info.samplingRate), encoding, 0, 0};
write(&format, sizeof(format));
}
if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless));
if (job.flags & RUN_MULTIPLE_FRAMES) {
const SidebandMultipleFrames multiple{frames, 0};
write(&multiple, sizeof(multiple));
}
}
void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
if (job.kind == JobKind::GetStatistics) {
AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast<unsigned long long>(job.sidebandSize));
if (job.sideband && job.sidebandSize) std::memset(job.sideband, 0, std::min<std::uint64_t>(job.sidebandSize, 24));
return;
}
std::lock_guard lock(g_lock);
auto* instance = Find(job.instance);
if (!instance) {
AJM_TRACE("[ajm] job kind %u on unknown instance %u\n", static_cast<unsigned>(job.kind), job.instance);
WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_INVALID_PARAMETER);
return;
}
switch (job.kind) {
case JobKind::Initialize: {
const std::int32_t result = InitializeInstance(*instance, job.parameters, job.parameterSize);
AJM_TRACE("[ajm] instance %u initialize: %llu parameter bytes (%02x %02x %02x %02x %02x %02x %02x %02x), sideband %llu bytes -> result 0x%x, %d channels, %d Hz, superframe %d bytes (%d frames of %d samples)\n", job.instance, static_cast<unsigned long long>(job.parameterSize), job.parameters[0], job.parameters[1], job.parameters[2], job.parameters[3], job.parameters[4], job.parameters[5], job.parameters[6], job.parameters[7], static_cast<unsigned long long>(job.sidebandSize), static_cast<unsigned>(result), instance->info.channels, instance->info.samplingRate, instance->info.superframeSize, instance->info.framesInSuperframe, instance->info.frameSamples);
WriteResult(job.sideband, job.sidebandSize, result);
break;
}
case JobKind::ClearContext:
AJM_TRACE("[ajm] instance %u clear context (sideband %llu bytes)\n", job.instance, static_cast<unsigned long long>(job.sidebandSize));
instance->totalDecodedSamples = 0;
instance->gapless.skippedSamples = 0;
if (instance->decoder && instance->initialized) ResetDecoder(*instance);
WriteResult(job.sideband, job.sidebandSize, 0);
break;
case JobKind::SetGaplessDecode: {
SidebandGaplessDecode gapless{};
std::memcpy(&gapless, job.parameters, sizeof(gapless));
AJM_TRACE("[ajm] instance %u set gapless decode: total %u, skip %u, reset %llu (sideband %llu bytes)\n", job.instance, gapless.totalSamples, gapless.skipSamples, static_cast<unsigned long long>(job.flags), static_cast<unsigned long long>(job.sidebandSize));
instance->gapless.totalSamples = gapless.totalSamples;
instance->gapless.skipSamples = gapless.skipSamples;
if (job.flags) instance->gapless.skippedSamples = 0;
WriteResult(job.sideband, job.sidebandSize, 0);
break;
}
case JobKind::Run:
if (!instance->initialized) {
AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance);
WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED);
} else if (instance->codec == CODEC_AT9) {
RunAt9(*instance, job, inputs, outputs);
} else {
AJM_TRACE("[ajm] instance %u run on codec %u without a decoder\n", job.instance, instance->codec);
WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_CODEC_ERROR);
}
break;
default:
AJM_TRACE("[ajm] instance %u unknown job kind %u\n", job.instance, static_cast<unsigned>(job.kind));
WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_INVALID_PARAMETER);
break;
}
}
}
extern "C" {
int APS5_VABI sceAjmInitialize(int64_t reserved, uint32_t* context) {
(void)reserved;
if (!context) return SCE_AJM_ERROR_INVALID_PARAMETER;
*context = g_nextContext.fetch_add(1, std::memory_order_relaxed);
return 0;
}
int APS5_VABI sceAjmFinalize(uint32_t context) {
(void)context;
return 0;
}
int APS5_VABI sceAjmModuleRegister(uint32_t context, uint32_t codec, int64_t reserved) {
(void)context;
(void)codec;
(void)reserved;
return 0;
}
int APS5_VABI sceAjmModuleUnregister(uint32_t context, uint32_t codec) {
(void)context;
(void)codec;
return 0;
}
int APS5_VABI sceAjmMemoryRegister(uint32_t context, void* ptr, size_t pages) {
(void)context;
(void)ptr;
(void)pages;
return 0;
}
int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) {
(void)context;
(void)ptr;
return 0;
}
int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) {
(void)context;
if (!instance) return SCE_AJM_ERROR_INVALID_PARAMETER;
static std::atomic<std::uint32_t> reportedCodecs{0};
if (codec != CODEC_AT9 && codec < 32 && (reportedCodecs.fetch_or(1u << codec) & (1u << codec)) == 0)
std::fprintf(stderr, "[ajm] codec %u has no decoder; its jobs report codec errors\n", codec);
auto created = std::make_unique<Instance>();
created->codec = codec;
created->flags = flags;
const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF);
std::lock_guard lock(g_lock);
g_instances[id] = std::move(created);
*instance = id;
AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast<unsigned long long>(flags), id);
return 0;
}
int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) {
(void)context;
AJM_TRACE("[ajm] instance %u destroy\n", instance);
std::lock_guard lock(g_lock);
return g_instances.erase(instance) ? 0 : SCE_AJM_ERROR_INVALID_INSTANCE;
}
int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9ConfigDataInfo* config_info) {
if (!config_data || !config_info) return SCE_AJM_ERROR_INVALID_PARAMETER;
void* decoder = Atrac9GetHandle();
unsigned char config[ATRAC9_CONFIG_DATA_SIZE];
std::memcpy(config, config_data, sizeof(config));
const bool valid = Atrac9InitDecoder(decoder, config) == 0;
Atrac9CodecInfo info{};
if (valid) Atrac9GetCodecInfo(decoder, &info);
Atrac9ReleaseHandle(decoder);
AJM_TRACE("[ajm] parse config %02x %02x %02x %02x -> %s, %d channels, %d Hz, superframe %d bytes (%d frames of %d samples)\n", config[0], config[1], config[2], config[3], valid ? "ok" : "invalid", info.channels, info.samplingRate, info.superframeSize, info.framesInSuperframe, info.frameSamples);
if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER;
config_info->channels = static_cast<std::uint32_t>(info.channels);
config_info->sample_rate = static_cast<std::uint32_t>(info.samplingRate);
config_info->frame_samples_per_channel = static_cast<std::uint32_t>(info.frameSamples);
config_info->superframe_samples_per_channel = static_cast<std::uint32_t>(info.frameSamples * info.framesInSuperframe);
config_info->superframe_size = static_cast<std::uint32_t>(info.superframeSize);
return 0;
}
int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) {
if (!buffer || !info) return SCE_AJM_ERROR_INVALID_PARAMETER;
info->p_buffer = buffer;
info->offset = 0;
info->size = size;
return 0;
}
int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo* info, uint32_t instance, const void* codec_parameters, size_t codec_parameters_size, void* result) {
auto header = MakeHeader(JobKind::Initialize, instance, result, sizeof(SidebandResult));
header.parameterSize = std::min<std::size_t>(codec_parameters_size, sizeof(header.parameters));
if (codec_parameters && header.parameterSize) std::memcpy(header.parameters, codec_parameters, header.parameterSize);
return Append(info, header, nullptr, nullptr);
}
int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo* info, uint32_t instance, void* result) {
return Append(info, MakeHeader(JobKind::ClearContext, instance, result, sizeof(SidebandResult)), nullptr, nullptr);
}
int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instance, const void* gapless_decode, int reset, void* result) {
auto header = MakeHeader(JobKind::SetGaplessDecode, instance, result, sizeof(SidebandResult));
if (gapless_decode) std::memcpy(header.parameters, gapless_decode, sizeof(SidebandGaplessDecode));
header.flags = reset ? 1 : 0;
return Append(info, header, nullptr, nullptr);
}
int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) {
auto header = MakeHeader(JobKind::Run, instance, sideband_output, sideband_output_size);
header.flags = flags;
header.inputCount = static_cast<std::uint32_t>(input_buffers_num);
header.outputCount = static_cast<std::uint32_t>(output_buffers_num);
return Append(info, header, input_buffers, output_buffers);
}
int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) {
(void)interval;
return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr);
}
int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) {
(void)context;
(void)priority;
if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER;
AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast<unsigned long long>(info->offset), static_cast<unsigned long long>(info->size));
const auto* cursor = static_cast<const std::uint8_t*>(info->p_buffer);
const auto* end = cursor + info->offset;
while (cursor < end) {
JobHeader job;
std::memcpy(&job, cursor, sizeof(job));
const auto* buffers = reinterpret_cast<const AjmBuffer*>(cursor + sizeof(JobHeader));
Execute(job, buffers, buffers + job.inputCount);
cursor += job.bytes;
}
if (error) std::memset(error, 0, sizeof(*error));
*batch = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
return 0;
}
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) {
(void)context;
(void)batch;
(void)timeout;
if (error) std::memset(error, 0, sizeof(*error));
return 0;
}
int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) {
(void)info;
if (error) std::memset(error, 0, sizeof(*error));
return 0;
}
}

View File

@ -185,8 +185,23 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector
}
static void queueAudio(Port& port, const void* data) {
if (port.device == 0 || data == nullptr) {
APS5_INVALID_ARG_EX;
// Without an SDL device there is nothing to queue: the callers already sleep for the block's
// duration so the game's timing holds. A null pointer is the documented way to wait until the
// port's queued audio has been output (sceAudioOutOutput(handle, NULL)); it queues nothing.
if (port.device == 0) return;
if (data == nullptr) {
const std::uint64_t waitStart = sceKernelGetProcessTime();
while (SDL_GetQueuedAudioSize(port.device) > 0) {
if (sceKernelGetProcessTime() - waitStart > DRAIN_TIMEOUT_US) {
SDL_ClearQueuedAudio(port.device);
break;
}
struct timespec req{};
req.tv_sec = 0;
req.tv_nsec = static_cast<long>(DRAIN_SLEEP_US * 1000ULL);
nanosleep(&req, nullptr);
}
return;
}
std::vector<std::uint8_t> prepareBuf;
@ -391,12 +406,12 @@ int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num)
}
for (std::uint32_t i = 0; i < num; i++) {
queueAudio(*getPort(param[i].handle), param[i].ptr);
if (auto* port = getPort(param[i].handle)) queueAudio(*port, param[i].ptr);
}
const std::uint64_t done = sceKernelGetProcessTime();
for (std::uint32_t i = 0; i < num; i++) {
getPort(param[i].handle)->lastOutputTime = done;
if (auto* port = getPort(param[i].handle)) port->lastOutputTime = done;
}
return static_cast<int>(first.samplesNum);

View File

@ -1,64 +1,277 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
#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;
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 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<int>(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;
}
// Mixes the ports' current grain and queues it on the SDL device. The caller holds the lock.
static std::uint32_t Render(AudioOut2Context& context) {
std::fill(context.mix.begin(), context.mix.end(), 0.0f);
const auto mixed = AudioOut2MixPorts(context, context.mix.data(), 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 (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<float> silence(static_cast<std::size_t>(CUSHION_MS) * AUDIO_OUT2_SAMPLE_RATE / 1000 * AUDIO_OUT2_OUTPUT_CHANNELS, 0.0f);
context.primes++;
SDL_QueueAudio(context.device, silence.data(), static_cast<Uint32>(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<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, sdl queue %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, SdlQueuedMs(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. 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) {
(void)ctx;
NotImplemented_nid_no_patch(__func__);
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) {
(void)params;
(void)buffer;
(void)buffer_size;
(void)ctx;
NotImplemented_nid_no_patch(__func__);
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) {
(void)ctx;
NotImplemented_nid_no_patch(__func__);
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));
{
std::lock_guard lock(context->lock);
CloseDevice(*context);
}
AudioOut2ReleasePorts(*context);
delete context;
return 0;
}
int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint32_t* queue_level, uint32_t* available_queues) {
(void)ctx;
(void)queue_level;
(void)available_queues;
NotImplemented_nid_no_patch(__func__);
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) {
(void)ctx;
(void)blocking;
NotImplemented_nid_no_patch(__func__);
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);
const auto mixed = Render(*context);
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<void*>(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) {
(void)params;
(void)memory_size;
NotImplemented_nid_no_patch(__func__);
if (!params || !memory_size) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
*memory_size = CONTEXT_MEMORY;
return 0;
}
int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam* params) {
(void)params;
NotImplemented_nid_no_patch(__func__);
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) {
(void)ctx;
(void)attributes;
(void)num;
NotImplemented_nid_no_patch(__func__);
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;
}

View File

@ -0,0 +1,95 @@
#ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP
#define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <mutex>
#include <vector>
#include "SDL.h"
#include "SceTypes.hpp"
// Shared by the AudioOut2 context and port files.
//
// APS5_TRACE_AUDIOOUT2=1 prints the title's use of the API to stderr: context and port parameters, every
// attribute the title sets (first few per port, then sampled), the first pushes and queue polls, and a
// once-per-second summary with the push rate and the SDL queue level.
bool AudioOut2TraceEnabled();
// Seconds since the first AudioOut2 call, for the trace.
double AudioOut2TraceSeconds();
#define AUDIOOUT2_TRACE(...) \
do { \
if (AudioOut2TraceEnabled()) std::fprintf(stderr, "[audioout2] " __VA_ARGS__); \
} while (0)
static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80260502);
static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE = static_cast<int>(0x80260503);
// Not recovered from a title or SDK header: follows the 0x802605xx pattern of the codes above. Returned
// by a non-blocking push when the modelled hardware queue is full.
static constexpr int SCE_AUDIO_OUT2_ERROR_QUEUE_FULL = static_cast<int>(0x80260507);
static constexpr std::uint32_t AUDIO_OUT2_SAMPLE_RATE = 48000;
// The grain a push carries, in samples per channel: a context's num_grains (Demon's Souls: 256, 5.33 ms).
static constexpr std::uint32_t AUDIO_OUT2_DEFAULT_GRAIN = 256;
static constexpr std::uint32_t AUDIO_OUT2_OUTPUT_CHANNELS = 2;
static constexpr std::uint32_t AUDIO_OUT2_PORT_CHANNELS_MAX = 8;
struct AudioOut2Context;
struct AudioOut2Port {
bool used = false;
AudioOut2Context* context = nullptr;
std::uint16_t type = 0;
std::uint32_t dataFormat = 0;
std::uint32_t samplingFreq = 0;
std::uint32_t flags = 0;
// Channel count decoded from dataFormat; 0 when the format is not understood (the port is then
// not rendered). Samples are float.
std::uint32_t channels = 0;
// The PCM buffer (one grain, float, interleaved) the title last handed over through the data
// attribute. It is guest memory the title rewrites every tick, so it is read when the context
// mixes, not when it is set.
const float* data = nullptr;
float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
std::uint64_t dataSets = 0;
std::uint64_t attributeTraces = 0;
};
struct AudioOut2Context {
std::mutex lock;
std::uint32_t grain = AUDIO_OUT2_DEFAULT_GRAIN;
std::uint32_t queueDepth = 1;
// Fallback hardware queue model for a context without an SDL device: pushes that have not
// finished playing, and the time the last of them finishes; playback is real time, so the queue
// drains as the wall clock advances. With a device the SDL queue is the hardware queue.
std::uint32_t queued = 0;
std::chrono::steady_clock::time_point playHead;
SDL_AudioDeviceID device = 0;
// Stereo float mix of the ports for one push.
std::vector<float> mix;
// Trace counters.
std::uint64_t pushes = 0;
std::uint64_t blockingPushes = 0;
std::uint64_t fullRejects = 0;
std::uint64_t advances = 0;
std::uint64_t queueLevelPolls = 0;
std::uint64_t primes = 0;
std::uint64_t dropped = 0;
std::uint64_t summaryPushes = 0;
std::uint64_t summaryAdvances = 0;
std::uint64_t summaryPolls = 0;
// Largest sample magnitude of the grains mixed since the last summary: the mixed signal's level.
float summaryPeak = 0.0f;
std::chrono::steady_clock::time_point summaryStart;
};
// Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing
// onto the zeroed buffer. Returns the number of ports mixed.
std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames);
// Forgets the ports of a context being destroyed.
void AudioOut2ReleasePorts(const AudioOut2Context& context);
#endif

View File

@ -1,35 +1,171 @@
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <vector>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "AudioOut2Internal.hpp"
static constexpr std::uint16_t OUTPUT_MAIN = 1;
static constexpr std::int16_t VOLUME_MAX = 127;
// Attribute sets are traced in full for the first few calls of a port, then once per this many.
static constexpr std::uint64_t ATTRIBUTE_TRACE_FULL = 8;
static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000;
// Port attributes as Demon's Souls sets them every tick (APS5_TRACE_AUDIOOUT2): id 0 carries the
// grain's PCM as an 8-byte value holding the buffer pointer, id 1 one float gain per channel. Ids 5,
// 8 and 9 (a u32, a u32 and a byte on object and voice ports) are not understood and ignored.
static constexpr std::uint32_t ATTRIBUTE_DATA = 0;
static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1;
// data_format bits 8..11 carry the channel count: the title opens 0x100 (mono object ports),
// 0x200 (stereo) and 0x880 (7.1 bed). The buffers behind them are float, as their spacing shows
// (1024 bytes per 256-sample mono grain). The low byte's meaning is not known.
static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8;
static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu;
static std::mutex g_portsLock;
// Grows on demand: the title opens its bed ports plus max_object_ports object ports at once.
static std::vector<AudioOut2Port> g_ports;
static AudioOut2Port* FromHandle(AudioOut2PortHandle handle) {
const auto index = handle - 1;
if (handle == 0 || index >= g_ports.size() || !g_ports[index].used) return nullptr;
return &g_ports[index];
}
// 8-channel order FL FR C LFE RL RR SL SR (a swapped rear/side pair order sums the same): the rear
// and side pairs fold into the front at -3 dB, the centre into both sides, and the LFE is dropped.
static constexpr float DOWNMIX_GAIN = 0.7071f;
static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) {
const auto ch = port.channels;
const float* volume = port.volume;
for (std::uint32_t frame = 0; frame < frames; frame++) {
const float* in = port.data + static_cast<std::size_t>(frame) * ch;
float left = 0.0f;
float right = 0.0f;
if (ch == 1) {
left = right = in[0] * volume[0];
} else if (ch < 8) {
left = in[0] * volume[0];
right = in[1] * volume[1];
} else {
const float centre = in[2] * volume[2] * DOWNMIX_GAIN;
left = in[0] * volume[0] + centre + (in[4] * volume[4] + in[6] * volume[6]) * DOWNMIX_GAIN;
right = in[1] * volume[1] + centre + (in[5] * volume[5] + in[7] * volume[7]) * DOWNMIX_GAIN;
}
out[frame * AUDIO_OUT2_OUTPUT_CHANNELS] += left;
out[frame * AUDIO_OUT2_OUTPUT_CHANNELS + 1] += right;
}
}
std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, float* out, std::uint32_t frames) {
std::lock_guard lock(g_portsLock);
std::uint32_t mixed = 0;
for (const auto& port : g_ports) {
if (!port.used || port.context != &context || port.data == nullptr || port.channels == 0) continue;
AccumulatePort(port, out, frames);
mixed++;
}
return mixed;
}
void AudioOut2ReleasePorts(const AudioOut2Context& context) {
std::lock_guard lock(g_portsLock);
for (auto& port : g_ports) {
if (port.used && port.context == &context) port = AudioOut2Port{};
}
}
static void TraceAttribute(AudioOut2PortHandle handle, const AudioOut2Port& port, const AudioOut2Attribute& attribute, const char* verdict) {
if (!AudioOut2TraceEnabled()) return;
char values[128] = "";
if (attribute.value != nullptr) {
std::size_t used = 0;
const auto words = std::min<std::size_t>(attribute.value_size / sizeof(std::uint32_t), 4);
for (std::size_t index = 0; index < words && used < sizeof(values); index++) {
std::uint32_t bits = 0;
float value = 0.0f;
std::memcpy(&bits, static_cast<const std::uint8_t*>(attribute.value) + index * sizeof(bits), sizeof(bits));
std::memcpy(&value, &bits, sizeof(value));
used += static_cast<std::size_t>(std::snprintf(values + used, sizeof(values) - used, " %08x(%g)", bits, static_cast<double>(value)));
}
}
std::fprintf(stderr, "[audioout2] t=%.3f port %llu set attribute id=0x%x size=%zu value=%p%s: %s (data sets so far %llu)\n",
AudioOut2TraceSeconds(), static_cast<unsigned long long>(handle), attribute.attribute_id, attribute.value_size, attribute.value, values, verdict,
static_cast<unsigned long long>(port.dataSets));
}
extern "C" {
int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2PortParam* params, AudioOut2PortHandle* port) {
(void)ctx;
(void)params;
(void)port;
NotImplemented_nid_no_patch(__func__);
if (!ctx) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
if (!params || !port) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
std::lock_guard lock(g_portsLock);
std::size_t index = 0;
while (index < g_ports.size() && g_ports[index].used) index++;
if (index == g_ports.size()) g_ports.emplace_back();
auto& entry = g_ports[index];
entry = AudioOut2Port{};
entry.used = true;
entry.context = reinterpret_cast<AudioOut2Context*>(ctx);
entry.type = params->port_type;
entry.dataFormat = params->data_format;
entry.samplingFreq = params->sampling_freq;
entry.flags = params->flags;
entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK;
if (entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX) entry.channels = 0;
*port = static_cast<AudioOut2PortHandle>(index) + 1;
AUDIOOUT2_TRACE("t=%.3f PortCreate ctx=%llx -> port %llu: type=0x%x data_format=0x%x (%u float ch) sampling_freq=%u flags=0x%x user=%llx\n",
AudioOut2TraceSeconds(), static_cast<unsigned long long>(ctx), static_cast<unsigned long long>(*port), params->port_type, params->data_format,
entry.channels, params->sampling_freq, params->flags, static_cast<unsigned long long>(params->user_handle));
return 0;
}
int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) {
(void)port;
NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_portsLock);
auto* entry = FromHandle(port);
if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
AUDIOOUT2_TRACE("t=%.3f PortDestroy port %llu (data sets %llu)\n", AudioOut2TraceSeconds(), static_cast<unsigned long long>(port), static_cast<unsigned long long>(entry->dataSets));
*entry = AudioOut2Port{};
return 0;
}
int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) {
(void)port;
(void)state;
NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_portsLock);
if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE;
if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
state->output = OUTPUT_MAIN;
state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS;
state->volume = VOLUME_MAX;
state->reroute_counter = 0;
state->flags = 0;
return 0;
}
int APS5_VABI sceAudioOut2PortSetAttributes(AudioOut2PortHandle port, const AudioOut2Attribute* attributes, uint32_t num) {
(void)port;
(void)attributes;
(void)num;
NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_portsLock);
auto* entry = FromHandle(port);
if (!entry) 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++) {
const auto& attribute = attributes[index];
const char* verdict = "ignored";
if (attribute.value == nullptr) {
verdict = "null value";
} else if (attribute.attribute_id == ATTRIBUTE_DATA && attribute.value_size == sizeof(entry->data)) {
std::memcpy(&entry->data, attribute.value, sizeof(entry->data));
entry->dataSets++;
verdict = "pcm data pointer";
} else if (attribute.attribute_id == ATTRIBUTE_VOLUME && entry->channels != 0 && attribute.value_size == entry->channels * sizeof(float)) {
std::memcpy(entry->volume, attribute.value, attribute.value_size);
verdict = "volume";
}
const auto traces = entry->attributeTraces++;
if (traces < ATTRIBUTE_TRACE_FULL || traces % ATTRIBUTE_TRACE_EVERY == 0) TraceAttribute(port, *entry, attribute, verdict);
}
return 0;
}

View File

@ -49,9 +49,15 @@ size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, ui
}
int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) {
(void)info;
(void)flags;
NotImplemented_nid_no_patch(__func__);
if (!info) return static_cast<int>(0x80260502);
constexpr std::uint8_t SpeakerTypeStereo = 0;
constexpr std::uint32_t FrontLeftAndRight = 0x3;
*info = AudioOut2SpeakerInfo{};
info->type = SpeakerTypeStereo;
info->available_bits = FrontLeftAndRight;
info->speaker_angle[0] = {-30, 0};
info->speaker_angle[1] = {30, 0};
return 0;
}

View File

@ -4,13 +4,12 @@
extern "C" {
int APS5_VABI sceAudioOut2Initialize(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState* state) {
(void)state;
NotImplemented_nid_no_patch(__func__);
if (!state) return static_cast<int>(0x80260502);
state->loudness = 0.0f;
return 0;
}

View File

@ -1,19 +1,23 @@
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
static constexpr int SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80260502);
static std::atomic<AudioOut2UserHandle> g_nextUser{1};
extern "C" {
int APS5_VABI sceAudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle) {
(void)user_id;
(void)handle;
NotImplemented_nid_no_patch(__func__);
if (!handle) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT;
*handle = g_nextUser.fetch_add(1, std::memory_order_relaxed);
return 0;
}
int APS5_VABI sceAudioOut2UserDestroy(AudioOut2UserHandle handle) {
(void)handle;
NotImplemented_nid_no_patch(__func__);
return 0;
}