LCOV - code coverage report
Current view: top level - src/MIDI_Inputs/MCU - VPotRing.hpp (source / functions) Coverage Total Hit
Test: 73449d9b107c772cf65493691543348214e5d5eb Lines: 68.2 % 85 58
Test Date: 2026-06-06 17:44:35 Functions: 77.8 % 36 28
Legend: Lines:     hit not hit

            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           12 :         : 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
        

Generated by: LCOV version 2.4-beta