]> git.sesse.net Git - nageru/commitdiff
Add support for feedback lights (LEDs) on MIDI controllers.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 19 Oct 2016 19:41:39 +0000 (21:41 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Tue, 25 Oct 2016 16:48:34 +0000 (18:48 +0200)
audio_mixer.cpp
audio_mixer.h
mainwindow.cpp
midi_mapper.cpp
midi_mapper.h
midi_mapping.proto
midi_mapping_dialog.cpp
midi_mapping_dialog.h

index e4eb495953baaa49cd487f2b5e16960857d88efb..d0737f217c260804c4272bf9d9d5382c109aaa94 100644 (file)
@@ -987,6 +987,12 @@ InputMapping AudioMixer::get_input_mapping() const
        return input_mapping;
 }
 
        return input_mapping;
 }
 
+unsigned AudioMixer::num_buses() const
+{
+       lock_guard<timed_mutex> lock(audio_mutex);
+       return input_mapping.buses.size();
+}
+
 void AudioMixer::reset_peak(unsigned bus_index)
 {
        lock_guard<timed_mutex> lock(audio_mutex);
 void AudioMixer::reset_peak(unsigned bus_index)
 {
        lock_guard<timed_mutex> lock(audio_mutex);
index 71f36629341f54a6151d33469271efd6361ccbce..38b98f7ce0673e025fb6c96df392281189a98fac 100644 (file)
@@ -114,6 +114,8 @@ public:
        MappingMode get_mapping_mode() const;
        InputMapping get_input_mapping() const;
 
        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;
        void set_locut_cutoff(float cutoff_hz)
        {
                locut_cutoff_hz = cutoff_hz;
index 79221e162e8cb89c6317407a55fc83594b0df29e..ec13cd741c4730ee82cf983ec7ed74d9df8ee00a 100644 (file)
@@ -46,6 +46,10 @@ Q_DECLARE_METATYPE(std::vector<std::string>);
 
 MainWindow *global_mainwindow = nullptr;
 
 
 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)
 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)));
 
 {
        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("");
                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);
        }
        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);
        });
        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);
        });
        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.
        });
 
        // 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);
        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);
        });
 
        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();
        });
        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));
 
        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();
        ui->compact_header->setVisible(!simple);
 
        midi_mapper.refresh_highlights();
+       midi_mapper.refresh_lights();
 }
 
 void MainWindow::setup_audio_miniview()
 }
 
 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);
                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,
                });
 
                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);
                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);
                });
 
                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);
                });
 
                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,
                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);
                                global_audio_mixer->reset_peak(bus_index);
+                               midi_mapper.refresh_lights();
                        });
 
                // Set up the compression attenuation meter.
                        });
 
                // Set up the compression attenuation meter.
@@ -569,6 +582,7 @@ void MainWindow::input_mapping_triggered()
                setup_audio_expanded_view();
        }
        midi_mapper.refresh_highlights();
                setup_audio_expanded_view();
        }
        midi_mapper.refresh_highlights();
+       midi_mapper.refresh_lights();
 }
 
 void MainWindow::midi_mapping_triggered()
 }
 
 void MainWindow::midi_mapping_triggered()
@@ -768,6 +782,8 @@ void MainWindow::audio_level_callback(float level_lufs, float peak_db, vector<Au
                                        QString("Gain: ") +
                                        QString::fromStdString(format_db(level.gain_staging_db, DB_WITH_SIGN)));
                                set_peak_label(view->peak_display_label, level.historic_peak_dbfs);
                                        QString("Gain: ") +
                                        QString::fromStdString(format_db(level.gain_staging_db, DB_WITH_SIGN)));
                                set_peak_label(view->peak_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);
                        }
                }
                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, vector<Au
                        QString::fromStdString(format_db(final_makeup_gain_db, DB_WITH_SIGN)));
                ui->makeup_gain_db_display_2->setText(
                        QString::fromStdString(format_db(final_makeup_gain_db, DB_WITH_SIGN)));
                        QString::fromStdString(format_db(final_makeup_gain_db, DB_WITH_SIGN)));
                ui->makeup_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)
 {
 
 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()
 }
 
 void MainWindow::clear_all_highlights()
@@ -1045,7 +1068,8 @@ void MainWindow::set_relative_value(T *control, float value)
 template<class T>
 void MainWindow::set_relative_value_if_exists(unsigned bus_idx, T *(Ui_AudioExpandedView::*control), float value)
 {
 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 &&
+       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);
        }
            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<class T>
 void MainWindow::click_button_if_exists(unsigned bus_idx, T *(Ui_AudioExpandedView::*control))
 {
 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();
-       }
+       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<class T>
 }
 
 template<class T>
index 220e176b7f6bc0895822420c288b2fbe99364e27..6395ff7381a25a008214188449e2f4c22ed53a6c 100644 (file)
@@ -1,4 +1,6 @@
 #include "midi_mapper.h"
 #include "midi_mapper.h"
+
+#include "audio_mixer.h"
 #include "midi_mapping.pb.h"
 
 #include <alsa/asoundlib.h>
 #include "midi_mapping.pb.h"
 
 #include <alsa/asoundlib.h>
@@ -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)
 {
 
 void MIDIMapper::set_midi_mapping(const MIDIMappingProto &new_mapping)
 {
-       lock_guard<mutex> lock(mapping_mu);
+       lock_guard<mutex> lock(mu);
        if (mapping_proto) {
                mapping_proto->CopyFrom(new_mapping);
        } else {
        if (mapping_proto) {
                mapping_proto->CopyFrom(new_mapping);
        } else {
@@ -108,13 +110,13 @@ void MIDIMapper::start_thread()
 
 const MIDIMappingProto &MIDIMapper::get_current_mapping() const
 {
 
 const MIDIMappingProto &MIDIMapper::get_current_mapping() const
 {
-       lock_guard<mutex> lock(mapping_mu);
+       lock_guard<mutex> lock(mu);
        return *mapping_proto;
 }
 
 ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver)
 {
        return *mapping_proto;
 }
 
 ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver)
 {
-       lock_guard<mutex> lock(mapping_mu);
+       lock_guard<mutex> lock(mu);
        swap(receiver, new_receiver);
        return new_receiver;  // Now old receiver.
 }
        swap(receiver, new_receiver);
        return new_receiver;  // Now old receiver.
 }
@@ -127,6 +129,13 @@ ControllerReceiver *MIDIMapper::set_receiver(ControllerReceiver *new_receiver)
        }                                                          \
 } while (false)
 
        }                                                          \
 } 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()
 {
 
 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",
        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_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<mutex> 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));
 
        // 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) {
                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<mutex> 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)
 {
 
 void MIDIMapper::handle_event(snd_seq_t *seq, snd_seq_event_t *event)
 {
-       lock_guard<mutex> lock(mapping_mu);
+       if (event->source.client == snd_seq_client_id(seq)) {
+               // Ignore events we sent out ourselves.
+               return;
+       }
+
+       lock_guard<mutex> 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);
        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();
                            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();
                        }
                        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();
                        }
                        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();
                        }
                        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();
                        }
                        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();
                        }
                        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();
                        }
                        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:
                        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);
                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) {
 {
        // 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);
        }
        } 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<void(unsigned, float)> func)
 }
 
 void MIDIMapper::match_controller(int controller, int field_number, int bank_field_number, float value, function<void(unsigned, float)> func)
@@ -412,6 +459,12 @@ void MIDIMapper::refresh_highlights()
        update_highlights();
 }
 
        update_highlights();
 }
 
+void MIDIMapper::refresh_lights()
+{
+       lock_guard<mutex> lock(mu);
+       update_lights_lock_held();
+}
+
 void MIDIMapper::update_highlights()
 {
        // Global controllers.
 void MIDIMapper::update_highlights()
 {
        // Global controllers.
@@ -470,3 +523,105 @@ void MIDIMapper::update_highlights()
                        bus_idx, MIDIMappingBusProto::kToggleCompressorFieldNumber, MIDIMappingProto::kToggleCompressorBankFieldNumber));
        }
 }
                        bus_idx, MIDIMappingBusProto::kToggleCompressorFieldNumber, MIDIMappingProto::kToggleCompressorBankFieldNumber));
        }
 }
+
+void MIDIMapper::update_lights_lock_held()
+{
+       if (alsa_seq == nullptr || global_audio_mixer == nullptr) {
+               return;
+       }
+
+       set<unsigned> 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<unsigned>(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<unsigned> *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<const MIDILightProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+       active_lights->insert(light_proto.note_number());
+}
+
+void MIDIMapper::activate_lights_all_buses(int field_number, set<unsigned> *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<const MIDILightProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+               active_lights->insert(light_proto.note_number());
+       }
+}
index 49c98cc31676d142d8d01b314556fe3e033965f7..be785ecdc709095340dbbe3cd98ccfd9a67174a8 100644 (file)
 
 #include <atomic>
 #include <functional>
 
 #include <atomic>
 #include <functional>
+#include <map>
 #include <memory>
 #include <mutex>
 #include <memory>
 #include <mutex>
+#include <set>
 #include <string>
 #include <thread>
 
 #include <string>
 #include <thread>
 
+#include "defs.h"
+
 class MIDIMappingProto;
 typedef struct snd_seq_addr snd_seq_addr_t;
 typedef struct snd_seq_event snd_seq_event_t;
 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();
        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);
 
 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<void(unsigned, float)> func);
        void match_button(int note, int field_number, int bank_field_number, std::function<void(unsigned)> func);
        bool has_active_controller(unsigned bus_idx, int field_number, int bank_field_number);  // Also works for buttons.
        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 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_highlights();
 
+       void update_lights_lock_held();
+       void activate_lights(unsigned bus_idx, int field_number, std::set<unsigned> *active_lights);
+       void activate_lights_all_buses(int field_number, std::set<unsigned> *active_lights);
+
        std::atomic<bool> should_quit{false};
        int should_quit_fd;
 
        std::atomic<bool> should_quit{false};
        int should_quit_fd;
 
-       mutable std::mutex mapping_mu;
-       ControllerReceiver *receiver;  // Under <mapping_mu>.
-       std::unique_ptr<MIDIMappingProto> mapping_proto;  // Under <mapping_mu>.
-       int num_controller_banks;  // Under <mapping_mu>.
+       std::atomic<bool> has_peaked[MAX_BUSES] {{ false }};
+
+       mutable std::mutex mu;
+       ControllerReceiver *receiver;  // Under <mu>.
+       std::unique_ptr<MIDIMappingProto> mapping_proto;  // Under <mu>.
+       int num_controller_banks;  // Under <mu>.
        std::atomic<int> current_controller_bank{0};
 
        std::thread midi_thread;
        std::atomic<int> current_controller_bank{0};
 
        std::thread midi_thread;
+       std::map<unsigned, bool> current_light_status;  // Keyed by note number. Under <mu>.
+       snd_seq_t *alsa_seq{nullptr};  // Under <mu>.
+       int alsa_queue_id{-1};  // Under <mu>.
 };
 
 bool load_midi_mapping_from_file(const std::string &filename, MIDIMappingProto *new_mapping);
 };
 
 bool load_midi_mapping_from_file(const std::string &filename, MIDIMappingProto *new_mapping);
index 5809ffe02bfb1cc95f5405931beab4bd961a42dd..d2b170d46396aec87b445fdb56fe3f7049714529 100644 (file)
@@ -14,6 +14,10 @@ message MIDIButtonProto {
        required int32 note_number = 1;
 }
 
        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
 // 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;
        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
 }
 
 // The top-level protobuf, containing all the bus mappings, as well as
index 2c84567d1015c8ff39341cfa5c82b2855f2b2a26..95de0d97c2150916270aeb673549e7b702c63525 100644 (file)
@@ -35,6 +35,12 @@ vector<MIDIMappingDialog::Control> per_bus_buttons = {
        { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
                                      MIDIMappingProto::kClearPeakBankFieldNumber }
 };
        { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
                                      MIDIMappingProto::kClearPeakBankFieldNumber }
 };
+vector<MIDIMappingDialog::Control> 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<MIDIMappingDialog::Control> global_controllers = {
        { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
        { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
 vector<MIDIMappingDialog::Control> global_controllers = {
        { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
        { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
@@ -53,6 +59,15 @@ vector<MIDIMappingDialog::Control> global_buttons = {
        { "Toggle limiter",           MIDIMappingBusProto::kToggleLimiterFieldNumber, MIDIMappingProto::kToggleLimiterBankFieldNumber },
        { "Toggle auto makeup gain",  MIDIMappingBusProto::kToggleAutoMakeupGainFieldNumber, MIDIMappingProto::kToggleAutoMakeupGainBankFieldNumber }
 };
        { "Toggle limiter",           MIDIMappingBusProto::kToggleLimiterFieldNumber, MIDIMappingProto::kToggleLimiterBankFieldNumber },
        { "Toggle auto makeup gain",  MIDIMappingBusProto::kToggleAutoMakeupGainFieldNumber, MIDIMappingProto::kToggleAutoMakeupGainBankFieldNumber }
 };
+vector<MIDIMappingDialog::Control> 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 {
 
 
 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;
        }
        if (!bus_reflection->HasField(bus_mapping, descriptor)) {
                return default_value;
        }
-       const MIDIControllerProto &controller_proto = 
+       const MIDIControllerProto &controller_proto =
                static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
        return controller_proto.controller_number();
 }
                static_cast<const MIDIControllerProto &>(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;
        }
        if (!bus_reflection->HasField(bus_mapping, descriptor)) {
                return default_value;
        }
-       const MIDIButtonProto &bus_proto = 
+       const MIDIButtonProto &bus_proto =
                static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
        return bus_proto.note_number();
 }
 
                static_cast<const MIDIButtonProto &>(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<const MIDILightProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
+       return bus_proto.note_number();
+}
+
 }  // namespace
 
 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
 }  // 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 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 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.
        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();
                }
        }
                        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()
 }
 
 void MIDIMappingDialog::ok_clicked()
@@ -294,6 +333,16 @@ unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui(
                        get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
                button_proto->set_note_number(val);
        }
                        get_mutable_bus_message<MIDIButtonProto>(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<MIDILightProto>(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();
        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 });
 
                        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 });
                                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<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
                        }
                        spinners[bus_idx][control.field_number] = SpinnerAndGroup{ spinner, spinner_group };
                        connect(spinner, static_cast<void(QSpinBox::*)(int)>(&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 : 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);
        }
        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();
                }
        }
                        is.spinner->selectAll();
                }
        }
+       for (const InstantiatedSpinner &is : light_spinners) {
+               if (is.spinner->hasFocus()) {
+                       is.spinner->setValue(note);
+                       is.spinner->selectAll();
+               }
+       }
 }
 
 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group)
 }
 
 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group)
index 6a895ba081455911cb4578e7acdc96d159bb7461..465f8fd5f0259a1ae2fe13a556d5d81a59b8545f 100644 (file)
@@ -99,13 +99,15 @@ private:
                ALL_GROUPS = -1,
                PER_BUS_CONTROLLERS,
                PER_BUS_BUTTONS,
                ALL_GROUPS = -1,
                PER_BUS_CONTROLLERS,
                PER_BUS_BUTTONS,
+               PER_BUS_LIGHTS,
                GLOBAL_CONTROLLERS,
                GLOBAL_CONTROLLERS,
-               GLOBAL_BUTTONS
+               GLOBAL_BUTTONS,
+               GLOBAL_LIGHTS
        };
 
        void add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number);
        
        };
 
        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<Control> &controls);
        void add_controls(const std::string &heading, ControlType control_type,
                          SpinnerGroup spinner_group,
                          const MIDIMappingProto &mapping_proto, const std::vector<Control> &controls);
@@ -147,6 +149,7 @@ private:
        };
        std::vector<InstantiatedSpinner> controller_spinners;
        std::vector<InstantiatedSpinner> button_spinners;
        };
        std::vector<InstantiatedSpinner> controller_spinners;
        std::vector<InstantiatedSpinner> button_spinners;
+       std::vector<InstantiatedSpinner> light_spinners;
        std::vector<InstantiatedComboBox> bank_combo_boxes;
 
        // Keyed on bus index, then field number.
        std::vector<InstantiatedComboBox> bank_combo_boxes;
 
        // Keyed on bus index, then field number.