audiomonitor: rewrote to handle audio independently of output
This gives the AudioMonitor more control over its own timing and appearance and also reduces the load on the audio processing threads.
This commit is contained in:
@@ -75,6 +75,8 @@ void AudioManager::StartOutput(const QString &filename, qint64 offset, int playb
|
||||
Q_ARG(const QString&, filename),
|
||||
Q_ARG(qint64, offset),
|
||||
Q_ARG(int, playback_speed));
|
||||
|
||||
emit OutputDeviceStarted(filename, offset, playback_speed);
|
||||
}
|
||||
|
||||
void AudioManager::StopOutput()
|
||||
@@ -82,6 +84,8 @@ void AudioManager::StopOutput()
|
||||
QMetaObject::invokeMethod(&output_manager_,
|
||||
"ResetToPushMode",
|
||||
Qt::QueuedConnection);
|
||||
|
||||
emit Stopped();
|
||||
}
|
||||
|
||||
void AudioManager::SetOutputDevice(const QAudioDeviceInfo &info)
|
||||
@@ -154,6 +158,8 @@ void AudioManager::SetOutputParams(const AudioRenderingParams ¶ms)
|
||||
|
||||
// Refresh output device
|
||||
SetOutputDevice(output_device_info_);
|
||||
|
||||
emit AudioParamsChanged(output_params_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,12 @@ signals:
|
||||
|
||||
void OutputNotified();
|
||||
|
||||
void OutputDeviceStarted(const QString& filename, qint64 offset, int playback_speed);
|
||||
|
||||
void AudioParamsChanged(const AudioRenderingParams& params);
|
||||
|
||||
void Stopped();
|
||||
|
||||
private:
|
||||
AudioManager();
|
||||
|
||||
|
||||
@@ -70,6 +70,13 @@ AudioRenderingParams::AudioRenderingParams(const AudioParams ¶ms, const Samp
|
||||
{
|
||||
}
|
||||
|
||||
int AudioRenderingParams::time_to_bytes(const double &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return time_to_samples(time) * channel_count() * bytes_per_sample_per_channel();
|
||||
}
|
||||
|
||||
const SampleFormat::Format &AudioRenderingParams::format() const
|
||||
{
|
||||
return format_;
|
||||
@@ -90,17 +97,20 @@ bool AudioRenderingParams::operator!=(const AudioRenderingParams &other) const
|
||||
}
|
||||
|
||||
int AudioRenderingParams::time_to_bytes(const rational &time) const
|
||||
{
|
||||
return time_to_bytes(time.toDouble());
|
||||
}
|
||||
|
||||
int AudioRenderingParams::time_to_samples(const double &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return time_to_samples(time) * channel_count() * bytes_per_sample_per_channel();
|
||||
return qFloor(time * sample_rate());
|
||||
}
|
||||
|
||||
int AudioRenderingParams::time_to_samples(const rational &time) const
|
||||
{
|
||||
Q_ASSERT(is_valid());
|
||||
|
||||
return qFloor(time.toDouble() * sample_rate());
|
||||
return time_to_samples(time.toDouble());
|
||||
}
|
||||
|
||||
int AudioRenderingParams::samples_to_bytes(const int &samples) const
|
||||
|
||||
@@ -51,7 +51,9 @@ public:
|
||||
AudioRenderingParams(const int& sample_rate, const uint64_t& channel_layout, const SampleFormat::Format& format);
|
||||
AudioRenderingParams(const AudioParams& params, const SampleFormat::Format& format);
|
||||
|
||||
int time_to_bytes(const double& time) const;
|
||||
int time_to_bytes(const rational& time) const;
|
||||
int time_to_samples(const double& time) const;
|
||||
int time_to_samples(const rational& time) const;
|
||||
int samples_to_bytes(const int& samples) const;
|
||||
rational samples_to_time(const int& samples) const;
|
||||
|
||||
@@ -31,17 +31,70 @@ OLIVE_NAMESPACE_ENTER
|
||||
|
||||
const int kDecibelStep = 6;
|
||||
const int kDecibelMinimum = -200;
|
||||
const int kClearTimerInterval = 500;
|
||||
const int kMaximumSmoothness = 4;
|
||||
|
||||
AudioMonitor::AudioMonitor(QWidget *parent) :
|
||||
QWidget(parent)
|
||||
QWidget(parent),
|
||||
cached_channels_(0)
|
||||
{
|
||||
clear_timer_.setInterval(kClearTimerInterval);
|
||||
clear_timer_.setSingleShot(true);
|
||||
update_timer_.setInterval(1000 / 60); // Hardcoded to 60 FPS
|
||||
connect(&update_timer_, &QTimer::timeout, this, static_cast<void(AudioMonitor::*)()>(&AudioMonitor::update));
|
||||
update_timer_.start();
|
||||
|
||||
connect(&clear_timer_, &QTimer::timeout, this, &AudioMonitor::Clear);
|
||||
values_.resize(kMaximumSmoothness);
|
||||
|
||||
connect(AudioManager::instance(), &AudioManager::OutputDeviceStarted, this, &AudioMonitor::OutputDeviceSet);
|
||||
connect(AudioManager::instance(), &AudioManager::AudioParamsChanged, this, &AudioMonitor::SetParams);
|
||||
connect(AudioManager::instance(), &AudioManager::Stopped, this, &AudioMonitor::Stop);
|
||||
}
|
||||
|
||||
AudioMonitor::~AudioMonitor()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void AudioMonitor::SetParams(const AudioRenderingParams ¶ms)
|
||||
{
|
||||
params_ = params;
|
||||
|
||||
for (int i=0;i<values_.size();i++) {
|
||||
values_[i].resize(params_.channel_count());
|
||||
values_[i].fill(0);
|
||||
}
|
||||
|
||||
peaked_.resize(params_.channel_count());
|
||||
peaked_.fill(false);
|
||||
}
|
||||
|
||||
void AudioMonitor::OutputDeviceSet(const QString &filename, qint64 offset, int playback_speed)
|
||||
{
|
||||
Stop();
|
||||
|
||||
file_.setFileName(filename);
|
||||
|
||||
if (!file_.open(QFile::ReadOnly)) {
|
||||
qWarning() << "Failed to open" << filename;
|
||||
return;
|
||||
}
|
||||
|
||||
file_.seek(offset);
|
||||
|
||||
playback_speed_ = playback_speed;
|
||||
|
||||
update_timer_.start();
|
||||
last_time_ = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void AudioMonitor::Stop()
|
||||
{
|
||||
if (file_.isOpen()) {
|
||||
file_.close();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
void AudioMonitor::SetValues(QVector<double> values)
|
||||
{
|
||||
values_ = values;
|
||||
@@ -56,18 +109,11 @@ void AudioMonitor::SetValues(QVector<double> values)
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void AudioMonitor::Clear()
|
||||
{
|
||||
values_.fill(0);
|
||||
update();
|
||||
}
|
||||
*/
|
||||
|
||||
void AudioMonitor::paintEvent(QPaintEvent *)
|
||||
{
|
||||
int channels = values_.size();
|
||||
|
||||
if (channels == 0) {
|
||||
if (!params_.channel_count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,75 +121,119 @@ void AudioMonitor::paintEvent(QPaintEvent *)
|
||||
QFontMetrics fm = p.fontMetrics();
|
||||
|
||||
int peaks_y = 0;
|
||||
int peaks_height = fm.height();
|
||||
int font_height = fm.height();
|
||||
|
||||
// Create rect where decibel markings will go on the side
|
||||
QRect db_labels_rect = rect();
|
||||
db_labels_rect.setWidth(QFontMetricsWidth(p.fontMetrics(), "-00"));
|
||||
db_labels_rect.adjust(0, peaks_height, 0, 0);
|
||||
db_labels_rect.adjust(0, font_height, 0, 0);
|
||||
|
||||
// Determine rect where the main meter will go
|
||||
QRect full_meter_rect = rect();
|
||||
full_meter_rect.adjust(db_labels_rect.width(), peaks_height, 0, 0);
|
||||
full_meter_rect.adjust(db_labels_rect.width(), font_height, 0, 0);
|
||||
|
||||
// Draw decibel markings
|
||||
QRect last_db_marking_rect;
|
||||
// Width of each channel in the meter
|
||||
int channel_width = full_meter_rect.width() / params_.channel_count();
|
||||
|
||||
for (int i=0;i>=kDecibelMinimum;i-=kDecibelStep) {
|
||||
QString db_label;
|
||||
if (cached_background_.size() != size()
|
||||
|| cached_channels_ != params_.channel_count()) {
|
||||
|
||||
if (i <= kDecibelMinimum) {
|
||||
db_label = "-∞";
|
||||
} else {
|
||||
db_label = QStringLiteral("%1").arg(i);
|
||||
cached_channels_ = params_.channel_count();
|
||||
|
||||
// Generate new background
|
||||
cached_background_ = QPixmap(size());
|
||||
cached_background_.fill(Qt::transparent);
|
||||
|
||||
QPainter cached_painter(&cached_background_);
|
||||
|
||||
{
|
||||
// Draw decibel markings
|
||||
QRect last_db_marking_rect;
|
||||
|
||||
cached_painter.setPen(palette().text().color());
|
||||
|
||||
for (int i=0;i>=kDecibelMinimum;i-=kDecibelStep) {
|
||||
QString db_label;
|
||||
|
||||
if (i <= kDecibelMinimum) {
|
||||
db_label = "-∞";
|
||||
} else {
|
||||
db_label = QStringLiteral("%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.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)) {
|
||||
cached_painter.drawText(db_marking_rect, Qt::AlignRight, db_label);
|
||||
cached_painter.drawLine(db_marking_rect.topLeft(), db_marking_rect.topRight());
|
||||
|
||||
last_db_marking_rect = db_marking_rect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qreal log_val = QAudio::convertVolume(i, QAudio::DecibelVolumeScale, QAudio::LogarithmicVolumeScale);
|
||||
{
|
||||
// Draw bars
|
||||
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)
|
||||
});
|
||||
|
||||
QRect db_marking_rect = db_labels_rect;
|
||||
db_marking_rect.adjust(0, db_labels_rect.height() - qRound(log_val * db_labels_rect.height()), 0, 0);
|
||||
db_marking_rect.setHeight(fm.height());
|
||||
cached_painter.setPen(Qt::black);
|
||||
|
||||
// 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());
|
||||
for (int i=0;i<params_.channel_count();i++) {
|
||||
int channel_x = full_meter_rect.x() + channel_width * i;
|
||||
|
||||
last_db_marking_rect = db_marking_rect;
|
||||
QRect peaks_rect(channel_x, peaks_y, channel_width, font_height);
|
||||
|
||||
QRect meter_rect = full_meter_rect;
|
||||
meter_rect.setX(channel_x);
|
||||
meter_rect.setWidth(channel_width);
|
||||
|
||||
// Draw peak rects
|
||||
cached_painter.setBrush(Qt::red);
|
||||
cached_painter.drawRect(peaks_rect);
|
||||
|
||||
// Draw gradient meter
|
||||
cached_painter.setBrush(g);
|
||||
cached_painter.drawRect(meter_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)
|
||||
});
|
||||
p.drawPixmap(0, 0, cached_background_);
|
||||
|
||||
int channel_width = full_meter_rect.width() / channels;
|
||||
QVector<double> v(params_.channel_count(), 0);
|
||||
|
||||
for (int i=0;i<channels;i++) {
|
||||
if (file_.isOpen()) {
|
||||
UpdateValuesFromFile(v);
|
||||
}
|
||||
|
||||
PushValue(v);
|
||||
|
||||
QVector<double> vals = GetAverages();
|
||||
|
||||
p.setBrush(QColor(0, 0, 0, 128));
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
for (int i=0;i<params_.channel_count();i++) {
|
||||
int channel_x = full_meter_rect.x() + channel_width * i;
|
||||
|
||||
QRect peaks_rect(channel_x, peaks_y, channel_width, peaks_height);
|
||||
QRect peaks_rect(channel_x, peaks_y, channel_width, font_height);
|
||||
|
||||
QRect meter_rect = full_meter_rect;
|
||||
meter_rect.setX(channel_x);
|
||||
meter_rect.setWidth(channel_width);
|
||||
|
||||
p.setPen(Qt::black);
|
||||
|
||||
// Draw peak rects
|
||||
p.setBrush(Qt::red);
|
||||
p.drawRect(peaks_rect);
|
||||
|
||||
// Draw gradient meter
|
||||
p.setBrush(g);
|
||||
p.drawRect(meter_rect);
|
||||
|
||||
// Draw inverted semi-transparent black overlay depending on information
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
// Validate value and whether it peaked
|
||||
double vol = values_.at(i);
|
||||
double vol = vals.at(i);
|
||||
if (vol > 1.0) {
|
||||
peaked_[i] = true;
|
||||
}
|
||||
@@ -151,13 +241,12 @@ void AudioMonitor::paintEvent(QPaintEvent *)
|
||||
// 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))
|
||||
if (!peaked_.at(i)) {
|
||||
p.drawRect(peaks_rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,4 +256,54 @@ void AudioMonitor::mousePressEvent(QMouseEvent *)
|
||||
update();
|
||||
}
|
||||
|
||||
void AudioMonitor::UpdateValuesFromFile(QVector<double>& v)
|
||||
{
|
||||
// Determines how many milliseconds have passed since last update
|
||||
qint64 current_time = QDateTime::currentMSecsSinceEpoch();
|
||||
qint64 time_passed = current_time - last_time_;
|
||||
|
||||
// Determine how many bytes this is
|
||||
int bytes_to_read = params_.time_to_bytes(static_cast<double>(time_passed) * 0.001);
|
||||
|
||||
QByteArray b = file_.read(bytes_to_read);
|
||||
|
||||
const float* samples = reinterpret_cast<const float*>(b.constData());
|
||||
int nb_samples = b.size() / sizeof(float);
|
||||
|
||||
for (int i=0;i<nb_samples;i++) {
|
||||
int channel = i % params_.channel_count();
|
||||
|
||||
float abs_sample = samples[i];
|
||||
|
||||
if (abs_sample > v.at(channel)) {
|
||||
v.replace(channel, abs_sample);
|
||||
}
|
||||
}
|
||||
|
||||
last_time_ = current_time;
|
||||
}
|
||||
|
||||
void AudioMonitor::PushValue(const QVector<double> &v)
|
||||
{
|
||||
values_.removeFirst();
|
||||
values_.append(v);
|
||||
}
|
||||
|
||||
QVector<double> AudioMonitor::GetAverages() const
|
||||
{
|
||||
QVector<double> v(params_.channel_count(), 0);
|
||||
|
||||
for (int i=0;i<values_.size();i++) {
|
||||
for (int j=0;j<v.size();j++) {
|
||||
v[j] += values_.at(i).at(j);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0;i<v.size();i++) {
|
||||
v[i] /= static_cast<double>(values_.size());
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
#ifndef AUDIOMONITORWIDGET_H
|
||||
#define AUDIOMONITORWIDGET_H
|
||||
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "render/audioparams.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
@@ -34,19 +36,41 @@ class AudioMonitor : public QWidget
|
||||
public:
|
||||
AudioMonitor(QWidget* parent = nullptr);
|
||||
|
||||
virtual ~AudioMonitor() override;
|
||||
|
||||
public slots:
|
||||
void SetValues(QVector<double> values);
|
||||
void Clear();
|
||||
void SetParams(const AudioRenderingParams& params);
|
||||
|
||||
void OutputDeviceSet(const QString& filename, qint64 offset, int playback_speed);
|
||||
|
||||
void Stop();
|
||||
|
||||
protected:
|
||||
virtual void paintEvent(QPaintEvent* event) override;
|
||||
virtual void mousePressEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
QVector<double> values_;
|
||||
void UpdateValuesFromFile(QVector<double> &v);
|
||||
|
||||
void PushValue(const QVector<double>& v);
|
||||
|
||||
QVector<double> GetAverages() const;
|
||||
|
||||
AudioRenderingParams params_;
|
||||
|
||||
QFile file_;
|
||||
qint64 last_time_;
|
||||
|
||||
int playback_speed_;
|
||||
|
||||
QVector< QVector<double> > values_;
|
||||
QVector<bool> peaked_;
|
||||
|
||||
QTimer clear_timer_;
|
||||
QPixmap cached_background_;
|
||||
int cached_channels_;
|
||||
|
||||
QTimer update_timer_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
Reference in New Issue
Block a user