From: Steinar H. Gunderson Date: Sun, 11 Aug 2019 08:50:52 +0000 (+0200) Subject: Begin working on a delay analyzer. X-Git-Url: https://git.sesse.net/?p=nageru;a=commitdiff_plain;h=bd01e11a41a19fef2315319909d8655cac359f0f Begin working on a delay analyzer. --- diff --git a/meson.build b/meson.build index fde9405..34cf9e8 100644 --- a/meson.build +++ b/meson.build @@ -172,15 +172,15 @@ nageru_link_with += protobuf_lib 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'] diff --git a/nageru/alsa_pool.cpp b/nageru/alsa_pool.cpp index bdff5af..c74ed26 100644 --- a/nageru/alsa_pool.cpp +++ b/nageru/alsa_pool.cpp @@ -53,11 +53,13 @@ ALSAPool::~ALSAPool() } } -std::vector ALSAPool::get_devices() +std::vector ALSAPool::get_devices(bool hold_devices) { lock_guard lock(mu); - for (Device &device : devices) { - device.held = true; + if (hold_devices) { + for (Device &device : devices) { + device.held = true; + } } return devices; } diff --git a/nageru/alsa_pool.h b/nageru/alsa_pool.h index 904e2ec..9c1715c 100644 --- a/nageru/alsa_pool.h +++ b/nageru/alsa_pool.h @@ -70,12 +70,13 @@ public: 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 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 get_devices(bool hold_devices); void hold_device(unsigned index); void release_device(unsigned index); // Note: index is allowed to go out of bounds. diff --git a/nageru/audio_clip.cpp b/nageru/audio_clip.cpp new file mode 100644 index 0000000..4dcb8db --- /dev/null +++ b/nageru/audio_clip.cpp @@ -0,0 +1,58 @@ +#include "audio_clip.h" + +#include + +using namespace std; +using namespace std::chrono; + +void AudioClip::clear() +{ + lock_guard 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 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 lock(mu); + if (vals.empty()) { + return 0.0; + } + + return double(vals.size()) / sample_rate; +} + + +unique_ptr[]> AudioClip::get_min_max_peaks(unsigned width) const +{ + unique_ptr[]> min_max(new pair[width]); + for (unsigned x = 0; x < width; ++x) { + min_max[x].first = min_max[x].second = 0.0 / 0.0; // NaN. + } + + lock_guard 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; +} diff --git a/nageru/audio_clip.h b/nageru/audio_clip.h new file mode 100644 index 0000000..71248af --- /dev/null +++ b/nageru/audio_clip.h @@ -0,0 +1,29 @@ +#ifndef AUDIO_CLIP_H +#define AUDIO_CLIP_H + +// A short single-channel recording of an audio clip, for the delay analyzer. +// Thread safe. + +#include +#include +#include +#include +#include + +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[]> get_min_max_peaks(unsigned width) const; + +private: + mutable std::mutex mu; + std::vector vals; // Under . + double sample_rate; // Under . + std::chrono::steady_clock::time_point first_sample; // Under . +}; + +#endif diff --git a/nageru/audio_mixer.cpp b/nageru/audio_mixer.cpp index 35e0a23..4d7632f 100644 --- a/nageru/audio_mixer.cpp +++ b/nageru/audio_mixer.cpp @@ -19,6 +19,7 @@ #include #include "decibel.h" +#include "delay_analyzer.h" #include "flags.h" #include "shared/metrics.h" #include "state.pb.h" @@ -263,7 +264,7 @@ AudioMixer::AudioMixer(unsigned num_capture_cards, unsigned num_ffmpeg_inputs) // 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", @@ -312,6 +313,10 @@ void AudioMixer::reset_resampler_mutex_held(DeviceSpec device_spec) 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 lock(audio_mutex, defer_lock); @@ -1030,7 +1035,7 @@ void AudioMixer::send_audio_level_callback() correlation.get_correlation()); } -map AudioMixer::get_devices() +map AudioMixer::get_devices(HoldDevices hold_devices) { lock_guard lock(audio_mutex); @@ -1043,7 +1048,7 @@ map AudioMixer::get_devices() info.num_channels = 8; devices.insert(make_pair(spec, info)); } - vector available_alsa_devices = alsa_pool.get_devices(); + vector 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]; @@ -1113,6 +1118,8 @@ void AudioMixer::set_simple_input(unsigned 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 lock(audio_mutex); current_mapping_mode = MappingMode::SIMPLE; set_input_mapping_lock_held(new_input_mapping); diff --git a/nageru/audio_mixer.h b/nageru/audio_mixer.h index 9dc5c89..0beaf10 100644 --- a/nageru/audio_mixer.h +++ b/nageru/audio_mixer.h @@ -6,7 +6,8 @@ // 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 #include @@ -31,6 +32,7 @@ #include "resampling_queue.h" #include "stereocompressor.h" +class DelayAnalyzerInterface; class DeviceSpecProto; namespace bmusb { @@ -41,7 +43,7 @@ struct AudioFormat; // Assumes little-endian and chunky, signed PCM throughout. std::vector 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); @@ -82,10 +84,15 @@ public: 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 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 get_devices(HoldDevices hold_devices); // See comments on ALSAPool::get_card_state(). ALSAPool::Device::State get_alsa_card_state(unsigned index) @@ -321,6 +328,12 @@ public: 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 resampling_queue; @@ -435,6 +448,8 @@ private: std::atomic compressor_attenuation_db{0.0/0.0}; }; std::unique_ptr bus_metrics; // One for each bus in . + + DelayAnalyzerInterface *delay_analyzer = nullptr; }; extern AudioMixer *global_audio_mixer; diff --git a/nageru/delay_analyzer.cpp b/nageru/delay_analyzer.cpp new file mode 100644 index 0000000..7e25738 --- /dev/null +++ b/nageru/delay_analyzer.cpp @@ -0,0 +1,127 @@ +#include "delay_analyzer.h" + +#include "audio_mixer.h" +#include "ui_delay_analyzer.h" + +#include + +#include + +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(&QComboBox::currentIndexChanged), + bind(&DelayAnalyzer::card_selected, this, ui->card_combo_1, _1)); + connect(ui->card_combo_2, static_cast(&QComboBox::currentIndexChanged), + bind(&DelayAnalyzer::card_selected, this, ui->card_combo_2, _1)); + connect(ui->channel_combo_1, static_cast(&QComboBox::currentIndexChanged), + bind(&DelayAnalyzer::channel_selected, this, ui->channel_combo_1)); + connect(ui->channel_combo_2, static_cast(&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 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; + } +} diff --git a/nageru/delay_analyzer.h b/nageru/delay_analyzer.h new file mode 100644 index 0000000..cc75325 --- /dev/null +++ b/nageru/delay_analyzer.h @@ -0,0 +1,47 @@ +#ifndef _DELAY_ANALYZER_H +#define _DELAY_ANALYZER_H 1 + +#include + +#include +#include + +#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 grabbing{false}; + std::map devices; +}; + +#endif // !defined(_DELAY_ANALYZER_H) diff --git a/nageru/delay_analyzer.ui b/nageru/delay_analyzer.ui new file mode 100644 index 0000000..ce3566a --- /dev/null +++ b/nageru/delay_analyzer.ui @@ -0,0 +1,169 @@ + + + DelayAnalyzer + + + + 0 + 0 + 1092 + 412 + + + + Delay analyzer + + + + + + + Grab + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Reference + + + + + + + + + + + 0 + 50 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 50 + + + + background: rgb(252, 175, 62); + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 50 + + + + background: rgb(252, 175, 62); + + + + + + + + + + + + PeakDisplay + QWidget +
peak_display.h
+ 1 +
+
+ + +
diff --git a/nageru/delay_analyzer_interface.h b/nageru/delay_analyzer_interface.h new file mode 100644 index 0000000..d4381ad --- /dev/null +++ b/nageru/delay_analyzer_interface.h @@ -0,0 +1,24 @@ +#ifndef _DELAY_ANALYZER_INTERFACE +#define _DELAY_ANALYZER_INTERFACE 1 + +// Abstract interface, in order to keep the Qt dependencies out of benchmark_audio_mixer. + +#include +#include + +#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) diff --git a/nageru/input_mapping_dialog.cpp b/nageru/input_mapping_dialog.cpp index 529b2e5..85972f4 100644 --- a/nageru/input_mapping_dialog.cpp +++ b/nageru/input_mapping_dialog.cpp @@ -33,7 +33,7 @@ InputMappingDialog::InputMappingDialog() : 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)); @@ -61,7 +61,7 @@ InputMappingDialog::InputMappingDialog() 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); } @@ -352,7 +352,7 @@ void InputMappingDialog::load_clicked() 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); } diff --git a/nageru/mainwindow.cpp b/nageru/mainwindow.cpp index 66a1144..980059a 100644 --- a/nageru/mainwindow.cpp +++ b/nageru/mainwindow.cpp @@ -216,6 +216,7 @@ MainWindow::MainWindow() 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); @@ -444,6 +445,9 @@ void MainWindow::mixer_created(Mixer *mixer) 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(); @@ -470,6 +474,7 @@ void MainWindow::reset_audio_mapping_ui() 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); @@ -678,6 +683,11 @@ void MainWindow::open_analyzer_triggered() 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) { diff --git a/nageru/mainwindow.h b/nageru/mainwindow.h index b6a053b..9a1115b 100644 --- a/nageru/mainwindow.h +++ b/nageru/mainwindow.h @@ -11,6 +11,7 @@ #include "analyzer.h" #include "audio_mixer.h" +#include "delay_analyzer.h" #include "midi_mapper.h" #include "mixer.h" @@ -49,6 +50,7 @@ public slots: 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(); @@ -182,6 +184,7 @@ private: int current_audio_view = -1; MIDIMapper midi_mapper; std::unique_ptr analyzer; + std::unique_ptr delay_analyzer; }; extern MainWindow *global_mainwindow; diff --git a/nageru/mainwindow.ui b/nageru/mainwindow.ui index d727155..71dfdf8 100644 --- a/nageru/mainwindow.ui +++ b/nageru/mainwindow.ui @@ -1518,6 +1518,7 @@ + @@ -1606,6 +1607,11 @@ Enable &quick-cut keys (Q, W, E, etc.) + + + Open &delay analyzer… + + diff --git a/nageru/peak_display.cpp b/nageru/peak_display.cpp new file mode 100644 index 0000000..eda6b54 --- /dev/null +++ b/nageru/peak_display.cpp @@ -0,0 +1,42 @@ +#include "peak_display.h" + +#include + +#include + +#include +#include +#include + +#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[]> 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); + } +} diff --git a/nageru/peak_display.h b/nageru/peak_display.h new file mode 100644 index 0000000..a7a3b93 --- /dev/null +++ b/nageru/peak_display.h @@ -0,0 +1,28 @@ +#ifndef PEAK_DISPLAY_H +#define PEAK_DISPLAY_H + +#include +#include + +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