diff --git a/app/audio/audiohybriddevice.cpp b/app/audio/audiohybriddevice.cpp index bf766568f..c4a942801 100644 --- a/app/audio/audiohybriddevice.cpp +++ b/app/audio/audiohybriddevice.cpp @@ -26,7 +26,8 @@ AudioHybridDevice::AudioHybridDevice(QObject *parent) : QIODevice(parent), device_(nullptr), - sample_index_(0) + sample_index_(0), + enable_sending_samples_(false) { } @@ -67,7 +68,47 @@ bool AudioHybridDevice::IsIdle() return device_ == nullptr && pushed_samples_.isEmpty(); } +void AudioHybridDevice::SetEnableSendingSamples(bool e) +{ + enable_sending_samples_ = e; +} + qint64 AudioHybridDevice::readData(char *data, qint64 maxSize) +{ + qint64 read_size = read_internal(data, maxSize); + + if (enable_sending_samples_ && read_size > 0) { + // FIXME: Assumes float and stereo + float* samples = reinterpret_cast(data); + int sample_count = static_cast(read_size / static_cast(sizeof(float))); + int channels = 2; + + // Create array of samples to send + QVector averages(channels); + averages.fill(0); + + // Add all samples together + for (int i=0;i(qAbs(samples[i]))); + } + + emit SentSamples(averages); + } + + return read_size; +} + +qint64 AudioHybridDevice::writeData(const char *data, qint64 maxSize) +{ + Q_UNUSED(data) + Q_UNUSED(maxSize) + + // This device doesn't support writing + + return -1; +} + +qint64 AudioHybridDevice::read_internal(char *data, qint64 maxSize) { // If a device is connected, passthrough to it if (device_ != nullptr) { @@ -103,13 +144,3 @@ qint64 AudioHybridDevice::readData(char *data, qint64 maxSize) memset(data, 0, static_cast(maxSize)); return maxSize; } - -qint64 AudioHybridDevice::writeData(const char *data, qint64 maxSize) -{ - Q_UNUSED(data) - Q_UNUSED(maxSize) - - // This device doesn't support writing - - return -1; -} diff --git a/app/audio/audiohybriddevice.h b/app/audio/audiohybriddevice.h index c106b8273..4a7c63dd1 100644 --- a/app/audio/audiohybriddevice.h +++ b/app/audio/audiohybriddevice.h @@ -81,12 +81,27 @@ public: */ bool IsIdle(); + /** + * @brief If enabled, this will emit the SentSamples() signal whenever samples are sent to the output device + */ + void SetEnableSendingSamples(bool e); + signals: /** * @brief Signal emitted when this leaves "idle" state and has valid audio data that is ready to be sent */ void HasSamples(); + /** + * @brief Signal emitted when samples are sent to the output device + * + * This sends an array of values corresponding to the channel count. Each value is an average of all the samples just + * sent to that channel. Useful for connecting to AudioMonitor. + * + * Can be disabled with SetEnableSendingSamples() to save CPU usage. + */ + void SentSamples(QVector averages); + protected: /** * @brief Internal QIODevice function for reading @@ -115,11 +130,15 @@ protected: virtual qint64 writeData(const char *data, qint64 maxSize) override; private: + qint64 read_internal(char *data, qint64 maxSize); + QIODevice* device_; QByteArray pushed_samples_; qint64 sample_index_; + bool enable_sending_samples_; + }; #endif // AUDIOHYBRIDDEVICE_H diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 7e62b63e6..9c7d6417e 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -133,7 +133,9 @@ AudioManager::AudioManager() : RefreshDevices(); connect(&output_manager_, SIGNAL(HasSamples()), this, SLOT(OutputManagerHasSamples())); + connect(&output_manager_, SIGNAL(SentSamples(QVector)), this, SIGNAL(SentSamples(QVector))); + output_manager_.SetEnableSendingSamples(true); output_manager_.open(AudioHybridDevice::ReadOnly); } diff --git a/app/audio/audiomanager.h b/app/audio/audiomanager.h index d2de36f2a..ec4b48f3d 100644 --- a/app/audio/audiomanager.h +++ b/app/audio/audiomanager.h @@ -94,6 +94,8 @@ public: signals: void DeviceListReady(); + void SentSamples(QVector averages); + private: AudioManager(); diff --git a/app/panel/CMakeLists.txt b/app/panel/CMakeLists.txt index c8c37999b..b9c212e66 100644 --- a/app/panel/CMakeLists.txt +++ b/app/panel/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(audiomonitor) add_subdirectory(node) add_subdirectory(param) add_subdirectory(project) diff --git a/app/panel/audiomonitor/CMakeLists.txt b/app/panel/audiomonitor/CMakeLists.txt new file mode 100644 index 000000000..833c406d3 --- /dev/null +++ b/app/panel/audiomonitor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + panel/audiomonitor/audiomonitor.h + panel/audiomonitor/audiomonitor.cpp + PARENT_SCOPE +) diff --git a/app/panel/audiomonitor/audiomonitor.cpp b/app/panel/audiomonitor/audiomonitor.cpp new file mode 100644 index 000000000..edbdd69f7 --- /dev/null +++ b/app/panel/audiomonitor/audiomonitor.cpp @@ -0,0 +1,27 @@ +#include "audiomonitor.h" + +AudioMonitorPanel::AudioMonitorPanel(QWidget *parent) : + PanelWidget(parent) +{ + // FIXME: This won't work if there's ever more than one of this panel + setObjectName("AudioMonitor"); + + audio_monitor_ = new AudioMonitor(this); + + setWidget(audio_monitor_); + + Retranslate(); +} + +void AudioMonitorPanel::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + Retranslate(); + } + PanelWidget::changeEvent(e); +} + +void AudioMonitorPanel::Retranslate() +{ + SetTitle(tr("Audio Monitor")); +} diff --git a/app/panel/audiomonitor/audiomonitor.h b/app/panel/audiomonitor/audiomonitor.h new file mode 100644 index 000000000..71179777b --- /dev/null +++ b/app/panel/audiomonitor/audiomonitor.h @@ -0,0 +1,24 @@ +#ifndef AUDIOMONITORPANEL_H +#define AUDIOMONITORPANEL_H + +#include "widget/audiomonitor/audiomonitor.h" +#include "widget/panel/panel.h" + +/** + * @brief PanelWidget wrapper around an AudioMonitor + */ +class AudioMonitorPanel : public PanelWidget +{ +public: + AudioMonitorPanel(QWidget* parent = nullptr); + +protected: + virtual void changeEvent(QEvent* e) override; + +private: + void Retranslate(); + + AudioMonitor* audio_monitor_; +}; + +#endif // AUDIOMONITORPANEL_H diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 894f3ad84..bf38b81fd 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -49,7 +49,7 @@ void NodePanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void NodePanel::Retranslate() diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index e4c3bd5eb..37575b4b9 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -45,7 +45,7 @@ void ParamPanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void ParamPanel::Retranslate() diff --git a/app/panel/project/project.cpp b/app/panel/project/project.cpp index 4306c715b..cfb0adb11 100644 --- a/app/panel/project/project.cpp +++ b/app/panel/project/project.cpp @@ -102,7 +102,7 @@ void ProjectPanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void ProjectPanel::Retranslate() diff --git a/app/panel/taskmanager/taskmanager.cpp b/app/panel/taskmanager/taskmanager.cpp index f4942738e..6019ece33 100644 --- a/app/panel/taskmanager/taskmanager.cpp +++ b/app/panel/taskmanager/taskmanager.cpp @@ -46,7 +46,7 @@ void TaskManagerPanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void TaskManagerPanel::Retranslate() diff --git a/app/panel/timeline/timeline.cpp b/app/panel/timeline/timeline.cpp index f230b947d..284aa89ca 100644 --- a/app/panel/timeline/timeline.cpp +++ b/app/panel/timeline/timeline.cpp @@ -114,7 +114,7 @@ void TimelinePanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void TimelinePanel::Retranslate() diff --git a/app/panel/tool/tool.cpp b/app/panel/tool/tool.cpp index abd431665..326c4cc35 100644 --- a/app/panel/tool/tool.cpp +++ b/app/panel/tool/tool.cpp @@ -50,7 +50,7 @@ void ToolPanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void ToolPanel::Retranslate() diff --git a/app/panel/viewer/viewer.cpp b/app/panel/viewer/viewer.cpp index 4f65e070d..409bd72c2 100644 --- a/app/panel/viewer/viewer.cpp +++ b/app/panel/viewer/viewer.cpp @@ -122,7 +122,7 @@ void ViewerPanel::changeEvent(QEvent *e) if (e->type() == QEvent::LanguageChange) { Retranslate(); } - QDockWidget::changeEvent(e); + PanelWidget::changeEvent(e); } void ViewerPanel::Retranslate() diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index 50b200b1c..eeb3575e8 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(audiomonitor) add_subdirectory(columnedgridlayout) add_subdirectory(flowlayout) add_subdirectory(footagecombobox) diff --git a/app/widget/audiomonitor/CMakeLists.txt b/app/widget/audiomonitor/CMakeLists.txt new file mode 100644 index 000000000..6bc5833fb --- /dev/null +++ b/app/widget/audiomonitor/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2019 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + widget/audiomonitor/audiomonitor.h + widget/audiomonitor/audiomonitor.cpp + PARENT_SCOPE +) diff --git a/app/widget/audiomonitor/audiomonitor.cpp b/app/widget/audiomonitor/audiomonitor.cpp new file mode 100644 index 000000000..977fe78d6 --- /dev/null +++ b/app/widget/audiomonitor/audiomonitor.cpp @@ -0,0 +1,147 @@ +#include "audiomonitor.h" + +#include +#include +#include + +#include "audio/audiomanager.h" +#include "common/qtversionabstraction.h" + +const int kDecibelStep = 6; +const int kDecibelMinimum = -200; +const int kClearTimerInterval = 500; + +AudioMonitor::AudioMonitor(QWidget *parent) : + QWidget(parent) +{ + clear_timer_.setInterval(kClearTimerInterval); + + connect(AudioManager::instance(), SIGNAL(SentSamples(QVector)), this, SLOT(SetValues(QVector))); + connect(&clear_timer_, SIGNAL(timeout()), this, SLOT(Clear())); + connect(&clear_timer_, SIGNAL(timeout()), &clear_timer_, SLOT(stop())); +} + +void AudioMonitor::SetValues(QVector values) +{ + values_ = values; + + if (values_.size() != peaked_.size()) { + peaked_.resize(values_.size()); + peaked_.fill(false); + } + + clear_timer_.stop(); + clear_timer_.start(); + + update(); +} + +void AudioMonitor::Clear() +{ + values_.fill(0); + update(); +} + +void AudioMonitor::paintEvent(QPaintEvent *) +{ + int channels = values_.size(); + + if (channels == 0) { + return; + } + + QPainter p(this); + QFontMetrics fm = p.fontMetrics(); + + int peaks_y = 0; + int peaks_height = fm.height(); + + QRect db_labels_rect = rect(); + db_labels_rect.setWidth(QFontMetricsWidth(p.fontMetrics(), "-00")); + db_labels_rect.adjust(0, peaks_height, 0, 0); + + QRect full_meter_rect = rect(); + full_meter_rect.adjust(db_labels_rect.width(), peaks_height, 0, 0); + + // Draw decibel markings + QRect last_db_marking_rect; + + for (int i=0;i>=kDecibelMinimum;i-=kDecibelStep) { + QString db_label; + + if (i <= kDecibelMinimum) { + db_label = "-∞"; + } else { + db_label = QString("%1").arg(i); + } + + qreal log_val = QAudio::convertVolume(i, QAudio::DecibelVolumeScale, QAudio::LogarithmicVolumeScale); + + QRect db_marking_rect = db_labels_rect; + db_marking_rect.adjust(0, db_labels_rect.y() + db_labels_rect.height() - qRound(log_val * db_labels_rect.height()), 0, 0); + db_marking_rect.setHeight(fm.height()); + + // Prevent any dB markings overlapping + if (i == 0 || !db_marking_rect.intersects(last_db_marking_rect)) { + p.drawText(db_marking_rect, Qt::AlignRight, db_label); + p.drawLine(db_marking_rect.topLeft(), db_marking_rect.topRight()); + + last_db_marking_rect = db_marking_rect; + } + } + + QLinearGradient g(full_meter_rect.topLeft(), full_meter_rect.bottomLeft()); + g.setStops({ + QGradientStop(0.0, Qt::red), + QGradientStop(0.25, Qt::yellow), + QGradientStop(1.0, Qt::green) + }); + + int channel_width = full_meter_rect.width() / channels; + + for (int i=0;i 1.0) { + peaked_[i] = true; + } + + // Convert val to logarithmic scale + vol = QAudio::convertVolume(vol, QAudio::LinearVolumeScale, QAudio::LogarithmicVolumeScale); + + p.setBrush(QColor(0, 0, 0, 128)); + + meter_rect.adjust(0, 0, 0, -qRound(meter_rect.height() * vol)); + p.drawRect(meter_rect); + + if (!peaked_.at(i)) + p.drawRect(peaks_rect); + } +} + +void AudioMonitor::mousePressEvent(QMouseEvent *) +{ + peaked_.fill(false); + update(); +} diff --git a/app/widget/audiomonitor/audiomonitor.h b/app/widget/audiomonitor/audiomonitor.h new file mode 100644 index 000000000..5754a2b24 --- /dev/null +++ b/app/widget/audiomonitor/audiomonitor.h @@ -0,0 +1,28 @@ +#ifndef AUDIOMONITORWIDGET_H +#define AUDIOMONITORWIDGET_H + +#include +#include + +class AudioMonitor : public QWidget +{ + Q_OBJECT +public: + AudioMonitor(QWidget* parent = nullptr); + +public slots: + void SetValues(QVector values); + void Clear(); + +protected: + virtual void paintEvent(QPaintEvent* event) override; + virtual void mousePressEvent(QMouseEvent* event) override; + +private: + QVector values_; + QVector peaked_; + + QTimer clear_timer_; +}; + +#endif // AUDIOMONITORWIDGET_H diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index c881568cf..dcd2f3b6a 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -26,6 +26,7 @@ // Panel objects #include "panel/panelmanager.h" +#include "panel/audiomonitor/audiomonitor.h" #include "panel/node/node.h" #include "panel/param/param.h" #include "panel/project/project.h" @@ -128,6 +129,9 @@ void olive::MainWindow::ProjectOpen(Project* p) TimelinePanel* timeline_panel = olive::panel_manager->CreatePanel(this); addDockWidget(Qt::BottomDockWidgetArea, timeline_panel); + AudioMonitorPanel* audio_monitor_panel = olive::panel_manager->CreatePanel(this); + addDockWidget(Qt::BottomDockWidgetArea, audio_monitor_panel); + TaskManagerPanel* task_man_panel = olive::panel_manager->CreatePanel(this); addDockWidget(Qt::BottomDockWidgetArea, task_man_panel); task_man_panel->setFloating(true);