# 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
// 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,
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");
{ "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 },
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;
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;
#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"
} // namespace
MainWindow::MainWindow()
- : ui(new Ui::MainWindow)
+ : ui(new Ui::MainWindow), midi_mapper(this)
{
global_mainwindow = this;
ui->setupUi(this);
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)
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()) {
#include <vector>
#include <sys/time.h>
+#include "midi_mapper.h"
#include "mixer.h"
class GLWidget;
+class Ui_AudioExpandedView;
class QResizeEvent;
namespace Ui {
class MainWindow;
} // namespace Ui
+class QDial;
class QLabel;
class QPushButton;
-class MainWindow : public QMainWindow
+class MainWindow : public QMainWindow, public ControllerReceiver
{
Q_OBJECT
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();
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;
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;
--- /dev/null
+#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);
+}
--- /dev/null
+#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)
--- /dev/null
+// 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;
+}
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 },