From: Steinar H. Gunderson Date: Sun, 9 Oct 2016 10:19:23 +0000 (+0200) Subject: Add support for controlling the audio using a MIDI controller. X-Git-Tag: 1.4.0~35 X-Git-Url: https://git.sesse.net/?p=nageru;a=commitdiff_plain;h=937b6ab9e4c5d13fe7f7bdcb5a17bf48929f44ce Add support for controlling the audio using a MIDI controller. 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. --- diff --git a/Makefile b/Makefile index 65956ac..af09d61 100644 --- 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 diff --git a/flags.cpp b/flags.cpp index b22d025..f3d9b65 100644 --- 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 5fed2a8..be02f38 100644 --- a/flags.h +++ b/flags.h @@ -38,6 +38,7 @@ struct Flags { std::map 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; diff --git a/mainwindow.cpp b/mainwindow.cpp index 9e4ad0d..1720940 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -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 +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 +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 +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 transition_names) { if (transition_names.size() < 1 || transition_names[0].empty()) { diff --git a/mainwindow.h b/mainwindow.h index 77742bf..307b77e 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -7,9 +7,11 @@ #include #include +#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 + void set_relative_value(T *control, float value); + + template + void set_relative_value_if_exists(unsigned bus_idx, T *Ui_AudioExpandedView::*control, float value); + + template + 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 audio_miniviews; std::vector 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 index 0000000..32fcc10 --- /dev/null +++ b/midi_mapper.cpp @@ -0,0 +1,269 @@ +#include "midi_mapper.h" +#include "midi_mapping.pb.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +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 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 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(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 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(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 index 0000000..5b8b98e --- /dev/null +++ b/midi_mapper.h @@ -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 +#include +#include +#include +#include + +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 func); + void match_button(int note, int field_number, int bank_field_number, std::function func); + bool bank_mismatch(int bank_field_number); + + ControllerReceiver *receiver; + std::atomic should_quit{false}; + int should_quit_fd; + + std::unique_ptr 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 index 0000000..4647a85 --- /dev/null +++ b/midi_mapping.proto @@ -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; +} diff --git a/nonlinear_fader.cpp b/nonlinear_fader.cpp index b548c29..61317e0 100644 --- a/nonlinear_fader.cpp +++ b/nonlinear_fader.cpp @@ -17,8 +17,11 @@ namespace { vector> 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 },