Compare commits

...

2 Commits

Author SHA1 Message Date
dmytro.bogovych 97c4c3aef0 - statistics improved + allow to use virtual microphone AND/OR speaker + minor cleanups 2026-05-18 10:46:21 +03:00
dmytro.bogovych 7cb3b4334f - expose AudioManager::setAudioInput() to inject custom input device
Lets a host inject e.g. NullInputDevice before start() so the default
platform microphone is not constructed, avoiding the RECORD_AUDIO
permission on Android while leaving the real speaker output in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 07:20:47 +03:00
12 changed files with 392 additions and 308 deletions
+21 -18
View File
@@ -26,16 +26,6 @@ AudioManager::~AudioManager()
// stop(); // stop();
} }
AudioManager& AudioManager::instance()
{
static std::shared_ptr<AudioManager> GAudioManager;
if (!GAudioManager)
GAudioManager = std::make_shared<AudioManager>();
return *GAudioManager;
}
void AudioManager::setTerminal(MT::Terminal* terminal) void AudioManager::setTerminal(MT::Terminal* terminal)
{ {
mTerminal = terminal; mTerminal = terminal;
@@ -67,6 +57,7 @@ void AudioManager::start(int usageId)
if (mUsage.obtain(usageId) > 1) if (mUsage.obtain(usageId) > 1)
return; return;
// Maybe it is time to initialize global audio support
if (Audio::OsEngine::instance()) if (Audio::OsEngine::instance())
Audio::OsEngine::instance()->open(); Audio::OsEngine::instance()->open();
@@ -89,14 +80,15 @@ void AudioManager::start(int usageId)
enumerator->open(Audio::myMicrophone); enumerator->open(Audio::myMicrophone);
int inputIndex = enumerator->indexOfDefaultDevice(); int inputIndex = enumerator->indexOfDefaultDevice();
// Construct and set to terminal's audio pair input device // Construct default platform input device
if (usageId != atNull) if (usageId != atNull)
mAudioInput = Audio::PInputDevice(Audio::InputDevice::make(enumerator->idAt(inputIndex))); mAudioInput = Audio::PInputDevice(Audio::InputDevice::make(enumerator->idAt(inputIndex)));
else else
mAudioInput = Audio::PInputDevice(new Audio::NullInputDevice()); mAudioInput = Audio::PInputDevice(new Audio::NullInputDevice());
mTerminal->audio()->setInput(mAudioInput);
} }
// Bind input to the terminal's device pair regardless of whether it was
// just constructed or externally injected via setAudioInput().
mTerminal->audio()->setInput(mAudioInput);
if (!mAudioOutput) if (!mAudioOutput)
{ {
@@ -104,7 +96,7 @@ void AudioManager::start(int usageId)
enumerator->open(Audio::mySpeaker); enumerator->open(Audio::mySpeaker);
int outputIndex = enumerator->indexOfDefaultDevice(); int outputIndex = enumerator->indexOfDefaultDevice();
// Construct and set terminal's audio pair output device // Construct default platform output device
if (usageId != atNull) if (usageId != atNull)
{ {
if (outputIndex >= enumerator->count()) if (outputIndex >= enumerator->count())
@@ -115,9 +107,8 @@ void AudioManager::start(int usageId)
} }
else else
mAudioOutput = Audio::POutputDevice(new Audio::NullOutputDevice()); mAudioOutput = Audio::POutputDevice(new Audio::NullOutputDevice());
mTerminal->audio()->setOutput(mAudioOutput);
} }
mTerminal->audio()->setOutput(mAudioOutput);
} }
// Open audio // Open audio
@@ -167,6 +158,18 @@ void AudioManager::stop(int usageId)
} }
} }
void AudioManager::setAudioInput(Audio::PInputDevice input)
{
LOCK_MANAGER;
mAudioInput = std::move(input);
}
void AudioManager::setAudioOutput(Audio::POutputDevice output)
{
LOCK_MANAGER;
mAudioOutput = std::move(output);
}
void AudioManager::startPlayFile(int usageId, const std::string& path, AudioTarget target, LoopMode lm, int timelimit) void AudioManager::startPlayFile(int usageId, const std::string& path, AudioTarget target, LoopMode lm, int timelimit)
{ {
// Check if file exists // Check if file exists
@@ -202,6 +205,6 @@ void AudioManager::process()
mPlayer.releasePlayed(); mPlayer.releasePlayed();
std::vector<int> ids; std::vector<int> ids;
mTerminal->audio()->player().retrieveUsageIds(ids); mTerminal->audio()->player().retrieveUsageIds(ids);
for (unsigned i=0; i<ids.size(); i++) for (int id : ids)
stop(ids[i]); stop(id);
} }
+7 -1
View File
@@ -37,7 +37,7 @@ public:
AudioManager(); AudioManager();
virtual ~AudioManager(); virtual ~AudioManager();
static AudioManager& instance(); // static AudioManager& instance();
// Enforces to close audio devices. Used to shutdown AudioManager on exit from application // Enforces to close audio devices. Used to shutdown AudioManager on exit from application
void close(); void close();
@@ -53,6 +53,12 @@ public:
void start(int usageId); void start(int usageId);
void stop(int usageId); void stop(int usageId);
// Inject a custom input device. Must be called before start(): when set,
// start() skips construction of the default platform microphone. Pass an
// empty pointer to clear the override.
void setAudioInput(Audio::PInputDevice input);
void setAudioOutput(Audio::POutputDevice output);
enum AudioTarget enum AudioTarget
{ {
atNull, atNull,
+13 -9
View File
@@ -3,10 +3,11 @@
#include "helper/HL_String.h" #include "helper/HL_String.h"
#include "helper/HL_StreamState.h" #include "helper/HL_StreamState.h"
#include "helper/HL_VariantMap.h" #include "helper/HL_VariantMap.h"
#include "helper/HL_CsvReader.h" // #include "helper/HL_CsvReader.h"
#include "helper/HL_Base64.h" // #include "helper/HL_Base64.h"
#include "media/MT_CodecList.h" #include "media/MT_CodecList.h"
#include <fstream> #include "audio/Audio_Null.h"
// #include <fstream>
const std::string Status_Ok = "ok"; const std::string Status_Ok = "ok";
@@ -226,12 +227,6 @@ void AgentImpl::processStart(JsonCpp::Value& request, JsonCpp::Value &answer)
for (int i=0; i<cl.count(); i++) for (int i=0; i<cl.count(); i++)
priorityConfig->at(i) = i; priorityConfig->at(i) = i;
// Disable dynamic payload codec types - commented for now
// if (cl.codecAt(i).payloadType() < 96)
// priorityConfig->at(i) = i;
// else
// priorityConfig->at(i) = -1;
config()[CONFIG_CODEC_PRIORITY] = priorityConfig; config()[CONFIG_CODEC_PRIORITY] = priorityConfig;
// Enable audio // Enable audio
@@ -341,6 +336,11 @@ void AgentImpl::processStartSession(JsonCpp::Value& request, JsonCpp::Value& ans
return; return;
} }
if (request["use_null_mic"].asBool())
mAudioManager->setAudioInput(std::make_shared<Audio::NullInputDevice>());
if (request["use_null_spk"].asBool())
mAudioManager->setAudioOutput(std::make_shared<Audio::NullOutputDevice>());
mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull); mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull);
auto sessionIter = mSessionMap.find(request["session_id"].asInt()); auto sessionIter = mSessionMap.find(request["session_id"].asInt());
@@ -440,6 +440,10 @@ void AgentImpl::processAcceptSession(JsonCpp::Value& request, JsonCpp::Value& an
else else
{ {
// Ensure audio manager is here // Ensure audio manager is here
if (request["use_null_mic"].asBool())
mAudioManager->setAudioInput(std::make_shared<Audio::NullInputDevice>());
if (request["use_null_spk"].asBool())
mAudioManager->setAudioOutput(std::make_shared<Audio::NullOutputDevice>());
mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull); mAudioManager->start(mUseNativeAudio ? AudioManager::atReceiver : AudioManager::atNull);
// Accept session on SIP level // Accept session on SIP level
+15 -9
View File
@@ -5,8 +5,9 @@
#define LOG_SUBSYSTEM "audio" #define LOG_SUBSYSTEM "audio"
using namespace Audio; using namespace Audio;
using namespace std::chrono_literals;
NullTimer::NullTimer(int interval, Delegate *delegate, const char* name) NullTimer::NullTimer(std::chrono::milliseconds interval, Delegate *delegate, const char* name)
:mShutdown(false), mDelegate(delegate), mInterval(interval), mThreadName(name) :mShutdown(false), mDelegate(delegate), mInterval(interval), mThreadName(name)
{ {
start(); start();
@@ -32,23 +33,23 @@ void NullTimer::stop()
void NullTimer::run() void NullTimer::run()
{ {
mTail = 0; mTail = 0us;
while (!mShutdown) while (!mShutdown)
{ {
// Get current timestamp // Get current timestamp
std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now(); std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now();
while (mTail >= mInterval * 1000) while (mTail >= mInterval)
{ {
if (mDelegate) if (mDelegate)
mDelegate->onTimerSignal(*this); mDelegate->onTimerSignal(*this);
mTail -= mInterval * 1000; mTail -= mInterval;
} }
// Sleep for mInterval - mTail milliseconds // Sleep for mInterval - mTail milliseconds
std::this_thread::sleep_for(std::chrono::microseconds(mInterval * 1000 - mTail)); std::this_thread::sleep_for(mInterval - mTail);
mTail += (int)std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - timestamp).count(); mTail = mTail + std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now() - timestamp);
} }
} }
@@ -65,16 +66,19 @@ NullInputDevice::~NullInputDevice()
bool NullInputDevice::open() bool NullInputDevice::open()
{ {
ICELogInfo(<< "Starting NullInputDevice for " << AUDIO_MIC_BUFFER_LENGTH << "ms buffers");
mBuffer = malloc(AUDIO_MIC_BUFFER_SIZE); mBuffer = malloc(AUDIO_MIC_BUFFER_SIZE);
memset(mBuffer, 0, AUDIO_MIC_BUFFER_SIZE); memset(mBuffer, 0, AUDIO_MIC_BUFFER_SIZE);
mTimeCounter = 0; mDataCounter = 0; mTimeCounter = 0; mDataCounter = 0;
// Creation of timer starts it also. So first onTimerSignal can come even before open() returns. // Creation of timer starts it also. So first onTimerSignal can come even before open() returns.
mTimer = std::make_shared<NullTimer>(AUDIO_MIC_BUFFER_LENGTH, this, "NullMicrophoneThread"); mTimer = std::make_shared<NullTimer>(std::chrono::milliseconds(AUDIO_MIC_BUFFER_LENGTH), this, "null_mic");
return true; return true;
} }
void NullInputDevice::internalClose() void NullInputDevice::internalClose()
{ {
ICELogInfo(<< "Stopping NullInputDevice");
mTimer.reset(); mTimer.reset();
if (mBuffer) if (mBuffer)
{ {
@@ -88,10 +92,12 @@ void NullInputDevice::close()
{ {
internalClose(); internalClose();
} }
Format NullInputDevice::getFormat() Format NullInputDevice::getFormat()
{ {
assert (Format().sizeFromTime(AUDIO_MIC_BUFFER_LENGTH) == AUDIO_MIC_BUFFER_SIZE); assert (Format().sizeFromTime(AUDIO_MIC_BUFFER_LENGTH) == AUDIO_MIC_BUFFER_SIZE);
return Format();
return {}; // Return library-define default format
} }
void NullInputDevice::onTimerSignal(NullTimer& timer) void NullInputDevice::onTimerSignal(NullTimer& timer)
@@ -119,7 +125,7 @@ bool NullOutputDevice::open()
mTimeCounter = 0; mDataCounter = 0; mTimeCounter = 0; mDataCounter = 0;
mBuffer = malloc(AUDIO_SPK_BUFFER_SIZE); mBuffer = malloc(AUDIO_SPK_BUFFER_SIZE);
// Creation of timer starts it also. So first onSpkData() can come before open() returns even. // Creation of timer starts it also. So first onSpkData() can come before open() returns even.
mTimer = std::make_shared<NullTimer>(AUDIO_SPK_BUFFER_LENGTH, this, "NullSpeakerThread"); mTimer = std::make_shared<NullTimer>(std::chrono::milliseconds(AUDIO_SPK_BUFFER_LENGTH), this, "null_spk");
return true; return true;
} }
+7 -6
View File
@@ -2,6 +2,7 @@
#define __AUDIO_NULL_H #define __AUDIO_NULL_H
#include <thread> #include <thread>
#include <chrono>
#include "Audio_Interface.h" #include "Audio_Interface.h"
namespace Audio namespace Audio
@@ -17,10 +18,10 @@ namespace Audio
protected: protected:
std::thread mWorkerThread; std::thread mWorkerThread;
volatile bool mShutdown; std::atomic_bool mShutdown = {false};
Delegate* mDelegate; Delegate* mDelegate = nullptr;
int mInterval, // Interval - wanted number of milliseconds std::chrono::milliseconds mInterval; // Interval - wanted number of milliseconds
mTail; // Number of milliseconds that can be sent immediately to sink std::chrono::microseconds mTail; // Number of milliseconds that can be sent immediately to sink
std::string mThreadName; std::string mThreadName;
void start(); void start();
@@ -28,7 +29,7 @@ namespace Audio
void run(); void run();
public: public:
/* Interval is in milliseconds. */ /* Interval is in milliseconds. */
NullTimer(int interval, Delegate* delegate, const char* name = nullptr); NullTimer(std::chrono::milliseconds interval, Delegate* delegate, const char* name = nullptr);
~NullTimer(); ~NullTimer();
}; };
@@ -83,8 +84,8 @@ namespace Audio
std::tstring nameAt(int index) override; std::tstring nameAt(int index) override;
int idAt(int index) override; int idAt(int index) override;
int indexOfDefaultDevice() override; int indexOfDefaultDevice() override;
}; };
} }
#endif #endif
+1 -1
View File
@@ -108,7 +108,7 @@ enum
CONFIG_ACCOUNT, // VariantMap with account configuration CONFIG_ACCOUNT, // VariantMap with account configuration
CONFIG_EXTERNALIP, // Use external/public IP in outgoing requests CONFIG_EXTERNALIP, // Use external/public IP in outgoing requests
CONFIG_OWN_DNS, // Use predefined DNS servers CONFIG_OWN_DNS, // Use predefined DNS servers
CONFIG_REGID // reg-id value from RFC5626 CONFIG_REGID // reg-id value from RFC5626,
}; };
// Conntype parameter for OnSessionEstablished event // Conntype parameter for OnSessionEstablished event
+1 -1
View File
@@ -453,7 +453,7 @@ void Session::getSessionInfo(Session::InfoOptions options, VariantMap& info)
media = &stream; media = &stream;
MT::Statistics s = stream.provider()->getStatistics(); MT::Statistics s = stream.provider()->getStatistics();
info[SessionInfo_NetworkMos] = static_cast<float>(s.calculateMos(4.14)); info[SessionInfo_NetworkMos] = static_cast<float>(s.calculateMos());
info[SessionInfo_AudioCodec] = s.mCodecName; info[SessionInfo_AudioCodec] = s.mCodecName;
stat += s; stat += s;
+6
View File
@@ -344,14 +344,20 @@ void AudioStream::dataArrived(PDatagramSocket s, const void* buffer, int length,
} }
mStat.mReceived += length; mStat.mReceived += length;
auto& perDst = mStat.mPerDestination[source];
perDst.mReceivedBytes += length;
if (RtpHelper::isRtp(mReceiveBuffer, receiveLength)) if (RtpHelper::isRtp(mReceiveBuffer, receiveLength))
{ {
if (!mStat.mFirstRtpTime) if (!mStat.mFirstRtpTime)
mStat.mFirstRtpTime = std::chrono::steady_clock::now(); mStat.mFirstRtpTime = std::chrono::steady_clock::now();
mStat.mReceivedRtp++; mStat.mReceivedRtp++;
perDst.mReceivedRtp++;
} }
else else
{
mStat.mReceivedRtcp++; mStat.mReceivedRtcp++;
perDst.mReceivedRtcp++;
}
mRtpSession.Poll(); // maybe it is extra with external transmitter mRtpSession.Poll(); // maybe it is extra with external transmitter
bool hasData = mRtpSession.GotoFirstSourceWithData(); bool hasData = mRtpSession.GotoFirstSourceWithData();
+6
View File
@@ -47,6 +47,9 @@ bool NativeRtpSender::SendRTP(const void *data, size_t len)
mSocket.mRtp->sendDatagram(mTarget.mRtp, mSendBuffer, sendLength); mSocket.mRtp->sendDatagram(mTarget.mRtp, mSendBuffer, sendLength);
mStat.mSentRtp++; mStat.mSentRtp++;
mStat.mSent += len; mStat.mSent += len;
auto& perDst = mStat.mPerDestination[mTarget.mRtp];
perDst.mSentRtp++;
perDst.mSentBytes += len;
return true; return true;
} }
@@ -73,6 +76,9 @@ bool NativeRtpSender::SendRTCP(const void *data, size_t len)
mSocket.mRtcp->sendDatagram(mTarget.mRtcp, mSendBuffer, sendLength); mSocket.mRtcp->sendDatagram(mTarget.mRtcp, mSendBuffer, sendLength);
mStat.mSentRtcp++; mStat.mSentRtcp++;
mStat.mSent += len; mStat.mSent += len;
auto& perDst = mStat.mPerDestination[mTarget.mRtcp];
perDst.mSentRtcp++;
perDst.mSentBytes += len;
return true; return true;
} }
+38 -3
View File
@@ -2,6 +2,7 @@
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include <sstream>
#include "MT_Statistics.h" #include "MT_Statistics.h"
#define LOG_SUBSYSTEM "media" #define LOG_SUBSYSTEM "media"
@@ -203,7 +204,7 @@ void Statistics::calculateBurstr(double* burstr, double* lossr) const
*lossr = 0; *lossr = 0;
} }
double Statistics::calculateMos(double maximalMos) const double Statistics::calculateMos() const
{ {
// Network MOS via the simplified ITU-T G.107 E-Model: // Network MOS via the simplified ITU-T G.107 E-Model:
// //
@@ -211,7 +212,7 @@ double Statistics::calculateMos(double maximalMos) const
// Id = 0.024*d + 0.11*max(0, d - 177.3) // Id = 0.024*d + 0.11*max(0, d - 177.3)
// Ie_eff = Ie + (95 - Ie) * Ppl / (Ppl + Bpl) (BurstR=1) // Ie_eff = Ie + (95 - Ie) * Ppl / (Ppl + Bpl) (BurstR=1)
// R = 93.2 - Id - Ie_eff (clamped to [0,100]) // R = 93.2 - Id - Ie_eff (clamped to [0,100])
// MOS = 1 + 0.035*R + 7e-6*R*(R-60)*(100-R) (clamped to [1, maximalMos]) // MOS = 1 + 0.035*R + 7e-6*R*(R-60)*(100-R) (clamped >= 1)
// //
// Ie/Bpl are looked up from a per-codec table; safe defaults are used // Ie/Bpl are looked up from a per-codec table; safe defaults are used
// when the codec is unknown. // when the codec is unknown.
@@ -254,7 +255,6 @@ double Statistics::calculateMos(double maximalMos) const
mos = 1.0 + 0.035 * R + 7e-6 * R * (R - 60.0) * (100.0 - R); mos = 1.0 + 0.035 * R + 7e-6 * R * (R - 60.0) * (100.0 - R);
if (mos < 1.0) mos = 1.0; if (mos < 1.0) mos = 1.0;
if (mos > maximalMos) mos = maximalMos;
return mos; return mos;
} }
@@ -306,6 +306,17 @@ Statistics& Statistics::operator += (const Statistics& src)
mRemotePeer = src.mRemotePeer; mRemotePeer = src.mRemotePeer;
mSsrc = src.mSsrc; mSsrc = src.mSsrc;
for (const auto& [addr, counts]: src.mPerDestination)
{
auto& dst = mPerDestination[addr];
dst.mSentRtp += counts.mSentRtp;
dst.mSentRtcp += counts.mSentRtcp;
dst.mSentBytes += counts.mSentBytes;
dst.mReceivedRtp += counts.mReceivedRtp;
dst.mReceivedRtcp += counts.mReceivedRtcp;
dst.mReceivedBytes += counts.mReceivedBytes;
}
return *this; return *this;
} }
@@ -330,6 +341,19 @@ Statistics& Statistics::operator -= (const Statistics& src)
mCodecCount[codecStat.first] -= codecStat.second; mCodecCount[codecStat.first] -= codecStat.second;
} }
for (const auto& [addr, counts]: src.mPerDestination)
{
auto it = mPerDestination.find(addr);
if (it == mPerDestination.end())
continue;
it->second.mSentRtp -= counts.mSentRtp;
it->second.mSentRtcp -= counts.mSentRtcp;
it->second.mSentBytes -= counts.mSentBytes;
it->second.mReceivedRtp -= counts.mReceivedRtp;
it->second.mReceivedRtcp -= counts.mReceivedRtcp;
it->second.mReceivedBytes -= counts.mReceivedBytes;
}
return *this; return *this;
} }
@@ -345,5 +369,16 @@ std::string Statistics::toString() const
<< ", decode requested: " << mDecodeRequested.average() << ", decode requested: " << mDecodeRequested.average()
<< ", packet interval: " << mPacketInterval.average(); << ", packet interval: " << mPacketInterval.average();
for (const auto& [addr, counts]: mPerDestination)
{
oss << "; peer " << addr.toBriefStdString()
<< " sent rtp=" << counts.mSentRtp
<< "/rtcp=" << counts.mSentRtcp
<< "/bytes=" << counts.mSentBytes
<< ", received rtp=" << counts.mReceivedRtp
<< "/rtcp=" << counts.mReceivedRtcp
<< "/bytes=" << counts.mReceivedBytes;
}
return oss.str(); return oss.str();
} }
+19 -2
View File
@@ -7,7 +7,7 @@
#include "helper/HL_Statistics.h" #include "helper/HL_Statistics.h"
#include "helper/HL_Types.h" #include "helper/HL_Types.h"
#include "helper/HL_InternetAddress.h" #include "ice/ICEAddress.h"
#include "jrtplib/src/rtptimeutilities.h" #include "jrtplib/src/rtptimeutilities.h"
#include "jrtplib/src/rtppacket.h" #include "jrtplib/src/rtppacket.h"
@@ -73,6 +73,19 @@ struct Dtmf2833Event
std::chrono::microseconds mTimestamp; std::chrono::microseconds mTimestamp;
}; };
// Per-remote-address packet/byte counters. Split out so an aggregate
// Statistics can break its totals down by destination/source — useful
// for diagnosing ICE candidate switches or symmetric-RTP issues.
struct DestinationStats
{
size_t mSentRtp = 0;
size_t mSentRtcp = 0;
size_t mSentBytes = 0;
size_t mReceivedRtp = 0;
size_t mReceivedRtcp = 0;
size_t mReceivedBytes = 0;
};
class Statistics class Statistics
{ {
public: public:
@@ -88,6 +101,10 @@ public:
mPacketDropped = 0, // Number of dropped packets (due to time unsync when playing)б mPacketDropped = 0, // Number of dropped packets (due to time unsync when playing)б
mIllegalRtp = 0; // Number of rtp packets with bad payload type mIllegalRtp = 0; // Number of rtp packets with bad payload type
// Per-remote-address breakdown of the totals above. Keyed by the remote
// RTP/RTCP socket address (NAT-mapped, after ICE selection).
std::map<ice::NetworkAddress, DestinationStats> mPerDestination;
TestResult<float> mDecodingInterval, // Average interval on call to packet decode TestResult<float> mDecodingInterval, // Average interval on call to packet decode
mDecodeRequested, // Average amount of requested audio frames to play mDecodeRequested, // Average amount of requested audio frames to play
mPacketInterval; // Average interval between packet adding to jitter buffer mPacketInterval; // Average interval between packet adding to jitter buffer
@@ -115,7 +132,7 @@ public:
// It is to calculate network MOS // It is to calculate network MOS
void calculateBurstr(double* burstr, double* loss) const; void calculateBurstr(double* burstr, double* loss) const;
double calculateMos(double maximalMos) const; double calculateMos() const;
Statistics(); Statistics();
~Statistics(); ~Statistics();