Initial version. Allows listing and measuring
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
|||||||
|
#include "Generator.h"
|
||||||
|
#include <qendian.h>
|
||||||
|
#include <qmath.h>
|
||||||
|
|
||||||
|
|
||||||
|
Generator::Generator(const QAudioFormat &format
|
||||||
|
, qint64 durationUs
|
||||||
|
, int sampleRate, bool right)
|
||||||
|
{
|
||||||
|
if (format.isValid())
|
||||||
|
GenerateData(format, durationUs, sampleRate, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Generator::start()
|
||||||
|
{
|
||||||
|
open(QIODevice::ReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Generator::stop()
|
||||||
|
{
|
||||||
|
pos = 0;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Generator::GenerateData(const QAudioFormat &format, qint64 durationUs, int sampleRate, bool right)
|
||||||
|
{
|
||||||
|
const int channelBytes = format.sampleSize() / 8;
|
||||||
|
const int sampleBytes = format.channelCount() * channelBytes;
|
||||||
|
qint64 length = (format.sampleRate() * format.channelCount() * (format.sampleSize() / 8))
|
||||||
|
* durationUs / 1000000;
|
||||||
|
Q_ASSERT(length % sampleBytes == 0);
|
||||||
|
Q_UNUSED(sampleBytes) // suppress warning in release builds
|
||||||
|
|
||||||
|
buffer.resize(length);
|
||||||
|
unsigned char *ptr = reinterpret_cast<unsigned char *>(buffer.data());
|
||||||
|
int sampleIndex = 0;
|
||||||
|
|
||||||
|
while (length)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < format.channelCount(); ++i)
|
||||||
|
{
|
||||||
|
qreal x = 0;
|
||||||
|
// Generate sine wave on first (left) or second (right) channel and silence on other.
|
||||||
|
if ( (i == 0 && !right) ||
|
||||||
|
(i == 1 && right))
|
||||||
|
{
|
||||||
|
x = qSin(2 * M_PI * sampleRate * sampleIndex / format.sampleRate()) * 0.1;
|
||||||
|
sampleIndex++;
|
||||||
|
}
|
||||||
|
if (format.sampleSize() == 8)
|
||||||
|
{
|
||||||
|
if (format.sampleType() == QAudioFormat::UnSignedInt)
|
||||||
|
{
|
||||||
|
const quint8 value = static_cast<quint8>((1.0 + x) / 2 * 255);
|
||||||
|
*reinterpret_cast<quint8 *>(ptr) = value;
|
||||||
|
}
|
||||||
|
else if (format.sampleType() == QAudioFormat::SignedInt)
|
||||||
|
{
|
||||||
|
const qint8 value = static_cast<qint8>(x * 127);
|
||||||
|
*reinterpret_cast<qint8 *>(ptr) = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (format.sampleSize() == 16)
|
||||||
|
{
|
||||||
|
if (format.sampleType() == QAudioFormat::UnSignedInt)
|
||||||
|
{
|
||||||
|
quint16 value = static_cast<quint16>((1.0 + x) / 2 * 65535);
|
||||||
|
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||||
|
qToLittleEndian<quint16>(value, ptr);
|
||||||
|
else
|
||||||
|
qToBigEndian<quint16>(value, ptr);
|
||||||
|
}
|
||||||
|
else if (format.sampleType() == QAudioFormat::SignedInt)
|
||||||
|
{
|
||||||
|
qint16 value = static_cast<qint16>(x * 32767);
|
||||||
|
if (format.byteOrder() == QAudioFormat::LittleEndian)
|
||||||
|
qToLittleEndian<qint16>(value, ptr);
|
||||||
|
else
|
||||||
|
qToBigEndian<qint16>(value, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr += channelBytes;
|
||||||
|
length -= channelBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 Generator::readData(char *data, qint64 len)
|
||||||
|
{
|
||||||
|
qint64 total = 0;
|
||||||
|
if (!buffer.isEmpty())
|
||||||
|
{
|
||||||
|
while (len - total > 0)
|
||||||
|
{
|
||||||
|
const qint64 chunk = qMin((buffer.size() - pos), len - total);
|
||||||
|
memcpy(data + total, buffer.constData() + pos, chunk);
|
||||||
|
pos = (pos + chunk) % buffer.size();
|
||||||
|
total += chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 Generator::writeData(const char *data, qint64 len)
|
||||||
|
{
|
||||||
|
Q_UNUSED(data);
|
||||||
|
Q_UNUSED(len);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 Generator::bytesAvailable() const
|
||||||
|
{
|
||||||
|
return buffer.size() + QIODevice::bytesAvailable();
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef GENERATOR_H
|
||||||
|
#define GENERATOR_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QAudioFormat>
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
|
class Generator : public QIODevice
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Generator(const QAudioFormat &format, qint64 durationUs, int sampleRate, bool right = false);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
qint64 readData(char *data, qint64 maxlen) override;
|
||||||
|
qint64 writeData(const char *data, qint64 len) override;
|
||||||
|
qint64 bytesAvailable() const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void GenerateData(const QAudioFormat &format, qint64 durationUs, int sampleRate, bool right);
|
||||||
|
|
||||||
|
private:
|
||||||
|
qint64 pos = 0;
|
||||||
|
QByteArray buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // GENERATOR_H
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#include "Measurer.h"
|
||||||
|
#include "Generator.h"
|
||||||
|
#include "fft.h"
|
||||||
|
|
||||||
|
#include <QAudioInput>
|
||||||
|
#include <QAudioOutput>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <qendian.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
Measurer::Measurer(QAudioDeviceInfo input, QAudioDeviceInfo output, MeasureChannel channel)
|
||||||
|
: input(input), output(output), channel(channel)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
long double CurrentTime()
|
||||||
|
{
|
||||||
|
return QDateTime::currentMSecsSinceEpoch() / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Measurer::Measure(float duration)
|
||||||
|
{
|
||||||
|
QAudioFormat format;
|
||||||
|
format.setSampleRate(SAMPLE_RATE);
|
||||||
|
format.setChannelCount(2);
|
||||||
|
format.setSampleSize(16);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
format.setByteOrder(QAudioFormat::LittleEndian);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
|
||||||
|
// configure generator of single-tone signal
|
||||||
|
auto device = new Generator(format, duration * 1000000, TONE_FREQUENCY, channel == MeasureChannel::Right);
|
||||||
|
device->start();
|
||||||
|
// begin outputting data to device
|
||||||
|
QAudioOutput * audio_output = new QAudioOutput(output, format);
|
||||||
|
audio_output->start(device);
|
||||||
|
// begin reading of samples from input
|
||||||
|
QAudioFormat input_format(format);
|
||||||
|
input_format.setChannelCount(1);
|
||||||
|
QAudioInput *audio_input = new QAudioInput(input, input_format);
|
||||||
|
auto reading_device = audio_input->start();
|
||||||
|
auto start = CurrentTime();
|
||||||
|
QByteArray input_buffer;
|
||||||
|
while (CurrentTime() < start + duration)
|
||||||
|
{
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
input_buffer += reading_device->readAll();
|
||||||
|
}
|
||||||
|
audio_input->stop();
|
||||||
|
audio_output->stop();
|
||||||
|
device->stop();
|
||||||
|
auto result = ProcessData(input_buffer);
|
||||||
|
delete audio_input;
|
||||||
|
delete audio_output;
|
||||||
|
delete device;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Measurer::ProcessData(QByteArray data)
|
||||||
|
{
|
||||||
|
// convert byte data to sample array
|
||||||
|
vector<float> samples;
|
||||||
|
// note that we fixed format to be little-endian, two-byte, signed
|
||||||
|
for (int i = 0; i < data.count(); i += 2)
|
||||||
|
{
|
||||||
|
int t = qFromLittleEndian<qint16>(data.data() + i);
|
||||||
|
samples.push_back(t / 32767.0);
|
||||||
|
}
|
||||||
|
// cut off starting and ending silence
|
||||||
|
int left_pos = 0;
|
||||||
|
while (left_pos < (int)samples.size() && abs(samples[left_pos]) < 1e-4) left_pos++;
|
||||||
|
int right_pos = (int)samples.size() - 1;
|
||||||
|
while (right_pos >= 0 && abs(samples[right_pos]) < 1e-4) right_pos--;
|
||||||
|
int len = right_pos - left_pos + 1;
|
||||||
|
int pow = 1, m = 0;
|
||||||
|
while (2 * pow <= len)
|
||||||
|
{
|
||||||
|
pow <<= 1;
|
||||||
|
m++;
|
||||||
|
}
|
||||||
|
Complex *d = new Complex[pow];
|
||||||
|
for (int i = 0; i < pow; i++)
|
||||||
|
{
|
||||||
|
d[i] = Complex(samples[left_pos + i], 0);
|
||||||
|
}
|
||||||
|
FFT(d, pow, false);
|
||||||
|
float max = -1;
|
||||||
|
int argmax = 0;
|
||||||
|
for (int i = 0; i < pow; i++)
|
||||||
|
{
|
||||||
|
float amp = (float)sqrt(d[i].real() * d[i].real() + d[i].imag() * d[i].imag());
|
||||||
|
if (amp > max)
|
||||||
|
{
|
||||||
|
max = amp;
|
||||||
|
argmax = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float desired = TONE_FREQUENCY * pow / SAMPLE_RATE;
|
||||||
|
if (abs(desired - argmax) < 1e-3)
|
||||||
|
{
|
||||||
|
return max / pow;
|
||||||
|
}
|
||||||
|
return nan("");
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef MEASURER_H
|
||||||
|
#define MEASURER_H
|
||||||
|
|
||||||
|
#include <QAudioDeviceInfo>
|
||||||
|
|
||||||
|
enum MeasureChannel
|
||||||
|
{
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
};
|
||||||
|
|
||||||
|
class Measurer
|
||||||
|
{
|
||||||
|
const int TONE_FREQUENCY = 6000;
|
||||||
|
const int SAMPLE_RATE = 44100;
|
||||||
|
|
||||||
|
MeasureChannel channel;
|
||||||
|
QAudioDeviceInfo input, output;
|
||||||
|
|
||||||
|
float ProcessData(QByteArray data);
|
||||||
|
public:
|
||||||
|
Measurer(QAudioDeviceInfo input, QAudioDeviceInfo output, MeasureChannel channel);
|
||||||
|
float Measure(float duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // MEASURER_H
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef FELIX_THE_CAT_FAST_FOURIER_TRANSFORM_HEADER_FILE_INCLUDED_JYUWRGHJBVJHVBWJKFGKJJKFHBJSJKGHJKGH
|
||||||
|
#define FELIX_THE_CAT_FAST_FOURIER_TRANSFORM_HEADER_FILE_INCLUDED_JYUWRGHJBVJHVBWJKFGKJJKFHBJSJKGHJKGH
|
||||||
|
|
||||||
|
#include <complex>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
#define Complex complex<long double>
|
||||||
|
|
||||||
|
/* Defines Fast Fourier Transform */
|
||||||
|
|
||||||
|
void FFT(Complex *a, int n, bool inv = false);
|
||||||
|
|
||||||
|
/*
|
||||||
|
IMPLEMENTATION
|
||||||
|
*/
|
||||||
|
|
||||||
|
const long double PI = 3.1415926535897932384626433832795;
|
||||||
|
|
||||||
|
int Rev(int i, int l)
|
||||||
|
{
|
||||||
|
int res = 0;
|
||||||
|
int j;
|
||||||
|
for (j = 0; j < l; j++)
|
||||||
|
{
|
||||||
|
res <<= 1;
|
||||||
|
if (i & (1 << j)) res++;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFT(Complex *a, int n, bool inv)
|
||||||
|
{
|
||||||
|
int i, j, k;
|
||||||
|
int logn = 0;
|
||||||
|
while ((1 << logn) < n) logn++;
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
j = Rev(i, logn);
|
||||||
|
if (j > i)
|
||||||
|
{
|
||||||
|
Complex t = a[i];
|
||||||
|
a[i] = a[j];
|
||||||
|
a[j] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Complex u, v;
|
||||||
|
for (i = 1; i <= logn; i++)
|
||||||
|
{
|
||||||
|
long double ang = 2 * PI / (1 << i);
|
||||||
|
if (inv) ang = -ang;
|
||||||
|
Complex w(cos(ang), sin(ang));
|
||||||
|
for (j = 0; j < n; j += (1 << i))
|
||||||
|
{
|
||||||
|
Complex wk(1, 0);
|
||||||
|
for (k = j; k < j + (1 << (i - 1)); k++)
|
||||||
|
{
|
||||||
|
u = a[k];
|
||||||
|
v = a[k + (1 << (i - 1))] * wk;
|
||||||
|
a[k] = u + v;
|
||||||
|
a[k + (1 << (i - 1))] = u - v;
|
||||||
|
wk *= w;
|
||||||
|
}
|
||||||
|
wk *= w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inv)
|
||||||
|
{
|
||||||
|
for (i = 0; i < n; i++) a[i] /= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QAudioDeviceInfo>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "Measurer.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
void ListDevices()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto devices = QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioOutput);
|
||||||
|
cout << "Output devices:\n";
|
||||||
|
for (int i = 0; i < devices.count(); i++)
|
||||||
|
{
|
||||||
|
cout << devices[i].deviceName().toStdString() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto devices = QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioInput);
|
||||||
|
cout << "Input devices:\n";
|
||||||
|
for (int i = 0; i < devices.count(); i++)
|
||||||
|
{
|
||||||
|
cout << devices[i].deviceName().toStdString() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QAudioDeviceInfo GetInputDevice(QString name)
|
||||||
|
{
|
||||||
|
auto devices = QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioInput);
|
||||||
|
for (int i = 0; i < devices.count(); i++)
|
||||||
|
{
|
||||||
|
if (devices[i].deviceName() == name) return devices[i];
|
||||||
|
}
|
||||||
|
throw (string)("Unable to find device " + name.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QAudioDeviceInfo GetOutputDevice(QString name)
|
||||||
|
{
|
||||||
|
auto devices = QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioOutput);
|
||||||
|
for (int i = 0; i < devices.count(); i++)
|
||||||
|
{
|
||||||
|
if (devices[i].deviceName() == name) return devices[i];
|
||||||
|
}
|
||||||
|
throw (string)("Unable to find device " + name.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Measure(QString input_device_name, QString output_device_name, float duration)
|
||||||
|
{
|
||||||
|
auto input = GetInputDevice(input_device_name);
|
||||||
|
auto output = GetOutputDevice(output_device_name);
|
||||||
|
Measurer left(input, output, MeasureChannel::Left);
|
||||||
|
float left_result = left.Measure(duration);
|
||||||
|
Measurer right(input, output, MeasureChannel::Right);
|
||||||
|
float right_result = right.Measure(duration);
|
||||||
|
float k = 2 * right_result;
|
||||||
|
float v0v = left_result / k;
|
||||||
|
// assume r2 = 10K
|
||||||
|
float r1 = 10000 / v0v - 10000;
|
||||||
|
float t = 1.0 / (log(r1 / 10000) / 4300 + 1 / 298.0) - 273.0;
|
||||||
|
cout.setf(ios::fixed | ios::showpoint);
|
||||||
|
cout.precision(4);
|
||||||
|
cout << t << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication a(argc, argv);
|
||||||
|
QCoreApplication::setApplicationName("soundthermo-cpp");
|
||||||
|
QCoreApplication::setApplicationVersion("0.1");
|
||||||
|
|
||||||
|
QCommandLineParser parser;
|
||||||
|
parser.setApplicationDescription("Simple measurement of temperature via sound card");
|
||||||
|
parser.addHelpOption();
|
||||||
|
parser.addVersionOption();
|
||||||
|
parser.addPositionalArgument("command", "Command to execute");
|
||||||
|
|
||||||
|
|
||||||
|
parser.parse(QCoreApplication::arguments());
|
||||||
|
|
||||||
|
const QStringList args = parser.positionalArguments();
|
||||||
|
const QString command = args.isEmpty() ? QString() : args.first();
|
||||||
|
if (command == "list")
|
||||||
|
{
|
||||||
|
parser.clearPositionalArguments();
|
||||||
|
ListDevices();
|
||||||
|
}
|
||||||
|
else if (command == "measure")
|
||||||
|
{
|
||||||
|
QCommandLineOption input_option(QStringList() << "i" << "input", "Name of device to receive input from", "input");
|
||||||
|
QCommandLineOption output_option(QStringList() << "o" << "output", "Name of device to send output to", "output");
|
||||||
|
QCommandLineOption duration_option(QStringList() << "d" << "duration", "How many seconds to record (uses twice this time)", "duration");
|
||||||
|
parser.addOption(input_option);
|
||||||
|
parser.addOption(output_option);
|
||||||
|
parser.addOption(duration_option);
|
||||||
|
parser.process(a);
|
||||||
|
QString input_device_name = parser.value(input_option);
|
||||||
|
QString output_device_name = parser.value(output_option);
|
||||||
|
float duration = parser.value(duration_option).toFloat();
|
||||||
|
Measure(input_device_name, output_device_name, duration);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parser.process(a);
|
||||||
|
parser.showHelp(0);
|
||||||
|
}
|
||||||
|
return a.exec();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
QT -= gui
|
||||||
|
QT += multimedia
|
||||||
|
|
||||||
|
CONFIG += c++11 console
|
||||||
|
CONFIG -= app_bundle
|
||||||
|
|
||||||
|
# The following define makes your compiler emit warnings if you use
|
||||||
|
# any Qt feature that has been marked deprecated (the exact warnings
|
||||||
|
# depend on your compiler). Please consult the documentation of the
|
||||||
|
# deprecated API in order to know how to port your code away from it.
|
||||||
|
DEFINES += QT_DEPRECATED_WARNINGS
|
||||||
|
|
||||||
|
# You can also make your code fail to compile if it uses deprecated APIs.
|
||||||
|
# In order to do so, uncomment the following line.
|
||||||
|
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||||
|
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
Generator.cpp \
|
||||||
|
Measurer.cpp \
|
||||||
|
main.cpp
|
||||||
|
|
||||||
|
# Default rules for deployment.
|
||||||
|
qnx: target.path = /tmp/$${TARGET}/bin
|
||||||
|
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||||||
|
!isEmpty(target.path): INSTALLS += target
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
Generator.h \
|
||||||
|
Measurer.h \
|
||||||
|
fft.h
|
||||||
Reference in New Issue
Block a user