]> git.sesse.net Git - nageru/blob - midi_mapping_dialog.cpp
Implement auto-training controllers in the MIDI input mapping dialog.
[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 <string>
14
15 using namespace google::protobuf;
16 using namespace std;
17
18 vector<MIDIMappingDialog::Control> per_bus_controllers = {
19         { "Treble",                   MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
20         { "Mid",                      MIDIMappingBusProto::kMidFieldNumber,    MIDIMappingProto::kMidBankFieldNumber },
21         { "Bass",                     MIDIMappingBusProto::kBassFieldNumber,   MIDIMappingProto::kBassBankFieldNumber },
22         { "Gain",                     MIDIMappingBusProto::kGainFieldNumber,   MIDIMappingProto::kGainBankFieldNumber },
23         { "Compressor threshold",     MIDIMappingBusProto::kCompressorThresholdFieldNumber,
24                                       MIDIMappingProto::kCompressorThresholdBankFieldNumber},
25         { "Fader",                    MIDIMappingBusProto::kFaderFieldNumber,  MIDIMappingProto::kFaderBankFieldNumber }
26 };
27 vector<MIDIMappingDialog::Control> per_bus_buttons = {
28         { "Toggle locut",             MIDIMappingBusProto::kToggleLocutFieldNumber,
29                                       MIDIMappingProto::kToggleLocutBankFieldNumber },
30         { "Togle auto gain staging",  MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
31                                       MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
32         { "Togle compressor",         MIDIMappingBusProto::kToggleCompressorFieldNumber,
33                                       MIDIMappingProto::kToggleCompressorBankFieldNumber },
34         { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
35                                       MIDIMappingProto::kClearPeakBankFieldNumber }
36 };
37 vector<MIDIMappingDialog::Control> global_controllers = {
38         { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
39         { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
40                                       MIDIMappingProto::kLimiterThresholdBankFieldNumber },
41         { "Makeup gain",              MIDIMappingBusProto::kMakeupGainFieldNumber,
42                                       MIDIMappingProto::kMakeupGainBankFieldNumber }
43 };
44 vector<MIDIMappingDialog::Control> global_buttons = {
45         { "Previous bank",            MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
46         { "Next bank",                MIDIMappingBusProto::kNextBankFieldNumber, 0 },
47         { "Select bank 1",            MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
48         { "Select bank 2",            MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
49         { "Select bank 3",            MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
50         { "Select bank 4",            MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
51         { "Select bank 5",            MIDIMappingBusProto::kSelectBank5FieldNumber, 0 }
52 };
53
54 namespace {
55
56 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
57 {
58         const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
59         const Reflection *reflection = mapping_proto.GetReflection();
60         if (!reflection->HasField(mapping_proto, bank_descriptor)) {
61                 return default_value;
62         }
63         return reflection->GetInt32(mapping_proto, bank_descriptor);
64 }
65
66 int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
67 {
68         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
69                 return default_value;
70         }
71
72         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
73         const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
74         const Reflection *bus_reflection = bus_mapping.GetReflection();
75         if (!bus_reflection->HasField(bus_mapping, descriptor)) {
76                 return default_value;
77         }
78         const MIDIControllerProto &controller_proto = 
79                 static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
80         return controller_proto.controller_number();
81 }
82
83 int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
84 {
85         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
86                 return default_value;
87         }
88
89         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
90         const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
91         const Reflection *bus_reflection = bus_mapping.GetReflection();
92         if (!bus_reflection->HasField(bus_mapping, descriptor)) {
93                 return default_value;
94         }
95         const MIDIButtonProto &bus_proto = 
96                 static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
97         return bus_proto.note_number();
98 }
99
100 }  // namespace
101
102 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
103         : ui(new Ui::MIDIMappingDialog),
104           mapper(mapper)
105 {
106         ui->setupUi(this);
107
108         const MIDIMappingProto mapping_proto = mapper->get_current_mapping();  // Take a copy.
109         old_receiver = mapper->set_receiver(this);
110
111         QStringList labels;
112         labels << "";
113         labels << "Controller bank";
114         for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
115                 char buf[256];
116                 snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
117                 labels << buf;
118         }
119         labels << "";
120         ui->treeWidget->setColumnCount(num_buses + 3);
121         ui->treeWidget->setHeaderLabels(labels);
122
123         add_controls("Per-bus controllers", ControlType::CONTROLLER, mapping_proto, per_bus_controllers);
124         add_controls("Per-bus buttons", ControlType::BUTTON, mapping_proto, per_bus_buttons);
125         add_controls("Global controllers", ControlType::CONTROLLER, mapping_proto, global_controllers);
126         add_controls("Global buttons", ControlType::BUTTON, mapping_proto, global_buttons);
127         fill_controls_from_mapping(mapping_proto);
128
129         // Auto-resize every column but the last.
130         for (unsigned column_idx = 0; column_idx < num_buses + 3; ++column_idx) {
131                 ui->treeWidget->resizeColumnToContents(column_idx);
132         }
133
134         connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
135         connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
136         connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
137         connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
138 }
139
140 MIDIMappingDialog::~MIDIMappingDialog()
141 {
142         mapper->set_receiver(old_receiver);
143 }
144
145 void MIDIMappingDialog::ok_clicked()
146 {
147         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
148         mapper->set_midi_mapping(*new_mapping);
149         mapper->set_receiver(old_receiver);
150         accept();
151 }
152
153 void MIDIMappingDialog::cancel_clicked()
154 {
155         mapper->set_receiver(old_receiver);
156         reject();
157 }
158
159 void MIDIMappingDialog::save_clicked()
160 {
161         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
162         QString filename = QFileDialog::getSaveFileName(this,
163                 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
164         if (!filename.endsWith(".midimapping")) {
165                 filename += ".midimapping";
166         }
167         if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
168                 QMessageBox box;
169                 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
170                 box.exec();
171         }
172 }
173
174 void MIDIMappingDialog::load_clicked()
175 {
176         QString filename = QFileDialog::getOpenFileName(this,
177                 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
178         MIDIMappingProto new_mapping;
179         if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
180                 QMessageBox box;
181                 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
182                 box.exec();
183                 return;
184         }
185
186         fill_controls_from_mapping(new_mapping);
187 }
188
189 namespace {
190
191 template<class T>
192 T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
193 {
194         while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
195                 mapping_proto->add_bus_mapping();
196         }
197
198         MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
199         const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
200         const Reflection *bus_reflection = bus_mapping->GetReflection();
201         return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
202 }
203
204 }  // namespace
205
206 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
207 {
208         unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
209         for (const InstantiatedSpinner &is : controller_spinners) {
210                 const int val = is.spinner->value();
211                 if (val == 0) {
212                         continue;
213                 }
214
215                 MIDIControllerProto *controller_proto =
216                         get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
217                 controller_proto->set_controller_number(val);
218         }
219         for (const InstantiatedSpinner &is : button_spinners) {
220                 const int val = is.spinner->value();
221                 if (val == 0) {
222                         continue;
223                 }
224
225                 MIDIButtonProto *button_proto =
226                         get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
227                 button_proto->set_note_number(val);
228         }
229         int highest_bank_used = 0;  // 1-indexed.
230         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
231                 const int val = ic.combo_box->currentIndex();
232                 highest_bank_used = std::max(highest_bank_used, val);
233                 if (val == 0) {
234                         continue;
235                 }
236
237                 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
238                 const Reflection *bus_reflection = mapping_proto->GetReflection();
239                 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
240         }
241         mapping_proto->set_num_controller_banks(highest_bank_used);
242         return mapping_proto;
243 }
244
245 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
246 {
247         if (bank_field_number == 0) {
248                 return;
249         }
250         QComboBox *bank_selector = new QComboBox(this);
251         bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
252         bank_selector->setAutoFillBackground(true);
253
254         bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
255
256         ui->treeWidget->setItemWidget(item, 1, bank_selector);
257 }
258
259 void MIDIMappingDialog::add_controls(const string &heading,
260                                      MIDIMappingDialog::ControlType control_type,
261                                      const MIDIMappingProto &mapping_proto,
262                                      const vector<MIDIMappingDialog::Control> &controls)
263 {
264         QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
265         heading_item->setText(0, QString::fromStdString(heading));
266         heading_item->setFirstColumnSpanned(true);
267         heading_item->setExpanded(true);
268         for (const Control &control : controls) {
269                 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
270                 heading_item->addChild(item);
271                 add_bank_selector(item, mapping_proto, control.bank_field_number);
272                 item->setText(0, QString::fromStdString(control.label + "   "));
273
274                 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
275                         QSpinBox *spinner = new QSpinBox(this);
276                         spinner->setRange(0, 127);
277                         spinner->setAutoFillBackground(true);
278                         spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
279                         ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
280
281                         if (control_type == ControlType::CONTROLLER) {
282                                 controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
283                         } else {
284                                 assert(control_type == ControlType::BUTTON);
285                                 button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
286                         }
287                 }
288         }
289         ui->treeWidget->addTopLevelItem(heading_item);
290 }
291
292 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
293 {
294         for (const InstantiatedSpinner &is : controller_spinners) {
295                 is.spinner->setValue(get_controller_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
296         }
297         for (const InstantiatedSpinner &is : button_spinners) {
298                 is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
299         }
300         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
301                 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
302         }
303 }
304
305 void MIDIMappingDialog::controller_changed(unsigned controller)
306 {
307         for (const InstantiatedSpinner &is : controller_spinners) {
308                 if (is.spinner->hasFocus()) {
309                         is.spinner->setValue(controller);
310                         is.spinner->selectAll();
311                 }
312         }
313 }
314
315 void MIDIMappingDialog::note_on(unsigned note)
316 {
317         for (const InstantiatedSpinner &is : button_spinners) {
318                 if (is.spinner->hasFocus()) {
319                         is.spinner->setValue(note);
320                         is.spinner->selectAll();
321                 }
322         }
323 }