Compare commits

..

14 Commits

Author SHA1 Message Date
Francois Best 99fe61c993 chore: Add Nano variants to CI checks 2022-10-08 17:02:14 +02:00
Francois Best f6d95113f5 chore: Add test for system common messages 2022-10-08 15:23:09 +02:00
Francois Best 21a4ad23df chore: Fix build (things got lost in merge) 2022-10-08 14:50:54 +02:00
lathoub 12f05866cf return mReceiverActiveSensingActive in SensingTimeout Handler
return mReceiverActiveSensingActive in SensingTimeout Handler:
- true when all is OK and active sensing is received on time
- false when active sensing is **not** received on tome
2022-10-08 14:41:56 +02:00
lathoub 59fafdbf1a ActiveSensingTimeout has its own callback handler
removed ErrorActiveSensingTimeout
2022-10-08 14:41:56 +02:00
lathoub 0db6859802 Create Hairless.ino 2022-10-08 14:40:45 +02:00
lathoub bb58efec1d Update midi_Settings.h 2022-10-08 14:40:45 +02:00
lathoub b82f241349 reworked ActiveSensing
reworked ActiveSensing using input from a variety of device MIDI Implementation manuals (Roland,  KORG, Yamaha) found on the internet.

Receiving ActiveSensing:
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.

Sending ActiveSensing: send x ms after the last sent command
2022-10-08 14:40:45 +02:00
lathoub 350f6d1ec4 reworked active Sensing 2022-10-08 14:40:45 +02:00
Francois Best f42c344375 feat: Export type definitions when using macros
Types have names prepended by the port name
(defaults to `MIDI`), to allow multi-port applications.

This allows referencing those types elsewhere in the
application, without re-writing the template arguments,
and simplifies referencing the underlying Message type,
for SoftThru `filter`/`map`function definitions.

Proposition & discussion:
https://github.com/FortySevenEffects/arduino_midi_library/pull/232#issuecomment-910355200
2022-10-08 14:34:30 +02:00
Francois Best c2a838d8cc chore: Add example 2022-10-08 14:34:30 +02:00
Eric Sherman 4469f2aa7f uses Transport::thruActivated for initial thru state 2022-10-08 14:34:30 +02:00
Eric Sherman 1fe67bec4f makes changes requested in #232 discussion 2022-10-08 14:34:30 +02:00
Eric Sherman a6c3a48571 thru filter overhaul
* resolves #40 with franky47's proposed thru filter overhaul
* removes thru filter modes
* adds thru filter callback
* adds thru map callback
* old thru filter unit tests have been replicated with filter callbacks
* does not yet include documentation changes

I believe this implements the latest proposal for #40 and exercises
everything necessary in the unit tests, including the immutability of
`mMessage` after a thru map callback has modified the outgoing message.

The thru filter callbacks in the unit tests are not suitable for copying
and pasting by end-users due to the difference in the MIDI namespace
when setup via the unit tests vs via `MIDI_CREATE_DEFAULT_INSTANCE()`.

If the changes here are deemed suitable, I'll work on documentation.
2022-10-08 14:33:53 +02:00
14 changed files with 418 additions and 280 deletions

View File

@ -31,6 +31,8 @@ jobs:
- leonardo
- micro
- nanoatmega328
- nano_every
- nano33ble
- megaatmega2560
- teensy2
- teensy30

View File

@ -0,0 +1,30 @@
#include <MIDI.h>
USING_NAMESPACE_MIDI
struct MySerialSettings : public MIDI_NAMESPACE::DefaultSerialSettings
{
static const long BaudRate = 115200;
};
unsigned long t1 = millis();
MIDI_NAMESPACE::SerialMIDI<HardwareSerial, MySerialSettings> serialMIDI(Serial1);
MIDI_NAMESPACE::MidiInterface<MIDI_NAMESPACE::SerialMIDI<HardwareSerial, MySerialSettings>> MIDI((MIDI_NAMESPACE::SerialMIDI<HardwareSerial, MySerialSettings>&)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);
}
}

View File

@ -0,0 +1,51 @@
#include <MIDI.h>
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();
}

View File

@ -0,0 +1,58 @@
#include <MIDI.h>
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);
}
}

View File

@ -0,0 +1,50 @@
#include <MIDI.h>
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();
}

View File

@ -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

View File

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

View File

@ -40,15 +40,13 @@ inline MidiInterface<Transport, Settings, Platform>::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<Transport, Settings, Platform>& MidiInterface<Transport, Settings,
mCurrentRpnNumber = 0xffff;
mCurrentNrpnNumber = 0xffff;
mLastMessageSentTime = Platform::now();
mLastMessageSentTime =
mLastMessageReceivedTime = Platform::now();
mMessage.valid = false;
mMessage.type = InvalidType;
@ -93,9 +92,8 @@ MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings,
mMessage.data2 = 0;
mMessage.length = 0;
mThruFilterMode = Thru::Full;
mThruActivated = mTransport.thruActivated;
mThruFilterCallback = Transport::thruActivated ? thruOn : thruOff;
mThruMapCallback = thruEcho;
return *this;
}
@ -385,7 +383,9 @@ MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings,
}
if (Settings::UseRunningStatus)
{
mRunningStatus_TX = InvalidType;
}
return *this;
}
@ -492,7 +492,9 @@ MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings,
}
if (Settings::UseRunningStatus)
{
mRunningStatus_TX = InvalidType;
}
return *this;
}
@ -760,27 +762,32 @@ template<class Transport, class Settings, class Platform>
inline bool MidiInterface<Transport, Settings, Platform>::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)
{
// Send ActiveSensing <Settings::ActiveSensingPeriodicity> ms after the last command
if ((Platform::now() - mLastMessageSentTime) > Settings::SenderActiveSensingPeriodicity)
sendActiveSensing();
mLastMessageSentTime = Platform::now();
}
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<Transport, Settings, Platform>::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;
// is ErrorActiveSensingTimeout bit in mLastError on
if (mLastError & (1 << (ErrorActiveSensingTimeout - 1)))
{
mLastError &= ~(1UL << ErrorActiveSensingTimeout); // clear the ErrorActiveSensingTimeout bit
if (mErrorCallback)
mErrorCallback(mLastError);
}
}
// Keep the time of the last received message, so we can check for the timeout
if (Settings::UseReceiverActiveSensing && mReceiverActiveSensingActivated)
mLastMessageReceivedTime = Platform::now();
if (mMessage.type == ActiveSensing && !mReceiverActiveSensingActive)
{
mReceiverActiveSensingActive = true;
mActiveSensingTimeoutCallback(mReceiverActiveSensingActive);
}
}
#endif
handleNullVelocityNoteOnAsNoteOff();
@ -819,7 +819,7 @@ inline bool MidiInterface<Transport, Settings, Platform>::read(Channel inChannel
if (channelMatch)
launchCallback();
thruFilter(inChannel);
processThru();
return channelMatch;
}
@ -1399,51 +1399,24 @@ void MidiInterface<Transport, Settings, Platform>::launchCallback()
@{
*/
/*! \brief Set the filter for thru mirroring
\param inThruFilterMode a filter mode
@see Thru::Mode
*/
template<class Transport, class Settings, class Platform>
inline MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings, Platform>::setThruFilterMode(Thru::Mode inThruFilterMode)
inline MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings, Platform>::turnThruOn(ThruFilterCallback fptr)
{
mThruFilterMode = inThruFilterMode;
mThruActivated = mThruFilterMode != Thru::Off;
return *this;
}
template<class Transport, class Settings, class Platform>
inline Thru::Mode MidiInterface<Transport, Settings, Platform>::getFilterMode() const
{
return mThruFilterMode;
}
template<class Transport, class Settings, class Platform>
inline bool MidiInterface<Transport, Settings, Platform>::getThruState() const
{
return mThruActivated;
}
template<class Transport, class Settings, class Platform>
inline MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings, Platform>::turnThruOn(Thru::Mode inThruFilterMode)
{
mThruActivated = true;
mThruFilterMode = inThruFilterMode;
mThruFilterCallback = fptr;
return *this;
}
template<class Transport, class Settings, class Platform>
inline MidiInterface<Transport, Settings, Platform>& MidiInterface<Transport, Settings, Platform>::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<Transport, Settings, Platform>& MidiInterface<Transport, Se
// - Channel messages are passed to the output whether their channel
// is matching the input channel and the filter setting
template<class Transport, class Settings, class Platform>
void MidiInterface<Transport, Settings, Platform>::thruFilter(Channel inChannel)
void MidiInterface<Transport, Settings, Platform>::processThru()
{
// If the feature is disabled, don't do anything.
if (!mThruActivated || (mThruFilterMode == Thru::Off))
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<Transport, Settings, Platform>::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:

View File

@ -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

View File

@ -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

View File

@ -104,8 +104,11 @@ END_MIDI_NAMESPACE
Then call midi2.begin(), midi2.read() etc..
*/
#define MIDI_CREATE_INSTANCE(Type, SerialPort, Name) \
MIDI_NAMESPACE::SerialMIDI<Type> serial##Name(SerialPort);\
MIDI_NAMESPACE::MidiInterface<MIDI_NAMESPACE::SerialMIDI<Type>> Name((MIDI_NAMESPACE::SerialMIDI<Type>&)serial##Name);
using Name##SerialTransport = MIDI_NAMESPACE::SerialMIDI<Type>; \
using Name##Interface = MIDI_NAMESPACE::MidiInterface<Name##SerialTransport>; \
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<Type> serial##Name(SerialPort);\
MIDI_NAMESPACE::MidiInterface<MIDI_NAMESPACE::SerialMIDI<Type>, Settings> Name((MIDI_NAMESPACE::SerialMIDI<Type>&)serial##Name);

View File

@ -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

View File

@ -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<true, true> Settings;

View File

@ -17,6 +17,7 @@ typedef test_mocks::SerialMock<32> SerialMock;
typedef midi::SerialMIDI<SerialMock> Transport;
typedef midi::MidiInterface<Transport> MidiInterface;
typedef std::vector<byte> Buffer;
typedef midi::Message<midi::DefaultSettings::SysExMaxSize> MidiMessage;
template<unsigned Size>
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<false, false> MultiByteParsing;
typedef midi::MidiInterface<Transport, MultiByteParsing> 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<true, true> Settings;
typedef midi::MidiInterface<Transport, Settings> 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