LCOV - code coverage report
Current view: top level - src/MIDI_Inputs/MCU - VPotRing.hpp (source / functions) Hit Total Coverage
Test: 169c36a3797bc662d84b5726f34a3f37d3c58247 Lines: 58 85 68.2 %
Date: 2024-11-09 15:32:27 Functions: 28 36 77.8 %
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           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

Generated by: LCOV version 1.15