From 7c8583d204d96e056359182735e07500e3a0e4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= <30907944+joaodiaslobo@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:53:20 +0100 Subject: [PATCH] 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 --- src/libs/ajm.cpp | 143 +----------------------- src/libs/ajm/opus_decoder.h | 216 ++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 141 deletions(-) create mode 100644 src/libs/ajm/opus_decoder.h diff --git a/src/libs/ajm.cpp b/src/libs/ajm.cpp index 5dd483916..88978b036 100644 --- a/src/libs/ajm.cpp +++ b/src/libs/ajm.cpp @@ -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(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(samples_to_write, gapless->current.skip_samples); - samples_to_write -= skip_samples; - if (gapless->HasSampleLimit()) { - samples_to_write = - std::min(samples_to_write, gapless->current.total_samples); - } - } - - const auto output_bytes = static_cast(samples_to_write) * - static_cast(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(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( - std::numeric_limits::max(), - static_cast(gapless->current.skipped_samples) + skipped_samples); - gapless->current.skipped_samples = static_cast(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(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 AjmCreateDecoder(uint32_t codec, uint64_t fla case static_cast(AjmCodec::DecM4aac): return std::make_unique(channels, 48000, encoding, flags); case static_cast(AjmCodec::DecOpus): - return std::make_unique(channels, 48000, encoding, flags); + return std::make_unique(channels, encoding); default: break; } diff --git a/src/libs/ajm/opus_decoder.h b/src/libs/ajm/opus_decoder.h new file mode 100644 index 000000000..4714ed4b3 --- /dev/null +++ b/src/libs/ajm/opus_decoder.h @@ -0,0 +1,216 @@ +#pragma once + +#include "libs/ajm/decoder.h" +#include "libs/ajm/ffmpeg_decoder_common.h" + +#include +#include + +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(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(params.channel_num)); + m_context->sample_rate = static_cast(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(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(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(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(samples) || + frame->ch_layout.nb_channels != static_cast(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(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(skip); + gapless->current.skipped_samples = static_cast( + std::min(std::numeric_limits::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(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(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; +}; + +}