From d41035ecc25d9d98cad180944eebb0fe2fa9366f Mon Sep 17 00:00:00 2001 From: Louis-Joseph Fournier Date: Sat, 26 Dec 2015 20:46:02 +0100 Subject: [PATCH] First commit for SailTuner Intergration of elements: - base from SailDBMeter - audio filters from personnal repos --- .gitignore | 2 + Tuner.pro | 18 +++++ qml/Desktop.qml | 4 + qml/desktop.qrc | 5 ++ src/Tuner.cpp | 98 +++++++++++++++++++++++++ src/Tuner.hpp | 47 ++++++++++++ src/audio/LinearFilter.cpp | 64 ++++++++++++++++ src/audio/LinearFilter.hpp | 21 ++++++ src/audio/ZeroCross.cpp | 146 +++++++++++++++++++++++++++++++++++++ src/audio/ZeroCross.hpp | 34 +++++++++ src/desktop.cpp | 14 ++++ 11 files changed, 453 insertions(+) create mode 100644 .gitignore create mode 100644 Tuner.pro create mode 100644 qml/Desktop.qml create mode 100644 qml/desktop.qrc create mode 100644 src/Tuner.cpp create mode 100644 src/Tuner.hpp create mode 100644 src/audio/LinearFilter.cpp create mode 100644 src/audio/LinearFilter.hpp create mode 100644 src/audio/ZeroCross.cpp create mode 100644 src/audio/ZeroCross.hpp create mode 100644 src/desktop.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f67ac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swo +*.swp diff --git a/Tuner.pro b/Tuner.pro new file mode 100644 index 0000000..4d3f892 --- /dev/null +++ b/Tuner.pro @@ -0,0 +1,18 @@ +QT += qml quick gui multimedia +TARGET = Tuner + +CONFIG += c++11 + +SOURCES += \ + src/desktop.cpp \ + src/Tuner.cpp \ + src/audio/LinearFilter.cpp \ + src/audio/ZeroCross.cpp + +HEADERS += \ + src/Tuner.hpp \ + src/audio/LinearFilter.hpp \ + src/audio/ZeroCross.hpp + +RESOURCES += \ + qml/desktop.qrc diff --git a/qml/Desktop.qml b/qml/Desktop.qml new file mode 100644 index 0000000..459c82a --- /dev/null +++ b/qml/Desktop.qml @@ -0,0 +1,4 @@ +import QtQuick 2.0 + +Item { +} diff --git a/qml/desktop.qrc b/qml/desktop.qrc new file mode 100644 index 0000000..ca5cc1e --- /dev/null +++ b/qml/desktop.qrc @@ -0,0 +1,5 @@ + + +Desktop.qml + + diff --git a/src/Tuner.cpp b/src/Tuner.cpp new file mode 100644 index 0000000..05298b1 --- /dev/null +++ b/src/Tuner.cpp @@ -0,0 +1,98 @@ +#include +#include + +#include +#include + +#include "Tuner.hpp" + +using namespace std; + +// high 10hz / 16k +static double a10[] = { 1 , -2.99214602, 2.98432286, -0.99217678 }; +static double b10[] = { 0.99608071, -2.98824212, 2.98824212, -0.99608071 }; + +Tuner::Tuner() +{ + running = false; + + high_filter = new LinearFilter(3, a10, b10); + + ZeroCross::Config cross_config({rate, defaultNbFrame, defaultFreqMin, defaultFreqMax}); + cross = new ZeroCross(cross_config); + + settings.setCodec("audio/PCM"); + settings.setChannelCount(1); + settings.setSampleRate(rate); + + recorder = new QAudioRecorder(this); + recorder->setAudioInput("pulseaudio:"); + recorder->setAudioSettings(settings); + + QUrl url = QString("/dev/null"); + recorder->setOutputLocation(url); + + probe = new QAudioProbe(this); + connect(probe, SIGNAL(audioBufferProbed(QAudioBuffer)), this, SLOT(AudioCb(QAudioBuffer))); + probe->setSource(recorder); +} + +Tuner::~Tuner() +{ + delete high_filter; + delete cross; + delete recorder; +} + +void Tuner::Start() +{ + high_filter->Clear(); + cross->Clear(); + recorder->record(); + running = true; + runningChanged(); +} + +void Tuner::Stop() +{ + running = false; + recorder->stop(); + runningChanged(); +} + +void Tuner::ComputeFrame(int16_t v) +{ + v = (*high_filter)(v); + (*cross)(v); +} + +void Tuner::AudioCb(const QAudioBuffer &buffer) +{ + const int16_t *ptr = buffer.constData(); + int nbFrame = buffer.sampleCount(); + + while (nbFrame--) ComputeFrame(*ptr++); + + if (freq != cross->Freq()) { + freq = cross->Freq(); + freqChanged(); + } +} + +bool Tuner::GetRunning() +{ + return running; +} + +void Tuner::SetRunning(bool r) +{ + if (running == r) return; + + if (r) Start(); + else Stop(); +} + +double Tuner::GetFreq() +{ + return freq; +} diff --git a/src/Tuner.hpp b/src/Tuner.hpp new file mode 100644 index 0000000..3925d41 --- /dev/null +++ b/src/Tuner.hpp @@ -0,0 +1,47 @@ +#include +#include + +#include "audio/LinearFilter.hpp" +#include "audio/ZeroCross.hpp" + +class Tuner : public QObject { + Q_OBJECT + Q_PROPERTY(bool running READ GetRunning WRITE SetRunning NOTIFY runningChanged) + Q_PROPERTY(double freq READ GetFreq NOTIFY freqChanged) + + private: + QAudioRecorder *recorder; + QAudioEncoderSettings settings; + QAudioProbe *probe; + + LinearFilter *high_filter; + ZeroCross *cross; + + bool running; + double freq; + + static const int rate = 16000; + static const int defaultNbFrame = 1024; + static const int defaultFreqMin = 50; + static const int defaultFreqMax = 2000; + + inline void ComputeFrame(int16_t v); + + private slots: + void AudioCb(const QAudioBuffer &buffer); + + public: + Tuner(); + ~Tuner(); + + void Start(); + void Stop(); + + bool GetRunning(); + void SetRunning(bool r); + double GetFreq(); + + signals: + void runningChanged(); + void freqChanged(); +}; diff --git a/src/audio/LinearFilter.cpp b/src/audio/LinearFilter.cpp new file mode 100644 index 0000000..2828da1 --- /dev/null +++ b/src/audio/LinearFilter.cpp @@ -0,0 +1,64 @@ +#include +#include +#include "LinearFilter.hpp" + +using namespace std; + +template LinearFilter::LinearFilter(int order, double *a, double *b) +{ + assert(order > 0); + assert(a); + assert(b); + assert(a[0]); + + this->a = a; + this->b = b; + this->order = order; + + backx = new double[order]; + backy = new double[order]; + + Clear(); +} + +template LinearFilter::~LinearFilter() +{ + delete [] backx; + delete [] backy; +} + +template void LinearFilter::Clear() +{ + fill(backx, backx + order, 0); + fill(backy, backy + order, 0); +} + +template A LinearFilter::operator() (A x) +{ + double y = (double) x * b[0]; + + for (int i = 1; i <= order; i++) { + y += (double) b[i] * backx[i-1]; + y -= (double) a[i] * backy[i-1]; + } + y /= a[0]; + + for (int i = order - 1; i > 0; i--) { + backx[i] = backx[i-1]; + backy[i] = backy[i-1]; + } + backx[0] = x; + backy[0] = y; + + return y; +} + +template void LinearFilter::operator() (A *ptr, int nbFrame) +{ + while(nbFrame--) { + *ptr = (*this)(*ptr); + ptr++; + } +} + +template class LinearFilter; diff --git a/src/audio/LinearFilter.hpp b/src/audio/LinearFilter.hpp new file mode 100644 index 0000000..9aba422 --- /dev/null +++ b/src/audio/LinearFilter.hpp @@ -0,0 +1,21 @@ +#ifndef LINEARFILTER_H +#define LINEARFILTER_H + +#include + +template class LinearFilter { + private: + int order; + double *a, *b; + double *backx, *backy; + + public: + LinearFilter(int order, double *a, double *b); + ~LinearFilter(); + + void Clear(); + A operator() (A x); + void operator() (A *ptr, int nbFrame); +}; + +#endif diff --git a/src/audio/ZeroCross.cpp b/src/audio/ZeroCross.cpp new file mode 100644 index 0000000..a20fcc1 --- /dev/null +++ b/src/audio/ZeroCross.cpp @@ -0,0 +1,146 @@ +#include "ZeroCross.hpp" + +#include +#include +#include + +using namespace std; + +/// Maximum rest of division to be a multiple of +#define EPS_DIVISOR 0.1 +/// Consider it is a correct pattern to avoid multiples of +#define CORRECT_DEVIATION 0.05 + +// local functions + +/** + * Return average and standart deviation of pattern approx. interval in values + */ +pair IsPattern(const vector &values, double interval) +{ + double sum = 0, deviation = 0, current = 0; + int nb = 0; + double diff = interval; + + for (double v : values) { + if (fabs(diff - v) < fabs(diff)) { + current += v; + diff = interval - current; + } + else { + sum += current; + deviation += diff * diff; + current = 0; + diff = interval; + nb++; + } + } + if (nb < 2) return pair(0,0); + sum /= nb; + deviation = sqrt(deviation) / nb; + return pair(sum, deviation); +} + +/** + * Find best matching pattern beetween min and max + * from an integer list + * + * @return best interval pattern, standart deviation for best pattern + */ +pair FindPattern(const vector &values, double pattern_min, double pattern_max) +{ + auto best = pair(0,0); + double interval = 0; + + for (double v : values) { + interval += v; + if (interval < pattern_min) continue; + if (interval > pattern_max) break; + + auto res = IsPattern(values, interval); +// cout << " " << res.first << " " << res.second << endl; + if (res.first && (res.second < best.second || best.first == 0)) { + if (best.first && best.second < CORRECT_DEVIATION) { + double div = res.first / best.first; + // it is a multiple of previous + if (fabs(div - round(div)) < EPS_DIVISOR) continue; +// cout << "... " << div - floor(div) << endl; + } + best = res; + } + } + if (best.second > CORRECT_DEVIATION) return pair(0,0); + cout << " -> " << best.first << " " << best.second << " "; + return best; +} + +/// Methods implementation + +template ZeroCross::ZeroCross(const Config &config) +{ + rate = config.rate; + nb_frame_to_analyse = config.nb_frame; + freq = 0; + sample_max = (double) rate / config.tonie_min; + sample_min = (double) rate / config.tonie_max; + Clear(); +} + +template void ZeroCross::Clear() +{ + nb_frame_current = -1; + nb_frame_analysed = 0; + freq = 0; + last_zero = false; + pattern.clear(); +} + +template void ZeroCross::ComputeFrame(sample_t x) +{ + double delta; + + // count frame only if ever crossed + if (nb_frame_current != -1) nb_frame_current++; + // get only from negative to positive + if (x > 0 && last_sample < 0) { + if (last_zero) delta = 1; + else delta = (double) x / (x - last_sample); + + if (nb_frame_current > 0) { + nb_frame_current -= delta; + pattern.push_back(nb_frame_current); + //cout << nb_frame_current << " "; + } + nb_frame_current = delta; + } + // drop 0 values to know sign change + if (x) last_sample = x, last_zero = false; + else last_zero = true; + // compute if window suffisent + if (nb_frame_analysed++ == nb_frame_to_analyse) { + auto res = FindPattern(pattern, sample_min, sample_max); + if (res.first) freq = rate / res.first; + else freq = 0; + nb_frame_analysed = 0; + pattern.clear(); + } +} + +template double ZeroCross::operator() (sample_t *ptr, int nbFrame) +{ + while(nbFrame--) ComputeFrame(*ptr++); + return freq; +} + +template void ZeroCross::operator() (sample_t v) +{ + ComputeFrame(v); +} + +template double ZeroCross::Freq() +{ + return freq; +} + +// instanciations +template class ZeroCross; diff --git a/src/audio/ZeroCross.hpp b/src/audio/ZeroCross.hpp new file mode 100644 index 0000000..91f26d3 --- /dev/null +++ b/src/audio/ZeroCross.hpp @@ -0,0 +1,34 @@ +#ifndef ZEROCROSS_H +#define ZEROCROSS_H + +#include +#include + +template class ZeroCross { + public: + struct Config { + uint16_t rate; + int nb_frame; + int tonie_min, tonie_max; + }; + + private: + int rate, nb_frame_to_analyse, nb_frame_analysed, diff_x; + double nb_frame_current; + int16_t last_sample; + double freq, sample_min, sample_max; + bool last_zero; + std::vector pattern; + + inline void ComputeFrame(sample_t x); + + public: + ZeroCross(const Config &config); + double operator() (sample_t *ptr, int nbFrame); + void operator() (sample_t v); + + void Clear(); + double Freq(); +}; + +#endif diff --git a/src/desktop.cpp b/src/desktop.cpp new file mode 100644 index 0000000..2ad74a3 --- /dev/null +++ b/src/desktop.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +int main(int argc, char* argv[]) +{ +// qmlRegisterType("LJDBMeter", 1, 0, "DBMeter"); + + QGuiApplication app(argc, argv); + QQuickView view; + view.setSource(QUrl("qrc:///qml/Desktop.qml")); + view.show(); + return app.exec(); +}