From a192f6dc977f4cab2692212271229e38926d0552 Mon Sep 17 00:00:00 2001 From: Alexandros Theodotou Date: Fri, 26 Nov 2021 11:41:00 +0000 Subject: [PATCH] upgrade to 5.0.0 --- CMakeLists.txt | 85 ++++-- README.md | 35 +-- configure.ac | 20 +- meson.build | 2 +- rtmidi.pc.in | 4 +- rtmidi/RtMidi.cpp | 703 +++++++++++++++++++++++++++++++++++++------- rtmidi/RtMidi.h | 41 ++- rtmidi/rtmidi_c.cpp | 72 +++-- rtmidi/rtmidi_c.h | 19 +- 9 files changed, 764 insertions(+), 217 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ee1f8..616fdaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,12 @@ cmake_minimum_required(VERSION 3.10 FATAL_ERROR) # Define a C++ project. -project(RtMidi LANGUAGES CXX) +project(RtMidi LANGUAGES CXX C) + +# standards version +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) # Check for Jack (any OS) find_library(JACK_LIB jack) @@ -76,8 +81,10 @@ string(REGEX REPLACE ${R} "\\1" PACKAGE_VERSION ${CONFIGAC}) # Init variables set(rtmidi_SOURCES RtMidi.cpp RtMidi.h rtmidi_c.cpp rtmidi_c.h) set(LINKLIBS) +set(PUBLICLINKLIBS) set(INCDIRS) set(PKGCONFIG_REQUIRES) +set(LIBS_REQUIRES) set(API_DEFS) set(API_LIST) @@ -106,7 +113,7 @@ if(RTMIDI_API_JACK) check_symbol_exists(jack_port_rename ${jack_INCLUDEDIR}/jack/jack.h JACK_HAS_PORT_RENAME) set(CMAKE_REQUIRED_LIBRARIES ${tmp_CMAKE_REQUIRED_LIBRARIES}) if(JACK_HAS_PORT_RENAME) - list(APPEND API_DEFS "JACK_HAS_PORT_RENAME") + list(APPEND API_DEFS "-DJACK_HAS_PORT_RENAME") endif() endif() @@ -133,12 +140,14 @@ endif() # CoreMIDI if(RTMIDI_API_CORE) + find_library(CORESERVICES_LIB CoreServices) + find_library(COREAUDIO_LIB CoreAudio) + find_library(COREMIDI_LIB CoreMIDI) + find_library(COREFOUNDATION_LIB CoreFoundation) list(APPEND API_DEFS "-D__MACOSX_CORE__") list(APPEND API_LIST "coremidi") - list(APPEND LINKLIBS "-framework CoreServices") - list(APPEND LINKLIBS "-framework CoreAudio") - list(APPEND LINKLIBS "-framework CoreMIDI") - list(APPEND LINKLIBS "-framework CoreFoundation") + list(APPEND LINKLIBS ${CORESERVICES_LIB} ${COREAUDIO_LIB} ${COREMIDI_LIB} ${COREFOUNDATION_LIB}) + list(APPEND LIBS_REQUIRES "-framework CoreServices -framework CoreAudio -framework CoreMIDI -framework CoreFoundation") list(APPEND LINKFLAGS "-Wl,-F/Library/Frameworks") endif() @@ -147,11 +156,10 @@ if (NEED_PTHREAD) find_package(Threads REQUIRED CMAKE_THREAD_PREFER_PTHREAD THREADS_PREFER_PTHREAD_FLAG) - list(APPEND LINKLIBS Threads::Threads) + list(APPEND PUBLICLINKLIBS Threads::Threads) endif() # Create library targets. -cmake_policy(SET CMP0042 OLD) set(LIB_TARGETS) # Use RTMIDI_BUILD_SHARED_LIBS / RTMIDI_BUILD_STATIC_LIBS if they @@ -174,30 +182,33 @@ endif() list(APPEND LIB_TARGETS rtmidi) # Add headers destination for install rule. -set_target_properties(rtmidi PROPERTIES PUBLIC_HEADER RtMidi.h +set_property(TARGET rtmidi PROPERTY PUBLIC_HEADER RtMidi.h rtmidi_c.h) +set_target_properties(rtmidi PROPERTIES SOVERSION ${SO_VER} VERSION ${FULL_VER}) # Set include paths, populate target interface. -target_include_directories(rtmidi PRIVATE - $ - $ - ${INCDIRS}) +target_include_directories(rtmidi PRIVATE ${INCDIRS} + PUBLIC + $ + $) # Set compile-time definitions target_compile_definitions(rtmidi PRIVATE ${API_DEFS}) target_compile_definitions(rtmidi PRIVATE RTMIDI_EXPORT) -target_link_libraries(rtmidi ${LINKLIBS}) +target_link_libraries(rtmidi PUBLIC ${PUBLICLINKLIBS} + PRIVATE ${LINKLIBS}) # Set standard installation directories. include(GNUInstallDirs) # Add tests if requested. -include(CTest) +option(RTMIDI_BUILD_TESTING "Build test programs" ON) if (NOT DEFINED RTMIDI_BUILD_TESTING OR RTMIDI_BUILD_TESTING STREQUAL "") set(RTMIDI_BUILD_TESTING ${BUILD_TESTING}) endif() if (RTMIDI_BUILD_TESTING) + include(CTest) add_executable(cmidiin tests/cmidiin.cpp) add_executable(midiclock tests/midiclock.cpp) add_executable(midiout tests/midiout.cpp) @@ -205,8 +216,9 @@ if (RTMIDI_BUILD_TESTING) add_executable(qmidiin tests/qmidiin.cpp) add_executable(sysextest tests/sysextest.cpp) add_executable(apinames tests/apinames.cpp) + add_executable(testcapi tests/testcapi.c) list(GET LIB_TARGETS 0 LIBRTMIDI) - set_target_properties(cmidiin midiclock midiout midiprobe qmidiin sysextest apinames + set_target_properties(cmidiin midiclock midiout midiprobe qmidiin sysextest apinames testcapi PROPERTIES RUNTIME_OUTPUT_DIRECTORY tests INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR} LINK_LIBRARIES ${LIBRTMIDI}) @@ -222,7 +234,9 @@ message(STATUS "Compiling with support for: ${apilist}") # PkgConfig file string(REPLACE ";" " " req "${PKGCONFIG_REQUIRES}") +string(REPLACE ";" " " req_libs "${LIBS_REQUIRES}") string(REPLACE ";" " " api "${API_DEFS}") +set(prefix ${CMAKE_INSTALL_PREFIX}) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/rtmidi.pc.in" "rtmidi.pc" @ONLY) # Add install rule. @@ -231,7 +245,7 @@ install(TARGETS ${LIB_TARGETS} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/rtmidi) # Store the package in the user registry. export(PACKAGE RtMidi) @@ -239,17 +253,6 @@ export(PACKAGE RtMidi) # Set installation path for CMake files. set(RTMIDI_CMAKE_DESTINATION share/rtmidi) -# Create CMake configuration export file. -if(NEED_PTHREAD) - file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/RtMidiConfig.cmake "find_package(Threads REQUIRED)\n") -endif() - -file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/RtMidiConfig.cmake "include(\${CMAKE_CURRENT_LIST_DIR}/RtMidiTargets.cmake)") - -# Install CMake configuration export file. -install(FILES ${CMAKE_BINARY_DIR}/RtMidiConfig.cmake - DESTINATION ${RTMIDI_CMAKE_DESTINATION}) - # Export library target (build-tree). export(EXPORT RtMidiTargets NAMESPACE RtMidi::) @@ -271,3 +274,29 @@ add_custom_target(${RTMIDI_TARGETNAME_UNINSTALL} install( FILES ${CMAKE_CURRENT_BINARY_DIR}/rtmidi.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +# Set up CMake package +include(CMakePackageConfigHelpers) + +# Write cmake package version file +write_basic_package_version_file( + RtMidi-config-version.cmake + VERSION ${FULL_VER} + COMPATIBILITY SameMajorVersion +) + +# Write cmake package config file +configure_package_config_file ( + cmake/RtMidi-config.cmake.in + RtMidi-config.cmake + INSTALL_DESTINATION "${RTMIDI_CMAKE_DESTINATION}" +) + +# Install package files +install ( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/RtMidi-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/RtMidi-config-version.cmake" + DESTINATION + "${RTMIDI_CMAKE_DESTINATION}" +) diff --git a/README.md b/README.md index 8793a2c..bdfab02 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # RtMidi -[![Build Status](https://travis-ci.org/thestk/rtmidi.svg?branch=master)](https://travis-ci.org/thestk/rtmidi) +![Build Status](https://github.com/thestk/rtmidi/actions/workflows/ci.yml/badge.svg) A set of C++ classes that provide a common API for realtime MIDI input/output across Linux (ALSA & JACK), Macintosh OS X (CoreMIDI & JACK) and Windows (Multimedia). -By Gary P. Scavone, 2003-2019. +By Gary P. Scavone, 2003-2021. This distribution of RtMidi contains the following: @@ -24,7 +24,7 @@ RtMidi is a set of C++ classes (`RtMidiIn`, `RtMidiOut`, and API specific classe - only one header and one source file for easy inclusion in programming projects - MIDI device enumeration -MIDI input and output functionality are separated into two classes, `RtMidiIn` and `RtMidiOut`. Each class instance supports only a single MIDI connection. RtMidi does not provide timing functionality (i.e., output messages are sent immediately). Input messages are timestamped with delta times in seconds (via a `double` floating point type). MIDI data is passed to the user as raw bytes using an `std::vector<unsigned char>`. +MIDI input and output functionality are separated into two classes, `RtMidiIn` and `RtMidiOut`. Each class instance supports only a single MIDI connection. RtMidi does not provide timing functionality (i.e., output messages are sent immediately). Input messages are timestamped with delta times in seconds (via a `double` floating point type). MIDI data is passed to the user as raw bytes using an `std::vector`. ## Windows @@ -36,31 +36,4 @@ For complete documentation on RtMidi, see the `doc` directory of the distributio ## Legal and ethical -The RtMidi license is similar to the MIT License, with the added *feature* that modifications be sent to the developer. - - RtMidi: realtime MIDI i/o C++ classes - Copyright (c) 2003-2019 Gary P. Scavone - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - Any person wishing to distribute modifications to the Software is - asked to send the modifications to the original developer so that - they can be incorporated into the canonical version. This is, - however, not a binding provision of this license. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The RtMidi license is similar to the MIT License, with the added *feature* that modifications be sent to the developer. Please see [LICENSE](LICENSE). diff --git a/configure.ac b/configure.ac index 6e7f1d8..960af42 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ # Process this file with autoconf to produce a configure script. -AC_INIT(RtMidi, 4.0.0, gary@music.mcgill.ca, rtmidi) +AC_INIT(RtMidi, 5.0.0, gary.scavone@mcgill.ca, rtmidi) AC_CONFIG_AUX_DIR(config) AC_CONFIG_SRCDIR(RtMidi.cpp) AC_CONFIG_FILES([rtmidi-config rtmidi.pc Makefile tests/Makefile doc/Makefile doc/doxygen/Doxyfile]) @@ -18,7 +18,7 @@ AM_INIT_AUTOMAKE([1.14 -Wall -Werror foreign subdir-objects]) # # If any interfaces have been removed since the last public release, then set # age to 0. -m4_define([lt_current], 5) +m4_define([lt_current], 6) m4_define([lt_revision], 0) m4_define([lt_age], 0) @@ -30,9 +30,11 @@ AC_SUBST(SO_VERSION) AC_SUBST(LIBS) AC_SUBST(api) AC_SUBST(req) +AC_SUBST(req_libs) api="" req="" +req_libs="" # Fill GXX with something before test. GXX="no" @@ -52,6 +54,10 @@ AS_IF( m4_ifdef([AM_MAINTAINER_MODE], [AM_MAINTAINER_MODE]) m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) +# standards version +m4_include([m4/ax_cxx_compile_stdcxx.m4]) +AX_CXX_COMPILE_STDCXX(11, noext, mandatory) + # configure flags AC_ARG_ENABLE([debug], [AS_HELP_STRING([--enable-debug], [enable various debugging output])]) AC_ARG_WITH(jack, [AS_HELP_STRING([--with-jack], [choose JACK server support])]) @@ -59,6 +65,7 @@ AC_ARG_WITH(alsa, [AS_HELP_STRING([--with-alsa], [choose native ALSA sequencer A AC_ARG_WITH(core, [AS_HELP_STRING([--with-core], [ choose CoreMIDI API support (mac only)])]) AC_ARG_WITH(winmm, [AS_HELP_STRING([--with-winmm], [ choose Windows MultiMedia (MM) API support (win32 only)])]) AC_ARG_WITH(winks, [AS_HELP_STRING([--with-winks], [ choose kernel streaming support (win32 only)])]) +AC_ARG_WITH(webmidi, [AS_HELP_STRING([--with-webmidi], [ choose Web MIDI support])]) # Checks for programs. @@ -132,6 +139,7 @@ AS_IF([test "x$with_alsa" = "xyes"], [systems="$systems alsa"]) AS_IF([test "x$with_core" = "xyes"], [systems="$systems core"]) AS_IF([test "x$with_winmm" = "xyes"], [systems="$systems winmm"]) AS_IF([test "x$with_winks" = "xyes"], [systems="$systems winks"]) +AS_IF([test "x$with_webmidi" = "xyes"], [systems="$systems webmidi"]) AS_IF([test "x$with_dummy" = "xyes"], [systems="$systems dummy"]) required=" $systems " @@ -153,6 +161,7 @@ AS_IF([test "x$with_alsa" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep AS_IF([test "x$with_winmm" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v winmm`]) AS_IF([test "x$with_winks" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v winks`]) AS_IF([test "x$with_core" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v core`]) +AS_IF([test "x$with_webmidi" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v webmidi`]) AS_IF([test "x$with_dummy" = "xno"], [systems=`echo $systems|tr ' ' \\\\n|grep -v dummy`]) systems=" `echo $systems|tr \\\\n ' '` " @@ -187,6 +196,7 @@ AS_CASE(["$systems"], [*" alsa "*], [ AS_CASE(["$systems"], [*" core "*], [ AC_CHECK_HEADER(CoreMIDI/CoreMIDI.h, [api="$api -D__MACOSX_CORE__" + req_libs="$req_libs -framework CoreMIDI -framework CoreAudio -framework CoreFoundation" need_pthread=yes found="$found CoreMIDI", LIBS="$LIBS -framework CoreMIDI -framework CoreFoundation -framework CoreAudio"], @@ -214,6 +224,12 @@ AS_CASE(["$systems"], [*" winks "*], [ LIBS="-lsetupapi -lksuser ${LIBS}"]) ]) +AS_CASE(["$systems"], [*" webmidi "*], [ + AC_CHECK_HEADERS(emscripten.h, + [api="$api -D__WEB_MIDI_API__" + found="$found Web MIDI"]) +]) + AS_IF([test -n "$need_ole32"], [LIBS="-lole32 $LIBS"]) AS_IF([test -n "$need_pthread"],[ diff --git a/meson.build b/meson.build index 2764558..4fb84b1 100644 --- a/meson.build +++ b/meson.build @@ -17,7 +17,7 @@ project ( 'zrythm-midi', ['c', 'cpp'], - version: '4.0.0', + version: '5.0.0', license: 'MIT', default_options: [ 'warning_level=1', diff --git a/rtmidi.pc.in b/rtmidi.pc.in index 9af1a04..acb6888 100644 --- a/rtmidi.pc.in +++ b/rtmidi.pc.in @@ -6,7 +6,7 @@ includedir=${prefix}/include/rtmidi Name: librtmidi Description: RtMidi - a set of C++ classes that provide a common API for realtime MIDI input/output Version: @PACKAGE_VERSION@ -Requires: @req@ +Requires.private: @req@ Libs: -L${libdir} -lrtmidi -Libs.private: -lpthread +Libs.private: -lpthread @req_libs@ Cflags: -pthread -I${includedir} @api@ diff --git a/rtmidi/RtMidi.cpp b/rtmidi/RtMidi.cpp index fb16ac4..6a1c89e 100644 --- a/rtmidi/RtMidi.cpp +++ b/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-2019 Gary P. Scavone + Copyright (c) 2003-2021 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -40,11 +40,31 @@ #include "RtMidi.h" #include -#if defined(__MACOSX_CORE__) - #if TARGET_OS_IPHONE +#if (TARGET_OS_IPHONE == 1) + #define AudioGetCurrentHostTime CAHostTimeBase::GetCurrentTime #define AudioConvertHostTimeToNanos CAHostTimeBase::ConvertToNanos - #endif + + #include + class CTime2nsFactor + { + public: + CTime2nsFactor() + { + mach_timebase_info_data_t tinfo; + mach_timebase_info(&tinfo); + Factor = (double)tinfo.numer / tinfo.denom; + } + static double Factor; + }; + double CTime2nsFactor::Factor; + static CTime2nsFactor InitTime2nsFactor; + #undef AudioGetCurrentHostTime + #undef AudioConvertHostTimeToNanos + #define AudioGetCurrentHostTime (uint64_t) mach_absolute_time + #define AudioConvertHostTimeToNanos(t) t *CTime2nsFactor::Factor + #define EndianS32_BtoN(n) n + #endif // Default for Windows is to add an identifier to the port names; this @@ -57,11 +77,12 @@ // // **************************************************************** // -#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) +#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) #define __RTMIDI_DUMMY__ #endif #if defined(__MACOSX_CORE__) +#include class MidiInCore: public MidiInApi { @@ -78,6 +99,7 @@ class MidiInCore: public MidiInApi std::string getPortName( unsigned int portNumber ); protected: + MIDIClientRef getCoreMidiClientSingleton(const std::string& clientName) throw(); void initialize( const std::string& clientName ); }; @@ -97,6 +119,7 @@ class MidiOutCore: public MidiOutApi void sendMessage( const unsigned char *message, size_t size ); protected: + MIDIClientRef getCoreMidiClientSingleton(const std::string& clientName) throw(); void initialize( const std::string& clientName ); }; @@ -231,6 +254,57 @@ class MidiOutWinMM: public MidiOutApi #endif +#if defined(__WEB_MIDI_API__) + +class MidiInWeb : public MidiInApi +{ + std::string client_name{}; + std::string web_midi_id{}; + int open_port_number{-1}; + + public: + MidiInWeb(const std::string &/*clientName*/, unsigned int queueSizeLimit ); + ~MidiInWeb( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::WEB_MIDI_API; }; + 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 ); + + protected: + void initialize( const std::string& clientName ); +}; + +class MidiOutWeb: public MidiOutApi +{ + std::string client_name{}; + std::string web_midi_id{}; + int open_port_number{-1}; + + public: + MidiOutWeb( const std::string &clientName ); + ~MidiOutWeb( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::WEB_MIDI_API; }; + 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 ); + + protected: + void initialize( const std::string& clientName ); +}; + +#endif + #if defined(__RTMIDI_DUMMY__) class MidiInDummy: public MidiInApi @@ -285,6 +359,11 @@ RtMidi :: ~RtMidi() rtapi_ = 0; } +RtMidi::RtMidi(RtMidi&& other) noexcept { + rtapi_ = other.rtapi_; + other.rtapi_ = nullptr; +} + std::string RtMidi :: getVersion( void ) throw() { return std::string( RTMIDI_VERSION ); @@ -299,6 +378,7 @@ const char* rtmidi_api_names[][2] = { { "alsa" , "ALSA" }, { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, + { "web" , "Web MIDI API" }, { "dummy" , "Dummy" }, }; const unsigned int rtmidi_num_api_names = @@ -319,6 +399,9 @@ extern "C" const RtMidi::Api rtmidi_compiled_apis[] = { #if defined(__WINDOWS_MM__) RtMidi::WINDOWS_MM, #endif +#if defined(__WEB_MIDI_API__) + RtMidi::WEB_MIDI_API, +#endif #if defined(__RTMIDI_DUMMY__) RtMidi::RTMIDI_DUMMY, #endif @@ -344,14 +427,14 @@ void RtMidi :: getCompiledApi( std::vector &apis ) throw() std::string RtMidi :: getApiName( RtMidi::Api api ) { - if (api < 0 || api >= RtMidi::NUM_APIS) + if (api < RtMidi::UNSPECIFIED || api >= RtMidi::NUM_APIS) return ""; return rtmidi_api_names[api][0]; } std::string RtMidi :: getApiDisplayName( RtMidi::Api api ) { - if (api < 0 || api >= RtMidi::NUM_APIS) + if (api < RtMidi::UNSPECIFIED || api >= RtMidi::NUM_APIS) return "Unknown"; return rtmidi_api_names[api][1]; } @@ -401,6 +484,10 @@ void RtMidiIn :: openMidiApi( RtMidi::Api api, const std::string &clientName, un if ( api == MACOSX_CORE ) rtapi_ = new MidiInCore( clientName, queueSizeLimit ); #endif +#if defined(__WEB_MIDI_API__) + if ( api == WEB_MIDI_API ) + rtapi_ = new MidiInWeb( clientName, queueSizeLimit ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiInDummy( clientName, queueSizeLimit ); @@ -469,6 +556,10 @@ void RtMidiOut :: openMidiApi( RtMidi::Api api, const std::string &clientName ) if ( api == MACOSX_CORE ) rtapi_ = new MidiOutCore( clientName ); #endif +#if defined(__WEB_MIDI_API__) + if ( api == WEB_MIDI_API ) + rtapi_ = new MidiOutWeb( clientName ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiOutDummy( clientName ); @@ -634,6 +725,12 @@ double MidiInApi :: getMessage( std::vector *message ) return timeStamp; } +void MidiInApi :: setBufferSize( unsigned int size, unsigned int count ) +{ + inputData_.bufferSize = size; + inputData_.bufferCount = count; +} + unsigned int MidiInApi::MidiQueue::size( unsigned int *__back, unsigned int *__front ) { @@ -716,10 +813,11 @@ MidiOutApi :: ~MidiOutApi( void ) // MIDI input. We convert the system specific time stamps to delta // time values. -// OS-X CoreMIDI header files. -#include -#include -#include +// These are not available on iOS. +#if (TARGET_OS_IPHONE == 0) + #include + #include +#endif // A structure to hold variables related to the CoreMIDI API // implementation. @@ -732,6 +830,20 @@ struct CoreMidiData { MIDISysexSendRequest sysexreq; }; +static MIDIClientRef CoreMidiClientSingleton = 0; + +void RtMidi_setCoreMidiClientSingleton(MIDIClientRef client){ + CoreMidiClientSingleton = client; +} + +void RtMidi_disposeCoreMidiClientSingleton(){ + if (CoreMidiClientSingleton == 0){ + return; + } + MIDIClientDispose( CoreMidiClientSingleton ); + CoreMidiClientSingleton = 0; +} + //*********************************************************************// // API: OS-X // Class Definitions: MidiInCore @@ -899,24 +1011,37 @@ MidiInCore :: ~MidiInCore( void ) // Cleanup. CoreMidiData *data = static_cast (apiData_); - MIDIClientDispose( data->client ); if ( data->endpoint ) MIDIEndpointDispose( data->endpoint ); delete data; } +MIDIClientRef MidiInCore::getCoreMidiClientSingleton(const std::string& clientName) throw() { + + if (CoreMidiClientSingleton == 0){ + // Set up our client. + MIDIClientRef client; + + CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); + OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); + if ( result != noErr ) { + std::ostringstream ost; + ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; + errorString_ = ost.str(); + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return 0; + } + CFRelease( name ); + + CoreMidiClientSingleton = client; + } + + return CoreMidiClientSingleton; +} + void MidiInCore :: initialize( const std::string& clientName ) { // Set up our client. - MIDIClientRef client; - CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); - OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); - if ( result != noErr ) { - std::ostringstream ost; - ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; - errorString_ = ost.str(); - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + MIDIClientRef client = getCoreMidiClientSingleton(clientName); // Save our api-specific connection information. CoreMidiData *data = (CoreMidiData *) new CoreMidiData; @@ -924,7 +1049,6 @@ void MidiInCore :: initialize( const std::string& clientName ) data->endpoint = 0; apiData_ = (void *) data; inputData_.apiData = (void *) data; - CFRelease( name ); } void MidiInCore :: openPort( unsigned int portNumber, const std::string &portName ) @@ -960,7 +1084,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam CFRelease( portNameRef ); if ( result != noErr ) { - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error creating OS-X MIDI input port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -970,7 +1093,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam MIDIEndpointRef endpoint = MIDIGetSource( portNumber ); if ( endpoint == 0 ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error getting MIDI input source reference."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -980,7 +1102,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam result = MIDIPortConnectSource( port, endpoint, NULL ); if ( result != noErr ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error connecting OS-X MIDI input port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1068,6 +1189,11 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) CFRelease( str ); } + // some MIDI devices have a leading space in endpoint name. trim + CFStringRef space = CFStringCreateWithCString(NULL, " ", kCFStringEncodingUTF8); + CFStringTrim(result, space); + CFRelease(space); + MIDIEntityRef entity = 0; MIDIEndpointGetEntity( endpoint, &entity ); if ( entity == 0 ) @@ -1223,31 +1349,43 @@ MidiOutCore :: ~MidiOutCore( void ) // Cleanup. CoreMidiData *data = static_cast (apiData_); - MIDIClientDispose( data->client ); if ( data->endpoint ) MIDIEndpointDispose( data->endpoint ); delete data; } +MIDIClientRef MidiOutCore::getCoreMidiClientSingleton(const std::string& clientName) throw() { + + if (CoreMidiClientSingleton == 0){ + // Set up our client. + MIDIClientRef client; + + CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); + OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); + if ( result != noErr ) { + std::ostringstream ost; + ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; + errorString_ = ost.str(); + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return 0; + } + CFRelease( name ); + + CoreMidiClientSingleton = client; + } + + return CoreMidiClientSingleton; +} + void MidiOutCore :: initialize( const std::string& clientName ) { // Set up our client. - MIDIClientRef client; - CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); - OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); - if ( result != noErr ) { - std::ostringstream ost; - ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; - errorString_ = ost.str(); - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + MIDIClientRef client = getCoreMidiClientSingleton(clientName); // Save our api-specific connection information. CoreMidiData *data = (CoreMidiData *) new CoreMidiData; data->client = client; data->endpoint = 0; apiData_ = (void *) data; - CFRelease( name ); } unsigned int MidiOutCore :: getPortCount() @@ -1310,7 +1448,6 @@ void MidiOutCore :: openPort( unsigned int portNumber, const std::string &portNa OSStatus result = MIDIOutputPortCreate( data->client, portNameRef, &port ); CFRelease( portNameRef ); if ( result != noErr ) { - MIDIClientDispose( data->client ); errorString_ = "MidiOutCore::openPort: error creating OS-X MIDI output port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1320,7 +1457,6 @@ void MidiOutCore :: openPort( unsigned int portNumber, const std::string &portNa MIDIEndpointRef destination = MIDIGetDestination( portNumber ); if ( destination == 0 ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiOutCore::openPort: error getting MIDI output destination reference."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1402,50 +1538,54 @@ void MidiOutCore :: sendMessage( const unsigned char *message, size_t size ) return; } - MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); - CoreMidiData *data = static_cast (apiData_); - OSStatus result; - if ( message[0] != 0xF0 && nBytes > 3 ) { errorString_ = "MidiOutCore::sendMessage: message format problem ... not sysex but > 3 bytes?"; error( RtMidiError::WARNING, errorString_ ); return; } - Byte buffer[nBytes+(sizeof( MIDIPacketList ))]; + MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + CoreMidiData *data = static_cast (apiData_); + OSStatus result; + + ByteCount bufsize = nBytes > 65535 ? 65535 : nBytes; + Byte buffer[bufsize+16]; // pad for other struct members ByteCount listSize = sizeof( buffer ); MIDIPacketList *packetList = (MIDIPacketList*)buffer; - MIDIPacket *packet = MIDIPacketListInit( packetList ); ByteCount remainingBytes = nBytes; - while ( remainingBytes && packet ) { - ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes; // 65535 = maximum size of a MIDIPacket + while ( remainingBytes ) { + MIDIPacket *packet = MIDIPacketListInit( packetList ); + // A MIDIPacketList can only contain a maximum of 64K of data, so if our message is longer, + // break it up into chunks of 64K or less and send out as a MIDIPacketList with only one + // MIDIPacket. Here, we reuse the memory allocated above on the stack for all. + ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes; const Byte* dataStartPtr = (const Byte *) &message[nBytes - remainingBytes]; packet = MIDIPacketListAdd( packetList, listSize, packet, timeStamp, bytesForPacket, dataStartPtr ); remainingBytes -= bytesForPacket; - } - if ( !packet ) { - errorString_ = "MidiOutCore::sendMessage: could not allocate packet list"; - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + if ( !packet ) { + errorString_ = "MidiOutCore::sendMessage: could not allocate packet list"; + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return; + } - // Send to any destinations that may have connected to us. - if ( data->endpoint ) { - result = MIDIReceived( data->endpoint, packetList ); - if ( result != noErr ) { - errorString_ = "MidiOutCore::sendMessage: error sending MIDI to virtual destinations."; - error( RtMidiError::WARNING, errorString_ ); + // Send to any destinations that may have connected to us. + if ( data->endpoint ) { + result = MIDIReceived( data->endpoint, packetList ); + if ( result != noErr ) { + errorString_ = "MidiOutCore::sendMessage: error sending MIDI to virtual destinations."; + error( RtMidiError::WARNING, errorString_ ); + } } - } - // And send to an explicit destination port if we're connected. - if ( connected_ ) { - result = MIDISend( data->port, data->destinationId, packetList ); - if ( result != noErr ) { - errorString_ = "MidiOutCore::sendMessage: error sending MIDI message to port."; - error( RtMidiError::WARNING, errorString_ ); + // And send to an explicit destination port if we're connected. + if ( connected_ ) { + result = MIDISend( data->port, data->destinationId, packetList ); + if ( result != noErr ) { + errorString_ = "MidiOutCore::sendMessage: error sending MIDI message to port."; + error( RtMidiError::WARNING, errorString_ ); + } } } } @@ -1487,6 +1627,7 @@ struct AlsaMidiData { snd_seq_port_subscribe_t *subscription; snd_midi_event_t *coder; unsigned int bufferSize; + unsigned int requestedBufferSize; unsigned char *buffer; pthread_t thread; pthread_t dummy_thread_id; @@ -1517,7 +1658,6 @@ static void *alsaMidiHandler( void *ptr ) snd_seq_event_t *ev; int result; - apiData->bufferSize = 32; result = snd_midi_event_new( 0, &apiData->coder ); if ( result < 0 ) { data->doInput = false; @@ -1769,6 +1909,7 @@ void MidiInAlsa :: initialize( const std::string& clientName ) data->thread = data->dummy_thread_id; data->trigger_fds[0] = -1; data->trigger_fds[1] = -1; + data->bufferSize = inputData_.bufferSize; apiData_ = (void *) data; inputData_.apiData = (void *) data; @@ -2293,7 +2434,7 @@ void MidiOutAlsa :: openVirtualPort( const std::string &portName ) void MidiOutAlsa :: sendMessage( const unsigned char *message, size_t size ) { - int result; + long result; AlsaMidiData *data = static_cast (apiData_); unsigned int nBytes = static_cast (size); if ( nBytes > data->bufferSize ) { @@ -2313,25 +2454,38 @@ void MidiOutAlsa :: sendMessage( const unsigned char *message, size_t size ) } } - snd_seq_event_t ev; - snd_seq_ev_clear( &ev ); - snd_seq_ev_set_source( &ev, data->vport ); - snd_seq_ev_set_subs( &ev ); - snd_seq_ev_set_direct( &ev ); for ( unsigned int i=0; ibuffer[i] = message[i]; - result = snd_midi_event_encode( data->coder, data->buffer, (long)nBytes, &ev ); - if ( result < (int)nBytes ) { - errorString_ = "MidiOutAlsa::sendMessage: event parsing error!"; - error( RtMidiError::WARNING, errorString_ ); - return; - } - // Send the event. - result = snd_seq_event_output( data->seq, &ev ); - if ( result < 0 ) { - errorString_ = "MidiOutAlsa::sendMessage: error sending MIDI message to port."; - error( RtMidiError::WARNING, errorString_ ); - return; + unsigned int offset = 0; + while (offset < nBytes) { + snd_seq_event_t ev; + snd_seq_ev_clear( &ev ); + snd_seq_ev_set_source( &ev, data->vport ); + snd_seq_ev_set_subs( &ev ); + snd_seq_ev_set_direct( &ev ); + result = snd_midi_event_encode( data->coder, data->buffer + offset, + (long)(nBytes - offset), &ev ); + if ( result < 0 ) { + errorString_ = "MidiOutAlsa::sendMessage: event parsing error!"; + error( RtMidiError::WARNING, errorString_ ); + return; + } + + if ( ev.type == SND_SEQ_EVENT_NONE ) { + errorString_ = "MidiOutAlsa::sendMessage: incomplete message!"; + error( RtMidiError::WARNING, errorString_ ); + return; + } + + offset += result; + + // Send the event. + result = snd_seq_event_output( data->seq, &ev ); + if ( result < 0 ) { + errorString_ = "MidiOutAlsa::sendMessage: error sending MIDI message to port."; + error( RtMidiError::WARNING, errorString_ ); + return; + } } snd_seq_drain_output( data->seq ); } @@ -2386,9 +2540,6 @@ static std::string ConvertToUTF8(const TCHAR *str) return u8str; } -#define RT_SYSEX_BUFFER_SIZE 1024 -#define RT_SYSEX_BUFFER_COUNT 4 - // A structure to hold variables related to the CoreMIDI API // implementation. struct WinMidiData { @@ -2396,7 +2547,7 @@ struct WinMidiData { HMIDIOUT outHandle; // Handle to Midi Output Device DWORD lastTime; MidiInApi::MidiMessage message; - LPMIDIHDR sysexBuffer[RT_SYSEX_BUFFER_COUNT]; + std::vector sysexBuffer; CRITICAL_SECTION _mutex; // [Patrice] see https://groups.google.com/forum/#!topic/mididev/6OUjHutMpEo }; @@ -2576,10 +2727,11 @@ void MidiInWinMM :: openPort( unsigned int portNumber, const std::string &/*port } // Allocate and init the sysex buffers. - for ( int i=0; isysexBuffer.resize( inputData_.bufferCount ); + for ( int i=0; i < inputData_.bufferCount; ++i ) { data->sysexBuffer[i] = (MIDIHDR*) new char[ sizeof(MIDIHDR) ]; - data->sysexBuffer[i]->lpData = new char[ RT_SYSEX_BUFFER_SIZE ]; - data->sysexBuffer[i]->dwBufferLength = RT_SYSEX_BUFFER_SIZE; + data->sysexBuffer[i]->lpData = new char[ inputData_.bufferSize ]; + data->sysexBuffer[i]->dwBufferLength = inputData_.bufferSize; data->sysexBuffer[i]->dwUser = i; // We use the dwUser parameter as buffer indicator data->sysexBuffer[i]->dwFlags = 0; @@ -2630,7 +2782,7 @@ void MidiInWinMM :: closePort( void ) midiInReset( data->inHandle ); midiInStop( data->inHandle ); - for ( int i=0; isysexBuffer.size(); ++i ) { int result = midiInUnprepareHeader(data->inHandle, data->sysexBuffer[i], sizeof(MIDIHDR)); delete [] data->sysexBuffer[i]->lpData; delete [] data->sysexBuffer[i]; @@ -2811,7 +2963,10 @@ void MidiOutWinMM :: closePort( void ) { if ( connected_ ) { WinMidiData *data = static_cast (apiData_); - midiOutReset( data->outHandle ); + // Disabled because midiOutReset triggers 0x7b (if any note was ON) and 0x79 "Reset All + // Controllers" (to all 16 channels) CC messages which is undesirable (see issue #222) + // midiOutReset( data->outHandle ); + midiOutClose( data->outHandle ); data->outHandle = 0; connected_ = false; @@ -2936,6 +3091,7 @@ void MidiOutWinMM :: sendMessage( const unsigned char *message, size_t size ) #include #include #include +#include #ifdef HAVE_SEMAPHORE #include #endif @@ -2945,8 +3101,8 @@ void MidiOutWinMM :: sendMessage( const unsigned char *message, size_t size ) struct JackMidiData { jack_client_t *client; jack_port_t *port; - jack_ringbuffer_t *buffSize; - jack_ringbuffer_t *buffMessage; + 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; @@ -3101,6 +3257,8 @@ void MidiInJack :: openPort( unsigned int portNumber, const std::string &portNam 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?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; } @@ -3123,6 +3281,8 @@ void MidiInJack :: openVirtualPort( const std::string &portName ) if ( data->port == NULL ) { errorString_ = "MidiInJack::openVirtualPort: JACK error creating virtual port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); } } @@ -3226,11 +3386,15 @@ static int jackProcessOut( jack_nframes_t nframes, void *arg ) void *buff = jack_port_get_buffer( data->port, nframes ); jack_midi_clear_buffer( buff ); - while ( jack_ringbuffer_read_space( data->buffSize ) > 0 ) { - jack_ringbuffer_read( data->buffSize, (char *) &space, (size_t) sizeof( space ) ); - midiData = jack_midi_event_reserve( buff, 0, space ); + while ( jack_ringbuffer_peek( data->buff, (char *) &space, sizeof( space ) ) == sizeof(space) && + jack_ringbuffer_read_space( data->buff ) >= sizeof(space) + space ) { + jack_ringbuffer_read_advance( data->buff, sizeof(space) ); - jack_ringbuffer_read( data->buffMessage, (char *) midiData, (size_t) space ); + midiData = jack_midi_event_reserve( buff, 0, space ); + if ( midiData ) + jack_ringbuffer_read( data->buff, (char *) midiData, (size_t) space ); + else + jack_ringbuffer_read_advance( data->buff, (size_t) space ); } #ifdef HAVE_SEMAPHORE @@ -3269,8 +3433,8 @@ void MidiOutJack :: connect() return; // Initialize output ringbuffers - data->buffSize = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); - data->buffMessage = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); + data->buff = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); + data->buffMaxWrite = (int) jack_ringbuffer_write_space( data->buff ); // Initialize JACK client if ( ( data->client = jack_client_open( clientName.c_str(), JackNoStartServer, NULL ) ) == 0 ) { @@ -3289,8 +3453,7 @@ MidiOutJack :: ~MidiOutJack() MidiOutJack::closePort(); // Cleanup - jack_ringbuffer_free( data->buffSize ); - jack_ringbuffer_free( data->buffMessage ); + jack_ringbuffer_free( data->buff ); if ( data->client ) { jack_client_close( data->client ); } @@ -3316,6 +3479,8 @@ void MidiOutJack :: openPort( unsigned int portNumber, const std::string &portNa if ( data->port == NULL ) { errorString_ = "MidiOutJack::openPort: JACK error creating port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; } @@ -3338,6 +3503,8 @@ void MidiOutJack :: openVirtualPort( const std::string &portName ) if ( data->port == NULL ) { errorString_ = "MidiOutJack::openVirtualPort: JACK error creating virtual port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); } } @@ -3437,9 +3604,331 @@ void MidiOutJack :: sendMessage( const unsigned char *message, size_t size ) int nBytes = static_cast(size); JackMidiData *data = static_cast (apiData_); + if ( size + sizeof(nBytes) > (size_t) data->buffMaxWrite ) + return; + + while ( jack_ringbuffer_write_space(data->buff) < sizeof(nBytes) + size ) + pthread_yield(); + // Write full message to buffer - jack_ringbuffer_write( data->buffMessage, ( const char * ) message, nBytes ); - jack_ringbuffer_write( data->buffSize, ( char * ) &nBytes, sizeof( nBytes ) ); + jack_ringbuffer_write( data->buff, ( char * ) &nBytes, sizeof( nBytes ) ); + jack_ringbuffer_write( data->buff, ( const char * ) message, nBytes ); } #endif // __UNIX_JACK__ + +//*********************************************************************// +// API: Web MIDI +// +// Written primarily by Atsushi Eno, February 2020. +// +// *********************************************************************// + +#if defined(__WEB_MIDI_API__) + +#include + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: WebMidiAccessShim +//*********************************************************************// + +class WebMidiAccessShim +{ +public: + WebMidiAccessShim(); + ~WebMidiAccessShim(); + std::string getPortName( unsigned int portNumber, bool isInput ); +}; + +std::unique_ptr shim{nullptr}; + +void ensureShim() +{ + if ( shim.get() != nullptr ) + return; + shim.reset( new WebMidiAccessShim() ); +} + +bool checkWebMidiAvailability() +{ + ensureShim(); + + return MAIN_THREAD_EM_ASM_INT( { + if ( typeof window._rtmidi_internals_waiting === "undefined" ) { + console.log ( "Attempted to use Web MIDI API without trying to open it." ); + return false; + } + if ( window._rtmidi_internals_waiting ) { + console.log ( "Attempted to use Web MIDI API while it is being queried." ); + return false; + } + if ( _rtmidi_internals_midi_access == null ) { + console.log ( "Attempted to use Web MIDI API while it already turned out to be unavailable." ); + return false; + } + return true; + } ); +} + +WebMidiAccessShim::WebMidiAccessShim() +{ + MAIN_THREAD_ASYNC_EM_ASM( { + if( typeof window._rtmidi_internals_midi_access !== "undefined" ) + return; + if( typeof window._rtmidi_internals_waiting !== "undefined" ) { + console.log( "MIDI Access was requested while another request is in progress." ); + return; + } + + // define functions + window._rtmidi_internals_get_port_by_number = function( portNumber, isInput ) { + var midi = window._rtmidi_internals_midi_access; + var devices = isInput ? midi.inputs : midi.outputs; + var i = 0; + for (var device of devices.values()) { + if ( i == portNumber ) + return device; + i++; + } + console.log( "MIDI " + (isInput ? "input" : "output") + " device of portNumber " + portNumber + " is not found."); + return null; + }; + + window._rtmidi_internals_waiting = true; + window.navigator.requestMIDIAccess( {"sysex": true} ).then( (midiAccess) => { + window._rtmidi_internals_midi_access = midiAccess; + window._rtmidi_internals_latest_message_timestamp = 0.0; + window._rtmidi_internals_waiting = false; + if( midiAccess == null ) { + console.log ( "Could not get access to MIDI API" ); + } + } ); + } ); +} + +WebMidiAccessShim::~WebMidiAccessShim() +{ +} + +std::string WebMidiAccessShim::getPortName( unsigned int portNumber, bool isInput ) +{ + if( !checkWebMidiAvailability() ) + return ""; + char *ret = (char*) MAIN_THREAD_EM_ASM_INT( { + var port = window._rtmidi_internals_get_port_by_number($0, $1); + if( port == null) + return null; + var length = lengthBytesUTF8(port.name) + 1; + var ret = _malloc(length); + stringToUTF8(port.name, ret, length); + return ret; + }, portNumber, isInput, &ret ); + if (ret == nullptr) + return ""; + std::string s = ret; + free(ret); + return s; +} + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: MidiInWeb +//*********************************************************************// + +MidiInWeb::MidiInWeb( const std::string &clientName, unsigned int queueSizeLimit ) + : MidiInApi( queueSizeLimit ) +{ + initialize( clientName ); +} + +MidiInWeb::~MidiInWeb( void ) +{ + closePort(); +} + +extern "C" void EMSCRIPTEN_KEEPALIVE rtmidi_onMidiMessageProc( MidiInApi::RtMidiInData* data, uint8_t* inputBytes, int32_t length, double domHighResTimeStamp ) +{ + auto &message = data->message; + message.bytes.resize(message.bytes.size() + length); + memcpy(message.bytes.data(), inputBytes, length); + // FIXME: handle timestamp + if ( data->usingCallback ) { + RtMidiIn::RtMidiCallback callback = (RtMidiIn::RtMidiCallback) data->userCallback; + callback( message.timeStamp, &message.bytes, data->userData ); + } +} + +void MidiInWeb::openPort( unsigned int portNumber, const std::string &portName ) +{ + if( !checkWebMidiAvailability() ) + return; + if (open_port_number >= 0) + return; + + MAIN_THREAD_EM_ASM( { + // In Web MIDI API world, there is no step to open a port, but we have to register the input callback instead. + var input = window._rtmidi_internals_get_port_by_number($0, true); + input.onmidimessage = function(e) { + // In RtMidi world, timestamps are delta time from previous message, while in Web MIDI world + // timestamps are relative to window creation time (i.e. kind of absolute time with window "epoch" time). + var rtmidiTimestamp = window._rtmidi_internals_latest_message_timestamp == 0.0 ? 0.0 : e.timeStamp - window._rtmidi_internals_latest_message_timestamp; + window._rtmidi_internals_latest_message_timestamp = e.timeStamp; + Module.ccall( 'rtmidi_onMidiMessageProc', 'void', ['number', 'array', 'number', 'number'], [$1, e.data, e.data.length, rtmidiTimestamp] ); + }; + }, portNumber, &inputData_ ); + open_port_number = portNumber; +} + +void MidiInWeb::openVirtualPort( const std::string &portName ) +{ + + errorString_ = "MidiInWeb::openVirtualPort: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +void MidiInWeb::closePort( void ) +{ + if( open_port_number < 0 ) + return; + + MAIN_THREAD_EM_ASM( { + var input = _rtmidi_internals_get_port_by_number($0, true); + if( input == null ) { + console.log( "Port #" + $0 + " could not be found."); + return; + } + // unregister event handler + input.onmidimessage = null; + }, open_port_number ); + open_port_number = -1; +} + +void MidiInWeb::setClientName( const std::string &clientName ) +{ + client_name = clientName; +} + +void MidiInWeb::setPortName( const std::string &portName ) +{ + + errorString_ = "MidiInWeb::setPortName: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +unsigned int MidiInWeb::getPortCount( void ) +{ + if( !checkWebMidiAvailability() ) + return 0; + return MAIN_THREAD_EM_ASM_INT( { return _rtmidi_internals_midi_access.inputs.size; } ); +} + +std::string MidiInWeb::getPortName( unsigned int portNumber ) +{ + if( !checkWebMidiAvailability() ) + return ""; + return shim->getPortName( portNumber, true ); +} + +void MidiInWeb::initialize( const std::string& clientName ) +{ + ensureShim(); + setClientName( clientName ); +} + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: MidiOutWeb +//*********************************************************************// + +MidiOutWeb::MidiOutWeb( const std::string &clientName ) +{ + initialize( clientName ); +} + +MidiOutWeb::~MidiOutWeb( void ) +{ + closePort(); +} + +void MidiOutWeb::openPort( unsigned int portNumber, const std::string &portName ) +{ + if( !checkWebMidiAvailability() ) + return; + if (open_port_number >= 0) + return; + // In Web MIDI API world, there is no step to open a port. + + open_port_number = portNumber; +} + +void MidiOutWeb::openVirtualPort( const std::string &portName ) +{ + + errorString_ = "MidiOutWeb::openVirtualPort: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +void MidiOutWeb::closePort( void ) +{ + // there is really nothing to do for output at JS side. + open_port_number = -1; +} + +void MidiOutWeb::setClientName( const std::string &clientName ) +{ + client_name = clientName; +} + +void MidiOutWeb::setPortName( const std::string &portName ) +{ + + errorString_ = "MidiOutWeb::setPortName: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +unsigned int MidiOutWeb::getPortCount( void ) +{ + if( !checkWebMidiAvailability() ) + return 0; + return MAIN_THREAD_EM_ASM_INT( { return _rtmidi_internals_midi_access.outputs.size; } ); +} + +std::string MidiOutWeb::getPortName( unsigned int portNumber ) +{ + if( !checkWebMidiAvailability() ) + return ""; + return shim->getPortName( portNumber, false ); +} + +void MidiOutWeb::sendMessage( const unsigned char *message, size_t size ) +{ + if( open_port_number < 0 ) + return; + + MAIN_THREAD_EM_ASM( { + var output = _rtmidi_internals_get_port_by_number( $0, false ); + if( output == null ) { + console.log( "Port #" + $0 + " could not be found."); + return; + } + var buf = new ArrayBuffer ($2); + var msg = new Uint8Array( buf ); + msg.set( new Uint8Array( Module.HEAPU8.buffer.slice( $1, $1 + $2 ) ) ); + output.send( msg ); + }, open_port_number, message, size ); +} + +void MidiOutWeb::initialize( const std::string& clientName ) +{ + if ( shim.get() != nullptr ) + return; + shim.reset( new WebMidiAccessShim() ); + setClientName( clientName ); +} + +#endif // __WEB_MIDI_API__ diff --git a/rtmidi/RtMidi.h b/rtmidi/RtMidi.h index 495e088..a6f5b79 100644 --- a/rtmidi/RtMidi.h +++ b/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-2019 Gary P. Scavone + Copyright (c) 2003-2021 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -58,13 +58,14 @@ #endif #endif -#define RTMIDI_VERSION "4.0.0" +#define RTMIDI_VERSION "5.0.0" #include #include #include #include + /************************************************************************/ /*! \class RtMidiError \brief Exception handling class for RtMidi. @@ -132,6 +133,8 @@ class MidiApi; class RTMIDI_DLL_PUBLIC RtMidi { public: + + RtMidi(RtMidi&& other) noexcept; //! MIDI API specifier arguments. enum Api { UNSPECIFIED, /*!< Search for a working compiled API. */ @@ -140,6 +143,7 @@ class RTMIDI_DLL_PUBLIC RtMidi UNIX_JACK, /*!< The JACK Low-Latency MIDI Server API. */ WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ + WEB_MIDI_API, /*!< W3C Web MIDI API. */ NUM_APIS /*!< Number of values in this enum. */ }; @@ -213,6 +217,10 @@ class RTMIDI_DLL_PUBLIC RtMidi RtMidi(); virtual ~RtMidi(); MidiApi *rtapi_; + + /* Make the class non-copyable */ + RtMidi(RtMidi& other) = delete; + RtMidi& operator=(RtMidi& other) = delete; }; /**********************************************************************/ @@ -228,8 +236,6 @@ class RTMIDI_DLL_PUBLIC RtMidi time. With the OS-X, Linux ALSA, and JACK MIDI APIs, it is also possible to open a virtual input port to which other MIDI software clients can connect. - - by Gary P. Scavone, 2003-2017. */ /**********************************************************************/ @@ -250,7 +256,6 @@ class RTMIDI_DLL_PUBLIC RtMidi class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi { public: - //! User callback function type definition. typedef void (*RtMidiCallback)( double timeStamp, std::vector *message, void *userData ); @@ -276,6 +281,8 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi const std::string& clientName = "RtMidi Input Client", unsigned int queueSizeLimit = 100 ); + RtMidiIn(RtMidiIn&& other) noexcept : RtMidi(std::move(other)) { } + //! If a MIDI connection is still open, it will be closed by the destructor. ~RtMidiIn ( void ) throw(); @@ -373,6 +380,19 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi */ virtual void setErrorCallback( RtMidiErrorCallback errorCallback = NULL, void *userData = 0 ); + //! Set maximum expected incoming message size. + /*! + For APIs that require manual buffer management, it can be useful to set the buffer + size and buffer count when expecting to receive large SysEx messages. Note that + currently this function has no effect when called after openPort(). The default + buffer size is 1024 with a count of 4 buffers, which should be sufficient for most + cases; as mentioned, this does not affect all API backends, since most either support + dynamically scalable buffers or take care of buffer handling themselves. It is + principally intended for users of the Windows MM backend who must support receiving + especially large messages. + */ + virtual void setBufferSize( unsigned int size, unsigned int count ); + protected: void openMidiApi( RtMidi::Api api, const std::string &clientName, unsigned int queueSizeLimit ); }; @@ -388,8 +408,6 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi connect to more than one MIDI device at the same time. With the OS-X, Linux ALSA and JACK MIDI APIs, it is also possible to open a virtual port to which other MIDI software clients can connect. - - by Gary P. Scavone, 2003-2017. */ /**********************************************************************/ @@ -407,6 +425,8 @@ class RTMIDI_DLL_PUBLIC RtMidiOut : public RtMidi RtMidiOut( RtMidi::Api api=UNSPECIFIED, const std::string& clientName = "RtMidi Output Client" ); + RtMidiOut(RtMidiOut&& other) noexcept : RtMidi(std::move(other)) { } + //! The destructor closes any open MIDI connections. ~RtMidiOut( void ) throw(); @@ -527,6 +547,7 @@ protected: RtMidiErrorCallback errorCallback_; bool firstErrorOccurred_; void *errorCallbackUserData_; + }; class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi @@ -539,6 +560,7 @@ class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi void cancelCallback( void ); virtual void ignoreTypes( bool midiSysex, bool midiTime, bool midiSense ); 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 // messages. Each message represents one and only one MIDI message. @@ -580,11 +602,13 @@ class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi RtMidiIn::RtMidiCallback userCallback; void *userData; bool continueSysex; + unsigned int bufferSize; + unsigned int bufferCount; // Default constructor. RtMidiInData() : ignoreFlags(7), doInput(false), firstMessage(true), apiData(0), usingCallback(false), - userCallback(0), userData(0), continueSysex(false) {} + userCallback(0), userData(0), continueSysex(false), bufferSize(1024), bufferCount(4) {} }; protected: @@ -618,6 +642,7 @@ inline std::string RtMidiIn :: getPortName( unsigned int portNumber ) { return r inline void RtMidiIn :: ignoreTypes( bool midiSysex, bool midiTime, bool midiSense ) { static_cast(rtapi_)->ignoreTypes( midiSysex, midiTime, midiSense ); } inline double RtMidiIn :: getMessage( std::vector *message ) { return static_cast(rtapi_)->getMessage( message ); } inline void RtMidiIn :: setErrorCallback( RtMidiErrorCallback errorCallback, void *userData ) { rtapi_->setErrorCallback(errorCallback, userData); } +inline void RtMidiIn :: setBufferSize( unsigned int size, unsigned int count ) { static_cast(rtapi_)->setBufferSize(size, count); } inline RtMidi::Api RtMidiOut :: getCurrentApi( void ) throw() { return rtapi_->getCurrentApi(); } inline void RtMidiOut :: openPort( unsigned int portNumber, const std::string &portName ) { rtapi_->openPort( portNumber, portName ); } diff --git a/rtmidi/rtmidi_c.cpp b/rtmidi/rtmidi_c.cpp index 248c9e5..19f586e 100644 --- a/rtmidi/rtmidi_c.cpp +++ b/rtmidi/rtmidi_c.cpp @@ -34,12 +34,12 @@ class StaticAssertions { StaticAssertions() { class CallbackProxyUserData { public: - CallbackProxyUserData (RtMidiCCallback cCallback, void *userData) - : c_callback (cCallback), user_data (userData) - { - } - RtMidiCCallback c_callback; - void *user_data; + CallbackProxyUserData (RtMidiCCallback cCallback, void *userData) + : c_callback (cCallback), user_data (userData) + { + } + RtMidiCCallback c_callback; + void *user_data; }; extern "C" const enum RtMidiApi rtmidi_compiled_apis[]; // casting from RtMidi::Api[] @@ -80,8 +80,8 @@ enum RtMidiApi rtmidi_compiled_api_by_name(const char *name) { void rtmidi_error (MidiApi *api, enum RtMidiErrorType type, const char* errorString) { - std::string msg = errorString; - api->error ((RtMidiError::Type) type, msg); + std::string msg = errorString; + api->error ((RtMidiError::Type) type, msg); } void rtmidi_open_port (RtMidiPtr device, unsigned int portNumber, const char *portName) @@ -89,7 +89,7 @@ void rtmidi_open_port (RtMidiPtr device, unsigned int portNumber, const char *po std::string name = portName; try { ((RtMidi*) device->ptr)->openPort (portNumber, name); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -101,7 +101,7 @@ void rtmidi_open_virtual_port (RtMidiPtr device, const char *portName) std::string name = portName; try { ((RtMidi*) device->ptr)->openVirtualPort (name); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -111,7 +111,7 @@ void rtmidi_open_virtual_port (RtMidiPtr device, const char *portName) void rtmidi_close_port (RtMidiPtr device) { - try { + try { ((RtMidi*) device->ptr)->closePort (); } catch (const RtMidiError & err) { @@ -132,32 +132,42 @@ unsigned int rtmidi_get_port_count (RtMidiPtr device) } } -const char* rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber) +int rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber, char * bufOut, int * bufLen) { + if (bufOut == nullptr && bufLen == nullptr) { + return -1; + } + + std::string name; try { - std::string name = ((RtMidi*) device->ptr)->getPortName (portNumber); - return strdup (name.c_str ()); - + name = ((RtMidi*) device->ptr)->getPortName (portNumber); } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); - return ""; + return -1; } + + if (bufOut == nullptr) { + *bufLen = static_cast(name.size()) + 1; + return 0; + } + + return snprintf(bufOut, static_cast(*bufLen), "%s", name.c_str()); } /* RtMidiIn API */ RtMidiInPtr rtmidi_in_create_default () { RtMidiWrapper* wrp = new RtMidiWrapper; - + try { RtMidiIn* rIn = new RtMidiIn (); - + wrp->ptr = (void*) rIn; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; @@ -172,10 +182,10 @@ RtMidiInPtr rtmidi_in_create (enum RtMidiApi api, const char *clientName, unsign { std::string name = clientName; RtMidiWrapper* wrp = new RtMidiWrapper; - + try { RtMidiIn* rIn = new RtMidiIn ((RtMidi::Api) api, name, queueSizeLimit); - + wrp->ptr = (void*) rIn; wrp->data = 0; wrp->ok = true; @@ -203,7 +213,7 @@ enum RtMidiApi rtmidi_in_get_current_api (RtMidiPtr device) { try { return (RtMidiApi) ((RtMidiIn*) device->ptr)->getCurrentApi (); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -215,8 +225,8 @@ enum RtMidiApi rtmidi_in_get_current_api (RtMidiPtr device) static void callback_proxy (double timeStamp, std::vector *message, void *userData) { - CallbackProxyUserData* data = reinterpret_cast (userData); - data->c_callback (timeStamp, message->data (), message->size (), data->user_data); + CallbackProxyUserData* data = reinterpret_cast (userData); + data->c_callback (timeStamp, message->data (), message->size (), data->user_data); } void rtmidi_in_set_callback (RtMidiInPtr device, RtMidiCCallback callback, void *userData) @@ -246,10 +256,10 @@ void rtmidi_in_cancel_callback (RtMidiInPtr device) void rtmidi_in_ignore_types (RtMidiInPtr device, bool midiSysex, bool midiTime, bool midiSense) { - ((RtMidiIn*) device->ptr)->ignoreTypes (midiSysex, midiTime, midiSense); + ((RtMidiIn*) device->ptr)->ignoreTypes (midiSysex, midiTime, midiSense); } -double rtmidi_in_get_message (RtMidiInPtr device, +double rtmidi_in_get_message (RtMidiInPtr device, unsigned char *message, size_t *size) { @@ -264,7 +274,7 @@ double rtmidi_in_get_message (RtMidiInPtr device, *size = v.size(); return ret; - } + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -284,12 +294,12 @@ RtMidiOutPtr rtmidi_out_create_default () try { RtMidiOut* rOut = new RtMidiOut (); - + wrp->ptr = (void*) rOut; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; @@ -307,12 +317,12 @@ RtMidiOutPtr rtmidi_out_create (enum RtMidiApi api, const char *clientName) try { RtMidiOut* rOut = new RtMidiOut ((RtMidi::Api) api, name); - + wrp->ptr = (void*) rOut; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; diff --git a/rtmidi/rtmidi_c.h b/rtmidi/rtmidi_c.h index 21f38ca..efbf977 100644 --- a/rtmidi/rtmidi_c.h +++ b/rtmidi/rtmidi_c.h @@ -41,7 +41,7 @@ struct RtMidiWrapper { void* ptr; void* data; - //! True when the last function call was OK. + //! True when the last function call was OK. bool ok; //! If an error occured (ok != true), set to an error message. @@ -136,8 +136,8 @@ RTMIDIAPI void rtmidi_error (enum RtMidiErrorType type, const char* errorString) */ RTMIDIAPI void rtmidi_open_port (RtMidiPtr device, unsigned int portNumber, const char *portName); -/*! \brief Creates a virtual MIDI port to which other software applications can - * connect. +/*! \brief Creates a virtual MIDI port to which other software applications can + * connect. * * \param portName Name for the application port. * @@ -155,10 +155,14 @@ RTMIDIAPI void rtmidi_close_port (RtMidiPtr device); */ RTMIDIAPI unsigned int rtmidi_get_port_count (RtMidiPtr device); -/*! \brief Return a string identifier for the specified MIDI input port number. +/*! \brief Access a string identifier for the specified MIDI input port number. + * + * To prevent memory leaks a char buffer must be passed to this function. + * NULL can be passed as bufOut parameter, and that will write the required buffer length in the bufLen. + * * See RtMidi::getPortName(). */ -RTMIDIAPI const char* rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber); +RTMIDIAPI int rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber, char * bufOut, int * bufLen); /* RtMidiIn API */ @@ -203,8 +207,9 @@ RTMIDIAPI void rtmidi_in_ignore_types (RtMidiInPtr device, bool midiSysex, bool * \param message Must point to a char* that is already allocated. * SYSEX messages maximum size being 1024, a statically * allocated array could - * be sufficient. - * \param size Is used to return the size of the message obtained. + * be sufficient. + * \param size Is used to return the size of the message obtained. + * Must be set to the size of \ref message when calling. * * See RtMidiIn::getMessage(). */