qt_files = qt5.preprocess(
moc_headers: ['nageru/analyzer.h', 'nageru/clickable_label.h', 'nageru/compression_reduction_meter.h', 'nageru/correlation_meter.h',
'nageru/ellipsis_label.h', 'nageru/glwidget.h', 'nageru/input_mapping_dialog.h', 'nageru/lrameter.h', 'nageru/mainwindow.h', 'nageru/midi_mapping_dialog.h',
- 'nageru/nonlinear_fader.h', 'nageru/vumeter.h'],
+ 'nageru/nonlinear_fader.h', 'nageru/vumeter.h', 'nageru/delay_analyzer.h', 'nageru/peak_display.h'],
ui_files: ['nageru/analyzer.ui', 'nageru/audio_expanded_view.ui', 'nageru/audio_miniview.ui', 'nageru/display.ui',
- 'nageru/input_mapping.ui', 'nageru/mainwindow.ui', 'nageru/midi_mapping.ui'],
+ 'nageru/input_mapping.ui', 'nageru/mainwindow.ui', 'nageru/midi_mapping.ui', 'nageru/delay_analyzer.ui'],
dependencies: qt5deps)
# Qt objects.
nageru_srcs += ['nageru/glwidget.cpp', 'nageru/mainwindow.cpp', 'nageru/vumeter.cpp', 'nageru/lrameter.cpp', 'nageru/compression_reduction_meter.cpp',
- 'nageru/correlation_meter.cpp', 'nageru/analyzer.cpp', 'nageru/input_mapping_dialog.cpp', 'nageru/midi_mapping_dialog.cpp',
- 'nageru/nonlinear_fader.cpp', 'nageru/context_menus.cpp', 'nageru/vu_common.cpp', 'nageru/piecewise_interpolator.cpp', 'nageru/midi_mapper.cpp']
+ 'nageru/correlation_meter.cpp', 'nageru/peak_display.cpp', 'nageru/analyzer.cpp', 'nageru/input_mapping_dialog.cpp', 'nageru/midi_mapping_dialog.cpp',
+ 'nageru/nonlinear_fader.cpp', 'nageru/context_menus.cpp', 'nageru/vu_common.cpp', 'nageru/piecewise_interpolator.cpp', 'nageru/midi_mapper.cpp', 'nageru/delay_analyzer.cpp', 'nageru/audio_clip.cpp']
# Auxiliary objects used for nearly everything.
aux_srcs = ['nageru/flags.cpp']
}
}
-std::vector<ALSAPool::Device> ALSAPool::get_devices()
+std::vector<ALSAPool::Device> ALSAPool::get_devices(bool hold_devices)
{
lock_guard<mutex> lock(mu);
- for (Device &device : devices) {
- device.held = true;
+ if (hold_devices) {
+ for (Device &device : devices) {
+ device.held = true;
+ }
}
return devices;
}
void init();
- // Get the list of all current devices. Note that this will implicitly mark
- // all of the returned devices as held, since the input mapping UI needs
- // some kind of stability when the user is to choose. Thus, when you are done
- // with the list and have set a new mapping, you must go through all the devices
- // you don't want and release them using release_device().
- std::vector<Device> get_devices();
+ // Get the list of all current devices. Note that if hold_devices==true,
+ // all of the returned devices will be marked as held (used by the input mapping UI,
+ // which needs some kind of stability when the user is to choose).
+ // Thus, when you are done with the list and have set a new mapping,
+ // you must go through all the devices you don't want and release them
+ // using release_device().
+ std::vector<Device> get_devices(bool hold_devices);
void hold_device(unsigned index);
void release_device(unsigned index); // Note: index is allowed to go out of bounds.
--- /dev/null
+#include "audio_clip.h"
+
+#include <math.h>
+
+using namespace std;
+using namespace std::chrono;
+
+void AudioClip::clear()
+{
+ lock_guard<mutex> lock(mu);
+ vals.clear();
+}
+
+void AudioClip::add_audio(const float *samples, size_t num_samples, double sample_rate, std::chrono::steady_clock::time_point frame_time)
+{
+ lock_guard<mutex> lock(mu);
+ if (!vals.empty() && sample_rate != this->sample_rate) {
+ vals.clear();
+ }
+ if (vals.empty()) {
+ first_sample = frame_time;
+ }
+ this->sample_rate = sample_rate;
+ vals.insert(vals.end(), samples, samples + num_samples);
+}
+
+double AudioClip::get_length_seconds() const
+{
+ lock_guard<mutex> lock(mu);
+ if (vals.empty()) {
+ return 0.0;
+ }
+
+ return double(vals.size()) / sample_rate;
+}
+
+
+unique_ptr<pair<float, float>[]> AudioClip::get_min_max_peaks(unsigned width) const
+{
+ unique_ptr<pair<float, float>[]> min_max(new pair<float, float>[width]);
+ for (unsigned x = 0; x < width; ++x) {
+ min_max[x].first = min_max[x].second = 0.0 / 0.0; // NaN.
+ }
+
+ lock_guard<mutex> lock(mu);
+ for (size_t i = 0; i < vals.size(); ++i) {
+ // We display one second.
+ int x = lrint(i * (double(width) / sample_rate));
+ if (x < 0 || x >= int(width)) continue;
+ if (isnan(min_max[x].first)) {
+ min_max[x].first = min_max[x].second = 0.0;
+ }
+ min_max[x].first = min(min_max[x].first, vals[i]);
+ min_max[x].second = max(min_max[x].second, vals[i]);
+ }
+
+ return min_max;
+}
--- /dev/null
+#ifndef AUDIO_CLIP_H
+#define AUDIO_CLIP_H
+
+// A short single-channel recording of an audio clip, for the delay analyzer.
+// Thread safe.
+
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+class AudioClip
+{
+public:
+ void clear();
+ void add_audio(const float *samples, size_t num_samples, double sample_rate, std::chrono::steady_clock::time_point frame_time);
+ double get_length_seconds() const;
+
+ std::unique_ptr<std::pair<float, float>[]> get_min_max_peaks(unsigned width) const;
+
+private:
+ mutable std::mutex mu;
+ std::vector<float> vals; // Under <mutex>.
+ double sample_rate; // Under <mutex>.
+ std::chrono::steady_clock::time_point first_sample; // Under <mutex>.
+};
+
+#endif
#include <utility>
#include "decibel.h"
+#include "delay_analyzer.h"
#include "flags.h"
#include "shared/metrics.h"
#include "state.pb.h"
// Must happen after ALSAPool is initialized, as it needs to know the card list.
current_mapping_mode = MappingMode::MULTICHANNEL;
InputMapping new_input_mapping;
- if (!load_input_mapping_from_file(get_devices(),
+ if (!load_input_mapping_from_file(get_devices(HOLD_ALSA_DEVICES),
global_flags.input_mapping_filename,
&new_input_mapping)) {
fprintf(stderr, "Failed to load input mapping from '%s', exiting.\n",
bool AudioMixer::add_audio(DeviceSpec device_spec, const uint8_t *data, unsigned num_samples, AudioFormat audio_format, steady_clock::time_point frame_time)
{
+ if (delay_analyzer != nullptr && delay_analyzer->is_grabbing()) {
+ delay_analyzer->add_audio(device_spec, data, num_samples, audio_format, frame_time);
+ }
+
AudioDevice *device = find_audio_device(device_spec);
unique_lock<timed_mutex> lock(audio_mutex, defer_lock);
correlation.get_correlation());
}
-map<DeviceSpec, DeviceInfo> AudioMixer::get_devices()
+map<DeviceSpec, DeviceInfo> AudioMixer::get_devices(HoldDevices hold_devices)
{
lock_guard<timed_mutex> lock(audio_mutex);
info.num_channels = 8;
devices.insert(make_pair(spec, info));
}
- vector<ALSAPool::Device> available_alsa_devices = alsa_pool.get_devices();
+ vector<ALSAPool::Device> available_alsa_devices = alsa_pool.get_devices(hold_devices);
for (unsigned card_index = 0; card_index < available_alsa_devices.size(); ++card_index) {
const DeviceSpec spec{ InputSourceType::ALSA_INPUT, card_index };
const ALSAPool::Device &device = available_alsa_devices[card_index];
new_input_mapping.buses.push_back(input);
+ // NOTE: Delay is implicitly at 0.0 ms, since none has been set in the mapping.
+
lock_guard<timed_mutex> lock(audio_mutex);
current_mapping_mode = MappingMode::SIMPLE;
set_input_mapping_lock_held(new_input_mapping);
// processing them with effects (if desired), and then mixing them
// all together into one final audio signal.
//
-// All operations on AudioMixer (except destruction) are thread-safe.
+// All operations on AudioMixer, except destruction and set_delay_analyzer(),
+// are thread-safe.
#include <assert.h>
#include <stdint.h>
#include "resampling_queue.h"
#include "stereocompressor.h"
+class DelayAnalyzerInterface;
class DeviceSpecProto;
namespace bmusb {
// Assumes little-endian and chunky, signed PCM throughout.
std::vector<int32_t> convert_audio_to_fixed32(const uint8_t *data, unsigned num_samples, bmusb::AudioFormat audio_format, unsigned num_destination_channels);
-// Similar, except converts ot floating-point instead, and converts only one channel.
+// Similar, except converts to floating-point instead, and converts only one channel.
void convert_audio_to_fp32(float *dst, size_t out_channel, size_t out_num_channels,
const uint8_t *src, size_t in_channel, bmusb::AudioFormat in_audio_format,
size_t num_samples);
bool get_mute(unsigned bus_index) const { return mute[bus_index]; }
void set_mute(unsigned bus_index, bool muted) { mute[bus_index] = muted; }
- // Note: This operation holds all ALSA devices (see ALSAPool::get_devices()).
- // You will need to call set_input_mapping() to get the hold state correctly,
- // or every card will be held forever.
- std::map<DeviceSpec, DeviceInfo> get_devices();
+ enum HoldDevices {
+ HOLD_NO_DEVICES,
+
+ // Note: Holds all ALSA devices (see ALSAPool::get_devices()).
+ // You will need to call set_input_mapping() to get the hold state correctly,
+ // or every card will be held forever.
+ HOLD_ALSA_DEVICES
+ };
+ std::map<DeviceSpec, DeviceInfo> get_devices(HoldDevices hold_devices);
// See comments on ALSAPool::get_card_state().
ALSAPool::Device::State get_alsa_card_state(unsigned index)
BusSettings get_bus_settings(unsigned bus_index) const;
void set_bus_settings(unsigned bus_index, const BusSettings &settings);
+ // Does not take ownership. Not thread-safe (so only call when the mixer is being created).
+ void set_delay_analyzer(DelayAnalyzerInterface *delay_analyzer)
+ {
+ this->delay_analyzer = delay_analyzer;
+ }
+
private:
struct AudioDevice {
std::unique_ptr<ResamplingQueue> resampling_queue;
std::atomic<double> compressor_attenuation_db{0.0/0.0};
};
std::unique_ptr<BusMetrics[]> bus_metrics; // One for each bus in <input_mapping>.
+
+ DelayAnalyzerInterface *delay_analyzer = nullptr;
};
extern AudioMixer *global_audio_mixer;
--- /dev/null
+#include "delay_analyzer.h"
+
+#include "audio_mixer.h"
+#include "ui_delay_analyzer.h"
+
+#include <bmusb/bmusb.h>
+
+#include <memory>
+
+using namespace bmusb;
+using namespace std;
+using namespace std::chrono;
+using namespace std::placeholders;
+
+DelayAnalyzer::DelayAnalyzer()
+ : ui(new Ui::DelayAnalyzer),
+ devices(global_audio_mixer->get_devices(AudioMixer::HOLD_NO_DEVICES))
+{
+ ui->setupUi(this);
+ connect(ui->grab_btn, &QPushButton::clicked, this, &DelayAnalyzer::grab_clicked);
+
+ connect(ui->card_combo_1, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+ bind(&DelayAnalyzer::card_selected, this, ui->card_combo_1, _1));
+ connect(ui->card_combo_2, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+ bind(&DelayAnalyzer::card_selected, this, ui->card_combo_2, _1));
+ connect(ui->channel_combo_1, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+ bind(&DelayAnalyzer::channel_selected, this, ui->channel_combo_1));
+ connect(ui->channel_combo_2, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+ bind(&DelayAnalyzer::channel_selected, this, ui->channel_combo_2));
+
+ for (const auto &spec_and_info : devices) {
+ QString label(QString::fromStdString(spec_and_info.second.display_name));
+ ui->card_combo_1->addItem(label + " ", qulonglong(DeviceSpec_to_key(spec_and_info.first)));
+ ui->card_combo_2->addItem(label + " ", qulonglong(DeviceSpec_to_key(spec_and_info.first)));
+ }
+
+ ui->peak_display_1->set_audio_clip(&clip1);
+ ui->peak_display_2->set_audio_clip(&clip2);
+}
+
+DelayAnalyzer::~DelayAnalyzer()
+{
+}
+
+void DelayAnalyzer::grab_clicked()
+{
+ grabbing = true;
+ clip1.clear();
+ clip2.clear();
+ ui->peak_display_1->audio_clip_updated();
+ ui->peak_display_2->audio_clip_updated();
+}
+
+void DelayAnalyzer::card_selected(QComboBox *card_combo, int selected_index)
+{
+ assert(card_combo == ui->card_combo_1 || card_combo == ui->card_combo_2);
+ QComboBox *channel_combo = (card_combo == ui->card_combo_1 ? ui->channel_combo_1 : ui->channel_combo_2);
+ int current_channel = channel_combo->currentIndex();
+
+ channel_combo->clear();
+
+ uint64_t key = card_combo->itemData(selected_index).toULongLong();
+ auto device_it = devices.find(key_to_DeviceSpec(key));
+ assert(device_it != devices.end());
+ unsigned num_device_channels = device_it->second.num_channels;
+ for (unsigned source = 0; source < num_device_channels; ++source) {
+ char buf[256];
+ snprintf(buf, sizeof(buf), "Channel %u ", source + 1);
+ channel_combo->addItem(QString(buf));
+ }
+
+ if (current_channel != -1 && current_channel < channel_combo->count()) {
+ channel_combo->setCurrentIndex(current_channel);
+ } else {
+ channel_combo->setCurrentIndex(0);
+ }
+
+ if (card_combo == ui->card_combo_1) {
+ clip1.clear();
+ ui->peak_display_1->audio_clip_updated();
+ } else {
+ clip2.clear();
+ ui->peak_display_2->audio_clip_updated();
+ }
+}
+
+void DelayAnalyzer::channel_selected(QComboBox *channel_combo)
+{
+ if (channel_combo == ui->channel_combo_1) {
+ clip1.clear();
+ ui->peak_display_1->audio_clip_updated();
+ } else {
+ clip2.clear();
+ ui->peak_display_2->audio_clip_updated();
+ }
+}
+
+DeviceSpec DelayAnalyzer::get_selected_device(QComboBox *card_combo)
+{
+ return key_to_DeviceSpec(card_combo->currentData().toULongLong());
+}
+
+void DelayAnalyzer::add_audio(DeviceSpec device_spec, const uint8_t *data, unsigned num_samples, AudioFormat audio_format, steady_clock::time_point frame_time)
+{
+ unique_ptr<float[]> tmp;
+
+ if (device_spec == get_selected_device(ui->card_combo_1)) {
+ tmp.reset(new float[num_samples]);
+
+ convert_audio_to_fp32(tmp.get(), /*out_channel=*/0, /*out_num_channels=*/1, data, ui->channel_combo_1->currentIndex(), audio_format, num_samples);
+ clip1.add_audio(tmp.get(), num_samples, audio_format.sample_rate, frame_time);
+ ui->peak_display_1->audio_clip_updated();
+ }
+ if (device_spec == get_selected_device(ui->card_combo_2)) {
+ if (tmp == nullptr) {
+ tmp.reset(new float[num_samples]);
+ }
+
+ convert_audio_to_fp32(tmp.get(), /*out_channel=*/0, /*out_num_channels=*/1, data, ui->channel_combo_2->currentIndex(), audio_format, num_samples);
+ clip2.add_audio(tmp.get(), num_samples, audio_format.sample_rate, frame_time);
+ ui->peak_display_2->audio_clip_updated();
+ }
+
+ if (clip1.get_length_seconds() >= 1.0 && clip2.get_length_seconds() >= 1.0) {
+ grabbing = false;
+ }
+}
--- /dev/null
+#ifndef _DELAY_ANALYZER_H
+#define _DELAY_ANALYZER_H 1
+
+#include <QMainWindow>
+
+#include <atomic>
+#include <string>
+
+#include "audio_clip.h"
+#include "delay_analyzer_interface.h"
+#include "input_mapping.h"
+
+namespace bmusb {
+struct AudioFormat;
+} // namespace bmusb
+
+namespace Ui {
+class DelayAnalyzer;
+} // namespace Ui
+
+class QComboBox;
+
+class DelayAnalyzer : public QMainWindow, public DelayAnalyzerInterface
+{
+ Q_OBJECT
+
+public:
+ DelayAnalyzer();
+ ~DelayAnalyzer();
+
+ bool is_grabbing() const override { return grabbing; }
+ void add_audio(DeviceSpec device_spec, const uint8_t *data, unsigned num_samples, bmusb::AudioFormat audio_format, std::chrono::steady_clock::time_point frame_time) override;
+
+private:
+ Ui::DelayAnalyzer *ui;
+ AudioClip clip1, clip2;
+
+ void grab_clicked();
+ void card_selected(QComboBox *card_combo, int selected_index);
+ void channel_selected(QComboBox *channel_combo);
+ DeviceSpec get_selected_device(QComboBox *card_combo);
+
+ std::atomic<bool> grabbing{false};
+ std::map<DeviceSpec, DeviceInfo> devices;
+};
+
+#endif // !defined(_DELAY_ANALYZER_H)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DelayAnalyzer</class>
+ <widget class="QMainWindow" name="DelayAnalyzer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1092</width>
+ <height>412</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Delay analyzer</string>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <layout class="QGridLayout" name="gridLayout_2" columnstretch="0,0,0,1">
+ <item row="6" column="0">
+ <widget class="QPushButton" name="grab_btn">
+ <property name="text">
+ <string>Grab</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="4" column="1">
+ <widget class="QComboBox" name="card_combo_2"/>
+ </item>
+ <item row="5" column="0">
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="4" column="2">
+ <widget class="QComboBox" name="channel_combo_2"/>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="card_combo_1"/>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Reference</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QComboBox" name="channel_combo_1"/>
+ </item>
+ <item row="2" column="3">
+ <widget class="QFrame" name="frame">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>50</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="PeakDisplay" name="peak_display_1" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>50</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">background: rgb(252, 175, 62);</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item row="4" column="3">
+ <widget class="QFrame" name="frame_2">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="PeakDisplay" name="peak_display_2" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>50</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">background: rgb(252, 175, 62);</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>PeakDisplay</class>
+ <extends>QWidget</extends>
+ <header>peak_display.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
--- /dev/null
+#ifndef _DELAY_ANALYZER_INTERFACE
+#define _DELAY_ANALYZER_INTERFACE 1
+
+// Abstract interface, in order to keep the Qt dependencies out of benchmark_audio_mixer.
+
+#include <stdint.h>
+#include <chrono>
+
+#include "input_mapping.h"
+
+namespace bmusb {
+struct AudioFormat;
+} // namespace bmusb
+
+class DelayAnalyzerInterface
+{
+public:
+ virtual ~DelayAnalyzerInterface() {}
+
+ virtual bool is_grabbing() const = 0;
+ virtual void add_audio(DeviceSpec device_spec, const uint8_t *data, unsigned num_samples, bmusb::AudioFormat audio_format, std::chrono::steady_clock::time_point frame_time) = 0;
+};
+
+#endif // !defined(_DELAY_ANALYZER_INTERFACE)
: ui(new Ui::InputMappingDialog),
mapping(global_audio_mixer->get_input_mapping()),
old_mapping(mapping),
- devices(global_audio_mixer->get_devices())
+ devices(global_audio_mixer->get_devices(AudioMixer::HOLD_ALSA_DEVICES))
{
for (unsigned bus_index = 0; bus_index < mapping.buses.size(); ++bus_index) {
bus_settings.push_back(global_audio_mixer->get_bus_settings(bus_index));
saved_callback = global_audio_mixer->get_state_changed_callback();
global_audio_mixer->set_state_changed_callback([this]{
post_to_main_thread([this]{
- devices = global_audio_mixer->get_devices();
+ devices = global_audio_mixer->get_devices(AudioMixer::HOLD_ALSA_DEVICES);
for (unsigned row = 0; row < mapping.buses.size(); ++row) {
fill_row_from_bus(row, mapping.buses[row], mapping);
}
for (unsigned bus_index = 0; bus_index < mapping.buses.size(); ++bus_index) {
bus_settings.push_back(global_audio_mixer->get_bus_settings(bus_index));
}
- devices = global_audio_mixer->get_devices(); // New dead cards may have been made.
+ devices = global_audio_mixer->get_devices(AudioMixer::HOLD_ALSA_DEVICES); // New dead cards may have been made.
fill_ui_from_mapping(mapping);
}
connect(ui->manual_action, &QAction::triggered, this, &MainWindow::manual_triggered);
connect(ui->about_action, &QAction::triggered, this, &MainWindow::about_triggered);
connect(ui->open_analyzer_action, &QAction::triggered, this, &MainWindow::open_analyzer_triggered);
+ connect(ui->open_delay_analyzer_action, &QAction::triggered, this, &MainWindow::open_delay_analyzer_triggered);
connect(ui->simple_audio_mode, &QAction::triggered, this, &MainWindow::simple_audio_mode_triggered);
connect(ui->multichannel_audio_mode, &QAction::triggered, this, &MainWindow::multichannel_audio_mode_triggered);
connect(ui->input_mapping_action, &QAction::triggered, this, &MainWindow::input_mapping_triggered);
midi_mapper.start_thread();
analyzer.reset(new Analyzer);
+ delay_analyzer.reset(new DelayAnalyzer);
+
+ mixer->get_audio_mixer()->set_delay_analyzer(delay_analyzer.get());
global_mixer->set_theme_menu_callback(bind(&MainWindow::setup_theme_menu, this));
setup_theme_menu();
ui->multichannel_audio_mode->setChecked(!simple);
ui->input_mapping_action->setEnabled(!simple);
ui->midi_mapping_action->setEnabled(!simple);
+ ui->open_delay_analyzer_action->setEnabled(!simple);
ui->locut_enabled->setVisible(simple);
ui->gainstaging_label->setVisible(simple);
analyzer->show();
}
+void MainWindow::open_delay_analyzer_triggered()
+{
+ delay_analyzer->show();
+}
+
void MainWindow::simple_audio_mode_triggered()
{
if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::SIMPLE) {
#include "analyzer.h"
#include "audio_mixer.h"
+#include "delay_analyzer.h"
#include "midi_mapper.h"
#include "mixer.h"
void manual_triggered();
void about_triggered();
void open_analyzer_triggered();
+ void open_delay_analyzer_triggered();
void simple_audio_mode_triggered();
void multichannel_audio_mode_triggered();
void input_mapping_triggered();
int current_audio_view = -1;
MIDIMapper midi_mapper;
std::unique_ptr<Analyzer> analyzer;
+ std::unique_ptr<DelayAnalyzer> delay_analyzer;
};
extern MainWindow *global_mainwindow;
<addaction name="separator"/>
<addaction name="input_mapping_action"/>
<addaction name="midi_mapping_action"/>
+ <addaction name="open_delay_analyzer_action"/>
</widget>
<addaction name="video_menu"/>
<addaction name="menu_Audio"/>
<string>Enable &quick-cut keys (Q, W, E, etc.)</string>
</property>
</action>
+ <action name="open_delay_analyzer_action">
+ <property name="text">
+ <string>Open &delay analyzer…</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
--- /dev/null
+#include "peak_display.h"
+
+#include <math.h>
+
+#include <memory>
+
+#include <QPainter>
+#include <QPaintEvent>
+#include <QRect>
+
+#include "audio_clip.h"
+
+using namespace std;
+
+PeakDisplay::PeakDisplay(QWidget *parent)
+ : QWidget(parent)
+{
+}
+
+void PeakDisplay::audio_clip_updated()
+{
+ QMetaObject::invokeMethod(this, "repaint", Qt::QueuedConnection);
+}
+
+void PeakDisplay::paintEvent(QPaintEvent *event)
+{
+ int w = width();
+ unique_ptr<pair<float, float>[]> min_max = audio_clip->get_min_max_peaks(w);
+
+ QPainter painter(this);
+ painter.fillRect(event->rect(), Qt::white);
+ painter.setClipRect(event->rect());
+ double mid_y = double(height()) * 0.5;
+ double scale_y = height() * 0.5;
+ for (int x = 0; x < w; ++x) {
+ if (isnan(min_max[x].first)) continue;
+
+ int y_min = lrint(min_max[x].first * scale_y + mid_y);
+ int y_max = lrint(min_max[x].second * scale_y + mid_y);
+ painter.drawLine(x, y_min, x, y_max);
+ }
+}
--- /dev/null
+#ifndef PEAK_DISPLAY_H
+#define PEAK_DISPLAY_H
+
+#include <QWidget>
+#include <mutex>
+
+class AudioClip;
+
+class PeakDisplay : public QWidget
+{
+ Q_OBJECT
+
+public:
+ PeakDisplay(QWidget *parent);
+
+ // Does not take ownership.
+ void set_audio_clip(AudioClip *audio_clip) {
+ this->audio_clip = audio_clip;
+ }
+ void audio_clip_updated();
+
+private:
+ void paintEvent(QPaintEvent *event) override;
+
+ AudioClip *audio_clip;
+};
+
+#endif