diff --git a/Generator.cpp b/Generator.cpp new file mode 100644 index 0000000..d8ef1f0 --- /dev/null +++ b/Generator.cpp @@ -0,0 +1,116 @@ +#include "Generator.h" +#include +#include + + +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(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((1.0 + x) / 2 * 255); + *reinterpret_cast(ptr) = value; + } + else if (format.sampleType() == QAudioFormat::SignedInt) + { + const qint8 value = static_cast(x * 127); + *reinterpret_cast(ptr) = value; + } + } + else if (format.sampleSize() == 16) + { + if (format.sampleType() == QAudioFormat::UnSignedInt) + { + quint16 value = static_cast((1.0 + x) / 2 * 65535); + if (format.byteOrder() == QAudioFormat::LittleEndian) + qToLittleEndian(value, ptr); + else + qToBigEndian(value, ptr); + } + else if (format.sampleType() == QAudioFormat::SignedInt) + { + qint16 value = static_cast(x * 32767); + if (format.byteOrder() == QAudioFormat::LittleEndian) + qToLittleEndian(value, ptr); + else + qToBigEndian(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(); +} diff --git a/Generator.h b/Generator.h new file mode 100644 index 0000000..0256183 --- /dev/null +++ b/Generator.h @@ -0,0 +1,30 @@ +#ifndef GENERATOR_H +#define GENERATOR_H + +#include +#include +#include + +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 diff --git a/Measurer.cpp b/Measurer.cpp new file mode 100644 index 0000000..221df6d --- /dev/null +++ b/Measurer.cpp @@ -0,0 +1,108 @@ +#include "Measurer.h" +#include "Generator.h" +#include "fft.h" + +#include +#include +#include +#include +#include + +#include + +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 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(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(""); +} diff --git a/Measurer.h b/Measurer.h new file mode 100644 index 0000000..341a7c3 --- /dev/null +++ b/Measurer.h @@ -0,0 +1,26 @@ +#ifndef MEASURER_H +#define MEASURER_H + +#include + +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 diff --git a/fft.h b/fft.h new file mode 100755 index 0000000..baf7038 --- /dev/null +++ b/fft.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 + +using namespace std; + +#define Complex complex + +/* 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 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..137e786 --- /dev/null +++ b/main.cpp @@ -0,0 +1,113 @@ +#include +#include +#include + +#include +#include + +#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(); +} diff --git a/soundthermo-cpp.pro b/soundthermo-cpp.pro new file mode 100644 index 0000000..4f25174 --- /dev/null +++ b/soundthermo-cpp.pro @@ -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