]> git.sesse.net Git - nageru/commitdiff
Add support for controlling the audio using a MIDI controller.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 9 Oct 2016 10:19:23 +0000 (12:19 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 19 Oct 2016 22:55:44 +0000 (00:55 +0200)
This allows the user to control the audio using a dedicated controller,
as opposed to the mouse, which generally is a more direct and intuitive
user interface.

This is rather raw at the moment (e.g., hardcoded MIDI address, no real UI
indications of bank switches, and no UI for editing the mapping), but the
required basics are generally in place.

Makefile
flags.cpp
flags.h
mainwindow.cpp
mainwindow.h
midi_mapper.cpp [new file with mode: 0644]
midi_mapper.h [new file with mode: 0644]
midi_mapping.proto [new file with mode: 0644]
nonlinear_fader.cpp

index 65956ac84093e94b948caf1d8c06e55555fc9a15..af09d6113ea9cfb244b9a4445edd08753dcdd840 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,7 @@ LDLIBS=$(shell pkg-config --libs $(PKG_MODULES)) -pthread -lva -lva-drm -lva-x11
 # 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 += midi_mapper.o midi_mapping.pb.o
 
 # Mixer objects
 AUDIO_MIXER_OBJS = audio_mixer.o alsa_input.o alsa_pool.o ebu_r128_proc.o stereocompressor.o resampling_queue.o flags.o correlation_measurer.o filter.o input_mapping.o state.pb.o
index b22d0254c720e04c21cc6595c312fc50c1418005..f3d9b658d286c125d057a1384cec6c1aff5538ad 100644 (file)
--- a/flags.cpp
+++ b/flags.cpp
@@ -14,6 +14,7 @@ Flags global_flags;
 // Long options that have no corresponding short option.
 enum LongOption {
        OPTION_MULTICHANNEL = 1000,
+       OPTION_MIDI_MAPPING,
        OPTION_FAKE_CARDS_AUDIO,
        OPTION_HTTP_UNCOMPRESSED_VIDEO,
        OPTION_HTTP_X264_VIDEO,
@@ -58,6 +59,7 @@ void usage()
        fprintf(stderr, "  -m, --map-signal=SIGNAL,CARD    set a default card mapping (can be given multiple times)\n");
        fprintf(stderr, "  -M, --input-mapping=FILE        start with the given audio input mapping (implies --multichannel)\n");
        fprintf(stderr, "      --multichannel              start in multichannel audio mapping mode\n");
+       fprintf(stderr, "      --midi-mapping=FILE         start with the given MIDI controller mapping (implies --multichannel)\n");
        fprintf(stderr, "      --fake-cards-audio          make fake (disconnected) cards output a simple tone\n");
        fprintf(stderr, "      --http-uncompressed-video   send uncompressed NV12 video to HTTP clients\n");
        fprintf(stderr, "      --http-x264-video           send x264-compressed video to HTTP clients\n");
@@ -106,6 +108,7 @@ void parse_flags(int argc, char * const argv[])
                { "input-mapping", required_argument, 0, 'M' },
                { "va-display", required_argument, 0, 'v' },
                { "multichannel", no_argument, 0, OPTION_MULTICHANNEL },
+               { "midi-mapping", required_argument, 0, OPTION_MIDI_MAPPING },
                { "fake-cards-audio", no_argument, 0, OPTION_FAKE_CARDS_AUDIO },
                { "http-uncompressed-video", no_argument, 0, OPTION_HTTP_UNCOMPRESSED_VIDEO },
                { "http-x264-video", no_argument, 0, OPTION_HTTP_X264_VIDEO },
@@ -181,6 +184,10 @@ void parse_flags(int argc, char * const argv[])
                case 'v':
                        global_flags.va_display = optarg;
                        break;
+               case OPTION_MIDI_MAPPING:
+                       global_flags.midi_mapping_filename = optarg;
+                       global_flags.multichannel_mapping_mode = true;
+                       break;
                case OPTION_FAKE_CARDS_AUDIO:
                        global_flags.fake_cards_audio = true;
                        break;
diff --git a/flags.h b/flags.h
index 5fed2a88517a5effd5da13eb49a111dda9d2d7e0..be02f388efd5dd5ce6868babf5495a46ff41e31b 100644 (file)
--- a/flags.h
+++ b/flags.h
@@ -38,6 +38,7 @@ struct Flags {
        std::map<int, int> default_stream_mapping;
        bool multichannel_mapping_mode = false;  // Implicitly true if input_mapping_filename is nonempty.
        std::string input_mapping_filename;  // Empty for none.
+       std::string midi_mapping_filename;  // Empty for none.
 };
 extern Flags global_flags;
 
index 9e4ad0d9e5dc4b6fa48036d6ea848bb51603145d..17209408d4668d6eb07450b83bbbcfe8c6aea2eb 100644 (file)
@@ -25,6 +25,7 @@
 #include "glwidget.h"
 #include "input_mapping_dialog.h"
 #include "lrameter.h"
+#include "midi_mapping.pb.h"
 #include "mixer.h"
 #include "post_to_main_thread.h"
 #include "ui_audio_miniview.h"
@@ -144,7 +145,7 @@ void set_peak_label(QLabel *peak_label, float peak_db)
 }  // namespace
 
 MainWindow::MainWindow()
-       : ui(new Ui::MainWindow)
+       : ui(new Ui::MainWindow), midi_mapper(this)
 {
        global_mainwindow = this;
        ui->setupUi(this);
@@ -206,6 +207,17 @@ MainWindow::MainWindow()
        connect(new QShortcut(QKeySequence::MoveToPreviousPage, this), &QShortcut::activated, switch_page);
 
        last_audio_level_callback = steady_clock::now() - seconds(1);
+
+       if (!global_flags.midi_mapping_filename.empty()) {
+               MIDIMappingProto midi_mapping;
+               if (!load_midi_mapping_from_file(global_flags.midi_mapping_filename, &midi_mapping)) {
+                       fprintf(stderr, "Couldn't load MIDI mapping '%s'; exiting.\n",
+                               global_flags.midi_mapping_filename.c_str());
+                       exit(1);
+               }
+               midi_mapper.set_midi_mapping(midi_mapping);
+       }
+       midi_mapper.start_thread();
 }
 
 void MainWindow::resizeEvent(QResizeEvent* event)
@@ -821,6 +833,99 @@ void MainWindow::relayout()
        ui->preview_displays->setStretch(previews.size(), lrintf(remaining_preview_width));
 }
 
+void MainWindow::set_locut(float value)
+{
+       set_relative_value(ui->locut_cutoff_knob, value);
+}
+
+void MainWindow::set_limiter_threshold(float value)
+{
+       set_relative_value(ui->limiter_threshold_knob, value);
+}
+
+void MainWindow::set_makeup_gain(float value)
+{
+       set_relative_value(ui->makeup_gain_knob, value);
+}
+
+void MainWindow::set_treble(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::treble_knob, value);
+}
+
+void MainWindow::set_mid(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::mid_knob, value);
+}
+
+void MainWindow::set_bass(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::bass_knob, value);
+}
+
+void MainWindow::set_gain(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::gainstaging_knob, value);
+}
+
+void MainWindow::set_compressor_threshold(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::compressor_threshold_knob, value);
+}
+
+void MainWindow::set_fader(unsigned bus_idx, float value)
+{
+       set_relative_value_if_exists(bus_idx, &Ui::AudioExpandedView::fader, value);
+}
+
+void MainWindow::toggle_locut(unsigned bus_idx)
+{
+       click_button_if_exists(bus_idx, &Ui::AudioExpandedView::locut_enabled);
+}
+
+void MainWindow::toggle_auto_gain_staging(unsigned bus_idx)
+{
+       click_button_if_exists(bus_idx, &Ui::AudioExpandedView::gainstaging_auto_checkbox);
+}
+
+void MainWindow::toggle_compressor(unsigned bus_idx)
+{
+       click_button_if_exists(bus_idx, &Ui::AudioExpandedView::compressor_enabled);
+}
+
+void MainWindow::clear_peak(unsigned bus_idx)
+{
+       if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL) {
+               global_audio_mixer->reset_peak(bus_idx);
+       }
+}
+
+template<class T>
+void MainWindow::set_relative_value(T *control, float value)
+{
+       post_to_main_thread([control, value]{
+               control->setValue(lrintf(control->minimum() + value * (control->maximum() - control->minimum())));
+       });
+}
+
+template<class T>
+void MainWindow::set_relative_value_if_exists(unsigned bus_idx, T *(Ui_AudioExpandedView::*control), float value)
+{
+       if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL &&
+           bus_idx < audio_expanded_views.size()) {
+               set_relative_value(audio_expanded_views[bus_idx]->*control, value);
+       }
+}
+
+template<class T>
+void MainWindow::click_button_if_exists(unsigned bus_idx, T *(Ui_AudioExpandedView::*control))
+{
+       if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL &&
+           bus_idx < audio_expanded_views.size()) {
+               (audio_expanded_views[bus_idx]->*control)->click();
+       }
+}
+
 void MainWindow::set_transition_names(vector<string> transition_names)
 {
        if (transition_names.size() < 1 || transition_names[0].empty()) {
index 77742bf7926ffc86e4b0c437c2279dd09f11e654..307b77e80b46f36d4d73d79acf7d6bb2a8c056a4 100644 (file)
@@ -7,9 +7,11 @@
 #include <vector>
 #include <sys/time.h>
 
+#include "midi_mapper.h"
 #include "mixer.h"
 
 class GLWidget;
+class Ui_AudioExpandedView;
 class QResizeEvent;
 
 namespace Ui {
@@ -19,10 +21,11 @@ class Display;
 class MainWindow;
 }  // namespace Ui
 
+class QDial;
 class QLabel;
 class QPushButton;
 
-class MainWindow : public QMainWindow
+class MainWindow : public QMainWindow, public ControllerReceiver
 {
        Q_OBJECT
 
@@ -59,6 +62,23 @@ public slots:
        void reset_meters_button_clicked();
        void relayout();
 
+       // ControllerReceiver interface.
+       void set_locut(float value) override;
+       void set_limiter_threshold(float value) override;
+       void set_makeup_gain(float value) override;
+
+       void set_treble(unsigned bus_idx, float value) override;
+       void set_mid(unsigned bus_idx, float value) override;
+       void set_bass(unsigned bus_idx, float value) override;
+       void set_gain(unsigned bus_idx, float value) override;
+       void set_compressor_threshold(unsigned bus_idx, float value) override;
+       void set_fader(unsigned bus_idx, float value) override;
+
+       void toggle_locut(unsigned bus_idx) override;
+       void toggle_auto_gain_staging(unsigned bus_idx) override;
+       void toggle_compressor(unsigned bus_idx) override;
+       void clear_peak(unsigned bus_idx) override;
+
 private:
        void reset_audio_mapping_ui();
        void setup_audio_miniview();
@@ -77,6 +97,15 @@ private:
 
        void audio_state_changed();
 
+       template<class T>
+       void set_relative_value(T *control, float value);
+
+       template<class T>
+       void set_relative_value_if_exists(unsigned bus_idx, T *Ui_AudioExpandedView::*control, float value);
+
+       template<class T>
+       void click_button_if_exists(unsigned bus_idx, T *Ui_AudioExpandedView::*control);
+
        Ui::MainWindow *ui;
        QLabel *disk_free_label;
        QPushButton *transition_btn1, *transition_btn2, *transition_btn3;
@@ -84,6 +113,7 @@ private:
        std::vector<Ui::AudioMiniView *> audio_miniviews;
        std::vector<Ui::AudioExpandedView *> audio_expanded_views;
        int current_wb_pick_display = -1;
+       MIDIMapper midi_mapper;
 };
 
 extern MainWindow *global_mainwindow;
diff --git a/midi_mapper.cpp b/midi_mapper.cpp
new file mode 100644 (file)
index 0000000..32fcc10
--- /dev/null
@@ -0,0 +1,269 @@
+#include "midi_mapper.h"
+#include "midi_mapping.pb.h"
+
+#include <alsa/asoundlib.h>
+#include <google/protobuf/text_format.h>
+#include <google/protobuf/io/zero_copy_stream.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <fcntl.h>
+#include <sys/eventfd.h>
+
+#include <functional>
+#include <thread>
+
+using namespace google::protobuf;
+using namespace std;
+using namespace std::placeholders;
+
+namespace {
+
+double map_controller_to_float(int val)
+{
+       // Slightly hackish mapping so that we can represent exactly 0.0, 0.5 and 1.0.
+       if (val <= 0) {
+               return 0.0;
+       } else if (val >= 127) {
+               return 1.0;
+       } else {
+               return (val + 0.5) / 127.0;
+       }
+}
+
+}  // namespace
+
+MIDIMapper::MIDIMapper(ControllerReceiver *receiver)
+       : receiver(receiver), mapping_proto(new MIDIMappingProto)
+{
+       should_quit_fd = eventfd(/*initval=*/0, /*flags=*/0);
+       assert(should_quit_fd != -1);
+}
+
+MIDIMapper::~MIDIMapper()
+{
+       should_quit = true;
+       const uint64_t one = 1;
+       write(should_quit_fd, &one, sizeof(one));
+       midi_thread.join();
+       close(should_quit_fd);
+}
+
+bool load_midi_mapping_from_file(const string &filename, MIDIMappingProto *new_mapping)
+{
+       // Read and parse the protobuf from disk.
+       int fd = open(filename.c_str(), O_RDONLY);
+       if (fd == -1) {
+               perror(filename.c_str());
+               return false;
+       }
+       io::FileInputStream input(fd);  // Takes ownership of fd.
+       if (!TextFormat::Parse(&input, new_mapping)) {
+               input.Close();
+               return false;
+       }
+       input.Close();
+       return true;
+}
+
+void MIDIMapper::set_midi_mapping(const MIDIMappingProto &new_mapping)
+{
+       if (mapping_proto) {
+               mapping_proto->CopyFrom(new_mapping);
+       } else {
+               mapping_proto.reset(new MIDIMappingProto(new_mapping));
+       }
+
+       num_controller_banks = min(max(mapping_proto->num_controller_banks(), 1), 5);
+        current_controller_bank = 0;
+}
+
+void MIDIMapper::start_thread()
+{
+       midi_thread = thread(&MIDIMapper::thread_func, this);
+}
+
+#define RETURN_ON_ERROR(msg, expr) do {                            \
+       int err = (expr);                                          \
+       if (err < 0) {                                             \
+               fprintf(stderr, msg ": %s\n", snd_strerror(err));  \
+               return;                                            \
+       }                                                          \
+} while (false)
+
+
+void MIDIMapper::thread_func()
+{
+       // TODO: Listen on any port, instead of hardcoding 24:0.
+       snd_seq_t *seq;
+       int err;
+
+       RETURN_ON_ERROR("snd_seq_open", snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0));
+       RETURN_ON_ERROR("snd_seq_client_name", snd_seq_set_client_name(seq, "nageru"));
+       RETURN_ON_ERROR("snd_seq_create_simple_port",
+               snd_seq_create_simple_port(seq, "nageru",
+                       SND_SEQ_PORT_CAP_WRITE |
+                       SND_SEQ_PORT_CAP_SUBS_WRITE,
+                       SND_SEQ_PORT_TYPE_MIDI_GENERIC |
+                       SND_SEQ_PORT_TYPE_APPLICATION));
+
+       snd_seq_addr_t addr;
+       RETURN_ON_ERROR("snd_seq_parse_address", snd_seq_parse_address(seq, &addr, "24:0"));
+       RETURN_ON_ERROR("snd_seq_connect_from", snd_seq_connect_from(seq, 0, addr.client, addr.port));
+
+       int num_alsa_fds = snd_seq_poll_descriptors_count(seq, POLLIN);
+       unique_ptr<pollfd[]> fds(new pollfd[num_alsa_fds + 1]);
+
+       while (!should_quit) {
+               snd_seq_poll_descriptors(seq, fds.get(), num_alsa_fds, POLLIN);
+               fds[num_alsa_fds].fd = should_quit_fd;
+               fds[num_alsa_fds].events = POLLIN;
+               fds[num_alsa_fds].revents = 0;
+
+               err = poll(fds.get(), num_alsa_fds + 1, -1);
+               if (err == 0 || (err == -1 && errno == EINTR)) {
+                       continue;
+               }
+               if (err == -1) {
+                       perror("poll");
+                       break;
+               }
+               if (fds[num_alsa_fds].revents) {
+                       // Activity on should_quit_fd.
+                       break;
+               }
+
+               snd_seq_event_t *event;
+               err = snd_seq_event_input(seq, &event);
+               if (event->type == SND_SEQ_EVENT_CONTROLLER) {
+                       printf("Controller %d changed to %d\n", event->data.control.param, event->data.control.value);
+
+                       const int controller = event->data.control.param;
+                       const float value = map_controller_to_float(event->data.control.value);
+
+                       match_controller(controller, MIDIMappingBusProto::kLocutFieldNumber, MIDIMappingProto::kLocutBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_locut, receiver, _2));
+                       match_controller(controller, MIDIMappingBusProto::kLimiterThresholdFieldNumber, MIDIMappingProto::kLimiterThresholdBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_limiter_threshold, receiver, _2));
+                       match_controller(controller, MIDIMappingBusProto::kMakeupGainFieldNumber, MIDIMappingProto::kMakeupGainBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_makeup_gain, receiver, _2));
+
+                       match_controller(controller, MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_treble, receiver, _1, _2));
+                       match_controller(controller, MIDIMappingBusProto::kMidFieldNumber, MIDIMappingProto::kMidBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_mid, receiver, _1, _2));
+                       match_controller(controller, MIDIMappingBusProto::kBassFieldNumber, MIDIMappingProto::kBassBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_bass, receiver, _1, _2));
+                       match_controller(controller, MIDIMappingBusProto::kGainFieldNumber, MIDIMappingProto::kGainBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_gain, receiver, _1, _2));
+                       match_controller(controller, MIDIMappingBusProto::kCompressorThresholdFieldNumber, MIDIMappingProto::kCompressorThresholdBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_compressor_threshold, receiver, _1, _2));
+                       match_controller(controller, MIDIMappingBusProto::kFaderFieldNumber, MIDIMappingProto::kFaderBankFieldNumber,
+                               value, bind(&ControllerReceiver::set_fader, receiver, _1, _2));
+               } else if (event->type == SND_SEQ_EVENT_NOTEON) {
+                       const int note = event->data.note.note;
+
+                       printf("Note: %d\n", note);
+
+                       // Bank change commands. TODO: Highlight the bank change in the UI.
+                       for (size_t bus_idx = 0; bus_idx < size_t(mapping_proto->bus_mapping_size()); ++bus_idx) {
+                               const MIDIMappingBusProto &bus_mapping = mapping_proto->bus_mapping(bus_idx);
+                               if (bus_mapping.has_prev_bank() &&
+                                   bus_mapping.prev_bank().note_number() == note) {
+                                       current_controller_bank = (current_controller_bank + num_controller_banks - 1) % num_controller_banks;
+                               }
+                               if (bus_mapping.has_next_bank() &&
+                                   bus_mapping.next_bank().note_number() == note) {
+                                       current_controller_bank = (current_controller_bank + 1) % num_controller_banks;
+                               }
+                               if (bus_mapping.has_select_bank_1() &&
+                                   bus_mapping.select_bank_1().note_number() == note) {
+                                       current_controller_bank = 0;
+                               }
+                               if (bus_mapping.has_select_bank_2() &&
+                                   bus_mapping.select_bank_2().note_number() == note &&
+                                   num_controller_banks >= 2) {
+                                       current_controller_bank = 1;
+                               }
+                               if (bus_mapping.has_select_bank_3() &&
+                                   bus_mapping.select_bank_3().note_number() == note &&
+                                   num_controller_banks >= 3) {
+                                       current_controller_bank = 2;
+                               }
+                               if (bus_mapping.has_select_bank_4() &&
+                                   bus_mapping.select_bank_4().note_number() == note &&
+                                   num_controller_banks >= 4) {
+                                       current_controller_bank = 3;
+                               }
+                               if (bus_mapping.has_select_bank_5() &&
+                                   bus_mapping.select_bank_5().note_number() == note &&
+                                   num_controller_banks >= 5) {
+                                       current_controller_bank = 4;
+                               }
+                       }
+
+                       match_button(note, MIDIMappingBusProto::kToggleLocutFieldNumber, MIDIMappingProto::kToggleLocutBankFieldNumber,
+                               bind(&ControllerReceiver::toggle_locut, receiver, _1));
+                       match_button(note, MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber, MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber,
+                               bind(&ControllerReceiver::toggle_auto_gain_staging, receiver, _1));
+                       match_button(note, MIDIMappingBusProto::kToggleCompressorFieldNumber, MIDIMappingProto::kToggleCompressorBankFieldNumber,
+                               bind(&ControllerReceiver::toggle_compressor, receiver, _1));
+                       match_button(note, MIDIMappingBusProto::kClearPeakFieldNumber, MIDIMappingProto::kClearPeakBankFieldNumber,
+                               bind(&ControllerReceiver::clear_peak, receiver, _1));
+               } else if (event->type == SND_SEQ_EVENT_NOTEOFF) {
+                       // Ignore.
+               } else {
+                       printf("Ignoring MIDI event of unknown type %d.\n", event->type);
+               }
+       }
+}
+
+void MIDIMapper::match_controller(int controller, int field_number, int bank_field_number, float value, function<void(unsigned, float)> func)
+{
+       if (bank_mismatch(bank_field_number)) {
+               return;
+       }
+
+       for (size_t bus_idx = 0; bus_idx < size_t(mapping_proto->bus_mapping_size()); ++bus_idx) {
+               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)) {
+                       continue;
+               }
+               const MIDIControllerProto &controller_proto =
+                       static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+               if (controller_proto.controller_number() == controller) {
+                       func(bus_idx, value);
+               }
+       }
+}
+
+void MIDIMapper::match_button(int note, int field_number, int bank_field_number, function<void(unsigned)> func)
+{
+       if (bank_mismatch(bank_field_number)) {
+               return;
+       }
+
+       for (size_t bus_idx = 0; bus_idx < size_t(mapping_proto->bus_mapping_size()); ++bus_idx) {
+               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)) {
+                       continue;
+               }
+               const MIDIButtonProto &button_proto =
+                       static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+               if (button_proto.note_number() == note) {
+                       func(bus_idx);
+               }
+       }
+}
+
+bool MIDIMapper::bank_mismatch(int bank_field_number)
+{
+       const FieldDescriptor *bank_descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(bank_field_number);
+       const Reflection *reflection = mapping_proto->GetReflection();
+       return (reflection->HasField(*mapping_proto, bank_descriptor) &&
+               reflection->GetInt32(*mapping_proto, bank_descriptor) != current_controller_bank);
+}
diff --git a/midi_mapper.h b/midi_mapper.h
new file mode 100644 (file)
index 0000000..5b8b98e
--- /dev/null
@@ -0,0 +1,67 @@
+#ifndef _MIDI_MAPPER_H
+#define _MIDI_MAPPER_H 1
+
+// MIDIMapper is a class that listens for incoming MIDI messages from
+// mixer controllers (ie., it is not meant to be used with regular
+// instruments), interprets them according to a device-specific, user-defined
+// mapping, and calls back into a receiver (typically the MainWindow).
+// This way, it is possible to control audio functionality using physical
+// pots and faders instead of the mouse.
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include <string>
+#include <thread>
+
+class MIDIMappingProto;
+
+// Interface for receiving interpreted controller messages.
+class ControllerReceiver {
+public:
+       // All values are [0.0, 1.0].
+       virtual void set_locut(float value) = 0;
+       virtual void set_limiter_threshold(float value) = 0;
+       virtual void set_makeup_gain(float value) = 0;
+
+       virtual void set_treble(unsigned bus_idx, float value) = 0;
+       virtual void set_mid(unsigned bus_idx, float value) = 0;
+       virtual void set_bass(unsigned bus_idx, float value) = 0;
+       virtual void set_gain(unsigned bus_idx, float value) = 0;
+       virtual void set_compressor_threshold(unsigned bus_idx, float value) = 0;
+       virtual void set_fader(unsigned bus_idx, float value) = 0;
+
+       virtual void toggle_locut(unsigned bus_idx) = 0;
+       virtual void toggle_auto_gain_staging(unsigned bus_idx) = 0;
+       virtual void toggle_compressor(unsigned bus_idx) = 0;
+       virtual void clear_peak(unsigned bus_idx) = 0;
+};
+
+class MIDIMapper {
+public:
+       MIDIMapper(ControllerReceiver *receiver);
+       virtual ~MIDIMapper();
+       void set_midi_mapping(const MIDIMappingProto &new_mapping);
+       void start_thread();
+       const MIDIMappingProto &get_current_mapping() const { return *mapping_proto; }
+
+private:
+       void thread_func();
+       void match_controller(int controller, int field_number, int bank_field_number, float value, std::function<void(unsigned, float)> func);
+       void match_button(int note, int field_number, int bank_field_number, std::function<void(unsigned)> func);
+       bool bank_mismatch(int bank_field_number);
+
+       ControllerReceiver *receiver;
+       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;
+
+       std::thread midi_thread;
+};
+
+bool load_midi_mapping_from_file(const std::string &filename, MIDIMappingProto *new_mapping);
+
+#endif  // !defined(_MIDI_MAPPER_H)
diff --git a/midi_mapping.proto b/midi_mapping.proto
new file mode 100644 (file)
index 0000000..4647a85
--- /dev/null
@@ -0,0 +1,91 @@
+// Mappings from MIDI controllers to the UI. (We don't really build
+// a more complicated data structure than this in Nageru itself either;
+// we just edit and match directly against the protobuf.)
+
+syntax = "proto2";
+
+// A single, given controller mapping.
+message MIDIControllerProto {
+       required int32 controller_number = 1;
+       // TODO: Add flags like invert here if/when we need them.
+}
+
+message MIDIButtonProto {
+       required int32 note_number = 1;
+}
+
+// All the mappings for a given a bus.
+message MIDIMappingBusProto {
+       // TODO: If we need support for lots of buses (i.e., more than the typical eight
+       // on a mixer), add a system for bus banks, like we have for controller banks.
+       // optional int32 bus_bank = 1;
+
+       optional MIDIControllerProto treble = 2;
+       optional MIDIControllerProto mid = 3;
+       optional MIDIControllerProto bass = 4;
+       optional MIDIControllerProto gain = 5;
+       optional MIDIControllerProto compressor_threshold = 6;
+       optional MIDIControllerProto fader = 7;
+
+       // TODO: Add mute and cue? (Of course, we should those to the UI before
+       // making them MIDI controllable.)
+       optional MIDIButtonProto toggle_locut = 8;
+       optional MIDIButtonProto toggle_auto_gain_staging = 9;
+       optional MIDIButtonProto toggle_compressor = 10;
+       optional MIDIButtonProto clear_peak = 11;
+
+       // These are really global (controller bank change affects all buss),
+       // but it's not uncommon that we'd want one button per bus to switch banks.
+       // E.g., if the user binds the “mute” button to “next bank”, they'd want every
+       // mute button on the mixer to do that, so they need one mapping per bus.
+       optional MIDIButtonProto prev_bank = 12;
+       optional MIDIButtonProto next_bank = 13;
+       optional MIDIButtonProto select_bank_1 = 14;
+       optional MIDIButtonProto select_bank_2 = 15;
+       optional MIDIButtonProto select_bank_3 = 16;
+       optional MIDIButtonProto select_bank_4 = 17;
+       optional MIDIButtonProto select_bank_5 = 18;
+
+       // These are also global (they belong to the master bus), and unlike
+       // the bank change commands, one would usually have only one of each,
+       // but there's no reason to limit them to one each, and the editor UI
+       // becomes simpler if they are the treated the same way as the bank
+       // commands.
+       optional MIDIControllerProto locut = 19;
+       optional MIDIControllerProto limiter_threshold = 20;
+       optional MIDIControllerProto makeup_gain = 21;
+}
+
+// The top-level protobuf, containing all the bus mappings, as well as
+// more global settings.
+//
+// Since a typical mixer will have fewer physical controls than what Nageru
+// could use, Nageru supports so-called controller banks. A mapping can
+// optionally belong to a bank, and if so, that mapping is only active when
+// that bank is selected. The user can then select the current bank using
+// other mappings, typically by having some mixer button assigned to
+// “next bank”. This yields effective multiplexing of lesser-used controls.
+message MIDIMappingProto {
+       optional int32 num_controller_banks = 1 [default = 0];  // Max 5.
+
+       // Bus controller banks.
+       optional int32 treble_bank = 2;
+       optional int32 mid_bank = 3;
+       optional int32 bass_bank = 4;
+       optional int32 gain_bank = 5;
+       optional int32 compressor_threshold_bank = 6;
+       optional int32 fader_bank = 7;
+
+       // Bus button banks.
+       optional int32 toggle_locut_bank = 8;
+       optional int32 toggle_auto_gain_staging_bank = 9;
+       optional int32 toggle_compressor_bank = 10;
+       optional int32 clear_peak_bank = 11;
+
+       // Global controller banks.
+       optional int32 locut_bank = 12;
+       optional int32 limiter_threshold_bank = 13;
+       optional int32 makeup_gain_bank = 14;
+
+       repeated MIDIMappingBusProto bus_mapping = 15;
+}
index b548c29493206bc6f5a3569ffc657f55f4178550..61317e0acfc0c5c9ba2e871d421e483b6df73b27 100644 (file)
@@ -17,8 +17,11 @@ namespace {
 
 vector<pair<double, double>> fader_control_points = {
        // The main area is from +6 to -12 dB (18 dB), and we use half the slider range for it.
+       // Adjust slightly so that the MIDI controller value of 106 becomes exactly 0.0 dB
+       // (cf. map_controller_to_float()); otherwise, we'd miss ever so slightly, which is
+       // really frustrating.
        { 6.0, 1.0 },
-       { -12.0, 0.5 },
+       { -12.0, 1.0 - (1.0 - 106.5/127.0) * 3.0 },  // About 0.492.
 
        // -12 to -21 is half the range (9 dB). Halve.
        { -21.0, 0.325 },