#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "libatrac9.h" extern "C" { #include #include #include } #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 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_CONTEXT = static_cast(0x80930002); constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); constexpr int SCE_AJM_ERROR_OUT_OF_RESOURCES = static_cast(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_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; constexpr std::uint32_t CODEC_OPUS = 24; // 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_GET_CODEC_INFO = 1ull << 11; constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; constexpr std::uint64_t CONTROL_RESET = 1ull << 13; constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; 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 SidebandAt9CodecInfo { std::uint32_t superframeSize; std::uint32_t framesInSuperframe; std::uint32_t nextFrameSize; std::uint32_t frameSamples; }; struct SidebandResampleInfo { float ratio; std::int32_t samples; std::uint32_t reserved[8]; }; struct Resampler { float ratio = 1.0f; std::vector frames; double position = 1.0; }; struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; void* decoder = nullptr; std::vector channelDecoders; std::vector channelBytes; std::vector blockPcm; Atrac9CodecInfo info{}; bool initialized = false; std::uint32_t superframeRemaining = 0; std::uint32_t frameInSuperframe = 0; std::uint64_t totalDecodedSamples = 0; std::uint64_t segmentSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; AVCodecContext* mp3 = nullptr; std::uint32_t mp3Channels = 0; std::uint32_t mp3SampleRate = 0; std::uint32_t mp3Bitrate = 0; AVCodecContext* opus = nullptr; std::uint32_t opusChannels = 0; std::uint32_t opusMappingFamily = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; Resampler resampler; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); for (void* channelDecoder : channelDecoders) Atrac9ReleaseHandle(channelDecoder); if (mp3) avcodec_free_context(&mp3); if (opus) avcodec_free_context(&opus); } }; std::mutex& g_lock = *new std::mutex(); std::map>& g_instances = *new std::map>(); std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic g_nextBatch{1}; std::mutex& g_batchLock = *new std::mutex(); std::set& g_batches = *new std::set(); enum class JobKind : std::uint32_t { Initialize = 1, ClearContext = 2, SetGaplessDecode = 3, Run = 4, GetStatistics = 5, Control = 6, SetResampleParameters = 7, GetResampleInfo = 8, }; 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]; }; struct PackedJob { std::uint8_t kind; std::uint8_t inputCount; std::uint8_t outputCount; std::uint8_t parameterSize; std::uint32_t instance; std::uint64_t flags; void* sideband; std::uint32_t sidebandSize; std::uint32_t bytes; }; static_assert(sizeof(PackedJob) == 32); int Append(AjmBatchInfo* info, const JobHeader& header, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (!info || !info->p_buffer) return SCE_AJM_ERROR_INVALID_PARAMETER; if (header.inputCount > 0xffu || header.outputCount > 0xffu || header.parameterSize > sizeof(header.parameters) || header.sidebandSize > 0xffffffffu) return SCE_AJM_ERROR_INVALID_PARAMETER; if ((header.inputCount && !inputs) || (header.outputCount && !outputs)) NotImplemented_nid_no_patch("AJM batch job with a null buffer list and a nonzero buffer count"); const std::size_t parameterBytes = (header.parameterSize + 7u) & ~std::size_t{7}; const std::size_t bytes = sizeof(PackedJob) + (header.inputCount + header.outputCount) * sizeof(AjmBuffer) + parameterBytes; if (info->offset > info->size || bytes > info->size - info->offset) return SCE_AJM_ERROR_OUT_OF_RESOURCES; auto* cursor = static_cast(info->p_buffer) + info->offset; const PackedJob record{static_cast(header.kind), static_cast(header.inputCount), static_cast(header.outputCount), static_cast(header.parameterSize), header.instance, header.flags, header.sideband, static_cast(header.sidebandSize), static_cast(bytes)}; std::memcpy(cursor, &record, sizeof(record)); cursor += sizeof(record); if (inputs) std::memcpy(cursor, inputs, header.inputCount * sizeof(AjmBuffer)); cursor += header.inputCount * sizeof(AjmBuffer); if (outputs) std::memcpy(cursor, outputs, header.outputCount * sizeof(AjmBuffer)); cursor += header.outputCount * sizeof(AjmBuffer); if (header.parameterSize) std::memcpy(cursor, header.parameters, header.parameterSize); 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(); } constexpr std::uint8_t AT9_MULTICHANNEL_HEADER = 0x30; struct At9MultichannelConfig { std::uint32_t channels; std::uint32_t frameBytes; std::uint32_t framesInSuperframe; unsigned char monoConfig[ATRAC9_CONFIG_DATA_SIZE]; }; bool ParseMultichannelConfig(const std::uint8_t* config, At9MultichannelConfig& parsed) { if (config[0] != AT9_MULTICHANNEL_HEADER) return false; const std::uint32_t bits = (std::uint32_t{config[1]} << 16) | (std::uint32_t{config[2]} << 8) | config[3]; if ((bits >> 13) & 1u) return false; const std::uint32_t sampleRateIndex = (bits >> 20) & 0xFu; const std::uint32_t superframeIndex = bits & 3u; parsed.channels = ((bits >> 14) & 0x3Fu) + 1; parsed.frameBytes = ((bits >> 2) & 0x7FFu) + 1; parsed.framesInSuperframe = 1u << superframeIndex; const std::uint32_t mono = (0xFEu << 24) | (sampleRateIndex << 20) | ((parsed.frameBytes - 1) << 5) | (superframeIndex << 3); for (std::size_t i = 0; i < ATRAC9_CONFIG_DATA_SIZE; ++i) parsed.monoConfig[i] = static_cast(mono >> (24 - 8 * i)); return true; } bool DescribeAt9Config(const std::uint8_t* configData, Atrac9CodecInfo& info) { At9MultichannelConfig multichannel{}; const bool isMultichannel = ParseMultichannelConfig(configData, multichannel); unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, isMultichannel ? multichannel.monoConfig : configData, sizeof(config)); void* decoder = Atrac9GetHandle(); const bool valid = Atrac9InitDecoder(decoder, config) == 0; if (valid) Atrac9GetCodecInfo(decoder, &info); Atrac9ReleaseHandle(decoder); if (!valid) return false; if (isMultichannel) { info.channels = static_cast(multichannel.channels); info.channelConfigIndex = -1; info.superframeSize = static_cast(multichannel.channels * multichannel.frameBytes * multichannel.framesInSuperframe); } std::memcpy(info.configData, configData, ATRAC9_CONFIG_DATA_SIZE); return true; } // 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 = nullptr; for (void* channelDecoder : instance.channelDecoders) Atrac9ReleaseHandle(channelDecoder); instance.channelDecoders.clear(); instance.superframeRemaining = 0; instance.frameInSuperframe = 0; unsigned char config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, instance.info.configData, sizeof(config)); At9MultichannelConfig multichannel{}; if (!ParseMultichannelConfig(config, multichannel)) { instance.decoder = Atrac9GetHandle(); return Atrac9InitDecoder(instance.decoder, config) == 0; } instance.channelBytes.assign(multichannel.channels, 0); for (std::uint32_t channel = 0; channel < multichannel.channels; ++channel) { instance.channelDecoders.push_back(Atrac9GetHandle()); if (Atrac9InitDecoder(instance.channelDecoders.back(), multichannel.monoConfig) != 0) return false; } return true; } bool HasAt9Decoder(const Instance& instance) { return instance.decoder || !instance.channelDecoders.empty(); } int DecodeAt9Block(void* decoder, std::uint32_t encoding, const std::uint8_t* data, int size, std::uint8_t* pcm, int* used) { switch (encoding) { case 0: return Atrac9Decode(decoder, data, size, reinterpret_cast(pcm), used, 0); case 1: return Atrac9DecodeS32(decoder, data, size, reinterpret_cast(pcm), used, 0); default: return Atrac9DecodeF32(decoder, data, size, reinterpret_cast(pcm), used, 0); } } constexpr int AT9_MULTICHANNEL_BLOCK_OVERRUN = 0x7F000001; int DecodeAt9Frame(Instance& instance, const std::uint8_t* data, int size, std::uint8_t* pcm, std::uint32_t encoding, std::size_t sampleBytes, int* used) { if (instance.channelDecoders.empty()) return DecodeAt9Block(instance.decoder, encoding, data, size, pcm, used); const std::size_t channels = instance.channelDecoders.size(); const auto frameSamples = static_cast(instance.info.frameSamples); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); const auto budget = static_cast(instance.info.superframeSize / static_cast(channels)); const bool lastFrame = instance.frameInSuperframe + 1 >= framesInSuperframe; if (instance.frameInSuperframe == 0) std::fill(instance.channelBytes.begin(), instance.channelBytes.end(), 0u); instance.blockPcm.resize(frameSamples * sampleBytes); int offset = 0; for (std::size_t channel = 0; channel < channels; ++channel) { const std::uint32_t left = budget - std::min(budget, instance.channelBytes[channel]); const int available = std::min(static_cast(left), size - offset); if (available <= 0) return AT9_MULTICHANNEL_BLOCK_OVERRUN; int blockUsed = 0; const int status = DecodeAt9Block(instance.channelDecoders[channel], encoding, data + offset, available, instance.blockPcm.data(), &blockUsed); if (status != 0) return status; if (blockUsed <= 0 || blockUsed > available) return AT9_MULTICHANNEL_BLOCK_OVERRUN; const std::uint32_t blockBytes = lastFrame ? left : static_cast(blockUsed); if (static_cast(blockBytes) > size - offset) return AT9_MULTICHANNEL_BLOCK_OVERRUN; for (std::size_t sample = 0; sample < frameSamples; ++sample) { std::memcpy(pcm + (sample * channels + channel) * sampleBytes, instance.blockPcm.data() + sample * sampleBytes, sampleBytes); } instance.channelBytes[channel] += blockBytes; offset += static_cast(blockBytes); } *used = offset; return 0; } void OpenOpus(Instance& instance); void ClearResampler(Instance& instance) { instance.resampler.frames.clear(); instance.resampler.position = 1.0; } std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { ClearResampler(instance); if (instance.codec == CODEC_OPUS) { if (size < 8) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t channels = 0; std::uint32_t sampleRate = 0; std::memcpy(&channels, parameters, sizeof(channels)); std::memcpy(&sampleRate, parameters + 4, sizeof(sampleRate)); if (channels == 0 || channels > 8 || sampleRate == 0) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t mappingFamily = 0; if (size >= 12) std::memcpy(&mappingFamily, parameters + 8, sizeof(mappingFamily)); if (sampleRate != 48000) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus sample rate other than 48000)"); if (mappingFamily > 1) NotImplemented_nid_no_patch("sceAjmBatchJobInitialize (Opus mapping family other than 0 or 1)"); if (mappingFamily == 0 && channels > 2) return AJM_RESULT_INVALID_PARAMETER; instance.opusChannels = channels; instance.opusMappingFamily = mappingFamily; instance.opusSampleRate = sampleRate; OpenOpus(instance); instance.opusPending.clear(); instance.initialized = true; instance.totalDecodedSamples = 0; instance.segmentSamples = 0; instance.gapless = {}; return 0; } 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); std::uint8_t config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, parameters, sizeof(config)); if (!ResetDecoder(instance) || !DescribeAt9Config(config, instance.info)) { instance.initialized = false; return AJM_RESULT_INVALID_PARAMETER; } instance.initialized = true; instance.totalDecodedSamples = 0; instance.segmentSamples = 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(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; } class JobInput { public: JobInput(const JobHeader& job, const AjmBuffer* inputs) { if (job.inputCount == 1) { bytes = {static_cast(inputs[0].ptr), static_cast(inputs[0].size)}; return; } for (std::uint32_t index = 0; index < job.inputCount; ++index) { const auto* data = static_cast(inputs[index].ptr); joined.insert(joined.end(), data, data + inputs[index].size); } bytes = joined; } JobInput(const JobInput&) = delete; JobInput& operator=(const JobInput&) = delete; std::size_t size() const { return bytes.size(); } bool empty() const { return bytes.empty(); } const std::uint8_t* data() const { return bytes.data(); } std::uint8_t operator[](std::size_t index) const { return bytes[index]; } private: std::vector joined; std::span bytes; }; struct PcmOutputs { const AjmBuffer* buffers; std::uint32_t count; std::size_t capacity = 0; std::size_t produced = 0; std::uint32_t index = 0; std::size_t offset = 0; PcmOutputs(const AjmBuffer* buffers, std::uint32_t count) : buffers(buffers), count(count) { for (std::uint32_t i = 0; i < count; ++i) capacity += buffers[i].size; } std::size_t Room() const { return capacity - produced; } void Emit(const std::uint8_t* source, std::size_t bytes) { while (bytes > 0 && index < count) { const std::size_t chunk = std::min(buffers[index].size - offset, bytes); std::memcpy(static_cast(buffers[index].ptr) + offset, source, chunk); source += chunk; bytes -= chunk; produced += chunk; offset += chunk; if (offset == buffers[index].size) { ++index; offset = 0; } } } }; std::uint32_t PcmEncoding(const Instance& instance) { return static_cast((instance.flags >> 7u) & 7u); } double PcmValue(const std::uint8_t* at, std::uint32_t encoding) { if (encoding == 0) { std::int16_t value = 0; std::memcpy(&value, at, sizeof(value)); return value / 32768.0; } if (encoding == 1) { std::int32_t value = 0; std::memcpy(&value, at, sizeof(value)); return value / 2147483648.0; } float value = 0; std::memcpy(&value, at, sizeof(value)); return value; } void StorePcm(double value, std::uint8_t* out, std::uint32_t encoding) { if (encoding == 0) { const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); std::memcpy(out, &converted, sizeof(converted)); } else if (encoding == 1) { const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); std::memcpy(out, &converted, sizeof(converted)); } else { const auto converted = static_cast(value); std::memcpy(out, &converted, sizeof(converted)); } } bool Resampling(const Instance& instance) { return instance.resampler.ratio != 1.0f || !instance.resampler.frames.empty(); } std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { const auto& resampler = instance.resampler; if (resampler.frames.empty() || channels == 0) return 0; const std::size_t frames = resampler.frames.size() / channels; const auto next = static_cast(resampler.position); return frames > next ? frames - next : 0; } void ResamplerPush(Instance& instance, const double* samples, std::size_t frames, std::size_t channels) { auto& stored = instance.resampler.frames; if (frames == 0) return; if (stored.empty()) stored.insert(stored.end(), samples, samples + channels); stored.insert(stored.end(), samples, samples + frames * channels); } void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t channels, std::uint32_t encoding, std::size_t sampleBytes) { auto& resampler = instance.resampler; if (resampler.frames.empty() || channels == 0) return; const std::size_t frameBytes = channels * sampleBytes; const std::size_t available = resampler.frames.size() / channels; std::vector frame(frameBytes); while (outputs.Room() >= frameBytes) { const auto index = static_cast(resampler.position); if (index + 2 >= available) break; const double t = resampler.position - static_cast(index); for (std::size_t channel = 0; channel < channels; ++channel) { const double p0 = resampler.frames[(index - 1) * channels + channel]; const double p1 = resampler.frames[index * channels + channel]; const double p2 = resampler.frames[(index + 1) * channels + channel]; const double p3 = resampler.frames[(index + 2) * channels + channel]; const double value = p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); StorePcm(value, frame.data() + channel * sampleBytes, encoding); } outputs.Emit(frame.data(), frameBytes); resampler.position += resampler.ratio; } const auto index = std::min(static_cast(resampler.position), available); if (index > 1) { resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast((index - 1) * channels)); resampler.position -= static_cast(index - 1); } } void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { std::size_t first = 0; std::size_t count = samples; if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); first += skip; count -= skip; } if (instance.gapless.totalSamples != 0) { const std::uint64_t remaining = instance.gapless.totalSamples > instance.segmentSamples ? instance.gapless.totalSamples - instance.segmentSamples : 0; count = static_cast(std::min(count, remaining)); } instance.segmentSamples += count; instance.totalDecodedSamples += count; if (!Resampling(instance)) { outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); return; } const auto encoding = PcmEncoding(instance); std::vector values(count * channels); for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(pcm + (first * channels + index) * sampleBytes, encoding); ResamplerPush(instance, values.data(), count, channels); ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } SidebandFormat CurrentFormat(const Instance& instance) { switch (instance.codec) { case CODEC_AT9: return {static_cast(instance.info.channels), ChannelMask(static_cast(instance.info.channels)), static_cast(instance.info.samplingRate), PcmEncoding(instance), 0, 0}; case CODEC_MP3: return {instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, PcmEncoding(instance), instance.mp3Bitrate, 0}; case CODEC_OPUS: return {instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, PcmEncoding(instance), 0, 0}; default: throw std::runtime_error("AJM: the format of codec " + std::to_string(instance.codec) + " is not implemented"); } } SidebandAt9CodecInfo At9CodecInfo(const Instance& instance) { const auto superframeSize = static_cast(instance.info.superframeSize); return {superframeSize, static_cast(instance.info.framesInSuperframe), instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining, static_cast(instance.info.frameSamples)}; } void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) { auto* sideband = static_cast(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(consumed), static_cast(produced), instance.totalDecodedSamples}; write(&stream, sizeof(stream)); } if (job.flags & SIDEBAND_FORMAT) write(&format, sizeof(format)); if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); if (job.flags & RUN_GET_CODEC_INFO) { const SidebandAt9CodecInfo codecInfo = At9CodecInfo(instance); write(&codecInfo, sizeof(codecInfo)); } if (job.flags & RUN_MULTIPLE_FRAMES) { const SidebandMultipleFrames multiple{frames, 0}; write(&multiple, sizeof(multiple)); } } void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto channels = static_cast(instance.info.channels); const auto frameSamples = static_cast(instance.info.frameSamples); const auto encoding = PcmEncoding(instance); 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 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(instance.flags), encoding); std::int32_t result = 0; int decodeStatus = 0; std::size_t consumed = 0; std::uint32_t frames = 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(instance.info.superframeSize); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); const auto segmentEnded = [&] { return instance.gapless.totalSamples != 0 && instance.segmentSamples >= instance.gapless.totalSamples; }; for (;;) { if (segmentEnded()) break; 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 (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { instance.superframeRemaining = static_cast(superframeSize); instance.frameInSuperframe = 0; } int used = 0; decodeStatus = DecodeAt9Frame(instance, input.data() + consumed, static_cast(instance.superframeRemaining), pcm.data(), encoding, sampleBytes, &used); if (decodeStatus != 0 || used <= 0 || static_cast(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(used); instance.superframeRemaining -= static_cast(used); if (++instance.frameInSuperframe == framesInSuperframe) { consumed += instance.superframeRemaining; instance.superframeRemaining = 0; } ++frames; EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); } if (segmentEnded()) instance.segmentSamples = 0; 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(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast(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(result), static_cast(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); for (std::size_t i = 0; i < consumed && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } struct Mp3Frame { std::size_t bytes; std::uint32_t channels; std::uint32_t samples; std::uint32_t sampleRate; std::uint32_t bitrate; }; bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) { if (size < 4) return false; const std::uint32_t header = (std::uint32_t{data[0]} << 24u) | (std::uint32_t{data[1]} << 16u) | (std::uint32_t{data[2]} << 8u) | data[3]; const auto version = (header >> 19u) & 3u; const auto layer = (header >> 17u) & 3u; const auto bitrateIndex = (header >> 12u) & 15u; const auto rateIndex = (header >> 10u) & 3u; if ((header & 0xFFE00000u) != 0xFFE00000u || version == 1 || layer != 1 || bitrateIndex == 0 || bitrateIndex == 15 || rateIndex == 3) return false; static constexpr std::uint32_t rates[4][3] = {{11025, 12000, 8000}, {0, 0, 0}, {22050, 24000, 16000}, {44100, 48000, 32000}}; static constexpr std::uint32_t mpeg1Kbps[16] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}; static constexpr std::uint32_t mpeg2Kbps[16] = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}; const bool mpeg1 = version == 3; frame.sampleRate = rates[version][rateIndex]; frame.bitrate = (mpeg1 ? mpeg1Kbps : mpeg2Kbps)[bitrateIndex] * 1000u; frame.samples = mpeg1 ? 1152u : 576u; frame.channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; frame.bytes = (mpeg1 ? 144u : 72u) * frame.bitrate / frame.sampleRate + ((header >> 9u) & 1u); return true; } constexpr std::uint32_t MP3_OFL_NONE = 0; constexpr std::uint32_t MP3_OFL_LAME = 1; constexpr std::uint32_t MP3_OFL_VBRI = 2; constexpr std::uint32_t MP3_OFL_FGH = 3; constexpr std::uint32_t MP3_OFL_VBRI_AND_FGH = 4; std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { std::uint32_t value = 0; for (std::size_t i = 0; i < bytes; ++i) value = (value << 8u) | data[i]; return value; } int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { const std::size_t end = std::min(streamSize, frame->frame_size); if (end <= 4) return; const std::size_t available = end - 4; std::size_t bit = 0; const auto readBits = [&](std::uint32_t count) { std::uint32_t value = 0; for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); return value; }; const bool mpeg1 = ((header >> 19u) & 3u) == 3u; const bool mono = ((header >> 6u) & 3u) == 3u; bit += mpeg1 ? (mono ? 18 : 20) : (mono ? 9 : 10); std::uint32_t mainDataBits = 0; for (std::uint32_t granule = 0; granule < (mpeg1 ? 2u : 1u); ++granule) { for (std::uint32_t channel = 0; channel < frame->num_channels; ++channel) { mainDataBits += readBits(12); bit += mpeg1 ? 47 : 51; } } bit += mainDataBits; std::size_t at = 4 + std::min((bit + 7) / 8, available); if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { const std::uint8_t flags = stream[at + 7]; std::size_t field = at + 8; std::uint32_t frames = 0; if (flags & 0x01u) { if (field + 4 > end) return; frames = ReadBigEndian(stream + field, 4); frame->num_frames = frames; field += 4; } if (flags & 0x02u) { field += 4; if (field > end) return; } if (flags & 0x04u) { field += 100; if (field > end) return; } if (flags & 0x08u) { field += 4; if (field > end) return; } if (field + 0x18 <= end && std::memcmp(stream + field, "LAME", 4) == 0) { const std::uint32_t delay = (std::uint32_t{stream[field + 0x15]} << 4u) | (stream[field + 0x16] >> 4u); const std::uint32_t padding = ((std::uint32_t{stream[field + 0x16]} & 0x0Fu) << 8u) | stream[field + 0x17]; if ((flags & 0x01u) && frames > 0) frame->total_samples = frames * frame->samples_per_channel - (delay + padding); frame->encoder_delay = frame->samples_per_channel + delay + 529; frame->ofl_type = MP3_OFL_LAME; } } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); frame->ofl_type = MP3_OFL_VBRI; if (frame->frame_size <= streamSize) { AjmDecMp3ParseFrame next{}; if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { frame->encoder_delay += next.encoder_delay; frame->total_samples = next.total_samples; frame->ofl_type = MP3_OFL_VBRI_AND_FGH; } } } else if (at + 10 <= end) { while (at + 9 < end && stream[at] != 0xB4u) ++at; if (at + 9 >= end) return; std::uint8_t crc = 0xFF; for (std::size_t i = 0; i < 9; ++i) { for (int shift = 7; shift >= 0; --shift) { const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); } } if (stream[at + 9] != crc) return; frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); frame->total_samples = ReadBigEndian(stream + at + 3, 4); frame->ofl_type = MP3_OFL_FGH; } } int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; static constexpr std::uint32_t kbps[4][16] = { {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, }; const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; const std::uint32_t version = (header >> 19u) & 3u; const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); const bool mpeg1 = version == 3; frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; frame->samples_per_channel = mpeg1 ? 1152u : 576u; frame->bitrate = bitrate; frame->sample_rate = sampleRate; frame->encoder_delay = 0; frame->num_frames = 0; frame->total_samples = 0; frame->ofl_type = MP3_OFL_NONE; if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); return 0; } void OpenMp3(Instance& instance) { static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); (void)quiet; const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_MP3); if (instance.mp3) avcodec_free_context(&instance.mp3); instance.mp3 = codec ? avcodec_alloc_context3(codec) : nullptr; if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder"); } void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto encoding = PcmEncoding(instance); if (encoding > 2) throw std::runtime_error("AJM MP3: PCM encoding " + std::to_string(encoding) + " is not implemented"); const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); if (!packet || !decoded) throw std::bad_alloc(); std::vector pcm; std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, instance.mp3Channels, encoding, sampleBytes); while (consumed < input.size()) { Mp3Frame frame{}; if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { result |= AJM_RESULT_INVALID_DATA; break; } if (input.size() - consumed < frame.bytes) { result |= AJM_RESULT_PARTIAL_INPUT; break; } if (pcmOutputs.Room() < (Resampling(instance) ? 1 : frame.samples) * frame.channels * sampleBytes) { if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } packet->data = const_cast(input.data() + consumed); packet->size = static_cast(frame.bytes); if (avcodec_send_packet(instance.mp3, packet.get()) < 0) { result |= AJM_RESULT_INVALID_DATA; break; } consumed += frame.bytes; ++frames; instance.mp3Channels = frame.channels; instance.mp3SampleRate = frame.sampleRate; instance.mp3Bitrate = frame.bitrate; int status = 0; while ((status = avcodec_receive_frame(instance.mp3, decoded.get())) == 0) { const auto format = static_cast(decoded->format); if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_S16P) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto channels = static_cast(decoded->ch_layout.nb_channels); const auto samples = static_cast(decoded->nb_samples); pcm.resize(samples * channels * sampleBytes); for (std::size_t sample = 0; sample < samples; ++sample) { for (std::size_t channel = 0; channel < channels; ++channel) { const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[channel])[sample] : reinterpret_cast(decoded->extended_data[channel])[sample] / 32768.0f; auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; if (encoding == 0) { const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); std::memcpy(out, &converted, sizeof(converted)); } else if (encoding == 1) { const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); std::memcpy(out, &converted, sizeof(converted)); } else { std::memcpy(out, &value, sizeof(value)); } } } EmitFrame(instance, pcmOutputs, pcm.data(), samples, channels, sampleBytes); av_frame_unref(decoded.get()); } if (status != AVERROR(EAGAIN)) { result |= AJM_RESULT_INVALID_DATA; break; } if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } void OpenOpus(Instance& instance) { static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); (void)quiet; const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); if (instance.opus) avcodec_free_context(&instance.opus); instance.opus = codec ? avcodec_alloc_context3(codec) : nullptr; if (!instance.opus) throw std::runtime_error("AJM: cannot allocate the FFmpeg Opus decoder"); av_channel_layout_default(&instance.opus->ch_layout, static_cast(instance.opusChannels)); instance.opus->sample_rate = 48000; if (instance.opusMappingFamily == 1) { static constexpr std::uint8_t streams[8] = {1, 1, 2, 2, 3, 4, 4, 5}; static constexpr std::uint8_t coupled[8] = {0, 1, 1, 2, 2, 2, 3, 3}; static constexpr std::uint8_t mapping[8][8] = { {0}, {0, 1}, {0, 2, 1}, {0, 1, 2, 3}, {0, 4, 1, 2, 3}, {0, 4, 1, 2, 3, 5}, {0, 4, 1, 2, 3, 5, 6}, {0, 6, 1, 2, 3, 4, 5, 7} }; const std::size_t index = instance.opusChannels - 1; instance.opus->extradata_size = static_cast(21 + instance.opusChannels); instance.opus->extradata = static_cast(av_mallocz(instance.opus->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE)); if (!instance.opus->extradata) throw std::bad_alloc(); auto* header = instance.opus->extradata; std::memcpy(header, "OpusHead", 8); header[8] = 1; header[9] = static_cast(instance.opusChannels); header[12] = 0x80; header[13] = 0xbb; header[18] = 1; header[19] = streams[index]; header[20] = coupled[index]; std::memcpy(header + 21, mapping[index], instance.opusChannels); } if (avcodec_open2(instance.opus, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg Opus decoder"); } std::size_t OpusPacketSamples(const std::uint8_t* packet, std::size_t size) { if (size < 1) return 0; const std::uint32_t config = packet[0] >> 3u; std::size_t frameSamples = 0; if (config < 12) frameSamples = (config & 3u) == 0 ? 480 : (config & 3u) == 1 ? 960 : (config & 3u) == 2 ? 1920 : 2880; else if (config < 16) frameSamples = (config & 1u) == 0 ? 480 : 960; else frameSamples = std::size_t{120} << (config & 3u); std::size_t frames = 0; switch (packet[0] & 3u) { case 0: frames = 1; break; case 1: case 2: frames = 2; break; default: if (size < 2) return 0; frames = packet[1] & 0x3fu; break; } return frames * frameSamples; } void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { const JobInput input(job, inputs); PcmOutputs pcmOutputs(outputs, job.outputCount); const auto encoding = PcmEncoding(instance); if (encoding > 2) throw std::runtime_error("AJM Opus: PCM encoding " + std::to_string(encoding) + " is not implemented"); const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); if (!packet || !decoded) throw std::bad_alloc(); std::vector pcm; const std::size_t channels = instance.opusChannels; std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; const std::size_t frameBytes = channels * sampleBytes; if (Resampling(instance) && !instance.opusPending.empty()) { std::vector values(instance.opusPending.size() / sampleBytes); for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(instance.opusPending.data() + index * sampleBytes, encoding); ResamplerPush(instance, values.data(), values.size() / channels, channels); instance.opusPending.clear(); } if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); if (!instance.opusPending.empty()) { const std::size_t bytes = std::min(instance.opusPending.size(), pcmOutputs.Room()); pcmOutputs.Emit(instance.opusPending.data(), bytes); instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); } while (consumed < input.size() && instance.opusPending.empty()) { if (frames != 0 && pcmOutputs.Room() < frameBytes) break; if (input.size() - consumed < 2) { result |= AJM_RESULT_PARTIAL_INPUT; break; } const std::size_t bytes = input[consumed] | (std::size_t{input[consumed + 1]} << 8u); if (input.size() - consumed - 2 < bytes) { result |= AJM_RESULT_PARTIAL_INPUT; break; } const auto* data = input.data() + consumed + 2; const std::size_t samples = OpusPacketSamples(data, bytes); if (bytes == 0 || samples == 0) { result |= AJM_RESULT_INVALID_DATA; break; } if (pcmOutputs.Room() < frameBytes) { if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } packet->data = const_cast(data); packet->size = static_cast(bytes); if (avcodec_send_packet(instance.opus, packet.get()) < 0) { result |= AJM_RESULT_INVALID_DATA; break; } consumed += 2 + bytes; ++frames; int status = 0; while ((status = avcodec_receive_frame(instance.opus, decoded.get())) == 0) { const auto format = static_cast(decoded->format); if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT) throw std::runtime_error("AJM Opus: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto frameChannels = static_cast(decoded->ch_layout.nb_channels); const auto frameSamples = static_cast(decoded->nb_samples); const auto value = [&](std::size_t sample, std::size_t channel) { const std::size_t sourceChannel = std::min(channel, frameChannels - 1); return format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; }; std::size_t first = 0; std::size_t count = frameSamples; if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); instance.gapless.skippedSamples = static_cast(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::min(count, remaining)); } instance.totalDecodedSamples += count; if (Resampling(instance)) { std::vector values(count * channels); for (std::size_t sample = 0; sample < count; ++sample) { for (std::size_t channel = 0; channel < channels; ++channel) values[sample * channels + channel] = value(first + sample, channel); } ResamplerPush(instance, values.data(), count, channels); ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); } else { pcm.assign(count * frameBytes, 0); for (std::size_t sample = 0; sample < count; ++sample) { for (std::size_t channel = 0; channel < channels; ++channel) StorePcm(value(first + sample, channel), pcm.data() + (sample * channels + channel) * sampleBytes, encoding); } const std::size_t fits = std::min(pcm.size(), pcmOutputs.Room() / frameBytes * frameBytes); pcmOutputs.Emit(pcm.data(), fits); instance.opusPending.insert(instance.opusPending.end(), pcm.begin() + static_cast(fits), pcm.end()); } av_frame_unref(decoded.get()); } if (status != AVERROR(EAGAIN)) { result |= AJM_RESULT_INVALID_DATA; break; } if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; AJM_TRACE("[ajm] instance %u opus run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples)); WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } void ClearContext(Instance& instance) { instance.totalDecodedSamples = 0; instance.segmentSamples = 0; instance.gapless.skippedSamples = 0; if (HasAt9Decoder(instance) && instance.initialized) ResetDecoder(instance); if (instance.mp3) avcodec_flush_buffers(instance.mp3); if (instance.opus) avcodec_flush_buffers(instance.opus); instance.opusPending.clear(); ClearResampler(instance); } std::size_t ControlInitializeSize(std::uint32_t codec) { switch (codec) { case CODEC_MP3: return 0; case CODEC_AT9: return 8; case CODEC_OPUS: return 12; default: NotImplemented_nid_no_patch("sceAjmBatchJobControl (INITIALIZE for a codec other than MP3, ATRAC9 and Opus)"); return 0; } } void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) { const auto* input = job.inputCount ? static_cast(inputs[0].ptr) : nullptr; const std::size_t inputSize = job.inputCount ? inputs[0].size : 0; const std::size_t gaplessSize = (job.flags & SIDEBAND_GAPLESS_DECODE) ? sizeof(SidebandGaplessDecode) : 0; const std::size_t initializeSize = (job.flags & CONTROL_INITIALIZE) ? ControlInitializeSize(instance.codec) : 0; if (inputSize != gaplessSize + initializeSize) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband input size other than the gapless decode and the codec's initialize parameters)"); std::int32_t result = 0; if (job.flags & CONTROL_RESET) ClearContext(instance); if (job.flags & CONTROL_INITIALIZE) result = InitializeInstance(instance, input + gaplessSize, initializeSize); if (gaplessSize) { SidebandGaplessDecode gapless{}; std::memcpy(&gapless, input, sizeof(gapless)); instance.gapless.totalSamples = gapless.totalSamples; instance.gapless.skipSamples = gapless.skipSamples; } AJM_TRACE("[ajm] instance %u control flags 0x%llx: %zu sideband input bytes -> result 0x%x, gapless total %u skip %u skipped %u\n", job.instance, static_cast(job.flags), inputSize, static_cast(result), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); WriteResult(job.sideband, job.sidebandSize, result); } std::size_t HeldSamples(const Instance& instance) { const std::size_t channels = instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : static_cast(std::max(0, instance.info.channels)); if (Resampling(instance)) return ResamplerHeld(instance, channels); const std::size_t sampleBytes = PcmEncoding(instance) == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); return channels == 0 ? 0 : instance.opusPending.size() / (channels * sampleBytes); } 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(job.sidebandSize)); if (job.sideband && job.sidebandSize) std::memset(job.sideband, 0, std::min(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(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(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(job.sidebandSize), static_cast(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(job.sidebandSize)); ClearContext(*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(job.flags), static_cast(job.sidebandSize)); instance->gapless.totalSamples = gapless.totalSamples; instance->gapless.skipSamples = gapless.skipSamples; if (job.flags) { instance->gapless.skippedSamples = 0; instance->segmentSamples = 0; } WriteResult(job.sideband, job.sidebandSize, 0); break; } case JobKind::Control: Control(*instance, job, inputs); break; case JobKind::SetResampleParameters: std::memcpy(&instance->resampler.ratio, job.parameters, sizeof(instance->resampler.ratio)); AJM_TRACE("[ajm] instance %u set resample ratio %f\n", job.instance, static_cast(instance->resampler.ratio)); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::GetResampleInfo: { const SidebandResampleInfo resample{instance->resampler.ratio, static_cast(HeldSamples(*instance)), {}}; WriteResult(job.sideband, job.sidebandSize, 0); if (job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(resample)) std::memcpy(static_cast(job.sideband) + sizeof(SidebandResult), &resample, sizeof(resample)); 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 && instance->codec != CODEC_MP3 && instance->codec != CODEC_OPUS) { throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } else if ((job.flags & RUN_GET_CODEC_INFO) && (job.flags & RUN_MULTIPLE_FRAMES)) { NotImplemented_nid_no_patch("AJM run job with both RUN_GET_CODEC_INFO and RUN_MULTIPLE_FRAMES (sideband order)"); } else if ((job.flags & RUN_GET_CODEC_INFO) && instance->codec != CODEC_AT9) { NotImplemented_nid_no_patch("AJM RUN_GET_CODEC_INFO for a codec other than ATRAC9"); } else if (job.inputCount == 0 && job.outputCount == 0) { AJM_TRACE("[ajm] instance %u run flags 0x%llx without buffers, sideband %llu bytes\n", job.instance, static_cast(job.flags), static_cast(job.sidebandSize)); WriteRunSideband(job, *instance, 0, 0, 0, 0, CurrentFormat(*instance)); } else if (instance->codec == CODEC_AT9) { RunAt9(*instance, job, inputs, outputs); } else if (instance->codec == CODEC_MP3) { RunMp3(*instance, job, inputs, outputs); } else { RunOpus(*instance, job, inputs, outputs); } break; default: AJM_TRACE("[ajm] instance %u unknown job kind %u\n", job.instance, static_cast(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; auto created = std::make_unique(); created->codec = codec; created->flags = flags; if (codec == CODEC_MP3) { OpenMp3(*created); created->initialized = true; } 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(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; std::uint8_t config[ATRAC9_CONFIG_DATA_SIZE]; std::memcpy(config, config_data, sizeof(config)); Atrac9CodecInfo info{}; const bool valid = DescribeAt9Config(config, info); 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(info.channels); config_info->sample_rate = static_cast(info.samplingRate); config_info->frame_samples_per_channel = static_cast(info.frameSamples); config_info->superframe_samples_per_channel = static_cast(info.frameSamples * info.framesInSuperframe); config_info->superframe_size = static_cast(info.superframeSize); return 0; } int APS5_VABI sceAjmDecMp3ParseFrame(const uint8_t* stream, uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { return ParseMp3Header(stream, streamSize, parseOfl, frame); } 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(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.parameterSize = gapless_decode ? sizeof(SidebandGaplessDecode) : 0; header.flags = reset ? 1 : 0; return Append(info, header, nullptr, nullptr); } int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { constexpr std::uint64_t supported = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; if (flags == 0 || (flags & ~supported) != 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (flags other than a combination of RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); if ((flags & SIDEBAND_GAPLESS_DECODE) != 0 && (flags & CONTROL_RESET) == 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (SIDEBAND_GAPLESS_DECODE without RESET)"); if (sideband_output_size != sizeof(SidebandResult)) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband output other than the 8-byte result)"); if (sideband_input_size != 0 && !sideband_input) NotImplemented_nid_no_patch("sceAjmBatchJobControl (null sideband input)"); auto header = MakeHeader(JobKind::Control, instance, sideband_output, sideband_output_size); header.flags = flags; header.inputCount = sideband_input_size != 0 ? 1 : 0; const AjmBuffer input{const_cast(sideband_input), sideband_input_size}; return Append(info, header, &input, 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(input_buffers_num); header.outputCount = static_cast(output_buffers_num); return Append(info, header, input_buffers, output_buffers); } int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { const AjmBuffer input{const_cast(data_input), data_input_size}; const AjmBuffer output{data_output, data_output_size}; return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); } int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { (void)flags; if (!std::isfinite(ratio) || ratio <= 0.0f) return SCE_AJM_ERROR_INVALID_PARAMETER; auto header = MakeHeader(JobKind::SetResampleParameters, instance, result, sizeof(SidebandResult)); std::memcpy(header.parameters, &ratio, sizeof(ratio)); header.parameterSize = sizeof(ratio); return Append(info, header, nullptr, nullptr); } int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { if (ratio_change_per_sample != 0.0f) throw std::runtime_error("sceAjmBatchJobSetResampleParametersEx: a ratio change per sample is unsupported"); return sceAjmBatchJobSetResampleParameters(info, instance, ratio_start, flags, result); } int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); } int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_STREAM, input_buffers, input_buffers_num, output_buffers, output_buffers_num, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instance, void* result) { return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_GAPLESS_DECODE, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandGaplessDecode)); } int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) { return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_FORMAT, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandFormat)); } int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, void* result, size_t result_size) { return sceAjmBatchJobRunSplit(info, instance, RUN_GET_CODEC_INFO, nullptr, 0, nullptr, 0, result, result_size); } 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(info->offset), static_cast(info->size)); const auto* cursor = static_cast(info->p_buffer); const auto* end = cursor + info->offset; std::vector buffers; while (cursor < end) { PackedJob record; std::memcpy(&record, cursor, sizeof(record)); JobHeader job{}; job.kind = static_cast(record.kind); job.bytes = record.bytes; job.instance = record.instance; job.flags = record.flags; job.sideband = record.sideband; job.sidebandSize = record.sidebandSize; job.inputCount = record.inputCount; job.outputCount = record.outputCount; job.parameterSize = record.parameterSize; buffers.resize(record.inputCount + record.outputCount); if (!buffers.empty()) std::memcpy(buffers.data(), cursor + sizeof(record), buffers.size() * sizeof(AjmBuffer)); if (record.parameterSize) std::memcpy(job.parameters, cursor + sizeof(record) + buffers.size() * sizeof(AjmBuffer), record.parameterSize); Execute(job, buffers.data(), buffers.data() + job.inputCount); cursor += record.bytes; } if (error) std::memset(error, 0, sizeof(*error)); const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed); { std::lock_guard lock(g_batchLock); g_batches.insert(id); } *batch = id; return 0; } int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) { (void)context; (void)timeout; { std::lock_guard lock(g_batchLock); if (g_batches.erase(batch) == 0) return SCE_AJM_ERROR_INVALID_BATCH; } if (error) std::memset(error, 0, sizeof(*error)); return 0; } int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; std::lock_guard lock(g_batchLock); return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; } int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { (void)info; if (error) std::memset(error, 0, sizeof(*error)); return 0; } }