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 : public MatchingMIDIInputElement<MIDIMessageType::ControlChange,
155 : VPotMatcher>,
156 : public Interfaces::MCU::IVPot {
157 : public:
158 : using Matcher = VPotMatcher;
159 : using Parent =
160 : MatchingMIDIInputElement<MIDIMessageType::ControlChange, Matcher>;
161 :
162 : /**
163 : * @brief Constructor.
164 : *
165 : * @param track
166 : * The track of the VPot [1, 8].
167 : * @param channelCN
168 : * The MIDI channel [Channel_1, Channel_16] and Cable Number
169 : * [Cable_1, Cable_16].
170 : */
171 1 : VPotRing(uint8_t track, MIDIChannelCable channelCN = Channel_1)
172 1 : : Parent({track, channelCN}) {}
173 :
174 : protected:
175 2 : bool handleUpdateImpl(typename Matcher::Result match) {
176 2 : return state.update(match.value);
177 : }
178 :
179 2 : void handleUpdate(typename Matcher::Result match) override {
180 2 : dirty |= handleUpdateImpl(match);
181 2 : }
182 :
183 : public:
184 : /// @name Data access
185 : /// @{
186 :
187 : /// Get the full state of the VPot ring.
188 : VPotState getState() const { return state; }
189 :
190 : /// @copydoc VPotState::getPosition
191 2 : uint8_t getPosition() const { return state.getPosition(); }
192 : /// @copydoc VPotState::getCenterLed
193 2 : bool getCenterLed() const override { return state.getCenterLed(); }
194 : /// @copydoc VPotState::getMode
195 2 : VPotState::Mode getMode() const { return state.getMode(); }
196 :
197 : /// @copydoc VPotState::getStartOn
198 0 : uint8_t getStartOn() const override { return state.getStartOn(); }
199 : /// @copydoc VPotState::getStartOff
200 0 : uint8_t getStartOff() const override { return state.getStartOff(); }
201 :
202 : /// @}
203 :
204 : /// Reset the state to zero.
205 0 : void reset() override {
206 0 : if (!ignoreReset) {
207 0 : state = {};
208 0 : dirty = true;
209 : }
210 0 : }
211 :
212 : private:
213 : VPotState state = {};
214 :
215 : public:
216 : /// Don't reset the state when calling the `reset` method. This is the
217 : /// default, because in the original MCU, VPots don't get reset when a
218 : /// Reset All Controllers message is received.
219 : bool ignoreReset = true;
220 : };
221 :
222 : // -------------------------------------------------------------------------- //
223 :
224 : namespace Bankable {
225 :
226 : /**
227 : * @brief A MIDI input element that represents a Mackie Control Universal VPot
228 : * ring. This version can be banked.
229 : *
230 : * @tparam BankSize
231 : * The number of banks.
232 : *
233 : * @ingroup BankableMIDIInputElements
234 : */
235 : template <uint8_t BankSize>
236 : class VPotRing
237 : : public BankableMatchingMIDIInputElement<MIDIMessageType::ControlChange,
238 : BankableVPotMatcher<BankSize>>,
239 : public Interfaces::MCU::IVPot {
240 : public:
241 : using Matcher = BankableVPotMatcher<BankSize>;
242 : using Parent =
243 : BankableMatchingMIDIInputElement<MIDIMessageType::ControlChange,
244 : Matcher>;
245 : /**
246 : * @brief Constructor.
247 : *
248 : * @param config
249 : * The bank configuration to use: the bank to add this element to,
250 : * and whether to change the address, channel or cable number.
251 : * @param track
252 : * The track of the VPot [1, 8].
253 : * @param channelCN
254 : * The MIDI channel [Channel_1, Channel_16] and Cable Number
255 : * [Cable_1, Cable_16].
256 : */
257 4 : VPotRing(BankConfig<BankSize> config, uint8_t track,
258 : MIDIChannelCable channelCN = Channel_1)
259 4 : : Parent({config, track, channelCN}) {}
260 :
261 : protected:
262 5 : bool handleUpdateImpl(typename Matcher::Result match) {
263 10 : return states[match.bankIndex].update(match.value) &&
264 10 : match.bankIndex == this->getActiveBank();
265 : // Only mark dirty if the value of the active bank changed
266 : }
267 :
268 3 : void handleUpdate(typename Matcher::Result match) override {
269 3 : dirty |= handleUpdateImpl(match);
270 3 : }
271 :
272 : public:
273 : /// @name Data access
274 : /// @{
275 :
276 : /// Get the full state of the VPot ring. (For the given bank.)
277 30 : VPotState getState(uint8_t bank) const { return states[bank]; }
278 :
279 : /// @copydoc VPotState::getPosition
280 : /// (For the given bank.)
281 9 : uint8_t getPosition(uint8_t bank) const {
282 9 : return getState(bank).getPosition();
283 : }
284 : /// @copydoc VPotState::getCenterLed
285 : /// (For the given bank.)
286 9 : bool getCenterLed(uint8_t bank) const {
287 9 : return getState(bank).getCenterLed();
288 : }
289 : /// @copydoc VPotState::getMode
290 : /// (For the given bank.)
291 9 : VPotState::Mode getMode(uint8_t bank) const {
292 9 : return getState(bank).getMode();
293 : }
294 :
295 : /// @copydoc VPotState::getStartOn
296 : /// (For the given bank.)
297 0 : uint8_t getStartOn(uint8_t bank) const {
298 0 : return getState(bank).getStartOn();
299 : }
300 : /// @copydoc VPotState::getStartOff
301 : /// (For the given bank.)
302 0 : uint8_t getStartOff(uint8_t bank) const {
303 0 : return getState(bank).getStartOff();
304 : }
305 :
306 : /// Get the full state of the VPot ring. (For the active bank.)
307 3 : VPotState getState() const { return getState(this->getActiveBank()); }
308 :
309 : /// @copydoc VPotState::getPosition
310 : /// (For the active bank.)
311 9 : uint8_t getPosition() const { return getPosition(this->getActiveBank()); }
312 : /// @copydoc VPotState::getCenterLed
313 : /// (For the active bank.)
314 9 : bool getCenterLed() const override {
315 9 : return getCenterLed(this->getActiveBank());
316 : }
317 : /// @copydoc VPotState::getMode
318 : /// (For the active bank.)
319 9 : VPotState::Mode getMode() const { return getMode(this->getActiveBank()); }
320 :
321 : /// @copydoc VPotState::getStartOn
322 : /// (For the active bank.)
323 0 : uint8_t getStartOn() const override {
324 0 : return getStartOn(this->getActiveBank());
325 : }
326 : /// @copydoc VPotState::getStartOff
327 : /// (For the active bank.)
328 0 : uint8_t getStartOff() const override {
329 0 : return getStartOff(this->getActiveBank());
330 : }
331 :
332 : /// @}
333 :
334 : /// Reset all states to zero.
335 0 : void reset() override {
336 0 : if (!ignoreReset) {
337 0 : states = {{}};
338 0 : dirty = true;
339 : }
340 0 : }
341 :
342 : protected:
343 4 : void onBankSettingChange() override { dirty = true; }
344 :
345 : private:
346 : AH::Array<VPotState, BankSize> states = {{}};
347 :
348 : public:
349 : /// Don't reset the state when calling the `reset` method. This is the
350 : /// default, because in the original MCU, VPots don't get reset when a
351 : /// Reset All Controllers message is received.
352 : bool ignoreReset = true;
353 : };
354 :
355 : } // namespace Bankable
356 :
357 : } // namespace MCU
358 :
359 : END_CS_NAMESPACE
|