feat(ajm): decode Opus (codec 24) through FFmpeg

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pacuka 2026-10-03 18:10:31 +02:00
parent 2cf901bcff
commit 5ecf80e2f5
2 changed files with 232 additions and 0 deletions

View File

@ -38,6 +38,7 @@ 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() {
@ -101,10 +102,15 @@ struct Instance {
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 opusSampleRate = 0;
std::vector<std::uint8_t> opusPending;
~Instance() {
if (decoder) Atrac9ReleaseHandle(decoder);
if (mp3) avcodec_free_context(&mp3);
if (opus) avcodec_free_context(&opus);
}
};
@ -212,7 +218,25 @@ bool ResetDecoder(Instance& instance) {
return Atrac9InitDecoder(instance.decoder, config) == 0;
}
void OpenOpus(Instance& instance);
std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) {
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;
instance.opusChannels = channels;
instance.opusSampleRate = sampleRate;
OpenOpus(instance);
instance.opusPending.clear();
instance.initialized = true;
instance.totalDecodedSamples = 0;
instance.gapless = {};
return 0;
}
if (instance.codec != CODEC_AT9) {
instance.initialized = true;
return 0;
@ -521,6 +545,143 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c
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});
}
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<int>(instance.opusChannels));
instance.opus->sample_rate = 48000;
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 auto input = JoinInputs(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<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); });
std::unique_ptr<AVFrame, void (*)(AVFrame*)> decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); });
if (!packet || !decoded) throw std::bad_alloc();
std::vector<std::uint8_t> pcm;
const std::size_t channels = instance.opusChannels;
std::int32_t result = 0;
std::size_t consumed = 0;
std::uint32_t frames = 0;
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<std::ptrdiff_t>(bytes));
}
const std::size_t frameBytes = channels * sampleBytes;
while (consumed < input.size() && instance.opusPending.empty()) {
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<std::uint8_t*>(data);
packet->size = static_cast<int>(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<AVSampleFormat>(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<std::size_t>(decoded->ch_layout.nb_channels);
const auto frameSamples = static_cast<std::size_t>(decoded->nb_samples);
pcm.assign(frameSamples * channels * sampleBytes, 0);
for (std::size_t sample = 0; sample < frameSamples; ++sample) {
for (std::size_t channel = 0; channel < channels; ++channel) {
const std::size_t sourceChannel = std::min(channel, frameChannels - 1);
const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast<const float*>(decoded->extended_data[sourceChannel])[sample]
: reinterpret_cast<const float*>(decoded->data[0])[sample * frameChannels + sourceChannel];
auto* out = pcm.data() + (sample * channels + channel) * sampleBytes;
if (encoding == 0) {
const auto converted = static_cast<std::int16_t>(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::int32_t>(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL));
std::memcpy(out, &converted, sizeof(converted));
} else {
std::memcpy(out, &value, sizeof(value));
}
}
}
std::size_t first = 0;
std::size_t count = frameSamples;
if (instance.gapless.skippedSamples < instance.gapless.skipSamples) {
const std::size_t skip = std::min<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(instance.gapless.skippedSamples + skip);
first += skip;
count -= skip;
}
if (instance.gapless.totalSamples != 0) {
const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0;
count = static_cast<std::size_t>(std::min<std::uint64_t>(count, remaining));
}
const auto* start = pcm.data() + first * frameBytes;
const std::size_t total = count * frameBytes;
const std::size_t fits = std::min(total, pcmOutputs.Room() / frameBytes * frameBytes);
pcmOutputs.Emit(start, fits);
instance.opusPending.insert(instance.opusPending.end(), start + fits, start + total);
instance.totalDecodedSamples += count;
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<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});
}
void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) {
if (job.kind == JobKind::GetStatistics) {
AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast<unsigned long long>(job.sidebandSize));
@ -547,6 +708,8 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out
instance->gapless.skippedSamples = 0;
if (instance->decoder && instance->initialized) ResetDecoder(*instance);
if (instance->mp3) avcodec_flush_buffers(instance->mp3);
if (instance->opus) avcodec_flush_buffers(instance->opus);
instance->opusPending.clear();
WriteResult(job.sideband, job.sidebandSize, 0);
break;
case JobKind::SetGaplessDecode: {
@ -567,6 +730,8 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out
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");
}

View File

@ -14,6 +14,7 @@ int APS5_VABI sceAjmDecAt9ParseConfigData(const void*, AjmDecAt9ConfigDataInfo*)
int APS5_VABI sceAjmInstanceCreate(std::uint32_t, std::uint32_t, std::uint64_t, std::uint32_t*);
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 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*);
@ -50,6 +51,21 @@ const std::uint8_t MP3_MONO[] = {
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
};
const std::uint8_t OPUS_STEREO[] = {
0x46, 0x00, 0x7C, 0x87, 0xFC, 0xB1, 0x1D, 0xC0, 0xE3, 0x07, 0xD5, 0x9C, 0x4D, 0x91, 0x62, 0x6B, 0x73, 0x86, 0x05, 0xE8, 0xDB, 0xC6, 0x23, 0x6D,
0x5E, 0x6F, 0x73, 0xF8, 0xF2, 0x47, 0xD9, 0x5E, 0xEA, 0xB3, 0xD2, 0x74, 0x6C, 0xE0, 0xCE, 0xE2, 0x3C, 0xFA, 0x59, 0x4A, 0xBA, 0x8F, 0x76, 0x0F,
0x15, 0xE1, 0x3E, 0x60, 0xDF, 0x80, 0xC5, 0x44, 0xB1, 0x1B, 0xEF, 0x43, 0x03, 0x4F, 0xF2, 0xDE, 0xE3, 0xAD, 0x8B, 0x20, 0xFF, 0x68, 0x0C, 0x0A,
0x3F, 0x00, 0x7C, 0x87, 0xFD, 0x45, 0xBD, 0x12, 0x00, 0xA5, 0xB1, 0xA0, 0x0A, 0x62, 0x24, 0x6F, 0x63, 0x70, 0xA5, 0x35, 0x13, 0x03, 0x74, 0x0D,
0x83, 0xC9, 0x74, 0x1B, 0x79, 0xED, 0xA3, 0x09, 0x83, 0xA5, 0x02, 0xC4, 0x60, 0x49, 0xC9, 0xB6, 0x9A, 0x60, 0xAD, 0x6C, 0x77, 0x84, 0xE5, 0xC2,
0xCE, 0x2E, 0x71, 0x07, 0x7E, 0x40, 0xCB, 0xDC, 0x13, 0xDF, 0xCF, 0x84, 0x5D, 0x55, 0x3B, 0x6B, 0x6F, 0x44, 0x00, 0x7C, 0x88, 0x01, 0xE8, 0xA1,
0x2A, 0x12, 0x7D, 0x5E, 0xF6, 0x74, 0x78, 0xB1, 0x27, 0x2B, 0x8C, 0xF0, 0x7F, 0x95, 0x51, 0x71, 0x28, 0x18, 0xEA, 0x66, 0xEB, 0xCA, 0xAC, 0xDF,
0x6C, 0x9C, 0xFD, 0xED, 0x88, 0x8E, 0x66, 0xD7, 0xDA, 0x36, 0xD7, 0xEB, 0x5F, 0xA6, 0x05, 0x72, 0xDA, 0x52, 0x14, 0x7F, 0xF5, 0x84, 0x54, 0xA1,
0xA9, 0xF1, 0xAA, 0xA8, 0x73, 0x9A, 0xCC, 0x91, 0x83, 0x92, 0xD0, 0x7F, 0x3B, 0x6B, 0x65, 0x43, 0x00, 0x7C, 0x88, 0x01, 0xE8, 0xA1, 0x2A, 0x12,
0x7D, 0x5F, 0x04, 0xF8, 0xF5, 0x1F, 0xA0, 0x85, 0x89, 0xED, 0xBA, 0x7C, 0x5F, 0x63, 0x2A, 0x9E, 0x05, 0xE8, 0x63, 0xD1, 0x73, 0x0C, 0xAF, 0xC8,
0xC9, 0x22, 0xF7, 0x11, 0x1D, 0x8B, 0xA9, 0x9A, 0x90, 0x77, 0xF2, 0x86, 0x49, 0x15, 0x6E, 0xD6, 0x34, 0x1F, 0x50, 0x15, 0xEC, 0x85, 0xC7, 0xF5,
0xA7, 0xFA, 0x99, 0xF9, 0x47, 0x32, 0x07, 0xAF, 0x24, 0xBF, 0x14, 0xC5,
};
struct DecodeSideband {
std::int32_t result;
std::int32_t internalResult;
@ -82,6 +98,56 @@ void TestMp3(std::uint32_t context) {
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
void RunDecode(std::uint32_t context, std::uint32_t instance, const std::uint8_t* input, std::size_t inputSize, void* output, std::size_t outputSize, DecodeSideband& sideband) {
std::vector<std::uint8_t> batch(4096);
AjmBatchInfo info{};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobDecode(&info, instance, input, inputSize, output, outputSize, &sideband) == 0);
std::uint32_t id = 0;
AjmBatchError error{};
Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0);
}
void TestOpus(std::uint32_t context) {
std::uint32_t instance = 0;
Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0);
const std::uint32_t parameters[3] = {2, 48000, 0};
std::vector<std::uint8_t> batch(4096);
AjmBatchInfo info{};
std::int64_t initResult[2] = {-1, -1};
Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0);
Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), initResult) == 0);
std::uint32_t id = 0;
AjmBatchError error{};
Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0 && sceAjmBatchWait(context, id, 0, &error) == 0);
Require(initResult[0] == 0);
std::vector<std::int16_t> pcm(960 * 2);
std::size_t offset = 0;
std::int16_t peak = 0;
for (int packet = 0; packet < 3; ++packet) {
const std::size_t bytes = OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u);
DecodeSideband sideband{};
RunDecode(context, instance, OPUS_STEREO + offset, sizeof(OPUS_STEREO) - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), sideband);
Require(sideband.result == 0 && static_cast<std::size_t>(sideband.inputConsumed) == 2 + bytes && sideband.outputWritten == 960 * 2 * 2);
Require(sideband.totalDecodedSamples == static_cast<std::uint64_t>(packet + 1) * 960);
if (packet > 0) {
for (const std::int16_t sample : pcm) peak = std::max<std::int16_t>(peak, static_cast<std::int16_t>(std::abs(sample)));
}
offset += static_cast<std::size_t>(sideband.inputConsumed);
}
Require(peak > 900 && peak < 1200);
std::vector<std::int16_t> small(512 * 2);
DecodeSideband first{};
RunDecode(context, instance, OPUS_STEREO + offset, sizeof(OPUS_STEREO) - offset, small.data(), small.size() * sizeof(std::int16_t), first);
Require(first.result == 0 && static_cast<std::size_t>(first.inputConsumed) == sizeof(OPUS_STEREO) - offset && first.outputWritten == 512 * 2 * 2);
DecodeSideband rest{};
RunDecode(context, instance, OPUS_STEREO + sizeof(OPUS_STEREO), 0, small.data(), small.size() * sizeof(std::int16_t), rest);
Require(rest.result == 0 && rest.inputConsumed == 0 && rest.outputWritten == (960 - 512) * 2 * 2);
Require(sceAjmInstanceDestroy(context, instance) == 0);
}
}
int main() {
@ -99,5 +165,6 @@ int main() {
Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter);
Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter);
TestMp3(context);
TestOpus(context);
Require(sceAjmFinalize(context) == 0);
}