Line data Source code
1 : #pragma once
2 :
3 : #include <string>
4 : #include <vector>
5 : #include <cstdint>
6 :
7 : #ifndef AMPLITRON_NO_MIDI
8 : #include "audio/utils/spsc_queue.h"
9 : #endif
10 :
11 : namespace Amplitron {
12 :
13 : class AudioEngine;
14 :
15 : // Raw MIDI event pushed from the RtMidi callback thread.
16 : // Must be trivially copyable for SPSCQueue.
17 47616 : struct MidiEvent {
18 : uint8_t status; // e.g. 0xB0 = CC on channel 0
19 : uint8_t data1; // CC number (0-127)
20 : uint8_t data2; // CC value (0-127)
21 23808 : uint8_t pad = 0; // Pad to 4 bytes
22 : };
23 :
24 : enum class MidiMappingMode : uint8_t {
25 : Continuous, // CC 0-127 maps linearly to param [min..max]
26 : Toggle, // CC >= 64 -> on, CC < 64 -> off
27 : };
28 :
29 : enum class MidiTargetType : uint8_t {
30 : EffectParam, // Maps to a specific effect parameter
31 : EffectBypass, // Maps to effect enabled/disabled
32 : InputGain, // Maps to master input gain
33 : OutputGain, // Maps to master output gain
34 : };
35 :
36 595 : struct MidiMapping {
37 92 : int cc_number = 0; // 0-127
38 92 : int midi_channel = -1; // 0-15, or -1 for "any channel"
39 92 : MidiTargetType target_type = MidiTargetType::EffectParam;
40 92 : MidiMappingMode mode = MidiMappingMode::Continuous;
41 :
42 : std::string effect_name; // For EffectParam/EffectBypass targets
43 : std::string param_name; // For EffectParam targets only
44 92 : mutable bool last_state = false; // Tracks pedal state for Toggle mode
45 : };
46 :
47 : #ifdef AMPLITRON_NO_MIDI
48 :
49 : // Stub implementation for non-desktop platforms (web, mobile)
50 : class MidiManager {
51 : friend class TestAccessor;
52 : public:
53 : MidiManager() = default;
54 : ~MidiManager() = default;
55 :
56 : bool initialize() { return false; }
57 : void shutdown() {}
58 :
59 : std::vector<std::string> get_available_ports() const { return {}; }
60 : bool open_port(int) { return false; }
61 : void close_port() {}
62 : int current_port() const { return -1; }
63 : std::string current_port_name() const { return ""; }
64 : bool is_port_open() const { return false; }
65 :
66 : void add_mapping(const MidiMapping&) {}
67 : void remove_mapping(int) {}
68 : void remove_mapping_for_param(const std::string&, const std::string&) {}
69 : void clear_mappings() {}
70 : const std::vector<MidiMapping>& mappings() const {
71 : static std::vector<MidiMapping> empty;
72 : return empty;
73 : }
74 :
75 : void install_default_mappings() {}
76 :
77 : void start_learn(MidiTargetType, const std::string&, const std::string&) {}
78 : void cancel_learn() {}
79 : bool is_learning() const { return false; }
80 : std::string learn_status() const { return ""; }
81 : const std::string& learn_effect_name() const { static std::string empty; return empty; }
82 : const std::string& learn_param_name() const { static std::string empty; return empty; }
83 :
84 : void poll(AudioEngine&) {}
85 : void save_config() const {}
86 : void load_config() {}
87 :
88 : void inject_event(const MidiEvent&) {}
89 : };
90 :
91 : #else
92 :
93 : /**
94 : * @brief MIDI input manager with CC-to-parameter mapping and MIDI learn.
95 : *
96 : * Runs a lock-free SPSC queue between RtMidi's callback thread and the
97 : * GUI thread. The GUI thread calls poll() each frame to drain events and
98 : * route CC values through the existing engine.push_param_change() path.
99 : */
100 : class MidiManager {
101 : friend class TestAccessor;
102 : public:
103 : MidiManager();
104 : ~MidiManager();
105 :
106 : /** @brief Open the first available MIDI input port. @return true on success. */
107 : bool initialize();
108 :
109 : /** @brief Close the MIDI port and release resources. */
110 : void shutdown();
111 :
112 : // --- Port management ---
113 :
114 : /** @brief List available MIDI input port names. */
115 : std::vector<std::string> get_available_ports() const;
116 :
117 : /** @brief Open a specific MIDI input port by index. @return true on success. */
118 : bool open_port(int port_index);
119 :
120 : /** @brief Close the currently open port. */
121 : void close_port();
122 :
123 : /** @brief Return the index of the currently open port, or -1 if none. */
124 0 : int current_port() const { return current_port_; }
125 :
126 : /** @brief Return the name of the currently open port, or empty string. */
127 8 : std::string current_port_name() const { return current_port_name_; }
128 :
129 : /** @brief Return true if a MIDI port is currently open. */
130 16 : bool is_port_open() const { return current_port_ >= 0; }
131 :
132 : // --- Mapping management ---
133 :
134 : void add_mapping(const MidiMapping& mapping);
135 : void remove_mapping(int index);
136 : void remove_mapping_for_param(const std::string& effect_name, const std::string& param_name);
137 : void clear_mappings();
138 783 : const std::vector<MidiMapping>& mappings() const { return mappings_; }
139 :
140 : /** @brief Install default CC mappings (CC7, CC11, CC64, CC74). */
141 : void install_default_mappings();
142 :
143 : // --- MIDI Learn ---
144 :
145 : /**
146 : * @brief Enter learn mode: the next CC event received will be bound to the given target.
147 : */
148 : void start_learn(MidiTargetType type, const std::string& effect_name, const std::string& param_name);
149 :
150 : /** @brief Cancel learn mode without creating a mapping. */
151 : void cancel_learn();
152 :
153 : /** @brief Return true if learn mode is active. */
154 1485 : bool is_learning() const { return learn_active_; }
155 :
156 : /** @brief Human-readable status for the learn indicator, or empty. */
157 : std::string learn_status() const;
158 :
159 18 : const std::string& learn_effect_name() const { return learn_effect_name_; }
160 18 : const std::string& learn_param_name() const { return learn_param_name_; }
161 :
162 : // --- Poll (called from GUI thread each frame) ---
163 :
164 : /**
165 : * @brief Drain the MIDI event queue and apply CC mappings.
166 : *
167 : * For each CC event:
168 : * - If learn mode is active, captures the CC and creates a mapping.
169 : * - Otherwise, resolves the mapping target and pushes the value to the engine.
170 : */
171 : void poll(AudioEngine& engine);
172 :
173 : // --- Persistence ---
174 :
175 : /** @brief Save mappings and port preference to midi_config.json. */
176 : void save_config() const;
177 :
178 : /** @brief Load mappings and port preference from midi_config.json. */
179 : void load_config();
180 :
181 : /**
182 : * @brief Push a MIDI event into the queue from test code.
183 : *
184 : * This is public so unit tests can inject events without hardware.
185 : */
186 : void inject_event(const MidiEvent& event);
187 :
188 : private:
189 : static void midi_callback(double timestamp, std::vector<unsigned char>* message, void* user_data);
190 :
191 : static std::string get_config_path();
192 :
193 : void* midi_in_ = nullptr; // RtMidiIn* (opaque to avoid header dependency)
194 : int current_port_ = -1;
195 : std::string current_port_name_;
196 :
197 : SPSCQueue<MidiEvent, 256> midi_queue_;
198 : std::vector<MidiMapping> mappings_;
199 :
200 : // Learn state
201 : bool learn_active_ = false;
202 : MidiTargetType learn_target_type_ = MidiTargetType::EffectParam;
203 : std::string learn_effect_name_;
204 : std::string learn_param_name_;
205 :
206 : // Helpers
207 : void apply_mapping(const MidiMapping& mapping, int cc_value, AudioEngine& engine);
208 : std::string mappings_to_json() const;
209 : bool mappings_from_json(const std::string& json);
210 : };
211 :
212 : #endif // AMPLITRON_NO_MIDI
213 :
214 : } // namespace Amplitron
|