#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 // 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(0x80930003); 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; // 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; AVCodecContext* mp3 = nullptr; std::uint32_t mp3Channels = 0; std::uint32_t mp3SampleRate = 0; std::uint32_t mp3Bitrate = 0; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); if (mp3) avcodec_free_context(&mp3); } }; std::mutex g_lock; std::map> g_instances; std::atomic g_nextContext{1}; std::atomic g_nextInstance{1}; std::atomic 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(info->p_buffer) + info->offset; JobHeader record = header; record.bytes = static_cast(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(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; } std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inputs) { std::vector input; for (std::uint32_t index = 0; index < job.inputCount; ++index) { const auto* data = static_cast(inputs[index].ptr); input.insert(input.end(), data, data + inputs[index].size); } return input; } 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); } 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.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); instance.totalDecodedSamples += count; } 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_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 auto input = JoinInputs(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)); 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 (pcmOutputs.Room() < frameBytes) { if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { instance.superframeRemaining = static_cast(superframeSize); instance.frameInSuperframe = 0; } int used = 0; switch (encoding) { case 0: decodeStatus = Atrac9Decode(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; case 1: decodeStatus = Atrac9DecodeS32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; default: decodeStatus = Atrac9DecodeF32(instance.decoder, input.data() + consumed, reinterpret_cast(pcm.data()), &used, 0); break; } 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 ((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(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 < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}); } 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; } 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 auto input = JoinInputs(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; 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() < frame.samples * frame.channels * sampleBytes) { if (frames == 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 && 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, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0}); } 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)); instance->totalDecodedSamples = 0; instance->gapless.skippedSamples = 0; if (instance->decoder && instance->initialized) ResetDecoder(*instance); if (instance->mp3) avcodec_flush_buffers(instance->mp3); 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; 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 if (instance->codec == CODEC_MP3) { RunMp3(*instance, job, inputs, outputs); } else { throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } 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; 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(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 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.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(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 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 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; while (cursor < end) { JobHeader job; std::memcpy(&job, cursor, sizeof(job)); const auto* buffers = reinterpret_cast(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; } }