]> git.sesse.net Git - nageru/blob - nageru/delay_analyzer.cpp
When the delay analyzer wants audio from an ALSA card, temporarily auto-enable captur...
[nageru] / nageru / delay_analyzer.cpp
1 #include "delay_analyzer.h"
2
3 #include "audio_mixer.h"
4 #include "ui_delay_analyzer.h"
5 #include "shared/post_to_main_thread.h"
6
7 #include <bmusb/bmusb.h>
8
9 #include <memory>
10
11 #include <QTimer>
12
13 using namespace bmusb;
14 using namespace std;
15 using namespace std::chrono;
16 using namespace std::placeholders;
17
18 DelayAnalyzer::DelayAnalyzer()
19         : ui(new Ui::DelayAnalyzer),
20           devices(global_audio_mixer->get_devices(AudioMixer::HOLD_NO_DEVICES)),
21           grab_timeout(new QTimer(this))
22 {
23         ui->setupUi(this);
24         connect(ui->grab_btn, &QPushButton::clicked, this, &DelayAnalyzer::grab_clicked);
25
26         connect(ui->card_combo_1, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
27                 bind(&DelayAnalyzer::card_selected, this, ui->card_combo_1, _1));
28         connect(ui->card_combo_2, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
29                 bind(&DelayAnalyzer::card_selected, this, ui->card_combo_2, _1));
30         connect(ui->channel_combo_1, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
31                 bind(&DelayAnalyzer::channel_selected, this, ui->channel_combo_1));
32         connect(ui->channel_combo_2, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
33                 bind(&DelayAnalyzer::channel_selected, this, ui->channel_combo_2));
34         connect(grab_timeout, &QTimer::timeout, bind(&DelayAnalyzer::grab_timed_out, this));
35
36         for (const auto &spec_and_info : devices) {
37                 QString label(QString::fromStdString(spec_and_info.second.display_name));
38                 ui->card_combo_1->addItem(label + "   ", qulonglong(DeviceSpec_to_key(spec_and_info.first)));
39                 ui->card_combo_2->addItem(label + "   ", qulonglong(DeviceSpec_to_key(spec_and_info.first)));
40         }
41
42         ui->peak_display_1->set_audio_clip(&clip1);
43         ui->peak_display_2->set_audio_clip(&clip2);
44 }
45
46 DelayAnalyzer::~DelayAnalyzer()
47 {
48 }
49
50 void DelayAnalyzer::grab_clicked()
51 {
52         grabbing = true;
53         grab_timeout->setSingleShot(true);
54         grab_timeout->start(milliseconds(2000));
55         clip1.clear();
56         clip2.clear();
57         ui->delay_estimate_label->clear();
58         ui->peak_display_1->reset_base();
59         ui->peak_display_2->reset_base();
60         ui->peak_display_1->audio_clip_updated();
61         ui->peak_display_2->audio_clip_updated();
62
63         set<DeviceSpec> devices;
64         devices.insert(get_selected_device(ui->card_combo_1));
65         devices.insert(get_selected_device(ui->card_combo_2));
66         global_audio_mixer->set_extra_devices(devices);
67 }
68
69 void DelayAnalyzer::card_selected(QComboBox *card_combo, int selected_index)
70 {
71         assert(card_combo == ui->card_combo_1 || card_combo == ui->card_combo_2);
72         QComboBox *channel_combo = (card_combo == ui->card_combo_1 ? ui->channel_combo_1 : ui->channel_combo_2);
73         int current_channel = channel_combo->currentIndex();
74
75         channel_combo->clear();
76
77         uint64_t key = card_combo->itemData(selected_index).toULongLong();
78         auto device_it = devices.find(key_to_DeviceSpec(key));
79         assert(device_it != devices.end());
80         unsigned num_device_channels = device_it->second.num_channels;
81         for (unsigned source = 0; source < num_device_channels; ++source) {
82                 char buf[256];
83                 snprintf(buf, sizeof(buf), "Channel %u   ", source + 1);
84                 channel_combo->addItem(QString(buf));
85         }
86
87         if (current_channel != -1 && current_channel < channel_combo->count()) {
88                 channel_combo->setCurrentIndex(current_channel);
89         } else {
90                 channel_combo->setCurrentIndex(0);
91         }
92
93         if (card_combo == ui->card_combo_1) {
94                 clip1.clear();
95                 ui->peak_display_1->audio_clip_updated();
96         } else {
97                 clip2.clear();
98                 ui->peak_display_2->audio_clip_updated();
99         }
100         ui->delay_estimate_label->clear();
101 }
102
103 void DelayAnalyzer::channel_selected(QComboBox *channel_combo)
104 {
105         if (channel_combo == ui->channel_combo_1) {
106                 clip1.clear();
107                 ui->peak_display_1->audio_clip_updated();
108         } else {
109                 clip2.clear();
110                 ui->peak_display_2->audio_clip_updated();
111         }
112         ui->delay_estimate_label->clear();
113 }
114
115 DeviceSpec DelayAnalyzer::get_selected_device(QComboBox *card_combo)
116 {
117         return key_to_DeviceSpec(card_combo->currentData().toULongLong());
118 }
119
120 void DelayAnalyzer::add_audio(DeviceSpec device_spec, const uint8_t *data, unsigned num_samples, AudioFormat audio_format, steady_clock::time_point frame_time)
121 {
122         unique_ptr<float[]> tmp;
123
124         if (device_spec == get_selected_device(ui->card_combo_1)) {
125                 tmp.reset(new float[num_samples]);
126
127                 convert_audio_to_fp32(tmp.get(), /*out_channel=*/0, /*out_num_channels=*/1, data, ui->channel_combo_1->currentIndex(), audio_format, num_samples);
128                 clip1.add_audio(tmp.get(), num_samples, audio_format.sample_rate, frame_time);
129                 ui->peak_display_1->audio_clip_updated();
130         }
131         if (device_spec == get_selected_device(ui->card_combo_2)) {
132                 if (tmp == nullptr) {
133                         tmp.reset(new float[num_samples]);
134                 }
135
136                 convert_audio_to_fp32(tmp.get(), /*out_channel=*/0, /*out_num_channels=*/1, data, ui->channel_combo_2->currentIndex(), audio_format, num_samples);
137                 clip2.add_audio(tmp.get(), num_samples, audio_format.sample_rate, frame_time);
138                 ui->peak_display_2->audio_clip_updated();
139         }
140
141         if (clip1.empty() && clip2.empty()) {
142                 // No interesting data yet.
143                 return;
144         }
145
146         steady_clock::time_point base;
147         if (!clip1.empty() && !clip2.empty()) {
148                 base = max(clip1.get_first_sample(), clip2.get_first_sample());
149         } else if (!clip1.empty()) {
150                 base = clip1.get_first_sample();
151         } else {
152                 assert(!clip2.empty());
153                 base = clip2.get_first_sample();
154         }
155         ui->peak_display_1->set_base(base);
156         ui->peak_display_2->set_base(base);
157
158         if (clip1.get_length_seconds_after_base(base) >= 1.0 &&
159             clip2.get_length_seconds_after_base(base) >= 1.0) {
160                 post_to_main_thread([this] {
161                         grab_timeout->stop();
162                         global_audio_mixer->set_extra_devices({});  // Put on another thread so that we don't get recursive locking.
163                 });
164                 grabbing = false;
165
166                 // The delay estimation is cheap, but it's not free, and we're on the main mixer thread,
167                 // so fire it off onto a background thread.
168                 std::thread(&DelayAnalyzer::estimate_delay, this).detach();
169         }
170 }
171
172 void DelayAnalyzer::estimate_delay()
173 {
174         AudioClip::BestCorrelation corr = clip1.find_best_correlation(&clip2);
175         char buf[256];
176         if (isnan(corr.correlation) || fabs(corr.correlation) < 0.1) {  // Very loose bound, basically to guard against digital silence.
177                 snprintf(buf, sizeof(buf), "Could not estimate delay.\n");
178         } else if (corr.correlation < 0.0) {
179                 snprintf(buf, sizeof(buf), "Best estimate: Channel is %.1f ms delayed compared to reference <em>and inverted</em> (correlation = %.3f)\n",
180                         corr.delay_ms, corr.correlation);
181         } else {
182                 snprintf(buf, sizeof(buf), "Best estimate: Channel is %.1f ms delayed compared to reference (correlation = %.3f)\n",
183                         corr.delay_ms, corr.correlation);
184         }
185         post_to_main_thread([this, buf] {
186                 ui->delay_estimate_label->setText(buf);
187         });
188 }
189
190 void DelayAnalyzer::grab_timed_out()
191 {
192         grabbing = false;
193         global_audio_mixer->set_extra_devices({});
194         ui->delay_estimate_label->setText("Could not capture audio (timed out).");
195 }