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:
parent
8a69fefe13
commit
91557ebade
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue