yate/modules/analyzer.cpp

782 lines
19 KiB
C++

/**
* analyzer.cpp
* This file is part of the YATE Project http://YATE.null.ro
*
* Test call generator and audio quality analyzer
*
* Yet Another Telephony Engine - a fully featured software PBX and IVR
* Copyright (C) 2004-2006 Null Team
*
* FFT routine taken from Murphy McCauley's FFT DLL based in turn on the
* work of Don Cross <dcross@intersrv.com>
* See http://www.fullspectrum.com/deeth/programming/fft.html
* and http://www.constantthought.com/
* Thank both for a sleek routine that doesn't even fill a floppy ;-)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <yatephone.h>
// minimum allowed for the maximum
#define ALLOW_MIN 2500.0
// threshold from max we consider a peak
#define PEAKS_THR 0.015
// expected number of peaks
#define PEAKS_NUM 2
#include <string.h>
#include <stdio.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#ifndef M_2PI
#define M_2PI (2*M_PI)
#endif
#ifndef M_4PI
#define M_4PI (4*M_PI)
#endif
using namespace TelEngine;
namespace { // anonymous
// Asynchronous FFT on a power of 2 sample buffer
class AsyncFFT : public Thread
{
public:
enum WinType {
None = 0,
Rectangle = None,
Triangle,
Bartlett = Triangle,
Hanning,
Hamming,
Blackman,
FlatTop
};
virtual ~AsyncFFT();
static AsyncFFT* create(unsigned int length, WinType window = Rectangle, Priority prio = Low);
inline unsigned int samples() const
{ return m_length; }
inline unsigned int length() const
{ return m_length >> 1; }
inline bool ready() const
{ return m_ready; }
double at(int index) const;
inline double operator[](int index) const
{ return at(index); }
bool prepare(const short* samp);
inline void stop()
{ m_notify = 0; m_stop = true; }
virtual void run();
inline void setNotify(Runnable* notified = 0)
{ m_notify = notified; }
private:
AsyncFFT(unsigned int length, WinType window, Priority prio);
void buildWindow(WinType window);
unsigned int revBits(unsigned int index);
void compute();
bool m_ready;
bool m_start;
bool m_stop;
Runnable* m_notify;
unsigned int m_length;
double* m_window;
double* m_real;
double* m_imag;
unsigned int m_nBits;
const char* m_winName;
};
class AnalyzerCons : public DataConsumer, public Runnable
{
YCLASS(AnalyzerCons,DataConsumer)
public:
AnalyzerCons(const String& type, const char* window = 0);
virtual ~AnalyzerCons();
virtual void Consume(const DataBlock& data, unsigned long tStamp);
virtual void statusParams(String& str);
virtual void run();
protected:
DataBlock m_data;
u_int64_t m_timeStart;
unsigned long m_tsStart;
unsigned int m_tsGapCount;
unsigned long m_tsGapLength;
AsyncFFT* m_spectrum;
unsigned long m_total;
unsigned long m_valid;
bool m_analyze;
};
class AnalyzerChan : public Channel
{
public:
AnalyzerChan(const String& type, bool outgoing, const char* window = 0);
virtual ~AnalyzerChan();
virtual void statusParams(String& str);
virtual bool callRouted(Message& msg);
virtual bool msgRinging(Message& msg);
virtual bool msgAnswered(Message& msg);
virtual void checkTimers(Message& msg, const Time& tmr);
void startChannel(NamedList& params);
void addSource();
void addConsumer();
protected:
void setDuration(NamedList& params);
void localParams(String& str);
u_int64_t m_stopTime;
u_int64_t m_timeStart;
unsigned long m_timeRoute;
unsigned long m_timeRing;
unsigned long m_timeAnswer;
String m_window;
};
class AttachHandler : public MessageHandler
{
public:
AttachHandler() : MessageHandler("chan.attach") { }
virtual bool received(Message& msg);
};
class AnalyzerDriver : public Driver
{
public:
AnalyzerDriver();
virtual ~AnalyzerDriver();
virtual void initialize();
virtual bool msgExecute(Message& msg, String& dest);
bool startCall(NamedList& params, const String& dest);
private:
AttachHandler* m_handler;
};
INIT_PLUGIN(AnalyzerDriver);
static TokenDict dict_windows[] = {
{ "rectangle", AsyncFFT::Rectangle },
{ "no", AsyncFFT::None },
{ "none", AsyncFFT::None },
{ "triangle", AsyncFFT::Triangle },
{ "bartlett", AsyncFFT::Bartlett },
{ "hanning", AsyncFFT::Hanning },
{ "hamming", AsyncFFT::Hamming },
{ "blackman", AsyncFFT::Blackman },
{ "flattop", AsyncFFT::FlatTop },
{ 0, 0 }
};
static Mutex s_mutex;
static int s_res = 1;
static const char* printTime(char* buf,unsigned long usec)
{
switch (s_res) {
case 2:
// microsecond resolution
sprintf(buf,"%u.%06u",(unsigned int)(usec / 1000000),(unsigned int)(usec % 1000000));
break;
case 1:
// millisecond resolution
usec = (usec + 500) / 1000;
sprintf(buf,"%u.%03u",(unsigned int)(usec / 1000),(unsigned int)(usec % 1000));
break;
default:
// 1-second resolution
usec = (usec + 500000) / 1000000;
sprintf(buf,"%u",(unsigned int)usec);
}
return buf;
}
AsyncFFT* AsyncFFT::create(unsigned int length, WinType window, Priority prio)
{
if (length < 2)
return 0;
// thanks to 'byang' for this cute power of two test!
if (length & (length - 1))
return 0;
AsyncFFT* fft = new AsyncFFT(length,window,prio);
if (fft->startup())
return fft;
delete fft;
return 0;
}
AsyncFFT::AsyncFFT(unsigned int length, WinType window, Priority prio)
: Thread("AsyncFFT",prio),
m_ready(false), m_start(false), m_stop(false), m_notify(0),
m_length(0), m_window(0), m_real(0), m_imag(0), m_nBits(0), m_winName(0)
{
DDebug(&__plugin,DebugAll,"AsyncFFT::AsyncFFT(%u) [%p]",length,this);
for (unsigned int i = 0; i <= 256 ;i++)
if (length & (1 << i)) {
m_nBits = i;
break;
}
if (!m_nBits)
return;
m_length = length;
m_real = new double[m_length];
m_imag = new double[m_length];
buildWindow(window);
}
AsyncFFT::~AsyncFFT()
{
DDebug(&__plugin,DebugAll,"AsyncFFT::~AsyncFFT() [%p]",this);
m_notify = 0;
m_ready = false;
m_start = false;
delete[] m_real;
delete[] m_imag;
delete[] m_window;
}
void AsyncFFT::buildWindow(WinType window)
{
m_winName = lookup(window,dict_windows);
if (window == Rectangle)
return;
m_window = new double[m_length];
unsigned int n2 = m_length >> 1;
for (unsigned int i = 0; i < m_length; i++) {
double omega = i * M_2PI / m_length;
switch (window) {
case Triangle:
{
int k = i - n2;
if (k > 0)
k = -k;
k += n2;
m_window[i] = k*1.0/n2;
}
break;
case Hanning:
m_window[i] = 0.5 - 0.5 * ::cos(omega);
break;
case Hamming:
m_window[i] = 0.54 - 0.46 * ::cos(omega);
break;
case Blackman:
m_window[i] = 0.42 - 0.5 * ::cos(omega) + 0.08 * ::cos(2*omega);
break;
case FlatTop:
m_window[i] = 0.2810639 - 0.5208972 * ::cos(omega) + 0.1980399 * ::cos(2*omega);
break;
default:
m_window[i] = 1.0;
}
}
}
double AsyncFFT::at(int index) const
{
if ((index < 0) || ((unsigned int)index > (m_length >> 1)))
return 0.0;
if (!m_ready)
return 0.0;
return m_real[index];
}
void AsyncFFT::run()
{
DDebug(&__plugin,DebugAll,"AsyncFFT::run() [%p]",this);
while (!m_stop) {
while (!m_start) {
Thread::msleep(5);
if (m_stop)
return;
}
m_ready = false;
compute();
m_ready = true;
s_mutex.lock();
if (m_notify)
m_notify->run();
s_mutex.unlock();
m_start = false;
}
}
bool AsyncFFT::prepare(const short* samp)
{
if (m_start || m_stop || !(samp && m_real))
return false;
m_ready = false;
XDebug(&__plugin,DebugAll,"Preparing FFT buffer from %u samples [%p]",m_length,this);
unsigned int i, j;
for (i = 0; i < m_length; i++) {
j = revBits(i);
if (m_window)
m_real[i] = m_window[j] * samp[j];
else
m_real[i] = samp[j];
m_imag[i] = 0.0;
}
m_start = true;
return true;
}
unsigned int AsyncFFT::revBits(unsigned int index)
{
unsigned int i, rev;
for (i = rev = 0; i < m_nBits; i++) {
rev = (rev << 1) | (index & 1);
index >>= 1;
}
return rev;
}
void AsyncFFT::compute()
{
#ifdef XDEBUG
Debug(&__plugin,DebugInfo,"Computing FFT with length %u [%p]",m_length,this);
Time t;
#endif
unsigned int i, j, n;
unsigned int blockEnd = 1;
for (unsigned int blockSize = 2; blockSize <= m_length; blockSize <<= 1) {
double delta_angle = M_2PI / blockSize;
double sm1 = ::sin(-delta_angle);
double sm2 = ::sin(-2 * delta_angle);
double cm1 = ::cos(-delta_angle);
double cm2 = ::cos(-2 * delta_angle);
double w = 2 * cm1;
double ar[3], ai[3];
for (i = 0; i < m_length; i += blockSize) {
ar[1] = cm1;
ai[1] = sm1;
ar[2] = cm2;
ai[2] = sm2;
if (m_stop)
return;
for (j = i, n = 0; n < blockEnd; j++, n++) {
ar[0] = w*ar[1] - ar[2];
ar[2] = ar[1];
ar[1] = ar[0];
ai[0] = w*ai[1] - ai[2];
ai[2] = ai[1];
ai[1] = ai[0];
unsigned int k = j + blockEnd;
double tr = ar[0]*m_real[k] - ai[0]*m_imag[k];
double ti = ar[0]*m_imag[k] + ai[0]*m_real[k];
m_real[k] = m_real[j] - tr;
m_imag[k] = m_imag[j] - ti;
m_real[j] += tr;
m_imag[j] += ti;
}
}
blockEnd = blockSize;
}
n = m_length >> 1;
for (i = 0; i < n; i++)
m_real[i] = ::sqrt(m_real[i]*m_real[i] + m_imag[i]*m_imag[i]) / n;
#ifdef XDEBUG
Debug(&__plugin,DebugInfo,"Computing FFT with length %u took " FMT64U " usec [%p]",
m_length,Time::now()-t,this);
#endif
#ifdef XDEBUG
for (i = 0; i < n; i++)
Debug(&__plugin,DebugAll,"fft[%u] = %0.2f",i,m_real[i]);
#endif
}
AnalyzerCons::AnalyzerCons(const String& type, const char* window)
: m_timeStart(0), m_tsStart(0), m_tsGapCount(0), m_tsGapLength(0),
m_spectrum(false), m_total(0), m_valid(0), m_analyze(false)
{
DDebug(&__plugin,DebugAll,"AnalyzerCons::AnalyzerCons('%s') [%p]",
type.c_str(),this);
unsigned int len = 0;
if ((type == "probe") || type.startsWith("tone/probe")) {
len = 256;
m_analyze = true;
m_spectrum = AsyncFFT::create(len,(AsyncFFT::WinType)lookup(window,dict_windows,AsyncFFT::Rectangle));
m_spectrum->setNotify(this);
return;
}
else if (type == "fft1024")
len = 1024;
else if (type == "fft512")
len = 512;
else if (type == "fft256")
len = 256;
else if (type == "fft128")
len = 128;
else if (type == "fft64")
len = 64;
if (len) {
m_spectrum = AsyncFFT::create(len,(AsyncFFT::WinType)lookup(window,dict_windows,AsyncFFT::Triangle));
m_spectrum->setNotify(this);
}
}
AnalyzerCons::~AnalyzerCons()
{
DDebug(&__plugin,DebugAll,"AnalyzerCons::~AnalyzerCons() %p [%p]",m_spectrum,this);
s_mutex.lock();
if (m_spectrum) {
AsyncFFT* tmp = m_spectrum;
m_spectrum = 0;
tmp->stop();
}
s_mutex.unlock();
}
void AnalyzerCons::Consume(const DataBlock& data, unsigned long tStamp)
{
if (!m_timeStart) {
// the first data block may be garbled or have bad timestamp - ignore
m_timeStart = Time::now();
m_tsStart = tStamp;
return;
}
unsigned int samples = data.length() / 2;
long delta = tStamp - timeStamp() - samples;
if (delta) {
XDebug(&__plugin,DebugMild,"Got %u samples with ts=%lu but old ts=%lu (delta=%ld)",
samples,tStamp,timeStamp(),delta);
if (delta < 0)
delta = -delta;
m_tsGapCount++;
m_tsGapLength += delta;
}
if (!m_spectrum)
return;
m_data += data;
unsigned int len = 2 * m_spectrum->samples();
if (m_data.length() < len)
return;
// limit the length of the buffer
int toCut = data.length() - (2 * len);
if (toCut > 0) {
DDebug(&__plugin,DebugInfo,"Dropping %d samples [%p]",toCut/2,this);
m_data.cut(-toCut);
}
if (m_spectrum->prepare((const short*)m_data.data()))
m_data.cut(-(int)len);
}
void AnalyzerCons::run()
{
// this method is called with the mutex hold
if (!m_spectrum)
return;
unsigned int n = m_spectrum->length();
double max = 0.0;
unsigned int i;
for (i = 1; i < n; i++) {
double val = m_spectrum->at(i);
if (max < val)
max = val;
}
if (!m_analyze)
return;
double limit = max;
if (max < ALLOW_MIN) {
// don't start until we get some data
if (!m_total)
return;
limit = ALLOW_MIN;
}
unsigned int peaks = 0;
limit *= PEAKS_THR;
for (i = 1; i < n; i++) {
if (m_spectrum->at(i) > limit)
peaks++;
}
DDebug(&__plugin,DebugInfo,"Got %u peaks, limit=%f, max=%f [%p]",peaks,limit,max,this);
m_total++;
if (peaks == PEAKS_NUM)
m_valid++;
}
void AnalyzerCons::statusParams(String& str)
{
unsigned long samples = timeStamp() - m_tsStart;
str.append("gaps=",",") << m_tsGapCount;
str << ",gaplen=" << (unsigned int)m_tsGapLength;
str << ",samples=" << (unsigned int)samples;
if (m_timeStart) {
u_int64_t utime = Time::now() - m_timeStart;
utime = utime ? ((1000000 * (u_int64_t)samples) + (utime / 2)) / utime : 0;
str << ",rate=" << (unsigned int)utime;
}
if (m_total > 0) {
double q = m_valid * 100.0 / m_total;
char buf[64];
snprintf(buf,sizeof(buf)-1,"quality=%0.2f",q);
str.append(buf,",");
}
}
AnalyzerChan::AnalyzerChan(const String& type, bool outgoing, const char* window)
: Channel(__plugin,0,outgoing), m_stopTime(0),
m_timeStart(Time::now()), m_timeRoute(0), m_timeRing(0), m_timeAnswer(0),
m_window(window)
{
DDebug(this,DebugAll,"AnalyzerChan::AnalyzerChan('%s',%s) [%p]",
type.c_str(),String::boolText(outgoing),this);
m_address = type;
}
AnalyzerChan::~AnalyzerChan()
{
DDebug(this,DebugAll,"AnalyzerChan::~AnalyzerChan() %s [%p]",id().c_str(),this);
RefPointer<AnalyzerCons> cons = YOBJECT(AnalyzerCons,getConsumer());
char buf[32];
printTime(buf,(unsigned int)(Time::now() - m_timeStart));
String str(status());
localParams(str);
str.append("totaltime=",",") << buf;
if (cons)
cons->statusParams(str);
Output("Finished '%s' status: %s",id().c_str(),str.c_str());
Engine::enqueue(message("chan.hangup"));
}
void AnalyzerChan::statusParams(String& str)
{
Channel::statusParams(str);
localParams(str);
RefPointer<AnalyzerCons> cons = YOBJECT(AnalyzerCons,getConsumer());
if (cons)
cons->statusParams(str);
}
void AnalyzerChan::localParams(String& str)
{
char buf[32];
if (m_timeRoute) {
printTime(buf,m_timeRoute);
str.append("routetime=",",") << buf;
}
if (m_timeRing) {
printTime(buf,m_timeRing);
str.append("ringtime=",",") << buf;
}
if (m_timeAnswer) {
printTime(buf,m_timeAnswer);
str.append("answertime=",",") << buf;
}
}
bool AnalyzerChan::callRouted(Message& msg)
{
if (!m_timeRoute)
m_timeRoute = Time::now() - m_timeStart;
setDuration(msg);
return Channel::callRouted(msg);
}
bool AnalyzerChan::msgRinging(Message& msg)
{
if (!m_timeRing)
m_timeRing = Time::now() - m_timeStart;
return Channel::msgRinging(msg);
}
bool AnalyzerChan::msgAnswered(Message& msg)
{
if (!m_timeAnswer)
m_timeAnswer = Time::now() - m_timeStart;
addConsumer();
addSource();
return Channel::msgAnswered(msg);
}
void AnalyzerChan::checkTimers(Message& msg, const Time& tmr)
{
if (m_stopTime && (m_stopTime < tmr))
msgDrop(msg,"finished");
else
Channel::checkTimers(msg,tmr);
}
void AnalyzerChan::startChannel(NamedList& params)
{
Message* m = message("chan.startup");
const char* tmp = params.getValue("caller");
if (tmp)
m->addParam("caller",tmp);
tmp = params.getValue("called");
if (tmp)
m->addParam("called",tmp);
if (isOutgoing()) {
tmp = params.getValue("billid");
if (tmp)
m->addParam("billid",tmp);
}
Engine::enqueue(m);
if (isOutgoing()) {
addConsumer();
addSource();
}
setDuration(params);
}
void AnalyzerChan::setDuration(NamedList& params)
{
int t = params.getIntValue("duration",120000);
if (t > 0)
m_stopTime = Time::now() + 1000 * (uint64_t)t;
}
void AnalyzerChan::addSource()
{
if (getSource())
return;
const char* src = "tone/probe";
if (m_address.startsWith("tone/"))
src = m_address;
Message m("chan.attach");
complete(m,true);
m.addParam("source",src);
m.addParam("single","true");
m.userData(this);
if (!Engine::dispatch(m))
Debug(this,DebugWarn,"Could not attach source '%s' [%p]",src,this);
}
void AnalyzerChan::addConsumer()
{
if (getConsumer())
return;
AnalyzerCons* cons = new AnalyzerCons(m_address,m_window);
setConsumer(cons);
cons->deref();
}
bool AnalyzerDriver::startCall(NamedList& params, const String& dest)
{
bool direct = true;
String tmp(params.getValue("direct"));
if (tmp.null()) {
direct = false;
tmp = params.getValue("target");
if (tmp.null()) {
Debug(DebugWarn,"Analyzer outgoing call with no target!");
return false;
}
}
// this is an incoming call!
AnalyzerChan *ac = new AnalyzerChan(dest,false,params.getValue("window"));
ac->startChannel(params);
Message* m = ac->message("call.route",false,true);
m->addParam("called",tmp);
if (direct)
m->addParam("callto",tmp);
tmp = params.getValue("caller");
if (tmp.null())
tmp << prefix() << dest;
m->addParam("caller",tmp);
params.setParam("id",ac->id());
return ac->startRouter(m);
}
bool AnalyzerDriver::msgExecute(Message& msg, String& dest)
{
CallEndpoint* ch = static_cast<CallEndpoint*>(msg.userObject("CallEndpoint"));
if (ch) {
AnalyzerChan *ac = new AnalyzerChan(dest,true,msg.getValue("window"));
if (ch->connect(ac,msg.getValue("reason"))) {
msg.setParam("peerid",ac->id());
ac->startChannel(msg);
ac->deref();
return true;
}
else {
ac->destruct();
return false;
}
}
else
return startCall(msg,dest);
}
bool AttachHandler::received(Message& msg)
{
String cons(msg.getValue("consumer"));
if (!cons.startSkip(__plugin.prefix(),false))
cons.clear();
if (cons.null())
return false;
CallEndpoint* ch = static_cast<CallEndpoint*>(msg.userObject("CallEndpoint"));
if (!ch) {
Debug(DebugWarn,"Analyzer attach request with no control channel!");
return false;
}
// if single attach was requested we can return true if everything is ok
bool ret = msg.getBoolValue("single");
AnalyzerCons *ac = new AnalyzerCons(cons,msg.getValue("window"));
ch->setConsumer(ac);
ac->deref();
return ret;
}
AnalyzerDriver::AnalyzerDriver()
: Driver("analyzer","misc"), m_handler(0)
{
Output("Loaded module Analyzer");
}
AnalyzerDriver::~AnalyzerDriver()
{
Output("Unloading module Analyzer");
lock();
channels().clear();
unlock();
}
void AnalyzerDriver::initialize()
{
Output("Initializing module Analyzer");
setup(0,true); // no need to install notifications
Driver::initialize();
installRelay(Ringing);
installRelay(Answered);
installRelay(Halt);
if (!m_handler) {
m_handler = new AttachHandler;
Engine::install(m_handler);
}
}
}; // anonymous namespace
/* vi: set ts=8 sw=4 sts=4 noet: */