]> git.sesse.net Git - nageru/commitdiff
Begin working on a delay analyzer.
authorSteinar H. Gunderson <steinar+nageru@gunderson.no>
Sun, 11 Aug 2019 08:50:52 +0000 (10:50 +0200)
committerSteinar H. Gunderson <steinar+nageru@gunderson.no>
Sun, 25 Aug 2019 22:39:08 +0000 (00:39 +0200)
17 files changed:
meson.build
nageru/alsa_pool.cpp
nageru/alsa_pool.h
nageru/audio_clip.cpp [new file with mode: 0644]
nageru/audio_clip.h [new file with mode: 0644]
nageru/audio_mixer.cpp
nageru/audio_mixer.h
nageru/delay_analyzer.cpp [new file with mode: 0644]
nageru/delay_analyzer.h [new file with mode: 0644]
nageru/delay_analyzer.ui [new file with mode: 0644]
nageru/delay_analyzer_interface.h [new file with mode: 0644]
nageru/input_mapping_dialog.cpp
nageru/mainwindow.cpp
nageru/mainwindow.h
nageru/mainwindow.ui
nageru/peak_display.cpp [new file with mode: 0644]
nageru/peak_display.h [new file with mode: 0644]

index fde9405193bcfae9e1d6c4b467d21beda42f4adf..34cf9e84c963e1f4b31c12053cfdeecb0424045b 100644 (file)
@@ -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']
index bdff5af09917b0c13a79bc5453b3e255ffd02ff4..c74ed26613a5a31449d7e5c9938a38dea45ec86d 100644 (file)
@@ -53,11 +53,13 @@ ALSAPool::~ALSAPool()
        }
 }
 
-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;
 }
index 904e2ecf971f9850744b5e5a723e00aae073c399..9c1715c56bb0bd2eb049de94c92cad588e93c8f0 100644 (file)
@@ -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<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.
diff --git a/nageru/audio_clip.cpp b/nageru/audio_clip.cpp
new file mode 100644 (file)
index 0000000..4dcb8db
--- /dev/null
@@ -0,0 +1,58 @@
+#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;
+}
diff --git a/nageru/audio_clip.h b/nageru/audio_clip.h
new file mode 100644 (file)
index 0000000..71248af
--- /dev/null
@@ -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 <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
index 35e0a23ee2da2b5b4e361838eae0d5aeeb765246..4d7632fad80a8e643840991284728ea61bb353cc 100644 (file)
@@ -19,6 +19,7 @@
 #include <utility>
 
 #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<timed_mutex> lock(audio_mutex, defer_lock);
@@ -1030,7 +1035,7 @@ void AudioMixer::send_audio_level_callback()
                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);
 
@@ -1043,7 +1048,7 @@ map<DeviceSpec, DeviceInfo> AudioMixer::get_devices()
                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];
@@ -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<timed_mutex> lock(audio_mutex);
        current_mapping_mode = MappingMode::SIMPLE;
        set_input_mapping_lock_held(new_input_mapping);
index 9dc5c89d9702666bd8f936e5096604196aead143..0beaf103ab08dcfcd03ecef99eaed32ff83292df 100644 (file)
@@ -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 <assert.h>
 #include <stdint.h>
@@ -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<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);
@@ -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<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)
@@ -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<ResamplingQueue> resampling_queue;
@@ -435,6 +448,8 @@ private:
                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;
diff --git a/nageru/delay_analyzer.cpp b/nageru/delay_analyzer.cpp
new file mode 100644 (file)
index 0000000..7e25738
--- /dev/null
@@ -0,0 +1,127 @@
+#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;
+       }
+}
diff --git a/nageru/delay_analyzer.h b/nageru/delay_analyzer.h
new file mode 100644 (file)
index 0000000..cc75325
--- /dev/null
@@ -0,0 +1,47 @@
+#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)
diff --git a/nageru/delay_analyzer.ui b/nageru/delay_analyzer.ui
new file mode 100644 (file)
index 0000000..ce3566a
--- /dev/null
@@ -0,0 +1,169 @@
+<?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>
diff --git a/nageru/delay_analyzer_interface.h b/nageru/delay_analyzer_interface.h
new file mode 100644 (file)
index 0000000..d4381ad
--- /dev/null
@@ -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 <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)
index 529b2e5aa93b41bc59d08bca9302289801ff089e..85972f4312c4feb315ebd209d8c8d9e5839bae22 100644 (file)
@@ -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);
 }
 
index 66a11441d5883a2e6aa758f5b280c68b277eee92..980059af5a1f9752bd0853ecbf4a8c2a6a2d4879 100644 (file)
@@ -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) {
index b6a053bd0f68ded9cc7d9d851d597bb8ebad4ff0..9a1115bae4f1b9b647240c3fe456874c161d5b1b 100644 (file)
@@ -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> analyzer;
+       std::unique_ptr<DelayAnalyzer> delay_analyzer;
 };
 
 extern MainWindow *global_mainwindow;
index d727155c1d3a557d229a4602070c49da67a7e559..71dfdf8cf281c00cfe76d9e7c8bcdf76d99f71fd 100644 (file)
     <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 &amp;quick-cut keys (Q, W, E, etc.)</string>
    </property>
   </action>
+  <action name="open_delay_analyzer_action">
+   <property name="text">
+    <string>Open &amp;delay analyzer…</string>
+   </property>
+  </action>
  </widget>
  <layoutdefault spacing="6" margin="11"/>
  <customwidgets>
diff --git a/nageru/peak_display.cpp b/nageru/peak_display.cpp
new file mode 100644 (file)
index 0000000..eda6b54
--- /dev/null
@@ -0,0 +1,42 @@
+#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);
+       }
+}
diff --git a/nageru/peak_display.h b/nageru/peak_display.h
new file mode 100644 (file)
index 0000000..a7a3b93
--- /dev/null
@@ -0,0 +1,28 @@
+#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