| // 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 <cstddef> | |
| #include <cstdint> | |
| #include <mutex> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <gtest/gtest.h> | |
| #include "src/media/codec/codecs/sw/lc3/codec_adapter_lc3_decoder.h" | |
| #include "src/media/codec/codecs/sw/lc3/codec_adapter_lc3_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 { | |
| class TestCodecAdapterLc3Decoder : public CodecAdapterLc3Decoder { | |
| public: | |
| TestCodecAdapterLc3Decoder(std::mutex& lock, CodecAdapterEvents* events) | |
| : CodecAdapterLc3Decoder(lock, events) {} | |
| using CodecAdapterLc3Decoder::CleanUpAfterStream; | |
| using CodecAdapterLc3Decoder::InputChunkSize; | |
| using CodecAdapterLc3Decoder::InputLoopStatus; | |
| using CodecAdapterLc3Decoder::MinOutputBufferSize; | |
| using CodecAdapterLc3Decoder::OutputFormatDetails; | |
| using CodecAdapterLc3Decoder::ProcessFormatDetails; | |
| using CodecAdapterLc3Decoder::ProcessInputChunkData; | |
| }; | |
| class TestCodecAdapterLc3Encoder : public CodecAdapterLc3Encoder { | |
| public: | |
| TestCodecAdapterLc3Encoder(std::mutex& lock, CodecAdapterEvents* events) | |
| : CodecAdapterLc3Encoder(lock, events) {} | |
| using CodecAdapterLc3Encoder::CleanUpAfterStream; | |
| using CodecAdapterLc3Encoder::CreateTimestampExtrapolator; | |
| using CodecAdapterLc3Encoder::InputChunkSize; | |
| using CodecAdapterLc3Encoder::InputLoopStatus; | |
| using CodecAdapterLc3Encoder::MinOutputBufferSize; | |
| using CodecAdapterLc3Encoder::ProcessFormatDetails; | |
| using CodecAdapterLc3Encoder::ProcessInputChunkData; | |
| }; | |
| fuchsia::media::FormatDetails MakeLc3FormatDetails(std::vector<uint8_t> oob_bytes) { | |
| fuchsia::media::FormatDetails format_details; | |
| format_details.set_mime_type(kLc3MimeType); | |
| format_details.set_oob_bytes(std::move(oob_bytes)); | |
| return format_details; | |
| } | |
| fuchsia::media::FormatDetails MakeValidLc3DecoderFormatDetails() { | |
| // Valid 16-byte LTV configuration: | |
| // Sampling_Frequency: 48 kHz (0x08) | |
| // Frame_Duration: 10 ms (0x01) | |
| // Audio_Channel_Allocation: LF (0x00000001) | |
| // Octets_Per_Codec_Frame: 40 (0x0028) | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency: 48 kHz | |
| 0x02, 0x02, 0x01, // Frame_Duration: 10 ms | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation: LF | |
| 0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame: 40 | |
| }; | |
| return MakeLc3FormatDetails(std::move(oob_bytes)); | |
| } | |
| fuchsia::media::FormatDetails MakeValidLc3EncoderFormatDetails() { | |
| fuchsia::media::PcmFormat pcm; | |
| pcm.pcm_mode = fuchsia::media::AudioPcmMode::LINEAR; | |
| pcm.bits_per_sample = 16; | |
| pcm.frames_per_second = 48000; | |
| 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::Lc3EncoderSettings lc3_settings; | |
| lc3_settings.set_nbytes(40); | |
| lc3_settings.set_frame_duration(fuchsia::media::Lc3FrameDuration::D10_MS); | |
| fuchsia::media::EncoderSettings encoder_settings; | |
| encoder_settings.set_lc3(std::move(lc3_settings)); | |
| fuchsia::media::FormatDetails format_details; | |
| format_details.set_domain(std::move(domain)); | |
| format_details.set_encoder_settings(std::move(encoder_settings)); | |
| return format_details; | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, ValidOobBytesSucceeds) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| auto status = decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| EXPECT_EQ(decoder.InputChunkSize(), 40u); | |
| EXPECT_EQ(decoder.MinOutputBufferSize(), 960u); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, MissingMimeTypeOrOobBytesRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| fuchsia::media::FormatDetails missing_mime; | |
| missing_mime.set_oob_bytes(std::vector<uint8_t>(16, 0)); | |
| EXPECT_EQ(decoder.ProcessFormatDetails(missing_mime), | |
| TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| fuchsia::media::FormatDetails short_oob; | |
| short_oob.set_mime_type(kLc3MimeType); | |
| short_oob.set_oob_bytes(std::vector<uint8_t>(15, 0)); | |
| EXPECT_EQ(decoder.ProcessFormatDetails(short_oob), TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 2u); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, OutOfRangeOctetsPerCodecFrameRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Octets_Per_Codec_Frame = 19 (< kMinExternalByteCount = 20). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x13 // Octets_Per_Codec_Frame = 19 | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Octets_Per_Codec_Frame"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, ValidOobBytesWithUnknownLtvParamSucceeds) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Valid 16-byte LTV configuration plus optional/unknown LTV parameter (e.g. | |
| // Codec_Frame_Blocks_Per_SDU = 0x05). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame | |
| 0x02, 0x05, 0x01 // Optional Codec_Frame_Blocks_Per_SDU | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, TruncatedAudioChannelAllocationRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Audio_Channel_Allocation with LTV length 2 (1 byte value instead of 4). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x02, 0x03, 0x00, // Truncated Audio_Channel_Allocation (len=2) | |
| 0x03, 0x04, 0x00, 0x28, // Valid Octets_Per_Codec_Frame | |
| 0x04, 0x99, 0x00, 0x00, 0x00 // Unknown LTV param to pad to >= 16B | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Audio_Channel_Allocation"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, TruncatedOctetsPerCodecFrameRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Octets_Per_Codec_Frame with LTV length 2 (1 byte value instead of 2). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x02, 0x04, 0x28, // Truncated Octets_Per_Codec_Frame (len=2) | |
| 0x01, 0x99 // Unknown param to pad to 16B | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Octets_Per_Codec_Frame"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, TruncatedSamplingFrequencyRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Sampling_Frequency with LTV length 1 (0 value bytes). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x01, 0x01, // Truncated Sampling_Frequency (len=1) | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame | |
| 0x01, 0x99 // Padding to >= 16B | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Sampling_Frequency"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, TruncatedFrameDurationRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Frame_Duration with LTV length 1 (0 value bytes). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x01, 0x02, // Truncated Frame_Duration (len=1) | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame | |
| 0x01, 0x99 // Padding to >= 16B | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Frame_Duration"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, ZeroLengthLtvEntryAtEndRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Valid LTVs followed by a trailing 0x00 length byte at the very end of oob_bytes. | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x28, // Octets_Per_Codec_Frame | |
| 0x00 // len == 0 at final byte | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("invalid LTV length"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, LtvLengthExceedsRemainingBufferRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Final LTV claims length 10 when only 3 bytes remain. | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x0a, 0x04, 0x00, 0x28 // len=10 exceeds buffer | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("invalid LTV length"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, DuplicateLtvParameterKeyRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Duplicate Sampling_Frequency (key 0x01) entries. | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency #1 | |
| 0x02, 0x01, 0x08, // Sampling_Frequency #2 (duplicate) | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("duplicate LTV parameter key"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, ZeroAudioChannelAllocationRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Audio_Channel_Allocation = 0x00000000 (0 channels). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x00, // Audio_Channel_Allocation = 0 | |
| 0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Audio_Channel_Allocation"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, IncompleteRequiredLtvParamsRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // oob_bytes >= 16 bytes (padded with unknown LTV key 0x05), missing Octets_Per_Codec_Frame | |
| // (0x04). | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x08, // Sampling_Frequency | |
| 0x02, 0x02, 0x01, // Frame_Duration | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation | |
| 0x04, 0x05, 0x01, 0x00, 0x00 // Unknown param to pad to >= 16B | |
| }; | |
| auto status = decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("incomplete"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, MidstreamFormatChangeRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()), | |
| TestCodecAdapterLc3Decoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| // A second ProcessFormatDetails call on an active stream should fail the codec. | |
| EXPECT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()), | |
| TestCodecAdapterLc3Decoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Midstream input format change"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, ProcessInputChunkDataBufferSizeValidation) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()), | |
| TestCodecAdapterLc3Decoder::kOk); | |
| const size_t input_size = decoder.InputChunkSize(); | |
| const size_t min_output_size = decoder.MinOutputBufferSize(); | |
| std::vector<uint8_t> input(input_size, 0); | |
| std::vector<uint8_t> output(min_output_size, 0); | |
| // Mismatched input_data_size should return -1. | |
| EXPECT_EQ( | |
| decoder.ProcessInputChunkData(input.data(), input_size - 1, output.data(), output.size()), | |
| -1); | |
| // Undersized output_buffer_size should return -1. | |
| EXPECT_EQ( | |
| decoder.ProcessInputChunkData(input.data(), input_size, output.data(), min_output_size - 1), | |
| -1); | |
| // Zeroed input triggers liblc3 Packet Loss Concealment (return code 1) and succeeds. | |
| EXPECT_EQ(decoder.ProcessInputChunkData(input.data(), input_size, output.data(), output.size()), | |
| static_cast<int>(min_output_size)); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, ValidFormatDetailsSucceeds) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto status = encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| EXPECT_EQ(encoder.InputChunkSize(), 960u); | |
| EXPECT_EQ(encoder.MinOutputBufferSize(), 40u); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, MissingDomainOrEncoderSettingsRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| fuchsia::media::FormatDetails missing_domain = MakeValidLc3EncoderFormatDetails(); | |
| missing_domain.clear_domain(); | |
| EXPECT_EQ(encoder.ProcessFormatDetails(missing_domain), | |
| TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| fuchsia::media::FormatDetails missing_settings = MakeValidLc3EncoderFormatDetails(); | |
| missing_settings.clear_encoder_settings(); | |
| EXPECT_EQ(encoder.ProcessFormatDetails(missing_settings), | |
| TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 2u); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, OutOfRangeEncoderNbytesRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_encoder_settings()->lc3().set_nbytes(401); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Byte count"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, EmptyChannelMapRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_domain()->audio().uncompressed().pcm().channel_map.clear(); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Unsupported channel count"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, ExcessiveChannelMapRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_domain()->audio().uncompressed().pcm().channel_map.assign( | |
| kMaxChannelCount + 1, fuchsia::media::AudioChannelId::LF); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Unsupported channel count"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, MissingNbytesRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_encoder_settings()->lc3().clear_nbytes(); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Byte count"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, MissingFrameDurationRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_encoder_settings()->lc3().clear_frame_duration(); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("frame duration"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, InvalidFrameDurationRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_encoder_settings()->lc3().set_frame_duration( | |
| static_cast<fuchsia::media::Lc3FrameDuration>(99)); | |
| auto status = encoder.ProcessFormatDetails(format_details); | |
| EXPECT_EQ(status, TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("frame duration"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, MidstreamFormatChangeRejected) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| ASSERT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()), | |
| TestCodecAdapterLc3Encoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| EXPECT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()), | |
| TestCodecAdapterLc3Encoder::kShouldTerminate); | |
| EXPECT_EQ(events.fail_codec_count(), 1u); | |
| EXPECT_NE(events.last_fail_message().find("Midstream input format change"), std::string::npos) | |
| << "Actual message: " << events.last_fail_message(); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, ProcessInputChunkDataValidationAndUnalignedInput) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_domain()->audio().uncompressed().pcm().bits_per_sample = 24; | |
| ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk); | |
| const size_t input_size = encoder.InputChunkSize(); | |
| const size_t min_output_size = encoder.MinOutputBufferSize(); | |
| // Allocate extra padding so input_storage.data() + 1 is guaranteed misaligned for int32_t. | |
| std::vector<uint8_t> input_storage(input_size + 4, 0); | |
| const uint8_t* unaligned_input = input_storage.data() + 1; | |
| std::vector<uint8_t> output(min_output_size, 0); | |
| // Mismatched input_data_size should return -1. | |
| EXPECT_EQ( | |
| encoder.ProcessInputChunkData(unaligned_input, input_size - 1, output.data(), output.size()), | |
| -1); | |
| // Undersized output_buffer_size should return -1. | |
| EXPECT_EQ(encoder.ProcessInputChunkData(unaligned_input, input_size, output.data(), | |
| min_output_size - 1), | |
| -1); | |
| // Valid unaligned input chunk should succeed via aligned scratch buffer copy. | |
| EXPECT_EQ( | |
| encoder.ProcessInputChunkData(unaligned_input, input_size, output.data(), output.size()), | |
| static_cast<int>(min_output_size)); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, SamplingFrequency44100HzPreservedInOutputFormat) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| // Sampling_Frequency 0x07 (44.1 kHz), Frame_Duration 0x01 (10 ms), 1 ch, 40 bytes. | |
| std::vector<uint8_t> oob_bytes = { | |
| 0x02, 0x01, 0x07, // Sampling_Frequency: 44.1 kHz | |
| 0x02, 0x02, 0x01, // Frame_Duration: 10 ms | |
| 0x05, 0x03, 0x00, 0x00, 0x00, 0x01, // Audio_Channel_Allocation: LF | |
| 0x03, 0x04, 0x00, 0x28 // Octets_Per_Codec_Frame: 40 | |
| }; | |
| ASSERT_EQ(decoder.ProcessFormatDetails(MakeLc3FormatDetails(std::move(oob_bytes))), | |
| TestCodecAdapterLc3Decoder::kOk); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| // liblc3 uses 480 samples per 10ms frame for 44.1 kHz, so MinOutputBufferSize is 480 * 2 = 960 B. | |
| EXPECT_EQ(decoder.MinOutputBufferSize(), 960u); | |
| // Output format details must report the actual PCM sample rate (44100 Hz), not coerced 48000 Hz. | |
| auto [out_format, min_size] = decoder.OutputFormatDetails(); | |
| EXPECT_EQ(out_format.domain().audio().uncompressed().pcm().frames_per_second, 44100u); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, TimestampExtrapolatorUsesTrueSampleRateFor44100Hz) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_domain()->audio().uncompressed().pcm().frames_per_second = 44100; | |
| format_details.set_timebase(1'000'000'000ull); // 1 second in ns | |
| ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk); | |
| // For 44.1 kHz 10ms nominal frame, liblc3 encodes 480 samples per frame (InputChunkSize = 960 B). | |
| // At 44,100 samples/sec, 480 samples lasts 480 / 44100 s = 10,884,353 ns (~10.884 ms). | |
| const size_t chunk_size = encoder.InputChunkSize(); | |
| EXPECT_EQ(chunk_size, 960u); | |
| auto extrapolator = encoder.CreateTimestampExtrapolator(format_details); | |
| extrapolator.Inform(0, 0); | |
| auto extrapolated = extrapolator.Extrapolate(chunk_size); | |
| ASSERT_TRUE(extrapolated.has_value()); | |
| EXPECT_EQ(*extrapolated, 480ull * 1'000'000'000ull / 44100ull); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, TimestampExtrapolatorUsesFourBytesPerSampleFor24BitAudio) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| auto format_details = MakeValidLc3EncoderFormatDetails(); | |
| format_details.mutable_domain()->audio().uncompressed().pcm().bits_per_sample = 24; | |
| format_details.set_timebase(1'000'000'000ull); // 1 second in ns | |
| ASSERT_EQ(encoder.ProcessFormatDetails(format_details), TestCodecAdapterLc3Encoder::kOk); | |
| // For 48 kHz 10ms frame, liblc3 encodes 480 samples per frame. Since 24-bit PCM | |
| // (LC3_PCM_FORMAT_S24) uses 4-byte (int32_t) words in memory, InputChunkSize is 480 * 4 = 1920 B. | |
| const size_t chunk_size = encoder.InputChunkSize(); | |
| EXPECT_EQ(chunk_size, 1920u); | |
| // One 1920-byte chunk at 48 kHz (480 samples) must advance the timestamp by 10 ms (10,000,000 | |
| // ns), not 13.333 ms (which would happen if bits_per_sample / 8 == 3 bytes/sample were used). | |
| auto extrapolator = encoder.CreateTimestampExtrapolator(format_details); | |
| extrapolator.Inform(0, 0); | |
| auto extrapolated = extrapolator.Extrapolate(chunk_size); | |
| ASSERT_TRUE(extrapolated.has_value()); | |
| EXPECT_EQ(*extrapolated, 10'000'000ull); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, OutputBufferConstraintsAvailableAfterCleanUpAfterStream) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| ASSERT_EQ(decoder.ProcessFormatDetails(MakeValidLc3DecoderFormatDetails()), | |
| TestCodecAdapterLc3Decoder::kOk); | |
| // Stop/clean up the stream (which resets codec_params_ and chunk_input_stream_) and verify | |
| // CoreCodecGetBufferCollectionConstraints2(kOutputPort) still succeeds using cached size. | |
| decoder.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()); | |
| EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 960u); | |
| } | |
| TEST(CodecAdapterLc3EncoderTest, OutputBufferConstraintsAvailableAfterCleanUpAfterStream) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Encoder encoder(lock, &events); | |
| ASSERT_EQ(encoder.ProcessFormatDetails(MakeValidLc3EncoderFormatDetails()), | |
| TestCodecAdapterLc3Encoder::kOk); | |
| encoder.CleanUpAfterStream(); | |
| 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()); | |
| EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 40u); | |
| } | |
| TEST(CodecAdapterLc3DecoderTest, EmptyStreamEndOfStreamAndStaleChunkInputStreamCleanedUp) { | |
| std::mutex lock; | |
| FakeCodecAdapterEvents events; | |
| TestCodecAdapterLc3Decoder decoder(lock, &events); | |
| auto format_details = MakeValidLc3DecoderFormatDetails(); | |
| format_details.set_format_details_version_ordinal(0); | |
| 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 and a partial input packet (10 bytes < 40-byte frame size) | |
| // so chunk_input_stream_ buffers partial bytes in scratch_block_, then stop stream without EOS. | |
| decoder.CoreCodecStartStream(); | |
| decoder.CoreCodecQueueInputFormatDetails(format_details); | |
| events.WaitForOutputConstraintsChangeCount(1); | |
| decoder.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(10); // Partial frame (< 40 bytes) | |
| decoder.CoreCodecQueueInputPacket(input_packet); | |
| events.WaitForInputPacketDoneCount(1); | |
| EXPECT_EQ(events.fail_codec_count(), 0u); | |
| decoder.CoreCodecStopStream(); | |
| // Between streams: CoreCodecGetBufferCollectionConstraints2(kOutputPort) must succeed. | |
| 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()); | |
| EXPECT_EQ(constraints.buffer_memory_constraints()->min_size_bytes(), 960u); | |
| // Stream 3: Queue EndOfStream without FormatDetails or Packet; must not flush stale partial | |
| // bytes from Stream 2 or dereference null codec_params_. | |
| 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(); | |
| } | |
| } // namespace |