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