diff --git a/.gitignore b/.gitignore index fab7372..547207e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ Makefile* *.prl *.app moc_*.cpp +moc_*.h ui_*.h qrc_*.cpp Thumbs.db @@ -70,4 +71,5 @@ Thumbs.db # -------- *.dll *.exe +oscilloscope diff --git a/.travis.yml b/.travis.yml index b86e2d0..ff4bd1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: cpp os: linux sudo: false -dist: trusty +dist: bionic compiler: - gcc - clang @@ -12,6 +12,11 @@ cache: - ~/.ccache - qt5 +before_install: + - sudo apt-get install -y libgl1-mesa-dev libglu1-mesa-dev + - sudo apt-get install -y libasound2-dev + - sudo apt-get install -y libpulse-dev + install: - mkdir -p qt5 - if [ ! -d qt5/.git ] ; then rm qt5 -Rf ; git clone --branch=5.12.5 git://code.qt.io/qt/qt5.git ; fi diff --git a/audiodevice.cpp b/audiodevice.cpp index 88443b2..d71b850 100644 --- a/audiodevice.cpp +++ b/audiodevice.cpp @@ -4,7 +4,6 @@ #include #include -namespace { //! private helper to allow QAudioInput to write to a io device class AudioDeviceHelper : public QIODevice { @@ -18,14 +17,13 @@ private: AudioDevice &m_audioDevice; }; -class AudioDevicePrivate { +class AudioDevice::AudioDevicePrivate { public: AudioDevicePrivate(AudioDevice &audioDevice, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format); AudioDeviceHelper helper; QAudioInput input; }; -} AudioDevice::AudioDevice(QObject *parent) : BaseDevice{parent} @@ -58,9 +56,6 @@ void AudioDevice::stop() m_private = nullptr; } - - -namespace { AudioDeviceHelper::AudioDeviceHelper(AudioDevice &audioDevice, QObject *parent) : QIODevice{parent}, m_audioDevice(audioDevice) { @@ -84,8 +79,7 @@ qint64 AudioDeviceHelper::writeData(const char *data, qint64 len) return len; } -AudioDevicePrivate::AudioDevicePrivate(AudioDevice &audioDevice, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format) : +AudioDevice::AudioDevicePrivate::AudioDevicePrivate(AudioDevice &audioDevice, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format) : helper{audioDevice}, input{audioDeviceInfo, format} { } -} diff --git a/audiodevice.h b/audiodevice.h index bcd319b..656cb0b 100644 --- a/audiodevice.h +++ b/audiodevice.h @@ -9,7 +9,6 @@ #include // forward declares -namespace { class AudioDevicePrivate; } class QAudioInput; class AudioDevice : public BaseDevice @@ -31,6 +30,7 @@ public: void setDevice(const QAudioDeviceInfo &device) { Q_ASSERT(!running()); m_device = device; } private: + class AudioDevicePrivate; std::unique_ptr m_private; int m_samplerate; diff --git a/basetonegenerator.cpp b/basetonegenerator.cpp index 94e65be..163f488 100644 --- a/basetonegenerator.cpp +++ b/basetonegenerator.cpp @@ -3,8 +3,6 @@ // Qt includes #include -namespace -{ //! private helper to allow QAudioOutput to read from a io device class BaseToneGeneratorHelper : public QIODevice { @@ -18,7 +16,7 @@ private: BaseToneGenerator &m_generator; }; -class BaseToneGeneratorPrivate +class BaseToneGenerator::BaseToneGeneratorPrivate { public: BaseToneGeneratorPrivate(BaseToneGenerator &generator, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format); @@ -28,8 +26,6 @@ public: QAudioOutput output; }; -} - BaseToneGenerator::BaseToneGenerator() = default; BaseToneGenerator::~BaseToneGenerator() = default; @@ -58,7 +54,6 @@ void BaseToneGenerator::stop() m_private = nullptr; } -namespace { BaseToneGeneratorHelper::BaseToneGeneratorHelper(BaseToneGenerator &generator, QObject *parent) : QIODevice{parent}, m_generator{generator} { @@ -80,8 +75,7 @@ qint64 BaseToneGeneratorHelper::writeData(const char *data, qint64 len) qFatal("writing is not allowed!"); } -BaseToneGeneratorPrivate::BaseToneGeneratorPrivate(BaseToneGenerator &generator, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format) : +BaseToneGenerator::BaseToneGeneratorPrivate::BaseToneGeneratorPrivate(BaseToneGenerator &generator, const QAudioDeviceInfo &audioDeviceInfo, const QAudioFormat &format) : helper{generator}, output{audioDeviceInfo, format} { } -} diff --git a/basetonegenerator.h b/basetonegenerator.h index 1c8b25f..b3d2397 100644 --- a/basetonegenerator.h +++ b/basetonegenerator.h @@ -9,9 +9,6 @@ // local includes #include "oscicommon.h" -// forward declares -namespace { class BaseToneGeneratorPrivate; } - class BaseToneGenerator { public: @@ -31,6 +28,7 @@ public: virtual std::size_t fill(SamplePair *begin, SamplePair *end) = 0; private: + class BaseToneGeneratorPrivate; std::unique_ptr m_private; int m_samplerate; diff --git a/mainwindow.cpp b/mainwindow.cpp index 680b789..d8df364 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,8 +1,9 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -// system includes -#include +// local includes +#include "audiodevice.h" +#include "debugtonegenerator.h" // Qt includes #include @@ -11,14 +12,13 @@ #include #include -// local includes -#include "audiodevice.h" -#include "debugtonegenerator.h" +// system includes +#include namespace { constexpr int samplerates[] = { 44100, 48000, 96000, 192000 }; -constexpr int refreshrates[] = { 15, 30, 50, 60 }; +constexpr int refreshrates[] = { 1, 15, 30, 50, 60 }; constexpr int zoomlevels[] = { 50, 75, 100, 200, 400, 800 }; @@ -117,17 +117,18 @@ MainWindow::MainWindow(QWidget *parent) : connect(&m_zoomlevelsGroup, &QActionGroup::triggered, this, &MainWindow::zoomChanged); //setting up menu Debug - connect(m_ui->actionToneGenerator, &QAction::triggered, this, &MainWindow::startGenerator); + connect(m_ui->actionToneGenerator, &QAction::triggered, this, &MainWindow::startStopGenerator); { auto widgetAction = new QWidgetAction(this); auto widget = new QWidget; auto layout = new QFormLayout(widget); { - auto input = new QSpinBox; - input->setRange(0, 255); + auto input = new QDoubleSpinBox; + input->setRange(0, 10000.0); + input->setSingleStep(1.0); input->setValue(m_ui->widget->afterglow()); - connect(input, qOverload(&QSpinBox::valueChanged), m_ui->widget, &OsciWidget::setAfterglow); + connect(input, qOverload(&QDoubleSpinBox::valueChanged), m_ui->widget, &OsciWidget::setAfterglow); layout->addRow(tr("Afterglow:"), input); } { @@ -199,9 +200,13 @@ void MainWindow::zoomChanged() m_ui->widget->setFactor(zoomlevel/100.f); } -void MainWindow::startGenerator() +void MainWindow::startStopGenerator() { - m_generator = nullptr; + if(m_generator){ + m_generator = nullptr; + return; + } + m_generator = std::make_unique(); m_generator->setDevice(m_outputDevices.at(m_outputDeviceGroup.actions().indexOf(m_outputDeviceGroup.checkedAction()))); m_generator->setSamplerate(samplerate()); diff --git a/mainwindow.h b/mainwindow.h index 1ba9e91..9d5dd90 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -28,7 +28,7 @@ private slots: void stop(); void refreshRateChanged(); void zoomChanged(); - void startGenerator(); + void startStopGenerator(); private: int samplerate() const; diff --git a/osciwidget.cpp b/osciwidget.cpp index 745d6fe..e6f3f21 100644 --- a/osciwidget.cpp +++ b/osciwidget.cpp @@ -1,19 +1,27 @@ #include "osciwidget.h" -// system includes -#include - // Qt includes #include #include #include #include -OsciWidget::OsciWidget(QWidget *parent) : - QOpenGLWidget{parent}, - m_redrawTimerId(startTimer(1000/m_fps)) +// system includes +#include + +qint32 framesForDuration(qint64 duration){ + return qint32(44100 * duration / 1000000LL); +} + + +OsciWidget::OsciWidget(QWidget *parent) + : QOpenGLWidget{parent} + , m_bufferOffset{m_buffer.begin()} + , m_lastBufferUpdate{0} + , m_redrawTimerId(startTimer(1000/m_fps, Qt::PreciseTimer)) { m_statsTimer.start(); + m_globalTimer.start(); } int OsciWidget::lightspeed() const @@ -27,10 +35,20 @@ void OsciWidget::setFps(int fps) m_fps = fps; - m_redrawTimerId = startTimer(1000/m_fps); + m_redrawTimerId = startTimer(1000/m_fps, Qt::PreciseTimer); } -void OsciWidget::setLightspeed(int lightspeed) { +void OsciWidget::setAfterglow(float afterglow) +{ + m_decayTime = afterglow; + // percentage of the image that should be visible after one second + // i.e. factor^fps=afterglow -> factor = afterglow^(1/fps) + // i.e. factor^(fps*persistence)=1/e -> factor = 1/e^(1/(fps*persistence/1000.0)) + +} + +void OsciWidget::setLightspeed(int lightspeed) +{ const auto temp = (float(lightspeed)/20.f); m_lightspeed = temp*temp*temp; qDebug() << m_lightspeed; @@ -42,7 +60,26 @@ void OsciWidget::renderSamples(const SamplePair *begin, const SamplePair *end) m_samplesCounter += std::distance(begin, end); + auto offset = std::distance(m_buffer.begin(), m_bufferOffset); + + if(m_globalTimer.elapsed()-m_lastBufferUpdate > 5000) + { + //qDebug() << "deleting: " << m_bufferOffset - m_buffer.begin() << m_buffer.size(); + // Delete drawn frames + //m_buffer.erase(m_buffer.begin(), m_bufferOffset); + + // TODO improve by drawing remaining(undrawn) buffer instead + drawBuffer(m_bufferOffset, m_buffer.end(), QColor(0, 255, 0)); + m_buffer.clear(); + offset = 0; + + m_lastBufferUpdate = m_globalTimer.elapsed(); + } + //qDebug () << " inserting " << std::distance(begin, end); m_buffer.insert(m_buffer.end(), begin, end); + m_bufferOffset = m_buffer.begin() + offset; + //m_bufferTimer.restart(); + //qDebug() << m_statsTimer.elapsed(); } void OsciWidget::paintEvent(QPaintEvent *event) @@ -52,75 +89,115 @@ void OsciWidget::paintEvent(QPaintEvent *event) m_frameCounter++; if (m_statsTimer.hasExpired(1000)) { - emit statusUpdate(QString("%0FPS (%1 callbacks, %2 samples, %3 avg per callback)").arg(m_frameCounter).arg(m_callbacksCounter).arg(m_samplesCounter).arg(m_callbacksCounter>0?m_samplesCounter/m_callbacksCounter:0)); + emit statusUpdate(QString("%0FPS (%1 callbacks, %2 samples, %3 avg per callback, bufferSize %4, elapsed %5)").arg(m_frameCounter).arg(m_callbacksCounter).arg(m_samplesCounter).arg(m_callbacksCounter>0?m_samplesCounter/m_callbacksCounter:0).arg(m_buffer.size()).arg(m_globalTimer.elapsed())); m_frameCounter = 0; m_callbacksCounter = 0; m_samplesCounter = 0; m_statsTimer.restart(); } + QPainter painter(this); + painter.drawPixmap(0, 0, m_pixmap); +} + +void OsciWidget::darkenFrame() +{ + QPainter painter(&m_pixmap); + + + //auto afterglowColor = 255 * pow(m_decayTime, 1.0/m_fps); + auto afterglowFactor = pow(exp(-1), 1000.0/m_decayTime/m_fps); + auto afterglowColor = static_cast(255*afterglowFactor); + //qDebug() << afterglowFactor; + //QColor blendColor; + //black.setAlpha(afterglowColor); + painter.setCompositionMode(QPainter::CompositionMode_Multiply); + painter.setBrush(QColor(afterglowColor, afterglowColor, afterglowColor)); + painter.drawRect(m_pixmap.rect()); + //m_pixmap.fill(Qt::black); +} + + +void OsciWidget::drawBuffer(SampleBuffer::iterator &bufferPos, const SampleBuffer::iterator &end, QColor color) +{ + QPainter painter(&m_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.translate(m_pixmap.width()/2, m_pixmap.height()/2); + painter.scale(m_factor * m_pixmap.width() / 2.0, m_factor * m_pixmap.height() / 2.0); + + QPen pen; + pen.setCosmetic(true); // let pen be scale invariant + pen.setWidth(2); + // This prevents most of the overlapping when drawing lines from and to the same points + pen.setCapStyle(Qt::FlatCap); + + for (;bufferPos < end; ++bufferPos) + { + const auto &frame = *bufferPos; + + const QPointF p{ + float(frame.first) / std::numeric_limits::max(), + float(-frame.second) / std::numeric_limits::max() + }; + + const QLineF line(m_lastPoint, p); + + auto beamBrightness = std::min(1.0, 1. / ((line.length() * m_lightspeed) + 1)); + + // time from bufferPos to end in ms + double time = 1000.0 * std::distance(bufferPos, end) / 44100.0; + + auto beamDecay = exp(-time/m_decayTime); + // HACK: gamma rolloff the brightness to make it look better (make configurable?) + auto beamVisibility = pow(beamBrightness, 2.0)*beamDecay; + + QColor drawColor(color); + drawColor.setAlphaF(beamVisibility); + //QColor drawColor(color.red()*beamVisibility, color.green()*beamVisibility, color.blue()*beamVisibility); + + pen.setColor(drawColor); + painter.setPen(pen); + //painter.setOpacity(beamVisibility); + painter.drawLine(m_lastPoint, p); + //painter.drawLine(m_lastPoint, m_lastPoint); + //painter.setPen(pen); + //painter.drawPoint(m_lastPoint); + + m_lastPoint = p; + } +} + +void OsciWidget::resizeDrawBuffer() +{ if (m_pixmap.size() != size()) { m_pixmap = QPixmap(size()); m_pixmap.fill(Qt::black); } +} - QPainter painter; - painter.begin(&m_pixmap); +void OsciWidget::updateDrawBuffer() +{ + // If there is no new data do not update + if(m_buffer.empty()) return; - // darkening last frame - painter.setCompositionMode(QPainter::CompositionMode_Multiply); - painter.setPen({}); - painter.setBrush(QColor(m_afterglow, m_afterglow, m_afterglow)); - painter.drawRect(m_pixmap.rect()); + resizeDrawBuffer(); + darkenFrame(); + // decay time is the time it needs to decay to 1/e ~ 36,7% - // drawing new lines ontop - QPen pen; - pen.setWidth(2); - pen.setColor(QColor(0, 255, 0)); - painter.setPen(pen); - painter.translate(m_pixmap.width()/2, m_pixmap.height()/2); - painter.setCompositionMode(QPainter::CompositionMode_Plus); + auto duration = 1000*(m_globalTimer.elapsed()-m_lastBufferUpdate); + size_t framesOffset = framesForDuration(duration); + //qDebug() << framesOffset << m_buffer.size()-framesOffset << m_bufferOffset - m_buffer.begin(); - const auto pointToCoordinates = [width=m_pixmap.width()/2,height=m_pixmap.height()/2,factor=m_factor](const QPointF &point) - { - return QPoint{ - int(point.x() * factor * width), - int(point.y() * factor * height) - }; - }; - - for (const auto &i : m_buffer) - { - const QPointF p{ - float(i.first) / std::numeric_limits::max(), - float(-i.second) / std::numeric_limits::max() - }; - - const QLineF line(m_lastPoint, p); - - painter.setOpacity(std::min(1.0, 1. / ((line.length() * m_lightspeed) + 1))); - - painter.drawLine(pointToCoordinates(m_lastPoint), pointToCoordinates(p)); - - m_lastPoint = p; - } - - painter.setOpacity(1); - painter.resetTransform(); - - painter.end(); - - painter.begin(this); - painter.drawPixmap(0, 0, m_pixmap); - painter.end(); - - m_buffer.clear(); + auto bufferEnd = m_buffer.begin() + std::min(framesOffset, m_buffer.size()); + drawBuffer(m_bufferOffset, bufferEnd, QColor(0, 255, 0)); } void OsciWidget::timerEvent(QTimerEvent *event) { QWidget::timerEvent(event); - if (event->timerId() == m_redrawTimerId) + if (event->timerId() == m_redrawTimerId){ + updateDrawBuffer(); repaint(); + } } diff --git a/osciwidget.h b/osciwidget.h index b0c7b2e..778e498 100644 --- a/osciwidget.h +++ b/osciwidget.h @@ -21,7 +21,7 @@ public: float factor() const { return m_factor; } int fps() const { return m_fps; } - int afterglow() const { return m_afterglow; } + float afterglow() const { return m_decayTime; } int lightspeed() const; signals: @@ -30,7 +30,7 @@ signals: public slots: void setFactor(float factor) { m_factor = factor; } void setFps(int fps); - void setAfterglow(int afterglow) { m_afterglow = afterglow; } + void setAfterglow(float afterglow); void setLightspeed(int lightspeed); void renderSamples(const SamplePair *begin, const SamplePair *end); @@ -40,16 +40,27 @@ protected: void timerEvent(QTimerEvent *event) override; private: - float m_factor{2.f}; - int m_fps{30}, m_afterglow{175}; - float m_lightspeed{35.f}; + void updateDrawBuffer(); - std::vector m_buffer; +private: + float m_factor{1.f}; + int m_fps{60}; + float m_decayTime{29.0}; + float m_lightspeed{18.f}; + + typedef std::vector SampleBuffer; + SampleBuffer m_buffer; + SampleBuffer::iterator m_bufferOffset; int m_frameCounter{0}, m_callbacksCounter{0}, m_samplesCounter{0}; QElapsedTimer m_statsTimer; + QElapsedTimer m_globalTimer; + qint64 m_lastBufferUpdate; int m_redrawTimerId; QPointF m_lastPoint; QPixmap m_pixmap; + void darkenFrame(); + void drawBuffer(SampleBuffer::iterator &bufferPos, const SampleBuffer::iterator &end, QColor color); + void resizeDrawBuffer(); };