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
|