diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml index b4f4891..2076b11 100644 --- a/.github/workflows/platformio.yml +++ b/.github/workflows/platformio.yml @@ -31,6 +31,8 @@ jobs: - leonardo - micro - nanoatmega328 + - nano_every + - nano33ble - megaatmega2560 - teensy2 - teensy30 diff --git a/examples/Hairless/Hairless.ino b/examples/Hairless/Hairless.ino new file mode 100644 index 0000000..f03937d --- /dev/null +++ b/examples/Hairless/Hairless.ino @@ -0,0 +1,30 @@ +#include +USING_NAMESPACE_MIDI + +struct MySerialSettings : public MIDI_NAMESPACE::DefaultSerialSettings +{ + static const long BaudRate = 115200; +}; + +unsigned long t1 = millis(); + +MIDI_NAMESPACE::SerialMIDI serialMIDI(Serial1); +MIDI_NAMESPACE::MidiInterface> MIDI((MIDI_NAMESPACE::SerialMIDI&)serialMIDI); + +void setup() +{ + MIDI.begin(1); +} + +void loop() +{ + MIDI.read(); + + // send a note every second + if ((millis() - t1) > 1000) + { + t1 = millis(); + + MIDI.sendNoteOn(random(1, 127), 55, 1); + } +} diff --git a/examples/ReceiverActiveSensing/ReceiverActiveSensing.ino b/examples/ReceiverActiveSensing/ReceiverActiveSensing.ino new file mode 100644 index 0000000..6d91822 --- /dev/null +++ b/examples/ReceiverActiveSensing/ReceiverActiveSensing.ino @@ -0,0 +1,51 @@ +#include +USING_NAMESPACE_MIDI + +struct MyMIDISettings : public MIDI_NAMESPACE::DefaultSettings +{ + // When setting UseReceiverActiveSensing to true, MIDI.read() *must* be called + // as often as possible (1000 / SenderActiveSensingPeriodicity per second). + // + // setting UseReceiverActiveSensing to true, adds 174 bytes of code. + // + // (Taken from a Roland MIDI Implementation Owner's manual) + // Once an Active Sensing message is received, the unit will begin monitoring + // the interval between all subsequent messages. If there is an interval of 420 ms + // or longer between messages while monitoring is active, the same processing + // as when All Sound Off, All Notes Off,and Reset All Controllers messages are + // received will be carried out. The unit will then stopmonitoring the message interval. + + static const bool UseReceiverActiveSensing = true; + + static const uint16_t ReceiverActiveSensingTimeout = 420; +}; + +MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial1, MIDI, MyMIDISettings); + +void activeSensingTimeoutExceptionHandler(bool active) +{ + if (!active) + { + MIDI.sendControlChange(AllSoundOff, 0, 1); + MIDI.sendControlChange(AllNotesOff, 0, 1); + MIDI.sendControlChange(ResetAllControllers, 0, 1); + + digitalWrite(LED_BUILTIN, HIGH); + } + else + digitalWrite(LED_BUILTIN, LOW); +} + +void setup() +{ + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); + + MIDI.setHandleActiveSensingTimeout(activeSensingTimeoutExceptionHandler); + MIDI.begin(1); +} + +void loop() +{ + MIDI.read(); +} diff --git a/examples/SenderActiveSensing/SenderActiveSensing.ino b/examples/SenderActiveSensing/SenderActiveSensing.ino new file mode 100644 index 0000000..c9c7718 --- /dev/null +++ b/examples/SenderActiveSensing/SenderActiveSensing.ino @@ -0,0 +1,58 @@ +#include +USING_NAMESPACE_MIDI + +struct MyMIDISettings : public MIDI_NAMESPACE::DefaultSettings +{ + // When setting UseSenderActiveSensing to true, MIDI.read() *must* be called + // as often as possible (1000 / SenderActiveSensingPeriodicity per second). + // + // setting UseSenderActiveSensing to true, adds 34 bytes of code. + // + // When using Active Sensing, call MIDI.read(); in the Arduino loop() + // + // from 'a' MIDI implementation manual: "Sent periodically" + // In the example here, a NoteOn is send every 1000ms (1s), ActiveSensing is + // send every 250ms after the last command. + // Logging the command will look like this: + // + // ... + // A.Sense FE + // A.Sense FE + // A.Sense FE + // NoteOn 90 04 37 [E-2] + // A.Sense FE + // A.Sense FE + // A.Sense FE + // NoteOn 90 04 37 [E-2] + // A.Sense FE + // A.Sense FE + // A.Sense FE + // NoteOn 90 04 37 [E-2] + // ... + + static const bool UseSenderActiveSensing = true; + + static const uint16_t SenderActiveSensingPeriodicity = 250; +}; + +unsigned long t1 = millis(); + +MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial1, MIDI, MyMIDISettings); + +void setup() +{ + MIDI.begin(1); +} + +void loop() +{ + MIDI.read(); + + // send a note every second + if ((millis() - t1) > 1000) + { + t1 = millis(); + + MIDI.sendNoteOn(random(1, 127), 55, 1); + } +} diff --git a/examples/ThruFilterMap/ThruFilterMap.ino b/examples/ThruFilterMap/ThruFilterMap.ino new file mode 100644 index 0000000..40c2f56 --- /dev/null +++ b/examples/ThruFilterMap/ThruFilterMap.ino @@ -0,0 +1,50 @@ +#include + +MIDI_CREATE_DEFAULT_INSTANCE(); + +/** + * This example shows how to make MIDI processors. + * + * The `filter` function defines whether to forward an incoming + * MIDI message to the output. + * + * The `map` function transforms the forwarded message before + * it is sent, allowing to change things. + * + * Here we will transform NoteOn messages into Program Change, + * allowing to use a keyboard to change patches on a MIDI device. + */ + +bool filter(const MIDIMessage& message) +{ + if (message.type == midi::NoteOn) + { + // Only forward NoteOn messages + return true; + } + return false; +} + +MIDIMessage map(const MIDIMessage& message) +{ + // Make a copy of the message + MIDIMessage output(message); + if (message.type == midi::NoteOn) + { + output.type = midi::ProgramChange; + output.data2 = 0; // Not needed in ProgramChange + } + return output; +} + +void setup() +{ + MIDI.begin(); + MIDI.setThruFilter(filter); + MIDI.setThruMap(map); +} + +void loop() +{ + MIDI.read(); +} diff --git a/keywords.txt b/keywords.txt index 845d798..0a355fd 100644 --- a/keywords.txt +++ b/keywords.txt @@ -55,14 +55,12 @@ getData1 KEYWORD2 getData2 KEYWORD2 getSysExArray KEYWORD2 getSysExArrayLength KEYWORD2 -getFilterMode KEYWORD2 getThruState KEYWORD2 getInputChannel KEYWORD2 check KEYWORD2 setInputChannel KEYWORD2 turnThruOn KEYWORD2 turnThruOff KEYWORD2 -setThruFilterMode KEYWORD2 disconnectCallbackFromType KEYWORD2 setHandleNoteOff KEYWORD2 setHandleNoteOn KEYWORD2 diff --git a/src/MIDI.h b/src/MIDI.h index 767feb9..5bc2bc5 100644 --- a/src/MIDI.h +++ b/src/MIDI.h @@ -212,6 +212,7 @@ private: void (*mMessageCallback)(const MidiMessage& message) = nullptr; ErrorCallback mErrorCallback = nullptr; + ActiveSensingTimeoutCallback mActiveSensingTimeoutCallback = nullptr; NoteOffCallback mNoteOffCallback = nullptr; NoteOnCallback mNoteOnCallback = nullptr; AfterTouchPolyCallback mAfterTouchPolyCallback = nullptr; @@ -236,15 +237,28 @@ private: // MIDI Soft Thru public: - inline Thru::Mode getFilterMode() const; - inline bool getThruState() const; - - inline MidiInterface& turnThruOn(Thru::Mode inThruFilterMode = Thru::Full); + using ThruFilterCallback = bool (*)(const MidiMessage& inMessage); + using ThruMapCallback = MidiMessage (*)(const MidiMessage& inMessage); + inline MidiInterface& turnThruOn(ThruFilterCallback fptr = thruOn); inline MidiInterface& turnThruOff(); - inline MidiInterface& setThruFilterMode(Thru::Mode inThruFilterMode); + inline MidiInterface& setThruFilter(ThruFilterCallback fptr) + { + mThruFilterCallback = fptr; + return *this; + } + inline MidiInterface& setThruMap(ThruMapCallback fptr) + { + mThruMapCallback = fptr; + return *this; + } private: - void thruFilter(byte inChannel); + void processThru(); + static inline bool thruOn(const MidiMessage& inMessage) { (void)inMessage; return true; } + static inline bool thruOff(const MidiMessage& inMessage) { (void)inMessage; return false; } + static inline MidiMessage thruEcho(const MidiMessage& inMessage) { return inMessage; } + ThruFilterCallback mThruFilterCallback; + ThruMapCallback mThruMapCallback; // ------------------------------------------------------------------------- // MIDI Parsing @@ -277,13 +291,11 @@ private: unsigned mPendingMessageIndex; unsigned mCurrentRpnNumber; unsigned mCurrentNrpnNumber; - bool mThruActivated : 1; - Thru::Mode mThruFilterMode : 7; MidiMessage mMessage; unsigned long mLastMessageSentTime; unsigned long mLastMessageReceivedTime; unsigned long mSenderActiveSensingPeriodicity; - bool mReceiverActiveSensingActivated; + bool mReceiverActiveSensingActive; int8_t mLastError; private: diff --git a/src/MIDI.hpp b/src/MIDI.hpp index be7c28a..610e144 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -40,15 +40,13 @@ inline MidiInterface::MidiInterface(Transport& in , mPendingMessageIndex(0) , mCurrentRpnNumber(0xffff) , mCurrentNrpnNumber(0xffff) - , mThruActivated(true) - , mThruFilterMode(Thru::Full) , mLastMessageSentTime(0) , mLastMessageReceivedTime(0) - , mSenderActiveSensingPeriodicity(0) - , mReceiverActiveSensingActivated(false) + , mSenderActiveSensingPeriodicity(Settings::SenderActiveSensingPeriodicity) + , mReceiverActiveSensingActive(false) , mLastError(0) { - mSenderActiveSensingPeriodicity = Settings::SenderActiveSensingPeriodicity; + static_assert(!(Settings::UseSenderActiveSensing && Settings::UseReceiverActiveSensing), "UseSenderActiveSensing and UseReceiverActiveSensing can't be both set to true."); } /*! \brief Destructor for MidiInterface. @@ -84,7 +82,8 @@ MidiInterface& MidiInterface& MidiInterface& MidiInterface& MidiInterface& MidiInterface inline bool MidiInterface::read(Channel inChannel) { #ifndef RegionActiveSending + // Active Sensing. This message is intended to be sent // repeatedly to tell the receiver that a connection is alive. Use - // of this message is optional. When initially received, the - // receiver will expect to receive another Active Sensing - // message each 300ms (max), and if it does not then it will - // assume that the connection has been terminated. At - // termination, the receiver will turn off all voices and return to - // normal (non- active sensing) operation. - if (Settings::UseSenderActiveSensing && (mSenderActiveSensingPeriodicity > 0) && (Platform::now() - mLastMessageSentTime) > mSenderActiveSensingPeriodicity) + // of this message is optional. + if (Settings::UseSenderActiveSensing) { - sendActiveSensing(); - mLastMessageSentTime = Platform::now(); + // Send ActiveSensing ms after the last command + if ((Platform::now() - mLastMessageSentTime) > Settings::SenderActiveSensingPeriodicity) + sendActiveSensing(); } - if (Settings::UseReceiverActiveSensing && mReceiverActiveSensingActivated && (mLastMessageReceivedTime + ActiveSensingTimeout < Platform::now())) + // Once an Active Sensing message is received, the unit will begin monitoring + // the intervalbetween all subsequent messages. If there is an interval of 420 ms + // or longer betweenmessages while monitoring is active, the same processing + // as when All Sound Off, All Notes Off,and Reset All Controllers messages are + // received will be carried out. The unit will then stopmonitoring the message interval. + if (Settings::UseReceiverActiveSensing && mReceiverActiveSensingActive) { - mReceiverActiveSensingActivated = false; + if ((Platform::now() - mLastMessageReceivedTime > Settings::ReceiverActiveSensingTimeout)) + { + mReceiverActiveSensingActive = false; - mLastError |= 1UL << ErrorActiveSensingTimeout; // set the ErrorActiveSensingTimeout bit - if (mErrorCallback) - mErrorCallback(mLastError); + // its up to the handler to send the stop processing messages + // (also, no clue what the channel is on which to send them) + mActiveSensingTimeoutCallback(mReceiverActiveSensingActive); + } } #endif @@ -792,25 +799,18 @@ inline bool MidiInterface::read(Channel inChannel #ifndef RegionActiveSending - if (Settings::UseReceiverActiveSensing && mMessage.type == ActiveSensing) + if (Settings::UseReceiverActiveSensing) { - // When an ActiveSensing message is received, the time keeping is activated. - // When a timeout occurs, an error message is send and time keeping ends. - mReceiverActiveSensingActivated = true; + mLastMessageReceivedTime = Platform::now(); - // is ErrorActiveSensingTimeout bit in mLastError on - if (mLastError & (1 << (ErrorActiveSensingTimeout - 1))) + if (mMessage.type == ActiveSensing && !mReceiverActiveSensingActive) { - mLastError &= ~(1UL << ErrorActiveSensingTimeout); // clear the ErrorActiveSensingTimeout bit - if (mErrorCallback) - mErrorCallback(mLastError); + mReceiverActiveSensingActive = true; + + mActiveSensingTimeoutCallback(mReceiverActiveSensingActive); } } - // Keep the time of the last received message, so we can check for the timeout - if (Settings::UseReceiverActiveSensing && mReceiverActiveSensingActivated) - mLastMessageReceivedTime = Platform::now(); - #endif handleNullVelocityNoteOnAsNoteOff(); @@ -819,7 +819,7 @@ inline bool MidiInterface::read(Channel inChannel if (channelMatch) launchCallback(); - thruFilter(inChannel); + processThru(); return channelMatch; } @@ -1399,51 +1399,24 @@ void MidiInterface::launchCallback() @{ */ -/*! \brief Set the filter for thru mirroring - \param inThruFilterMode a filter mode - - @see Thru::Mode - */ template -inline MidiInterface& MidiInterface::setThruFilterMode(Thru::Mode inThruFilterMode) +inline MidiInterface& MidiInterface::turnThruOn(ThruFilterCallback fptr) { - mThruFilterMode = inThruFilterMode; - mThruActivated = mThruFilterMode != Thru::Off; - - return *this; -} - -template -inline Thru::Mode MidiInterface::getFilterMode() const -{ - return mThruFilterMode; -} - -template -inline bool MidiInterface::getThruState() const -{ - return mThruActivated; -} - -template -inline MidiInterface& MidiInterface::turnThruOn(Thru::Mode inThruFilterMode) -{ - mThruActivated = true; - mThruFilterMode = inThruFilterMode; - + mThruFilterCallback = fptr; return *this; } template inline MidiInterface& MidiInterface::turnThruOff() { - mThruActivated = false; - mThruFilterMode = Thru::Off; - + mThruFilterCallback = thruOff; + if (Settings::UseSenderActiveSensing) + { + mLastMessageSentTime = Platform::now(); + } return *this; } - /*! @} */ // End of doc group MIDI Thru // This method is called upon reception of a message @@ -1453,56 +1426,25 @@ inline MidiInterface& MidiInterface -void MidiInterface::thruFilter(Channel inChannel) +void MidiInterface::processThru() { - // If the feature is disabled, don't do anything. - if (!mThruActivated || (mThruFilterMode == Thru::Off)) - return; + if (!Transport::thruActivated || !mThruFilterCallback(mMessage)) + return; + + MidiMessage thruMessage = mThruMapCallback(mMessage); // First, check if the received message is Channel - if (mMessage.type >= NoteOff && mMessage.type <= PitchBend) + if (thruMessage.type >= NoteOff && thruMessage.type <= PitchBend) { - const bool filter_condition = ((mMessage.channel == inChannel) || - (inChannel == MIDI_CHANNEL_OMNI)); - - // Now let's pass it to the output - switch (mThruFilterMode) - { - case Thru::Full: - send(mMessage.type, - mMessage.data1, - mMessage.data2, - mMessage.channel); - break; - - case Thru::SameChannel: - if (filter_condition) - { - send(mMessage.type, - mMessage.data1, - mMessage.data2, - mMessage.channel); - } - break; - - case Thru::DifferentChannel: - if (!filter_condition) - { - send(mMessage.type, - mMessage.data1, - mMessage.data2, - mMessage.channel); - } - break; - - default: - break; - } + send(thruMessage.type, + thruMessage.data1, + thruMessage.data2, + thruMessage.channel); } else { // Send the message to the output - switch (mMessage.type) + switch (thruMessage.type) { // Real Time and 1 byte case Clock: @@ -1512,24 +1454,24 @@ void MidiInterface::thruFilter(Channel inChannel) case ActiveSensing: case SystemReset: case TuneRequest: - sendRealTime(mMessage.type); + sendRealTime(thruMessage.type); break; case SystemExclusive: // Send SysEx (0xf0 and 0xf7 are included in the buffer) - sendSysEx(getSysExArrayLength(), getSysExArray(), true); + sendSysEx(thruMessage.getSysExSize(), thruMessage.sysexArray, true); break; case SongSelect: - sendSongSelect(mMessage.data1); + sendSongSelect(thruMessage.data1); break; case SongPosition: - sendSongPosition(mMessage.data1 | ((unsigned)mMessage.data2 << 7)); + sendSongPosition(thruMessage.data1 | ((unsigned)thruMessage.data2 << 7)); break; case TimeCodeQuarterFrame: - sendTimeCodeQuarterFrame(mMessage.data1,mMessage.data2); + sendTimeCodeQuarterFrame(thruMessage.data1,thruMessage.data2); break; default: diff --git a/src/midi_Defs.h b/src/midi_Defs.h index 1da019d..409407b 100644 --- a/src/midi_Defs.h +++ b/src/midi_Defs.h @@ -46,28 +46,23 @@ BEGIN_MIDI_NAMESPACE #define MIDI_PITCHBEND_MIN -8192 #define MIDI_PITCHBEND_MAX 8191 -/*! Receiving Active Sensing -*/ -static const uint16_t ActiveSensingTimeout = 300; - // ----------------------------------------------------------------------------- // Type definitions typedef byte StatusByte; typedef byte DataByte; typedef byte Channel; -typedef byte FilterMode; // ----------------------------------------------------------------------------- // Errors static const uint8_t ErrorParse = 0; -static const uint8_t ErrorActiveSensingTimeout = 1; static const uint8_t WarningSplitSysEx = 2; // ----------------------------------------------------------------------------- // Aliasing using ErrorCallback = void (*)(int8_t); +using ActiveSensingTimeoutCallback = void (*)(bool); using NoteOffCallback = void (*)(Channel channel, byte note, byte velocity); using NoteOnCallback = void (*)(Channel channel, byte note, byte velocity); using AfterTouchPolyCallback = void (*)(Channel channel, byte note, byte velocity); @@ -123,20 +118,6 @@ enum MidiType: uint8_t // ----------------------------------------------------------------------------- -/*! Enumeration of Thru filter modes */ -struct Thru -{ - enum Mode - { - Off = 0, ///< Thru disabled (nothing passes through). - Full = 1, ///< Fully enabled Thru (every incoming message is sent back). - SameChannel = 2, ///< Only the messages on the Input Channel will be sent back. - DifferentChannel = 3, ///< All the messages but the ones on the Input Channel will be sent back. - }; -}; - -// ----------------------------------------------------------------------------- - /*! \brief Enumeration of Control Change command numbers. See the detailed controllers numbers & description here: http://www.somascape.org/midi/tech/spec.html#ctrlnums diff --git a/src/midi_Settings.h b/src/midi_Settings.h index 179b773..6c40922 100644 --- a/src/midi_Settings.h +++ b/src/midi_Settings.h @@ -72,18 +72,15 @@ struct DefaultSettings */ static const unsigned SysExMaxSize = 128; - /*! Global switch to turn on/off sender ActiveSensing - Set to true to send ActiveSensing - Set to false will not send ActiveSensing message (will also save memory) + /*! Global switch to turn on/off sending and receiving ActiveSensing + Set to true to activate ActiveSensing + Set to false will not send/receive ActiveSensing message (will also save 236 bytes of memory) + + When setting UseActiveSensing to true, MIDI.read() *must* be called + as often as possible (1000 / ActiveSensingPeriodicity per second). */ static const bool UseSenderActiveSensing = false; - /*! Global switch to turn on/off receiver ActiveSensing - Set to true to check for message timeouts (via ErrorCallback) - Set to false will not check if chained device are still alive (if they use ActiveSensing) (will also save memory) - */ - static const bool UseReceiverActiveSensing = false; - /*! Active Sensing is intended to be sent repeatedly by the sender to tell the receiver that a connection is alive. Use of this message is optional. When initially received, the @@ -94,11 +91,20 @@ struct DefaultSettings normal (non- active sensing) operation. Typical value is 250 (ms) - an Active Sensing command is send every 250ms. - (All Roland devices send Active Sensing every 250ms) - - Setting this field to 0 will disable sending MIDI active sensing. + (Most Roland devices send Active Sensing every 250ms) */ - static const uint16_t SenderActiveSensingPeriodicity = 0; + static const uint16_t SenderActiveSensingPeriodicity = 300; + + /*! Once an Active Sensing message is received, the unit will begin monitoring + the intervalbetween all subsequent messages. If there is an interval of ActiveSensingPeriodicity ms + or longer betweenmessages while monitoring is active, the same processing + as when All Sound Off, All Notes Off,and Reset All Controllers messages are + received will be carried out. The unit will then stopmonitoring the message interval. + */ + static const bool UseReceiverActiveSensing = false; + + static const uint16_t ReceiverActiveSensingTimeout = 300; + }; END_MIDI_NAMESPACE diff --git a/src/serialMIDI.h b/src/serialMIDI.h index e69e9b2..2bb673e 100644 --- a/src/serialMIDI.h +++ b/src/serialMIDI.h @@ -52,7 +52,7 @@ public: public: static const bool thruActivated = true; - + void begin() { // Initialise the Serial port @@ -103,9 +103,12 @@ END_MIDI_NAMESPACE Example: MIDI_CREATE_INSTANCE(HardwareSerial, Serial2, midi2); Then call midi2.begin(), midi2.read() etc.. */ -#define MIDI_CREATE_INSTANCE(Type, SerialPort, Name) \ - MIDI_NAMESPACE::SerialMIDI serial##Name(SerialPort);\ - MIDI_NAMESPACE::MidiInterface> Name((MIDI_NAMESPACE::SerialMIDI&)serial##Name); +#define MIDI_CREATE_INSTANCE(Type, SerialPort, Name) \ + using Name##SerialTransport = MIDI_NAMESPACE::SerialMIDI; \ + using Name##Interface = MIDI_NAMESPACE::MidiInterface; \ + using Name##Message = Name##Interface::MidiMessage; \ + Name##SerialTransport serial##Name(SerialPort); \ + Name##Interface Name((Name##SerialTransport&)serial##Name); #if defined(ARDUINO_SAM_DUE) || defined(USBCON) || defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MKL26Z64__) // Leonardo, Due and other USB boards use Serial1 by default. @@ -121,10 +124,11 @@ END_MIDI_NAMESPACE #endif /*! \brief Create an instance of the library attached to a serial port with - custom settings. + custom MIDI settings (not to be confused with modified Serial Settings, like BaudRate) @see DefaultSettings @see MIDI_CREATE_INSTANCE */ #define MIDI_CREATE_CUSTOM_INSTANCE(Type, SerialPort, Name, Settings) \ MIDI_NAMESPACE::SerialMIDI serial##Name(SerialPort);\ MIDI_NAMESPACE::MidiInterface, Settings> Name((MIDI_NAMESPACE::SerialMIDI&)serial##Name); + diff --git a/test/unit-tests/CMakeLists.txt b/test/unit-tests/CMakeLists.txt index eaf1f3f..d98ac8e 100644 --- a/test/unit-tests/CMakeLists.txt +++ b/test/unit-tests/CMakeLists.txt @@ -23,6 +23,8 @@ add_executable(unit-tests tests/unit-tests_MidiThru.cpp ) +set_source_files_properties(tests/unit-tests_MidiThru.cpp PROPERTIES COMPILE_FLAGS -Wno-shadow) + target_link_libraries(unit-tests gtest gmock diff --git a/test/unit-tests/tests/unit-tests_MidiOutput.cpp b/test/unit-tests/tests/unit-tests_MidiOutput.cpp index c243966..ffe5591 100644 --- a/test/unit-tests/tests/unit-tests_MidiOutput.cpp +++ b/test/unit-tests/tests/unit-tests_MidiOutput.cpp @@ -63,7 +63,7 @@ TEST(MidiOutput, sendGenericWithRunningStatus) SerialMock serial; Transport transport(serial); RsMidiInterface midi((Transport&)transport); - + Buffer buffer; buffer.resize(5); @@ -85,7 +85,7 @@ TEST(MidiOutput, sendGenericWithoutRunningStatus) SerialMock serial; Transport transport(serial); NoRsMidiInterface midi((Transport&)transport); - + Buffer buffer; buffer.resize(6); @@ -336,7 +336,7 @@ TEST(MidiOutput, sendSysEx) LargeSerialMock serial; LargeTransport transport(serial); LargeMidiInterface midi((LargeTransport&)transport); - + Buffer buffer; // Short frame @@ -547,6 +547,41 @@ TEST(MidiOutput, sendRealTime) } } +TEST(MidiOutput, sendCommon) +{ + SerialMock serial; + Transport transport(serial); + MidiInterface midi((Transport&)transport); + + Buffer buffer; + + // Test valid Common messages + { + buffer.clear(); + buffer.resize(8); + + midi.begin(); + midi.sendCommon(midi::TimeCodeQuarterFrame, 1); + midi.sendCommon(midi::SongPosition, 0x5555); + midi.sendCommon(midi::SongSelect, 3); + midi.sendCommon(midi::TuneRequest, 4); + + EXPECT_EQ(serial.mTxBuffer.getLength(), 8); + serial.mTxBuffer.read(&buffer[0], 8); + EXPECT_THAT(buffer, ElementsAreArray({ + 0xf1, 0x01, 0xf2, 0x55, 0x2a, 0xf3, 0x03, 0xf6 + })); + } + // Test invalid messages + { + midi.begin(); + midi.sendCommon(midi::Undefined_F4, 0); + midi.sendCommon(midi::Undefined_F5, 0); + midi.sendCommon(midi::InvalidType, 0); + EXPECT_EQ(serial.mTxBuffer.getLength(), 0); + } +} + TEST(MidiOutput, RPN) { typedef VariableSettings Settings; @@ -555,7 +590,7 @@ TEST(MidiOutput, RPN) SerialMock serial; Transport transport(serial); RsMidiInterface midi((Transport&)transport); - + Buffer buffer; // 14-bit Value Single Frame @@ -673,7 +708,7 @@ TEST(MidiOutput, NRPN) SerialMock serial; Transport transport(serial); RsMidiInterface midi((Transport&)transport); - + Buffer buffer; // 14-bit Value Single Frame @@ -791,7 +826,7 @@ TEST(MidiOutput, runningStatusCancellation) SerialMock serial; Transport transport(serial); RsMidiInterface midi((Transport&)transport); - + Buffer buffer; static const unsigned sysExLength = 13; diff --git a/test/unit-tests/tests/unit-tests_MidiThru.cpp b/test/unit-tests/tests/unit-tests_MidiThru.cpp index dc0c0c1..b89c262 100644 --- a/test/unit-tests/tests/unit-tests_MidiThru.cpp +++ b/test/unit-tests/tests/unit-tests_MidiThru.cpp @@ -17,6 +17,7 @@ typedef test_mocks::SerialMock<32> SerialMock; typedef midi::SerialMIDI Transport; typedef midi::MidiInterface MidiInterface; typedef std::vector Buffer; +typedef midi::Message MidiMessage; template struct VariableSysExSettings : midi::DefaultSettings @@ -24,75 +25,42 @@ struct VariableSysExSettings : midi::DefaultSettings static const unsigned SysExMaxSize = Size; }; +SerialMock serial; +Transport transport(serial); +MidiInterface midi((Transport&)transport); + +bool thruFilterSameChannel(const MidiMessage& inMessage) +{ + if (!midi.isChannelMessage(inMessage.type)) + return true; + + return MIDI_CHANNEL_OMNI == midi.getInputChannel() || + inMessage.channel == midi.getInputChannel(); +} + +bool thruFilterDifferentChannel(const MidiMessage& inMessage) +{ + if (!midi.isChannelMessage(inMessage.type)) + return true; + + return MIDI_CHANNEL_OMNI != midi.getInputChannel() && + inMessage.channel != midi.getInputChannel(); +} + +MidiMessage thruMapNoteOnFullVelocity(const MidiMessage& inMessage) +{ + if (inMessage.type != midi::MidiType::NoteOn) + return inMessage; + + MidiMessage modified = inMessage; + modified.data2 = 127; + return modified; +} + // ----------------------------------------------------------------------------- -TEST(MidiThru, defaultValues) -{ - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Full); - midi.begin(); // Should not change the state - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Full); -} - -TEST(MidiThru, beginEnablesThru) -{ - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - - midi.turnThruOff(); - EXPECT_EQ(midi.getThruState(), false); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Off); - midi.begin(); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Full); -} - -TEST(MidiThru, setGet) -{ - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - - midi.turnThruOff(); - EXPECT_EQ(midi.getThruState(), false); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Off); - - midi.turnThruOn(); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Full); - midi.turnThruOn(midi::Thru::SameChannel); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::SameChannel); - midi.turnThruOn(midi::Thru::DifferentChannel); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::DifferentChannel); - - midi.setThruFilterMode(midi::Thru::Full); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Full); - midi.setThruFilterMode(midi::Thru::SameChannel); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::SameChannel); - midi.setThruFilterMode(midi::Thru::DifferentChannel); - EXPECT_EQ(midi.getThruState(), true); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::DifferentChannel); - midi.setThruFilterMode(midi::Thru::Off); - EXPECT_EQ(midi.getThruState(), false); - EXPECT_EQ(midi.getFilterMode(), midi::Thru::Off); -} - TEST(MidiThru, off) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - midi.begin(MIDI_CHANNEL_OMNI); midi.turnThruOff(); @@ -110,14 +78,9 @@ TEST(MidiThru, off) TEST(MidiThru, full) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::Full); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; @@ -154,14 +117,10 @@ TEST(MidiThru, full) TEST(MidiThru, sameChannel) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - Buffer buffer; midi.begin(12); - midi.setThruFilterMode(midi::Thru::SameChannel); + midi.setThruFilter(thruFilterSameChannel); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; @@ -185,14 +144,10 @@ TEST(MidiThru, sameChannel) TEST(MidiThru, sameChannelOmni) // Acts like full { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::SameChannel); + midi.setThruFilter(thruFilterSameChannel); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; @@ -229,14 +184,10 @@ TEST(MidiThru, sameChannelOmni) // Acts like full TEST(MidiThru, differentChannel) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - Buffer buffer; midi.begin(12); - midi.setThruFilterMode(midi::Thru::DifferentChannel); + midi.setThruFilter(thruFilterDifferentChannel); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; @@ -260,14 +211,10 @@ TEST(MidiThru, differentChannel) TEST(MidiThru, differentChannelOmni) // Acts like off { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); - Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::DifferentChannel); + midi.setThruFilter(thruFilterDifferentChannel); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; @@ -293,14 +240,11 @@ TEST(MidiThru, multiByteThru) typedef VariableSettings MultiByteParsing; typedef midi::MidiInterface MultiByteMidiInterface; - SerialMock serial; - Transport transport(serial); MultiByteMidiInterface midi((Transport&)transport); Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::Full); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 56, 78 }; @@ -324,14 +268,11 @@ TEST(MidiThru, withTxRunningStatus) typedef VariableSettings Settings; typedef midi::MidiInterface RsMidiInterface; - SerialMock serial; - Transport transport(serial); RsMidiInterface midi((Transport&)transport); Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::Full); static const unsigned rxSize = 5; static const byte rxData[rxSize] = { 0x9b, 12, 34, 56, 78 }; @@ -364,26 +305,52 @@ TEST(MidiThru, withTxRunningStatus) })); } -TEST(MidiThru, invalidMode) +TEST(MidiThru, mapNoteOnFullVelocity) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); + Buffer buffer; midi.begin(MIDI_CHANNEL_OMNI); - midi.setThruFilterMode(midi::Thru::Mode(42)); + midi.setThruMap(thruMapNoteOnFullVelocity); static const unsigned rxSize = 6; static const byte rxData[rxSize] = { 0x9b, 12, 34, 0x9c, 56, 78 }; serial.mRxBuffer.write(rxData, rxSize); + EXPECT_EQ(midi.read(), false); + EXPECT_EQ(serial.mTxBuffer.getLength(), 0); EXPECT_EQ(midi.read(), false); - EXPECT_EQ(midi.read(), true); - EXPECT_EQ(midi.read(), false); - EXPECT_EQ(midi.read(), false); + EXPECT_EQ(serial.mTxBuffer.getLength(), 0); EXPECT_EQ(midi.read(), true); + buffer.clear(); + buffer.resize(3); + EXPECT_EQ(serial.mTxBuffer.getLength(), 3); + serial.mTxBuffer.read(&buffer[0], 3); + EXPECT_THAT(buffer, ElementsAreArray({ + 0x9b, 12, 127 // thru message full velocity + })); + EXPECT_EQ(midi.getType(), midi::NoteOn); + EXPECT_EQ(midi.getChannel(), 12); + EXPECT_EQ(midi.getData1(), 12); + EXPECT_EQ(midi.getData2(), 34); // mMessage velocity unchanged + + EXPECT_EQ(midi.read(), false); EXPECT_EQ(serial.mTxBuffer.getLength(), 0); + EXPECT_EQ(midi.read(), false); + EXPECT_EQ(serial.mTxBuffer.getLength(), 0); + EXPECT_EQ(midi.read(), true); + + buffer.clear(); + buffer.resize(3); + EXPECT_EQ(serial.mTxBuffer.getLength(), 3); + serial.mTxBuffer.read(&buffer[0], 3); + EXPECT_THAT(buffer, ElementsAreArray({ + 0x9c, 56, 127 // thru message full velocity + })); + EXPECT_EQ(midi.getType(), midi::NoteOn); + EXPECT_EQ(midi.getChannel(), 13); + EXPECT_EQ(midi.getData1(), 56); + EXPECT_EQ(midi.getData2(), 78); // mMessage velocity unchanged } END_UNNAMED_NAMESPACE