diff --git a/examples/ThruFilterMap/ThruFilterMap.ino b/examples/ThruFilterMap/ThruFilterMap.ino new file mode 100644 index 00000000..40c2f56a --- /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 cfb39f4f..aa3673b0 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 d15888fc..fb0ef06d 100644 --- a/src/MIDI.h +++ b/src/MIDI.h @@ -236,15 +236,20 @@ class MidiInterface // MIDI Soft Thru public: - inline Thru::Mode getFilterMode() const; - inline bool getThruState() const; - - inline void turnThruOn(Thru::Mode inThruFilterMode = Thru::Full); + using ThruFilterCallback = bool (*)(const MidiMessage& inMessage); + using ThruMapCallback = MidiMessage (*)(const MidiMessage& inMessage); + inline void turnThruOn(ThruFilterCallback fptr = thruOn); inline void turnThruOff(); - inline void setThruFilterMode(Thru::Mode inThruFilterMode); + inline void setThruFilter(ThruFilterCallback fptr) { mThruFilterCallback = fptr; } + inline void setThruMap(ThruMapCallback fptr) { mThruMapCallback = fptr; } 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,8 +282,6 @@ class MidiInterface unsigned mPendingMessageIndex; unsigned mCurrentRpnNumber; unsigned mCurrentNrpnNumber; - bool mThruActivated : 1; - Thru::Mode mThruFilterMode : 7; MidiMessage mMessage; unsigned long mLastMessageSentTime; unsigned long mLastMessageReceivedTime; diff --git a/src/MIDI.hpp b/src/MIDI.hpp index 144e40e8..32d20c6a 100644 --- a/src/MIDI.hpp +++ b/src/MIDI.hpp @@ -40,8 +40,6 @@ inline MidiInterface::MidiInterface(Transport& in , mPendingMessageIndex(0) , mCurrentRpnNumber(0xffff) , mCurrentNrpnNumber(0xffff) - , mThruActivated(true) - , mThruFilterMode(Thru::Full) , mLastMessageSentTime(0) , mLastMessageReceivedTime(0) , mSenderActiveSensingPeriodicity(0) @@ -93,8 +91,8 @@ void MidiInterface::begin(Channel inChannel) mMessage.data2 = 0; mMessage.length = 0; - mThruFilterMode = Thru::Full; - mThruActivated = mTransport.thruActivated; + mThruFilterCallback = Transport::thruActivated ? thruOn : thruOff; + mThruMapCallback = thruEcho; } // ----------------------------------------------------------------------------- @@ -771,7 +769,7 @@ inline bool MidiInterface::read(Channel inChannel if (channelMatch) launchCallback(); - thruFilter(inChannel); + processThru(); return channelMatch; } @@ -1343,42 +1341,16 @@ void MidiInterface::launchCallback() @{ */ -/*! \brief Set the filter for thru mirroring - \param inThruFilterMode a filter mode - - @see Thru::Mode - */ -template -inline void MidiInterface::setThruFilterMode(Thru::Mode inThruFilterMode) -{ - mThruFilterMode = inThruFilterMode; - mThruActivated = mThruFilterMode != Thru::Off; -} - -template -inline Thru::Mode MidiInterface::getFilterMode() const -{ - return mThruFilterMode; -} - -template -inline bool MidiInterface::getThruState() const -{ - return mThruActivated; -} - template -inline void MidiInterface::turnThruOn(Thru::Mode inThruFilterMode) +inline void MidiInterface::turnThruOn(ThruFilterCallback fptr) { - mThruActivated = true; - mThruFilterMode = inThruFilterMode; + mThruFilterCallback = fptr; } template inline void MidiInterface::turnThruOff() { - mThruActivated = false; - mThruFilterMode = Thru::Off; + mThruFilterCallback = thruOff; } template @@ -1397,56 +1369,25 @@ inline void MidiInterface::UpdateLastSentTime() // - Channel messages are passed to the output whether their channel // is matching the input channel and the filter setting template -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: @@ -1456,24 +1397,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 ef74621c..eb3d2366 100644 --- a/src/midi_Defs.h +++ b/src/midi_Defs.h @@ -56,7 +56,6 @@ static const uint16_t ActiveSensingTimeout = 300; typedef byte StatusByte; typedef byte DataByte; typedef byte Channel; -typedef byte FilterMode; // ----------------------------------------------------------------------------- // Errors @@ -123,20 +122,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_Message.h b/src/midi_Message.h index dadb8960..bbd8d6a1 100644 --- a/src/midi_Message.h +++ b/src/midi_Message.h @@ -54,6 +54,20 @@ struct Message memset(sysexArray, 0, sSysExMaxSize * sizeof(DataByte)); } + inline Message(const Message& inOther) + : channel(inOther.channel) + , type(inOther.type) + , data1(inOther.data1) + , data2(inOther.data2) + , valid(inOther.valid) + , length(inOther.length) + { + if (type == midi::SystemExclusive) + { + memcpy(sysexArray, inOther.sysexArray, sSysExMaxSize * sizeof(DataByte)); + } + } + /*! The maximum size for the System Exclusive array. */ static const unsigned sSysExMaxSize = SysExMaxSize; @@ -94,7 +108,7 @@ struct Message /*! Total Length of the message. */ unsigned length; - + inline unsigned getSysExSize() const { const unsigned size = unsigned(data2) << 8 | data1; diff --git a/src/serialMIDI.h b/src/serialMIDI.h index e69e9b2d..f783439a 100644 --- a/src/serialMIDI.h +++ b/src/serialMIDI.h @@ -52,7 +52,7 @@ class SerialMIDI 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. @@ -125,6 +128,9 @@ END_MIDI_NAMESPACE @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); +#define MIDI_CREATE_CUSTOM_INSTANCE(Type, SerialPort, Name, Settings) \ + 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); diff --git a/test/unit-tests/CMakeLists.txt b/test/unit-tests/CMakeLists.txt index eaf1f3fd..d98ac8e4 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_MidiThru.cpp b/test/unit-tests/tests/unit-tests_MidiThru.cpp index dc0c0c1b..b89c262b 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); -TEST(MidiThru, defaultValues) +bool thruFilterSameChannel(const MidiMessage& inMessage) { - 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); + if (!midi.isChannelMessage(inMessage.type)) + return true; + + return MIDI_CHANNEL_OMNI == midi.getInputChannel() || + inMessage.channel == midi.getInputChannel(); } -TEST(MidiThru, beginEnablesThru) +bool thruFilterDifferentChannel(const MidiMessage& inMessage) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); + if (!midi.isChannelMessage(inMessage.type)) + return true; - 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); + return MIDI_CHANNEL_OMNI != midi.getInputChannel() && + inMessage.channel != midi.getInputChannel(); } -TEST(MidiThru, setGet) +MidiMessage thruMapNoteOnFullVelocity(const MidiMessage& inMessage) { - SerialMock serial; - Transport transport(serial); - MidiInterface midi((Transport&)transport); + if (inMessage.type != midi::MidiType::NoteOn) + return inMessage; - 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); + MidiMessage modified = inMessage; + modified.data2 = 127; + return modified; } +// ----------------------------------------------------------------------------- + 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(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); - EXPECT_EQ(serial.mTxBuffer.getLength(), 0); + 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