From 2c1bcf2cb4a9e19a337e52fd576242e04168d5e9 Mon Sep 17 00:00:00 2001 From: "minyue@webrtc.org" Date: Tue, 17 Feb 2015 10:17:09 +0000 Subject: [PATCH] Adding decoded_fec_rate to NetEQ Network Statistics. A statistic is introduced to reflect the actual benefits of Opus FEC. It shows what percentage of samples in the rendered audio come from FEC data. BUG=3867 R=henrik.lundin@webrtc.org Review URL: https://webrtc-codereview.appspot.com/34969004 Cr-Commit-Position: refs/heads/master@{#8384} git-svn-id: http://webrtc.googlecode.com/svn/trunk@8384 4adac7df-926f-26a2-2b94-8c16560cd09d --- .../audio_coding/neteq/interface/neteq.h | 2 + .../neteq/mock/mock_external_decoder_pcm16b.h | 2 - .../neteq/neteq_external_decoder_unittest.cc | 408 +++++++++--------- .../modules/audio_coding/neteq/neteq_impl.cc | 12 +- .../neteq/neteq_network_stats_unittest.cc | 258 +++++++++++ .../audio_coding/neteq/neteq_tests.gypi | 2 + .../audio_coding/neteq/neteq_unittest.cc | 35 +- .../neteq/statistics_calculator.cc | 12 +- .../neteq/statistics_calculator.h | 4 + .../tools/neteq_external_decoder_test.cc | 64 +++ .../neteq/tools/neteq_external_decoder_test.h | 61 +++ webrtc/modules/modules.gyp | 2 + 12 files changed, 636 insertions(+), 226 deletions(-) create mode 100644 webrtc/modules/audio_coding/neteq/neteq_network_stats_unittest.cc create mode 100644 webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.cc create mode 100644 webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h diff --git a/webrtc/modules/audio_coding/neteq/interface/neteq.h b/webrtc/modules/audio_coding/neteq/interface/neteq.h index 2c0dbdc61..484721397 100644 --- a/webrtc/modules/audio_coding/neteq/interface/neteq.h +++ b/webrtc/modules/audio_coding/neteq/interface/neteq.h @@ -38,6 +38,8 @@ struct NetEqNetworkStatistics { // expansion (in Q14). uint16_t accelerate_rate; // Fraction of data removed through acceleration // (in Q14). + uint16_t secondary_decoded_rate; // Fraction of data coming from secondary + // decoding (in Q14). int32_t clockdrift_ppm; // Average clock-drift in parts-per-million // (positive or negative). int added_zero_samples; // Number of zero samples added in "off" mode. diff --git a/webrtc/modules/audio_coding/neteq/mock/mock_external_decoder_pcm16b.h b/webrtc/modules/audio_coding/neteq/mock/mock_external_decoder_pcm16b.h index 974976921..19f069ae3 100644 --- a/webrtc/modules/audio_coding/neteq/mock/mock_external_decoder_pcm16b.h +++ b/webrtc/modules/audio_coding/neteq/mock/mock_external_decoder_pcm16b.h @@ -80,8 +80,6 @@ class MockExternalPcm16B : public ExternalPcm16B { uint32_t arrival_timestamp)); MOCK_METHOD0(ErrorCode, int()); - MOCK_CONST_METHOD0(codec_type, - NetEqDecoder()); private: ExternalPcm16B real_; diff --git a/webrtc/modules/audio_coding/neteq/neteq_external_decoder_unittest.cc b/webrtc/modules/audio_coding/neteq/neteq_external_decoder_unittest.cc index 579c0badd..a3dd2717f 100644 --- a/webrtc/modules/audio_coding/neteq/neteq_external_decoder_unittest.cc +++ b/webrtc/modules/audio_coding/neteq/neteq_external_decoder_unittest.cc @@ -10,221 +10,237 @@ // Test to verify correct operation for externally created decoders. -#include -#include - #include "testing/gmock/include/gmock/gmock.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "webrtc/modules/audio_coding/neteq/interface/neteq.h" #include "webrtc/modules/audio_coding/neteq/mock/mock_external_decoder_pcm16b.h" #include "webrtc/modules/audio_coding/neteq/tools/input_audio_file.h" +#include "webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h" #include "webrtc/modules/audio_coding/neteq/tools/rtp_generator.h" #include "webrtc/system_wrappers/interface/scoped_ptr.h" #include "webrtc/test/testsupport/fileutils.h" -#include "webrtc/test/testsupport/gtest_disable.h" namespace webrtc { using ::testing::_; using ::testing::Return; -// This test encodes a few packets of PCM16b 32 kHz data and inserts it into two -// different NetEq instances. The first instance uses the internal version of -// the decoder object, while the second one uses an externally created decoder -// object (ExternalPcm16B wrapped in MockExternalPcm16B, both defined above). -// The test verifies that the output from both instances match. -class NetEqExternalDecoderTest : public ::testing::Test { +class NetEqExternalDecoderUnitTest : public test::NetEqExternalDecoderTest { protected: - static const int kTimeStepMs = 10; - static const int kMaxBlockSize = 480; // 10 ms @ 48 kHz. - static const uint8_t kPayloadType = 95; - static const int kSampleRateHz = 32000; + static const int kFrameSizeMs = 10; // Frame size of Pcm16B. - NetEqExternalDecoderTest() - : sample_rate_hz_(kSampleRateHz), - samples_per_ms_(sample_rate_hz_ / 1000), - frame_size_ms_(10), - frame_size_samples_(frame_size_ms_ * samples_per_ms_), - output_size_samples_(frame_size_ms_ * samples_per_ms_), - external_decoder_(new MockExternalPcm16B), + NetEqExternalDecoderUnitTest(NetEqDecoder codec, + MockExternalPcm16B* decoder) + : NetEqExternalDecoderTest(codec, decoder), + external_decoder_(decoder), + samples_per_ms_(CodecSampleRateHz(codec) / 1000), + frame_size_samples_(kFrameSizeMs * samples_per_ms_), rtp_generator_(new test::RtpGenerator(samples_per_ms_)), + input_(new int16_t[frame_size_samples_]), + // Payload should be no larger than input. + encoded_(new uint8_t[2 * frame_size_samples_]), payload_size_bytes_(0), last_send_time_(0), last_arrival_time_(0) { - config_.sample_rate_hz = sample_rate_hz_; - neteq_external_ = NetEq::Create(config_); - neteq_ = NetEq::Create(config_); - input_ = new int16_t[frame_size_samples_]; - encoded_ = new uint8_t[2 * frame_size_samples_]; - } - - ~NetEqExternalDecoderTest() { - delete neteq_external_; - delete neteq_; - // We will now delete the decoder ourselves, so expecting Die to be called. - EXPECT_CALL(*external_decoder_, Die()).Times(1); - delete [] input_; - delete [] encoded_; - } - - virtual void SetUp() { - const std::string file_name = - webrtc::test::ResourcePath("audio_coding/testfile32kHz", "pcm"); - input_file_.reset(new test::InputAudioFile(file_name)); - assert(sample_rate_hz_ == 32000); - NetEqDecoder decoder = kDecoderPCM16Bswb32kHz; + // Init() will trigger external_decoder_->Init(). EXPECT_CALL(*external_decoder_, Init()); // NetEq is not allowed to delete the external decoder (hence Times(0)). EXPECT_CALL(*external_decoder_, Die()).Times(0); - ASSERT_EQ(NetEq::kOK, - neteq_external_->RegisterExternalDecoder( - external_decoder_.get(), decoder, kPayloadType)); - ASSERT_EQ(NetEq::kOK, - neteq_->RegisterPayloadType(decoder, kPayloadType)); + Init(); + + const std::string file_name = + webrtc::test::ResourcePath("audio_coding/testfile32kHz", "pcm"); + input_file_.reset(new test::InputAudioFile(file_name)); } - virtual void TearDown() {} + virtual ~NetEqExternalDecoderUnitTest() { + delete [] input_; + delete [] encoded_; + // ~NetEqExternalDecoderTest() will delete |external_decoder_|, so expecting + // Die() to be called. + EXPECT_CALL(*external_decoder_, Die()).Times(1); + } - int GetNewPackets() { + // Method to draw kFrameSizeMs audio and verify the output. + // Use gTest methods. e.g. ASSERT_EQ() inside to trigger errors. + virtual void GetAndVerifyOutput() = 0; + + // Method to get the number of calls to the Decode() method of the external + // decoder. + virtual int NumExpectedDecodeCalls(int num_loops) = 0; + + // Method to generate packets and return the send time of the packet. + int GetNewPacket() { if (!input_file_->Read(frame_size_samples_, input_)) { return -1; } payload_size_bytes_ = WebRtcPcm16b_Encode(input_, frame_size_samples_, - encoded_); - if (frame_size_samples_ * 2 != payload_size_bytes_) { - return -1; - } + encoded_);; + int next_send_time = rtp_generator_->GetRtpHeader( kPayloadType, frame_size_samples_, &rtp_header_); return next_send_time; } - virtual void VerifyOutput(size_t num_samples) const { - for (size_t i = 0; i < num_samples; ++i) { - ASSERT_EQ(output_[i], output_external_[i]) << - "Diff in sample " << i << "."; - } - } + // Method to decide packet losses. + virtual bool Lost() { return false; } - virtual int GetArrivalTime(int send_time) { + // Method to calculate packet arrival time. + int GetArrivalTime(int send_time) { int arrival_time = last_arrival_time_ + (send_time - last_send_time_); last_send_time_ = send_time; last_arrival_time_ = arrival_time; return arrival_time; } - virtual bool Lost() { return false; } - - virtual void InsertPackets(int next_arrival_time) { - // Insert packet in regular instance. - ASSERT_EQ( - NetEq::kOK, - neteq_->InsertPacket( - rtp_header_, encoded_, payload_size_bytes_, next_arrival_time)); - // Insert packet in external decoder instance. - EXPECT_CALL(*external_decoder_, - IncomingPacket(_, - payload_size_bytes_, - rtp_header_.header.sequenceNumber, - rtp_header_.header.timestamp, - next_arrival_time)); - ASSERT_EQ( - NetEq::kOK, - neteq_external_->InsertPacket( - rtp_header_, encoded_, payload_size_bytes_, next_arrival_time)); - } - - virtual void GetOutputAudio() { - NetEqOutputType output_type; - // Get audio from regular instance. - int samples_per_channel; - int num_channels; - EXPECT_EQ(NetEq::kOK, - neteq_->GetAudio(kMaxBlockSize, - output_, - &samples_per_channel, - &num_channels, - &output_type)); - EXPECT_EQ(1, num_channels); - EXPECT_EQ(output_size_samples_, samples_per_channel); - // Get audio from external decoder instance. - ASSERT_EQ(NetEq::kOK, - neteq_external_->GetAudio(kMaxBlockSize, - output_external_, - &samples_per_channel, - &num_channels, - &output_type)); - EXPECT_EQ(1, num_channels); - EXPECT_EQ(output_size_samples_, samples_per_channel); - } - - virtual int NumExpectedDecodeCalls(int num_loops) const { return num_loops; } - void RunTest(int num_loops) { // Get next input packets (mono and multi-channel). - int next_send_time; - int next_arrival_time; + uint32_t next_send_time; + uint32_t next_arrival_time; do { - next_send_time = GetNewPackets(); - ASSERT_NE(-1, next_send_time); + next_send_time = GetNewPacket(); next_arrival_time = GetArrivalTime(next_send_time); } while (Lost()); // If lost, immediately read the next packet. EXPECT_CALL(*external_decoder_, Decode(_, payload_size_bytes_, _, _)) .Times(NumExpectedDecodeCalls(num_loops)); - int time_now = 0; + uint32_t time_now = 0; for (int k = 0; k < num_loops; ++k) { while (time_now >= next_arrival_time) { - InsertPackets(next_arrival_time); - + InsertPacket(rtp_header_, encoded_, payload_size_bytes_, + next_arrival_time); // Get next input packet. do { - next_send_time = GetNewPackets(); - ASSERT_NE(-1, next_send_time); + next_send_time = GetNewPacket(); next_arrival_time = GetArrivalTime(next_send_time); } while (Lost()); // If lost, immediately read the next packet. } - GetOutputAudio(); - std::ostringstream ss; ss << "Lap number " << k << "."; SCOPED_TRACE(ss.str()); // Print out the parameter values on failure. // Compare mono and multi-channel. - ASSERT_NO_FATAL_FAILURE(VerifyOutput(output_size_samples_)); + ASSERT_NO_FATAL_FAILURE(GetAndVerifyOutput()); - time_now += kTimeStepMs; + time_now += kOutputLengthMs; } } - NetEq::Config config_; - int sample_rate_hz_; - int samples_per_ms_; - const int frame_size_ms_; - size_t frame_size_samples_; - int output_size_samples_; - NetEq* neteq_external_; - NetEq* neteq_; + void InsertPacket(WebRtcRTPHeader rtp_header, const uint8_t* payload, + size_t payload_size_bytes, + uint32_t receive_timestamp) override { + EXPECT_CALL(*external_decoder_, + IncomingPacket(_, + payload_size_bytes, + rtp_header.header.sequenceNumber, + rtp_header.header.timestamp, + receive_timestamp)); + NetEqExternalDecoderTest::InsertPacket(rtp_header, payload, + payload_size_bytes, + receive_timestamp); + } + + MockExternalPcm16B* external_decoder() { return external_decoder_.get(); } + + void ResetRtpGenerator(test::RtpGenerator* rtp_generator) { + rtp_generator_.reset(rtp_generator); + } + + int samples_per_ms() const { return samples_per_ms_; } + private: scoped_ptr external_decoder_; + int samples_per_ms_; + size_t frame_size_samples_; scoped_ptr rtp_generator_; int16_t* input_; uint8_t* encoded_; - int16_t output_[kMaxBlockSize]; - int16_t output_external_[kMaxBlockSize]; - WebRtcRTPHeader rtp_header_; size_t payload_size_bytes_; - int last_send_time_; - int last_arrival_time_; + uint32_t last_send_time_; + uint32_t last_arrival_time_; scoped_ptr input_file_; + WebRtcRTPHeader rtp_header_; }; -TEST_F(NetEqExternalDecoderTest, RunTest) { +// This test encodes a few packets of PCM16b 32 kHz data and inserts it into two +// different NetEq instances. The first instance uses the internal version of +// the decoder object, while the second one uses an externally created decoder +// object (ExternalPcm16B wrapped in MockExternalPcm16B, both defined above). +// The test verifies that the output from both instances match. +class NetEqExternalVsInternalDecoderTest : public NetEqExternalDecoderUnitTest, + public ::testing::Test { + protected: + static const int kMaxBlockSize = 480; // 10 ms @ 48 kHz. + + NetEqExternalVsInternalDecoderTest() + : NetEqExternalDecoderUnitTest(kDecoderPCM16Bswb32kHz, + new MockExternalPcm16B), + sample_rate_hz_(CodecSampleRateHz(kDecoderPCM16Bswb32kHz)) { + NetEq::Config config; + config.sample_rate_hz = CodecSampleRateHz(kDecoderPCM16Bswb32kHz); + neteq_internal_.reset(NetEq::Create(config)); + } + + void SetUp() override { + ASSERT_EQ(NetEq::kOK, + neteq_internal_->RegisterPayloadType(kDecoderPCM16Bswb32kHz, + kPayloadType)); + } + + void GetAndVerifyOutput() override { + NetEqOutputType output_type; + int samples_per_channel; + int num_channels; + // Get audio from internal decoder instance. + EXPECT_EQ(NetEq::kOK, + neteq_internal_->GetAudio(kMaxBlockSize, + output_internal_, + &samples_per_channel, + &num_channels, + &output_type)); + EXPECT_EQ(1, num_channels); + EXPECT_EQ(kOutputLengthMs * sample_rate_hz_ / 1000, samples_per_channel); + + // Get audio from external decoder instance. + samples_per_channel = GetOutputAudio(kMaxBlockSize, output_, &output_type); + + for (int i = 0; i < samples_per_channel; ++i) { + ASSERT_EQ(output_[i], output_internal_[i]) << + "Diff in sample " << i << "."; + } + } + + void InsertPacket(WebRtcRTPHeader rtp_header, const uint8_t* payload, + size_t payload_size_bytes, + uint32_t receive_timestamp) override { + // Insert packet in internal decoder. + ASSERT_EQ( + NetEq::kOK, + neteq_internal_->InsertPacket( + rtp_header, payload, payload_size_bytes, receive_timestamp)); + + // Insert packet in external decoder instance. + NetEqExternalDecoderUnitTest::InsertPacket(rtp_header, payload, + payload_size_bytes, + receive_timestamp); + } + + int NumExpectedDecodeCalls(int num_loops) override { return num_loops; } + + private: + int sample_rate_hz_; + scoped_ptr neteq_internal_; + int16_t output_internal_[kMaxBlockSize]; + int16_t output_[kMaxBlockSize]; +}; + +TEST_F(NetEqExternalVsInternalDecoderTest, RunTest) { RunTest(100); // Run 100 laps @ 10 ms each in the test loop. } -class LargeTimestampJumpTest : public NetEqExternalDecoderTest { +class LargeTimestampJumpTest : public NetEqExternalDecoderUnitTest, + public ::testing::Test { protected: + static const int kMaxBlockSize = 480; // 10 ms @ 48 kHz. + enum TestStates { kInitialPhase, kNormalPhase, @@ -234,60 +250,11 @@ class LargeTimestampJumpTest : public NetEqExternalDecoderTest { }; LargeTimestampJumpTest() - : NetEqExternalDecoderTest(), test_state_(kInitialPhase) { - sample_rate_hz_ = 8000; - samples_per_ms_ = sample_rate_hz_ / 1000; - frame_size_samples_ = frame_size_ms_ * samples_per_ms_; - output_size_samples_ = frame_size_ms_ * samples_per_ms_; - EXPECT_CALL(*external_decoder_, Die()).Times(1); - external_decoder_.reset(new MockExternalPcm16B); - } - - void SetUp() OVERRIDE { - const std::string file_name = - webrtc::test::ResourcePath("audio_coding/testfile32kHz", "pcm"); - input_file_.reset(new test::InputAudioFile(file_name)); - assert(sample_rate_hz_ == 8000); - NetEqDecoder decoder = kDecoderPCM16B; - EXPECT_CALL(*external_decoder_, Init()); - EXPECT_CALL(*external_decoder_, HasDecodePlc()) + : NetEqExternalDecoderUnitTest(kDecoderPCM16B, + new MockExternalPcm16B), + test_state_(kInitialPhase) { + EXPECT_CALL(*external_decoder(), HasDecodePlc()) .WillRepeatedly(Return(false)); - // NetEq is not allowed to delete the external decoder (hence Times(0)). - EXPECT_CALL(*external_decoder_, Die()).Times(0); - ASSERT_EQ(NetEq::kOK, - neteq_external_->RegisterExternalDecoder( - external_decoder_.get(), decoder, kPayloadType)); - ASSERT_EQ(NetEq::kOK, neteq_->RegisterPayloadType(decoder, kPayloadType)); - } - - void InsertPackets(int next_arrival_time) OVERRIDE { - // Insert packet in external decoder instance. - EXPECT_CALL(*external_decoder_, - IncomingPacket(_, - payload_size_bytes_, - rtp_header_.header.sequenceNumber, - rtp_header_.header.timestamp, - next_arrival_time)); - ASSERT_EQ( - NetEq::kOK, - neteq_external_->InsertPacket( - rtp_header_, encoded_, payload_size_bytes_, next_arrival_time)); - } - - void GetOutputAudio() OVERRIDE { - NetEqOutputType output_type; - int samples_per_channel; - int num_channels; - // Get audio from external decoder instance. - ASSERT_EQ(NetEq::kOK, - neteq_external_->GetAudio(kMaxBlockSize, - output_external_, - &samples_per_channel, - &num_channels, - &output_type)); - EXPECT_EQ(1, num_channels); - EXPECT_EQ(output_size_samples_, samples_per_channel); - UpdateState(output_type); } virtual void UpdateState(NetEqOutputType output_type) { @@ -324,20 +291,26 @@ class LargeTimestampJumpTest : public NetEqExternalDecoderTest { } } - void VerifyOutput(size_t num_samples) const OVERRIDE { + void GetAndVerifyOutput() override { + int num_samples; + NetEqOutputType output_type; + num_samples = GetOutputAudio(kMaxBlockSize, output_, &output_type); + UpdateState(output_type); + if (test_state_ == kExpandPhase || test_state_ == kFadedExpandPhase) { // Don't verify the output in this phase of the test. return; } - for (size_t i = 0; i < num_samples; ++i) { - if (output_external_[i] != 0) + + for (int i = 0; i < num_samples; ++i) { + if (output_[i] != 0) return; } EXPECT_TRUE(false) << "Expected at least one non-zero sample in each output block."; } - int NumExpectedDecodeCalls(int num_loops) const OVERRIDE { + int NumExpectedDecodeCalls(int num_loops) override { // Some packets at the end of the stream won't be decoded. When the jump in // timestamp happens, NetEq will do Expand during one GetAudio call. In the // next call it will decode the packet after the jump, but the net result is @@ -349,6 +322,9 @@ class LargeTimestampJumpTest : public NetEqExternalDecoderTest { } TestStates test_state_; + + private: + int16_t output_[kMaxBlockSize]; }; TEST_F(LargeTimestampJumpTest, JumpLongerThanHalfRange) { @@ -365,11 +341,11 @@ TEST_F(LargeTimestampJumpTest, JumpLongerThanHalfRange) { static_cast(kJumpToTimestamp - kJumpFromTimestamp) > 0x7FFFFFFF, "jump should be larger than half range"); // Replace the default RTP generator with one that jumps in timestamp. - rtp_generator_.reset(new test::TimestampJumpRtpGenerator(samples_per_ms_, - kStartSeqeunceNumber, - kStartTimestamp, - kJumpFromTimestamp, - kJumpToTimestamp)); + ResetRtpGenerator(new test::TimestampJumpRtpGenerator(samples_per_ms(), + kStartSeqeunceNumber, + kStartTimestamp, + kJumpFromTimestamp, + kJumpToTimestamp)); RunTest(130); // Run 130 laps @ 10 ms each in the test loop. EXPECT_EQ(kRecovered, test_state_); @@ -389,11 +365,11 @@ TEST_F(LargeTimestampJumpTest, JumpLongerThanHalfRangeAndWrap) { static_cast(kJumpToTimestamp - kJumpFromTimestamp) > 0x7FFFFFFF, "jump should be larger than half range"); // Replace the default RTP generator with one that jumps in timestamp. - rtp_generator_.reset(new test::TimestampJumpRtpGenerator(samples_per_ms_, - kStartSeqeunceNumber, - kStartTimestamp, - kJumpFromTimestamp, - kJumpToTimestamp)); + ResetRtpGenerator(new test::TimestampJumpRtpGenerator(samples_per_ms(), + kStartSeqeunceNumber, + kStartTimestamp, + kJumpFromTimestamp, + kJumpToTimestamp)); RunTest(130); // Run 130 laps @ 10 ms each in the test loop. EXPECT_EQ(kRecovered, test_state_); @@ -428,7 +404,7 @@ class ShortTimestampJumpTest : public LargeTimestampJumpTest { } } - int NumExpectedDecodeCalls(int num_loops) const OVERRIDE { + int NumExpectedDecodeCalls(int num_loops) override { // Some packets won't be decoded because of the timestamp jump. return num_loops - 2; } @@ -448,11 +424,11 @@ TEST_F(ShortTimestampJumpTest, JumpShorterThanHalfRange) { static_cast(kJumpToTimestamp - kJumpFromTimestamp) < 0x7FFFFFFF, "jump should be smaller than half range"); // Replace the default RTP generator with one that jumps in timestamp. - rtp_generator_.reset(new test::TimestampJumpRtpGenerator(samples_per_ms_, - kStartSeqeunceNumber, - kStartTimestamp, - kJumpFromTimestamp, - kJumpToTimestamp)); + ResetRtpGenerator(new test::TimestampJumpRtpGenerator(samples_per_ms(), + kStartSeqeunceNumber, + kStartTimestamp, + kJumpFromTimestamp, + kJumpToTimestamp)); RunTest(130); // Run 130 laps @ 10 ms each in the test loop. EXPECT_EQ(kRecovered, test_state_); @@ -472,11 +448,11 @@ TEST_F(ShortTimestampJumpTest, JumpShorterThanHalfRangeAndWrap) { static_cast(kJumpToTimestamp - kJumpFromTimestamp) < 0x7FFFFFFF, "jump should be smaller than half range"); // Replace the default RTP generator with one that jumps in timestamp. - rtp_generator_.reset(new test::TimestampJumpRtpGenerator(samples_per_ms_, - kStartSeqeunceNumber, - kStartTimestamp, - kJumpFromTimestamp, - kJumpToTimestamp)); + ResetRtpGenerator(new test::TimestampJumpRtpGenerator(samples_per_ms(), + kStartSeqeunceNumber, + kStartTimestamp, + kJumpFromTimestamp, + kJumpToTimestamp)); RunTest(130); // Run 130 laps @ 10 ms each in the test loop. EXPECT_EQ(kRecovered, test_state_); diff --git a/webrtc/modules/audio_coding/neteq/neteq_impl.cc b/webrtc/modules/audio_coding/neteq/neteq_impl.cc index 7ce4238ef..bfbf4b3cd 100644 --- a/webrtc/modules/audio_coding/neteq/neteq_impl.cc +++ b/webrtc/modules/audio_coding/neteq/neteq_impl.cc @@ -1799,10 +1799,14 @@ int NetEqImpl::ExtractPackets(int required_samples, PacketList* packet_list) { if (packet->sync_packet) { packet_duration = decoder_frame_length_; } else { - packet_duration = packet->primary ? - decoder->PacketDuration(packet->payload, packet->payload_length) : - decoder->PacketDurationRedundant(packet->payload, - packet->payload_length); + if (packet->primary) { + packet_duration = decoder->PacketDuration(packet->payload, + packet->payload_length); + } else { + packet_duration = decoder-> + PacketDurationRedundant(packet->payload, packet->payload_length); + stats_.SecondaryDecodedSamples(packet_duration); + } } } else { LOG_FERR1(LS_WARNING, GetDecoder, diff --git a/webrtc/modules/audio_coding/neteq/neteq_network_stats_unittest.cc b/webrtc/modules/audio_coding/neteq/neteq_network_stats_unittest.cc new file mode 100644 index 000000000..c2ae4864d --- /dev/null +++ b/webrtc/modules/audio_coding/neteq/neteq_network_stats_unittest.cc @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "testing/gmock/include/gmock/gmock.h" +#include "webrtc/modules/audio_coding/neteq/audio_decoder_impl.h" +#include "webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h" +#include "webrtc/modules/audio_coding/neteq/tools/rtp_generator.h" +#include "webrtc/system_wrappers/interface/scoped_ptr.h" + +namespace webrtc { +namespace test { + +using ::testing::_; +using ::testing::SetArgPointee; +using ::testing::Return; + + +class MockAudioDecoderOpus : public AudioDecoderOpus { + public: + static const int kPacketDuration = 960; // 48 kHz * 20 ms + + explicit MockAudioDecoderOpus(int num_channels) + : AudioDecoderOpus(num_channels), + fec_enabled_(false) { + } + virtual ~MockAudioDecoderOpus() { Die(); } + MOCK_METHOD0(Die, void()); + + MOCK_METHOD0(Init, int()); + + // Override the following methods such that no actual payload is needed. + int Decode(const uint8_t* encoded, size_t encoded_len, int16_t* decoded, + SpeechType* speech_type) override { + *speech_type = kSpeech; + memset(decoded, 0, sizeof(int16_t) * kPacketDuration * channels_); + return kPacketDuration * channels_; + } + + int DecodeRedundant(const uint8_t* encoded, size_t encoded_len, + int16_t* decoded, SpeechType* speech_type) override { + return Decode(encoded, encoded_len, decoded, speech_type); + } + + int PacketDuration(const uint8_t* encoded, + size_t encoded_len) const override { + return kPacketDuration; + } + + int PacketDurationRedundant(const uint8_t* encoded, + size_t encoded_len) const override { + return kPacketDuration; + } + + bool PacketHasFec(const uint8_t* encoded, size_t encoded_len) const override { + return fec_enabled_; + } + + void set_fec_enabled(bool enable_fec) { fec_enabled_ = enable_fec; } + + bool fec_enabled() const { return fec_enabled_; } + + private: + bool fec_enabled_; +}; + +class NetEqNetworkStatsTest : public NetEqExternalDecoderTest { + public: + static const int kPayloadSizeByte = 30; + static const int kFrameSizeMs = 20; // frame size of Opus + static const int kMaxOutputSize = 960; // 10 ms * 48 kHz * 2 channels. + +enum logic { + IGNORE, + EQUAL, + SMALLER_THAN, + LARGER_THAN, +}; + +struct NetEqNetworkStatsCheck { + logic current_buffer_size_ms; + logic preferred_buffer_size_ms; + logic jitter_peaks_found; + logic packet_loss_rate; + logic packet_discard_rate; + logic expand_rate; + logic preemptive_rate; + logic accelerate_rate; + logic secondary_decoded_rate; + logic clockdrift_ppm; + logic added_zero_samples; + NetEqNetworkStatistics stats_ref; +}; + + NetEqNetworkStatsTest(NetEqDecoder codec, + MockAudioDecoderOpus* decoder) + : NetEqExternalDecoderTest(codec, decoder), + external_decoder_(decoder), + samples_per_ms_(CodecSampleRateHz(codec) / 1000), + frame_size_samples_(kFrameSizeMs * samples_per_ms_), + rtp_generator_(new test::RtpGenerator(samples_per_ms_)), + last_lost_time_(0), + packet_loss_interval_(0xffffffff) { + Init(); + } + + bool Lost(uint32_t send_time) { + if (send_time - last_lost_time_ >= packet_loss_interval_) { + last_lost_time_ = send_time; + return true; + } + return false; + } + + void SetPacketLossRate(double loss_rate) { + packet_loss_interval_ = (loss_rate >= 1e-3 ? + static_cast(kFrameSizeMs) / loss_rate : 0xffffffff); + } + + // |stats_ref| + // expects.x = -1, do not care + // expects.x = 0, 'x' in current stats should equal 'x' in |stats_ref| + // expects.x = 1, 'x' in current stats should < 'x' in |stats_ref| + // expects.x = 2, 'x' in current stats should > 'x' in |stats_ref| + void CheckNetworkStatistics(NetEqNetworkStatsCheck expects) { + NetEqNetworkStatistics stats; + neteq()->NetworkStatistics(&stats); + +#define CHECK_NETEQ_NETWORK_STATS(x)\ + switch (expects.x) {\ + case EQUAL:\ + EXPECT_EQ(stats.x, expects.stats_ref.x);\ + break;\ + case SMALLER_THAN:\ + EXPECT_LT(stats.x, expects.stats_ref.x);\ + break;\ + case LARGER_THAN:\ + EXPECT_GT(stats.x, expects.stats_ref.x);\ + break;\ + default:\ + break;\ + } + + CHECK_NETEQ_NETWORK_STATS(current_buffer_size_ms); + CHECK_NETEQ_NETWORK_STATS(preferred_buffer_size_ms); + CHECK_NETEQ_NETWORK_STATS(jitter_peaks_found); + CHECK_NETEQ_NETWORK_STATS(packet_loss_rate); + CHECK_NETEQ_NETWORK_STATS(packet_discard_rate); + CHECK_NETEQ_NETWORK_STATS(expand_rate); + CHECK_NETEQ_NETWORK_STATS(preemptive_rate); + CHECK_NETEQ_NETWORK_STATS(accelerate_rate); + CHECK_NETEQ_NETWORK_STATS(secondary_decoded_rate); + CHECK_NETEQ_NETWORK_STATS(clockdrift_ppm); + CHECK_NETEQ_NETWORK_STATS(added_zero_samples); + +#undef CHECK_NETEQ_NETWORK_STATS + } + + void RunTest(int num_loops, NetEqNetworkStatsCheck expects) { + NetEqOutputType output_type; + uint32_t time_now; + uint32_t next_send_time; + + // Initiate |last_lost_time_|. + time_now = next_send_time = last_lost_time_ = + rtp_generator_->GetRtpHeader(kPayloadType, frame_size_samples_, + &rtp_header_); + for (int k = 0; k < num_loops; ++k) { + // Delay by one frame such that the FEC can come in. + while (time_now + kFrameSizeMs >= next_send_time) { + next_send_time = rtp_generator_->GetRtpHeader(kPayloadType, + frame_size_samples_, + &rtp_header_); + if (!Lost(next_send_time)) { + InsertPacket(rtp_header_, payload_, kPayloadSizeByte, + next_send_time); + } + } + GetOutputAudio(kMaxOutputSize, output_, &output_type); + time_now += kOutputLengthMs; + } + CheckNetworkStatistics(expects); + neteq()->FlushBuffers(); + } + + void DecodeFecTest() { + external_decoder_->set_fec_enabled(false); + NetEqNetworkStatsCheck expects = { + IGNORE, // current_buffer_size_ms + IGNORE, // preferred_buffer_size_ms + IGNORE, // jitter_peaks_found + EQUAL, // packet_loss_rate + EQUAL, // packet_discard_rate + EQUAL, // expand_rate + IGNORE, // preemptive_rate + EQUAL, // accelerate_rate + EQUAL, // decoded_fec_rate + IGNORE, // clockdrift_ppm + EQUAL, // added_zero_samples + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + }; + RunTest(50, expects); + + // Next we introduce packet losses. + SetPacketLossRate(0.1); + expects.stats_ref.packet_loss_rate = 1337; + expects.stats_ref.expand_rate = 1065; + RunTest(50, expects); + + // Next we enable Opus FEC. + external_decoder_->set_fec_enabled(true); + // If FEC fills in the lost packets, no packet loss will be counted. + expects.stats_ref.packet_loss_rate = 0; + expects.stats_ref.expand_rate = 0; + expects.stats_ref.secondary_decoded_rate = 2006; + RunTest(50, expects); + } + + private: + MockAudioDecoderOpus* external_decoder_; + const int samples_per_ms_; + const size_t frame_size_samples_; + scoped_ptr rtp_generator_; + WebRtcRTPHeader rtp_header_; + uint32_t last_lost_time_; + uint32_t packet_loss_interval_; + uint8_t payload_[kPayloadSizeByte]; + int16_t output_[kMaxOutputSize]; +}; + +TEST(NetEqNetworkStatsTest, OpusDecodeFec) { + MockAudioDecoderOpus decoder(1); + EXPECT_CALL(decoder, Init()); + NetEqNetworkStatsTest test(kDecoderOpus, &decoder); + test.DecodeFecTest(); + EXPECT_CALL(decoder, Die()).Times(1); +} + +TEST(NetEqNetworkStatsTest, StereoOpusDecodeFec) { + MockAudioDecoderOpus decoder(2); + EXPECT_CALL(decoder, Init()); + NetEqNetworkStatsTest test(kDecoderOpus, &decoder); + test.DecodeFecTest(); + EXPECT_CALL(decoder, Die()).Times(1); +} + +} // namespace test +} // namespace webrtc + + + + diff --git a/webrtc/modules/audio_coding/neteq/neteq_tests.gypi b/webrtc/modules/audio_coding/neteq/neteq_tests.gypi index 3a0ebdbd4..ed9c84a69 100644 --- a/webrtc/modules/audio_coding/neteq/neteq_tests.gypi +++ b/webrtc/modules/audio_coding/neteq/neteq_tests.gypi @@ -164,6 +164,8 @@ '<(DEPTH)/third_party/gflags/gflags.gyp:gflags', ], 'sources': [ + 'tools/neteq_external_decoder_test.cc', + 'tools/neteq_external_decoder_test.h', 'tools/neteq_performance_test.cc', 'tools/neteq_performance_test.h', 'tools/neteq_quality_test.cc', diff --git a/webrtc/modules/audio_coding/neteq/neteq_unittest.cc b/webrtc/modules/audio_coding/neteq/neteq_unittest.cc index 498cde359..420e7bf48 100644 --- a/webrtc/modules/audio_coding/neteq/neteq_unittest.cc +++ b/webrtc/modules/audio_coding/neteq/neteq_unittest.cc @@ -137,13 +137,42 @@ void RefFiles::WriteToFile(const NetEqNetworkStatistics& stats) { void RefFiles::ReadFromFileAndCompare( const NetEqNetworkStatistics& stats) { + // TODO(minyue): Update resource/audio_coding/neteq_network_stats.dat and + // resource/audio_coding/neteq_network_stats_win32.dat. + struct NetEqNetworkStatisticsOld { + uint16_t current_buffer_size_ms; // Current jitter buffer size in ms. + uint16_t preferred_buffer_size_ms; // Target buffer size in ms. + uint16_t jitter_peaks_found; // 1 if adding extra delay due to peaky + // jitter; 0 otherwise. + uint16_t packet_loss_rate; // Loss rate (network + late) in Q14. + uint16_t packet_discard_rate; // Late loss rate in Q14. + uint16_t expand_rate; // Fraction (of original stream) of synthesized + // speech inserted through expansion (in Q14). + uint16_t preemptive_rate; // Fraction of data inserted through pre-emptive + // expansion (in Q14). + uint16_t accelerate_rate; // Fraction of data removed through acceleration + // (in Q14). + int32_t clockdrift_ppm; // Average clock-drift in parts-per-million + // (positive or negative). + int added_zero_samples; // Number of zero samples added in "off" mode. + }; if (input_fp_) { // Read from ref file. - size_t stat_size = sizeof(NetEqNetworkStatistics); - NetEqNetworkStatistics ref_stats; + size_t stat_size = sizeof(NetEqNetworkStatisticsOld); + NetEqNetworkStatisticsOld ref_stats; ASSERT_EQ(1u, fread(&ref_stats, stat_size, 1, input_fp_)); // Compare - ASSERT_EQ(0, memcmp(&stats, &ref_stats, stat_size)); + ASSERT_EQ(stats.current_buffer_size_ms, ref_stats.current_buffer_size_ms); + ASSERT_EQ(stats.preferred_buffer_size_ms, + ref_stats.preferred_buffer_size_ms); + ASSERT_EQ(stats.jitter_peaks_found, ref_stats.jitter_peaks_found); + ASSERT_EQ(stats.packet_loss_rate, ref_stats.packet_loss_rate); + ASSERT_EQ(stats.packet_discard_rate, ref_stats.packet_discard_rate); + ASSERT_EQ(stats.preemptive_rate, ref_stats.preemptive_rate); + ASSERT_EQ(stats.accelerate_rate, ref_stats.accelerate_rate); + ASSERT_EQ(stats.clockdrift_ppm, ref_stats.clockdrift_ppm); + ASSERT_EQ(stats.added_zero_samples, ref_stats.added_zero_samples); + ASSERT_EQ(stats.secondary_decoded_rate, 0); } } diff --git a/webrtc/modules/audio_coding/neteq/statistics_calculator.cc b/webrtc/modules/audio_coding/neteq/statistics_calculator.cc index 097e19fab..863923fae 100644 --- a/webrtc/modules/audio_coding/neteq/statistics_calculator.cc +++ b/webrtc/modules/audio_coding/neteq/statistics_calculator.cc @@ -28,7 +28,8 @@ StatisticsCalculator::StatisticsCalculator() lost_timestamps_(0), timestamps_since_last_report_(0), len_waiting_times_(0), - next_waiting_time_index_(0) { + next_waiting_time_index_(0), + secondary_decoded_samples_(0) { memset(waiting_times_, 0, kLenWaitingTimes * sizeof(waiting_times_[0])); } @@ -38,6 +39,7 @@ void StatisticsCalculator::Reset() { added_zero_samples_ = 0; expanded_voice_samples_ = 0; expanded_noise_samples_ = 0; + secondary_decoded_samples_ = 0; } void StatisticsCalculator::ResetMcu() { @@ -90,6 +92,10 @@ void StatisticsCalculator::IncreaseCounter(int num_samples, int fs_hz) { } } +void StatisticsCalculator::SecondaryDecodedSamples(int num_samples) { + secondary_decoded_samples_ += num_samples; +} + void StatisticsCalculator::StoreWaitingTime(int waiting_time_ms) { assert(next_waiting_time_index_ < kLenWaitingTimes); waiting_times_[next_waiting_time_index_] = waiting_time_ms; @@ -140,6 +146,10 @@ void StatisticsCalculator::GetNetworkStatistics( CalculateQ14Ratio(expanded_voice_samples_ + expanded_noise_samples_, timestamps_since_last_report_); + stats->secondary_decoded_rate = + CalculateQ14Ratio(secondary_decoded_samples_, + timestamps_since_last_report_); + // Reset counters. ResetMcu(); Reset(); diff --git a/webrtc/modules/audio_coding/neteq/statistics_calculator.h b/webrtc/modules/audio_coding/neteq/statistics_calculator.h index 95379f45a..03f4835b8 100644 --- a/webrtc/modules/audio_coding/neteq/statistics_calculator.h +++ b/webrtc/modules/audio_coding/neteq/statistics_calculator.h @@ -70,6 +70,9 @@ class StatisticsCalculator { // Stores new packet waiting time in waiting time statistics. void StoreWaitingTime(int waiting_time_ms); + // Reports that |num_samples| samples were decoded from secondary packets. + void SecondaryDecodedSamples(int num_samples); + // Returns the current network statistics in |stats|. The current sample rate // is |fs_hz|, the total number of samples in packet buffer and sync buffer // yet to play out is |num_samples_in_buffers|, and the number of samples per @@ -101,6 +104,7 @@ class StatisticsCalculator { int waiting_times_[kLenWaitingTimes]; // Used as a circular buffer. int len_waiting_times_; int next_waiting_time_index_; + uint32_t secondary_decoded_samples_; DISALLOW_COPY_AND_ASSIGN(StatisticsCalculator); }; diff --git a/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.cc b/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.cc new file mode 100644 index 000000000..17391daeb --- /dev/null +++ b/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.cc @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + + +#include "webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace webrtc { +namespace test { + +NetEqExternalDecoderTest::NetEqExternalDecoderTest(NetEqDecoder codec, + AudioDecoder* decoder) + : codec_(codec), + decoder_(decoder), + sample_rate_hz_(CodecSampleRateHz(codec_)), + channels_(static_cast(decoder_->channels())) { + NetEq::Config config; + config.sample_rate_hz = sample_rate_hz_; + neteq_.reset(NetEq::Create(config)); + printf("%d\n", channels_); +} + +void NetEqExternalDecoderTest::Init() { + ASSERT_EQ(NetEq::kOK, + neteq_->RegisterExternalDecoder(decoder_, codec_, kPayloadType)); +} + +void NetEqExternalDecoderTest::InsertPacket(WebRtcRTPHeader rtp_header, + const uint8_t* payload, + size_t payload_size_bytes, + uint32_t receive_timestamp) { + ASSERT_EQ( + NetEq::kOK, + neteq_->InsertPacket( + rtp_header, payload, payload_size_bytes, receive_timestamp)); +} + +int NetEqExternalDecoderTest::GetOutputAudio(size_t max_length, + int16_t* output, + NetEqOutputType* output_type) { + // Get audio from regular instance. + int samples_per_channel; + int num_channels; + EXPECT_EQ(NetEq::kOK, + neteq_->GetAudio(max_length, + output, + &samples_per_channel, + &num_channels, + output_type)); + EXPECT_EQ(channels_, num_channels); + EXPECT_EQ(kOutputLengthMs * sample_rate_hz_ / 1000, samples_per_channel); + return samples_per_channel; +} + +} // namespace test +} // namespace webrtc diff --git a/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h b/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h new file mode 100644 index 000000000..8cf6ef888 --- /dev/null +++ b/webrtc/modules/audio_coding/neteq/tools/neteq_external_decoder_test.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_MODULES_AUDIO_CODING_NETEQ_TOOLS_NETEQ_EXTERNAL_DECODER_TEST_H_ +#define WEBRTC_MODULES_AUDIO_CODING_NETEQ_TOOLS_NETEQ_EXTERNAL_DECODER_TEST_H_ + +#include "webrtc/modules/audio_coding/codecs/audio_decoder.h" +#include "webrtc/modules/audio_coding/neteq/interface/neteq.h" +#include "webrtc/modules/interface/module_common_types.h" +#include "webrtc/system_wrappers/interface/scoped_ptr.h" + +namespace webrtc { +namespace test { +// This test class provides a way run NetEQ with an external decoder. +class NetEqExternalDecoderTest { + protected: + static const uint8_t kPayloadType = 95; + static const int kOutputLengthMs = 10; + + // The external decoder |decoder| is suppose to be of type |codec|. + NetEqExternalDecoderTest(NetEqDecoder codec, AudioDecoder* decoder); + + virtual ~NetEqExternalDecoderTest() { } + + // In Init(), we register the external decoder. + void Init(); + + // Inserts a new packet with |rtp_header| and |payload| of + // |payload_size_bytes| bytes. The |receive_timestamp| is an indication + // of the time when the packet was received, and should be measured with + // the same tick rate as the RTP timestamp of the current payload. + virtual void InsertPacket(WebRtcRTPHeader rtp_header, const uint8_t* payload, + size_t payload_size_bytes, + uint32_t receive_timestamp); + + // Get 10 ms of audio data. The data is written to |output|, which can hold + // (at least) |max_length| elements. Returns number of samples. + int GetOutputAudio(size_t max_length, int16_t* output, + NetEqOutputType* output_type); + + NetEq* neteq() { return neteq_.get(); } + + private: + NetEqDecoder codec_; + AudioDecoder* decoder_; + int sample_rate_hz_; + int channels_; + scoped_ptr neteq_; +}; + +} // namespace test +} // namespace webrtc + +#endif // WEBRTC_MODULES_AUDIO_CODING_NETEQ_TOOLS_NETEQ_EXTERNAL_DECODER_TEST_H_ diff --git a/webrtc/modules/modules.gyp b/webrtc/modules/modules.gyp index 27cb363e8..c1c8dabb2 100644 --- a/webrtc/modules/modules.gyp +++ b/webrtc/modules/modules.gyp @@ -64,6 +64,7 @@ 'iSACFix', 'media_file', 'neteq', + 'neteq_test_support', 'neteq_unittest_tools', 'paced_sender', 'PCM16B', # Needed by NetEq tests. @@ -125,6 +126,7 @@ 'audio_coding/neteq/merge_unittest.cc', 'audio_coding/neteq/neteq_external_decoder_unittest.cc', 'audio_coding/neteq/neteq_impl_unittest.cc', + 'audio_coding/neteq/neteq_network_stats_unittest.cc', 'audio_coding/neteq/neteq_stereo_unittest.cc', 'audio_coding/neteq/neteq_unittest.cc', 'audio_coding/neteq/normal_unittest.cc',