/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar 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 . ***/ #include "audiomonitor.h" #include #include #include #include "oakutil/decibel.h" #include "oakengine/preview.h" #include "oakengine/viewer.h" #include "oakutil/qtutils.h" namespace olive { const int k_decibel_step = 6; const int k_decibel_minimum = -198; // Must be divisible by kDecibelStep for infinity to appear const int k_maximum_smoothness = 8; /// Channel capacity of the summary buffers passed to /// oakengine_waveform_cache_get_summary() const int k_max_summary_channels = 64; QVector AudioMonitor::instances; AudioMonitor::AudioMonitor(QWidget *parent) : QOpenGLWidget(parent) , waveform_(nullptr) , cached_channels_(0) { instances.append(this); values_.resize(k_maximum_smoothness); this->setMinimumWidth(this->fontMetrics().height()); } AudioMonitor::~AudioMonitor() { instances.removeOne(this); } void AudioMonitor::set_params(const AudioParams ¶ms) { if (params_ != params) { 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); update(); } } void AudioMonitor::stop() { waveform_ = nullptr; // We don't stop the update loop here so that the monitor can show a smooth fade out. The update // loop will stop itself since file_ and waveform_ are null. } void AudioMonitor::push_sample_buffer(const SampleBuffer &d) { if (!params_.channel_count()) { return; } QVector v(params_.channel_count(), 0); if (d.is_allocated() && d.channel_count() > 0) { int channels = d.channel_count(); QVector ptrs(channels); for (int ch = 0; ch < channels; ch++) { ptrs[ch] = d.data(ch); } QVector levels(channels, 0.0); if (oakengine_audio_analyze_levels(ptrs.data(), channels, d.sample_count(), levels.data()) == OAKENGINE_OK) { for (int i = 0; i < v.size() && i < levels.size(); i++) { v[i] = levels[i]; } } } // Fill values because they get averaged out for smoothing values_.fill(v); set_update_loop(true); } void AudioMonitor::start_waveform(const void *waveform, const Rational &start, int playback_speed) { stop(); // The waveform cache C ABI reports the length in sample frames at the // cache's own sample rate; convert to seconds (AudioWaveformCache::length()) const int sample_rate = oakengine_waveform_cache_sample_rate(waveform); if (sample_rate <= 0) { return; } waveform_length_ = Rational(static_cast(oakengine_waveform_cache_length(waveform)), sample_rate); if (start >= waveform_length_) { return; } waveform_ = waveform; waveform_time_ = start; playback_speed_ = playback_speed; last_time_ = QDateTime::currentMSecsSinceEpoch(); set_update_loop(true); } void AudioMonitor::set_update_loop(bool e) { if (e) { connect(this, &AudioMonitor::frameSwapped, this, static_cast(&AudioMonitor::update)); update(); } else { disconnect( this, &AudioMonitor::frameSwapped, this, static_cast(&AudioMonitor::update)); } } void AudioMonitor::paintGL() { QPainter p(this); QPalette palette = qApp->palette(); QRect geometry(0, 0, width(), height()); p.fillRect(geometry, palette.window().color()); if (!params_.channel_count()) { return; } QFont f = p.font(); f.setPointSizeF(f.pointSizeF() * 0.75); p.setFont(f); QFontMetrics fm = p.fontMetrics(); int font_height = fm.height(); bool horizontal = this->width() > this->height(); int minimum_width = this->fontMetrics().height() * 3; // Determine rect where the main meter will go QRect full_meter_rect = geometry; // Create rect where decibel markings will go on the side QRect db_labels_rect; bool draw_text; // Width of each channel in the meter int peaks_pos; int channel_size; int db_line_length = fm.horizontalAdvance(QStringLiteral("-")); int db_width = QtUtils::q_font_metrics_width(p.fontMetrics(), "-00 "); if (horizontal) { // Insert peaks area full_meter_rect.adjust(0, 0, -font_height, 0); draw_text = (height() >= minimum_width); // Derive dB label rect if (draw_text) { // Insert meter rect full_meter_rect.adjust(db_width / 2, 0, 0, -font_height); db_labels_rect = full_meter_rect; db_labels_rect.setTop(db_labels_rect.bottom()); db_labels_rect.setHeight(font_height + db_line_length); } // Divide height by channel count channel_size = full_meter_rect.height() / params_.channel_count(); // Set peaks Y to right-most peaks_pos = full_meter_rect.right(); } else { // Insert peaks rect full_meter_rect.adjust(0, font_height, 0, 0); draw_text = (width() >= minimum_width); // Derive dB label rect if (draw_text) { int db_width_and_line = db_width + db_line_length; // Insert meter rect full_meter_rect.adjust(db_width_and_line, 0, 0, -font_height / 2); db_labels_rect = full_meter_rect; db_labels_rect.setX(0); db_labels_rect.setWidth(db_width_and_line); } // Divide width by channel count channel_size = full_meter_rect.width() / params_.channel_count(); // Set peaks Y to very top peaks_pos = 0; } if (cached_background_.size() != size() || cached_channels_ != params_.channel_count()) { cached_channels_ = params_.channel_count(); // Generate new background cached_background_ = QPixmap(size()); cached_background_.fill(Qt::transparent); QPainter cached_painter(&cached_background_); if (draw_text) { cached_painter.setFont(f); // Draw decibel markings QRect last_db_marking_rect; cached_painter.setPen(palette.text().color()); for (int i = 0; i >= k_decibel_minimum; i -= k_decibel_step) { QString db_label; qreal log_val; if (i == k_decibel_minimum) { db_label = QStringLiteral("-∞ "); log_val = 0; } else { db_label = QStringLiteral("%1 ").arg(i); log_val = Decibel::to_logarithmic(i); } QLine db_line; QRect db_marking_rect; bool overlaps_infinity = false; if (horizontal) { int x = db_labels_rect.x() + qRound(log_val * db_labels_rect.width()); db_marking_rect = QRect( x - db_width / 2, db_labels_rect.top() + db_line_length, db_width, db_labels_rect.height() - db_line_length); db_line = QLine(x, db_labels_rect.top(), x, db_labels_rect.top() + db_line_length); overlaps_infinity = db_marking_rect.left() < db_labels_rect.left() + db_width / 2; } else { int y = db_labels_rect.y() + db_labels_rect.height() - qRound(log_val * db_labels_rect.height()); db_marking_rect = QRect( db_labels_rect.x(), y - font_height / 2, db_labels_rect.width() - db_line_length, font_height); db_line = QLine(db_marking_rect.right() + 1, y, db_labels_rect.width(), y); overlaps_infinity = db_marking_rect.bottom() >= db_labels_rect.bottom() - font_height; } if (overlaps_infinity && i == k_decibel_minimum) { overlaps_infinity = false; } // Prevent any dB markings overlapping if (i == 0 || (!db_marking_rect.intersects(last_db_marking_rect) && !overlaps_infinity)) { cached_painter.drawText(db_marking_rect, Qt::AlignCenter, db_label); cached_painter.drawLine(db_line); last_db_marking_rect = db_marking_rect; } } } { // Draw bars QLinearGradient g; if (horizontal) { g.setStart(full_meter_rect.topRight()); g.setFinalStop(full_meter_rect.topLeft()); } else { g.setStart(full_meter_rect.topLeft()); g.setFinalStop(full_meter_rect.bottomLeft()); } g.setStops({ QGradientStop(0.0, Qt::red), QGradientStop(0.25, Qt::yellow), QGradientStop(1.0, Qt::green) }); cached_painter.setPen(Qt::black); for (int i = 0; i < params_.channel_count(); i++) { QRect peaks_rect, meter_rect; if (horizontal) { int channel_y = full_meter_rect.y() + channel_size * i; peaks_rect = QRect(peaks_pos, channel_y, font_height, channel_size); meter_rect = full_meter_rect; meter_rect.setY(channel_y); meter_rect.setHeight(channel_size); } else { int channel_x = full_meter_rect.x() + channel_size * i; peaks_rect = QRect(channel_x, peaks_pos, channel_size, font_height); meter_rect = full_meter_rect; meter_rect.setX(channel_x); meter_rect.setWidth(channel_size); } // 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); } } } p.drawPixmap(0, 0, cached_background_); QVector v(params_.channel_count(), 0); if (is_playing()) { // Determines how many milliseconds have passed since last update qint64 current_time = QDateTime::currentMSecsSinceEpoch(); qint64 delta_time = current_time - last_time_; int abs_speed = qAbs(playback_speed_); // Multiply by speed if the speed is not 1 if (abs_speed != 1) { delta_time *= abs_speed; } if (waveform_) { update_values_from_waveform(v, delta_time); if (waveform_time_ >= waveform_length_) { stop(); } } last_time_ = current_time; } push_value(v); QVector vals = get_averages(); p.setBrush(QColor(0, 0, 0, 128)); p.setPen(Qt::NoPen); bool all_zeroes = true; for (int i = 0; i < params_.channel_count(); i++) { // Validate value and whether it peaked double vol = vals.at(i); if (vol > 1.0) { peaked_[i] = true; } if (all_zeroes && !qIsNull(vol)) { all_zeroes = false; } // Convert val to logarithmic scale vol = Decibel::linear_to_logarithmic(vol); QRect peaks_rect, meter_rect; if (horizontal) { int channel_y = full_meter_rect.y() + channel_size * i; peaks_rect = QRect(peaks_pos, channel_y, font_height, channel_size); meter_rect = full_meter_rect; meter_rect.setY(channel_y); meter_rect.setHeight(channel_size); meter_rect.adjust(qRound(meter_rect.width() * vol), 0, 0, 0); } else { int channel_x = full_meter_rect.x() + channel_size * i; peaks_rect = QRect(channel_x, peaks_pos, channel_size, font_height); meter_rect = full_meter_rect; meter_rect.setX(channel_x); meter_rect.setWidth(channel_size); meter_rect.adjust(0, 0, 0, -qRound(meter_rect.height() * vol)); } p.drawRect(meter_rect); if (!peaked_.at(i)) { p.drawRect(peaks_rect); } } if (all_zeroes && !is_playing()) { // Optimize by disabling the update loop set_update_loop(false); } } void AudioMonitor::mousePressEvent(QMouseEvent *) { peaked_.fill(false); update(); } void AudioMonitor::update_values_from_waveform(QVector &v, qint64 delta_time) { // Delta time is provided in milliseconds, so we convert to seconds in Rational Rational length(delta_time, 1000); const int sample_rate = oakengine_waveform_cache_sample_rate(waveform_); if (sample_rate <= 0) { return; } // The summary C ABI works in sample frames at the cache's sample rate // (AudioWaveformCache::get_summary_from_time() equivalent) const Rational sample_tb(1, sample_rate); const int64_t start_ts = core::Timecode::time_to_timestamp( waveform_time_, sample_tb, core::Timecode::k_round); const int64_t end_ts = core::Timecode::time_to_timestamp( waveform_time_ + length, sample_tb, core::Timecode::k_round); double min_vals[k_max_summary_channels]; double max_vals[k_max_summary_channels]; int channels = 0; if (oakengine_waveform_cache_get_summary(waveform_, start_ts, end_ts, min_vals, max_vals, k_max_summary_channels, &channels) == OAKENGINE_OK) { audio_visual_waveform_sample_to_internal_values(min_vals, max_vals, channels, v); } waveform_time_ += length; } void AudioMonitor::audio_visual_waveform_sample_to_internal_values( const double *min_vals, const double *max_vals, int channels, QVector &out) { for (int i = 0; i < channels; i++) { double max = qMax(qAbs(min_vals[i]), qAbs(max_vals[i])); int output_index = i % out.size(); if (max > out.at(output_index)) { out[output_index] = max; } } } void AudioMonitor::push_value(const QVector &v) { int lim = values_.size() - 1; for (int i = 0; i < lim; i++) { values_[i] = values_[i + 1]; } values_[lim] = v; } void AudioMonitor::bytes_to_sample_summary(const QByteArray &b, QVector &v) { const float *samples = reinterpret_cast(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); } } } QVector AudioMonitor::get_averages() const { QVector 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(values_.size()); } return v; } }