Line data Source code
1 : #include "gui/views/gui_midi.h"
2 :
3 : #include <imgui.h>
4 :
5 : #include <cstdio>
6 :
7 : #include "gui/theme/theme.h"
8 : #include "midi/midi_manager.h"
9 :
10 : namespace Amplitron {
11 :
12 132 : GuiMidi::GuiMidi(IMidiManager& midi) : midi_(midi) {}
13 :
14 : // ---------------------------------------------------------------------------
15 : // MIDI Settings window
16 : // ---------------------------------------------------------------------------
17 :
18 6 : static const char* target_type_label(MidiTargetType type) {
19 6 : switch (type) {
20 0 : case MidiTargetType::EffectParam:
21 0 : return "Param";
22 0 : case MidiTargetType::EffectBypass:
23 0 : return "Bypass";
24 2 : case MidiTargetType::InputGain:
25 3 : return "Input Gain";
26 2 : case MidiTargetType::OutputGain:
27 3 : return "Output Gain";
28 0 : default:
29 0 : break; // Fail at compile time if enum is extended
30 : }
31 0 : return "?"; // Unreachable if all cases handled
32 2 : }
33 :
34 12 : static const char* mode_label(MidiMappingMode mode) {
35 12 : switch (mode) {
36 3 : case MidiMappingMode::Continuous:
37 6 : return "Continuous";
38 2 : case MidiMappingMode::Toggle:
39 3 : return "Toggle";
40 0 : default:
41 0 : break; // Fail at compile time if enum is extended
42 : }
43 0 : return "?"; // Unreachable if all cases handled
44 4 : }
45 :
46 6 : void GuiMidi::render(bool& show) {
47 6 : ImGui::SetNextWindowSize(ImVec2(620, 480), ImGuiCond_FirstUseEver);
48 6 : if (!ImGui::Begin("MIDI Settings", &show)) {
49 0 : ImGui::End();
50 0 : return;
51 : }
52 :
53 : // --- Port selection ---
54 6 : ImGui::TextColored(Theme::Gold(), "MIDI INPUT PORT");
55 6 : ImGui::BeginChild("PortSection", ImVec2(0, 70), true);
56 :
57 : // Refresh cached ports on first render or when user clicks refresh
58 6 : cached_ports_ = midi_.get_available_ports();
59 :
60 6 : if (cached_ports_.empty()) {
61 6 : ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "No MIDI input devices detected");
62 2 : } else {
63 0 : int current = midi_.current_port();
64 0 : std::string preview = (current >= 0) ? midi_.current_port_name() : "(none)";
65 :
66 0 : ImGui::SetNextItemWidth(400);
67 0 : if (ImGui::BeginCombo("##MidiPort", preview.c_str())) {
68 : // "(none)" option to close port
69 0 : if (ImGui::Selectable("(none)", current < 0)) {
70 0 : midi_.close_port();
71 0 : }
72 0 : for (int i = 0; i < static_cast<int>(cached_ports_.size()); ++i) {
73 0 : bool selected = (i == current);
74 0 : if (ImGui::Selectable(cached_ports_[i].c_str(), selected)) {
75 0 : midi_.open_port(i);
76 0 : }
77 0 : }
78 0 : ImGui::EndCombo();
79 0 : }
80 0 : }
81 :
82 6 : ImGui::SameLine();
83 6 : if (ImGui::SmallButton("Refresh")) {
84 : // Re-query ports from RtMidi
85 0 : cached_ports_ = midi_.get_available_ports();
86 0 : }
87 :
88 6 : ImGui::EndChild();
89 :
90 6 : ImGui::Spacing();
91 :
92 : // --- Learn status ---
93 6 : if (midi_.is_learning()) {
94 0 : ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", midi_.learn_status().c_str());
95 0 : ImGui::SameLine();
96 0 : if (ImGui::SmallButton("Cancel Learn")) {
97 0 : midi_.cancel_learn();
98 0 : }
99 0 : ImGui::Spacing();
100 0 : }
101 :
102 : // --- Mapping table ---
103 6 : ImGui::TextColored(Theme::Gold(), "CC MAPPINGS");
104 :
105 6 : if (ImGui::SmallButton("Add Defaults")) {
106 0 : midi_.install_default_mappings();
107 0 : }
108 6 : ImGui::SameLine();
109 6 : if (ImGui::SmallButton("Clear All")) {
110 0 : midi_.clear_mappings();
111 0 : }
112 :
113 6 : ImGui::Spacing();
114 :
115 6 : const auto& mappings = midi_.mappings();
116 6 : if (mappings.empty()) {
117 3 : ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
118 : "No mappings. Right-click any knob to MIDI Learn, "
119 : "or click \"Add Defaults\".");
120 1 : } else {
121 3 : ImGui::BeginChild("MappingTable", ImVec2(0, 0), true);
122 :
123 : // Table header
124 3 : if (ImGui::BeginTable("##MidiMappings", 6,
125 : ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
126 : ImGuiTableFlags_SizingStretchProp)) {
127 3 : ImGui::TableSetupColumn("CC#", ImGuiTableColumnFlags_WidthFixed, 40);
128 3 : ImGui::TableSetupColumn("Ch", ImGuiTableColumnFlags_WidthFixed, 35);
129 3 : ImGui::TableSetupColumn("Target", ImGuiTableColumnFlags_WidthStretch);
130 3 : ImGui::TableSetupColumn("Param", ImGuiTableColumnFlags_WidthStretch);
131 3 : ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_WidthFixed, 80);
132 3 : ImGui::TableSetupColumn("##Del", ImGuiTableColumnFlags_WidthFixed, 25);
133 3 : ImGui::TableHeadersRow();
134 :
135 3 : int remove_index = -1;
136 15 : for (int i = 0; i < static_cast<int>(mappings.size()); ++i) {
137 12 : const auto& m = mappings[i];
138 12 : ImGui::TableNextRow();
139 :
140 12 : ImGui::TableNextColumn();
141 12 : ImGui::Text("%d", m.cc_number);
142 :
143 12 : ImGui::TableNextColumn();
144 12 : if (m.midi_channel < 0) {
145 3 : ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "All");
146 1 : } else {
147 9 : ImGui::Text("%d", m.midi_channel + 1); // Display 1-based
148 : }
149 :
150 12 : ImGui::TableNextColumn();
151 12 : switch (m.target_type) {
152 4 : case MidiTargetType::EffectParam:
153 2 : case MidiTargetType::EffectBypass:
154 6 : ImGui::Text("%s", m.effect_name.c_str());
155 4 : break;
156 4 : default:
157 6 : ImGui::Text("%s", target_type_label(m.target_type));
158 4 : break;
159 : }
160 :
161 12 : ImGui::TableNextColumn();
162 12 : switch (m.target_type) {
163 2 : case MidiTargetType::EffectParam:
164 3 : ImGui::Text("%s", m.param_name.c_str());
165 2 : break;
166 2 : case MidiTargetType::EffectBypass:
167 3 : ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "(bypass)");
168 3 : break;
169 4 : default:
170 6 : ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "-");
171 6 : break;
172 : }
173 :
174 12 : ImGui::TableNextColumn();
175 13 : ImGui::Text("%s", mode_label(m.mode));
176 :
177 12 : ImGui::TableNextColumn();
178 4 : char btn_id[16];
179 12 : snprintf(btn_id, sizeof(btn_id), "X##%d", i);
180 12 : if (ImGui::SmallButton(btn_id)) {
181 0 : remove_index = i;
182 0 : }
183 4 : }
184 :
185 3 : ImGui::EndTable();
186 :
187 3 : if (remove_index >= 0) {
188 0 : midi_.remove_mapping(remove_index);
189 0 : }
190 1 : }
191 :
192 3 : ImGui::EndChild();
193 : }
194 :
195 6 : ImGui::End();
196 2 : }
197 :
198 : // ---------------------------------------------------------------------------
199 : // MIDI Learn menu items (for knob right-click popups)
200 : // ---------------------------------------------------------------------------
201 :
202 6 : bool GuiMidi::render_learn_menu_item(const std::string& effect_name,
203 : const std::string& param_name) {
204 : // Check if this param already has a mapping
205 6 : bool has_mapping = false;
206 6 : int mapping_cc = -1;
207 6 : for (const auto& m : midi_.mappings()) {
208 4 : if (m.target_type == MidiTargetType::EffectParam && m.effect_name == effect_name &&
209 3 : m.param_name == param_name) {
210 3 : has_mapping = true;
211 3 : mapping_cc = m.cc_number;
212 3 : break;
213 : }
214 : }
215 :
216 5 : if (has_mapping) {
217 1 : char label[64];
218 3 : snprintf(label, sizeof(label), "MIDI: CC %d", mapping_cc);
219 3 : ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "%s", label);
220 :
221 3 : if (ImGui::MenuItem("Remove MIDI Mapping")) {
222 0 : midi_.remove_mapping_for_param(effect_name, param_name);
223 0 : return true;
224 : }
225 1 : }
226 :
227 6 : if (ImGui::MenuItem("MIDI Learn")) {
228 0 : midi_.start_learn(MidiTargetType::EffectParam, effect_name, param_name);
229 0 : return true;
230 : }
231 4 : return false;
232 2 : }
233 :
234 3 : bool GuiMidi::render_learn_bypass_item(const std::string& effect_name) {
235 3 : if (ImGui::MenuItem("MIDI Learn (Bypass)")) {
236 0 : midi_.start_learn(MidiTargetType::EffectBypass, effect_name, "");
237 0 : return true;
238 : }
239 2 : return false;
240 1 : }
241 :
242 3 : bool GuiMidi::render_remove_mapping_item(const std::string& effect_name,
243 : const std::string& param_name) {
244 3 : int remove_idx = -1;
245 3 : const auto& mappings = midi_.mappings();
246 3 : for (int i = 0; i < static_cast<int>(mappings.size()); ++i) {
247 5 : if (mappings[i].target_type == MidiTargetType::EffectParam &&
248 4 : mappings[i].effect_name == effect_name && mappings[i].param_name == param_name) {
249 2 : remove_idx = i;
250 2 : break;
251 : }
252 0 : }
253 :
254 3 : if (remove_idx >= 0) {
255 3 : if (ImGui::MenuItem("Remove MIDI Mapping")) {
256 0 : midi_.remove_mapping(remove_idx);
257 0 : return true;
258 : }
259 1 : }
260 2 : return false;
261 1 : }
262 :
263 3 : bool GuiMidi::render_remove_bypass_item(const std::string& effect_name) {
264 3 : int remove_idx = -1;
265 3 : const auto& mappings = midi_.mappings();
266 6 : for (int i = 0; i < static_cast<int>(mappings.size()); ++i) {
267 7 : if (mappings[i].target_type == MidiTargetType::EffectBypass &&
268 3 : mappings[i].effect_name == effect_name) {
269 2 : remove_idx = i;
270 2 : break;
271 : }
272 1 : }
273 :
274 3 : if (remove_idx >= 0) {
275 3 : if (ImGui::MenuItem("Remove MIDI Bypass Mapping")) {
276 0 : midi_.remove_mapping(remove_idx);
277 0 : return true;
278 : }
279 1 : }
280 2 : return false;
281 1 : }
282 :
283 597 : std::string GuiMidi::get_mapping_info(const std::string& effect_name,
284 : const std::string& param_name) const {
285 612 : for (const auto& m : midi_.mappings()) {
286 38 : if (m.target_type == MidiTargetType::EffectParam && m.effect_name == effect_name &&
287 24 : m.param_name == param_name) {
288 25 : return "\n\n[MIDI: CC" + std::to_string(m.cc_number) +
289 25 : (m.mode == MidiMappingMode::Toggle ? " Toggle]" : " Range]");
290 : }
291 : }
292 776 : return "";
293 199 : }
294 :
295 : } // namespace Amplitron
|