}
}
- // 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) {
samples_out[i] += samples_bus[i] * volume;
}
}
+
+ deinterleave_samples(samples_bus, &left, &right);
+ measure_bus_levels(bus_index, left, right, volume);
}
{
return samples_out;
}
-void AudioMixer::measure_bus_levels(unsigned bus_index, const vector<float> &left, const vector<float> &right)
+void AudioMixer::measure_bus_levels(unsigned bus_index, const vector<float> &left, const vector<float> &right, float volume)
{
- const float *ptrs[] = { left.data(), right.data() };
- {
- lock_guard<mutex> lock(audio_measure_mutex);
- bus_r128[bus_index]->process(left.size(), const_cast<float **>(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;
}
}
bus_levels.resize(input_mapping.buses.size());
{
lock_guard<mutex> 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());
}
}
- {
- lock_guard<mutex> 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;
}
}
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.
};
void reset_alsa_mutex_held(DeviceSpec device_spec);
std::map<DeviceSpec, DeviceInfo> get_devices_mutex_held() const;
void update_meters(const std::vector<float> &samples);
- void measure_bus_levels(unsigned bus_index, const std::vector<float> &left, const std::vector<float> &right);
+ void measure_bus_levels(unsigned bus_index, const std::vector<float> &left, const std::vector<float> &right, float volume);
void send_audio_level_callback();
unsigned num_cards;
std::atomic<float> compressor_threshold_dbfs[MAX_BUSES];
std::atomic<bool> 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.
CorrelationMeasurer correlation; // Under audio_measure_mutex.
Resampler peak_resampler; // Under audio_measure_mutex.
std::atomic<float> 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<std::unique_ptr<Ebu_r128_proc>> bus_r128;
};
#endif // !defined(_AUDIO_MIXER_H)
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);
}
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);
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);
steady_clock::time_point now = steady_clock::now();
// The meters are somewhat inefficient to update. Only update them
- // every 100 ms or so (we get updates every 5β20 ms).
+ // every 100 ms or so (we get updates every 5β20 ms). Note that this
+ // means that the digital peak meters are ever so slightly too low
+ // (each update won't be a faithful representation of the highest peak
+ // since the previous update, since there are frames we won't draw),
+ // but the _peak_ of the peak meters will be correct (it's tracked in
+ // AudioMixer, not here), and that's much more important.
double last_update_age = duration<double>(now - last_audio_level_callback).count();
if (last_update_age < 0.100) {
return;
for (unsigned bus_index = 0; bus_index < bus_levels.size(); ++bus_index) {
if (bus_index < audio_miniviews.size()) {
const AudioMixer::BusLevel &level = bus_levels[bus_index];
- audio_miniviews[bus_index]->vu_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));
<item>
<layout class="QHBoxLayout" name="vu_centerer">
<item>
- <widget class="VUMeter" name="vu_meter_meter" native="true">
+ <widget class="VUMeter" name="peak_meter" native="true">
<property name="maximumSize">
<size>
<width>20</width>
<number>0</number>
</property>
<item>
- <widget class="VUMeter" name="vu_meter_meter" native="true">
+ <widget class="VUMeter" name="peak_meter" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
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<double>(min_y, y);
min_y = std::min<double>(min_y, y + 1);
max_y = std::max<double>(max_y, y);
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));
}
}
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)
{
QPainter painter(this);
- float level_lufs[2];
+ float level_lufs[2], peak_lufs[2];
{
unique_lock<mutex> 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;
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);
}
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<std::mutex> 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);
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