]> git.sesse.net Git - nageru/commitdiff
Add an editor for the MIDI mappings.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 15 Oct 2016 22:45:29 +0000 (00:45 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 22 Oct 2016 13:16:23 +0000 (15:16 +0200)
Some of the buttons (save, load, guess) are inert, and it doesn't
listen to MIDI events yet.

.gitignore
Makefile
mainwindow.cpp
mainwindow.h
midi_mapper.cpp
midi_mapper.h
midi_mapping_dialog.cpp [new file with mode: 0644]
midi_mapping_dialog.h [new file with mode: 0644]
ui_mainwindow.ui
ui_midi_mapping.ui [new file with mode: 0644]

index 8724f1cb1b84bf9ddd7fe85e06acd2255d871b0f..a64e22d09a46be0cdf5e1fe0d4bda853849a8fce 100644 (file)
@@ -9,5 +9,6 @@ ui_audio_miniview.h
 ui_display.h
 ui_input_mapping.h
 ui_mainwindow.h
+ui_midi_mapping.h
 nageru
 benchmark_audio_mixer
index af09d6113ea9cfb244b9a4445edd08753dcdd840..0e317aa95710d10f9cd5582f9fa1b4eb514bb758 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -14,8 +14,8 @@ endif
 LDLIBS=$(shell pkg-config --libs $(PKG_MODULES)) -pthread -lva -lva-drm -lva-x11 -lX11 -lavformat -lavcodec -lavutil -lswscale -lavresample -lzita-resampler -lasound -ldl
 
 # Qt objects
-OBJS=glwidget.o main.o mainwindow.o vumeter.o lrameter.o vu_common.o correlation_meter.o aboutdialog.o input_mapping_dialog.o nonlinear_fader.o
-OBJS += glwidget.moc.o mainwindow.moc.o vumeter.moc.o lrameter.moc.o correlation_meter.moc.o aboutdialog.moc.o ellipsis_label.moc.o input_mapping_dialog.moc.o nonlinear_fader.moc.o clickable_label.moc.o
+OBJS=glwidget.o main.o mainwindow.o vumeter.o lrameter.o vu_common.o correlation_meter.o aboutdialog.o input_mapping_dialog.o midi_mapping_dialog.o nonlinear_fader.o
+OBJS += glwidget.moc.o mainwindow.moc.o vumeter.moc.o lrameter.moc.o correlation_meter.moc.o aboutdialog.moc.o ellipsis_label.moc.o input_mapping_dialog.moc.o midi_mapping_dialog.moc.o nonlinear_fader.moc.o clickable_label.moc.o
 OBJS += midi_mapper.o midi_mapping.pb.o
 
 # Mixer objects
@@ -56,7 +56,7 @@ nageru: $(OBJS)
 benchmark_audio_mixer: $(BM_OBJS)
        $(CXX) -o $@ $^ $(LDFLAGS) $(LDLIBS)
 
-mainwindow.o: mainwindow.cpp ui_mainwindow.h ui_display.h ui_audio_miniview.h ui_audio_expanded_view.h
+mainwindow.o: mainwindow.cpp ui_mainwindow.h ui_display.h ui_audio_miniview.h ui_audio_expanded_view.h ui_midi_mapping.h
 
 aboutdialog.o: aboutdialog.cpp ui_aboutdialog.h
 
index 17209408d4668d6eb07450b83bbbcfe8c6aea2eb..f312705dac51839739fe206033b26a81373dd40a 100644 (file)
@@ -26,6 +26,7 @@
 #include "input_mapping_dialog.h"
 #include "lrameter.h"
 #include "midi_mapping.pb.h"
+#include "midi_mapping_dialog.h"
 #include "mixer.h"
 #include "post_to_main_thread.h"
 #include "ui_audio_miniview.h"
@@ -169,6 +170,7 @@ MainWindow::MainWindow()
        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);
+       connect(ui->midi_mapping_action, &QAction::triggered, this, &MainWindow::midi_mapping_triggered);
 
        if (global_flags.x264_video_to_http) {
                connect(ui->x264_bitrate_action, &QAction::triggered, this, &MainWindow::x264_bitrate_triggered);
@@ -342,6 +344,7 @@ void MainWindow::reset_audio_mapping_ui()
        ui->simple_audio_mode->setChecked(simple);
        ui->multichannel_audio_mode->setChecked(!simple);
        ui->input_mapping_action->setEnabled(!simple);
+       ui->midi_mapping_action->setEnabled(!simple);
 
        ui->locut_enabled->setVisible(simple);
        ui->gainstaging_label->setVisible(simple);
@@ -563,6 +566,11 @@ void MainWindow::input_mapping_triggered()
        }
 }
 
+void MainWindow::midi_mapping_triggered()
+{
+       MIDIMappingDialog(&midi_mapper).exec();
+}
+
 void MainWindow::gain_staging_knob_changed(unsigned bus_index, int value)
 {
        if (bus_index == 0) {
index 307b77e80b46f36d4d73d79acf7d6bb2a8c056a4..35e788e131e7f5d6ecb133a15fe257a8f8e34613 100644 (file)
@@ -46,6 +46,7 @@ public slots:
        void simple_audio_mode_triggered();
        void multichannel_audio_mode_triggered();
        void input_mapping_triggered();
+       void midi_mapping_triggered();
        void transition_clicked(int transition_number);
        void channel_clicked(int channel_number);
        void wb_button_clicked(int channel_number);
index dc5eab0342477668049263a44d50b579962a8267..27e843ee33e7b2f2cfb02a3a6ed68656e8884d53 100644 (file)
@@ -66,6 +66,7 @@ bool load_midi_mapping_from_file(const string &filename, MIDIMappingProto *new_m
 
 void MIDIMapper::set_midi_mapping(const MIDIMappingProto &new_mapping)
 {
+       lock_guard<mutex> lock(mapping_mu);
        if (mapping_proto) {
                mapping_proto->CopyFrom(new_mapping);
        } else {
@@ -81,6 +82,12 @@ void MIDIMapper::start_thread()
        midi_thread = thread(&MIDIMapper::thread_func, this);
 }
 
+const MIDIMappingProto &MIDIMapper::get_current_mapping() const
+{
+       lock_guard<mutex> lock(mapping_mu);
+       return *mapping_proto;
+}
+
 #define RETURN_ON_ERROR(msg, expr) do {                            \
        int err = (expr);                                          \
        if (err < 0) {                                             \
@@ -171,6 +178,7 @@ void MIDIMapper::thread_func()
 
 void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event)
 {
+       lock_guard<mutex> lock(mapping_mu);
        switch (event->type) {
        case SND_SEQ_EVENT_CONTROLLER: {
                printf("Controller %d changed to %d\n", event->data.control.param, event->data.control.value);
index 2b59b9ff53428cb1a02656049313014ee04548b4..84baf1f98d707760051e0142bf35f067b7a2cce0 100644 (file)
@@ -11,6 +11,7 @@
 #include <atomic>
 #include <functional>
 #include <memory>
+#include <mutex>
 #include <string>
 #include <thread>
 
@@ -46,7 +47,7 @@ public:
        virtual ~MIDIMapper();
        void set_midi_mapping(const MIDIMappingProto &new_mapping);
        void start_thread();
-       const MIDIMappingProto &get_current_mapping() const { return *mapping_proto; }
+       const MIDIMappingProto &get_current_mapping() const;
 
 private:
        void thread_func();
@@ -60,9 +61,10 @@ private:
        std::atomic<bool> should_quit{false};
        int should_quit_fd;
 
-       std::unique_ptr<MIDIMappingProto> mapping_proto;
-       int num_controller_banks;
-       int current_controller_bank = 0;
+       mutable std::mutex mapping_mu;
+       std::unique_ptr<MIDIMappingProto> mapping_proto;  // Under <mapping_mu>.
+       int num_controller_banks;  // Under <mapping_mu>.
+       std::atomic<int> current_controller_bank{0};
 
        std::thread midi_thread;
 };
diff --git a/midi_mapping_dialog.cpp b/midi_mapping_dialog.cpp
new file mode 100644 (file)
index 0000000..cd622d7
--- /dev/null
@@ -0,0 +1,256 @@
+#include "midi_mapping_dialog.h"
+
+#include "midi_mapper.h"
+#include "midi_mapping.pb.h"
+#include "post_to_main_thread.h"
+#include "ui_midi_mapping.h"
+
+#include <QComboBox>
+#include <QFileDialog>
+#include <QMessageBox>
+#include <QSpinBox>
+
+#include <string>
+
+using namespace google::protobuf;
+using namespace std;
+
+vector<MIDIMappingDialog::Control> per_bus_controllers = {
+       { "Treble",                   MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
+       { "Mid",                      MIDIMappingBusProto::kMidFieldNumber,    MIDIMappingProto::kMidBankFieldNumber },
+       { "Bass",                     MIDIMappingBusProto::kBassFieldNumber,   MIDIMappingProto::kBassBankFieldNumber },
+       { "Gain",                     MIDIMappingBusProto::kGainFieldNumber,   MIDIMappingProto::kGainBankFieldNumber },
+       { "Compressor threshold",     MIDIMappingBusProto::kCompressorThresholdFieldNumber,
+                                     MIDIMappingProto::kCompressorThresholdBankFieldNumber},
+       { "Fader",                    MIDIMappingBusProto::kFaderFieldNumber,  MIDIMappingProto::kFaderBankFieldNumber }
+};
+vector<MIDIMappingDialog::Control> per_bus_buttons = {
+       { "Toggle locut",             MIDIMappingBusProto::kToggleLocutFieldNumber,
+                                     MIDIMappingProto::kToggleLocutBankFieldNumber },
+       { "Togle auto gain staging",  MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
+                                     MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
+       { "Togle compressor",         MIDIMappingBusProto::kToggleCompressorFieldNumber,
+                                     MIDIMappingProto::kToggleCompressorBankFieldNumber },
+       { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
+                                     MIDIMappingProto::kClearPeakBankFieldNumber }
+};
+vector<MIDIMappingDialog::Control> global_controllers = {
+       { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
+       { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
+                                     MIDIMappingProto::kLimiterThresholdBankFieldNumber },
+       { "Makeup gain",              MIDIMappingBusProto::kMakeupGainFieldNumber,
+                                     MIDIMappingProto::kMakeupGainBankFieldNumber }
+};
+vector<MIDIMappingDialog::Control> global_buttons = {
+       { "Previous bank",            MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
+       { "Next bank",                MIDIMappingBusProto::kNextBankFieldNumber, 0 },
+       { "Select bank 1",            MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
+       { "Select bank 2",            MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
+       { "Select bank 3",            MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
+       { "Select bank 4",            MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
+       { "Select bank 5",            MIDIMappingBusProto::kSelectBank5FieldNumber, 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);
+}
+
+int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
+{
+       if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
+               return default_value;
+       }
+
+       const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
+       const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
+       const Reflection *bus_reflection = bus_mapping.GetReflection();
+       if (!bus_reflection->HasField(bus_mapping, descriptor)) {
+               return default_value;
+       }
+       const MIDIControllerProto &controller_proto = 
+               static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+       return controller_proto.controller_number();
+}
+
+int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
+{
+       if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
+               return default_value;
+       }
+
+       const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
+       const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
+       const Reflection *bus_reflection = bus_mapping.GetReflection();
+       if (!bus_reflection->HasField(bus_mapping, descriptor)) {
+               return default_value;
+       }
+       const MIDIButtonProto &bus_proto = 
+               static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+       return bus_proto.note_number();
+}
+
+}  // 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.
+
+       QStringList labels;
+       labels << "";
+       labels << "Controller bank";
+       for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
+               char buf[256];
+               snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
+               labels << buf;
+       }
+       labels << "";
+       ui->treeWidget->setColumnCount(num_buses + 3);
+       ui->treeWidget->setHeaderLabels(labels);
+
+       add_controls("Per-bus controllers", ControlType::CONTROLLER, mapping_proto, per_bus_controllers);
+       add_controls("Per-bus buttons", ControlType::BUTTON, mapping_proto, per_bus_buttons);
+       add_controls("Global controllers", ControlType::CONTROLLER, mapping_proto, global_controllers);
+       add_controls("Global buttons", ControlType::BUTTON, mapping_proto, global_buttons);
+
+       // Auto-resize every column but the last.
+       for (unsigned column_idx = 0; column_idx < num_buses + 3; ++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);
+}
+
+MIDIMappingDialog::~MIDIMappingDialog()
+{
+}
+
+void MIDIMappingDialog::ok_clicked()
+{
+       unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
+       mapper->set_midi_mapping(*new_mapping);
+       accept();
+}
+
+void MIDIMappingDialog::cancel_clicked()
+{
+       reject();
+}
+
+namespace {
+
+template<class T>
+T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
+{
+       while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
+               mapping_proto->add_bus_mapping();
+       }
+
+       MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
+       const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
+       const Reflection *bus_reflection = bus_mapping->GetReflection();
+       return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
+}
+
+}  // namespace
+
+unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
+{
+       unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
+       for (const InstantiatedSpinner &is : controller_spinners) {
+               const int val = is.spinner->value();
+               if (val == 0) {
+                       continue;
+               }
+
+               MIDIControllerProto *controller_proto =
+                       get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
+               controller_proto->set_controller_number(val);
+       }
+       for (const InstantiatedSpinner &is : button_spinners) {
+               const int val = is.spinner->value();
+               if (val == 0) {
+                       continue;
+               }
+
+               MIDIButtonProto *button_proto =
+                       get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
+               button_proto->set_note_number(val);
+       }
+       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);
+       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_selector->setCurrentIndex(get_bank(mapping_proto, bank_field_number, -1) + 1);
+
+       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<MIDIMappingDialog::Control> &controls)
+{
+       QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
+       heading_item->setText(0, QString::fromStdString(heading));
+       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 + "   "));
+
+               for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
+                       QSpinBox *spinner = new QSpinBox(this);
+                       spinner->setRange(0, 127);
+                       spinner->setAutoFillBackground(true);
+                       spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
+                       ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
+
+                       if (control_type == ControlType::CONTROLLER) {
+                               spinner->setValue(get_controller_mapping(mapping_proto, bus_idx, control.field_number, 0));
+                               controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
+                       } else {
+                               assert(control_type == ControlType::BUTTON);
+                               spinner->setValue(get_button_mapping(mapping_proto, bus_idx, control.field_number, 0));
+                               button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
+                       }
+               }
+       }
+       ui->treeWidget->addTopLevelItem(heading_item);
+}
diff --git a/midi_mapping_dialog.h b/midi_mapping_dialog.h
new file mode 100644 (file)
index 0000000..17cdc18
--- /dev/null
@@ -0,0 +1,72 @@
+#ifndef _MIDI_MAPPING_DIALOG_H
+#define _MIDI_MAPPING_DIALOG_H
+
+#include <QDialog>
+#include <string>
+#include <vector>
+#include <sys/time.h>
+
+#include "audio_mixer.h"
+#include "mixer.h"
+
+namespace Ui {
+class MIDIMappingDialog;
+}  // namespace Ui
+
+class MIDIMapper;
+class MIDIMappingProto;
+class QComboBox;
+class QSpinBox;
+class QTreeWidgetItem;
+
+class MIDIMappingDialog : public QDialog
+{
+       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.
+       };
+
+public slots:
+       void ok_clicked();
+       void cancel_clicked();
+
+private:
+       static constexpr unsigned num_buses = 8;
+
+       void add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number);
+       
+       enum class ControlType { CONTROLLER, BUTTON };
+       void add_controls(const std::string &heading, ControlType control_type,
+                         const MIDIMappingProto &mapping_proto, const std::vector<Control> &controls);
+
+       std::unique_ptr<MIDIMappingProto> construct_mapping_proto_from_ui();
+
+
+       Ui::MIDIMappingDialog *ui;
+       MIDIMapper *mapper;
+
+       // All controllers actually laid out on the grid (we need to store them
+       // so that we can read its values back into the new protobuf).
+       struct InstantiatedSpinner {
+               QSpinBox *spinner;
+               unsigned bus_idx;
+               int field_number;  // In MIDIMappingBusProto.
+       };
+       struct InstantiatedComboBox {
+               QComboBox *combo_box;
+               int field_number;  // In MIDIMappingProto.
+       };
+       std::vector<InstantiatedSpinner> controller_spinners;
+       std::vector<InstantiatedSpinner> button_spinners;
+       std::vector<InstantiatedComboBox> bank_combo_boxes;
+};
+
+#endif  // !defined(_MIDI_MAPPING_DIALOG_H)
index 67bf75cee31f517d60258f381a0ae47adc4e73b8..8fb46ff966f0f2dfa6d567edecabfb6490818470 100644 (file)
     <addaction name="multichannel_audio_mode"/>
     <addaction name="separator"/>
     <addaction name="input_mapping_action"/>
+    <addaction name="midi_mapping_action"/>
    </widget>
    <addaction name="menuWhat"/>
    <addaction name="menu_Audio"/>
     <string>Multichannel</string>
    </property>
   </action>
+  <action name="midi_mapping_action">
+   <property name="text">
+    <string>Setup MIDI controller…</string>
+   </property>
+  </action>
  </widget>
  <layoutdefault spacing="6" margin="11"/>
  <customwidgets>
diff --git a/ui_midi_mapping.ui b/ui_midi_mapping.ui
new file mode 100644 (file)
index 0000000..9e4c6ec
--- /dev/null
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MIDIMappingDialog</class>
+ <widget class="QDialog" name="MIDIMappingDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>879</width>
+    <height>583</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Input mapping</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTreeWidget" name="treeWidget">
+     <column>
+      <property name="text">
+       <string notr="true">1</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Add or change a mapping by clicking in the cell, then moving the corresponding control on your MIDI device.</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1,0,0,1,0">
+     <item>
+      <widget class="QPushButton" name="guess_button">
+       <property name="text">
+        <string>Guess &amp;bus</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="save_button">
+       <property name="text">
+        <string>&amp;Save…</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="load_button">
+       <property name="text">
+        <string>&amp;Load…</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QDialogButtonBox" name="ok_cancel_buttons">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="standardButtons">
+        <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>