diff --git a/audioformat.h b/audioformat.h new file mode 100644 index 0000000..ea8025e --- /dev/null +++ b/audioformat.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace { +constexpr int frameRate = 44100; +constexpr int channelCount = 2; +constexpr int sampleSize = 32; +constexpr auto codec = "audio/pcm"; +constexpr QAudioFormat::Endian byteOrder = QAudioFormat::LittleEndian; +constexpr QAudioFormat::SampleType sampleType = QAudioFormat::Float; +using sample_t = float; +using frame_t = std::array; + +auto makeFormat() +{ + QAudioFormat format; + format.setSampleRate(frameRate); + format.setChannelCount(channelCount); + format.setSampleSize(sampleSize); + format.setCodec(codec); + format.setByteOrder(byteOrder); + format.setSampleType(sampleType); + return format; +} +} diff --git a/bpmdetector.cpp b/bpmdetector.cpp new file mode 100644 index 0000000..6e30974 --- /dev/null +++ b/bpmdetector.cpp @@ -0,0 +1,32 @@ +#include "bpmdetector.h" + +// system includes +#include + +// Qt includes +#include + +BpmDetector::BpmDetector(QObject *parent) : + QIODevice{parent} +{ + setOpenMode(QIODevice::WriteOnly); +} + +qint64 BpmDetector::readData(char *data, qint64 maxlen) +{ + qCritical("read not supported"); + return -1; +} + +qint64 BpmDetector::writeData(const char *data, qint64 len) +{ + QVector frames{int(len / sizeof(frame_t))}; + frames.resize(len / sizeof(frame_t)); + + const auto begin = reinterpret_cast(data); + std::copy(begin, begin + frames.size(), std::begin(frames)); + + emit receivedFrames(frames); + + return len; +} diff --git a/bpmdetector.h b/bpmdetector.h new file mode 100644 index 0000000..9c06e4f --- /dev/null +++ b/bpmdetector.h @@ -0,0 +1,22 @@ +#pragma once + +// Qt includes +#include +#include + +// local includes +#include "audioformat.h" + +class BpmDetector : public QIODevice +{ + Q_OBJECT + +public: + explicit BpmDetector(QObject *parent = nullptr); + + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + +signals: + void receivedFrames(const QVector &frames); +}; diff --git a/main.cpp b/main.cpp index 8bfb909..f353051 100644 --- a/main.cpp +++ b/main.cpp @@ -1,14 +1,22 @@ -#include +#include #include #include #include #include #include "r3client.h" +#include "mainwindow.h" + +#include int main(int argc, char *argv[]) { - QCoreApplication a(argc, argv); + QApplication a(argc, argv); + + MainWindow mainWindow; + mainWindow.show(); + + return a.exec(); const auto arguments = [&](){ auto arguments = a.arguments(); diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..c6d7012 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,272 @@ +#include "mainwindow.h" + +// system includes +#include + +// Qt includes +#include +#include +#include +#include +#include +#include + +namespace { +template +QString enumToString(const QEnum value) +{ + return QMetaEnum::fromType().valueToKey(value); +} +} + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow{parent} +{ + m_ui.setupUi(this); + + connect(m_ui.connectButton, &QAbstractButton::pressed, this, &MainWindow::connectPressed); + connect(m_ui.disconnectButton, &QAbstractButton::pressed, this, &MainWindow::disconnectPressed); + + connect(m_ui.lamp1OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight1", "on"); }); + connect(m_ui.lamp1OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight1", "off"); }); + connect(m_ui.lamp2OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight2", "on"); }); + connect(m_ui.lamp2OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight2", "off"); }); + connect(m_ui.lamp3OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight3", "on"); }); + connect(m_ui.lamp3OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight3", "off"); }); + connect(m_ui.lamp4OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight4", "on"); }); + connect(m_ui.lamp4OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight4", "off"); }); + connect(m_ui.lamp5OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight5", "on"); }); + connect(m_ui.lamp5OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight5", "off"); }); + connect(m_ui.lamp6OnButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight6", "on"); }); + connect(m_ui.lamp6OffButton, &QAbstractButton::pressed, this, [&](){ lightCmd("basiclight6", "off"); }); + + connect(m_ui.toggleAllButton, &QAbstractButton::pressed, this, &MainWindow::toggleAllPressed); + connect(m_ui.discoLeftButton, &QAbstractButton::pressed, this, &MainWindow::discoLeftPressed); + connect(m_ui.discoRightButton, &QAbstractButton::pressed, this, &MainWindow::discoRightPressed); + + connect(m_ui.bpmButton, &QAbstractButton::pressed, this, &MainWindow::bpmPressed); + + connect(m_ui.openAudioDeviceButton, &QAbstractButton::pressed, this, &MainWindow::openAudioDevicePressed); + connect(m_ui.closeAudioDeviceButton, &QAbstractButton::pressed, this, &MainWindow::closeAudioDevicePressed); + + connect(&m_client, &R3Client::connected, this, &MainWindow::connected); + connect(&m_client, &R3Client::disconnected, this, &MainWindow::disconnected); + connect(&m_client, &R3Client::error, this, &MainWindow::error); + connect(&m_client, &R3Client::statusReceived, this, &MainWindow::statusReceived); + + updateAudioDevices(); + + m_timer.setTimerType(Qt::PreciseTimer); + connect(&m_timer, &QTimer::timeout, this, &MainWindow::bpmTimeout); + + connect(m_ui.spinBox, &QSpinBox::valueChanged, &m_timer, qOverload(&QTimer::setInterval)); + + connect(&m_detector, &BpmDetector::receivedFrames, this, &MainWindow::receivedFrames); + + connect(&m_timer2, &QTimer::timeout, this, [&](){ + const sample_t min = -m_min * 100; + m_ui.verticalSlider->setValue(min); + m_min = 0; + + const sample_t max = m_max * 100; + m_ui.verticalSlider_2->setValue(max); + m_max = 0; + + const sample_t avg = m_distanceSum / m_count * 100.; + m_ui.verticalSlider_3->setValue(avg); + m_distanceSum = 0.; + m_count = 0; + + const auto selectedWert = [&](){ + if (m_ui.radioButton->isChecked()) + return min; + else if (m_ui.radioButton_2->isChecked()) + return max; + else if (m_ui.radioButton_3->isChecked()) + return avg; + }(); + + if (selectedWert > m_ui.verticalSlider_4->value()) + switch (m_ui.comboBox->currentIndex()) + { + case 0: discoLeftPressed(); break; + case 1: discoRightPressed(); break; + case 2: toggleAllPressed(); break; + } + }); + m_timer2.start(1000/20); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::connectPressed() +{ + log("Connecting..."); + m_client.open(); +} + +void MainWindow::disconnectPressed() +{ + log("Disconnecting..."); + m_client.close(); +} + +void MainWindow::connected() +{ + log("Connected successfully!"); +} + +void MainWindow::disconnected() +{ + log("Disconnected!"); +} + +void MainWindow::error(QAbstractSocket::SocketError error) +{ + log(QString{"Error: %0"}.arg(enumToString(error))); +} + +void MainWindow::statusReceived(const QString &ctx, const QJsonValue &jsonValue) +{ + qDebug() << jsonValue; + log(QString{"ctx=%0"}.arg(ctx)); +} + +void MainWindow::toggleAllPressed() +{ + lightCmd("basiclight1", m_toggleState ? "on" : "off"); + lightCmd("basiclight2", m_toggleState ? "on" : "off"); + lightCmd("basiclight3", m_toggleState ? "on" : "off"); + lightCmd("basiclight4", m_toggleState ? "on" : "off"); + lightCmd("basiclight5", m_toggleState ? "on" : "off"); + lightCmd("basiclight6", m_toggleState ? "on" : "off"); + m_toggleState = !m_toggleState; +} + +void MainWindow::discoLeftPressed() +{ + if (m_discoState == 0) + m_discoState = 5; + else + m_discoState--; + updateDisco(); +} + +void MainWindow::discoRightPressed() +{ + if (m_discoState == 5) + m_discoState = 0; + else + m_discoState++; + updateDisco(); +} + +void MainWindow::bpmPressed() +{ + discoRightPressed(); + + if (m_bpmTap) + { + m_bpmTap->count++; + const auto elapsed = m_bpmTap->begin.msecsTo(QDateTime::currentDateTime()); + m_bpmTap->msPerBeat = elapsed / m_bpmTap->count; + m_bpmTap->bpm = 60000 / m_bpmTap->msPerBeat; + m_ui.bpmButton->setText(QString{"%0 %1 BPM"}.arg(m_bpmTap->count).arg(m_bpmTap->bpm)); + m_ui.spinBox->setValue(m_bpmTap->msPerBeat); + m_timer.start(m_bpmTap->msPerBeat); + m_bpmTap->flag = false; + } + else + { + m_timer.start(2000); + m_bpmTap = BpmTap{}; + } +} + +void MainWindow::openAudioDevicePressed() +{ + const auto index = m_ui.audioDeviceSelection->currentIndex(); + if (index < 0 || index >= m_devices.count()) + return; + + m_input = std::make_unique(m_devices.at(index), makeFormat()); + m_input->start(&m_detector); +} + +void MainWindow::closeAudioDevicePressed() +{ + m_input = nullptr; +} + +void MainWindow::bpmTimeout() +{ + if (m_bpmTap) + { + if (!m_bpmTap->flag) + { + m_bpmTap->flag = true; + return; + } + + if (m_bpmTap->count) + { + qDebug() << "starting normal"; + discoRightPressed(); + } + else + { + m_ui.bpmButton->setText(tr("BPM tap")); + qDebug() << "aborted"; + m_timer.stop(); + } + m_bpmTap = std::nullopt; + } + else + { + qDebug() << "disco timer"; + discoRightPressed(); + } +} + +void MainWindow::receivedFrames(const QVector &frames) +{ + for (const auto &frame : frames) + { + if (frame[0] < m_min) + m_min = frame[0]; + + if (frame[0] > m_max) + m_max = frame[0]; + + m_distanceSum += std::abs(frame[0]); + m_count++; + } +} + +void MainWindow::log(const QString &msg) +{ + m_ui.logView->appendPlainText(QString{"%0 %1"}.arg(QDateTime::currentDateTime().toString(), msg)); +} + +void MainWindow::lightCmd(const QString &light, const QString &status) +{ + m_client.sendMQTT("action/GoLightCtrl/" + light, QJsonObject{ {"Action", status }}); +} + +void MainWindow::updateDisco() +{ + lightCmd("basiclight1", m_discoState == 0 ? "on" : "off"); + lightCmd("basiclight2", m_discoState == 1 ? "on" : "off"); + lightCmd("basiclight3", m_discoState == 2 ? "on" : "off"); + lightCmd("basiclight4", m_discoState == 3 ? "on" : "off"); + lightCmd("basiclight5", m_discoState == 4 ? "on" : "off"); + lightCmd("basiclight6", m_discoState == 5 ? "on" : "off"); +} + +void MainWindow::updateAudioDevices() +{ + m_ui.audioDeviceSelection->clear(); + m_devices = QAudioDeviceInfo::availableDevices(QAudio::AudioInput); + for (const auto &x : m_devices) + m_ui.audioDeviceSelection->addItem(x.deviceName()); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..52452f6 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,81 @@ +#pragma once + +// system includes +#include +#include + +// Qt includes +#include +#include +#include +#include +#include +#include + +// local includes +#include "ui_mainwindow.h" +#include "r3client.h" +#include "bpmdetector.h" +#include "audioformat.h" + +class QAudioInput; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private slots: + void connectPressed(); + void disconnectPressed(); + + void connected(); + void disconnected(); + void error(QAbstractSocket::SocketError error); + void statusReceived(const QString &ctx, const QJsonValue &jsonValue); + + void toggleAllPressed(); + void discoLeftPressed(); + void discoRightPressed(); + + void bpmPressed(); + + void openAudioDevicePressed(); + void closeAudioDevicePressed(); + + void bpmTimeout(); + + void receivedFrames(const QVector &frames); + +private: + void log(const QString &msg); + void lightCmd(const QString &light, const QString &status); + void updateDisco(); + void updateAudioDevices(); + +private: + Ui::MainWindow m_ui; + R3Client m_client; + + bool m_toggleState{}; + uint8_t m_discoState{}; + + struct BpmTap { QDateTime begin{QDateTime::currentDateTime()}; int count{}; int msPerBeat, bpm; bool flag{}; }; + std::optional m_bpmTap; + + QTimer m_timer; + + QList m_devices; + std::unique_ptr m_input; + BpmDetector m_detector; + + sample_t m_min{}, m_max{}; + + double m_distanceSum{}; + std::size_t m_count{}; + + QTimer m_timer2; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..4eadfa4 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,495 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 70 + 30 + 80 + 23 + + + + Connect + + + + + + 160 + 30 + 80 + 23 + + + + Disconnect + + + + + + 20 + 280 + 591 + 231 + + + + true + + + + + + 120 + 60 + 481 + 201 + + + + + + + + + Lamp 4 + + + + + + + On + + + + + + + Off + + + + + + + + + + + Lamp 1 + + + + + + + On + + + + + + + Off + + + + + + + + + + + Lamp 2 + + + + + + + On + + + + + + + Off + + + + + + + + + + + Lamp 5 + + + + + + + On + + + + + + + Off + + + + + + + + + + + Lamp 3 + + + + + + + On + + + + + + + Off + + + + + + + + + + + Lamp 6 + + + + + + + On + + + + + + + Off + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 620 + 130 + 80 + 23 + + + + Disco L + + + + + + 660 + 100 + 80 + 23 + + + + Toggle all + + + + + + 710 + 130 + 80 + 23 + + + + Disco R + + + + + + 630 + 190 + 151 + 23 + + + + BPM tap + + + + + + 660 + 220 + 61 + 24 + + + + 10000 + + + + + + 630 + 300 + 131 + 23 + + + + + + + 620 + 340 + 80 + 23 + + + + Open + + + + + + 710 + 340 + 80 + 23 + + + + Close + + + + + + 620 + 370 + 170 + 151 + + + + + + + + + + + + false + + + + + + + 100 + + + Qt::Vertical + + + + + + + + + + + + + + + + + + 100 + + + Qt::Vertical + + + + + + + + + + + + + + true + + + + + + + 100 + + + 0 + + + Qt::Vertical + + + + + + + + + 100 + + + 50 + + + Qt::Vertical + + + + + + + + + 620 + 520 + 79 + 23 + + + + + Disco L + + + + + Disco R + + + + + Toggle All + + + + + + + + 0 + 0 + 800 + 20 + + + + + + + + diff --git a/r3ctl.pro b/r3ctl.pro index cb9415b..d77deea 100644 --- a/r3ctl.pro +++ b/r3ctl.pro @@ -1,12 +1,20 @@ -QT = core network websockets +QT = core gui widgets network websockets multimedia -CONFIG += c++14 +CONFIG += c++latest DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000 SOURCES += \ + bpmdetector.cpp \ main.cpp \ + mainwindow.cpp \ r3client.cpp HEADERS += \ + audioformat.h \ + bpmdetector.h \ + mainwindow.h \ r3client.h + +FORMS += \ + mainwindow.ui