feat(ajm): decode Opus (codec 24) through FFmpeg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2cf901bcff
commit
5ecf80e2f5
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue