Line data Source code
1 : #pragma once
2 :
3 : #include <AH/STL/algorithm>
4 : #include <AH/Timing/MillisMicrosTimer.hpp>
5 : #include <MIDI_Inputs/InterfaceMIDIInputElements.hpp>
6 : #include <MIDI_Inputs/MIDIInputElementMatchers.hpp>
7 :
8 : BEGIN_CS_NAMESPACE
9 :
10 : namespace MCU {
11 :
12 : /// Struct that keeps track of the value and overload indicator of a Mackie
13 : /// Control Universal VPot LED ring.
14 : struct VPotState {
15 : /// Constructor.
16 9 : VPotState(uint8_t value = 0) : value(value) {}
17 :
18 : uint8_t value; ///< The value representing the VPot position, mode and LED.
19 :
20 7 : bool update(uint8_t data) {
21 7 : data = sanitizeData(data);
22 7 : bool changed = data != this->value;
23 7 : this->value = data;
24 7 : return changed;
25 : }
26 :
27 : /// Determines how the VPot value is displayed using the LEDs.
28 : enum Mode {
29 : SingleDot = 0, ///< Single dot.
30 : BoostCut = 1, ///< Boost/Cut.
31 : Wrap = 2, ///< Wrap.
32 : Spread = 3, ///< Spread.
33 : };
34 :
35 : /// @name Data access
36 : /// @{
37 :
38 : /// Return the position of the V-Pot ring. [0, 11]
39 17 : uint8_t getPosition() const { return value & 0x0F; }
40 : /// Return the status of the center LED of the V-Pot ring.
41 11 : bool getCenterLed() const { return value & 0x40; }
42 : /// Return the mode of the V-Pot ring.
43 16 : Mode getMode() const { return static_cast<Mode>((value & 0x30) >> 4); }
44 :
45 : /// Get the first segment that should be on.
46 3 : uint8_t getStartOn() const {
47 3 : int8_t position = getPosition();
48 3 : if (position == 0)
49 1 : return 0;
50 2 : int8_t value = position - 1;
51 2 : switch (getMode()) {
52 0 : case SingleDot: return value;
53 2 : case BoostCut: return std::min(+value, 5);
54 0 : case Wrap: return 0;
55 0 : case Spread: return std::max(5 - value, 0);
56 0 : default: return 0;
57 : }
58 : }
59 :
60 : /// Get the first segment that should be off.
61 3 : uint8_t getStartOff() const {
62 3 : uint8_t value = getPosition();
63 3 : switch (getMode()) {
64 1 : case SingleDot: return value;
65 2 : case BoostCut: return std::max(+value, 6);
66 0 : case Wrap: return value;
67 0 : case Spread: return std::min(5 + value, 11);
68 0 : default: return 0;
69 : }
70 : }
71 :
72 : /// @}
73 :
74 : /// Make sure that the received value is valid and will not result in array
75 : /// out of bounds conditions.
76 7 : static uint8_t sanitizeData(uint8_t data) {
77 : // Maximum valid position value is 0xB.
78 7 : return (data & 0x0F) <= 0x0B ? data : ((data & 0xF0) | 0x0B);
79 : }
80 : };
81 :
82 : // -------------------------------------------------------------------------- //
83 :
84 : /**
85 : * @brief MIDI Input matcher for Mackie Control Universal VPot LED rings.
86 : *
87 : * In the Mackie Control Universal protocol, the VPot LED rings are updated
88 : * using Control Change events.
89 : * Each device (cable number) has eight VPots for the eight tracks. Only
90 : * MIDI channel 1 is used in the original protocol.
91 : *
92 : * The format of the MIDI message is as follows:
93 : * | Status | Data 1 | Data 2 |
94 : * |:-----------:|:-----------:|:-----------:|
95 : * | `1011 cccc` | `0011 0ttt` | `0pxx vvvv` |
96 : *
97 : * - `1011` (or `0xB`) is the status for Control Change events
98 : * - `cccc` is the MIDI channel [0-15]
99 : * - `ttt` is the track index [0-7]
100 : * - `p` is the state of the center LED
101 : * - `xx` is the display mode
102 : * - `vvvv` is the VPot value [0, 11]
103 : *
104 : * The display modes are defined in @ref MCU::VPotState::Mode.
105 : *
106 : * @ingroup MIDIInputMatchers
107 : */
108 : struct VPotMatcher : public TwoByteMIDIMatcher {
109 : /**
110 : * @brief Constructor.
111 : *
112 : * @param track
113 : * The track of the VPot [1, 8].
114 : * @param channelCN
115 : * The MIDI channel [CHANNEL_1, CHANNEL_16] and Cable Number
116 : * [CABLE_1, CABLE_16].
117 : */
118 1 : VPotMatcher(uint8_t track, MIDIChannelCable channelCN)
119 1 : : TwoByteMIDIMatcher({track + 0x30 - 1, channelCN}) {}
120 : };
121 :
122 : /// MIDI Input matcher for Mackie Control Universal VPot LED rings with bank
123 : /// support.
124 : /// @see @ref MCU::VPotMatcher
125 : /// @ingroup MIDIInputMatchers
126 : template <uint8_t BankSize>
127 : struct BankableVPotMatcher : public BankableTwoByteMIDIMatcher<BankSize> {
128 : /**
129 : * @brief Constructor.
130 : *
131 : * @param config
132 : * The bank configuration to use: the bank to add this element to,
133 : * and whether to change the address, channel or cable number.
134 : * @param track
135 : * The track of the VPot [1, 8].
136 : * @param channelCN
137 : * The MIDI channel [CHANNEL_1, CHANNEL_16] and Cable Number
138 : * [CABLE_1, CABLE_16].
139 : */
140 4 : BankableVPotMatcher(BankConfig<BankSize> config, uint8_t track,
141 : MIDIChannelCable channelCN)
142 : : BankableTwoByteMIDIMatcher<BankSize>(config,
143 4 : {track + 0x30 - 1, channelCN}) {}
144 : };
145 :
146 : // -------------------------------------------------------------------------- //
147 :
148 : /**
149 : * @brief A MIDI input element that represents a Mackie Control Universal VPot
150 : * ring.
151 : *
152 : * @ingroup MIDIInputElements
153 : */
154 : class VPotRing
155 : : public MatchingMIDIInputElement<MIDIMessageType::CONTROL_CHANGE,
156 : VPotMatcher>,
157 : public Interfaces::MCU::IVPot {
158 : public:
159 : using Matcher = VPotMatcher;
160 : using Parent
161 : = MatchingMIDIInputElement<MIDIMessageType::CONTROL_CHANGE, Matcher>;
162 :
163 : /**
164 : * @brief Constructor.
165 : *
166 : * @param track
167 : * The track of the VPot [1, 8].
168 : * @param channelCN
169 : * The MIDI channel [CHANNEL_1, CHANNEL_16] and Cable Number
170 : * [CABLE_1, CABLE_16].
171 : */
172 1 : VPotRing(uint8_t track, MIDIChannelCable channelCN = CHANNEL_1)
173 1 : : Parent({track, channelCN}) {}
174 :
175 : protected:
176 2 : bool handleUpdateImpl(typename Matcher::Result match) {
177 2 : return state.update(match.value);
178 : }
179 :
180 2 : void handleUpdate(typename Matcher::Result match) override {
181 2 : dirty |= handleUpdateImpl(match);
182 2 : }
183 :
184 : public:
185 : /// @name Data access
186 : /// @{
187 :
188 : /// Get the full state of the VPot ring.
189 : VPotState getState() const { return state; }
190 :
191 : /// @copydoc VPotState::getPosition
192 2 : uint8_t getPosition() const { return state.getPosition(); }
193 : /// @copydoc VPotState::getCenterLed
194 2 : bool getCenterLed() const override { return state.getCenterLed(); }
195 : /// @copydoc VPotState::getMode
196 2 : VPotState::Mode getMode() const { return state.getMode(); }
197 :
198 : /// @copydoc VPotState::getStartOn
199 0 : uint8_t getStartOn() const override { return state.getStartOn(); }
200 : /// @copydoc VPotState::getStartOff
201 0 : uint8_t getStartOff() const override { return state.getStartOff(); }
202 :
203 : /// @}
204 :
205 : /// Reset the state to zero.
206 0 : void reset() override {
207 0 : if (!ignoreReset) {
208 0 : state = {};
209 0 : dirty = true;
210 : }
211 0 : }
212 :
213 : private:
214 : VPotState state = {};
215 :
216 : public:
217 : /// Don't reset the state when calling the `reset` method. This is the
218 : /// default, because in the original MCU, VPots don't get reset when a
219 : /// Reset All Controllers message is received.
220 : bool ignoreReset = true;
221 : };
222 :
223 : // -------------------------------------------------------------------------- //
224 :
225 : namespace Bankable {
226 :
227 : /**
228 : * @brief A MIDI input element that represents a Mackie Control Universal VPot
229 : * ring. This version can be banked.
230 : *
231 : * @tparam BankSize
232 : * The number of banks.
233 : *
234 : * @ingroup BankableMIDIInputElements
235 : */
236 : template <uint8_t BankSize>
237 : class VPotRing
238 : : public BankableMatchingMIDIInputElement<MIDIMessageType::CONTROL_CHANGE,
239 : BankableVPotMatcher<BankSize>>,
240 : public Interfaces::MCU::IVPot {
241 : public:
242 : using Matcher = BankableVPotMatcher<BankSize>;
243 : using Parent
244 : = BankableMatchingMIDIInputElement<MIDIMessageType::CONTROL_CHANGE,
245 : Matcher>;
246 : /**
247 : * @brief Constructor.
248 : *
249 : * @param config
250 : * The bank configuration to use: the bank to add this element to,
251 : * and whether to change the address, channel or cable number.
252 : * @param track
253 : * The track of the VPot [1, 8].
254 : * @param channelCN
255 : * The MIDI channel [CHANNEL_1, CHANNEL_16] and Cable Number
256 : * [CABLE_1, CABLE_16].
257 : */
258 4 : VPotRing(BankConfig<BankSize> config, uint8_t track,
259 : MIDIChannelCable channelCN = CHANNEL_1)
260 4 : : Parent({config, track, channelCN}) {}
261 :
262 : protected:
263 5 : bool handleUpdateImpl(typename Matcher::Result match) {
264 10 : return states[match.bankIndex].update(match.value) &&
265 10 : match.bankIndex == this->getActiveBank();
266 : // Only mark dirty if the value of the active bank changed
267 : }
268 :
269 3 : void handleUpdate(typename Matcher::Result match) override {
270 3 : dirty |= handleUpdateImpl(match);
271 3 : }
272 :
273 : public:
274 : /// @name Data access
275 : /// @{
276 :
277 : /// Get the full state of the VPot ring. (For the given bank.)
278 30 : VPotState getState(uint8_t bank) const { return states[bank]; }
279 :
280 : /// @copydoc VPotState::getPosition
281 : /// (For the given bank.)
282 9 : uint8_t getPosition(uint8_t bank) const {
283 9 : return getState(bank).getPosition();
284 : }
285 : /// @copydoc VPotState::getCenterLed
286 : /// (For the given bank.)
287 9 : bool getCenterLed(uint8_t bank) const {
288 9 : return getState(bank).getCenterLed();
289 : }
290 : /// @copydoc VPotState::getMode
291 : /// (For the given bank.)
292 9 : VPotState::Mode getMode(uint8_t bank) const {
293 9 : return getState(bank).getMode();
294 : }
295 :
296 : /// @copydoc VPotState::getStartOn
297 : /// (For the given bank.)
298 0 : uint8_t getStartOn(uint8_t bank) const {
299 0 : return getState(bank).getStartOn();
300 : }
301 : /// @copydoc VPotState::getStartOff
302 : /// (For the given bank.)
303 0 : uint8_t getStartOff(uint8_t bank) const {
304 0 : return getState(bank).getStartOff();
305 : }
306 :
307 : /// Get the full state of the VPot ring. (For the active bank.)
308 3 : VPotState getState() const { return getState(this->getActiveBank()); }
309 :
310 : /// @copydoc VPotState::getPosition
311 : /// (For the active bank.)
312 9 : uint8_t getPosition() const { return getPosition(this->getActiveBank()); }
313 : /// @copydoc VPotState::getCenterLed
314 : /// (For the active bank.)
315 9 : bool getCenterLed() const override {
316 9 : return getCenterLed(this->getActiveBank());
317 : }
318 : /// @copydoc VPotState::getMode
319 : /// (For the active bank.)
320 9 : VPotState::Mode getMode() const { return getMode(this->getActiveBank()); }
321 :
322 : /// @copydoc VPotState::getStartOn
323 : /// (For the active bank.)
324 0 : uint8_t getStartOn() const override {
325 0 : return getStartOn(this->getActiveBank());
326 : }
327 : /// @copydoc VPotState::getStartOff
328 : /// (For the active bank.)
329 0 : uint8_t getStartOff() const override {
330 0 : return getStartOff(this->getActiveBank());
331 : }
332 :
333 : /// @}
334 :
335 : /// Reset all states to zero.
336 0 : void reset() override {
337 0 : if (!ignoreReset) {
338 0 : states = {{}};
339 0 : dirty = true;
340 : }
341 0 : }
342 :
343 : protected:
344 4 : void onBankSettingChange() override { dirty = true; }
345 :
346 : private:
347 : AH::Array<VPotState, BankSize> states = {{}};
348 :
349 : public:
350 : /// Don't reset the state when calling the `reset` method. This is the
351 : /// default, because in the original MCU, VPots don't get reset when a
352 : /// Reset All Controllers message is received.
353 : bool ignoreReset = true;
354 : };
355 :
356 : } // namespace Bankable
357 :
358 : } // namespace MCU
359 :
360 : END_CS_NAMESPACE
|