blob: 0272b5e1cb02b5163d7c113825c522b5cc6b39b9 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/media/cpp/fidl.h>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "src/media/codec/codecs/sw/sbc/codec_adapter_sbc_decoder.h"
#include "src/media/codec/codecs/sw/sbc/codec_adapter_sbc_encoder.h"
#include "src/media/codec/codecs/test/test_codec_packets.h"
#include "src/media/codec/codecs/test/test_fake_codec_adapter_events.h"
namespace {
constexpr size_t kUnpaddedMsbcFrameBytes = 57;
constexpr size_t kScoPaddedMsbcFrameBytes = 58;
constexpr size_t kMsbcPcmBatchBytes = 240; // 15 blocks * 8 subbands * 2 bytes/sample
constexpr uint8_t kMsbcSyncword = 0xAD;
fuchsia::media::FormatDetails MakeValidSbcDecoderFormatDetails() {
SbcCodecInfo codec_info = {
.sampling_frequency = kSbcSamplingFrequency44100Hz,
.channel_mode = kSbcChannelModeJointStereo,
.block_length = 0b0001, // 16 blocks
.subbands = 0b01, // 8 subbands
.allocation_method = 0b01, // Loudness
.min_bitpool_value = 2,
.max_bitpool_value = 53,
};
std::vector<uint8_t> oob_bytes(sizeof(SbcCodecInfo));
std::memcpy(oob_bytes.data(), &codec_info, sizeof(SbcCodecInfo));
fuchsia::media::FormatDetails format_details;
format_details.set_format_details_version_ordinal(0);
format_details.set_mime_type("audio/sbc");
format_details.set_oob_bytes(std::move(oob_bytes));
return format_details;
}
fuchsia::media::FormatDetails MakeValidMsbcDecoderFormatDetails() {
// Matches bt-hfp CodecId::MSBC::oob_bytes(): 16 kHz, Mono, 16 blocks, 8 subbands, Loudness,
// bitpool 26.
SbcCodecInfo codec_info = {
.sampling_frequency = kSbcSamplingFrequency16000Hz,
.channel_mode = kSbcChannelModeMono,
.block_length = 0b0001, // 16 blocks
.subbands = 0b01, // 8 subbands
.allocation_method = 0b01, // Loudness
.min_bitpool_value = 26,
.max_bitpool_value = 26,
};
std::vector<uint8_t> oob_bytes(sizeof(SbcCodecInfo));
std::memcpy(oob_bytes.data(), &codec_info, sizeof(SbcCodecInfo));
fuchsia::media::FormatDetails format_details;
format_details.set_format_details_version_ordinal(0);
format_details.set_mime_type("audio/msbc");
format_details.set_oob_bytes(std::move(oob_bytes));
return format_details;
}
fuchsia::media::FormatDetails MakeValidMsbcEncoderFormatDetails() {
fuchsia::media::PcmFormat pcm;
pcm.pcm_mode = fuchsia::media::AudioPcmMode::LINEAR;
pcm.bits_per_sample = 16;
pcm.frames_per_second = 16000;
pcm.channel_map = {fuchsia::media::AudioChannelId::LF};
fuchsia::media::AudioUncompressedFormat uncompressed;
uncompressed.set_pcm(std::move(pcm));
fuchsia::media::AudioFormat audio;
audio.set_uncompressed(std::move(uncompressed));
fuchsia::media::DomainFormat domain;
domain.set_audio(std::move(audio));
fuchsia::media::EncoderSettings encoder_settings;
encoder_settings.set_msbc(fuchsia::media::MSbcEncoderSettings{});
fuchsia::media::FormatDetails format_details;
format_details.set_format_details_version_ordinal(0);
format_details.set_domain(std::move(domain));
format_details.set_encoder_settings(std::move(encoder_settings));
return format_details;
}
// Encodes one 15-block * 8-subband = 120-sample (240-byte) 16 kHz mono PCM frame into a 57-byte
// mSBC frame (starting with OI_SBC_MSBC_SYNCWORD = 0xAD).
std::vector<uint8_t> EncodeSingleMsbcFrame() {
std::mutex lock;
FakeCodecAdapterEvents events;
CodecAdapterSbcEncoder encoder(lock, &events);
auto format_details = MakeValidMsbcEncoderFormatDetails();
encoder.CoreCodecInit(format_details);
ZX_ASSERT(events.fail_codec_count() == 0u);
auto output_buffers = Buffers({512});
auto output_packets = Packets(1);
encoder.CoreCodecAddBuffer(kOutputPort, output_buffers.ptr(0));
output_packets.ptr(0)->SetIsNew(false);
encoder.CoreCodecRecycleOutputPacket(output_packets.ptr(0));
encoder.CoreCodecStartStream();
encoder.CoreCodecQueueInputFormatDetails(format_details);
events.WaitForOutputConstraintsChangeCount(1);
encoder.CoreCodecMidStreamOutputBufferReConfigFinish();
auto input_buffers = Buffers({kMsbcPcmBatchBytes});
std::memset(input_buffers.ptr(0)->base(), 0, kMsbcPcmBatchBytes);
auto input_packets = Packets(1);
CodecPacket* input_packet = input_packets.ptr(0);
input_packet->SetBuffer(input_buffers.ptr(0));
input_packet->SetStartOffset(0);
input_packet->SetValidLengthBytes(kMsbcPcmBatchBytes);
encoder.CoreCodecQueueInputPacket(input_packet);
encoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(1);
ZX_ASSERT(events.fail_codec_count() == 0u);
ZX_ASSERT(events.output_packet_count() == 1u);
CodecPacket* out_packet = output_packets.ptr(0);
ZX_ASSERT(out_packet->valid_length_bytes() == kUnpaddedMsbcFrameBytes);
const uint8_t* begin = out_packet->buffer()->base() + out_packet->start_offset();
std::vector<uint8_t> msbc_frame(begin, begin + out_packet->valid_length_bytes());
encoder.CoreCodecStopStream();
return msbc_frame;
}
fuchsia::media::FormatDetails MakeValidSbcEncoderFormatDetails() {
fuchsia::media::PcmFormat pcm;
pcm.pcm_mode = fuchsia::media::AudioPcmMode::LINEAR;
pcm.bits_per_sample = 16;
pcm.frames_per_second = 44100;
pcm.channel_map = {fuchsia::media::AudioChannelId::LF, fuchsia::media::AudioChannelId::RF};
fuchsia::media::AudioUncompressedFormat uncompressed;
uncompressed.set_pcm(std::move(pcm));
fuchsia::media::AudioFormat audio;
audio.set_uncompressed(std::move(uncompressed));
fuchsia::media::DomainFormat domain;
domain.set_audio(std::move(audio));
fuchsia::media::SbcEncoderSettings sbc_settings;
sbc_settings.sub_bands = static_cast<fuchsia::media::SbcSubBands>(SUB_BANDS_8);
sbc_settings.block_count = fuchsia::media::SbcBlockCount::BLOCK_COUNT_16;
sbc_settings.allocation = fuchsia::media::SbcAllocation::ALLOC_LOUDNESS;
sbc_settings.channel_mode = fuchsia::media::SbcChannelMode::JOINT_STEREO;
sbc_settings.bit_pool = 53;
fuchsia::media::EncoderSettings encoder_settings;
encoder_settings.set_sbc(sbc_settings);
fuchsia::media::FormatDetails format_details;
format_details.set_format_details_version_ordinal(0);
format_details.set_domain(std::move(domain));
format_details.set_encoder_settings(std::move(encoder_settings));
return format_details;
}
TEST(CodecAdapterSbcDecoderTest, EmptyStreamEndOfStreamAndPostStopOutputConstraints) {
std::mutex lock;
FakeCodecAdapterEvents events;
CodecAdapterSbcDecoder decoder(lock, &events);
auto format_details = MakeValidSbcDecoderFormatDetails();
decoder.CoreCodecInit(format_details);
ASSERT_EQ(events.fail_codec_count(), 0u);
// Stream 1: Queue EndOfStream immediately without any FormatDetails or Packet.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 1u);
decoder.CoreCodecStopStream();
// Stream 2: Queue FormatDetails so CreateContext caches min_output_buffer_size_, then stop
// stream (which resets context_ in CleanUpAfterStream()).
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputFormatDetails(format_details);
events.WaitForOutputConstraintsChangeCount(1);
decoder.CoreCodecMidStreamOutputBufferReConfigFinish();
EXPECT_EQ(events.fail_codec_count(), 0u);
decoder.CoreCodecStopStream();
// Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed even
// though context_ was reset by CleanUpAfterStream().
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = decoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
// (16 bits / 8) * SBC_MAX_SAMPLES_PER_FRAME (128) * SBC_MAX_CHANNELS (2) = 512 bytes.
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 512u);
// Stream 3: Queue EndOfStream without FormatDetails or Packet after a configured stream.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(2);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 2u);
EXPECT_EQ(events.output_packet_count(), 0u);
decoder.CoreCodecStopStream();
}
TEST(CodecAdapterSbcDecoderTest, MsbcEmptyStreamEndOfStreamDecodeAndPostStopOutputConstraints) {
std::vector<uint8_t> msbc_frame = EncodeSingleMsbcFrame();
ASSERT_EQ(msbc_frame.size(), kUnpaddedMsbcFrameBytes);
ASSERT_EQ(msbc_frame[0], kMsbcSyncword);
std::mutex lock;
FakeCodecAdapterEvents events;
CodecAdapterSbcDecoder decoder(lock, &events);
auto format_details = MakeValidMsbcDecoderFormatDetails();
decoder.CoreCodecInit(format_details);
ASSERT_EQ(events.fail_codec_count(), 0u);
// Stream 1: Queue EndOfStream immediately without any FormatDetails or Packet.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 1u);
decoder.CoreCodecStopStream();
// Stream 2: Queue "audio/msbc" FormatDetails, decode one 58-byte SCO-padded mSBC frame (57-byte
// mSBC frame + 1 zero padding byte), and verify OI_CODEC_SBC_DecoderConfigureMSbc() ran (plain
// SBC mode would reject syncword 0xAD with OI_CODEC_SBC_NO_SYNCWORD).
auto output_buffers = Buffers({512});
auto output_packets = Packets(1);
decoder.CoreCodecAddBuffer(kOutputPort, output_buffers.ptr(0));
output_packets.ptr(0)->SetIsNew(false);
decoder.CoreCodecRecycleOutputPacket(output_packets.ptr(0));
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputFormatDetails(format_details);
events.WaitForOutputConstraintsChangeCount(1);
decoder.CoreCodecMidStreamOutputBufferReConfigFinish();
EXPECT_EQ(events.fail_codec_count(), 0u);
auto input_buffers = Buffers({kScoPaddedMsbcFrameBytes});
std::memcpy(input_buffers.ptr(0)->base(), msbc_frame.data(), msbc_frame.size());
input_buffers.ptr(0)->base()[msbc_frame.size()] = 0; // 1-byte SCO padding
auto input_packets = Packets(1);
CodecPacket* input_packet = input_packets.ptr(0);
input_packet->SetBuffer(input_buffers.ptr(0));
input_packet->SetStartOffset(0);
input_packet->SetValidLengthBytes(kScoPaddedMsbcFrameBytes);
decoder.CoreCodecQueueInputPacket(input_packet);
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(2);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_packet_count(), 1u);
// 15 blocks * 8 subbands * 1 channel * 2 bytes/sample = 240 bytes of PCM output.
EXPECT_EQ(output_packets.ptr(0)->valid_length_bytes(), kMsbcPcmBatchBytes);
decoder.CoreCodecRecycleOutputPacket(output_packets.ptr(0));
decoder.CoreCodecStopStream();
// Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed even
// though context_ was reset by CleanUpAfterStream().
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = decoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
// (16 bits / 8) * SBC_MAX_SAMPLES_PER_FRAME (128) * SBC_MAX_CHANNELS (2) = 512 bytes.
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 512u);
// Stream 3: Queue EndOfStream without FormatDetails or Packet after a configured stream.
decoder.CoreCodecStartStream();
decoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(3);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 3u);
EXPECT_EQ(events.output_packet_count(), 1u);
decoder.CoreCodecStopStream();
}
TEST(CodecAdapterSbcEncoderTest, EmptyStreamEndOfStreamAndPostStopOutputConstraints) {
std::mutex lock;
FakeCodecAdapterEvents events;
CodecAdapterSbcEncoder encoder(lock, &events);
auto format_details = MakeValidSbcEncoderFormatDetails();
encoder.CoreCodecInit(format_details);
ASSERT_EQ(events.fail_codec_count(), 0u);
// Stream 1: Queue EndOfStream immediately without any FormatDetails or Packet.
encoder.CoreCodecStartStream();
encoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 1u);
encoder.CoreCodecStopStream();
// Stream 2: Queue FormatDetails and a partial input packet (16 bytes < 512-byte PCM batch size)
// so chunk_input_stream_ buffers partial bytes in scratch_block_, then stop stream without EOS.
encoder.CoreCodecStartStream();
encoder.CoreCodecQueueInputFormatDetails(format_details);
events.WaitForOutputConstraintsChangeCount(1);
encoder.CoreCodecMidStreamOutputBufferReConfigFinish();
auto input_buffers = Buffers({4096});
auto input_packets = Packets(1);
CodecPacket* input_packet = input_packets.ptr(0);
input_packet->SetBuffer(input_buffers.ptr(0));
input_packet->SetStartOffset(0);
input_packet->SetValidLengthBytes(16); // Partial PCM batch (< 512 bytes)
encoder.CoreCodecQueueInputPacket(input_packet);
events.WaitForInputPacketDoneCount(1);
EXPECT_EQ(events.fail_codec_count(), 0u);
encoder.CoreCodecStopStream();
// Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed using
// cached min_output_buffer_size_.
fuchsia::media::StreamBufferConstraints stream_constraints;
fuchsia::media::StreamBufferPartialSettings partial_settings;
auto constraints = encoder.CoreCodecGetBufferCollectionConstraints2(
kOutputPort, stream_constraints, partial_settings);
ASSERT_TRUE(constraints.buffer_memory_constraints().has_value());
// For 8 subbands, 16 blocks, JOINT_STEREO, bitpool 53: 4 + (8 * 2 / 2) + ceil((8 + 16 * 53) / 8)
// = 12 + 107 = 119 bytes.
EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 119u);
// Stream 3: Queue EndOfStream without FormatDetails or Packet; must not flush stale partial
// bytes from Stream 2 or dereference null context_.
encoder.CoreCodecStartStream();
encoder.CoreCodecQueueInputEndOfStream();
events.WaitForOutputEosCount(2);
EXPECT_EQ(events.fail_codec_count(), 0u);
EXPECT_EQ(events.output_eos_count(), 2u);
EXPECT_EQ(events.output_packet_count(), 0u);
encoder.CoreCodecStopStream();
}
} // namespace