From 55bc1e47c47dfdeeec06c55de9a53b95bdbbd326 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sat, 19 Jan 2019 22:38:38 +0100 Subject: [PATCH] Add a MIDI mapping editor for Futatabi. --- futatabi/mainwindow.cpp | 7 + futatabi/mainwindow.h | 1 + futatabi/mainwindow.ui | 6 + futatabi/midi_mapping.ui | 78 +++++ futatabi/midi_mapping_dialog.cpp | 503 +++++++++++++++++++++++++++++++ futatabi/midi_mapping_dialog.h | 101 +++++++ meson.build | 6 +- shared/midi_mapper_util.h | 16 +- 8 files changed, 711 insertions(+), 7 deletions(-) create mode 100644 futatabi/midi_mapping.ui create mode 100644 futatabi/midi_mapping_dialog.cpp create mode 100644 futatabi/midi_mapping_dialog.h diff --git a/futatabi/mainwindow.cpp b/futatabi/mainwindow.cpp index 6c25018..f465468 100644 --- a/futatabi/mainwindow.cpp +++ b/futatabi/mainwindow.cpp @@ -6,6 +6,7 @@ #include "frame_on_disk.h" #include "player.h" #include "futatabi_midi_mapping.pb.h" +#include "midi_mapping_dialog.h" #include "shared/aboutdialog.h" #include "shared/disk_space_estimator.h" #include "shared/post_to_main_thread.h" @@ -77,6 +78,7 @@ MainWindow::MainWindow() save_settings(); // The menus. + connect(ui->midi_mapping_action, &QAction::triggered, this, &MainWindow::midi_mapping_triggered); connect(ui->exit_action, &QAction::triggered, this, &MainWindow::exit_triggered); connect(ui->export_cliplist_clip_multitrack_action, &QAction::triggered, this, &MainWindow::export_cliplist_clip_multitrack_triggered); connect(ui->export_playlist_clip_interpolated_action, &QAction::triggered, this, &MainWindow::export_playlist_clip_interpolated_triggered); @@ -982,6 +984,11 @@ void MainWindow::report_disk_space(off_t free_bytes, double estimated_seconds_le }); } +void MainWindow::midi_mapping_triggered() +{ + MIDIMappingDialog(&midi_mapper).exec(); +} + void MainWindow::exit_triggered() { close(); diff --git a/futatabi/mainwindow.h b/futatabi/mainwindow.h index 028c91b..5fd0954 100644 --- a/futatabi/mainwindow.h +++ b/futatabi/mainwindow.h @@ -168,6 +168,7 @@ private: bool eventFilter(QObject *watched, QEvent *event) override; void report_disk_space(off_t free_bytes, double estimated_seconds_left); + void midi_mapping_triggered(); void exit_triggered(); void export_cliplist_clip_multitrack_triggered(); void export_playlist_clip_interpolated_triggered(); diff --git a/futatabi/mainwindow.ui b/futatabi/mainwindow.ui index da05caf..addfbe2 100644 --- a/futatabi/mainwindow.ui +++ b/futatabi/mainwindow.ui @@ -391,6 +391,7 @@ + @@ -525,6 +526,11 @@ &5 seconds + + + Setup MIDI controller… + + diff --git a/futatabi/midi_mapping.ui b/futatabi/midi_mapping.ui new file mode 100644 index 0000000..99eb7fd --- /dev/null +++ b/futatabi/midi_mapping.ui @@ -0,0 +1,78 @@ + + + MIDIMappingDialog + + + + 0 + 0 + 879 + 583 + + + + MIDI controller setup + + + + + + + 1 + + + + + + + + Add or change a mapping by clicking in the cell, then moving the corresponding control on your MIDI device. + + + + + + + + + &Save… + + + + + + + &Load… + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + diff --git a/futatabi/midi_mapping_dialog.cpp b/futatabi/midi_mapping_dialog.cpp new file mode 100644 index 0000000..30d562f --- /dev/null +++ b/futatabi/midi_mapping_dialog.cpp @@ -0,0 +1,503 @@ +#include "midi_mapping_dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shared/controller_spin_box.h" +#include "midi_mapper.h" +#include "futatabi_midi_mapping.pb.h" +#include "shared/midi_mapper_util.h" +#include "shared/post_to_main_thread.h" +#include "ui_midi_mapping.h" + +class QObject; + +using namespace google::protobuf; +using namespace std; + +vector controllers = { + { "Jog", MIDIMappingProto::kJogFieldNumber, + MIDIMappingProto::kJogBankFieldNumber }, + { "Master speed", MIDIMappingProto::kMasterSpeedFieldNumber, + MIDIMappingProto::kMasterSpeedBankFieldNumber }, +}; +vector controller_lights = { + { "Master speed light", MIDIMappingProto::kMasterSpeedLightFieldNumber, 0 }, +}; +vector buttons = { + { "Preview", MIDIMappingProto::kPreviewFieldNumber, + MIDIMappingProto::kPreviewBankFieldNumber }, + { "Queue", MIDIMappingProto::kQueueFieldNumber, + MIDIMappingProto::kQueueBankFieldNumber }, + { "Play", MIDIMappingProto::kPlayFieldNumber, + MIDIMappingProto::kPlayBankFieldNumber }, + { "Lock master speed", MIDIMappingProto::kToggleLockFieldNumber, + MIDIMappingProto::kToggleLockBankFieldNumber }, + { "Cue in", MIDIMappingProto::kCueInFieldNumber, + MIDIMappingProto::kCueInBankFieldNumber }, + { "Cue out", MIDIMappingProto::kCueOutFieldNumber, + MIDIMappingProto::kCueOutBankFieldNumber }, + { "Previous bank", MIDIMappingProto::kPrevBankFieldNumber, 0 }, + { "Next bank", MIDIMappingProto::kNextBankFieldNumber, 0 }, + { "Select bank 1", MIDIMappingProto::kSelectBank1FieldNumber, 0 }, + { "Select bank 2", MIDIMappingProto::kSelectBank2FieldNumber, 0 }, + { "Select bank 3", MIDIMappingProto::kSelectBank3FieldNumber, 0 }, + { "Select bank 4", MIDIMappingProto::kSelectBank4FieldNumber, 0 }, + { "Select bank 5", MIDIMappingProto::kSelectBank5FieldNumber, 0 }, +}; +vector button_lights = { + { "Preview playing", MIDIMappingProto::kPreviewPlayingFieldNumber, 0 }, + { "Preview ready", MIDIMappingProto::kPreviewReadyFieldNumber, 0 }, + { "Queue button enabled", MIDIMappingProto::kQueueEnabledFieldNumber, 0 }, + { "Playing", MIDIMappingProto::kPlayingFieldNumber, 0 }, + { "Play ready", MIDIMappingProto::kPlayReadyFieldNumber, 0 }, + { "Master speed locked", MIDIMappingProto::kLockedFieldNumber, 0 }, + { "Master speed locked (blinking)", + MIDIMappingProto::kLockedBlinkingFieldNumber, 0 }, + { "Cue in enabled", MIDIMappingProto::kCueInEnabledFieldNumber, 0 }, + { "Cue out enabled", MIDIMappingProto::kCueOutEnabledFieldNumber, 0 }, + { "Bank 1 is selected", MIDIMappingProto::kBank1IsSelectedFieldNumber, 0 }, + { "Bank 2 is selected", MIDIMappingProto::kBank2IsSelectedFieldNumber, 0 }, + { "Bank 3 is selected", MIDIMappingProto::kBank3IsSelectedFieldNumber, 0 }, + { "Bank 4 is selected", MIDIMappingProto::kBank4IsSelectedFieldNumber, 0 }, + { "Bank 5 is selected", MIDIMappingProto::kBank5IsSelectedFieldNumber, 0 }, +}; + +namespace { + +int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value) +{ + const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number); + const Reflection *reflection = mapping_proto.GetReflection(); + if (!reflection->HasField(mapping_proto, bank_descriptor)) { + return default_value; + } + return reflection->GetInt32(mapping_proto, bank_descriptor); +} + +} // namespace + +MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper) + : ui(new Ui::MIDIMappingDialog), + mapper(mapper) +{ + ui->setupUi(this); + + const MIDIMappingProto mapping_proto = mapper->get_current_mapping(); // Take a copy. + old_receiver = mapper->set_receiver(this); + + QStringList labels; + labels << ""; + labels << "Controller bank"; + labels << ""; + labels << ""; + labels << ""; + labels << ""; + ui->treeWidget->setColumnCount(6); + ui->treeWidget->setHeaderLabels(labels); + + vector camera_select_buttons; + vector camera_is_selected_lights; + for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) { + char str[256]; + snprintf(str, sizeof(str), "Switch to camera %zu", camera_idx + 1); + camera_select_buttons.emplace_back(Control{ str, CameraMIDIMappingProto::kButtonFieldNumber, 0 }); + + snprintf(str, sizeof(str), "Camera %zu is current", camera_idx + 1); + camera_is_selected_lights.emplace_back(Control{ str, CameraMIDIMappingProto::kIsCurrentFieldNumber, 0 }); + } + + add_controls("Controllers", ControlType::CONTROLLER, mapping_proto, controllers); + add_controls("Controller lights", ControlType::CONTROLLER_LIGHT, mapping_proto, controller_lights); + add_controls("Buttons", ControlType::BUTTON, mapping_proto, buttons); + add_controls("Button lights", ControlType::BUTTON_LIGHT, mapping_proto, button_lights); + add_controls("Camera select buttons", ControlType::CAMERA_BUTTON, mapping_proto, camera_select_buttons); + add_controls("Camera is selected lights", ControlType::CAMERA_BUTTON_LIGHT, mapping_proto, camera_is_selected_lights); + fill_controls_from_mapping(mapping_proto); + + // Auto-resize every column but the last. + for (unsigned column_idx = 0; column_idx < 5; ++column_idx) { + ui->treeWidget->resizeColumnToContents(column_idx); + } + + connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked); + connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked); + connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked); + connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked); +} + +MIDIMappingDialog::~MIDIMappingDialog() +{ + mapper->set_receiver(old_receiver); +} + +void MIDIMappingDialog::ok_clicked() +{ + unique_ptr new_mapping = construct_mapping_proto_from_ui(); + mapper->set_midi_mapping(*new_mapping); + mapper->set_receiver(old_receiver); + accept(); +} + +void MIDIMappingDialog::cancel_clicked() +{ + mapper->set_receiver(old_receiver); + reject(); +} + +void MIDIMappingDialog::save_clicked() +{ + QFileDialog::Options options; + unique_ptr new_mapping = construct_mapping_proto_from_ui(); + QString filename = QFileDialog::getSaveFileName(this, + "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options); + if (!filename.endsWith(".midimapping")) { + filename += ".midimapping"; + } + if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) { + QMessageBox box; + box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again."); + box.exec(); + } +} + +void MIDIMappingDialog::load_clicked() +{ + QFileDialog::Options options; + QString filename = QFileDialog::getOpenFileName(this, + "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options); + MIDIMappingProto new_mapping; + if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) { + QMessageBox box; + box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid."); + box.exec(); + return; + } + + fill_controls_from_mapping(new_mapping); +} + +namespace { + +template +T *get_mutable_message(Proto *proto, int field_number) +{ + const FieldDescriptor *descriptor = proto->GetDescriptor()->FindFieldByNumber(field_number); + const Reflection *bus_reflection = proto->GetReflection(); + return static_cast(bus_reflection->MutableMessage(proto, descriptor)); +} + +} // namespace + +unique_ptr MIDIMappingDialog::construct_mapping_proto_from_ui() +{ + unique_ptr mapping_proto(new MIDIMappingProto); + for (const InstantiatedSpinner &is : controller_spinners) { + const int val = is.spinner->value(); + if (val == -1) { + continue; + } + + MIDIControllerProto *controller_proto = + get_mutable_message(mapping_proto.get(), is.field_number); + controller_proto->set_controller_number(val); + } + for (const InstantiatedSpinner &is : controller_light_spinners) { + const int val = is.spinner->value(); + if (val == -1) { + continue; + } + + MIDIControllerProto *controller_proto = + get_mutable_message(mapping_proto.get(), is.field_number); + controller_proto->set_controller_number(val); + + // HACK: We only have one of these right now, so min/max is a given; + // no need to store proto field numbers. + int val2 = is.spinner2->value(); + if (val2 != -1) { + mapping_proto->set_master_speed_light_min(val2); + } + int val3 = is.spinner3->value(); + if (val3 != -1) { + mapping_proto->set_master_speed_light_max(val3); + } + } + for (const InstantiatedSpinner &is : button_spinners) { + const int val = is.spinner->value(); + if (val == -1) { + continue; + } + + MIDIButtonProto *button_proto = + get_mutable_message(mapping_proto.get(), is.field_number); + button_proto->set_note_number(val); + } + for (const InstantiatedSpinner &is : button_light_spinners) { + const int val = is.spinner->value(); + if (val == -1) { + continue; + } + + MIDILightProto *light_proto = + get_mutable_message(mapping_proto.get(), is.field_number); + light_proto->set_note_number(val); + + int val2 = is.spinner2->value(); + if (val2 != -1) { + light_proto->set_velocity(val2); + } + } + int highest_bank_used = 0; // 1-indexed. + for (const InstantiatedComboBox &ic : bank_combo_boxes) { + const int val = ic.combo_box->currentIndex(); + highest_bank_used = std::max(highest_bank_used, val); + if (val == 0) { + continue; + } + + const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number); + const Reflection *bus_reflection = mapping_proto->GetReflection(); + bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1); + } + mapping_proto->set_num_controller_banks(highest_bank_used); + + size_t num_cameras_used = 0; + for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) { + if (camera_button_spinners[camera_idx].spinner->value() != -1) { + num_cameras_used = camera_idx + 1; + } else if (camera_button_light_spinners[camera_idx].spinner->value() != -1) { + num_cameras_used = camera_idx + 1; + } + } + for (size_t camera_idx = 0; camera_idx < num_cameras_used; ++camera_idx) { + CameraMIDIMappingProto *camera_proto = mapping_proto->add_camera(); + + { + const InstantiatedSpinner &is = camera_button_spinners[camera_idx]; + MIDIButtonProto *button_proto = + get_mutable_message(camera_proto, is.field_number); + int val = is.spinner->value(); + if (val != -1) { + button_proto->set_note_number(val); + } + } + { + const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx]; + MIDILightProto *light_proto = + get_mutable_message(camera_proto, is.field_number); + int val = is.spinner->value(); + if (val != -1) { + light_proto->set_note_number(val); + } + + int val2 = is.spinner2->value(); + if (val2 != -1) { + light_proto->set_velocity(val2); + } + } + } + + return mapping_proto; +} + +void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number) +{ + if (bank_field_number == 0) { + return; + } + QComboBox *bank_selector = new QComboBox(this); + bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5"); + bank_selector->setAutoFillBackground(true); + + bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number }); + + ui->treeWidget->setItemWidget(item, 1, bank_selector); +} + +void MIDIMappingDialog::add_controls(const string &heading, + MIDIMappingDialog::ControlType control_type, + const MIDIMappingProto &mapping_proto, + const vector &controls) +{ + QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget); + heading_item->setText(0, QString::fromStdString(heading)); + if (control_type == ControlType::BUTTON_LIGHT) { + heading_item->setText(3, "Velocity"); + } else if (control_type == ControlType::CONTROLLER_LIGHT) { + heading_item->setText(3, "Min"); + heading_item->setText(4, "Max"); + } else { + heading_item->setFirstColumnSpanned(true); + } + heading_item->setExpanded(true); + for (const Control &control : controls) { + QTreeWidgetItem *item = new QTreeWidgetItem(heading_item); + heading_item->addChild(item); + add_bank_selector(item, mapping_proto, control.bank_field_number); + item->setText(0, QString::fromStdString(control.label + " ")); + + QSpinBox *spinner; + if (control_type == ControlType::CONTROLLER) { + spinner = new ControllerSpinBox(this); + spinner->setRange(-1, 128); // 128 for pitch bend. + } else { + spinner = new QSpinBox(this); + spinner->setRange(-1, 127); + } + spinner->setAutoFillBackground(true); + spinner->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty). + ui->treeWidget->setItemWidget(item, 2, spinner); + + if (control_type == ControlType::CONTROLLER) { + controller_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number }); + } else if (control_type == ControlType::CONTROLLER_LIGHT) { + QSpinBox *spinner2 = new QSpinBox(this); + spinner2->setRange(-1, 127); + spinner2->setAutoFillBackground(true); + spinner2->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty). + + QSpinBox *spinner3 = new QSpinBox(this); + spinner3->setRange(-1, 127); + spinner3->setAutoFillBackground(true); + spinner3->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty). + + ui->treeWidget->setItemWidget(item, 3, spinner2); + ui->treeWidget->setItemWidget(item, 4, spinner3); + + controller_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, spinner3, control.field_number }); + } else if (control_type == ControlType::BUTTON) { + button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number }); + } else if (control_type == ControlType::CAMERA_BUTTON) { + camera_button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number }); + } else { + assert(control_type == ControlType::BUTTON_LIGHT || control_type == ControlType::CAMERA_BUTTON_LIGHT); + QSpinBox *spinner2 = new QSpinBox(this); + spinner2->setRange(-1, 127); + spinner2->setAutoFillBackground(true); + spinner2->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty). + ui->treeWidget->setItemWidget(item, 3, spinner2); + if (control_type == ControlType::BUTTON_LIGHT) { + button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number }); + } else { + assert(control_type == ControlType::CAMERA_BUTTON_LIGHT); + camera_button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number }); + } + } + spinners[control.field_number] = spinner; + } + ui->treeWidget->addTopLevelItem(heading_item); +} + +void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto) +{ + for (const InstantiatedSpinner &is : controller_spinners) { + is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1)); + } + for (const InstantiatedSpinner &is : controller_light_spinners) { + is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1)); + + // HACK: We only have one of these right now, so min/max is a given; + // no need to store proto field numbers. + if (mapping_proto.has_master_speed_light_min()) { + is.spinner2->setValue(mapping_proto.master_speed_light_min()); + } + if (mapping_proto.has_master_speed_light_max()) { + is.spinner3->setValue(mapping_proto.master_speed_light_max()); + } + } + for (const InstantiatedSpinner &is : button_spinners) { + is.spinner->setValue(get_button_mapping_helper(mapping_proto, is.field_number, -1)); + } + for (const InstantiatedSpinner &is : button_light_spinners) { + MIDILightProto light_proto = get_light_mapping_helper(mapping_proto, is.field_number); + if (light_proto.has_note_number()) { + is.spinner->setValue(light_proto.note_number()); + } else { + is.spinner->setValue(-1); + } + if (light_proto.has_velocity()) { + is.spinner2->setValue(light_proto.velocity()); + } else { + is.spinner2->setValue(-1); + } + } + for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) { + CameraMIDIMappingProto camera_proto; + if (camera_idx < size_t(mapping_proto.camera_size())) { + camera_proto = mapping_proto.camera(camera_idx); + } + { + const InstantiatedSpinner &is = camera_button_spinners[camera_idx]; + is.spinner->setValue(get_button_mapping_helper(camera_proto, is.field_number, -1)); + } + { + const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx]; + const MIDILightProto &light_proto = get_light_mapping_helper(camera_proto, is.field_number); + if (light_proto.has_note_number()) { + is.spinner->setValue(light_proto.note_number()); + } else { + is.spinner->setValue(-1); + } + if (light_proto.has_velocity()) { + is.spinner2->setValue(light_proto.velocity()); + } else { + is.spinner2->setValue(-1); + } + } + } + for (const InstantiatedComboBox &ic : bank_combo_boxes) { + ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1); + } +} + +void MIDIMappingDialog::controller_changed(unsigned controller) +{ + post_to_main_thread([=]{ + for (const InstantiatedSpinner &is : controller_spinners) { + if (is.spinner->hasFocus()) { + is.spinner->setValue(controller); + is.spinner->selectAll(); + } + } + for (const InstantiatedSpinner &is : controller_light_spinners) { + if (is.spinner->hasFocus()) { + is.spinner->setValue(controller); + is.spinner->selectAll(); + } + } + }); +} + +void MIDIMappingDialog::note_on(unsigned note) +{ + post_to_main_thread([=]{ + for (const InstantiatedSpinner &is : button_spinners) { + if (is.spinner->hasFocus()) { + is.spinner->setValue(note); + is.spinner->selectAll(); + } + } + for (const InstantiatedSpinner &is : button_light_spinners) { + if (is.spinner->hasFocus()) { + is.spinner->setValue(note); + is.spinner->selectAll(); + } + } + }); +} diff --git a/futatabi/midi_mapping_dialog.h b/futatabi/midi_mapping_dialog.h new file mode 100644 index 0000000..6b3e944 --- /dev/null +++ b/futatabi/midi_mapping_dialog.h @@ -0,0 +1,101 @@ +#ifndef _MIDI_MAPPING_DIALOG_H +#define _MIDI_MAPPING_DIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "midi_mapper.h" + +class QEvent; +class QObject; + +namespace Ui { +class MIDIMappingDialog; +} // namespace Ui + +class MIDIMappingProto; +class QComboBox; +class QSpinBox; +class QTreeWidgetItem; + +class MIDIMappingDialog : public QDialog, public ControllerReceiver +{ + Q_OBJECT + +public: + MIDIMappingDialog(MIDIMapper *mapper); + ~MIDIMappingDialog(); + + // For use in midi_mapping_dialog.cpp only. + struct Control { + std::string label; + int field_number; // In MIDIMappingBusProto. + int bank_field_number; // In MIDIMappingProto. + }; + + // ControllerReceiver interface. We only implement the raw events. + void preview() override {} + void queue() override {} + void play() override {} + void toggle_lock() override {} + void jog(int delta) override {} + void switch_camera(unsigned camera_idx) override {} + void set_master_speed(float speed) override {} + void cue_in() override {} + void cue_out() override {} + + // Raw events; used for the editor dialog only. + void controller_changed(unsigned controller) override; + void note_on(unsigned note) override; + +private: + void ok_clicked(); + void cancel_clicked(); + void save_clicked(); + void load_clicked(); + + void add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number); + + enum class ControlType { CONTROLLER, CONTROLLER_LIGHT, BUTTON, BUTTON_LIGHT, CAMERA_BUTTON, CAMERA_BUTTON_LIGHT }; + void add_controls(const std::string &heading, ControlType control_type, + const MIDIMappingProto &mapping_proto, const std::vector &controls); + void fill_controls_from_mapping(const MIDIMappingProto &mapping_proto); + + std::unique_ptr construct_mapping_proto_from_ui(); + + Ui::MIDIMappingDialog *ui; + MIDIMapper *mapper; + ControllerReceiver *old_receiver; + + // All controllers actually laid out on the grid (we need to store them + // so that we can move values back and forth between the controls and + // the protobuf on save/load). + struct InstantiatedSpinner { + QSpinBox *spinner; + QSpinBox *spinner2; // Value for button lights, min value for controller lights. + QSpinBox *spinner3; // Max value for controller lights. + int field_number; // In MIDIMappingBusProto. + }; + struct InstantiatedComboBox { + QComboBox *combo_box; + int field_number; // In MIDIMappingProto. + }; + std::vector controller_spinners; + std::vector controller_light_spinners; + std::vector button_spinners; + std::vector button_light_spinners; + std::vector camera_button_spinners; // One per camera. + std::vector camera_button_light_spinners; // One per camera. + std::vector bank_combo_boxes; + + // Keyed on field number. + std::map spinners; +}; + +#endif // !defined(_MIDI_MAPPING_DIALOG_H) diff --git a/meson.build b/meson.build index 193901e..d785000 100644 --- a/meson.build +++ b/meson.build @@ -273,8 +273,8 @@ proto_generated = gen.process('futatabi/state.proto', 'futatabi/frame.proto', 'f # Preprocess Qt as needed. moc_files = qt5.preprocess( - moc_headers: ['futatabi/mainwindow.h', 'futatabi/jpeg_frame_view.h', 'futatabi/clip_list.h'], - ui_files: ['futatabi/mainwindow.ui'], + moc_headers: ['futatabi/mainwindow.h', 'futatabi/jpeg_frame_view.h', 'futatabi/clip_list.h', 'futatabi/midi_mapping_dialog.h'], + ui_files: ['futatabi/mainwindow.ui', 'futatabi/midi_mapping.ui'], qresources: ['futatabi/mainwindow.qrc'], dependencies: qt5deps) @@ -285,7 +285,7 @@ futatabi_srcs = ['futatabi/flow.cpp', 'futatabi/gpu_timers.cpp'] futatabi_srcs += ['futatabi/main.cpp', 'futatabi/player.cpp', 'futatabi/video_stream.cpp', 'futatabi/chroma_subsampler.cpp'] futatabi_srcs += ['futatabi/vaapi_jpeg_decoder.cpp', 'futatabi/db.cpp', 'futatabi/ycbcr_converter.cpp', 'futatabi/flags.cpp'] futatabi_srcs += ['futatabi/mainwindow.cpp', 'futatabi/jpeg_frame_view.cpp', 'futatabi/clip_list.cpp', 'futatabi/frame_on_disk.cpp'] -futatabi_srcs += ['futatabi/export.cpp', 'futatabi/midi_mapper.cpp'] +futatabi_srcs += ['futatabi/export.cpp', 'futatabi/midi_mapper.cpp', 'futatabi/midi_mapping_dialog.cpp'] futatabi_srcs += moc_files futatabi_srcs += proto_generated diff --git a/shared/midi_mapper_util.h b/shared/midi_mapper_util.h index 072494e..c9c96bb 100644 --- a/shared/midi_mapper_util.h +++ b/shared/midi_mapper_util.h @@ -59,18 +59,26 @@ inline bool match_bank_helper(const Proto &msg, int bank_field_number, int bank) return reflection->GetInt32(msg, bank_descriptor) == bank; } -// Find what MIDI note the given light (as given by field_number) is mapped to, and enable it. template -void activate_mapped_light(const Proto &msg, int field_number, std::map *active_lights) +inline MIDILightProto get_light_mapping_helper(const Proto &msg, int field_number) { using namespace google::protobuf; const FieldDescriptor *descriptor = msg.GetDescriptor()->FindFieldByNumber(field_number); const Reflection *reflection = msg.GetReflection(); if (!reflection->HasField(msg, descriptor)) { + return MIDILightProto(); + } + return static_cast(reflection->GetMessage(msg, descriptor)); +} + +// Find what MIDI note the given light (as given by field_number) is mapped to, and enable it. +template +void activate_mapped_light(const Proto &msg, int field_number, std::map *active_lights) +{ + MIDILightProto light_proto = get_light_mapping_helper(msg, field_number); + if (!light_proto.has_note_number()) { return; } - const MIDILightProto &light_proto = - static_cast(reflection->GetMessage(msg, descriptor)); active_lights->emplace(MIDIDevice::LightKey{MIDIDevice::LightKey::NOTE, unsigned(light_proto.note_number())}, light_proto.velocity()); } -- 2.39.2