From: Steinar H. Gunderson Date: Wed, 19 Oct 2016 19:41:39 +0000 (+0200) Subject: Add support for feedback lights (LEDs) on MIDI controllers. X-Git-Tag: 1.4.0~23 X-Git-Url: https://git.sesse.net/?p=nageru;a=commitdiff_plain;h=7a81f05c63a992acf4a859fd1136700e1d8b98ac Add support for feedback lights (LEDs) on MIDI controllers. --- diff --git a/audio_mixer.cpp b/audio_mixer.cpp index e4eb495..d0737f2 100644 --- a/audio_mixer.cpp +++ b/audio_mixer.cpp @@ -987,6 +987,12 @@ InputMapping AudioMixer::get_input_mapping() const return input_mapping; } +unsigned AudioMixer::num_buses() const +{ + lock_guard lock(audio_mutex); + return input_mapping.buses.size(); +} + void AudioMixer::reset_peak(unsigned bus_index) { lock_guard lock(audio_mutex); diff --git a/audio_mixer.h b/audio_mixer.h index 71f3662..38b98f7 100644 --- a/audio_mixer.h +++ b/audio_mixer.h @@ -114,6 +114,8 @@ public: MappingMode get_mapping_mode() const; InputMapping get_input_mapping() const; + unsigned num_buses() const; + void set_locut_cutoff(float cutoff_hz) { locut_cutoff_hz = cutoff_hz; diff --git a/mainwindow.cpp b/mainwindow.cpp index 79221e1..ec13cd7 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -46,6 +46,10 @@ Q_DECLARE_METATYPE(std::vector); MainWindow *global_mainwindow = nullptr; +// -0.1 dBFS is EBU peak limit. We use it consistently, even for the bus meters +// (which don't calculate interpolate peak, and in general don't follow EBU recommendations). +constexpr float peak_limit_dbfs = -0.1f; + namespace { void schedule_cut_signal(int ignored) @@ -134,9 +138,7 @@ void set_peak_label(QLabel *peak_label, float peak_db) { peak_label->setText(QString::fromStdString(format_db(peak_db, DB_BARE))); - // -0.1 dBFS is EBU peak limit. We use it consistently, even for the bus meters - // (which don't calculate interpolate peak, and in general don't follow EBU recommendations). - if (peak_db > -0.1f) { + if (peak_db > peak_limit_dbfs) { peak_label->setStyleSheet("QLabel { background-color: red; color: white; }"); } else { peak_label->setStyleSheet(""); @@ -290,16 +292,19 @@ void MainWindow::mixer_created(Mixer *mixer) } connect(ui->locut_enabled, &QCheckBox::stateChanged, [this](int state){ global_audio_mixer->set_locut_enabled(simple_bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui->gainstaging_knob, &QAbstractSlider::valueChanged, bind(&MainWindow::gain_staging_knob_changed, this, simple_bus_index, _1)); connect(ui->gainstaging_auto_checkbox, &QCheckBox::stateChanged, [this, simple_bus_index](int state){ global_audio_mixer->set_gain_staging_auto(simple_bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui->compressor_threshold_knob, &QDial::valueChanged, bind(&MainWindow::compressor_threshold_knob_changed, this, simple_bus_index, _1)); connect(ui->compressor_enabled, &QCheckBox::stateChanged, [this, simple_bus_index](int state){ global_audio_mixer->set_compressor_enabled(simple_bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); // Global mastering controls. @@ -314,16 +319,19 @@ void MainWindow::mixer_created(Mixer *mixer) connect(ui->makeup_gain_knob, &QAbstractSlider::valueChanged, this, &MainWindow::final_makeup_gain_knob_changed); connect(ui->makeup_gain_auto_checkbox, &QCheckBox::stateChanged, [this](int state){ global_audio_mixer->set_final_makeup_gain_auto(state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui->limiter_threshold_knob, &QDial::valueChanged, this, &MainWindow::limiter_threshold_knob_changed); connect(ui->limiter_enabled, &QCheckBox::stateChanged, [this](int state){ global_audio_mixer->set_limiter_enabled(state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui->reset_meters_button, &QPushButton::clicked, this, &MainWindow::reset_meters_button_clicked); mixer->get_audio_mixer()->set_audio_level_callback(bind(&MainWindow::audio_level_callback, this, _1, _2, _3, _4, _5, _6, _7, _8)); midi_mapper.refresh_highlights(); + midi_mapper.refresh_lights(); struct sigaction act; memset(&act, 0, sizeof(act)); @@ -367,6 +375,7 @@ void MainWindow::reset_audio_mapping_ui() ui->compact_header->setVisible(!simple); midi_mapper.refresh_highlights(); + midi_mapper.refresh_lights(); } void MainWindow::setup_audio_miniview() @@ -444,6 +453,7 @@ void MainWindow::setup_audio_expanded_view() ui_audio_expanded_view->locut_enabled->setChecked(global_audio_mixer->get_locut_enabled(bus_index)); connect(ui_audio_expanded_view->locut_enabled, &QCheckBox::stateChanged, [this, bus_index](int state){ global_audio_mixer->set_locut_enabled(bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui_audio_expanded_view->treble_knob, &QDial::valueChanged, @@ -460,11 +470,13 @@ void MainWindow::setup_audio_expanded_view() connect(ui_audio_expanded_view->gainstaging_knob, &QAbstractSlider::valueChanged, bind(&MainWindow::gain_staging_knob_changed, this, bus_index, _1)); connect(ui_audio_expanded_view->gainstaging_auto_checkbox, &QCheckBox::stateChanged, [this, bus_index](int state){ global_audio_mixer->set_gain_staging_auto(bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); connect(ui_audio_expanded_view->compressor_threshold_knob, &QDial::valueChanged, bind(&MainWindow::compressor_threshold_knob_changed, this, bus_index, _1)); connect(ui_audio_expanded_view->compressor_enabled, &QCheckBox::stateChanged, [this, bus_index](int state){ global_audio_mixer->set_compressor_enabled(bus_index, state == Qt::Checked); + midi_mapper.refresh_lights(); }); slave_fader(audio_miniviews[bus_index]->fader, ui_audio_expanded_view->fader); @@ -476,8 +488,9 @@ void MainWindow::setup_audio_expanded_view() peak_meter->set_ref_level(0.0f); connect(ui_audio_expanded_view->peak_display_label, &ClickableLabel::clicked, - [bus_index]() { + [this, bus_index]() { global_audio_mixer->reset_peak(bus_index); + midi_mapper.refresh_lights(); }); // Set up the compression attenuation meter. @@ -569,6 +582,7 @@ void MainWindow::input_mapping_triggered() setup_audio_expanded_view(); } midi_mapper.refresh_highlights(); + midi_mapper.refresh_lights(); } void MainWindow::midi_mapping_triggered() @@ -768,6 +782,8 @@ void MainWindow::audio_level_callback(float level_lufs, float peak_db, vectorpeak_display_label, level.historic_peak_dbfs); + + midi_mapper.set_has_peaked(bus_index, level.historic_peak_dbfs >= -0.1f); } } ui->lra_meter->set_levels(global_level_lufs, range_low_lufs, range_high_lufs); @@ -790,6 +806,9 @@ void MainWindow::audio_level_callback(float level_lufs, float peak_db, vectormakeup_gain_db_display_2->setText( QString::fromStdString(format_db(final_makeup_gain_db, DB_WITH_SIGN))); + + // Peak labels could have changed. + midi_mapper.refresh_lights(); }); } @@ -908,9 +927,13 @@ void MainWindow::toggle_compressor(unsigned bus_idx) void MainWindow::clear_peak(unsigned bus_idx) { - if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL) { - global_audio_mixer->reset_peak(bus_idx); - } + post_to_main_thread([=]{ + if (global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL) { + global_audio_mixer->reset_peak(bus_idx); + midi_mapper.set_has_peaked(bus_idx, false); + midi_mapper.refresh_lights(); + } + }); } void MainWindow::clear_all_highlights() @@ -1045,7 +1068,8 @@ void MainWindow::set_relative_value(T *control, float value) 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 && + if (global_audio_mixer != nullptr && + 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); } @@ -1054,10 +1078,13 @@ void MainWindow::set_relative_value_if_exists(unsigned bus_idx, T *(Ui_AudioExpa 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(); - } + post_to_main_thread([this, bus_idx, control]{ + if (global_audio_mixer != nullptr && + global_audio_mixer->get_mapping_mode() == AudioMixer::MappingMode::MULTICHANNEL && + bus_idx < audio_expanded_views.size()) { + (audio_expanded_views[bus_idx]->*control)->click(); + } + }); } template diff --git a/midi_mapper.cpp b/midi_mapper.cpp index 220e176..6395ff7 100644 --- a/midi_mapper.cpp +++ b/midi_mapper.cpp @@ -1,4 +1,6 @@ #include "midi_mapper.h" + +#include "audio_mixer.h" #include "midi_mapping.pb.h" #include @@ -87,7 +89,7 @@ bool save_midi_mapping_to_file(const MIDIMappingProto &mapping_proto, const stri void MIDIMapper::set_midi_mapping(const MIDIMappingProto &new_mapping) { - lock_guard lock(mapping_mu); + lock_guard lock(mu); if (mapping_proto) { mapping_proto->CopyFrom(new_mapping); } else { @@ -108,13 +110,13 @@ void MIDIMapper::start_thread() const MIDIMappingProto &MIDIMapper::get_current_mapping() const { - lock_guard lock(mapping_mu); + lock_guard lock(mu); return *mapping_proto; } ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver) { - lock_guard lock(mapping_mu); + lock_guard lock(mu); swap(receiver, new_receiver); return new_receiver; // Now old receiver. } @@ -127,6 +129,13 @@ ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver) } \ } while (false) +#define WARN_ON_ERROR(msg, expr) do { \ + int err = (expr); \ + if (err < 0) { \ + fprintf(stderr, msg ": %s\n", snd_strerror(err)); \ + } \ +} while (false) + void MIDIMapper::thread_func() { @@ -138,10 +147,23 @@ void MIDIMapper::thread_func() 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_CAP_READ | + SND_SEQ_PORT_CAP_SUBS_READ | + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_MIDI_GENERIC | - SND_SEQ_PORT_TYPE_APPLICATION)); + SND_SEQ_PORT_TYPE_APPLICATION)); + + int queue_id = snd_seq_alloc_queue(seq); + RETURN_ON_ERROR("snd_seq_create_queue", queue_id); + RETURN_ON_ERROR("snd_seq_start_queue", snd_seq_start_queue(seq, queue_id, nullptr)); + + // The sequencer object is now ready to be used from other threads. + { + lock_guard lock(mu); + alsa_seq = seq; + alsa_queue_id = queue_id; + } // Listen to the announce port (0:1), which will tell us about new ports. RETURN_ON_ERROR("snd_seq_connect_from", snd_seq_connect_from(seq, 0, /*client=*/0, /*port=*/1)); @@ -162,7 +184,8 @@ void MIDIMapper::thread_func() while (snd_seq_query_next_port(seq, pinfo) >= 0) { constexpr int mask = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ; if ((snd_seq_port_info_get_capability(pinfo) & mask) == mask) { - subscribe_to_port(seq, *snd_seq_port_info_get_addr(pinfo)); + lock_guard lock(mu); + subscribe_to_port_lock_held(seq, *snd_seq_port_info_get_addr(pinfo)); } } } @@ -209,7 +232,12 @@ void MIDIMapper::thread_func() void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event) { - lock_guard lock(mapping_mu); + if (event->source.client == snd_seq_client_id(seq)) { + // Ignore events we sent out ourselves. + return; + } + + lock_guard lock(mu); switch (event->type) { case SND_SEQ_EVENT_CONTROLLER: { printf("Controller %d changed to %d\n", event->data.control.param, event->data.control.value); @@ -255,40 +283,47 @@ void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event) bus_mapping.prev_bank().note_number() == note) { current_controller_bank = (current_controller_bank + num_controller_banks - 1) % num_controller_banks; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_next_bank() && bus_mapping.next_bank().note_number() == note) { current_controller_bank = (current_controller_bank + 1) % num_controller_banks; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_select_bank_1() && bus_mapping.select_bank_1().note_number() == note) { current_controller_bank = 0; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_select_bank_2() && bus_mapping.select_bank_2().note_number() == note && num_controller_banks >= 2) { current_controller_bank = 1; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_select_bank_3() && bus_mapping.select_bank_3().note_number() == note && num_controller_banks >= 3) { current_controller_bank = 2; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_select_bank_4() && bus_mapping.select_bank_4().note_number() == note && num_controller_banks >= 4) { current_controller_bank = 3; update_highlights(); + update_lights_lock_held(); } if (bus_mapping.has_select_bank_5() && bus_mapping.select_bank_5().note_number() == note && num_controller_banks >= 5) { current_controller_bank = 4; update_highlights(); + update_lights_lock_held(); } } @@ -306,7 +341,7 @@ void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event) bind(&ControllerReceiver::toggle_auto_makeup_gain, receiver)); } case SND_SEQ_EVENT_PORT_START: - subscribe_to_port(seq, event->data.addr); + subscribe_to_port_lock_held(seq, event->data.addr); break; case SND_SEQ_EVENT_PORT_EXIT: printf("MIDI port %d:%d went away.\n", event->data.addr.client, event->data.addr.port); @@ -324,7 +359,7 @@ void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event) } } -void MIDIMapper::subscribe_to_port(snd_seq_t *seq, const snd_seq_addr_t &addr) +void MIDIMapper::subscribe_to_port_lock_held(snd_seq_t *seq, const snd_seq_addr_t &addr) { // Client 0 is basically the system; ignore it. if (addr.client == 0) { @@ -340,6 +375,18 @@ void MIDIMapper::subscribe_to_port(snd_seq_t *seq, const snd_seq_addr_t &addr) } else { printf("Subscribed to MIDI port %d:%d.\n", addr.client, addr.port); } + + // For sending data back. + err = snd_seq_connect_to(seq, 0, addr.client, addr.port); + if (err < 0) { + printf("Couldn't subscribe MIDI port %d:%d (%s) to us.\n", + addr.client, addr.port, snd_strerror(err)); + } else { + printf("Subscribed MIDI port %d:%d to us.\n", addr.client, addr.port); + } + + current_light_status.clear(); // The current state of the device is unknown. + update_lights_lock_held(); } void MIDIMapper::match_controller(int controller, int field_number, int bank_field_number, float value, function func) @@ -412,6 +459,12 @@ void MIDIMapper::refresh_highlights() update_highlights(); } +void MIDIMapper::refresh_lights() +{ + lock_guard lock(mu); + update_lights_lock_held(); +} + void MIDIMapper::update_highlights() { // Global controllers. @@ -470,3 +523,105 @@ void MIDIMapper::update_highlights() bus_idx, MIDIMappingBusProto::kToggleCompressorFieldNumber, MIDIMappingProto::kToggleCompressorBankFieldNumber)); } } + +void MIDIMapper::update_lights_lock_held() +{ + if (alsa_seq == nullptr || global_audio_mixer == nullptr) { + return; + } + + set active_lights; // Desired state. + if (current_controller_bank == 0) { + activate_lights_all_buses(MIDIMappingBusProto::kBank1IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 1) { + activate_lights_all_buses(MIDIMappingBusProto::kBank2IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 2) { + activate_lights_all_buses(MIDIMappingBusProto::kBank3IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 3) { + activate_lights_all_buses(MIDIMappingBusProto::kBank4IsSelectedFieldNumber, &active_lights); + } + if (current_controller_bank == 4) { + activate_lights_all_buses(MIDIMappingBusProto::kBank5IsSelectedFieldNumber, &active_lights); + } + if (global_audio_mixer->get_limiter_enabled()) { + activate_lights_all_buses(MIDIMappingBusProto::kLimiterIsOnFieldNumber, &active_lights); + } + if (global_audio_mixer->get_final_makeup_gain_auto()) { + activate_lights_all_buses(MIDIMappingBusProto::kAutoMakeupGainIsOnFieldNumber, &active_lights); + } + unsigned num_buses = min(global_audio_mixer->num_buses(), mapping_proto->bus_mapping_size()); + for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) { + if (global_audio_mixer->get_locut_enabled(bus_idx)) { + activate_lights(bus_idx, MIDIMappingBusProto::kLocutIsOnFieldNumber, &active_lights); + } + if (global_audio_mixer->get_gain_staging_auto(bus_idx)) { + activate_lights(bus_idx, MIDIMappingBusProto::kAutoGainStagingIsOnFieldNumber, &active_lights); + } + if (global_audio_mixer->get_compressor_enabled(bus_idx)) { + activate_lights(bus_idx, MIDIMappingBusProto::kCompressorIsOnFieldNumber, &active_lights); + } + if (has_peaked[bus_idx]) { + activate_lights(bus_idx, MIDIMappingBusProto::kHasPeakedFieldNumber, &active_lights); + } + } + + unsigned num_events = 0; + for (unsigned note_num = 1; note_num <= 127; ++note_num) { + bool active = active_lights.count(note_num); + if (current_light_status.count(note_num) && + current_light_status[note_num] == active) { + // Already known to be in the desired state. + continue; + } + + snd_seq_event_t ev; + snd_seq_ev_clear(&ev); + + // Some devices drop events if we throw them onto them + // too quickly. Add a 1 ms delay for each. + snd_seq_real_time_t tm{0, num_events++ * 1000000}; + snd_seq_ev_schedule_real(&ev, alsa_queue_id, true, &tm); + snd_seq_ev_set_source(&ev, 0); + snd_seq_ev_set_subs(&ev); + + // For some reason, not all devices respond to note off. + // Use note-on with velocity of 0 (which is equivalent) instead. + snd_seq_ev_set_noteon(&ev, /*channel=*/0, note_num, active ? 127 : 0); + WARN_ON_ERROR("snd_seq_event_output", snd_seq_event_output(alsa_seq, &ev)); + current_light_status[note_num] = active; + } + WARN_ON_ERROR("snd_seq_drain_output", snd_seq_drain_output(alsa_seq)); +} + +void MIDIMapper::activate_lights(unsigned bus_idx, int field_number, set *active_lights) +{ + 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; + } + const MIDILightProto &light_proto = + static_cast(bus_reflection->GetMessage(bus_mapping, descriptor)); + active_lights->insert(light_proto.note_number()); +} + +void MIDIMapper::activate_lights_all_buses(int field_number, set *active_lights) +{ + 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 MIDILightProto &light_proto = + static_cast(bus_reflection->GetMessage(bus_mapping, descriptor)); + active_lights->insert(light_proto.note_number()); + } +} diff --git a/midi_mapper.h b/midi_mapper.h index 49c98cc..be785ec 100644 --- a/midi_mapper.h +++ b/midi_mapper.h @@ -10,11 +10,15 @@ #include #include +#include #include #include +#include #include #include +#include "defs.h" + class MIDIMappingProto; typedef struct snd_seq_addr snd_seq_addr_t; typedef struct snd_seq_event snd_seq_event_t; @@ -81,11 +85,17 @@ public: ControllerReceiver *set_receiver(ControllerReceiver *new_receiver); void refresh_highlights(); + void refresh_lights(); + + void set_has_peaked(unsigned bus_idx, bool has_peaked) + { + this->has_peaked[bus_idx] = has_peaked; + } private: void thread_func(); void handle_event(snd_seq_t *seq, snd_seq_event_t *event); - void subscribe_to_port(snd_seq_t *seq, const snd_seq_addr_t &addr); + void subscribe_to_port_lock_held(snd_seq_t *seq, const snd_seq_addr_t &addr); 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 has_active_controller(unsigned bus_idx, int field_number, int bank_field_number); // Also works for buttons. @@ -93,16 +103,25 @@ private: void update_highlights(); + void update_lights_lock_held(); + void activate_lights(unsigned bus_idx, int field_number, std::set *active_lights); + void activate_lights_all_buses(int field_number, std::set *active_lights); + std::atomic should_quit{false}; int should_quit_fd; - mutable std::mutex mapping_mu; - ControllerReceiver *receiver; // Under . - std::unique_ptr mapping_proto; // Under . - int num_controller_banks; // Under . + std::atomic has_peaked[MAX_BUSES] {{ false }}; + + mutable std::mutex mu; + ControllerReceiver *receiver; // Under . + std::unique_ptr mapping_proto; // Under . + int num_controller_banks; // Under . std::atomic current_controller_bank{0}; std::thread midi_thread; + std::map current_light_status; // Keyed by note number. Under . + snd_seq_t *alsa_seq{nullptr}; // Under . + int alsa_queue_id{-1}; // Under . }; bool load_midi_mapping_from_file(const std::string &filename, MIDIMappingProto *new_mapping); diff --git a/midi_mapping.proto b/midi_mapping.proto index 5809ffe..d2b170d 100644 --- a/midi_mapping.proto +++ b/midi_mapping.proto @@ -14,6 +14,10 @@ message MIDIButtonProto { required int32 note_number = 1; } +message MIDILightProto { + 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 @@ -56,6 +60,21 @@ message MIDIMappingBusProto { optional MIDIControllerProto locut = 21; optional MIDIControllerProto limiter_threshold = 22; optional MIDIControllerProto makeup_gain = 23; + + // Per-bus lights. + optional MIDILightProto locut_is_on = 24; + optional MIDILightProto auto_gain_staging_is_on = 25; + optional MIDILightProto compressor_is_on = 26; + optional MIDILightProto has_peaked = 27; + + // Global lights. Same logic as above for why they're in this proto. + optional MIDILightProto bank_1_is_selected = 28; + optional MIDILightProto bank_2_is_selected = 29; + optional MIDILightProto bank_3_is_selected = 30; + optional MIDILightProto bank_4_is_selected = 31; + optional MIDILightProto bank_5_is_selected = 32; + optional MIDILightProto limiter_is_on = 33; + optional MIDILightProto auto_makeup_gain_is_on = 34; } // The top-level protobuf, containing all the bus mappings, as well as diff --git a/midi_mapping_dialog.cpp b/midi_mapping_dialog.cpp index 2c84567..95de0d9 100644 --- a/midi_mapping_dialog.cpp +++ b/midi_mapping_dialog.cpp @@ -35,6 +35,12 @@ vector per_bus_buttons = { { "Clear peak", MIDIMappingBusProto::kClearPeakFieldNumber, MIDIMappingProto::kClearPeakBankFieldNumber } }; +vector per_bus_lights = { + { "Locut is on", MIDIMappingBusProto::kLocutIsOnFieldNumber, 0 }, + { "Auto gain staging is on", MIDIMappingBusProto::kAutoGainStagingIsOnFieldNumber, 0 }, + { "Compressor is on", MIDIMappingBusProto::kCompressorIsOnFieldNumber, 0 }, + { "Bus has peaked", MIDIMappingBusProto::kHasPeakedFieldNumber, 0 } +}; vector global_controllers = { { "Locut cutoff", MIDIMappingBusProto::kLocutFieldNumber, MIDIMappingProto::kLocutBankFieldNumber }, { "Limiter threshold", MIDIMappingBusProto::kLimiterThresholdFieldNumber, @@ -53,6 +59,15 @@ vector global_buttons = { { "Toggle limiter", MIDIMappingBusProto::kToggleLimiterFieldNumber, MIDIMappingProto::kToggleLimiterBankFieldNumber }, { "Toggle auto makeup gain", MIDIMappingBusProto::kToggleAutoMakeupGainFieldNumber, MIDIMappingProto::kToggleAutoMakeupGainBankFieldNumber } }; +vector global_lights = { + { "Bank 1 is selected", MIDIMappingBusProto::kBank1IsSelectedFieldNumber, 0 }, + { "Bank 2 is selected", MIDIMappingBusProto::kBank2IsSelectedFieldNumber, 0 }, + { "Bank 3 is selected", MIDIMappingBusProto::kBank3IsSelectedFieldNumber, 0 }, + { "Bank 4 is selected", MIDIMappingBusProto::kBank4IsSelectedFieldNumber, 0 }, + { "Bank 5 is selected", MIDIMappingBusProto::kBank5IsSelectedFieldNumber, 0 }, + { "Limiter is on", MIDIMappingBusProto::kLimiterIsOnFieldNumber, 0 }, + { "Auto makeup gain is on", MIDIMappingBusProto::kAutoMakeupGainIsOnFieldNumber, 0 }, +}; namespace { @@ -78,7 +93,7 @@ int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx if (!bus_reflection->HasField(bus_mapping, descriptor)) { return default_value; } - const MIDIControllerProto &controller_proto = + const MIDIControllerProto &controller_proto = static_cast(bus_reflection->GetMessage(bus_mapping, descriptor)); return controller_proto.controller_number(); } @@ -95,11 +110,28 @@ int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, in if (!bus_reflection->HasField(bus_mapping, descriptor)) { return default_value; } - const MIDIButtonProto &bus_proto = + const MIDIButtonProto &bus_proto = static_cast(bus_reflection->GetMessage(bus_mapping, descriptor)); return bus_proto.note_number(); } +int get_light_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 MIDILightProto &bus_proto = + static_cast(bus_reflection->GetMessage(bus_mapping, descriptor)); + return bus_proto.note_number(); +} + } // namespace MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper) @@ -125,8 +157,10 @@ MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper) add_controls("Per-bus controllers", ControlType::CONTROLLER, SpinnerGroup::PER_BUS_CONTROLLERS, mapping_proto, per_bus_controllers); add_controls("Per-bus buttons", ControlType::BUTTON, SpinnerGroup::PER_BUS_BUTTONS, mapping_proto, per_bus_buttons); + add_controls("Per-bus lights", ControlType::LIGHT, SpinnerGroup::PER_BUS_LIGHTS, mapping_proto, per_bus_lights); add_controls("Global controllers", ControlType::CONTROLLER, SpinnerGroup::GLOBAL_CONTROLLERS, mapping_proto, global_controllers); add_controls("Global buttons", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_buttons); + add_controls("Global lights", ControlType::LIGHT, SpinnerGroup::GLOBAL_LIGHTS, mapping_proto, global_lights); fill_controls_from_mapping(mapping_proto); // Auto-resize every column but the last. @@ -208,6 +242,11 @@ void MIDIMappingDialog::guess_clicked(bool limit_to_group) is.spinner->setFocus(); } } + for (const InstantiatedSpinner &is : light_spinners) { + if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) { + is.spinner->setFocus(); + } + } } void MIDIMappingDialog::ok_clicked() @@ -294,6 +333,16 @@ unique_ptr MIDIMappingDialog::construct_mapping_proto_from_ui( get_mutable_bus_message(mapping_proto.get(), is.bus_idx, is.field_number); button_proto->set_note_number(val); } + for (const InstantiatedSpinner &is : light_spinners) { + const int val = is.spinner->value(); + if (val == 0) { + continue; + } + + MIDILightProto *light_proto = + get_mutable_bus_message(mapping_proto.get(), is.bus_idx, is.field_number); + light_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(); @@ -350,9 +399,11 @@ void MIDIMappingDialog::add_controls(const string &heading, if (control_type == ControlType::CONTROLLER) { controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number }); - } else { - assert(control_type == ControlType::BUTTON); + } else if (control_type == ControlType::BUTTON) { button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number }); + } else { + assert(control_type == ControlType::LIGHT); + light_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number }); } spinners[bus_idx][control.field_number] = SpinnerAndGroup{ spinner, spinner_group }; connect(spinner, static_cast(&QSpinBox::valueChanged), @@ -370,6 +421,9 @@ void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mappi for (const InstantiatedSpinner &is : button_spinners) { is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, 0)); } + for (const InstantiatedSpinner &is : light_spinners) { + is.spinner->setValue(get_light_mapping(mapping_proto, is.bus_idx, is.field_number, 0)); + } for (const InstantiatedComboBox &ic : bank_combo_boxes) { ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1); } @@ -393,6 +447,12 @@ void MIDIMappingDialog::note_on(unsigned note) is.spinner->selectAll(); } } + for (const InstantiatedSpinner &is : light_spinners) { + if (is.spinner->hasFocus()) { + is.spinner->setValue(note); + is.spinner->selectAll(); + } + } } pair MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group) diff --git a/midi_mapping_dialog.h b/midi_mapping_dialog.h index 6a895ba..465f8fd 100644 --- a/midi_mapping_dialog.h +++ b/midi_mapping_dialog.h @@ -99,13 +99,15 @@ private: ALL_GROUPS = -1, PER_BUS_CONTROLLERS, PER_BUS_BUTTONS, + PER_BUS_LIGHTS, GLOBAL_CONTROLLERS, - GLOBAL_BUTTONS + GLOBAL_BUTTONS, + GLOBAL_LIGHTS }; void add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number); - enum class ControlType { CONTROLLER, BUTTON }; + enum class ControlType { CONTROLLER, BUTTON, LIGHT }; void add_controls(const std::string &heading, ControlType control_type, SpinnerGroup spinner_group, const MIDIMappingProto &mapping_proto, const std::vector &controls); @@ -147,6 +149,7 @@ private: }; std::vector controller_spinners; std::vector button_spinners; + std::vector light_spinners; std::vector bank_combo_boxes; // Keyed on bus index, then field number.