03c58c4572
G722Codec::info() reported an 8 kHz sample rate for its 16 kHz output (640 bytes of PCM per 20 ms frame, read as 40 ms at 8 kHz). AudioReceiver therefore fetched packets at half the rate they arrived, and the high-water trim discarded about 44% of every G.722 stream: 109 of 250 packets per 5 s, with the buffer at 360 ms instead of 100 ms. The 8 kHz figure is right for one thing: RFC 3551 keeps G.722's RTP clock at 8000. Codec::Info already had mTimestampUnit for that. G.722 now reports mSamplerate 16000 and mTimestampUnit 1/8000, and Codec::rtpClockRate() gives the clock rate. The uses of samplerate() that mean RTP time now use it: - the jitter statistics in AudioReceiver::add; - the timestamp-gap-to-milliseconds conversion in AudioReceiver; - AudioStream's transmit timestamp unit and increment (numerically unchanged for every codec, including G.722). PCM uses stay on samplerate(), so AudioStream now also feeds the G.722 encoder 16 kHz audio instead of 8 kHz. The factory still advertises G722/8000 in SDP, as it must; only its backwards comment is fixed. AudioReceiver::infoFor() also applied a "G729A silence" rule to payload type 9, which is G.722; G.729 is 18. It now applies to PT 18. Other fixed-frame payloads are timed proportionally, as AudioReceiver::add already does, so a 10 ms G.722 packet (half a 160-byte frame) counts as 10 ms, not 20. Measured in vq-core with 16 replayed calls (NISQA speech; G.722 from ffmpeg and pjmedia, G.711 controls of the same speech; 3 runs each, all identical): - G.722 packet drops: 44% -> 0; buffer 100 ms like G.711; no underruns; - Sevana MOS of every G.722 call now equals its G.711 control, normal and loud (with the previous code three calls read 4.45 against 4.25); - DeadAir-01 0.68-0.89 -> 0.01-0.27 (G.711: 0.00-0.14); SilentCall within 0.01 of G.711 for the ffmpeg-encoded calls (was 0.15-0.17 higher); - AmpClipping on loud calls 0.004/0.001, as G.711; the 0.02-0.03 seen after the codec swap alone came from the dropped packets; - G.711 results, packet counts, loss, jitter and audio durations unchanged. The transmit path (AudioStream) builds but was not exercised; vq-core only receives. Needs the vq_net change that computes jitter with rtpClockRate(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1228 lines
44 KiB
C++
1228 lines
44 KiB
C++
/* Copyright(C) 2007-2026 VoIPobjects (voipobjects.com)
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "../engine_config.h"
|
|
#include "MT_AudioReceiver.h"
|
|
#include "MT_AudioCodec.h"
|
|
#include "MT_CngHelper.h"
|
|
#include "MT_Dtmf.h"
|
|
#include "../helper/HL_Log.h"
|
|
#include "../helper/HL_Time.h"
|
|
#include "../helper/HL_PoolAllocator.h"
|
|
#include "../audio/Audio_Interface.h"
|
|
#include "../audio/Audio_Resampler.h"
|
|
#include <cmath>
|
|
#include <iostream>
|
|
|
|
#if !defined(TARGET_ANDROID) && !defined(TARGET_OPENWRT) && !defined(TARGET_WIN) && !defined(TARGET_RPI) && \
|
|
defined(USE_AMR_CODEC)
|
|
#include "MT_AmrCodec.h"
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
|
|
#define LOG_SUBSYSTEM "media"
|
|
|
|
using namespace MT;
|
|
|
|
// ----------------- RtpBuffer::Packet --------------
|
|
RtpBuffer::Packet::Packet(const std::shared_ptr<RTPPacket>& packet, std::chrono::milliseconds timelength,
|
|
int samplerate)
|
|
: mRtp(packet), mTimelength(timelength), mSamplerate(samplerate)
|
|
{
|
|
}
|
|
|
|
const std::shared_ptr<RTPPacket>& RtpBuffer::Packet::rtp() const
|
|
{
|
|
return mRtp;
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::Packet::timelength() const
|
|
{
|
|
return mTimelength;
|
|
}
|
|
|
|
int RtpBuffer::Packet::samplerate() const
|
|
{
|
|
return mSamplerate;
|
|
}
|
|
|
|
const std::vector<short>& RtpBuffer::Packet::pcm() const
|
|
{
|
|
return mPcm;
|
|
}
|
|
|
|
std::vector<short>& RtpBuffer::Packet::pcm()
|
|
{
|
|
return mPcm;
|
|
}
|
|
|
|
// ------------ RtpBuffer ----------------
|
|
RtpBuffer::RtpBuffer(Statistics& stat) : mStat(stat) {}
|
|
|
|
RtpBuffer::~RtpBuffer()
|
|
{
|
|
if (mAddCounter)
|
|
ICELogDebug(<< "Number of add packets: " << mAddCounter << ", number of retrieved packets "
|
|
<< mReturnedCounter);
|
|
}
|
|
|
|
void RtpBuffer::setHigh(std::chrono::milliseconds t)
|
|
{
|
|
mHigh = t;
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::high() const
|
|
{
|
|
return mHigh;
|
|
}
|
|
|
|
void RtpBuffer::setLow(std::chrono::milliseconds t)
|
|
{
|
|
mLow = t;
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::low() const
|
|
{
|
|
return mLow;
|
|
}
|
|
|
|
void RtpBuffer::setPrebuffer(std::chrono::milliseconds t)
|
|
{
|
|
mPrebuffer = t;
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::prebuffer() const
|
|
{
|
|
return mPrebuffer;
|
|
}
|
|
|
|
int RtpBuffer::getCount() const
|
|
{
|
|
Lock l(mGuard);
|
|
return static_cast<int>(mPacketList.size());
|
|
}
|
|
|
|
bool SequenceSort(const std::shared_ptr<RtpBuffer::Packet>& p1, const std::shared_ptr<RtpBuffer::Packet>& p2)
|
|
{
|
|
return p1->rtp()->GetExtendedSequenceNumber() < p2->rtp()->GetExtendedSequenceNumber();
|
|
}
|
|
|
|
std::shared_ptr<RtpBuffer::Packet> RtpBuffer::add(const std::shared_ptr<jrtplib::RTPPacket>& packet,
|
|
std::chrono::milliseconds timelength, int rate)
|
|
{
|
|
if (!packet)
|
|
return std::shared_ptr<Packet>();
|
|
|
|
Lock l(mGuard);
|
|
|
|
// Update statistics
|
|
if (mLastAddTime == 0.0)
|
|
mLastAddTime = now_ms();
|
|
else
|
|
{
|
|
float t = now_ms();
|
|
mStat.mPacketInterval.process(t - mLastAddTime);
|
|
mLastAddTime = t;
|
|
}
|
|
mStat.mSsrc = packet->GetSSRC();
|
|
|
|
// Update jitter
|
|
ICELogMedia(<< "Adding new packet seqno " << packet->GetSequenceNumber() << " into jitter buffer");
|
|
mAddCounter++;
|
|
|
|
// Look for maximum&minimal sequence number; check for dublicates
|
|
unsigned maxno = 0, minno = 0xFFFFFFFF;
|
|
|
|
// New sequence number
|
|
unsigned newSeqno = packet->GetExtendedSequenceNumber();
|
|
|
|
for (auto& p : mPacketList)
|
|
{
|
|
unsigned seqno = p->rtp()->GetExtendedSequenceNumber();
|
|
|
|
if (seqno == newSeqno)
|
|
{
|
|
mStat.mDuplicatedRtp++;
|
|
ICELogMedia(<< "Discovered duplicated packet, skipping");
|
|
return std::shared_ptr<Packet>();
|
|
}
|
|
|
|
if (seqno > maxno)
|
|
maxno = seqno;
|
|
if (seqno < minno)
|
|
minno = seqno;
|
|
}
|
|
|
|
// Get amount of available audio (in milliseconds) in jitter buffer
|
|
auto available = findTimelength();
|
|
|
|
if (newSeqno > minno || (available < mHigh))
|
|
{
|
|
// Insert into queue. Pool the per-packet Packet node (object + shared_ptr control block)
|
|
// via allocate_shared; this churns at the RTP packet rate even on network-MOS-only streams.
|
|
auto p = std::allocate_shared<Packet>(hl::PoolAllocator<Packet>{}, packet, timelength, rate);
|
|
mPacketList.push_back(p);
|
|
|
|
// Sort again
|
|
std::sort(mPacketList.begin(), mPacketList.end(), SequenceSort);
|
|
|
|
// Limit by max timelength
|
|
available = findTimelength();
|
|
|
|
if (available > mHigh)
|
|
ICELogMedia(<< "Available " << available << " with limit " << mHigh);
|
|
|
|
return p;
|
|
}
|
|
else
|
|
{
|
|
ICELogMedia(<< "Too old packet, skipping");
|
|
mStat.mOldRtp++;
|
|
|
|
return std::shared_ptr<Packet>();
|
|
}
|
|
|
|
return std::shared_ptr<Packet>();
|
|
}
|
|
|
|
void RtpBuffer::trimToHighWater(size_t maxPackets)
|
|
{
|
|
Lock l(mGuard);
|
|
|
|
auto total = findTimelength();
|
|
|
|
// Drop the oldest packet while either bound is exceeded: the time-based
|
|
// high-water mark (mHigh, when set) or, if maxPackets != 0, the packet-count
|
|
// cap. Always keep at least one packet so loss/gap accounting has a reference.
|
|
while (mPacketList.size() > 1 &&
|
|
((0ms != mHigh && total > mHigh) || (maxPackets != 0 && mPacketList.size() > maxPackets)))
|
|
{
|
|
ICELogMedia(<< "Dropping RTP packets from jitter buffer");
|
|
total -= mPacketList.front()->timelength();
|
|
|
|
// Before advancing mLastSeqno over the dropped packet, record a loss event for any
|
|
// sequence-number gap on the wire between the previous packet we saw and this one.
|
|
// Without this, drops silently mask real packet loss that happened between them.
|
|
auto droppingPacket = mPacketList.front();
|
|
uint32_t droppingSeq = droppingPacket->rtp()->GetExtendedSequenceNumber();
|
|
if (mLastSeqno)
|
|
{
|
|
int gap = (int64_t)droppingSeq - (int64_t)*mLastSeqno - 1;
|
|
if (gap > 0)
|
|
{
|
|
mStat.mPacketLoss += gap;
|
|
if (mStat.mPacketLossTimeline.empty() || (mStat.mPacketLossTimeline.back().mEndSeqno != droppingSeq))
|
|
{
|
|
auto gapStart = RtpHelper::toMicroseconds(*mLastReceiveTime);
|
|
auto gapEnd = RtpHelper::toMicroseconds(droppingPacket->rtp()->GetReceiveTime());
|
|
mStat.mPacketLossTimeline.emplace_back(PacketLossEvent{.mStartSeqno = *mLastSeqno,
|
|
.mEndSeqno = droppingSeq,
|
|
.mGap = gap,
|
|
.mTimestampStart = gapStart,
|
|
.mTimestampEnd = gapEnd});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save it as last packet however - to not confuse loss packet counter
|
|
mFetchedPacket = droppingPacket;
|
|
mLastSeqno = droppingSeq;
|
|
mLastReceiveTime = mFetchedPacket->rtp()->GetReceiveTime();
|
|
|
|
// Erase from packet list
|
|
mPacketList.erase(mPacketList.begin());
|
|
|
|
// Increase number in statistics
|
|
mStat.mPacketDropped++;
|
|
}
|
|
}
|
|
|
|
RtpBuffer::FetchResult RtpBuffer::fetch()
|
|
{
|
|
Lock l(mGuard);
|
|
|
|
FetchResult result;
|
|
|
|
// Bound the buffer to the high-water mark before fetching.
|
|
trimToHighWater();
|
|
|
|
// See how much audio is buffered now.
|
|
auto total = findTimelength();
|
|
|
|
if (total < mLow || total == 0ms)
|
|
{
|
|
// Still not prebuffered
|
|
result = {FetchResult::Status::NoPacket};
|
|
}
|
|
else
|
|
{
|
|
if (mLastSeqno) // It means we had previous packet
|
|
{
|
|
if (mPacketList.empty())
|
|
{
|
|
// Don't increase counter of lost packets here; maybe it is DTX
|
|
result = {FetchResult::Status::NoPacket};
|
|
}
|
|
else
|
|
{
|
|
// Current sequence number ?
|
|
auto& packet = *mPacketList.front();
|
|
uint32_t seqno = packet.rtp()->GetExtendedSequenceNumber();
|
|
|
|
// Gap between new packet and previous on
|
|
int gap = (int64_t)seqno - (int64_t)*mLastSeqno - 1;
|
|
if (gap > 0)
|
|
{
|
|
// std::cout << "Increase the packet loss for SSRC " << std::hex << mSsrc << std::endl;
|
|
mStat.mPacketLoss += gap;
|
|
|
|
// Report is the onetime; there is no many sequential 1-packet gap reports
|
|
if (mStat.mPacketLossTimeline.empty() || (mStat.mPacketLossTimeline.back().mEndSeqno != seqno))
|
|
{
|
|
auto gapStart = RtpHelper::toMicroseconds(*mLastReceiveTime);
|
|
auto gapEnd = RtpHelper::toMicroseconds(packet.rtp()->GetReceiveTime());
|
|
mStat.mPacketLossTimeline.emplace_back(PacketLossEvent{.mStartSeqno = *mLastSeqno,
|
|
.mEndSeqno = seqno,
|
|
.mGap = gap,
|
|
.mTimestampStart = gapStart,
|
|
.mTimestampEnd = gapEnd});
|
|
}
|
|
|
|
// ToDo: here we should decide smth - 2-packet gap shoud report Status::Gap two times at least; but
|
|
// current implementation gives only one. It is not big problem - as gap is detected when we have
|
|
// smth to return usually
|
|
mLastSeqno = seqno;
|
|
mLastReceiveTime = packet.rtp()->GetReceiveTime();
|
|
result = {FetchResult::Status::Gap};
|
|
}
|
|
else
|
|
{
|
|
result = {FetchResult::Status::RegularPacket, mPacketList.front()};
|
|
|
|
// Save last returned normal packet
|
|
mFetchedPacket = result.mPacket;
|
|
mLastSeqno = result.mPacket->rtp()->GetExtendedSequenceNumber();
|
|
mLastReceiveTime = result.mPacket->rtp()->GetReceiveTime();
|
|
|
|
// Remove returned packet from the list
|
|
mPacketList.erase(mPacketList.begin());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// See if prebuffer limit is reached
|
|
if (findTimelength() >= mPrebuffer && !mPacketList.empty())
|
|
{
|
|
// Normal packet will be returned
|
|
result = {FetchResult::Status::RegularPacket, mPacketList.front()};
|
|
|
|
// Remember returned packet
|
|
mFetchedPacket = result.mPacket;
|
|
mLastSeqno = result.mPacket->rtp()->GetExtendedSequenceNumber();
|
|
mLastReceiveTime = result.mPacket->rtp()->GetReceiveTime();
|
|
|
|
// Remove returned packet from buffer list
|
|
mPacketList.erase(mPacketList.begin());
|
|
}
|
|
else
|
|
{
|
|
ICELogMedia(<< "Jitter buffer was not prebuffered yet; resulting no packet");
|
|
result = {FetchResult::Status::NoPacket};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.mStatus != FetchResult::Status::NoPacket)
|
|
mReturnedCounter++;
|
|
|
|
return result;
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::findTimelength()
|
|
{
|
|
std::chrono::milliseconds r = 0ms;
|
|
for (const auto& p : mPacketList)
|
|
r += p->timelength();
|
|
return r;
|
|
}
|
|
|
|
std::shared_ptr<RtpBuffer::Packet> RtpBuffer::peekFront() const
|
|
{
|
|
Lock l(mGuard);
|
|
return mPacketList.empty() ? std::shared_ptr<Packet>() : mPacketList.front();
|
|
}
|
|
|
|
std::chrono::milliseconds RtpBuffer::bufferedTime() const
|
|
{
|
|
Lock l(mGuard);
|
|
std::chrono::milliseconds r = 0ms;
|
|
for (const auto& p : mPacketList)
|
|
r += p->timelength();
|
|
return r;
|
|
}
|
|
|
|
int RtpBuffer::getNumberOfReturnedPackets() const
|
|
{
|
|
return mReturnedCounter;
|
|
}
|
|
|
|
int RtpBuffer::getNumberOfAddPackets() const
|
|
{
|
|
return mAddCounter;
|
|
}
|
|
|
|
//-------------- Receiver ---------------
|
|
Receiver::Receiver(Statistics& stat) : mStat(stat) {}
|
|
|
|
Receiver::~Receiver() {}
|
|
|
|
//-------------- AudioReceiver ----------------
|
|
AudioReceiver::AudioReceiver(const CodecList::Settings& settings, MT::Statistics& stat)
|
|
: Receiver(stat), mRtpBuffer(stat), mDtmfBuffer(stat), mCodecSettings(settings), mCodecList(settings),
|
|
mDtmfReceiver(stat)
|
|
{
|
|
// Init codecs
|
|
mCodecList.setSettings(settings);
|
|
mCodecList.fillCodecMap(mCodecMap);
|
|
|
|
mDtmfBuffer.setPrebuffer(0ms);
|
|
mDtmfBuffer.setLow(0ms);
|
|
mDtmfBuffer.setHigh(1ms);
|
|
|
|
// Jitter-buffer target delay. Diagnostics on bursty networks showed the buffer level
|
|
// swinging ~140ms peak-to-trough and hitting BOTH rails within one call - starving
|
|
// (underrun -> silence) and overflowing (trim -> drop) - with real packet loss near 1%.
|
|
// Widened the window to give burst jitter more room: prebuffer/low-water raised so a
|
|
// late clump arrives before starvation, and the high-water raised so the extra depth
|
|
// is not immediately trimmed back off.
|
|
mRtpBuffer.setHigh(360ms);
|
|
|
|
// Prebuffer: initial fill before the first packet is released (also the depth we aim to
|
|
// hold). Was the RTP_BUFFER_PREBUFFER default (100ms); set explicitly alongside low-water.
|
|
mRtpBuffer.setPrebuffer(120ms);
|
|
|
|
// Low-water cushion. With low-water at 0 the buffer drains to empty before reacting, so
|
|
// ordinary clock drift / jitter periodically starves the decoder and forces silence/PLC
|
|
// insertion (heard as periodic artifacts, uncounted by rtp_lost / rtp_dropped). Holding
|
|
// ~120ms lets a late packet clump arrive before the buffer underruns.
|
|
mRtpBuffer.setLow(120ms);
|
|
|
|
// Resamplers are lazy inside; there is no actual memory allocation
|
|
mResampler8.start(AUDIO_CHANNELS, 8000, AUDIO_SAMPLERATE);
|
|
mResampler16.start(AUDIO_CHANNELS, 16000, AUDIO_SAMPLERATE);
|
|
mResampler32.start(AUDIO_CHANNELS, 32000, AUDIO_SAMPLERATE);
|
|
mResampler48.start(AUDIO_CHANNELS, 48000, AUDIO_SAMPLERATE);
|
|
|
|
#if defined(DUMP_DECODED)
|
|
mDecodedDump = std::make_shared<Audio::WavFileWriter>();
|
|
mDecodedDump->open("decoded.wav", 8000 /*G711*/, AUDIO_CHANNELS);
|
|
#endif
|
|
}
|
|
|
|
AudioReceiver::~AudioReceiver()
|
|
{
|
|
mResampler8.stop();
|
|
mResampler16.stop();
|
|
mResampler32.stop();
|
|
mResampler48.stop();
|
|
mDecodedDump.reset();
|
|
|
|
if (mRequestedAudio != 0ms)
|
|
ICELogDebug(<< "Requested " << mRequestedAudio << ", produced " << mProducedAudio);
|
|
if (mDecodeCount)
|
|
ICELogDebug(<< "Average interval between packet decoding " << mIntervalBetweenDecode / mDecodeCount);
|
|
}
|
|
|
|
// Update codec settings
|
|
void AudioReceiver::setCodecSettings(const CodecList::Settings& codecSettings)
|
|
{
|
|
if (mCodecSettings == codecSettings)
|
|
return;
|
|
|
|
// Preserve the lazy-codec-map policy across SDP-driven updates: codecSettings comes from
|
|
// parseSdp()/per-call negotiation and defaults mLazyCodecMap to false, but the policy is
|
|
// set by the owner (vq-core) and must not be lost mid-stream.
|
|
const bool lazy = mCodecSettings.mLazyCodecMap;
|
|
mCodecSettings = codecSettings;
|
|
mCodecSettings.mLazyCodecMap = lazy;
|
|
mCodecList.setSettings(
|
|
mCodecSettings); // This builds factory list with proper payload types according to payload types in settings
|
|
|
|
// Rebuild codec map from factory list
|
|
mCodecList.fillCodecMap(mCodecMap);
|
|
}
|
|
|
|
Codec* AudioReceiver::ensureCodec(int payloadType)
|
|
{
|
|
auto codecIter = mCodecMap.find(payloadType);
|
|
if (codecIter == mCodecMap.end())
|
|
return nullptr;
|
|
|
|
// mLazyCodecMap leaves the value null until first use - create it now.
|
|
if (!codecIter->second)
|
|
codecIter->second = mCodecList.createCodecByPayloadType(payloadType);
|
|
|
|
return codecIter->second.get();
|
|
}
|
|
|
|
CngDecoder& AudioReceiver::cng()
|
|
{
|
|
if (!mCngDecoder)
|
|
mCngDecoder = std::make_unique<CngDecoder>();
|
|
return *mCngDecoder;
|
|
}
|
|
|
|
CodecList::Settings& AudioReceiver::getCodecSettings()
|
|
{
|
|
return mCodecSettings;
|
|
}
|
|
|
|
Codec* AudioReceiver::add(const std::shared_ptr<jrtplib::RTPPacket>& p)
|
|
{
|
|
Codec* codec = nullptr;
|
|
|
|
// Estimate time length
|
|
int time_length = 0, samplerate = 8000, payloadLength = p->GetPayloadLength(), ptype = p->GetPayloadType();
|
|
|
|
// ICELogMedia(<< "Adding packet No " << p->GetSequenceNumber());
|
|
|
|
// Increase codec counter
|
|
mStat.mCodecCount[ptype]++;
|
|
|
|
// Check if we deal with telephone-event
|
|
if (p->GetPayloadType() == mCodecSettings.mTelephoneEvent)
|
|
{
|
|
codec = nullptr;
|
|
mDtmfBuffer.add(p, 10ms, 8000);
|
|
}
|
|
else
|
|
{
|
|
// Look for codec
|
|
// Check if codec can be handled
|
|
auto codecIter = mCodecMap.find(ptype);
|
|
if (codecIter != mCodecMap.end())
|
|
{
|
|
// Check if codec is creating lazily
|
|
if (!codecIter->second)
|
|
{
|
|
codecIter->second = mCodecList.createCodecByPayloadType(ptype);
|
|
}
|
|
codec = codecIter->second.get();
|
|
|
|
// Return pointer to codec if needed.get()
|
|
if (mStat.mCodecName.empty() && codec)
|
|
mStat.mCodecName = codec->name();
|
|
|
|
|
|
if (!codec)
|
|
time_length = 10;
|
|
else if (!codec->rtpLength())
|
|
time_length = codec->frameTime();
|
|
else
|
|
time_length = lround(double(payloadLength) / codec->rtpLength() * codec->frameTime());
|
|
|
|
if (codec)
|
|
samplerate = codec->rtpClockRate(); // RTP timestamp units, not PCM rate (G.722)
|
|
}
|
|
|
|
// Process jitter anyway - can we decode payload or not
|
|
mJitterStats.process(p.get(), samplerate);
|
|
mStat.mJitter = static_cast<float>(mJitterStats.get());
|
|
|
|
if (!codec)
|
|
return nullptr;
|
|
|
|
// Check if packet is CNG
|
|
if (payloadLength >= 1 && payloadLength <= 6 && (ptype == 0 || ptype == 8))
|
|
time_length = mLastPacketTimeLength ? mLastPacketTimeLength : 20;
|
|
else
|
|
// Check if packet is too short from time length side - smth strange with found codec...
|
|
if (time_length < 2)
|
|
{
|
|
// It will cause statistics to report about bad RTP packet
|
|
// I have to replay last packet payload here to avoid report about lost packet
|
|
mRtpBuffer.add(p, std::chrono::milliseconds(time_length), samplerate);
|
|
return nullptr;
|
|
}
|
|
|
|
// Queue packet to buffer
|
|
mRtpBuffer.add(p, std::chrono::milliseconds(time_length), samplerate).get();
|
|
}
|
|
return codec;
|
|
}
|
|
|
|
void AudioReceiver::processDecoded(Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
// Write to audio dump if requested
|
|
if (mDecodedDump && mDecodedLength)
|
|
mDecodedDump->write(mDecodedFrame.data(), mDecodedLength);
|
|
|
|
// Resample to target rate
|
|
makeMonoAndResample(options.mResampleToMainRate ? mCodec->samplerate() : 0, mCodec->channels());
|
|
|
|
// Send to output
|
|
output.add(mResampledFrame.data(), mResampledLength);
|
|
}
|
|
|
|
void AudioReceiver::produceSilence(std::chrono::milliseconds length, Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
if (!mCodec)
|
|
return;
|
|
|
|
// Fill mDecodeBuffer as much as needed and call processDecoded()
|
|
// Depending on used codec mono or stereo silence should be produced
|
|
|
|
size_t chunks = length.count() / 10;
|
|
size_t tail = length.count() % 10;
|
|
size_t chunk_size = 10 * sizeof(int16_t) * mCodec->samplerate() / 1000 * mCodec->channels();
|
|
size_t tail_size = tail * sizeof(int16_t) * mCodec->samplerate() / 1000 * mCodec->channels();
|
|
for (size_t i = 0; i < chunks; i++)
|
|
{
|
|
memset(mDecodedFrame.data(), 0, chunk_size);
|
|
mDecodedLength = chunk_size;
|
|
processDecoded(output, options);
|
|
}
|
|
if (tail)
|
|
{
|
|
memset(mDecodedFrame.data(), 0, tail_size);
|
|
mDecodedLength = tail_size;
|
|
processDecoded(output, options);
|
|
}
|
|
}
|
|
|
|
void AudioReceiver::produceCNG(std::chrono::milliseconds length, Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
int frames100ms = length.count() / 100;
|
|
for (int frameIndex = 0; frameIndex < frames100ms; frameIndex++)
|
|
{
|
|
if (options.mSkipDecode)
|
|
mDecodedLength = 0;
|
|
else
|
|
mDecodedLength = cng().produce(mCodec->samplerate(), 100, mDecodedFrame.data(), false);
|
|
|
|
if (mDecodedLength)
|
|
processDecoded(output, options);
|
|
}
|
|
|
|
// Do not forget about tail!
|
|
int tail = length.count() % 100;
|
|
if (tail)
|
|
{
|
|
if (options.mSkipDecode)
|
|
mDecodedLength = 0;
|
|
else
|
|
mDecodedLength =
|
|
cng().produce(mCodec->samplerate(), tail, reinterpret_cast<short*>(mDecodedFrame.data()), false);
|
|
|
|
if (mDecodedLength)
|
|
processDecoded(output, options);
|
|
}
|
|
}
|
|
|
|
AudioReceiver::DecodeResult AudioReceiver::decodeGapTo(Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
ICELogMedia(<< "Gap detected.");
|
|
|
|
mDecodedLength = mResampledLength = 0;
|
|
if (mCngPacket && mCodec)
|
|
{
|
|
if (mCngPacket->rtp()->GetPayloadType() == 13)
|
|
{
|
|
// Synthesize comfort noise. It will be done on AUDIO_SAMPLERATE rate directly to mResampledFrame buffer.
|
|
// Do not forget to send this noise to analysis
|
|
mDecodedLength = cng().produce(mCodec->samplerate(), mLastPacketTimeLength,
|
|
reinterpret_cast<short*>(mDecodedFrame.data()), false);
|
|
}
|
|
else
|
|
decodePacketTo(output, options, mCngPacket);
|
|
}
|
|
else if (mCodec && mFrameCount && !mCodecSettings.mSkipDecode)
|
|
{
|
|
// Conceal the gap into mDecodedFrame/mDecodedLength.
|
|
if (options.mSkipDecode)
|
|
mDecodedLength = 0;
|
|
else
|
|
{
|
|
std::span<uint8_t> concealBuffer{(uint8_t*)mDecodedFrame.data(), mDecodedFrame.size() * sizeof(int16_t)};
|
|
mDecodedLength = 0;
|
|
|
|
// In-band FEC first: the packet sitting past the gap (still at the front
|
|
// of the jitter buffer - fetch() leaves it there on a Gap) may carry a
|
|
// redundant copy of the frame we just lost. Recover it exactly instead of
|
|
// synthesising it. Only attempt this when that packet decodes with the
|
|
// very same codec instance we have been using.
|
|
if (auto next = mRtpBuffer.peekFront(); next && next->rtp())
|
|
{
|
|
auto codecIter = mCodecMap.find(next->rtp()->GetPayloadType());
|
|
if (codecIter != mCodecMap.end() && codecIter->second == mCodec)
|
|
{
|
|
mDecodedLength = mCodec->fec(
|
|
{next->rtp()->GetPayloadData(), (size_t)next->rtp()->GetPayloadLength()}, concealBuffer);
|
|
}
|
|
}
|
|
|
|
// No FEC available (unsupported codec, no redundant data, or no next
|
|
// packet yet): fall back to the codec's own concealment.
|
|
if (!mDecodedLength)
|
|
mDecodedLength = mCodec->plc(mFrameCount, concealBuffer);
|
|
|
|
if (!mDecodedLength)
|
|
{
|
|
// PLC unsupported or failed - substitute one frame of silence.
|
|
size_t frameBytes = (size_t)mCodec->frameTime() * mCodec->samplerate() / 1000 * sizeof(short) *
|
|
std::max(1, mCodec->channels());
|
|
frameBytes = std::min(frameBytes, concealBuffer.size_bytes());
|
|
mDecodedLength = frameBytes;
|
|
memset(mDecodedFrame.data(), 0, mDecodedLength);
|
|
}
|
|
}
|
|
|
|
// Fix double-concealment: fetch() advanced mLastSeqno across the gap but not
|
|
// mLastPacketTimestamp, so without this the next real packet's timestamp-gap
|
|
// check in decodePacketTo() would pad the same span again with silence (gap
|
|
// concealed twice - once here as FEC/PLC, once there as silence). Advance the
|
|
// expected-timestamp cursor past the last real packet and record the audio we
|
|
// just produced as the new "last unit", so decodePacketTo() fills only whatever
|
|
// gap remains beyond it.
|
|
if (mDecodedLength && mLastPacketTimestamp && mCodec)
|
|
{
|
|
int sr = mCodec->samplerate();
|
|
int chans = std::max(1, mCodec->channels());
|
|
*mLastPacketTimestamp += (uint32_t)(mLastPacketTimeLength * (sr / 1000));
|
|
int concealedSamples = (int)(mDecodedLength / (sizeof(short) * chans));
|
|
mLastPacketTimeLength = concealedSamples * 1000 / sr;
|
|
}
|
|
}
|
|
|
|
if (mDecodedLength)
|
|
{
|
|
processDecoded(output, options);
|
|
return {
|
|
.mStatus = DecodeResult::Status::Ok, .mSamplerate = mCodec->samplerate(), .mChannels = mCodec->channels()};
|
|
}
|
|
else
|
|
return {.mStatus = DecodeResult::Status::Skip};
|
|
}
|
|
|
|
AudioReceiver::DecodeResult AudioReceiver::decodePacketTo(Audio::DataWindow& output, DecodeOptions options,
|
|
const std::shared_ptr<RtpBuffer::Packet>& packet)
|
|
{
|
|
if (!packet || !packet->rtp())
|
|
return {DecodeResult::Status::Skip};
|
|
|
|
DecodeResult result = {.mStatus = DecodeResult::Status::Skip};
|
|
auto& rtp = *packet->rtp(); // Syntax sugar
|
|
|
|
mFailedCount = 0;
|
|
|
|
// Check if we need to emit silence - it may happen in the case if next packet has RTP timestamp much beyond the
|
|
// previous one; maybe DTX was active.
|
|
if (mLastPacketTimestamp && mLastPacketTimeLength && mCodec)
|
|
{
|
|
int units = rtp.GetTimestamp() - *mLastPacketTimestamp;
|
|
int milliseconds = units / (mCodec->rtpClockRate() / 1000);
|
|
if (milliseconds > mLastPacketTimeLength)
|
|
{
|
|
auto silenceLength = std::chrono::milliseconds(milliseconds - mLastPacketTimeLength);
|
|
ICELogDebug(<< "Emit " << silenceLength << " silence while requested " << options.mElapsed);
|
|
silenceLength = std::min(silenceLength, options.mElapsed);
|
|
if (mCngPacket && options.mFillGapByCNG)
|
|
produceCNG(silenceLength, output, options);
|
|
else
|
|
produceSilence(silenceLength, output, options);
|
|
}
|
|
}
|
|
|
|
mLastPacketTimestamp = rtp.GetTimestamp();
|
|
|
|
// Find codec by payload type
|
|
int ptype = rtp.GetPayloadType();
|
|
|
|
// Look into mCodecMap if exists
|
|
auto codecIter = mCodecMap.find(ptype);
|
|
if (codecIter == mCodecMap.end())
|
|
return {};
|
|
|
|
if (!codecIter->second)
|
|
codecIter->second = mCodecList.createCodecByPayloadType(ptype);
|
|
|
|
mCodec = codecIter->second;
|
|
if (mCodec)
|
|
{
|
|
result.mChannels = mCodec->channels();
|
|
result.mSamplerate = mCodec->samplerate();
|
|
|
|
// Check if it is CNG packet
|
|
if (((ptype == 0 || ptype == 8) && rtp.GetPayloadLength() >= 1 && rtp.GetPayloadLength() <= 6) ||
|
|
rtp.GetPayloadType() == 13)
|
|
{
|
|
if (options.mSkipDecode)
|
|
mDecodedLength = 0;
|
|
else
|
|
{
|
|
ICELogDebug(<< "Decoding CNG");
|
|
mCngPacket = packet;
|
|
cng().decode3389(rtp.GetPayloadData(), rtp.GetPayloadLength());
|
|
|
|
// Emit CNG mLastPacketLength milliseconds
|
|
mDecodedLength =
|
|
cng().produce(mCodec->samplerate(), mLastPacketTimeLength, (short*)mDecodedFrame.data(), true);
|
|
if (mDecodedLength)
|
|
processDecoded(output, options);
|
|
}
|
|
result.mStatus = DecodeResult::Status::Ok;
|
|
}
|
|
else
|
|
{
|
|
// Reset CNG packet as we get regular RTP packet
|
|
mCngPacket.reset();
|
|
|
|
// Handle here regular RTP packets
|
|
// Check if payload length is ok
|
|
size_t payload_length = rtp.GetPayloadLength();
|
|
size_t rtp_frame_length = mCodec->rtpLength();
|
|
|
|
int tail = rtp_frame_length ? payload_length % rtp_frame_length : 0;
|
|
|
|
if (!tail)
|
|
{
|
|
// Find number of frames
|
|
mFrameCount = mCodec->rtpLength() ? rtp.GetPayloadLength() / mCodec->rtpLength() : 1;
|
|
int frameLength = mCodec->rtpLength() ? mCodec->rtpLength() : (int)rtp.GetPayloadLength();
|
|
|
|
// Save last packet time length
|
|
mLastPacketTimeLength = mFrameCount * mCodec->frameTime();
|
|
|
|
// Decode
|
|
for (int i = 0; i < mFrameCount && !mCodecSettings.mSkipDecode; i++)
|
|
{
|
|
if (options.mSkipDecode)
|
|
mDecodedLength = 0;
|
|
else
|
|
{
|
|
// Decode frame by frame
|
|
auto codecInput =
|
|
std::span{rtp.GetPayloadData() + i * mCodec->rtpLength(), (size_t)frameLength};
|
|
auto codecOutput =
|
|
std::span{(uint8_t*)mDecodedFrame.data(), mDecodedFrame.size() * sizeof(int16_t)};
|
|
auto r = mCodec->decode(codecInput, codecOutput);
|
|
mDecodedLength = r.mDecoded;
|
|
if (mDecodedLength > 0)
|
|
processDecoded(output, options);
|
|
|
|
// What is important - here we may have packet marked as CNG
|
|
if (r.mIsCng)
|
|
mCngPacket = packet;
|
|
}
|
|
}
|
|
result.mStatus = mFrameCount > 0 ? DecodeResult::Status::Ok : DecodeResult::Status::Skip;
|
|
|
|
// Check for bitrate counter
|
|
updateAmrCodecStats(mCodec.get());
|
|
}
|
|
else
|
|
{
|
|
// RTP packet with tail - it should not happen
|
|
result.mStatus = DecodeResult::Status::BadPacket;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
AudioReceiver::DecodeResult AudioReceiver::decodeEmptyTo(Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
// There are two cases
|
|
// First is we have no ready time estimated how much audio should be emitted i.e. audio is decoded right after the
|
|
// next packet arrives. In this case we just skip the analysis - we should not be called in this situation
|
|
if (options.mElapsed == 0ms || !mCodec)
|
|
return {.mStatus = DecodeResult::Status::Skip};
|
|
|
|
// No packet available at all (and no previous CNG packet) - so return the silence
|
|
if (options.mElapsed != 0ms && mCodec)
|
|
{
|
|
Audio::Format fmt = options.mResampleToMainRate ? Audio::Format(AUDIO_SAMPLERATE, 1) : mCodec->getAudioFormat();
|
|
if (mCngPacket)
|
|
{
|
|
// Try to decode it - replay previous audio decoded or use CNG decoder (if payload type is 13)
|
|
if (mCngPacket->rtp()->GetPayloadType() == 13)
|
|
{
|
|
// Using latest CNG packet to produce comfort noise.
|
|
// Clamp the produced amount to the remaining capacity of the output window -
|
|
// the CNG decoder writes straight into its buffer.
|
|
size_t bytesPerMs = (size_t)fmt.rate() / 1000 * sizeof(short) * fmt.channels();
|
|
size_t room = output.capacity() - output.filled();
|
|
int ms =
|
|
bytesPerMs ? (int)std::min<int64_t>(options.mElapsed.count(), (int64_t)(room / bytesPerMs)) : 0;
|
|
if (ms <= 0)
|
|
return {.mStatus = DecodeResult::Status::Skip};
|
|
auto produced = cng().produce(fmt.rate(), ms, (short*)(output.mutableData() + output.filled()), false);
|
|
output.setFilled(output.filled() + produced);
|
|
return {.mStatus = DecodeResult::Status::Ok, .mSamplerate = fmt.rate(), .mChannels = fmt.channels()};
|
|
}
|
|
else
|
|
{
|
|
// Here we have another packet marked as CNG - for another decoder
|
|
// Just decode it +1 time
|
|
return decodePacketTo(output, options, mCngPacket);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Emit silence if codec information is available - it is to properly handle the gaps
|
|
auto avail = output.getTimeLength(fmt);
|
|
if (options.mElapsed > avail)
|
|
{
|
|
// Genuine decoder starvation: the buffer drained below low-water with no
|
|
// packet (and no CNG) to play, so we fill the timeline with silence. Count
|
|
// it for the heartbeat - this is the signature of jitter/clock-drift
|
|
// underruns that carry no packet-loss.
|
|
output.addZero(fmt.sizeFromTime(options.mElapsed - avail));
|
|
mUnderrunCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
mFailedCount++;
|
|
return {.mStatus = DecodeResult::Status::Skip};
|
|
}
|
|
|
|
void MT::AudioReceiver::processDtmf()
|
|
{
|
|
if (mDtmfBuffer.getCount())
|
|
{
|
|
auto fr = mDtmfBuffer.fetch();
|
|
if (fr.mPacket && fr.mStatus == RtpBuffer::FetchResult::Status::RegularPacket)
|
|
mDtmfReceiver.add(fr.mPacket->rtp());
|
|
}
|
|
}
|
|
|
|
void MT::AudioReceiver::logReceiveHeartbeat()
|
|
{
|
|
float t = now_ms();
|
|
if (mStatLogLast == 0.0f)
|
|
{
|
|
// First call - just latch the baselines, nothing to report yet.
|
|
mStatLogLast = t;
|
|
mStatLossLast = mStat.mPacketLoss;
|
|
mStatDropLast = mStat.mPacketDropped;
|
|
return;
|
|
}
|
|
|
|
if (t - mStatLogLast < 5000.0f)
|
|
return;
|
|
|
|
// Deltas over the elapsed window. Loss = missing sequence numbers seen on fetch;
|
|
// dropped = packets discarded by the high-water trim (late/overflow), which is
|
|
// NOT network loss; underruns = decoder starvation (buffer under low-water) that
|
|
// forced silence. These three separate the candidate impairment causes.
|
|
size_t lossDelta = mStat.mPacketLoss - mStatLossLast;
|
|
size_t dropDelta = mStat.mPacketDropped - mStatDropLast;
|
|
|
|
ICELogInfo(<< "[rx-heartbeat] buffered=" << mRtpBuffer.bufferedTime().count() << "ms"
|
|
<< " packets=" << mRtpBuffer.getCount() << " underruns/5s=" << mUnderrunCount << " gaps/5s=" << mGapCount
|
|
<< " loss/5s=" << lossDelta << " dropped/5s=" << dropDelta);
|
|
|
|
mStatLogLast = t;
|
|
mUnderrunCount = 0;
|
|
mGapCount = 0;
|
|
mStatLossLast = mStat.mPacketLoss;
|
|
mStatDropLast = mStat.mPacketDropped;
|
|
}
|
|
|
|
void MT::AudioReceiver::updateDecodingTimeStatistics()
|
|
{
|
|
if (!mDecodeTimestamp)
|
|
mDecodeTimestamp = std::chrono::steady_clock::now();
|
|
else
|
|
{
|
|
auto t = std::chrono::steady_clock::now();
|
|
mStat.mDecodingInterval.process(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(t - *mDecodeTimestamp).count());
|
|
mDecodeTimestamp = t;
|
|
}
|
|
}
|
|
|
|
AudioReceiver::DecodeResult AudioReceiver::getAudioTo(Audio::DataWindow& output, DecodeOptions options)
|
|
{
|
|
// ICELogDebug(<< "getAudioTo() for " << options.mElapsed);
|
|
assert(options.mElapsed != 0ms);
|
|
|
|
// First decode on this receiver: allocate the scratch buffers. Network-MOS-only
|
|
// streams never reach this point, so they never pay for them.
|
|
ensureDecodeBuffers();
|
|
|
|
// Increase counter of requested audio
|
|
mRequestedAudio += options.mElapsed;
|
|
|
|
DecodeResult result = {.mStatus = DecodeResult::Status::Skip};
|
|
|
|
// Process RFC2833 here; it doesn't result in any audio - only callbacks and statistics
|
|
processDtmf();
|
|
|
|
// How much time length audio we produced here
|
|
auto produced = 0ms;
|
|
Audio::Format fmt;
|
|
|
|
// Have we anything from the previous decode attempts ?
|
|
if (mAvailable.filled())
|
|
{
|
|
// Find what audio format is used in mAvailable data
|
|
fmt = options.mResampleToMainRate ? Audio::Format(AUDIO_SAMPLERATE, 1) : mCodec->getAudioFormat();
|
|
|
|
// How much milliseconds are available ?
|
|
auto availTime = mAvailable.getTimeLength(fmt);
|
|
if (availTime != 0ms)
|
|
{
|
|
// How much we can consume from the mAvailable buffer ?
|
|
std::chrono::milliseconds resultTime = std::min(availTime, options.mElapsed);
|
|
|
|
// Number of bytes
|
|
mAvailable.moveTo(output, fmt.sizeFromTime(resultTime));
|
|
|
|
// Increase the counter of produced milliseconds
|
|
produced += resultTime;
|
|
}
|
|
}
|
|
|
|
while (produced < options.mElapsed)
|
|
{
|
|
// Get next packet from buffer
|
|
RtpBuffer::FetchResult fr = mRtpBuffer.fetch();
|
|
|
|
// Decode to mAvailable buffer
|
|
switch (fr.mStatus)
|
|
{
|
|
case RtpBuffer::FetchResult::Status::Gap:
|
|
mGapCount++;
|
|
result = decodeGapTo(mAvailable, options.decreaseElapsedBy(produced));
|
|
break;
|
|
case RtpBuffer::FetchResult::Status::NoPacket:
|
|
result = decodeEmptyTo(mAvailable, options.decreaseElapsedBy(produced));
|
|
break;
|
|
case RtpBuffer::FetchResult::Status::RegularPacket:
|
|
result = decodePacketTo(mAvailable, options.decreaseElapsedBy(produced), fr.mPacket);
|
|
updateDecodeIntervalStatistics();
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
// Was there decoding at all ?
|
|
if (!mCodec)
|
|
break; // No sense to continue - we have no information at all
|
|
|
|
fmt = options.mResampleToMainRate ? Audio::Format(AUDIO_SAMPLERATE, 1) : mCodec->getAudioFormat();
|
|
result.mSamplerate = fmt.rate();
|
|
result.mChannels = fmt.channels();
|
|
|
|
// How much milliseconds we have in audio buffer ?
|
|
auto bufferAvailable = mAvailable.getTimeLength(fmt);
|
|
if (bufferAvailable == 0ms)
|
|
break; // No sense to continue - decoding / CNG / PLC stopped totally
|
|
|
|
// How much data should be moved to result buffer ?
|
|
std::chrono::milliseconds resultTime = std::min(bufferAvailable, options.mElapsed - produced);
|
|
mAvailable.moveTo(output, fmt.sizeFromTime(resultTime));
|
|
produced += resultTime;
|
|
}
|
|
|
|
if (produced != 0ms)
|
|
{
|
|
result.mStatus = DecodeResult::Status::Ok;
|
|
updateDecodingTimeStatistics();
|
|
}
|
|
|
|
mProducedAudio += produced;
|
|
// ICELogDebug(<< "Requested " << options.mElapsed << ", produced " << produced << ", remains " <<
|
|
// mAvailable.getTimeLength(fmt) << ", packets " << getRtpBuffer().getCount());
|
|
|
|
logReceiveHeartbeat();
|
|
return result;
|
|
}
|
|
|
|
void AudioReceiver::ensureDecodeBuffers()
|
|
{
|
|
// Allocate the decode/convert/resample scratch buffers to full capacity on the
|
|
// first decode. mDecodedFrame being empty means none are allocated yet; they
|
|
// are always allocated together, so checking one is enough.
|
|
if (mDecodedFrame.empty())
|
|
{
|
|
mDecodedFrame.resize(MT_MAX_DECODEBUFFER);
|
|
mConvertedFrame.resize(MT_MAX_DECODEBUFFER * 2);
|
|
mResampledFrame.resize(MT_MAX_DECODEBUFFER);
|
|
}
|
|
|
|
if (!mAvailable.capacity())
|
|
{
|
|
// 10 seconds is the maximum length of decoded audio in single step
|
|
// It is important - DTX may produce silence up to few seconds easily
|
|
mAvailable.setCapacity(AUDIO_SAMPLERATE * 10 * sizeof(short));
|
|
}
|
|
}
|
|
|
|
void AudioReceiver::makeMonoAndResample(int rate, int channels)
|
|
{
|
|
// Make mono from stereo - engine works with mono only for now
|
|
mConvertedLength = 0;
|
|
if (channels != AUDIO_CHANNELS)
|
|
{
|
|
if (channels == 1)
|
|
mConvertedLength = Audio::ChannelConverter::monoToStereo(mDecodedFrame.data(), mDecodedLength,
|
|
mConvertedFrame.data(), mDecodedLength * 2);
|
|
else
|
|
mDecodedLength = Audio::ChannelConverter::stereoToMono(mDecodedFrame.data(), mDecodedLength,
|
|
mDecodedFrame.data(), mDecodedLength / 2);
|
|
}
|
|
|
|
void* frames = mConvertedLength ? (void*)mConvertedFrame.data() : (void*)mDecodedFrame.data();
|
|
unsigned length = mConvertedLength ? mConvertedLength : mDecodedLength;
|
|
|
|
Audio::Resampler* r = nullptr;
|
|
switch (rate)
|
|
{
|
|
case 8000:
|
|
r = &mResampler8;
|
|
break;
|
|
case 16000:
|
|
r = &mResampler16;
|
|
break;
|
|
case 32000:
|
|
r = &mResampler32;
|
|
break;
|
|
case 48000:
|
|
r = &mResampler48;
|
|
break;
|
|
default:
|
|
memcpy(mResampledFrame.data(), frames, length);
|
|
mResampledLength = length;
|
|
return;
|
|
}
|
|
|
|
size_t processedInput = 0;
|
|
mResampledLength =
|
|
r->processBuffer(frames, length, processedInput, mResampledFrame.data(), r->getDestLength(length));
|
|
// processedInput result value is ignored - it is always equal to length as internal sample rate is 8/16/32/48K
|
|
}
|
|
|
|
Codec* AudioReceiver::findCodec(int payloadType)
|
|
{
|
|
return ensureCodec(payloadType);
|
|
}
|
|
|
|
|
|
void AudioReceiver::updateAmrCodecStats(Codec* c)
|
|
{
|
|
#if !defined(TARGET_ANDROID) && !defined(TARGET_OPENWRT) && !defined(TARGET_WIN) && !defined(TARGET_RPI) && \
|
|
defined(USE_AMR_CODEC)
|
|
AmrNbCodec* nb = dynamic_cast<AmrNbCodec*>(c);
|
|
AmrWbCodec* wb = dynamic_cast<AmrWbCodec*>(c);
|
|
|
|
if (nb != nullptr)
|
|
{
|
|
mStat.mBitrateSwitchCounter = nb->getSwitchCounter();
|
|
mStat.mCng = nb->getCngCounter();
|
|
}
|
|
else if (wb != nullptr)
|
|
{
|
|
mStat.mBitrateSwitchCounter = wb->getSwitchCounter();
|
|
mStat.mCng = wb->getCngCounter();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
int AudioReceiver::getSize() const
|
|
{
|
|
int result = 0;
|
|
result += sizeof(*this) + mResampler8.getSize() + mResampler16.getSize() + mResampler32.getSize() +
|
|
mResampler48.getSize();
|
|
|
|
if (mCodec)
|
|
; // ToDo: need the way to calculate size of codec instances
|
|
|
|
return result;
|
|
}
|
|
|
|
AudioReceiver::MediaInfo AudioReceiver::infoFor(jrtplib::RTPPacket& p)
|
|
{
|
|
Codec* codec = ensureCodec(p.GetPayloadType());
|
|
if (!codec)
|
|
return {};
|
|
|
|
std::chrono::milliseconds packetTime = 0ms;
|
|
|
|
if (codec->rtpLength() != 0)
|
|
{
|
|
// A G.729 (PT 18) SID frame is shorter than a voice frame but stands for one; other
|
|
// codecs scale with the payload (G.722 at 10 ms ptime carries half a 160-byte frame).
|
|
if (p.GetPayloadType() == 18 && p.GetPayloadLength() % codec->rtpLength())
|
|
{
|
|
int frameCount = static_cast<int>(p.GetPayloadLength() / codec->rtpLength()) + 1;
|
|
packetTime = std::chrono::milliseconds(frameCount * codec->frameTime());
|
|
}
|
|
else
|
|
packetTime = std::chrono::milliseconds(
|
|
lround(double(p.GetPayloadLength()) / codec->rtpLength() * codec->frameTime()));
|
|
}
|
|
else if (typeid(*codec) == typeid(OpusCodec))
|
|
{
|
|
OpusCodec* oc = dynamic_cast<OpusCodec*>(codec);
|
|
assert(oc);
|
|
size_t samplesCount = oc->getNumberOfSamples({p.GetPayloadData(), p.GetPayloadLength()});
|
|
int sampleratePerMs = codec->samplerate() / 1000;
|
|
packetTime = std::chrono::milliseconds(samplesCount / sampleratePerMs);
|
|
}
|
|
else
|
|
{
|
|
packetTime = std::chrono::milliseconds(codec->frameTime());
|
|
}
|
|
|
|
return {packetTime, codec->samplerate()};
|
|
}
|
|
|
|
void AudioReceiver::updateDecodeIntervalStatistics()
|
|
{
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (mLastDecodeTimestamp)
|
|
{
|
|
mIntervalBetweenDecode += std::chrono::duration_cast<std::chrono::microseconds>(now - *mLastDecodeTimestamp);
|
|
mDecodeCount++;
|
|
}
|
|
mLastDecodeTimestamp = now;
|
|
}
|
|
|
|
// ----------------------- DtmfReceiver -------------------
|
|
DtmfReceiver::DtmfReceiver(Statistics& stat) : Receiver(stat) {}
|
|
|
|
DtmfReceiver::~DtmfReceiver() {}
|
|
|
|
void DtmfReceiver::add(const std::shared_ptr<RTPPacket>& p)
|
|
{
|
|
auto ev = DtmfBuilder::parseRfc2833({p->GetPayloadData(), p->GetPayloadLength()});
|
|
if (!ev.mTone)
|
|
return; // Malformed or unknown event payload
|
|
|
|
// A new digit begins when the tone changes, or when the same tone starts
|
|
// again after the previous occurrence ended. Retransmitted start/end
|
|
// packets keep both fields unchanged and are ignored. The end packet of
|
|
// the current tone only updates state - the digit was already reported.
|
|
bool newEvent = (ev.mTone != mEvent) || (mEventEnded && !ev.mEnd);
|
|
|
|
if (newEvent)
|
|
{
|
|
if (mCallback)
|
|
mCallback(ev.mTone);
|
|
|
|
// Queue statistics item
|
|
mStat.mDtmf2833Timeline.emplace_back(
|
|
Dtmf2833Event{.mTone = ev.mTone, .mTimestamp = RtpHelper::toMicroseconds(p->GetReceiveTime())});
|
|
}
|
|
|
|
mEvent = ev.mTone;
|
|
mEventEnded = ev.mEnd;
|
|
}
|