feat(ajm): implement DecodeSingle, GetGaplessDecode and GetCodecInfo

sceAjmBatchJobDecodeSingle runs a single-frame decode job with the stream
sideband. sceAjmBatchJobGetGaplessDecode and sceAjmBatchJobGetCodecInfo
append run jobs without buffers that return the gapless-decode and codec-info
sidebands.

A run job without input or output buffers no longer decodes: it reports the
instance state with a zero result instead of PARTIAL_INPUT on MP3 and Opus.

RUN_GET_CODEC_INFO (bit 11) writes the ATRAC9 codec info after the gapless
sideband. Codec info for other codecs, and codec info together with
RUN_MULTIPLE_FRAMES, throw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
raghav314 2026-10-05 05:13:03 +05:30
parent 07404b67e4
commit 2771eeea77
3 changed files with 180 additions and 35 deletions

View File

@ -33,18 +33,6 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6
return 0;
}
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) {
(void)info;
(void)instance;
(void)bitstream_input;
(void)bitstream_input_size;
(void)pcm_output;
(void)pcm_output_size;
(void)result;
AjmStub(__func__);
return 0;
}
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) {
(void)info;
(void)instance;
@ -69,23 +57,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const
return 0;
}
int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, void* result, size_t result_size) {
(void)info;
(void)instance;
(void)result;
(void)result_size;
AjmStub(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instance, void* result) {
(void)info;
(void)instance;
(void)result;
AjmStub(__func__);
return 0;
}
int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) {
(void)info;
(void)instance;

View File

@ -51,6 +51,7 @@ bool TraceEnabled() {
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 SIDEBAND_GAPLESS_DECODE = 1ull << 45;
constexpr std::uint64_t SIDEBAND_FORMAT = 1ull << 46;
@ -87,6 +88,13 @@ struct SidebandMultipleFrames {
std::uint32_t reserved;
};
struct SidebandAt9CodecInfo {
std::uint32_t superframeSize;
std::uint32_t framesInSuperframe;
std::uint32_t nextFrameSize;
std::uint32_t frameSamples;
};
struct Instance {
std::uint32_t codec = 0;
std::uint64_t flags = 0;
@ -337,6 +345,20 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm,
instance.totalDecodedSamples += count;
}
SidebandFormat CurrentFormat(const Instance& instance) {
switch (instance.codec) {
case CODEC_AT9: return {static_cast<std::uint32_t>(instance.info.channels), ChannelMask(static_cast<std::size_t>(instance.info.channels)), static_cast<std::uint32_t>(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<std::uint32_t>(instance.info.superframeSize);
return {superframeSize, static_cast<std::uint32_t>(instance.info.framesInSuperframe), instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining, static_cast<std::uint32_t>(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<std::uint8_t*>(job.sideband);
std::size_t offset = 0;
@ -353,6 +375,10 @@ void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32
}
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));
@ -431,7 +457,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
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<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(instance.info.samplingRate), encoding, 0, 0});
WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance));
}
struct Mp3Frame {
@ -546,7 +572,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
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<unsigned long long>(job.flags), input.size(), pcmOutputs.capacity, static_cast<unsigned>(result), frames, consumed, pcmOutputs.produced,
instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast<unsigned long long>(instance.totalDecodedSamples));
WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0});
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);
@ -682,7 +708,7 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs,
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<unsigned long long>(job.flags), input.size(), pcmOutputs.capacity, static_cast<unsigned>(result), frames, consumed, pcmOutputs.produced, static_cast<unsigned long long>(instance.totalDecodedSamples));
WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.opusChannels, ChannelMask(instance.opusChannels), instance.opusSampleRate, encoding, 0, 0});
WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance));
}
void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
@ -729,14 +755,21 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out
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<unsigned long long>(job.flags), static_cast<unsigned long long>(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 if (instance->codec == CODEC_OPUS) {
RunOpus(*instance, job, inputs, outputs);
} else {
throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented");
RunOpus(*instance, job, inputs, outputs);
}
break;
default:
@ -907,6 +940,18 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const
return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, 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 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);

View File

@ -5,6 +5,7 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <vector>
extern "C" {
@ -17,6 +18,11 @@ int APS5_VABI sceAjmInstanceDestroy(std::uint32_t, std::uint32_t);
int APS5_VABI sceAjmBatchInitialize(void*, std::size_t, AjmBatchInfo*);
int APS5_VABI sceAjmBatchJobInitialize(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*);
int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*);
int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*);
int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t, void*, std::size_t);
int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo*, std::uint32_t, const void*, int, void*);
int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void*);
int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t);
int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*);
int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*);
}
@ -195,6 +201,126 @@ void TestMp3ParseFrame() {
Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter);
}
struct GaplessDecode {
std::uint32_t totalSamples;
std::uint16_t skipSamples;
std::uint16_t skippedSamples;
};
struct GaplessSideband {
std::int32_t result;
std::int32_t internalResult;
std::uint32_t totalSamples;
std::uint16_t skipSamples;
std::uint16_t skippedSamples;
};
struct At9CodecInfoSideband {
std::int32_t result;
std::int32_t internalResult;
std::uint32_t superframeSize;
std::uint32_t framesInSuperframe;
std::uint32_t nextFrameSize;
std::uint32_t frameSamples;
};
void Submit(std::uint32_t context, const AjmBatchInfo& info) {
std::uint32_t id = 0;
AjmBatchError error{};
Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0);
}
bool Refused(std::uint32_t context, const AjmBatchInfo& info) {
std::uint32_t id = 0;
AjmBatchError error{};
try {
static_cast<void>(sceAjmBatchStart(context, &info, 0, &error, &id));
} catch (const std::runtime_error&) {
return true;
}
return false;
}
void TestDecodeSingle(std::uint32_t context) {
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0);
std::vector<std::uint8_t> batch(0x40);
std::vector<std::int16_t> pcm(1152 * 6);
AjmBatchInfo info{};
DecodeSideband sideband{};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobDecodeSingle(&info, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0);
Submit(context, info);
Require(sideband.result == 0 && sideband.inputConsumed == 96 && sideband.outputWritten == 1152 * 2 && sideband.totalDecodedSamples == 1152);
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestGaplessDecode(std::uint32_t context) {
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0);
std::vector<std::uint8_t> batch(4096);
const GaplessDecode gapless{2000, 100, 0};
AjmBatchInfo info{};
std::int32_t setResult[2] = {-1, -1};
GaplessSideband before{-1, -1, 0, 0, 0xffff};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 1, setResult) == 0);
Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &before) == 0);
Submit(context, info);
Require(setResult[0] == 0);
Require(before.result == 0 && before.internalResult == 0 && before.totalSamples == 2000 && before.skipSamples == 100 && before.skippedSamples == 0);
std::vector<std::int16_t> pcm(1152);
DecodeSideband decoded{};
GaplessSideband after{-1, -1, 0, 0, 0};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobDecodeSingle(&info, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded) == 0);
Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &after) == 0);
Submit(context, info);
Require(decoded.result == 0 && decoded.inputConsumed == 96 && decoded.outputWritten == (1152 - 100) * 2);
Require(after.result == 0 && after.skippedSamples == 100);
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void TestCodecInfo(std::uint32_t context) {
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0);
std::vector<std::uint8_t> batch(4096);
AjmBatchInfo info{};
At9CodecInfoSideband early{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobGetCodecInfo(&info, instance, &early, sizeof(early)) == 0);
Submit(context, info);
Require(early.result == 1 && early.superframeSize == 0xaaaaaaaau);
const std::uint8_t config[8] = {0xFE, 0x72, 0x1F, 0xF0};
std::int32_t initResult[2] = {-1, -1};
At9CodecInfoSideband codec{-1, -1, 0, 0, 0, 0};
At9CodecInfoSideband bounded{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), initResult) == 0);
Require(sceAjmBatchJobGetCodecInfo(&info, instance, &codec, sizeof(codec)) == 0);
Require(sceAjmBatchJobGetCodecInfo(&info, instance, &bounded, 2 * sizeof(std::int32_t)) == 0);
Submit(context, info);
Require(initResult[0] == 0);
Require(codec.result == 0 && codec.internalResult == 0 && codec.superframeSize == 1024 && codec.framesInSuperframe == 4 && codec.nextFrameSize == 1024 && codec.frameSamples == 256);
Require(bounded.result == 0 && bounded.superframeSize == 0xaaaaaaaau && bounded.frameSamples == 0xaaaaaaaau);
constexpr std::uint64_t runGetCodecInfo = 1ull << 11;
constexpr std::uint64_t runMultipleFrames = 1ull << 12;
std::uint8_t sideband[64] = {};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobRun(&info, instance, runGetCodecInfo | runMultipleFrames, nullptr, 0, nullptr, 0, sideband, sizeof(sideband)) == 0);
Require(Refused(context, info));
Require(sceAjmInstanceDestroy(context, instance) == 0);
Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0);
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobGetCodecInfo(&info, instance, sideband, sizeof(sideband)) == 0);
Require(Refused(context, info));
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
}
int main() {
@ -214,5 +340,8 @@ int main() {
TestMp3ParseFrame();
TestMp3(context);
TestOpus(context);
TestDecodeSingle(context);
TestGaplessDecode(context);
TestCodecInfo(context);
Require(sceAjmFinalize(context) == 0);
}