LCOV - code coverage report
Current view: top level - src/MIDI_Interfaces - USBMIDI_Sender.hpp (source / functions) Hit Total Coverage
Test: 3a807a259ebe0769dd942f7f612dca5273937539 Lines: 52 67 77.6 %
Date: 2024-03-24 17:16:54 Functions: 6 7 85.7 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : #pragma once
       2             : 
       3             : #include <MIDI_Parsers/MIDI_MessageTypes.hpp>
       4             : #include <Settings/SettingsWrapper.hpp>
       5             : 
       6             : BEGIN_CS_NAMESPACE
       7             : 
       8             : /**
       9             :  * @brief   A class for sending MIDI USB messages. Includes support for chunked
      10             :  *          MIDI USB SysEx messages.
      11             :  */
      12             : class USBMIDI_Sender {
      13             :   public:
      14             :     /// Send a MIDI Channel message using the given sender.
      15             :     template <class Send>
      16             :     void sendChannelMessage(ChannelMessage, Send &&send);
      17             : 
      18             :     /// Send a MIDI System Common message using the given sender.
      19             :     template <class Send>
      20             :     void sendSysCommonMessage(SysCommonMessage, Send &&send);
      21             : 
      22             :     /// Send a MIDI Real-Time message using the given sender.
      23             :     template <class Send>
      24             :     void sendRealTimeMessage(RealTimeMessage, Send &&send);
      25             : 
      26             :     /// Send a MIDI System Exclusive message using the given sender.
      27             :     /// Message may be chunked, remaining bytes are stored until next chunk is
      28             :     /// sent.
      29             :     template <class Send>
      30             :     void sendSysEx(SysExMessage, Send &&send);
      31             : 
      32             :     /// Send a MIDI System Exclusive message using the given sender.
      33             :     /// Message should be complete, i.e. from SysEx Start to SysEx End.
      34             :     template <class Send>
      35             :     void sendFullSysEx(SysExMessage, Send &&send);
      36             : 
      37             : #if !NO_SYSEX_OUTPUT
      38             :   private:
      39             :     /// Send a single SysEx starts or continues USB packet. Exactly 3 bytes are
      40             :     /// sent. The `data` pointer is not incremented.
      41             :     template <class Send>
      42             :     void sendSysExStartCont1(const uint8_t *data, Cable cable, Send &send);
      43             :     /// Send as many SysEx starts or continues USB packets, such that the
      44             :     /// remaining length is 3, 2 or 1 byte. The `data` pointer is incremented,
      45             :     /// and the `length` is decremented.
      46             :     /// The reason for leaving 3, 2 or 1 bytes remaining is so the message can
      47             :     /// be finished using a SysExEnd USB packet, which has to have 3, 2 or 1
      48             :     /// bytes.
      49             :     template <class Send>
      50             :     void sendSysExStartCont(const uint8_t *&data, uint16_t &length, Cable cable,
      51             :                             Send &send);
      52             :     /// Send a SysExEnd USB packet. The `length` should be either 3, 2 or 1
      53             :     /// bytes, and the last byte of `data` should be a SysExEnd byte.
      54             :     template <class Send>
      55             :     void sendSysExEnd(const uint8_t *data, uint16_t length, Cable cable,
      56             :                       Send &send);
      57             : 
      58             :   private:
      59             :     /// Stores remainder of outgoing SysEx chunks. Each USB packet (except the
      60             :     /// last one) should contain a multiple of 3 SysEx bytes. If the SysEx chunk
      61             :     /// size is not a multiple of 3, there will be remaining bytes that can't be
      62             :     /// sent yet, until the next chunk arrives. See the comments in the
      63             :     /// @ref USBMIDI_Sender::sendSysEx() implementation for more details.
      64             :     uint8_t storedSysExData[16][3];
      65             :     /// Number of remaining SysEx bytes stored.
      66             :     uint8_t storedSysExLength[16] = {};
      67             : #endif
      68             : 
      69             :   private:
      70             :     using CIN = MIDICodeIndexNumber;
      71             : };
      72             : 
      73             : template <class Send>
      74           3 : void USBMIDI_Sender::sendChannelMessage(ChannelMessage msg, Send &&send) {
      75           3 :     send(msg.cable, CIN(msg.header >> 4), // CN|CIN
      76           3 :          msg.header,                      // status
      77           3 :          msg.data1,                       // data 1
      78           3 :          msg.data2);                      // data 2
      79           3 : }
      80             : 
      81             : template <class Send>
      82           0 : void USBMIDI_Sender::sendSysCommonMessage(SysCommonMessage msg, Send &&send) {
      83           0 :     auto cn = msg.cable;
      84           0 :     switch (msg.getNumberOfDataBytes()) {
      85           0 :         case 2: // 3B
      86           0 :             send(cn, CIN::SystemCommon3B, msg.header, msg.data1, msg.data2);
      87           0 :             break;
      88           0 :         case 1: // 2B
      89           0 :             send(cn, CIN::SystemCommon2B, msg.header, msg.data1, 0);
      90           0 :             break;
      91           0 :         case 0: // 1B
      92           0 :             send(cn, CIN::SystemCommon1B, msg.header, 0, 0);
      93           0 :             break;
      94           0 :         default: break;
      95             :     }
      96           0 : }
      97             : 
      98             : template <class Send>
      99           1 : void USBMIDI_Sender::sendRealTimeMessage(RealTimeMessage msg, Send &&send) {
     100           1 :     send(msg.cable, CIN::SingleByte, msg.message, 0, 0);
     101           1 : }
     102             : 
     103             : // This is the readable documentation version for sending full SysEx messages,
     104             : // the chunked version below is more complicated, but the principle is similar.
     105             : template <class Send>
     106             : void USBMIDI_Sender::sendFullSysEx(SysExMessage msg, Send &&send) {
     107             :     size_t length = msg.length;
     108             :     const uint8_t *data = msg.data;
     109             :     Cable cn = msg.cable;
     110             :     while (length > 3) {
     111             :         send(cn, CIN::SysExStartCont, data[0], data[1], data[2]);
     112             :         data += 3;
     113             :         length -= 3;
     114             :     }
     115             :     switch (length) {
     116             :         case 3: send(cn, CIN::SysExEnd3B, data[0], data[1], data[2]); break;
     117             :         case 2: send(cn, CIN::SysExEnd2B, data[0], data[1], 0); break;
     118             :         case 1: send(cn, CIN::SysExEnd1B, data[0], 0, 0); break;
     119             :         default: break;
     120             :     }
     121             : }
     122             : 
     123             : #if !NO_SYSEX_OUTPUT
     124             : 
     125             : template <class Send>
     126          15 : void USBMIDI_Sender::sendSysExStartCont1(const uint8_t *data, Cable cable,
     127             :                                          Send &send) {
     128          15 :     send(cable, CIN::SysExStartCont, data[0], data[1], data[2]);
     129          15 : }
     130             : 
     131             : template <class Send>
     132           8 : void USBMIDI_Sender::sendSysExStartCont(const uint8_t *&data, uint16_t &length,
     133             :                                         Cable cable, Send &send) {
     134          11 :     while (length > 3) {
     135           3 :         sendSysExStartCont1(data, cable, send);
     136           3 :         data += 3;
     137           3 :         length -= 3;
     138             :     }
     139           8 : }
     140             : 
     141             : template <class Send>
     142           9 : void USBMIDI_Sender::sendSysExEnd(const uint8_t *data, uint16_t length,
     143             :                                   Cable cn, Send &send) {
     144           9 :     switch (length) {
     145           4 :         case 3: send(cn, CIN::SysExEnd3B, data[0], data[1], data[2]); break;
     146           3 :         case 2: send(cn, CIN::SysExEnd2B, data[0], data[1], 0); break;
     147           2 :         case 1: send(cn, CIN::SysExEnd1B, data[0], 0, 0); break;
     148             :         default: break; // LCOV_EXCL_LINE
     149             :     }
     150           9 : }
     151             : 
     152             : template <class Send>
     153          19 : void USBMIDI_Sender::sendSysEx(const SysExMessage msg, Send &&send) {
     154             :     // Don't bother trying to send empty messages
     155          19 :     if (msg.length == 0)
     156           0 :         return;
     157             : 
     158          19 :     Cable cable = msg.cable;
     159          19 :     uint8_t c = cable.getRaw();
     160          19 :     uint16_t length = msg.length;
     161          19 :     const uint8_t *data = msg.data;
     162             : 
     163             :     // Even if the previous chunk wasn't terminated correctly, if this is a new
     164             :     // SysEx message, always forget the previous unsent chunk.
     165          19 :     if (msg.isFirstChunk()) {
     166             :         // TODO: send a SysExEnd for previous chunk?
     167          10 :         storedSysExLength[c] = 0;
     168             :     }
     169             : 
     170             :     // Complete the previous unsent chunk to (at most) 3 bytes
     171          60 :     while (length > 0 && storedSysExLength[c] < 3) {
     172          41 :         storedSysExData[c][storedSysExLength[c]++] = *data++;
     173          41 :         --length;
     174             :     }
     175             : 
     176             :     // If all bytes of the new chunk were used, there are <= 3 stored bytes
     177          19 :     if (length == 0) {
     178             :         // If this chunk is the last one, terminate the SysEx (termination can
     179             :         // have 1, 2 or 3 bytes, so no need to store anything)
     180          11 :         if (msg.isLastChunk()) {
     181           3 :             sendSysExEnd(storedSysExData[c], storedSysExLength[c], cable, send);
     182           3 :             storedSysExLength[c] = 0;
     183             :         }
     184             :         // If it's the end of the chunk but not the end of the SysEx, and if we
     185             :         // have exactly 3 bytes left, we can send them immediately
     186           8 :         else if (storedSysExLength[c] == 3) {
     187           3 :             sendSysExStartCont1(storedSysExData[c], cable, send);
     188           3 :             storedSysExLength[c] = 0;
     189             :         }
     190             :         // If we have less than 3 bytes, we cannot send them now, we have to
     191             :         // store them and wait for the next chunk
     192             :     }
     193             :     // If there are new bytes left in the chunk, there are exactly 3 stored
     194             :     // bytes
     195             :     else {
     196             :         // First send the 3 stored bytes
     197           8 :         sendSysExStartCont1(storedSysExData[c], cable, send);
     198             :         // Then send whatever new data is left in the new chunk (but leave at
     199             :         // least 3 bytes)
     200           8 :         sendSysExStartCont(data, length, cable, send);
     201             :         // If this chunk is the last one, terminate the SysEx (termination can
     202             :         // have 1, 2 or 3 bytes, so no need to store anything)
     203           8 :         if (msg.isLastChunk()) {
     204           6 :             sendSysExEnd(data, length, cable, send);
     205           6 :             storedSysExLength[c] = 0;
     206             :         }
     207             :         // If it's the end of the chunk but not the end of the SysEx, and if we
     208             :         // have exactly 3 bytes left, we can send them immediately
     209           2 :         else if (length == 3) {
     210           1 :             sendSysExStartCont1(data, cable, send);
     211           1 :             storedSysExLength[c] = 0;
     212             :         }
     213             :         // If we have 1 or 2 bytes left, we cannot send them now, so store them
     214             :         // until we get enough data to fill a 3-byte packet
     215             :         else {
     216           1 :             storedSysExLength[c] = length;
     217           1 :             memcpy(storedSysExData[c], data, length);
     218             :         }
     219             :     }
     220             : }
     221             : 
     222             : #else
     223             : 
     224             : template <class Send>
     225             : void USBMIDI_Sender::sendSysEx(SysExMessage, Send &&) {}
     226             : 
     227             : #endif
     228             : 
     229             : END_CS_NAMESPACE

Generated by: LCOV version 1.15