LCOV - code coverage report
Current view: top level - src/midi - midi_manager_mapping.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 91.0 % 166 151
Test Date: 2026-06-03 09:13:19 Functions: 100.0 % 11 11

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

Generated by: LCOV version 2.0-1