]> git.sesse.net Git - nageru/blobdiff - midi_mapper.cpp
Add support for feedback lights (LEDs) on MIDI controllers.
[nageru] / midi_mapper.cpp
index 220e176b7f6bc0895822420c288b2fbe99364e27..6395ff7381a25a008214188449e2f4c22ed53a6c 100644 (file)
@@ -1,4 +1,6 @@
 #include "midi_mapper.h"
+
+#include "audio_mixer.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)
 {
-       lock_guard<mutex> lock(mapping_mu);
+       lock_guard<mutex> 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<mutex> lock(mapping_mu);
+       lock_guard<mutex> lock(mu);
        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.
 }
@@ -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<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));
@@ -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<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)
 {
-       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);
@@ -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<void(unsigned, float)> func)
@@ -412,6 +459,12 @@ void MIDIMapper::refresh_highlights()
        update_highlights();
 }
 
+void MIDIMapper::refresh_lights()
+{
+       lock_guard<mutex> 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<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());
+       }
+}