From: Steinar H. Gunderson Date: Sun, 28 Aug 2016 15:49:23 +0000 (+0200) Subject: Replace the R128 meters for each channel with a digital peak meter. X-Git-Tag: 1.4.0~72 X-Git-Url: https://git.sesse.net/?p=nageru;a=commitdiff_plain;h=54067dbc70999d936adf9d263b5ff2b1efb4dfd0 Replace the R128 meters for each channel with a digital peak meter. R128 is a superior way of measuring loudness, but for each bus, we care more about getting the recording levels right (ie., that we don't peak) than loudness. Since we do our mixing in float, unlike a typical digital mixer, it's not as critical (at least assuming that the limiter is on and will save us later in the mastering chain), but it's probably better to be consistent with convention here. Thus, use a simple digital meter for each bus, even though we of course still use R128 for the mastering section. --- diff --git a/audio_mixer.cpp b/audio_mixer.cpp index 13eed33..6b7ffeb 100644 --- a/audio_mixer.cpp +++ b/audio_mixer.cpp @@ -462,10 +462,6 @@ vector AudioMixer::get_output(double pts, unsigned num_samples, Resamplin } } - // TODO: We should measure post-fader. - deinterleave_samples(samples_bus, &left, &right); - measure_bus_levels(bus_index, left, right); - float volume = from_db(fader_volume_db[bus_index]); if (bus_index == 0) { for (unsigned i = 0; i < num_samples * 2; ++i) { @@ -476,6 +472,9 @@ vector AudioMixer::get_output(double pts, unsigned num_samples, Resamplin samples_out[i] += samples_bus[i] * volume; } } + + deinterleave_samples(samples_bus, &left, &right); + measure_bus_levels(bus_index, left, right, volume); } { @@ -542,12 +541,36 @@ vector AudioMixer::get_output(double pts, unsigned num_samples, Resamplin return samples_out; } -void AudioMixer::measure_bus_levels(unsigned bus_index, const vector &left, const vector &right) +void AudioMixer::measure_bus_levels(unsigned bus_index, const vector &left, const vector &right, float volume) { - const float *ptrs[] = { left.data(), right.data() }; - { - lock_guard lock(audio_measure_mutex); - bus_r128[bus_index]->process(left.size(), const_cast(ptrs)); + assert(left.size() == right.size()); + const float peak_levels[2] = { + find_peak(left.data(), left.size()) * volume, + find_peak(right.data(), right.size()) * volume + }; + for (unsigned channel = 0; channel < 2; ++channel) { + // Compute the current value, including hold and falloff. + // The constants are borrowed from zita-mu1 by Fons Adriaensen. + static constexpr float hold_sec = 0.5f; + static constexpr float falloff_db_sec = 15.0f; // dB/sec falloff after hold. + float current_peak; + PeakHistory &history = peak_history[bus_index][channel]; + if (history.age_seconds < hold_sec) { + current_peak = history.last_peak; + } else { + current_peak = history.last_peak * from_db(-falloff_db_sec * (history.age_seconds - hold_sec)); + } + + // See if we have a new peak to replace the old (possibly falling) one. + if (peak_levels[channel] > current_peak) { + history.last_peak = peak_levels[channel]; + history.age_seconds = 0.0f; // Not 100% correct, but more than good enough given our frame sizes. + current_peak = peak_levels[channel]; + } else { + history.age_seconds += float(left.size()) / OUTPUT_FREQUENCY; + } + history.current_level = peak_levels[channel]; + history.current_peak = current_peak; } } @@ -611,8 +634,11 @@ void AudioMixer::send_audio_level_callback() bus_levels.resize(input_mapping.buses.size()); { lock_guard lock(compressor_mutex); - for (unsigned bus_index = 0; bus_index < bus_r128.size(); ++bus_index) { - bus_levels[bus_index].loudness_lufs = bus_r128[bus_index]->loudness_S(); + for (unsigned bus_index = 0; bus_index < bus_levels.size(); ++bus_index) { + bus_levels[bus_index].current_level_dbfs[0] = to_db(peak_history[bus_index][0].current_level); + bus_levels[bus_index].current_level_dbfs[1] = to_db(peak_history[bus_index][1].current_level); + bus_levels[bus_index].peak_level_dbfs[0] = to_db(peak_history[bus_index][0].current_peak); + bus_levels[bus_index].peak_level_dbfs[1] = to_db(peak_history[bus_index][1].current_peak); bus_levels[bus_index].gain_staging_db = gain_staging_db[bus_index]; if (compressor_enabled[bus_index]) { bus_levels[bus_index].compressor_attenuation_db = -to_db(compressor[bus_index]->get_attenuation()); @@ -693,17 +719,6 @@ void AudioMixer::set_input_mapping(const InputMapping &new_input_mapping) } } - { - lock_guard lock(audio_measure_mutex); - bus_r128.resize(new_input_mapping.buses.size()); - for (unsigned bus_index = 0; bus_index < bus_r128.size(); ++bus_index) { - if (bus_r128[bus_index] == nullptr) { - bus_r128[bus_index].reset(new Ebu_r128_proc); - } - bus_r128[bus_index]->init(2, OUTPUT_FREQUENCY); - } - } - input_mapping = new_input_mapping; } diff --git a/audio_mixer.h b/audio_mixer.h index 5a3f2af..6e7719f 100644 --- a/audio_mixer.h +++ b/audio_mixer.h @@ -202,7 +202,8 @@ public: } struct BusLevel { - float loudness_lufs; + float current_level_dbfs[2]; // Digital peak of last frame, left and right. + float peak_level_dbfs[2]; // Digital peak with hold, left and right. float gain_staging_db; float compressor_attenuation_db; // A positive number; 0.0 for no attenuation. }; @@ -236,7 +237,7 @@ private: void reset_alsa_mutex_held(DeviceSpec device_spec); std::map get_devices_mutex_held() const; void update_meters(const std::vector &samples); - void measure_bus_levels(unsigned bus_index, const std::vector &left, const std::vector &right); + void measure_bus_levels(unsigned bus_index, const std::vector &left, const std::vector &right, float volume); void send_audio_level_callback(); unsigned num_cards; @@ -269,6 +270,14 @@ private: std::atomic compressor_threshold_dbfs[MAX_BUSES]; std::atomic compressor_enabled[MAX_BUSES]; + struct PeakHistory { + float current_level = 0.0f; // Peak of the last frame (not in dB). + float current_peak = 0.0f; // Current peak of the peak meter (not in dB). + float last_peak = 0.0f; + float age_seconds = 0.0f; // Time since "last_peak" was set. + }; + PeakHistory peak_history[MAX_BUSES][2]; // Separate for each channel. + double final_makeup_gain = 1.0; // Under compressor_mutex. Read/write by the user. Note: Not in dB, we want the numeric precision so that we can change it slowly. bool final_makeup_gain_auto = true; // Under compressor_mutex. @@ -281,12 +290,6 @@ private: CorrelationMeasurer correlation; // Under audio_measure_mutex. Resampler peak_resampler; // Under audio_measure_mutex. std::atomic peak{0.0f}; - - // Under audio_measure_mutex. Note that Ebu_r128_proc has a broken - // copy constructor (it uses the default, but holds arrays), - // so we can't just use raw Ebu_r128_proc elements, but need to use - // unique_ptrs. - std::vector> bus_r128; }; #endif // !defined(_AUDIO_MIXER_H) diff --git a/lrameter.cpp b/lrameter.cpp index d7736e3..8121ee4 100644 --- a/lrameter.cpp +++ b/lrameter.cpp @@ -99,10 +99,10 @@ void LRAMeter::recalculate_pixmaps() on_pixmap = QPixmap(width(), height()); QPainter on_painter(&on_pixmap); on_painter.fillRect(0, 0, width(), height(), parentWidget()->palette().window()); - draw_vu_meter(on_painter, width(), height(), margin, true, min_level, max_level, /*flip=*/false); + draw_vu_meter(on_painter, width(), height(), margin, 2.0, true, min_level, max_level, /*flip=*/false); off_pixmap = QPixmap(width(), height()); QPainter off_painter(&off_pixmap); off_painter.fillRect(0, 0, width(), height(), parentWidget()->palette().window()); - draw_vu_meter(off_painter, width(), height(), margin, false, min_level, max_level, /*flip=*/false); + draw_vu_meter(off_painter, width(), height(), margin, 2.0, false, min_level, max_level, /*flip=*/false); } diff --git a/mainwindow.cpp b/mainwindow.cpp index 09dcaba..923c4f9 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -318,6 +318,13 @@ void MainWindow::setup_audio_miniview() ui_audio_miniview->bus_desc_label->setFullText( QString::fromStdString(mapping.buses[bus_index].name)); audio_miniviews[bus_index] = ui_audio_miniview; + + // Set up the peak meter. + VUMeter *peak_meter = ui_audio_miniview->peak_meter; + peak_meter->set_min_level(-30.0f); + peak_meter->set_max_level(0.0f); + peak_meter->set_ref_level(0.0f); + // TODO: Set the fader position. ui->faders->addWidget(channel); @@ -369,6 +376,12 @@ void MainWindow::setup_audio_expanded_view() slave_fader(audio_miniviews[bus_index]->fader, ui_audio_expanded_view->fader); + // Set up the peak meter. + VUMeter *peak_meter = ui_audio_expanded_view->peak_meter; + peak_meter->set_min_level(-30.0f); + peak_meter->set_max_level(0.0f); + peak_meter->set_ref_level(0.0f); + // Set up the compression attenuation meter. VUMeter *reduction_meter = ui_audio_expanded_view->reduction_meter; reduction_meter->set_min_level(0.0f); @@ -544,7 +557,12 @@ void MainWindow::audio_level_callback(float level_lufs, float peak_db, vector(now - last_audio_level_callback).count(); if (last_update_age < 0.100) { return; @@ -556,10 +574,17 @@ void MainWindow::audio_level_callback(float level_lufs, float peak_db, vectorvu_meter_meter->set_level(level.loudness_lufs); + Ui::AudioMiniView *miniview = audio_miniviews[bus_index]; + miniview->peak_meter->set_level( + level.current_level_dbfs[0], level.current_level_dbfs[1]); + miniview->peak_meter->set_peak( + level.peak_level_dbfs[0], level.peak_level_dbfs[1]); Ui::AudioExpandedView *view = audio_expanded_views[bus_index]; - view->vu_meter_meter->set_level(level.loudness_lufs); + view->peak_meter->set_level( + level.current_level_dbfs[0], level.current_level_dbfs[1]); + view->peak_meter->set_peak( + level.peak_level_dbfs[0], level.peak_level_dbfs[1]); view->reduction_meter->set_level(level.compressor_attenuation_db); view->gainstaging_knob->blockSignals(true); view->gainstaging_knob->setValue(lrintf(level.gain_staging_db * 10.0f)); diff --git a/ui_audio_expanded_view.ui b/ui_audio_expanded_view.ui index 7d9492e..d48721f 100644 --- a/ui_audio_expanded_view.ui +++ b/ui_audio_expanded_view.ui @@ -371,7 +371,7 @@ - + 20 diff --git a/ui_audio_miniview.ui b/ui_audio_miniview.ui index a261e88..96c0bd8 100644 --- a/ui_audio_miniview.ui +++ b/ui_audio_miniview.ui @@ -134,7 +134,7 @@ 0 - + 0 diff --git a/vu_common.cpp b/vu_common.cpp index 87254da..d4abab2 100644 --- a/vu_common.cpp +++ b/vu_common.cpp @@ -32,16 +32,16 @@ double lufs_to_pos(float level_lu, int height, float min_level, float max_level) return y; } -void draw_vu_meter(QPainter &painter, int width, int height, int margin, bool is_on, float min_level, float max_level, bool flip) +void draw_vu_meter(QPainter &painter, int width, int height, int horizontal_margin, double segment_margin, bool is_on, float min_level, float max_level, bool flip) { - painter.fillRect(margin, 0, width - 2 * margin, height, Qt::black); + painter.fillRect(horizontal_margin, 0, width - 2 * horizontal_margin, height, Qt::black); for (int y = 0; y < height; ++y) { // Find coverage of “on” rectangles in this pixel row. double coverage = 0.0; for (int level = floor(min_level); level <= ceil(max_level); ++level) { - double min_y = lufs_to_pos(level + 1.0, height, min_level, max_level) + 1.0; - double max_y = lufs_to_pos(level, height, min_level, max_level) - 1.0; + double min_y = lufs_to_pos(level + 1.0, height, min_level, max_level) + segment_margin * 0.5; + double max_y = lufs_to_pos(level, height, min_level, max_level) - segment_margin * 0.5; min_y = std::max(min_y, y); min_y = std::min(min_y, y + 1); max_y = std::max(max_y, y); @@ -70,6 +70,6 @@ void draw_vu_meter(QPainter &painter, int width, int height, int margin, bool is int g = lrintf(255 * pow(on_g * coverage, 1.0 / 2.2)); int b = lrintf(255 * pow(on_b * coverage, 1.0 / 2.2)); int draw_y = flip ? (height - y - 1) : y; - painter.fillRect(margin, draw_y, width - 2 * margin, 1, QColor(r, g, b)); + painter.fillRect(horizontal_margin, draw_y, width - 2 * horizontal_margin, 1, QColor(r, g, b)); } } diff --git a/vu_common.h b/vu_common.h index c5b1ac0..9c7098e 100644 --- a/vu_common.h +++ b/vu_common.h @@ -5,6 +5,6 @@ double lufs_to_pos(float level_lu, int height, float min_level, float max_level); -void draw_vu_meter(QPainter &painter, int width, int height, int margin, bool is_on, float min_level, float max_level, bool flip); +void draw_vu_meter(QPainter &painter, int width, int height, int horizontal_margin, double segment_margin, bool is_on, float min_level, float max_level, bool flip); #endif // !defined(_VU_COMMON_H) diff --git a/vumeter.cpp b/vumeter.cpp index 50f0f14..2bf3639 100644 --- a/vumeter.cpp +++ b/vumeter.cpp @@ -19,11 +19,13 @@ void VUMeter::paintEvent(QPaintEvent *event) { QPainter painter(this); - float level_lufs[2]; + float level_lufs[2], peak_lufs[2]; { unique_lock lock(level_mutex); level_lufs[0] = this->level_lufs[0]; level_lufs[1] = this->level_lufs[1]; + peak_lufs[0] = this->peak_lufs[0]; + peak_lufs[1] = this->peak_lufs[1]; } int mid = width() / 2; @@ -35,28 +37,39 @@ void VUMeter::paintEvent(QPaintEvent *event) int on_pos = lrint(lufs_to_pos(level_lu, height())); if (flip) { - QRect on_rect(left, 0, right, height() - on_pos); - QRect off_rect(left, height() - on_pos, right, height()); + QRect on_rect(left, 0, right - left, height() - on_pos); + QRect off_rect(left, height() - on_pos, right - left, height()); painter.drawPixmap(on_rect, on_pixmap, on_rect); painter.drawPixmap(off_rect, off_pixmap, off_rect); } else { - QRect off_rect(left, 0, right, on_pos); - QRect on_rect(left, on_pos, right, height() - on_pos); + QRect off_rect(left, 0, right - left, on_pos); + QRect on_rect(left, on_pos, right - left, height() - on_pos); painter.drawPixmap(off_rect, off_pixmap, off_rect); painter.drawPixmap(on_rect, on_pixmap, on_rect); } + + float peak_lu = peak_lufs[channel] - ref_level_lufs; + if (peak_lu >= min_level && peak_lu <= max_level) { + int peak_pos = lrint(lufs_to_pos(peak_lu, height())); + QRect peak_rect(left, peak_pos - 1, right, 2); + painter.drawPixmap(peak_rect, full_on_pixmap, peak_rect); + } } } void VUMeter::recalculate_pixmaps() { + full_on_pixmap = QPixmap(width(), height()); + QPainter full_on_painter(&full_on_pixmap); + draw_vu_meter(full_on_painter, width(), height(), 0, 0.0, true, min_level, max_level, flip); + on_pixmap = QPixmap(width(), height()); QPainter on_painter(&on_pixmap); - draw_vu_meter(on_painter, width(), height(), 0, true, min_level, max_level, flip); + draw_vu_meter(on_painter, width(), height(), 0, 2.0, true, min_level, max_level, flip); off_pixmap = QPixmap(width(), height()); QPainter off_painter(&off_pixmap); - draw_vu_meter(off_painter, width(), height(), 0, false, min_level, max_level, flip); + draw_vu_meter(off_painter, width(), height(), 0, 2.0, false, min_level, max_level, flip); } diff --git a/vumeter.h b/vumeter.h index 1fecf7e..9b17f43 100644 --- a/vumeter.h +++ b/vumeter.h @@ -27,6 +27,17 @@ public: QMetaObject::invokeMethod(this, "update", Qt::AutoConnection); } + void set_peak(float peak_lufs) { + set_peak(peak_lufs, peak_lufs); + } + + void set_peak(float peak_lufs_left, float peak_lufs_right) { + std::unique_lock lock(level_mutex); + this->peak_lufs[0] = peak_lufs_left; + this->peak_lufs[1] = peak_lufs_right; + QMetaObject::invokeMethod(this, "update", Qt::AutoConnection); + } + double lufs_to_pos(float level_lu, int height) { return ::lufs_to_pos(level_lu, height, min_level, max_level); @@ -62,10 +73,11 @@ private: std::mutex level_mutex; float level_lufs[2] { -HUGE_VALF, -HUGE_VALF }; + float peak_lufs[2] { -HUGE_VALF, -HUGE_VALF }; float min_level = -18.0f, max_level = 9.0f, ref_level_lufs = -23.0f; bool flip = false; - QPixmap on_pixmap, off_pixmap; + QPixmap full_on_pixmap, on_pixmap, off_pixmap; }; #endif