From ed3cd8f74ac5ead3e88cd1a1744e45682319bae0 Mon Sep 17 00:00:00 2001 From: Sam Aaron Date: Sun, 1 Oct 2023 00:27:52 +0100 Subject: [PATCH] SP Midi - update RTMidi dep --- external_libs/rtmidi/RtMidi.cpp | 1656 ++++++++++++++++++++++++++++--- external_libs/rtmidi/RtMidi.h | 45 +- 2 files changed, 1525 insertions(+), 176 deletions(-) diff --git a/external_libs/rtmidi/RtMidi.cpp b/external_libs/rtmidi/RtMidi.cpp index 6a1c89e670..0c3dcbec7c 100644 --- a/external_libs/rtmidi/RtMidi.cpp +++ b/external_libs/rtmidi/RtMidi.cpp @@ -9,7 +9,7 @@ RtMidi WWW site: http://www.music.mcgill.ca/~gary/rtmidi/ RtMidi: realtime MIDI i/o C++ classes - Copyright (c) 2003-2021 Gary P. Scavone + Copyright (c) 2003-2023 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -39,6 +39,9 @@ #include "RtMidi.h" #include +#if defined(__APPLE__) +#include +#endif #if (TARGET_OS_IPHONE == 1) @@ -71,13 +74,19 @@ // flag can be defined (e.g. in your project file) to disable this behaviour. //#define RTMIDI_DO_NOT_ENSURE_UNIQUE_PORTNAMES +// Default for Windows UWP is to enable a workaround to fix BLE-MIDI IN ports' +// wrong timestamps that occur at least in Windows 10 21H2; +// this flag can be defined (e.g. in your project file) +// to disable this behavior. +//#define RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + // **************************************************************** // // // MidiInApi and MidiOutApi subclass prototypes. // // **************************************************************** // -#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) +#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(__WINDOWS_UWP__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) && !defined(__AMIDI__) #define __RTMIDI_DUMMY__ #endif @@ -254,6 +263,48 @@ class MidiOutWinMM: public MidiOutApi #endif +#if defined(__WINDOWS_UWP__) + +class MidiInWinUWP : public MidiInApi +{ +public: + MidiInWinUWP(const std::string& clientName, unsigned int queueSizeLimit); + ~MidiInWinUWP(void) override; + RtMidi::Api getCurrentApi(void) override { return RtMidi::WINDOWS_UWP; }; + void openPort(unsigned int portNumber, const std::string& portName) override; + void openVirtualPort(const std::string& portName) override; + void closePort(void) override; + void setClientName(const std::string& clientName) override; + void setPortName(const std::string& portName) override; + unsigned int getPortCount(void) override; + std::string getPortName(unsigned int portNumber) override; + double getMessage(std::vector* message) override; + +protected: + void initialize(const std::string& clientName) override; +}; + +class MidiOutWinUWP : public MidiOutApi +{ +public: + MidiOutWinUWP(const std::string& clientName); + ~MidiOutWinUWP(void) override; + RtMidi::Api getCurrentApi(void) override { return RtMidi::WINDOWS_UWP; }; + void openPort(unsigned int portNumber, const std::string& portName) override; + void openVirtualPort(const std::string& portName) override; + void closePort(void) override; + void setClientName(const std::string& clientName) override; + void setPortName(const std::string& portName) override; + unsigned int getPortCount(void) override; + std::string getPortName(unsigned int portNumber) override; + void sendMessage(const unsigned char* message, size_t size) override; + +protected: + void initialize(const std::string& clientName) override; +}; + +#endif + #if defined(__WEB_MIDI_API__) class MidiInWeb : public MidiInApi @@ -305,6 +356,67 @@ class MidiOutWeb: public MidiOutApi #endif +#if defined(__AMIDI__) + +#define LOG_TAG "RtMidi" +#include +#include +#include +#include +#include +#include +#include +#include + +class MidiInAndroid : public MidiInApi +{ + public: + MidiInAndroid(const std::string &/*clientName*/, unsigned int queueSizeLimit ); + ~MidiInAndroid( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::ANDROID_AMIDI; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + + void onMidiMessage( uint8_t* data, double domHishResTimeStamp ); + + void initialize( const std::string& clientName ); + void connect(); + AMidiDevice* receiveDevice = NULL; + AMidiOutputPort* midiOutputPort = NULL; + pthread_t readThread; + std::atomic reading = ATOMIC_VAR_INIT(false); + static void* pollMidi(void* context); + double lastTime; +}; + +class MidiOutAndroid: public MidiOutApi +{ + public: + MidiOutAndroid( const std::string &clientName ); + ~MidiOutAndroid( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::ANDROID_AMIDI; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + void sendMessage( const unsigned char *message, size_t size ); + + void initialize( const std::string& clientName ); + void connect(); + AMidiDevice* sendDevice = NULL; + AMidiInputPort* midiInputPort = NULL; +}; + +#endif + #if defined(__RTMIDI_DUMMY__) class MidiInDummy: public MidiInApi @@ -378,8 +490,10 @@ const char* rtmidi_api_names[][2] = { { "alsa" , "ALSA" }, { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, - { "web" , "Web MIDI API" }, { "dummy" , "Dummy" }, + { "web" , "Web MIDI API" }, + { "winuwp" , "Windows UWP" }, + { "amidi" , "Android MIDI API" }, }; const unsigned int rtmidi_num_api_names = sizeof(rtmidi_api_names)/sizeof(rtmidi_api_names[0]); @@ -399,11 +513,17 @@ extern "C" const RtMidi::Api rtmidi_compiled_apis[] = { #if defined(__WINDOWS_MM__) RtMidi::WINDOWS_MM, #endif +#if defined(__WINDOWS_UWP__) + RtMidi::WINDOWS_UWP, +#endif #if defined(__WEB_MIDI_API__) RtMidi::WEB_MIDI_API, #endif -#if defined(__RTMIDI_DUMMY__) - RtMidi::RTMIDI_DUMMY, +#if defined(__WEB_MIDI_API__) + RtMidi::WEB_MIDI_API, +#endif +#if defined(__AMIDI__) + RtMidi::ANDROID_AMIDI, #endif RtMidi::UNSPECIFIED, }; @@ -480,6 +600,10 @@ void RtMidiIn :: openMidiApi( RtMidi::Api api, const std::string &clientName, un if ( api == WINDOWS_MM ) rtapi_ = new MidiInWinMM( clientName, queueSizeLimit ); #endif +#if defined(__WINDOWS_UWP__) + if (api == WINDOWS_UWP) + rtapi_ = new MidiInWinUWP(clientName, queueSizeLimit); +#endif #if defined(__MACOSX_CORE__) if ( api == MACOSX_CORE ) rtapi_ = new MidiInCore( clientName, queueSizeLimit ); @@ -488,6 +612,10 @@ void RtMidiIn :: openMidiApi( RtMidi::Api api, const std::string &clientName, un if ( api == WEB_MIDI_API ) rtapi_ = new MidiInWeb( clientName, queueSizeLimit ); #endif +#if defined(__AMIDI__) + if ( api == ANDROID_AMIDI ) + rtapi_ = new MidiInAndroid( clientName, queueSizeLimit ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiInDummy( clientName, queueSizeLimit ); @@ -552,6 +680,10 @@ void RtMidiOut :: openMidiApi( RtMidi::Api api, const std::string &clientName ) if ( api == WINDOWS_MM ) rtapi_ = new MidiOutWinMM( clientName ); #endif +#if defined(__WINDOWS_UWP__) + if (api == WINDOWS_UWP) + rtapi_ = new MidiOutWinUWP(clientName); +#endif #if defined(__MACOSX_CORE__) if ( api == MACOSX_CORE ) rtapi_ = new MidiOutCore( clientName ); @@ -560,6 +692,10 @@ void RtMidiOut :: openMidiApi( RtMidi::Api api, const std::string &clientName ) if ( api == WEB_MIDI_API ) rtapi_ = new MidiOutWeb( clientName ); #endif +#if defined(__AMIDI__) + if ( api == ANDROID_AMIDI ) + rtapi_ = new MidiOutAndroid( clientName ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiOutDummy( clientName ); @@ -1176,7 +1312,7 @@ unsigned int MidiInCore :: getPortCount() // This function was submitted by Douglas Casey Tucker and apparently // derived largely from PortMidi. -CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) +CFStringRef CreateEndpointName( MIDIEndpointRef endpoint, bool isExternal ) { CFMutableStringRef result = CFStringCreateMutable( NULL, 0 ); CFStringRef str; @@ -1186,13 +1322,10 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) MIDIObjectGetStringProperty( endpoint, kMIDIPropertyName, &str ); if ( str != NULL ) { CFStringAppend( result, str ); - CFRelease( str ); } // some MIDI devices have a leading space in endpoint name. trim - CFStringRef space = CFStringCreateWithCString(NULL, " ", kCFStringEncodingUTF8); - CFStringTrim(result, space); - CFRelease(space); + CFStringTrim(result, CFSTR(" ")); MIDIEntityRef entity = 0; MIDIEndpointGetEntity( endpoint, &entity ); @@ -1206,7 +1339,6 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) MIDIObjectGetStringProperty( entity, kMIDIPropertyName, &str ); if ( str != NULL ) { CFStringAppend( result, str ); - CFRelease( str ); } } // now consider the device's name @@ -1219,6 +1351,7 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) MIDIObjectGetStringProperty( device, kMIDIPropertyName, &str ); if ( CFStringGetLength( result ) == 0 ) { CFRelease( result ); + CFRetain( str ); return str; } if ( str != NULL ) { @@ -1226,10 +1359,10 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) // the endpoint name and just use the device name if ( isExternal && MIDIDeviceGetNumberOfEntities( device ) < 2 ) { CFRelease( result ); + CFRetain( str ); return str; } else { if ( CFStringGetLength( str ) == 0 ) { - CFRelease( str ); return result; } // does the entity name already start with the device name? @@ -1244,7 +1377,6 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) CFStringInsert( result, 0, str ); } - CFRelease( str ); } } return result; @@ -1252,7 +1384,7 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) // This function was submitted by Douglas Casey Tucker and apparently // derived largely from PortMidi. -static CFStringRef ConnectedEndpointName( MIDIEndpointRef endpoint ) +static CFStringRef CreateConnectedEndpointName( MIDIEndpointRef endpoint ) { CFMutableStringRef result = CFStringCreateMutable( NULL, 0 ); CFStringRef str; @@ -1279,11 +1411,12 @@ static CFStringRef ConnectedEndpointName( MIDIEndpointRef endpoint ) if ( connObjectType == kMIDIObjectType_ExternalSource || connObjectType == kMIDIObjectType_ExternalDestination ) { // Connected to an external device's endpoint (10.3 and later). - str = EndpointName( (MIDIEndpointRef)(connObject), true ); + str = CreateEndpointName( (MIDIEndpointRef)(connObject), true ); } else { // Connected to an external device (10.2) (or something else, catch- str = NULL; MIDIObjectGetStringProperty( connObject, kMIDIPropertyName, &str ); + if ( str ) CFRetain ( str ); } if ( str != NULL ) { if ( anyStrings ) @@ -1304,7 +1437,7 @@ static CFStringRef ConnectedEndpointName( MIDIEndpointRef endpoint ) CFRelease( result ); // Here, either the endpoint had no connections, or we failed to obtain names - return EndpointName( endpoint, false ); + return CreateEndpointName( endpoint, false ); } std::string MidiInCore :: getPortName( unsigned int portNumber ) @@ -1324,7 +1457,7 @@ std::string MidiInCore :: getPortName( unsigned int portNumber ) } portRef = MIDIGetSource( portNumber ); - nameRef = ConnectedEndpointName( portRef ); + nameRef = CreateConnectedEndpointName( portRef ); CFStringGetCString( nameRef, name, sizeof(name), kCFStringEncodingUTF8 ); CFRelease( nameRef ); @@ -1411,7 +1544,7 @@ std::string MidiOutCore :: getPortName( unsigned int portNumber ) } portRef = MIDIGetDestination( portNumber ); - nameRef = ConnectedEndpointName(portRef); + nameRef = CreateConnectedEndpointName(portRef); CFStringGetCString( nameRef, name, sizeof(name), kCFStringEncodingUTF8 ); CFRelease( nameRef ); @@ -2728,7 +2861,7 @@ void MidiInWinMM :: openPort( unsigned int portNumber, const std::string &/*port // Allocate and init the sysex buffers. data->sysexBuffer.resize( inputData_.bufferCount ); - for ( int i=0; i < inputData_.bufferCount; ++i ) { + for ( unsigned int i=0; i < inputData_.bufferCount; ++i ) { data->sysexBuffer[i] = (MIDIHDR*) new char[ sizeof(MIDIHDR) ]; data->sysexBuffer[i]->lpData = new char[ inputData_.bufferSize ]; data->sysexBuffer[i]->dwBufferLength = inputData_.bufferSize; @@ -2782,7 +2915,7 @@ void MidiInWinMM :: closePort( void ) midiInReset( data->inHandle ); midiInStop( data->inHandle ); - for ( int i=0; i < data->sysexBuffer.size(); ++i ) { + for ( size_t i=0; i < data->sysexBuffer.size(); ++i ) { int result = midiInUnprepareHeader(data->inHandle, data->sysexBuffer[i], sizeof(MIDIHDR)); delete [] data->sysexBuffer[i]->lpData; delete [] data->sysexBuffer[i]; @@ -3023,7 +3156,7 @@ void MidiOutWinMM :: sendMessage( const unsigned char *message, size_t size ) for ( unsigned int i=0; i -#include -#include -#include -#ifdef HAVE_SEMAPHORE - #include -#endif - -#define JACK_RINGBUFFER_SIZE 16384 // Default size for ringbuffer - -struct JackMidiData { - jack_client_t *client; - jack_port_t *port; - jack_ringbuffer_t *buff; - int buffMaxWrite; // actual writable size, usually 1 less than ringbuffer - jack_time_t lastTime; -#ifdef HAVE_SEMAPHORE - sem_t sem_cleanup; - sem_t sem_needpost; -#endif - MidiInApi :: RtMidiInData *rtMidiIn; - }; +#if defined(__WINDOWS_UWP__) -//*********************************************************************// -// API: JACK -// Class Definitions: MidiInJack -//*********************************************************************// +#include +#include +#include +#include +#include -static int jackProcessIn( jack_nframes_t nframes, void *arg ) +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace winrt; +using namespace Windows::Foundation; +using namespace Windows::Devices::Enumeration; +using namespace Windows::Devices::Midi; +using namespace Windows::Storage::Streams; +using namespace Windows::Security::Cryptography; + +// Class for initializing C++/WinRT +class UWPMidiInit { - JackMidiData *jData = (JackMidiData *) arg; - MidiInApi :: RtMidiInData *rtData = jData->rtMidiIn; - jack_midi_event_t event; - jack_time_t time; +public: + UWPMidiInit() + { + winrt::init_apartment(); + } +}; - // Is port created? - if ( jData->port == NULL ) return 0; +// Class for handling UWP MIDI +class UWPMidiClass +{ +public: + // Structure to store MIDI port name and ID + struct port + { + std::string name; + std::wstring id; + std::string hex_id; + std::string display_name; + }; - void *buff = jack_port_get_buffer( jData->port, nframes ); - bool& continueSysex = rtData->continueSysex; - unsigned char& ignoreFlags = rtData->ignoreFlags; + UWPMidiClass(MidiApi& midi_api) : + midi_api_(midi_api) + { + } - // We have midi events in buffer - int evCount = jack_midi_get_event_count( buff ); - for (int j = 0; j < evCount; j++) { - MidiInApi::MidiMessage& message = rtData->message; - jack_midi_event_get( &event, buff, j ); + ~UWPMidiClass() + { + close(); + } - // Compute the delta time. - time = jack_get_time(); - if ( rtData->firstMessage == true ) { - message.timeStamp = 0.0; - rtData->firstMessage = false; - } else - message.timeStamp = ( time - jData->lastTime ) * 0.000001; + // Initialize for MIDI IN + void in_init(MidiInApi::RtMidiInData* input_data) + { + input_data_ = input_data; - jData->lastTime = time; + try + { + ports_ = list_ports(MidiInPort::GetDeviceSelector()); + } + catch (hresult_error const& ex) + { + raise_hresult_error("UWPMidiClass::in_init: ", ex); + } + sort_display_name(ports_); + } - if ( !continueSysex ) - message.bytes.clear(); + // Initialize for MIDI OUT + void out_init() + { + try + { + ports_ = list_ports(MidiOutPort::GetDeviceSelector()); + fix_display_name(list_ports(MidiInPort::GetDeviceSelector()), ports_); + } + catch (hresult_error const& ex) + { + raise_hresult_error("UWPMidiClass::out_init: ", ex); + } + sort_display_name(ports_); + } - if ( !( ( continueSysex || event.buffer[0] == 0xF0 ) && ( ignoreFlags & 0x01 ) ) ) { - // Unless this is a (possibly continued) SysEx message and we're ignoring SysEx, - // copy the event buffer into the MIDI message struct. - for ( unsigned int i = 0; i < event.size; i++ ) - message.bytes.push_back( event.buffer[i] ); + size_t get_num_ports() + { + return ports_.size(); } - switch ( event.buffer[0] ) { - case 0xF0: - // Start of a SysEx message - continueSysex = event.buffer[event.size - 1] != 0xF7; - if ( ignoreFlags & 0x01 ) continue; - break; - case 0xF1: - case 0xF8: - // MIDI Time Code or Timing Clock message - if ( ignoreFlags & 0x02 ) continue; - break; - case 0xFE: - // Active Sensing message - if ( ignoreFlags & 0x04 ) continue; - break; - default: - if ( continueSysex ) { - // Continuation of a SysEx message - continueSysex = event.buffer[event.size - 1] != 0xF7; - if ( ignoreFlags & 0x01 ) continue; - } - // All other MIDI messages + std::string get_port_name(size_t n) + { + return ports_[n].display_name; } - if ( !continueSysex ) { - // If not a continuation of a SysEx message, - // invoke the user callback function or queue the message. - if ( rtData->usingCallback ) { - RtMidiIn::RtMidiCallback callback = (RtMidiIn::RtMidiCallback) rtData->userCallback; - callback( message.timeStamp, &message.bytes, rtData->userData ); - } - else { - // As long as we haven't reached our queue size limit, push the message. - if ( !rtData->queue.push( message ) ) - std::cerr << "\nMidiInJack: message queue limit reached!!\n\n"; - } + bool in_open(size_t port_number); + bool out_open(size_t port_number); + void close(); + + void midi_in_callback(const MidiInPort&, const MidiMessageReceivedEventArgs& e); + bool send_buffer(const unsigned char* buff, size_t len); + + // Raise RtMidi error for hresult error + void raise_hresult_error(std::string_view message, hresult_error const& ex) + { + std::ostringstream ss; + ss << message << "exception HRESULT 0x" << std::hex << ex.code() << ", " + << utf16_to_utf8(static_cast(ex.message())) + << "\n"; + midi_api_.error(RtMidiError::DRIVER_ERROR, ss.str()); } - } - return 0; -} + // Mutex for MIDI port open/close + std::mutex mtx_open_close_; + // Mutex for MIDI IN message queue access + std::mutex mtx_queue_; + +private: + std::vector list_ports(winrt::hstring device_selector); + void fix_display_name(const std::vector& in_ports, + std::vector& out_ports); + void sort_display_name(std::vector& ports); + std::string utf16_to_utf8(const std::wstring_view wstr); + + template + IMidiPort_T open(size_t port_number); + + // MidiApi class + MidiApi& midi_api_; + + // List of MIDI ports + std::vector ports_; + // MIDI IN port + MidiInPort in_port_{ nullptr }; + // MIDI OUT port + IMidiOutPort out_port_{ nullptr }; + // Backup initial MessageReceived event token + winrt::event_token before_token_; + // Input data + MidiInApi::RtMidiInData* input_data_{ nullptr }; + // Last timestamp + std::chrono::duration last_time_{ 0 }; + + // C++/WinRT initializer + static UWPMidiInit uwp_midi_init_; + // Regex pattern to extract 8 hex digits from UWP MIDI ID string + static const std::wregex hex_id_pattern_; + +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + // QueryPerformanceFrequency + LONGLONG qpc_freq_{ 0 }; + // Last QueryPerformanceCounter + LONGLONG before_qpc_; + // Weather overflow low occurred or not + bool b_overflow_low_{ false }; + + // BLE-MIDI timestamp periods + inline constexpr static std::chrono::duration ble_midi_period_low_{ std::chrono::milliseconds{128} }; + inline constexpr static std::chrono::duration ble_midi_period_high_{ std::chrono::milliseconds{8192} }; + // QPC threshold 4096 ms + inline constexpr static LONGLONG qpc_threshold_{ 4096 }; + + // Regex pattern to detect BLE-MIDI IN + static const std::wregex ble_midi_pattern_; +#endif +}; -MidiInJack :: MidiInJack( const std::string &clientName, unsigned int queueSizeLimit ) - : MidiInApi( queueSizeLimit ) -{ - MidiInJack::initialize( clientName ); -} +// C++/WinRT initializer +UWPMidiInit UWPMidiClass::uwp_midi_init_; +// Regex pattern to extract 8 hex digits from UWP MIDI ID string +const std::wregex UWPMidiClass::hex_id_pattern_{ std::wregex(L"#MIDII_([0-9A-F]{8})\\..+#") }; -void MidiInJack :: initialize( const std::string& clientName ) +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS +const std::wregex UWPMidiClass::ble_midi_pattern_{ std::wregex(L"#MIDII_[0-9A-F]{8}\\.BLE[0-9]{2}#") }; +#endif + +// Find and create a list of UWP MIDI ports +std::vector UWPMidiClass::list_ports(winrt::hstring device_selector) { - JackMidiData *data = new JackMidiData; - apiData_ = (void *) data; + const auto devs{ DeviceInformation::FindAllAsync(device_selector).get() }; - data->rtMidiIn = &inputData_; - data->port = NULL; - data->client = NULL; - this->clientName = clientName; + std::vector retval; + for (const auto& d : devs) + { + port p; + p.name = utf16_to_utf8(d.Name()); + p.id = d.Id(); - connect(); + std::wsmatch m; + if (std::regex_search(p.id, m, hex_id_pattern_)) + { + // Ordinary MIDI ports + // Append hex digits extracted from the UWP MIDI ID string to the port name. + p.hex_id = utf16_to_utf8(m[1].str()); + + std::ostringstream ss; + ss << p.name + << " [ " + << p.hex_id + << " ]"; + p.display_name = ss.str(); + } + else + { + // Microsoft GS Wavetable Synth etc. + // Unable to extract hex digits from UWP MIDI ID string. + // Use the device name as the port name. + p.display_name = p.name; + } + + retval.push_back(p); + } + return retval; } -void MidiInJack :: connect() +// Fix MIDI OUT port names starting with `MIDI` to MIDI IN port names with similar ID strings +void UWPMidiClass::fix_display_name(const std::vector& in_ports, + std::vector& out_ports) { - JackMidiData *data = static_cast (apiData_); - if ( data->client ) - return; - - // Initialize JACK client - if (( data->client = jack_client_open( clientName.c_str(), JackNoStartServer, NULL )) == 0) { - errorString_ = "MidiInJack::initialize: JACK server not running?"; - error( RtMidiError::WARNING, errorString_ ); - return; - } + for (auto& outp : out_ports) + { + if (outp.hex_id.empty() || + std::string_view{ outp.name }.substr(0, 4) != "MIDI") + continue; - jack_set_process_callback( data->client, jackProcessIn, data ); - jack_activate( data->client ); + for (const auto& inp : in_ports) + { + if (outp.hex_id == inp.hex_id) + { + outp.display_name = inp.display_name; + break; + } + } + } } -MidiInJack :: ~MidiInJack() +void UWPMidiClass::sort_display_name(std::vector& ports) { - JackMidiData *data = static_cast (apiData_); - MidiInJack::closePort(); + std::sort(ports.begin(), ports.end(), + [](const auto& lhs, const auto& rhs) + { + return lhs.display_name < rhs.display_name; + }); +} - if ( data->client ) - jack_client_close( data->client ); - delete data; +std::string UWPMidiClass::utf16_to_utf8(const std::wstring_view wstr) +{ + auto len{ WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), nullptr, 0, nullptr, nullptr) }; + std::string u8str(len, '\0'); + if (len) + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), static_cast(wstr.size()), u8str.data(), len, nullptr, nullptr); + return u8str; } -void MidiInJack :: openPort( unsigned int portNumber, const std::string &portName ) +// Open MIDI IN/OUT port +template +IMidiPort_T UWPMidiClass::open(size_t port_number) { - JackMidiData *data = static_cast (apiData_); + try + { + auto async{ MidiPort_T::FromIdAsync(ports_[port_number].id) }; + // Timeout 3 seconds + if (async.wait_for(std::chrono::seconds(3)) == AsyncStatus::Completed) + return async.GetResults(); + } + catch (hresult_error const& ex) + { + raise_hresult_error("UWPMidiClass::open: ", ex); + } + return nullptr; +} - connect(); +// Open MIDI IN port +bool UWPMidiClass::in_open(size_t port_number) +{ + if (in_port_) + in_port_.Close(); - // Creating new port - if ( data->port == NULL ) - data->port = jack_port_register( data->client, portName.c_str(), - JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0 ); + in_port_ = open(port_number); + if (!in_port_) + return false; - if ( data->port == NULL ) { +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + std::wsmatch m; + if (std::regex_search(ports_[port_number].id, m, ble_midi_pattern_)) + { + // BLE-MIDI IN port + LARGE_INTEGER li; + if (::QueryPerformanceFrequency(&li)) + qpc_freq_ = li.QuadPart; + } +#endif + + try + { + before_token_ = in_port_.MessageReceived({ this, &UWPMidiClass::midi_in_callback }); + } + catch (hresult_error const& ex) + { + raise_hresult_error("UWPMidiClass::in_open: ", ex); + } + + return true; +} + +// Open MIDI Out port +bool UWPMidiClass::out_open(size_t port_number) +{ + if (out_port_) + out_port_.Close(); + + out_port_ = open(port_number); + if (!out_port_) + return false; + + return true; +} + +// Close MIDI IN/OUT port +void UWPMidiClass::close() +{ + if (in_port_) + { + if (before_token_) + in_port_.MessageReceived(before_token_); + + in_port_.Close(); + in_port_ = nullptr; + } + if (out_port_) + { + out_port_.Close(); + out_port_ = nullptr; + } +} + +// MessageReceived event handler +void UWPMidiClass::midi_in_callback(const MidiInPort&, const MidiMessageReceivedEventArgs& e) +{ +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + LARGE_INTEGER qpc; + if (qpc_freq_) + { + if (!::QueryPerformanceCounter(&qpc)) + qpc_freq_ = 0; + } +#endif + + const auto& m{ e.Message() }; + if (!m) + return; + + MidiInApi::MidiMessage message; + const std::chrono::duration duration{ m.Timestamp() }; + + // Calculate time stamp. + if (input_data_->firstMessage == true) + { + message.timeStamp = 0.0; + input_data_->firstMessage = false; + last_time_ = duration; + +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + if (qpc_freq_) + before_qpc_ = qpc.QuadPart; +#endif + } + else + { + auto delta{ duration - last_time_ }; + +#ifndef RTMIDI_DO_NOT_ENABLE_WORKAROUND_UWP_WRONG_TIMESTAMPS + if (qpc_freq_) + { + if (b_overflow_low_) + { + if (delta >= ble_midi_period_low_) + { + // Fix after overflow low + // https://github.com/trueroad/BLE_MIDI_packet_data_set#page-7-overflow-both + delta -= ble_midi_period_low_; + b_overflow_low_ = false; + } + } + else + { + if ((ble_midi_period_high_ - ble_midi_period_low_) < delta && delta < ble_midi_period_high_ && + ((before_qpc_ - qpc.QuadPart) * 1000 / qpc_freq_) < qpc_threshold_) + { + // Fix overflow low + // https://github.com/trueroad/BLE_MIDI_packet_data_set#page-7-overflow-low + delta = delta - ble_midi_period_high_ + ble_midi_period_low_; + b_overflow_low_ = true; + } + } + + before_qpc_ = qpc.QuadPart; + } +#endif + + const std::chrono::duration sec{ delta }; + message.timeStamp = sec.count(); + } + + if (((input_data_->ignoreFlags & 0x01) && + (m.Type() == MidiMessageType::SystemExclusive || m.Type() == MidiMessageType::EndSystemExclusive)) || + ((input_data_->ignoreFlags & 0x02) && + (m.Type() == MidiMessageType::MidiTimeCode || m.Type() == MidiMessageType::TimingClock)) || + ((input_data_->ignoreFlags & 0x04) && + m.Type() == MidiMessageType::ActiveSensing)) + { + return; + } + + const auto& raw_data{ m.RawData() }; + const size_t len{ raw_data.Length() }; + + if (len) + message.bytes.assign(raw_data.data(), raw_data.data() + len); + + last_time_ = duration; + + if (input_data_->usingCallback) + { + (input_data_->userCallback)(message.timeStamp, &message.bytes, input_data_->userData); + } + else + { + std::lock_guard lock(mtx_queue_); + + if (!input_data_->queue.push(message)) + { + std::cerr << "\nMidiInWinUWP: message queue limit reached!!\n\n"; + } + } +} + +// Send MIDI message +bool UWPMidiClass::send_buffer(const unsigned char* buff, size_t len) +{ + if (!out_port_) + return false; + + try + { + out_port_.SendBuffer(CryptographicBuffer::CreateFromByteArray(array_view(buff, buff + len))); + } + catch (hresult_error const& ex) + { + raise_hresult_error("UWPMidiClass::send_buffer: ", ex); + } + + return true; +} + +//*********************************************************************// +// API: Windows UWP +// Class Definitions: MidiInWinUWP +//*********************************************************************// + +MidiInWinUWP::MidiInWinUWP(const std::string& clientName, unsigned int queueSizeLimit) + : MidiInApi(queueSizeLimit) +{ + MidiInWinUWP::initialize(clientName); +} + +MidiInWinUWP :: ~MidiInWinUWP() +{ + // Close a connection if it exists. + MidiInWinUWP::closePort(); + + // Cleanup. + UWPMidiClass *data = static_cast (apiData_); + delete data; +} + +void MidiInWinUWP::initialize(const std::string& /*clientName*/) +{ + // Save our api-specific connection information. + UWPMidiClass* data{ new UWPMidiClass(*this) }; + data->in_init(&inputData_); + apiData_ = static_cast(data); + + // We'll issue a warning here if no devices are available but not + // throw an error since the user can plugin something later. + const auto nDevices{ data->get_num_ports() }; + if (nDevices == 0) + { + errorString_ = "MidiInWinUWP::initialize: no MIDI input devices currently available."; + error(RtMidiError::WARNING, errorString_); + } +} + +void MidiInWinUWP::openPort(unsigned int portNumber, const std::string&/*portName*/) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + std::lock_guard lock(data->mtx_open_close_); + + if (connected_) + { + errorString_ = "MidiInWinUWP::openPort: a valid connection already exists!"; + error(RtMidiError::WARNING, errorString_); + return; + } + + if (data->get_num_ports() == 0) + { + errorString_ = "MidiInWinUWP::openPort: no MIDI input sources found!"; + error(RtMidiError::NO_DEVICES_FOUND, errorString_); + return; + } + + if (portNumber >= data->get_num_ports()) + { + std::ostringstream ost; + ost << "MidiInWinUWP::openPort: the 'portNumber' argument (" << portNumber << ") is invalid."; + errorString_ = ost.str(); + error(RtMidiError::INVALID_PARAMETER, errorString_); + return; + } + + if (!data->in_open(portNumber)) + { + errorString_ = "MidiInWinUWP::openPort: error creating Windows UWP MIDI input port."; + error(RtMidiError::DRIVER_ERROR, errorString_); + return; + } + + connected_ = true; +} + +void MidiInWinUWP::openVirtualPort(const std::string&/*portName*/) +{ + // This function cannot be implemented for the Windows UWP MIDI API. + errorString_ = "MidiInWinUWP::openVirtualPort: cannot be implemented in Windows UWP MIDI API!"; + error(RtMidiError::WARNING, errorString_); +} + +void MidiInWinUWP::closePort(void) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + std::lock_guard lock(data->mtx_open_close_); + + if (connected_) + { + data->close(); + connected_ = false; + } +} + +void MidiInWinUWP::setClientName(const std::string&) +{ + errorString_ = "MidiInWinUWP::setClientName: this function is not implemented for the WINDOWS_UWP API!"; + error(RtMidiError::WARNING, errorString_); +} + +void MidiInWinUWP::setPortName(const std::string&) +{ + errorString_ = "MidiInWinUWP::setPortName: this function is not implemented for the WINDOWS_UWP API!"; + error(RtMidiError::WARNING, errorString_); +} + +unsigned int MidiInWinUWP::getPortCount() +{ + UWPMidiClass* data{ static_cast(apiData_) }; + return static_cast(data->get_num_ports()); +} + +std::string MidiInWinUWP::getPortName(unsigned int portNumber) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + + const auto nDevices{ data->get_num_ports() }; + if (portNumber >= nDevices) + { + std::ostringstream ost; + ost << "MidiInWinUWP::getPortName: the 'portNumber' argument (" << portNumber << ") is invalid."; + errorString_ = ost.str(); + error(RtMidiError::WARNING, errorString_); + return ""; + } + + return data->get_port_name(portNumber); +} + +double MidiInWinUWP::getMessage(std::vector* message) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + std::lock_guard lock(data->mtx_queue_); + + return MidiInApi::getMessage(message); +} + +//*********************************************************************// +// API: Windows UWP +// Class Definitions: MidiOutWinUWP +//*********************************************************************// + +MidiOutWinUWP::MidiOutWinUWP(const std::string& clientName) : MidiOutApi() +{ + MidiOutWinUWP::initialize(clientName); +} + +MidiOutWinUWP :: ~MidiOutWinUWP() +{ + // Close a connection if it exists. + MidiOutWinUWP::closePort(); + + // Cleanup. + UWPMidiClass* data = static_cast (apiData_); + delete data; +} + +void MidiOutWinUWP::initialize(const std::string& /*clientName*/) +{ + // Save our api-specific connection information. + UWPMidiClass* data{ new UWPMidiClass(*this) }; + data->out_init(); + apiData_ = static_cast(data); + + // We'll issue a warning here if no devices are available but not + // throw an error since the user can plug something in later. + const auto nDevices{ data->get_num_ports() }; + if (nDevices == 0) + { + errorString_ = "MidiOutWinUWP::initialize: no MIDI output devices currently available."; + error(RtMidiError::WARNING, errorString_); + } +} + +unsigned int MidiOutWinUWP::getPortCount() +{ + UWPMidiClass* data{ static_cast(apiData_) }; + return static_cast(data->get_num_ports()); +} + +std::string MidiOutWinUWP::getPortName(unsigned int portNumber) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + + const auto nDevices{ data->get_num_ports() }; + if (portNumber >= nDevices) + { + std::ostringstream ost; + ost << "MidiOutWinUWP::getPortName: the 'portNumber' argument (" << portNumber << ") is invalid."; + errorString_ = ost.str(); + error(RtMidiError::WARNING, errorString_); + return ""; + } + + return data->get_port_name(portNumber); +} + +void MidiOutWinUWP::openPort(unsigned int portNumber, const std::string&/*portName*/) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + std::lock_guard lock(data->mtx_open_close_); + + if (connected_) + { + errorString_ = "MidiOutWinUWP::openPort: a valid connection already exists!"; + error(RtMidiError::WARNING, errorString_); + return; + } + + if (data->get_num_ports() == 0) + { + errorString_ = "MidiOutWinUWP::openPort: no MIDI output destinations found!"; + error(RtMidiError::NO_DEVICES_FOUND, errorString_); + return; + } + + if (portNumber >= data->get_num_ports()) + { + std::ostringstream ost; + ost << "MidiOutWinUWP::openPort: the 'portNumber' argument (" << portNumber << ") is invalid."; + errorString_ = ost.str(); + error(RtMidiError::INVALID_PARAMETER, errorString_); + return; + } + + if (!data->out_open(portNumber)) + { + errorString_ = "MidiOutWinUWP::openPort: error creating Windows UWP MIDI output port."; + error(RtMidiError::DRIVER_ERROR, errorString_); + return; + } + + connected_ = true; +} + +void MidiOutWinUWP::closePort(void) +{ + UWPMidiClass* data{ static_cast(apiData_) }; + std::lock_guard lock(data->mtx_open_close_); + + if (connected_) + { + data->close(); + connected_ = false; + } +} + +void MidiOutWinUWP::setClientName(const std::string&) +{ + errorString_ = "MidiOutWinUWP::setClientName: this function is not implemented for the WINDOWS_UWP API!"; + error(RtMidiError::WARNING, errorString_); +} + +void MidiOutWinUWP::setPortName(const std::string&) +{ + errorString_ = "MidiOutWinUWP::setPortName: this function is not implemented for the WINDOWS_UWP API!"; + error(RtMidiError::WARNING, errorString_); +} + +void MidiOutWinUWP::openVirtualPort(const std::string&/*portName*/) +{ + // This function cannot be implemented for the Windows UWP MIDI API. + errorString_ = "MidiOutWinUWP::openVirtualPort: cannot be implemented in Windows UWP MIDI API!"; + error(RtMidiError::WARNING, errorString_); +} + +void MidiOutWinUWP::sendMessage(const unsigned char* message, size_t size) +{ + if (!connected_) + return; + + if (size == 0) + { + errorString_ = "MidiOutWinUWP::sendMessage: message argument is empty!"; + error(RtMidiError::WARNING, errorString_); + return; + } + + UWPMidiClass* data{ static_cast(apiData_) }; + if (!data->send_buffer(message, size)) + { + errorString_ = "MidiOutWinUWP::sendMessage: error sending message."; + error(RtMidiError::DRIVER_ERROR, errorString_); + } +} + +#endif // __WINDOWS_UWP__ + + +//*********************************************************************// +// API: UNIX JACK +// +// Written primarily by Alexander Svetalkin, with updates for delta +// time by Gary Scavone, April 2011. +// +// *********************************************************************// + +#if defined(__UNIX_JACK__) + +// JACK header files +#include +#include +#include +#include +#include +#ifdef HAVE_SEMAPHORE + #include +#endif + +#define JACK_RINGBUFFER_SIZE 16384 // Default size for ringbuffer + +struct JackMidiData { + jack_client_t *client; + jack_port_t *port; + jack_ringbuffer_t *buff; + int buffMaxWrite; // actual writable size, usually 1 less than ringbuffer + jack_time_t lastTime; +#ifdef HAVE_SEMAPHORE + sem_t sem_cleanup; + sem_t sem_needpost; +#endif + MidiInApi :: RtMidiInData *rtMidiIn; + }; + +//*********************************************************************// +// API: JACK +// Class Definitions: MidiInJack +//*********************************************************************// + +static int jackProcessIn( jack_nframes_t nframes, void *arg ) +{ + JackMidiData *jData = (JackMidiData *) arg; + MidiInApi :: RtMidiInData *rtData = jData->rtMidiIn; + jack_midi_event_t event; + jack_time_t time; + + // Is port created? + if ( jData->port == NULL ) return 0; + + void *buff = jack_port_get_buffer( jData->port, nframes ); + bool& continueSysex = rtData->continueSysex; + unsigned char& ignoreFlags = rtData->ignoreFlags; + + // We have midi events in buffer + int evCount = jack_midi_get_event_count( buff ); + for (int j = 0; j < evCount; j++) { + MidiInApi::MidiMessage& message = rtData->message; + jack_midi_event_get( &event, buff, j ); + + // Compute the delta time. + time = jack_get_time(); + if ( rtData->firstMessage == true ) { + message.timeStamp = 0.0; + rtData->firstMessage = false; + } else + message.timeStamp = ( time - jData->lastTime ) * 0.000001; + + jData->lastTime = time; + + if ( !continueSysex ) + message.bytes.clear(); + + if ( !( ( continueSysex || event.buffer[0] == 0xF0 ) && ( ignoreFlags & 0x01 ) ) ) { + // Unless this is a (possibly continued) SysEx message and we're ignoring SysEx, + // copy the event buffer into the MIDI message struct. + for ( unsigned int i = 0; i < event.size; i++ ) + message.bytes.push_back( event.buffer[i] ); + } + + switch ( event.buffer[0] ) { + case 0xF0: + // Start of a SysEx message + continueSysex = event.buffer[event.size - 1] != 0xF7; + if ( ignoreFlags & 0x01 ) continue; + break; + case 0xF1: + case 0xF8: + // MIDI Time Code or Timing Clock message + if ( ignoreFlags & 0x02 ) continue; + break; + case 0xFE: + // Active Sensing message + if ( ignoreFlags & 0x04 ) continue; + break; + default: + if ( continueSysex ) { + // Continuation of a SysEx message + continueSysex = event.buffer[event.size - 1] != 0xF7; + if ( ignoreFlags & 0x01 ) continue; + } + // All other MIDI messages + } + + if ( !continueSysex ) { + // If not a continuation of a SysEx message, + // invoke the user callback function or queue the message. + if ( rtData->usingCallback ) { + RtMidiIn::RtMidiCallback callback = (RtMidiIn::RtMidiCallback) rtData->userCallback; + callback( message.timeStamp, &message.bytes, rtData->userData ); + } + else { + // As long as we haven't reached our queue size limit, push the message. + if ( !rtData->queue.push( message ) ) + std::cerr << "\nMidiInJack: message queue limit reached!!\n\n"; + } + } + } + + return 0; +} + +MidiInJack :: MidiInJack( const std::string &clientName, unsigned int queueSizeLimit ) + : MidiInApi( queueSizeLimit ) +{ + MidiInJack::initialize( clientName ); +} + +void MidiInJack :: initialize( const std::string& clientName ) +{ + JackMidiData *data = new JackMidiData; + apiData_ = (void *) data; + + data->rtMidiIn = &inputData_; + data->port = NULL; + data->client = NULL; + this->clientName = clientName; + + connect(); +} + +void MidiInJack :: connect() +{ + JackMidiData *data = static_cast (apiData_); + if ( data->client ) + return; + + // Initialize JACK client + if (( data->client = jack_client_open( clientName.c_str(), JackNoStartServer, NULL )) == 0) { + errorString_ = "MidiInJack::initialize: JACK server not running?"; + error( RtMidiError::WARNING, errorString_ ); + return; + } + + jack_set_process_callback( data->client, jackProcessIn, data ); + jack_activate( data->client ); +} + +MidiInJack :: ~MidiInJack() +{ + JackMidiData *data = static_cast (apiData_); + MidiInJack::closePort(); + + if ( data->client ) + jack_client_close( data->client ); + delete data; +} + +void MidiInJack :: openPort( unsigned int portNumber, const std::string &portName ) +{ + JackMidiData *data = static_cast (apiData_); + + connect(); + + // Creating new port + if ( data->port == NULL ) + data->port = jack_port_register( data->client, portName.c_str(), + JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0 ); + + if ( data->port == NULL ) { errorString_ = "MidiInJack::openPort: JACK error creating port"; if (portName.size() >= (size_t)jack_port_name_size()) errorString_ += " (port name too long?)"; @@ -3608,7 +4503,7 @@ void MidiOutJack :: sendMessage( const unsigned char *message, size_t size ) return; while ( jack_ringbuffer_write_space(data->buff) < sizeof(nBytes) + size ) - pthread_yield(); + sched_yield(); // Write full message to buffer jack_ringbuffer_write( data->buff, ( char * ) &nBytes, sizeof( nBytes ) ); @@ -3723,7 +4618,7 @@ std::string WebMidiAccessShim::getPortName( unsigned int portNumber, bool isInpu var ret = _malloc(length); stringToUTF8(port.name, ret, length); return ret; - }, portNumber, isInput, &ret ); + }, portNumber, isInput); if (ret == nullptr) return ""; std::string s = ret; @@ -3778,6 +4673,7 @@ void MidiInWeb::openPort( unsigned int portNumber, const std::string &portName ) }; }, portNumber, &inputData_ ); open_port_number = portNumber; + connected_ = true; } void MidiInWeb::openVirtualPort( const std::string &portName ) @@ -3803,6 +4699,7 @@ void MidiInWeb::closePort( void ) input.onmidimessage = null; }, open_port_number ); open_port_number = -1; + connected_ = false; } void MidiInWeb::setClientName( const std::string &clientName ) @@ -3862,6 +4759,7 @@ void MidiOutWeb::openPort( unsigned int portNumber, const std::string &portName // In Web MIDI API world, there is no step to open a port. open_port_number = portNumber; + connected_ = true; } void MidiOutWeb::openVirtualPort( const std::string &portName ) @@ -3876,6 +4774,7 @@ void MidiOutWeb::closePort( void ) { // there is really nothing to do for output at JS side. open_port_number = -1; + connected_ = false; } void MidiOutWeb::setClientName( const std::string &clientName ) @@ -3932,3 +4831,434 @@ void MidiOutWeb::initialize( const std::string& clientName ) } #endif // __WEB_MIDI_API__ + + +//*********************************************************************// +// API: ANDROID AMIDI +// +// Written by Yellow Labrador, May 2023. +// https://github.com/YellowLabrador/rtmidi +// *********************************************************************// + +#if defined(__AMIDI__) + +#include + +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) + +static std::string androidClientName; +static std::vector androidMidiDevices; + +//*********************************************************************// +// API: Android AMIDI +// Class Definitions: MidiInAndroid +//*********************************************************************// + +static JNIEnv* androidGetThreadEnv() { + // Every Android app has only one JVM. Calling JNI_GetCreatedJavaVMs + // will retrieve the JVM running the app. + jsize jvmsFound = 0; + JavaVM jvms[1]; + JavaVM* pjvms = jvms; + jint result = JNI_GetCreatedJavaVMs(&pjvms, 1, &jvmsFound); + + // Something went terribly wrong, no JVM was found + if (jvmsFound != 1 || result != JNI_OK) { + LOGE("No JVM found"); + return NULL; + } + + // Get the JNIEnv for the current thread + JNIEnv* env = NULL; + int rc = pjvms->GetEnv((void**)&env, JNI_VERSION_1_6); + + // The current thread was not attached to the JVM. Add it to the JVM + if (rc == JNI_EDETACHED) { + pjvms->AttachCurrentThreadAsDaemon(&env, NULL); + } + + // Neither way to retrieve the JNIEnv worked + if (env == NULL) { + LOGE("Unable to retrieve JNI environment"); + } + + return env; +} + +static jobject androidGetContext(JNIEnv *env) { + auto activityThread = env->FindClass("android/app/ActivityThread"); + auto currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;"); + auto at = env->CallStaticObjectMethod(activityThread, currentActivityThread); + if (at == NULL) { + LOGE("Unable to locate the global ActivityThread"); + return NULL; + } + + auto getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;"); + auto context = env->CallObjectMethod(at, getApplication); + if (context == NULL) { + LOGE("Application context was NULL"); + } + + return context; +} + +static jobject androidGetMidiManager(JNIEnv *env, jobject context) { + // MidiManager midiManager = (MidiManager) getSystemService(Context.MIDI_SERVICE); + auto contextClass = env->FindClass("android/content/Context"); + auto getServiceMethod = env->GetMethodID(contextClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + return env->CallObjectMethod(context, getServiceMethod, env->NewStringUTF("midi")); +} + +static void androidRefreshMidiDevices(JNIEnv *env, jobject context, bool isOutput) { + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + auto midiService = androidGetMidiManager(env, context); + + // MidiDeviceInfo[] devInfos = mMidiManager.getDevices(); + auto midiMgrClass = env->FindClass("android/media/midi/MidiManager"); + auto getDevicesMethod = env->GetMethodID(midiMgrClass, "getDevices", "()[Landroid/media/midi/MidiDeviceInfo;"); + auto jDevices = (jobjectArray) env->CallObjectMethod(midiService, getDevicesMethod); + + auto deviceInfoClass = env->FindClass("android/media/midi/MidiDeviceInfo"); + auto getInputPortCountMethod = env->GetMethodID(deviceInfoClass, "getInputPortCount", "()I"); + auto getOutputPortCountMethod = env->GetMethodID(deviceInfoClass, "getOutputPortCount", "()I"); + + jsize len = env->GetArrayLength((jarray)jDevices); + for (int i=0; iGetObjectArrayElement(jDevices, i); + + int numPorts = env->CallIntMethod(jDeviceInfo, isOutput ? getOutputPortCountMethod : getInputPortCountMethod); + if (numPorts > 0) { + androidMidiDevices.push_back(env->NewGlobalRef(jDeviceInfo)); + } + } +} + + +extern "C" +JNIEXPORT void JNICALL +Java_com_yellowlab_rtmidi_MidiDeviceOpenedListener_midiDeviceOpened(JNIEnv *env, jclass clazz, + jobject midi_device, jlong targetPtr, jboolean isOutput) { + if (isOutput) { + auto midiOut = reinterpret_cast(targetPtr); + AMidiDevice_fromJava(env, midi_device, &midiOut->sendDevice); + AMidiInputPort_open(midiOut->sendDevice, 0, &midiOut->midiInputPort); + } else { + auto midiIn = reinterpret_cast(targetPtr); + AMidiDevice_fromJava(env, midi_device, &midiIn->receiveDevice); + AMidiOutputPort_open(midiIn->receiveDevice, 0, &midiIn->midiOutputPort); + pthread_create(&midiIn->readThread, NULL, MidiInAndroid::pollMidi, midiIn); + } +} + +static void androidOpenDevice(jobject deviceInfo, void* target, bool isOutput) { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + auto midiMgr = androidGetMidiManager(env, context); + + // openDevice(MidiDeviceInfo deviceInfo, OnDeviceOpenedListener listener, Handler handler) + auto midiMgrClass = env->GetObjectClass(midiMgr); + auto openDevicesMethod = env->GetMethodID(midiMgrClass, "openDevice", "(Landroid/media/midi/MidiDeviceInfo;Landroid/media/midi/MidiManager$OnDeviceOpenedListener;Landroid/os/Handler;)V"); + + auto handlerClass = env->FindClass("android/os/Handler"); + auto handlerCtor = env->GetMethodID(handlerClass, "", "()V"); + auto handler = env->NewObject(handlerClass, handlerCtor); + + auto listenerClass = env->FindClass("com/yellowlab/rtmidi/MidiDeviceOpenedListener"); + if (!listenerClass) { + LOGE("Midi listener class not found com.yellowlab.rtmidi.MidiDeviceOpenedListener. Did you forget to add it to your APK?"); + return; + } + + auto targetPtr = reinterpret_cast(target); + auto listenerCtor = env->GetMethodID(listenerClass, "", "(JZ)V"); + auto listener = env->NewObject(listenerClass, listenerCtor, targetPtr, isOutput); + + env->CallVoidMethod(midiMgr, openDevicesMethod, deviceInfo, listener, handler); + env->DeleteLocalRef(handler); +} + +static std::string androidPortName(JNIEnv *env, unsigned int portNumber) { + if (portNumber >= androidMidiDevices.size()) { + LOGE("androidPortName: Invalid port number"); + return ""; + } + + // String deviceName = devInfo.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME); + auto deviceInfoClass = env->FindClass("android/media/midi/MidiDeviceInfo"); + auto getPropsMethod = env->GetMethodID(deviceInfoClass, "getProperties", "()Landroid/os/Bundle;"); + auto bundle = env->CallObjectMethod(androidMidiDevices[portNumber], getPropsMethod); + + auto bundleClass = env->FindClass("android/os/Bundle"); + auto getStringMethod = env->GetMethodID(bundleClass, "getString", "(Ljava/lang/String;)Ljava/lang/String;"); + auto jPortName = (jstring) env->CallObjectMethod(bundle, getStringMethod, env->NewStringUTF("name")); + + auto portNameChars = env->GetStringUTFChars(jPortName, NULL); + auto name = std::string(portNameChars); + env->ReleaseStringUTFChars(jPortName, portNameChars); + + return name; +} + +MidiInAndroid :: MidiInAndroid( const std::string &clientName, unsigned int queueSizeLimit ) + : MidiInApi( queueSizeLimit ) { + MidiInAndroid::initialize( clientName ); +} + +void MidiInAndroid :: initialize( const std::string& clientName ) { + androidClientName = clientName; + connect(); +} + +void MidiInAndroid :: connect() { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + androidRefreshMidiDevices(env, context, true); + + env->DeleteLocalRef(context); +} + +MidiInAndroid :: ~MidiInAndroid() { + auto env = androidGetThreadEnv(); + + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + androidClientName = ""; +} + +void MidiInAndroid :: openPort(unsigned int portNumber, const std::string &portName) { + if (portNumber >= androidMidiDevices.size()) { + errorString_ = "MidiInAndroid::openPort: Invalid port number"; + error( RtMidiError::INVALID_PARAMETER, errorString_ ); + + return; + } + + if (reading) { + errorString_ = "MidiInAndroid::openPort: A port is already open"; + error( RtMidiError::INVALID_USE, errorString_ ); + + return; + } + + androidOpenDevice(androidMidiDevices[portNumber], this, false); +} + +void MidiInAndroid :: openVirtualPort(const std::string &portName) { + errorString_ = "MidiInAndroid::openVirtualPort: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +unsigned int MidiInAndroid :: getPortCount() { + connect(); + return androidMidiDevices.size(); +} + +std::string MidiInAndroid :: getPortName(unsigned int portNumber) { + auto env = androidGetThreadEnv(); + return androidPortName(env, portNumber); +} + +void MidiInAndroid :: closePort() { + // Don't try to close a port before it was open + if (!reading) { + return; + } + + reading = false; + pthread_join(readThread, NULL); + + AMidiDevice_release(receiveDevice); + receiveDevice = NULL; + midiOutputPort = NULL; +} + +void MidiInAndroid:: setClientName(const std::string& clientName) { + androidClientName = clientName; +} + +void MidiInAndroid :: setPortName(const std::string &portName) { + errorString_ = "MidiInAndroid::setPortName: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +void* MidiInAndroid :: pollMidi(void* context) { + auto self = (MidiInAndroid*) context; + self->reading = true; + + const size_t MAX_BYTES_TO_RECEIVE = 128; + uint8_t incomingMessage[MAX_BYTES_TO_RECEIVE]; + + while (self->reading) { + // AMidiOutputPort_receive is non-blocking, must poll with some sleep + usleep(2000); + auto ignoreFlags = self->inputData_.ignoreFlags; + bool& continueSysex = self->inputData_.continueSysex; + + int32_t opcode; + size_t numBytesReceived; + int64_t timestamp; + ssize_t numMessagesReceived = AMidiOutputPort_receive( + self->midiOutputPort, &opcode, incomingMessage, MAX_BYTES_TO_RECEIVE, + &numBytesReceived, ×tamp); + + if (numMessagesReceived < 0) { + self->errorString_ = "MidiInAndroid::pollMidi: error receiving MIDI data"; + self->error( RtMidiError::SYSTEM_ERROR, self->errorString_ ); + self->reading = false; + break; + } + + switch (incomingMessage[0]) { + case 0xF0: + // Start of a SysEx message + continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; + if (ignoreFlags & 0x01) continue; + break; + case 0xF1: + case 0xF8: + // MIDI Time Code or Timing Clock message + if (ignoreFlags & 0x02) continue; + break; + case 0xFE: + // Active Sensing message + if (ignoreFlags & 0x04) continue; + break; + default: + if (continueSysex) { + // Continuation of a SysEx message + continueSysex = incomingMessage[numBytesReceived - 1] != 0xF7; + if (ignoreFlags & 0x01) continue; + } + // All other MIDI messages + } + + if (numMessagesReceived > 0 && numBytesReceived >= 0) { + auto message = self->inputData_.message; + + if (self->inputData_.firstMessage == true) { + message.timeStamp = 0.0; + self->inputData_.firstMessage = false; + } else { + message.timeStamp = (timestamp * 0.000001) - self->lastTime; + } + self->lastTime = (timestamp * 0.000001); + + if (!continueSysex) message.bytes.clear(); + + if ( !( ( continueSysex || incomingMessage[0] == 0xF0 ) && ( ignoreFlags & 0x01 ) ) ) { + // Unless this is a (possibly continued) SysEx message and we're ignoring SysEx, + // copy the event buffer into the MIDI message struct. + for (unsigned int i=0; iinputData_.usingCallback) { + auto callback = (RtMidiIn::RtMidiCallback) self->inputData_.userCallback; + callback(message.timeStamp, &message.bytes, self->inputData_.userData); + } else { + if (!self->inputData_.queue.push(message)) + std::cerr << "\nMidiInAndroid: message queue limit reached!!\n\n"; + } + } + } + } + + return NULL; +} + + +//*********************************************************************// +// API: Android AMIDI +// Class Definitions: MidiOutAndroid +//*********************************************************************// + + +MidiOutAndroid :: MidiOutAndroid( const std::string &clientName ) : MidiOutApi() { + MidiOutAndroid::initialize( clientName ); +} + +void MidiOutAndroid :: initialize( const std::string& clientName ) { + androidClientName = clientName; + connect(); +} + +void MidiOutAndroid :: connect() { + auto env = androidGetThreadEnv(); + auto context = androidGetContext(env); + androidRefreshMidiDevices(env, context, false); + + env->DeleteLocalRef(context); +} + +MidiOutAndroid :: ~MidiOutAndroid() { + auto env = androidGetThreadEnv(); + + // Remove all midi devices + for (jobject jMidiDevice : androidMidiDevices) { + env->DeleteGlobalRef(jMidiDevice); + } + androidMidiDevices.clear(); + + androidClientName = ""; +} + +void MidiOutAndroid :: openPort( unsigned int portNumber, const std::string &portName ) { + if (portNumber >= androidMidiDevices.size()) { + errorString_ = "MidiOutAndroid::openPort: Invalid port number"; + error( RtMidiError::INVALID_PARAMETER, errorString_ ); + + return; + } + + androidOpenDevice(androidMidiDevices[portNumber], this, true); +} + +void MidiOutAndroid :: openVirtualPort( const std::string &portName ) { + errorString_ = "MidiOutAndroid::openVirtualPort: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +unsigned int MidiOutAndroid :: getPortCount() { + connect(); + return androidMidiDevices.size(); +} + +std::string MidiOutAndroid :: getPortName( unsigned int portNumber ) { + auto env = androidGetThreadEnv(); + return androidPortName(env, portNumber); +} + +void MidiOutAndroid :: closePort() { + AMidiDevice_release(sendDevice); + sendDevice = NULL; + midiInputPort = NULL; +} + +void MidiOutAndroid:: setClientName( const std::string& name ) { + androidClientName = name; +} + +void MidiOutAndroid :: setPortName( const std::string &portName ) { + errorString_ = "MidiOutAndroid::setPortName: this function is not implemented for the Android API!"; + error( RtMidiError::WARNING, errorString_ ); +} + +void MidiOutAndroid :: sendMessage( const unsigned char *message, size_t size ) { + AMidiInputPort_send(midiInputPort, (uint8_t*)message, size); +} + +#endif // __AMIDI__ diff --git a/external_libs/rtmidi/RtMidi.h b/external_libs/rtmidi/RtMidi.h index a6f5b794bf..1d6454416e 100644 --- a/external_libs/rtmidi/RtMidi.h +++ b/external_libs/rtmidi/RtMidi.h @@ -9,7 +9,7 @@ RtMidi WWW site: http://www.music.mcgill.ca/~gary/rtmidi/ RtMidi: realtime MIDI i/o C++ classes - Copyright (c) 2003-2021 Gary P. Scavone + Copyright (c) 2003-2023 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -58,7 +58,24 @@ #endif #endif -#define RTMIDI_VERSION "5.0.0" +#define RTMIDI_VERSION_MAJOR 6 +#define RTMIDI_VERSION_MINOR 0 +#define RTMIDI_VERSION_PATCH 0 +#define RTMIDI_VERSION_BETA 0 + +#define RTMIDI_TOSTRING2(n) #n +#define RTMIDI_TOSTRING(n) RTMIDI_TOSTRING2(n) + +#if RTMIDI_VERSION_BETA > 0 + #define RTMIDI_VERSION RTMIDI_TOSTRING(RTMIDI_VERSION_MAJOR) \ + "." RTMIDI_TOSTRING(RTMIDI_VERSION_MINOR) \ + "." RTMIDI_TOSTRING(RTMIDI_VERSION_PATCH) \ + "beta" RTMIDI_TOSTRING(RTMIDI_VERSION_BETA) +#else + #define RTMIDI_VERSION RTMIDI_TOSTRING(RTMIDI_VERSION_MAJOR) \ + "." RTMIDI_TOSTRING(RTMIDI_VERSION_MINOR) \ + "." RTMIDI_TOSTRING(RTMIDI_VERSION_PATCH) +#endif #include #include @@ -86,12 +103,12 @@ class RTMIDI_DLL_PUBLIC RtMidiError : public std::exception UNSPECIFIED, /*!< The default, unspecified error type. */ NO_DEVICES_FOUND, /*!< No devices found on system. */ INVALID_DEVICE, /*!< An invalid device ID was specified. */ - MEMORY_ERROR, /*!< An error occured during memory allocation. */ + MEMORY_ERROR, /*!< An error occurred during memory allocation. */ INVALID_PARAMETER, /*!< An invalid parameter was specified to a function. */ INVALID_USE, /*!< The function was called incorrectly. */ - DRIVER_ERROR, /*!< A system driver error occured. */ - SYSTEM_ERROR, /*!< A system error occured. */ - THREAD_ERROR /*!< A thread error occured. */ + DRIVER_ERROR, /*!< A system driver error occurred. */ + SYSTEM_ERROR, /*!< A system error occurred. */ + THREAD_ERROR /*!< A thread error occurred. */ }; //! The constructor. @@ -144,6 +161,8 @@ class RTMIDI_DLL_PUBLIC RtMidi WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ WEB_MIDI_API, /*!< W3C Web MIDI API. */ + WINDOWS_UWP, /*!< The Microsoft Universal Windows Platform MIDI API. */ + ANDROID_AMIDI, /*!< Native Android MIDI API. */ NUM_APIS /*!< Number of values in this enum. */ }; @@ -206,9 +225,9 @@ class RTMIDI_DLL_PUBLIC RtMidi */ virtual bool isPortOpen( void ) const = 0; - //! Set an error callback function to be invoked when an error has occured. + //! Set an error callback function to be invoked when an error has occurred. /*! - The callback function will be called whenever an error has occured. It is best + The callback function will be called whenever an error has occurred. It is best to set the error callback function before opening a port. */ virtual void setErrorCallback( RtMidiErrorCallback errorCallback = NULL, void *userData = 0 ) = 0; @@ -373,9 +392,9 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi */ double getMessage( std::vector *message ); - //! Set an error callback function to be invoked when an error has occured. + //! Set an error callback function to be invoked when an error has occurred. /*! - The callback function will be called whenever an error has occured. It is best + The callback function will be called whenever an error has occurred. It is best to set the error callback function before opening a port. */ virtual void setErrorCallback( RtMidiErrorCallback errorCallback = NULL, void *userData = 0 ); @@ -491,9 +510,9 @@ class RTMIDI_DLL_PUBLIC RtMidiOut : public RtMidi */ void sendMessage( const unsigned char *message, size_t size ); - //! Set an error callback function to be invoked when an error has occured. + //! Set an error callback function to be invoked when an error has occurred. /*! - The callback function will be called whenever an error has occured. It is best + The callback function will be called whenever an error has occurred. It is best to set the error callback function before opening a port. */ virtual void setErrorCallback( RtMidiErrorCallback errorCallback = NULL, void *userData = 0 ); @@ -559,7 +578,7 @@ class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi void setCallback( RtMidiIn::RtMidiCallback callback, void *userData ); void cancelCallback( void ); virtual void ignoreTypes( bool midiSysex, bool midiTime, bool midiSense ); - double getMessage( std::vector *message ); + virtual double getMessage( std::vector *message ); virtual void setBufferSize( unsigned int size, unsigned int count ); // A MIDI structure used internally by the class to store incoming