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
|