]> git.sesse.net Git - nageru/blob - midi_mapping_dialog.cpp
Support group-local extrapolation in the MIDI mapping editor.
[nageru] / midi_mapping_dialog.cpp
1 #include "midi_mapping_dialog.h"
2
3 #include "midi_mapper.h"
4 #include "midi_mapping.pb.h"
5 #include "post_to_main_thread.h"
6 #include "ui_midi_mapping.h"
7
8 #include <QComboBox>
9 #include <QFileDialog>
10 #include <QMessageBox>
11 #include <QSpinBox>
12
13 #include <algorithm>
14 #include <string>
15
16 using namespace google::protobuf;
17 using namespace std;
18
19 vector<MIDIMappingDialog::Control> per_bus_controllers = {
20         { "Treble",                   MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
21         { "Mid",                      MIDIMappingBusProto::kMidFieldNumber,    MIDIMappingProto::kMidBankFieldNumber },
22         { "Bass",                     MIDIMappingBusProto::kBassFieldNumber,   MIDIMappingProto::kBassBankFieldNumber },
23         { "Gain",                     MIDIMappingBusProto::kGainFieldNumber,   MIDIMappingProto::kGainBankFieldNumber },
24         { "Compressor threshold",     MIDIMappingBusProto::kCompressorThresholdFieldNumber,
25                                       MIDIMappingProto::kCompressorThresholdBankFieldNumber},
26         { "Fader",                    MIDIMappingBusProto::kFaderFieldNumber,  MIDIMappingProto::kFaderBankFieldNumber }
27 };
28 vector<MIDIMappingDialog::Control> per_bus_buttons = {
29         { "Toggle locut",             MIDIMappingBusProto::kToggleLocutFieldNumber,
30                                       MIDIMappingProto::kToggleLocutBankFieldNumber },
31         { "Togle auto gain staging",  MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
32                                       MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
33         { "Togle compressor",         MIDIMappingBusProto::kToggleCompressorFieldNumber,
34                                       MIDIMappingProto::kToggleCompressorBankFieldNumber },
35         { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
36                                       MIDIMappingProto::kClearPeakBankFieldNumber }
37 };
38 vector<MIDIMappingDialog::Control> global_controllers = {
39         { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
40         { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
41                                       MIDIMappingProto::kLimiterThresholdBankFieldNumber },
42         { "Makeup gain",              MIDIMappingBusProto::kMakeupGainFieldNumber,
43                                       MIDIMappingProto::kMakeupGainBankFieldNumber }
44 };
45 vector<MIDIMappingDialog::Control> global_buttons = {
46         { "Previous bank",            MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
47         { "Next bank",                MIDIMappingBusProto::kNextBankFieldNumber, 0 },
48         { "Select bank 1",            MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
49         { "Select bank 2",            MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
50         { "Select bank 3",            MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
51         { "Select bank 4",            MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
52         { "Select bank 5",            MIDIMappingBusProto::kSelectBank5FieldNumber, 0 }
53 };
54
55 namespace {
56
57 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
58 {
59         const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
60         const Reflection *reflection = mapping_proto.GetReflection();
61         if (!reflection->HasField(mapping_proto, bank_descriptor)) {
62                 return default_value;
63         }
64         return reflection->GetInt32(mapping_proto, bank_descriptor);
65 }
66
67 int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
68 {
69         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
70                 return default_value;
71         }
72
73         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
74         const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
75         const Reflection *bus_reflection = bus_mapping.GetReflection();
76         if (!bus_reflection->HasField(bus_mapping, descriptor)) {
77                 return default_value;
78         }
79         const MIDIControllerProto &controller_proto = 
80                 static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
81         return controller_proto.controller_number();
82 }
83
84 int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
85 {
86         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
87                 return default_value;
88         }
89
90         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
91         const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
92         const Reflection *bus_reflection = bus_mapping.GetReflection();
93         if (!bus_reflection->HasField(bus_mapping, descriptor)) {
94                 return default_value;
95         }
96         const MIDIButtonProto &bus_proto = 
97                 static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
98         return bus_proto.note_number();
99 }
100
101 }  // namespace
102
103 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
104         : ui(new Ui::MIDIMappingDialog),
105           mapper(mapper)
106 {
107         ui->setupUi(this);
108
109         const MIDIMappingProto mapping_proto = mapper->get_current_mapping();  // Take a copy.
110         old_receiver = mapper->set_receiver(this);
111
112         QStringList labels;
113         labels << "";
114         labels << "Controller bank";
115         for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
116                 char buf[256];
117                 snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
118                 labels << buf;
119         }
120         labels << "";
121         ui->treeWidget->setColumnCount(num_buses + 3);
122         ui->treeWidget->setHeaderLabels(labels);
123
124         add_controls("Per-bus controllers", ControlType::CONTROLLER, SpinnerGroup::PER_BUS_CONTROLLERS, mapping_proto, per_bus_controllers);
125         add_controls("Per-bus buttons", ControlType::BUTTON, SpinnerGroup::PER_BUS_BUTTONS, mapping_proto, per_bus_buttons);
126         add_controls("Global controllers", ControlType::CONTROLLER, SpinnerGroup::GLOBAL_CONTROLLERS, mapping_proto, global_controllers);
127         add_controls("Global buttons", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_buttons);
128         fill_controls_from_mapping(mapping_proto);
129
130         // Auto-resize every column but the last.
131         for (unsigned column_idx = 0; column_idx < num_buses + 3; ++column_idx) {
132                 ui->treeWidget->resizeColumnToContents(column_idx);
133         }
134
135         connect(ui->guess_bus_button, &QPushButton::clicked,
136                 bind(&MIDIMappingDialog::guess_clicked, this, false));
137         connect(ui->guess_group_button, &QPushButton::clicked,
138                 bind(&MIDIMappingDialog::guess_clicked, this, true));
139         connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
140         connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
141         connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
142         connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
143
144         update_guess_button_state();
145 }
146
147 MIDIMappingDialog::~MIDIMappingDialog()
148 {
149         mapper->set_receiver(old_receiver);
150 }
151
152 bool MIDIMappingDialog::eventFilter(QObject *obj, QEvent *event)
153 {
154         if (event->type() == QEvent::FocusIn ||
155             event->type() == QEvent::FocusOut) {
156                 // We ignore the guess buttons themselves; it should be allowed
157                 // to navigate from a spinner to focus on a button (to click it).
158                 if (obj != ui->guess_bus_button && obj != ui->guess_group_button) {
159                         update_guess_button_state();
160                 }
161         }
162         return false;
163 }
164
165 void MIDIMappingDialog::guess_clicked(bool limit_to_group)
166 {
167         FocusInfo focus = find_focus();
168         if (focus.bus_idx == -1) {
169                 // The guess button probably took the focus away from us.
170                 focus = last_focus;
171         }
172         assert(focus.bus_idx != -1);  // The button should have been disabled.
173         pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, limit_to_group ? focus.spinner_group : SpinnerGroup::ALL_GROUPS);
174         const int source_bus_idx = bus_and_offset.first;
175         const int offset = bus_and_offset.second;
176         assert(source_bus_idx != -1);  // The button should have been disabled.
177
178         for (const auto &field_number_and_spinner : spinners[focus.bus_idx]) {
179                 int field_number = field_number_and_spinner.first;
180                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
181                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
182
183                 if (limit_to_group && this_spinner_group != focus.spinner_group) {
184                         continue;
185                 }
186
187                 assert(spinners[source_bus_idx].count(field_number));
188                 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
189                 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
190
191                 if (source_spinner->value() != 0) {
192                         spinner->setValue(source_spinner->value() + offset);
193                 }
194         }
195 }
196
197 void MIDIMappingDialog::ok_clicked()
198 {
199         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
200         mapper->set_midi_mapping(*new_mapping);
201         mapper->set_receiver(old_receiver);
202         accept();
203 }
204
205 void MIDIMappingDialog::cancel_clicked()
206 {
207         mapper->set_receiver(old_receiver);
208         reject();
209 }
210
211 void MIDIMappingDialog::save_clicked()
212 {
213         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
214         QString filename = QFileDialog::getSaveFileName(this,
215                 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
216         if (!filename.endsWith(".midimapping")) {
217                 filename += ".midimapping";
218         }
219         if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
220                 QMessageBox box;
221                 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
222                 box.exec();
223         }
224 }
225
226 void MIDIMappingDialog::load_clicked()
227 {
228         QString filename = QFileDialog::getOpenFileName(this,
229                 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
230         MIDIMappingProto new_mapping;
231         if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
232                 QMessageBox box;
233                 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
234                 box.exec();
235                 return;
236         }
237
238         fill_controls_from_mapping(new_mapping);
239 }
240
241 namespace {
242
243 template<class T>
244 T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
245 {
246         while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
247                 mapping_proto->add_bus_mapping();
248         }
249
250         MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
251         const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
252         const Reflection *bus_reflection = bus_mapping->GetReflection();
253         return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
254 }
255
256 }  // namespace
257
258 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
259 {
260         unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
261         for (const InstantiatedSpinner &is : controller_spinners) {
262                 const int val = is.spinner->value();
263                 if (val == 0) {
264                         continue;
265                 }
266
267                 MIDIControllerProto *controller_proto =
268                         get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
269                 controller_proto->set_controller_number(val);
270         }
271         for (const InstantiatedSpinner &is : button_spinners) {
272                 const int val = is.spinner->value();
273                 if (val == 0) {
274                         continue;
275                 }
276
277                 MIDIButtonProto *button_proto =
278                         get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
279                 button_proto->set_note_number(val);
280         }
281         int highest_bank_used = 0;  // 1-indexed.
282         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
283                 const int val = ic.combo_box->currentIndex();
284                 highest_bank_used = std::max(highest_bank_used, val);
285                 if (val == 0) {
286                         continue;
287                 }
288
289                 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
290                 const Reflection *bus_reflection = mapping_proto->GetReflection();
291                 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
292         }
293         mapping_proto->set_num_controller_banks(highest_bank_used);
294         return mapping_proto;
295 }
296
297 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
298 {
299         if (bank_field_number == 0) {
300                 return;
301         }
302         QComboBox *bank_selector = new QComboBox(this);
303         bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
304         bank_selector->setAutoFillBackground(true);
305
306         bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
307
308         ui->treeWidget->setItemWidget(item, 1, bank_selector);
309 }
310
311 void MIDIMappingDialog::add_controls(const string &heading,
312                                      MIDIMappingDialog::ControlType control_type,
313                                      MIDIMappingDialog::SpinnerGroup spinner_group,
314                                      const MIDIMappingProto &mapping_proto,
315                                      const vector<MIDIMappingDialog::Control> &controls)
316 {
317         QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
318         heading_item->setText(0, QString::fromStdString(heading));
319         heading_item->setFirstColumnSpanned(true);
320         heading_item->setExpanded(true);
321         for (const Control &control : controls) {
322                 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
323                 heading_item->addChild(item);
324                 add_bank_selector(item, mapping_proto, control.bank_field_number);
325                 item->setText(0, QString::fromStdString(control.label + "   "));
326
327                 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
328                         QSpinBox *spinner = new QSpinBox(this);
329                         spinner->setRange(0, 127);
330                         spinner->setAutoFillBackground(true);
331                         spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
332                         spinner->installEventFilter(this);  // So we know when the focus changes.
333                         ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
334
335                         if (control_type == ControlType::CONTROLLER) {
336                                 controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
337                         } else {
338                                 assert(control_type == ControlType::BUTTON);
339                                 button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
340                         }
341                         spinners[bus_idx][control.field_number] = SpinnerAndGroup{ spinner, spinner_group };
342                         connect(spinner, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
343                                 bind(&MIDIMappingDialog::update_guess_button_state, this));
344                 }
345         }
346         ui->treeWidget->addTopLevelItem(heading_item);
347 }
348
349 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
350 {
351         for (const InstantiatedSpinner &is : controller_spinners) {
352                 is.spinner->setValue(get_controller_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
353         }
354         for (const InstantiatedSpinner &is : button_spinners) {
355                 is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
356         }
357         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
358                 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
359         }
360 }
361
362 void MIDIMappingDialog::controller_changed(unsigned controller)
363 {
364         for (const InstantiatedSpinner &is : controller_spinners) {
365                 if (is.spinner->hasFocus()) {
366                         is.spinner->setValue(controller);
367                         is.spinner->selectAll();
368                 }
369         }
370 }
371
372 void MIDIMappingDialog::note_on(unsigned note)
373 {
374         for (const InstantiatedSpinner &is : button_spinners) {
375                 if (is.spinner->hasFocus()) {
376                         is.spinner->setValue(note);
377                         is.spinner->selectAll();
378                 }
379         }
380 }
381
382 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group)
383 {
384         constexpr pair<int, int> not_found(-1, 0);
385
386         if (bus_is_empty(bus_idx, spinner_group)) {
387                 return not_found;
388         }
389
390         // See if we can find a non-empty bus to source from (prefer from the left).
391         unsigned source_bus_idx;
392         if (bus_idx > 0 && !bus_is_empty(bus_idx - 1, spinner_group)) {
393                 source_bus_idx = bus_idx - 1;
394         } else if (bus_idx < num_buses - 1 && !bus_is_empty(bus_idx + 1, spinner_group)) {
395                 source_bus_idx = bus_idx + 1;
396         } else {
397                 return not_found;
398         }
399
400         // See if we can find a consistent offset.
401         bool found_offset = false;
402         int offset = 0;
403         for (const auto &field_number_and_spinner : spinners[bus_idx]) {
404                 int field_number = field_number_and_spinner.first;
405                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
406                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
407
408                 if (spinner->value() == 0) {
409                         continue;
410                 }
411                 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
412                     spinner_group != this_spinner_group) {
413                         continue;
414                 }
415
416                 assert(spinners[source_bus_idx].count(field_number));
417                 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
418                 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
419                 if (source_spinner->value() == 0) {
420                         // The bus has a controller set that the source bus doesn't set.
421                         return not_found;
422                 }
423
424                 int candidate_offset = spinner->value() - source_spinner->value();
425                 if (!found_offset) {
426                         offset = candidate_offset;
427                         found_offset = true;
428                 } else if (candidate_offset != offset) {
429                         return not_found;
430                 }
431         }
432
433         if (!found_offset) {
434                 // Given that the bus wasn't empty, this shouldn't happen.
435                 assert(false);
436                 return not_found;
437         }
438         return make_pair(source_bus_idx, offset);
439 }
440
441 bool MIDIMappingDialog::bus_is_empty(unsigned bus_idx, SpinnerGroup spinner_group)
442 {
443         for (const auto &field_number_and_spinner : spinners[bus_idx]) {
444                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
445                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
446                 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
447                     spinner_group != this_spinner_group) {
448                         continue;
449                 }
450                 if (spinner->value() != 0) {
451                         return false;
452                 }
453         }
454         return true;
455 }
456
457 void MIDIMappingDialog::update_guess_button_state()
458 {
459         FocusInfo focus = find_focus();
460         if (focus.bus_idx < 0) {
461                 return;
462         }
463         {
464                 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, SpinnerGroup::ALL_GROUPS);
465                 ui->guess_bus_button->setEnabled(bus_and_offset.first != -1);
466         }
467         {
468                 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, focus.spinner_group);
469                 ui->guess_group_button->setEnabled(bus_and_offset.first != -1);
470         }
471         last_focus = focus;
472 }
473
474 MIDIMappingDialog::FocusInfo MIDIMappingDialog::find_focus() const
475 {
476         for (const InstantiatedSpinner &is : controller_spinners) {
477                 if (is.spinner->hasFocus()) {
478                         return FocusInfo{ int(is.bus_idx), is.spinner_group };
479                 }
480         }
481         for (const InstantiatedSpinner &is : button_spinners) {
482                 if (is.spinner->hasFocus()) {
483                         return FocusInfo{ int(is.bus_idx), is.spinner_group };
484                 }
485         }
486         return FocusInfo{ -1, SpinnerGroup::ALL_GROUPS };
487 }