audio: implement the AJM Opus decoder (#858)

* audio: implement the AJM Opus decoder

* Audio: simplify Opus decoding and preserve packet boundaries

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
João Lobo
2026-09-27 12:53:20 +01:00
committed by GitHub
parent 9229c04ebf
commit 7c8583d204
2 changed files with 218 additions and 141 deletions
+2 -141
View File
@@ -5,6 +5,7 @@
#include "libs/ajm/atrac9_decoder.h"
#include "libs/ajm/decoder.h"
#include "libs/ajm/mp3_decoder.h"
#include "libs/ajm/opus_decoder.h"
#include "libs/audio.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -75,16 +76,6 @@ struct AjmGetStatisticsResult {
uint32_t memory[6];
};
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
enum class AjmCodec : uint32_t {
DecMp3 = 0,
DecAt9 = 1,
@@ -120,8 +111,6 @@ constexpr uint64_t AJM_FLAG_SIDEBAND_STREAM = 1ull << 47u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_OFFSET = 7u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_MASK = 0x7u;
constexpr uint64_t AJM_INSTANCE_FLAG_MAX_CHANNEL_MASK = 0x7fu;
constexpr uint32_t AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH = 10;
constexpr uint32_t AJM_DEC_OPUS_FRAME_SAMPLES = 960;
static std::atomic_uint32_t g_ajm_next_instance {1};
static std::atomic_uint32_t g_ajm_next_batch {1};
@@ -196,134 +185,6 @@ int KYTY_SYSV_ABI AjmDecAt9ParseConfigData(const void* config_data,
return OK;
}
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t channels, uint32_t sample_rate, AjmSampleEncoding encoding,
uint64_t flags)
: AjmDecoder(channels, sample_rate, encoding) {
(void)flags;
}
AjmDecodeResult Initialize(const void* codec_parameters,
size_t codec_parameters_size) override {
auto result = MakeResult();
if (codec_parameters == nullptr ||
codec_parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
const auto* params = static_cast<const AjmDecOpusInitializeParameters*>(codec_parameters);
if (params->channel_num == 0 || params->channel_num > AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH ||
params->sample_rate == 0) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
SetFormat(params->channel_num, params->sample_rate, m_sample_encoding);
m_mapping_family = params->mapping_family;
m_total_decoded_samples = 0;
m_is_initialized = true;
m_frames_per_packet = 1;
LOGF("AJM Opus initialized: %" PRIu32 " Hz, %" PRIu32 " ch, mapping=%" PRIu32 "\n",
params->sample_rate, params->channel_num, params->mapping_family);
return MakeResult();
}
void Reset() override { m_total_decoded_samples = 0; }
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
(void)multiple_frames;
auto result = MakeResult();
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
if (!m_is_initialized) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
auto samples_to_write = AJM_DEC_OPUS_FRAME_SAMPLES;
auto skip_samples = 0u;
if (gapless != nullptr) {
skip_samples = std::min<uint32_t>(samples_to_write, gapless->current.skip_samples);
samples_to_write -= skip_samples;
if (gapless->HasSampleLimit()) {
samples_to_write =
std::min<uint32_t>(samples_to_write, gapless->current.total_samples);
}
}
const auto output_bytes = static_cast<size_t>(samples_to_write) *
static_cast<size_t>(m_channels) *
AjmBytesPerSample(m_sample_encoding);
if (output_bytes != 0 && output == nullptr) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes > output_size) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes != 0) {
std::memset(output, 0, output_bytes);
}
if (gapless != nullptr) {
gapless->current.skip_samples =
static_cast<uint16_t>(gapless->current.skip_samples - skip_samples);
if (gapless->HasSampleLimit()) {
gapless->current.total_samples =
(gapless->current.total_samples > samples_to_write
? gapless->current.total_samples - samples_to_write
: 0);
}
const auto skipped_samples = AJM_DEC_OPUS_FRAME_SAMPLES - samples_to_write;
const auto skipped_total = std::min<uint32_t>(
std::numeric_limits<uint16_t>::max(),
static_cast<uint32_t>(gapless->current.skipped_samples) + skipped_samples);
gapless->current.skipped_samples = static_cast<uint16_t>(skipped_total);
}
result.input_consumed = input_size;
result.output_written = output_bytes;
result.frames = 1;
result.frames_per_packet = m_frames_per_packet;
m_total_decoded_samples += samples_to_write;
result.total_decoded_samples = m_total_decoded_samples;
result.format = GetFormat();
return result;
}
void WriteCodecInfo(void* output, size_t output_size,
const AjmDecodeResult& result) const override {
(void)result;
if (output == nullptr || output_size < sizeof(AjmSidebandDecOpusCodecInfo)) {
return;
}
auto* info = static_cast<AjmSidebandDecOpusCodecInfo*>(output);
info->frames_per_packet = m_frames_per_packet;
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
bool m_is_initialized = false;
uint32_t m_mapping_family = 0;
uint32_t m_frames_per_packet = 1;
};
struct AjmInstanceState {
uint32_t context = 0;
uint32_t codec = 0;
@@ -376,7 +237,7 @@ static std::unique_ptr<AjmDecoder> AjmCreateDecoder(uint32_t codec, uint64_t fla
case static_cast<uint32_t>(AjmCodec::DecM4aac):
return std::make_unique<AjmAacDecoder>(channels, 48000, encoding, flags);
case static_cast<uint32_t>(AjmCodec::DecOpus):
return std::make_unique<AjmOpusDecoder>(channels, 48000, encoding, flags);
return std::make_unique<AjmOpusDecoder>(channels, encoding);
default: break;
}
+216
View File
@@ -0,0 +1,216 @@
#pragma once
#include "libs/ajm/decoder.h"
#include "libs/ajm/ffmpeg_decoder_common.h"
#include <algorithm>
#include <cstring>
namespace Libs::Audio::Ajm {
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
static_assert(sizeof(AjmDecOpusInitializeParameters) == 12);
static_assert(sizeof(AjmSidebandDecOpusCodecInfo) == 4);
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t max_channels, AjmSampleEncoding encoding)
: AjmDecoder(0, 48000, encoding), m_max_channels(max_channels) {}
~AjmOpusDecoder() override { avcodec_free_context(&m_context); }
AjmDecodeResult Initialize(const void* parameters, size_t parameters_size) override {
avcodec_free_context(&m_context);
Reset();
auto result = MakeResult();
if (parameters == nullptr || parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
const auto& params = *static_cast<const AjmDecOpusInitializeParameters*>(parameters);
if (params.channel_num == 0 || params.channel_num > 2 || params.sample_rate != 48000 ||
params.mapping_family != 0) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
if (params.channel_num > m_max_channels) {
result.result = AJM_RESULT_TOO_MANY_CHANNELS | AJM_RESULT_FATAL;
return result;
}
const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
m_context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
if (m_context != nullptr) {
av_channel_layout_default(&m_context->ch_layout, static_cast<int>(params.channel_num));
m_context->sample_rate = static_cast<int>(params.sample_rate);
if (avcodec_open2(m_context, codec, nullptr) < 0) {
avcodec_free_context(&m_context);
}
}
if (m_context == nullptr) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return result;
}
SetFormat(params.channel_num, params.sample_rate, m_sample_encoding);
return MakeResult();
}
void Reset() override {
if (m_context != nullptr) {
avcodec_flush_buffers(m_context);
}
m_total_decoded_samples = 0;
m_frames_per_packet = 0;
}
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
auto result = MakeResult();
if (m_context == nullptr) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
while (gapless == nullptr || !gapless->IsEnd()) {
const auto* data = static_cast<const uint8_t*>(input) + result.input_consumed;
const auto remaining = input_size - result.input_consumed;
if (remaining < 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
const auto packet_size = static_cast<uint32_t>(data[0]) | (uint32_t {data[1]} << 8u);
if (packet_size > remaining - 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
data += 2;
if (packet_size == 0 || ((data[0] & 3u) == 3 && packet_size < 2)) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
const uint32_t config = data[0] >> 3u;
const uint32_t frames = (data[0] & 3u) == 0 ? 1
: (data[0] & 3u) == 3 ? data[1] & 0x3fu
: 2;
const uint32_t frame_samples = config >= 16 ? 120u << (config & 3u)
: config >= 12 ? 480u << (config & 1u)
: (config & 3u) == 3 ? 2880u
: 480u << (config & 3u);
const uint32_t samples = frame_samples * frames;
if (frames == 0 || frames > 6 || frame_samples > 960) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = static_cast<int32_t>(0xffff8800u);
break;
}
const auto skip = gapless != nullptr
? std::min(samples, uint32_t {gapless->current.skip_samples})
: 0;
const auto write = gapless != nullptr && gapless->HasSampleLimit()
? std::min(samples - skip, gapless->current.total_samples)
: samples - skip;
const auto sample_bytes = m_channels * AjmBytesPerSample(m_sample_encoding);
const auto output_bytes = write * sample_bytes;
if (output_bytes > output_size - result.output_written ||
(output_bytes != 0 && output == nullptr)) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
AVFrame* frame = DecodePacket(data, packet_size, &result);
if (frame == nullptr) {
break;
}
if (frame->nb_samples != static_cast<int>(samples) ||
frame->ch_layout.nb_channels != static_cast<int>(m_channels)) {
av_frame_free(&frame);
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
if (output_bytes != 0) {
std::memcpy(static_cast<uint8_t*>(output) + result.output_written,
frame->data[0] + skip * sample_bytes, output_bytes);
}
av_frame_free(&frame);
if (gapless != nullptr) {
gapless->current.skip_samples -= static_cast<uint16_t>(skip);
gapless->current.skipped_samples = static_cast<uint16_t>(
std::min<uint32_t>(std::numeric_limits<uint16_t>::max(),
gapless->current.skipped_samples + samples - write));
if (gapless->HasSampleLimit()) {
gapless->current.total_samples -= write;
}
}
result.input_consumed += packet_size + 2;
result.output_written += output_bytes;
result.frames += frames;
m_frames_per_packet = frames;
m_total_decoded_samples += write;
if (!multiple_frames) {
break;
}
}
result.frames_per_packet = m_frames_per_packet;
result.total_decoded_samples = m_total_decoded_samples;
return result;
}
void WriteCodecInfo(void* output, size_t size, const AjmDecodeResult&) const override {
if (output != nullptr && size >= sizeof(AjmSidebandDecOpusCodecInfo)) {
static_cast<AjmSidebandDecOpusCodecInfo*>(output)->frames_per_packet =
m_frames_per_packet;
}
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
AVFrame* DecodePacket(const uint8_t* data, uint32_t size, AjmDecodeResult* result) {
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (packet == nullptr || frame == nullptr ||
av_new_packet(packet, static_cast<int>(size)) < 0) {
av_packet_free(&packet);
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return nullptr;
}
std::memcpy(packet->data, data, size);
int rc = avcodec_send_packet(m_context, packet);
av_packet_free(&packet);
if (rc >= 0) {
rc = avcodec_receive_frame(m_context, frame);
}
if (rc < 0) {
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result->internal_result = 0x1043;
return nullptr;
}
AVFrame* converted = AjmConvertFfmpegFrame(frame, m_sample_encoding, result);
av_frame_free(&frame);
return converted;
}
AVCodecContext* m_context = nullptr;
const uint32_t m_max_channels;
uint32_t m_frames_per_packet = 0;
};
}