AudioMixer::AudioMixer(unsigned num_cards)
: num_cards(num_cards),
- level_compressor(OUTPUT_FREQUENCY),
limiter(OUTPUT_FREQUENCY),
- compressor(OUTPUT_FREQUENCY),
correlation(OUTPUT_FREQUENCY)
{
for (unsigned bus_index = 0; bus_index < MAX_BUSES; ++bus_index) {
locut[bus_index].init(FILTER_HPF, 2);
locut_enabled[bus_index] = global_flags.locut_enabled;
+ gain_staging_db[bus_index] = global_flags.initial_gain_staging_db;
+ compressor[bus_index].reset(new StereoCompressor(OUTPUT_FREQUENCY));
+ compressor_threshold_dbfs[bus_index] = ref_level_dbfs - 12.0f; // -12 dB.
+ compressor_enabled[bus_index] = global_flags.compressor_enabled;
+ level_compressor[bus_index].reset(new StereoCompressor(OUTPUT_FREQUENCY));
+ level_compressor_enabled[bus_index] = global_flags.gain_staging_auto;
}
- set_gain_staging_db(global_flags.initial_gain_staging_db);
- set_gain_staging_auto(global_flags.gain_staging_auto);
- set_compressor_enabled(global_flags.compressor_enabled);
set_limiter_enabled(global_flags.limiter_enabled);
set_final_makeup_gain_auto(global_flags.final_makeup_gain_auto);
}
}
- // TODO: Move lo-cut etc. into each bus.
vector<float> samples_out, left, right;
samples_out.resize(num_samples * 2);
samples_bus.resize(num_samples * 2);
locut[bus_index].render(samples_bus.data(), samples_bus.size() / 2, locut_cutoff_hz * 2.0 * M_PI / OUTPUT_FREQUENCY, 0.5f);
}
+ {
+ lock_guard<mutex> lock(compressor_mutex);
+
+ // Apply a level compressor to get the general level right.
+ // Basically, if it's over about -40 dBFS, we squeeze it down to that level
+ // (or more precisely, near it, since we don't use infinite ratio),
+ // then apply a makeup gain to get it to -14 dBFS. -14 dBFS is, of course,
+ // entirely arbitrary, but from practical tests with speech, it seems to
+ // put ut around -23 LUFS, so it's a reasonable starting point for later use.
+ if (level_compressor_enabled[bus_index]) {
+ float threshold = 0.01f; // -40 dBFS.
+ float ratio = 20.0f;
+ float attack_time = 0.5f;
+ float release_time = 20.0f;
+ float makeup_gain = from_db(ref_level_dbfs - (-40.0f)); // +26 dB.
+ level_compressor[bus_index]->process(samples_bus.data(), samples_bus.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain);
+ gain_staging_db[bus_index] = to_db(level_compressor[bus_index]->get_attenuation() * makeup_gain);
+ } else {
+ // Just apply the gain we already had.
+ float g = from_db(gain_staging_db[bus_index]);
+ for (size_t i = 0; i < samples_bus.size(); ++i) {
+ samples_bus[i] *= g;
+ }
+ }
+
+#if 0
+ printf("level=%f (%+5.2f dBFS) attenuation=%f (%+5.2f dB) end_result=%+5.2f dB\n",
+ level_compressor.get_level(), to_db(level_compressor.get_level()),
+ level_compressor.get_attenuation(), to_db(level_compressor.get_attenuation()),
+ to_db(level_compressor.get_level() * level_compressor.get_attenuation() * makeup_gain));
+#endif
+
+ // The real compressor.
+ if (compressor_enabled[bus_index]) {
+ float threshold = from_db(compressor_threshold_dbfs[bus_index]);
+ float ratio = 20.0f;
+ float attack_time = 0.005f;
+ float release_time = 0.040f;
+ float makeup_gain = 2.0f; // +6 dB.
+ compressor[bus_index]->process(samples_bus.data(), samples_bus.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain);
+ // compressor_att = compressor.get_attenuation();
+ }
+ }
+
// TODO: We should measure post-fader.
deinterleave_samples(samples_bus, &left, &right);
measure_bus_levels(bus_index, left, right);
{
lock_guard<mutex> lock(compressor_mutex);
- // Apply a level compressor to get the general level right.
- // Basically, if it's over about -40 dBFS, we squeeze it down to that level
- // (or more precisely, near it, since we don't use infinite ratio),
- // then apply a makeup gain to get it to -14 dBFS. -14 dBFS is, of course,
- // entirely arbitrary, but from practical tests with speech, it seems to
- // put ut around -23 LUFS, so it's a reasonable starting point for later use.
- {
- if (level_compressor_enabled) {
- float threshold = 0.01f; // -40 dBFS.
- float ratio = 20.0f;
- float attack_time = 0.5f;
- float release_time = 20.0f;
- float makeup_gain = from_db(ref_level_dbfs - (-40.0f)); // +26 dB.
- level_compressor.process(samples_out.data(), samples_out.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain);
- gain_staging_db = to_db(level_compressor.get_attenuation() * makeup_gain);
- } else {
- // Just apply the gain we already had.
- float g = from_db(gain_staging_db);
- for (size_t i = 0; i < samples_out.size(); ++i) {
- samples_out[i] *= g;
- }
- }
- }
-
- #if 0
- printf("level=%f (%+5.2f dBFS) attenuation=%f (%+5.2f dB) end_result=%+5.2f dB\n",
- level_compressor.get_level(), to_db(level_compressor.get_level()),
- level_compressor.get_attenuation(), to_db(level_compressor.get_attenuation()),
- to_db(level_compressor.get_level() * level_compressor.get_attenuation() * makeup_gain));
- #endif
-
- // float limiter_att, compressor_att;
-
- // The real compressor.
- if (compressor_enabled) {
- float threshold = from_db(compressor_threshold_dbfs);
- float ratio = 20.0f;
- float attack_time = 0.005f;
- float release_time = 0.040f;
- float makeup_gain = 2.0f; // +6 dB.
- compressor.process(samples_out.data(), samples_out.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain);
- // compressor_att = compressor.get_attenuation();
- }
-
// Finally a limiter at -4 dB (so, -10 dBFS) to take out the worst peaks only.
// Note that since ratio is not infinite, we could go slightly higher than this.
if (limiter_enabled) {
// printf("limiter=%+5.1f compressor=%+5.1f\n", to_db(limiter_att), to_db(compressor_att));
}
- // At this point, we are most likely close to +0 LU, but all of our
+ // At this point, we are most likely close to +0 LU (at least if the
+ // faders sum to 0 dB and the compressors are on), but all of our
// measurements have been on raw sample values, not R128 values.
// So we have a final makeup gain to get us to +0 LU; the gain
// adjustments required should be relatively small, and also, the
audio_level_callback(loudness_s, to_db(peak), bus_loudness,
loudness_i, loudness_range_low, loudness_range_high,
- gain_staging_db,
+ vector<float>(gain_staging_db, gain_staging_db + MAX_BUSES),
to_db(final_makeup_gain),
correlation.get_correlation());
}
return limiter_threshold_dbfs;
}
- float get_compressor_threshold_dbfs() const
+ float get_compressor_threshold_dbfs(unsigned bus_index) const
{
- return compressor_threshold_dbfs;
+ return compressor_threshold_dbfs[bus_index];
}
void set_limiter_threshold_dbfs(float threshold_dbfs)
limiter_threshold_dbfs = threshold_dbfs;
}
- void set_compressor_threshold_dbfs(float threshold_dbfs)
+ void set_compressor_threshold_dbfs(unsigned bus_index, float threshold_dbfs)
{
- compressor_threshold_dbfs = threshold_dbfs;
+ compressor_threshold_dbfs[bus_index] = threshold_dbfs;
}
void set_limiter_enabled(bool enabled)
return limiter_enabled;
}
- void set_compressor_enabled(bool enabled)
+ void set_compressor_enabled(unsigned bus_index, bool enabled)
{
- compressor_enabled = enabled;
+ compressor_enabled[bus_index] = enabled;
}
- bool get_compressor_enabled() const
+ bool get_compressor_enabled(unsigned bus_index) const
{
- return compressor_enabled;
+ return compressor_enabled[bus_index];
}
- void set_gain_staging_db(float gain_db)
+ void set_gain_staging_db(unsigned bus_index, float gain_db)
{
std::unique_lock<std::mutex> lock(compressor_mutex);
- level_compressor_enabled = false;
- gain_staging_db = gain_db;
+ level_compressor_enabled[bus_index] = false;
+ gain_staging_db[bus_index] = gain_db;
}
- float get_gain_staging_db() const
+ float get_gain_staging_db(unsigned bus_index) const
{
std::unique_lock<std::mutex> lock(compressor_mutex);
- return gain_staging_db;
+ return gain_staging_db[bus_index];
}
- void set_gain_staging_auto(bool enabled)
+ void set_gain_staging_auto(unsigned bus_index, bool enabled)
{
std::unique_lock<std::mutex> lock(compressor_mutex);
- level_compressor_enabled = enabled;
+ level_compressor_enabled[bus_index] = enabled;
}
- bool get_gain_staging_auto() const
+ bool get_gain_staging_auto(unsigned bus_index) const
{
std::unique_lock<std::mutex> lock(compressor_mutex);
- return level_compressor_enabled;
+ return level_compressor_enabled[bus_index];
}
void set_final_makeup_gain_db(float gain_db)
typedef std::function<void(float level_lufs, float peak_db,
std::vector<float> bus_level_lufs,
float global_level_lufs, float range_low_lufs, float range_high_lufs,
- float gain_staging_db, float final_makeup_gain_db,
+ std::vector<float> gain_staging_db, float final_makeup_gain_db,
float correlation)> audio_level_callback_t;
void set_audio_level_callback(audio_level_callback_t callback)
{
// First compressor; takes us up to about -12 dBFS.
mutable std::mutex compressor_mutex;
- StereoCompressor level_compressor; // Under compressor_mutex. Used to set/override gain_staging_db if <level_compressor_enabled>.
- float gain_staging_db = 0.0f; // Under compressor_mutex.
- bool level_compressor_enabled = true; // Under compressor_mutex.
+ std::unique_ptr<StereoCompressor> level_compressor[MAX_BUSES]; // Under compressor_mutex. Used to set/override gain_staging_db if <level_compressor_enabled>.
+ float gain_staging_db[MAX_BUSES]; // Under compressor_mutex.
+ bool level_compressor_enabled[MAX_BUSES]; // Under compressor_mutex.
static constexpr float ref_level_dbfs = -14.0f; // Chosen so that we end up around 0 LU in practice.
static constexpr float ref_level_lufs = -23.0f; // 0 LU, more or less by definition.
StereoCompressor limiter;
std::atomic<float> limiter_threshold_dbfs{ref_level_dbfs + 4.0f}; // 4 dB.
std::atomic<bool> limiter_enabled{true};
- StereoCompressor compressor;
- std::atomic<float> compressor_threshold_dbfs{ref_level_dbfs - 12.0f}; // -12 dB.
- std::atomic<bool> compressor_enabled{true};
+ std::unique_ptr<StereoCompressor> compressor[MAX_BUSES];
+ std::atomic<float> compressor_threshold_dbfs[MAX_BUSES];
+ std::atomic<bool> compressor_enabled[MAX_BUSES];
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.
connect(ui->locut_enabled, &QCheckBox::stateChanged, [this](int state){
global_mixer->get_audio_mixer()->set_locut_enabled(0, state == Qt::Checked);
});
-#else
- ui->locut_enabled->setVisible(false);
-#endif
ui->gainstaging_knob->setValue(global_mixer->get_audio_mixer()->get_gain_staging_db());
ui->gainstaging_auto_checkbox->setChecked(global_mixer->get_audio_mixer()->get_gain_staging_auto());
ui->compressor_enabled->setChecked(global_mixer->get_audio_mixer()->get_compressor_enabled());
+ connect(ui->gainstaging_knob, &QAbstractSlider::valueChanged, this, &MainWindow::gain_staging_knob_changed);
+ connect(ui->gainstaging_auto_checkbox, &QCheckBox::stateChanged, [this](int state){
+ global_mixer->get_audio_mixer()->set_gain_staging_auto(state == Qt::Checked);
+ });
+ ui->compressor_threshold_db_display->setText(
+ QString::fromStdString(format_db(mixer->get_audio_mixer()->get_compressor_threshold_dbfs(), DB_WITH_SIGN)));
+ ui->compressor_threshold_db_display->setText(buf);
+ connect(ui->compressor_threshold_knob, &QDial::valueChanged, this, &MainWindow::compressor_threshold_knob_changed);
+ connect(ui->compressor_enabled, &QCheckBox::stateChanged, [this](int state){
+ global_mixer->get_audio_mixer()->set_compressor_enabled(state == Qt::Checked);
+ });
+#else
+ ui->locut_enabled->setVisible(false);
+ ui->gainstaging_label->setVisible(false);
+ ui->gainstaging_knob->setVisible(false);
+ ui->gainstaging_db_display->setVisible(false);
+ ui->gainstaging_auto_checkbox->setVisible(false);
+ ui->compressor_threshold_label->setVisible(false);
+ ui->compressor_threshold_knob->setVisible(false);
+ ui->compressor_threshold_db_display->setVisible(false);
+ ui->compressor_enabled->setVisible(false);
+#endif
ui->limiter_enabled->setChecked(global_mixer->get_audio_mixer()->get_limiter_enabled());
ui->makeup_gain_auto_checkbox->setChecked(global_mixer->get_audio_mixer()->get_final_makeup_gain_auto());
QString::fromStdString(format_db(mixer->get_audio_mixer()->get_limiter_threshold_dbfs(), DB_WITH_SIGN)));
ui->limiter_threshold_db_display->setText(limiter_threshold_label);
ui->limiter_threshold_db_display_2->setText(limiter_threshold_label);
- ui->compressor_threshold_db_display->setText(
- QString::fromStdString(format_db(mixer->get_audio_mixer()->get_compressor_threshold_dbfs(), DB_WITH_SIGN)));
connect(ui->locut_cutoff_knob, &QDial::valueChanged, this, &MainWindow::cutoff_knob_changed);
cutoff_knob_changed(ui->locut_cutoff_knob->value());
- connect(ui->gainstaging_knob, &QAbstractSlider::valueChanged, this, &MainWindow::gain_staging_knob_changed);
- connect(ui->gainstaging_auto_checkbox, &QCheckBox::stateChanged, [this](int state){
- global_mixer->get_audio_mixer()->set_gain_staging_auto(state == Qt::Checked);
- });
connect(ui->makeup_gain_knob, &QAbstractSlider::valueChanged, this, &MainWindow::final_makeup_gain_knob_changed);
connect(ui->makeup_gain_auto_checkbox, &QCheckBox::stateChanged, [this](int state){
global_mixer->get_audio_mixer()->set_final_makeup_gain_auto(state == Qt::Checked);
});
connect(ui->limiter_threshold_knob, &QDial::valueChanged, this, &MainWindow::limiter_threshold_knob_changed);
- connect(ui->compressor_threshold_knob, &QDial::valueChanged, this, &MainWindow::compressor_threshold_knob_changed);
connect(ui->limiter_enabled, &QCheckBox::stateChanged, [this](int state){
global_mixer->get_audio_mixer()->set_limiter_enabled(state == Qt::Checked);
});
- connect(ui->compressor_enabled, &QCheckBox::stateChanged, [this](int state){
- global_mixer->get_audio_mixer()->set_compressor_enabled(state == Qt::Checked);
- });
connect(ui->reset_meters_button, &QPushButton::clicked, this, &MainWindow::reset_meters_button_clicked);
mixer->get_audio_mixer()->set_audio_level_callback(bind(&MainWindow::audio_level_callback, this, _1, _2, _3, _4, _5, _6, _7, _8, _9));
ui->buses->addWidget(channel);
ui_audio_expanded_view->locut_enabled->setChecked(global_mixer->get_audio_mixer()->get_locut_enabled(bus_index));
- connect(ui->locut_enabled, &QCheckBox::stateChanged, [this, bus_index](int state){
+ connect(ui_audio_expanded_view->locut_enabled, &QCheckBox::stateChanged, [this, bus_index](int state){
global_mixer->get_audio_mixer()->set_locut_enabled(bus_index, state == Qt::Checked);
});
+ ui_audio_expanded_view->gainstaging_knob->setValue(global_mixer->get_audio_mixer()->get_gain_staging_db(bus_index));
+ ui_audio_expanded_view->gainstaging_auto_checkbox->setChecked(global_mixer->get_audio_mixer()->get_gain_staging_auto(bus_index));
+ ui_audio_expanded_view->compressor_enabled->setChecked(global_mixer->get_audio_mixer()->get_compressor_enabled(bus_index));
+
+ connect(ui_audio_expanded_view->gainstaging_knob, &QAbstractSlider::valueChanged, bind(&MainWindow::gain_staging_knob_changed, this, bus_index, _1));
+ connect(ui_audio_expanded_view->gainstaging_auto_checkbox, &QCheckBox::stateChanged, [this, bus_index](int state){
+ global_mixer->get_audio_mixer()->set_gain_staging_auto(bus_index, state == Qt::Checked);
+ });
+
+ connect(ui_audio_expanded_view->compressor_threshold_knob, &QDial::valueChanged, bind(&MainWindow::compressor_threshold_knob_changed, this, bus_index, _1));
+ connect(ui_audio_expanded_view->compressor_enabled, &QCheckBox::stateChanged, [this, bus_index](int state){
+ global_mixer->get_audio_mixer()->set_compressor_enabled(bus_index, state == Qt::Checked);
+ });
+
slave_fader(audio_miniviews[bus_index]->fader, ui_audio_expanded_view->fader);
}
}
}
}
-void MainWindow::gain_staging_knob_changed(int value)
+void MainWindow::gain_staging_knob_changed(unsigned bus_index, int value)
{
- ui->gainstaging_auto_checkbox->setCheckState(Qt::Unchecked);
+ if (bus_index == 0) {
+ ui->gainstaging_auto_checkbox->setCheckState(Qt::Unchecked);
+ }
+ if (bus_index < audio_expanded_views.size()) {
+ audio_expanded_views[bus_index]->gainstaging_auto_checkbox->setCheckState(Qt::Unchecked);
+ }
float gain_db = value * 0.1f;
- global_mixer->get_audio_mixer()->set_gain_staging_db(gain_db);
+ global_mixer->get_audio_mixer()->set_gain_staging_db(bus_index, gain_db);
// The label will be updated by the audio level callback.
}
QString::fromStdString(format_db(threshold_dbfs, DB_WITH_SIGN)));
}
-void MainWindow::compressor_threshold_knob_changed(int value)
+void MainWindow::compressor_threshold_knob_changed(unsigned bus_index, int value)
{
float threshold_dbfs = value * 0.1f;
- global_mixer->get_audio_mixer()->set_compressor_threshold_dbfs(threshold_dbfs);
- ui->compressor_threshold_db_display->setText(
- QString::fromStdString(format_db(threshold_dbfs, DB_WITH_SIGN)));
+ global_mixer->get_audio_mixer()->set_compressor_threshold_dbfs(bus_index, threshold_dbfs);
+
+ QString label(QString::fromStdString(format_db(threshold_dbfs, DB_WITH_SIGN)));
+ if (bus_index == 0) {
+ ui->compressor_threshold_db_display->setText(label);
+ }
+ if (bus_index < audio_expanded_views.size()) {
+ audio_expanded_views[bus_index]->compressor_threshold_db_display->setText(label);
+ }
}
void MainWindow::mini_fader_changed(int bus, double volume_db)
void MainWindow::audio_level_callback(float level_lufs, float peak_db, vector<float> bus_level_lufs,
float global_level_lufs,
float range_low_lufs, float range_high_lufs,
- float gain_staging_db, float final_makeup_gain_db,
+ vector<float> gain_staging_db, float final_makeup_gain_db,
float correlation)
{
steady_clock::time_point now = steady_clock::now();
if (bus_index < audio_miniviews.size()) {
audio_miniviews[bus_index]->vu_meter_meter->set_level(
bus_level_lufs[bus_index]);
+
+ Ui::AudioExpandedView *view = audio_expanded_views[bus_index];
+ view->vu_meter_meter->set_level(bus_level_lufs[bus_index]);
+ view->gainstaging_knob->blockSignals(true);
+ view->gainstaging_knob->setValue(lrintf(gain_staging_db[bus_index] * 10.0f));
+ view->gainstaging_knob->blockSignals(false);
+ view->gainstaging_db_display->setText(
+ QString("Gain: ") +
+ QString::fromStdString(format_db(gain_staging_db[bus_index], DB_WITH_SIGN)));
}
}
ui->lra_meter->set_levels(global_level_lufs, range_low_lufs, range_high_lufs);
ui->peak_display->setStyleSheet("");
}
+ // NOTE: Will be invisible when using multitrack audio.
ui->gainstaging_knob->blockSignals(true);
- ui->gainstaging_knob->setValue(lrintf(gain_staging_db * 10.0f));
+ ui->gainstaging_knob->setValue(lrintf(gain_staging_db[0] * 10.0f));
ui->gainstaging_knob->blockSignals(false);
ui->gainstaging_db_display->setText(
- QString::fromStdString(format_db(gain_staging_db, DB_WITH_SIGN)));
+ QString::fromStdString(format_db(gain_staging_db[0], DB_WITH_SIGN)));
ui->makeup_gain_knob->blockSignals(true);
ui->makeup_gain_knob->setValue(lrintf(final_makeup_gain_db * 10.0f));
void set_transition_names(std::vector<std::string> transition_names);
void update_channel_name(Mixer::Output output, const std::string &name);
void update_channel_color(Mixer::Output output, const std::string &color);
- void gain_staging_knob_changed(int value);
+ void gain_staging_knob_changed(unsigned bus_index, int value);
void final_makeup_gain_knob_changed(int value);
void cutoff_knob_changed(int value);
void limiter_threshold_knob_changed(int value);
- void compressor_threshold_knob_changed(int value);
+ void compressor_threshold_knob_changed(unsigned bus_index, int value);
void mini_fader_changed(int bus, double db_volume);
void reset_meters_button_clicked();
void relayout();
void report_disk_space(off_t free_bytes, double estimated_seconds_left);
// Called from the mixer.
- void audio_level_callback(float level_lufs, float peak_db, std::vector<float> bus_level_lufs, float global_level_lufs, float range_low_lufs, float range_high_lufs, float gain_staging_db, float final_makeup_gain_db, float correlation);
+ void audio_level_callback(float level_lufs, float peak_db, std::vector<float> bus_level_lufs, float global_level_lufs, float range_low_lufs, float range_high_lufs, std::vector<float> gain_staging_db, float final_makeup_gain_db, float correlation);
std::chrono::steady_clock::time_point last_audio_level_callback;
Ui::MainWindow *ui;