LCOV - code coverage report
Current view: top level - src/midi - midi_manager_mapping.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 87.3 % 173 151
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 13 13

            Line data    Source code
       1              : #include "audio/engine/audio_engine.h"
       2              : #include "midi/midi_manager.h"
       3              : 
       4              : namespace Amplitron {
       5              : 
       6              : namespace {
       7           30 : int find_node_id_for_effect(IAudioEngine& engine, const std::shared_ptr<Effect>& effect,
       8              :                             int fallback_id) {
       9           60 :     for (const auto& node : engine.graph().get_nodes()) {
      10           60 :         if (node.pedal == effect) {
      11           30 :             return node.id;
      12              :         }
      13              :     }
      14            0 :     return fallback_id;
      15           10 : }
      16              : }  // namespace
      17              : 
      18              : // ---------------------------------------------------------------------------
      19              : // Mapping management
      20              : // ---------------------------------------------------------------------------
      21              : 
      22          183 : void MidiManager::add_mapping(const MidiMapping& mapping) {
      23              :     // Remove any existing mapping with the same CC + channel
      24          291 :     for (auto it = mappings_.begin(); it != mappings_.end(); ++it) {
      25          114 :         if (it->cc_number == mapping.cc_number && it->midi_channel == mapping.midi_channel) {
      26            6 :             mappings_.erase(it);
      27            6 :             break;
      28              :         }
      29           36 :     }
      30          183 :     mappings_.push_back(mapping);
      31          183 : }
      32              : 
      33            3 : void MidiManager::remove_mapping(int index) {
      34            3 :     if (index >= 0 && index < static_cast<int>(mappings_.size())) {
      35            3 :         mappings_.erase(mappings_.begin() + index);
      36            1 :     }
      37            3 : }
      38              : 
      39           15 : void MidiManager::remove_mapping_for_param(const std::string& effect_name,
      40              :                                            const std::string& param_name) {
      41           24 :     for (auto it = mappings_.begin(); it != mappings_.end(); ++it) {
      42           18 :         if (it->target_type == MidiTargetType::EffectParam && it->effect_name == effect_name &&
      43            9 :             it->param_name == param_name) {
      44            6 :             mappings_.erase(it);
      45            6 :             return;
      46              :         }
      47            3 :     }
      48            5 : }
      49              : 
      50           60 : void MidiManager::clear_mappings() { mappings_.clear(); }
      51              : 
      52            6 : void MidiManager::install_default_mappings() {
      53            6 :     MidiMapping cc7;
      54            6 :     cc7.cc_number = 7;
      55            6 :     cc7.midi_channel = -1;
      56            6 :     cc7.target_type = MidiTargetType::OutputGain;
      57            6 :     cc7.mode = MidiMappingMode::Continuous;
      58            6 :     add_mapping(cc7);
      59              : 
      60            6 :     MidiMapping cc11;
      61            6 :     cc11.cc_number = 11;
      62            6 :     cc11.midi_channel = -1;
      63            6 :     cc11.target_type = MidiTargetType::InputGain;
      64            6 :     cc11.mode = MidiMappingMode::Continuous;
      65            6 :     add_mapping(cc11);
      66              : 
      67            6 :     MidiMapping cc64;
      68            6 :     cc64.cc_number = 64;
      69            6 :     cc64.midi_channel = -1;
      70            6 :     cc64.target_type = MidiTargetType::EffectBypass;
      71            6 :     cc64.mode = MidiMappingMode::Toggle;
      72            6 :     cc64.effect_name = "AmpSimulator";
      73            6 :     add_mapping(cc64);
      74              : 
      75            6 :     MidiMapping cc74;
      76            6 :     cc74.cc_number = 74;
      77            6 :     cc74.midi_channel = -1;
      78            6 :     cc74.target_type = MidiTargetType::EffectParam;
      79            6 :     cc74.mode = MidiMappingMode::Continuous;
      80            6 :     cc74.effect_name = "WahPedal";
      81            6 :     cc74.param_name = "Sweep";
      82            6 :     add_mapping(cc74);
      83              : 
      84              : #ifdef __EMSCRIPTEN__
      85              :     // Web-specific MIDI defaults
      86              : 
      87              :     // CC11 (Expression pedal) → Output Gain
      88              :     MidiMapping cc11_output;
      89              :     cc11_output.cc_number = 11;
      90              :     cc11_output.midi_channel = -1;  // Respond on any channel
      91              :     cc11_output.target_type = MidiTargetType::OutputGain;
      92              :     cc11_output.mode = MidiMappingMode::Continuous;
      93              :     add_mapping(cc11_output);
      94              : 
      95              :     // CC7 (Volume) → Also Output Gain (alternative)
      96              :     MidiMapping cc7_output;
      97              :     cc7_output.cc_number = 7;
      98              :     cc7_output.midi_channel = -1;
      99              :     cc7_output.target_type = MidiTargetType::OutputGain;
     100              :     cc7_output.mode = MidiMappingMode::Continuous;
     101              :     add_mapping(cc7_output);
     102              : 
     103              :     // CC64 (Sustain/Damper pedal) → Bypass toggle
     104              :     // (Already implemented as EffectBypass for AmpSimulator above,
     105              :     // but redefined here explicitly for Web defaults)
     106              : 
     107              :     // CC64 (Sustain) → acts as bypass via OutputGain toggle (web fallback)
     108              : 
     109              :     // CC1 (Modulation) → EffectParam (e.g., Chorus Depth)
     110              :     MidiMapping cc1_mod;
     111              :     cc1_mod.cc_number = 1;
     112              :     cc1_mod.midi_channel = -1;
     113              :     cc1_mod.target_type = MidiTargetType::EffectParam;
     114              :     cc1_mod.mode = MidiMappingMode::Continuous;
     115              :     cc1_mod.effect_name = "Chorus";
     116              :     cc1_mod.param_name = "Depth";
     117              :     add_mapping(cc1_mod);
     118              : 
     119              :     MidiMapping cc64_bypass;
     120              :     cc64_bypass.cc_number = 64;
     121              :     cc64_bypass.midi_channel = -1;
     122              :     cc64_bypass.target_type = MidiTargetType::EffectBypass;
     123              :     cc64_bypass.mode = MidiMappingMode::Toggle;
     124              :     cc64_bypass.effect_name = "AmpSimulator";
     125              :     add_mapping(cc64_bypass);
     126              : #endif
     127           12 : }
     128              : 
     129              : // ---------------------------------------------------------------------------
     130              : // MIDI Learn
     131              : // ---------------------------------------------------------------------------
     132              : 
     133           30 : void MidiManager::start_learn(MidiTargetType type, const std::string& effect_name,
     134              :                               const std::string& param_name) {
     135           30 :     learn_active_ = true;
     136           30 :     learn_target_type_ = type;
     137           30 :     learn_effect_name_ = effect_name;
     138           30 :     learn_param_name_ = param_name;
     139           30 : }
     140              : 
     141           18 : void MidiManager::cancel_learn() {
     142           18 :     learn_active_ = false;
     143           18 :     learn_effect_name_.clear();
     144           18 :     learn_param_name_.clear();
     145           18 : }
     146              : 
     147           36 : std::string MidiManager::learn_status() const {
     148           46 :     if (!learn_active_) return "";
     149              : 
     150           21 :     std::string target;
     151           21 :     switch (learn_target_type_) {
     152            2 :         case MidiTargetType::EffectParam:
     153            3 :             target = learn_effect_name_ + " > " + learn_param_name_;
     154            3 :             break;
     155            4 :         case MidiTargetType::EffectBypass:
     156            6 :             target = learn_effect_name_ + " (bypass)";
     157            6 :             break;
     158            4 :         case MidiTargetType::InputGain:
     159            6 :             target = "Input Gain";
     160            4 :             break;
     161            4 :         case MidiTargetType::OutputGain:
     162            6 :             target = "Output Gain";
     163            4 :             break;
     164              :     }
     165           28 :     return "MIDI Learn: move a CC for \"" + target + "\"...";
     166           26 : }
     167              : 
     168              : // ---------------------------------------------------------------------------
     169              : // Poll — called from GUI thread each frame
     170              : // ---------------------------------------------------------------------------
     171              : 
     172           57 : void MidiManager::inject_event(const MidiEvent& event) { midi_queue_.try_push(event); }
     173              : 
     174           57 : void MidiManager::poll(IAudioEngine& engine) {
     175           57 :     MidiEvent event{};
     176           76 :     while (midi_queue_.try_pop(event)) {
     177           57 :         uint8_t cc_number = event.data1;
     178           57 :         uint8_t cc_value = event.data2;
     179           57 :         int channel = event.status & 0x0F;
     180              : 
     181              :         // MIDI Learn: capture the first CC and create a mapping
     182           57 :         if (learn_active_) {
     183            3 :             MidiMapping mapping;
     184            3 :             mapping.cc_number = cc_number;
     185            3 :             mapping.midi_channel = channel;
     186            3 :             mapping.target_type = learn_target_type_;
     187            5 :             mapping.mode = (learn_target_type_ == MidiTargetType::EffectBypass)
     188            2 :                                ? MidiMappingMode::Toggle
     189              :                                : MidiMappingMode::Continuous;
     190            3 :             mapping.effect_name = learn_effect_name_;
     191            3 :             mapping.param_name = learn_param_name_;
     192            3 :             add_mapping(mapping);
     193            3 :             learn_active_ = false;
     194            2 :             continue;
     195            3 :         }
     196              : 
     197              :         // Normal mode: resolve mapping and apply
     198          105 :         for (const auto& m : mappings_) {
     199           51 :             if (m.cc_number != cc_number) continue;
     200           48 :             if (m.midi_channel >= 0 && m.midi_channel != channel) continue;
     201           45 :             apply_mapping(m, cc_value, engine);
     202              :         }
     203              :     }
     204           38 : }
     205              : 
     206           45 : void MidiManager::apply_mapping(const MidiMapping& mapping, int cc_value, IAudioEngine& engine) {
     207           45 :     float normalized = static_cast<float>(cc_value) / 127.0f;
     208              : 
     209           45 :     switch (mapping.target_type) {
     210            6 :         case MidiTargetType::InputGain: {
     211            9 :             float gain = normalized * 2.0f;
     212            9 :             engine.set_input_gain(gain);
     213            9 :             break;
     214              :         }
     215            2 :         case MidiTargetType::OutputGain: {
     216            3 :             float gain = normalized * 2.0f;
     217            3 :             engine.set_output_gain(gain);
     218            3 :             break;
     219              :         }
     220            8 :         case MidiTargetType::EffectBypass: {
     221              :             // Find the effect by name
     222           12 :             auto& effects = engine.effects();
     223           12 :             for (int i = 0; i < static_cast<int>(effects.size()); ++i) {
     224           12 :                 if (effects[i]->name() == mapping.effect_name) {
     225           12 :                     bool is_pressed = (cc_value >= 64);
     226              : 
     227              :                     // Toggle on either edge: press (false→true) or release (true→false)
     228           12 :                     if (is_pressed != mapping.last_state) {
     229           12 :                         effects[i]->set_enabled(!effects[i]->is_enabled());
     230           12 :                         int node_id = find_node_id_for_effect(engine, effects[i], i);
     231           14 :                         engine.push_effect_enabled(node_id, effects[i]->is_enabled() ? 1.0f : 0.0f);
     232            4 :                     }
     233              : 
     234              :                     // Update state for next event
     235           12 :                     mapping.last_state = is_pressed;
     236           12 :                     break;
     237              :                 }
     238            0 :             }
     239            8 :             break;
     240              :         }
     241           14 :         case MidiTargetType::EffectParam: {
     242              :             // Check if it's a Mixer Gain mapping
     243           21 :             if (mapping.effect_name.find("Mixer_") == 0) {
     244            0 :                 int node_id = -1;
     245            0 :                 try {
     246            0 :                     node_id = std::stoi(mapping.effect_name.substr(6));
     247            0 :                 } catch (...) {
     248            0 :                 }
     249            0 :                 if (node_id != -1) {
     250            0 :                     int pin_idx = -1;
     251            0 :                     if (mapping.param_name.find("Gain ") == 0) {
     252            0 :                         try {
     253            0 :                             pin_idx = std::stoi(mapping.param_name.substr(5));
     254            0 :                         } catch (...) {
     255            0 :                         }
     256            0 :                     }
     257            0 :                     if (pin_idx != -1) {
     258            0 :                         float gain = normalized * 2.0f;
     259            0 :                         engine.graph().set_mixer_input_gain(node_id, pin_idx, gain);
     260            0 :                         engine.push_mixer_gain_change(node_id, pin_idx, gain);
     261            0 :                         break;
     262              :                     }
     263            0 :                 }
     264            0 :             }
     265              : 
     266              :             // Find the effect by name, then the param by name
     267           21 :             auto& effects = engine.effects();
     268           21 :             for (int i = 0; i < static_cast<int>(effects.size()); ++i) {
     269           18 :                 if (effects[i]->name() != mapping.effect_name) continue;
     270              : 
     271           18 :                 auto& params = effects[i]->params();
     272           21 :                 for (int p = 0; p < static_cast<int>(params.size()); ++p) {
     273           21 :                     if (params[p].name != mapping.param_name) continue;
     274              : 
     275           12 :                     float value =
     276           18 :                         params[p].min_val + normalized * (params[p].max_val - params[p].min_val);
     277           18 :                     params[p].value = value;  // GUI sync
     278           18 :                     int node_id = find_node_id_for_effect(engine, effects[i], i);
     279           18 :                     engine.push_param_change(node_id, p, value);  // Audio sync
     280           18 :                     break;
     281              :                 }
     282           12 :                 break;  // Only map to the first matching effect
     283              :             }
     284           14 :             break;
     285              :         }
     286              :     }
     287           45 : }
     288              : 
     289              : }  // namespace Amplitron
        

Generated by: LCOV version 2.0-1