diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-10-07 06:50:04 +0200 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-10-07 06:50:04 +0200 |
| commit | 988f5d2b5343850e19ad1512cefe6c53953aa02e (patch) | |
| tree | 1ff29934294e73b2575488c06df91866ce251dbe | |
| parent | 9b5839c58a2e1df8bddf6b98805998508ea417d5 (diff) | |
| download | ttdaw-988f5d2b5343850e19ad1512cefe6c53953aa02e.tar.gz | |
Added bunch of examples
80 files changed, 21161 insertions, 27 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c21c3a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +example1 +example2 +example3 + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..51e4b2d --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +tests: build-portmidi + $(CC) -Wall example1.c minisdl_audio.c -lm -ldl -lpthread -o example1 + $(CC) -Wall example2.c minisdl_audio.c -lm -ldl -lpthread -o example2 + $(CC) -Wall example3.c -L./portmidi -lportmidi -o example3 + +run-example3: + LD_LIBRARY_PATH=./portmidi ./example3 + +build-portmidi: + cd portmidi && cmake . && make diff --git a/README.md b/README.md new file mode 100644 index 0000000..379eb42 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Tiny terminal DAW + +## Soundfonts + +- https://dev.nando.audio/pages/soundfonts.html +- https://archive.org/details/500-soundfonts-full-gm-sets +- https://github.com/marmooo/free-soundfonts + @@ -4,9 +4,8 @@ #include "minisdl_audio.h"
//This is a minimal SoundFont with a single loopin saw-wave sample/instrument/preset (484 bytes)
-const static unsigned char MinimalSoundFont[] =
-{
- #define TEN0 0,0,0,0,0,0,0,0,0,0
+const static unsigned char MinimalSoundFont[] = {
+#define TEN0 0,0,0,0,0,0,0,0,0,0
'R','I','F','F',220,1,0,0,'s','f','b','k',
'L','I','S','T',88,1,0,0,'p','d','t','a',
'p','h','d','r',76,TEN0,TEN0,TEN0,TEN0,0,0,0,0,TEN0,0,0,0,0,0,0,0,255,0,255,0,1,TEN0,0,0,0,
@@ -16,15 +15,14 @@ const static unsigned char MinimalSoundFont[] = 'i','g','e','n',12,0,0,0,54,0,1,0,53,0,0,0,0,0,0,0,
's','h','d','r',92,TEN0,TEN0,0,0,0,0,0,0,0,50,0,0,0,0,0,0,0,49,0,0,0,34,86,0,0,60,0,0,0,1,TEN0,TEN0,TEN0,TEN0,0,0,0,0,0,0,0,
'L','I','S','T',112,0,0,0,'s','d','t','a','s','m','p','l',100,0,0,0,86,0,119,3,31,7,147,10,43,14,169,17,58,21,189,24,73,28,204,31,73,35,249,38,46,42,71,46,250,48,150,53,242,55,126,60,151,63,108,66,126,72,207,
- 70,86,83,100,72,74,100,163,39,241,163,59,175,59,179,9,179,134,187,6,186,2,194,5,194,15,200,6,202,96,206,159,209,35,213,213,216,45,220,221,223,76,227,221,230,91,234,242,237,105,241,8,245,118,248,32,252
+ 70,86,83,100,72,74,100,163,39,241,163,59,175,59,179,9,179,134,187,6,186,2,194,5,194,15,200,6,202,96,206,159,209,35,213,213,216,45,220,221,223,76,227,221,230,91,234,242,237,105,241,8,245,118,248,32,252
};
// Holds the global instance pointer
static tsf* g_TinySoundFont;
// Callback function called by the audio thread
-static void AudioCallback(void* data, Uint8 *stream, int len)
-{
+static void AudioCallback(void* data, Uint8 *stream, int len) {
// Note we don't do any thread concurrency control here because in this
// example all notes are started before the audio playback begins.
// If you do play notes while the audio thread renders output you
@@ -33,8 +31,7 @@ static void AudioCallback(void* data, Uint8 *stream, int len) tsf_render_short(g_TinySoundFont, (short*)stream, SampleCount, 0);
}
-int main(int argc, char *argv[])
-{
+int main(int argc, char *argv[]) {
// Define the desired audio output format we request
SDL_AudioSpec OutputAudioSpec;
OutputAudioSpec.freq = 44100;
@@ -44,16 +41,14 @@ int main(int argc, char *argv[]) OutputAudioSpec.callback = AudioCallback;
// Initialize the audio system
- if (SDL_AudioInit(NULL) < 0)
- {
+ if (SDL_AudioInit(NULL) < 0) {
fprintf(stderr, "Could not initialize audio hardware or driver\n");
return 1;
}
// Load the SoundFont from the memory block
g_TinySoundFont = tsf_load_memory(MinimalSoundFont, sizeof(MinimalSoundFont));
- if (!g_TinySoundFont)
- {
+ if (!g_TinySoundFont) {
fprintf(stderr, "Could not load soundfont\n");
return 1;
}
@@ -66,8 +61,7 @@ int main(int argc, char *argv[]) tsf_note_on(g_TinySoundFont, 0, 52, 1.0f); //E2
// Request the desired audio output format
- if (SDL_OpenAudio(&OutputAudioSpec, NULL) < 0)
- {
+ if (SDL_OpenAudio(&OutputAudioSpec, NULL) < 0) {
fprintf(stderr, "Could not open the audio hardware or the desired audio output format\n");
return 1;
}
@@ -84,3 +78,4 @@ int main(int argc, char *argv[]) // because the process ends here.
return 0;
}
+
@@ -9,8 +9,7 @@ static tsf* g_TinySoundFont; // A Mutex so we don't call note_on/note_off while rendering audio samples
static SDL_mutex* g_Mutex;
-static void AudioCallback(void* data, Uint8 *stream, int len)
-{
+static void AudioCallback(void* data, Uint8 *stream, int len) {
// Render the audio samples in float format
int SampleCount = (len / (2 * sizeof(float))); //2 output channels
SDL_LockMutex(g_Mutex); //get exclusive lock
@@ -18,8 +17,7 @@ static void AudioCallback(void* data, Uint8 *stream, int len) SDL_UnlockMutex(g_Mutex);
}
-int main(int argc, char *argv[])
-{
+int main(int argc, char *argv[]) {
int i, Notes[7] = { 48, 50, 52, 53, 55, 57, 59 };
// Define the desired audio output format we request
@@ -31,16 +29,14 @@ int main(int argc, char *argv[]) OutputAudioSpec.callback = AudioCallback;
// Initialize the audio system
- if (SDL_AudioInit(TSF_NULL) < 0)
- {
+ if (SDL_AudioInit(TSF_NULL) < 0) {
fprintf(stderr, "Could not initialize audio hardware or driver\n");
return 1;
}
// Load the SoundFont from a file
- g_TinySoundFont = tsf_load_filename("florestan-subset.sf2");
- if (!g_TinySoundFont)
- {
+ g_TinySoundFont = tsf_load_filename("sf2/florestan-subset.sf2");
+ if (!g_TinySoundFont) {
fprintf(stderr, "Could not load SoundFont\n");
return 1;
}
@@ -52,8 +48,7 @@ int main(int argc, char *argv[]) g_Mutex = SDL_CreateMutex();
// Request the desired audio output format
- if (SDL_OpenAudio(&OutputAudioSpec, TSF_NULL) < 0)
- {
+ if (SDL_OpenAudio(&OutputAudioSpec, TSF_NULL) < 0) {
fprintf(stderr, "Could not open the audio hardware or the desired audio output format\n");
return 1;
}
@@ -63,8 +58,7 @@ int main(int argc, char *argv[]) SDL_PauseAudio(0);
// Loop through all the presets in the loaded SoundFont
- for (i = 0; i < tsf_get_presetcount(g_TinySoundFont); i++)
- {
+ for (i = 0; i < tsf_get_presetcount(g_TinySoundFont); i++) {
//Get exclusive mutex lock, end the previous note and play a new note
printf("Play note %d with preset #%d '%s'\n", Notes[i % 7], i, tsf_get_presetname(g_TinySoundFont, i));
SDL_LockMutex(g_Mutex);
@@ -79,3 +73,4 @@ int main(int argc, char *argv[]) // because the process ends here.
return 0;
}
+
diff --git a/example3.c b/example3.c new file mode 100644 index 0000000..9dea995 --- /dev/null +++ b/example3.c @@ -0,0 +1,53 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "portmidi/pm_common/portmidi.h" + +#define NUM_INPUTS 16 + +int main() { + PmError err; + PmStream *stream; + PmEvent events[128]; + int i, num_events; + + // Initialize PortMIDI + err = Pm_Initialize(); + if (err != pmNoError) { + fprintf(stderr, "Error initializing PortMIDI: %s\n", Pm_GetErrorText(err)); + exit(1); + } + + /* // Open a MIDI input device */ + /* stream = Pm_OpenInput(&err, NULL, NULL, NUM_INPUTS, NULL, 0); */ + /* if (err != pmNoError) { */ + /* fprintf(stderr, "Error opening MIDI input device: %s\n", Pm_GetErrorText(err)); */ + /* Pm_Terminate(); */ + /* exit(1); */ + /* } */ + + /* // Read MIDI messages from the input device */ + /* while (1) { */ + /* num_events = Pm_Read(stream, events, 128); */ + /* if (num_events > 0) { */ + /* for (i = 0; i < num_events; i++) { */ + /* if (events[i].message & 0xff00) { */ + /* // This is a status message (note on, note off, etc.) */ + /* printf("Message: 0x%02x\n", events[i].message); */ + /* /1* printf("Status: 0x%02x, Data 1: 0x%02x, Data 2: 0x%02x\n", *1/ */ + /* /1* events[i].message & 0xf0, events[i].message[0], events[i].message[1]); *1/ */ + /* } */ + /* } */ + /* } */ + /* } */ + + /* // Close the MIDI input device */ + /* Pm_Close(stream); */ + + // Terminate PortMIDI + Pm_Terminate(); + + return 0; +} + diff --git a/portmidi.h b/portmidi.h new file mode 100755 index 0000000..8696a73 --- /dev/null +++ b/portmidi.h @@ -0,0 +1,974 @@ +#ifndef PORTMIDI_PORTMIDI_H +#define PORTMIDI_PORTMIDI_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* + * PortMidi Portable Real-Time MIDI Library + * PortMidi API Header File + * Latest version available at: http://sourceforge.net/projects/portmedia + * + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * Copyright (c) 2001-2006 Roger B. Dannenberg + * + * 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. + * + * 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 text above constitutes the entire PortMidi license; however, + * the PortMusic community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/* CHANGELOG FOR PORTMIDI + * (see ../CHANGELOG.txt) + * + * NOTES ON HOST ERROR REPORTING: + * + * PortMidi errors (of type PmError) are generic, + * system-independent errors. When an error does not map to one of + * the more specific PmErrors, the catch-all code pmHostError is + * returned. This means that PortMidi has retained a more specific + * system-dependent error code. The caller can get more information + * by calling Pm_GetHostErrorText() to get a text string describing + * the error. Host errors can arise asynchronously from callbacks, + * * so there is no specific return code. Asynchronous errors are + * checked and reported by Pm_Poll. You can also check by calling + * Pm_HasHostError(). If this returns TRUE, Pm_GetHostErrorText() + * will return a text description of the error. + * + * NOTES ON COMPILE-TIME SWITCHES + * + * DEBUG assumes stdio and a console. Use this if you want + * automatic, simple error reporting, e.g. for prototyping. If + * you are using MFC or some other graphical interface with no + * console, DEBUG probably should be undefined. + * PM_CHECK_ERRORS more-or-less takes over error checking for + * return values, stopping your program and printing error + * messages when an error occurs. This also uses stdio for + * console text I/O. You can selectively disable this error + * checking by declaring extern int pm_check_errors; and + * setting pm_check_errors = FALSE; You can also reenable. + */ +/** + \defgroup grp_basics Basic Definitions + @{ +*/ + +#include <stdint.h> + +#ifdef _WINDLL +#define PMEXPORT __declspec(dllexport) +#else +#define PMEXPORT +#endif + +#ifndef FALSE + #define FALSE 0 +#endif +#ifndef TRUE + #define TRUE 1 +#endif + +/* default size of buffers for sysex transmission: */ +#define PM_DEFAULT_SYSEX_BUFFER_SIZE 1024 + + +typedef enum { + pmNoError = 0, /**< Normal return value indicating no error. */ + pmNoData = 0, /**< @brief No error, also indicates no data available. + * Use this constant where a value greater than zero would + * indicate data is available. + */ + pmGotData = 1, /**< A "no error" return also indicating data available. */ + pmHostError = -10000, + pmInvalidDeviceId, /**< Out of range or + * output device when input is requested or + * input device when output is requested or + * device is already opened. + */ + pmInsufficientMemory, + pmBufferTooSmall, + pmBufferOverflow, + pmBadPtr, /**< #PortMidiStream parameter is NULL or + * stream is not opened or + * stream is output when input is required or + * stream is input when output is required. */ + pmBadData, /**< Illegal midi data, e.g., missing EOX. */ + pmInternalError, + pmBufferMaxSize, /**< Buffer is already as large as it can be. */ + pmNotImplemented, /**< The function is not implemented, nothing was done. */ + pmInterfaceNotSupported, /**< The requested interface is not supported. */ + pmNameConflict, /**< Cannot create virtual device because name is taken. */ + pmDeviceRemoved /**< Output attempted after (USB) device was removed. */ + /* NOTE: If you add a new error type, you must update Pm_GetErrorText(). */ +} PmError; /**< @brief @enum PmError PortMidi error code; a common return type. + * No error is indicated by zero; errors are indicated by < 0. + */ + +/** + Pm_Initialize() is the library initialization function - call this before + using the library. + + *NOTE:* PortMidi scans for available devices when #Pm_Initialize + is called. To observe subsequent changes in the available + devices, you must shut down PortMidi by calling #Pm_Terminate and + then restart by calling #Pm_Initialize again. *IMPORTANT*: On + MacOS, #Pm_Initialize *must* always be called on the same + thread. Otherwise, changes in the available MIDI devices will + *not* be seen by PortMidi. As an example, if you start PortMidi in + a thread for processing MIDI, do not try to rescan devices by + calling #Pm_Initialize in a GUI thread. Instead, start PortMidi + the first time and every time in the GUI thread. Alternatively, + let the GUI request a restart in the MIDI thread. (These + restrictions only apply to macOS.) Speaking of threads, on all + platforms, you are allowed to call #Pm_Initialize in one thread, + yet send MIDI or poll for incoming MIDI in another + thread. However, PortMidi is not "thread safe," which means you + cannot allow threads to call PortMidi functions concurrently. + + @return pmNoError. + + PortMidi is designed to support multiple interfaces (such as ALSA, + CoreMIDI and WinMM). It is possible to return pmNoError because there + are no supported interfaces. In that case, zero devices will be + available. +*/ +PMEXPORT PmError Pm_Initialize(void); + +/** + Pm_Terminate() is the library termination function - call this after + using the library. +*/ +PMEXPORT PmError Pm_Terminate(void); + +/** Represents an open MIDI device. */ +typedef void PortMidiStream; + +/** A shorter form of #PortMidiStream. */ +#define PmStream PortMidiStream + +/** Test whether stream has a pending host error. Normally, the client + finds out about errors through returned error codes, but some + errors can occur asynchronously where the client does not + explicitly call a function, and therefore cannot receive an error + code. The client can test for a pending error using + Pm_HasHostError(). If true, the error can be accessed by calling + Pm_GetHostErrorText(). Pm_Poll() is similar to Pm_HasHostError(), + but if there is no error, it will return TRUE (1) if there is a + pending input message. +*/ +PMEXPORT int Pm_HasHostError(PortMidiStream * stream); + + +/** Translate portmidi error number into human readable message. + These strings are constants (set at compile time) so client has + no need to allocate storage. +*/ +PMEXPORT const char *Pm_GetErrorText(PmError errnum); + +/** Translate portmidi host error into human readable message. + These strings are computed at run time, so client has to allocate storage. + After this routine executes, the host error is cleared. +*/ +PMEXPORT void Pm_GetHostErrorText(char * msg, unsigned int len); + +/** Any host error msg has at most this many characters, including EOS. */ +#define PM_HOST_ERROR_MSG_LEN 256u + +/** Devices are represented as small integers. Device ids range from 0 + to Pm_CountDevices()-1. Pm_GetDeviceInfo() is used to get information + about the device, and Pm_OpenInput() and PmOpenOutput() are used to + open the device. +*/ +typedef int PmDeviceID; + +/** This PmDeviceID (constant) value represents no device and may be + returned by Pm_GetDefaultInputDeviceID() or + Pm_GetDefaultOutputDeviceID() if no default exists. +*/ +#define pmNoDevice -1 + +/** MIDI device information is returned in this structure, which is + owned by PortMidi and read-only to applications. See Pm_GetDeviceInfo(). +*/ +#define PM_DEVICEINFO_VERS 200 +typedef struct { + int structVersion; /**< @brief this internal structure version */ + const char *interf; /**< @brief underlying MIDI API, e.g. + "MMSystem" or "DirectX" */ + char *name; /**< @brief device name, e.g. "USB MidiSport 1x1" */ + int input; /**< @brief true iff input is available */ + int output; /**< @brief true iff output is available */ + int opened; /**< @brief used by generic PortMidi for error checking */ + int is_virtual; /**< @brief true iff this is/was a virtual device */ +} PmDeviceInfo; + +/** MIDI system-dependent device or driver info is passed in this + structure, which is owned by the caller. +*/ +#define PM_SYSDEPINFO_VERS 210 + +enum PmSysDepPropertyKey { + pmKeyNone = 0, /**< a "noop" key value */ + /** CoreMIDI Manufacturer name, value is string */ + pmKeyCoreMidiManufacturer = 1, + /** Linux ALSA snd_seq_port_info_set_name, value is a string. Can be + passed in PmSysDepInfo to Pm_OpenInput or Pm_OpenOutput when opening + a device. The created port will be named accordingly and will be + visible for externally made connections (subscriptions). (Linux ALSA + ports are always enabled for this, but only get application-specific + names if you give it one.) This key/value is ignored when opening + virtual ports, which are named when they are created.) */ + pmKeyAlsaPortName = 2, + /** Linux ALSA snd_seq_set_client_name, value is a string. + Can be passed in PmSysDepInfo to Pm_OpenInput or Pm_OpenOutput. + Pm_CreateVirtualInput or Pm_CreateVirtualOutput. Will override + any previously set client name and applies to all ports. */ + pmKeyAlsaClientName = 3 + /* if system-dependent code introduces more options, register + the key here to avoid conflicts. */ +}; + +/** System-dependent information can be passed when creating and opening + ports using this data structure, which stores alternating keys and + values (addresses). See `pm_test/sendvirtual.c`, `pm_test/recvvirtual.c`, + and `pm_test/testio.c` for examples. + */ +typedef struct { + int structVersion; /**< @brief this structure version */ + int length; /**< @brief number of properties in this structure */ + struct { + enum PmSysDepPropertyKey key; + const void *value; + } properties[]; +} PmSysDepInfo; + + +/** Get devices count, ids range from 0 to Pm_CountDevices()-1. */ +PMEXPORT int Pm_CountDevices(void); + +/** + Return the default device ID or pmNoDevice if there are no devices. + The result (but not pmNoDevice) can be passed to Pm_OpenMidi(). + + The use of these functions is not recommended. There is no natural + "default device" on any system, so defaults must be set by users. + (Currently, PortMidi just returns the first device it finds as + "default", so if there *is* a default, implementors should use + pm_add_device to add system default input and output devices + first.) + + The recommended solution is pass the burden to applications. It is + easy to scan devices with PortMidi and build a device menu, and to + save menu selections in application preferences for next + time. This is my recommendation for any GUI program. For simple + command-line applications and utilities, see pm_test where all the + test programs now accept device numbers on the command line and/or + prompt for their entry. + + On linux, you can create virtual ports and use an external program + to set up inter-application and device connections. + + Some advice for preferences: MIDI devices used to be built-in or + plug-in cards, so the numbers rarely changed. Now MIDI devices are + often plug-in USB devices, so device numbers change, and you + probably need to design to reinitialize PortMidi to rescan + devices. MIDI is pretty stateless, so this isn't a big problem, + although it means you cannot find a new device while playing or + recording MIDI. + + Since device numbering can change whenever a USB device is plugged + in, preferences should record *names* of devices rather than + device numbers. It is simple enough to use string matching to find + a prefered device, so PortMidi does not provide any built-in + lookup function. +*/ +PMEXPORT PmDeviceID Pm_GetDefaultInputDeviceID(void); + +/** @brief see PmDeviceID Pm_GetDefaultInputDeviceID() */ +PMEXPORT PmDeviceID Pm_GetDefaultOutputDeviceID(void); + +/** Find a device that matches a pattern. + + @param pattern a substring of the device name, or if the pattern + contains the two-character separator ", ", then the first part of + the pattern represents a device interface substring and the second + part after the separator represents a device name substring. + + @param is_input restricts the search to an input when true, or an + output when false. + + @return the number of the first device whose device interface + contains the interface pattern (if any), whose device name + contains the name pattern, and whose direction (input or output) + matches the #is_input parameter. If no match is found, #pmNoDevice + (-1) is returned. +*/ +PMEXPORT PmDeviceID Pm_FindDevice(char *pattern, int is_input); + + +/** Represents a millisecond clock with arbitrary start time. + This type is used for all MIDI timestamps and clocks. +*/ +typedef int32_t PmTimestamp; +typedef PmTimestamp (*PmTimeProcPtr)(void *time_info); + +/** TRUE if t1 before t2 */ +#define PmBefore(t1,t2) (((t1)-(t2)) < 0) +/** @} */ +/** + \defgroup grp_device Input/Output Devices Handling + @{ +*/ +/** Get a PmDeviceInfo structure describing a MIDI device. + + @param id the device to be queried. + + If \p id is out of range or if the device designates a deleted + virtual device, the function returns NULL. + + The returned structure is owned by the PortMidi implementation and + must not be manipulated or freed. The pointer is guaranteed to be + valid between calls to Pm_Initialize() and Pm_Terminate(). +*/ +PMEXPORT const PmDeviceInfo *Pm_GetDeviceInfo(PmDeviceID id); + +/** Open a MIDI device for input. + + @param stream the address of a #PortMidiStream pointer which will + receive a pointer to the newly opened stream. + + @param inputDevice the ID of the device to be opened (see #PmDeviceID). + + @param inputSysDepInfo a pointer to an optional system-dependent + data structure (a #PmSysDepInfo struct) containing additional + information for device setup or handle processing. This parameter + is never required for correct operation. If not used, specify + NULL. Declared `void *` here for backward compatibility. Note that + with Linux ALSA, you can use this parameter to specify a client name + and port name. + + @param bufferSize the number of input events to be buffered + waiting to be read using Pm_Read(). Messages will be lost if the + number of unread messages exceeds this value. + + @param time_proc (address of) a procedure that returns time in + milliseconds. It may be NULL, in which case a default millisecond + timebase (PortTime) is used. If the application wants to use + PortTime, it should start the timer (call Pt_Start) before calling + Pm_OpenInput or Pm_OpenOutput. If the application tries to start + the timer *after* Pm_OpenInput or Pm_OpenOutput, it may get a + ptAlreadyStarted error from Pt_Start, and the application's + preferred time resolution and callback function will be ignored. + \p time_proc result values are appended to incoming MIDI data, + normally by mapping system-provided timestamps to the \p time_proc + timestamps to maintain the precision of system-provided + timestamps. + + @param time_info is a pointer passed to time_proc. + + @return #pmNoError and places a pointer to a valid + #PortMidiStream in the stream argument. If the open operation + fails, a nonzero error code is returned (see #PMError) and + the value of stream is invalid. + + Any stream that is successfully opened should eventually be closed + by calling Pm_Close(). +*/ +PMEXPORT PmError Pm_OpenInput(PortMidiStream** stream, + PmDeviceID inputDevice, + void *inputSysDepInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info); + +/** Open a MIDI device for output. + + @param stream the address of a #PortMidiStream pointer which will + receive a pointer to the newly opened stream. + + @param outputDevice the ID of the device to be opened (see #PmDeviceID). + + @param inputSysDepInfo a pointer to an optional system-specific + data structure (a #PmSysDepInfo struct) containing additional + information for device setup or handle processing. This parameter + is never required for correct operation. If not used, specify + NULL. Declared `void *` here for backward compatibility. Note that + with Linux ALSA, you can use this parameter to specify a client name + and port name. + + @param bufferSize the number of output events to be buffered + waiting for output. In some cases -- see below -- PortMidi does + not buffer output at all and merely passes data to a lower-level + API, in which case \p bufferSize is ignored. Since MIDI speeds now + vary from 1 to 50 or more messages per ms (over USB), put some + thought into this number. E.g. if latency is 20ms and you want to + burst 100 messages in that time (5000 messages per second), you + should set \p bufferSize to at least 100. The default on Windows + assumes an average rate of 500 messages per second and in this + example, output would be slowed waiting for free buffers. + + @param latency the delay in milliseconds applied to timestamps + to determine when the output should actually occur. (If latency is + < 0, 0 is assumed.) If latency is zero, timestamps are ignored + and all output is delivered immediately. If latency is greater + than zero, output is delayed until the message timestamp plus the + latency. (NOTE: the time is measured relative to the time source + indicated by time_proc. Timestamps are absolute, not relative + delays or offsets.) In some cases, PortMidi can obtain better + timing than your application by passing timestamps along to the + device driver or hardware, so the best strategy to minimize jitter + is: wait until the real time to send the message, compute the + message, attach the *ideal* output time (not the current real + time, because some time may have elapsed), and send the + message. The \p latency will be added to the timestamp, and + provided the elapsed computation time has not exceeded \p latency, + the message will be delivered according to the timestamp. If the + real time is already past the timestamp, the message will be + delivered as soon as possible. Latency may also help you to + synchronize MIDI data to audio data by matching \p latency to the + audio buffer latency. + + @param time_proc (address of) a pointer to a procedure that + returns time in milliseconds. It may be NULL, in which case a + default millisecond timebase (PortTime) is used. If the + application wants to use PortTime, it should start the timer (call + Pt_Start) before calling #Pm_OpenInput or #Pm_OpenOutput. If the + application tries to start the timer *after* #Pm_OpenInput or + #Pm_OpenOutput, it may get a #ptAlreadyStarted error from #Pt_Start, + and the application's preferred time resolution and callback + function will be ignored. \p time_proc times are used to schedule + outgoing MIDI data (when latency is non-zero), usually by mapping + from time_proc timestamps to internal system timestamps to + maintain the precision of system-supported timing. + + @param time_info a pointer passed to time_proc. + + @return #pmNoError and places a pointer to a valid #PortMidiStream + in the stream argument. If the operation fails, a nonzero error + code is returned (see PMError) and the value of \p stream is + invalid. + + Note: ALSA appears to have a fixed-size priority queue for timed + output messages. Testing indicates the queue can hold a little + over 400 3-byte MIDI messages. Thus, you can send 10,000 + messages/second if the latency is 30ms (30ms * 10000 msgs/sec * + 0.001 sec/ms = 300 msgs), but not if the latency is 50ms + (resulting in about 500 pending messages, which is greater than + the 400 message limit). Since timestamps in ALSA are relative, + they are of less value than absolute timestamps in macOS and + Windows. This is a limitation of ALSA and apparently a design + flaw. + + Example 1: If I provide a timestamp of 5000, latency is 1, and + time_proc returns 4990, then the desired output time will be when + time_proc returns timestamp+latency = 5001. This will be 5001-4990 + = 11ms from now. + + Example 2: If I want to send at exactly 5010, and latency is 10, I + should wait until 5000, compute the messages and provide a + timestamp of 5000. As long as computation takes less than 10ms, + the message will be delivered at time 5010. + + Example 3 (recommended): It is often convenient to ignore latency. + E.g. if a sequence says to output at time 5010, just wait until + 5010, compute the message and use 5010 for the timestamp. Delivery + will then be at 5010+latency, but unless you are synchronizing to + something else, the absolute delay by latency will not matter. + + Any stream that is successfully opened should eventually be closed + by calling Pm_Close(). +*/ +PMEXPORT PmError Pm_OpenOutput(PortMidiStream** stream, + PmDeviceID outputDevice, + void *outputSysDepInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info, + int32_t latency); + +/** Create a virtual input device. + + @param name gives the virtual device name, which is visible to + other applications. + + @param interf is the interface (System API) used to create the + device Default interfaces are "MMSystem", "CoreMIDI" and + "ALSA". Currently, these are the only ones implemented, but future + implementations could support DirectMusic, Jack, sndio, or others. + + @param sysDepInfo contains interface-dependent additional + information (a #PmSysDepInfo struct), e.g., hints or options. This + parameter is never required for correct operation. If not used, + specify NULL. Declared `void *` here for backward compatibility. + + @return a device ID or #pmNameConflict (\p name is invalid or + already exists) or #pmInterfaceNotSupported (\p interf is does not + match a supported interface). + + The created virtual device appears to other applications as if it + is an output device. The device must be opened to obtain a stream + and read from it. + + Virtual devices are not supported by Windows (Multimedia API). Calls + on Windows do nothing except return #pmNotImplemented. +*/ +PMEXPORT PmError Pm_CreateVirtualInput(const char *name, + const char *interf, + void *sysDepInfo); + +/** Create a virtual output device. + + @param name gives the virtual device name, which is visible to + other applications. + + @param interf is the interface (System API) used to create the + device Default interfaces are "MMSystem", "CoreMIDI" and + "ALSA". Currently, these are the only ones implemented, but future + implementations could support DirectMusic, Jack, sndio, or others. + + @param sysDepInfo contains interface-dependent additional + information (a #PmSysDepInfo struct), e.g., hints or options. This + parameter is never required for correct operation. If not used, + specify NULL. Declared `void *` here for backward compatibility. + + @return a device ID or #pmInvalidDeviceId (\p name is invalid or + already exists) or #pmInterfaceNotSupported (\p interf is does not + match a supported interface). + + The created virtual device appears to other applications as if it + is an input device. The device must be opened to obtain a stream + and write to it. + + Virtual devices are not supported by Windows (Multimedia API). Calls + on Windows do nothing except return #pmNotImplemented. +*/ +PMEXPORT PmError Pm_CreateVirtualOutput(const char *name, + const char *interf, + void *sysDepInfo); + +/** Remove a virtual device. + + @param device a device ID (small integer) designating the device. + + The device is removed; other applications can no longer see or open + this virtual device, which may be either for input or output. The + device must not be open. The device ID may be reused, but existing + devices are not renumbered. This means that the device ID could be + in the range from 0 to #Pm_CountDevices(), yet the device ID does + not designate a device. In that case, passing the ID to + #Pm_GetDeviceInfo() will return NULL. + + @return #pmNoError if the device was deleted or #pmInvalidDeviceId + if the device is open, already deleted, or \p device is out of + range. +*/ +PMEXPORT PmError Pm_DeleteVirtualDevice(PmDeviceID device); + /** @} */ + +/** + @defgroup grp_events_filters Events and Filters Handling + @{ +*/ + +/* Filter bit-mask definitions */ +/** filter active sensing messages (0xFE): */ +#define PM_FILT_ACTIVE (1 << 0x0E) +/** filter system exclusive messages (0xF0): */ +#define PM_FILT_SYSEX (1 << 0x00) +/** filter MIDI clock message (0xF8) */ +#define PM_FILT_CLOCK (1 << 0x08) +/** filter play messages (start 0xFA, stop 0xFC, continue 0xFB) */ +#define PM_FILT_PLAY ((1 << 0x0A) | (1 << 0x0C) | (1 << 0x0B)) +/** filter tick messages (0xF9) */ +#define PM_FILT_TICK (1 << 0x09) +/** filter undefined FD messages */ +#define PM_FILT_FD (1 << 0x0D) +/** filter undefined real-time messages */ +#define PM_FILT_UNDEFINED PM_FILT_FD +/** filter reset messages (0xFF) */ +#define PM_FILT_RESET (1 << 0x0F) +/** filter all real-time messages */ +#define PM_FILT_REALTIME (PM_FILT_ACTIVE | PM_FILT_SYSEX | PM_FILT_CLOCK | \ + PM_FILT_PLAY | PM_FILT_UNDEFINED | PM_FILT_RESET | PM_FILT_TICK) +/** filter note-on and note-off (0x90-0x9F and 0x80-0x8F */ +#define PM_FILT_NOTE ((1 << 0x19) | (1 << 0x18)) +/** filter channel aftertouch (most midi controllers use this) (0xD0-0xDF)*/ +#define PM_FILT_CHANNEL_AFTERTOUCH (1 << 0x1D) +/** per-note aftertouch (0xA0-0xAF) */ +#define PM_FILT_POLY_AFTERTOUCH (1 << 0x1A) +/** filter both channel and poly aftertouch */ +#define PM_FILT_AFTERTOUCH (PM_FILT_CHANNEL_AFTERTOUCH | \ + PM_FILT_POLY_AFTERTOUCH) +/** Program changes (0xC0-0xCF) */ +#define PM_FILT_PROGRAM (1 << 0x1C) +/** Control Changes (CC's) (0xB0-0xBF)*/ +#define PM_FILT_CONTROL (1 << 0x1B) +/** Pitch Bender (0xE0-0xEF*/ +#define PM_FILT_PITCHBEND (1 << 0x1E) +/** MIDI Time Code (0xF1)*/ +#define PM_FILT_MTC (1 << 0x01) +/** Song Position (0xF2) */ +#define PM_FILT_SONG_POSITION (1 << 0x02) +/** Song Select (0xF3)*/ +#define PM_FILT_SONG_SELECT (1 << 0x03) +/** Tuning request (0xF6) */ +#define PM_FILT_TUNE (1 << 0x06) +/** All System Common messages (mtc, song position, song select, tune request) */ +#define PM_FILT_SYSTEMCOMMON (PM_FILT_MTC | PM_FILT_SONG_POSITION | \ + PM_FILT_SONG_SELECT | PM_FILT_TUNE) + + +/* Set filters on an open input stream to drop selected input types. + + @param stream an open MIDI input stream. + + @param filters indicate message types to filter (block). + + @return #pmNoError or an error code. + + By default, only active sensing messages are filtered. + To prohibit, say, active sensing and sysex messages, call + Pm_SetFilter(stream, PM_FILT_ACTIVE | PM_FILT_SYSEX); + + Filtering is useful when midi routing or midi thru functionality + is being provided by the user application. + For example, you may want to exclude timing messages (clock, MTC, + start/stop/continue), while allowing note-related messages to pass. + Or you may be using a sequencer or drum-machine for MIDI clock + information but want to exclude any notes it may play. + */ +PMEXPORT PmError Pm_SetFilter(PortMidiStream* stream, int32_t filters); + +/** Create a mask that filters one channel. */ +#define Pm_Channel(channel) (1<<(channel)) + +/** Filter incoming messages based on channel. + + @param stream an open MIDI input stream. + + @param mask indicates channels to be received. + + @return #pmNoError or an error code. + + The \p mask is a 16-bit bitfield corresponding to appropriate channels. + The #Pm_Channel macro can assist in calling this function. + I.e. to receive only input on channel 1, call with + Pm_SetChannelMask(Pm_Channel(1)); + Multiple channels should be OR'd together, like + Pm_SetChannelMask(Pm_Channel(10) | Pm_Channel(11)) + + Note that channels are numbered 0 to 15 (not 1 to 16). Most + synthesizer and interfaces number channels starting at 1, but + PortMidi numbers channels starting at 0. + + All channels are allowed by default +*/ +PMEXPORT PmError Pm_SetChannelMask(PortMidiStream *stream, int mask); + +/** Terminate outgoing messages immediately. + + @param stream an open MIDI output stream. + + @result #pmNoError or an error code. + + The caller should immediately close the output port; this call may + result in transmission of a partial MIDI message. There is no + abort for Midi input because the user can simply ignore messages + in the buffer and close an input device at any time. If the + specified behavior cannot be achieved through the system-level + interface (ALSA, CoreMIDI, etc.), the behavior may be that of + Pm_Close(). + */ +PMEXPORT PmError Pm_Abort(PortMidiStream* stream); + +/** Close a midi stream, flush any pending buffers if possible. + + @param stream an open MIDI input or output stream. + + @result #pmNoError or an error code. + + If the system-level interface (ALSA, CoreMIDI, etc.) does not + support flushing remaining messages, the behavior may be one of + the following (most preferred first): block until all pending + timestamped messages are delivered; deliver messages to a server + or kernel process for later delivery but return immediately; drop + messages (as in Pm_Abort()). Therefore, to be safe, applications + should wait until the output queue is empty before calling + Pm_Close(). E.g. calling Pt_Sleep(100 + latency); will give a + 100ms "cushion" beyond latency (if any) before closing. +*/ +PMEXPORT PmError Pm_Close(PortMidiStream* stream); + +/** (re)synchronize to the time_proc passed when the stream was opened. + + @param stream an open MIDI input or output stream. + + @result #pmNoError or an error code. + + Typically, this is used when the stream must be opened before the + time_proc reference is actually advancing. In this case, message + timing may be erratic, but since timestamps of zero mean "send + immediately," initialization messages with zero timestamps can be + written without a functioning time reference and without + problems. Before the first MIDI message with a non-zero timestamp + is written to the stream, the time reference must begin to advance + (for example, if the time_proc computes time based on audio + samples, time might begin to advance when an audio stream becomes + active). After time_proc return values become valid, and BEFORE + writing the first non-zero timestamped MIDI message, call + Pm_Synchronize() so that PortMidi can observe the difference + between the current time_proc value and its MIDI stream time. + + In the more normal case where time_proc values advance + continuously, there is no need to call #Pm_Synchronize. PortMidi + will always synchronize at the first output message and + periodically thereafter. +*/ +PMEXPORT PmError Pm_Synchronize(PortMidiStream* stream); + + +/** Encode a short Midi message into a 32-bit word. If data1 + and/or data2 are not present, use zero. +*/ +#define Pm_Message(status, data1, data2) \ + ((((data2) << 16) & 0xFF0000) | \ + (((data1) << 8) & 0xFF00) | \ + ((status) & 0xFF)) +/** Extract the status field from a 32-bit midi message. */ +#define Pm_MessageStatus(msg) ((msg) & 0xFF) +/** Extract the 1st data field (e.g., pitch) from a 32-bit midi message. */ +#define Pm_MessageData1(msg) (((msg) >> 8) & 0xFF) +/** Extract the 2nd data field (e.g., velocity) from a 32-bit midi message. */ +#define Pm_MessageData2(msg) (((msg) >> 16) & 0xFF) + +typedef uint32_t PmMessage; /**< @brief see #PmEvent */ +/** + All MIDI data comes in the form of PmEvent structures. A sysex + message is encoded as a sequence of PmEvent structures, with each + structure carrying 4 bytes of the message, i.e. only the first + PmEvent carries the status byte. + + All other MIDI messages take 1 to 3 bytes and are encoded in a whole + PmMessage with status in the low-order byte and remaining bytes + unused, i.e., a 3-byte note-on message will occupy 3 low-order bytes + of PmMessage, leaving the high-order byte unused. + + Note that MIDI allows nested messages: the so-called "real-time" MIDI + messages can be inserted into the MIDI byte stream at any location, + including within a sysex message. MIDI real-time messages are one-byte + messages used mainly for timing (see the MIDI spec). PortMidi retains + the order of non-real-time MIDI messages on both input and output, but + it does not specify exactly how real-time messages are processed. This + is particulary problematic for MIDI input, because the input parser + must either prepare to buffer an unlimited number of sysex message + bytes or to buffer an unlimited number of real-time messages that + arrive embedded in a long sysex message. To simplify things, the input + parser is allowed to pass real-time MIDI messages embedded within a + sysex message, and it is up to the client to detect, process, and + remove these messages as they arrive. + + When receiving sysex messages, the sysex message is terminated + by either an EOX status byte (anywhere in the 4 byte messages) or + by a non-real-time status byte in the low order byte of the message. + If you get a non-real-time status byte but there was no EOX byte, it + means the sysex message was somehow truncated. This is not + considered an error; e.g., a missing EOX can result from the user + disconnecting a MIDI cable during sysex transmission. + + A real-time message can occur within a sysex message. A real-time + message will always occupy a full PmEvent with the status byte in + the low-order byte of the PmEvent message field. (This implies that + the byte-order of sysex bytes and real-time message bytes may not + be preserved -- for example, if a real-time message arrives after + 3 bytes of a sysex message, the real-time message will be delivered + first. The first word of the sysex message will be delivered only + after the 4th byte arrives, filling the 4-byte PmEvent message field. + + The timestamp field is observed when the output port is opened with + a non-zero latency. A timestamp of zero means "use the current time", + which in turn means to deliver the message with a delay of + latency (the latency parameter used when opening the output port.) + Do not expect PortMidi to sort data according to timestamps -- + messages should be sent in the correct order, and timestamps MUST + be non-decreasing. See also "Example" for Pm_OpenOutput() above. + + A sysex message will generally fill many #PmEvent structures. On + output to a #PortMidiStream with non-zero latency, the first timestamp + on sysex message data will determine the time to begin sending the + message. PortMidi implementations may ignore timestamps for the + remainder of the sysex message. + + On input, the timestamp ideally denotes the arrival time of the + status byte of the message. The first timestamp on sysex message + data will be valid. Subsequent timestamps may denote + when message bytes were actually received, or they may be simply + copies of the first timestamp. + + Timestamps for nested messages: If a real-time message arrives in + the middle of some other message, it is enqueued immediately with + the timestamp corresponding to its arrival time. The interrupted + non-real-time message or 4-byte packet of sysex data will be enqueued + later. The timestamp of interrupted data will be equal to that of + the interrupting real-time message to insure that timestamps are + non-decreasing. + */ +typedef struct { + PmMessage message; + PmTimestamp timestamp; +} PmEvent; + +/** @} */ + +/** \defgroup grp_io Reading and Writing Midi Messages + @{ +*/ +/** Retrieve midi data into a buffer. + + @param stream the open input stream. + + @return the number of events read, or, if the result is negative, + a #PmError value will be returned. + + The Buffer Overflow Problem + + The problem: if an input overflow occurs, data will be lost, + ultimately because there is no flow control all the way back to + the data source. When data is lost, the receiver should be + notified and some sort of graceful recovery should take place, + e.g. you shouldn't resume receiving in the middle of a long sysex + message. + + With a lock-free fifo, which is pretty much what we're stuck with + to enable portability to the Mac, it's tricky for the producer and + consumer to synchronously reset the buffer and resume normal + operation. + + Solution: the entire buffer managed by PortMidi will be flushed + when an overflow occurs. The consumer (Pm_Read()) gets an error + message (#pmBufferOverflow) and ordinary processing resumes as + soon as a new message arrives. The remainder of a partial sysex + message is not considered to be a "new message" and will be + flushed as well. +*/ +PMEXPORT int Pm_Read(PortMidiStream *stream, PmEvent *buffer, int32_t length); + +/** Test whether input is available. + + @param stream an open input stream. + + @return TRUE, FALSE, or an error value. + + If there was an asynchronous error, pmHostError is returned and you must + call again to determine if input is (also) available. + + You should probably *not* use this function. Call Pm_Read() + instead. If it returns 0, then there is no data available. It is + possible for Pm_Poll() to return TRUE before the complete message + is available, so Pm_Read() could return 0 even after Pm_Poll() + returns TRUE. Only call Pm_Poll() if you want to know that data is + probably available even though you are not ready to receive data. +*/ +PMEXPORT PmError Pm_Poll(PortMidiStream *stream); + +/** Write MIDI data from a buffer. + + @param stream an open output stream. + + @param buffer (address of) an array of MIDI event data. + + @param length the length of the \p buffer. + + @return TRUE, FALSE, or an error value. + + \b buffer may contain: + - short messages + - sysex messages that are converted into a sequence of PmEvent + structures, e.g. sending data from a file or forwarding them + from midi input, with 4 SysEx bytes per PmEvent message, + low-order byte first, until the last message, which may + contain from 1 to 4 bytes ending in MIDI EOX (0xF7). + - PortMidi allows 1-byte real-time messages to be embedded + within SysEx messages, but only on 4-byte boundaries so + that SysEx data always uses a full 4 bytes (except possibly + at the end). Each real-time message always occupies a full + PmEvent (3 of the 4 bytes in the PmEvent's message are + ignored) even when embedded in a SysEx message. + + Use Pm_WriteSysEx() to write a sysex message stored as a contiguous + array of bytes. + + Sysex data may contain embedded real-time messages. + + \p buffer is managed by the caller. The buffer may be destroyed + as soon as this call returns. +*/ +PMEXPORT PmError Pm_Write(PortMidiStream *stream, PmEvent *buffer, + int32_t length); + +/** Write a timestamped non-system-exclusive midi message. + + @param stream an open output stream. + + @param when timestamp for the event. + + @param msg the data for the event. + + @result #pmNoError or an error code. + + Messages are delivered in order, and timestamps must be + non-decreasing. (But timestamps are ignored if the stream was + opened with latency = 0, and otherwise, non-decreasing timestamps + are "corrected" to the lowest valid value.) +*/ +PMEXPORT PmError Pm_WriteShort(PortMidiStream *stream, PmTimestamp when, + PmMessage msg); + +/** Write a timestamped system-exclusive midi message. + + @param stream an open output stream. + + @param when timestamp for the event. + + @param msg the sysex message, terminated with an EOX status byte. + + @result #pmNoError or an error code. + + \p msg is managed by the caller and may be destroyed when this + call returns. +*/ +PMEXPORT PmError Pm_WriteSysEx(PortMidiStream *stream, PmTimestamp when, + unsigned char *msg); + +/** @} */ + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* PORTMIDI_PORTMIDI_H */ diff --git a/portmidi/.github/workflows/build.yml b/portmidi/.github/workflows/build.yml new file mode 100644 index 0000000..351b5cb --- /dev/null +++ b/portmidi/.github/workflows/build.yml @@ -0,0 +1,47 @@ +name: build + +on: + push: + pull_request: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - name: Ubuntu + os: ubuntu-latest + install_dir: ~/portmidi + cmake_extras: -DCMAKE_BUILD_TYPE=RelWithDebInfo + - name: macOS + os: macos-latest + install_dir: ~/portmidi + cmake_extras: -DCMAKE_BUILD_TYPE=RelWithDebInfo + - name: Windows + os: windows-latest + install_dir: C:\portmidi + cmake_config: --config RelWithDebInfo + + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + - name: "[Ubuntu] Install dependencies" + run: sudo apt install -y libasound2-dev + if: runner.os == 'Linux' + - name: Configure + run: cmake -D CMAKE_INSTALL_PREFIX=${{ matrix.install_dir }} ${{ matrix.cmake_extras }} -S . -B build + - name: Build + run: cmake --build build ${{ matrix.cmake_config }} + env: + CMAKE_BUILD_PARALLEL_LEVEL: 2 + - name: Install + run: cmake --install . ${{ matrix.cmake_config }} + working-directory: build + - name: Upload Build Artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.name }} portmidi build + path: ${{ matrix.install_dir }} diff --git a/portmidi/.github/workflows/docs.yml b/portmidi/.github/workflows/docs.yml new file mode 100644 index 0000000..d0e251b --- /dev/null +++ b/portmidi/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: Generate Docs + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + doxygen: + name: Doxygen + runs-on: ubuntu-latest + steps: + - name: "Check out repository" + uses: actions/checkout@v2 + + - name: Install Doxygen + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends doxygen + + - name: Generate Documentation + run: doxygen + working-directory: . + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/html diff --git a/portmidi/.gitignore b/portmidi/.gitignore new file mode 100644 index 0000000..19d6650 --- /dev/null +++ b/portmidi/.gitignore @@ -0,0 +1,62 @@ +.DS_Store +build*/ +*~ +CMakeCache.txt +CMakeFiles/ +CMakeScripts/ +/portmidi.pc +/x64/ +/Debug/ +/Release/ +/pm_java/pmdefaults/pmdefaults.jar +/pm_java/pmdefaults.sln +/pm_java/pmjni.dir/ +/pm_java/x64/ +portmidi.build/ +cmake_install.cmake +*.xcodeproj/ +/.vs/ +/portmidi.sln +*.vcxproj +*.vcxproj.filters +*.vcxproj.user +/Makefile +/libportmidi.so* +/libportmidi.a +/libportmidi_static.a +/libpmjni.so* +/packaging/PortMidiConfig.cmake +/packaging/PortMidiConfigVersion.cmake +/packaging/portmidi.pc +/pm_common/Makefile +/pm_common/portmidi.dir/ +/pm_java/Makefile +/pm_test/Debug/ +/pm_test/Release/ +/pm_test/Makefile +/pm_test/fastrcv +/pm_test/fastrcv.dir/ +/pm_test/latency +/pm_test/latency.dir/ +/pm_test/midiclock +/pm_test/midiclock.dir/ +/pm_test/midithread +/pm_test/midithread.dir/ +/pm_test/midithru +/pm_test/midithru.dir/ +/pm_test/mm +/pm_test/mm.dir/ +/pm_test/multivirtual +/pm_test/qtest +/pm_test/qtest.dir/ +/pm_test/recvvirtual +/pm_test/sendvirtual +/pm_test/sysex +/pm_test/sysex.dir/ +/pm_test/testio +/pm_test/testio.dir/ +/pm_test/virttest +/pm_test/fast +/pm_test/fast.dir/ +/pm_test/pmlist +/pm_test/pmlist.dir/ diff --git a/portmidi/CHANGELOG.txt b/portmidi/CHANGELOG.txt new file mode 100644 index 0000000..fee4330 --- /dev/null +++ b/portmidi/CHANGELOG.txt @@ -0,0 +1,213 @@ +/* CHANGELOG FOR PORTMIDI + * + * 21Feb22 v2.0.3 Roger Dannenberg + * - this version allows multiple hardware devices to have the same name. + * + * 03Jan22 v2.0.2 Roger Dannenberg + * - many changes for CMake including install support + * - bare-bones Java and PmDefaults support. It runs, but no + * installation. + * + * 16Sep21 Roger Dannenberg + * - Added CreateVirtualInput and CreateVirtualOutput functions (macOS + * & Linux) only. + * - Fix for unicode endpoints on macOS CoreMIDI. + * - Parsing in macOS of realtime message embedded in short messages + * (can this actually happen?) + * - renamed pm_test/test.c to pm_test/testio.c + * - with this release, pm_java, pm_csharp, pm_cl, pm_python, pm_qt + * are marked as "legacy code" and README.txt's refer to other + * projects. I had hoped for "one-stop shopping" for language + * bindings, but developers decided to move work to independent + * repositories. Maybe that's better. + * + * 19Oct09 Roger Dannenberg + * - Changes dynamic library names from portmidi_d to portmidi to + * be backward-compatible with programs expecting a library by + * the old name. + * + * 04Oct09 Roger Dannenberg + * - Converted to using Cmake. + * - Renamed static and dynamic library files to portmidi_s and portmidi_d + * - Eliminated VC9 and VC8 files (went back to simply test.vcproj, etc., + * use Cmake to switch from the provided VC9 files to VC8 or other) + * - Many small changes to prepare for 64-bit architectures (but only + * tested on 32-bit machines) + * + * 16Jun09 Roger Dannenberg + * - Started using Microsoft Visual C++ Version 9 (Express). Converted + * all *-VC9.vcproj file to *.vcproj and renamed old project files to + * *-VC8.proj. Previously, output from VC9 went to special VC9 files, + * that breaks any program or script looking for output in release or + * debug files, so now both compiler version output to the same folders. + * Now, debug version uses static linking with debug DLL runtime, and + * release version uses static linking with statically linked runtime. + * Converted to Inno Setup and worked on scripts to make things build + * properly, especially pmdefaults. + * + * 02Jan09 Roger Dannenberg + * - Created Java interface and wrote PmDefaults application to set + * values for Pm_GetDefaultInputDeviceID() and + * Pm_GetDefaultOutputDeviceID(). Other fixes. + * + * 19Jun08 Roger Dannenberg and Austin Sung + * - Removed USE_DLL_FOR_CLEANUP -- Windows 2000 through Vista seem to be + * fixed now, and can recover if MIDI ports are left open + * - Various other minor patches + * + * 17Jan07 Roger Dannenberg + * - Lots more help for Common Lisp user in pm_cl + * - Minor fix to eliminate a compiler warning + * - Went back to single library in OS X for both portmidi and porttime + * + * 16Jan07 Roger Dannenberg + * - OOPS! fixed bug where short messages all had zero data + * - Makefile.osx static library build now makes universal (i386 + ppc) + * binaries + * + * 15Jan07 Roger Dannenberg + * - multiple rewrites of sysex handling code to take care of + * error-handling, embedded messages, message filtering, + * driver bugs, and host limitations. + * - fixed windows to use dwBufferLength rather than + * dwBytesRecorded for long buffer output (fix by Nigel Brown) + * - Win32 MME code always appends an extra zero to long buffer + * output to work around a problem with earlier versions of Midi Yoke + * - Added mm, a command line Midi Monitor to pm_test suite + * - Revised copyright notice to match PortAudio/MIT license (requests + * are moved out of the license proper and into a separate paragraph) + * + * 18Oct06 Roger Dannenberg + * - replace FIFO in pmutil with Light Pipe-based multiprocessor-safe alg. + * - replace FIFO in portmidi.c with PmQueue from pmutil + * + * 07Oct06 cpr & Roger Dannenberg + * - overhaul of CoreMIDI input to handle running status and multiple + * - messages per packet, with additional error detection + * - added Leigh Smith and Rick Taube support for Common Lisp and + * - dynamic link libraries in OSX + * - initialize static global seq = NULL in pmlinuxalsa.c + * + * 05Sep06 Sebastien Frippiat + * - check if (ALSA) seq exists before closing it in pm_linuxalsa_term() + * + * 05Sep06 Andreas Micheler and Cecilio + * - fixed memory leak by freeing someo objects in pm_winmm_term() + * - and another leak by freeing descriptors in Pm_Terminate() + * + * 23Aug06 RBD + * - various minor fixes + * + * 04Nov05 Olivier Tristan + * - changes to OS X to properly retrieve real device name on CoreMidi + * + * 19Jul05 Roger Dannenberg + * - included pmBufferMaxSize in Pm_GetErrorText() + * + * 23Mar05 Torgier Strand Henriksen + * - cleaner termination of porttime thread under Linux + * + * 15Nov04 Ben Allison + * - sysex output now uses one buffer/message and reallocates buffer + * - if needed + * - filters expanded for many message types and channels + * - detailed changes are as follows: + * ------------- in pmwinmm.c -------------- + * - new #define symbol: OUTPUT_BYTES_PER_BUFFER + * - change SYSEX_BYTES_PER_BUFFER to 1024 + * - added MIDIHDR_BUFFER_LENGTH(x) to correctly count midihdr buffer length + * - change MIDIHDR_SIZE(x) to (MIDIHDR_BUFFER_LENGTH(x) + sizeof(MIDIHDR)) + * - change allocate_buffer to use new MIDIHDR_BUFFER_LENGTH macro + * - new macros for MIDIHDR_SYSEX_SIZE and MIDIHDR_SYSEX_BUFFER_LENGTH + * - similar to above, but counts appropriately for sysex messages + * - added the following members to midiwinmm_struct for sysex data: + * - LPMIDIHDR *sysex_buffers; ** pool of buffers for sysex data ** + * - int num_sysex_buffers; ** how many sysex buffers ** + * - int next_sysex_buffer; ** index of next sysexbuffer to send ** + * - HANDLE sysex_buffer_signal; ** to wait for free sysex buffer ** + * - duplicated allocate_buffer, alocate_buffers and get_free_output_buffer + * - into equivalent sysex_buffer form + * - changed winmm_in_open to initialize new midiwinmm_struct members and + * - to use the new allocate_sysex_buffer() function instead of + * - allocate_buffer() + * - changed winmm_out_open to initialize new members, create sysex buffer + * - signal, and allocate 2 sysex buffers + * - changed winmm_out_delete to free sysex buffers and shut down the sysex + * - buffer signal + * - create new function resize_sysex_buffer which resizes m->hdr to the + * - passed size, and corrects the midiwinmm_struct accordingly. + * - changed winmm_write_byte to use new resize_sysex_buffer function, + * - if resize fails, write current buffer to output and continue + * - changed winmm_out_callback to use buffer_signal or sysex_buffer_signal + * - depending on which buffer was finished + * ------------- in portmidi.h -------------- + * - added pmBufferMaxSize to PmError to indicate that the buffer would be + * - too large for the underlying API + * - added additional filters + * - added prototype, documentation, and helper macro for Pm_SetChannelMask + * ------------- in portmidi.c -------------- + * - added pm_status_filtered() and pm_realtime_filtered() functions to + * separate filtering logic from buffer logic in pm_read_short + * - added Pm_SetChannelMask function + * - added pm_channel_filtered() function + * ------------- in pminternal.h -------------- + * - added member to PortMidiStream for channel mask + * + * 25May04 RBD + * - removed support for MIDI THRU + * - moved filtering from Pm_Read to pm_enqueue to avoid buffer ovfl + * - extensive work on Mac OS X port, especially sysex and error handling + * + * 18May04 RBD + * - removed side-effects from assert() calls. Now you can disable assert(). + * - no longer check pm_hosterror everywhere, fixing a bug where an open + * failure could cause a write not to work on a previously opened port + * until you call Pm_GetHostErrorText(). + * 16May04 RBD and Chris Roberts + * - Some documentation wordsmithing in portmidi.h + * - Dynamically allocate port descriptor structures + * - Fixed parameter error in midiInPrepareBuffer and midiInAddBuffer. + * + * 09Oct03 RBD + * - Changed Thru handling. Now the client does all the work and the client + * must poll or read to keep thru messages flowing. + * + * 31May03 RBD + * - Fixed various bugs. + * - Added linux ALSA support with help from Clemens Ladisch + * - Added Mac OS X support, implemented by Jon Parise, updated and + * integrated by Andrew Zeldis and Zico Kolter + * - Added latency program to build histogram of system latency using PortTime. + * + * 30Jun02 RBD Extensive rewrite of sysex handling. It works now. + * Extensive reworking of error reporting and error text -- no + * longer use dictionary call to delete data; instead, Pm_Open + * and Pm_Close clean up before returning an error code, and + * error text is saved in a system-independent location. + * Wrote sysex.c to test sysex message handling. + * + * 15Jun02 BCT changes: + * - Added pmHostError text handling. + * - For robustness, check PortMidi stream args not NULL. + * - Re-C-ANSI-fied code (changed many C++ comments to C style) + * - Reorganized code in pmwinmm according to input/output functionality (made + * cleanup handling easier to reason about) + * - Fixed Pm_Write calls (portmidi.h says these should not return length but Pm_Error) + * - Cleaned up memory handling (now system specific data deleted via dictionary + * call in PortMidi, allows client to query host errors). + * - Added explicit asserts to verify various aspects of pmwinmm implementation behaves as + * logic implies it should. Specifically: verified callback routines not reentrant and + * all verified status for all unchecked Win32 MMedia API calls perform successfully + * - Moved portmidi initialization and clean-up routines into DLL to fix Win32 MMedia API + * bug (i.e. if devices not explicitly closed, must reboot to debug application further). + * With this change, clients no longer need explicitly call Pm_Initialize, Pm_Terminate, or + * explicitly Pm_Close open devices when using WinMM version of PortMidi. + * + * 23Jan02 RBD Fixed bug in pmwinmm.c thru handling + * + * 21Jan02 RBD Added tests in Pm_OpenInput() and Pm_OpenOutput() to prevent + * opening an input as output and vice versa. + * Added comments and documentation. + * Implemented Pm_Terminate(). + * + */ diff --git a/portmidi/CMakeLists.txt b/portmidi/CMakeLists.txt new file mode 100644 index 0000000..0107e8c --- /dev/null +++ b/portmidi/CMakeLists.txt @@ -0,0 +1,188 @@ +# portmidi +# Roger B. Dannenberg (and others) +# Sep 2009 - 2021 + +cmake_minimum_required(VERSION 3.21) +# (ALSA::ALSA new in 3.12 and used in pm_common/CMakeLists.txt) +# Some Java stuff failed on 3.17 but works with 3.20+ + +cmake_policy(SET CMP0091 NEW) # enables MSVC_RUNTIME_LIBRARY target property + +# Previously, PortMidi versions were simply SVN commit version numbers. +# Versions are now in the form x.y.z +# Changed 1.0 to 2.0 because API is extended with virtual ports: +set(SOVERSION "2") +set(VERSION "2.0.4") + +project(portmidi VERSION "${VERSION}" + DESCRIPTION "Cross-Platform MIDI IO") + +set(LIBRARY_SOVERSION "${SOVERSION}") +set(LIBRARY_VERSION "${VERSION}") + +option(BUILD_SHARED_LIBS "Build shared libraries" ON) + +option(PM_USE_STATIC_RUNTIME + "Use MSVC static runtime. Only applies when BUILD_SHARED_LIBS is OFF" + ON) + +option(USE_SNDIO "Use sndio" OFF) + +# MSVCRT_DLL is used to construct the MSVC_RUNTIME_LIBRARY property +# (see pm_common/CMakeLists.txt and pm_test/CMakeLists.txt) +if(PM_USE_STATIC_RUNTIME AND NOT BUILD_SHARED_LIBS) + set(MSVCRT_DLL "") +else() + set(MSVCRT_DLL "DLL") +endif() + +# Always build with position-independent code (-fPIC) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9 CACHE STRING + "make for this OS version or higher") + +# PM_ACTUAL_LIB_NAME is in this scope -- see pm_common/CMakeLists.txt +# PM_NEEDED_LIBS is in this scope -- see pm_common/CMakeLists.txt + +include(GNUInstallDirs) + +# Build Types +# credit: http://cliutils.gitlab.io/modern-cmake/chapters/features.html +set(DEFAULT_BUILD_TYPE "Release") +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS + "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.") + set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE + STRING "Choose the type of build." FORCE) + # Set the possible values of build type for cmake-gui + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() + +# where to put libraries? Everything goes here in this directory +# (or Debug or Release, depending on the OS) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +option(BUILD_JAVA_NATIVE_INTERFACE + "build the Java PortMidi interface library" OFF) + +# Defines are used in both portmidi (in pm_common/) and pmjni (in pm_java), +# so define them here to be inherited by both libraries. +# +# PortMidi software architecture supports multiple system API's to lower- +# level MIDI drivers, e.g. PMNULL (no drivers), Jack (but not supported yet), +# and sndio (BSD, not supported yet). Interfaces are selected by defining, +# e.g., PMALSA. (In principle, we should require PMCOREMIDI (for macOS) +# and PMWINMM (for windows), but these are assumed. +# +if(APPLE OR WIN32) +else(APPLE_OR_WIN32) + set(LINUX_DEFINES "PMALSA" CACHE STRING "must define either PMALSA or PMNULL") + add_compile_definitions(${LINUX_DEFINES}) +endif(APPLE OR WIN32) + +if(BUILD_JAVA_NATIVE_INTERFACE) + message(WARNING + "Java API and PmDefaults program updated 2021, but support has " + "been discontinued. If you need/use this, let developers know.") + set(PMJNI_IF_EXISTS "pmjni") # used by INSTALL below +else(BUILD_JAVA_NATIVE_INTERFACE) + set(PMJNI_IF_EXISTS "") # used by INSTALL below +endif(BUILD_JAVA_NATIVE_INTERFACE) + + +# Something like this might help if you need to build for a specific cpu type: +# set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING +# "change to support other architectures" FORCE) + +include_directories(pm_common porttime) +add_subdirectory(pm_common) + +option(BUILD_PORTMIDI_TESTS + "Build test programs, including midi monitor (mm)" OFF) +if(BUILD_PORTMIDI_TESTS) + add_subdirectory(pm_test) +endif(BUILD_PORTMIDI_TESTS) + +# See note above about Java support (probably) discontinued +if(BUILD_JAVA_NATIVE_INTERFACE) + add_subdirectory(pm_java) +endif(BUILD_JAVA_NATIVE_INTERFACE) + +# Install the libraries and headers (Linux and Mac OS X command line) +INSTALL(TARGETS portmidi ${PMJNI_IF_EXISTS} + EXPORT PortMidiTargets + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + +INSTALL(FILES + pm_common/portmidi.h + pm_common/pmutil.h + porttime/porttime.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +# pkgconfig - generate pc file +# See https://cmake.org/cmake/help/latest/command/configure_file.html +if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(PKGCONFIG_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}") +else() + set(PKGCONFIG_INCLUDEDIR "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") +endif() +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PKGCONFIG_LIBDIR "${CMAKE_INSTALL_LIBDIR}") +else() + set(PKGCONFIG_LIBDIR "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}") +endif() +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/packaging/portmidi.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/packaging/portmidi.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/packaging/portmidi.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +# CMake config +set(PORTMIDI_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/PortMidi") +install( + EXPORT PortMidiTargets + FILE PortMidiTargets.cmake + NAMESPACE PortMidi:: + DESTINATION "${PORTMIDI_INSTALL_CMAKEDIR}" +) +include(CMakePackageConfigHelpers) +configure_package_config_file(packaging/PortMidiConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/packaging/PortMidiConfig.cmake" + INSTALL_DESTINATION "${PORTMIDI_INSTALL_CMAKEDIR}" +) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/packaging/PortMidiConfigVersion.cmake" + VERSION "${CMAKE_PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion +) +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/packaging/PortMidiConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/packaging/PortMidiConfigVersion.cmake" + DESTINATION "${PORTMIDI_INSTALL_CMAKEDIR}" +) + + + + +# Finding out what CMake is doing is really hard, e.g. COMPILE_FLAGS +# does not include COMPILE_OPTIONS or COMPILE_DEFINTIONS. Thus, the +# following report is probably not complete... +MESSAGE(STATUS "PortMidi Library name: " ${PM_ACTUAL_LIB_NAME}) +MESSAGE(STATUS "Build type: " ${CMAKE_BUILD_TYPE}) +MESSAGE(STATUS "Library Type: " ${LIB_TYPE}) +MESSAGE(STATUS "Compiler flags: " ${CMAKE_CXX_COMPILE_FLAGS}) +get_directory_property(prop COMPILE_DEFINITIONS) +MESSAGE(STATUS "Compile definitions: " ${prop}) +get_directory_property(prop COMPILE_OPTIONS) +MESSAGE(STATUS "Compile options: " ${prop}) +MESSAGE(STATUS "Compiler cxx debug flags: " ${CMAKE_CXX_FLAGS_DEBUG}) +MESSAGE(STATUS "Compiler cxx release flags: " ${CMAKE_CXX_FLAGS_RELEASE}) +MESSAGE(STATUS "Compiler cxx min size flags: " ${CMAKE_CXX_FLAGS_MINSIZEREL}) +MESSAGE(STATUS "Compiler cxx flags: " ${CMAKE_CXX_FLAGS}) + diff --git a/portmidi/Doxyfile b/portmidi/Doxyfile new file mode 100644 index 0000000..95e3708 --- /dev/null +++ b/portmidi/Doxyfile @@ -0,0 +1,2682 @@ +# Doxyfile 1.9.2 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = PortMidi + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "Cross-platform MIDI IO library" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = portmusic_logo.png + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = ../github-portmidi-portmidi_docs + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = YES + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = YES + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if <section_label> ... \endif and \cond <section_label> +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT =pm_common porttime/porttime.h pm_common/pmutil.h + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, +# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C +# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.l \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = TRUE, FALSE, PMEXPORT + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# <filter> <input-file> +# +# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = docs + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATOR_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html +# #tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use <access key> + S +# (what the <access key> is depends on the OS and browser, but it is typically +# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down +# key> to jump into the search results window, the results can be navigated +# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel +# the search. The filter options can be selected when the cursor is inside the +# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys> +# to select a filter and <Enter> or <escape> to activate or cancel the filter +# option. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using JavaScript. There +# are two flavors of web server based searching depending on the EXTERNAL_SEARCH +# setting. When disabled, doxygen will generate a PHP script for searching and +# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing +# and searching needs to be provided by external tools. See the section +# "External Indexing and Searching" for details. +# The default value is: NO. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SERVER_BASED_SEARCH = NO + +# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP +# script for searching. Instead the search results are written to an XML file +# which needs to be processed by an external indexer. Doxygen will invoke an +# external search engine pointed to by the SEARCHENGINE_URL option to obtain the +# search results. +# +# Doxygen ships with an example indexer (doxyindexer) and search engine +# (doxysearch.cgi) which are based on the open source search engine library +# Xapian (see: +# https://xapian.org/). +# +# See the section "External Indexing and Searching" for details. +# The default value is: NO. +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTERNAL_SEARCH = NO + +# The SEARCHENGINE_URL should point to a search engine hosted by a web server +# which will return the search results when EXTERNAL_SEARCH is enabled. +# +# Doxygen ships with an example indexer (doxyindexer) and search engine +# (doxysearch.cgi) which are based on the open source search engine library +# Xapian (see: +# https://xapian.org/). See the section "External Indexing and Searching" for +# details. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SEARCHENGINE_URL = + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed +# search data is written to a file for indexing by an external tool. With the +# SEARCHDATA_FILE tag the name of this file can be specified. +# The default file is: searchdata.xml. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SEARCHDATA_FILE = searchdata.xml + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the +# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is +# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple +# projects and redirect the results back to the right project. +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTERNAL_SEARCH_ID = + +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# projects other than the one defined by this configuration file, but that are +# all added to the same external search index. Each project needs to have a +# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of +# to a relative location where the documentation can be found. The format is: +# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. +# The default value is: YES. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: latex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. +# +# Note that when not enabling USE_PDFLATEX the default is latex when enabling +# USE_PDFLATEX the default is pdflatex and when in the later case latex is +# chosen this is overwritten by pdflatex. For specific output languages the +# default can have been set differently, this depends on the implementation of +# the output language. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_CMD_NAME = + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate +# index for LaTeX. +# Note: This tag is used in the Makefile / make.bat. +# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file +# (.tex). +# The default file is: makeindex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +MAKEINDEX_CMD_NAME = makeindex + +# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to +# generate index for LaTeX. In case there is no backslash (\) as first character +# it will be automatically added in the LaTeX code. +# Note: This tag is used in the generated output file (.tex). +# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. +# The default value is: makeindex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_MAKEINDEX_CMD = makeindex + +# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX +# documents. This may be useful for small projects and may help to save some +# trees in general. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used by the +# printer. +# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x +# 14 inches) and executive (7.25 x 10.5 inches). +# The default value is: a4. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names +# that should be included in the LaTeX output. The package can be specified just +# by its name or with the correct syntax as to be used with the LaTeX +# \usepackage command. To get the times font for instance you can specify : +# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} +# To use the option intlimits with the amsmath package you can specify: +# EXTRA_PACKAGES=[intlimits]{amsmath} +# If left blank no extra packages will be included. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for +# the generated LaTeX document. The header should contain everything until the +# first chapter. If it is left blank doxygen will generate a standard header. It +# is highly recommended to start with a default header using +# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty +# and then modify the file new_header.tex. See also section "Doxygen usage" for +# information on how to generate the default header that doxygen normally uses. +# +# Note: Only use a user-defined header if you know what you are doing! +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. The following +# commands have a special meaning inside the header (and footer): For a +# description of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for +# the generated LaTeX document. The footer should contain everything after the +# last chapter. If it is left blank doxygen will generate a standard footer. See +# LATEX_HEADER for more information on how to generate a default footer and what +# special commands can be used inside the footer. See also section "Doxygen +# usage" for information on how to generate the default footer that doxygen +# normally uses. Note: Only use a user-defined footer if you know what you are +# doing! +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_FOOTER = + +# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# LaTeX style sheets that are included after the standard style sheets created +# by doxygen. Using this option one can overrule certain style aspects. Doxygen +# will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EXTRA_STYLESHEET = + +# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the LATEX_OUTPUT output +# directory. Note that the files will be copied as-is; there are no commands or +# markers available. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EXTRA_FILES = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is +# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will +# contain links (just like the HTML output) instead of page references. This +# makes the output suitable for online browsing using a PDF viewer. +# The default value is: YES. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. +# The default value is: YES. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode +# command to the generated LaTeX files. This will instruct LaTeX to keep running +# if errors occur, instead of asking the user for help. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_BATCHMODE = NO + +# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the +# index chapters (such as File Index, Compound Index, etc.) in the output. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_HIDE_INDICES = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. See +# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. +# The default value is: plain. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_BIB_STYLE = plain + +# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_TIMESTAMP = NO + +# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) +# path from which the emoji images will be read. If a relative path is entered, +# it will be relative to the LATEX_OUTPUT directory. If left blank the +# LATEX_OUTPUT directory will be used. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EMOJI_DIRECTORY = + +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The +# RTF output is optimized for Word 97 and may not look too pretty with other RTF +# readers/editors. +# The default value is: NO. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: rtf. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF +# documents. This may be useful for small projects and may help to save some +# trees in general. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will +# contain hyperlink fields. The RTF file will contain links (just like the HTML +# output) instead of page references. This makes the output suitable for online +# browsing using Word or some other Word compatible readers that support those +# fields. +# +# Note: WordPad (write) and others do not support links. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# configuration file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. +# +# See also section "Doxygen usage" for information on how to generate the +# default style sheet that doxygen normally uses. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an RTF document. Syntax is +# similar to doxygen's configuration file. A template extensions file can be +# generated using doxygen -e rtf extensionFile. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for +# classes and files. +# The default value is: NO. + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. A directory man3 will be created inside the directory specified by +# MAN_OUTPUT. +# The default directory is: man. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to the generated +# man pages. In case the manual section does not start with a number, the number +# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is +# optional. +# The default value is: .3. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_EXTENSION = .3 + +# The MAN_SUBDIR tag determines the name of the directory created within +# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by +# MAN_EXTENSION with the initial . removed. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_SUBDIR = + +# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it +# will generate one additional man file for each entity documented in the real +# man page(s). These additional files only source the real man page, but without +# them the man command would be unable to find the correct page. +# The default value is: NO. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that +# captures the structure of the code including all documentation. +# The default value is: NO. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: xml. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_OUTPUT = xml + +# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program +# listings (including syntax highlighting and cross-referencing information) to +# the XML output. Note that enabling this will significantly increase the size +# of the XML output. +# The default value is: YES. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_PROGRAMLISTING = YES + +# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include +# namespace members in file scope as well, matching the HTML output. +# The default value is: NO. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_NS_MEMB_FILE_SCOPE = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- + +# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files +# that can be used to generate PDF. +# The default value is: NO. + +GENERATE_DOCBOOK = NO + +# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in +# front of it. +# The default directory is: docbook. +# This tag requires that the tag GENERATE_DOCBOOK is set to YES. + +DOCBOOK_OUTPUT = docbook + +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an +# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# the structure of the code including all documentation. Note that this feature +# is still experimental and incomplete at the moment. +# The default value is: NO. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module +# file that captures the structure of the code including all documentation. +# +# Note that this feature is still experimental and incomplete at the moment. +# The default value is: NO. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary +# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI +# output from the Perl module output. +# The default value is: NO. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely +# formatted so it can be parsed by a human reader. This is useful if you want to +# understand what is going on. On the other hand, if this tag is set to NO, the +# size of the Perl module output will be much smaller and Perl will parse it +# just the same. +# The default value is: YES. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file are +# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful +# so different doxyrules.make files included by the same Makefile don't +# overwrite each other's variables. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all +# C-preprocessor directives found in the sources and include files. +# The default value is: YES. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# in the source code. If set to NO, only conditional compilation will be +# performed. Macro expansion can be done in a controlled way by setting +# EXPAND_ONLY_PREDEF to YES. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then +# the macro expansion is limited to the macros specified with the PREDEFINED and +# EXPAND_AS_DEFINED tags. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES, the include files in the +# INCLUDE_PATH will be searched if a #include is found. +# The default value is: YES. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by the +# preprocessor. +# This tag requires that the tag SEARCH_INCLUDES is set to YES. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will be +# used. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that are +# defined before the preprocessor is started (similar to the -D option of e.g. +# gcc). The argument of the tag is a list of macros of the form: name or +# name=definition (no spaces). If the definition and the "=" are omitted, "=1" +# is assumed. To prevent a macro definition from being undefined via #undef or +# recursively expanded use the := operator instead of the = operator. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this +# tag can be used to specify a list of macro names that should be expanded. The +# macro definition that is found in the sources will be used. Use the PREDEFINED +# tag if you want to use a different macro definition that overrules the +# definition found in the source code. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will +# remove all references to function-like macros that are alone on a line, have +# an all uppercase name, and do not end with a semicolon. Such function macros +# are typically used for boiler-plate code, and will confuse the parser if not +# removed. +# The default value is: YES. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES tag can be used to specify one or more tag files. For each tag +# file the location of the external documentation should be added. The format of +# a tag file without this location is as follows: +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where loc1 and loc2 can be relative or absolute paths or URLs. See the +# section "Linking to external documentation" for more information about the use +# of tag files. +# Note: Each tag file must have a unique name (where the name does NOT include +# the path). If a tag file is not located in the directory in which doxygen is +# run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create a +# tag file that is based on the input files it reads. See section "Linking to +# external documentation" for more information about the usage of tag files. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES, all external class will be listed in +# the class index. If set to NO, only the inherited external classes will be +# listed. +# The default value is: NO. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will be +# listed. +# The default value is: YES. + +EXTERNAL_GROUPS = YES + +# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in +# the related pages index. If set to NO, only the current project's pages will +# be listed. +# The default value is: YES. + +EXTERNAL_PAGES = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram +# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to +# NO turns the diagrams off. Note that this option also works with HAVE_DOT +# disabled, but it is recommended to install and use dot, since it yields more +# powerful graphs. +# The default value is: YES. + +CLASS_DIAGRAMS = NO + +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. + +DIA_PATH = + +# If set to YES the inheritance and collaboration graphs will hide inheritance +# and usage relations if the target is undocumented or is not a class. +# The default value is: YES. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz (see: +# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# Bell Labs. The other options in this section have no effect if this option is +# set to NO +# The default value is: NO. + +HAVE_DOT = NO + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed +# to run in parallel. When set to 0 doxygen will base this on the number of +# processors available in the system. You can set it explicitly to a value +# larger than 0 to get control over the balance between CPU load and processing +# speed. +# Minimum value: 0, maximum value: 32, default value: 0. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_NUM_THREADS = 0 + +# When you want a differently looking font in the dot files that doxygen +# generates you can specify the font name using DOT_FONTNAME. You need to make +# sure dot is able to find the font, which can be done by putting it in a +# standard location or by setting the DOTFONTPATH environment variable or by +# setting DOT_FONTPATH to the directory containing the font. +# The default value is: Helvetica. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of +# dot graphs. +# Minimum value: 4, maximum value: 24, default value: 10. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the default font as specified with +# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set +# the path where dot can find it using this tag. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTPATH = + +# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for +# each documented class showing the direct and indirect inheritance relations. +# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a +# graph for each documented class showing the direct and indirect implementation +# dependencies (inheritance, containment, and class references variables) of the +# class with other documented classes. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for +# groups, showing the direct groups dependencies. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside the +# class node. If there are many fields or methods and many nodes the graph may +# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the +# number of items for each type to make the size more manageable. Set this to 0 +# for no limit. Note that the threshold may be exceeded by 50% before the limit +# is enforced. So when you set the threshold to 10, up to 15 fields may appear, +# but if the number exceeds 15, the total amount of fields shown is limited to +# 10. +# Minimum value: 0, maximum value: 100, default value: 10. +# This tag requires that the tag UML_LOOK is set to YES. + +UML_LIMIT_NUM_FIELDS = 10 + +# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and +# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS +# tag is set to YES, doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen +# will not generate fields with class member information in the UML graphs. The +# class diagrams will look similar to the default class diagrams but using UML +# notation for the relationships. +# Possible values are: NO, YES and NONE. +# The default value is: NO. +# This tag requires that the tag UML_LOOK is set to YES. + +DOT_UML_DETAILS = NO + +# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters +# to display on a single line. If the actual line length exceeds this threshold +# significantly it will wrapped across multiple lines. Some heuristics are apply +# to avoid ugly line breaks. +# Minimum value: 0, maximum value: 1000, default value: 17. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_WRAP_THRESHOLD = 17 + +# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and +# collaboration graphs will show the relations between templates and their +# instances. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +TEMPLATE_RELATIONS = NO + +# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to +# YES then doxygen will generate a graph for each documented file showing the +# direct and indirect include dependencies of the file with other documented +# files. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +INCLUDE_GRAPH = YES + +# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are +# set to YES then doxygen will generate a graph for each documented file showing +# the direct and indirect include dependencies of the file with other documented +# files. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH tag is set to YES then doxygen will generate a call +# dependency graph for every global function or class method. +# +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable call graphs for selected +# functions only using the \callgraph command. Disabling a call graph can be +# accomplished by means of the command \hidecallgraph. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller +# dependency graph for every global function or class method. +# +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable caller graphs for selected +# functions only using the \callergraph command. Disabling a caller graph can be +# accomplished by means of the command \hidecallergraph. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical +# hierarchy of all classes instead of a textual one. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the +# dependencies a directory has on other directories in a graphical way. The +# dependency relations are determined by the #include relations between the +# files in the directories. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. For an explanation of the image formats see the section +# output formats in the documentation of the dot tool (Graphviz (see: +# http://www.graphviz.org/)). +# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order +# to make the SVG files visible in IE 9+ (other browsers do not have this +# requirement). +# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, +# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and +# png:gdiplus:gdiplus. +# The default value is: png. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# +# Note that this requires a modern browser other than Internet Explorer. Tested +# and working are Firefox, Chrome, Safari, and Opera. +# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make +# the SVG files visible. Older versions of IE do not have SVG support. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +INTERACTIVE_SVG = NO + +# The DOT_PATH tag can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the \dotfile +# command). +# This tag requires that the tag HAVE_DOT is set to YES. + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = + +# The DIAFILE_DIRS tag can be used to specify one or more directories that +# contain dia files that are included in the documentation (see the \diafile +# command). + +DIAFILE_DIRS = + +# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the +# path where java can find the plantuml.jar file. If left blank, it is assumed +# PlantUML is not used or called during a preprocessing step. Doxygen will +# generate a warning when it encounters a \startuml command in this case and +# will not generate output for the diagram. + +PLANTUML_JAR_PATH = + +# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a +# configuration file for plantuml. + +PLANTUML_CFG_FILE = + +# When using plantuml, the specified paths are searched for files specified by +# the !include statement in a plantuml block. + +PLANTUML_INCLUDE_PATH = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes +# that will be shown in the graph. If the number of nodes in a graph becomes +# larger than this value, doxygen will truncate the graph, which is visualized +# by representing a node as a red box. Note that doxygen if the number of direct +# children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that +# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. +# Minimum value: 0, maximum value: 10000, default value: 50. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs +# generated by dot. A depth value of 3 means that only nodes reachable from the +# root by following a path via at most 3 edges will be shown. Nodes that lay +# further from the root node will be omitted. Note that setting this option to 1 +# or 2 may greatly reduce the computation time needed for large code bases. Also +# note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. +# Minimum value: 0, maximum value: 1000, default value: 0. +# This tag requires that the tag HAVE_DOT is set to YES. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not seem +# to support this out of the box. +# +# Warning: Depending on the platform used, enabling this option may lead to +# badly anti-aliased labels on the edges of a graph (i.e. they become hard to +# read). +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) support +# this, this feature is disabled by default. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page +# explaining the meaning of the various boxes and arrows in the dot generated +# graphs. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate +# files that are used to generate the various graphs. +# +# Note: This setting is not only used for dot files but also for msc temporary +# files. +# The default value is: YES. + +DOT_CLEANUP = YES diff --git a/portmidi/README.md b/portmidi/README.md new file mode 100644 index 0000000..3f0463c --- /dev/null +++ b/portmidi/README.md @@ -0,0 +1,128 @@ +# PortMidi - Cross-Platform MIDI IO + +This is the canonical release of PortMidi. + +See other repositories within [PortMidi](https://github.com/PortMidi) +for related code and bindings (although currently, not much is here). + +## [Full C API documentation is here.](https://portmidi.github.io/portmidi_docs/) + +## Compiling and Using PortMidi + +Use CMake (or ccmake) to create a Makefile for Linux/BSD or a +project file for Xcode or MS Visual Studio. Use make or an IDE to compile: +``` +sudo apt install libasound2-dev # on Linux, you need ALSA +cd portmidi # start in the top-level portmidi directory +ccmake . # set any options interactively, type c to configure + # type g to generate a Makefile or IDE project + # type q to quit + # (alternatively, run the CMake GUI and use + # Configure and Generate buttons to build IDE project) +make # compile sources and build PortMidi library + # (alternatively, open project file with your IDE) +sudo make install # if you want to install to your system +``` + +## Installation + +My advice is to build PortMidi and statically link it to your +application. This gives you more control over versions. However, +installing PortMidi to your system is preferred by some, and the +following should do it: +``` +cmake . +make +sudo make install +``` + +## Language Bindings + +Here is a guide to some other projects using PortMidi. There is not +much coordination, so let us know if there are better or alternative +bindings for these and other languages: + +- Python: Various libraries and packages exist; search and ye shall + find. If you wouldn't like to do research, check out [mido](https://mido.readthedocs.io/en/stable/) +- [SML](https://github.com/jh-midi/portmidi-sml2) +- [OCaml](https://ocaml.org/p/portmidi/0.1) +- [Haskell](https://hackage.haskell.org/package/PortMidi) +- [Erlang](https://hexdocs.pm/portmidi/PortMidi.html) +- [Julia](https://github.com/SteffenPL/PortMidi.jl) +- [C#](https://github.com/net-core-audio/portmidi) +- [Rust](https://musitdev.github.io/portmidi-rs/) +- [Go](https://github.com/rakyll/portmidi) +- [Odin](https://pkg.odin-lang.org/vendor/portmidi/) +- [Serpent](https://sourceforge.net/projects/serpent/) - a real-time + Python-like language has PortMidi built-in, a MIDI-timestamp-aware + scheduler, and GUI support for device selection. +- [Pd (Pure Data)](https://puredata.info/) uses PortMidi. + + +## What's New? + +(Not so new, but significant:) Support for the **PmDefaults** program, +which enabled a graphical interface to select default MIDI devices, +has been removed for lack of interest. This allowed us to also remove +C code to read and parse Java preference files on various systems, +simplifying the library. **PmDefaults** allowed simple command-line +programs to use `Pm_DefaultInputDeviceID()` and +`Pm_DefaultOutputDeviceID()` rather than creating device selection +interfaces. Now, you should either pass devices on the command line or +create your own selection interface when building a GUI +application. (See pm_tests for examples.) `Pm_DefaultInputDeviceID()` +and `Pm_DefaultOutputDeviceID()` now return a valid device if +possible, but they may not actually reflect any user preference. + +Haiku support in a minimal implementation. See TODO's in source. + +sndio is also minimally supported, allowing basic PortMidi functions +in OpenBSD, FreeBSD, and NetBSD by setting USE_SNDIO for CMake, but +not delayed/timestamped output and virtual devices. + +# Other Repositories + +PortMidi used to be part of the PortMedia suite, but this repo has +been reduced to mostly just C/C++ code for PortMidi. You will find +some other repositories in this PortMidi project set up for language +bindings (volunteers and contributors are invited!). Other code +removed from previous releases of PortMedia include: + +## PortSMF + +A Standard MIDI File (SMF) (and more) library is in the [portsmf +repository](https://github.com/rbdannenberg/portsmf). + +PortSMF is a library for reading/writing/editing Standard MIDI +Files. It is actually much more, with a general representation of +events and updates with properties consisting of attributes and typed +values. Familiar properties of pitch, time, duration, and channel are +built into events and updates to make them faster to access and more +compact. + +To my knowledge, PortSMF has the most complete and useful handling of +MIDI tempo tracks. E.g., you can edit notes according to either beat +or time, and you can edit tempo tracks, for example, flattening the +tempo while preserving the beat alignment, preserving the real time +while changing the tempo or stretching the tempo over some interval. + +In addition to Standard MIDI Files, PortSMF supports an ASCII +representation called Allegro. PortSMF and Allegro are used for +Audacity Note Tracks. + +## scorealign + +Scorealign used to be part of the PortMedia suite. It is now at the +[scorealign repository](https://github.com/rbdannenberg/scorealign). + +Scorealign aligns audio-to-audio, audio-to-MIDI or MIDI-to-MIDI using +dynamic time warping (DTW) of a computed chromagram +representation. There are some added smoothing tricks to improve +performance. This library is written in C and runs substantially +faster than most other implementations, especially those written in +MATLAB, due to the core DTW algorithm. Users should be warned that +while chromagrams are robust features for alignment, they achieve +robustness by operating at fairly high granularity, e.g., durations of +around 100ms, which limits time precision. Other more recent +algorithms can doubtless do better, but be cautious of claims, since +it all depends on what assumptions you can make about the music. diff --git a/portmidi/README.txt b/portmidi/README.txt new file mode 100755 index 0000000..e09aa2e --- /dev/null +++ b/portmidi/README.txt @@ -0,0 +1,88 @@ +README for PortMidi
+
+Roger B. Dannenberg
+
+Documentation for PortMidi is found in pm_common/portmidi.h.
+Documentation in HTML is available at portmidi.github.io/portmidi_docs/
+
+Additional documentation:
+ - README.md (overview, how to build, what's new)
+ - Windows: see pm_win/README_WIN.txt and pm_win/debugging_dlls.txt
+ - Linux: see pm_linux/README_LINUX.txt
+ - Mac OSX: see pm_mac/README_MAC.txt
+ - Other Languages: look for other repos at github.com/PortMidi,
+ and search README.md for pointers to other projects.
+
+---------- some notes on the design of PortMidi ----------
+
+POINTERS VS DEVICE NUMBERS
+
+When you open a MIDI port, PortMidi allocates a structure to
+maintain the state of the open device. Since every device is
+also listed in a table, you might think it would be simpler to
+use the table index rather than a pointer to identify a device.
+This would also help with error checking (it's hard to make
+sure a pointer is valid). PortMidi's design parallels that of
+PortAudio.
+
+ERROR HANDLING
+
+Error handling turned out to be much more complicated than expected.
+PortMidi functions return error codes that the caller can check.
+In addition, errors may occur asynchronously due to MIDI input.
+However, for Windows, there are virtually no errors that can
+occur if the code is correct and not passing bogus values. One
+exception is an error that the system is out of memory, but my
+guess is that one is unlikely to recover gracefully from that.
+Therefore, all errors in callbacks are guarded by assert(), which
+means not guarded at all in release configurations.
+
+Ordinarily, the caller checks for an error code. If the error is
+system-dependent, pmHostError is returned and the caller can
+call Pm_GetHostErrorText to get a text description of the error.
+
+Host error codes are system-specific and are recorded in the
+system-specific data allocated for each open MIDI port.
+However, if an error occurs on open or close,
+we cannot store the error with the device because there will be
+no device data (assuming PortMidi cleans up after devices that
+are not open). For open and close, we will convert the error
+to text, copy it to a global string, and set pm_hosterror, a
+global flag.
+
+Similarly, whenever a Read or Write operation returns pmHostError,
+the corresponding error string is copied to a global string
+and pm_hosterror is set. This makes getting error strings
+simple and uniform, although it does cost a string copy and some
+overhead even if the user does not want to look at the error data.
+
+The system-specific Read, Write, Poll, etc. implementations should
+check for asynchronous errors and return immediately if one is
+found so that these get reported. This happens in the Mac OS X
+code, where lots of things are happening in callbacks, but again,
+in Windows, there are no error codes recorded in callbacks.
+
+DEBUGGING
+
+If you are building a console application for research, we suggest
+compiling with the option PM_CHECK_ERRORS. This will insert a
+check for error return values at the end of each PortMidi
+function. If an error is encountered, a text message is printed
+using printf(), the user is asked to type ENTER, and then exit(-1)
+is called to clean up and terminate the program.
+
+You should not use PM_CHECK_ERRORS if printf() does not work
+(e.g. this is not a console application under Windows, or there
+is no visible console on some other OS), and you should not use
+PM_CHECK_ERRORS if you intend to recover from errors rather than
+abruptly terminate the program.
+
+The Windows version (and perhaps others) also offers a DEBUG
+compile-time option. See README_WIN.txt.
+
+RELEASE
+
+To make a new release, update VERSION variable in CMakeLists.txt.
+
+Update CHANGELOG.txt. What's new?
+
diff --git a/portmidi/license.txt b/portmidi/license.txt new file mode 100644 index 0000000..c757b37 --- /dev/null +++ b/portmidi/license.txt @@ -0,0 +1,40 @@ +/* + * PortMidi Portable Real-Time MIDI Library + * + * license.txt -- a copy of the PortMidi copyright notice and license information + * + * Latest version available at: http://sourceforge.net/projects/portmedia + * + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * Copyright (c) 2001-2009 Roger B. Dannenberg + * + * 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. + * + * 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 text above constitutes the entire PortMidi license; however, + * the PortMusic community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ diff --git a/portmidi/packaging/PortMidiConfig.cmake.in b/portmidi/packaging/PortMidiConfig.cmake.in new file mode 100644 index 0000000..0d24f4d --- /dev/null +++ b/portmidi/packaging/PortMidiConfig.cmake.in @@ -0,0 +1,15 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +if(UNIX AND NOT APPLE AND NOT HAIKU AND (@LINUX_DEFINES@ MATCHES ".*PMALSA.*")) + find_dependency(ALSA) +endif() + +if(NOT WIN32) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/PortMidiTargets.cmake") + +check_required_components(PortMidi) diff --git a/portmidi/packaging/portmidi.pc.in b/portmidi/packaging/portmidi.pc.in new file mode 100644 index 0000000..e9d929c --- /dev/null +++ b/portmidi/packaging/portmidi.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=@PKGCONFIG_LIBDIR@ +includedir=@PKGCONFIG_INCLUDEDIR@ + +Name: @CMAKE_PROJECT_NAME@ +Description: @CMAKE_PROJECT_DESCRIPTION@ +Version: @CMAKE_PROJECT_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@CMAKE_PROJECT_NAME@ +Requires.private: @PKGCONFIG_REQUIRES_PRIVATE@ diff --git a/portmidi/pm_common/CMakeLists.txt b/portmidi/pm_common/CMakeLists.txt new file mode 100644 index 0000000..1ad54ad --- /dev/null +++ b/portmidi/pm_common/CMakeLists.txt @@ -0,0 +1,167 @@ +# pm_common/CMakeLists.txt -- how to build portmidi library + +# creates the portmidi library +# exports PM_NEEDED_LIBS to parent. It seems that PM_NEEDED_LIBS for +# Linux should include Thread::Thread and ALSA::ALSA, but these +# are not visible in other CMake files, even though the portmidi +# target is. Therefore, Thread::Thread is replaced by +# CMAKE_THREAD_LIBS_INIT and ALSA::ALSA is replaced by ALSA_LIBRARIES. +# Is there a better way to do this? Maybe this whole file should be +# at the parent level. + +# Support alternative name for static libraries to avoid confusion. +# (In particular, Xcode has automatically converted portmidi.a to +# portmidi.dylib without warning, so using portmidi-static.a eliminates +# this possibility, but default for all libs is "portmidi"): +set(PM_STATIC_LIB_NAME "portmidi" CACHE STRING + "For static builds, the PortMidi library name, e.g. portmidi-static. + Default is portmidi") +set(PM_ACTUAL_LIB_NAME "portmidi") +if(NOT BUILD_SHARED_LIBS) + set(PM_ACTUAL_LIB_NAME ${PM_STATIC_LIB_NAME}) +endif() + +# set the build directory for libportmidi.a to be in portmidi, not in +# portmidi/pm_common. Must be done here BEFORE add_library below. +if(APPLE OR WIN32) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + # set the build directory for .dylib libraries + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +endif(APPLE OR WIN32) + +# we need full paths to sources because they are shared with other targets +# (in particular pmjni). Set PMDIR to the top-level portmidi directory: +get_filename_component(PMDIR ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY) +set(PM_LIB_PUBLIC_SRC ${PMDIR}/pm_common/portmidi.c + ${PMDIR}/pm_common/pmutil.c + ${PMDIR}/porttime/porttime.c) +add_library(portmidi ${PM_LIB_PUBLIC_SRC}) + +# MSVCRT_DLL is "DLL" for shared runtime library, and "" for static: +set_target_properties(portmidi PROPERTIES + VERSION ${LIBRARY_VERSION} + SOVERSION ${LIBRARY_SOVERSION} + OUTPUT_NAME "${PM_ACTUAL_LIB_NAME}" + MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$<CONFIG:Debug>:Debug>${MSVCRT_DLL}" + WINDOWS_EXPORT_ALL_SYMBOLS TRUE) +target_include_directories(portmidi PUBLIC + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> + $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>) + + +option(PM_CHECK_ERRORS +"Insert a check for error return values at the end of each PortMidi function. +If an error is encountered, a text message is printed using printf(), the user +is asked to type ENTER, and then exit(-1) is called to clean up and terminate +the program. + +You should not use PM_CHECK_ERRORS if printf() does not work (e.g. this is not +a console application under Windows, or there is no visible console on some +other OS), and you should not use PM_CHECK_ERRORS if you intend to recover +from errors rather than abruptly terminate the program." OFF) +if(PM_CHECK_ERRORS) + target_compile_definitions(portmidi PRIVATE PM_CHECK_ERRORS) +endif(PM_CHECK_ERRORS) + +macro(prepend_path RESULT PATH) + set(${RESULT}) + foreach(FILE ${ARGN}) + list(APPEND ${RESULT} "${PATH}${FILE}") + endforeach(FILE) +endmacro(prepend_path) + +# UNIX needs pthread library +if(NOT WIN32) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) +endif() + +# Check for sndio +if(USE_SNDIO) + include (FindPackageHandleStandardArgs) + find_path(SNDIO_INCLUDE_DIRS NAMES sndio.h) + find_library(SNDIO_LIBRARY sndio) + find_package_handle_standard_args(Sndio + REQUIRED_VARS SNDIO_LIBRARY SNDIO_INCLUDE_DIRS) +endif(USE_SNDIO) + +# first include the appropriate system-dependent file: +if(SNDIO_FOUND AND USE_SNDIO) + set(PM_LIB_PRIVATE_SRC + ${PMDIR}/porttime/ptlinux.c + ${PMDIR}/pm_sndio/pmsndio.c) + set(PM_NEEDED_LIBS Threads::Threads ${SNDIO_LIBRARY} PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE Threads::Threads ${SNDIO_LIBRARY}) + target_include_directories(portmidi PRIVATE ${SNDIO_INCLUDE_DIRS}) +elseif(UNIX AND APPLE) + set(Threads::Threads "" PARENT_SCOPE) + set(PM_LIB_PRIVATE_SRC + ${PMDIR}/porttime/ptmacosx_mach.c + ${PMDIR}/pm_mac/pmmac.c + ${PMDIR}/pm_mac/pmmacosxcm.c) + set(PM_NEEDED_LIBS + ${CMAKE_THREAD_LIBS_INIT} + -Wl,-framework,CoreAudio + -Wl,-framework,CoreFoundation + -Wl,-framework,CoreMidi + -Wl,-framework,CoreServices + PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE + Threads::Threads + -Wl,-framework,CoreAudio + -Wl,-framework,CoreFoundation + -Wl,-framework,CoreMidi + -Wl,-framework,CoreServices + ) + # set to CMake default; is this right?: + set_target_properties(portmidi PROPERTIES MACOSX_RPATH ON) +elseif(HAIKU) + set(PM_LIB_PRIVATE_SRC + ${PMDIR}/porttime/pthaiku.cpp + ${PMDIR}/pm_haiku/pmhaiku.cpp) + set(PM_NEEDED_LIBS be midi midi2 PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE be midi midi2) +elseif(UNIX) + target_compile_definitions(portmidi PRIVATE ${LINUX_FLAGS}) + set(PM_LIB_PRIVATE_SRC + ${PMDIR}/porttime/ptlinux.c + ${PMDIR}/pm_linux/pmlinux.c + ${PMDIR}/pm_linux/pmlinuxnull.c) + if(${LINUX_DEFINES} MATCHES ".*PMALSA.*") + # Note that ALSA is not required if PMNULL is defined -- PortMidi will then + # compile without ALSA and report no MIDI devices. Later, PMSNDIO or PMJACK + # might be additional options. + find_package(ALSA REQUIRED) + list(APPEND PM_LIB_PRIVATE_SRC ${PMDIR}/pm_linux/pmlinuxalsa.c) + set(PM_NEEDED_LIBS ${CMAKE_THREAD_LIBS_INIT} ${ALSA_LIBRARIES} PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE Threads::Threads ALSA::ALSA) + set(PKGCONFIG_REQUIRES_PRIVATE "alsa" PARENT_SCOPE) + else() + message(WARNING "No PMALSA, so PortMidi will not use ALSA, " + "and will not find or open MIDI devices.") + set(PM_NEEDED_LIBS ${CMAKE_THREAD_LIBS_INIT} PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE Threads::Threads) + endif() +elseif(WIN32) + set(PM_LIB_PRIVATE_SRC + ${PMDIR}/porttime/ptwinmm.c + ${PMDIR}/pm_win/pmwin.c + ${PMDIR}/pm_win/pmwinmm.c) + set(PM_NEEDED_LIBS winmm PARENT_SCOPE) + target_link_libraries(portmidi PRIVATE winmm) +# if(NOT BUILD_SHARED_LIBS AND PM_USE_STATIC_RUNTIME) + # /MDd is multithread debug DLL, /MTd is multithread debug + # /MD is multithread DLL, /MT is multithread. Change to static: +# include(../pm_win/static.cmake) +# endif() +else() + message(FATAL_ERROR "Operating system not supported.") +endif() + +set(PM_LIB_PUBLIC_SRC ${PM_LIB_PUBLIC_SRC} PARENT_SCOPE) # export to parent +set(PM_LIB_PRIVATE_SRC ${PM_LIB_PRIVATE_SRC} PARENT_SCOPE) # export to parent + +target_sources(portmidi PRIVATE ${PM_LIB_PRIVATE_SRC}) + diff --git a/portmidi/pm_common/pminternal.h b/portmidi/pm_common/pminternal.h new file mode 100755 index 0000000..8b3d8f5 --- /dev/null +++ b/portmidi/pm_common/pminternal.h @@ -0,0 +1,190 @@ +/** @file pminternal.h header for PortMidi implementations */ + +/* this file is included by files that implement library internals */ +/* Here is a guide to implementers: + provide an initialization function similar to pm_winmm_init() + add your initialization function to pm_init() + Note that your init function should never require not-standard + libraries or fail in any way. If the interface is not available, + simply do not call pm_add_device. This means that non-standard + libraries should try to do dynamic linking at runtime using a DLL + and return without error if the DLL cannot be found or if there + is any other failure. + implement functions as indicated in pm_fns_type to open, read, write, + close, etc. + call pm_add_device() for each input and output device, passing it a + pm_fns_type structure. + assumptions about pm_fns_type functions are given below. + */ + +/** @cond INTERNAL - add INTERNAL to Doxygen ENABLED_SECTIONS to include */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern int pm_initialized; /* see note in portmidi.c */ +extern PmDeviceID pm_default_input_device_id; +extern PmDeviceID pm_default_output_device_id; + +/* these are defined in system-specific file */ +void *pm_alloc(size_t s); +void pm_free(void *ptr); + +/* if a host error (an error reported by the host MIDI API that is not + * mapped to a PortMidi error code) occurs in a synchronous operation + * (i.e., not in a callback from another thread) set these: */ +extern int pm_hosterror; /* boolean */ +extern char pm_hosterror_text[PM_HOST_ERROR_MSG_LEN]; + +struct pm_internal_struct; + +/* these do not use PmInternal because it is not defined yet... */ +typedef PmError (*pm_write_short_fn)(struct pm_internal_struct *midi, + PmEvent *buffer); +typedef PmError (*pm_begin_sysex_fn)(struct pm_internal_struct *midi, + PmTimestamp timestamp); +typedef PmError (*pm_end_sysex_fn)(struct pm_internal_struct *midi, + PmTimestamp timestamp); +typedef PmError (*pm_write_byte_fn)(struct pm_internal_struct *midi, + unsigned char byte, PmTimestamp timestamp); +typedef PmError (*pm_write_realtime_fn)(struct pm_internal_struct *midi, + PmEvent *buffer); +typedef PmError (*pm_write_flush_fn)(struct pm_internal_struct *midi, + PmTimestamp timestamp); +typedef PmTimestamp (*pm_synchronize_fn)(struct pm_internal_struct *midi); +/* pm_open_fn should clean up all memory and close the device if any part + of the open fails */ +typedef PmError (*pm_open_fn)(struct pm_internal_struct *midi, + void *driverInfo); +typedef PmError (*pm_create_fn)(int is_input, const char *name, + void *driverInfo); +typedef PmError (*pm_delete_fn)(PmDeviceID id); +typedef PmError (*pm_abort_fn)(struct pm_internal_struct *midi); +/* pm_close_fn should clean up all memory and close the device if any + part of the close fails. */ +typedef PmError (*pm_close_fn)(struct pm_internal_struct *midi); +typedef PmError (*pm_poll_fn)(struct pm_internal_struct *midi); +typedef unsigned int (*pm_check_host_error_fn)(struct pm_internal_struct *midi); + +typedef struct { + pm_write_short_fn write_short; /* output short MIDI msg */ + pm_begin_sysex_fn begin_sysex; /* prepare to send a sysex message */ + pm_end_sysex_fn end_sysex; /* marks end of sysex message */ + pm_write_byte_fn write_byte; /* accumulate one more sysex byte */ + pm_write_realtime_fn write_realtime; /* send real-time msg within sysex */ + pm_write_flush_fn write_flush; /* send any accumulated but unsent data */ + pm_synchronize_fn synchronize; /* synchronize PM time to stream time */ + pm_open_fn open; /* open MIDI device */ + pm_abort_fn abort; /* abort */ + pm_close_fn close; /* close device */ + pm_poll_fn poll; /* read pending midi events into portmidi buffer */ + pm_check_host_error_fn check_host_error; /* true when device has had host */ + /* error; sets pm_hosterror and writes message to pm_hosterror_text */ +} pm_fns_node, *pm_fns_type; + + +/* when open fails, the dictionary gets this set of functions: */ +extern pm_fns_node pm_none_dictionary; + +typedef struct { + PmDeviceInfo pub; /* some portmidi state also saved in here (for automatic + device closing -- see PmDeviceInfo struct) */ + int deleted; /* is this is a deleted virtual device? */ + void *descriptor; /* ID number passed to win32 multimedia API open, + * coreMIDI endpoint, etc., representing the device */ + struct pm_internal_struct *pm_internal; /* points to PmInternal device */ + /* when the device is open, allows automatic device closing */ + pm_fns_type dictionary; +} descriptor_node, *descriptor_type; + +extern int pm_descriptor_max; +extern descriptor_type pm_descriptors; +extern int pm_descriptor_len; + +typedef uint32_t (*time_get_proc_type)(void *time_info); + +typedef struct pm_internal_struct { + int device_id; /* which device is open (index to pm_descriptors) */ + short is_input; /* MIDI IN (true) or MIDI OUT (false) */ + short is_removed; /* MIDI device was removed */ + PmTimeProcPtr time_proc; /* where to get the time */ + void *time_info; /* pass this to get_time() */ + int32_t buffer_len; /* how big is the buffer or queue? */ + PmQueue *queue; + + int32_t latency; /* time delay in ms between timestamps and actual output */ + /* set to zero to get immediate, simple blocking output */ + /* if latency is zero, timestamps will be ignored; */ + /* if midi input device, this field ignored */ + + int sysex_in_progress; /* when sysex status is seen, this flag becomes + * true until EOX is seen. When true, new data is appended to the + * stream of outgoing bytes. When overflow occurs, sysex data is + * dropped (until an EOX or non-real-timei status byte is seen) so + * that, if the overflow condition is cleared, we don't start + * sending data from the middle of a sysex message. If a sysex + * message is filtered, sysex_in_progress is false, causing the + * message to be dropped. */ + PmMessage message; /* buffer for 4 bytes of sysex data */ + int message_count; /* how many bytes in sysex_message so far */ + int short_message_count; /* how many bytes are expected in short message */ + unsigned char running_status; /* running status byte or zero if none */ + int32_t filters; /* flags that filter incoming message classes */ + int32_t channel_mask; /* filter incoming messages based on channel */ + PmTimestamp last_msg_time; /* timestamp of last message */ + PmTimestamp sync_time; /* time of last synchronization */ + PmTimestamp now; /* set by PmWrite to current time */ + int first_message; /* initially true, used to run first synchronization */ + pm_fns_type dictionary; /* implementation functions */ + void *api_info; /* system-dependent state */ + /* the following are used to expedite sysex data */ + /* on windows, in debug mode, based on some profiling, these optimizations + * cut the time to process sysex bytes from about 7.5 to 0.26 usec/byte, + * but this does not count time in the driver, so I don't know if it is + * important + */ + unsigned char *fill_base; /* addr of ptr to sysex data */ + uint32_t *fill_offset_ptr; /* offset of next sysex byte */ + uint32_t fill_length; /* how many sysex bytes to write */ +} PmInternal; + +/* what is the length of this short message? */ +int pm_midi_length(PmMessage msg); + +/* defined by system specific implementation, e.g. pmwinmm, used by PortMidi */ +void pm_init(void); +void pm_term(void); + +/* defined by portMidi, used by pmwinmm */ +PmError none_write_short(PmInternal *midi, PmEvent *buffer); +PmError none_write_byte(PmInternal *midi, unsigned char byte, + PmTimestamp timestamp); +PmTimestamp none_synchronize(PmInternal *midi); + +PmError pm_fail_fn(PmInternal *midi); +PmError pm_fail_timestamp_fn(PmInternal *midi, PmTimestamp timestamp); +PmError pm_success_fn(PmInternal *midi); +PmError pm_add_interf(char *interf, pm_create_fn create_fn, + pm_delete_fn delete_fn); +PmError pm_add_device(char *interf, const char *name, int is_input, + int is_virtual, void *descriptor, pm_fns_type dictionary); +void pm_undo_add_device(int id); +uint32_t pm_read_bytes(PmInternal *midi, const unsigned char *data, int len, + PmTimestamp timestamp); +void pm_read_short(PmInternal *midi, PmEvent *event); + +#define none_write_flush pm_fail_timestamp_fn +#define none_sysex pm_fail_timestamp_fn +#define none_poll pm_fail_fn +#define success_poll pm_success_fn + +#define MIDI_REALTIME_MASK 0xf8 +#define is_real_time(msg) \ + ((Pm_MessageStatus(msg) & MIDI_REALTIME_MASK) == MIDI_REALTIME_MASK) + +#ifdef __cplusplus +} +#endif + +/** @endcond */ diff --git a/portmidi/pm_common/pmutil.c b/portmidi/pm_common/pmutil.c new file mode 100755 index 0000000..a70fe2f --- /dev/null +++ b/portmidi/pm_common/pmutil.c @@ -0,0 +1,284 @@ +/* pmutil.c -- some helpful utilities for building midi + applications that use PortMidi + */ +#include <stdlib.h> +#include <assert.h> +#include <string.h> +#include "portmidi.h" +#include "pmutil.h" +#include "pminternal.h" + +#ifdef WIN32 +#define bzero(addr, siz) memset(addr, 0, siz) +#endif + +// #define QUEUE_DEBUG 1 +#ifdef QUEUE_DEBUG +#include "stdio.h" +#endif + +typedef struct { + long head; + long tail; + long len; + long overflow; + int32_t msg_size; /* number of int32_t in a message including extra word */ + int32_t peek_overflow; + int32_t *buffer; + int32_t *peek; + int32_t peek_flag; +} PmQueueRep; + + +PMEXPORT PmQueue *Pm_QueueCreate(long num_msgs, int32_t bytes_per_msg) +{ + int32_t int32s_per_msg = + (int32_t) (((bytes_per_msg + sizeof(int32_t) - 1) & + ~(sizeof(int32_t) - 1)) / sizeof(int32_t)); + PmQueueRep *queue = (PmQueueRep *) pm_alloc(sizeof(PmQueueRep)); + if (!queue) /* memory allocation failed */ + return NULL; + + /* need extra word per message for non-zero encoding */ + queue->len = num_msgs * (int32s_per_msg + 1); + queue->buffer = (int32_t *) pm_alloc(queue->len * sizeof(int32_t)); + bzero(queue->buffer, queue->len * sizeof(int32_t)); + if (!queue->buffer) { + pm_free(queue); + return NULL; + } else { /* allocate the "peek" buffer */ + queue->peek = (int32_t *) pm_alloc(int32s_per_msg * sizeof(int32_t)); + if (!queue->peek) { + /* free everything allocated so far and return */ + pm_free(queue->buffer); + pm_free(queue); + return NULL; + } + } + bzero(queue->buffer, queue->len * sizeof(int32_t)); + queue->head = 0; + queue->tail = 0; + /* msg_size is in words */ + queue->msg_size = int32s_per_msg + 1; /* note extra word is counted */ + queue->overflow = FALSE; + queue->peek_overflow = FALSE; + queue->peek_flag = FALSE; + return queue; +} + + +PMEXPORT PmError Pm_QueueDestroy(PmQueue *q) +{ + PmQueueRep *queue = (PmQueueRep *) q; + + /* arg checking */ + if (!queue || !queue->buffer || !queue->peek) + return pmBadPtr; + + pm_free(queue->peek); + pm_free(queue->buffer); + pm_free(queue); + return pmNoError; +} + + +PMEXPORT PmError Pm_Dequeue(PmQueue *q, void *msg) +{ + long head; + PmQueueRep *queue = (PmQueueRep *) q; + int i; + int32_t *msg_as_int32 = (int32_t *) msg; + + /* arg checking */ + if (!queue) + return pmBadPtr; + /* a previous peek operation encountered an overflow, but the overflow + * has not yet been reported to client, so do it now. No message is + * returned, but on the next call, we will return the peek buffer. + */ + if (queue->peek_overflow) { + queue->peek_overflow = FALSE; + return pmBufferOverflow; + } + if (queue->peek_flag) { + memcpy(msg, queue->peek, (queue->msg_size - 1) * sizeof(int32_t)); + queue->peek_flag = FALSE; + return pmGotData; + } + + head = queue->head; + /* if writer overflows, it writes queue->overflow = tail+1 so that + * when the reader gets to that position in the buffer, it can + * return the overflow condition to the reader. The problem is that + * at overflow, things have wrapped around, so tail == head, and the + * reader will detect overflow immediately instead of waiting until + * it reads everything in the buffer, wrapping around again to the + * point where tail == head. So the condition also checks that + * queue->buffer[head] is zero -- if so, then the buffer is now + * empty, and we're at the point in the msg stream where overflow + * occurred. It's time to signal overflow to the reader. If + * queue->buffer[head] is non-zero, there's a message there and we + * should read all the way around the buffer before signalling overflow. + * There is a write-order dependency here, but to fail, the overflow + * field would have to be written while an entire buffer full of + * writes are still pending. I'm assuming out-of-order writes are + * possible, but not that many. + */ + if (queue->overflow == head + 1 && !queue->buffer[head]) { + queue->overflow = 0; /* non-overflow condition */ + return pmBufferOverflow; + } + + /* test to see if there is data in the queue -- test from back + * to front so if writer is simultaneously writing, we don't + * waste time discovering the write is not finished + */ + for (i = queue->msg_size - 1; i >= 0; i--) { + if (!queue->buffer[head + i]) { + return pmNoData; + } + } + memcpy(msg, (char *) &queue->buffer[head + 1], + sizeof(int32_t) * (queue->msg_size - 1)); + /* fix up zeros */ + i = queue->buffer[head]; + while (i < queue->msg_size) { + int32_t j; + i--; /* msg does not have extra word so shift down */ + j = msg_as_int32[i]; + msg_as_int32[i] = 0; + i = j; + } + /* signal that data has been removed by zeroing: */ + bzero((char *) &queue->buffer[head], sizeof(int32_t) * queue->msg_size); + + /* update head */ + head += queue->msg_size; + if (head == queue->len) head = 0; + queue->head = head; + return pmGotData; /* success */ +} + + + +PMEXPORT PmError Pm_SetOverflow(PmQueue *q) +{ + PmQueueRep *queue = (PmQueueRep *) q; + long tail; + /* arg checking */ + if (!queue) + return pmBadPtr; + /* no more enqueue until receiver acknowledges overflow */ + if (queue->overflow) return pmBufferOverflow; + tail = queue->tail; + queue->overflow = tail + 1; + return pmBufferOverflow; +} + + +PMEXPORT PmError Pm_Enqueue(PmQueue *q, void *msg) +{ + PmQueueRep *queue = (PmQueueRep *) q; + long tail; + int i; + int32_t *src = (int32_t *) msg; + int32_t *ptr; + int32_t *dest; + int rslt; + if (!queue) + return pmBadPtr; + /* no more enqueue until receiver acknowledges overflow */ + if (queue->overflow) return pmBufferOverflow; + rslt = Pm_QueueFull(q); + /* already checked above: if (rslt == pmBadPtr) return rslt; */ + tail = queue->tail; + if (rslt) { + queue->overflow = tail + 1; + return pmBufferOverflow; + } + + /* queue is has room for message, and overflow flag is cleared */ + ptr = &queue->buffer[tail]; + dest = ptr + 1; + for (i = 1; i < queue->msg_size; i++) { + int32_t j = src[i - 1]; + if (!j) { + *ptr = i; + ptr = dest; + } else { + *dest = j; + } + dest++; + } + *ptr = i; + tail += queue->msg_size; + if (tail == queue->len) tail = 0; + queue->tail = tail; + return pmNoError; +} + + +PMEXPORT int Pm_QueueEmpty(PmQueue *q) +{ + PmQueueRep *queue = (PmQueueRep *) q; + return (!queue) || /* null pointer -> return "empty" */ + (queue->buffer[queue->head] == 0 && !queue->peek_flag); +} + + +PMEXPORT int Pm_QueueFull(PmQueue *q) +{ + long tail; + int i; + PmQueueRep *queue = (PmQueueRep *) q; + /* arg checking */ + if (!queue) + return pmBadPtr; + tail = queue->tail; + /* test to see if there is space in the queue */ + for (i = 0; i < queue->msg_size; i++) { + if (queue->buffer[tail + i]) { + return TRUE; + } + } + return FALSE; +} + + +PMEXPORT void *Pm_QueuePeek(PmQueue *q) +{ + PmError rslt; + int32_t temp; + PmQueueRep *queue = (PmQueueRep *) q; + /* arg checking */ + if (!queue) + return NULL; + + if (queue->peek_flag) { + return queue->peek; + } + /* this is ugly: if peek_overflow is set, then Pm_Dequeue() + * returns immediately with pmBufferOverflow, but here, we + * want Pm_Dequeue() to really check for data. If data is + * there, we can return it + */ + temp = queue->peek_overflow; + queue->peek_overflow = FALSE; + rslt = Pm_Dequeue(q, queue->peek); + queue->peek_overflow = temp; + + if (rslt == 1) { + queue->peek_flag = TRUE; + return queue->peek; + } else if (rslt == pmBufferOverflow) { + /* when overflow is indicated, the queue is empty and the + * first message that was dropped by Enqueue (signalling + * pmBufferOverflow to its caller) would have been the next + * message in the queue. Pm_QueuePeek will return NULL, but + * remember that an overflow occurred. (see Pm_Dequeue) + */ + queue->peek_overflow = TRUE; + } + return NULL; +} + diff --git a/portmidi/pm_common/pmutil.h b/portmidi/pm_common/pmutil.h new file mode 100755 index 0000000..46c618e --- /dev/null +++ b/portmidi/pm_common/pmutil.h @@ -0,0 +1,184 @@ +/** @file pmutil.h lock-free queue for building MIDI
+ applications with PortMidi.
+
+ PortMidi is not reentrant, and locks can suffer from priority
+ inversion. To support coordination between system callbacks, a
+ high-priority thread created with PortTime, and the main
+ application thread, PortMidi uses a lock-free, non-blocking
+ queue. The queue implementation is not particular to MIDI and is
+ available for other uses.
+ */
+
+#ifndef PORTMIDI_PMUTIL_H
+#define PORTMIDI_PMUTIL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+/** @defgroup grp_pmutil Lock-free Queue
+ @{
+*/
+
+/** The queue representation is opaque. Declare a queue as PmQueue * */
+typedef void PmQueue;
+
+/** create a single-reader, single-writer queue.
+
+ @param num_msgs the number of messages the queue can hold
+
+ @param the fixed message size
+
+ @return the allocated and initialized queue, or NULL if memory
+ cannot be allocated. Allocation uses #pm_malloc().
+
+ The queue only accepts fixed sized messages.
+
+ This queue implementation uses the "light pipe" algorithm which
+ operates correctly even with multi-processors and out-of-order
+ memory writes. (see Alexander Dokumentov, "Lock-free Interprocess
+ Communication," Dr. Dobbs Portal, http://www.ddj.com/,
+ articleID=189401457, June 15, 2006. This algorithm requires that
+ messages be translated to a form where no words contain
+ zeros. Each word becomes its own "data valid" tag. Because of this
+ translation, we cannot return a pointer to data still in the queue
+ when the "peek" method is called. Instead, a buffer is
+ preallocated so that data can be copied there. Pm_QueuePeek()
+ dequeues a message into this buffer and returns a pointer to it. A
+ subsequent Pm_Dequeue() will copy from this buffer.
+
+ This implementation does not try to keep reader/writer data in
+ separate cache lines or prevent thrashing on cache lines.
+ However, this algorithm differs by doing inserts/removals in
+ units of messages rather than units of machine words. Some
+ performance improvement might be obtained by not clearing data
+ immediately after a read, but instead by waiting for the end
+ of the cache line, especially if messages are smaller than
+ cache lines. See the Dokumentov article for explanation.
+
+ The algorithm is extended to handle "overflow" reporting. To
+ report an overflow, the sender writes the current tail position to
+ a field. The receiver must acknowlege receipt by zeroing the
+ field. The sender will not send more until the field is zeroed.
+ */
+PMEXPORT PmQueue *Pm_QueueCreate(long num_msgs, int32_t bytes_per_msg);
+
+/** destroy a queue and free its storage.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @return pmNoError or an error code.
+
+ Uses #pm_free().
+
+ */
+PMEXPORT PmError Pm_QueueDestroy(PmQueue *queue);
+
+/** remove one message from the queue, copying it into \p msg.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @param msg address to which the message, if any, is copied.
+
+ @return 1 if successful, and 0 if the queue is empty. Returns
+ #pmBufferOverflow if what would have been the next thing in the
+ queue was dropped due to overflow. (So when overflow occurs, the
+ receiver can receive a queue full of messages before getting the
+ overflow report. This protocol ensures that the reader will be
+ notified when data is lost due to overflow.
+ */
+PMEXPORT PmError Pm_Dequeue(PmQueue *queue, void *msg);
+
+/** insert one message into the queue, copying it from \p msg.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @param msg address of the message to be enqueued.
+
+ @return #pmNoError if successful and #pmBufferOverflow if the
+ queue was already full. If #pmBufferOverflow is returned, the
+ overflow flag is set.
+ */
+PMEXPORT PmError Pm_Enqueue(PmQueue *queue, void *msg);
+
+/** test if the queue is full.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @return non-zero iff the queue is empty, and @pmBadPtr if \p queue
+ is NULL.
+
+ The full condition may change immediately because a parallel
+ dequeue operation could be in progress. The result is
+ pessimistic: if it returns false (zero) to the single writer, then
+ #Pm_Enqueue() is guaranteed to succeed.
+ */
+PMEXPORT int Pm_QueueFull(PmQueue *queue);
+
+/** test if the queue is empty.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @return zero iff the queue is either empty or NULL.
+
+ The empty condition may change immediately because a parallel
+ enqueue operation could be in progress. Furthermore, the
+ result is optimistic: it may say false, when due to
+ out-of-order writes, the full message has not arrived. Therefore,
+ #Pm_Dequeue() could still return 0 after #Pm_QueueEmpty() returns
+ false.
+*/
+PMEXPORT int Pm_QueueEmpty(PmQueue *queue);
+
+/** get a pointer to the item at the head of the queue.
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @result a pointer to the head message or NULL if the queue is empty.
+
+ The message is not removed from the queue. #Pm_QueuePeek() will
+ not indicate when an overflow occurs. If you want to get and check
+ #pmBufferOverflow messages, use the return value of
+ #Pm_QueuePeek() *only* as an indication that you should call
+ #Pm_Dequeue(). At the point where a direct call to #Pm_Dequeue()
+ would return #pmBufferOverflow, #Pm_QueuePeek() will return NULL,
+ but internally clear the #pmBufferOverflow flag, enabling
+ #Pm_Enqueue() to resume enqueuing messages. A subsequent call to
+ #Pm_QueuePeek() will return a pointer to the first message *after*
+ the overflow. Using this as an indication to call #Pm_Dequeue(),
+ the first call to #Pm_Dequeue() will return #pmBufferOverflow. The
+ second call will return success, copying the same message pointed
+ to by the previous #Pm_QueuePeek().
+
+ When to use #Pm_QueuePeek(): (1) when you need to look at the message
+ data to decide who should be called to receive it. (2) when you need
+ to know a message is ready but cannot accept the message.
+
+ Note that #Pm_QueuePeek() is not a fast check, so if possible, you
+ might as well just call #Pm_Dequeue() and accept the data if it is there.
+ */
+PMEXPORT void *Pm_QueuePeek(PmQueue *queue);
+
+/** allows the writer (enqueuer) to signal an overflow
+ condition to the reader (dequeuer).
+
+ @param queue a queue created by #Pm_QueueCreate().
+
+ @return #pmNoError if overflow is set, or #pmBadPtr if queue is
+ NULL, or #pmBufferOverflow if buffer is already in an overflow
+ state.
+
+ E.g., when transfering data from the OS to an application, if the
+ OS indicates a buffer overrun, #Pm_SetOverflow() can be used to
+ insure that the reader receives a #pmBufferOverflow result from
+ #Pm_Dequeue().
+ */
+PMEXPORT PmError Pm_SetOverflow(PmQueue *queue);
+
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+
+#endif // PORTMIDI_PMUTIL_H
diff --git a/portmidi/pm_common/portmidi.c b/portmidi/pm_common/portmidi.c new file mode 100755 index 0000000..e78ee73 --- /dev/null +++ b/portmidi/pm_common/portmidi.c @@ -0,0 +1,1472 @@ +/* portmidi.c -- cross-platform MIDI I/O library */ +/* see license.txt for license */ + +#include "stdlib.h" +#include "string.h" +#include "portmidi.h" +#include "porttime.h" +#include "pmutil.h" +#include "pminternal.h" +#include <assert.h> + +#define MIDI_CLOCK 0xf8 +#define MIDI_ACTIVE 0xfe +#define MIDI_STATUS_MASK 0x80 +#define MIDI_SYSEX 0xf0 +#define MIDI_EOX 0xf7 +#define MIDI_START 0xFA +#define MIDI_STOP 0xFC +#define MIDI_CONTINUE 0xFB +#define MIDI_F9 0xF9 +#define MIDI_FD 0xFD +#define MIDI_RESET 0xFF +#define MIDI_NOTE_ON 0x90 +#define MIDI_NOTE_OFF 0x80 +#define MIDI_CHANNEL_AT 0xD0 +#define MIDI_POLY_AT 0xA0 +#define MIDI_PROGRAM 0xC0 +#define MIDI_CONTROL 0xB0 +#define MIDI_PITCHBEND 0xE0 +#define MIDI_MTC 0xF1 +#define MIDI_SONGPOS 0xF2 +#define MIDI_SONGSEL 0xF3 +#define MIDI_TUNE 0xF6 + +#define is_empty(midi) ((midi)->tail == (midi)->head) + +/* these are not static so that (possibly) some system-dependent code + * could override the portmidi.c default which is to use the first + * device added using pm_add_device() + */ +PmDeviceID pm_default_input_device_id = -1; +PmDeviceID pm_default_output_device_id = -1; + +/* this is not static so that pm_init can set it directly + * (see pmmac.c:pm_init()) + */ +int pm_initialized = FALSE; + +int pm_hosterror; /* boolean */ + +/* if PM_CHECK_ERRORS is enabled, but the caller wants to + * handle an error condition, declare this as extern and + * set to FALSE (this override is provided specifically + * for the test program virttest.c, where pmNameConflict + * is expected in a call to Pm_CreateVirtualInput()): + */ +int pm_check_errors = TRUE; + +char pm_hosterror_text[PM_HOST_ERROR_MSG_LEN]; + +#ifdef PM_CHECK_ERRORS + +#include <stdio.h> + +#define STRING_MAX 80 + +static void prompt_and_exit(void) +{ + char line[STRING_MAX]; + printf("type ENTER..."); + char *rslt = fgets(line, STRING_MAX, stdin); + /* this will clean up open ports: */ + exit(-1); +} + +static PmError pm_errmsg(PmError err) +{ + if (!pm_check_errors) { /* see pm_check_errors declaration above */ + ; + } else if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + printf("PortMidi found host error...\n %s\n", pm_hosterror_text); + pm_hosterror = FALSE; + pm_hosterror_text[0] = 0; /* clear the message */ + prompt_and_exit(); + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + prompt_and_exit(); + } + return err; +} +#else +#define pm_errmsg(err) err +#endif + + +int pm_midi_length(PmMessage msg) +{ + int status, high, low; + static int high_lengths[] = { + 1, 1, 1, 1, 1, 1, 1, 1, /* 0x00 through 0x70 */ + 3, 3, 3, 3, 2, 2, 3, 1 /* 0x80 through 0xf0 */ + }; + static int low_lengths[] = { + 1, 2, 3, 2, 1, 1, 1, 1, /* 0xf0 through 0xf8 */ + 1, 1, 1, 1, 1, 1, 1, 1 /* 0xf9 through 0xff */ + }; + + status = msg & 0xFF; + high = status >> 4; + low = status & 15; + + return (high != 0xF) ? high_lengths[high] : low_lengths[low]; +} + + +/* +==================================================================== +system implementation of portmidi interface +==================================================================== +*/ + +int pm_descriptor_max = 0; +int pm_descriptor_len = 0; +descriptor_type pm_descriptors = NULL; + +/* interface pm_descriptors are simple: an array of string/fnptr pairs: */ +#define MAX_INTERF 4 +static struct { + const char *interf; + pm_create_fn create_fn; + pm_delete_fn delete_fn; +} pm_interf_list[MAX_INTERF]; + +static int pm_interf_list_len = 0; + + +/* pm_add_interf -- describe an interface to library + * + * This is called at initialization time, once for each + * supported interface (e.g., CoreMIDI). The strings + * are retained but NOT COPIED, so do not destroy them! + * + * The purpose is to register functions that create/delete + * a virtual input or output device. + * + * returns pmInsufficientMemor if interface memory is + * exceeded, otherwise returns pmNoError. + */ +PmError pm_add_interf(char *interf, pm_create_fn create_fn, + pm_delete_fn delete_fn) +{ + if (pm_interf_list_len >= MAX_INTERF) { + return pmInsufficientMemory; + } + pm_interf_list[pm_interf_list_len].interf = interf; + pm_interf_list[pm_interf_list_len].create_fn = create_fn; + pm_interf_list[pm_interf_list_len].delete_fn = delete_fn; + pm_interf_list_len++; + return pmNoError; +} + + +PmError pm_create_virtual(PmInternal *midi, int is_input, const char *interf, + const char *name, void *device_info) +{ + int i; + if (pm_interf_list_len == 0) { + return pmNotImplemented; + } + if (!interf) { + /* default interface is the first one */ + interf = pm_interf_list[0].interf; + } + for (i = 0; i < pm_interf_list_len; i++) { + if (strcmp(pm_interf_list[i].interf, + interf) == 0) { + int id = (*pm_interf_list[i].create_fn)(is_input, name, + device_info); + pm_descriptors[id].pub.is_virtual = TRUE; + return id; + } + } + return pmInterfaceNotSupported; +} + + +/* pm_add_device -- describe interface/device pair to library + * + * This is called at intialization time, once for each + * interface (e.g. DirectSound) and device (e.g. SoundBlaster 1). + * This is also called when user creates a virtual device. + * + * Normally, increasing integer indices are returned. If the device + * is virtual, a linear search is performed to ensure that the name + * is unique. If the name is already taken, the call will fail and + * no device is added. + * + * interf is assumed to be static memory, so it is NOT COPIED and + * NOT FREED. + * name is owned by caller, COPIED if needed, and FREED by PortMidi. + * Caller is resposible for freeing name when pm_add_device returns. + * + * returns pmInvalidDeviceId if device memory is exceeded or a virtual + * device would take the name of an existing device. + * otherwise returns index (portmidi device_id) of the added device + */ +PmError pm_add_device(char *interf, const char *name, int is_input, + int is_virtual, void *descriptor, pm_fns_type dictionary) { + /* printf("pm_add_device: %s %s %d %p %p\n", + interf, name, is_input, descriptor, dictionary); */ + int device_id; + PmDeviceInfo *d; + /* if virtual, search for duplicate name or unused ID; otherwise, + * just add a new device at the next integer available: + */ + for (device_id = (is_virtual ? 0 : pm_descriptor_len); + device_id < pm_descriptor_len; device_id++) { + d = &pm_descriptors[device_id].pub; + d->structVersion = PM_DEVICEINFO_VERS; + if (strcmp(d->interf, interf) == 0 && strcmp(d->name, name) == 0) { + /* only reuse a name if it is a deleted virtual device with + * a matching direction (input or output) */ + if (pm_descriptors[device_id].deleted && is_input == d->input) { + /* here, we know d->is_virtual because only virtual devices + * can be deleted, and we know is_virtual because we are + * in this loop. + */ + pm_free((void *) d->name); /* reuse this device entry */ + d->name = NULL; + break; + /* name conflict exists if the new device appears to others as + * the same direction (input or output) as the existing device. + * Note that virtual inputs appear to others as outputs and + * vice versa. + * The direction of the new virtual device to others is "output" + * if is_input, i.e., virtual inputs appear to others as outputs. + * The existing device appears to others as "output" if + * (d->is_virtual == d->input) by the same logic. + * The compare will detect if device directions are the same: + */ + } else if (is_input == (d->is_virtual == d->input)) { + return pmNameConflict; + } + } + } + if (device_id >= pm_descriptor_max) { + // expand pm_descriptors + descriptor_type new_descriptors = (descriptor_type) + pm_alloc(sizeof(descriptor_node) * (pm_descriptor_max + 32)); + if (!new_descriptors) return pmInsufficientMemory; + if (pm_descriptors) { + memcpy(new_descriptors, pm_descriptors, + sizeof(descriptor_node) * pm_descriptor_max); + pm_free(pm_descriptors); + } + pm_descriptor_max += 32; + pm_descriptors = new_descriptors; + } + if (device_id == pm_descriptor_len) { + pm_descriptor_len++; /* extending array of pm_descriptors */ + } + d = &pm_descriptors[device_id].pub; + d->interf = interf; + d->name = pm_alloc(strlen(name) + 1); + if (!d->name) { + return pmInsufficientMemory; + } +#if defined(WIN32) && !defined(_WIN32) +#pragma warning(suppress: 4996) // don't use suggested strncpy_s +#endif + strcpy(d->name, name); + d->input = is_input; + d->output = !is_input; + d->is_virtual = FALSE; /* caller should set to TRUE if this is virtual */ + + /* default state: nothing to close (for automatic device closing) */ + d->opened = FALSE; + + pm_descriptors[device_id].deleted = FALSE; + + /* ID number passed to win32 multimedia API open */ + pm_descriptors[device_id].descriptor = descriptor; + + /* points to PmInternal, allows automatic device closing */ + pm_descriptors[device_id].pm_internal = NULL; + + pm_descriptors[device_id].dictionary = dictionary; + + /* set the defaults to the first input and output we see */ + if (is_input && pm_default_input_device_id == -1) { + pm_default_input_device_id = device_id; + } else if (!is_input && pm_default_output_device_id == -1) { + pm_default_output_device_id = device_id; + } + + return device_id; +} + + +/* Undo a successful call to pm_add_device(). If a new device was + * allocated, it must be the last device in pm_descriptors, so it is + * easy to delete by decrementing the length of pm_descriptors, but + * first free the name (which was copied to the heap). Otherwise, + * the device must be a virtual device that was created previously + * and is in the interior of the array of pm_descriptors. Leave it, + * but mark it as deleted. + */ +void pm_undo_add_device(int id) +{ + /* Clear some fields (not all are strictly necessary) */ + pm_descriptors[id].deleted = TRUE; + pm_descriptors[id].descriptor = NULL; + pm_descriptors[id].pm_internal = NULL; + + if (id == pm_descriptor_len - 1) { + pm_free(pm_descriptors[id].pub.name); + pm_descriptor_len--; + } +} + + +/* utility to look up device, given a pattern, + note: pattern is modified + */ +int Pm_FindDevice(char *pattern, int is_input) +{ + int id = pmNoDevice; + int i; + /* first parse pattern into name, interf parts */ + char *interf_pref = ""; /* initially assume it is not there */ + char *name_pref = strstr(pattern, ", "); + + if (name_pref) { /* found separator, adjust the pointer */ + interf_pref = pattern; + name_pref[0] = 0; + name_pref += 2; + } else { + name_pref = pattern; /* whole string is the name pattern */ + } + for (i = 0; i < pm_descriptor_len; i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (info->input == is_input && + strstr(info->name, name_pref) && + strstr(info->interf, interf_pref)) { + id = i; + break; + } + } + return id; +} + + +/* +==================================================================== +portmidi implementation +==================================================================== +*/ + +PMEXPORT int Pm_CountDevices(void) +{ + Pm_Initialize(); + /* no error checking -- Pm_Initialize() does not fail */ + return pm_descriptor_len; +} + + +PMEXPORT const PmDeviceInfo* Pm_GetDeviceInfo(PmDeviceID id) +{ + Pm_Initialize(); /* no error check needed */ + if (id >= 0 && id < pm_descriptor_len && !pm_descriptors[id].deleted) { + return &pm_descriptors[id].pub; + } + return NULL; +} + +/* pm_success_fn -- "noop" function pointer */ +PmError pm_success_fn(PmInternal *midi) +{ + return pmNoError; +} + +/* none_write -- returns an error if called */ +PmError none_write_short(PmInternal *midi, PmEvent *buffer) +{ + return pmBadPtr; +} + +/* pm_fail_timestamp_fn -- placeholder for begin_sysex and flush */ +PmError pm_fail_timestamp_fn(PmInternal *midi, PmTimestamp timestamp) +{ + return pmBadPtr; +} + +PmError none_write_byte(PmInternal *midi, unsigned char byte, + PmTimestamp timestamp) +{ + return pmBadPtr; +} + +/* pm_fail_fn -- generic function, returns error if called */ +PmError pm_fail_fn(PmInternal *midi) +{ + return pmBadPtr; +} + +static PmError none_open(PmInternal *midi, void *driverInfo) +{ + return pmBadPtr; +} + +static unsigned int none_check_host_error(PmInternal * midi) +{ + return FALSE; +} + +PmTimestamp none_synchronize(PmInternal *midi) +{ + return 0; +} + +#define none_abort pm_fail_fn +#define none_close pm_fail_fn + +pm_fns_node pm_none_dictionary = { + none_write_short, + none_sysex, + none_sysex, + none_write_byte, + none_write_short, + none_write_flush, + none_synchronize, + none_open, + none_abort, + none_close, + none_poll, + none_check_host_error, +}; + + +PMEXPORT const char *Pm_GetErrorText(PmError errnum) +{ + const char *msg; + + switch(errnum) + { + case pmNoError: + msg = ""; + break; + case pmHostError: + msg = "PortMidi: Host error"; + break; + case pmInvalidDeviceId: + msg = "PortMidi: Invalid device ID"; + break; + case pmInsufficientMemory: + msg = "PortMidi: Insufficient memory"; + break; + case pmBufferTooSmall: + msg = "PortMidi: Buffer too small"; + break; + case pmBadPtr: + msg = "PortMidi: Bad pointer"; + break; + case pmInternalError: + msg = "PortMidi: Internal PortMidi Error"; + break; + case pmBufferOverflow: + msg = "PortMidi: Buffer overflow"; + break; + case pmBadData: + msg = "PortMidi: Invalid MIDI message Data"; + break; + case pmBufferMaxSize: + msg = "PortMidi: Buffer cannot be made larger"; + break; + case pmNotImplemented: + msg = "PortMidi: Function is not implemented"; + break; + case pmInterfaceNotSupported: + msg = "PortMidi: Interface not supported"; + break; + case pmNameConflict: + msg = "PortMidi: Cannot create virtual device: name is taken"; + break; + case pmDeviceRemoved: + msg = "PortMidi: Output attempted after (USB) device removed"; + break; + default: + msg = "PortMidi: Illegal error number"; + break; + } + return msg; +} + + +/* This can be called whenever you get a pmHostError return value + * or TRUE from Pm_HasHostError(). + * The error will always be in the global pm_hosterror_text. + */ +PMEXPORT void Pm_GetHostErrorText(char * msg, unsigned int len) +{ + assert(msg); + assert(len > 0); + if (pm_hosterror) { +#if defined(WIN32) && !defined(_WIN32) +#pragma warning(suppress: 4996) // don't use suggested strncpy_s +#endif + strncpy(msg, (char *) pm_hosterror_text, len); + pm_hosterror = FALSE; + pm_hosterror_text[0] = 0; /* clear the message; not necessary, but it + might help with debugging */ + msg[len - 1] = 0; /* make sure string is terminated */ + } else { + msg[0] = 0; /* no string to return */ + } +} + + +PMEXPORT int Pm_HasHostError(PortMidiStream * stream) +{ + if (pm_hosterror) return TRUE; + if (stream) { + PmInternal * midi = (PmInternal *) stream; + return (*midi->dictionary->check_host_error)(midi); + } + return FALSE; +} + + +PMEXPORT PmError Pm_Initialize(void) +{ + if (!pm_initialized) { + pm_descriptor_len = 0; + pm_interf_list_len = 0; + pm_hosterror = FALSE; + pm_hosterror_text[0] = 0; /* the null string */ + pm_init(); + pm_initialized = TRUE; + } + return pmNoError; +} + + +PMEXPORT PmError Pm_Terminate(void) +{ + if (pm_initialized) { + pm_term(); + /* if there are no devices, pm_descriptors might still be NULL */ + if (pm_descriptors != NULL) { + int i; /* free names copied into pm_descriptors */ + for (i = 0; i < pm_descriptor_len; i++) { + if (pm_descriptors[i].pub.name) { + pm_free(pm_descriptors[i].pub.name); + } + } + pm_free(pm_descriptors); + pm_descriptors = NULL; + } + pm_descriptor_len = 0; + pm_descriptor_max = 0; + pm_interf_list_len = 0; + pm_initialized = FALSE; + } + return pmNoError; +} + + +/* Pm_Read -- read up to length messages from source into buffer */ +/* + * returns number of messages actually read, or error code + */ +PMEXPORT int Pm_Read(PortMidiStream *stream, PmEvent *buffer, int32_t length) +{ + PmInternal *midi = (PmInternal *) stream; + int n = 0; + PmError err = pmNoError; + pm_hosterror = FALSE; + /* arg checking */ + if(midi == NULL) + err = pmBadPtr; + else if(!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + else if(!pm_descriptors[midi->device_id].pub.input) + err = pmBadPtr; + /* First poll for data in the buffer... + * This either simply checks for data, or attempts first to fill the buffer + * with data from the MIDI hardware; this depends on the implementation. + * We could call Pm_Poll here, but that would redo a lot of redundant + * parameter checking, so I copied some code from Pm_Poll to here: */ + else err = (*(midi->dictionary->poll))(midi); + + if (err != pmNoError) { + if (err == pmHostError) { + midi->dictionary->check_host_error(midi); + } + return pm_errmsg(err); + } + + while (n < length) { + err = Pm_Dequeue(midi->queue, buffer++); + if (err == pmBufferOverflow) { + /* ignore the data we have retreived so far */ + return pm_errmsg(pmBufferOverflow); + } else if (err == 0) { /* empty queue */ + break; + } + n++; + } + return n; +} + +PMEXPORT PmError Pm_Poll(PortMidiStream *stream) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err; + + pm_hosterror = FALSE; + /* arg checking */ + if(midi == NULL) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.input) + err = pmBadPtr; + else + err = (*(midi->dictionary->poll))(midi); + + if (err != pmNoError) { + return pm_errmsg(err); + } + + return (PmError) !Pm_QueueEmpty(midi->queue); +} + + +/* this is called from Pm_Write and Pm_WriteSysEx to issue a + * call to the system-dependent end_sysex function and handle + * the error return + */ +static PmError pm_end_sysex(PmInternal *midi) +{ + PmError err = (*midi->dictionary->end_sysex)(midi, 0); + midi->sysex_in_progress = FALSE; + return err; +} + + +/* to facilitate correct error-handling, Pm_Write, Pm_WriteShort, and + Pm_WriteSysEx all operate a state machine that "outputs" calls to + write_short, begin_sysex, write_byte, end_sysex, and write_realtime */ + +PMEXPORT PmError Pm_Write(PortMidiStream *stream, PmEvent *buffer, + int32_t length) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + int i; + int bits; + + pm_hosterror = FALSE; + /* arg checking */ + if (midi == NULL) { + err = pmBadPtr; + } else { + descriptor_type desc = &pm_descriptors[midi->device_id]; + if (!desc || !desc->pub.opened || + !desc->pub.output || !desc->pm_internal) { + err = pmBadPtr; + } else if (desc->pm_internal->is_removed) { + err = pmDeviceRemoved; + } + } + if (err != pmNoError) goto pm_write_error; + + if (midi->latency == 0) { + midi->now = 0; + } else { + midi->now = (*(midi->time_proc))(midi->time_info); + if (midi->first_message || midi->sync_time + 100 /*ms*/ < midi->now) { + /* time to resync */ + midi->now = (*midi->dictionary->synchronize)(midi); + midi->first_message = FALSE; + } + } + /* error recovery: when a sysex is detected, we call + * dictionary->begin_sysex() followed by calls to + * dictionary->write_byte() and dictionary->write_realtime() + * until an end-of-sysex is detected, when we call + * dictionary->end_sysex(). After an error occurs, + * Pm_Write() continues to call functions. For example, + * it will continue to call write_byte() even after + * an error sending a sysex message, and end_sysex() will be + * called when an EOX or non-real-time status is found. + * When errors are detected, Pm_Write() returns immediately, + * so it is possible that this will drop data and leave + * sysex messages in a partially transmitted state. + */ + for (i = 0; i < length; i++) { + uint32_t msg = buffer[i].message; + bits = 0; + /* is this a sysex message? */ + if (Pm_MessageStatus(msg) == MIDI_SYSEX) { + if (midi->sysex_in_progress) { + /* error: previous sysex was not terminated by EOX */ + midi->sysex_in_progress = FALSE; + err = pmBadData; + goto pm_write_error; + } + midi->sysex_in_progress = TRUE; + if ((err = (*midi->dictionary->begin_sysex)(midi, + buffer[i].timestamp)) != pmNoError) + goto pm_write_error; + if ((err = (*midi->dictionary->write_byte)(midi, MIDI_SYSEX, + buffer[i].timestamp)) != pmNoError) + goto pm_write_error; + bits = 8; + /* fall through to continue sysex processing */ + } else if ((msg & MIDI_STATUS_MASK) && + (Pm_MessageStatus(msg) != MIDI_EOX)) { + /* a non-sysex message */ + if (midi->sysex_in_progress) { + /* this should be a realtime message */ + if (is_real_time(msg)) { + if ((err = (*midi->dictionary->write_realtime)(midi, + &(buffer[i]))) != pmNoError) + goto pm_write_error; + } else { + midi->sysex_in_progress = FALSE; + err = pmBadData; + /* ignore any error from this, because we already have one */ + /* pass 0 as timestamp -- it's ignored */ + (*midi->dictionary->end_sysex)(midi, 0); + goto pm_write_error; + } + } else { /* regular short midi message */ + if ((err = (*midi->dictionary->write_short)(midi, + &(buffer[i]))) != pmNoError) + goto pm_write_error; + continue; + } + } + if (midi->sysex_in_progress) { /* send sysex bytes until EOX */ + /* see if we can accelerate data transfer */ + if (bits == 0 && midi->fill_base && /* 4 bytes to copy */ + (*midi->fill_offset_ptr) + 4 <= midi->fill_length && + (msg & 0x80808080) == 0) { /* all data */ + /* copy 4 bytes from msg to fill_base + fill_offset */ + unsigned char *ptr = midi->fill_base + + *(midi->fill_offset_ptr); + ptr[0] = msg; ptr[1] = msg >> 8; + ptr[2] = msg >> 16; ptr[3] = msg >> 24; + (*midi->fill_offset_ptr) += 4; + continue; + } + /* no acceleration, so do byte-by-byte copying */ + while (bits < 32) { + unsigned char midi_byte = (unsigned char) (msg >> bits); + if ((err = (*midi->dictionary->write_byte)(midi, midi_byte, + buffer[i].timestamp)) != pmNoError) + goto pm_write_error; + if (midi_byte == MIDI_EOX) { + err = pm_end_sysex(midi); + if (err != pmNoError) goto error_exit; + break; /* from while loop */ + } + bits += 8; + } + } else { + /* not in sysex mode, but message did not start with status */ + err = pmBadData; + goto pm_write_error; + } + } + /* after all messages are processed, send the data */ + if (!midi->sysex_in_progress) + err = (*midi->dictionary->write_flush)(midi, 0); +pm_write_error: + if (err == pmHostError) { + midi->dictionary->check_host_error(midi); + } +error_exit: + return pm_errmsg(err); +} + + +PMEXPORT PmError Pm_WriteShort(PortMidiStream *stream, PmTimestamp when, + PmMessage msg) +{ + PmEvent event; + + event.timestamp = when; + event.message = msg; + return Pm_Write(stream, &event, 1); +} + + +PMEXPORT PmError Pm_WriteSysEx(PortMidiStream *stream, PmTimestamp when, + unsigned char *msg) +{ + /* allocate buffer space for PM_DEFAULT_SYSEX_BUFFER_SIZE bytes */ + /* each PmEvent holds sizeof(PmMessage) bytes of sysex data */ + #define BUFLEN ((int) (PM_DEFAULT_SYSEX_BUFFER_SIZE / sizeof(PmMessage))) + PmEvent buffer[BUFLEN]; + int buffer_size = 1; /* first time, send 1. After that, it's BUFLEN */ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + /* the next byte in the buffer is represented by an index, bufx, and + a shift in bits */ + int shift = 0; + int bufx = 0; + buffer[0].message = 0; + buffer[0].timestamp = when; + + while (1) { + /* insert next byte into buffer */ + buffer[bufx].message |= ((*msg) << shift); + shift += 8; + if (*msg++ == MIDI_EOX) break; + if (shift == 32) { + shift = 0; + bufx++; + if (bufx == buffer_size) { + err = Pm_Write(stream, buffer, buffer_size); + /* note: Pm_Write has already called errmsg() */ + if (err) return err; + /* prepare to fill another buffer */ + bufx = 0; + buffer_size = BUFLEN; + /* optimization: maybe we can just copy bytes */ + if (midi->fill_base) { + while (*(midi->fill_offset_ptr) < midi->fill_length) { + midi->fill_base[(*midi->fill_offset_ptr)++] = *msg; + if (*msg++ == MIDI_EOX) { + err = pm_end_sysex(midi); + if (err != pmNoError) return pm_errmsg(err); + goto end_of_sysex; + } + } + /* I thought that I could do a pm_Write here and + * change this if to a loop, avoiding calls in Pm_Write + * to the slower write_byte, but since + * sysex_in_progress is true, this will not flush + * the buffer, and we'll infinite loop: */ + /* err = Pm_Write(stream, buffer, 0); + if (err) return err; */ + /* instead, the way this works is that Pm_Write calls + * write_byte on 4 bytes. The first, since the buffer + * is full, will flush the buffer and allocate a new + * one. This primes the buffer so + * that we can return to the loop above and fill it + * efficiently without a lot of function calls. + */ + buffer_size = 1; /* get another message started */ + } + } + buffer[bufx].message = 0; + buffer[bufx].timestamp = when; + } + /* keep inserting bytes until you find MIDI_EOX */ + } +end_of_sysex: + /* we're finished sending full buffers, but there may + * be a partial one left. + */ + if (shift != 0) bufx++; /* add partial message to buffer len */ + if (bufx) { /* bufx is number of PmEvents to send from buffer */ + err = Pm_Write(stream, buffer, bufx); + if (err) return err; + } + return pmNoError; +} + + + +PmError pm_create_internal(PmInternal **stream, PmDeviceID device_id, + int is_input, int latency, PmTimeProcPtr time_proc, + void *time_info, int buffer_size) +{ + PmInternal *midi; + if (device_id < 0 || device_id >= pm_descriptor_len) { + return pmInvalidDeviceId; + } + if (latency < 0) { /* force a legal value */ + latency = 0; + } + /* create portMidi internal data */ + midi = (PmInternal *) pm_alloc(sizeof(PmInternal)); + *stream = midi; + if (!midi) { + return pmInsufficientMemory; + } + midi->device_id = device_id; + midi->is_input = is_input; + midi->is_removed = FALSE; + midi->time_proc = time_proc; + /* if latency != 0, we need a time reference for output. + we always need a time reference for input. + If none is provided, use PortTime library */ + if (time_proc == NULL && (latency != 0 || is_input)) { + if (!Pt_Started()) + Pt_Start(1, 0, 0); + /* time_get does not take a parameter, so coerce */ + midi->time_proc = (PmTimeProcPtr) Pt_Time; + } + midi->time_info = time_info; + if (is_input) { + midi->latency = 0; /* unused by input */ + if (buffer_size <= 0) buffer_size = 256; /* default buffer size */ + midi->queue = Pm_QueueCreate(buffer_size, (int32_t) sizeof(PmEvent)); + if (!midi->queue) { + /* free portMidi data */ + *stream = NULL; + pm_free(midi); + return pmInsufficientMemory; + } + } else { + /* if latency zero, output immediate (timestamps ignored) */ + /* if latency < 0, use 0 but don't return an error */ + if (latency < 0) latency = 0; + midi->latency = latency; + midi->queue = NULL; /* unused by output; input needs to allocate: */ + } + midi->buffer_len = buffer_size; /* portMidi input storage */ + midi->sysex_in_progress = FALSE; + midi->message = 0; + midi->message_count = 0; + midi->filters = (is_input ? PM_FILT_ACTIVE : 0); + midi->channel_mask = 0xFFFF; + midi->sync_time = 0; + midi->first_message = TRUE; + midi->api_info = NULL; + midi->fill_base = NULL; + midi->fill_offset_ptr = NULL; + midi->fill_length = 0; + midi->dictionary = pm_descriptors[device_id].dictionary; + pm_descriptors[device_id].pm_internal = midi; + return pmNoError; +} + + +PMEXPORT PmError Pm_OpenInput(PortMidiStream** stream, + PmDeviceID inputDevice, + void *inputDriverInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info) +{ + PmInternal *midi; + PmError err = pmNoError; + pm_hosterror = FALSE; + *stream = NULL; /* invariant: *stream == midi */ + + /* arg checking */ + if (!pm_descriptors[inputDevice].pub.input) + err = pmInvalidDeviceId; + else if (pm_descriptors[inputDevice].pub.opened) + err = pmInvalidDeviceId; + if (err != pmNoError) + goto error_return; + + /* common initialization of PmInternal structure (midi): */ + err = pm_create_internal(&midi, inputDevice, TRUE, 0, time_proc, + time_info, bufferSize); + *stream = midi; + if (err) { + goto error_return; + } + + /* open system dependent input device */ + err = (*midi->dictionary->open)(midi, inputDriverInfo); + if (err) { + *stream = NULL; + pm_descriptors[inputDevice].pm_internal = NULL; + /* free portMidi data */ + Pm_QueueDestroy(midi->queue); + pm_free(midi); + } else { + /* portMidi input open successful */ + pm_descriptors[inputDevice].pub.opened = TRUE; + } +error_return: + /* note: if there is a pmHostError, it is the responsibility + * of the system-dependent code (*midi->dictionary->open)() + * to set pm_hosterror and pm_hosterror_text + */ + return pm_errmsg(err); +} + + +PMEXPORT PmError Pm_OpenOutput(PortMidiStream** stream, + PmDeviceID outputDevice, + void *outputDriverInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info, + int32_t latency) +{ + PmInternal *midi; + PmError err = pmNoError; + pm_hosterror = FALSE; + *stream = NULL; + + /* arg checking */ + if (outputDevice < 0 || outputDevice >= pm_descriptor_len) + err = pmInvalidDeviceId; + else if (!pm_descriptors[outputDevice].pub.output) + err = pmInvalidDeviceId; + else if (pm_descriptors[outputDevice].pub.opened) + err = pmInvalidDeviceId; + if (err != pmNoError) + goto error_return; + + /* common initialization of PmInternal structure (midi): */ + err = pm_create_internal(&midi, outputDevice, FALSE, latency, time_proc, + time_info, bufferSize); + *stream = midi; + if (err) { + goto error_return; + } + + /* open system dependent output device */ + err = (*midi->dictionary->open)(midi, outputDriverInfo); + if (err) { + *stream = NULL; + pm_descriptors[outputDevice].pm_internal = NULL; + /* free portMidi data */ + pm_free(midi); + } else { + /* portMidi input open successful */ + pm_descriptors[outputDevice].pub.opened = TRUE; + } +error_return: + /* note: system-dependent code must set pm_hosterror and + * pm_hosterror_text if a pmHostError occurs + */ + return pm_errmsg(err); +} + + +static PmError create_virtual_device(const char *name, const char *interf, + void *device_info, int is_input) +{ + PmError err = pmNoError; + int i; + pm_hosterror = FALSE; + + /* arg checking */ + if (!name) { + err = pmInvalidDeviceId; + goto error_return; + } + + Pm_Initialize(); /* just in case */ + + /* create the virtual device */ + if (pm_interf_list_len == 0) { + return pmNotImplemented; + } + if (!interf) { + /* default interface is the first one */ + interf = pm_interf_list[0].interf; + } + /* look up and call the create_fn for interf(ace), e.g. "CoreMIDI" */ + for (i = 0; i < pm_interf_list_len; i++) { + if (strcmp(pm_interf_list[i].interf, interf) == 0) { + int id = (*pm_interf_list[i].create_fn)(is_input, + name, device_info); + /* id could be pmNameConflict or an actual descriptor index */ + if (id >= 0) { + pm_descriptors[id].pub.is_virtual = TRUE; + } + err = id; + goto error_return; + } + } + err = pmInterfaceNotSupported; + +error_return: + /* note: if there is a pmHostError, it is the responsibility + * of the system-dependent code (*midi->dictionary->open)() + * to set pm_hosterror and pm_hosterror_text + */ + return pm_errmsg(err); +} + + +PMEXPORT PmError Pm_CreateVirtualInput(const char *name, + const char *interf, + void *deviceInfo) +{ + return create_virtual_device(name, interf, deviceInfo, TRUE); +} + +PMEXPORT PmError Pm_CreateVirtualOutput(const char *name, const char *interf, + void *deviceInfo) +{ + return create_virtual_device(name, interf, deviceInfo, FALSE); +} + +PmError Pm_DeleteVirtualDevice(PmDeviceID id) +{ + int i; + const char *interf = pm_descriptors[id].pub.interf; + PmError err = pmBadData; /* returned if we cannot find the interface- + * specific delete function */ + /* arg checking */ + if (id < 0 || id >= pm_descriptor_len || + pm_descriptors[id].pub.opened || pm_descriptors[id].deleted) { + return pmInvalidDeviceId; + } + /* delete function pointer is in interfaces list */ + for (i = 0; i < pm_interf_list_len; i++) { + if (strcmp(pm_interf_list[i].interf, interf) == 0) { + err = (*pm_interf_list[i].delete_fn)(id); + break; + } + } + pm_descriptors[id].deleted = TRUE; + /* (pm_internal should already be NULL because !pub.opened) */ + pm_descriptors[id].pm_internal = NULL; + pm_descriptors[id].descriptor = NULL; + return err; +} + +PMEXPORT PmError Pm_SetChannelMask(PortMidiStream *stream, int mask) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + + if (midi == NULL) + err = pmBadPtr; + else + midi->channel_mask = mask; + + return pm_errmsg(err); +} + + +PMEXPORT PmError Pm_SetFilter(PortMidiStream *stream, int32_t filters) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + + /* arg checking */ + if (midi == NULL) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + else + midi->filters = filters; + return pm_errmsg(err); +} + + +PMEXPORT PmError Pm_Close(PortMidiStream *stream) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + + pm_hosterror = FALSE; + /* arg checking */ + if (midi == NULL) /* midi must point to something */ + err = pmBadPtr; + /* if it is an open device, the device_id will be valid */ + else if (midi->device_id < 0 || midi->device_id >= pm_descriptor_len) + err = pmBadPtr; + /* and the device should be in the opened state */ + else if (!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + + if (err != pmNoError) + goto error_return; + + /* close the device */ + err = (*midi->dictionary->close)(midi); + /* even if an error occurred, continue with cleanup */ + pm_descriptors[midi->device_id].pm_internal = NULL; + pm_descriptors[midi->device_id].pub.opened = FALSE; + if (midi->queue) Pm_QueueDestroy(midi->queue); + pm_free(midi); +error_return: + /* system dependent code must set pm_hosterror and + * pm_hosterror_text if a pmHostError occurs. + */ + return pm_errmsg(err); +} + +PmError Pm_Synchronize(PortMidiStream* stream) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err = pmNoError; + if (midi == NULL) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.output) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + else + midi->first_message = TRUE; + return err; +} + +PMEXPORT PmError Pm_Abort(PortMidiStream* stream) +{ + PmInternal *midi = (PmInternal *) stream; + PmError err; + /* arg checking */ + if (midi == NULL) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.output) + err = pmBadPtr; + else if (!pm_descriptors[midi->device_id].pub.opened) + err = pmBadPtr; + else + err = (*midi->dictionary->abort)(midi); + + if (err == pmHostError) { + midi->dictionary->check_host_error(midi); + } + return pm_errmsg(err); +} + + + +/* pm_channel_filtered returns non-zero if the channel mask is + blocking the current channel */ +#define pm_channel_filtered(status, mask) \ + ((((status) & 0xF0) != 0xF0) && (!(Pm_Channel((status) & 0x0F) & (mask)))) + + +/* The following two functions will checks to see if a MIDI message + matches the filtering criteria. Since the sysex routines only want + to filter realtime messages, we need to have separate routines. + */ + + +/* pm_realtime_filtered returns non-zero if the filter will kill the + current message. Note that only realtime messages are checked here. + */ +#define pm_realtime_filtered(status, filters) \ + ((((status) & 0xF0) == 0xF0) && ((1 << ((status) & 0xF)) & (filters))) + +/* + return ((status == MIDI_ACTIVE) && (filters & PM_FILT_ACTIVE)) + || ((status == MIDI_CLOCK) && (filters & PM_FILT_CLOCK)) + || ((status == MIDI_START) && (filters & PM_FILT_PLAY)) + || ((status == MIDI_STOP) && (filters & PM_FILT_PLAY)) + || ((status == MIDI_CONTINUE) && (filters & PM_FILT_PLAY)) + || ((status == MIDI_F9) && (filters & PM_FILT_F9)) + || ((status == MIDI_FD) && (filters & PM_FILT_FD)) + || ((status == MIDI_RESET) && (filters & PM_FILT_RESET)) + || ((status == MIDI_MTC) && (filters & PM_FILT_MTC)) + || ((status == MIDI_SONGPOS) && (filters & PM_FILT_SONG_POSITION)) + || ((status == MIDI_SONGSEL) && (filters & PM_FILT_SONG_SELECT)) + || ((status == MIDI_TUNE) && (filters & PM_FILT_TUNE)); +}*/ + + +/* pm_status_filtered returns non-zero if a filter will kill the + current message, based on status. Note that sysex and real time are + not checked. It is up to the subsystem (winmm, core midi, alsa) to + filter sysex, as it is handled more easily and efficiently at that + level. Realtime message are filtered in pm_realtime_filtered. + */ +#define pm_status_filtered(status, filters) \ + ((1 << (16 + ((status) >> 4))) & (filters)) + + +/* + return ((status == MIDI_NOTE_ON) && (filters & PM_FILT_NOTE)) + || ((status == MIDI_NOTE_OFF) && (filters & PM_FILT_NOTE)) + || ((status == MIDI_CHANNEL_AT) && + (filters & PM_FILT_CHANNEL_AFTERTOUCH)) + || ((status == MIDI_POLY_AT) && (filters & PM_FILT_POLY_AFTERTOUCH)) + || ((status == MIDI_PROGRAM) && (filters & PM_FILT_PROGRAM)) + || ((status == MIDI_CONTROL) && (filters & PM_FILT_CONTROL)) + || ((status == MIDI_PITCHBEND) && (filters & PM_FILT_PITCHBEND)); + +} +*/ + +static void pm_flush_sysex(PmInternal *midi, PmTimestamp timestamp) +{ + PmEvent event; + + /* there may be nothing in the buffer */ + if (midi->message_count == 0) return; /* nothing to flush */ + + event.message = midi->message; + event.timestamp = timestamp; + /* copied from pm_read_short, avoids filtering */ + if (Pm_Enqueue(midi->queue, &event) == pmBufferOverflow) { + midi->sysex_in_progress = FALSE; + } + midi->message_count = 0; + midi->message = 0; +} + + +/* pm_read_short and pm_read_bytes + are the interface between system-dependent MIDI input handlers + and the system-independent PortMIDI code. + The input handler MUST obey these rules: + 1) all short input messages must be sent to pm_read_short, which + enqueues them to a FIFO for the application. + 2) each buffer of sysex bytes should be reported by calling pm_read_bytes + (which sets midi->sysex_in_progress). After the eox byte, + pm_read_bytes will clear sysex_in_progress + */ + +/* pm_read_short is the place where all input messages arrive from + system-dependent code such as pmwinmm.c. Here, the messages + are entered into the PortMidi input buffer. + */ +void pm_read_short(PmInternal *midi, PmEvent *event) +{ + int status; + /* arg checking */ + assert(midi != NULL); + /* midi filtering is applied here */ + status = Pm_MessageStatus(event->message); + if (!pm_status_filtered(status, midi->filters) + && (!is_real_time(status) || + !pm_realtime_filtered(status, midi->filters)) + && !pm_channel_filtered(status, midi->channel_mask)) { + /* if sysex is in progress and we get a status byte, it had + better be a realtime message or the starting SYSEX byte; + otherwise, we exit the sysex_in_progress state + */ + if (midi->sysex_in_progress && (status & MIDI_STATUS_MASK)) { + /* two choices: real-time or not. If it's real-time, then + * this should be delivered as a sysex byte because it is + * embedded in a sysex message + */ + if (is_real_time(status)) { + midi->message |= (status << (8 * midi->message_count++)); + if (midi->message_count == 4) { + pm_flush_sysex(midi, event->timestamp); + } + } else { /* otherwise, it's not real-time. This interrupts + * a sysex message in progress */ + midi->sysex_in_progress = FALSE; + } + } else if (Pm_Enqueue(midi->queue, event) == pmBufferOverflow) { + midi->sysex_in_progress = FALSE; + } + } +} + + +/* pm_read_bytes -- a sequence of bytes has been read from a device. + * parse the bytes into PmEvents and put them in the queue. + * midi - the midi device + * data - the bytes + * len - the number of bytes + * timestamp - when were the bytes received? + * + * returns how many bytes processed, which is always the len parameter + */ +unsigned int pm_read_bytes(PmInternal *midi, const unsigned char *data, + int len, PmTimestamp timestamp) +{ + int i = 0; /* index into data, must not be unsigned (!) */ + PmEvent event; + event.timestamp = timestamp; + assert(midi); + + /* Since sysex messages may have embedded real-time messages, we + * cannot simply send every consecutive group of 4 bytes as sysex + * data. Instead, we insert each data byte into midi->message and + * keep count using midi->message_count. If we encounter a + * real-time message, it is sent immediately as a 1-byte PmEvent, + * while sysex bytes are sent as PmEvents in groups of 4 bytes + * until the sysex is either terminated by EOX (F7) or a + * non-real-time message is encountered, indicating that the EOX + * was dropped. + */ + + /* This is a finite state machine so that we can accept any number + * of bytes, even if they contain partial messages. + * + * midi->sysex_in_progress says we are expecting sysex message bytes + * (otherwise, expect a short message or real-time message) + * midi->message accumulates bytes to enqueue for application + * midi->message_count is the number of bytes accumulated + * midi->short_message_count is how many bytes we need in midi->message, + * therefore midi->message_count, before we have a complete message + * midi->running_status is running status or 0 if there is none + * + * Set running status when: A status byte < F0 is received. + * Clear running status when: A status byte from F0 through F7 is + * received. + * Ignore (drop) data bytes when running status is 0. + * + * Our output buffer (the application input buffer) can overflow + * at any time. If that occurs, we have to clear sysex_in_progress + * (otherwise, the buffer could be flushed and we could resume + * inserting sysex bytes into the buffer, resulting in a continuation + * of a sysex message even though a buffer full of bytes was dropped.) + * + * Since we have to parse everything and form <=4-byte PmMessages, + * we send all messages via pm_read_short, which filters messages + * according to midi->filters and clears sysex_in_progress on + * buffer overflow. This also provides a "short cut" for short + * messages that are already parsed, allowing API-specific code + * to bypass this more expensive state machine. What if we are + * getting a sysex message, but it is interrupted by a short + * message (status 80-EF) and a direct call to pm_read_short? + * Without some care, the state machine would still be in + * sysex_in_progress mode, and subsequent data bytes would be + * accumulated as more sysex data, which is wrong since you + * cannot have a short message in the middle of a sysex message. + * To avoid this problem, pm_read_short clears sysex_in_progress + * when a non-real-time short message arrives. + */ + + while (i < len) { + unsigned char byte = data[i++]; + if (is_real_time(byte)) { + event.message = byte; + pm_read_short(midi, &event); + } else if (byte & MIDI_STATUS_MASK && byte != MIDI_EOX) { + midi->message = byte; + midi->message_count = 1; + if (byte == MIDI_SYSEX) { + midi->sysex_in_progress = TRUE; + } else { + midi->sysex_in_progress = FALSE; + midi->short_message_count = pm_midi_length(midi->message); + /* maybe we're done already with a 1-byte message: */ + if (midi->short_message_count == 1) { + pm_read_short(midi, &event); + midi->message_count = 0; + } + } + } else if (midi->sysex_in_progress) { /* sysex data byte */ + /* accumulate sysex message data or EOX */ + midi->message |= (byte << (8 * midi->message_count++)); + if (midi->message_count == 4 || byte == MIDI_EOX) { + event.message = midi->message; + /* enqueue if not filtered, and then if there is overflow, + stop sysex_in_progress */ + if (!(midi->filters & PM_FILT_SYSEX) && + Pm_Enqueue(midi->queue, &event) == pmBufferOverflow) { + midi->sysex_in_progress = FALSE; + } else if (byte == MIDI_EOX) { /* continue unless EOX */ + midi->sysex_in_progress = FALSE; + } + midi->message_count = 0; + midi->message = 0; + } + } else { /* no sysex in progress, must be short message */ + if (midi->message_count == 0) { /* need a running status */ + if (midi->running_status) { + midi->message = midi->running_status; + midi->message_count = 1; + } else { /* drop data byte - not sysex and no status byte */ + continue; + } + } + midi->message |= (byte << (8 * midi->message_count++)); + if (midi->message_count == midi->short_message_count) { + event.message = midi->message; + pm_read_short(midi, &event); + } + } + } + return i; +} diff --git a/portmidi/pm_common/portmidi.h b/portmidi/pm_common/portmidi.h new file mode 100755 index 0000000..8696a73 --- /dev/null +++ b/portmidi/pm_common/portmidi.h @@ -0,0 +1,974 @@ +#ifndef PORTMIDI_PORTMIDI_H +#define PORTMIDI_PORTMIDI_H + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* + * PortMidi Portable Real-Time MIDI Library + * PortMidi API Header File + * Latest version available at: http://sourceforge.net/projects/portmedia + * + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * Copyright (c) 2001-2006 Roger B. Dannenberg + * + * 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. + * + * 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 text above constitutes the entire PortMidi license; however, + * the PortMusic community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/* CHANGELOG FOR PORTMIDI + * (see ../CHANGELOG.txt) + * + * NOTES ON HOST ERROR REPORTING: + * + * PortMidi errors (of type PmError) are generic, + * system-independent errors. When an error does not map to one of + * the more specific PmErrors, the catch-all code pmHostError is + * returned. This means that PortMidi has retained a more specific + * system-dependent error code. The caller can get more information + * by calling Pm_GetHostErrorText() to get a text string describing + * the error. Host errors can arise asynchronously from callbacks, + * * so there is no specific return code. Asynchronous errors are + * checked and reported by Pm_Poll. You can also check by calling + * Pm_HasHostError(). If this returns TRUE, Pm_GetHostErrorText() + * will return a text description of the error. + * + * NOTES ON COMPILE-TIME SWITCHES + * + * DEBUG assumes stdio and a console. Use this if you want + * automatic, simple error reporting, e.g. for prototyping. If + * you are using MFC or some other graphical interface with no + * console, DEBUG probably should be undefined. + * PM_CHECK_ERRORS more-or-less takes over error checking for + * return values, stopping your program and printing error + * messages when an error occurs. This also uses stdio for + * console text I/O. You can selectively disable this error + * checking by declaring extern int pm_check_errors; and + * setting pm_check_errors = FALSE; You can also reenable. + */ +/** + \defgroup grp_basics Basic Definitions + @{ +*/ + +#include <stdint.h> + +#ifdef _WINDLL +#define PMEXPORT __declspec(dllexport) +#else +#define PMEXPORT +#endif + +#ifndef FALSE + #define FALSE 0 +#endif +#ifndef TRUE + #define TRUE 1 +#endif + +/* default size of buffers for sysex transmission: */ +#define PM_DEFAULT_SYSEX_BUFFER_SIZE 1024 + + +typedef enum { + pmNoError = 0, /**< Normal return value indicating no error. */ + pmNoData = 0, /**< @brief No error, also indicates no data available. + * Use this constant where a value greater than zero would + * indicate data is available. + */ + pmGotData = 1, /**< A "no error" return also indicating data available. */ + pmHostError = -10000, + pmInvalidDeviceId, /**< Out of range or + * output device when input is requested or + * input device when output is requested or + * device is already opened. + */ + pmInsufficientMemory, + pmBufferTooSmall, + pmBufferOverflow, + pmBadPtr, /**< #PortMidiStream parameter is NULL or + * stream is not opened or + * stream is output when input is required or + * stream is input when output is required. */ + pmBadData, /**< Illegal midi data, e.g., missing EOX. */ + pmInternalError, + pmBufferMaxSize, /**< Buffer is already as large as it can be. */ + pmNotImplemented, /**< The function is not implemented, nothing was done. */ + pmInterfaceNotSupported, /**< The requested interface is not supported. */ + pmNameConflict, /**< Cannot create virtual device because name is taken. */ + pmDeviceRemoved /**< Output attempted after (USB) device was removed. */ + /* NOTE: If you add a new error type, you must update Pm_GetErrorText(). */ +} PmError; /**< @brief @enum PmError PortMidi error code; a common return type. + * No error is indicated by zero; errors are indicated by < 0. + */ + +/** + Pm_Initialize() is the library initialization function - call this before + using the library. + + *NOTE:* PortMidi scans for available devices when #Pm_Initialize + is called. To observe subsequent changes in the available + devices, you must shut down PortMidi by calling #Pm_Terminate and + then restart by calling #Pm_Initialize again. *IMPORTANT*: On + MacOS, #Pm_Initialize *must* always be called on the same + thread. Otherwise, changes in the available MIDI devices will + *not* be seen by PortMidi. As an example, if you start PortMidi in + a thread for processing MIDI, do not try to rescan devices by + calling #Pm_Initialize in a GUI thread. Instead, start PortMidi + the first time and every time in the GUI thread. Alternatively, + let the GUI request a restart in the MIDI thread. (These + restrictions only apply to macOS.) Speaking of threads, on all + platforms, you are allowed to call #Pm_Initialize in one thread, + yet send MIDI or poll for incoming MIDI in another + thread. However, PortMidi is not "thread safe," which means you + cannot allow threads to call PortMidi functions concurrently. + + @return pmNoError. + + PortMidi is designed to support multiple interfaces (such as ALSA, + CoreMIDI and WinMM). It is possible to return pmNoError because there + are no supported interfaces. In that case, zero devices will be + available. +*/ +PMEXPORT PmError Pm_Initialize(void); + +/** + Pm_Terminate() is the library termination function - call this after + using the library. +*/ +PMEXPORT PmError Pm_Terminate(void); + +/** Represents an open MIDI device. */ +typedef void PortMidiStream; + +/** A shorter form of #PortMidiStream. */ +#define PmStream PortMidiStream + +/** Test whether stream has a pending host error. Normally, the client + finds out about errors through returned error codes, but some + errors can occur asynchronously where the client does not + explicitly call a function, and therefore cannot receive an error + code. The client can test for a pending error using + Pm_HasHostError(). If true, the error can be accessed by calling + Pm_GetHostErrorText(). Pm_Poll() is similar to Pm_HasHostError(), + but if there is no error, it will return TRUE (1) if there is a + pending input message. +*/ +PMEXPORT int Pm_HasHostError(PortMidiStream * stream); + + +/** Translate portmidi error number into human readable message. + These strings are constants (set at compile time) so client has + no need to allocate storage. +*/ +PMEXPORT const char *Pm_GetErrorText(PmError errnum); + +/** Translate portmidi host error into human readable message. + These strings are computed at run time, so client has to allocate storage. + After this routine executes, the host error is cleared. +*/ +PMEXPORT void Pm_GetHostErrorText(char * msg, unsigned int len); + +/** Any host error msg has at most this many characters, including EOS. */ +#define PM_HOST_ERROR_MSG_LEN 256u + +/** Devices are represented as small integers. Device ids range from 0 + to Pm_CountDevices()-1. Pm_GetDeviceInfo() is used to get information + about the device, and Pm_OpenInput() and PmOpenOutput() are used to + open the device. +*/ +typedef int PmDeviceID; + +/** This PmDeviceID (constant) value represents no device and may be + returned by Pm_GetDefaultInputDeviceID() or + Pm_GetDefaultOutputDeviceID() if no default exists. +*/ +#define pmNoDevice -1 + +/** MIDI device information is returned in this structure, which is + owned by PortMidi and read-only to applications. See Pm_GetDeviceInfo(). +*/ +#define PM_DEVICEINFO_VERS 200 +typedef struct { + int structVersion; /**< @brief this internal structure version */ + const char *interf; /**< @brief underlying MIDI API, e.g. + "MMSystem" or "DirectX" */ + char *name; /**< @brief device name, e.g. "USB MidiSport 1x1" */ + int input; /**< @brief true iff input is available */ + int output; /**< @brief true iff output is available */ + int opened; /**< @brief used by generic PortMidi for error checking */ + int is_virtual; /**< @brief true iff this is/was a virtual device */ +} PmDeviceInfo; + +/** MIDI system-dependent device or driver info is passed in this + structure, which is owned by the caller. +*/ +#define PM_SYSDEPINFO_VERS 210 + +enum PmSysDepPropertyKey { + pmKeyNone = 0, /**< a "noop" key value */ + /** CoreMIDI Manufacturer name, value is string */ + pmKeyCoreMidiManufacturer = 1, + /** Linux ALSA snd_seq_port_info_set_name, value is a string. Can be + passed in PmSysDepInfo to Pm_OpenInput or Pm_OpenOutput when opening + a device. The created port will be named accordingly and will be + visible for externally made connections (subscriptions). (Linux ALSA + ports are always enabled for this, but only get application-specific + names if you give it one.) This key/value is ignored when opening + virtual ports, which are named when they are created.) */ + pmKeyAlsaPortName = 2, + /** Linux ALSA snd_seq_set_client_name, value is a string. + Can be passed in PmSysDepInfo to Pm_OpenInput or Pm_OpenOutput. + Pm_CreateVirtualInput or Pm_CreateVirtualOutput. Will override + any previously set client name and applies to all ports. */ + pmKeyAlsaClientName = 3 + /* if system-dependent code introduces more options, register + the key here to avoid conflicts. */ +}; + +/** System-dependent information can be passed when creating and opening + ports using this data structure, which stores alternating keys and + values (addresses). See `pm_test/sendvirtual.c`, `pm_test/recvvirtual.c`, + and `pm_test/testio.c` for examples. + */ +typedef struct { + int structVersion; /**< @brief this structure version */ + int length; /**< @brief number of properties in this structure */ + struct { + enum PmSysDepPropertyKey key; + const void *value; + } properties[]; +} PmSysDepInfo; + + +/** Get devices count, ids range from 0 to Pm_CountDevices()-1. */ +PMEXPORT int Pm_CountDevices(void); + +/** + Return the default device ID or pmNoDevice if there are no devices. + The result (but not pmNoDevice) can be passed to Pm_OpenMidi(). + + The use of these functions is not recommended. There is no natural + "default device" on any system, so defaults must be set by users. + (Currently, PortMidi just returns the first device it finds as + "default", so if there *is* a default, implementors should use + pm_add_device to add system default input and output devices + first.) + + The recommended solution is pass the burden to applications. It is + easy to scan devices with PortMidi and build a device menu, and to + save menu selections in application preferences for next + time. This is my recommendation for any GUI program. For simple + command-line applications and utilities, see pm_test where all the + test programs now accept device numbers on the command line and/or + prompt for their entry. + + On linux, you can create virtual ports and use an external program + to set up inter-application and device connections. + + Some advice for preferences: MIDI devices used to be built-in or + plug-in cards, so the numbers rarely changed. Now MIDI devices are + often plug-in USB devices, so device numbers change, and you + probably need to design to reinitialize PortMidi to rescan + devices. MIDI is pretty stateless, so this isn't a big problem, + although it means you cannot find a new device while playing or + recording MIDI. + + Since device numbering can change whenever a USB device is plugged + in, preferences should record *names* of devices rather than + device numbers. It is simple enough to use string matching to find + a prefered device, so PortMidi does not provide any built-in + lookup function. +*/ +PMEXPORT PmDeviceID Pm_GetDefaultInputDeviceID(void); + +/** @brief see PmDeviceID Pm_GetDefaultInputDeviceID() */ +PMEXPORT PmDeviceID Pm_GetDefaultOutputDeviceID(void); + +/** Find a device that matches a pattern. + + @param pattern a substring of the device name, or if the pattern + contains the two-character separator ", ", then the first part of + the pattern represents a device interface substring and the second + part after the separator represents a device name substring. + + @param is_input restricts the search to an input when true, or an + output when false. + + @return the number of the first device whose device interface + contains the interface pattern (if any), whose device name + contains the name pattern, and whose direction (input or output) + matches the #is_input parameter. If no match is found, #pmNoDevice + (-1) is returned. +*/ +PMEXPORT PmDeviceID Pm_FindDevice(char *pattern, int is_input); + + +/** Represents a millisecond clock with arbitrary start time. + This type is used for all MIDI timestamps and clocks. +*/ +typedef int32_t PmTimestamp; +typedef PmTimestamp (*PmTimeProcPtr)(void *time_info); + +/** TRUE if t1 before t2 */ +#define PmBefore(t1,t2) (((t1)-(t2)) < 0) +/** @} */ +/** + \defgroup grp_device Input/Output Devices Handling + @{ +*/ +/** Get a PmDeviceInfo structure describing a MIDI device. + + @param id the device to be queried. + + If \p id is out of range or if the device designates a deleted + virtual device, the function returns NULL. + + The returned structure is owned by the PortMidi implementation and + must not be manipulated or freed. The pointer is guaranteed to be + valid between calls to Pm_Initialize() and Pm_Terminate(). +*/ +PMEXPORT const PmDeviceInfo *Pm_GetDeviceInfo(PmDeviceID id); + +/** Open a MIDI device for input. + + @param stream the address of a #PortMidiStream pointer which will + receive a pointer to the newly opened stream. + + @param inputDevice the ID of the device to be opened (see #PmDeviceID). + + @param inputSysDepInfo a pointer to an optional system-dependent + data structure (a #PmSysDepInfo struct) containing additional + information for device setup or handle processing. This parameter + is never required for correct operation. If not used, specify + NULL. Declared `void *` here for backward compatibility. Note that + with Linux ALSA, you can use this parameter to specify a client name + and port name. + + @param bufferSize the number of input events to be buffered + waiting to be read using Pm_Read(). Messages will be lost if the + number of unread messages exceeds this value. + + @param time_proc (address of) a procedure that returns time in + milliseconds. It may be NULL, in which case a default millisecond + timebase (PortTime) is used. If the application wants to use + PortTime, it should start the timer (call Pt_Start) before calling + Pm_OpenInput or Pm_OpenOutput. If the application tries to start + the timer *after* Pm_OpenInput or Pm_OpenOutput, it may get a + ptAlreadyStarted error from Pt_Start, and the application's + preferred time resolution and callback function will be ignored. + \p time_proc result values are appended to incoming MIDI data, + normally by mapping system-provided timestamps to the \p time_proc + timestamps to maintain the precision of system-provided + timestamps. + + @param time_info is a pointer passed to time_proc. + + @return #pmNoError and places a pointer to a valid + #PortMidiStream in the stream argument. If the open operation + fails, a nonzero error code is returned (see #PMError) and + the value of stream is invalid. + + Any stream that is successfully opened should eventually be closed + by calling Pm_Close(). +*/ +PMEXPORT PmError Pm_OpenInput(PortMidiStream** stream, + PmDeviceID inputDevice, + void *inputSysDepInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info); + +/** Open a MIDI device for output. + + @param stream the address of a #PortMidiStream pointer which will + receive a pointer to the newly opened stream. + + @param outputDevice the ID of the device to be opened (see #PmDeviceID). + + @param inputSysDepInfo a pointer to an optional system-specific + data structure (a #PmSysDepInfo struct) containing additional + information for device setup or handle processing. This parameter + is never required for correct operation. If not used, specify + NULL. Declared `void *` here for backward compatibility. Note that + with Linux ALSA, you can use this parameter to specify a client name + and port name. + + @param bufferSize the number of output events to be buffered + waiting for output. In some cases -- see below -- PortMidi does + not buffer output at all and merely passes data to a lower-level + API, in which case \p bufferSize is ignored. Since MIDI speeds now + vary from 1 to 50 or more messages per ms (over USB), put some + thought into this number. E.g. if latency is 20ms and you want to + burst 100 messages in that time (5000 messages per second), you + should set \p bufferSize to at least 100. The default on Windows + assumes an average rate of 500 messages per second and in this + example, output would be slowed waiting for free buffers. + + @param latency the delay in milliseconds applied to timestamps + to determine when the output should actually occur. (If latency is + < 0, 0 is assumed.) If latency is zero, timestamps are ignored + and all output is delivered immediately. If latency is greater + than zero, output is delayed until the message timestamp plus the + latency. (NOTE: the time is measured relative to the time source + indicated by time_proc. Timestamps are absolute, not relative + delays or offsets.) In some cases, PortMidi can obtain better + timing than your application by passing timestamps along to the + device driver or hardware, so the best strategy to minimize jitter + is: wait until the real time to send the message, compute the + message, attach the *ideal* output time (not the current real + time, because some time may have elapsed), and send the + message. The \p latency will be added to the timestamp, and + provided the elapsed computation time has not exceeded \p latency, + the message will be delivered according to the timestamp. If the + real time is already past the timestamp, the message will be + delivered as soon as possible. Latency may also help you to + synchronize MIDI data to audio data by matching \p latency to the + audio buffer latency. + + @param time_proc (address of) a pointer to a procedure that + returns time in milliseconds. It may be NULL, in which case a + default millisecond timebase (PortTime) is used. If the + application wants to use PortTime, it should start the timer (call + Pt_Start) before calling #Pm_OpenInput or #Pm_OpenOutput. If the + application tries to start the timer *after* #Pm_OpenInput or + #Pm_OpenOutput, it may get a #ptAlreadyStarted error from #Pt_Start, + and the application's preferred time resolution and callback + function will be ignored. \p time_proc times are used to schedule + outgoing MIDI data (when latency is non-zero), usually by mapping + from time_proc timestamps to internal system timestamps to + maintain the precision of system-supported timing. + + @param time_info a pointer passed to time_proc. + + @return #pmNoError and places a pointer to a valid #PortMidiStream + in the stream argument. If the operation fails, a nonzero error + code is returned (see PMError) and the value of \p stream is + invalid. + + Note: ALSA appears to have a fixed-size priority queue for timed + output messages. Testing indicates the queue can hold a little + over 400 3-byte MIDI messages. Thus, you can send 10,000 + messages/second if the latency is 30ms (30ms * 10000 msgs/sec * + 0.001 sec/ms = 300 msgs), but not if the latency is 50ms + (resulting in about 500 pending messages, which is greater than + the 400 message limit). Since timestamps in ALSA are relative, + they are of less value than absolute timestamps in macOS and + Windows. This is a limitation of ALSA and apparently a design + flaw. + + Example 1: If I provide a timestamp of 5000, latency is 1, and + time_proc returns 4990, then the desired output time will be when + time_proc returns timestamp+latency = 5001. This will be 5001-4990 + = 11ms from now. + + Example 2: If I want to send at exactly 5010, and latency is 10, I + should wait until 5000, compute the messages and provide a + timestamp of 5000. As long as computation takes less than 10ms, + the message will be delivered at time 5010. + + Example 3 (recommended): It is often convenient to ignore latency. + E.g. if a sequence says to output at time 5010, just wait until + 5010, compute the message and use 5010 for the timestamp. Delivery + will then be at 5010+latency, but unless you are synchronizing to + something else, the absolute delay by latency will not matter. + + Any stream that is successfully opened should eventually be closed + by calling Pm_Close(). +*/ +PMEXPORT PmError Pm_OpenOutput(PortMidiStream** stream, + PmDeviceID outputDevice, + void *outputSysDepInfo, + int32_t bufferSize, + PmTimeProcPtr time_proc, + void *time_info, + int32_t latency); + +/** Create a virtual input device. + + @param name gives the virtual device name, which is visible to + other applications. + + @param interf is the interface (System API) used to create the + device Default interfaces are "MMSystem", "CoreMIDI" and + "ALSA". Currently, these are the only ones implemented, but future + implementations could support DirectMusic, Jack, sndio, or others. + + @param sysDepInfo contains interface-dependent additional + information (a #PmSysDepInfo struct), e.g., hints or options. This + parameter is never required for correct operation. If not used, + specify NULL. Declared `void *` here for backward compatibility. + + @return a device ID or #pmNameConflict (\p name is invalid or + already exists) or #pmInterfaceNotSupported (\p interf is does not + match a supported interface). + + The created virtual device appears to other applications as if it + is an output device. The device must be opened to obtain a stream + and read from it. + + Virtual devices are not supported by Windows (Multimedia API). Calls + on Windows do nothing except return #pmNotImplemented. +*/ +PMEXPORT PmError Pm_CreateVirtualInput(const char *name, + const char *interf, + void *sysDepInfo); + +/** Create a virtual output device. + + @param name gives the virtual device name, which is visible to + other applications. + + @param interf is the interface (System API) used to create the + device Default interfaces are "MMSystem", "CoreMIDI" and + "ALSA". Currently, these are the only ones implemented, but future + implementations could support DirectMusic, Jack, sndio, or others. + + @param sysDepInfo contains interface-dependent additional + information (a #PmSysDepInfo struct), e.g., hints or options. This + parameter is never required for correct operation. If not used, + specify NULL. Declared `void *` here for backward compatibility. + + @return a device ID or #pmInvalidDeviceId (\p name is invalid or + already exists) or #pmInterfaceNotSupported (\p interf is does not + match a supported interface). + + The created virtual device appears to other applications as if it + is an input device. The device must be opened to obtain a stream + and write to it. + + Virtual devices are not supported by Windows (Multimedia API). Calls + on Windows do nothing except return #pmNotImplemented. +*/ +PMEXPORT PmError Pm_CreateVirtualOutput(const char *name, + const char *interf, + void *sysDepInfo); + +/** Remove a virtual device. + + @param device a device ID (small integer) designating the device. + + The device is removed; other applications can no longer see or open + this virtual device, which may be either for input or output. The + device must not be open. The device ID may be reused, but existing + devices are not renumbered. This means that the device ID could be + in the range from 0 to #Pm_CountDevices(), yet the device ID does + not designate a device. In that case, passing the ID to + #Pm_GetDeviceInfo() will return NULL. + + @return #pmNoError if the device was deleted or #pmInvalidDeviceId + if the device is open, already deleted, or \p device is out of + range. +*/ +PMEXPORT PmError Pm_DeleteVirtualDevice(PmDeviceID device); + /** @} */ + +/** + @defgroup grp_events_filters Events and Filters Handling + @{ +*/ + +/* Filter bit-mask definitions */ +/** filter active sensing messages (0xFE): */ +#define PM_FILT_ACTIVE (1 << 0x0E) +/** filter system exclusive messages (0xF0): */ +#define PM_FILT_SYSEX (1 << 0x00) +/** filter MIDI clock message (0xF8) */ +#define PM_FILT_CLOCK (1 << 0x08) +/** filter play messages (start 0xFA, stop 0xFC, continue 0xFB) */ +#define PM_FILT_PLAY ((1 << 0x0A) | (1 << 0x0C) | (1 << 0x0B)) +/** filter tick messages (0xF9) */ +#define PM_FILT_TICK (1 << 0x09) +/** filter undefined FD messages */ +#define PM_FILT_FD (1 << 0x0D) +/** filter undefined real-time messages */ +#define PM_FILT_UNDEFINED PM_FILT_FD +/** filter reset messages (0xFF) */ +#define PM_FILT_RESET (1 << 0x0F) +/** filter all real-time messages */ +#define PM_FILT_REALTIME (PM_FILT_ACTIVE | PM_FILT_SYSEX | PM_FILT_CLOCK | \ + PM_FILT_PLAY | PM_FILT_UNDEFINED | PM_FILT_RESET | PM_FILT_TICK) +/** filter note-on and note-off (0x90-0x9F and 0x80-0x8F */ +#define PM_FILT_NOTE ((1 << 0x19) | (1 << 0x18)) +/** filter channel aftertouch (most midi controllers use this) (0xD0-0xDF)*/ +#define PM_FILT_CHANNEL_AFTERTOUCH (1 << 0x1D) +/** per-note aftertouch (0xA0-0xAF) */ +#define PM_FILT_POLY_AFTERTOUCH (1 << 0x1A) +/** filter both channel and poly aftertouch */ +#define PM_FILT_AFTERTOUCH (PM_FILT_CHANNEL_AFTERTOUCH | \ + PM_FILT_POLY_AFTERTOUCH) +/** Program changes (0xC0-0xCF) */ +#define PM_FILT_PROGRAM (1 << 0x1C) +/** Control Changes (CC's) (0xB0-0xBF)*/ +#define PM_FILT_CONTROL (1 << 0x1B) +/** Pitch Bender (0xE0-0xEF*/ +#define PM_FILT_PITCHBEND (1 << 0x1E) +/** MIDI Time Code (0xF1)*/ +#define PM_FILT_MTC (1 << 0x01) +/** Song Position (0xF2) */ +#define PM_FILT_SONG_POSITION (1 << 0x02) +/** Song Select (0xF3)*/ +#define PM_FILT_SONG_SELECT (1 << 0x03) +/** Tuning request (0xF6) */ +#define PM_FILT_TUNE (1 << 0x06) +/** All System Common messages (mtc, song position, song select, tune request) */ +#define PM_FILT_SYSTEMCOMMON (PM_FILT_MTC | PM_FILT_SONG_POSITION | \ + PM_FILT_SONG_SELECT | PM_FILT_TUNE) + + +/* Set filters on an open input stream to drop selected input types. + + @param stream an open MIDI input stream. + + @param filters indicate message types to filter (block). + + @return #pmNoError or an error code. + + By default, only active sensing messages are filtered. + To prohibit, say, active sensing and sysex messages, call + Pm_SetFilter(stream, PM_FILT_ACTIVE | PM_FILT_SYSEX); + + Filtering is useful when midi routing or midi thru functionality + is being provided by the user application. + For example, you may want to exclude timing messages (clock, MTC, + start/stop/continue), while allowing note-related messages to pass. + Or you may be using a sequencer or drum-machine for MIDI clock + information but want to exclude any notes it may play. + */ +PMEXPORT PmError Pm_SetFilter(PortMidiStream* stream, int32_t filters); + +/** Create a mask that filters one channel. */ +#define Pm_Channel(channel) (1<<(channel)) + +/** Filter incoming messages based on channel. + + @param stream an open MIDI input stream. + + @param mask indicates channels to be received. + + @return #pmNoError or an error code. + + The \p mask is a 16-bit bitfield corresponding to appropriate channels. + The #Pm_Channel macro can assist in calling this function. + I.e. to receive only input on channel 1, call with + Pm_SetChannelMask(Pm_Channel(1)); + Multiple channels should be OR'd together, like + Pm_SetChannelMask(Pm_Channel(10) | Pm_Channel(11)) + + Note that channels are numbered 0 to 15 (not 1 to 16). Most + synthesizer and interfaces number channels starting at 1, but + PortMidi numbers channels starting at 0. + + All channels are allowed by default +*/ +PMEXPORT PmError Pm_SetChannelMask(PortMidiStream *stream, int mask); + +/** Terminate outgoing messages immediately. + + @param stream an open MIDI output stream. + + @result #pmNoError or an error code. + + The caller should immediately close the output port; this call may + result in transmission of a partial MIDI message. There is no + abort for Midi input because the user can simply ignore messages + in the buffer and close an input device at any time. If the + specified behavior cannot be achieved through the system-level + interface (ALSA, CoreMIDI, etc.), the behavior may be that of + Pm_Close(). + */ +PMEXPORT PmError Pm_Abort(PortMidiStream* stream); + +/** Close a midi stream, flush any pending buffers if possible. + + @param stream an open MIDI input or output stream. + + @result #pmNoError or an error code. + + If the system-level interface (ALSA, CoreMIDI, etc.) does not + support flushing remaining messages, the behavior may be one of + the following (most preferred first): block until all pending + timestamped messages are delivered; deliver messages to a server + or kernel process for later delivery but return immediately; drop + messages (as in Pm_Abort()). Therefore, to be safe, applications + should wait until the output queue is empty before calling + Pm_Close(). E.g. calling Pt_Sleep(100 + latency); will give a + 100ms "cushion" beyond latency (if any) before closing. +*/ +PMEXPORT PmError Pm_Close(PortMidiStream* stream); + +/** (re)synchronize to the time_proc passed when the stream was opened. + + @param stream an open MIDI input or output stream. + + @result #pmNoError or an error code. + + Typically, this is used when the stream must be opened before the + time_proc reference is actually advancing. In this case, message + timing may be erratic, but since timestamps of zero mean "send + immediately," initialization messages with zero timestamps can be + written without a functioning time reference and without + problems. Before the first MIDI message with a non-zero timestamp + is written to the stream, the time reference must begin to advance + (for example, if the time_proc computes time based on audio + samples, time might begin to advance when an audio stream becomes + active). After time_proc return values become valid, and BEFORE + writing the first non-zero timestamped MIDI message, call + Pm_Synchronize() so that PortMidi can observe the difference + between the current time_proc value and its MIDI stream time. + + In the more normal case where time_proc values advance + continuously, there is no need to call #Pm_Synchronize. PortMidi + will always synchronize at the first output message and + periodically thereafter. +*/ +PMEXPORT PmError Pm_Synchronize(PortMidiStream* stream); + + +/** Encode a short Midi message into a 32-bit word. If data1 + and/or data2 are not present, use zero. +*/ +#define Pm_Message(status, data1, data2) \ + ((((data2) << 16) & 0xFF0000) | \ + (((data1) << 8) & 0xFF00) | \ + ((status) & 0xFF)) +/** Extract the status field from a 32-bit midi message. */ +#define Pm_MessageStatus(msg) ((msg) & 0xFF) +/** Extract the 1st data field (e.g., pitch) from a 32-bit midi message. */ +#define Pm_MessageData1(msg) (((msg) >> 8) & 0xFF) +/** Extract the 2nd data field (e.g., velocity) from a 32-bit midi message. */ +#define Pm_MessageData2(msg) (((msg) >> 16) & 0xFF) + +typedef uint32_t PmMessage; /**< @brief see #PmEvent */ +/** + All MIDI data comes in the form of PmEvent structures. A sysex + message is encoded as a sequence of PmEvent structures, with each + structure carrying 4 bytes of the message, i.e. only the first + PmEvent carries the status byte. + + All other MIDI messages take 1 to 3 bytes and are encoded in a whole + PmMessage with status in the low-order byte and remaining bytes + unused, i.e., a 3-byte note-on message will occupy 3 low-order bytes + of PmMessage, leaving the high-order byte unused. + + Note that MIDI allows nested messages: the so-called "real-time" MIDI + messages can be inserted into the MIDI byte stream at any location, + including within a sysex message. MIDI real-time messages are one-byte + messages used mainly for timing (see the MIDI spec). PortMidi retains + the order of non-real-time MIDI messages on both input and output, but + it does not specify exactly how real-time messages are processed. This + is particulary problematic for MIDI input, because the input parser + must either prepare to buffer an unlimited number of sysex message + bytes or to buffer an unlimited number of real-time messages that + arrive embedded in a long sysex message. To simplify things, the input + parser is allowed to pass real-time MIDI messages embedded within a + sysex message, and it is up to the client to detect, process, and + remove these messages as they arrive. + + When receiving sysex messages, the sysex message is terminated + by either an EOX status byte (anywhere in the 4 byte messages) or + by a non-real-time status byte in the low order byte of the message. + If you get a non-real-time status byte but there was no EOX byte, it + means the sysex message was somehow truncated. This is not + considered an error; e.g., a missing EOX can result from the user + disconnecting a MIDI cable during sysex transmission. + + A real-time message can occur within a sysex message. A real-time + message will always occupy a full PmEvent with the status byte in + the low-order byte of the PmEvent message field. (This implies that + the byte-order of sysex bytes and real-time message bytes may not + be preserved -- for example, if a real-time message arrives after + 3 bytes of a sysex message, the real-time message will be delivered + first. The first word of the sysex message will be delivered only + after the 4th byte arrives, filling the 4-byte PmEvent message field. + + The timestamp field is observed when the output port is opened with + a non-zero latency. A timestamp of zero means "use the current time", + which in turn means to deliver the message with a delay of + latency (the latency parameter used when opening the output port.) + Do not expect PortMidi to sort data according to timestamps -- + messages should be sent in the correct order, and timestamps MUST + be non-decreasing. See also "Example" for Pm_OpenOutput() above. + + A sysex message will generally fill many #PmEvent structures. On + output to a #PortMidiStream with non-zero latency, the first timestamp + on sysex message data will determine the time to begin sending the + message. PortMidi implementations may ignore timestamps for the + remainder of the sysex message. + + On input, the timestamp ideally denotes the arrival time of the + status byte of the message. The first timestamp on sysex message + data will be valid. Subsequent timestamps may denote + when message bytes were actually received, or they may be simply + copies of the first timestamp. + + Timestamps for nested messages: If a real-time message arrives in + the middle of some other message, it is enqueued immediately with + the timestamp corresponding to its arrival time. The interrupted + non-real-time message or 4-byte packet of sysex data will be enqueued + later. The timestamp of interrupted data will be equal to that of + the interrupting real-time message to insure that timestamps are + non-decreasing. + */ +typedef struct { + PmMessage message; + PmTimestamp timestamp; +} PmEvent; + +/** @} */ + +/** \defgroup grp_io Reading and Writing Midi Messages + @{ +*/ +/** Retrieve midi data into a buffer. + + @param stream the open input stream. + + @return the number of events read, or, if the result is negative, + a #PmError value will be returned. + + The Buffer Overflow Problem + + The problem: if an input overflow occurs, data will be lost, + ultimately because there is no flow control all the way back to + the data source. When data is lost, the receiver should be + notified and some sort of graceful recovery should take place, + e.g. you shouldn't resume receiving in the middle of a long sysex + message. + + With a lock-free fifo, which is pretty much what we're stuck with + to enable portability to the Mac, it's tricky for the producer and + consumer to synchronously reset the buffer and resume normal + operation. + + Solution: the entire buffer managed by PortMidi will be flushed + when an overflow occurs. The consumer (Pm_Read()) gets an error + message (#pmBufferOverflow) and ordinary processing resumes as + soon as a new message arrives. The remainder of a partial sysex + message is not considered to be a "new message" and will be + flushed as well. +*/ +PMEXPORT int Pm_Read(PortMidiStream *stream, PmEvent *buffer, int32_t length); + +/** Test whether input is available. + + @param stream an open input stream. + + @return TRUE, FALSE, or an error value. + + If there was an asynchronous error, pmHostError is returned and you must + call again to determine if input is (also) available. + + You should probably *not* use this function. Call Pm_Read() + instead. If it returns 0, then there is no data available. It is + possible for Pm_Poll() to return TRUE before the complete message + is available, so Pm_Read() could return 0 even after Pm_Poll() + returns TRUE. Only call Pm_Poll() if you want to know that data is + probably available even though you are not ready to receive data. +*/ +PMEXPORT PmError Pm_Poll(PortMidiStream *stream); + +/** Write MIDI data from a buffer. + + @param stream an open output stream. + + @param buffer (address of) an array of MIDI event data. + + @param length the length of the \p buffer. + + @return TRUE, FALSE, or an error value. + + \b buffer may contain: + - short messages + - sysex messages that are converted into a sequence of PmEvent + structures, e.g. sending data from a file or forwarding them + from midi input, with 4 SysEx bytes per PmEvent message, + low-order byte first, until the last message, which may + contain from 1 to 4 bytes ending in MIDI EOX (0xF7). + - PortMidi allows 1-byte real-time messages to be embedded + within SysEx messages, but only on 4-byte boundaries so + that SysEx data always uses a full 4 bytes (except possibly + at the end). Each real-time message always occupies a full + PmEvent (3 of the 4 bytes in the PmEvent's message are + ignored) even when embedded in a SysEx message. + + Use Pm_WriteSysEx() to write a sysex message stored as a contiguous + array of bytes. + + Sysex data may contain embedded real-time messages. + + \p buffer is managed by the caller. The buffer may be destroyed + as soon as this call returns. +*/ +PMEXPORT PmError Pm_Write(PortMidiStream *stream, PmEvent *buffer, + int32_t length); + +/** Write a timestamped non-system-exclusive midi message. + + @param stream an open output stream. + + @param when timestamp for the event. + + @param msg the data for the event. + + @result #pmNoError or an error code. + + Messages are delivered in order, and timestamps must be + non-decreasing. (But timestamps are ignored if the stream was + opened with latency = 0, and otherwise, non-decreasing timestamps + are "corrected" to the lowest valid value.) +*/ +PMEXPORT PmError Pm_WriteShort(PortMidiStream *stream, PmTimestamp when, + PmMessage msg); + +/** Write a timestamped system-exclusive midi message. + + @param stream an open output stream. + + @param when timestamp for the event. + + @param msg the sysex message, terminated with an EOX status byte. + + @result #pmNoError or an error code. + + \p msg is managed by the caller and may be destroyed when this + call returns. +*/ +PMEXPORT PmError Pm_WriteSysEx(PortMidiStream *stream, PmTimestamp when, + unsigned char *msg); + +/** @} */ + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* PORTMIDI_PORTMIDI_H */ diff --git a/portmidi/pm_haiku/pmhaiku.cpp b/portmidi/pm_haiku/pmhaiku.cpp new file mode 100644 index 0000000..0c592f1 --- /dev/null +++ b/portmidi/pm_haiku/pmhaiku.cpp @@ -0,0 +1,473 @@ +/* pmhaiku.cpp -- PortMidi os-dependent code */ + +#include <stdio.h> +#include <stdlib.h> +#include <vector> +#include <MidiConsumer.h> +#include <MidiEndpoint.h> +#include <MidiProducer.h> +#include <MidiRoster.h> +#include <MidiSynth.h> +#include "portmidi.h" +#include "pmutil.h" +#include "pminternal.h" + +namespace { + struct PmInputConsumer : BMidiLocalConsumer { + PmInputConsumer(PmInternal *midi) : + BMidiLocalConsumer("PortMidi input consumer"), + midi(midi) + { + } + + + void Data(uchar *data, size_t length, bool atomic, bigtime_t time) + { + if (!atomic) + return; // should these be also supported? + + if (data[0] == B_SYS_EX_START) { + pm_read_bytes(midi, data, length, time / 1000); + } else { + PmEvent event; + switch (length) { + case 1: + event.message = Pm_Message(data[0], 0, 0); + break; + case 2: + event.message = Pm_Message(data[0], data[1], 0); + break; + case 3: + event.message = Pm_Message(data[0], data[1], data[2]); + break; + default: + printf("Unexpected message length for short message, got %" B_PRIuSIZE "\n", length); + break; + } + event.timestamp = time / 1000; + pm_read_short(midi, &event); + } + } + + private: + PmInternal *midi; + }; + + struct PmOutputInfo { + BMidiLocalProducer *producer; + std::vector<unsigned char> sysexBuffer; + }; + + struct PmSynthOutputInfo { + BMidiSynth midiSynth; + std::vector<unsigned char> sysexBuffer; + }; + + + PmTimestamp synchronize(PmInternal *midi) + { + return 0; + } + + + PmError in_open(PmInternal *midi, void *driverInfo) + { + int32 endpointID = (int32)(intptr_t)pm_descriptors[midi->device_id].descriptor; + BMidiProducer *producer = BMidiRoster::FindProducer(endpointID); + if (!producer) + return pmInvalidDeviceId; + PmInputConsumer *consumer = new PmInputConsumer(midi); + status_t status = producer->Connect(consumer); + if (status != B_OK) { + consumer->Release(); + producer->Release(); + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + midi->api_info = consumer; + producer->Release(); + return pmNoError; + } + + + PmError in_abort(PmInternal *midi) + { + return pmNoError; + } + + + PmError in_close(PmInternal *midi) + { + int32 endpointID = (int32)(intptr_t)pm_descriptors[midi->device_id].descriptor; + BMidiProducer *producer = BMidiRoster::FindProducer(endpointID); + if (!producer) + return pmInvalidDeviceId; + PmInputConsumer *consumer = (PmInputConsumer*)midi->api_info; + status_t status = producer->Disconnect(consumer); + if (status != B_OK) { + consumer->Release(); + producer->Release(); + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + consumer->Release(); + midi->api_info = NULL; + producer->Release(); + return pmNoError; + } + + + PmError out_open(PmInternal *midi, void *driverInfo) + { + int32 endpointID = (int32)(intptr_t)pm_descriptors[midi->device_id].descriptor; + BMidiConsumer *consumer = BMidiRoster::FindConsumer(endpointID); + if (!consumer) + return pmInvalidDeviceId; + BMidiLocalProducer *producer = new BMidiLocalProducer("PortMidi output producer"); + status_t status = producer->Connect(consumer); + if (status != B_OK) { + consumer->Release(); + producer->Release(); + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + PmOutputInfo *info = new PmOutputInfo; + info->producer = producer; + midi->api_info = info; + consumer->Release(); + return pmNoError; + } + + + PmError out_abort(PmInternal *midi) + { + return pmNoError; + } + + + PmError out_close(PmInternal *midi) + { + int32 endpointID = (int32)(intptr_t)pm_descriptors[midi->device_id].descriptor; + BMidiConsumer *consumer = BMidiRoster::FindConsumer(endpointID); + if (!consumer) + return pmInvalidDeviceId; + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + status_t status = info->producer->Disconnect(consumer); + if (status != B_OK) { + consumer->Release(); + midi->api_info = NULL; + info->producer->Release(); + delete info; + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + consumer->Release(); + midi->api_info = NULL; + info->producer->Release(); + delete info; + return pmNoError; + } + + + PmError write_short(PmInternal *midi, PmEvent *buffer) + { + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + uchar data[3]; + data[0] = Pm_MessageStatus(buffer->message); + data[1] = Pm_MessageData1(buffer->message); + data[2] = Pm_MessageData2(buffer->message); + size_t length = pm_midi_length(data[0]); + + info->producer->SprayData(data, length, true, buffer->timestamp * 1000); + + // TODO: handle latency != 0 + return pmNoError; + } + + + PmError begin_sysex(PmInternal *midi, PmTimestamp timestamp) + { + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + info->sysexBuffer.clear(); + return pmNoError; + } + + + PmError end_sysex(PmInternal *midi, PmTimestamp timestamp) + { + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + info->producer->SpraySystemExclusive(&info->sysexBuffer[0], info->sysexBuffer.size(), timestamp * 1000); + info->sysexBuffer.clear(); + return pmNoError; + } + + + PmError write_byte(PmInternal *midi, unsigned char byte, PmTimestamp timestamp) + { + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + info->sysexBuffer.push_back(byte); + return pmNoError; + } + + + PmError write_realtime(PmInternal *midi, PmEvent *buffer) + { + PmOutputInfo *info = (PmOutputInfo*)midi->api_info; + info->producer->SpraySystemRealTime(Pm_MessageStatus(buffer->message), buffer->timestamp * 1000); + return pmNoError; + } + + + PmError synth_open(PmInternal *midi, void *driverInfo) + { + PmSynthOutputInfo *info = new PmSynthOutputInfo; + info->midiSynth.EnableInput(true, true); + midi->api_info = info; + return pmNoError; + } + + + PmError synth_abort(PmInternal *midi) + { + return pmNoError; + } + + + PmError synth_close(PmInternal *midi) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + delete info; + midi->api_info = NULL; + return pmNoError; + } + + + PmError write_short_synth(PmInternal *midi, PmEvent *buffer) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + uchar data[3]; + data[0] = Pm_MessageStatus(buffer->message); + data[1] = Pm_MessageData1(buffer->message); + data[2] = Pm_MessageData2(buffer->message); + + switch(data[0] & 0xf0) { + case B_NOTE_OFF: + info->midiSynth.NoteOff((data[0] & 0x0f) + 1, data[1], data[2], buffer->timestamp); + break; + case B_NOTE_ON: + info->midiSynth.NoteOn((data[0] & 0x0f) + 1, data[1], data[2], buffer->timestamp); + break; + case B_KEY_PRESSURE: + info->midiSynth.KeyPressure((data[0] & 0x0f + 1), data[1], data[2], buffer->timestamp); + break; + case B_CONTROL_CHANGE: + info->midiSynth.ControlChange((data[0] & 0x0f) + 1, data[1], data[2], buffer->timestamp); + break; + case B_PROGRAM_CHANGE: + info->midiSynth.ProgramChange((data[0] & 0x0f) + 1, data[1], buffer->timestamp); + break; + case B_CHANNEL_PRESSURE: + info->midiSynth.ChannelPressure((data[0] & 0x0f) + 1, data[1], buffer->timestamp); + break; + case B_PITCH_BEND: + info->midiSynth.PitchBend((data[0] & 0x0f) + 1, data[1], data[2], buffer->timestamp); + break; + } + + // TODO: handle latency != 0 + return pmNoError; + } + + + PmError begin_sysex_synth(PmInternal *midi, PmTimestamp timestamp) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + info->sysexBuffer.clear(); + return pmNoError; + } + + + PmError end_sysex_synth(PmInternal *midi, PmTimestamp timestamp) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + info->midiSynth.SystemExclusive(&info->sysexBuffer[0], info->sysexBuffer.size(), timestamp); + info->sysexBuffer.clear(); + return pmNoError; + } + + + PmError write_byte_synth(PmInternal *midi, unsigned char byte, PmTimestamp timestamp) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + info->sysexBuffer.push_back(byte); + return pmNoError; + } + + + PmError write_realtime_synth(PmInternal *midi, PmEvent *buffer) + { + PmSynthOutputInfo *info = (PmSynthOutputInfo*)midi->api_info; + info->midiSynth.SystemRealTime(Pm_MessageStatus(buffer->message), buffer->timestamp); + return pmNoError; + } + + + PmError write_flush(PmInternal *midi, PmTimestamp timestamp) + { + return pmNoError; + } + + + unsigned int check_host_error(PmInternal *midi) + { + return 0; + } + + + pm_fns_node pm_in_dictionary = { + none_write_short, + none_sysex, + none_sysex, + none_write_byte, + none_write_short, + none_write_flush, + synchronize, + in_open, + in_abort, + in_close, + success_poll, + check_host_error + }; + + pm_fns_node pm_out_dictionary = { + write_short, + begin_sysex, + end_sysex, + write_byte, + write_realtime, + write_flush, + synchronize, + out_open, + out_abort, + out_close, + none_poll, + check_host_error + }; + + + pm_fns_node pm_synth_dictionary = { + write_short_synth, + begin_sysex_synth, + end_sysex_synth, + write_byte_synth, + write_realtime_synth, + write_flush, + synchronize, + synth_open, + synth_abort, + synth_close, + none_poll, + check_host_error + }; + + + PmError create_virtual(int is_input, const char *name, void *driverInfo) + { + BMidiEndpoint *endpoint = is_input ? (BMidiEndpoint*)new BMidiLocalProducer(name) : new BMidiLocalConsumer(name); + if (!endpoint->IsValid()) { + endpoint->Release(); + strcpy(pm_hosterror_text, "Endpoint could not be created"); + pm_hosterror = TRUE; + return pmHostError; + } + status_t status = endpoint->Register(); + if (status != B_OK) { + endpoint->Release(); + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + return pm_add_device(const_cast<char*>("Haiku MIDI kit"), name, is_input, TRUE, (void*)(intptr_t)endpoint->ID(), is_input ? &pm_in_dictionary : &pm_out_dictionary); + } + + PmError delete_virtual(PmDeviceID id) + { + int32 endpointID = (int32)(intptr_t)pm_descriptors[id].descriptor; + BMidiEndpoint *endpoint = BMidiRoster::FindEndpoint(endpointID); + //TODO: handle connected producers and consumers + status_t status = endpoint->Unregister(); + // release twice to actually free the endpoint (FindEndpoint increases the ref-count) + endpoint->Release(); + endpoint->Release(); + if (status != B_OK) { + strcpy(pm_hosterror_text, strerror(status)); + pm_hosterror = TRUE; + return pmHostError; + } + return pmNoError; + } +} + +extern "C" { + void pm_init() + { + pm_add_interf(const_cast<char*>("Haiku MIDI kit"), create_virtual, delete_virtual); + + pm_add_device(const_cast<char*>("Haiku MIDI kit"), "Soft Synth", FALSE, FALSE, NULL, &pm_synth_dictionary); + + int32 id = 0; + BMidiEndpoint *endpoint; + + while ((endpoint = BMidiRoster::NextEndpoint(&id)) != NULL) { + bool isInput = endpoint->IsProducer(); + pm_add_device(const_cast<char*>("Haiku MIDI kit"), endpoint->Name(), isInput, FALSE, (void*)(intptr_t)id, isInput ? &pm_in_dictionary : &pm_out_dictionary); + endpoint->Release(); + } + } + + + void pm_term() + { + int i; + for (i = 0; i < pm_descriptor_len; i++) { + PmInternal *midi = pm_descriptors[i].pm_internal; + if (midi && midi->api_info) { + // device is still open, close it + (*midi->dictionary->close)(midi); + } + if (pm_descriptors[i].pub.is_virtual && !pm_descriptors[i].deleted) { + delete_virtual(i); + } + } + } + + + PmDeviceID Pm_GetDefaultInputDeviceID() + { + Pm_Initialize(); + return pm_default_input_device_id; + } + + + PmDeviceID Pm_GetDefaultOutputDeviceID() + { + Pm_Initialize(); + return pm_default_output_device_id; + } + + + void *pm_alloc(size_t s) + { + return malloc(s); + } + + + void pm_free(void *ptr) + { + free(ptr); + } +} diff --git a/portmidi/pm_java/CMakeLists.txt b/portmidi/pm_java/CMakeLists.txt new file mode 100644 index 0000000..55a20f4 --- /dev/null +++ b/portmidi/pm_java/CMakeLists.txt @@ -0,0 +1,56 @@ +# pm_java/CMakeLists.txt -- builds pmjni + +find_package(Java) +message(STATUS "Java_JAVA_EXECUTABLE is " ${Java_JAVA_EXECUTABLE}) + +# Build pmjni +# this CMakeLists.txt is only loaded if BUILD_JAVA_NATIVE_INTERFACE +# This jni library includes portmidi sources to give just +# one library for JPortMidi users to manage rather than two. +if(UNIX) + include(FindJNI) + # message(STATUS "JAVA_JVM_LIB_PATH is " ${JAVA_JVM_LIB_PATH}) + # message(STATUS "JAVA_INCLUDE_PATH is " ${JAVA_INCLUDE_PATH}) + # note: should use JAVA_JVM_LIB_PATH, but it is not set properly + # note: user might need to set JAVA_INCLUDE_PATH manually + # + # this will probably break on BSD and other Unix systems; the fix + # depends on whether FindJNI can find Java or not. If yes, then + # we should try to rely on automatically set JAVA_INCLUDE_PATH and + # JAVA_INCLUDE_PATH2; if no, then we need to make both JAVA_INCLUDE_PATH + # and JAVA_INCLUDE_PATH2 set by user (will need clear documentation + # because JAVA_INCLUDE_PATH2 is pretty obscure) + set(JAVA_INCLUDE_PATH ${JAVA_INCLUDE_PATH-UNKNOWN} + CACHE STRING "where to find Java SDK include directory") + # libjvm.so is found relative to JAVA_INCLUDE_PATH: + if (HAIKU) + set(JAVA_INCLUDE_PATHS ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH}/haiku) + else() + set(JAVA_INCLUDE_PATHS ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH}/linux) + endif() +elseif(WIN32) + include(FindJNI) + # note: should use JAVA_JVM_LIB_PATH, but it is not set properly + set(JAVAVM_LIB ${JAVA_INCLUDE_PATH}/../lib/jvm.lib) + + set(JAVA_INCLUDE_PATHS ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2}) + # message(STATUS "JAVA_INCLUDE_PATHS: " ${JAVA_INCLUDE_PATHS}) + # message(STATUS "JAVAVM_LIB: " ${JAVAVM_LIB}) +endif() + +add_library(pmjni SHARED pmjni/pmjni.c) +target_sources(pmjni PRIVATE ${PM_LIB_PUBLIC_SRC} ${PM_LIB_PRIVATE_SRC}) +message(STATUS "Java paths ${JAVA_INCLUDE_PATHS}") +# message(STATUS "Java pmjni src: pmjni/pmjni.c ${PM_LIB_SHARED_SRC} " +# "${PM_LIB_PRIVATE_SRC}") +target_include_directories(pmjni PUBLIC ${JAVA_INCLUDE_PATHS}) +target_link_libraries(pmjni ${PM_NEEDED_LIBS}) +set_target_properties(pmjni PROPERTIES + VERSION ${LIBRARY_VERSION} + SOVERSION ${LIBRARY_SOVERSION} + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + EXECUTABLE_EXTENSION "jnilib" + MACOSX_RPATH ON) + diff --git a/portmidi/pm_java/README.txt b/portmidi/pm_java/README.txt new file mode 100644 index 0000000..d1e5ad5 --- /dev/null +++ b/portmidi/pm_java/README.txt @@ -0,0 +1,62 @@ +README.txt
+Roger B. Dannenberg
+16 Jun 2009
+updated 2021
+
+This directory implements a JNI library so that Java programs can use
+the PortMidi API. This was mainly created to implement PmDefaults, a
+program to set default input and output devices for PortMidi
+applications. Because it is rarely used, PmDefaults was dropped from
+PortMidi starting with v3. I recommend you implement per-application
+preferences and store default PortMidi device numbers for input and
+output there. (Or better yet, store device *names* since numbers can
+change if you plug in or remove USB devices.)
+
+Even without PmDefaults, a PortMidi API for Java is probably an
+improvement over other Java libraries, but there is very little MIDI
+development in Java, so I have not maintained this API. The only thing
+probably seriously wrong now is an interface to the
+Pm_CreateVirtualInput and Pm_CreateVirtualOutput functions, which are
+new additions.
+
+I will leave the code here, and if there is a demand, please either
+update it or let your needs be known. Perhaps I or someone can help.
+
+==================================================================
+
+BUILDING Java EXTERNAL LIBRARY
+
+You must have a JDK installed (Java development kit including javac
+(the Java compiler), jni.h, etc.
+
+Test java on the command line, e.g., type: javac -version
+
+Enable these options in the main CMakeLists.txt file (run CMake
+from your top-level repository directory):
+ BUILD_JAVA_NATIVE_INTERFACE
+In my Ubuntu linux with jdk-15, ccmake was unable to find my JDK, so
+I have to manually set CMake variables as follows (type 't' to see
+these in ccmake):
+ JAVA_AWT_INCLUDE_PATH /usr/lib/jvm/jdk-15/include
+ JAVA_AWT_LIBRARY /usr/lib/jvm/jdk-15/lib
+ JAVA_INCLUDE_PATH /usr/lib/jvm/jdk-15/include
+ JAVA_INCLUDE_PATH2 /usr/lib/jvm/jdk-15/include
+ JAVA_JVM_LIBRARY /usr/lib/jvm/jdk-15/lib
+Of course, your paths may differ.
+
+
+---- old implementation notes ----
+
+For Windows, we use the free software JavaExe.exe. The copy here was
+downloaded from
+
+http://software.techrepublic.com.com/abstract.aspx?kw=javaexe&docid=767485
+
+I found this page by visiting http://software.techrepublic.com.com and
+searching in the "Software" category for "JavaExe"
+
+JavaExe works by placing the JavaExe.exe file in the directory with the
+Java application jar file and then *renaming* JavaExe.exe to the name
+of the jar file, but keeping the .exe extension. (See make.bat for this
+step.) Documentation for JavaExe can be obtained by downloading the
+whole program from the URL(s) above.
diff --git a/portmidi/pm_java/jportmidi/JPortMidi.java b/portmidi/pm_java/jportmidi/JPortMidi.java new file mode 100644 index 0000000..7116e19 --- /dev/null +++ b/portmidi/pm_java/jportmidi/JPortMidi.java @@ -0,0 +1,541 @@ +package jportmidi; + +/* PortMidi is a general class intended for any Java program using + the PortMidi library. It encapsulates JPortMidiApi with a more + object-oriented interface. A single PortMidi object can manage + up to one input stream and one output stream. + + This class is not safely callable from multiple threads. It + is the client's responsibility to periodically call the Poll + method which checks for midi input and handles it. +*/ + +import jportmidi.*; +import jportmidi.JPortMidiApi.*; + +public class JPortMidi { + + // timecode to send message immediately + public final int NOW = 0; + + // midi codes + public final int MIDI_NOTE_OFF = 0x80; + public final int MIDI_NOTE_ON = 0x90; + public final int CTRL_ALL_OFF = 123; + public final int MIDI_PITCH_BEND = 0xE0; + public final int MIDI_CLOCK = 0xF8; + public final int MIDI_CONTROL = 0xB0; + public final int MIDI_PROGRAM = 0xC0; + public final int MIDI_START = 0xFA; + public final int MIDI_STOP = 0xFC; + public final int MIDI_POLY_TOUCH = 0xA0; + public final int MIDI_TOUCH = 0xD0; + + // error code -- cannot refresh device list while stream is open: + public final int pmStreamOpen = -5000; + public final int pmOutputNotOpen = -4999; + + // access to JPortMidiApi is through a single, global instance + private static JPortMidiApi pm; + // a reference count tracks how many objects have it open + private static int pmRefCount = 0; + private static int openCount = 0; + + public int error; // user can check here for error codes + private PortMidiStream input; + private PortMidiStream output; + private PmEvent buffer; + protected int timestamp; // remember timestamp from incoming messages + protected boolean trace = false; // used to print midi msgs for debugging + + + public JPortMidi() throws JPortMidiException { + if (pmRefCount == 0) { + pm = new JPortMidiApi(); + pmRefCount++; + System.out.println("Calling Pm_Initialize"); + checkError(pm.Pm_Initialize()); + System.out.println("Called Pm_Initialize"); + } + buffer = new PmEvent(); + } + + public boolean getTrace() { return trace; } + + // set the trace flag and return previous value + public boolean setTrace(boolean flag) { + boolean previous = trace; + trace = flag; + return previous; + } + + // WARNING: you must not call this if any devices are open + public void refreshDeviceLists() + throws JPortMidiException + { + if (openCount > 0) { + throw new JPortMidiException(pmStreamOpen, + "RefreshDeviceLists called while stream is open"); + } + if (trace) System.out.println("Pm_Terminate"); + checkError(pm.Pm_Terminate()); + if (trace) System.out.println("Pm_Initialize"); + checkError(pm.Pm_Initialize()); + } + + // there is no control over when/whether this is called, but it seems + // to be a good idea to close things when this object is collected + public void finalize() { + if (input != null) { + error = pm.Pm_Close(input); + } + if (input != null) { + int rslt = pm.Pm_Close(output); + // we may lose an error code from closing output, but don't + // lose any real error from closing input... + if (error == pm.pmNoError) error = rslt; + } + pmRefCount--; + if (pmRefCount == 0) { + error = pm.Pm_Terminate(); + } + } + + int checkError(int err) throws JPortMidiException + { + // note that Pm_Read and Pm_Write return positive result values + // which are not errors, so compare with >= + if (err >= pm.pmNoError) return err; + if (err == pm.pmHostError) { + throw new JPortMidiException(err, pm.Pm_GetHostErrorText()); + } else { + throw new JPortMidiException(err, pm.Pm_GetErrorText(err)); + } + } + + // ******** ACCESS TO TIME *********** + + public void timeStart(int resolution) throws JPortMidiException { + checkError(pm.Pt_TimeStart(resolution)); + } + + public void timeStop() throws JPortMidiException { + checkError(pm.Pt_TimeStop()); + } + + public int timeGet() { + return pm.Pt_Time(); + } + + public boolean timeStarted() { + return pm.Pt_TimeStarted(); + } + + // ******* QUERY DEVICE INFORMATION ********* + + public int countDevices() throws JPortMidiException { + return checkError(pm.Pm_CountDevices()); + } + + public int getDefaultInputDeviceID() throws JPortMidiException { + return checkError(pm.Pm_GetDefaultInputDeviceID()); + } + + public int getDefaultOutputDeviceID() throws JPortMidiException { + return checkError(pm.Pm_GetDefaultOutputDeviceID()); + } + + public String getDeviceInterf(int i) { + return pm.Pm_GetDeviceInterf(i); + } + + public String getDeviceName(int i) { + return pm.Pm_GetDeviceName(i); + } + + public boolean getDeviceInput(int i) { + return pm.Pm_GetDeviceInput(i); + } + + public boolean getDeviceOutput(int i) { + return pm.Pm_GetDeviceOutput(i); + } + + // ********** MIDI INTERFACE ************ + + public boolean isOpenInput() { + return input != null; + } + + public void openInput(int inputDevice, int bufferSize) + throws JPortMidiException + { + openInput(inputDevice, "", bufferSize); + } + + public void openInput(int inputDevice, String inputDriverInfo, int bufferSize) + throws JPortMidiException + { + if (isOpenInput()) pm.Pm_Close(input); + else input = new PortMidiStream(); + if (trace) { + System.out.println("openInput " + getDeviceName(inputDevice)); + } + checkError(pm.Pm_OpenInput(input, inputDevice, + inputDriverInfo, bufferSize)); + // if no exception, then increase count of open streams + openCount++; + } + + public boolean isOpenOutput() { + return output != null; + } + + public void openOutput(int outputDevice, int bufferSize, int latency) + throws JPortMidiException + { + openOutput(outputDevice, "", bufferSize, latency); + } + + public void openOutput(int outputDevice, String outputDriverInfo, + int bufferSize, int latency) throws JPortMidiException { + if (isOpenOutput()) pm.Pm_Close(output); + else output = new PortMidiStream(); + if (trace) { + System.out.println("openOutput " + getDeviceName(outputDevice)); + } + checkError(pm.Pm_OpenOutput(output, outputDevice, outputDriverInfo, + bufferSize, latency)); + // if no exception, then increase count of open streams + openCount++; + } + + public void setFilter(int filters) throws JPortMidiException { + if (input == null) return; // no effect if input not open + checkError(pm.Pm_SetFilter(input, filters)); + } + + public void setChannelMask(int mask) throws JPortMidiException { + if (input == null) return; // no effect if input not open + checkError(pm.Pm_SetChannelMask(input, mask)); + } + + public void abort() throws JPortMidiException { + if (output == null) return; // no effect if output not open + checkError(pm.Pm_Abort(output)); + } + + // In keeping with the idea that this class represents an input and output, + // there are separate Close methods for input and output streams, avoiding + // the need for clients to ever deal directly with a stream object + public void closeInput() throws JPortMidiException { + if (input == null) return; // no effect if input not open + checkError(pm.Pm_Close(input)); + input = null; + openCount--; + } + + public void closeOutput() throws JPortMidiException { + if (output == null) return; // no effect if output not open + checkError(pm.Pm_Close(output)); + output = null; + openCount--; + } + + // Poll should be called by client to process input messages (if any) + public void poll() throws JPortMidiException { + if (input == null) return; // does nothing until input is opened + while (true) { + int rslt = pm.Pm_Read(input, buffer); + checkError(rslt); + if (rslt == 0) return; // no more messages + handleMidiIn(buffer); + } + } + + public void writeShort(int when, int msg) throws JPortMidiException { + if (output == null) + throw new JPortMidiException(pmOutputNotOpen, + "Output stream not open"); + if (trace) { + System.out.println("writeShort: " + Integer.toHexString(msg)); + } + checkError(pm.Pm_WriteShort(output, when, msg)); + } + + public void writeSysEx(int when, byte msg[]) throws JPortMidiException { + if (output == null) + throw new JPortMidiException(pmOutputNotOpen, + "Output stream not open"); + if (trace) { + System.out.print("writeSysEx: "); + for (int i = 0; i < msg.length; i++) { + System.out.print(Integer.toHexString(msg[i])); + } + System.out.print("\n"); + } + checkError(pm.Pm_WriteSysEx(output, when, msg)); + } + + public int midiChanMessage(int chan, int status, int data1, int data2) { + return (((data2 << 16) & 0xFF0000) | + ((data1 << 8) & 0xFF00) | + (status & 0xF0) | + (chan & 0xF)); + } + + public int midiMessage(int status, int data1, int data2) { + return ((((data2) << 16) & 0xFF0000) | + (((data1) << 8) & 0xFF00) | + ((status) & 0xFF)); + } + + public void midiAllOff(int channel) throws JPortMidiException { + midiAllOff(channel, NOW); + } + + public void midiAllOff(int chan, int when) throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_CONTROL, CTRL_ALL_OFF, 0)); + } + + public void midiPitchBend(int chan, int value) throws JPortMidiException { + midiPitchBend(chan, value, NOW); + } + + public void midiPitchBend(int chan, int value, int when) + throws JPortMidiException { + writeShort(when, + midiChanMessage(chan, MIDI_PITCH_BEND, value, value >> 7)); + } + + public void midiClock() throws JPortMidiException { + midiClock(NOW); + } + + public void midiClock(int when) throws JPortMidiException { + writeShort(when, midiMessage(MIDI_CLOCK, 0, 0)); + } + + public void midiControl(int chan, int control, int value) + throws JPortMidiException { + midiControl(chan, control, value, NOW); + } + + public void midiControl(int chan, int control, int value, int when) + throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_CONTROL, control, value)); + } + + public void midiNote(int chan, int pitch, int vel) + throws JPortMidiException { + midiNote(chan, pitch, vel, NOW); + } + + public void midiNote(int chan, int pitch, int vel, int when) + throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_NOTE_ON, pitch, vel)); + } + + public void midiProgram(int chan, int program) + throws JPortMidiException { + midiProgram(chan, program, NOW); + } + + public void midiProgram(int chan, int program, int when) + throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_PROGRAM, program, 0)); + } + + public void midiStart() + throws JPortMidiException { + midiStart(NOW); + } + + public void midiStart(int when) + throws JPortMidiException { + writeShort(when, midiMessage(MIDI_START, 0, 0)); + } + + public void midiStop() + throws JPortMidiException { + midiStop(NOW); + } + + public void midiStop(int when) + throws JPortMidiException { + writeShort(when, midiMessage(MIDI_STOP, 0, 0)); + } + + public void midiPolyTouch(int chan, int key, int value) + throws JPortMidiException { + midiPolyTouch(chan, key, value, NOW); + } + + public void midiPolyTouch(int chan, int key, int value, int when) + throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_POLY_TOUCH, key, value)); + } + + public void midiTouch(int chan, int value) + throws JPortMidiException { + midiTouch(chan, value, NOW); + } + + public void midiTouch(int chan, int value, int when) + throws JPortMidiException { + writeShort(when, midiChanMessage(chan, MIDI_TOUCH, value, 0)); + } + + // ****** now we implement the message handlers ****** + + // an array for incoming sysex messages that can grow. + // The downside is that after getting a message, we + + private byte sysexBuffer[] = null; + private int sysexBufferIndex = 0; + + void sysexBufferReset() { + sysexBufferIndex = 0; + if (sysexBuffer == null) sysexBuffer = new byte[256]; + } + + void sysexBufferCheck() { + if (sysexBuffer.length < sysexBufferIndex + 4) { + byte bigger[] = new byte[sysexBuffer.length * 2]; + for (int i = 0; i < sysexBufferIndex; i++) { + bigger[i] = sysexBuffer[i]; + } + sysexBuffer = bigger; + } + // now we have space to write some bytes + } + + // call this to insert Sysex and EOX status bytes + // call sysexBufferAppendBytes to insert anything else + void sysexBufferAppendStatus(byte status) { + sysexBuffer[sysexBufferIndex++] = status; + } + + void sysexBufferAppendBytes(int msg, int len) { + for (int i = 0; i < len; i++) { + byte b = (byte) msg; + if ((msg & 0x80) != 0) { + if (b == 0xF7) { // end of sysex + sysexBufferAppendStatus(b); + sysex(sysexBuffer, sysexBufferIndex); + return; + } + // recursively handle embedded real-time messages + PmEvent buffer = new PmEvent(); + buffer.timestamp = timestamp; + buffer.message = b; + handleMidiIn(buffer); + } else { + sysexBuffer[sysexBufferIndex++] = b; + } + msg = msg >> 8; + } + } + + void sysexBegin(int msg) { + sysexBufferReset(); // start from 0, we have at least 256 bytes now + sysexBufferAppendStatus((byte) (msg & 0xFF)); // first byte is special + sysexBufferAppendBytes(msg >> 8, 3); // process remaining bytes + } + + public void handleMidiIn(PmEvent buffer) + { + if (trace) { + System.out.println("handleMidiIn: " + + Integer.toHexString(buffer.message)); + } + // rather than pass timestamps to every handler, where typically + // timestamps are ignored, just save the timestamp as a member + // variable where methods can access it if they want it + timestamp = buffer.timestamp; + int status = buffer.message & 0xFF; + if (status < 0x80) { + sysexBufferCheck(); // make enough space + sysexBufferAppendBytes(buffer.message, 4); // process 4 bytes + return; + } + int command = status & 0xF0; + int channel = status & 0x0F; + int data1 = (buffer.message >> 8) & 0xFF; + int data2 = (buffer.message >> 16) & 0xFF; + switch (command) { + case MIDI_NOTE_OFF: + noteOff(channel, data1, data2); break; + case MIDI_NOTE_ON: + if (data2 > 0) { + noteOn(channel, data1, data2); break; + } else { + noteOff(channel, data1); + } + break; + case MIDI_CONTROL: + control(channel, data1, data2); break; + case MIDI_POLY_TOUCH: + polyTouch(channel, data1, data2); break; + case MIDI_TOUCH: + touch(channel, data1); break; + case MIDI_PITCH_BEND: + pitchBend(channel, (data1 + (data2 << 7)) - 8192); break; + case MIDI_PROGRAM: + program(channel, data1); break; + case 0xF0: + switch (channel) { + case 0: sysexBegin(buffer.message); break; + case 1: mtcQuarterFrame(data1); + case 2: songPosition(data1 + (data2 << 7)); break; + case 3: songSelect(data1); break; + case 4: /* unused */ break; + case 5: /* unused */ break; + case 6: tuneRequest(); break; + case 7: sysexBufferAppendBytes(buffer.message, buffer.message); break; + case 8: clock(); break; + case 9: tick(); break; + case 0xA: clockStart(); break; + case 0xB: clockContinue(); break; + case 0xC: clockStop(); break; + case 0xD: /* unused */ break; + case 0xE: activeSense(); break; + case 0xF: reset(); break; + } + } + } + + // the value ranges from +8181 to -8192. The interpretation is + // synthesizer dependent. Often the range is +/- one whole step + // (two semitones), but the range is usually adjustable within + // the synthesizer. + void pitchBend(int channel, int value) { return; } + void control(int channel, int control, int value) { return; } + void noteOn(int channel, int pitch, int velocity) { return; } + // you can handle velocity in note-off if you want, but the default + // is to drop the velocity and call the simpler NoteOff handler + void noteOff(int channel, int pitch, int velocity) { + noteOff(channel, pitch); + } + // if the subclass wants to implement NoteOff with velocity, it + // should override the following to make sure all NoteOffs are handled + void noteOff(int channel, int pitch) { return; } + void program(int channel, int program) { return; } + // the byte array may be bigger than the message, length tells how + // many bytes in the array are part of the message + void sysex(byte[] msg, int length) { return; } + void polyTouch(int channel, int key, int value) { return; } + void touch(int channel, int value) { return; } + void mtcQuarterFrame(int value) { return; } + // the value is a 14-bit integer representing 16th notes + void songPosition(int value) { return; } + void songSelect(int value) { return; } + void tuneRequest() { return; } + void clock() { return; } // represents 1/24th of a quarter note + void tick() { return; } // represents 10ms + void clockStart() { return; } + void clockStop() { return; } + void clockContinue() { return; } + void activeSense() { return; } + void reset() { return; } +} diff --git a/portmidi/pm_java/jportmidi/JPortMidiApi.java b/portmidi/pm_java/jportmidi/JPortMidiApi.java new file mode 100644 index 0000000..45dd9d9 --- /dev/null +++ b/portmidi/pm_java/jportmidi/JPortMidiApi.java @@ -0,0 +1,117 @@ +package jportmidi; + +public class JPortMidiApi { + public static class PortMidiStream { + private long address; + } + public static class PmEvent { + public int message; + public int timestamp; + } + + // PmError bindings + public final int pmNoError = 0; + public final int pmNoData = 0; + public final int pmGotData = -1; + public final int pmHostError = -10000; + public final int pmInvalidDeviceId = -9999; + public final int pmInsufficientMemory = -9998; + public final int pmBufferTooSmall = -9997; + public final int pmBufferOverflow = -9996; + public final int pmBadPtr = -9995; + public final int pmBadData = -9994; + public final int pmInternalError = -9993; + public final int pmBufferMaxSize = -9992; + + static public native int Pm_Initialize(); + static public native int Pm_Terminate(); + static public native int Pm_HasHostError(PortMidiStream stream); + static public native String Pm_GetErrorText(int errnum); + static public native String Pm_GetHostErrorText(); + final int pmNoDevice = -1; + static public native int Pm_CountDevices(); + static public native int Pm_GetDefaultInputDeviceID(); + static public native int Pm_GetDefaultOutputDeviceID(); + static public native String Pm_GetDeviceInterf(int i); + static public native String Pm_GetDeviceName(int i); + static public native boolean Pm_GetDeviceInput(int i); + static public native boolean Pm_GetDeviceOutput(int i); + static public native int Pm_OpenInput(PortMidiStream stream, + int inputDevice, + String inputDriverInfo, + int bufferSize); + static public native int Pm_OpenOutput(PortMidiStream stream, + int outputDevice, + String outnputDriverInfo, + int bufferSize, + int latency); + final static public int PM_FILT_ACTIVE = (1 << 0x0E); + final static public int PM_FILT_SYSEX = (1 << 0x00); + final static public int PM_FILT_CLOCK = (1 << 0x08); + final static public int PM_FILT_PLAY = + (1 << 0x0A) | (1 << 0x0C) | (1 << 0x0B); + final static public int PM_FILT_TICK = (1 << 0x09); + final static public int PM_FILT_FD = (1 << 0x0D); + final static public int PM_FILT_UNDEFINED = PM_FILT_FD; + final static public int PM_FILT_RESET = (1 << 0x0F); + final static public int PM_FILT_REALTIME = + PM_FILT_ACTIVE | PM_FILT_SYSEX | PM_FILT_CLOCK; + final static public int PM_FILT_NOTE = (1 << 0x19) | (1 << 0x18); + final static public int PM_FILT_CHANNEL_AFTERTOUCH = (1 << 0x1D); + final static public int PM_FILT_POLY_AFTERTOUCH = (1 << 0x1A); + final static public int PM_FILT_AFTERTOUCH = + (PM_FILT_CHANNEL_AFTERTOUCH | PM_FILT_POLY_AFTERTOUCH); + final static public int PM_FILT_PROGRAM = (1 << 0x1C); + final static public int PM_FILT_CONTROL = (1 << 0x1B); + final static public int PM_FILT_PITCHBEND = (1 << 0x1E); + final static public int PM_FILT_MTC = (1 << 0x01); + final static public int PM_FILT_SONG_POSITION = (1 << 0x02); + final static public int PM_FILT_SONG_SELECT = (1 << 0x03); + final static public int PM_FILT_TUNE = (1 << 0x06); + final static public int PM_FILT_SYSTEMCOMMON = + (PM_FILT_MTC | PM_FILT_SONG_POSITION | + PM_FILT_SONG_SELECT | PM_FILT_TUNE); + static public native int Pm_SetFilter(PortMidiStream stream, int filters); + static public int Pm_Channel(int channel) { return 1 << channel; } + final static public native int Pm_SetChannelMask(PortMidiStream stream, + int mask); + final static public native int Pm_Abort(PortMidiStream stream); + final static public native int Pm_Close(PortMidiStream stream); + static public int Pm_Message(int status, int data1, int data2) { + return (((data2 << 16) & 0xFF0000) | + ((data1 << 8) & 0xFF00) | + (status & 0xFF)); + } + static public int Pm_MessageStatus(int msg) { + return msg & 0xFF; + } + static public int Pm_MessageData1(int msg) { + return (msg >> 8) & 0xFF; + } + static public int Pm_MessageData2(int msg) { + return (msg >> 16) & 0xFF; + } + // only supports reading one buffer at a time + static public native int Pm_Read(PortMidiStream stream, PmEvent buffer); + static public native int Pm_Poll(PortMidiStream stream); + // only supports writing one buffer at a time + static public native int Pm_Write(PortMidiStream stream, PmEvent buffer); + static public native int Pm_WriteShort(PortMidiStream stream, + int when, int msg); + static public native int Pm_WriteSysEx(PortMidiStream stream, + int when, byte msg[]); + + public final int ptNoError = 0; + public final int ptAlreadyStarted = -10000; + public final int ptAlreadyStopped = -9999; + public final int PtInsufficientMemory = -9998; + static public native int Pt_TimeStart(int resolution); + static public native int Pt_TimeStop(); + static public native int Pt_Time(); + static public native boolean Pt_TimeStarted(); + static { + System.out.println("Loading pmjni"); + System.loadLibrary("pmjni"); + System.out.println("done loading pmjni"); + } +} diff --git a/portmidi/pm_java/jportmidi/JPortMidiException.java b/portmidi/pm_java/jportmidi/JPortMidiException.java new file mode 100644 index 0000000..9be8aaf --- /dev/null +++ b/portmidi/pm_java/jportmidi/JPortMidiException.java @@ -0,0 +1,12 @@ +// JPortMidiException -- thrown by JPortMidi methods + +package jportmidi; + +public class JPortMidiException extends Exception { + public int error = 0; + public JPortMidiException(int err, String msg) { + super(msg); + error = err; + } +} + diff --git a/portmidi/pm_java/make.bat b/portmidi/pm_java/make.bat new file mode 100644 index 0000000..ff15c2b --- /dev/null +++ b/portmidi/pm_java/make.bat @@ -0,0 +1,50 @@ +@echo off
+
+rem This is an out-of-date script for Windows to build a Java application
+rem (PmDefaults) with PortMidi external library.xb
+
+rem Compile the java PortMidi interface classes.
+javac jportmidi/*.java
+
+rem Compile the pmdefaults application.
+javac -classpath . pmdefaults/*.java
+
+rem Temporarily copy the portmusic_logo.png file here to add to the jar file.
+copy pmdefaults\portmusic_logo.png . > nul
+
+rem Create a directory to hold the distribution.
+mkdir win32
+
+rem Attempt to copy the interface DLL to the distribution directory.
+
+if exist "..\release\pmjni.dll" goto have-dll
+
+echo "ERROR: pmjni.dll not found!"
+exit /b 1
+
+:have-dll
+copy "..\release\pmjni.dll" win32\pmjni.dll > nul
+
+rem Create a java archive (jar) file of the distribution.
+jar cmf pmdefaults\manifest.txt win32\pmdefaults.jar pmdefaults\*.class portmusic_logo.png jportmidi\*.class
+
+rem Clean up the temporary image file now that it is in the jar file.
+del portmusic_logo.png
+
+rem Copy the java execution code obtained from
+rem http://devwizard.free.fr/html/en/JavaExe.html to the distribution
+rem directory. The copy also renames the file to our desired executable
+rem name.
+copy JavaExe.exe win32\pmdefaults.exe > nul
+
+rem Integrate the icon into the executable using UpdateRsrcJavaExe from
+rem http://devwizard.free.fr
+UpdateRsrcJavaExe -run -exe=win32\pmdefaults.exe -ico=pmdefaults\pmdefaults.ico
+
+rem Copy the 32-bit windows read me file to the distribution directory.
+copy pmdefaults\readme-win32.txt win32\README.txt > nul
+
+rem Copy the license file to the distribution directory.
+copy pmdefaults\pmdefaults-license.txt win32\license.txt > nul
+
+echo "You can run pmdefaults.exe in win32"
diff --git a/portmidi/pm_java/pmjni/jportmidi_JportMidiApi.h b/portmidi/pm_java/pmjni/jportmidi_JportMidiApi.h new file mode 100644 index 0000000..2208be6 --- /dev/null +++ b/portmidi/pm_java/pmjni/jportmidi_JportMidiApi.h @@ -0,0 +1,293 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include <jni.h> +/* Header for class jportmidi_JPortMidiApi */ + +#ifndef _Included_jportmidi_JPortMidiApi +#define _Included_jportmidi_JPortMidiApi +#ifdef __cplusplus +extern "C" { +#endif +#undef jportmidi_JPortMidiApi_PM_FILT_ACTIVE +#define jportmidi_JPortMidiApi_PM_FILT_ACTIVE 16384L +#undef jportmidi_JPortMidiApi_PM_FILT_SYSEX +#define jportmidi_JPortMidiApi_PM_FILT_SYSEX 1L +#undef jportmidi_JPortMidiApi_PM_FILT_CLOCK +#define jportmidi_JPortMidiApi_PM_FILT_CLOCK 256L +#undef jportmidi_JPortMidiApi_PM_FILT_PLAY +#define jportmidi_JPortMidiApi_PM_FILT_PLAY 7168L +#undef jportmidi_JPortMidiApi_PM_FILT_TICK +#define jportmidi_JPortMidiApi_PM_FILT_TICK 512L +#undef jportmidi_JPortMidiApi_PM_FILT_FD +#define jportmidi_JPortMidiApi_PM_FILT_FD 8192L +#undef jportmidi_JPortMidiApi_PM_FILT_UNDEFINED +#define jportmidi_JPortMidiApi_PM_FILT_UNDEFINED 8192L +#undef jportmidi_JPortMidiApi_PM_FILT_RESET +#define jportmidi_JPortMidiApi_PM_FILT_RESET 32768L +#undef jportmidi_JPortMidiApi_PM_FILT_REALTIME +#define jportmidi_JPortMidiApi_PM_FILT_REALTIME 16641L +#undef jportmidi_JPortMidiApi_PM_FILT_NOTE +#define jportmidi_JPortMidiApi_PM_FILT_NOTE 50331648L +#undef jportmidi_JPortMidiApi_PM_FILT_CHANNEL_AFTERTOUCH +#define jportmidi_JPortMidiApi_PM_FILT_CHANNEL_AFTERTOUCH 536870912L +#undef jportmidi_JPortMidiApi_PM_FILT_POLY_AFTERTOUCH +#define jportmidi_JPortMidiApi_PM_FILT_POLY_AFTERTOUCH 67108864L +#undef jportmidi_JPortMidiApi_PM_FILT_AFTERTOUCH +#define jportmidi_JPortMidiApi_PM_FILT_AFTERTOUCH 603979776L +#undef jportmidi_JPortMidiApi_PM_FILT_PROGRAM +#define jportmidi_JPortMidiApi_PM_FILT_PROGRAM 268435456L +#undef jportmidi_JPortMidiApi_PM_FILT_CONTROL +#define jportmidi_JPortMidiApi_PM_FILT_CONTROL 134217728L +#undef jportmidi_JPortMidiApi_PM_FILT_PITCHBEND +#define jportmidi_JPortMidiApi_PM_FILT_PITCHBEND 1073741824L +#undef jportmidi_JPortMidiApi_PM_FILT_MTC +#define jportmidi_JPortMidiApi_PM_FILT_MTC 2L +#undef jportmidi_JPortMidiApi_PM_FILT_SONG_POSITION +#define jportmidi_JPortMidiApi_PM_FILT_SONG_POSITION 4L +#undef jportmidi_JPortMidiApi_PM_FILT_SONG_SELECT +#define jportmidi_JPortMidiApi_PM_FILT_SONG_SELECT 8L +#undef jportmidi_JPortMidiApi_PM_FILT_TUNE +#define jportmidi_JPortMidiApi_PM_FILT_TUNE 64L +#undef jportmidi_JPortMidiApi_PM_FILT_SYSTEMCOMMON +#define jportmidi_JPortMidiApi_PM_FILT_SYSTEMCOMMON 78L +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Initialize + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Initialize + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Terminate + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Terminate + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_HasHostError + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1HasHostError + (JNIEnv *, jclass, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetErrorText + * Signature: (I)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetErrorText + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetHostErrorText + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetHostErrorText + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_CountDevices + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1CountDevices + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDefaultInputDeviceID + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDefaultInputDeviceID + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDefaultOutputDeviceID + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDefaultOutputDeviceID + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDeviceInterf + * Signature: (I)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceInterf + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDeviceName + * Signature: (I)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceName + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDeviceInput + * Signature: (I)Z + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceInput + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_GetDeviceOutput + * Signature: (I)Z + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceOutput + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_OpenInput + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;ILjava/lang/String;I)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1OpenInput + (JNIEnv *, jclass, jobject, jint, jstring, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_OpenOutput + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;ILjava/lang/String;II)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1OpenOutput + (JNIEnv *, jclass, jobject, jint, jstring, jint, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_SetFilter + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;I)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1SetFilter + (JNIEnv *, jclass, jobject, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_SetChannelMask + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;I)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1SetChannelMask + (JNIEnv *, jclass, jobject, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Abort + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Abort + (JNIEnv *, jclass, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Close + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Close + (JNIEnv *, jclass, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Read + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;Ljportmidi/JPortMidiApi/PmEvent;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Read + (JNIEnv *, jclass, jobject, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Poll + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Poll + (JNIEnv *, jclass, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_Write + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;Ljportmidi/JPortMidiApi/PmEvent;)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Write + (JNIEnv *, jclass, jobject, jobject); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_WriteShort + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;II)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1WriteShort + (JNIEnv *, jclass, jobject, jint, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pm_WriteSysEx + * Signature: (Ljportmidi/JPortMidiApi/PortMidiStream;I[B)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1WriteSysEx + (JNIEnv *, jclass, jobject, jint, jbyteArray); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pt_TimeStart + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStart + (JNIEnv *, jclass, jint); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pt_TimeStop + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStop + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pt_Time + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1Time + (JNIEnv *, jclass); + +/* + * Class: jportmidi_JPortMidiApi + * Method: Pt_TimeStarted + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStarted + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif +/* Header for class jportmidi_JPortMidiApi_PmEvent */ + +#ifndef _Included_jportmidi_JPortMidiApi_PmEvent +#define _Included_jportmidi_JPortMidiApi_PmEvent +#ifdef __cplusplus +extern "C" { +#endif +#ifdef __cplusplus +} +#endif +#endif +/* Header for class jportmidi_JPortMidiApi_PortMidiStream */ + +#ifndef _Included_jportmidi_JPortMidiApi_PortMidiStream +#define _Included_jportmidi_JPortMidiApi_PortMidiStream +#ifdef __cplusplus +extern "C" { +#endif +#ifdef __cplusplus +} +#endif +#endif diff --git a/portmidi/pm_java/pmjni/pmjni.c b/portmidi/pm_java/pmjni/pmjni.c new file mode 100644 index 0000000..c60cffb --- /dev/null +++ b/portmidi/pm_java/pmjni/pmjni.c @@ -0,0 +1,354 @@ +#include "portmidi.h" +#include "porttime.h" +#include "jportmidi_JportMidiApi.h" +#include <stdio.h> + +// these macros assume JNIEnv *env is declared and valid: +// +#define CLASS(c, obj) jclass c = (*env)->GetObjectClass(env, obj) +#define ADDRESS_FID(fid, c) \ + jfieldID fid = (*env)->GetFieldID(env, c, "address", "J") +// Uses Java Long (64-bit) to make sure there is room to store a +// pointer. Cast this to a C long (either 32 or 64 bit) to match +// the size of a pointer. Finally cast int to pointer. All this +// is supposed to avoid C compiler warnings and (worse) losing +// address bits. +#define PMSTREAM(obj, fid) ((PmStream *) (intptr_t) (*env)->GetLongField(env, obj, fid)) +// Cast stream to long to convert integer to pointer, then expand +// integer to 64-bit jlong. This avoids compiler warnings. +#define SET_PMSTREAM(obj, fid, stream) \ + (*env)->SetLongField(env, obj, fid, (jlong) (intptr_t) stream) + + +/* + * Method: Pm_Initialize + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Initialize + (JNIEnv *env, jclass cl) +{ + return Pm_Initialize(); +} + + +/* + * Method: Pm_Terminate + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Terminate + (JNIEnv *env, jclass cl) +{ + return Pm_Terminate(); +} + + +/* + * Method: Pm_HasHostError + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1HasHostError + (JNIEnv *env, jclass cl, jobject jstream) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_HasHostError(PMSTREAM(jstream, fid)); +} + + +/* + * Method: Pm_GetErrorText + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetErrorText + (JNIEnv *env, jclass cl, jint i) +{ + return (*env)->NewStringUTF(env, Pm_GetErrorText(i)); +} + + +/* + * Method: Pm_GetHostErrorText + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetHostErrorText + (JNIEnv *env, jclass cl) +{ + char msg[PM_HOST_ERROR_MSG_LEN]; + Pm_GetHostErrorText(msg, PM_HOST_ERROR_MSG_LEN); + return (*env)->NewStringUTF(env, msg); +} + + +/* + * Method: Pm_CountDevices + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1CountDevices + (JNIEnv *env, jclass cl) +{ + return Pm_CountDevices(); +} + + +/* + * Method: Pm_GetDefaultInputDeviceID + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDefaultInputDeviceID + (JNIEnv *env, jclass cl) +{ + return Pm_GetDefaultInputDeviceID(); +} + + +/* + * Method: Pm_GetDefaultOutputDeviceID + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDefaultOutputDeviceID + (JNIEnv *env, jclass cl) +{ + return Pm_GetDefaultOutputDeviceID(); +} + + +/* + * Method: Pm_GetDeviceInterf + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceInterf + (JNIEnv *env, jclass cl, jint i) +{ + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (!info) return NULL; + return (*env)->NewStringUTF(env, info->interf); +} + + +/* + * Method: Pm_GetDeviceName + */ +JNIEXPORT jstring JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceName + (JNIEnv *env, jclass cl, jint i) +{ + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (!info) return NULL; + return (*env)->NewStringUTF(env, info->name); +} + + +/* + * Method: Pm_GetDeviceInput + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceInput + (JNIEnv *env, jclass cl, jint i) +{ + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (!info) return (jboolean) 0; + return (jboolean) info->input; +} + + +/* + * Method: Pm_GetDeviceOutput + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pm_1GetDeviceOutput + (JNIEnv *env, jclass cl, jint i) +{ + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (!info) return (jboolean) 0; + return (jboolean) info->output; +} + + +/* + * Method: Pm_OpenInput + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1OpenInput + (JNIEnv *env, jclass cl, + jobject jstream, jint index, jstring extras, jint bufsiz) +{ + PmError rslt; + PortMidiStream *stream; + CLASS(c, jstream); + ADDRESS_FID(fid, c); + rslt = Pm_OpenInput(&stream, index, NULL, bufsiz, NULL, NULL); + SET_PMSTREAM(jstream, fid, stream); + return rslt; +} + + +/* + * Method: Pm_OpenOutput + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1OpenOutput + (JNIEnv *env, jclass cl, jobject jstream, jint index, jstring extras, + jint bufsiz, jint latency) +{ + PmError rslt; + PortMidiStream *stream; + CLASS(c, jstream); + ADDRESS_FID(fid, c); + rslt = Pm_OpenOutput(&stream, index, NULL, bufsiz, NULL, NULL, latency); + SET_PMSTREAM(jstream, fid, stream); + return rslt; +} + + +/* + * Method: Pm_SetFilter + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1SetFilter + (JNIEnv *env, jclass cl, jobject jstream, jint filters) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_SetFilter(PMSTREAM(jstream, fid), filters); +} + + +/* + * Method: Pm_SetChannelMask + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1SetChannelMask + (JNIEnv *env, jclass cl, jobject jstream, jint mask) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_SetChannelMask(PMSTREAM(jstream, fid), mask); +} + + +/* + * Method: Pm_Abort + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Abort + (JNIEnv *env, jclass cl, jobject jstream) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_Abort(PMSTREAM(jstream, fid)); +} + + +/* + * Method: Pm_Close + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Close + (JNIEnv *env, jclass cl, jobject jstream) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_Close(PMSTREAM(jstream, fid)); +} + + +/* + * Method: Pm_Read + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Read + (JNIEnv *env, jclass cl, jobject jstream, jobject jpmevent) +{ + CLASS(jstream_class, jstream); + ADDRESS_FID(address_fid, jstream_class); + jclass jpmevent_class = (*env)->GetObjectClass(env, jpmevent); + jfieldID message_fid = + (*env)->GetFieldID(env, jpmevent_class, "message", "I"); + jfieldID timestamp_fid = + (*env)->GetFieldID(env, jpmevent_class, "timestamp", "I"); + PmEvent buffer; + PmError rslt = Pm_Read(PMSTREAM(jstream, address_fid), &buffer, 1); + (*env)->SetIntField(env, jpmevent, message_fid, buffer.message); + (*env)->SetIntField(env, jpmevent, timestamp_fid, buffer.timestamp); + return rslt; +} + + +/* + * Method: Pm_Poll + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Poll + (JNIEnv *env, jclass cl, jobject jstream) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_Poll(PMSTREAM(jstream, fid)); +} + + +/* + * Method: Pm_Write + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1Write + (JNIEnv *env, jclass cl, jobject jstream, jobject jpmevent) +{ + CLASS(jstream_class, jstream); + ADDRESS_FID(address_fid, jstream_class); + jclass jpmevent_class = (*env)->GetObjectClass(env, jpmevent); + jfieldID message_fid = + (*env)->GetFieldID(env, jpmevent_class, "message", "I"); + jfieldID timestamp_fid = + (*env)->GetFieldID(env, jpmevent_class, "timestamp", "I"); + // note that we call WriteShort because it's simpler than constructing + // a buffer and passing it to Pm_Write + return Pm_WriteShort(PMSTREAM(jstream, address_fid), + (*env)->GetIntField(env, jpmevent, timestamp_fid), + (*env)->GetIntField(env, jpmevent, message_fid)); +} + + +/* + * Method: Pm_WriteShort + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1WriteShort + (JNIEnv *env, jclass cl, jobject jstream, jint when, jint msg) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + return Pm_WriteShort(PMSTREAM(jstream, fid), when, msg); +} + + +/* + * Method: Pm_WriteSysEx + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pm_1WriteSysEx + (JNIEnv *env, jclass cl, jobject jstream, jint when, jbyteArray jmsg) +{ + CLASS(c, jstream); + ADDRESS_FID(fid, c); + jbyte *bytes = (*env)->GetByteArrayElements(env, jmsg, 0); + PmError rslt = Pm_WriteSysEx(PMSTREAM(jstream, fid), when, + (unsigned char *) bytes); + (*env)->ReleaseByteArrayElements(env, jmsg, bytes, 0); + return rslt; +} + +/* + * Method: Pt_TimeStart + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStart + (JNIEnv *env, jclass c, jint resolution) +{ + return Pt_Start(resolution, NULL, NULL); +} + +/* + * Method: Pt_TimeStop + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStop + (JNIEnv *env, jclass c) + { + return Pt_Stop(); + } + +/* + * Method: Pt_Time + */ +JNIEXPORT jint JNICALL Java_jportmidi_JPortMidiApi_Pt_1Time + (JNIEnv *env, jclass c) + { + return Pt_Time(); + } + +/* + * Method: Pt_TimeStarted + */ +JNIEXPORT jboolean JNICALL Java_jportmidi_JPortMidiApi_Pt_1TimeStarted + (JNIEnv *env, jclass c) +{ + return Pt_Started(); +} + + diff --git a/portmidi/pm_java/pmjni/pmjni.rc b/portmidi/pm_java/pmjni/pmjni.rc new file mode 100644 index 0000000..1b7522b --- /dev/null +++ b/portmidi/pm_java/pmjni/pmjni.rc @@ -0,0 +1,63 @@ +// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "afxres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (U.S.) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+#ifdef _WIN32
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+#endif //_WIN32
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""afxres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+#endif // English (U.S.) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
+
diff --git a/portmidi/pm_linux/README_LINUX.txt b/portmidi/pm_linux/README_LINUX.txt new file mode 100755 index 0000000..cfbc43f --- /dev/null +++ b/portmidi/pm_linux/README_LINUX.txt @@ -0,0 +1,99 @@ +README_LINUX.txt for PortMidi +Roger Dannenberg +6 Dec 2012, revised May 2022 + +Contents: + To make PortMidi + The pmdefaults program + Setting LD_LIBRARY_PATH + A note about amd64 + Using autoconf + Using configure + Changelog + + +See ../README.md for general instructions. + +THE pmdefaults PROGRAM + +(This may be obsolete. It is older than `../README.md` which +also discusses pmdefaults, and Java support may be removed +unless someone claims they use it... -RBD) + +You should install pmdefaults. It provides a graphical interface +for selecting default MIDI IN and OUT devices so that you don't +have to build device selection interfaces into all your programs +and so users have a single place to set a preference. + +Follow the instructions above to run ccmake, making sure that +CMAKE_BUILD_TYPE is Release. Run make as described above. Then: + +sudo make install + +This will install PortMidi libraries and the pmdefault program. +You must alos have the environment variable LD_LIBRARY_PATH set +to include /usr/local/lib (where libpmjni.so is installed). + +Now, you can run pmdefault. + + +SETTING LD_LIBRARY_PATH + +pmdefaults will not work unless LD_LIBRARY_PATH includes a +directory (normally /usr/local/lib) containing libpmjni.so, +installed as described above. + +To set LD_LIBRARY_PATH, you might want to add this to your +~/.profile (if you use the bash shell): + +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib +export LD_LIBRARY_PATH + + +A NOTE ABOUT AMD64: + +When compiling portmidi under linux on an AMD64, I had to add the -fPIC +flag to the gcc flags. + +Reason: when trying to build John Harrison's pyPortMidi gcc bailed out +with this error: + +./linux/libportmidi.a(pmlinux.o): relocation R_X86_64_32 against `a local symbol' can not be used when making a shared object; recompile with -fPIC +./linux/libportmidi.a: could not read symbols: Bad value +collect2: ld returned 1 exit status +error: command 'gcc' failed with exit status 1 + +What they said: +http://www.gentoo.org/proj/en/base/amd64/howtos/index.xml?part=1&chap=3 +On certain architectures (AMD64 amongst them), shared libraries *must* +be "PIC-enabled". + +CHANGELOG + +27-may-2022 Roger B. Dannenberg + Some updates to this file. + +6-dec-2012 Roger B. Dannenberg + Copied notes on Autoconf from Audacity sources + +22-jan-2010 Roger B. Dannenberg + Updated instructions about Java paths + +14-oct-2009 Roger B. Dannenberg + Using CMake now for building and configuration + +29-aug-2006 Roger B. Dannenberg + Fixed PortTime to join with time thread for clean exit. + +28-aug-2006 Roger B. Dannenberg + Updated this documentation. + +08-Jun-2004 Roger B. Dannenberg + Updated code to use new system abstraction. + +12-Apr-2003 Roger B. Dannenberg + Fixed pm_test/test.c to filter clocks and active messages. + Integrated changes from Clemens Ladisch: + cleaned up pmlinuxalsa.c + record timestamp on sysex input + deallocate some resources previously left open diff --git a/portmidi/pm_linux/pmlinux.c b/portmidi/pm_linux/pmlinux.c new file mode 100755 index 0000000..3766427 --- /dev/null +++ b/portmidi/pm_linux/pmlinux.c @@ -0,0 +1,68 @@ +/* pmlinux.c -- PortMidi os-dependent code */
+
+/* This file only needs to implement pm_init(), which calls various
+ routines to register the available midi devices. This file must
+ be separate from the main portmidi.c file because it is system
+ dependent, and it is separate from, pmlinuxalsa.c, because it
+ might need to register non-alsa devices as well.
+
+ NOTE: if you add non-ALSA support, you need to fix :alsa_poll()
+ in pmlinuxalsa.c, which assumes all input devices are ALSA.
+ */
+
+#include "stdlib.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "pminternal.h"
+
+#ifdef PMALSA
+ #include "pmlinuxalsa.h"
+#endif
+
+#ifdef PMNULL
+ #include "pmlinuxnull.h"
+#endif
+
+#if !(defined(PMALSA) || defined(PMNULL))
+#error One of PMALSA or PMNULL must be defined
+#endif
+
+void pm_init()
+{
+ /* Note: it is not an error for PMALSA to fail to initialize.
+ * It may be a design error that the client cannot query what subsystems
+ * are working properly other than by looking at the list of available
+ * devices.
+ */
+#ifdef PMALSA
+ pm_linuxalsa_init();
+#endif
+#ifdef PMNULL
+ pm_linuxnull_init();
+#endif
+}
+
+void pm_term(void)
+{
+ #ifdef PMALSA
+ pm_linuxalsa_term();
+ #endif
+ #ifdef PMNULL
+ pm_linuxnull_term();
+ #endif
+}
+
+PmDeviceID Pm_GetDefaultInputDeviceID() {
+ Pm_Initialize();
+ return pm_default_input_device_id;
+}
+
+PmDeviceID Pm_GetDefaultOutputDeviceID() {
+ Pm_Initialize();
+ return pm_default_output_device_id;
+}
+
+void *pm_alloc(size_t s) { return malloc(s); }
+
+void pm_free(void *ptr) { free(ptr); }
+
diff --git a/portmidi/pm_linux/pmlinuxalsa.c b/portmidi/pm_linux/pmlinuxalsa.c new file mode 100755 index 0000000..b2e43e1 --- /dev/null +++ b/portmidi/pm_linux/pmlinuxalsa.c @@ -0,0 +1,893 @@ +/*
+ * pmlinuxalsa.c -- system specific definitions
+ *
+ * written by:
+ * Roger Dannenberg (port to Alsa 0.9.x)
+ * Clemens Ladisch (provided code examples and invaluable consulting)
+ * Jason Cohen, Rico Colon, Matt Filippone (Alsa 0.5.x implementation)
+ */
+
+/* omit this code if PMALSA is not defined -- this mechanism allows
+ * selection of different MIDI interfaces (at compile time).
+ */
+#ifdef PMALSA
+
+#include "stdlib.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "pminternal.h"
+#include "pmlinuxalsa.h"
+#include "string.h"
+#include "porttime.h"
+
+#include <alsa/asoundlib.h>
+
+/* I used many print statements to debug this code. I left them in the
+ * source, and you can turn them on by changing false to true below:
+ */
+#define VERBOSE_ON 0
+#define VERBOSE if (VERBOSE_ON)
+
+#define MIDI_SYSEX 0xf0
+#define MIDI_EOX 0xf7
+
+#if SND_LIB_MAJOR == 0 && SND_LIB_MINOR < 9
+#error needs ALSA 0.9.0 or later
+#endif
+
+/* to store client/port in the device descriptor */
+#define MAKE_DESCRIPTOR(client, port) ((void*)(long)(((client) << 8) | (port)))
+#define GET_DESCRIPTOR_CLIENT(info) ((((long)(info)) >> 8) & 0xff)
+#define GET_DESCRIPTOR_PORT(info) (((long)(info)) & 0xff)
+
+#define BYTE unsigned char
+
+extern pm_fns_node pm_linuxalsa_in_dictionary;
+extern pm_fns_node pm_linuxalsa_out_dictionary;
+
+static snd_seq_t *seq = NULL; // all input comes here,
+ // output queue allocated on seq
+static int queue, queue_used; /* one for all ports, reference counted */
+
+#define PORT_IS_CLOSED -999999
+
+typedef struct alsa_info_struct {
+ int is_virtual;
+ int client;
+ int port;
+ int this_port;
+ int in_sysex;
+ snd_midi_event_t *parser;
+} alsa_info_node, *alsa_info_type;
+
+
+/* get_alsa_error_text -- copy error text to potentially short string */
+/**/
+static void get_alsa_error_text(char *msg, int len, int err)
+{
+ int errlen = strlen(snd_strerror(err));
+ if (errlen > 0 && errlen < len) {
+ strcpy(msg, snd_strerror(err));
+ } else if (len > 20) {
+ sprintf(msg, "Alsa error %d", err);
+ } else {
+ msg[0] = 0;
+ }
+}
+
+
+static PmError check_hosterror(int err)
+{
+ if (err < 0) {
+ get_alsa_error_text(pm_hosterror_text, PM_HOST_ERROR_MSG_LEN, err);
+ pm_hosterror = TRUE;
+ return pmHostError;
+ }
+ return pmNoError;
+}
+
+
+/* queue is shared by both input and output, reference counted */
+static PmError alsa_use_queue(void)
+{
+ int err = 0;
+ if (queue_used == 0) {
+ snd_seq_queue_tempo_t *tempo;
+
+ queue = snd_seq_alloc_queue(seq);
+ if (queue < 0) {
+ return check_hosterror(queue);
+ }
+ snd_seq_queue_tempo_alloca(&tempo);
+ snd_seq_queue_tempo_set_tempo(tempo, 480000);
+ snd_seq_queue_tempo_set_ppq(tempo, 480);
+ err = snd_seq_set_queue_tempo(seq, queue, tempo);
+ if (err < 0) {
+ return check_hosterror(err);
+ }
+ snd_seq_start_queue(seq, queue, NULL);
+ snd_seq_drain_output(seq);
+ }
+ ++queue_used;
+ return pmNoError;
+}
+
+
+static void alsa_unuse_queue(void)
+{
+ if (--queue_used == 0) {
+ snd_seq_stop_queue(seq, queue, NULL);
+ snd_seq_drain_output(seq);
+ snd_seq_free_queue(seq, queue);
+ VERBOSE printf("queue freed\n");
+ }
+}
+
+
+/* midi_message_length -- how many bytes in a message? */
+static int midi_message_length(PmMessage message)
+{
+ message &= 0xff;
+ if (message < 0x80) {
+ return 0;
+ } else if (message < 0xf0) {
+ static const int length[] = {3, 3, 3, 3, 2, 2, 3};
+ return length[(message - 0x80) >> 4];
+ } else {
+ static const int length[] = {
+ -1, 2, 3, 2, 0, 0, 1, -1, 1, 0, 1, 1, 1, 0, 1, 1};
+ return length[message - 0xf0];
+ }
+}
+
+
+static alsa_info_type alsa_info_create(int client_port, long id, int is_virtual)
+{
+ alsa_info_type info = (alsa_info_type) pm_alloc(sizeof(alsa_info_node));
+ info->is_virtual = is_virtual;
+ info->this_port = id;
+ info->client = GET_DESCRIPTOR_CLIENT(client_port);
+ info->port = GET_DESCRIPTOR_PORT(client_port);
+ info->in_sysex = 0;
+ return info;
+}
+
+
+/* search system dependent extra parameters for string */
+static const char *get_sysdep_name(enum PmSysDepPropertyKey key,
+ PmSysDepInfo *info)
+{
+ /* the version where all current properties were introduced is 210 */
+ if (info && info->structVersion >= 210) {
+ int i;
+ for (i = 0; i < info->length; i++) { /* search for key */
+ if (info->properties[i].key == key) {
+ return info->properties[i].value;
+ }
+ }
+ }
+ return NULL;
+}
+
+
+static void maybe_set_client_name(PmSysDepInfo *driverInfo)
+{
+ if (!seq) { // make sure seq is created and we have info
+ return;
+ }
+
+ const char *client_name = get_sysdep_name(pmKeyAlsaClientName,
+ (PmSysDepInfo *) driverInfo);
+ if (client_name) {
+ snd_seq_set_client_name(seq, client_name);
+ printf("maybe_set_client_name set client to %s\n", client_name);
+ }
+}
+
+
+static PmError alsa_out_open(PmInternal *midi, void *driverInfo)
+{
+ int id = midi->device_id;
+ void *client_port = pm_descriptors[id].descriptor;
+ alsa_info_type ainfo = alsa_info_create((long) client_port, id,
+ pm_descriptors[id].pub.is_virtual);
+ snd_seq_port_info_t *pinfo;
+ int err = 0;
+ int using_the_queue = 0;
+
+ if (!ainfo) return pmInsufficientMemory;
+ midi->api_info = ainfo;
+
+ snd_seq_port_info_alloca(&pinfo);
+ if (!ainfo->is_virtual) {
+ snd_seq_port_info_set_port(pinfo, id);
+ snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_WRITE |
+ SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ);
+ snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC |
+ SND_SEQ_PORT_TYPE_APPLICATION);
+ const char *port_name = get_sysdep_name(pmKeyAlsaPortName,
+ (PmSysDepInfo *) driverInfo);
+ if (port_name) {
+ snd_seq_port_info_set_name(pinfo, port_name);
+ }
+ snd_seq_port_info_set_port_specified(pinfo, 1);
+
+ err = snd_seq_create_port(seq, pinfo);
+ if (err < 0) goto free_ainfo;
+
+ }
+
+ err = snd_midi_event_new(PM_DEFAULT_SYSEX_BUFFER_SIZE, &ainfo->parser);
+ if (err < 0) goto free_this_port;
+
+ if (midi->latency > 0) { /* must delay output using a queue */
+ err = alsa_use_queue();
+ if (err < 0) goto free_parser;
+ using_the_queue++;
+ }
+
+ if (!ainfo->is_virtual) {
+ err = snd_seq_connect_to(seq, ainfo->this_port, ainfo->client,
+ ainfo->port);
+ if (err < 0) goto unuse_queue; /* clean up and return on error */
+ }
+
+ maybe_set_client_name(driverInfo);
+
+ return pmNoError;
+
+ unuse_queue:
+ if (using_the_queue > 0) /* only for latency>0 case */
+ alsa_unuse_queue();
+ free_parser:
+ snd_midi_event_free(ainfo->parser);
+ free_this_port:
+ snd_seq_delete_port(seq, ainfo->this_port);
+ free_ainfo:
+ pm_free(ainfo);
+ return check_hosterror(err);
+}
+
+
+static PmError alsa_write_byte(PmInternal *midi, unsigned char byte,
+ PmTimestamp timestamp)
+{
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ snd_seq_event_t ev;
+ int err = 0;
+
+ snd_seq_ev_clear(&ev);
+ if (snd_midi_event_encode_byte(info->parser, byte, &ev) == 1) {
+ if (info->is_virtual) {
+ snd_seq_ev_set_subs(&ev);
+ } else {
+ snd_seq_ev_set_dest(&ev, info->client, info->port);
+ }
+ snd_seq_ev_set_source(&ev, info->this_port);
+ if (midi->latency > 0) {
+ /* compute relative time of event = timestamp - now + latency */
+ PmTimestamp now = (midi->time_proc ?
+ midi->time_proc(midi->time_info) :
+ Pt_Time());
+ int when = timestamp;
+ /* if timestamp is zero, send immediately */
+ /* otherwise compute time delay and use delay if positive */
+ if (when == 0) when = now;
+ when = (when - now) + midi->latency;
+ if (when < 0) when = 0;
+ VERBOSE printf("timestamp %d now %d latency %d, ",
+ (int) timestamp, (int) now, midi->latency);
+ VERBOSE printf("scheduling event after %d\n", when);
+ /* message is sent in relative ticks, where 1 tick = 1 ms */
+ snd_seq_ev_schedule_tick(&ev, queue, 1, when);
+ /* NOTE: for cases where the user does not supply a time function,
+ we could optimize the code by not starting Pt_Time and using
+ the alsa tick time instead. I didn't do this because it would
+ entail changing the queue management to start the queue tick
+ count when PortMidi is initialized and keep it running until
+ PortMidi is terminated. (This should be simple, but it's not
+ how the code works now.) -RBD */
+ } else { /* send event out without queueing */
+ VERBOSE printf("direct\n");
+ /* ev.queue = SND_SEQ_QUEUE_DIRECT;
+ ev.dest.client = SND_SEQ_ADDRESS_SUBSCRIBERS; */
+ snd_seq_ev_set_direct(&ev);
+ }
+ VERBOSE printf("sending event, timestamp %d (%d+%dns) (%s, %s)\n",
+ ev.time.tick, ev.time.time.tv_sec, ev.time.time.tv_nsec,
+ (ev.flags & SND_SEQ_TIME_STAMP_MASK ? "real" : "tick"),
+ (ev.flags & SND_SEQ_TIME_MODE_MASK ? "rel" : "abs"));
+ err = snd_seq_event_output(seq, &ev);
+ }
+ return check_hosterror(err);
+}
+
+
+static PmError alsa_out_close(PmInternal *midi)
+{
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ int err = 0;
+ int error2 = 0;
+ if (!info) return pmBadPtr;
+
+ if (info->this_port != PORT_IS_CLOSED) {
+ if (!info->is_virtual) {
+ err = snd_seq_disconnect_to(seq, info->this_port,
+ info->client, info->port);
+ /* even if there was an error, we still try to delete the port */
+ error2 = snd_seq_delete_port(seq, info->this_port);
+
+ if (!err) { /* retain original error if there was one */
+ err = error2; /* otherwise, use port delete status */
+ }
+ }
+ }
+ if (midi->latency > 0) alsa_unuse_queue();
+ snd_midi_event_free(info->parser);
+ midi->api_info = NULL; /* destroy the pointer to signify "closed" */
+ pm_free(info);
+ return check_hosterror(err);
+}
+
+
+static PmError alsa_create_virtual(int is_input, const char *name,
+ void *device_info)
+{
+ snd_seq_port_info_t *pinfo;
+ int err;
+ int client, port;
+
+ /* we need the id to set the port. */
+ PmDeviceID id = pm_add_device("ALSA", name, is_input, TRUE, NULL,
+ (is_input ? &pm_linuxalsa_in_dictionary :
+ &pm_linuxalsa_out_dictionary));
+ if (id < 0) { /* error -- out of memory? */
+ return id;
+ }
+ snd_seq_port_info_alloca(&pinfo);
+ snd_seq_port_info_set_capability(pinfo,
+ (is_input ? SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE :
+ SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ));
+ snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC |
+ SND_SEQ_PORT_TYPE_APPLICATION);
+ snd_seq_port_info_set_name(pinfo, name);
+ snd_seq_port_info_set_port(pinfo, id);
+ snd_seq_port_info_set_port_specified(pinfo, 1);
+ /* next 3 lines needed to generate timestamp - PaulLiu */
+ snd_seq_port_info_set_timestamping(pinfo, 1);
+ snd_seq_port_info_set_timestamp_real(pinfo, 0);
+ snd_seq_port_info_set_timestamp_queue(pinfo, queue);
+
+ err = snd_seq_create_port(seq, pinfo);
+ if (err < 0) {
+ pm_undo_add_device(id);
+ return check_hosterror(err);
+ }
+
+ client = snd_seq_port_info_get_client(pinfo);
+ port = snd_seq_port_info_get_port(pinfo);
+ pm_descriptors[id].descriptor = MAKE_DESCRIPTOR(client, port);
+ return id;
+}
+
+
+ static PmError alsa_delete_virtual(PmDeviceID id)
+ {
+ int err = snd_seq_delete_port(seq, id);
+ return check_hosterror(err);
+ }
+
+
+static PmError alsa_in_open(PmInternal *midi, void *driverInfo)
+{
+ int id = midi->device_id;
+ void *client_port = pm_descriptors[id].descriptor;
+ alsa_info_type ainfo = alsa_info_create((long) client_port, id,
+ pm_descriptors[id].pub.is_virtual);
+ snd_seq_port_info_t *pinfo;
+ snd_seq_port_subscribe_t *sub;
+ snd_seq_addr_t addr;
+ int err = 0;
+ int is_virtual = pm_descriptors[id].pub.is_virtual;
+
+ if (!ainfo) return pmInsufficientMemory;
+ midi->api_info = ainfo;
+
+ err = alsa_use_queue();
+ if (err < 0) goto free_ainfo;
+
+ snd_seq_port_info_alloca(&pinfo);
+ if (is_virtual) {
+ ainfo->is_virtual = TRUE;
+ if (snd_seq_get_port_info(seq, ainfo->port, pinfo)) {
+ pinfo = NULL;
+ goto free_ainfo;
+ }
+ } else {
+ /* create a port for this alsa client (seq) where the port
+ number matches the portmidi device ID of the input device */
+ snd_seq_port_info_set_port(pinfo, id);
+ snd_seq_port_info_set_capability(pinfo, SND_SEQ_PORT_CAP_WRITE |
+ SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_WRITE);
+
+ snd_seq_port_info_set_type(pinfo, SND_SEQ_PORT_TYPE_MIDI_GENERIC |
+ SND_SEQ_PORT_TYPE_APPLICATION);
+ snd_seq_port_info_set_port_specified(pinfo, 1);
+
+ const char *port_name = get_sysdep_name(pmKeyAlsaPortName,
+ (PmSysDepInfo *) driverInfo);
+ if (port_name) {
+ snd_seq_port_info_set_name(pinfo, port_name);
+ }
+
+ err = snd_seq_create_port(seq, pinfo);
+ if (err < 0) goto free_queue;
+
+ /* forward messages from input to this alsa client, so this
+ * alsa client is the destination, and the destination port is the
+ * port we just created using the device ID as port number
+ */
+ snd_seq_port_subscribe_alloca(&sub);
+ addr.client = snd_seq_client_id(seq);
+ addr.port = ainfo->this_port;
+ snd_seq_port_subscribe_set_dest(sub, &addr);
+
+ /* forward from the sender which is the device named by
+ client and port */
+ addr.client = ainfo->client;
+ addr.port = ainfo->port;
+ snd_seq_port_subscribe_set_sender(sub, &addr);
+ snd_seq_port_subscribe_set_time_update(sub, 1);
+ /* this doesn't seem to work: messages come in with real timestamps */
+ snd_seq_port_subscribe_set_time_real(sub, 0);
+ err = snd_seq_subscribe_port(seq, sub);
+ if (err < 0) goto free_this_port; /* clean up and return on error */
+ }
+
+ maybe_set_client_name(driverInfo);
+
+ return pmNoError;
+ free_this_port:
+ snd_seq_delete_port(seq, ainfo->this_port);
+ free_queue:
+ alsa_unuse_queue();
+ free_ainfo:
+ pm_free(ainfo);
+ return check_hosterror(err);
+}
+
+static PmError alsa_in_close(PmInternal *midi)
+{
+ int err = 0;
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ if (!info) return pmBadPtr;
+ /* virtual ports stay open because the represent devices */
+ if (!info->is_virtual && info->this_port != PORT_IS_CLOSED) {
+ err = snd_seq_delete_port(seq, info->this_port);
+ }
+ alsa_unuse_queue();
+ midi->api_info = NULL;
+ pm_free(info);
+ return check_hosterror(err);
+}
+
+
+static PmError alsa_abort(PmInternal *midi)
+{
+ /* NOTE: ALSA documentation is vague. This is supposed to
+ * remove any pending output messages. If you can test and
+ * confirm this code is correct, please update this comment. -RBD
+ */
+ /* Unfortunately, I can't even compile it -- my ALSA version
+ * does not implement snd_seq_remove_events_t, so this does
+ * not compile. I'll try again, but it looks like I'll need to
+ * upgrade my entire Linux OS -RBD
+ */
+ /*
+ info_type info = (info_type) midi->api_info;
+ snd_seq_remove_events_t info;
+ snd_seq_addr_t addr;
+ addr.client = info->client;
+ addr.port = info->port;
+ snd_seq_remove_events_set_dest(&info, &addr);
+ snd_seq_remove_events_set_condition(&info, SND_SEQ_REMOVE_DEST);
+ pm_hosterror = snd_seq_remove_events(seq, &info);
+ if (pm_hosterror) {
+ get_alsa_error_text(pm_hosterror_text, PM_HOST_ERROR_MSG_LEN,
+ pm_hosterror);
+ return pmHostError;
+ }
+ */
+ printf("WARNING: alsa_abort not implemented\n");
+ return pmNoError;
+}
+
+
+static PmError alsa_write_flush(PmInternal *midi, PmTimestamp timestamp)
+{
+ int err;
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ if (!info) return pmBadPtr;
+ VERBOSE printf("snd_seq_drain_output: %p\n", seq);
+ err = snd_seq_drain_output(seq);
+ return check_hosterror(err);
+}
+
+
+static PmError alsa_write_short(PmInternal *midi, PmEvent *event)
+{
+ int bytes = midi_message_length(event->message);
+ PmMessage msg = event->message;
+ int i;
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ if (!info) return pmBadPtr;
+ for (i = 0; i < bytes; i++) {
+ unsigned char byte = msg;
+ VERBOSE printf("sending 0x%x\n", byte);
+ alsa_write_byte(midi, byte, event->timestamp);
+ if (pm_hosterror) break;
+ msg >>= 8; /* shift next byte into position */
+ }
+ if (pm_hosterror) return pmHostError;
+ return pmNoError;
+}
+
+
+/* alsa_sysex -- implements begin_sysex and end_sysex */
+PmError alsa_sysex(PmInternal *midi, PmTimestamp timestamp) {
+ return pmNoError;
+}
+
+
+static PmTimestamp alsa_synchronize(PmInternal *midi)
+{
+ return 0; /* linux implementation does not use this synchronize function */
+ /* Apparently, Alsa data is relative to the time you send it, and there
+ is no reference. If this is true, this is a serious shortcoming of
+ Alsa. If not true, then PortMidi has a serious shortcoming -- it
+ should be scheduling relative to Alsa's time reference. */
+}
+
+
+static void handle_event(snd_seq_event_t *ev)
+{
+ int device_id = ev->dest.port;
+ PmInternal *midi = pm_descriptors[device_id].pm_internal;
+ // There is a race condition when closing a device and
+ // continuing to poll other open devices. The closed device may
+ // have outstanding events from before the close operation.
+ if (!midi) {
+ return;
+ }
+ PmEvent pm_ev;
+ PmTimestamp timestamp = midi->time_proc(midi->time_info);
+
+ /* time stamp should be in ticks, using our queue where 1 tick = 1ms */
+ /* assert((ev->flags & SND_SEQ_TIME_STAMP_MASK) == SND_SEQ_TIME_STAMP_TICK);
+ * Currently, event timestamp is ignored. See long note below. */
+
+ VERBOSE {
+ /* translate time to time_proc basis */
+ snd_seq_queue_status_t *queue_status;
+ snd_seq_queue_status_alloca(&queue_status);
+ snd_seq_get_queue_status(seq, queue, queue_status);
+ printf("handle_event: alsa_now %d, "
+ "event timestamp %d (%d+%dns) (%s, %s)\n",
+ snd_seq_queue_status_get_tick_time(queue_status),
+ ev->time.tick, ev->time.time.tv_sec, ev->time.time.tv_nsec,
+ (ev->flags & SND_SEQ_TIME_STAMP_MASK ? "real" : "tick"),
+ (ev->flags & SND_SEQ_TIME_MODE_MASK ? "rel" : "abs"));
+ /* OLD: portmidi timestamp is (now - alsa_now) + alsa_timestamp */
+ /* timestamp = (*time_proc)(midi->time_info) + ev->time.tick -
+ snd_seq_queue_status_get_tick_time(queue_status); */
+ }
+ /* CURRENT: portmidi timestamp is "now". In a test, timestamps from
+ * hardware (MIDI over USB) were timestamped with the current ALSA
+ * time (snd_seq_queue_status_get_tick_time) and flags indicating
+ * absolute ticks, but timestamps from another application's virtual
+ * port, sent direct with 0 absolute ticks, were received with a
+ * large value that is apparently the time since the start time of
+ * the other application. Without any reference to our local time,
+ * this seems useless. PortMidi is supposed to return the local
+ * PortMidi time of the arrival of the message, so the best we can
+ * do is set the timestamp to our local clock. This seems to be a
+ * design flaw in ALSA -- I pointed this out a decade ago, but if
+ * there is a workaround, I'd still like to know. Maybe there is a
+ * way to use absolute real time and maybe that's sharable across
+ * applications by referencing the system time?
+ */
+ pm_ev.timestamp = timestamp;
+ switch (ev->type) {
+ case SND_SEQ_EVENT_NOTEON:
+ pm_ev.message = Pm_Message(0x90 | ev->data.note.channel,
+ ev->data.note.note & 0x7f,
+ ev->data.note.velocity & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_NOTEOFF:
+ pm_ev.message = Pm_Message(0x80 | ev->data.note.channel,
+ ev->data.note.note & 0x7f,
+ ev->data.note.velocity & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_KEYPRESS:
+ pm_ev.message = Pm_Message(0xa0 | ev->data.note.channel,
+ ev->data.note.note & 0x7f,
+ ev->data.note.velocity & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_CONTROLLER:
+ pm_ev.message = Pm_Message(0xb0 | ev->data.note.channel,
+ ev->data.control.param & 0x7f,
+ ev->data.control.value & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_PGMCHANGE:
+ pm_ev.message = Pm_Message(0xc0 | ev->data.note.channel,
+ ev->data.control.value & 0x7f, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_CHANPRESS:
+ pm_ev.message = Pm_Message(0xd0 | ev->data.note.channel,
+ ev->data.control.value & 0x7f, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_PITCHBEND:
+ pm_ev.message = Pm_Message(0xe0 | ev->data.note.channel,
+ (ev->data.control.value + 0x2000) & 0x7f,
+ ((ev->data.control.value + 0x2000) >> 7) & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_CONTROL14:
+ if (ev->data.control.param < 0x20) {
+ pm_ev.message = Pm_Message(0xb0 | ev->data.note.channel,
+ ev->data.control.param,
+ (ev->data.control.value >> 7) & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ pm_ev.message = Pm_Message(0xb0 | ev->data.note.channel,
+ ev->data.control.param + 0x20,
+ ev->data.control.value & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ } else {
+ pm_ev.message = Pm_Message(0xb0 | ev->data.note.channel,
+ ev->data.control.param & 0x7f,
+ ev->data.control.value & 0x7f);
+
+ pm_read_short(midi, &pm_ev);
+ }
+ break;
+ case SND_SEQ_EVENT_SONGPOS:
+ pm_ev.message = Pm_Message(0xf2,
+ ev->data.control.value & 0x7f,
+ (ev->data.control.value >> 7) & 0x7f);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_SONGSEL:
+ pm_ev.message = Pm_Message(0xf3,
+ ev->data.control.value & 0x7f, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_QFRAME:
+ pm_ev.message = Pm_Message(0xf1,
+ ev->data.control.value & 0x7f, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_START:
+ pm_ev.message = Pm_Message(0xfa, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_CONTINUE:
+ pm_ev.message = Pm_Message(0xfb, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_STOP:
+ pm_ev.message = Pm_Message(0xfc, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_CLOCK:
+ pm_ev.message = Pm_Message(0xf8, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_TUNE_REQUEST:
+ pm_ev.message = Pm_Message(0xf6, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_RESET:
+ pm_ev.message = Pm_Message(0xff, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_SENSING:
+ pm_ev.message = Pm_Message(0xfe, 0, 0);
+ pm_read_short(midi, &pm_ev);
+ break;
+ case SND_SEQ_EVENT_SYSEX: {
+ const BYTE *ptr = (const BYTE *) ev->data.ext.ptr;
+ /* assume there is one sysex byte to process */
+ pm_read_bytes(midi, ptr, ev->data.ext.len, timestamp);
+ break;
+ }
+ case SND_SEQ_EVENT_PORT_UNSUBSCRIBED: {
+ /* this happens if you have an input port open and the
+ * device or application with virtual ports closes. We
+ * mark the port as closed to avoid closing a 2nd time
+ * when Pm_Close() is called.
+ */
+ alsa_info_type info = (alsa_info_type) midi->api_info;
+ /* printf("SND_SEQ_EVENT_UNSUBSCRIBE message\n"); */
+ info->this_port = PORT_IS_CLOSED;
+ break;
+ }
+ case SND_SEQ_EVENT_PORT_SUBSCRIBED:
+ break; /* someone connected to a virtual output port, not reported */
+ default:
+ printf("portmidi handle_event: not handled type %x\n", ev->type);
+ break;
+ }
+}
+
+
+static PmError alsa_poll(PmInternal *midi)
+{
+ if (!midi) {
+ return pmBadPtr;
+ }
+ snd_seq_event_t *ev;
+ /* expensive check for input data, gets data from device: */
+ while (snd_seq_event_input_pending(seq, TRUE) > 0) {
+ /* cheap check on local input buffer */
+ while (snd_seq_event_input_pending(seq, FALSE) > 0) {
+ /* check for and ignore errors, e.g. input overflow */
+ /* note: if there's overflow, this should be reported
+ * all the way through to client. Since input from all
+ * devices is merged, we need to find all input devices
+ * and set all to the overflow state.
+ * NOTE: this assumes every input is ALSA based.
+ */
+ int rslt = snd_seq_event_input(seq, &ev);
+ if (rslt >= 0) {
+ handle_event(ev);
+ } else if (rslt == -ENOSPC) {
+ int i;
+ for (i = 0; i < pm_descriptor_len; i++) {
+ if (pm_descriptors[i].pub.input) {
+ PmInternal *midi_i = pm_descriptors[i].pm_internal;
+ /* careful, device may not be open! */
+ if (midi_i) Pm_SetOverflow(midi_i->queue);
+ }
+ }
+ }
+ }
+ }
+ return pmNoError;
+}
+
+
+static unsigned int alsa_check_host_error(PmInternal *midi)
+{
+ return FALSE;
+}
+
+
+pm_fns_node pm_linuxalsa_in_dictionary = {
+ none_write_short,
+ none_sysex,
+ none_sysex,
+ none_write_byte,
+ none_write_short,
+ none_write_flush,
+ alsa_synchronize,
+ alsa_in_open,
+ alsa_abort,
+ alsa_in_close,
+ alsa_poll,
+ alsa_check_host_error
+};
+
+pm_fns_node pm_linuxalsa_out_dictionary = {
+ alsa_write_short,
+ alsa_sysex,
+ alsa_sysex,
+ alsa_write_byte,
+ alsa_write_short, /* short realtime message */
+ alsa_write_flush,
+ alsa_synchronize,
+ alsa_out_open,
+ alsa_abort,
+ alsa_out_close,
+ none_poll,
+ alsa_check_host_error
+};
+
+
+/* pm_strdup -- copy a string to the heap. Use this rather than strdup so
+ * that we call pm_alloc, not malloc. This allows portmidi to avoid
+ * malloc which might cause priority inversion. Probably ALSA is going
+ * to call malloc anyway, so this extra work here may be pointless.
+ */
+char *pm_strdup(const char *s)
+{
+ int len = strlen(s);
+ char *dup = (char *) pm_alloc(len + 1);
+ strcpy(dup, s);
+ return dup;
+}
+
+
+PmError pm_linuxalsa_init(void)
+{
+ int err;
+ snd_seq_client_info_t *cinfo;
+ snd_seq_port_info_t *pinfo;
+ unsigned int caps;
+
+ /* Register interface ALSA with create_virtual fn */
+ pm_add_interf("ALSA", &alsa_create_virtual, &alsa_delete_virtual);
+
+ /* Previously, the last parameter was SND_SEQ_NONBLOCK, but this
+ * would cause messages to be dropped if the ALSA buffer fills up.
+ * The correct behavior is for writes to block until there is
+ * room to send all the data. The client should normally allocate
+ * a large enough buffer to avoid blocking on output.
+ * Now that blocking is enabled, the seq_event_input() will block
+ * if there is no input data. This is not what we want, so must
+ * call seq_event_input_pending() to avoid blocking.
+ */
+ err = snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0);
+ if (err < 0) goto error_return;
+
+ snd_seq_client_info_alloca(&cinfo);
+ snd_seq_port_info_alloca(&pinfo);
+
+ snd_seq_client_info_set_client(cinfo, -1);
+ while (snd_seq_query_next_client(seq, cinfo) == 0) {
+ snd_seq_port_info_set_client(pinfo,
+ snd_seq_client_info_get_client(cinfo));
+ snd_seq_port_info_set_port(pinfo, -1);
+ while (snd_seq_query_next_port(seq, pinfo) == 0) {
+ if (snd_seq_port_info_get_client(pinfo) == SND_SEQ_CLIENT_SYSTEM)
+ continue; /* ignore Timer and Announce ports on client 0 */
+ caps = snd_seq_port_info_get_capability(pinfo);
+ if (!(caps & (SND_SEQ_PORT_CAP_SUBS_READ |
+ SND_SEQ_PORT_CAP_SUBS_WRITE)))
+ continue; /* ignore if you cannot read or write port */
+ if (caps & SND_SEQ_PORT_CAP_SUBS_WRITE) {
+ if (pm_default_output_device_id == -1)
+ pm_default_output_device_id = pm_descriptor_len;
+ pm_add_device("ALSA",
+ pm_strdup(snd_seq_port_info_get_name(pinfo)),
+ FALSE, FALSE,
+ MAKE_DESCRIPTOR(snd_seq_port_info_get_client(pinfo),
+ snd_seq_port_info_get_port(pinfo)),
+ &pm_linuxalsa_out_dictionary);
+ }
+ if (caps & SND_SEQ_PORT_CAP_SUBS_READ) {
+ if (pm_default_input_device_id == -1)
+ pm_default_input_device_id = pm_descriptor_len;
+ pm_add_device("ALSA",
+ pm_strdup(snd_seq_port_info_get_name(pinfo)),
+ TRUE, FALSE,
+ MAKE_DESCRIPTOR(snd_seq_port_info_get_client(pinfo),
+ snd_seq_port_info_get_port(pinfo)),
+ &pm_linuxalsa_in_dictionary);
+ }
+ }
+ }
+ return pmNoError;
+ error_return:
+ pm_linuxalsa_term(); /* clean up */
+ return check_hosterror(err);
+}
+
+
+void pm_linuxalsa_term(void)
+{
+ if (seq) {
+ snd_seq_close(seq);
+ pm_free(pm_descriptors);
+ pm_descriptors = NULL;
+ pm_descriptor_len = 0;
+ pm_descriptor_max = 0;
+ }
+}
+
+#endif
diff --git a/portmidi/pm_linux/pmlinuxalsa.h b/portmidi/pm_linux/pmlinuxalsa.h new file mode 100755 index 0000000..d4bff16 --- /dev/null +++ b/portmidi/pm_linux/pmlinuxalsa.h @@ -0,0 +1,6 @@ +/* pmlinuxalsa.h -- system-specific definitions */
+
+PmError pm_linuxalsa_init(void);
+void pm_linuxalsa_term(void);
+
+
diff --git a/portmidi/pm_linux/pmlinuxnull.c b/portmidi/pm_linux/pmlinuxnull.c new file mode 100644 index 0000000..257f3d6 --- /dev/null +++ b/portmidi/pm_linux/pmlinuxnull.c @@ -0,0 +1,31 @@ +/*
+ * pmlinuxnull.c -- system specific definitions
+ *
+ * written by:
+ * Roger Dannenberg
+ *
+ * If there is no ALSA, you can define PMNULL and build PortMidi. It will
+ * not report any devices, so you will not be able to open any, but if
+ * you wanted to disable MIDI from some application, this could be used.
+ * Mainly, this code shows the possibility of supporting multiple
+ * interfaces, e.g., ALSA and Sndio on BSD, or ALSA and Jack on Linux.
+ * But as of Dec, 2021, the only supported MIDI API for Linux is ALSA.
+ */
+
+#ifdef PMNULL
+
+#include "portmidi.h"
+#include "pmlinuxnull.h"
+
+
+PmError pm_linuxnull_init(void)
+{
+ return pmNoError;
+}
+
+
+void pm_linuxnull_term(void)
+{
+}
+
+#endif
diff --git a/portmidi/pm_linux/pmlinuxnull.h b/portmidi/pm_linux/pmlinuxnull.h new file mode 100644 index 0000000..9835825 --- /dev/null +++ b/portmidi/pm_linux/pmlinuxnull.h @@ -0,0 +1,6 @@ +/* pmlinuxnull.h -- system-specific definitions */
+
+PmError pm_linuxnull_init(void);
+void pm_linuxnull_term(void);
+
+
diff --git a/portmidi/pm_mac/Makefile.osx b/portmidi/pm_mac/Makefile.osx new file mode 100755 index 0000000..2044381 --- /dev/null +++ b/portmidi/pm_mac/Makefile.osx @@ -0,0 +1,125 @@ +# MAKEFILE FOR PORTMIDI
+
+# Roger B. Dannenberg
+# Sep 2009
+
+# NOTE: PortMidi is currently built and tested with CMake.
+# This makefile is probably broken, but if you want to
+# directly use make, start here, and please contribute any
+# fixes.
+
+# NOTE: you can use
+# make -f pm_mac/Makefile.osx configuration=Release
+# to override the default Debug configuration
+configuration=Release
+
+PF=/usr/local
+
+# For debugging, define PM_CHECK_ERRORS
+ifeq ($(configuration),Release)
+ CONFIG = Release
+else
+ CONFIG = Debug
+endif
+
+current: all
+
+all: $(CONFIG)/CMakeCache.txt
+ cd $(CONFIG); make
+
+$(CONFIG)/CMakeCache.txt:
+ rm -f $(CONFIG)/CMakeCache.txt
+ mkdir -p $(CONFIG)
+ cd $(CONFIG); cmake .. -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=$(CONFIG)
+
+
+**** For instructions: make -f pm_mac/Makefile.osx help ****\n'
+
+help:
+ echo $$'\n\n\
+This is help for portmidi/pm_mac/Makefile.osx\n\n\
+Installation path for dylib is $(PF)\n\
+To build Release version libraries and test applications,\n \
+make -f pm_mac/Makefile.osx\n\
+To build Debug version libraries and test applications,\n \
+make -f pm_mac/Makefile.osx configuration=Debug\n\
+To install universal dynamic library,\n \
+sudo make -f pm_mac/Makefile.osx install\n\
+To install universal dynamic library with xcode,\n \
+make -f pm_mac/Makefile.osx install-with-xcode\n\
+To make PmDefaults Java application,\n \
+make -f pm_mac/Makefile.osx pmdefaults\n\n \
+configuration = $(configuration)\n'
+
+
+clean:
+ rm -f *.o *~ core* */*.o */*/*.o */*~ */core* pm_test/*/pm_dll.dll
+ rm -f *.opt *.ncb *.plg pm_win/Debug/pm_dll.lib pm_win/Release/pm_dll.lib
+ rm -f pm_test/*.opt pm_test/*.ncb
+ rm -f pm_java/pmjni/*.o pm_java/pmjni/*~ pm_java/*.h
+ rm -rf Release/CMakeFiles Debug/CMakeFiles
+ rm -rf pm_mac/pmdefaults/lib pm_mac/pmdefaults/src
+
+cleaner: clean
+ rm -rf pm_mac/build
+ rm -rf pm_mac/Debug pm_mac/Release pm_test/Debug pm_test/Release
+ rm -f Debug/*.dylib Release/*.dylib
+ rm -f pm_java/pmjni/Debug/*.jnilib
+ rm -f pm_java/pmjni/Release/*.jnilib
+
+cleanest: cleaner
+ rm -f Debug/CMakeCache.txt Release/CMakeCache.txt
+ rm -f CMakeCache.txt
+ rm -f Debug/libportmidi_s.a Release/libportmidi_s.a
+ rm -f pm_test/Debug/test pm_test/Debug/sysex pm_test/Debug/midithread
+ rm -f pm_test/Debug/latency pm_test/Debug/midithru
+ rm -f pm_test/Debug/qtest pm_test/Debug/mm
+ rm -f pm_test/Release/test pm_test/Release/sysex pm_test/Release/midithread
+ rm -f pm_test/Release/latency pm_test/Release/midithru
+ rm -f pm_test/Release/qtest pm_test/Release/mm
+ rm -f pm_java/*/*.class
+ rm -f pm_java/pmjni/jportmidi_JPortMidiApi_PortMidiStream.h
+
+backup: cleanest
+ cd ..; zip -r portmidi.zip portmidi
+
+install: porttime/porttime.h pm_common/portmidi.h \
+ $(CONFIG)/libportmidi.dylib
+ install porttime/porttime.h $(PF)/include/
+ install pm_common/portmidi.h $(PF)/include
+ install $(CONFIG)/libportmidi.dylib $(PF)/lib/
+
+# note - this uses xcode to build and install portmidi universal binaries
+install-with-xcode:
+ sudo xcodebuild -project pm_mac/pm_mac.xcodeproj \
+ -configuration Release install DSTROOT=/
+
+##### build pmdefault ######
+
+pm_java/pmjni/jportmidi_JPortMidiApi.h: pm_java/jportmidi/JPortMidiApi.class
+ cd pm_java; javah jportmidi.JPortMidiApi
+ mv pm_java/jportmidi_JportMidiApi.h pm_java/pmjni
+
+JAVASRC = pmdefaults/PmDefaultsFrame.java \
+ pmdefaults/PmDefaults.java \
+ jportmidi/JPortMidiApi.java jportmidi/JPortMidi.java \
+ jportmidi/JPortMidiException.java
+
+# this compiles ALL of the java code
+pm_java/jportmidi/JPortMidiApi.class: $(JAVASRC:%=pm_java/%)
+ cd pm_java; javac $(JAVASRC)
+
+$(CONFIG)/libpmjni.dylib:
+ mkdir -p $(CONFIG)
+ cd $(CONFIG); make -f ../pm_mac/$(MAKEFILE)
+
+pmdefaults: $(CONFIG)/libpmjni.dylib pm_java/jportmidi/JPortMidiApi.class
+ifeq ($(CONFIG),Debug)
+ echo "Error: you cannot build pmdefaults in a Debug configuration \n\
+ You should use configuration=Release in the Makefile command line. "
+ @exit 2
+endif
+ xcodebuild -project pm_mac/pm_mac.xcodeproj \
+ -configuration Release -target PmDefaults
+ echo "pmdefaults java application is made"
+
diff --git a/portmidi/pm_mac/README_MAC.txt b/portmidi/pm_mac/README_MAC.txt new file mode 100644 index 0000000..41e8341 --- /dev/null +++ b/portmidi/pm_mac/README_MAC.txt @@ -0,0 +1,65 @@ +README_MAC.txt for PortMidi
+Roger Dannenberg
+20 nov 2009
+
+revised Mar 2024 to remove pmdefaults references
+revised Jan 2022 for the PortMidi/portmidi repo on github.com
+revised 20 Sep 2010 for Xcode 4.3.2 and CMake 2.8.8
+
+This documents how I build PortMidi for macOS. It's not the only way,
+and command-line/scripting enthusiasts will say it's not even a good
+way. Feel free to contribute your approach if you are willing to
+describe it carefully and test it.
+
+Install Xcode and the CMake application, CMake.app. I use the GUI
+version of CMake which makes it easy to see/edit variables and
+options.
+
+==== USING CMAKE ====
+
+Run CMake.app and select your portmidi repo working directory as the
+location for source and build. (Yes, I use so called "in-tree"
+builds -- it doesn't hurt, but I don't think it is necessary.)
+
+Default settings should all be fine, but select options under BUILD if
+you wish:
+
+BUILD_NATIVE_JAVA_INTERFACE to build a Java interface (JNI) library.
+
+BUILD_PORTMIDI_TESTS to create some test programs. Of particular
+interest are test/mm, a handy command-line MIDI Input Monitor, and
+test/testio, a simple command-line program to send or receive some
+MIDI notes in case you need a quick test: What devices do I have? Does
+this input work? Does this output work?
+
+I disable BUILD_SHARED_LIBS and always link statically: Static linking only
+adds about 40KB to any application and then you don't have to worry
+about versions, instally, copying or finding the dynamic link library,
+etc.
+
+To make sure you link statically, I rename the library to
+libportmidi_static.a. To do this, set PM_STATIC_LIB_NAME (in CMake,
+under the "PM" group) to "portmidi_static", and of course your
+application will have to specify portmidi_static as the library to
+link to.
+
+If you are building simple command-line applications, you might want
+to enable PM_CHECK_ERRORS. If you do, then calls into the PortMidi
+library will print error messages and exit in the event of an error
+(such as trying to open a device that does not exist). This saves you
+from having to check for errors everytime you call a library function
+or getting confused when errors are detected but not reported. For
+high-quality applications, do NOT enable PM_CHECK_ERRORS -- any
+failure could immediately abort your whole application, which is not
+very friendly to users.
+
+Click on Configure (maybe a couple of times).
+
+Click on Generate and make an Xcode project.
+
+Open portmidi/portmidi.xcodeproj with Xcode and build what you
+need. The simplest thing is to build the ALL_BUILD target. Be careful
+to specify a Debug or Release depending on what you want. "ALL_BUILD"
+is a misnomer -- it only builds the version you select.
+
+
diff --git a/portmidi/pm_mac/pmmac.c b/portmidi/pm_mac/pmmac.c new file mode 100755 index 0000000..48ac17a --- /dev/null +++ b/portmidi/pm_mac/pmmac.c @@ -0,0 +1,44 @@ +/* pmmac.c -- PortMidi os-dependent code */
+
+/* This file only needs to implement:
+pm_init(), which calls various routines to register the
+available midi devices,
+Pm_GetDefaultInputDeviceID(), and
+Pm_GetDefaultOutputDeviceID().
+It is seperate from pmmacosxcm because we might want to register
+non-CoreMIDI devices.
+*/
+
+#include "stdlib.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "pminternal.h"
+#include "pmmacosxcm.h"
+
+void pm_init(void)
+{
+ pm_macosxcm_init();
+}
+
+
+void pm_term(void)
+{
+ pm_macosxcm_term();
+}
+
+PmDeviceID Pm_GetDefaultInputDeviceID(void)
+{
+ Pm_Initialize();
+ return pm_default_input_device_id;
+}
+
+PmDeviceID Pm_GetDefaultOutputDeviceID(void) {
+ Pm_Initialize();
+ return pm_default_output_device_id;
+}
+
+void *pm_alloc(size_t s) { return malloc(s); }
+
+void pm_free(void *ptr) { free(ptr); }
+
+
diff --git a/portmidi/pm_mac/pmmacosxcm.c b/portmidi/pm_mac/pmmacosxcm.c new file mode 100755 index 0000000..e8b196c --- /dev/null +++ b/portmidi/pm_mac/pmmacosxcm.c @@ -0,0 +1,1179 @@ +/* + * Platform interface to the MacOS X CoreMIDI framework + * + * Jon Parise <jparise at cmu.edu> + * and subsequent work by Andrew Zeldis and Zico Kolter + * and Roger B. Dannenberg + * + * $Id: pmmacosx.c,v 1.17 2002/01/27 02:40:40 jon Exp $ + */ + +/* Notes: + + Since the input and output streams are represented by + MIDIEndpointRef values and almost no other state, we store the + MIDIEndpointRef on pm_descriptors[midi->device_id].descriptor. + + OS X does not seem to have an error-code-to-text function, so we + will just use text messages instead of error codes. + + Virtual device input synchronization: Once we create a virtual + device, it is always "on" and receiving messages, but it must drop + messages unless the device has been opened with Pm_OpenInput. To + open, the main thread should create all the data structures, then + call OSMemoryBarrier so that writes are observed, then set + is_opened = TRUE. To close without locks, we need to get the + callback to set is_opened to FALSE before we free data structures; + otherwise, there's a race condition where closing could delete + structures in use by the virtual_read_callback function. We send + 8 MIDI resets (FF) in a single packet to our own port to signal + the virtual_read_callback to close it. Then, we wait for the + callback to recognize the "close" packet and reset is_opened. + + Device scanning is done when you first open an application. + PortMIDI does not actively update the devices. Instead, you must + Pm_Terminate() and Pm_Initialize(), basically starting over. But + CoreMIDI does not have a way to shut down(!), and even + MIDIClientDispose() somehow retains state (and docs say do not + call it even if it worked). The solution, apparently, is to + call CFRunLoopRunInMode(), which somehow updates CoreMIDI + state. + + But when do we call CFRunLoopRunInMode()? I tried calling it + in midi_in_poll() which is called when you call Pm_Read() since + that is called often. I observed that this caused the program + to block for as long as 50ms and fairly often for 2 or 3ms. + What was Apple thinking? Is it really OK to design systems that + can only function with a tricky multi-threaded, non-blocking + priority-based solution, and then not provide a proof of concept + or documentation? Or is Apple's design really flawed? If anyone + at Apple reads this, please let me know -- I'm curious. + + But I digress... Here's the PortMidi approach: Since + CFRunLoopRunInMode() is potentially a non-realtime operation, + we only call it in Pm_Initialize(), where other calls to look + up devices and device names are quite slow to begin with. Again, + PortMidi does not actively scan for new or deleted devices, so + if devices change, you won't see it until the next Pm_Terminate + and Pm_Initialize. + + Calling CFRunLoopRunInMode() once is probably not enough. There + might be better way, but it seems to work to just call it 100 + times and insert 20 1ms delays (in case some inter-process + communication or synchronization is going on). + This adds 20ms to the wall time of Pm_Initialize(), but it + typically runs 30ms to much more (~4s), so this has little impact. + */ + +#include <stdlib.h> + +/* turn on lots of debugging print statements */ +#define CM_DEBUG if (0) +/* #define CM_DEBUG if (1) */ + +#include "portmidi.h" +#include "pmutil.h" +#include "pminternal.h" +#include "porttime.h" +#include "pmmacosxcm.h" + +#include <stdio.h> +#include <string.h> + +#include <CoreServices/CoreServices.h> +#include <CoreMIDI/MIDIServices.h> +#include <CoreAudio/HostTime.h> +#include <unistd.h> +#include <libkern/OSAtomic.h> + +#define PACKET_BUFFER_SIZE 1024 +/* maximum overall data rate (OS X limits MIDI rate in case there + * is a cycle among IAC ports. + */ + +#define MAX_BYTES_PER_S 5400 + +/* Apple reports that packets are dropped when the MIDI bytes/sec + exceeds 15000. This is computed by "tracking the number of MIDI + bytes scheduled into 1-second buckets over the last six seconds and + averaging these counts." This was confirmed in measurements + (2021) with pm_test/fast.c and pm_test/fastrcv.c Now, in 2022, with + macOS 12, pm_test/fast{rcv}.c show problems begin at 6000 bytes/sec. + Previously, we set MAX_BYTES_PER_S to 14000. This is reduced to + 5400 based on testing (which shows 5700 is too high) to fix the + packet loss problem that showed up with macOS 12. + + Experiments show this restriction applies to IAC bus MIDI, but not + to hardware interfaces. (I measured 0.5 Mbps each way over USB to a + Teensy 3.2 microcontroller implementing a USB MIDI loopback. Maybe + it would get 1 Mbps one-way, which would make the CoreMIDI + restriction 18x slower than USB. Maybe other USB MIDI + implementations are faster -- USB top speed for other protocols is + certainly higher than 1 Mbps!) + + This is apparently based on timestamps, not on real time, so we + have to avoid constructing packets that schedule high speed output + regardless of when writes occur. The solution is to alter + timestamps to limit data rates. This adds a slight time + distortion, e.g. an 11 note chord with all notes on the same + timestamp will be altered so that the last message is delayed by + 11 messages x 3 bytes/message / 5400 bytes/second = 6.1 ms. + Note that this is about 2x MIDI speed, but at least 18x slower + than USB MIDI. + + Altering timestamps creates another problem, which is that a sender + that exceeds the maximum rate can queue up an unbounded number of + messages. With non-USB MIDI devices, you could be writing 5x faster + to CoreMIDI than the hardware interface can send, causing an + unbounded backlog, not to mention that the output stream will be a + steady byte stream (e.g., one 3-byte MIDI message every 0.55 ms), + losing any original timing or rhythm. PortMidi does not guarantee + delivery if, over the long run, you write faster than the hardware + can send. + + The LIMIT_RATE symbol, if defined (which is the default), enables + code to modify timestamps for output to an IAC device as follows: + + Before a packet is formed, the message timestamp is set to the + maximum of the PortMidi timestamp (converted to CoreMIDI time) + and min_next_time. After each send, min_next_time is updated to + the packet time + packet length * delay_per_byte, which limits + the scheduled bytes-per-second. Also, after each packet list + flush, min_next_time is updated to the maximum of min_next_time + and the real time, which prevents many bytes to be scheduled in + the past. (We could more directly just say packets are never + scheduled in the past, but we prefer to get the current time -- a + system call -- only when we perform the more expensive operation + of flushing packets, so that's when we update min_next_time to + the current real time. If we are sending a lot, we have to flush + a lot, so the time will be updated frequently when it matters.) + + This possible adjustment to timestamps can distort accurate + timestamps by up to 0.556 us per 3-byte MIDI message. + + Nothing blocks the sender from queueing up an arbitrary number of + messages. Timestamps should be used for accurate timing by sending + timestamped messages a little ahead of real time, not for + scheduling an entire MIDI sequence at once! + */ +#define LIMIT_RATE 1 + +#define SYSEX_BUFFER_SIZE 128 +/* What is the maximum PortMidi device number for an IAC device? A + * cleaner design would be to not use the endpoint as our device + * representation. Instead, we could have a private extensible struct + * to keep all device information, including whether the device is + * implemented with the AppleMIDIIACDriver, which we need because we + * have to limit the data rate to this particular driver to avoid + * dropping messages. Rather than rewrite a lot of code, I am just + * allocating 64 bytes to flag which devices are IAC ones. If an IAC + * device number is greater than 63, PortMidi will fail to limit + * writes to it, but will not complain and will not access memory + * outside the 64-element array of char. + */ +#define MAX_IAC_NUM 63 + +#define VERBOSE_ON 1 +#define VERBOSE if (VERBOSE_ON) + +#define MIDI_SYSEX 0xf0 +#define MIDI_EOX 0xf7 +#define MIDI_CLOCK 0xf8 +#define MIDI_STATUS_MASK 0x80 + +// "Ref"s are pointers on 32-bit machines and ints on 64 bit machines +// NULL_REF is our representation of either 0 or NULL +#ifdef __LP64__ +#define NULL_REF 0 +#else +#define NULL_REF NULL +#endif + +static MIDIClientRef client = NULL_REF; /* Client handle to the MIDI server */ +static MIDIPortRef portIn = NULL_REF; /* Input port handle */ +static MIDIPortRef portOut = NULL_REF; /* Output port handle */ +static char isIAC[MAX_IAC_NUM + 1]; /* is device an IAC device */ + +extern pm_fns_node pm_macosx_in_dictionary; +extern pm_fns_node pm_macosx_out_dictionary; + +typedef struct coremidi_info_struct { + int is_virtual; /* virtual device (TRUE) or actual device (FALSE)? */ + UInt64 delta; /* difference between stream time and real time in ns */ + int sysex_mode; /* middle of sending sysex */ + uint32_t sysex_word; /* accumulate data when receiving sysex */ + uint32_t sysex_byte_count; /* count how many received */ + char error[PM_HOST_ERROR_MSG_LEN]; + char callback_error[PM_HOST_ERROR_MSG_LEN]; + Byte packetBuffer[PACKET_BUFFER_SIZE]; + MIDIPacketList *packetList; /* a pointer to packetBuffer */ + MIDIPacket *packet; + Byte sysex_buffer[SYSEX_BUFFER_SIZE]; /* temp storage for sysex data */ + MIDITimeStamp sysex_timestamp; /* host timestamp to use with sysex data */ + /* allow for running status (is running status possible here? -rbd): -cpr */ + UInt64 min_next_time; /* when can the next send take place? (host time) */ + int isIACdevice; + Float64 us_per_host_tick; /* host clock frequency, units of min_next_time */ + UInt64 host_ticks_per_byte; /* host clock units per byte at maximum rate */ +} coremidi_info_node, *coremidi_info_type; + +/* private function declarations */ +MIDITimeStamp timestamp_pm_to_cm(PmTimestamp timestamp); // returns host time +PmTimestamp timestamp_cm_to_pm(MIDITimeStamp timestamp); // returns ms + +char* cm_get_full_endpoint_name(MIDIEndpointRef endpoint, int *iac_flag); + +static PmError check_hosterror(OSStatus err, const char *msg) +{ + if (err != noErr) { + snprintf(pm_hosterror_text, PM_HOST_ERROR_MSG_LEN, "Host error %ld: %s", (long) err, msg); + pm_hosterror = TRUE; + return pmHostError; + } + return pmNoError; +} + + +static PmTimestamp midi_synchronize(PmInternal *midi) +{ + coremidi_info_type info = (coremidi_info_type) midi->api_info; + UInt64 pm_stream_time_2 = // current time in ns + AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()); + PmTimestamp real_time; // in ms + UInt64 pm_stream_time; // in ns + /* if latency is zero and this is an output, there is no + time reference and midi_synchronize should never be called */ + assert(midi->time_proc); + assert(midi->is_input || midi->latency != 0); + do { + /* read real_time between two reads of stream time */ + pm_stream_time = pm_stream_time_2; + real_time = (*midi->time_proc)(midi->time_info); + pm_stream_time_2 = AudioConvertHostTimeToNanos( + AudioGetCurrentHostTime()); + /* repeat if more than 0.5 ms has elapsed */ + } while (pm_stream_time_2 > pm_stream_time + 500000); + info->delta = pm_stream_time - ((UInt64) real_time * (UInt64) 1000000); + midi->sync_time = real_time; + return real_time; +} + + +/* called when MIDI packets are received */ +static void read_callback(const MIDIPacketList *newPackets, PmInternal *midi) +{ + PmTimestamp timestamp; + MIDIPacket *packet; + unsigned int packetIndex; + uint32_t now; + /* Retrieve the context for this connection */ + coremidi_info_type info = (coremidi_info_type) midi->api_info; + assert(info); + + CM_DEBUG printf("read_callback: numPackets %d: ", newPackets->numPackets); + + /* synchronize time references every 100ms */ + now = (*midi->time_proc)(midi->time_info); + if (midi->first_message || midi->sync_time + 100 /*ms*/ < now) { + /* time to resync */ + now = midi_synchronize(midi); + midi->first_message = FALSE; + } + + packet = (MIDIPacket *) &newPackets->packet[0]; + /* hardware devices get untimed messages and apply timestamps. We + * want to preserve them because they should be more accurate than + * applying the current time here. virtual devices just pass on the + * packet->timeStamp, which could be anything. PortMidi says the + * PortMidi timestamp is the time the message is received. We do not + * know if we are receiving from a device driver or a virtual device. + * PortMidi sends to virtual devices get a current timestamp, so we + * can treat them as the receive time. If the timestamp is zero, + * suggested by CoreMIDI as the value to use for immediate delivery, + * then we plug in `now` which is obtained above. If another + * application sends bogus non-zero timestamps, we will convert them + * to this port's reference time and pass them as event.timestamp. + * Receiver beware. + */ + CM_DEBUG printf("read_callback packet @ %lld ns (host %lld) " + "status %x length %d\n", + AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()), + AudioGetCurrentHostTime(), + packet->data[0], packet->length); + for (packetIndex = 0; packetIndex < newPackets->numPackets; packetIndex++) { + /* Set the timestamp and dispatch this message */ + CM_DEBUG printf(" packet->timeStamp %lld ns %lld host\n", + packet->timeStamp, + AudioConvertHostTimeToNanos(packet->timeStamp)); + if (packet->timeStamp == 0) { + timestamp = now; + } else { + timestamp = (PmTimestamp) /* explicit conversion */ ( + (AudioConvertHostTimeToNanos(packet->timeStamp) - info->delta) / + (UInt64) 1000000); + } + pm_read_bytes(midi, packet->data, packet->length, timestamp); + packet = MIDIPacketNext(packet); + } +} + +/* callback for real devices - redirects to read_callback */ +static void device_read_callback(const MIDIPacketList *newPackets, + void *refCon, void *connRefCon) +{ + read_callback(newPackets, (PmInternal *) connRefCon); +} + + +/* callback for virtual devices - redirects to read_callback */ +static void virtual_read_callback(const MIDIPacketList *newPackets, + void *refCon, void *connRefCon) +{ + /* this refCon is the device ID -- if there is a valid ID and + the pm_descriptors table has a non-null pointer to a PmInternal, + then then device is open and should receive this data */ + PmDeviceID id = (PmDeviceID) (size_t) refCon; + if (id >= 0 && id < pm_descriptor_len) { + if (pm_descriptors[id].pub.opened) { + /* check for close request (7 reset status bytes): */ + if (newPackets->numPackets == 1 && + newPackets->packet[0].length == 8 && + /* CoreMIDI declares packets with 4-byte alignment, so we + * should be safe to test for 8 0xFF's as 2 32-bit values: */ + *(SInt32 *) &newPackets->packet[0].data[0] == -1 && + *(SInt32 *) &newPackets->packet[0].data[4] == -1) { + CM_DEBUG printf("got close request packet\n"); + pm_descriptors[id].pub.opened = FALSE; + return; + } else { + read_callback(newPackets, pm_descriptors[id].pm_internal); + } + } + } +} + + +/* allocate and initialize our internal coremidi connection info */ +static coremidi_info_type create_macosxcm_info(int is_virtual, int is_input) +{ + coremidi_info_type info = (coremidi_info_type) + pm_alloc(sizeof(coremidi_info_node)); + if (!info) { + return NULL; + } + info->is_virtual = is_virtual; + info->delta = 0; + info->sysex_mode = FALSE; + info->sysex_word = 0; + info->sysex_byte_count = 0; + info->packet = NULL; + info->min_next_time = 0; + info->isIACdevice = FALSE; + info->us_per_host_tick = 1000000.0 / AudioGetHostClockFrequency(); + info->host_ticks_per_byte = + (UInt64) (1000000.0 / (info->us_per_host_tick * MAX_BYTES_PER_S)); + info->packetList = (is_input ? NULL : + (MIDIPacketList *) info->packetBuffer); + return info; +} + + +static PmError midi_in_open(PmInternal *midi, void *driverInfo) +{ + MIDIEndpointRef endpoint; + coremidi_info_type info; + OSStatus macHostError; + int is_virtual = pm_descriptors[midi->device_id].pub.is_virtual; + + /* if this is an external device, descriptor is a MIDIEndpointRef. + * if this is a virtual device for this application, descriptor is NULL. + */ + if (!is_virtual) { + endpoint = (MIDIEndpointRef) (intptr_t) + pm_descriptors[midi->device_id].descriptor; + if (endpoint == NULL_REF) { + return pmInvalidDeviceId; + } + } + + info = create_macosxcm_info(is_virtual, TRUE); + midi->api_info = info; + if (!info) { + return pmInsufficientMemory; + } + if (!is_virtual) { + macHostError = MIDIPortConnectSource(portIn, endpoint, midi); + if (macHostError != noErr) { + midi->api_info = NULL; + pm_free(info); + return check_hosterror(macHostError, + "MIDIPortConnectSource() in midi_in_open()"); + } + } + return pmNoError; +} + +static PmError midi_in_close(PmInternal *midi) +{ + MIDIEndpointRef endpoint; + OSStatus macHostError; + PmError err = pmNoError; + + coremidi_info_type info = (coremidi_info_type) midi->api_info; + + if (!info) return pmBadPtr; + + endpoint = (MIDIEndpointRef) (intptr_t) + pm_descriptors[midi->device_id].descriptor; + if (endpoint == NULL_REF) { + return pmBadPtr; + } + + if (!info->is_virtual) { + /* shut off the incoming messages before freeing data structures */ + macHostError = MIDIPortDisconnectSource(portIn, endpoint); + /* If the source closes, you get paramErr == -50 here. It seems + * possible to monitor changes like sources closing by getting + * notifications ALL changes, but the CoreMIDI documentation is + * really terrible overall, and it seems easier to just ignore + * this host error. + */ + if (macHostError != noErr && macHostError != -50) { + pm_hosterror = TRUE; + err = check_hosterror(macHostError, + "MIDIPortDisconnectSource() in midi_in_close()"); + } + } else { + /* make "close virtual port" message */ + SInt64 close_port_bytes = 0xFFFFFFFFFFFFFFFF; + /* memory requirements: packet count (4), timestamp (8), length (2), + * data (8). Total: 22, but we allocate plenty more: + */ + Byte packetBuffer[64]; + MIDIPacketList *plist = (MIDIPacketList *) packetBuffer; + MIDIPacket *packet = MIDIPacketListInit(plist); + MIDIPacketListAdd(plist, 64, packet, 0, 8, + (const Byte *) &close_port_bytes); + macHostError = MIDISend(portOut, endpoint, plist); + if (macHostError != noErr) { + err = check_hosterror(macHostError, "MIDISend() (PortMidi close " + "port packet) in midi_in_close()"); + } + /* when packet is delivered, callback thread will clear opened; + * we must wait for that before removing the input queues etc. + * Maybe this could use signals of some kind, but if signals use + * locks, locks can cause priority inversion problems, so we will + * just sleep as needed. On the MIDI timescale, inserting a 0.5ms + * latency should be OK, as the application has no business + * opening/closing devices during time-critical moments. + * + * We expect the MIDI thread to close the device quickly (<0.5ms), + * but we wait up to 50ms in case something terrible happens like + * getting paged out in the middle of deliving packets to this + * virtual device. If there is still no response, we time out and + * force the close without the MIDI thread (even this will probably + * succeed - the problem would be: this thread proceeds to delete + * the input queues, and the freed memory is reallocated and + * overwritten so that queues are no longer usable. Meanwhile, + * the MIDI thread has already begun to deliver packets, so the + * check for opened == TRUE passed, but MIDI thread does not insert + * into queue until queue is freed, reallocated and overwritten. + */ + for (int i = 0; i < 100; i++) { /* up to 50ms delay */ + if (!pm_descriptors[midi->device_id].pub.opened) { + break; + } + usleep(500); /* 0.5ms */ + } + pm_descriptors[midi->device_id].pub.opened = FALSE; /* force it */ + } + midi->api_info = NULL; + pm_free(info); + return err; +} + + +static PmError midi_out_open(PmInternal *midi, void *driverInfo) +{ + coremidi_info_type info; + int is_virtual = pm_descriptors[midi->device_id].pub.is_virtual; + + info = create_macosxcm_info(is_virtual, FALSE); + if (midi->device_id <= MAX_IAC_NUM) { + info->isIACdevice = isIAC[midi->device_id]; + CM_DEBUG printf("midi_out_open isIACdevice %d\n", info->isIACdevice); + } + midi->api_info = info; + if (!info) { + return pmInsufficientMemory; + } + return pmNoError; +} + + +static PmError midi_out_close(PmInternal *midi) +{ + coremidi_info_type info = (coremidi_info_type) midi->api_info; + if (!info) return pmBadPtr; + midi->api_info = NULL; + pm_free(info); + return pmNoError; +} + + +/* MIDIDestinationCreate apparently cannot create a virtual device + * without a callback and a "refCon" parameter, but when we create + * a virtual device, we do not want a PortMidi stream yet -- that + * should wait for the user to open the stream. So, for the refCon, + * use the PortMidi device ID. The callback will check if the + * device is opened within PortMidi, and if so, use the pm_descriptors + * table to locate the corresponding PmStream. + */ +static PmError midi_create_virtual(int is_input, const char *name, + void *device_info) +{ + OSStatus macHostError; + MIDIEndpointRef endpoint; + CFStringRef nameRef; + PmDeviceID id = pm_add_device("CoreMIDI", name, is_input, TRUE, NULL, + (is_input ? &pm_macosx_in_dictionary : + &pm_macosx_out_dictionary)); + if (id < 0) { /* error -- out of memory or name conflict? */ + return id; + } + + nameRef = CFStringCreateWithCString(NULL, name, kCFStringEncodingUTF8); + if (is_input) { + macHostError = MIDIDestinationCreate(client, nameRef, + virtual_read_callback, (void *) (intptr_t) id, &endpoint); + } else { + macHostError = MIDISourceCreate(client, nameRef, &endpoint); + } + CFRelease(nameRef); + + if (macHostError != noErr) { + /* undo the device we just allocated */ + pm_undo_add_device(id); + return check_hosterror(macHostError, (is_input ? + "MIDIDestinationCreateWithProtocol() in midi_create_virtual()" : + "MIDISourceCreateWithProtocol() in midi_create_virtual()")); + } + + /* Do we have a manufacturer name? If not, set to "PortMidi" */ + const char *mfr_name = "PortMidi"; + PmSysDepInfo *info = (PmSysDepInfo *) device_info; + /* the version where pmKeyCoreMidiManufacturer was introduced is 210 */ + if (info && info->structVersion >= 210) { + int i; + for (i = 0; i < info->length; i++) { /* search for key */ + if (info->properties[i].key == pmKeyCoreMidiManufacturer) { + mfr_name = info->properties[i].value; + break; + } /* no other keys are recognized; they are ignored */ + } + } + nameRef = CFStringCreateWithCString(NULL, mfr_name, kCFStringEncodingUTF8); + MIDIObjectSetStringProperty(endpoint, kMIDIPropertyManufacturer, nameRef); + CFRelease(nameRef); + + pm_descriptors[id].descriptor = (void *) (intptr_t) endpoint; + return id; +} + + +static PmError midi_delete_virtual(PmDeviceID id) +{ + MIDIEndpointRef endpoint; + OSStatus macHostError; + + endpoint = (MIDIEndpointRef) (long) pm_descriptors[id].descriptor; + if (endpoint == NULL_REF) { + return pmBadPtr; + } + macHostError = MIDIEndpointDispose(endpoint); + return check_hosterror(macHostError, + "MIDIEndpointDispose() in midi_in_close()"); +} + + +static PmError midi_abort(PmInternal *midi) +{ + OSStatus macHostError; + MIDIEndpointRef endpoint = (MIDIEndpointRef) (intptr_t) + pm_descriptors[midi->device_id].descriptor; + macHostError = MIDIFlushOutput(endpoint); + return check_hosterror(macHostError, + "MIDIFlushOutput() in midi_abort()"); +} + + +static PmError midi_write_flush(PmInternal *midi, PmTimestamp timestamp) +{ + OSStatus macHostError = 0; + coremidi_info_type info = (coremidi_info_type) midi->api_info; + MIDIEndpointRef endpoint = (MIDIEndpointRef) (intptr_t) + pm_descriptors[midi->device_id].descriptor; + assert(info); + assert(endpoint); + if (info->packet != NULL) { + /* out of space, send the buffer and start refilling it */ + /* update min_next_time each flush to support rate limit */ + UInt64 host_now = AudioGetCurrentHostTime(); + if (host_now > info->min_next_time) + info->min_next_time = host_now; + if (info->is_virtual) { + macHostError = MIDIReceived(endpoint, info->packetList); + } else { + macHostError = MIDISend(portOut, endpoint, info->packetList); + } + info->packet = NULL; /* indicate no data in packetList now */ + } + return check_hosterror(macHostError, (info->is_virtual ? + "MIDIReceived() in midi_write()" : + "MIDISend() in midi_write()")); +} + + +static PmError send_packet(PmInternal *midi, Byte *message, + unsigned int messageLength, MIDITimeStamp timestamp) +{ + PmError err; + coremidi_info_type info = (coremidi_info_type) midi->api_info; + assert(info); + + CM_DEBUG printf("add %d to packet %p len %d timestamp %lld @ %lld ns " + "(host %lld)\n", + message[0], info->packet, messageLength, timestamp, + AudioConvertHostTimeToNanos(AudioGetCurrentHostTime()), + AudioGetCurrentHostTime()); + info->packet = MIDIPacketListAdd(info->packetList, + sizeof(info->packetBuffer), info->packet, + timestamp, messageLength, message); +#if LIMIT_SEND_RATE + info->byte_count += messageLength; +#endif + if (info->packet == NULL) { + /* out of space, send the buffer and start refilling it */ + /* make midi->packet non-null to fool midi_write_flush into sending */ + info->packet = (MIDIPacket *) 4; + /* timestamp is 0 because midi_write_flush ignores timestamp since + * timestamps are already in packets. The timestamp parameter is here + * because other API's need it. midi_write_flush can be called + * from system-independent code that must be cross-API. + */ + if ((err = midi_write_flush(midi, 0)) != pmNoError) return err; + info->packet = MIDIPacketListInit(info->packetList); + assert(info->packet); /* if this fails, it's a programming error */ + info->packet = MIDIPacketListAdd(info->packetList, + sizeof(info->packetBuffer), info->packet, + timestamp, messageLength, message); + assert(info->packet); /* can't run out of space on first message */ + } + return pmNoError; +} + + +static PmError midi_write_short(PmInternal *midi, PmEvent *event) +{ + PmTimestamp when = event->timestamp; + PmMessage what = event->message; + MIDITimeStamp timestamp; + coremidi_info_type info = (coremidi_info_type) midi->api_info; + Byte message[4]; + unsigned int messageLength; + + if (info->packet == NULL) { + info->packet = MIDIPacketListInit(info->packetList); + /* this can never fail, right? failure would indicate something + unrecoverable */ + assert(info->packet); + } + + /* PortMidi specifies that incoming timestamps are the receive + * time. Devices attach their receive times, but virtual devices + * do not. Instead, they pass along whatever timestamp was sent to + * them. We do not know if we are connected to real or virtual + * device. To avoid wild timestamps on the receiving end, we + * consider 2 cases: PortMidi timestamp is zero or latency is + * zero. Both mean send immediately, so we attach the current time + * which will go out immediately and arrive with a sensible + * timestamp (not zero and not zero mapped to the client's local + * time). Otherwise, we assume the timestamp is reasonable. It + * might be slighly in the past, but we pass it along after + * translation to MIDITimeStamp units. + * + * Compute timestamp: use current time if timestamp is zero or + * latency is zero. Both mean no timing and send immediately. + */ + if (when == 0 || midi->latency == 0) { + timestamp = AudioGetCurrentHostTime(); + } else { /* translate PortMidi time + latency to CoreMIDI time */ + timestamp = ((UInt64) (when + midi->latency) * (UInt64) 1000000) + + info->delta; + timestamp = AudioConvertNanosToHostTime(timestamp); + } + + message[0] = Pm_MessageStatus(what); + message[1] = Pm_MessageData1(what); + message[2] = Pm_MessageData2(what); + messageLength = pm_midi_length((int32_t) what); + +#ifdef LIMIT_RATE + /* Make sure we go forward in time. */ + if (timestamp < info->min_next_time) { + timestamp = info->min_next_time; + } + /* Note that if application is way behind and slowly catching up, then + * timestamps could be increasing faster than real time, and since + * timestamps are used to estimate data rate, our estimate could be + * low, causing CoreMIDI to drop packets. This seems very unlikely. + */ + if (info->isIACdevice || info->is_virtual) { + info->min_next_time = timestamp + messageLength * + info->host_ticks_per_byte; + } +#endif + /* Add this message to the packet list */ + return send_packet(midi, message, messageLength, timestamp); +} + + +static PmError midi_begin_sysex(PmInternal *midi, PmTimestamp when) +{ + UInt64 when_ns; + coremidi_info_type info = (coremidi_info_type) midi->api_info; + assert(info); + info->sysex_byte_count = 0; + + /* compute timestamp */ + if (when == 0) when = midi->now; + /* if latency == 0, midi->now is not valid. We will just set it to zero */ + if (midi->latency == 0) when = 0; + when_ns = ((UInt64) (when + midi->latency) * (UInt64) 1000000) + + info->delta; + info->sysex_timestamp = + (MIDITimeStamp) AudioConvertNanosToHostTime(when_ns); + UInt64 now; /* only make system time call when writing a virtual port */ + if (info->is_virtual && info->sysex_timestamp < + (now = AudioGetCurrentHostTime())) { + info->sysex_timestamp = now; + } + + if (info->packet == NULL) { + info->packet = MIDIPacketListInit(info->packetList); + /* this can never fail, right? failure would indicate something + unrecoverable */ + assert(info->packet); + } + return pmNoError; +} + + +static PmError midi_end_sysex(PmInternal *midi, PmTimestamp when) +{ + PmError err; + coremidi_info_type info = (coremidi_info_type) midi->api_info; + assert(info); + +#ifdef LIMIT_RATE + /* make sure we go foreward in time */ + if (info->sysex_timestamp < info->min_next_time) + info->sysex_timestamp = info->min_next_time; + + if (info->isIACdevice) { + info->min_next_time = info->sysex_timestamp + info->sysex_byte_count * + info->host_ticks_per_byte; + } +#endif + + /* now send what's in the buffer */ + err = send_packet(midi, info->sysex_buffer, info->sysex_byte_count, + info->sysex_timestamp); + info->sysex_byte_count = 0; + if (err != pmNoError) { + info->packet = NULL; /* flush everything in the packet list */ + } + return err; +} + + +static PmError midi_write_byte(PmInternal *midi, unsigned char byte, + PmTimestamp timestamp) +{ + coremidi_info_type info = (coremidi_info_type) midi->api_info; + assert(info); + if (info->sysex_byte_count >= SYSEX_BUFFER_SIZE) { + PmError err = midi_end_sysex(midi, timestamp); + if (err != pmNoError) return err; + } + info->sysex_buffer[info->sysex_byte_count++] = byte; + return pmNoError; +} + + +static PmError midi_write_realtime(PmInternal *midi, PmEvent *event) +{ + /* to send a realtime message during a sysex message, first + flush all pending sysex bytes into packet list */ + PmError err = midi_end_sysex(midi, 0); + if (err != pmNoError) return err; + /* then we can just do a normal midi_write_short */ + return midi_write_short(midi, event); +} + + +static unsigned int midi_check_host_error(PmInternal *midi) +{ + return FALSE; +} + + +MIDITimeStamp timestamp_pm_to_cm(PmTimestamp timestamp) +{ + UInt64 nanos; + if (timestamp <= 0) { + return (MIDITimeStamp)0; + } else { + nanos = (UInt64)timestamp * (UInt64)1000000; + return (MIDITimeStamp)AudioConvertNanosToHostTime(nanos); + } +} + + +PmTimestamp timestamp_cm_to_pm(MIDITimeStamp timestamp) +{ + UInt64 nanos; + nanos = AudioConvertHostTimeToNanos(timestamp); + return (PmTimestamp)(nanos / (UInt64)1000000); +} + + +// +// Code taken from http://developer.apple.com/qa/qa2004/qa1374.html +////////////////////////////////////// +// Obtain the name of an endpoint without regard for whether it has connections. +// The result should be released by the caller. +CFStringRef EndpointName(MIDIEndpointRef endpoint, bool isExternal, + int *iac_flag) +{ + CFMutableStringRef result = CFStringCreateMutable(NULL, 0); + CFStringRef str; + *iac_flag = FALSE; + + // begin with the endpoint's name + str = NULL; + MIDIObjectGetStringProperty(endpoint, kMIDIPropertyName, &str); + if (str != NULL) { + CFStringAppend(result, str); + CFRelease(str); + } + MIDIEntityRef entity = NULL_REF; + MIDIEndpointGetEntity(endpoint, &entity); + if (entity == NULL_REF) { + // probably virtual + return result; + } + if (!isExternal) { /* detect IAC devices */ + //extern const CFStringRef kMIDIPropertyDriverOwner; + MIDIObjectGetStringProperty(entity, kMIDIPropertyDriverOwner, &str); + if (str != NULL) { + char s[32]; /* driver name may truncate, but that's OK */ + CFStringGetCString(str, s, 31, kCFStringEncodingUTF8); + s[31] = 0; /* make sure it is terminated just to be safe */ + CM_DEBUG printf("driver %s\n", s); + *iac_flag = (strcmp(s, "com.apple.AppleMIDIIACDriver") == 0); + } + } + + if (CFStringGetLength(result) == 0) { + // endpoint name has zero length -- try the entity + str = NULL; + MIDIObjectGetStringProperty(entity, kMIDIPropertyName, &str); + if (str != NULL) { + CFStringAppend(result, str); + CFRelease(str); + } + } + // now consider the device's name + MIDIDeviceRef device = NULL_REF; + MIDIEntityGetDevice(entity, &device); + if (device == NULL_REF) + return result; + + str = NULL; + MIDIObjectGetStringProperty(device, kMIDIPropertyName, &str); + if (CFStringGetLength(result) == 0) { + CFRelease(result); + return str; + } + if (str != NULL) { + // if an external device has only one entity, throw away + // the endpoint name and just use the device name + if (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2) { + CFRelease(result); + return str; + } else { + if (CFStringGetLength(str) == 0) { + CFRelease(str); + return result; + } + // does the entity name already start with the device name? + // (some drivers do this though they shouldn't) + // if so, do not prepend + if (CFStringCompareWithOptions(result, /* endpoint name */ + str, /* device name */ + CFRangeMake(0, CFStringGetLength(str)), 0) != + kCFCompareEqualTo) { + // prepend the device name to the entity name + if (CFStringGetLength(result) > 0) + CFStringInsert(result, 0, CFSTR(" ")); + CFStringInsert(result, 0, str); + } + CFRelease(str); + } + } + return result; +} + + +// Obtain the name of an endpoint, following connections. +// The result should be released by the caller. +static CFStringRef ConnectedEndpointName(MIDIEndpointRef endpoint, + int *iac_flag) +{ + CFMutableStringRef result = CFStringCreateMutable(NULL, 0); + CFStringRef str; + OSStatus err; + long i; + + // Does the endpoint have connections? + CFDataRef connections = NULL; + long nConnected = 0; + bool anyStrings = false; + err = MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, + &connections); + if (connections != NULL) { + // It has connections, follow them + // Concatenate the names of all connected devices + nConnected = CFDataGetLength(connections) / + (int32_t) sizeof(MIDIUniqueID); + if (nConnected) { + const SInt32 *pid = (const SInt32 *)(CFDataGetBytePtr(connections)); + for (i = 0; i < nConnected; ++i, ++pid) { + MIDIUniqueID id = EndianS32_BtoN(*pid); + MIDIObjectRef connObject; + MIDIObjectType connObjectType; + err = MIDIObjectFindByUniqueID(id, &connObject, + &connObjectType); + if (err == noErr) { + if (connObjectType == kMIDIObjectType_ExternalSource || + connObjectType == kMIDIObjectType_ExternalDestination) { + // Connected to an external device's endpoint (>=10.3) + str = EndpointName((MIDIEndpointRef)(connObject), true, + iac_flag); + } else { + // Connected to an external device (10.2) + // (or something else, catch-all) + str = NULL; + MIDIObjectGetStringProperty(connObject, + kMIDIPropertyName, &str); + } + if (str != NULL) { + if (anyStrings) + CFStringAppend(result, CFSTR(", ")); + else anyStrings = true; + CFStringAppend(result, str); + CFRelease(str); + } + } + } + } + CFRelease(connections); + } + if (anyStrings) + return result; // caller should release result + + CFRelease(result); + + // Here, either the endpoint had no connections, or we failed to + // obtain names for any of them. + return EndpointName(endpoint, false, iac_flag); +} + + +char *cm_get_full_endpoint_name(MIDIEndpointRef endpoint, int *iac_flag) +{ + /* Thanks to Dan Wilcox for fixes for Unicode handling */ + CFStringRef fullName = ConnectedEndpointName(endpoint, iac_flag); + CFIndex utf16_len = CFStringGetLength(fullName) + 1; + CFIndex max_byte_len = CFStringGetMaximumSizeForEncoding( + utf16_len, kCFStringEncodingUTF8) + 1; + char* pmname = (char *) pm_alloc(CFStringGetLength(fullName) + 1); + + /* copy the string into our buffer; note that there may be some wasted + space, but the total waste is not large */ + CFStringGetCString(fullName, pmname, max_byte_len, kCFStringEncodingUTF8); + + /* clean up */ + if (fullName) CFRelease(fullName); + return pmname; +} + + +pm_fns_node pm_macosx_in_dictionary = { + none_write_short, + none_sysex, + none_sysex, + none_write_byte, + none_write_short, + none_write_flush, + none_synchronize, + midi_in_open, + midi_abort, + midi_in_close, + success_poll, + midi_check_host_error +}; + +pm_fns_node pm_macosx_out_dictionary = { + midi_write_short, + midi_begin_sysex, + midi_end_sysex, + midi_write_byte, + midi_write_realtime, + midi_write_flush, + midi_synchronize, + midi_out_open, + midi_abort, + midi_out_close, + success_poll, + midi_check_host_error +}; + + +/* We do nothing with callbacks, but generating the callbacks also + * updates CoreMIDI state. Callback may not be essential, but calling + * the CFRunLoopRunInMode is necessary. + */ +void cm_notify(const MIDINotification *msg, void *refCon) +{ + /* for debugging, trace change notifications: + const char *descr[] = { + "undefined (0)", + "kMIDIMsgSetupChanged", + "kMIDIMsgObjectAdded", + "kMIDIMsgObjectRemoved", + "kMIDIMsgPropertyChanged", + "kMIDIMsgThruConnectionsChanged", + "kMIDIMsgSerialPortOwnerChanged", + "kMIDIMsgIOError"}; + + printf("MIDI Notify, messageID %d (%s)\n", (int) msg->messageID, + descr[(int) msg->messageID]); + */ + return; +} + + +PmError pm_macosxcm_init(void) +{ + ItemCount numInputs, numOutputs, numDevices; + MIDIEndpointRef endpoint; + OSStatus macHostError = noErr; + char *error_text; + + memset(isIAC, 0, sizeof(isIAC)); /* initialize all FALSE */ + + /* Register interface CoreMIDI with create_virtual fn */ + pm_add_interf("CoreMIDI", &midi_create_virtual, &midi_delete_virtual); + /* no check for error return because this always succeeds */ + + /* Determine the number of MIDI devices on the system */ + numDevices = MIDIGetNumberOfDevices(); + + /* Return prematurely if no devices exist on the system + Note that this is not an error. There may be no devices. + Pm_CountDevices() will return zero, which is correct and + useful information + */ + if (numDevices <= 0) { + return pmNoError; + } + + /* Initialize the client handle */ + if (client == NULL_REF) { + macHostError = MIDIClientCreate(CFSTR("PortMidi"), &cm_notify, NULL, + &client); + } else { /* see notes above on device scanning */ + for (int i = 0; i < 100; i++) { + // look for any changes before scanning for devices + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true); + if (i % 5 == 0) Pt_Sleep(1); /* insert 20 delays */ + } + } + if (macHostError != noErr) { + error_text = "MIDIClientCreate() in pm_macosxcm_init()"; + goto error_return; + } + numInputs = MIDIGetNumberOfSources(); + numOutputs = MIDIGetNumberOfDestinations(); + + /* Create the input port */ + macHostError = MIDIInputPortCreate(client, CFSTR("Input port"), + device_read_callback, NULL, &portIn); + if (macHostError != noErr) { + error_text = "MIDIInputPortCreate() in pm_macosxcm_init()"; + goto error_return; + } + + /* Create the output port */ + macHostError = MIDIOutputPortCreate(client, CFSTR("Output port"), &portOut); + if (macHostError != noErr) { + error_text = "MIDIOutputPortCreate() in pm_macosxcm_init()"; + goto error_return; + } + + /* Iterate over the MIDI input devices */ + for (int i = 0; i < numInputs; i++) { + int iac_flag; + endpoint = MIDIGetSource(i); + if (endpoint == NULL_REF) { + continue; + } + /* Register this device with PortMidi */ + pm_add_device("CoreMIDI", + cm_get_full_endpoint_name(endpoint, &iac_flag), TRUE, FALSE, + (void *) (intptr_t) endpoint, &pm_macosx_in_dictionary); + } + + /* Iterate over the MIDI output devices */ + for (int i = 0; i < numOutputs; i++) { + int iac_flag; + PmDeviceID id; + endpoint = MIDIGetDestination(i); + if (endpoint == NULL_REF) { + continue; + } + /* Register this device with PortMidi */ + id = pm_add_device("CoreMIDI", + cm_get_full_endpoint_name(endpoint, &iac_flag), FALSE, FALSE, + (void *) (intptr_t) endpoint, &pm_macosx_out_dictionary); + /* if this is an IAC device, tuck that info away for write functions */ + if (iac_flag && id <= MAX_IAC_NUM) { + isIAC[id] = TRUE; + } + } + return pmNoError; + +error_return: + pm_macosxcm_term(); /* clear out any opened ports */ + return check_hosterror(macHostError, error_text); +} + +void pm_macosxcm_term(void) +{ + /* docs say do not explicitly dispose of client + if (client != NULL_REF) MIDIClientDispose(client); */ + if (portIn != NULL_REF) MIDIPortDispose(portIn); + if (portOut != NULL_REF) MIDIPortDispose(portOut); +} diff --git a/portmidi/pm_mac/pmmacosxcm.h b/portmidi/pm_mac/pmmacosxcm.h new file mode 100755 index 0000000..166a0b7 --- /dev/null +++ b/portmidi/pm_mac/pmmacosxcm.h @@ -0,0 +1,4 @@ +/* system-specific definitions */
+
+PmError pm_macosxcm_init(void);
+void pm_macosxcm_term(void);
diff --git a/portmidi/pm_sndio/pmsndio.c b/portmidi/pm_sndio/pmsndio.c new file mode 100644 index 0000000..0c1ea11 --- /dev/null +++ b/portmidi/pm_sndio/pmsndio.c @@ -0,0 +1,365 @@ +/* pmsndio.c -- PortMidi os-dependent code */ + +#include <stdlib.h> +#include <stdio.h> +#include <sndio.h> +#include <string.h> +#include <poll.h> +#include <errno.h> +#include <pthread.h> +#include "portmidi.h" +#include "pmutil.h" +#include "pminternal.h" +#include "porttime.h" + +#define NDEVS 9 +#define SYSEX_MAXLEN 1024 + +#define SYSEX_START 0xf0 +#define SYSEX_END 0xf7 + +extern pm_fns_node pm_sndio_in_dictionary; +extern pm_fns_node pm_sndio_out_dictionary; + +/* length of voice and common messages (status byte included) */ +unsigned int voice_len[] = { 3, 3, 3, 3, 2, 2, 3 }; +unsigned int common_len[] = { 0, 2, 3, 2, 0, 0, 1, 1 }; + +struct mio_dev { + char name[16]; + struct mio_hdl *hdl; + int mode; + char errmsg[PM_HOST_ERROR_MSG_LEN]; + pthread_t thread; +} devs[NDEVS]; + +static void set_mode(struct mio_dev *, unsigned int); + +void pm_init() +{ + int i, j, k = 0; + char devices[][16] = {"midithru", "rmidi", "midi", "snd"}; + + /* default */ + strcpy(devs[0].name, MIO_PORTANY); + pm_add_device("SNDIO", devs[k].name, TRUE, FALSE, (void *) &devs[k], + &pm_sndio_in_dictionary); + pm_add_device("SNDIO", devs[k].name, FALSE, FALSE, (void *) &devs[k], + &pm_sndio_out_dictionary); + k++; + + for (i = 0; i < 4; i++) { + for (j = 0; j < 2; j++) { + sprintf(devs[k].name, "%s/%d", devices[i], j); + pm_add_device("SNDIO", devs[k].name, TRUE, FALSE, (void *) &devs[k], + &pm_sndio_in_dictionary); + pm_add_device("SNDIO", devs[k].name, FALSE, FALSE, (void *) &devs[k], + &pm_sndio_out_dictionary); + k++; + } + } + + // this is set when we return to Pm_Initialize, but we need it + // now in order to (successfully) call Pm_CountDevices() + pm_initialized = TRUE; + pm_default_input_device_id = 0; + pm_default_output_device_id = 1; +} + +void pm_term(void) +{ + int i; + for(i = 0; i < NDEVS; i++) { + if (devs[i].mode != 0) { + set_mode(&devs[i], 0); + if (devs[i].thread) { + pthread_join(devs[i].thread, NULL); + devs[i].thread = NULL; + } + } + } +} + +PmDeviceID Pm_GetDefaultInputDeviceID() { + Pm_Initialize(); + return pm_default_input_device_id; +} + +PmDeviceID Pm_GetDefaultOutputDeviceID() { + Pm_Initialize(); + return pm_default_output_device_id; +} + +void *pm_alloc(size_t s) { return malloc(s); } + +void pm_free(void *ptr) { free(ptr); } + +/* midi_message_length -- how many bytes in a message? */ +static int midi_message_length(PmMessage message) +{ + unsigned char st = message & 0xff; + if (st >= 0xf8) + return 1; + else if (st >= 0xf0) + return common_len[st & 7]; + else if (st >= 0x80) + return voice_len[(st >> 4) & 7]; + else + return 0; +} + +void* input_thread(void *param) +{ + PmInternal *midi = (PmInternal*)param; + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + struct pollfd pfd[1]; + nfds_t nfds; + unsigned char st = 0, c = 0; + int rc, revents, idx = 0, len = 0; + size_t todo = 0; + unsigned char buf[0x200], *p; + PmEvent pm_ev, pm_ev_rt; + unsigned char sysex_data[SYSEX_MAXLEN]; + + while(dev->mode & MIO_IN) { + if (todo == 0) { + nfds = mio_pollfd(dev->hdl, pfd, POLLIN); + rc = poll(pfd, nfds, 100); + if (rc < 0) { + if (errno == EINTR) + continue; + break; + } + revents = mio_revents(dev->hdl, pfd); + if (!(revents & POLLIN)) + continue; + + todo = mio_read(dev->hdl, buf, sizeof(buf)); + if (todo == 0) + continue; + p = buf; + } + c = *p++; + todo--; + + if (c >= 0xf8) { + pm_ev_rt.message = c; + pm_ev_rt.timestamp = Pt_Time(); + pm_read_short(midi, &pm_ev_rt); + } else if (c == SYSEX_END) { + /* note: PortMidi is designed to avoid the need for SYSEX_MAXLEN. + With the new implementation of pm_read_bytes, it would be + better to simply call pm_read_bytes() and let it parse buf, + which can contain any number of whole or partial messages with + interleaved realtime messages. I did not change the code because + I cannot test it. -RBD */ + if (st == SYSEX_START) { + sysex_data[idx++] = c; + pm_read_bytes(midi, sysex_data, idx, Pt_Time()); + } + st = 0; + idx = 0; + } else if (c == SYSEX_START) { + st = c; + idx = 0; + sysex_data[idx++] = c; + } else if (c >= 0xf0) { + pm_ev.message = c; + len = common_len[c & 7]; + st = c; + idx = 1; + } else if (c >= 0x80) { + pm_ev.message = c; + len = voice_len[(c >> 4) & 7]; + st = c; + idx = 1; + } else if (st == SYSEX_START) { + if (idx == SYSEX_MAXLEN) { + fprintf(stderr, "the message is too long\n"); + idx = st = 0; + } else { + sysex_data[idx++] = c; + } + } else if (st) { + if (idx == 0 && st != SYSEX_START) + pm_ev.message |= (c << (8 * idx++)); + pm_ev.message |= (c << (8 * idx++)); + if (idx == len) { + pm_read_short(midi, &pm_ev); + if (st >= 0xf0) + st = 0; + idx = 0; + } + } + } + + pthread_exit(NULL); + return NULL; +} + +static void set_mode(struct mio_dev *dev, unsigned int mode) { + if (dev->mode != 0) + mio_close(dev->hdl); + dev->mode = 0; + if (mode != 0) + dev->hdl = mio_open(dev->name, mode, 0); + if (dev->hdl) + dev->mode = mode; +} + +static PmError sndio_out_open(PmInternal *midi, void *driverInfo) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + if (dev->mode & MIO_OUT) + return pmNoError; + + set_mode(dev, dev->mode | MIO_OUT); + if (!(dev->mode & MIO_OUT)) { + snprintf(dev->errmsg, PM_HOST_ERROR_MSG_LEN, + "mio_open (output) failed: %s\n", dev->name); + return pmHostError; + } + + return pmNoError; +} + +static PmError sndio_in_open(PmInternal *midi, void *driverInfo) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + if (dev->mode & MIO_IN) + return pmNoError; + + set_mode(dev, dev->mode | MIO_IN); + if (!(dev->mode & MIO_IN)) { + snprintf(dev->errmsg, PM_HOST_ERROR_MSG_LEN, + "mio_open (input) failed: %s\n", dev->name); + return pmHostError; + } + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_create(&dev->thread, &attr, input_thread, ( void* )midi); + return pmNoError; +} + +static PmError sndio_out_close(PmInternal *midi) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + if (dev->mode & MIO_OUT) + set_mode(dev, dev->mode & ~MIO_OUT); + return pmNoError; +} + +static PmError sndio_in_close(PmInternal *midi) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + if (dev->mode & MIO_IN) { + set_mode(dev, dev->mode & ~MIO_IN); + pthread_join(dev->thread, NULL); + dev->thread = NULL; + } + return pmNoError; +} + +static PmError sndio_abort(PmInternal *midi) +{ + return pmNoError; +} + +static PmTimestamp sndio_synchronize(PmInternal *midi) +{ + return 0; +} + +static PmError do_write(struct mio_dev *dev, const void *addr, size_t nbytes) +{ + size_t w = mio_write(dev->hdl, addr, nbytes); + + if (w != nbytes) { + snprintf(dev->errmsg, PM_HOST_ERROR_MSG_LEN, + "mio_write failed, bytes written:%zu\n", w); + return pmHostError; + } + return pmNoError; +} + +static PmError sndio_write_byte(PmInternal *midi, unsigned char byte, + PmTimestamp timestamp) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + return do_write(dev, &byte, 1); +} + +static PmError sndio_write_short(PmInternal *midi, PmEvent *event) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + int nbytes = midi_message_length(event->message); + + if (midi->latency > 0) { + /* XXX the event should be queued for later playback */ + return do_write(dev, &event->message, nbytes); + } else { + return do_write(dev, &event->message, nbytes); + } + return pmNoError; +} + +static PmError sndio_write_flush(PmInternal *midi, PmTimestamp timestamp) +{ + return pmNoError; +} + +PmError sndio_sysex(PmInternal *midi, PmTimestamp timestamp) +{ + return pmNoError; +} + +static unsigned int sndio_has_host_error(PmInternal *midi) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + return (dev->errmsg[0] != '\0'); +} + +static void sndio_get_host_error(PmInternal *midi, char *msg, unsigned int len) +{ + struct mio_dev *dev = pm_descriptors[midi->device_id].descriptor; + + strlcpy(msg, dev->errmsg, len); + dev->errmsg[0] = '\0'; +} + +pm_fns_node pm_sndio_in_dictionary = { + none_write_short, + none_sysex, + none_sysex, + none_write_byte, + none_write_short, + none_write_flush, + sndio_synchronize, + sndio_in_open, + sndio_abort, + sndio_in_close, + success_poll, + sndio_has_host_error, +}; + +pm_fns_node pm_sndio_out_dictionary = { + sndio_write_short, + sndio_sysex, + sndio_sysex, + sndio_write_byte, + sndio_write_short, + sndio_write_flush, + sndio_synchronize, + sndio_out_open, + sndio_abort, + sndio_out_close, + none_poll, + sndio_has_host_error, +}; + diff --git a/portmidi/pm_sndio/pmsndio.h b/portmidi/pm_sndio/pmsndio.h new file mode 100644 index 0000000..4096d9b --- /dev/null +++ b/portmidi/pm_sndio/pmsndio.h @@ -0,0 +1,5 @@ +/* pmsndio.h */ + +extern PmDeviceID pm_default_input_device_id; +extern PmDeviceID pm_default_output_device_id; + diff --git a/portmidi/pm_test/CMakeLists.txt b/portmidi/pm_test/CMakeLists.txt new file mode 100644 index 0000000..ae0fe48 --- /dev/null +++ b/portmidi/pm_test/CMakeLists.txt @@ -0,0 +1,46 @@ +# CMake file to build tests in this directory: pm_test + +# set the build directory to be in portmidi, not in portmidi/pm_test +# this is required for Xcode: +if(APPLE) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +endif(APPLE) + +# if(WIN32) +# if(NOT BUILD_SHARED_LIBS) + # /MDd is multithread debug DLL, /MTd is multithread debug + # /MD is multithread DLL, /MT is multithread. Change to static: +# include(../pm_win/static.cmake) +# endif() +# endif(WIN32) + +if(HAIKU) + add_compile_options(-fPIC) # Haiku x86_64 needs this explicitly +endif() + +macro(add_test name) + add_executable(${name} ${name}.c) + target_link_libraries(${name} PRIVATE portmidi) + set_property(TARGET ${name} PROPERTY MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$<CONFIG:Debug>:Debug>${MSVCRT_DLL}") +endmacro(add_test) + +add_test(testio) +add_test(midithread) +add_test(midithru) +add_test(sysex) +add_test(latency) +add_test(mm) +add_test(midiclock) +add_test(qtest) +add_test(fast) +add_test(fastrcv) +add_test(pmlist) +if(WIN32) +# windows does not implement Pm_CreateVirtualInput or Pm_CreateVirtualOutput +else(WIN32) +add_test(recvvirtual) +add_test(sendvirtual) +add_test(multivirtual) +add_test(virttest) +endif(WIN32) diff --git a/portmidi/pm_test/README.txt b/portmidi/pm_test/README.txt new file mode 100644 index 0000000..6c0c7ab --- /dev/null +++ b/portmidi/pm_test/README.txt @@ -0,0 +1,363 @@ +README.txt - for pm_test directory + +These are all test programs for PortMidi + +Because device numbers depend on the system, there is no automated +script to run all tests on PortMidi. + +To run the full set of tests manually: + +Note: everything is run from the ../Debug or ../Release directory. +Actual or example input is marked with >>, e.g., >>0 means type 0<ENTER> +Comments are shown in square brackets [like this] + +1. ./qtest -- output should show a bunch of tests and no error message. + +2. ./testio [test input] +Latency in ms: >>0 +enter your choice... >>1 +Type input number: >>6 [pick a working input device] +[play some notes, look for note-on (0x90) with pitch and velocity data] + +3. ./testio [test input (fail w/assert)] +Latency in ms: >>0 +enter your choice... >>2 +Type input number: >>6 [pick a working input device] +[play some notes, program will abort after 5 messages +(this test only applies to a Debug build, otherwise +the assert() macro is disabled.)] + +4. ./testio [test input (fail w/NULL assign)] +Latency in ms: >>0 +enter your choice... >>3 +Type input number: >>6 [pick a working input device] +[play some notes, program will Segmentation fault after 5 messages +(this test may not Segfault in the Release build; if not +try testing with a Debug build.)] + +5. ./testio [test output, no latency] +Latency in ms: >>0 +enter your choice... >>4 +Type output number: >>2 [pick a working output device] +>> [type ENTER when prompted (7 times)] +[hear note on, note off, note on, note off, chord] + +6. ./testio [test output, latency > 0] +Latency in ms: >>300 +enter your choice... >>4 +Type output number: >>2 [pick a working output device] +>> [type ENTER when prompted (7 times)] +[hear note on, note off, note on, note off, arpeggiated chord + (delay of 300ms should be apparent)] + +7. ./testio [for both, no latency] +Latency in ms: >>0 +enter your choice... >>5 +Type input number: >>6 [pick a working input device] +Type output number: >>2 [pick a working output device] +[play notes on input, hear them on output] + +8. ./testio [for both, latency > 0] +Latency in ms: >>300 +enter your choice... >>5 +Type input number: >>6 [pick a working input device] +Type output number: >>2 [pick a working output device] +[play notes on input, hear them on output (delay of 300ms is apparent)] + +9. ./testio [stream test] +Latency in ms: >>0 [does not matter] +enter your choice... >>6 +Type output number: >>2 [pick a working output device] +>> [type ENTER to start] +[hear 4 notes: C D E F# at one note per second, then all turn off] +ready to close and terminate... (type ENTER) :>> [type ENTER (twice)] + +10. ./testio [isochronous out] +Latency in ms: >>300 +enter your choice... >>7 +Type output number: >>2 [pick a working output device] +ready to send program 1 change... (type ENTER): >> [type ENTER] +[hear 80 notes, exactly 4 notes per second, no jitter] + +11. ./latency [no MIDI, histogram] +Choose timer period (in ms, >= 1): >>1 +? >>1 [No MIDI traffic option] +[wait about 10 seconds] +>> [type ENTER] +[output should be something like ... Maximum latency: 1 milliseconds] + +12. ./latency [MIDI input, histogram] +Choose timer period (in ms, >= 1): >>1 +? >>2 [MIDI input option] +Midi input device number: >>6 [pick a working input device] +[wait about 5 seconds, play input for 10 seconds ] +>> [type ENTER] +[output should be something like ... Maximum latency: 3 milliseconds] + +13. ./latency [MIDI output, histogram] +Choose timer period (in ms, >= 1): >>1 +? >>3 [MIDI output option] +Midi output device number: >>2 [pick a working output device] +Midi output should be sent every __ callback iterations: >>50 +[wait until you hear notes for 5 or 10 seconds] +>> [type ENTER to stop] +[output should be something like ... Maximum latency: 2 milliseconds] + +14. ./latency [MIDI input and output, histogram] +Choose timer period (in ms, >= 1): >>1 +? >>4 [MIDI input and output option] +Midi input device number: >>6 [pick a working input device] +Midi output device number: >>2 [pick a working output device] +Midi output should be sent every __ callback iterations: >>50 +[wait until you hear notes, simultaneously play notes for 5 or 10 seconds] +>> [type ENTER to stop] +[output should be something like ... Maximum latency: 1 milliseconds] + +15. ./mm [test with device input] +Type input device number: >>6 [pick a working input device] +[play some notes, see notes printed] +>>q [Type q ENTER when finished to exit] + +16. ./midithread -i 6 -o 2 [use working input/output device numbers] +>>5 [enter a transposition number] +[play some notes, hear parallel 4ths] +>>q [quit after ENTER a couple of times] + +17. ./midiclock [in one shell] + ./mm [in another shell] +[Goal is send clock messages to MIDI monitor program. This requires + either a hardware loopback (MIDI cable from OUT to IN on interface) + or a software loopback (macOS IAC bus or ALSA MIDI Through Port)] +[For midiclock application:] + Type output device number: >>0 [pick a device with loopback] + Type ENTER to start MIDI CLOCK: >> [type ENTER] +[For mm application:] + Type input device number: >>1 [pick device with loopback] + [Wait a few seconds] + >>s [to get Clock Count] + >>s [expect to get a higher Clock Count] +[For midiclock application:] + >>c [turn off clocks] +[For mm application:] + >>s [to get Clock Count] + >>s [expect to Clock Count stays the same] +[For midiclock application:] + >>t [turn on time code, see Time Code Quarter Frame messages from mm] + >>q [to quit] +[For mm application:] + >>q [to quit] + +18. ./midithru -i 6 -o 2 [use working input/output device numbers] +[Play notes on input evice; notes are sent immediately and also with a + 2 sec delay to the output device; program terminates in 60 seconds or + when you play B3 (B below Middle C)] +>> [ENTER to exit] + +19. ./recvvirtual -h [in one shell, macOS and Linux only] + ./recvvirtual -m vvv [for mac, or -c vvv -p vvvport for linux] + ./testio [in another shell] +[For testio application:] + Latency in ms: >>0 + enter your choice... >>4 [test output] + Type output number: >>9 [select the "portmidi (output)" device] + [type ENTER to each prompt, see that recvvirtual "Got message 0" + through "Got message 9"] + >> [ENTER to quit] +[For recvvirtual application:] + >> [ENTER to quit] + +20. ./sendvirtual -h [in one shell, macOS and Linux only] + ./sendvirtual -m vvv [for mac, or -c vvv -p vvvport for linux] + ./mm [in another shell] +[For mm application:] + Type input device number: >>10 [select the "portmidi" device] +[For sendvirtual application:] + Type ENTER to send messages: >> [type ENTER] + [see NoteOn and off messages received by mm for Key 60-64] + >> [ENTER to quit] +[For mm application:] + >>q [and ENTER twice to quit] + +21. ./sysex [no latency] +[This requires either a hardware loopback (MIDI cable from OUT to IN + on interface) or a software loopback (macOS IAC bus or ALSA MIDI + Through Port)] +>>l [for loopback test] +Type output device number: >>0 [pick output device to loopback] +Latency in milliseconds: >>0 +Type input device number: >>0 [pick input device for loopback] +[Program will send 100,000 bytes. After awhile, program will quit. + You can read the Cummulative bytes/sec value.] + +22. ./sysex [latency > 0] +[This requires either a hardware loopback (MIDI cable from OUT to IN + on interface) or a software loopback (macOS IAC bus or ALSA MIDI + Through Port)] +>>l [for loopback test] +Type output device number: >>0 [pick output device to loopback] +Latency in milliseconds: >>100 +Type input device number: >>0 [pick input device for loopback] +[Program will send 100,000 bytes. After awhile, program will quit. You + can read the Cummulative bytes/sec value; it is affected by latency.] + +23. ./fast [no latency] + ./fastrcv [in another shell] +[This is a speed check, especially for macOSX IAC bus connections, + which are known to drop messages if you send messages too fast. + fast and fastrcv must use a loopback to function.] +[In fastrcv:] + Input device number: >>1 [pick a non-hardware device if possible] +[In fast:] + Latency in ms: >>0 + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>0 [pick a non-hardware device if possible] + sending output... +[see message counts and times; on Linux you should get about 10000 + messages/s; on macOS you should get about 1800 messages/s; Windows + does not have software ports, so data rate might be limited by the + loopback device you use.] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +24. ./fast [latency > 0] + ./fastrcv [in another shell] +[This is a speed check, especially for macOSX IAC bus connections, + which are known to drop messages if you send messages too fast. + fast and fastrcv must use a loopback to function.] +[In fastrcv:] + Input device number: >>1 [pick a non-hardware device if possible] +[In fast:] + Latency in ms: >>30 [Note for ALSA, use latency * msgs/ms < 400] + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>0 [pick a non-hardware device if possible] + sending output... +[see message counts and times; on Linux you should get about 10000 + messages/s; on macOS you should get about 1800 messages/s; Windows + does not have software ports, so data rate might be limited by the + loopback device you use.] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +25. ./fast [virtual output port, latency = 0, macOS and Linux only] + ./fastrcv [in another shell] +[Start fast first:] + Latency in ms: >>0 + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>9 [enter number listed for "Create virtual + port named 'fast' (output)"] + Pausing so you can connect a receiver to the newly created + "fast" port. Type ENTER to proceed: +[In fastrcv:] + Input device number: >>3 [pick the device named "fast (input)"] +[In fast:] + >> [type ENTER to start] +[see message counts and times as above ] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +26. ./fast [virtual output port, latency > 0, macOS and Linux only] + ./fastrcv [in another shell] +[Start fast first:] + Latency in ms: >>30 [Note for ALSA, use latency * msgs/ms < 400] + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>9 [enter number listed for "Create virtual + port named 'fast' (output)"] + Pausing so you can connect a receiver to the newly created + "fast" port. Type ENTER to proceed: +[In fastrcv:] + Input device number: >>3 [pick the device named "fast (input)"] +[In fast:] + >> [type ENTER to start] +[see message counts and times as above ] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +27. ./fast [latency = 0, macOS and Linux only] + ./fastrcv [virtual input port, in another shell] +[In fastrcv:] + Input device number: >>8 [enter number listed for "Create virtual + port named 'fastrcv' (input)"] +[In fast:] + Latency in ms: >>0 + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>7 [pick the device named "fastrcv (output)"] + sending output... +[see message counts and times as above ] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +28. ./fast [latency > 0, macOS and Linux only] + ./fastrcv [virtual input port, in another shell] +[In fastrcv:] + Input device number: >>8 [enter number listed for "Create virtual + port named 'fastrcv' (input)"] +[In fast:] + Latency in ms: >>30 [Note for ALSA, use latency * msgs/ms < 400] + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>7 [pick the device named "fastrcv (output)"] + sending output... +[see message counts and times as above ] + +Check output of fastrcv: there should be no errors, just msg/sec.] + +29. ./midithru -v -n [virtual input and output, macOS and Linux only] + ./fast [latency = 0] + ./fastrcv [in another shell] +[Start midithru first, it will run for 60 seconds] +[In fastrcv:] + Input device number: >>3 [pick the device named + port named "midithru (input)"] +[In fast:] + Latency in ms: >>0 + Rate in messages per second: >>10000 + Duration in seconds: >>10 + Output device number: >>8 [pick the device named "midithru (output)"] + sending output... +[see message counts and times as above, on Mac, output from fast to + midithru AND output from midithru to fastrcv are rate limited, so + as in other tests, it will take more than 10s to receive all the + messages and the receiving message rate will be about 1800 messages/second] + +30. ./multivirtual [macOS and Linux only] + ./testio + ./testio +[Start multivirtual first] +[In first testio:] + Latency in ms: >>0 + enter your choice... >>5 [test both] + Type input number: >>1 [pick portmidi1 (input) + Type output number: >>4 [pick portmidi1 (output) +[In second testio:] + Latency in ms: >>10 + enter your choice... >>5 [test both] + Type input number: >>2 [pick portmidi2 (input) + Type output number: >>5 [pick portmidi2 (output) +[In multivirtual:] + Type ENTER to send messages: >> [type ENTER to start] +[see that each testio gets 11 messages (0 to 10) at reasonable times + (e.g. 2077 to 7580, and the "@" times (real times) should match the + timestamps). multivirtual should also report reasonable times and + line near the end of output should be "Got 11 messages from + portmidi1 and 11 from portmidi2; expected 11."] + +31. ./multivirtual [macOS and Linux only] + ./multivirtual +[Second instance should report "PortMidi call failed... + PortMidi: Cannot create virtual device: name is taken"] + +32. pmlist + ./pmlist [check the output] + [plug in or remove a device] + >> [type RETURN] + [check for changes in device list] + >>q + + + + diff --git a/portmidi/pm_test/fast.c b/portmidi/pm_test/fast.c new file mode 100644 index 0000000..102697e --- /dev/null +++ b/portmidi/pm_test/fast.c @@ -0,0 +1,290 @@ +/* fast.c -- send many MIDI messages very fast. + * + * This is a stress test created to explore reports of + * pm_write() call blocking (forever) on Linux when + * sending very dense MIDI sequences. + * + * Modified 8 Aug 2017 with -n to send expired timestamps + * to test a theory about why Linux ALSA hangs in Audacity. + * + * Modified 9 Aug 2017 with -m, -p to test when timestamps are + * wrapping from negative to positive or positive to negative. + * + * Roger B. Dannenberg, Aug 2017 + */ + +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define DEVICE_INFO NULL +#define DRIVER_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define STRING_MAX 80 /* used for console input */ +// need to get declaration for Sleep() +#ifdef WIN32 +#include "windows.h" +#else +#include <unistd.h> +#define Sleep(n) usleep(n * 1000) +#endif + + +int32_t latency = 0; +int32_t msgrate = 0; +int deviceno = -9999; +int duration = 0; +int expired_timestamps = FALSE; +int use_timeoffset = 0; + +/* read a number from console */ +/**/ +int get_number(const char *prompt) +{ + int n = 0, i; + fputs(prompt, stdout); + while (n != 1) { + n = scanf("%d", &i); + while (getchar() != '\n') ; + } + return i; +} + + +/* get_time -- the time reference. Normally, this will be the default + * time, Pt_Time(), but if you use the -p or -m option, the time + * reference will start at an offset of -10s for -m, or + * maximum_time - 10s for -p, so that we can observe what happens + * with negative time or when time changes sign or wraps (by + * generating output for more than 10s). + */ +PmTimestamp get_time(void *info) +{ + PmTimestamp now = (PmTimestamp) (Pt_Time() + use_timeoffset); + return now; +} + + +void fast_test() +{ + PmStream *midi; + char line[STRING_MAX]; + int pause = FALSE; /* pause if this is a virtual output port */ + PmError err = pmNoError; + /* output buffer size should be a little more than + msgrate * latency / 1000. PortMidi will guarantee + a minimum of latency / 2 */ + int buffer_size = msgrate * latency / 900; + PmTimestamp start, now; + int msgcnt = 0; + int polling_count = 0; + int pitch = 60; + int printtime = 1000; + + /* It is recommended to start timer before PortMidi */ + TIME_START; + + /* open output device */ + if (deviceno == Pm_CountDevices()) { + deviceno = Pm_CreateVirtualOutput("fast", NULL, DEVICE_INFO); + if (deviceno >= 0) { + err = Pm_OpenOutput(&midi, deviceno, DRIVER_INFO, buffer_size, + get_time, NULL, latency); + pause = TRUE; + } + } else if (err >= pmNoError) { + err = Pm_OpenOutput(&midi, deviceno, DRIVER_INFO, buffer_size, + get_time, NULL, latency); + } + if (err == pmHostError) { + Pm_GetHostErrorText(line, STRING_MAX); + printf("PortMidi found host error...\n %s\n", line); + goto done; + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + goto done; + } + printf("Midi Output opened with %ld ms latency.\n", (long) latency); + if (pause) { + printf("Pausing so you can connect a receiver to the newly created\n" + " \"fast\" port. Type ENTER to proceed: "); + while (getchar() != '\n') ; + } + /* wait a sec after printing previous line */ + start = get_time(NULL) + 1000; + while (start > get_time(NULL)) { + Sleep(10); + } + printf("sending output...\n"); + fflush(stdout); /* make sure message goes to console */ + + /* every 10ms send on/off pairs at timestamps set to current time */ + now = get_time(NULL); + /* if expired_timestamps, we want to send timestamps that have + * expired. They should be sent immediately, but there's a suggestion + * that negative delay might cause problems in the ALSA implementation + * so this is something we can test using the -n flag. + */ + if (expired_timestamps) { + now = now - 2 * latency; + } + + while (((PmTimestamp) (now - start)) < duration * 1000 || pitch != 60) { + /* how many messages do we send? Total should be + * (elapsed * rate) / 1000 + */ + int send_total = (((PmTimestamp) ((now - start))) * msgrate) / 1000; + /* always send until pitch would be 60 so if we run again, the + next pitch (60) will be expected */ + if (msgcnt < send_total) { + if ((msgcnt & 1) == 0) { + Pm_WriteShort(midi, now, Pm_Message(0x90, pitch, 100)); + } else { + Pm_WriteShort(midi, now, Pm_Message(0x90, pitch, 0)); + /* play 60, 61, 62, ... 71, then wrap back to 60, 61, ... */ + pitch = (pitch - 59) % 12 + 60; + } + msgcnt += 1; + if (((PmTimestamp) (now - start)) >= printtime) { + printf("%d at %dms, polling count %d\n", msgcnt, now - start, + polling_count); + fflush(stdout); /* make sure message goes to console */ + printtime += 1000; /* next msg in 1s */ + } + } + now = get_time(NULL); + polling_count++; + } + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close and terminate... (type RETURN):"); + while (getchar() != '\n') ; + + Pm_Close(midi); + done: + Pm_Terminate(); + printf("done closing and terminating...\n"); +} + + +void show_usage() +{ + printf("Usage: fast [-h] [-l latency] [-r rate] [-d device] [-s dur] " + "[-n] [-p] [-m]\n" + ", where latency is in ms,\n" + " rate is messages per second,\n" + " device is the PortMidi device number,\n" + " dur is the length of the test in seconds,\n" + " -n means send timestamps in the past,\n" + " -p means use a large positive time offset,\n" + " -m means use a large negative time offset, and\n" + " -h means help.\n"); +} + +int main(int argc, char *argv[]) +{ + int default_in; + int default_out; + char *deflt; + int i = 0; + int latency_valid = FALSE; + int rate_valid = FALSE; + int device_valid = FALSE; + int dur_valid = FALSE; + + if (sizeof(void *) == 8) + printf("Apparently this is a 64-bit machine.\n"); + else if (sizeof(void *) == 4) + printf ("Apparently this is a 32-bit machine.\n"); + + if (argc <= 1) { + show_usage(); + } else { + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + } else if (strcmp(argv[i], "-l") == 0 && (i + 1 < argc)) { + i = i + 1; + latency = atoi(argv[i]); + printf("Latency will be %ld\n", (long) latency); + latency_valid = TRUE; + } else if (strcmp(argv[i], "-r") == 0) { + i = i + 1; + msgrate = atoi(argv[i]); + printf("Rate will be %d messages/second\n", msgrate); + rate_valid = TRUE; + } else if (strcmp(argv[i], "-d") == 0) { + i = i + 1; + deviceno = atoi(argv[i]); + printf("Device will be %d\n", deviceno); + } else if (strcmp(argv[i], "-s") == 0) { + i = i + 1; + duration = atoi(argv[i]); + printf("Duration will be %d seconds\n", duration); + dur_valid = TRUE; + } else if (strcmp(argv[i], "-n") == 0) { + printf("Sending expired timestamps (-n)\n"); + expired_timestamps = TRUE; + } else if (strcmp(argv[i], "-p") == 0) { + printf("Time offset set to 2147473648 (-p)\n"); + use_timeoffset = 2147473648; + } else if (strcmp(argv[i], "-m") == 0) { + printf("Time offset set to -10000 (-m)\n"); + use_timeoffset = -10000; + } else { + show_usage(); + } + } + } + + if (!latency_valid) { + // coerce to known size + latency = (int32_t) get_number("Latency in ms: "); + } + + if (!rate_valid) { + // coerce from "%d" to known size + msgrate = (int32_t) get_number("Rate in messages per second: "); + } + + if (!dur_valid) { + duration = get_number("Duration in seconds: "); + } + + /* list device information */ + default_in = Pm_GetDefaultInputDeviceID(); + default_out = Pm_GetDefaultOutputDeviceID(); + for (i = 0; i < Pm_CountDevices(); i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (info->output) { + printf("%d: %s, %s", i, info->interf, info->name); + if (i == deviceno) { + device_valid = TRUE; + deflt = "selected "; + } else if (i == default_out) { + deflt = "default "; + } else { + deflt = ""; + } + printf(" (%soutput)\n", deflt); + } + } + printf("%d: Create virtual port named \"fast\"", i); + if (i == deviceno) { + device_valid = TRUE; + deflt = "selected "; + } else { + deflt = ""; + } + printf(" (%soutput)\n", deflt); + + if (!device_valid) { + deviceno = get_number("Output device number: "); + } + + fast_test(); + return 0; +} diff --git a/portmidi/pm_test/fastrcv.c b/portmidi/pm_test/fastrcv.c new file mode 100644 index 0000000..dabf9fa --- /dev/null +++ b/portmidi/pm_test/fastrcv.c @@ -0,0 +1,255 @@ +/* fastrcv.c -- send many MIDI messages very fast. + * + * This is a stress test created to explore reports of + * pm_write() call blocking (forever) on Linux when + * sending very dense MIDI sequences. + * + * Modified 8 Aug 2017 with -n to send expired timestamps + * to test a theory about why Linux ALSA hangs in Audacity. + * + * Roger B. Dannenberg, Aug 2017 + */ + +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define INPUT_BUFFER_SIZE 1000 /* big to avoid losing any input */ +#define DEVICE_INFO NULL +#define DRIVER_INFO NULL +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define STRING_MAX 80 /* used for console input */ +// need to get declaration for Sleep() +#ifdef WIN32 +#include "windows.h" +#else +#include <unistd.h> +#define Sleep(n) usleep(n * 1000) +#endif + + +int deviceno = -9999; +int verbose = FALSE; + + +static void prompt_and_exit(void) +{ + printf("type ENTER..."); + while (getchar() != '\n') ; + /* this will clean up open ports: */ + exit(-1); +} + + +static PmError checkerror(PmError err) +{ + if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + char errmsg[80]; + Pm_GetHostErrorText(errmsg, 80); + printf("PortMidi found host error...\n %s\n", errmsg); + prompt_and_exit(); + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + prompt_and_exit(); + } + return err; +} + + +/* read a number from console */ +/**/ +int get_number(const char *prompt) +{ + int n = 0, i; + fputs(prompt, stdout); + while (n != 1) { + n = scanf("%d", &i); + while (getchar() != '\n') ; + } + return i; +} + + +void fastrcv_test() +{ + PmStream * midi; + PmError status, length; + PmEvent buffer[1]; + PmTimestamp start; + /* every 10ms read all messages, keep counts */ + /* every 1000ms, print report */ + int msgcnt = 0; + /* expect repeating sequence of 60 through 71, alternating on/off */ + int expected_pitch = 60; + int expected_on = TRUE; + int report_time; + PmTimestamp last_timestamp = -1; + PmTimestamp last_delta = -1; + + /* It is recommended to start timer before PortMidi */ + TIME_START; + + /* open output device */ + if (deviceno == Pm_CountDevices()) { + int id = Pm_CreateVirtualInput("fastrcv", NULL, DEVICE_INFO); + if (id < 0) checkerror(id); /* error reporting */ + checkerror(Pm_OpenInput(&midi, id, DRIVER_INFO, + INPUT_BUFFER_SIZE, TIME_PROC, TIME_INFO)); + } else { + Pm_OpenInput(&midi, deviceno, DRIVER_INFO, INPUT_BUFFER_SIZE, + TIME_PROC, TIME_INFO); + } + printf("Midi Input opened.\n"); + + /* wait a sec after printing previous line */ + start = Pt_Time() + 1000; + while (start > Pt_Time()) { + Sleep(10); + } + + report_time = Pt_Time() + 1000; /* report every 1s */ + while (TRUE) { + PmTimestamp now = Pt_Time(); + status = Pm_Poll(midi); + if (status == TRUE) { + length = Pm_Read(midi, buffer, 1); + if (length > 0) { + int status = Pm_MessageStatus(buffer[0].message); + if (status == 0x80) { /* convert NoteOff to NoteOn, vel=0 */ + status = 0x90; + buffer[0].message = Pm_Message(status, + Pm_MessageData1(buffer[0].message), 0); + } + /* only listen to NOTEON messages */ + if (status == 0x90) { + int pitch = Pm_MessageData1(buffer[0].message); + int vel = Pm_MessageData2(buffer[0].message); + int is_on = (vel > 0); + if (verbose) { + printf("Note pitch %d vel %d\n", pitch, vel); + } + msgcnt++; + if (pitch != expected_pitch || expected_on != is_on) { + printf("Unexpected note-on: pitch %d vel %d, " + "expected: pitch %d Note%s\n", pitch, vel, + expected_pitch, (expected_on ? "On" : "Off")); + } + if (is_on) { + expected_on = FALSE; + expected_pitch = pitch; + } else { + expected_on = TRUE; + expected_pitch = (pitch + 1) % 72; + if (expected_pitch < 60) expected_pitch = 60; + } + if (last_timestamp >= 0) { + last_delta = buffer[0].timestamp - last_timestamp; + } + last_timestamp = buffer[0].timestamp; + } + } + } + if (now >= report_time) { + printf("%d msgs/sec", msgcnt); + /* if available, print the last timestamp and last delta time */ + if (last_timestamp >= 0) { + printf(" last timestamp %d", (int) last_timestamp); + last_timestamp = -1; + } + if (last_delta >= 0) { + printf(" last delta time %d", (int) last_delta); + last_delta = -1; + } + printf("\n"); + report_time += 1000; + msgcnt = 0; + } + } +} + + +void show_usage() +{ + printf("Usage: fastrcv [-h] [-v] [-d device], where\n" + "device is the PortMidi device number,\n" + "-h means help,\n" + "-v means verbose (print messages)\n"); +} + +int main(int argc, char *argv[]) +{ + int default_in; + int default_out; + char *deflt; + + int i = 0; + int test_input = 0, test_output = 0, test_both = 0; + int stream_test = 0; + int device_valid = FALSE; + + if (sizeof(void *) == 8) + printf("Apparently this is a 64-bit machine.\n"); + else if (sizeof(void *) == 4) + printf ("Apparently this is a 32-bit machine.\n"); + + if (argc <= 1) { + show_usage(); + } else { + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + } else if (strcmp(argv[i], "-v") == 0) { + verbose = TRUE; + } else if (strcmp(argv[i], "-d") == 0) { + i = i + 1; + deviceno = atoi(argv[i]); + printf("Device will be %d\n", deviceno); + } else { + show_usage(); + } + } + } + + /* list device information */ + default_in = Pm_GetDefaultInputDeviceID(); + default_out = Pm_GetDefaultOutputDeviceID(); + for (i = 0; i < Pm_CountDevices(); i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (!info->output) { + printf("%d: %s, %s", i, info->interf, info->name); + if (i == deviceno) { + device_valid = TRUE; + deflt = "selected "; + } else if (i == default_out) { + deflt = "default "; + } else { + deflt = ""; + } + printf(" (%sinput)\n", deflt); + } + } + printf("%d: Create virtual port named \"fastrcv\"", i); + if (i == deviceno) { + device_valid = TRUE; + deflt = "selected "; + } else { + deflt = ""; + } + printf(" (%sinput)\n", deflt); + + if (!device_valid) { + deviceno = get_number("Input device number: "); + } + + fastrcv_test(); + return 0; +} diff --git a/portmidi/pm_test/latency.c b/portmidi/pm_test/latency.c new file mode 100755 index 0000000..06ea80d --- /dev/null +++ b/portmidi/pm_test/latency.c @@ -0,0 +1,287 @@ +/* latency.c -- measure latency of OS */
+
+#include "porttime.h"
+#include "portmidi.h"
+#include "stdlib.h"
+#include "stdio.h"
+#include "string.h"
+#include "assert.h"
+
+/* Latency is defined here to mean the time starting when a
+ process becomes ready to run, and ending when the process
+ actually runs. Latency is due to contention for the
+ processor, usually due to other processes, OS activity
+ including device drivers handling interrupts, and
+ waiting for the scheduler to suspend the currently running
+ process and activate the one that is waiting.
+
+ Latency can affect PortMidi applications: if a process fails
+ to wake up promptly, MIDI input may sit in the input buffer
+ waiting to be handled, and MIDI output may not be generated
+ with accurate timing. Using the latency parameter when
+ opening a MIDI output port allows the caller to defer timing
+ to PortMidi, which in most implementations will pass the
+ data on to the OS. By passing timestamps and data to the
+ OS kernel, device driver, or even hardware, there are fewer
+ sources of latency that can affect the ultimate timing of
+ the data. On the other hand, the application must generate
+ and deliver the data ahead of the timestamp. The amount by
+ which data is computed early must be at least as large as
+ the worst-case latency to avoid timing problems.
+
+ Latency is even more important in audio applications. If an
+ application lets an audio output buffer underflow, an audible
+ pop or click is produced. Audio input buffers can overflow,
+ causing data to be lost. In general the audio buffers must
+ be large enough to buffer the worst-case latency that the
+ application will encounter.
+
+ This program measures latency by recording the difference
+ between the scheduled callback time and the current real time.
+ We do not really know the scheduled callback time, so we will
+ record the differences between the real time of each callback
+ and the real time of the previous callback. Differences that
+ are larger than the scheduled difference are recorded. Smaller
+ differences indicate the system is recovering from an earlier
+ latency, so these are ignored.
+ Since printing by the callback process can cause all sorts of
+ delays, this program records latency observations in a
+ histogram. When the program is stopped, the histogram is
+ printed to the console.
+
+ Optionally the system can be tested under a load of MIDI input,
+ MIDI output, or both. If MIDI input is selected, the callback
+ thread will read any waiting MIDI events each iteration. You
+ must generate events on this interface for the test to actually
+ put any appreciable load on PortMidi. If MIDI output is
+ selected, alternating note on and note off events are sent each
+ X iterations, where you specify X. For example, with a timer
+ callback period of 2ms and X=1, a MIDI event is sent every 2ms.
+
+
+ INTERPRETING RESULTS: Time is quantized to 1ms, so there is
+ some uncertainty due to rounding. A microsecond latency that
+ spans the time when the clock is incremented will be reported
+ as a latency of 1. On the other hand, a latency of almost
+ 1ms that falls between two clock ticks will be reported as
+ zero. In general, if the highest nonzero bin is numbered N,
+ then the maximum latency is N+1.
+
+CHANGE LOG
+
+18-Jul-03 Mark Nelson -- Added code to generate MIDI or receive
+ MIDI during test, and made period user-settable.
+ */
+
+#define HIST_LEN 21 /* how many 1ms bins in the histogram */
+
+#define STRING_MAX 80 /* used for console input */
+
+#define INPUT_BUFFER_SIZE 100
+#define OUTPUT_BUFFER_SIZE 0
+
+#ifndef max
+#define max(a, b) ((a) > (b) ? (a) : (b))
+#endif
+#ifndef min
+#define min(a, b) ((a) <= (b) ? (a) : (b))
+#endif
+
+int get_number(const char *prompt);
+
+PtTimestamp previous_callback_time = 0;
+
+int period; /* milliseconds per callback */
+
+int histogram[HIST_LEN];
+int max_latency = 0; /* worst latency observed */
+int out_of_range = 0; /* how many points outside of HIST_LEN? */
+
+int test_in, test_out; /* test MIDI in and/or out? */
+int output_period; /* output MIDI every __ iterations if test_out true */
+int iteration = 0;
+PmStream *in, *out;
+int note_on = 0; /* is the note currently on? */
+
+/* callback function for PortTime -- computes histogram */
+void pt_callback(PtTimestamp timestamp, void *userData)
+{
+ PtTimestamp difference = timestamp - previous_callback_time - period;
+ previous_callback_time = timestamp;
+
+ /* allow 5 seconds for the system to settle down */
+ if (timestamp < 5000) return;
+
+ iteration++;
+ /* send a note on/off if user requested it */
+ if (test_out && (iteration % output_period == 0)) {
+ PmEvent buffer[1];
+ buffer[0].timestamp = Pt_Time();
+ if (note_on) {
+ /* note off */
+ buffer[0].message = Pm_Message(0x90, 60, 0);
+ note_on = 0;
+ } else {
+ /* note on */
+ buffer[0].message = Pm_Message(0x90, 60, 100);
+ note_on = 1;
+ }
+ Pm_Write(out, buffer, 1);
+ iteration = 0;
+ }
+
+ /* read all waiting events (if user requested) */
+ if (test_in) {
+ PmError status;
+ PmEvent buffer[1];
+ do {
+ status = Pm_Poll(in);
+ if (status == TRUE) {
+ Pm_Read(in,buffer,1);
+ }
+ } while (status == TRUE);
+ }
+
+ if (difference < 0) return; /* ignore when system is "catching up" */
+
+ /* update the histogram */
+ if (difference < HIST_LEN) {
+ histogram[difference]++;
+ } else {
+ out_of_range++;
+ }
+
+ if (max_latency < difference) max_latency = difference;
+}
+
+
+int main()
+{
+ int i;
+ int len;
+ int choice;
+ PtTimestamp stop;
+ printf("Latency histogram.\n");
+ period = 0;
+ while (period < 1) {
+ period = get_number("Choose timer period (in ms, >= 1): ");
+ }
+ printf("Benchmark with:\n\t%s\n\t%s\n\t%s\n\t%s\n",
+ "1. No MIDI traffic",
+ "2. MIDI input",
+ "3. MIDI output",
+ "4. MIDI input and output");
+ choice = get_number("? ");
+ switch (choice) {
+ case 1: test_in = 0; test_out = 0; break;
+ case 2: test_in = 1; test_out = 0; break;
+ case 3: test_in = 0; test_out = 1; break;
+ case 4: test_in = 1; test_out = 1; break;
+ default: assert(0);
+ }
+ if (test_in || test_out) {
+ /* list device information */
+ for (i = 0; i < Pm_CountDevices(); i++) {
+ const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
+ if ((test_in && info->input) ||
+ (test_out && info->output)) {
+ printf("%d: %s, %s", i, info->interf, info->name);
+ if (info->input) printf(" (input)");
+ if (info->output) printf(" (output)");
+ printf("\n");
+ }
+ }
+ /* open stream(s) */
+ if (test_in) {
+ int i = get_number("MIDI input device number: ");
+ Pm_OpenInput(&in,
+ i,
+ NULL,
+ INPUT_BUFFER_SIZE,
+ (PmTimestamp (*)(void *)) Pt_Time,
+ NULL);
+ /* turn on filtering; otherwise, input might overflow in the
+ 5-second period before timer callback starts reading midi */
+ Pm_SetFilter(in, PM_FILT_ACTIVE | PM_FILT_CLOCK);
+ }
+ if (test_out) {
+ int i = get_number("MIDI output device number: ");
+ PmEvent buffer[1];
+ Pm_OpenOutput(&out,
+ i,
+ NULL,
+ OUTPUT_BUFFER_SIZE,
+ (PmTimestamp (*)(void *)) Pt_Time,
+ NULL,
+ 0); /* no latency scheduling */
+
+ /* send a program change to force a status byte -- this fixes
+ a problem with a buggy linux MidiSport driver, and shouldn't
+ hurt anything else
+ */
+ buffer[0].timestamp = 0;
+ buffer[0].message = Pm_Message(0xC0, 0, 0); /* program change */
+ Pm_Write(out, buffer, 1);
+
+ output_period = get_number(
+ "MIDI out should be sent every __ callback iterations: ");
+
+ assert(output_period >= 1);
+ }
+ }
+
+ printf("Latency measurements will start in 5 seconds. "
+ "Type return to stop: ");
+ Pt_Start(period, &pt_callback, 0);
+ while (getchar() != '\n') ;
+ stop = Pt_Time();
+ Pt_Stop();
+
+ /* courteously turn off the last note, if necessary */
+ if (note_on) {
+ PmEvent buffer[1];
+ buffer[0].timestamp = Pt_Time();
+ buffer[0].message = Pm_Message(0x90, 60, 0);
+ Pm_Write(out, buffer, 1);
+ }
+
+ /* print the histogram */
+ printf("Duration of test: %g seconds\n\n", max(0, stop - 5000) * 0.001);
+ printf("Latency(ms) Number of occurrences\n");
+ /* avoid printing beyond last non-zero histogram entry */
+ len = min(HIST_LEN, max_latency + 1);
+ for (i = 0; i < len; i++) {
+ printf("%2d %10d\n", i, histogram[i]);
+ }
+ printf("Number of points greater than %dms: %d\n",
+ HIST_LEN - 1, out_of_range);
+ printf("Maximum latency: %d milliseconds\n", max_latency);
+ printf("\nNote that due to rounding, actual latency can be 1ms higher\n");
+ printf("than the numbers reported here.\n");
+ printf("Type return to exit...");
+ while (getchar() != '\n') ;
+
+ if(choice == 2)
+ Pm_Close(in);
+ else if(choice == 3)
+ Pm_Close(out);
+ else if(choice == 4)
+ {
+ Pm_Close(in);
+ Pm_Close(out);
+ }
+ return 0;
+}
+
+
+/* read a number from console */
+int get_number(const char *prompt)
+{
+ int n = 0, i;
+ fputs(prompt, stdout);
+ while (n != 1) {
+ n = scanf("%d", &i);
+ while (getchar() != '\n') ;
+ }
+ return i;
+}
diff --git a/portmidi/pm_test/midiclock.c b/portmidi/pm_test/midiclock.c new file mode 100644 index 0000000..f0a6897 --- /dev/null +++ b/portmidi/pm_test/midiclock.c @@ -0,0 +1,282 @@ +/* miditime.c -- a test program that sends midi clock and MTC */ + +#include "portmidi.h" +#include "porttime.h" +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> +#include <ctype.h> + +#ifndef false +#define false 0 +#define true 1 +#endif + +#define private static +typedef int boolean; + +#define MIDI_TIME_CLOCK 0xf8 +#define MIDI_START 0xfa +#define MIDI_CONTINUE 0xfb +#define MIDI_STOP 0xfc +#define MIDI_Q_FRAME 0xf1 + +#define OUTPUT_BUFFER_SIZE 0 +#define DRIVER_INFO NULL +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define LATENCY 0 +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define STRING_MAX 80 /* used for console input */ + +/* to determine ms per clock: + * time per beat in seconds = 60 / tempo + * multiply by 1000 to get time per beat in ms: 60000 / tempo + * divide by 24 CLOCKs per beat: (60000/24) / tempo + * simplify: 2500 / tempo + */ +#define TEMPO_TO_CLOCK 2500.0 + +boolean done = false; +PmStream *midi; +/* shared flags to control callback output generation: */ +boolean clock_running = false; +boolean send_start_stop = false; +boolean time_code_running = false; +boolean active = false; /* tells callback to do its thing */ +float tempo = 60.0F; +/* protocol for handing off portmidi to callback thread: + main owns portmidi + main sets active = true: ownership transfers to callback + main sets active = false: main requests ownership + callback sees active == false, yields ownership back to main + main waits 2ms to make sure callback has a chance to yield + (stop making PortMidi calls), then assumes it can close + PortMidi + */ + +/* timer_poll -- the timer callback function */ +/* + * All MIDI sends take place here + */ +void timer_poll(PtTimestamp timestamp, void *userData) +{ + static int callback_owns_portmidi = false; + static PmTimestamp clock_start_time = 0; + static double next_clock_time = 0; + /* SMPTE time */ + static int frames = 0; + static int seconds = 0; + static int minutes = 0; + static int hours = 0; + static int mtc_count = 0; /* where are we in quarter frame sequence? */ + static int smpte_start_time = 0; + static double next_smpte_time = 0; + #define QUARTER_FRAME_PERIOD (1.0 / 120.0) /* 30fps, 1/4 frame */ + + if (callback_owns_portmidi && !active) { + /* main is requesting (by setting active to false) that we shut down */ + callback_owns_portmidi = false; + return; + } + if (!active) return; /* main still getting ready or it's closing down */ + callback_owns_portmidi = true; /* main is ready, we have portmidi */ + if (send_start_stop) { + if (clock_running) { + Pm_WriteShort(midi, 0, MIDI_STOP); + } else { + Pm_WriteShort(midi, 0, MIDI_START); + clock_start_time = timestamp; + next_clock_time = TEMPO_TO_CLOCK / tempo; + } + clock_running = !clock_running; + send_start_stop = false; /* until main sets it again */ + /* note that there's a slight race condition here: main could + set send_start_stop asynchronously, but we assume user is + typing slower than the clock rate */ + } + if (clock_running) { + if ((timestamp - clock_start_time) > next_clock_time) { + Pm_WriteShort(midi, 0, MIDI_TIME_CLOCK); + next_clock_time += TEMPO_TO_CLOCK / tempo; + } + } + if (time_code_running) { + int data = 0; // initialization avoids compiler warning + if ((timestamp - smpte_start_time) < next_smpte_time) + return; + switch (mtc_count) { + case 0: /* frames low nibble */ + data = frames; + break; + case 1: /* frames high nibble */ + data = frames >> 4; + break; + case 2: /* frames seconds low nibble */ + data = seconds; + break; + case 3: /* frames seconds high nibble */ + data = seconds >> 4; + break; + case 4: /* frames minutes low nibble */ + data = minutes; + break; + case 5: /* frames minutes high nibble */ + data = minutes >> 4; + break; + case 6: /* hours low nibble */ + data = hours; + break; + case 7: /* hours high nibble */ + data = hours >> 4; + break; + } + data &= 0xF; /* take only 4 bits */ + Pm_WriteShort(midi, 0, + Pm_Message(MIDI_Q_FRAME, (mtc_count << 4) + data, 0)); + mtc_count = (mtc_count + 1) & 7; /* wrap around */ + if (mtc_count == 0) { /* update time by two frames */ + frames += 2; + if (frames >= 30) { + frames = 0; + seconds++; + if (seconds >= 60) { + seconds = 0; + minutes++; + if (minutes >= 60) { + minutes = 0; + hours++; + /* just let hours wrap if it gets that far */ + } + } + } + } + next_smpte_time += QUARTER_FRAME_PERIOD; + } else { /* time_code_running is false */ + smpte_start_time = timestamp; + /* so that when it finally starts, we'll be in sync */ + } +} + + +/* read a number from console */ +/**/ +int get_number(const char *prompt) +{ + int n = 0, i; + fputs(prompt, stdout); + while (n != 1) { + n = scanf("%d", &i); + while (getchar() != '\n') ; + } + return i; +} + +/**************************************************************************** +* showhelp +* Effect: print help text +****************************************************************************/ + +private void showhelp() +{ + printf("\n"); + printf("t toggles sending MIDI Time Code (MTC)\n"); + printf("c toggles sending MIDI CLOCK (initially on)\n"); + printf("m to set tempo (from 1bpm to 300bpm)\n"); + printf("q quits\n"); + printf("\n"); +} + +/**************************************************************************** +* doascii +* Inputs: +* char c: input character +* Effect: interpret to control output +****************************************************************************/ + +private void doascii(char c) +{ + if (isupper(c)) c = tolower(c); + if (c == 'q') done = true; + else if (c == 'c') { + printf("%s MIDI CLOCKs\n", (clock_running ? "Stopping" : "Starting")); + send_start_stop = true; + } else if (c == 't') { + printf("%s MIDI Time Code\n", + (time_code_running ? "Stopping" : "Starting")); + time_code_running = !time_code_running; + } else if (c == 'm') { + int input_tempo = get_number("Enter new tempo (bpm): "); + if (input_tempo >= 1 && input_tempo <= 300) { + printf("Changing tempo to %d\n", input_tempo); + tempo = (float) input_tempo; + } else { + printf("Tempo range is 1 to 300, current tempo is %g bpm\n", + tempo); + } + } else { + showhelp(); + } +} + + +/* main - prompt for parameters, start processing */ +/* + * Prompt user to type return. + * Then send START and MIDI CLOCK for 60 beats/min. + * Commands: + * t - toggle sending MIDI Time Code (MTC) + * c - toggle sending MIDI CLOCK + * m - set tempo + * q - quit + */ +int main(int argc, char **argv) +{ + int outp; + PmError err; + int i; + if (argc > 1) { + printf("Warning: command line arguments ignored\n"); + } + showhelp(); + /* use porttime callback to send midi */ + Pt_Start(1, timer_poll, 0); + /* list device information */ + printf("MIDI output devices:\n"); + for (i = 0; i < Pm_CountDevices(); i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (info->output) printf("%d: %s, %s\n", i, info->interf, info->name); + } + outp = get_number("Type output device number: "); + err = Pm_OpenOutput(&midi, outp, DRIVER_INFO, OUTPUT_BUFFER_SIZE, + TIME_PROC, TIME_INFO, LATENCY); + if (err) { + puts(Pm_GetErrorText(err)); + goto error_exit_no_device; + } + active = true; + + printf("Type ENTER to start MIDI CLOCK:\n"); + while (getchar() != '\n') ; + send_start_stop = true; /* send START and then CLOCKs */ + + while (!done) { + doascii(getchar()); + while (getchar() != '\n') ; + } + + active = false; + Pt_Sleep(2); /* this is to allow callback to complete -- it's + real time, so it's either ok and it runs on + time, or there's no point to synchronizing + with it */ + /* now we "own" portmidi again */ + Pm_Close(midi); + error_exit_no_device: + Pt_Stop(); + Pm_Terminate(); + exit(0); +} + diff --git a/portmidi/pm_test/midithread.c b/portmidi/pm_test/midithread.c new file mode 100755 index 0000000..ea0613b --- /dev/null +++ b/portmidi/pm_test/midithread.c @@ -0,0 +1,343 @@ +/* midithread.c -- example program showing how to do midi processing + in a preemptive thread + + Notes: if you handle midi I/O from your main program, there will be + some delay before handling midi messages whenever the program is + doing something like file I/O, graphical interface updates, etc. + + To handle midi with minimal delay, you should do all midi processing + in a separate, high priority thread. A convenient way to get a high + priority thread in windows is to use the timer callback provided by + the PortTime library. That is what we show here. + + If the high priority thread writes to a file, prints to the console, + or does just about anything other than midi processing, this may + create delays, so all this processing should be off-loaded to the + "main" process or thread. Communication between threads can be tricky. + If one thread is writing at the same time the other is reading, very + tricky race conditions can arise, causing programs to behave + incorrectly, but only under certain timing conditions -- a terrible + thing to debug. Advanced programmers know this as a synchronization + problem. See any operating systems textbook for the complete story. + + To avoid synchronization problems, a simple, reliable approach is + to communicate via messages. PortMidi offers a message queue as a + datatype, and operations to insert and remove messages. Use two + queues as follows: midi_to_main transfers messages from the midi + thread to the main thread, and main_to_midi transfers messages from + the main thread to the midi thread. Queues are safe for use between + threads as long as ONE thread writes and ONE thread reads. You must + NEVER allow two threads to write to the same queue. + + This program transposes incoming midi data by an amount controlled + by the main program. To change the transposition, type an integer + followed by return. The main program sends this via a message queue + to the midi thread. To quit, type 'q' followed by return. + + The midi thread can also send a pitch to the main program on request. + Type 'm' followed by return to wait for the next midi message and + print the pitch. + + This program illustrates: + Midi processing in a high-priority thread. + Communication with a main process via message queues. + + */ + +#include "stdio.h" +#include "stdlib.h" +#include "string.h" +#include "assert.h" +#include "portmidi.h" +#include "pmutil.h" +#include "porttime.h" + +/* if INPUT_BUFFER_SIZE is 0, PortMidi uses a default value */ +#define INPUT_BUFFER_SIZE 0 + +#define OUTPUT_BUFFER_SIZE 100 +#define DRIVER_INFO NULL +#define TIME_PROC NULL +#define TIME_INFO NULL +/* use zero latency because we want output to be immediate */ +#define LATENCY 0 + +#define STRING_MAX 80 + +/**********************************/ +/* DATA USED ONLY BY process_midi */ +/* (except during initialization) */ +/**********************************/ + +int active = FALSE; +int monitor = FALSE; +int midi_thru = TRUE; + +int transpose; +PmStream *midi_in; +PmStream *midi_out; + +/****************************/ +/* END OF process_midi DATA */ +/****************************/ + +/* shared queues */ +PmQueue *midi_to_main; +PmQueue *main_to_midi; + +#define QUIT_MSG 1000 +#define MONITOR_MSG 1001 +#define THRU_MSG 1002 + +/* timer interrupt for processing midi data */ +void process_midi(PtTimestamp timestamp, void *userData) +{ + PmError result; + PmEvent buffer; /* just one message at a time */ + int32_t msg; + + /* do nothing until initialization completes */ + if (!active) + return; + + /* check for messages */ + do { + result = Pm_Dequeue(main_to_midi, &msg); + if (result) { + if (msg >= -127 && msg <= 127) + transpose = msg; + else if (msg == QUIT_MSG) { + /* acknowledge receipt of quit message */ + Pm_Enqueue(midi_to_main, &msg); + active = FALSE; + return; + } else if (msg == MONITOR_MSG) { + /* main has requested a pitch. monitor is a flag that + * records the request: + */ + monitor = TRUE; + } else if (msg == THRU_MSG) { + /* toggle Thru on or off */ + midi_thru = !midi_thru; + } + } + } while (result); + + /* see if there is any midi input to process */ + do { + result = Pm_Poll(midi_in); + if (result) { + int status, data1, data2; + if (Pm_Read(midi_in, &buffer, 1) == pmBufferOverflow) + continue; + if (midi_thru) + Pm_Write(midi_out, &buffer, 1); + /* unless there was overflow, we should have a message now */ + status = Pm_MessageStatus(buffer.message); + data1 = Pm_MessageData1(buffer.message); + data2 = Pm_MessageData2(buffer.message); + if ((status & 0xF0) == 0x90 || + (status & 0xF0) == 0x80) { + + /* this is a note-on or note-off, so transpose and send */ + data1 += transpose; + + /* keep within midi pitch range, keep proper pitch class */ + while (data1 > 127) + data1 -= 12; + while (data1 < 0) + data1 += 12; + + /* send the message */ + buffer.message = Pm_Message(status, data1, data2); + Pm_Write(midi_out, &buffer, 1); + + /* if monitor is set, send the pitch to the main thread */ + if (monitor) { + Pm_Enqueue(midi_to_main, &data1); + monitor = FALSE; /* only send one pitch per request */ + } + } + } + } while (result); +} + +void exit_with_message(char *msg) +{ + printf("%s\n", msg); + while (getchar() != '\n') ; + exit(1); +} + +int main(int argc, char *argv[]) +{ + int32_t n; + const PmDeviceInfo *info; + char line[STRING_MAX]; + int spin; + int done = FALSE; + int i; + int input = -1, output = -1; + + printf("Usage: midithread [-i input] [-o output]\n" + "where input and output are portmidi device numbers\n"); + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-i") == 0) { + i++; + input = atoi(argv[i]); + printf("Input device number: %d\n", input); + } else if (strcmp(argv[i], "-o") == 0) { + i++; + output = atoi(argv[i]); + printf("Output device number: %d\n", output); + } else { + return -1; + } + } + printf("begin PortMidi multithread test...\n"); + + /* note that it is safe to call PortMidi from the main thread for + initialization and opening devices. You should not make any + calls to PortMidi from this thread once the midi thread begins. + to make PortMidi calls. + */ + + /* make the message queues */ + /* messages can be of any size and any type, but all messages in + * a given queue must have the same size. We'll just use int32_t's + * for our messages in this simple example + */ + midi_to_main = Pm_QueueCreate(32, sizeof(int32_t)); + assert(midi_to_main != NULL); + main_to_midi = Pm_QueueCreate(32, sizeof(int32_t)); + assert(main_to_midi != NULL); + + /* a little test of enqueue and dequeue operations. Ordinarily, + * you would call Pm_Enqueue from one thread and Pm_Dequeue from + * the other. Since the midi thread is not running, this is safe. + */ + n = 1234567890; + Pm_Enqueue(midi_to_main, &n); + n = 987654321; + Pm_Enqueue(midi_to_main, &n); + Pm_Dequeue(midi_to_main, &n); + if (n != 1234567890) { + exit_with_message("Pm_Dequeue produced unexpected result."); + } + Pm_Dequeue(midi_to_main, &n); + if(n != 987654321) { + exit_with_message("Pm_Dequeue produced unexpected result."); + } + + /* always start the timer before you start midi */ + Pt_Start(1, &process_midi, 0); /* start a timer with millisecond accuracy */ + /* the timer will call our function, process_midi() every millisecond */ + + Pm_Initialize(); + + output = (output < 0 ? Pm_GetDefaultOutputDeviceID() : output); + info = Pm_GetDeviceInfo(output); + if (info == NULL) { + printf("Could not open output device (%d).", output); + exit_with_message(""); + } + printf("Opening output device %s %s\n", info->interf, info->name); + + /* use zero latency because we want output to be immediate */ + Pm_OpenOutput(&midi_out, + output, + DRIVER_INFO, + OUTPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO, + LATENCY); + + input = (input < 0 ? Pm_GetDefaultInputDeviceID() : input); + info = Pm_GetDeviceInfo(input); + if (info == NULL) { + printf("Could not open default input device (%d).", input); + exit_with_message(""); + } + printf("Opening input device %s %s\n", info->interf, info->name); + Pm_OpenInput(&midi_in, + input, + DRIVER_INFO, + INPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO); + + active = TRUE; /* enable processing in the midi thread -- yes, this + is a shared variable without synchronization, but + this simple assignment is safe */ + + printf("Enter midi input; it will be transformed as specified by...\n"); + printf("Type 'q' to quit, 'm' to monitor next pitch, t to toggle thru or\n" + "type a number to specify transposition.\n" + "Must terminate with [ENTER]\n"); + + while (!done) { + int32_t msg; + int input; + int len; + if (!fgets(line, STRING_MAX, stdin)) break; /* no stdin? */ + /* remove the newline: */ + len = (int) strlen(line); + if (len > 0) line[len - 1] = 0; /* overwrite the newline char */ + if (strcmp(line, "q") == 0) { + msg = QUIT_MSG; + Pm_Enqueue(main_to_midi, &msg); + /* wait for acknowlegement */ + do { + spin = Pm_Dequeue(midi_to_main, &msg); + } while (spin == 0); /* spin */ ; + done = TRUE; /* leave the command loop and wrap up */ + } else if (strcmp(line, "m") == 0) { + msg = MONITOR_MSG; + Pm_Enqueue(main_to_midi, &msg); + printf("Waiting for note...\n"); + do { + spin = Pm_Dequeue(midi_to_main, &msg); + } while (spin == 0); /* spin */ ; + // convert int32_t to long for safe printing + printf("... pitch is %ld\n", (long) msg); + } else if (strcmp(line, "t") == 0) { + /* reading midi_thru asynchronously could give incorrect results, + e.g. if you type "t" twice before the midi thread responds to + the first one, but we'll do it this way anyway. Perhaps a more + correct way would be to wait for an acknowledgement message + containing the new state. */ + printf("Setting THRU %s\n", (midi_thru ? "off" : "on")); + msg = THRU_MSG; + Pm_Enqueue(main_to_midi, &msg); + } else if (sscanf(line, "%d", &input) == 1) { + if (input >= -127 && input <= 127) { + /* send transposition value, make sur */ + printf("Transposing by %d\n", input); + msg = (int32_t) input; + Pm_Enqueue(main_to_midi, &msg); + } else { + printf("Transposition must be within -127...127\n"); + } + } else { + printf("%s\n%s\n", + "Type 'q[ENTER]' to quit, 'm[ENTER]' to monitor next pitch, or", + "enter a number to specify transposition."); + } + } + + /* at this point, midi thread is inactive and we need to shut down + * the midi input and output + */ + Pt_Stop(); /* stop the timer */ + Pm_QueueDestroy(midi_to_main); + Pm_QueueDestroy(main_to_midi); + + /* Belinda! if close fails here, some memory is deleted, right??? */ + Pm_Close(midi_in); + Pm_Close(midi_out); + + fputs("finished portMidi multithread test.\n" + "type ENTER to quit:", stdout); + while (getchar() != '\n') ; + return 0; +} diff --git a/portmidi/pm_test/midithru.c b/portmidi/pm_test/midithru.c new file mode 100755 index 0000000..94b4f13 --- /dev/null +++ b/portmidi/pm_test/midithru.c @@ -0,0 +1,455 @@ +/* midithru.c -- example program implementing background thru processing */
+
+/* suppose you want low-latency midi-thru processing, but your
+ application wants to take advantage of the input buffer and
+ timestamped data so that it does not have to operate with very low
+ latency.
+
+ This program illustrates how to use a timer callback from PortTime
+ to implement a low-latency process that handles midi thru,
+ including correctly merging midi data from the application with
+ midi data from the input port.
+
+ The main application, which runs in the main program thread, will
+ use an interface similar to that of PortMidi, but since PortMidi
+ does not allow concurrent threads to share access to a stream, the
+ application will call private methods that transfer MIDI messages
+ to and from the timer thread using lock-free queues. All PortMidi
+ API calls are made from the timer thread.
+ */
+
+/* DESIGN
+
+All setup will be done by the main thread. Then, all direct access to
+PortMidi will be handed off to the timer callback thread.
+
+After this hand-off, the main thread will get/send messages via a queue.
+
+The goal is to send incoming messages to the midi output while merging
+any midi data generated by the application. Sysex is a problem here
+because you cannot insert (merge) a midi message while a sysex is in
+progress. There are at least three ways to implement midi thru with
+sysex messages:
+
+1) Turn them off. If your application does not need them, turn them off
+ with Pm_SetFilter(midi_in, PM_FILT_ACTIVE | PM_FILT_SYSEX). You will
+ not receive sysex (or active sensing messages), so you will not have
+ to handle them.
+
+2) Make them atomic. As you receive sysex messages, copy the data into
+ a (big) buffer. Ideally, expand the buffer as needed -- sysex messages
+ do not have any maximum length. Even more ideally, use a list structure
+ and real-time memory allocation to avoid latency in the timer thread.
+ When a full sysex message is received, send it to the midi output all
+ at once.
+
+3) Process sysex incrementally. Send sysex data to midi output as it
+ arrives. Block any non-real-time messages from the application until
+ the sysex message completes. There is the risk that an incomplete
+ sysex message will block messages forever, so implement a 5-second
+ timeout: if no sysex data is seen for 5 seconds, release the block,
+ possibly losing the rest of the sysex message.
+
+ Application messages must be processed similarly: once started, a
+ sysex message will block MIDI THRU processing. We will assume that
+ the application will not abort a sysex message, so timeouts are not
+ necessary here.
+
+This code implements (3).
+
+Latency is also an issue. PortMidi requires timestamps to be in
+non-decreasing order. Since we'll be operating with a low-latency
+timer thread, we can just set the latency to zero meaning timestamps
+are ignored by PortMidi. This will allow thru to go through with
+minimal latency. The application, however, needs to use timestamps
+because we assume it is high latency (the whole purpose of this
+example is to illustrate how to get low-latency thru with a high-latency
+application.) So the callback thread will implement midi timing by
+observing timestamps. The current timestamp will be available in the
+global variable current_timestamp.
+
+*/
+
+
+#include "stdio.h"
+#include "stdlib.h"
+#include "string.h"
+#include "assert.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "porttime.h"
+
+#define MIDI_SYSEX 0xf0
+#define MIDI_EOX 0xf7
+#define STRING_MAX 80 /* used for console input */
+
+/* active is set true when midi processing should start, must be
+ * volatile to force thread to check for updates by other thread */
+int active = FALSE;
+/* process_midi_exit_flag is set when the timer thread shuts down;
+ * must be volatile so it is re-read in the while loop that waits on it */
+volatile int process_midi_exit_flag;
+
+PmStream *midi_in;
+PmStream *midi_out;
+
+/* shared queues */
+#define IN_QUEUE_SIZE 1024
+#define OUT_QUEUE_SIZE 1024
+PmQueue *in_queue;
+PmQueue *out_queue;
+/* this is volatile because it is set in the process_midi callback and
+ * the main thread reads it to sense elapsed time. Without volatile, the
+ * optimizer can put it in a register and not see the updates.
+ */
+volatile PmTimestamp current_timestamp = 0;
+int thru_sysex_in_progress = FALSE;
+int app_sysex_in_progress = FALSE;
+PmTimestamp last_timestamp = 0;
+
+
+static void prompt_and_exit(void)
+{
+ printf("type ENTER...");
+ while (getchar() != '\n') ;
+ /* this will clean up open ports: */
+ exit(-1);
+}
+
+
+static PmError checkerror(PmError err)
+{
+ if (err == pmHostError) {
+ /* it seems pointless to allocate memory and copy the string,
+ * so I will do the work of Pm_GetHostErrorText directly
+ */
+ char errmsg[80];
+ Pm_GetHostErrorText(errmsg, 80);
+ printf("PortMidi found host error...\n %s\n", errmsg);
+ prompt_and_exit();
+ } else if (err < 0) {
+ printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err));
+ prompt_and_exit();
+ }
+ return err;
+}
+
+
+/* time proc parameter for Pm_MidiOpen */
+PmTimestamp midithru_time_proc(void *info)
+{
+ return current_timestamp;
+}
+
+
+/* timer interrupt for processing midi data.
+ Incoming data is delivered to main program via in_queue.
+ Outgoing data from main program is delivered via out_queue.
+ Incoming data from midi_in is copied with low latency to midi_out.
+ Sysex messages from either source block messages from the other.
+ */
+void process_midi(PtTimestamp timestamp, void *userData)
+{
+ PmError result;
+ PmEvent buffer; /* just one message at a time */
+
+ current_timestamp++; /* update every millisecond */
+
+ /* do nothing until initialization completes */
+ if (!active) {
+ /* this flag signals that no more midi processing will be done */
+ process_midi_exit_flag = TRUE;
+ return;
+ }
+
+ /* see if there is any midi input to process */
+ if (!app_sysex_in_progress) {
+ do {
+ result = Pm_Poll(midi_in);
+ if (result) {
+ int status;
+ PmError rslt = Pm_Read(midi_in, &buffer, 1);
+ if (rslt == pmBufferOverflow)
+ continue;
+ assert(rslt == 1);
+
+ /* record timestamp of most recent data */
+ last_timestamp = current_timestamp;
+
+ /* the data might be the end of a sysex message that
+ has timed out, in which case we must ignore it.
+ It's a continuation of a sysex message if status
+ is actually a data byte (high-order bit is zero). */
+ status = Pm_MessageStatus(buffer.message);
+ if (((status & 0x80) == 0) && !thru_sysex_in_progress) {
+ continue; /* ignore this data */
+ }
+
+ /* implement midi thru */
+ /* note that you could output to multiple ports or do other
+ processing here if you wanted
+ */
+ /* printf("thru: %x\n", buffer.message); */
+ Pm_Write(midi_out, &buffer, 1);
+
+ /* send the message to the application */
+ /* you might want to filter clock or active sense messages here
+ to avoid sending a bunch of junk to the application even if
+ you want to send it to MIDI THRU
+ */
+ Pm_Enqueue(in_queue, &buffer);
+
+ /* sysex processing */
+ if (status == MIDI_SYSEX) thru_sysex_in_progress = TRUE;
+ else if ((status & 0xF8) != 0xF8) {
+ /* not MIDI_SYSEX and not real-time, so */
+ thru_sysex_in_progress = FALSE;
+ }
+ if (thru_sysex_in_progress && /* look for EOX */
+ (((buffer.message & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 8) & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 16) & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 24) & 0xFF) == MIDI_EOX))) {
+ thru_sysex_in_progress = FALSE;
+ }
+ }
+ } while (result);
+ }
+
+
+ /* see if there is application midi data to process */
+ while (!Pm_QueueEmpty(out_queue)) {
+ /* see if it is time to output the next message */
+ PmEvent *next = (PmEvent *) Pm_QueuePeek(out_queue);
+ assert(next); /* must be non-null because queue is not empty */
+ if (next->timestamp <= current_timestamp) {
+ /* time to send a message, first make sure it's not blocked */
+ int status = Pm_MessageStatus(next->message);
+ if ((status & 0xF8) == 0xF8) {
+ ; /* real-time messages are not blocked */
+ } else if (thru_sysex_in_progress) {
+ /* maybe sysex has timed out (output becomes unblocked) */
+ if (last_timestamp + 5000 < current_timestamp) {
+ thru_sysex_in_progress = FALSE;
+ } else break; /* output is blocked, so exit loop */
+ }
+ Pm_Dequeue(out_queue, &buffer);
+ Pm_Write(midi_out, &buffer, 1);
+
+ /* inspect message to update app_sysex_in_progress */
+ if (status == MIDI_SYSEX) app_sysex_in_progress = TRUE;
+ else if ((status & 0xF8) != 0xF8) {
+ /* not MIDI_SYSEX and not real-time, so */
+ app_sysex_in_progress = FALSE;
+ }
+ if (app_sysex_in_progress && /* look for EOX */
+ (((buffer.message & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 8) & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 16) & 0xFF) == MIDI_EOX) ||
+ (((buffer.message >> 24) & 0xFF) == MIDI_EOX))) {
+ app_sysex_in_progress = FALSE;
+ }
+ } else break; /* wait until indicated timestamp */
+ }
+}
+
+
+void exit_with_message(char *msg)
+{
+#define STRING_MAX 80
+ printf("%s\nType ENTER...", msg);
+ while (getchar() != '\n') ;
+ exit(1);
+}
+
+
+void initialize(int input, int output, int virtual)
+/* set up midi processing thread and open midi streams */
+{
+ /* note that it is safe to call PortMidi from the main thread for
+ initialization and opening devices. You should not make any
+ calls to PortMidi from this thread once the midi thread begins.
+ to make PortMidi calls.
+ */
+
+ /* note that this routine provides minimal error checking. If
+ you use the PortMidi library compiled with PM_CHECK_ERRORS,
+ then error messages will be printed and the program will exit
+ if an error is encountered. Otherwise, you should add some
+ error checking to this code.
+ */
+
+ const PmDeviceInfo *info;
+
+ /* make the message queues */
+ in_queue = Pm_QueueCreate(IN_QUEUE_SIZE, sizeof(PmEvent));
+ assert(in_queue != NULL);
+ out_queue = Pm_QueueCreate(OUT_QUEUE_SIZE, sizeof(PmEvent));
+ assert(out_queue != NULL);
+
+ /* always start the timer before you start midi */
+ Pt_Start(1, &process_midi, 0); /* start a timer with millisecond accuracy */
+ /* the timer will call our function, process_midi() every millisecond */
+
+ Pm_Initialize();
+
+ if (output < 0) {
+ if (!virtual) {
+ output = Pm_GetDefaultOutputDeviceID();
+ }
+ }
+ if (output >= 0) {
+ info = Pm_GetDeviceInfo(output);
+ if (info == NULL) {
+ printf("Could not open default output device (%d).", output);
+ exit_with_message("");
+ }
+
+ printf("Opening output device %s %s\n", info->interf, info->name);
+
+ /* use zero latency because we want output to be immediate */
+ Pm_OpenOutput(&midi_out,
+ output,
+ NULL /* driver info */,
+ OUT_QUEUE_SIZE,
+ &midithru_time_proc,
+ NULL /* time info */,
+ 0 /* Latency */);
+ } else { /* send to virtual port */
+ int id; + printf("Opening virtual output device \"midithru\"\n");
+ id = Pm_CreateVirtualOutput("midithru", NULL, NULL); + if (id < 0) checkerror(id); /* error reporting */
+ checkerror(Pm_OpenOutput(&midi_out, id, NULL, OUT_QUEUE_SIZE,
+ &midithru_time_proc, NULL, 0));
+ }
+ if (input < 0) {
+ if (!virtual) {
+ input = Pm_GetDefaultInputDeviceID();
+ }
+ }
+ if (input >= 0) {
+ info = Pm_GetDeviceInfo(input);
+ if (info == NULL) {
+ printf("Could not open default input device (%d).", input);
+ exit_with_message("");
+ }
+
+ printf("Opening input device %s %s\n", info->interf, info->name);
+ Pm_OpenInput(&midi_in,
+ input,
+ NULL /* driver info */,
+ 0 /* use default input size */,
+ &midithru_time_proc,
+ NULL /* time info */);
+ } else { /* receive from virtual port */
+ int id; + printf("Opening virtual input device \"midithru\"\n");
+ id = Pm_CreateVirtualInput("midithru", NULL, NULL); + if (id < 0) checkerror(id); /* error reporting */
+ checkerror(Pm_OpenInput(&midi_in, id, NULL, 0,
+ &midithru_time_proc, NULL));
+ }
+ /* Note: if you set a filter here, then this will filter what goes
+ to the MIDI THRU port. You may not want to do this.
+ */
+ Pm_SetFilter(midi_in, PM_FILT_ACTIVE | PM_FILT_CLOCK);
+
+ active = TRUE; /* enable processing in the midi thread -- yes, this
+ is a shared variable without synchronization, but
+ this simple assignment is safe */
+
+}
+
+
+void finalize()
+{
+ /* the timer thread could be in the middle of accessing PortMidi stuff */
+ /* to detect that it is done, we first clear process_midi_exit_flag and
+ then wait for the timer thread to set it
+ */
+ process_midi_exit_flag = FALSE;
+ active = FALSE;
+ /* busy wait for flag from timer thread that it is done */
+ while (!process_midi_exit_flag) ;
+ /* at this point, midi thread is inactive and we need to shut down
+ * the midi input and output
+ */
+ Pt_Stop(); /* stop the timer */
+ Pm_QueueDestroy(in_queue);
+ Pm_QueueDestroy(out_queue);
+
+ Pm_Close(midi_in);
+ Pm_Close(midi_out);
+
+ Pm_Terminate();
+}
+
+
+int main(int argc, char *argv[])
+{
+ PmTimestamp last_time = 0;
+ PmEvent buffer;
+ int i;
+ int input = -1, output = -1;
+ int virtual = FALSE;
+ int delay_enable = TRUE;
+
+ printf("Usage: midithru [-i input] [-o output] [-v] [-n]\n"
+ "where input and output are portmidi device numbers\n"
+ "if -v and input and/or output are not specified,\n"
+ "then virtual ports are created and used instead.\n"
+ "-n turns off the default MIDI delay effect.\n");
+ for (i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "-i") == 0) {
+ i++;
+ input = atoi(argv[i]);
+ printf("Input device number: %d\n", input);
+ } else if (strcmp(argv[i], "-o") == 0) {
+ i++;
+ output = atoi(argv[i]);
+ printf("Output device number: %d\n", output);
+ } else if (strcmp(argv[i], "-v") == 0) {
+ virtual = TRUE;
+ } else if (strcmp(argv[i], "-n") == 0) {
+ delay_enable = FALSE;
+ printf("delay_effect is disabled\n");
+ } else {
+ return -1;
+ }
+ }
+ printf("begin PortMidi midithru program...\n");
+
+ initialize(input, output, virtual); /* set up and start midi processing */
+
+ printf("This program will run for 60 seconds, "
+ "or until you play B below middle C,\n"
+ "All input is sent immediately, implementing software MIDI THRU.\n"
+ "Also, all input is echoed with a 2 second delay.\n");
+
+ while (current_timestamp < 60000) {
+ /* just to make the point that this is not a low-latency process,
+ spin until half a second has elapsed */
+ last_time = last_time + 500;
+ while (last_time > current_timestamp) ;
+
+ /* now read data and send it after changing timestamps */
+ while (Pm_Dequeue(in_queue, &buffer) == 1) {
+ /* printf("timestamp %d\n", buffer.timestamp); */
+ /* printf("message %x\n", buffer.message); */
+ if (delay_enable) {
+ buffer.timestamp = buffer.timestamp + 2000; /* delay */
+ Pm_Enqueue(out_queue, &buffer);
+ }
+ /* play B3 to break out of loop */
+ if (Pm_MessageStatus(buffer.message) == 0x90 &&
+ Pm_MessageData1(buffer.message) == 59) {
+ goto quit_now;
+ }
+ }
+ }
+quit_now:
+ finalize();
+ exit_with_message("finished PortMidi midithru program.");
+ return 0; /* never executed, but keeps the compiler happy */
+}
diff --git a/portmidi/pm_test/mm.c b/portmidi/pm_test/mm.c new file mode 100755 index 0000000..ab9d32e --- /dev/null +++ b/portmidi/pm_test/mm.c @@ -0,0 +1,595 @@ +/* mm.c -- midi monitor */
+
+/*****************************************************************************
+* Change Log
+* Date | Change
+*-----------+-----------------------------------------------------------------
+* 7-Apr-86 | Created changelog
+* 31-Jan-90 | GWL : use new cmdline
+* 5-Apr-91 | JDW : Further changes
+* 16-Feb-92 | GWL : eliminate label mmexit:; add error recovery
+* 18-May-92 | GWL : continuous clocks, etc.
+* 17-Jan-94 | GWL : option to display notes
+* 20-Nov-06 | RBD : port mm.c from CMU Midi Toolkit to PortMidi
+* | mm.c -- revealing MIDI secrets for over 20 years!
+*****************************************************************************/
+
+#include "stdlib.h"
+#include "ctype.h"
+#include "string.h"
+#include "stdio.h"
+#include "porttime.h"
+#include "portmidi.h"
+
+#define STRING_MAX 80
+
+#define MIDI_CODE_MASK 0xf0
+#define MIDI_CHN_MASK 0x0f
+/*#define MIDI_REALTIME 0xf8
+ #define MIDI_CHAN_MODE 0xfa */
+#define MIDI_OFF_NOTE 0x80
+#define MIDI_ON_NOTE 0x90
+#define MIDI_POLY_TOUCH 0xa0
+#define MIDI_CTRL 0xb0
+#define MIDI_CH_PROGRAM 0xc0
+#define MIDI_TOUCH 0xd0
+#define MIDI_BEND 0xe0
+
+#define MIDI_SYSEX 0xf0
+#define MIDI_Q_FRAME 0xf1
+#define MIDI_SONG_POINTER 0xf2
+#define MIDI_SONG_SELECT 0xf3
+#define MIDI_TUNE_REQ 0xf6
+#define MIDI_EOX 0xf7
+#define MIDI_TIME_CLOCK 0xf8
+#define MIDI_START 0xfa
+#define MIDI_CONTINUE 0xfb
+#define MIDI_STOP 0xfc
+#define MIDI_ACTIVE_SENSING 0xfe
+#define MIDI_SYS_RESET 0xff
+
+#define MIDI_ALL_SOUND_OFF 0x78
+#define MIDI_RESET_CONTROLLERS 0x79
+#define MIDI_LOCAL 0x7a
+#define MIDI_ALL_OFF 0x7b
+#define MIDI_OMNI_OFF 0x7c
+#define MIDI_OMNI_ON 0x7d
+#define MIDI_MONO_ON 0x7e
+#define MIDI_POLY_ON 0x7f
+
+
+#define private static
+
+#ifndef false
+#define false 0
+#define true 1
+#endif
+
+typedef int boolean;
+
+int debug = false; /* never set, but referenced by userio.c */
+PmStream *midi_in; /* midi input */
+boolean active = false; /* set when midi_in is ready for reading */
+boolean in_sysex = false; /* we are reading a sysex message */
+boolean inited = false; /* suppress printing during command line parsing */
+boolean done = false; /* when true, exit */
+boolean notes = true; /* show notes? */
+boolean controls = true; /* show continuous controllers */
+boolean bender = true; /* record pitch bend etc.? */
+boolean excldata = true; /* record system exclusive data? */
+boolean verbose = true; /* show text representation? */
+boolean realdata = true; /* record real time messages? */
+boolean clksencnt = true; /* clock and active sense count on */
+boolean chmode = true; /* show channel mode messages */
+boolean pgchanges = true; /* show program changes */
+boolean flush = false; /* flush all pending MIDI data */
+
+uint32_t filter = 0; /* remember state of midi filter */
+
+uint32_t clockcount = 0; /* count of clocks */
+uint32_t actsensecount = 0; /* cout of active sensing bytes */
+uint32_t notescount = 0; /* #notes since last request */
+uint32_t notestotal = 0; /* total #notes */
+
+char val_format[] = " Val %d\n";
+
+/*****************************************************************************
+* Imported variables
+*****************************************************************************/
+
+extern int abort_flag;
+
+/*****************************************************************************
+* Routines local to this module
+*****************************************************************************/
+
+private void mmexit(int code);
+private void output(PmMessage data);
+private int put_pitch(int p);
+private void showhelp();
+private void showbytes(PmMessage data, int len, boolean newline);
+private void showstatus(boolean flag);
+private void doascii(char c);
+private int get_number(const char *prompt);
+
+
+/* read a number from console */
+/**/
+int get_number(const char *prompt)
+{
+ int n = 0, i;
+ fputs(prompt, stdout);
+ while (n != 1) {
+ n = scanf("%d", &i);
+ while (getchar() != '\n') ;
+ }
+ return i;
+}
+
+
+void receive_poll(PtTimestamp timestamp, void *userData)
+{
+ PmEvent event;
+ int count;
+ if (!active) return;
+ while ((count = Pm_Read(midi_in, &event, 1))) {
+ if (count == 1) output(event.message);
+ else puts(Pm_GetErrorText(count));
+ }
+}
+
+
+/****************************************************************************
+* main
+* Effect: prompts for parameters, starts monitor
+****************************************************************************/
+
+int main(int argc, char **argv)
+{
+ char *argument;
+ int inp;
+ PmError err;
+ int i;
+ if (argc > 1) { /* first arg can change defaults */
+ argument = argv[1];
+ while (*argument) doascii(*argument++);
+ }
+ showhelp();
+ /* use porttime callback to empty midi queue and print */
+ Pt_Start(1, receive_poll, 0);
+ /* list device information */
+ puts("MIDI input devices:");
+ for (i = 0; i < Pm_CountDevices(); i++) {
+ const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
+ if (info->input) printf("%d: %s, %s\n", i, info->interf, info->name);
+ }
+ inp = get_number("Type input device number: ");
+ err = Pm_OpenInput(&midi_in, inp, NULL, 512, NULL, NULL);
+ if (err) {
+ puts(Pm_GetErrorText(err));
+ Pt_Stop();
+ mmexit(1);
+ }
+ Pm_SetFilter(midi_in, filter);
+ inited = true; /* now can document changes, set filter */
+ printf("Midi Monitor ready.\n");
+ active = true;
+ while (!done) {
+ doascii(getchar());
+ while (getchar() != '\n') ;
+ }
+ active = false;
+ Pm_Close(midi_in);
+ Pt_Stop();
+ Pm_Terminate();
+ mmexit(0);
+ return 0; // make the compiler happy be returning a value
+}
+
+
+/****************************************************************************
+* doascii
+* Inputs:
+* char c: input character
+* Effect: interpret to revise flags
+****************************************************************************/
+
+private void doascii(char c)
+{
+ if (isupper(c)) c = tolower(c);
+ if (c == 'q') done = true;
+ else if (c == 'b') {
+ bender = !bender;
+ filter ^= PM_FILT_PITCHBEND;
+ if (inited)
+ printf("Pitch Bend, etc. %s\n", (bender ? "ON" : "OFF"));
+ } else if (c == 'c') {
+ controls = !controls;
+ filter ^= PM_FILT_CONTROL;
+ if (inited)
+ printf("Control Change %s\n", (controls ? "ON" : "OFF"));
+ } else if (c == 'h') {
+ pgchanges = !pgchanges;
+ filter ^= PM_FILT_PROGRAM;
+ if (inited)
+ printf("Program Changes %s\n", (pgchanges ? "ON" : "OFF"));
+ } else if (c == 'n') {
+ notes = !notes;
+ filter ^= PM_FILT_NOTE;
+ if (inited)
+ printf("Notes %s\n", (notes ? "ON" : "OFF"));
+ } else if (c == 'x') {
+ excldata = !excldata;
+ filter ^= PM_FILT_SYSEX;
+ if (inited)
+ printf("System Exclusive data %s\n", (excldata ? "ON" : "OFF"));
+ } else if (c == 'r') {
+ realdata = !realdata;
+ filter ^= (PM_FILT_PLAY | PM_FILT_RESET | PM_FILT_TICK | PM_FILT_UNDEFINED);
+ if (inited)
+ printf("Real Time messages %s\n", (realdata ? "ON" : "OFF"));
+ } else if (c == 'k') {
+ clksencnt = !clksencnt;
+ if (inited) {
+ printf("Clock and Active Sense Counting %s\n", (clksencnt ? "ON" : "OFF"));
+ printf("Resetting Clock and Active Sense counts.\n");
+ clockcount = actsensecount = 0;
+ }
+ } else if (c == 's') {
+ if (inited) {
+ printf("Clock Count %ld\nActive Sense Count %ld\n",
+ (long) clockcount, (long) actsensecount);
+ }
+ } else if (c == 't') {
+ notestotal+=notescount;
+ if (inited)
+ printf("This Note Count %ld\nTotal Note Count %ld\n",
+ (long) notescount, (long) notestotal);
+ notescount=0;
+ } else if (c == 'v') {
+ verbose = !verbose;
+ if (inited)
+ printf("Verbose %s\n", (verbose ? "ON" : "OFF"));
+ } else if (c == 'm') {
+ chmode = !chmode;
+ if (inited)
+ printf("Channel Mode Messages %s", (chmode ? "ON" : "OFF"));
+ } else {
+ if (inited) {
+ if (c == ' ') {
+ PmEvent event;
+ while (Pm_Read(midi_in, &event, 1)) ; /* flush midi input */
+ printf("...FLUSHED MIDI INPUT\n\n");
+ } else showhelp();
+ }
+ }
+ if (inited) Pm_SetFilter(midi_in, filter);
+}
+
+
+
+private void mmexit(int code)
+{
+ /* if this is not being run from a console, maybe we should wait for
+ * the user to read error messages before exiting
+ */
+ exit(code);
+}
+
+
+/****************************************************************************
+* output
+* Inputs:
+* data: midi message buffer holding one command or 4 bytes of sysex msg
+* Effect: format and print midi data
+****************************************************************************/
+
+char vel_format[] = " Vel %d\n";
+
+private void output(PmMessage data)
+{
+ int command; /* the current command */
+ int chan; /* the midi channel of the current event */
+ int len; /* used to get constant field width */
+
+ /* printf("output data %8x; ", data); */
+
+ command = Pm_MessageStatus(data) & MIDI_CODE_MASK;
+ chan = Pm_MessageStatus(data) & MIDI_CHN_MASK;
+
+ if (in_sysex || Pm_MessageStatus(data) == MIDI_SYSEX) {
+#define sysex_max 16
+ int i;
+ PmMessage data_copy = data;
+ in_sysex = true;
+ /* look for MIDI_EOX in first 3 bytes
+ * if realtime messages are embedded in sysex message, they will
+ * be printed as if they are part of the sysex message
+ */
+ for (i = 0; (i < 4) && ((data_copy & 0xFF) != MIDI_EOX); i++)
+ data_copy >>= 8;
+ if (i < 4) {
+ in_sysex = false;
+ i++; /* include the EOX byte in output */
+ }
+ showbytes(data, i, verbose);
+ if (verbose) printf("System Exclusive\n");
+ } else if (command == MIDI_ON_NOTE && Pm_MessageData2(data) != 0) {
+ notescount++;
+ if (notes) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf("NoteOn Chan %2d Key %3d ", chan, Pm_MessageData1(data));
+ len = put_pitch(Pm_MessageData1(data));
+ printf(vel_format + len, Pm_MessageData2(data));
+ }
+ }
+ } else if ((command == MIDI_ON_NOTE /* && Pm_MessageData2(data) == 0 */ ||
+ command == MIDI_OFF_NOTE)) {
+ if (notes) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf("NoteOff Chan %2d Key %3d ", chan,
+ Pm_MessageData1(data));
+ len = put_pitch(Pm_MessageData1(data));
+ printf(vel_format + len, Pm_MessageData2(data));
+ }
+ }
+ } else if (command == MIDI_CH_PROGRAM) {
+ if (pgchanges) {
+ showbytes(data, 2, verbose);
+ if (verbose) {
+ printf(" ProgChg Chan %2d Prog %2d\n", chan,
+ Pm_MessageData1(data) + 1);
+ }
+ }
+ } else if (command == MIDI_CTRL) {
+ /* controls 121 (MIDI_RESET_CONTROLLER) to 127 are channel
+ * mode messages. */
+ if (Pm_MessageData1(data) < MIDI_ALL_SOUND_OFF) {
+ if (controls) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf("CtrlChg Chan %2d Ctrl %2d Val %2d\n",
+ chan, Pm_MessageData1(data), Pm_MessageData2(data));
+ }
+ } else /* channel mode */ if (chmode) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ switch (Pm_MessageData1(data)) {
+ case MIDI_ALL_SOUND_OFF:
+ printf("All Sound Off, Chan %2d\n", chan);
+ break;
+ case MIDI_RESET_CONTROLLERS:
+ printf("Reset All Controllers, Chan %2d\n", chan);
+ break;
+ case MIDI_LOCAL:
+ printf("LocCtrl Chan %2d %s\n",
+ chan, Pm_MessageData2(data) ? "On" : "Off");
+ break;
+ case MIDI_ALL_OFF:
+ printf("All Off Chan %2d\n", chan);
+ break;
+ case MIDI_OMNI_OFF:
+ printf("OmniOff Chan %2d\n", chan);
+ break;
+ case MIDI_OMNI_ON:
+ printf("Omni On Chan %2d\n", chan);
+ break;
+ case MIDI_MONO_ON:
+ printf("Mono On Chan %2d\n", chan);
+ if (Pm_MessageData2(data))
+ printf(" to %d received channels\n",
+ Pm_MessageData2(data));
+ else
+ printf(" to all received channels\n");
+ break;
+ case MIDI_POLY_ON:
+ printf("Poly On Chan %2d\n", chan);
+ break;
+ }
+ }
+ }
+ }
+ } else if (command == MIDI_POLY_TOUCH) {
+ if (bender) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf("P.Touch Chan %2d Key %2d ", chan,
+ Pm_MessageData1(data));
+ len = put_pitch(Pm_MessageData1(data));
+ printf(val_format + len, Pm_MessageData2(data));
+ }
+ }
+ } else if (command == MIDI_TOUCH) {
+ if (bender) {
+ showbytes(data, 2, verbose);
+ if (verbose) {
+ printf(" A.Touch Chan %2d Val %2d\n", chan,
+ Pm_MessageData1(data));
+ }
+ }
+ } else if (command == MIDI_BEND) {
+ if (bender) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf("P.Bend Chan %2d Val %2d\n", chan,
+ (Pm_MessageData1(data) + (Pm_MessageData2(data)<<7)));
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_SONG_POINTER) {
+ showbytes(data, 3, verbose);
+ if (verbose) {
+ printf(" Song Position %d\n",
+ (Pm_MessageData1(data) + (Pm_MessageData2(data)<<7)));
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_SONG_SELECT) {
+ showbytes(data, 2, verbose);
+ if (verbose) {
+ printf(" Song Select %d\n", Pm_MessageData1(data));
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_TUNE_REQ) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Tune Request\n");
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_Q_FRAME) {
+ if (realdata) {
+ showbytes(data, 2, verbose);
+ if (verbose) {
+ printf(" Time Code Quarter Frame Type %d Values %d\n",
+ (Pm_MessageData1(data) & 0x70) >> 4,
+ Pm_MessageData1(data) & 0xf);
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_START) {
+ if (realdata) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Start\n");
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_CONTINUE) {
+ if (realdata) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Continue\n");
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_STOP) {
+ if (realdata) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Stop\n");
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_SYS_RESET) {
+ if (realdata) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" System Reset\n");
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_TIME_CLOCK) {
+ clockcount++;
+ if (clksencnt) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Clock\n");
+ }
+ }
+ } else if (Pm_MessageStatus(data) == MIDI_ACTIVE_SENSING) {
+ actsensecount++;
+ if (clksencnt) {
+ showbytes(data, 1, verbose);
+ if (verbose) {
+ printf(" Active Sensing\n");
+ }
+ }
+ } else showbytes(data, 3, verbose);
+ fflush(stdout);
+}
+
+
+/****************************************************************************
+* put_pitch
+* Inputs:
+* int p: pitch number
+* Effect: write out the pitch name for a given number
+****************************************************************************/
+
+private int put_pitch(int p)
+{
+ char result[8];
+ static char *ptos[] = {
+ "c", "cs", "d", "ef", "e", "f", "fs", "g",
+ "gs", "a", "bf", "b" };
+ /* note octave correction below */
+ sprintf(result, "%s%d", ptos[p % 12], (p / 12) - 1);
+ fputs(result, stdout);
+ return (int) strlen(result);
+}
+
+
+/****************************************************************************
+* showbytes
+* Effect: print hex data, precede with newline if asked
+****************************************************************************/
+
+char nib_to_hex[] = "0123456789ABCDEF";
+
+private void showbytes(PmMessage data, int len, boolean newline)
+{
+ int count = 0;
+ int i;
+
+/* if (newline) {
+ putchar('\n');
+ count++;
+ } */
+ for (i = 0; i < len; i++) {
+ putchar(nib_to_hex[(data >> 4) & 0xF]);
+ putchar(nib_to_hex[data & 0xF]);
+ count += 2;
+ if (count > 72) {
+ putchar('.');
+ putchar('.');
+ putchar('.');
+ break;
+ }
+ data >>= 8;
+ }
+ putchar(' ');
+}
+
+
+
+/****************************************************************************
+* showhelp
+* Effect: print help text
+****************************************************************************/
+
+private void showhelp()
+{
+ printf("\n");
+ printf(" Item Reported Range Item Reported Range\n");
+ printf(" ------------- ----- ------------- -----\n");
+ printf(" Channels 1 - 16 Programs 1 - 128\n");
+ printf(" Controllers 0 - 127 After Touch 0 - 127\n");
+ printf(" Loudness 0 - 127 Pitch Bend 0 - 16383, "
+ "center = 8192\n");
+ printf(" Pitches 0 - 127, 60 = c4 = middle C\n");
+ printf(" \n");
+ printf("n toggles notes");
+ showstatus(notes);
+ printf("t displays noteon count since last t\n");
+ printf("b toggles pitch bend, aftertouch");
+ showstatus(bender);
+ printf("c toggles continuous control");
+ showstatus(controls);
+ printf("h toggles program changes");
+ showstatus(pgchanges);
+ printf("x toggles system exclusive");
+ showstatus(excldata);
+ printf("k toggles clock and sense messages, clears counts");
+ showstatus(clksencnt);
+ printf("r toggles other real time messages & SMPTE");
+ showstatus(realdata);
+ printf("s displays clock and sense count since last k\n");
+ printf("m toggles channel mode messages");
+ showstatus(chmode);
+ printf("v toggles verbose text");
+ showstatus(verbose);
+ printf("q quits\n");
+ printf("\n");
+}
+
+/****************************************************************************
+* showstatus
+* Effect: print status of flag
+****************************************************************************/
+
+private void showstatus(boolean flag)
+{
+ printf(", now %s\n", flag ? "ON" : "OFF");
+}
diff --git a/portmidi/pm_test/multivirtual.c b/portmidi/pm_test/multivirtual.c new file mode 100644 index 0000000..b90d860 --- /dev/null +++ b/portmidi/pm_test/multivirtual.c @@ -0,0 +1,223 @@ +/* multivirtual.c -- test for creating two input and two output virtual ports */ +/* + * Roger B. Dannenberg + * Oct 2021 + */ +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define OUTPUT_BUFFER_SIZE 0 +#define DEVICE_INFO NULL +#define DRIVER_INFO NULL +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +int latency = 0; + +static void prompt_and_exit(void) +{ + printf("type ENTER..."); + while (getchar() != '\n') ; + /* this will clean up open ports: */ + exit(-1); +} + + +static PmError checkerror(PmError err) +{ + if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + char errmsg[80]; + Pm_GetHostErrorText(errmsg, 80); + printf("PortMidi found host error...\n %s\n", errmsg); + prompt_and_exit(); + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + prompt_and_exit(); + } + return err; +} + +static int msg_count[2] = {0, 0}; + +void poll_input(PmStream *in, int which) +{ + PmEvent buffer[1]; + int pitch, expected, length; + PmError status = Pm_Poll(in); + if (status == TRUE) { + length = Pm_Read(in, buffer, 1); + if (length > 0) { + printf("Got message %d from portmidi%d: " + "time %ld, %2x %2x %2x\n", + msg_count[which], which + 1, (long) buffer[0].timestamp, + (status = Pm_MessageStatus(buffer[0].message)), + (pitch = Pm_MessageData1(buffer[0].message)), + Pm_MessageData2(buffer[0].message)); + if (status == 0x90) { /* 1 & 2 are on/off 60, 3 & 4 are 61, etc. */ + expected = (((msg_count[which] - 1) / 2) % 12) + 60 + + which * 12; + if (pitch != expected) { + printf("WARNING: expected pitch %d, got pitch %d\n", + expected, pitch); + } + } + msg_count[which]++; + } else { + assert(0); + } + } +} + + +void wait_until(PmTimestamp when, PmStream *in1, PmStream *in2) +{ + while (when > Pt_Time()) { + poll_input(in1, 0); + poll_input(in2, 1); + Pt_Sleep(10); + } +} + + +/* create one virtual output device and one input device */ +void init(const char *name, PmStream **midi_out, PmStream **midi_in, + int *id_out, int *id_in) +{ + PmEvent buffer[1]; + + *id_out = checkerror(Pm_CreateVirtualOutput(name, NULL, DEVICE_INFO)); + checkerror(Pm_OpenOutput(midi_out, *id_out, DRIVER_INFO, OUTPUT_BUFFER_SIZE, + TIME_PROC, TIME_INFO, latency)); + printf("Virtual Output \"%s\" id %d created and opened.\n", name, *id_out); + + *id_in = checkerror(Pm_CreateVirtualInput(name, NULL, DRIVER_INFO)); + checkerror(Pm_OpenInput(midi_in, *id_in, NULL, 0, NULL, NULL)); + printf("Virtual Input \"%s\" id %d created and opened.\n", name, *id_in); + Pm_SetFilter(*midi_in, PM_FILT_ACTIVE | PM_FILT_CLOCK | PM_FILT_SYSEX); + /* empty the buffer after setting filter, just in case anything + got through */ + while (Pm_Read(*midi_in, buffer, 1)) ; +} + + +void main_test(int num) +{ + PmStream *midi1_out; + PmStream *midi2_out; + PmStream *midi1_in; + PmStream *midi2_in; + int id1_out; + int id2_out; + int id1_in; + int id2_in; + int32_t next_time; + PmEvent buffer[1]; + int pitch = 60; + int expected_count = num + 1; /* add 1 for MIDI Program message */ + + /* It is recommended to start timer before Midi; otherwise, PortMidi may + start the timer with its (default) parameters + */ + TIME_START; + + init("portmidi1", &midi1_out, &midi1_in, &id1_out, &id1_in); + init("portmidi2", &midi2_out, &midi2_in, &id2_out, &id2_in); + + printf("Type ENTER to send messages: "); + while (getchar() != '\n') ; + + buffer[0].timestamp = Pt_Time(); +#define PROGRAM 0 + buffer[0].message = Pm_Message(0xC0, PROGRAM, 0); + Pm_Write(midi1_out, buffer, 1); + Pm_Write(midi2_out, buffer, 1); + next_time = Pt_Time() + 1000; /* wait 1s */ + while (num > 0) { + wait_until(next_time, midi1_in, midi2_in); + Pm_WriteShort(midi1_out, next_time, Pm_Message(0x90, pitch, 100)); + Pm_WriteShort(midi2_out, next_time, Pm_Message(0x90, pitch + 12, 100)); + printf("Note On pitch %d\n", pitch); + num--; + next_time += 500; + + wait_until(next_time, midi1_in, midi2_in); + Pm_WriteShort(midi1_out, next_time, Pm_Message(0x90, pitch, 0)); + Pm_WriteShort(midi2_out, next_time, Pm_Message(0x90, pitch + 12, 0)); + printf("Note Off pitch %d\n", pitch); + num--; + pitch = (pitch + 1) % 12 + 60; + next_time += 500; + } + wait_until(next_time, midi1_in, midi2_in); /* get final note-offs */ + + printf("Got %d messages from portmidi1 and %d from portmidi2; " + "expected %d.\n", msg_count[0], msg_count[1], expected_count); + + /* close devices (this not explicitly needed in most implementations) */ + printf("ready to close..."); + checkerror(Pm_Close(midi1_out)); + checkerror(Pm_Close(midi2_out)); + checkerror(Pm_Close(midi1_in)); + checkerror(Pm_Close(midi2_in)); + printf("done closing.\nNow delete the virtual devices..."); + checkerror(Pm_DeleteVirtualDevice(id1_out)); + checkerror(Pm_DeleteVirtualDevice(id1_in)); + checkerror(Pm_DeleteVirtualDevice(id2_out)); + checkerror(Pm_DeleteVirtualDevice(id2_in)); + printf("done deleting.\n"); +} + + +void show_usage() +{ + printf("Usage: multivirtual [-h] [-l latency-in-ms] [n]\n" + " -h for this message,\n" + " -l ms designates latency for precise timing (default 0),\n" + " n is number of message to send each output, not counting\n" + " initial program change.\n" + "sends change program to 1, then one note per second with 0.5s on,\n" + "0.5s off, for n/2 seconds to both output ports portmidi1 and\n" + "portmidi2. portmidi1 gets pitches from C4 (60). portmidi2 gets\n" + "pitches an octave higher. Latency >0 uses the device driver for \n" + "precise timing (see PortMidi documentation). Inputs print what\n" + "they get and print WARNING if they get something unexpected.\n" + "The expected test is use two instances of testio to loop\n" + "portmidi1 back to portmidi1 and portmidi2 back to portmidi2.\n"); + exit(0); +} + + +int main(int argc, char *argv[]) +{ + int num = 10; + int i; + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + } else if (strcmp(argv[i], "-l") == 0 && (i + 1 < argc)) { + i = i + 1; + latency = atoi(argv[i]); + printf("Latency will be %d\n", latency); + } else { + num = atoi(argv[1]); + if (num <= 0) { + show_usage(); + } + printf("Sending %d messages.\n", num); + } + } + + main_test(num); + + printf("finished sendvirtual test...type ENTER to quit..."); + while (getchar() != '\n') ; + return 0; +} diff --git a/portmidi/pm_test/pmlist.c b/portmidi/pm_test/pmlist.c new file mode 100644 index 0000000..5e3d1db --- /dev/null +++ b/portmidi/pm_test/pmlist.c @@ -0,0 +1,63 @@ +/* pmlist.c -- list portmidi devices and numbers + * + * This program lists devices. When you type return, it + * restarts portmidi and lists devices again. It is mainly + * a test for shutting down and restarting. + * + * Roger B. Dannenberg, Feb 2022 + */ + +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define DEVICE_INFO NULL +#define DRIVER_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define STRING_MAX 80 /* used for console input */ + +void show_usage() +{ + printf("Usage: pmlist [-h]\n -h means help.\n" + " Type return to rescan and list devices, q<ret> to quit\n"); +} + + +int main(int argc, char *argv[]) +{ + if (argc > 1) { + show_usage(); + exit(0); + } + + while (1) { + char input[STRING_MAX]; + const char *deflt; + const char *in_or_out; + int default_in, default_out, i; + + // Pm_Initialize(); + /* list device information */ + default_in = Pm_GetDefaultInputDeviceID(); + default_out = Pm_GetDefaultOutputDeviceID(); + for (i = 0; i < Pm_CountDevices(); i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + printf("%d: %s, %s", i, info->interf, info->name); + deflt = ""; + if (i == default_out || i == default_in) { + deflt = "default "; + } + in_or_out = (info->input ? "input" : "output"); + printf(" (%s%s)\n", deflt, in_or_out); + } + if (fgets(input, STRING_MAX, stdin) && input[0] == 'q') { + return 0; + } + Pm_Terminate(); + } + return 0; +} diff --git a/portmidi/pm_test/qtest.c b/portmidi/pm_test/qtest.c new file mode 100644 index 0000000..14d803e --- /dev/null +++ b/portmidi/pm_test/qtest.c @@ -0,0 +1,174 @@ +#include "portmidi.h" +#include "pmutil.h" +#include "stdlib.h" +#include "stdio.h" + + +/* make_msg -- make a psuedo-random message of length n whose content + * is purely a function of i + */ +void make_msg(long msg[], int n, int i) +{ + int j; + for (j = 0; j < n; j++) { + msg[j] = i % (j + 5); + } +} + + +/* print_msg -- print the content of msg of length n to stdout */ +/**/ +void print_msg(long msg[], int n) +{ + int i; + for (i = 0; i < n; i++) { + printf(" %li", msg[i]); + } +} + + +/* cmp_msg -- compare two messages of length n */ +/**/ +int cmp_msg(long msg[], long msg2[], int n, int i) +{ + int j; + for (j = 0; j < n; j++) { + if (msg[j] != msg2[j]) { + printf("Received message %d doesn't match sent message\n", i); + printf("in: "); print_msg(msg, n); printf("\n"); + printf("out:"); print_msg(msg2, n); printf("\n"); + return FALSE; + } + } + return TRUE; +} + + +int main() +{ + int msg_len; + for (msg_len = 4; msg_len < 100; msg_len += 5) { + PmQueue *queue = Pm_QueueCreate(100, msg_len * sizeof(long)); + int i; + long msg[100]; + long msg2[100]; + + printf("msg_len = %d\n", msg_len); + if (!queue) { + printf("Could not allocate queue\n"); + return 1; + } + + /* insert/remove 1000 messages */ + printf("test 1\n"); + for (i = 0; i < 1357; i++) { + make_msg(msg, msg_len, i); + if (Pm_Enqueue(queue, msg)) { + printf("Pm_Enqueue error\n"); + return 1; + } + if (Pm_Dequeue(queue, msg2) != 1) { + printf("Pm_Dequeue error\n"); + return 1; + } + if (!cmp_msg(msg, msg2, msg_len, i)) { + return 1; + } + } + + /* make full */ + printf("test 2\n"); + for (i = 0; i < 100; i++) { + make_msg(msg, msg_len, i); + if (Pm_Enqueue(queue, msg)) { + printf("Pm_Enqueue error\n"); + return 1; + } + } + + /* alternately remove and insert */ + for (i = 100; i < 1234; i++) { + make_msg(msg, msg_len, i - 100); /* what we expect */ + if (Pm_Dequeue(queue, msg2) != 1) { + printf("Pm_Dequeue error\n"); + return 1; + } + if (!cmp_msg(msg, msg2, msg_len, i)) { + return 1; + } + make_msg(msg, msg_len, i); + if (Pm_Enqueue(queue, msg)) { + printf("Pm_Enqueue error\n"); + return 1; + } + } + + /* remove all */ + while (!Pm_QueueEmpty(queue)) { + make_msg(msg, msg_len, i - 100); /* what we expect */ + if (Pm_Dequeue(queue, msg2) != 1) { + printf("Pm_Dequeue error\n"); + return 1; + } + if (!cmp_msg(msg, msg2, msg_len, i)) { + return 1; + } + i++; + } + if (i != 1334) { + printf("Message count error\n"); + return 1; + } + + /* now test overflow */ + printf("test 3\n"); + for (i = 0; i < 110; i++) { + make_msg(msg, msg_len, i); + if (Pm_Enqueue(queue, msg) == pmBufferOverflow) { + break; /* this is supposed to execute after 100 messages */ + } + } + for (i = 0; i < 100; i++) { + make_msg(msg, msg_len, i); + if (Pm_Dequeue(queue, msg2) != 1) { + printf("Pm_Dequeue error\n"); + return 1; + } + if (!cmp_msg(msg, msg2, msg_len, i)) { + return 1; + } + } + /* we should detect overflow after removing 100 messages */ + if (Pm_Dequeue(queue, msg2) != pmBufferOverflow) { + printf("Pm_Dequeue overflow expected\n"); + return 1; + } + + /* after overflow is detected (and cleared), sender can + * send again + */ + /* see if function is restored, also test peek */ + printf("test 4\n"); + for (i = 0; i < 1357; i++) { + long *peek; + make_msg(msg, msg_len, i); + if (Pm_Enqueue(queue, msg)) { + printf("Pm_Enqueue error\n"); + return 1; + } + peek = (long *) Pm_QueuePeek(queue); + if (!cmp_msg(msg, peek, msg_len, i)) { + return 1; + } + if (Pm_Dequeue(queue, msg2) != 1) { + printf("Pm_Dequeue error\n"); + return 1; + } + if (!cmp_msg(msg, msg2, msg_len, i)) { + return 1; + } + } + Pm_QueueDestroy(queue); + } + return 0; +} diff --git a/portmidi/pm_test/recvvirtual.c b/portmidi/pm_test/recvvirtual.c new file mode 100644 index 0000000..f8d9848 --- /dev/null +++ b/portmidi/pm_test/recvvirtual.c @@ -0,0 +1,175 @@ +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define INPUT_BUFFER_SIZE 100 +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define STRING_MAX 80 /* used for console input */ + +char *portname = "portmidi"; + +PmSysDepInfo *sysdepinfo = NULL; +char *port_name = "portmidi"; + +static void set_sysdepinfo(char m_or_p, const char *name) +{ + if (!sysdepinfo) { + // allocate some space we will alias with open-ended PmDriverInfo: + // there is space for 4 parameters: + static char dimem[sizeof(PmSysDepInfo) + sizeof(void *) * 8]; + sysdepinfo = (PmSysDepInfo *) dimem; + // build the driver info structure: + sysdepinfo->structVersion = PM_SYSDEPINFO_VERS; + sysdepinfo->length = 0; + } + if (sysdepinfo->length > 1) { + printf("Error: sysdepinfo was allocated to hold 2 parameters\n"); + exit(1); + } + int i = sysdepinfo->length++; + enum PmSysDepPropertyKey k = pmKeyNone; + if (m_or_p == 'm') k = pmKeyCoreMidiManufacturer; + else if (m_or_p == 'p') k = pmKeyAlsaPortName; + else if (m_or_p == 'c') k = pmKeyAlsaClientName; + sysdepinfo->properties[i].key = k; + sysdepinfo->properties[i].value = name; +} + + +static void prompt_and_exit(void) +{ + printf("type ENTER..."); + while (getchar() != '\n') ; + /* this will clean up open ports: */ + exit(-1); +} + + +static PmError checkerror(PmError err) +{ + if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + char errmsg[80]; + Pm_GetHostErrorText(errmsg, 80); + printf("PortMidi found host error...\n %s\n", errmsg); + prompt_and_exit(); + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + prompt_and_exit(); + } + return err; +} + + +void main_test_input(int num) +{ + PmStream *midi; + PmError status, length; + PmEvent buffer[1]; + int id; + int i = 0; /* count messages as they arrive */ + /* It is recommended to start timer before Midi; otherwise, PortMidi may + start the timer with its (default) parameters + */ + TIME_START; + + /* create a virtual input device */ + id = checkerror(Pm_CreateVirtualInput(port_name, NULL, sysdepinfo)); + checkerror(Pm_OpenInput(&midi, id, sysdepinfo, 0, NULL, NULL)); + + printf("Midi Input opened. Reading %d Midi messages...\n", num); + Pm_SetFilter(midi, PM_FILT_ACTIVE | PM_FILT_CLOCK | PM_FILT_SYSEX); + /* empty the buffer after setting filter, just in case anything + got through */ + while (Pm_Poll(midi)) { + Pm_Read(midi, buffer, 1); + } + /* now start paying attention to messages */ + while (i < num) { + status = Pm_Poll(midi); + if (status == TRUE) { + length = Pm_Read(midi, buffer, 1); + if (length > 0) { + printf("Got message %d: time %ld, %2lx %2lx %2lx\n", + i, + (long) buffer[0].timestamp, + (long) Pm_MessageStatus(buffer[0].message), + (long) Pm_MessageData1(buffer[0].message), + (long) Pm_MessageData2(buffer[0].message)); + i++; + } else { + assert(0); + } + } + } + + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close..."); + Pm_Close(midi); + printf("done closing.\nNow delete the virtual device..."); + checkerror(Pm_DeleteVirtualDevice(id)); + printf("done deleting.\n"); +} + + +void show_usage() +{ + printf("Usage: recvvirtual [-h] [-m manufacturer] [-c clientname] " + "[-p portname] [n]\n" + " -h for this message,\n" + " -m name designates a manufacturer name (macOS only),\n" + " -c name designates a client name (linux only),\n" + " -p name designates a port name (linux only),\n" + " n is number of message to wait for.\n"); + exit(0); +} + + +int main(int argc, char *argv[]) +{ + char line[STRING_MAX]; + int num = 10; + int i; + if (argc <= 1) { + show_usage(); + } + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + } else if (strcmp(argv[i], "-m") == 0 && (i + 1 < argc)) { + i = i + 1; + set_sysdepinfo('m', argv[i]); + printf("Manufacturer name will be %s\n", argv[i]); + } else if (strcmp(argv[i], "-p") == 0 && (i + 1 < argc)) { + i = i + 1; + port_name = argv[i]; + set_sysdepinfo('p', port_name); + printf("Port name will be %s\n", port_name); + } else if (strcmp(argv[i], "-c") == 0 && (i + 1 < argc)) { + i = i + 1; + set_sysdepinfo('c', argv[i]); + printf("Client name will be %s\n", argv[i]); + } else { + num = atoi(argv[i]); + if (num <= 0) { + printf("Zero value or non-number for n\n"); + show_usage(); + } + printf("Waiting for %d messages.\n", num); + } + } + + main_test_input(num); + + printf("finished portMidi test...type ENTER to quit..."); + while (getchar() != '\n') ; + return 0; +} diff --git a/portmidi/pm_test/sendvirtual.c b/portmidi/pm_test/sendvirtual.c new file mode 100644 index 0000000..a60a48f --- /dev/null +++ b/portmidi/pm_test/sendvirtual.c @@ -0,0 +1,194 @@ +/* sendvirtual.c -- test for creating a virtual device and sending to it */ +/* + * Roger B. Dannenberg + * Sep 2021 + */ +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define OUTPUT_BUFFER_SIZE 0 +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +int latency = 0; +PmSysDepInfo *sysdepinfo = NULL; +char *port_name = "portmidi"; + +static void set_sysdepinfo(char m_or_p, const char *name) +{ + if (!sysdepinfo) { + // allocate some space we will alias with open-ended PmDriverInfo: + // there is space for 4 parameters: + static char dimem[sizeof(PmSysDepInfo) + sizeof(void *) * 8]; + sysdepinfo = (PmSysDepInfo *) dimem; + // build the driver info structure: + sysdepinfo->structVersion = PM_SYSDEPINFO_VERS; + sysdepinfo->length = 0; + } + if (sysdepinfo->length > 1) { + printf("Error: sysdepinfo was allocated to hold 2 parameters\n"); + exit(1); + } + int i = sysdepinfo->length++; + enum PmSysDepPropertyKey k = pmKeyNone; + if (m_or_p == 'm') k = pmKeyCoreMidiManufacturer; + else if (m_or_p == 'p') k = pmKeyAlsaPortName; + else if (m_or_p == 'c') k = pmKeyAlsaClientName; + sysdepinfo->properties[i].key = k; + sysdepinfo->properties[i].value = name; +} + + +static void prompt_and_exit(void) +{ + printf("type ENTER..."); + while (getchar() != '\n') ; + /* this will clean up open ports: */ + exit(-1); +} + + +static PmError checkerror(PmError err) +{ + if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + char errmsg[80]; + Pm_GetHostErrorText(errmsg, 80); + printf("PortMidi found host error...\n %s\n", errmsg); + prompt_and_exit(); + } else if (err < 0) { + printf("PortMidi call failed...\n %s\n", Pm_GetErrorText(err)); + prompt_and_exit(); + } + return err; +} + + +void wait_until(PmTimestamp when) +{ + PtTimestamp now = Pt_Time(); + if (when > now) { + Pt_Sleep(when - now); + } +} + + +void main_test_output(int num) +{ + PmStream *midi; + int32_t next_time; + PmEvent buffer[1]; + PmTimestamp timestamp; + int pitch = 60; + int id; + + /* It is recommended to start timer before Midi; otherwise, PortMidi may + start the timer with its (default) parameters + */ + TIME_START; + + /* create a virtual output device */ + id = checkerror(Pm_CreateVirtualOutput(port_name, NULL, sysdepinfo)); + checkerror(Pm_OpenOutput(&midi, id, sysdepinfo, OUTPUT_BUFFER_SIZE, + TIME_PROC, TIME_INFO, latency)); + + printf("Midi Output Virtual Device \"%s\" created.\n", port_name); + printf("Type ENTER to send messages: "); + while (getchar() != '\n') ; + + buffer[0].timestamp = Pt_Time(); +#define PROGRAM 0 + buffer[0].message = Pm_Message(0xC0, PROGRAM, 0); + Pm_Write(midi, buffer, 1); + next_time = Pt_Time() + 1000; /* wait 1s */ + while (num > 0) { + wait_until(next_time); + Pm_WriteShort(midi, next_time, Pm_Message(0x90, pitch, 100)); + printf("Note On pitch %d\n", pitch); + num--; + next_time += 500; + + wait_until(next_time); + Pm_WriteShort(midi, next_time, Pm_Message(0x90, pitch, 0)); + printf("Note Off pitch %d\n", pitch); + num--; + pitch = (pitch + 1) % 12 + 60; + next_time += 500; + } + + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close..."); + Pm_Close(midi); + printf("done closing.\nNow delete the virtual device..."); + checkerror(Pm_DeleteVirtualDevice(id)); + printf("done deleting.\n"); +} + + +void show_usage() +{ + printf("Usage: sendvirtual [-h] [-l latency-in-ms] [-m manufacturer] " + "[-c clientname] [-p portname] [n]\n" + " -h for this message,\n" + " -l ms designates latency for precise timing (default 0),\n" + " -m name designates a manufacturer name (macOS only),\n" + " -c name designates a client name (linux only),\n" + " -p name designates a port name (linux only),\n" + " n is number of message to send.\n" + "sends change program to 1, then one note per second with 0.5s on,\n" + "0.5s off, for n/2 seconds. Latency >0 uses the device driver for \n" + "precise timing (see PortMidi documentation).\n"); + exit(0); +} + + +int main(int argc, char *argv[]) +{ + int num = 10; + int i; + if (argc <= 1) { + show_usage(); + } + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + } else if (strcmp(argv[i], "-l") == 0 && (i + 1 < argc)) { + i = i + 1; + latency = atoi(argv[i]); + printf("Latency will be %d\n", latency); + } else if (strcmp(argv[i], "-m") == 0 && (i + 1 < argc)) { + i = i + 1; + set_sysdepinfo('m', argv[i]); + printf("Manufacturer name will be %s\n", argv[i]); + } else if (strcmp(argv[i], "-p") == 0 && (i + 1 < argc)) { + i = i + 1; + port_name = argv[i]; + set_sysdepinfo('p', port_name); + printf("Port name will be %s\n", port_name); + } else if (strcmp(argv[i], "-c") == 0 && (i + 1 < argc)) { + i = i + 1; + set_sysdepinfo('c', argv[i]); + printf("Client name will be %s\n", argv[i]); + } else { + num = atoi(argv[i]); + if (num <= 0) { + printf("Zero value or non-number for n\n"); + show_usage(); + } + printf("Sending %d messages.\n", num); + } + } + + main_test_output(num); + + printf("finished sendvirtual test...type ENTER to quit..."); + while (getchar() != '\n') ; + return 0; +} diff --git a/portmidi/pm_test/sysex.c b/portmidi/pm_test/sysex.c new file mode 100755 index 0000000..c2c7187 --- /dev/null +++ b/portmidi/pm_test/sysex.c @@ -0,0 +1,556 @@ +/* sysex.c -- example program showing how to send and receive sysex
+ messages
+
+ Messages are stored in a file using 2-digit hexadecimal numbers,
+ one per byte, separated by blanks, with up to 32 numbers per line:
+ F0 14 A7 4B ...
+
+ */
+
+#include "stdio.h"
+#include "stdlib.h"
+#include "assert.h"
+#include "portmidi.h"
+#include "porttime.h"
+#include "string.h"
+#ifdef WIN32
+// need to get declaration for Sleep()
+#include "windows.h"
+#else
+#include <unistd.h>
+#define Sleep(n) usleep(n * 1000)
+#endif
+
+// enable some extra printing
+#ifndef VERBOSE
+#define VERBOSE 0
+#endif
+
+#define MIDI_SYSEX 0xf0
+#define MIDI_EOX 0xf7
+
+#define STRING_MAX 80
+
+#ifndef true
+#define true 1
+#define false 0
+#endif
+
+int latency = 0;
+
+/* read a number from console */
+/**/
+int get_number(const char *prompt)
+{
+ int n = 0, i;
+ fputs(prompt, stdout);
+ while (n != 1) {
+ n = scanf("%d", &i);
+ while (getchar() != '\n') ;
+ }
+ return i;
+}
+
+
+/* loopback test -- send/rcv from 2 to 1000 bytes of random midi data */
+/**/
+void loopback_test()
+{
+ int outp;
+ int inp;
+ PmStream *midi_in;
+ PmStream *midi_out;
+ unsigned char msg[1024];
+ int32_t len;
+ int i;
+ int data;
+ PmEvent event;
+ int shift;
+ long total_bytes = 0;
+ int32_t begin_time;
+
+ Pt_Start(1, 0, 0);
+
+ printf("Connect a midi cable from an output port to an input port.\n");
+ printf("This test will send random data via sysex message from output\n");
+ printf("to input and check that the correct data was received.\n");
+ outp = get_number("Type output device number: ");
+ /* Open output with 1ms latency -- when latency is non-zero, the Win32
+ implementation supports sending sysex messages incrementally in a
+ series of buffers. This is nicer than allocating a big buffer for the
+ message, and it also seems to work better. Either way works.
+ */
+ while ((latency = get_number(
+ "Latency in milliseconds (0 to send data immediatedly,\n"
+ " >0 to send timestamped messages): ")) < 0);
+ Pm_OpenOutput(&midi_out, outp, NULL, 0, NULL, NULL, latency);
+ inp = get_number("Type input device number: ");
+ /* since we are going to send and then receive, make sure the input buffer
+ is large enough for the entire message */
+ Pm_OpenInput(&midi_in, inp, NULL, 512, NULL, NULL);
+
+ srand((unsigned int) Pt_Time()); /* seed for random numbers */
+
+ begin_time = Pt_Time();
+ while (total_bytes < 100000) {
+ PmError count;
+ int32_t start_time;
+ int error_position = -1; /* 0; -1; -1 for continuous */
+ int expected = 0;
+ int actual = 0;
+ /* this modification will run until an error is detected */
+ /* set error_position above to 0 for interactive, -1 for */
+ /* continuous */
+ if (error_position >= 0) {
+ int c;
+ printf("Type return to send message, q to quit: ");
+ while ((c = getchar()) != '\n') {
+ if (c == 'q') goto cleanup;
+ }
+ }
+
+ /* compose the message */
+ len = rand() % 998 + 2; /* len only counts data bytes */
+ msg[0] = (char) MIDI_SYSEX; /* start of SYSEX message */
+ /* data bytes go from 1 to len */
+ for (i = 0; i < len; i++) {
+/* pick whether data is sequential or random... (docs say random) */
+#define DATA_EXPR (i+1)
+// #define DATA_EXPR rand()
+ msg[i + 1] = DATA_EXPR & 0x7f; /* MIDI data */
+ }
+ /* final EOX goes in len+1, total of len+2 bytes in msg */
+ msg[len + 1] = (char) MIDI_EOX;
+
+ /* sanity check: before we send, there should be no queued data */
+ count = Pm_Read(midi_in, &event, 1);
+
+ if (count != 0) {
+ printf("Before sending anything, a MIDI message was found in\n");
+ printf("the input buffer. Please try again.\n");
+ break;
+ }
+
+ /* send the message two ways: 1) Pm_WriteSysEx, 2) Pm_Write */
+ if (total_bytes & 1) {
+ printf("Sending %d byte sysex msg via Pm_WriteSysEx.\n", len + 2);
+ Pm_WriteSysEx(midi_out, 0, msg);
+ } else {
+ PmEvent event = {0, 0};
+ int bits = 0;
+ printf("Sending %d byte sysex msg via Pm_Write(s).\n", len + 2);
+ for (i = 0; i < len + 2; i++) {
+ event.message |= (msg[i] << bits);
+ bits += 8;
+ if (bits == 32) { /* full message - send it */
+ Pm_Write(midi_out, &event, 1);
+ bits = 0;
+ event.message = 0;
+ }
+ }
+ if (bits > 0) { /* last message is partially full */
+ Pm_Write(midi_out, &event, 1);
+ }
+ }
+
+ /* receive the message and compare to msg[] */
+ data = 0;
+ shift = 0;
+ i = 0;
+ start_time = Pt_Time();
+ if (VERBOSE) {
+ printf("start_time %d\n", start_time);
+ }
+ error_position = -1;
+ /* allow up to 2 seconds for transmission */
+ while (data != MIDI_EOX && start_time + 2000 > Pt_Time()) {
+ count = Pm_Read(midi_in, &event, 1);
+ if (count == 0) {
+ Sleep(1); /* be nice: give some CPU time to the system */
+ continue; /* continue polling for input */
+ }
+ if (VERBOSE) {
+ printf("read %08x ", event.message);
+ fflush(stdout);
+ }
+ /* compare 4 bytes of data until you reach an eox */
+ for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
+ data = (event.message >> shift) & 0xFF;
+ if (data != msg[i] && error_position < 0) {
+ error_position = i;
+ expected = msg[i];
+ actual = data;
+ }
+ i++;
+ }
+ }
+ if (error_position >= 0) {
+ printf("Error at time %d byte %d: sent %x recd %x.\n", Pt_Time(),
+ error_position, expected, actual);
+ break;
+ } else if (i != len + 2) {
+ printf("Error at time %d: byte %d not received.\n", Pt_Time(), i);
+ break;
+ } else {
+ int seconds = (Pt_Time() - begin_time) / 1000;
+ if (seconds == 0) seconds = 1;
+ printf("Correctly received %d byte sysex message.\n", i);
+ total_bytes += i;
+ printf("Cummulative bytes/sec: %d, %d%% done.\n",
+ (int) (total_bytes / seconds),
+ (int) (100 * total_bytes / 100000));
+ }
+ }
+cleanup:
+ Pm_Close(midi_out);
+ Pm_Close(midi_in);
+ return;
+}
+
+
+/* send_multiple test -- send many sysex messages */
+/**/
+void send_multiple_test()
+{
+ int outp;
+ int length;
+ int num_msgs;
+ PmStream *midi_out;
+ unsigned char msg[1024];
+ int i;
+ PtTimestamp start_time;
+ PtTimestamp stop_time;
+
+ Pt_Start(1, 0, 0);
+
+ printf("This is for performance testing. You should be sending to this\n");
+ printf("program running the receive multiple test. Do NOT send to\n");
+ printf("a synthesizer or you risk reprogramming it\n");
+ outp = get_number("Type output device number: ");
+ while ((latency = get_number(
+ "Latency in milliseconds (0 to send data immediatedly,\n"
+ " >0 to send timestamped messages): ")) < 0);
+ Pm_OpenOutput(&midi_out, outp, NULL, 0, NULL, NULL, latency);
+ while ((length = get_number("Message length (7 - 1024): ")) < 7 ||
+ length > 1024) ;
+ while ((num_msgs = get_number("Number of messages: ")) < 1);
+ /* latency, length, and num_msgs should now all be valid */
+ /* compose the message except for sequence number in first 5 bytes */
+ msg[0] = (char) MIDI_SYSEX;
+ for (i = 6; i < length - 1; i++) {
+ msg[i] = i % 128; /* this is just filler */
+ }
+ msg[length - 1] = (char) MIDI_EOX;
+
+ start_time = Pt_Time();
+ /* send the messages */
+ for (i = num_msgs; i > 0; i--) {
+ /* insert sequence number into first 5 data bytes */
+ /* sequence counts down to zero */
+ int j;
+ int count = i;
+ /* 7 bits of message count i goes into each data byte */
+ for (j = 1; j <= 5; j++) {
+ msg[j] = count & 127;
+ count >>= 7;
+ }
+ /* send the message */
+ Pm_WriteSysEx(midi_out, 0, msg);
+ }
+ stop_time = Pt_Time();
+ Pm_Close(midi_out);
+ return;
+}
+
+#define MAX_MSG_LEN 1024
+static unsigned char receive_msg[MAX_MSG_LEN];
+static int receive_msg_index;
+static int receive_msg_length;
+static int receive_msg_count;
+static int receive_msg_error;
+static int receive_msg_messages;
+static PmStream *receive_msg_midi_in;
+static int receive_poll_running;
+
+/* receive_poll -- callback function to check for midi input */
+/**/
+void receive_poll(PtTimestamp timestamp, void *userData)
+{
+ PmError count;
+ PmEvent event;
+ int shift;
+ int data = 0;
+ int i;
+
+ if (!receive_poll_running) return; /* wait until midi device is opened */
+ shift = 0;
+ while (data != MIDI_EOX) {
+ count = Pm_Read(receive_msg_midi_in, &event, 1);
+ if (count == 0) return;
+
+ /* compare 4 bytes of data until you reach an eox */
+ for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
+ receive_msg[receive_msg_index++] = data =
+ (event.message >> shift) & 0xFF;
+ if (receive_msg_index >= MAX_MSG_LEN) {
+ printf("error: incoming sysex too long\n");
+ goto error;
+ }
+ }
+ }
+ /* check the message */
+ if (receive_msg_length == 0) {
+ receive_msg_length = receive_msg_index;
+ }
+ if (receive_msg_length != receive_msg_index) {
+ printf("error: incoming sysex wrong length\n");
+ goto error;
+ }
+ if (receive_msg[0] != MIDI_SYSEX) {
+ printf("error: incoming sysex missing status byte\n");
+ goto error;
+ }
+ /* get and check the count */
+ count = 0;
+ for (i = 0; i < 5; i++) {
+ count += receive_msg[i + 1] << (7 * i);
+ }
+ if (receive_msg_count == -1) {
+ receive_msg_count = count;
+ receive_msg_messages = count;
+ }
+ if (receive_msg_count != count) {
+ printf("error: incoming sysex has wrong count\n");
+ goto error;
+ }
+ for (i = 6; i < receive_msg_index - 1; i++) {
+ if (receive_msg[i] != i % 128) {
+ printf("error: incoming sysex has bad data\n");
+ goto error;
+ }
+ }
+ if (receive_msg[receive_msg_length - 1] != MIDI_EOX) goto error;
+ receive_msg_index = 0; /* get ready for next message */
+ receive_msg_count--;
+ return;
+ error:
+ receive_msg_error = 1;
+ return;
+}
+
+
+/* receive_multiple_test -- send/rcv from 2 to 1000 bytes of random midi data */
+/**/
+void receive_multiple_test()
+{
+ PmError err;
+ int inp;
+
+ printf("This test expects to receive data sent by the send_multiple test\n");
+ printf("The test will check that correct data is received.\n");
+
+ /* Important: start PortTime first -- if it is not started first, it will
+ be started by PortMidi, and then our attempt to open again will fail */
+ receive_poll_running = false;
+ if ((err = Pt_Start(1, receive_poll, 0))) {
+ printf("PortTime error code: %d\n", err);
+ goto cleanup;
+ }
+ inp = get_number("Type input device number: ");
+ Pm_OpenInput(&receive_msg_midi_in, inp, NULL, 512, NULL, NULL);
+ receive_msg_index = 0;
+ receive_msg_length = 0;
+ receive_msg_count = -1;
+ receive_msg_error = 0;
+ receive_poll_running = true;
+ while ((!receive_msg_error) && (receive_msg_count != 0)) {
+#ifdef WIN32
+ Sleep(1000);
+#else
+ sleep(1); /* block and wait */
+#endif
+ }
+ if (receive_msg_error) {
+ printf("Receive_multiple test encountered an error\n");
+ } else {
+ printf("Receive_multiple test successfully received %d sysex messages\n",
+ receive_msg_messages);
+ }
+cleanup:
+ receive_poll_running = false;
+ Pm_Close(receive_msg_midi_in);
+ Pt_Stop();
+ return;
+}
+
+
+#define is_real_time_msg(msg) ((0xF0 & Pm_MessageStatus(msg)) == 0xF8)
+
+
+void receive_sysex()
+{
+ char line[80];
+ FILE *f;
+ PmStream *midi;
+ int shift = 0;
+ int data = 0;
+ int bytes_on_line = 0;
+ PmEvent msg;
+
+ /* determine which output device to use */
+ int i = get_number("Type input device number: ");
+
+ /* open input device */
+ Pm_OpenInput(&midi, i, NULL, 512, NULL, NULL);
+ printf("Midi Input opened, type file for sysex data: ");
+
+ /* open file */
+ if (!fgets(line, STRING_MAX, stdin)) return; /* no more stdin? */
+ /* remove the newline character */
+ if (strlen(line) > 0) line[strlen(line) - 1] = 0;
+ f = fopen(line, "w");
+ if (!f) {
+ printf("Could not open %s\n", line);
+ Pm_Close(midi);
+ return;
+ }
+
+ printf("Ready to receive a sysex message\n");
+
+ /* read data and write to file */
+ while (data != MIDI_EOX) {
+ PmError count;
+ count = Pm_Read(midi, &msg, 1);
+ /* CAUTION: this causes busy waiting. It would be better to
+ be in a polling loop to avoid being compute bound. PortMidi
+ does not support a blocking read since this is so seldom
+ useful.
+ */
+ if (count == 0) continue;
+ /* ignore real-time messages */
+ if (is_real_time_msg(Pm_MessageStatus(msg.message))) continue;
+
+ /* write 4 bytes of data until you reach an eox */
+ for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
+ data = (msg.message >> shift) & 0xFF;
+ /* if this is a status byte that's not MIDI_EOX, the sysex
+ message is incomplete and there is no more sysex data */
+ if (data & 0x80 && data != MIDI_EOX) break;
+ fprintf(f, "%2x ", data);
+ if (++bytes_on_line >= 16) {
+ fprintf(f, "\n");
+ bytes_on_line = 0;
+ }
+ }
+ }
+ fclose(f);
+ Pm_Close(midi);
+}
+
+
+void send_sysex()
+{
+ char line[80];
+ FILE *f;
+ PmStream *midi;
+ int data;
+ int shift = 0;
+ PmEvent msg;
+
+ /* determine which output device to use */
+ int i = get_number("Type output device number: ");
+ while ((latency = get_number(
+ "Latency in milliseconds (0 to send data immediatedly,\n"
+ " >0 to send timestamped messages): ")) < 0);
+
+ msg.timestamp = 0; /* no need for timestamp */
+
+ /* open output device */
+ Pm_OpenOutput(&midi, i, NULL, 0, NULL, NULL, latency);
+ printf("Midi Output opened, type file with sysex data: ");
+
+ /* open file */
+ if (!fgets(line, STRING_MAX, stdin)) return; /* no more stdin? */
+ /* remove the newline character */
+ if (strlen(line) > 0) line[strlen(line) - 1] = 0;
+ f = fopen(line, "r");
+ if (!f) {
+ printf("Could not open %s\n", line);
+ Pm_Close(midi);
+ return;
+ }
+
+ /* read file and send data */
+ msg.message = 0;
+ while (1) {
+ /* get next byte from file */
+
+ if (fscanf(f, "%x", &data) == 1) {
+ /* printf("read %x, ", data); */
+ /* OR byte into message at proper offset */
+ msg.message |= (data << shift);
+ shift += 8;
+ }
+ /* send the message if it's full (shift == 32) or if we are at end */
+ if (shift == 32 || data == MIDI_EOX) {
+ /* this will send sysex data 4 bytes at a time -- it would
+ be much more efficient to send multiple PmEvents at once
+ but this method is simpler. See Pm_WriteSysEx for a more
+ efficient code example.
+ */
+ Pm_Write(midi, &msg, 1);
+ msg.message = 0;
+ shift = 0;
+ }
+ if (data == MIDI_EOX) { /* end of message */
+ fclose(f);
+ Pm_Close(midi);
+ return;
+ }
+ }
+}
+
+
+int main()
+{
+ int i;
+
+ /* list device information */
+ for (i = 0; i < Pm_CountDevices(); i++) {
+ const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
+ printf("%d: %s, %s", i, info->interf, info->name);
+ if (info->input) printf(" (input)");
+ if (info->output) printf(" (output)");
+ printf("\n");
+ }
+ while (1) {
+ char cmd;
+ printf("Type r to receive sysex, s to send,"
+ " l for loopback test, m to send multiple,"
+ " n to receive multiple, q to quit: ");
+ cmd = getchar();
+ while (getchar() != '\n') ;
+ switch (cmd) {
+ case 'r':
+ receive_sysex();
+ break;
+ case 's':
+ send_sysex();
+ break;
+ case 'l':
+ loopback_test();
+ break;
+ case 'm':
+ send_multiple_test();
+ break;
+ case 'n':
+ receive_multiple_test();
+ break;
+ case 'q':
+ exit(0);
+ default:
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/portmidi/pm_test/testio.c b/portmidi/pm_test/testio.c new file mode 100755 index 0000000..2711286 --- /dev/null +++ b/portmidi/pm_test/testio.c @@ -0,0 +1,594 @@ +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define INPUT_BUFFER_SIZE 100 +#define OUTPUT_BUFFER_SIZE 0 +#define TIME_PROC ((int32_t (*)(void *)) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + +#define WAIT_FOR_ENTER while (getchar() != '\n') ; + +int32_t latency = 0; +int verbose = FALSE; +PmSysDepInfo *sysdepinfo = NULL; + +/* crash the program to test whether midi ports are closed */ +/**/ +void doSomethingReallyStupid() { + int * tmp = NULL; + *tmp = 5; +} + + +/* exit the program without any explicit cleanup */ +/**/ +void doSomethingStupid() { + assert(0); +} + + +/* read a number from console */ +/**/ +int get_number(const char *prompt) +{ + int n = 0, i; + fputs(prompt, stdout); + while (n != 1) { + n = scanf("%d", &i); + WAIT_FOR_ENTER + } + return i; +} + + +static void set_sysdepinfo(char m_or_p, const char *name) +{ + if (!sysdepinfo) { + // allocate some space we will alias with open-ended PmDriverInfo: + // there is space for 4 parameters: + static char dimem[sizeof(PmSysDepInfo) + sizeof(void *) * 8]; + sysdepinfo = (PmSysDepInfo *) dimem; + // build the driver info structure: + sysdepinfo->structVersion = PM_SYSDEPINFO_VERS; + sysdepinfo->length = 0; + } + if (sysdepinfo->length > 1) { + printf("Error: sysdepinfo was allocated to hold 2 parameters\n"); + exit(1); + } + int i = sysdepinfo->length++; + enum PmSysDepPropertyKey k = pmKeyNone; + if (m_or_p == 'm') k = pmKeyCoreMidiManufacturer; + else if (m_or_p == 'p') k = pmKeyAlsaPortName; + else if (m_or_p == 'c') k = pmKeyAlsaClientName; + sysdepinfo->properties[i].key = k; + sysdepinfo->properties[i].value = name; +} + + +/* + * the somethingStupid parameter can be set to simulate a program crash. + * We want PortMidi to close Midi ports automatically in the event of a + * crash because Windows does not (and this may cause an OS crash) + */ +void main_test_input(unsigned int somethingStupid) { + PmStream * midi; + PmError status, length; + PmEvent buffer[1]; + int num = 10; + int i = get_number("Type input number: "); + /* It is recommended to start timer before Midi; otherwise, PortMidi may + start the timer with its (default) parameters + */ + TIME_START; + + /* open input device */ + Pm_OpenInput(&midi, + i, + sysdepinfo, + INPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO); + + printf("Midi Input opened. Reading %d Midi messages...\n", num); + Pm_SetFilter(midi, PM_FILT_ACTIVE | PM_FILT_CLOCK | PM_FILT_SYSEX); + /* empty the buffer after setting filter, just in case anything + got through */ + while (Pm_Poll(midi)) { + Pm_Read(midi, buffer, 1); + } + /* now start paying attention to messages */ + i = 0; /* count messages as they arrive */ + while (i < num) { + status = Pm_Poll(midi); + if (status == TRUE) { + length = Pm_Read(midi, buffer, 1); + if (length > 0) { + printf("Got message %d @ time %ld: timestamp %ld, " + "%2lx %2lx %2lx\n", i, (long) Pt_Time(), + (long) buffer[0].timestamp, + (long) Pm_MessageStatus(buffer[0].message), + (long) Pm_MessageData1(buffer[0].message), + (long) Pm_MessageData2(buffer[0].message)); + i++; + } else { + assert(0); + } + } + /* simulate crash if somethingStupid is 1 or 2 */ + if ((i > (num/2)) && (somethingStupid == 1)) { + doSomethingStupid(); + } else if ((i > (num/2)) && (somethingStupid == 2)) { + doSomethingReallyStupid(); + } + } + + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close..."); + + Pm_Close(midi); + printf("done closing..."); +} + + + +void main_test_output(int isochronous_test) +{ + PmStream * midi; + int32_t off_time; + int chord[] = { 60, 67, 76, 83, 90 }; + #define chord_size 5 + PmEvent buffer[chord_size]; + PmTimestamp timestamp; + + /* determine which output device to use */ + int i = get_number("Type output number: "); + + /* It is recommended to start timer before PortMidi */ + TIME_START; + + /* open output device -- since PortMidi avoids opening a timer + when latency is zero, we will pass in a NULL timer pointer + for that case. If PortMidi tries to access the time_proc, + we will crash, so this test will tell us something. */ + Pm_OpenOutput(&midi, + i, + sysdepinfo, + OUTPUT_BUFFER_SIZE, + (latency == 0 ? NULL : TIME_PROC), + (latency == 0 ? NULL : TIME_INFO), + latency); + printf("Midi Output opened with %ld ms latency.\n", (long) latency); + + /* output note on/off w/latency offset; hold until user prompts */ + printf("ready to send program 1 change... (type ENTER):"); + WAIT_FOR_ENTER + /* if we were writing midi for immediate output, we could always use + timestamps of zero, but since we may be writing with latency, we + will explicitly set the timestamp to "now" by getting the time. + The source of timestamps should always correspond to the TIME_PROC + and TIME_INFO parameters used in Pm_OpenOutput(). */ + buffer[0].timestamp = Pt_Time(); + /* Send a program change to increase the chances we will hear notes */ + /* Program 0 is usually a piano, but you can change it here: */ +#define PROGRAM 0 + buffer[0].message = Pm_Message(0xC0, PROGRAM, 0); + Pm_Write(midi, buffer, 1); + + if (isochronous_test) { // play 4 notes per sec for 20s + int count; + PmTimestamp start; + if (latency < 100) { + printf("Warning: latency < 100, but this test sends messages" + " at times that are jittered by up to 100ms, so you" + " may hear uneven timing\n"); + } + printf("Starting in 1s..."); fflush(stdout); + Pt_Sleep(1000); + start = Pt_Time(); + for (count = 0; count < 80; count++) { + PmTimestamp next_time; + buffer[0].timestamp = start + count * 250; + buffer[0].message = Pm_Message(0x90, 69, 100); + buffer[1].timestamp = start + count * 250 + 200; + buffer[1].message = Pm_Message(0x90, 69, 0); + Pm_Write(midi, buffer, 2); + next_time = start + (count + 1) * 250; + // sleep for a random time up to 100ms to add jitter to + // the times at which we send messages. PortMidi timing + // should remove the jitter if latency > 100 + while (Pt_Time() < next_time) { + Pt_Sleep(rand() % 100); + } + } + printf("Done sending 80 notes at 4 notes per second.\n"); + } else { + PmError err = 0; + printf("ready to note-on... (type ENTER):"); + WAIT_FOR_ENTER + buffer[0].timestamp = Pt_Time(); + buffer[0].message = Pm_Message(0x90, 60, 100); + if ((err = Pm_Write(midi, buffer, 1))) { + printf("Pm_Write returns error: %d (%s)\n", + err, Pm_GetErrorText(err)); + if (err == pmHostError) { + char errmsg[128]; + Pm_GetHostErrorText(errmsg, 127); + printf(" Host error: %s\n", errmsg); + } + } + printf("ready to note-off... (type ENTER):"); + WAIT_FOR_ENTER + buffer[0].timestamp = Pt_Time(); + buffer[0].message = Pm_Message(0x90, 60, 0); + if ((err = Pm_Write(midi, buffer, 1))) { + printf("Pm_Write returns error: %d (%s)\n", + err, Pm_GetErrorText(err)); + if (err == pmHostError) { + char errmsg[128]; + Pm_GetHostErrorText(errmsg, 127); + printf(" Host error: %s\n", errmsg); + } + } + + /* output short note on/off w/latency offset; hold until user prompts */ + printf("ready to note-on (short form)... (type ENTER):"); + WAIT_FOR_ENTER + Pm_WriteShort(midi, Pt_Time(), + Pm_Message(0x90, 60, 100)); + printf("ready to note-off (short form)... (type ENTER):"); + WAIT_FOR_ENTER + Pm_WriteShort(midi, Pt_Time(), + Pm_Message(0x90, 60, 0)); + + /* output several note on/offs to test timing. + Should be 1s between notes */ + if (latency == 0) { + printf("chord should not arpeggiate, latency == 0\n"); + } else { + printf("chord should arpeggiate (latency = %ld > 0\n", + (long) latency); + } + printf("ready to chord-on/chord-off... (type ENTER):"); + WAIT_FOR_ENTER + timestamp = Pt_Time(); + printf("starting timestamp %ld\n", (long) timestamp); + for (i = 0; i < chord_size; i++) { + buffer[i].timestamp = timestamp + 1000 * i; + buffer[i].message = Pm_Message(0x90, chord[i], 100); + } + Pm_Write(midi, buffer, chord_size); + + off_time = timestamp + 1000 + chord_size * 1000; + while (Pt_Time() < off_time) + /* There was a report that Pm_Write with zero length sent last + * message again, so call Pm_Write here to see if note repeats + */ + Pm_Write(midi, buffer, 0); + Pt_Sleep(20); /* wait */ + + for (i = 0; i < chord_size; i++) { + buffer[i].timestamp = timestamp + 1000 * i; + buffer[i].message = Pm_Message(0x90, chord[i], 0); + } + Pm_Write(midi, buffer, chord_size); + } + + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close and terminate... (type ENTER):"); + WAIT_FOR_ENTER + + Pm_Close(midi); + Pm_Terminate(); + printf("done closing and terminating...\n"); +} + + +void main_test_both() +{ + int i = 0; + int in, out; + PmStream * midi, * midiOut; + PmEvent buffer[1]; + PmError status, length; + int num = 11; + + in = get_number("Type input number: "); + out = get_number("Type output number: "); + + /* In is recommended to start timer before PortMidi */ + TIME_START; + + Pm_OpenOutput(&midiOut, + out, + sysdepinfo, + OUTPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO, + latency); + printf("Midi Output opened with %ld ms latency.\n", (long) latency); + /* open input device */ + Pm_OpenInput(&midi, + in, + sysdepinfo, + INPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO); + printf("Midi Input opened. Reading %d Midi messages...\n", num); + Pm_SetFilter(midi, PM_FILT_ACTIVE | PM_FILT_CLOCK); + /* empty the buffer after setting filter, just in case anything + got through */ + while (Pm_Poll(midi)) { + Pm_Read(midi, buffer, 1); + } + i = 0; + while (i < num) { + status = Pm_Poll(midi); + if (status == TRUE) { + length = Pm_Read(midi,buffer,1); + if (length > 0) { + Pm_Write(midiOut, buffer, 1); + printf("Got message %d @ time %ld: timestamp %ld, " + "%2lx %2lx %2lx\n", i, (long) Pt_Time(), + (long) buffer[0].timestamp, + (long) Pm_MessageStatus(buffer[0].message), + (long) Pm_MessageData1(buffer[0].message), + (long) Pm_MessageData2(buffer[0].message)); + i++; + } else { + assert(0); + } + } + } + /* allow time for last message to go out */ + Pt_Sleep(100 + latency); + + /* close midi devices */ + Pm_Close(midi); + Pm_Close(midiOut); + Pm_Terminate(); +} + + +/* main_test_stream exercises windows winmm API's stream mode */ +/* The winmm stream mode is used for latency>0, and sends + timestamped messages. The timestamps are relative (delta) + times, whereas PortMidi times are absolute. Since peculiar + things happen when messages are not always sent in advance, + this function allows us to exercise the system and test it. + */ +void main_test_stream() { + PmStream * midi; + PmEvent buffer[16]; + + /* determine which output device to use */ + int i = get_number("Type output number: "); + + latency = 500; /* ignore LATENCY for this test and + fix the latency at 500ms */ + + /* It is recommended to start timer before PortMidi */ + TIME_START; + + /* open output device */ + Pm_OpenOutput(&midi, + i, + sysdepinfo, + OUTPUT_BUFFER_SIZE, + TIME_PROC, + TIME_INFO, + latency); + printf("Midi Output opened with %ld ms latency.\n", (long) latency); + + /* output note on/off w/latency offset; hold until user prompts */ + printf("ready to send output... (type ENTER):"); + WAIT_FOR_ENTER + + /* if we were writing midi for immediate output, we could always use + timestamps of zero, but since we may be writing with latency, we + will explicitly set the timestamp to "now" by getting the time. + The source of timestamps should always correspond to the TIME_PROC + and TIME_INFO parameters used in Pm_OpenOutput(). */ + buffer[0].timestamp = Pt_Time(); + buffer[0].message = Pm_Message(0xC0, 0, 0); + buffer[1].timestamp = buffer[0].timestamp; + buffer[1].message = Pm_Message(0x90, 60, 100); + buffer[2].timestamp = buffer[0].timestamp + 1000; + buffer[2].message = Pm_Message(0x90, 62, 100); + buffer[3].timestamp = buffer[0].timestamp + 2000; + buffer[3].message = Pm_Message(0x90, 64, 100); + buffer[4].timestamp = buffer[0].timestamp + 3000; + buffer[4].message = Pm_Message(0x90, 66, 100); + buffer[5].timestamp = buffer[0].timestamp + 4000; + buffer[5].message = Pm_Message(0x90, 60, 0); + buffer[6].timestamp = buffer[0].timestamp + 4000; + buffer[6].message = Pm_Message(0x90, 62, 0); + buffer[7].timestamp = buffer[0].timestamp + 4000; + buffer[7].message = Pm_Message(0x90, 64, 0); + buffer[8].timestamp = buffer[0].timestamp + 4000; + buffer[8].message = Pm_Message(0x90, 66, 0); + + Pm_Write(midi, buffer, 9); +#ifdef SEND8 + /* Now, we're ready for the real test. + Play 4 notes at now, now+500, now+1000, and now+1500 + Then wait until now+2000. + Play 4 more notes as before. + We should hear 8 evenly spaced notes. */ + now = Pt_Time(); + for (i = 0; i < 4; i++) { + buffer[i * 2].timestamp = now + (i * 500); + buffer[i * 2].message = Pm_Message(0x90, 60, 100); + buffer[i * 2 + 1].timestamp = now + 250 + (i * 500); + buffer[i * 2 + 1].message = Pm_Message(0x90, 60, 0); + } + Pm_Write(midi, buffer, 8); + + while (Pt_Time() < now + 2500) + Pt_Sleep(10); + /* now we are 500 ms behind schedule, but since the latency + is 500, the delay should not be audible */ + now += 2000; + for (i = 0; i < 4; i++) { + buffer[i * 2].timestamp = now + (i * 500); + buffer[i * 2].message = Pm_Message(0x90, 60, 100); + buffer[i * 2 + 1].timestamp = now + 250 + (i * 500); + buffer[i * 2 + 1].message = Pm_Message(0x90, 60, 0); + } + Pm_Write(midi, buffer, 8); +#endif + /* close device (this not explicitly needed in most implementations) */ + printf("ready to close and terminate... (type ENTER):"); + WAIT_FOR_ENTER + + Pm_Close(midi); + Pm_Terminate(); + printf("done closing and terminating...\n"); +} + + +void show_usage() +{ + printf("Usage: test [-h] [-l latency-in-ms] [-c clientname] " + "[-p portname] [-v]\n" + " -h for this help message (only)\n" + " -l for latency\n" + " -c name designates a client name (linux only),\n" + " -p name designates a port name (linux only),\n" + " -v for verbose (enables more output)\n"); +} + +int main(int argc, char *argv[]) +{ + int default_in; + int default_out; + int i = 0, n = 0; + int test_input = 0, test_output = 0, test_both = 0, somethingStupid = 0; + int isochronous_test = 0; + int stream_test = 0; + int latency_valid = FALSE; + + show_usage(); + if (sizeof(void *) == 8) + printf("Apparently this is a 64-bit machine.\n"); + else if (sizeof(void *) == 4) + printf ("Apparently this is a 32-bit machine.\n"); + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0) { + exit(0); + } else if (strcmp(argv[i], "-p") == 0 && (i + 1 < argc)) { + i = i + 1; + const char *port_name = argv[i]; + set_sysdepinfo('p', port_name); + printf("Port name will be %s\n", port_name); + } else if (strcmp(argv[i], "-c") == 0 && (i + 1 < argc)) { + i = i + 1; + set_sysdepinfo('c', argv[i]); + printf("Client name will be %s\n", argv[i]); + } else if (strcmp(argv[i], "-l") == 0 && (i + 1 < argc)) { + i = i + 1; + latency = atoi(argv[i]); + printf("Latency will be %ld\n", (long) latency); + latency_valid = TRUE; + } else if (strcmp(argv[i], "-v") == 0) { + printf("Verbose is now TRUE\n"); + verbose = TRUE; /* not currently used for anything */ + } else { + show_usage(); + exit(0); + } + } + + while (!latency_valid) { + int lat; // declared int to match "%d" + printf("Latency in ms: "); + if (scanf("%d", &lat) == 1) { + latency = (int32_t) lat; // coerce from "%d" to known size + latency_valid = TRUE; + } + } + + /* determine what type of test to run */ + printf("begin portMidi test...\n"); + printf("enter your choice...\n 1: test input\n" + " 2: test input (fail w/assert)\n" + " 3: test input (fail w/NULL assign)\n" + " 4: test output\n 5: test both\n" + " 6: stream test (for WinMM)\n" + " 7. isochronous out\n"); + while (n != 1) { + n = scanf("%d", &i); + WAIT_FOR_ENTER + switch(i) { + case 1: + test_input = 1; + break; + case 2: + test_input = 1; + somethingStupid = 1; + break; + case 3: + test_input = 1; + somethingStupid = 2; + break; + case 4: + test_output = 1; + break; + case 5: + test_both = 1; + break; + case 6: + stream_test = 1; + break; + case 7: + test_output = 1; + isochronous_test = 1; + break; + default: + printf("got %d (invalid input)\n", n); + break; + } + } + + /* list device information */ + default_in = Pm_GetDefaultInputDeviceID(); + default_out = Pm_GetDefaultOutputDeviceID(); + for (i = 0; i < Pm_CountDevices(); i++) { + char *deflt; + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (((test_input | test_both) & info->input) | + ((test_output | test_both | stream_test) & info->output)) { + printf("%d: %s, %s", i, info->interf, info->name); + if (info->input) { + deflt = (i == default_in ? "default " : ""); + printf(" (%sinput)", deflt); + } + if (info->output) { + deflt = (i == default_out ? "default " : ""); + printf(" (%soutput)", deflt); + } + printf("\n"); + } + } + + /* run test */ + if (stream_test) { + main_test_stream(); + } else if (test_input) { + main_test_input(somethingStupid); + } else if (test_output) { + main_test_output(isochronous_test); + } else if (test_both) { + main_test_both(); + } + + printf("finished portMidi test...type ENTER to quit..."); + WAIT_FOR_ENTER + return 0; +} diff --git a/portmidi/pm_test/txdata.syx b/portmidi/pm_test/txdata.syx new file mode 100755 index 0000000..1e06e5a --- /dev/null +++ b/portmidi/pm_test/txdata.syx @@ -0,0 +1,257 @@ +20 0 1d 4 c 6 0 34 1 4d 4 d 1f 7 3 6
+ c 5e 4 4d d b 18 5 3 6 0 3d 1 4a 16 18
+1f 8 3 6 d 0 1 63 4 13 3a 23 0 0 0 2
+ c 2 4 0 63 32 0 0 0 32 0 47 72 61 6e 64
+50 69 61 6e 6f 63 63 63 32 32 32 0 0 0 0 0
+10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 9 9 f c 27 2 35 37 10 1f 4 3 4
+ d 19 4 56 5 16 1f f 8 d c 0 43 60 4 e
+1f c 3 7 e 0 43 63 5 10 3c 14 8 2 1b 56
+ 5 2 4 0 63 32 0 0 0 32 0 4c 6f 54 69 6e
+65 38 31 5a 20 63 63 63 32 32 32 0 7f 0 1 0
+18 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f e f e 9 0 3 43 2d e 1f f 5 7
+ f 16 43 5a 0 0 1f 12 6 8 d 0 3 63 4 0
+1f 12 6 8 f 0 2 63 4 6 34 14 0 1 2 4e
+18 2 4 0 63 32 0 32 0 32 0 44 79 6e 6f 6d
+69 74 65 45 50 63 63 63 32 32 32 0 70 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f b 1 b 8 18 40 5f a e 1f 1f 0 a
+ f 0 40 5f 4 0 1f 1f 0 a f 0 40 63 5 6
+1f 1f 0 a f 0 40 5f 0 8 1f 20 0 3 0 5a
+18 4 4 0 63 32 32 0 0 32 0 50 65 72 63 4f
+72 67 61 6e 20 63 63 63 32 32 32 0 0 0 0 0
+ 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f b 7 f 9 0 4 49 13 13 1f 8 7 5
+ e 0 2 58 0 c 1f 6 4 6 f 23 3 46 10 a
+1f 7 8 c d 0 2 63 8 b 2 1c 0 0 0 52
+18 4 4 0 63 32 0 32 0 32 0 54 68 69 6e 20
+43 6c 61 76 20 63 63 63 32 32 32 0 70 0 20 0
+10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f c 0 6 1 a 4 50 20 e 1f c 0 6
+ 1 a 4 50 1f 8 1f b 9 5 e 0 2 63 5 e
+1f b 9 5 e 0 3 63 4 8 4 1a 0 0 0 52
+1d 2 4 0 63 32 0 32 0 32 0 42 72 69 74 65
+43 65 6c 73 74 63 63 63 32 32 32 0 20 0 26 0
+ 1 0 8 4 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 f 1f 4 8 f 0 3a 51 4 b e 1f 0 8
+ f 0 22 4b 4 3 f 1a b 8 d 0 3b 36 9 3
+12 1f 0 8 f 0 22 5d 4 b 3a 1e 19 5 0 52
+18 4 4 0 63 32 0 0 0 32 0 54 72 75 6d 70
+65 74 38 31 5a 63 63 63 32 32 32 0 0 0 50 0
+51 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 c 5 0 8 0 0 2 4a 4 b f 1f 0 8
+ f 0 2 3f 4 3 1f f 0 8 0 23 3 44 b 3
+10 1f 0 9 f 0 2 5e 4 c 3a 1f 19 7 0 52
+18 4 4 0 63 32 0 0 0 32 0 46 6c 75 67 65
+6c 68 6f 72 6e 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 10 1f 0 8 f 0 42 4a 0 3 11 1f 0 8
+ f a 43 51 0 3 11 9 0 8 d 0 42 2b 16 6
+10 1f 0 9 f 0 42 63 4 b 3a 1e 9 9 0 5a
+24 4 4 0 63 32 31 0 0 32 0 52 61 73 70 41
+6c 74 6f 20 20 63 63 63 32 32 32 0 10 0 20 0
+54 0 20 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 10 9 2 6 d 0 41 3e 4 15 c b 2 3
+ e 0 41 4f 4 12 c e 2 8 d 0 42 4b a 1c
+ d b 1 9 e 0 3 63 a 14 0 23 f 2 1b 5e
+18 4 5 0 63 28 50 32 0 32 0 48 61 72 6d 6f
+6e 69 63 61 20 63 63 63 32 32 32 0 50 10 50 0
+50 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1c 2 0 4 e 63 0 4e 4 3 d 5 0 6
+ e 63 1 56 a 8 12 7 0 6 9 63 2 47 1b e
+ a a 0 5 f 0 1 63 4 b 32 1a 8 d 0 52
+ c 4 4 0 63 32 0 0 0 32 0 44 6f 75 62 6c
+65 42 61 73 73 63 63 63 32 32 32 0 10 0 0 0
+ 3 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 b 4 0 4 f 14 2 49 9 6 a 7 0 4
+ f 14 2 51 a 0 8 1f 0 5 f 0 1 63 9 6
+ a 1f 0 5 f 0 1 63 a 0 3c 1f 6 9 0 52
+ 5 4 4 0 63 32 0 0 0 32 0 48 69 53 74 72
+69 6e 67 20 31 63 63 63 32 32 32 0 2 0 30 0
+32 0 10 5 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 10 13 f 4 a 0 3 3b 14 14 1f e 8 7
+ 9 0 2 42 5 e 18 13 d 9 c 0 2 3c 13 8
+1f 11 7 4 f 0 42 63 4 10 3a 1b 0 0 0 52
+1d 4 4 0 63 32 0 0 0 32 0 48 61 72 70 20
+20 20 20 20 20 63 63 63 32 32 32 8 0 0 21 0
+ 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 6 6 4 f 0 40 48 5 0 c 8 7 5
+ f 5 0 52 4 0 f 7 3 7 e 8 3 63 4 6
+ f 8 4 5 f 0 3 63 4 6 7c 1f 0 6 0 4a
+11 2 4 0 63 32 0 0 0 32 0 46 61 6e 66 61
+72 54 70 74 73 63 63 63 32 32 32 6 1 0 38 0
+ 8 0 48 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 d b 0 1 c 0 2 2c 3d 3 d 7 0 1
+ c 0 2 1f 3c 3 d 1f 0 5 f 0 2 63 5 6
+ d 1f 0 5 f 0 2 63 4 0 3c 63 0 2f 0 53
+11 4 4 0 63 32 0 0 0 32 0 42 72 65 61 74
+68 4f 72 67 6e 63 63 63 32 32 32 4 30 5 50 0
+11 0 18 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 9 0 6 0 27 2 51 19 b 1c 6 0 8
+ 0 37 2 47 a 3 1f a 0 9 0 3d 2 4d a e
+1f 12 8 8 f 0 3 61 4 b 28 1f 0 3 0 52
+ c 3 4 0 63 32 1 32 0 32 0 4e 79 6c 6f 6e
+47 75 69 74 20 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f e e f f 0 3 48 2d 6 1f f 4 f
+ f 25 3 5b 0 0 1f 12 6 c e 1c 3 55 0 10
+1f 13 7 8 e 6 4 62 4 e 3b 14 0 0 0 42
+18 2 4 0 63 32 0 32 0 32 0 47 75 69 74 61
+72 20 23 31 20 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 19 8 a 3 0 3 63 10 18 1f c 5 b
+ 5 0 3 52 0 b 1f 19 6 b 5 0 3 63 a 16
+1f f 11 9 7 0 4 63 4 3 3a 14 0 0 0 42
+18 2 4 0 63 32 0 32 0 32 0 46 75 6e 6b 79
+20 50 69 63 6b 63 63 63 32 32 32 0 30 0 0 0
+ 0 0 0 7 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 1 0 8 4 0 3 3d a 1e 1f 1 0 8
+ 0 0 0 43 0 10 1f 9 6 8 c 1b 7 46 1c 1e
+1f 9 0 9 9 0 1 63 4 3 3a 1c 0 0 0 52
+ c 4 5 0 63 4b 0 0 0 32 0 45 6c 65 63 42
+61 73 73 20 31 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f f f e 9 0 3 46 1d 16 1f f 5 e
+ e d 3 63 0 b 1f 13 6 5 d 1c 3 63 0 0
+1f 13 6 8 f 0 4 63 4 6 3b 1f 0 0 0 42
+ c 4 4 0 63 32 0 32 0 32 0 53 79 6e 46 75
+6e 6b 42 61 73 63 63 63 32 32 32 d 6c 0 0 0
+70 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 10 7 8 3 0 3 4f 4 3 1f 9 0 8
+ 0 0 1 4a 0 b 1f 11 0 8 0 0 1 47 4 8
+1f 9 0 8 0 0 0 63 0 b 39 19 0 7 0 52
+ c 2 4 0 63 32 0 32 0 32 0 4c 61 74 65 6c
+79 42 61 73 73 63 63 63 32 32 32 2 0 0 0 0
+40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 13 12 0 9 d 22 0 51 0 b 1f 14 0 5
+ 8 24 40 5c 0 3 1f 11 0 6 c 2c 0 53 9 0
+10 1f 0 b f 0 0 5c a e 3a 22 11 e 1e 5e
+18 7 4 0 63 32 0 32 0 32 0 53 79 6e 63 20
+4c 65 61 64 20 63 63 63 32 32 32 0 70 0 40 0
+ 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 13 1e 0 9 e 0 0 63 3f b 1f 14 0 5
+ e 24 1 51 4 3 1f 14 0 f 1 0 41 4d 8 3
+ f 1f 0 b f 0 2 63 4 b 3b 20 11 12 33 56
+18 4 4 0 63 37 e 0 0 32 0 4a 61 7a 7a 20
+46 6c 75 74 65 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 15 13 d 3 d 1e 2 50 18 e 15 14 9 4
+ c 1e 2 56 11 8 1b 1f f 7 f 0 1 63 4 6
+1a 1f e 6 f 0 2 63 4 0 7c b 0 8 0 62
+18 4 4 0 63 32 0 0 0 32 0 4a 61 76 61 20
+4a 69 76 65 20 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 0 0 4 f 0 40 63 3c 0 b 8 7 7
+ f 5 0 63 4 6 f 5 3 7 f 8 0 3b 5 6
+ e 8 4 5 f 0 3 63 3 0 7e 1d 6 f 0 4a
+11 0 4 0 63 32 0 0 0 32 0 42 61 61 64 42
+72 65 61 74 68 63 63 63 32 32 32 6 30 0 38 0
+ 1 0 46 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 0 0 4 f 0 40 47 2f 0 e 8 7 7
+ f 5 0 4c 0 6 13 1c d c 6 8 0 63 5 6
+14 11 d b 0 0 3 63 4 0 7a 10 0 51 0 68
+17 0 4 0 63 32 0 0 0 32 0 56 6f 63 61 6c
+4e 75 74 73 20 63 63 63 32 32 32 6 30 0 30 0
+ 1 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 1f 0 5 f 0 0 41 32 3 1f 14 10 5
+ 5 1 2 63 7 3 1f b 12 8 f 0 1 63 c 3
+1f 1f f 8 f 0 1 63 4 3 39 23 0 0 0 62
+18 7 4 0 63 32 0 0 0 32 0 57 61 74 65 72
+47 6c 61 73 73 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 7 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 16 2 0 4 6 9 1 4f 8 0 19 e 1 4
+ 0 20 1 43 19 0 1f 12 10 6 7 0 0 54 3d 3
+16 d 6 6 2 1e 3 61 8 e 3a 20 1 14 0 42
+ c 2 4 2 63 63 63 0 0 32 0 46 75 7a 7a 79
+20 4b 6f 74 6f 63 63 63 32 32 32 0 0 0 0 b
+50 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1c 8 0 3 e 0 1 55 12 3 1c 7 0 1
+ e 2e 1 58 27 b e 4 0 2 a 0 2 63 4 a
+ d 9 0 2 c 1 2 63 10 b 4 54 0 47 0 53
+18 7 4 0 63 32 0 0 0 32 0 42 72 74 68 62
+65 6c 6c 73 20 63 63 63 32 32 32 0 4 0 40 0
+40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1a 4 1 1 b 16 0 47 5 3 15 e 0 1
+ d 0 0 4c 5 16 1c 6 4 2 7 0 0 63 4 16
+18 18 3 1 e 0 0 5e 4 10 24 7 0 4 0 62
+24 4 4 0 63 32 0 0 0 32 0 54 75 62 65 20
+42 65 6c 6c 73 63 63 63 32 32 32 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 1f 13 3 0 0 0 5f 3d 6 1f 12 13 2
+ 0 0 1 52 5 2 1f 14 13 3 0 0 1 56 28 5
+1e b 13 f 9 0 0 63 6 3 3b 63 0 63 0 73
+23 7 4 0 63 32 0 0 0 32 0 4e 6f 69 73 65
+20 53 68 6f 74 63 63 63 32 32 32 8 0 0 0 8
+ 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 1f 16 0 3 7 0 1 50 0 3 1f 18 3 3
+ 3 22 0 63 0 14 1d 7 6 3 6 0 1 3c 8 3
+1f 5 7 3 0 0 1 63 4 1b 39 23 0 8 0 42
+18 4 4 0 63 32 0 0 0 32 0 48 61 6e 64 20
+44 72 75 6d 20 63 63 63 32 32 32 0 1 0 3 0
+ 1 0 1 3 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+ 0 0 7d f7
\ No newline at end of file diff --git a/portmidi/pm_test/virttest.c b/portmidi/pm_test/virttest.c new file mode 100644 index 0000000..1aeb09b --- /dev/null +++ b/portmidi/pm_test/virttest.c @@ -0,0 +1,339 @@ +/* virttest.c -- test for creating/deleting virtual ports */ +/* + * Roger B. Dannenberg + * Oct 2021 + +This test is performed by running 2 instances of the program. The +first instance makes input and output ports named portmidi and waits +for a message. The second tries to do the same, but will fail because +portmidi already exists. It then opens portmidi (both input and +output). In greater detail: + +FIRST INSTANCE SECOND INSTANCE +-------------- --------------- + +initialize PortMidi initialize PortMidi +create portmidi in +create portmidi out +wait for input + create portmidi in -> fails + open portmidi in/out + send to portmidi +recv from portmidi +send to portmidi +wait 1s recv from portmidi + close portmidi in and out + terminate PortMidi +list all devices: + - check for correct number + - check for good description of portmidi in port (open) + - check for good description of portmidi out port (open) +close portmidi in +list all devices: + - check for correct number + - check for good description of portmidi in port (closed) + - check for good description of portmidi out port (open) +close portmidi out +list all devices: + - check for correct number + - check for good description of portmidi in port (closed) + - check for good description of portmidi out port (closed) +delete portmidi in + - check for correct number + - check for NULL description of portmidi in port + - check for good description of portmidi out port (closed) +delete portmidi out + - check for correct number + - check for NULL description of portmidi in port + - check for NULL description of portmidi out port +terminate portmidi +REPEAT 3 TIMES wait 2 seconds to give head start to other instance + REPEAT 3 TIMES + */ + +#include "portmidi.h" +#include "porttime.h" +#include "stdlib.h" +#include "stdio.h" +#include "string.h" +#include "assert.h" + +#define OUTPUT_BUFFER_SIZE 0 +#define INPUT_BUFFER_SIZE 10 +#define DEVICE_INFO NULL +#define DRIVER_INFO NULL +#define TIME_PROC ((PmTimeProcPtr) Pt_Time) +#define TIME_INFO NULL +#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ + + +static void prompt_and_exit(void) +{ + printf("type ENTER..."); + while (getchar() != '\n') ; + /* this will clean up open ports: */ + exit(-1); +} + + +static PmError printerror(PmError err, const char *msg) +{ + if (err == pmHostError) { + /* it seems pointless to allocate memory and copy the string, + * so I will do the work of Pm_GetHostErrorText directly + */ + char errmsg[80]; + Pm_GetHostErrorText(errmsg, 80); + printf("%s\n %s\n", msg, errmsg); + } else if (err < 0) { + printf("%s\n %s\n", msg, Pm_GetErrorText(err)); + } + return err; +} + + +static PmError checkerror(PmError err) +{ + if (err < 0) { + printerror(err, "PortMidi call failed..."); + prompt_and_exit(); + } + return err; +} + + +void wait_until(PmTimestamp when) +{ + PtTimestamp now = Pt_Time(); + if (when > now) { + Pt_Sleep(when - now); + } +} + + +void show_usage() +{ + printf("Usage: virttest\n" + " run two instances to test virtual port create/delete\n"); +} + + +void check_info(int id, char stat, int input, int virtual) +{ + const PmDeviceInfo *info = Pm_GetDeviceInfo(id); + if (stat == 'd') { + if (info) { + printf("Expected device %d to be deleted.\n", id); + prompt_and_exit(); + } + return; + } + if (!info) { + printf("Expected device %d to not be deleted.\n", id); + prompt_and_exit(); + } + if (strcmp("portmidi", info->name) != 0) { + printf("Device %d name is %s, not \"portmidi\".\n", id, info->name); + prompt_and_exit(); + } + if (info->input != input || (!info->output) != input) { + printf("Device %d input/output fields are wrong.\n", id); + prompt_and_exit(); + } + if ((!info->opened && stat == 'o') || (info->opened && stat == 'c')) { + printf("Device %d opened==%d, status should be %c.\n", id, + info->opened, stat); + prompt_and_exit(); + } + if (info->is_virtual != virtual) { + printf("Expected device %d to be virtual.\n", id); + prompt_and_exit(); + } +} + + +/* stat is 'o' for open, 'c' for closed, 'd' for deleted device */ +void check_ports(int cnt, int in_id, char in_stat, + int out_id, char out_stat, int virtual) +{ + if (cnt != Pm_CountDevices()) { + printf("Device count changed from %d to %d.\n", cnt, Pm_CountDevices()); + prompt_and_exit(); + } + check_info(in_id, in_stat, TRUE, virtual); + check_info(out_id, out_stat, FALSE, virtual); +} + + +void devices_list() +{ + int i; + for (i = 0; i < Pm_CountDevices(); i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (info) { + printf("%d: %s %s %s %s\n", i, info->name, + (info->input ? "input" : "output"), + (info->is_virtual ? "virtual" : "real_device"), + (info->opened ? "opened" : "closed")); + } + } +} + + +void test2() +{ + PmStream *out = NULL; + PmStream *in = NULL; + int out_id; + int in_id; + PmEvent buffer[1]; + PmTimestamp timestamp; + int pitch = 60; + int device_count = 0; + int i; + + printf("This must be virttest instance #2\n"); + + /* find and open portmidi in and out */ + device_count = Pm_CountDevices(); + for (i = 0; i < device_count; i++) { + const PmDeviceInfo *info = Pm_GetDeviceInfo(i); + if (info && strcmp(info->name, "portmidi") == 0) { + if (info->input) { + checkerror(Pm_OpenInput(&in, i, DRIVER_INFO, + INPUT_BUFFER_SIZE, TIME_PROC, TIME_INFO)); + in_id = i; + } else { + checkerror(Pm_OpenOutput(&out, i, DRIVER_INFO, + OUTPUT_BUFFER_SIZE, NULL, NULL, 0)); + out_id = i; + } + } + } + if (!in) { + printf("Did not open portmidi as input (virtual output).\n"); + prompt_and_exit(); + } + if (!out) { + printf("Did not open portmidi as output (virtual input).\n"); + prompt_and_exit(); + } + printf("Input device %d and output device %d are open.\n", in_id, out_id); + + /* send a message */ + buffer[0].timestamp = 0; + buffer[0].message = Pm_Message(0x90, pitch, 100); + checkerror(Pm_Write(out, buffer, 1)); + + /* wait for reply */ + printf("Sent message, waiting for reply...\n"); + while (Pm_Read(in, buffer, 1) < 1) Pt_Sleep(10); + + printf("********** GOT THE MESSAGE, SHUTTING DOWN ************\n"); + + /* close in */ + checkerror(Pm_Close(in)); + check_ports(device_count, in_id, 'c', out_id, 'o', FALSE); + printf("Closed input %d\n", in_id); + + /* close out */ + checkerror(Pm_Close(out)); + check_ports(device_count, in_id, 'c', out_id, 'c', FALSE); + printf("Closed output %d\n", out_id); + + Pt_Sleep(1000); + /* wrap it up */ + Pm_Terminate(); + printf("Got reply and terminated...\n"); + Pt_Sleep(2000); /* 2 seconds because other is waiting 1s. */ + /* 1 more second to make sure other shuts down before test repeats. */ +} + +extern int pm_check_errors; + +void test() +{ + PmStream *out; + PmStream *in; + int out_id; + int in_id; + PmEvent buffer[1]; + PmTimestamp timestamp; + int device_count = 0; + + TIME_START; + + printf("******** INITIALIZING PORTMIDI ***********\n"); + timestamp = Pt_Time(); + Pm_Initialize(); + printf("Pm_Initialize took %dms\n", Pt_Time() - timestamp); + devices_list(); + + pm_check_errors = FALSE; /* otherwise, PM_CHECK_ERRORS, if defined, */ + /* can cause this program to report an error and exit on pmNameConflict. */ + in_id = Pm_CreateVirtualInput("portmidi", NULL, DEVICE_INFO); + pm_check_errors = TRUE; /* there should be no other errors */ + if (in_id < 0) { + printerror(in_id, "Pm_CreateVirtualInput failed..."); + test2(); + return; + } + printf("Created portmidi virtual input; this is virttest instance #1\n"); + out_id = checkerror(Pm_CreateVirtualOutput("portmidi", NULL, DRIVER_INFO)); + device_count = Pm_CountDevices(); + + checkerror(Pm_OpenInput(&in, in_id, NULL, 0, NULL, NULL)); + checkerror(Pm_OpenOutput(&out, out_id, DRIVER_INFO, OUTPUT_BUFFER_SIZE, + TIME_PROC, TIME_INFO, 0)); + printf("Created/Opened input %d and output %d\n", in_id, out_id); + Pm_SetFilter(in, PM_FILT_ACTIVE | PM_FILT_CLOCK | PM_FILT_SYSEX); + /* empty the buffer after setting filter, just in case anything + got through */ + while (Pm_Read(in, buffer, 1)) ; + + /* wait for input */ + printf("Waiting for input...\n"); + while (Pm_Read(in, buffer, 1) < 1) Pt_Sleep(10); + + /* send two replies (only one would be fine) */ + checkerror(Pm_Write(out, buffer, 1)); + printf("Received input, writing output...\n"); + + /* wait 1s so receiver can get the message before we shut down */ + Pt_Sleep(1000); + printf("****** Closing everything and shutting down...\n"); + + /* expect 2 open ports */ + check_ports(device_count, in_id, 'o', out_id, 'o', TRUE); + /* close in */ + checkerror(Pm_Close(in)); + check_ports(device_count, in_id, 'c', out_id, 'o', TRUE); + + /* close out */ + checkerror(Pm_Close(out)); + check_ports(device_count, in_id, 'c', out_id, 'c', TRUE); + + /* delete in */ + checkerror(Pm_DeleteVirtualDevice(in_id)); + check_ports(device_count, in_id, 'd', out_id, 'c', TRUE); + + /* delete out */ + checkerror(Pm_DeleteVirtualDevice(out_id)); + check_ports(device_count, in_id, 'd', out_id, 'd', TRUE); + + /* we are done */ + Pm_Terminate(); +} + + +int main(int argc, char *argv[]) +{ + int i; + show_usage(); + for (i = 0; i < 3; i++) { + test(); + } + printf("finished virttest (SUCCESS). Type ENTER to quit..."); + while (getchar() != '\n') ; + return 0; +} diff --git a/portmidi/pm_win/README_WIN.txt b/portmidi/pm_win/README_WIN.txt new file mode 100755 index 0000000..8bfb467 --- /dev/null +++ b/portmidi/pm_win/README_WIN.txt @@ -0,0 +1,174 @@ +File: PortMidi Win32 Readme
+Author: Belinda Thom, June 16 2002
+Revised by: Roger Dannenberg, June 2002, May 2004, June 2007,
+ Umpei Kurokawa, June 2007
+ Roger Dannenberg Sep 2009, May 2022
+
+Contents:
+ Using Portmidi
+ To Install Portmidi
+ To Compile Portmidi
+ About Cmake
+ Using other versions of Visual C++
+ To Create Your Own Portmidi Client Application
+
+
+
+=============================================================================
+USING PORTMIDI:
+=============================================================================
+
+I recommend building a static library and linking with your
+application. PortMidi is not large. See ../README.md for
+basic compiling instructions.
+
+The Windows version has a couple of extra switches: You can define
+DEBUG and MMDEBUG for a few extra messages (see the code).
+
+If PM_CHECK_ERRORS is defined, PortMidi reports and exits on any
+error. This requires terminal output to see, and aborts your
+application, so it's only intended for quick command line programs
+where you do not care to check return values and handle errors
+more robustly.
+
+PortMidi is designed to run without a console and should work perfectly
+well within a graphical user interface application.
+
+Read the portmidi.h file for PortMidi API details on using the PortMidi API.
+See <...>\pm_test\testio.c and other files in pm_test for usage examples.
+
+There are many other programs in pm_test, including a MIDI monitor.
+
+
+============================================================================
+DESIGN NOTES
+============================================================================
+
+Orderly cleanup after errors are encountered is based on a fixed order of
+steps and state changes to reflect each step. Here's the order:
+
+To open input:
+ initialize return value to NULL
+ - allocate the PmInternal strucure (representation of PortMidiStream)
+ return value is (non-null) PmInternal structure
+ - allocate midi buffer
+ set buffer field of PmInternal structure
+ - call system-dependent open code
+ - allocate midiwinmm_type for winmm dependent data
+ set descriptor field of PmInternal structure
+ - open device
+ set handle field of midiwinmm_type structure
+ - allocate buffers
+ - start device
+ - return
+ - return
+
+SYSEX HANDLING
+
+There are three cases: simple output, stream output, input
+Each must deal with:
+ 1. Buffer Initialization (creating buffers)
+ 2. Buffer Allocation (finding a free buffer)
+ 3. Buffer Fill (putting bytes in the buffer)
+ 4. Buffer Preparation (midiOutPrepare, etc.)
+ 5. Buffer Send (to Midi device)
+ 6. Buffer Receive (in callback)
+ 7. Buffer Empty (removing bytes from buffer)
+ 8. Buffer Free (returning to the buffer pool)
+ 9. Buffer Finalization (returning to heap)
+
+Here's how simple output handles sysex:
+ 1. Buffer Initialization (creating buffers)
+ allocated when code tries to write first byte to a buffer
+ the test is "if (!m->sysex_buffers[0]) { ... }"
+ this field is initialized to NULL when device is opened
+ the size is SYSEX_BYTES_PER_BUFFER
+ allocate_sysex_buffers() does the initialization
+ note that the actual size of the allocation includes
+ additional space for a MIDIEVENT (3 longs) which are
+ not used in this case
+ 2. Buffer Allocation (finding a free buffer)
+ see get_free_sysex_buffer()
+ cycle through m->sysex_buffers[] using m->next_sysex_buffer
+ to determine where to look next
+ if nothing is found, wait by blocking on m->sysex_buffer_signal
+ this is signaled by the callback every time a message is
+ received
+ 3. Buffer Fill (putting bytes in the buffer)
+ essentially a state machine approach
+ hdr->dwBytesRecorded is a position in message pointed to by m->hdr
+ keep appending bytes until dwBytesRecorded >= SYSEX_BYTES_PER_BUFFER
+ then send the message, reseting the state to initial values
+ 4. Buffer Preparation (midiOutPrepare, etc.)
+ just before sending in winmm_end_sysex()
+ 5. Buffer Send (to Midi device)
+ message is padded with zero at end (since extra space was allocated
+ this is ok) -- the zero works around a bug in (an old version of)
+ MIDI YOKE drivers
+ dwBufferLength gets dwBytesRecorded, and dwBytesRecorded gets 0
+ uses midiOutLongMsg()
+ 6. Buffer Receive (in callback)
+ 7. Buffer Empty (removing bytes from buffer)
+ not applicable for output
+ 8. Buffer Free (returning to the buffer pool)
+ unprepare message to indicate that it is free
+ SetEvent on m->buffer_signal in case client is waiting
+ 9. Buffer Finalization (returning to heap)
+ when device is closed, winmm_out_delete frees all sysex buffers
+
+Here's how stream output handles sysex:
+ 1. Buffer Initialization (creating buffers)
+ same code as simple output (see above)
+ 2. Buffer Allocation (finding a free buffer)
+ same code as simple output (see above)
+ 3. Buffer Fill (putting bytes in the buffer)
+ essentially a state machine approach
+ m->dwBytesRecorded is a position in message
+ keep appending bytes until buffer is full (one byte to spare)
+ 4. Buffer Preparation (midiOutPrepare, etc.)
+ done before sending message
+ dwBytesRecorded and dwBufferLength are set in winmm_end_sysex
+ 5. Buffer Send (to Midi device)
+ uses midiStreamOutMsg()
+ 6. Buffer Receive (in callback)
+ 7. Buffer Empty (removing bytes from buffer)
+ not applicable for output
+ 8. Buffer Free (returning to the buffer pool)
+ unprepare message to indicate that it is free
+ SetEvent on m->buffer_signal in case client is waiting
+ 9. Buffer Finalization (returning to heap)
+ when device is closed, winmm_out_delete frees all sysex buffers
+
+
+Here's how input handles sysex:
+ 1. Buffer Initialization (creating buffers)
+ two buffers are allocated in winmm_in_open
+ 2. Buffer Allocation (finding a free buffer)
+ same code as simple output (see above)
+ 3. Buffer Fill (putting bytes in the buffer)
+ not applicable for input
+ 4. Buffer Preparation (midiOutPrepare, etc.)
+ done before sending message -- in winmm_in_open and in callback
+ 5. Buffer Send (to Midi device)
+ uses midiInAddbuffer in allocate_sysex_input_buffer (called from
+ winmm_in_open) and callback
+ 6. Buffer Receive (in callback)
+ 7. Buffer Empty (removing bytes from buffer)
+ done without pause in loop in callback
+ 8. Buffer Free (returning to the buffer pool)
+ done by midiInAddBuffer in callback, no pointer to buffers
+ is retained except by device
+ 9. Buffer Finalization (returning to heap)
+ when device is closed, empty buffers are delivered to callback,
+ which frees them
+
+IMPORTANT: In addition to the above, PortMidi now has
+"shortcuts" to optimize the transfer of sysex data. To enable
+the optimization for sysex output, the system-dependent code
+sets fields in the pmInternal structure: fill_base, fill_offset_ptr,
+and fill_length. When fill_base is non-null, the system-independent
+part of PortMidi is allowed to directly copy sysex bytes to
+"fill_base[*fill_offset_ptr++]" until *fill_offset_ptr reaches
+fill_length. See the code for details.
+
+
diff --git a/portmidi/pm_win/debugging_dlls.txt b/portmidi/pm_win/debugging_dlls.txt new file mode 100755 index 0000000..82b81a5 --- /dev/null +++ b/portmidi/pm_win/debugging_dlls.txt @@ -0,0 +1,145 @@ +========================================================================================================================
+Methods for Debugging DLLs
+========================================================================================================================
+If you have the source for both the DLL and the calling program, open the project for the calling executable file and
+debug the DLL from there. If you load a DLL dynamically, you must specify it in the Additional DLLs category of the
+Debug tab in the Project Settings dialog box.
+
+If you have the source for the DLL only, open the project that builds the DLL. Use the Debug tab in the Project
+Settings dialog box to specify the executable file that calls the DLL.
+
+You can also debug a DLL without a project. For example, maybe you just picked up a DLL and source code but you
+don’t have an associated project or workspace. You can use the Open command on the File menu to select the .DLL
+file you want to debug. The debug information should be in either the .DLL or the related .PDB file. After
+Visual C++ opens the file, on the Build menu click Start Debug and Go to begin debugging.
+
+To debug a DLL using the project for the executable file
+
+From the Project menu, click Settings.
+The Project Settings dialog box appears.
+
+Choose the Debug tab.
+
+
+In the Category drop-down list box, select General.
+
+
+In the Program Arguments text box, type any command-line arguments required by the executable file.
+
+
+In the Category drop-down list box, select Additional DLLs.
+
+
+In the Local Name column, type the names of DLLs to debug.
+If you are debugging remotely, the Remote Name column appears. In this column, type the complete path for the
+remote module to map to the local module name.
+
+In the Preload column, select the check box if you want to load the module before debugging begins.
+
+
+Click OK to store the information in your project.
+
+
+From the Build menu, click Start Debug and Go to start the debugger.
+You can set breakpoints in the DLL or the calling program. You can open a source file for the DLL and set breakpoints
+in that file, even though it is not a part of the executable file’s project.
+
+To debug a DLL using the project for the DLL
+
+From the Project menu, click Settings.
+The Project Settings dialog box appears.
+
+Choose the Debug tab.
+
+
+In the Category drop-down list box, select General.
+
+
+In the Executable For Debug Session text box, type the name of the executable file that calls the DLL.
+
+
+In the Category list box, select Additional DLLs.
+
+
+In the Local Module Name column, type the name of the DLLs you want to debug.
+
+
+Click OK to store the information in your project.
+
+
+Set breakpoints as required in your DLL source files or on function symbols in the DLL.
+
+
+From the Build menu, click Start Debug and Go to start the debugger.
+To debug a DLL created with an external project
+
+From the Project menu, click Settings.
+The Project Settings dialog box appears.
+
+Choose the Debug tab.
+
+
+In the Category drop-down list box, select General.
+
+
+In the Executable For Debug Session text box, type the name of the DLL that your external makefile builds.
+
+
+Click OK to store the information in your project.
+
+
+Build a debug version of the DLL with symbolic debugging information, if you don’t already have one.
+
+
+Follow one of the two procedures immediately preceding this one to debug the DLL.
+
+========================================================================================================================
+Why Don’t My DLL Breakpoints Work?
+========================================================================================================================
+Some reasons why your breakpoints don’t work as expected are listed here, along with solutions or work-arounds for each.
+If you follow the instructions in one topic and are still having breakpoint problems, look at some of the other topics.
+Often breakpoint problems result from a combination of conditions.
+
+You can't set a breakpoint in a source file when the corresponding symbolic information isn't loaded into memory by
+the debugger.
+You cannot set a breakpoint in any source file when the corresponding symbolic information will not be loaded into memory
+by the debugger.
+Symptoms include messages such as "the breakpoint cannot be set" or a simple, noninformational beep.
+
+When setting breakpoints before the code to be debugged has been started, the debugger uses a breakpoint list to keep
+track of how and where to set breakpoints. When you actually begin the debugging session, the debugger loads the symbolic
+information for all the code to be debugged and then walks through its breakpoint list, attempting to set the
+breakpoints.
+
+However, if one or more of the code modules have not been designated to the debugger, there will be no symbolic
+information for the debugger to use when walking through its breakpoint list. Situations where this is likely to
+occur include:
+
+Attempts to set breakpoints in a DLL before the call to LoadLibrary.
+
+Setting a breakpoint in an ActiveX server before the container has started the server.
+
+Other similar cases.
+
+To prevent this behavior in Visual C++, specify all additional DLLs and COM servers in the Additional DLLs field
+in the Debug/Options dialog box to notify the debugger that you want it to load symbolic debug information for
+additional .DLL files. When this has been done, breakpoints set in code that has not yet been loaded into memory
+will be "virtual" breakpoints. When the code is actually loaded into memory by the loader, these become physical
+breakpoints. Make sure that these additional debugging processes are not already running when you start your
+debugging session. The debugging process and these additional processes must be sychronized at the same beginning
+point to work correctly, hitting all breakpoints.
+
+Breakpoints are missed when more than one copy of a DLL is on your hard disk.
+Having more than one copy of a DLL on your hard drive, especially if it is in your Windows directory, can cause
+debugger confusion. The debugger will load the symbolic information for the DLL specified to it at run time (with the
+Additional DLLs field in the Debug/Options dialog box), while Windows has actually loaded a different copy of the
+DLL itself into memory. Because there is no way to force the debugger to load a specific DLL, it is a good idea to
+keep only one version of a DLL at a time in your path, current directory, and Windows directory.
+
+You can’t set "Break When Expression Has Changed" breakpoints on a variable local to a DLL.
+Setting a "Break When Expression Has Changed" breakpoint on a variable local to a DLL function before the call
+to LoadLibrary causes the breakpoint to be virtual (there are no physical addresses for the DLL in memory yet).
+Virtual breakpoints involving expressions pose a special problem. The DLL must be specified to the debugger at
+startup (causing its symbolic information to be loaded). In addition, the DLL's executable code must also be loaded
+into memory before this kind of breakpoint can be set. This means that the calling application's code must be
+executed to the point after its call to LoadLibrary before the debugger will allow this type of breakpoint to be set.
diff --git a/portmidi/pm_win/pmwin.c b/portmidi/pm_win/pmwin.c new file mode 100755 index 0000000..5cb73b0 --- /dev/null +++ b/portmidi/pm_win/pmwin.c @@ -0,0 +1,98 @@ +/* pmwin.c -- PortMidi os-dependent code */
+
+/* This file only needs to implement:
+ pm_init(), which calls various routines to register the
+ available midi devices,
+ Pm_GetDefaultInputDeviceID(), and
+ Pm_GetDefaultOutputDeviceID().
+ This file must
+ be separate from the main portmidi.c file because it is system
+ dependent, and it is separate from, say, pmwinmm.c, because it
+ might need to register devices for winmm, directx, and others.
+
+ */
+
+#include "stdlib.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "pminternal.h"
+#include "pmwinmm.h"
+#ifdef DEBUG
+#include "stdio.h"
+#endif
+#include <windows.h>
+
+/* pm_exit is called when the program exits.
+ It calls pm_term to make sure PortMidi is properly closed.
+ If DEBUG is on, we prompt for input to avoid losing error messages.
+ */
+static void pm_exit(void) {
+ pm_term();
+}
+
+
+static BOOL WINAPI ctrl_c_handler(DWORD fdwCtrlType)
+{
+ exit(1); /* invokes pm_exit() */
+ ExitProcess(1); /* probably never called */
+ return TRUE;
+}
+
+/* pm_init is the windows-dependent initialization.*/
+void pm_init(void)
+{
+ atexit(pm_exit);
+ SetConsoleCtrlHandler(ctrl_c_handler, TRUE);
+#ifdef DEBUG
+ printf("registered pm_exit with atexit()\n");
+#endif
+ pm_winmm_init();
+ /* initialize other APIs (DirectX?) here */
+}
+
+
+void pm_term(void) {
+ pm_winmm_term();
+}
+
+
+static PmDeviceID pm_get_default_device_id(int is_input, char *key) {
+#define PATTERN_MAX 256
+ /* Find first input or device -- this is the default. */
+ PmDeviceID id = pmNoDevice;
+ int i;
+ Pm_Initialize(); /* make sure descriptors exist! */
+ for (i = 0; i < pm_descriptor_len; i++) {
+ if (pm_descriptors[i].pub.input == is_input) {
+ id = i;
+ break;
+ }
+ }
+ return id;
+}
+
+
+PmDeviceID Pm_GetDefaultInputDeviceID() {
+ return pm_get_default_device_id(TRUE,
+ "/P/M_/R/E/C/O/M/M/E/N/D/E/D_/I/N/P/U/T_/D/E/V/I/C/E");
+}
+
+
+PmDeviceID Pm_GetDefaultOutputDeviceID() {
+ return pm_get_default_device_id(FALSE,
+ "/P/M_/R/E/C/O/M/M/E/N/D/E/D_/O/U/T/P/U/T_/D/E/V/I/C/E");
+}
+
+
+#include "stdio.h"
+
+void *pm_alloc(size_t s) {
+ return malloc(s);
+}
+
+
+void pm_free(void *ptr) {
+ free(ptr);
+}
+
+
diff --git a/portmidi/pm_win/pmwinmm.c b/portmidi/pm_win/pmwinmm.c new file mode 100755 index 0000000..6f4b6f3 --- /dev/null +++ b/portmidi/pm_win/pmwinmm.c @@ -0,0 +1,1196 @@ +/* pmwinmm.c -- system specific definitions */
+
+#ifndef _WIN32_WINNT
+ /* without this define, InitializeCriticalSectionAndSpinCount is
+ * undefined. This version level means "Windows 2000 and higher"
+ */
+ #define _WIN32_WINNT 0x0500
+#endif
+
+#define UNICODE 1
+#include <wchar.h>
+#include "windows.h"
+#include "mmsystem.h"
+#include "portmidi.h"
+#include "pmutil.h"
+#include "pminternal.h"
+#include "pmwinmm.h"
+#include <string.h>
+#include "porttime.h"
+#ifndef UNICODE
+#error Expected UNICODE to be defined
+#endif
+
+
+/* asserts used to verify portMidi code logic is sound; later may want
+ something more graceful */
+#include <assert.h>
+#ifdef MMDEBUG
+/* this printf stuff really important for debugging client app w/host errors.
+ probably want to do something else besides read/write from/to console
+ for portability, however */
+#define STRING_MAX 80
+#include "stdio.h"
+#endif
+
+#define streql(x, y) (strcmp(x, y) == 0)
+
+#define MIDI_SYSEX 0xf0
+#define MIDI_EOX 0xf7
+
+/* callback routines */
+static void CALLBACK winmm_in_callback(HMIDIIN hMidiIn,
+ UINT wMsg, DWORD_PTR dwInstance,
+ DWORD_PTR dwParam1, DWORD_PTR dwParam2);
+static void CALLBACK winmm_streamout_callback(HMIDIOUT hmo, UINT wMsg,
+ DWORD_PTR dwInstance,
+ DWORD_PTR dwParam1,
+ DWORD_PTR dwParam2);
+
+extern pm_fns_node pm_winmm_in_dictionary;
+extern pm_fns_node pm_winmm_out_dictionary;
+
+static void winmm_out_delete(PmInternal *midi); /* forward reference */
+
+/*
+A note about buffers: WinMM seems to hold onto buffers longer than
+one would expect, e.g. when I tried using 2 small buffers to send
+long sysex messages, at some point WinMM held both buffers. This problem
+was fixed by making buffers bigger. Therefore, it seems that there should
+be enough buffer space to hold a whole sysex message.
+
+The bufferSize passed into Pm_OpenInput (passed into here as buffer_len)
+will be used to estimate the largest sysex message (= buffer_len * 4 bytes).
+Call that the max_sysex_len = buffer_len * 4.
+
+For simple midi output (latency == 0), allocate 3 buffers, each with half
+the size of max_sysex_len, but each at least 256 bytes.
+
+For stream output, there will already be enough space in very short
+buffers, so use them, but make sure there are at least 16.
+
+For input, use many small buffers rather than 2 large ones so that when
+there are short sysex messages arriving frequently (as in control surfaces)
+there will be more free buffers to fill. Use max_sysex_len / 64 buffers,
+but at least 16, of size 64 bytes each.
+
+The following constants help to represent these design parameters:
+*/
+#define NUM_SIMPLE_SYSEX_BUFFERS 3
+#define MIN_SIMPLE_SYSEX_LEN 256
+
+#define MIN_STREAM_BUFFERS 16
+#define STREAM_BUFFER_LEN 24
+
+#define INPUT_SYSEX_LEN 64
+#define MIN_INPUT_BUFFERS 16
+
+/* if we run out of space for output (assume this is due to a sysex msg,
+ expand by up to NUM_EXPANSION_BUFFERS in increments of EXPANSION_BUFFER_LEN
+ */
+#define NUM_EXPANSION_BUFFERS 128
+#define EXPANSION_BUFFER_LEN 1024
+
+/* A sysex buffer has 3 DWORDS as a header plus the actual message size */
+#define MIDIHDR_SYSEX_BUFFER_LENGTH(x) ((x) + sizeof(long)*3)
+/* A MIDIHDR with a sysex message is the buffer length plus the header size */
+#define MIDIHDR_SYSEX_SIZE(x) (MIDIHDR_SYSEX_BUFFER_LENGTH(x) + sizeof(MIDIHDR))
+
+/*
+==============================================================================
+win32 mmedia system specific structure passed to midi callbacks
+==============================================================================
+*/
+
+/* global winmm device info */
+MIDIINCAPS *midi_in_caps = NULL;
+MIDIINCAPS midi_in_mapper_caps;
+UINT midi_num_inputs = 0;
+MIDIOUTCAPS *midi_out_caps = NULL;
+MIDIOUTCAPS midi_out_mapper_caps;
+UINT midi_num_outputs = 0;
+
+/* per device info */
+typedef struct winmm_info_struct {
+ union {
+ HMIDISTRM stream; /* windows handle for stream */
+ HMIDIOUT out; /* windows handle for out calls */
+ HMIDIIN in; /* windows handle for in calls */
+ } handle;
+
+ /* midi output messages are sent in these buffers, which are allocated
+ * in a round-robin fashion, using next_buffer as an index
+ */
+ LPMIDIHDR *buffers; /* pool of buffers for midi in or out data */
+ int max_buffers; /* length of buffers array */
+ int buffers_expanded; /* buffers array expanded for extra msgs? */
+ int num_buffers; /* how many buffers allocated in buffers array */
+ int next_buffer; /* index of next buffer to send */
+ HANDLE buffer_signal; /* used to wait for buffer to become free */
+ unsigned long last_time; /* last output time */
+ int first_message; /* flag: treat first message differently */
+ int sysex_mode; /* middle of sending sysex */
+ unsigned long sysex_word; /* accumulate data when receiving sysex */
+ unsigned int sysex_byte_count; /* count how many received */
+ LPMIDIHDR hdr; /* the message accumulating sysex to send */
+ unsigned long sync_time; /* when did we last determine delta? */
+ long delta; /* difference between stream time and
+ real time */
+ CRITICAL_SECTION lock; /* prevents reentrant callbacks (input only) */
+} winmm_info_node, *winmm_info_type;
+
+
+/*
+=============================================================================
+general MIDI device queries
+=============================================================================
+*/
+
+/* add a device after converting device (product) name to UTF-8 */
+static void pm_add_device_w(char *api, WCHAR *device_name, int is_input,
+ int is_virtual, void *descriptor, pm_fns_type dictionary)
+{
+ char utf8name[4 * MAXPNAMELEN];
+ WideCharToMultiByte(CP_UTF8, 0, device_name, -1,
+ utf8name, 4 * MAXPNAMELEN - 1, NULL, NULL);
+ /* ignore errors here -- if pm_descriptor_max is exceeded,
+ some devices will not be accessible. */
+ pm_add_device(api, utf8name, is_input, is_virtual, descriptor, dictionary);
+}
+
+
+static void pm_winmm_general_inputs()
+{
+ UINT i;
+ WORD wRtn;
+ midi_num_inputs = midiInGetNumDevs();
+ midi_in_caps = (MIDIINCAPS *) pm_alloc(sizeof(MIDIINCAPS) *
+ midi_num_inputs);
+ if (midi_in_caps == NULL) {
+ /* if you can't open a particular system-level midi interface
+ * (such as winmm), we just consider that system or API to be
+ * unavailable and move on without reporting an error.
+ */
+ return;
+ }
+
+ for (i = 0; i < midi_num_inputs; i++) {
+ wRtn = midiInGetDevCaps(i, (LPMIDIINCAPS) & midi_in_caps[i],
+ sizeof(MIDIINCAPS));
+ if (wRtn == MMSYSERR_NOERROR) {
+ pm_add_device_w("MMSystem", midi_in_caps[i].szPname, TRUE, FALSE,
+ (void *) (intptr_t) i, &pm_winmm_in_dictionary);
+ }
+ }
+}
+
+
+static void pm_winmm_mapper_input()
+{
+ WORD wRtn;
+ /* Note: if MIDIMAPPER opened as input (documentation implies you
+ can, but current system fails to retrieve input mapper
+ capabilities) then you still should retrieve some form of
+ setup info. */
+ wRtn = midiInGetDevCaps((UINT) MIDIMAPPER,
+ (LPMIDIINCAPS) & midi_in_mapper_caps,
+ sizeof(MIDIINCAPS));
+ if (wRtn == MMSYSERR_NOERROR) {
+ pm_add_device_w("MMSystem", midi_in_mapper_caps.szPname, TRUE, FALSE,
+ (void *) (intptr_t) MIDIMAPPER,
+ &pm_winmm_in_dictionary);
+ }
+}
+
+
+static void pm_winmm_general_outputs()
+{
+ UINT i;
+ DWORD wRtn;
+ midi_num_outputs = midiOutGetNumDevs();
+ midi_out_caps = pm_alloc(sizeof(MIDIOUTCAPS) * midi_num_outputs);
+
+ if (midi_out_caps == NULL) {
+ /* no error is reported -- see pm_winmm_general_inputs */
+ return ;
+ }
+
+ for (i = 0; i < midi_num_outputs; i++) {
+ wRtn = midiOutGetDevCaps(i, (LPMIDIOUTCAPS) & midi_out_caps[i],
+ sizeof(MIDIOUTCAPS));
+ if (wRtn == MMSYSERR_NOERROR) {
+ pm_add_device_w("MMSystem", midi_out_caps[i].szPname, FALSE, FALSE,
+ (void *) (intptr_t) i, &pm_winmm_out_dictionary);
+ }
+ }
+}
+
+
+static void pm_winmm_mapper_output()
+{
+ WORD wRtn;
+ /* Note: if MIDIMAPPER opened as output (pseudo MIDI device
+ maps device independent messages into device dependant ones,
+ via NT midimapper program) you still should get some setup info */
+ wRtn = midiOutGetDevCaps((UINT) MIDIMAPPER, (LPMIDIOUTCAPS)
+ & midi_out_mapper_caps, sizeof(MIDIOUTCAPS));
+ if (wRtn == MMSYSERR_NOERROR) {
+ pm_add_device_w("MMSystem", midi_out_mapper_caps.szPname, FALSE, FALSE,
+ (void *) (intptr_t) MIDIMAPPER,
+ &pm_winmm_out_dictionary);
+ }
+}
+
+
+/*
+============================================================================
+host error handling
+============================================================================
+*/
+
+static unsigned int winmm_check_host_error(PmInternal *midi)
+{
+ return FALSE;
+}
+
+
+/*
+=============================================================================
+buffer handling
+=============================================================================
+*/
+static MIDIHDR *allocate_buffer(long data_size)
+{
+ LPMIDIHDR hdr = (LPMIDIHDR) pm_alloc(MIDIHDR_SYSEX_SIZE(data_size));
+ MIDIEVENT *evt;
+ if (!hdr) return NULL;
+ evt = (MIDIEVENT *) (hdr + 1); /* place MIDIEVENT after header */
+ hdr->lpData = (LPSTR) evt;
+ hdr->dwBufferLength = MIDIHDR_SYSEX_BUFFER_LENGTH(data_size);
+ hdr->dwBytesRecorded = 0;
+ hdr->dwFlags = 0;
+ hdr->dwUser = hdr->dwBufferLength;
+ return hdr;
+}
+
+
+static PmError allocate_buffers(winmm_info_type info, long data_size,
+ long count)
+{
+ int i;
+ /* buffers is an array of count pointers to MIDIHDR/MIDIEVENT struct */
+ info->num_buffers = 0; /* in case no memory can be allocated */
+ info->buffers = (LPMIDIHDR *) pm_alloc(sizeof(LPMIDIHDR) * count);
+ if (!info->buffers) return pmInsufficientMemory;
+ info->max_buffers = count;
+ for (i = 0; i < count; i++) {
+ LPMIDIHDR hdr = allocate_buffer(data_size);
+ if (!hdr) { /* free everything allocated so far and return */
+ for (i = i - 1; i >= 0; i--) pm_free(info->buffers[i]);
+ pm_free(info->buffers);
+ info->max_buffers = 0;
+ return pmInsufficientMemory;
+ }
+ info->buffers[i] = hdr; /* this may be NULL if allocation fails */
+ }
+ info->num_buffers = count;
+ return pmNoError;
+}
+
+
+static LPMIDIHDR get_free_output_buffer(PmInternal *midi)
+{
+ LPMIDIHDR r = NULL;
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ while (TRUE) {
+ int i;
+ for (i = 0; i < info->num_buffers; i++) {
+ /* cycle through buffers, modulo info->num_buffers */
+ info->next_buffer++;
+ if (info->next_buffer >= info->num_buffers) info->next_buffer = 0;
+ r = info->buffers[info->next_buffer];
+ if ((r->dwFlags & MHDR_PREPARED) == 0) goto found_buffer;
+ }
+ /* after scanning every buffer and not finding anything, block */
+ if (WaitForSingleObject(info->buffer_signal, 1000) == WAIT_TIMEOUT) {
+#ifdef MMDEBUG
+ printf("PortMidi warning: get_free_output_buffer() "
+ "wait timed out after 1000ms\n");
+#endif
+ /* if we're trying to send a sysex message, maybe the
+ * message is too big and we need more message buffers.
+ * Expand the buffer pool by 128KB using 1024-byte buffers.
+ */
+ /* first, expand the buffers array if necessary */
+ if (!info->buffers_expanded) {
+ LPMIDIHDR *new_buffers = (LPMIDIHDR *) pm_alloc(
+ (info->num_buffers + NUM_EXPANSION_BUFFERS) *
+ sizeof(LPMIDIHDR));
+ /* if no memory, we could return a no-memory error, but user
+ * probably will be unprepared to deal with it. Maybe the
+ * MIDI driver is temporarily hung so we should just wait.
+ * I don't know the right answer, but waiting is easier.
+ */
+ if (!new_buffers) continue;
+ /* copy buffers to new_buffers and replace buffers */
+ memcpy(new_buffers, info->buffers,
+ info->num_buffers * sizeof(LPMIDIHDR));
+ pm_free(info->buffers);
+ info->buffers = new_buffers;
+ info->max_buffers = info->num_buffers + NUM_EXPANSION_BUFFERS;
+ info->buffers_expanded = TRUE;
+ }
+ /* next, add one buffer and return it */
+ if (info->num_buffers < info->max_buffers) {
+ r = allocate_buffer(EXPANSION_BUFFER_LEN);
+ /* again, if there's no memory, we may not really be
+ * dead -- maybe the system is temporarily hung and
+ * we can just wait longer for a message buffer */
+ if (!r) continue;
+ info->buffers[info->num_buffers++] = r;
+ goto found_buffer; /* break out of 2 loops */
+ }
+ /* else, we've allocated all NUM_EXPANSION_BUFFERS buffers,
+ * and we have no free buffers to send. We'll just keep
+ * polling to see if any buffers show up.
+ */
+ }
+ }
+found_buffer:
+ r->dwBytesRecorded = 0;
+ /* actual buffer length is saved in dwUser field */
+ r->dwBufferLength = (DWORD) r->dwUser;
+ return r;
+}
+
+/*
+============================================================================
+begin midi input implementation
+============================================================================
+*/
+
+
+static unsigned int allocate_input_buffer(HMIDIIN h, long buffer_len)
+{
+ LPMIDIHDR hdr = allocate_buffer(buffer_len);
+ if (!hdr) return pmInsufficientMemory;
+ /* note: pm_hosterror is normally a boolean, but here, we store Win
+ * error code. The caller must test the value for nonzero, set
+ * pm_hosterror_text, and then set pm_hosterror to TRUE */
+ pm_hosterror = midiInPrepareHeader(h, hdr, sizeof(MIDIHDR));
+ if (pm_hosterror) {
+ pm_free(hdr);
+ return pm_hosterror;
+ }
+ pm_hosterror = midiInAddBuffer(h, hdr, sizeof(MIDIHDR));
+ return pm_hosterror;
+}
+
+
+static winmm_info_type winmm_info_create()
+{
+ winmm_info_type info = (winmm_info_type) pm_alloc(sizeof(winmm_info_node));
+ info->handle.in = NULL;
+ info->handle.out = NULL;
+ info->buffers = NULL; /* not used for input */
+ info->num_buffers = 0; /* not used for input */
+ info->max_buffers = 0; /* not used for input */
+ info->buffers_expanded = FALSE; /* not used for input */
+ info->next_buffer = 0; /* not used for input */
+ info->buffer_signal = 0; /* not used for input */
+ info->last_time = 0;
+ info->first_message = TRUE; /* not used for input */
+ info->sysex_mode = FALSE;
+ info->sysex_word = 0;
+ info->sysex_byte_count = 0;
+ info->hdr = NULL; /* not used for input */
+ info->sync_time = 0;
+ info->delta = 0;
+ return info;
+}
+
+
+static void report_hosterror(LPWCH error_msg)
+{
+ WideCharToMultiByte(CP_UTF8, 0, error_msg, -1, pm_hosterror_text,
+ sizeof(pm_hosterror_text), NULL, NULL);
+ if (pm_hosterror == MMSYSERR_NOMEM) {
+ /* add explanation to Window's confusing error message */
+ /* if there's room: */
+ if (PM_HOST_ERROR_MSG_LEN - strlen(pm_hosterror_text) > 60) {
+ strcat_s(pm_hosterror_text, PM_HOST_ERROR_MSG_LEN,
+ " Probably this MIDI device is open "
+ "in another application.");
+ }
+ }
+ pm_hosterror = TRUE;
+}
+
+
+static void report_hosterror_in()
+{
+ WCHAR error_msg[PM_HOST_ERROR_MSG_LEN];
+ int err = midiInGetErrorText(pm_hosterror, error_msg,
+ PM_HOST_ERROR_MSG_LEN);
+ assert(err == MMSYSERR_NOERROR);
+ report_hosterror(error_msg);
+}
+
+
+static void report_hosterror_out()
+{
+ WCHAR error_msg[PM_HOST_ERROR_MSG_LEN];
+ int err = midiOutGetErrorText(pm_hosterror, error_msg,
+ PM_HOST_ERROR_MSG_LEN);
+ assert(err == MMSYSERR_NOERROR);
+ report_hosterror(error_msg);
+}
+
+
+static PmError winmm_in_open(PmInternal *midi, void *driverInfo)
+{
+ DWORD dwDevice;
+ int i = midi->device_id;
+ int max_sysex_len = midi->buffer_len * 4;
+ int num_input_buffers = max_sysex_len / INPUT_SYSEX_LEN;
+ winmm_info_type info;
+
+ dwDevice = (DWORD) (intptr_t) pm_descriptors[i].descriptor;
+
+ /* create system dependent device data */
+ info = winmm_info_create();
+ midi->api_info = info;
+ if (!info) goto no_memory;
+ /* 4000 is based on Windows documentation -- that's the value used
+ in the memory manager. It's small enough that it should not
+ hurt performance even if it's not optimal.
+ */
+ InitializeCriticalSectionAndSpinCount(&info->lock, 4000);
+ /* open device */
+ pm_hosterror = midiInOpen(
+ &(info->handle.in), /* input device handle */
+ dwDevice, /* device ID */
+ (DWORD_PTR) winmm_in_callback, /* callback address */
+ (DWORD_PTR) midi, /* callback instance data */
+ CALLBACK_FUNCTION); /* callback is a procedure */
+ if (pm_hosterror) goto free_descriptor;
+
+ if (num_input_buffers < MIN_INPUT_BUFFERS)
+ num_input_buffers = MIN_INPUT_BUFFERS;
+ for (i = 0; i < num_input_buffers; i++) {
+ if (allocate_input_buffer(info->handle.in, INPUT_SYSEX_LEN)) {
+ /* either pm_hosterror was set, or the proper return code
+ is pmInsufficientMemory */
+ goto close_device;
+ }
+ }
+ /* start device */
+ pm_hosterror = midiInStart(info->handle.in);
+ if (!pm_hosterror) {
+ return pmNoError;
+ }
+
+ /* undo steps leading up to the detected error */
+
+ /* ignore return code (we already have an error to report) */
+ midiInReset(info->handle.in);
+close_device:
+ midiInClose(info->handle.in); /* ignore return code */
+free_descriptor:
+ midi->api_info = NULL;
+ pm_free(info);
+no_memory:
+ if (pm_hosterror) {
+ report_hosterror_in();
+ return pmHostError;
+ }
+ /* if !pm_hosterror, then the error must be pmInsufficientMemory */
+ return pmInsufficientMemory;
+ /* note: if we return an error code, the device will be
+ closed and memory will be freed. It's up to the caller
+ to free the parameter midi */
+}
+
+
+/* winmm_in_close -- close an open midi input device */
+/*
+ * assume midi is non-null (checked by caller)
+ */
+static PmError winmm_in_close(PmInternal *midi)
+{
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ if (!info) return pmBadPtr;
+ /* device to close */
+ if ((pm_hosterror = midiInStop(info->handle.in))) {
+ midiInReset(info->handle.in); /* try to reset and close port */
+ midiInClose(info->handle.in);
+ } else if ((pm_hosterror = midiInReset(info->handle.in))) {
+ midiInClose(info->handle.in); /* best effort to close midi port */
+ } else {
+ pm_hosterror = midiInClose(info->handle.in);
+ }
+ midi->api_info = NULL;
+ DeleteCriticalSection(&info->lock);
+ pm_free(info); /* delete */
+ if (pm_hosterror) {
+ report_hosterror_in();
+ return pmHostError;
+ }
+ return pmNoError;
+}
+
+
+/* Callback function executed via midiInput SW interrupt (via midiInOpen). */
+static void FAR PASCAL winmm_in_callback(
+ HMIDIIN hMidiIn, /* midiInput device Handle */
+ UINT wMsg, /* midi msg */
+ DWORD_PTR dwInstance, /* application data */
+ DWORD_PTR dwParam1, /* MIDI data */
+ DWORD_PTR dwParam2) /* device timestamp (wrt most recent midiInStart) */
+{
+ PmInternal *midi = (PmInternal *) dwInstance;
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+
+ /* NOTE: we do not just EnterCriticalSection() here because an
+ * MIM_CLOSE message arrives when the port is closed, but then
+ * the info->lock has been destroyed.
+ */
+
+ switch (wMsg) {
+ case MIM_DATA: {
+ /* if this callback is reentered with data, we're in trouble.
+ * It's hard to imagine that Microsoft would allow callbacks
+ * to be reentrant -- isn't the model that this is like a
+ * hardware interrupt? -- but I've seen reentrant behavior
+ * using a debugger, so it happens.
+ */
+ EnterCriticalSection(&info->lock);
+
+ /* dwParam1 is MIDI data received, packed into DWORD w/ 1st byte of
+ message LOB;
+ dwParam2 is time message received by input device driver, specified
+ in [ms] from when midiInStart called.
+ each message is expanded to include the status byte */
+
+ if ((dwParam1 & 0x80) == 0) {
+ /* not a status byte -- ignore it. This happened running the
+ sysex.c test under Win2K with MidiMan USB 1x1 interface,
+ but I can't reproduce it. -RBD
+ */
+ /* printf("non-status byte found\n"); */
+ } else { /* data to process */
+ PmEvent event;
+ if (midi->time_proc)
+ dwParam2 = (*midi->time_proc)(midi->time_info);
+ event.timestamp = (PmTimestamp)dwParam2;
+ event.message = (PmMessage)dwParam1;
+ pm_read_short(midi, &event);
+ }
+ LeaveCriticalSection(&info->lock);
+ break;
+ }
+ case MIM_LONGDATA: {
+ MIDIHDR *lpMidiHdr = (MIDIHDR *) dwParam1;
+ unsigned char *data = (unsigned char *) lpMidiHdr->lpData;
+ unsigned int processed = 0;
+ int remaining = lpMidiHdr->dwBytesRecorded;
+
+ EnterCriticalSection(&info->lock);
+ /* printf("midi_in_callback -- lpMidiHdr %x, %d bytes, %2x...\n",
+ lpMidiHdr, lpMidiHdr->dwBytesRecorded, *data); */
+ if (midi->time_proc)
+ dwParam2 = (*midi->time_proc)(midi->time_info);
+ /* can there be more than one message in one buffer? */
+ /* assume yes and iterate through them */
+ pm_read_bytes(midi, data + processed, remaining, (PmTimestamp)dwParam2);
+
+ /* when a device is closed, the pending MIM_LONGDATA buffers are
+ returned to this callback with dwBytesRecorded == 0. In this
+ case, we do not want to send them back to the interface (if
+ we do, the interface will not close, and Windows OS may hang). */
+ if (lpMidiHdr->dwBytesRecorded > 0) {
+ MMRESULT rslt;
+ lpMidiHdr->dwBytesRecorded = 0;
+ lpMidiHdr->dwFlags = 0;
+
+ /* note: no error checking -- can this actually fail? */
+ rslt = midiInPrepareHeader(hMidiIn, lpMidiHdr, sizeof(MIDIHDR));
+ assert(rslt == MMSYSERR_NOERROR);
+ /* note: I don't think this can fail except possibly for
+ * MMSYSERR_NOMEM, but the pain of reporting this
+ * unlikely but probably catastrophic error does not seem
+ * worth it.
+ */
+ rslt = midiInAddBuffer(hMidiIn, lpMidiHdr, sizeof(MIDIHDR));
+ assert(rslt == MMSYSERR_NOERROR);
+ LeaveCriticalSection(&info->lock);
+ } else {
+ midiInUnprepareHeader(hMidiIn,lpMidiHdr,sizeof(MIDIHDR));
+ LeaveCriticalSection(&info->lock);
+ pm_free(lpMidiHdr);
+ }
+ break;
+ }
+ case MIM_OPEN:
+ break;
+ case MIM_CLOSE:
+ break;
+ case MIM_ERROR:
+ /* printf("MIM_ERROR\n"); */
+ break;
+ case MIM_LONGERROR:
+ /* printf("MIM_LONGERROR\n"); */
+ break;
+ default:
+ break;
+ }
+}
+
+/*
+===========================================================================
+begin midi output implementation
+===========================================================================
+*/
+
+/* begin helper routines used by midiOutStream interface */
+
+/* add_to_buffer -- adds timestamped short msg to buffer, returns fullp */
+static int add_to_buffer(winmm_info_type m, LPMIDIHDR hdr,
+ unsigned long delta, unsigned long msg)
+{
+ unsigned long *ptr = (unsigned long *)
+ (hdr->lpData + hdr->dwBytesRecorded);
+ *ptr++ = delta; /* dwDeltaTime */
+ *ptr++ = 0; /* dwStream */
+ *ptr++ = msg; /* dwEvent */
+ hdr->dwBytesRecorded += 3 * sizeof(long);
+ /* if the addition of three more words (a message) would extend beyond
+ the buffer length, then return TRUE (full)
+ */
+ return hdr->dwBytesRecorded + 3 * sizeof(long) > hdr->dwBufferLength;
+}
+
+
+static PmTimestamp pm_time_get(winmm_info_type info)
+{
+ MMTIME mmtime;
+ MMRESULT wRtn;
+ mmtime.wType = TIME_TICKS;
+ mmtime.u.ticks = 0;
+ wRtn = midiStreamPosition(info->handle.stream, &mmtime, sizeof(mmtime));
+ assert(wRtn == MMSYSERR_NOERROR);
+ return mmtime.u.ticks;
+}
+
+
+/* end helper routines used by midiOutStream interface */
+
+
+static PmError winmm_out_open(PmInternal *midi, void *driverInfo)
+{
+ DWORD dwDevice;
+ int i = midi->device_id;
+ winmm_info_type info;
+ MIDIPROPTEMPO propdata;
+ MIDIPROPTIMEDIV divdata;
+ int max_sysex_len = midi->buffer_len * 4;
+ int output_buffer_len;
+ int num_buffers;
+ dwDevice = (DWORD) (intptr_t) pm_descriptors[i].descriptor;
+
+ /* create system dependent device data */
+ info = winmm_info_create();
+ midi->api_info = info;
+ if (!info) goto no_memory;
+ /* create a signal */
+ info->buffer_signal = CreateEvent(NULL, FALSE, FALSE, NULL);
+ /* this should only fail when there are very serious problems */
+ assert(info->buffer_signal);
+ /* open device */
+ if (midi->latency == 0) {
+ /* use simple midi out calls */
+ pm_hosterror = midiOutOpen(
+ (LPHMIDIOUT) & info->handle.out, /* device Handle */
+ dwDevice, /* device ID */
+ /* note: same callback fn as for StreamOpen: */
+ (DWORD_PTR) winmm_streamout_callback, /* callback fn */
+ (DWORD_PTR) midi, /* callback instance data */
+ CALLBACK_FUNCTION); /* callback type */
+ } else {
+ /* use stream-based midi output (schedulable in future) */
+ pm_hosterror = midiStreamOpen(
+ &info->handle.stream, /* device Handle */
+ (LPUINT) & dwDevice, /* device ID pointer */
+ 1, /* reserved, must be 1 */
+ (DWORD_PTR) winmm_streamout_callback,
+ (DWORD_PTR) midi, /* callback instance data */
+ CALLBACK_FUNCTION);
+ }
+ if (pm_hosterror != MMSYSERR_NOERROR) {
+ goto free_descriptor;
+ }
+
+ if (midi->latency == 0) {
+ num_buffers = NUM_SIMPLE_SYSEX_BUFFERS;
+ output_buffer_len = max_sysex_len / num_buffers;
+ if (output_buffer_len < MIN_SIMPLE_SYSEX_LEN)
+ output_buffer_len = MIN_SIMPLE_SYSEX_LEN;
+ } else {
+ num_buffers = max(midi->buffer_len, midi->latency / 2);
+ if (num_buffers < MIN_STREAM_BUFFERS)
+ num_buffers = MIN_STREAM_BUFFERS;
+ output_buffer_len = STREAM_BUFFER_LEN;
+
+ propdata.cbStruct = sizeof(MIDIPROPTEMPO);
+ propdata.dwTempo = 480000; /* microseconds per quarter */
+ pm_hosterror = midiStreamProperty(info->handle.stream,
+ (LPBYTE) & propdata,
+ MIDIPROP_SET | MIDIPROP_TEMPO);
+ if (pm_hosterror) goto close_device;
+
+ divdata.cbStruct = sizeof(MIDIPROPTEMPO);
+ divdata.dwTimeDiv = 480; /* divisions per quarter */
+ pm_hosterror = midiStreamProperty(info->handle.stream,
+ (LPBYTE) & divdata,
+ MIDIPROP_SET | MIDIPROP_TIMEDIV);
+ if (pm_hosterror) goto close_device;
+ }
+ /* allocate buffers */
+ if (allocate_buffers(info, output_buffer_len, num_buffers))
+ goto free_buffers;
+ /* start device */
+ if (midi->latency != 0) {
+ pm_hosterror = midiStreamRestart(info->handle.stream);
+ if (pm_hosterror != MMSYSERR_NOERROR) goto free_buffers;
+ }
+ return pmNoError;
+
+free_buffers:
+ /* buffers are freed below by winmm_out_delete */
+close_device:
+ midiOutClose(info->handle.out);
+free_descriptor:
+ midi->api_info = NULL;
+ winmm_out_delete(midi); /* frees buffers and m */
+no_memory:
+ if (pm_hosterror) {
+ report_hosterror_out();
+ return pmHostError;
+ }
+ return pmInsufficientMemory;
+}
+
+
+/* winmm_out_delete -- carefully free data associated with midi */
+/**/
+static void winmm_out_delete(PmInternal *midi)
+{
+ int i;
+ /* delete system dependent device data */
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ if (info) {
+ if (info->buffer_signal) {
+ /* don't report errors -- better not to stop cleanup */
+ CloseHandle(info->buffer_signal);
+ }
+ /* if using stream output, free buffers */
+ for (i = 0; i < info->num_buffers; i++) {
+ if (info->buffers[i]) pm_free(info->buffers[i]);
+ }
+ info->num_buffers = 0;
+ pm_free(info->buffers);
+ info->max_buffers = 0;
+ }
+ midi->api_info = NULL;
+ pm_free(info); /* delete */
+}
+
+
+/* see comments for winmm_in_close */
+static PmError winmm_out_close(PmInternal *midi)
+{
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ if (info->handle.out) {
+ /* device to close */
+ if (midi->latency == 0) {
+ pm_hosterror = midiOutClose(info->handle.out);
+ } else {
+ pm_hosterror = midiStreamClose(info->handle.stream);
+ }
+ /* regardless of outcome, free memory */
+ winmm_out_delete(midi);
+ }
+ if (pm_hosterror) {
+ report_hosterror_out();
+ return pmHostError;
+ }
+ return pmNoError;
+}
+
+
+static PmError winmm_out_abort(PmInternal *midi)
+{
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+
+ /* only stop output streams */
+ if (midi->latency > 0) {
+ pm_hosterror = midiStreamStop(info->handle.stream);
+ if (pm_hosterror) {
+ report_hosterror_out();
+ return pmHostError;
+ }
+ }
+ return pmNoError;
+}
+
+
+static PmError winmm_write_flush(PmInternal *midi, PmTimestamp timestamp)
+{
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ assert(info);
+ if (info->hdr) {
+ pm_hosterror = midiOutPrepareHeader(info->handle.out, info->hdr,
+ sizeof(MIDIHDR));
+ if (pm_hosterror) {
+ /* do not send message */
+ } else if (midi->latency == 0) {
+ /* As pointed out by Nigel Brown, 20Sep06, dwBytesRecorded
+ * should be zero. This is set in get_free_sysex_buffer().
+ * The msg length goes in dwBufferLength in spite of what
+ * Microsoft documentation says (or doesn't say). */
+ info->hdr->dwBufferLength = info->hdr->dwBytesRecorded;
+ info->hdr->dwBytesRecorded = 0;
+ pm_hosterror = midiOutLongMsg(info->handle.out, info->hdr,
+ sizeof(MIDIHDR));
+ } else {
+ pm_hosterror = midiStreamOut(info->handle.stream, info->hdr,
+ sizeof(MIDIHDR));
+ }
+ midi->fill_base = NULL;
+ info->hdr = NULL;
+ if (pm_hosterror) {
+ report_hosterror_out();
+ return pmHostError;
+ }
+ }
+ return pmNoError;
+}
+
+
+static PmError winmm_write_short(PmInternal *midi, PmEvent *event)
+{
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ PmError rslt = pmNoError;
+ assert(info);
+
+ if (midi->latency == 0) { /* use midiOut interface, ignore timestamps */
+ pm_hosterror = midiOutShortMsg(info->handle.out, event->message);
+ if (pm_hosterror) {
+ if (info->hdr) { /* device disconnect may delete hdr */
+ info->hdr->dwFlags = 0; /* release the buffer */
+ }
+ report_hosterror_out();
+ return pmHostError;
+ }
+ } else { /* use midiStream interface -- pass data through buffers */
+ unsigned long when = event->timestamp;
+ unsigned long delta;
+ int full;
+ if (when == 0) when = midi->now;
+ /* when is in real_time; translate to intended stream time */
+ when = when + info->delta + midi->latency;
+ /* make sure we don't go backward in time */
+ if (when < info->last_time) when = info->last_time;
+ delta = when - info->last_time;
+ info->last_time = when;
+ /* before we insert any data, we must have a buffer */
+ if (info->hdr == NULL) {
+ /* stream interface: buffers allocated when stream is opened */
+ info->hdr = get_free_output_buffer(midi);
+ }
+ full = add_to_buffer(info, info->hdr, delta, event->message);
+ /* note: winmm_write_flush sets pm_hosterror etc. on host error */
+ if (full) rslt = winmm_write_flush(midi, when);
+ }
+ return rslt;
+}
+
+#define winmm_begin_sysex winmm_write_flush
+#ifndef winmm_begin_sysex
+static PmError winmm_begin_sysex(PmInternal *midi, PmTimestamp timestamp)
+{
+ winmm_info_type m = (winmm_info_type) midi->api_info;
+ PmError rslt = pmNoError;
+
+ if (midi->latency == 0) {
+ /* do nothing -- it's handled in winmm_write_byte */
+ } else {
+ /* sysex expects an empty sysex buffer, so send whatever is here */
+ rslt = winmm_write_flush(midi);
+ }
+ return rslt;
+}
+#endif
+
+static PmError winmm_end_sysex(PmInternal *midi, PmTimestamp timestamp)
+{
+ /* could check for callback_error here, but I haven't checked
+ * what happens if we exit early and don't finish the sysex msg
+ * and clean up
+ */
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ PmError rslt = pmNoError;
+ LPMIDIHDR hdr = info->hdr;
+ if (!hdr) return rslt; /* something bad happened earlier,
+ do not report an error because it would have been
+ reported (at least) once already */
+ /* a(n old) version of MIDI YOKE requires a zero byte after
+ * the sysex message, but do not increment dwBytesRecorded: */
+ hdr->lpData[hdr->dwBytesRecorded] = 0;
+ if (midi->latency == 0) {
+#ifdef DEBUG_PRINT_BEFORE_SENDING_SYSEX
+ /* DEBUG CODE: */
+ { int i; int len = info->hdr->dwBufferLength;
+ printf("OutLongMsg %d ", len);
+ for (i = 0; i < len; i++) {
+ printf("%2x ", (unsigned char) (info->hdr->lpData[i]));
+ }
+ }
+#endif
+ } else {
+ /* Using stream interface. There are accumulated bytes in info->hdr
+ to send using midiStreamOut
+ */
+ /* add bytes recorded to MIDIEVENT length, but don't
+ count the MIDIEVENT data (3 longs) */
+ MIDIEVENT *evt = (MIDIEVENT *) (hdr->lpData);
+ evt->dwEvent += hdr->dwBytesRecorded - 3 * sizeof(long);
+ /* round up BytesRecorded to multiple of 4 */
+ hdr->dwBytesRecorded = (hdr->dwBytesRecorded + 3) & ~3;
+ }
+ rslt = winmm_write_flush(midi, timestamp);
+ return rslt;
+}
+
+
+static PmError winmm_write_byte(PmInternal *midi, unsigned char byte,
+ PmTimestamp timestamp)
+{
+ /* write a sysex byte */
+ PmError rslt = pmNoError;
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ LPMIDIHDR hdr = info->hdr;
+ unsigned char *msg_buffer;
+ assert(info);
+ if (!hdr) {
+ info->hdr = hdr = get_free_output_buffer(midi);
+ assert(hdr);
+ midi->fill_base = (unsigned char *) info->hdr->lpData;
+ midi->fill_offset_ptr = (uint32_t *) &(hdr->dwBytesRecorded);
+ /* when buffer fills, Pm_WriteSysEx will revert to calling
+ * pmwin_write_byte, which expect to have space, so leave
+ * one byte free for pmwin_write_byte. Leave another byte
+ * of space for zero after message to make early version of
+ * MIDI YOKE driver happy -- therefore dwBufferLength - 2 */
+ midi->fill_length = hdr->dwBufferLength - 2;
+ if (midi->latency != 0) {
+ unsigned long when = (unsigned long) timestamp;
+ unsigned long delta;
+ unsigned long *ptr;
+ if (when == 0) when = midi->now;
+ /* when is in real_time; translate to intended stream time */
+ when = when + info->delta + midi->latency;
+ /* make sure we don't go backward in time */
+ if (when < info->last_time) when = info->last_time;
+ delta = when - info->last_time;
+ info->last_time = when;
+
+ ptr = (unsigned long *) hdr->lpData;
+ *ptr++ = delta;
+ *ptr++ = 0;
+ *ptr = MEVT_F_LONG;
+ hdr->dwBytesRecorded = 3 * sizeof(long);
+ /* data will be added at an offset of dwBytesRecorded ... */
+ }
+ }
+ /* add the data byte */
+ msg_buffer = (unsigned char *) (hdr->lpData);
+ msg_buffer[hdr->dwBytesRecorded++] = byte;
+
+ /* see if buffer is full, leave one byte extra for pad */
+ if (hdr->dwBytesRecorded >= hdr->dwBufferLength - 1) {
+ /* write what we've got and continue */
+ rslt = winmm_end_sysex(midi, timestamp);
+ }
+ return rslt;
+}
+
+
+static PmTimestamp winmm_synchronize(PmInternal *midi)
+{
+ winmm_info_type info;
+ unsigned long pm_stream_time_2;
+ unsigned long real_time;
+ unsigned long pm_stream_time;
+
+ /* only synchronize if we are using stream interface */
+ if (midi->latency == 0) return 0;
+
+ /* figure out the time */
+ info = (winmm_info_type) midi->api_info;
+ pm_stream_time_2 = pm_time_get(info);
+
+ do {
+ /* read real_time between two reads of stream time */
+ pm_stream_time = pm_stream_time_2;
+ real_time = (*midi->time_proc)(midi->time_info);
+ pm_stream_time_2 = pm_time_get(info);
+ /* repeat if more than 1ms elapsed */
+ } while (pm_stream_time_2 > pm_stream_time + 1);
+ info->delta = pm_stream_time - real_time;
+ info->sync_time = real_time;
+ return real_time;
+}
+
+
+/* winmm_streamout_callback -- unprepare (free) buffer header */
+static void CALLBACK winmm_streamout_callback(HMIDIOUT hmo, UINT wMsg,
+ DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
+{
+ PmInternal *midi = (PmInternal *) dwInstance;
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ LPMIDIHDR hdr = (LPMIDIHDR) dwParam1;
+ int err;
+
+ /* Even if an error is pending, I think we should unprepare msgs and
+ signal their arrival
+ */
+ /* printf("streamout_callback: hdr %x, wMsg %x, MOM_DONE %x\n",
+ hdr, wMsg, MOM_DONE); */
+ if (wMsg == MOM_DONE) {
+ MMRESULT ret = midiOutUnprepareHeader(info->handle.out, hdr,
+ sizeof(MIDIHDR));
+ assert(ret == MMSYSERR_NOERROR);
+ } else if (wMsg == MOM_CLOSE) {
+ /* The streaming API gets a callback when the device is closed.
+ * The non-streaming API gets a callback when the device is
+ * removed or closed. It is misleading to set is_removed when
+ * the device is closed normally, but in that case, midi itself
+ * will be freed immediately, so there should be no way to
+ * observe is_removed == TRUE. On the other hand, if the device
+ * is removed, setting is_removed will cause PortMidi to return
+ * the pmDeviceRemoved error on attempts to output to the device.
+ * In the case of normal closing, due to midiOutClose(),
+ * the call below is reentrant (!), but for some reason this does
+ * not cause an error or infinite recursion, so we are not taking
+ * any precautions to flag midi as "in the process of closing."
+ */
+ midi->is_removed = TRUE;
+ midiOutClose(info->handle.out);
+ }
+ /* signal client in case it is blocked waiting for buffer */
+ err = SetEvent(info->buffer_signal);
+ assert(err); /* false -> error */
+}
+
+
+/*
+===========================================================================
+begin exported functions
+===========================================================================
+*/
+
+#define winmm_in_abort pm_fail_fn
+pm_fns_node pm_winmm_in_dictionary = {
+ none_write_short,
+ none_sysex,
+ none_sysex,
+ none_write_byte,
+ none_write_short,
+ none_write_flush,
+ winmm_synchronize,
+ winmm_in_open,
+ winmm_in_abort,
+ winmm_in_close,
+ success_poll,
+ winmm_check_host_error
+ };
+
+pm_fns_node pm_winmm_out_dictionary = {
+ winmm_write_short,
+ winmm_begin_sysex,
+ winmm_end_sysex,
+ winmm_write_byte,
+ /* short realtime message: */ winmm_write_short,
+ winmm_write_flush,
+ winmm_synchronize,
+ winmm_out_open,
+ winmm_out_abort,
+ winmm_out_close,
+ none_poll,
+ winmm_check_host_error
+ };
+
+
+/* initialize winmm interface. Note that if there is something wrong
+ with winmm (e.g. it is not supported or installed), it is not an
+ error. We should simply return without having added any devices to
+ the table. Hence, no error code is returned. Furthermore, this init
+ code is called along with every other supported interface, so the
+ user would have a very hard time figuring out what hardware and API
+ generated the error. Finally, it would add complexity to pmwin.c to
+ remember where the error code came from in order to convert to text.
+ */
+void pm_winmm_init( void )
+{
+ pm_winmm_mapper_input();
+ pm_winmm_mapper_output();
+ pm_winmm_general_inputs();
+ pm_winmm_general_outputs();
+}
+
+
+/* no error codes are returned, even if errors are encountered, because
+ there is probably nothing the user could do (e.g. it would be an error
+ to retry.
+ */
+void pm_winmm_term( void )
+{
+ int i;
+#ifdef MMDEBUG
+ int doneAny = 0;
+ printf("pm_winmm_term called\n");
+#endif
+ for (i = 0; i < pm_descriptor_len; i++) {
+ PmInternal *midi = pm_descriptors[i].pm_internal;
+ if (midi) {
+ winmm_info_type info = (winmm_info_type) midi->api_info;
+ if (info->handle.out) {
+ /* close next open device*/
+#ifdef MMDEBUG
+ if (doneAny == 0) {
+ printf("begin closing open devices...\n");
+ doneAny = 1;
+ }
+#endif
+ /* close all open ports */
+ (*midi->dictionary->close)(midi);
+ }
+ }
+ }
+ if (midi_in_caps) {
+ pm_free(midi_in_caps);
+ midi_in_caps = NULL;
+ }
+ if (midi_out_caps) {
+ pm_free(midi_out_caps);
+ midi_out_caps = NULL;
+ }
+#ifdef MMDEBUG
+ if (doneAny) {
+ printf("warning: devices were left open. They have been closed.\n");
+ }
+ printf("pm_winmm_term exiting\n");
+#endif
+ pm_descriptor_len = 0;
+}
diff --git a/portmidi/pm_win/pmwinmm.h b/portmidi/pm_win/pmwinmm.h new file mode 100755 index 0000000..4a353e2 --- /dev/null +++ b/portmidi/pm_win/pmwinmm.h @@ -0,0 +1,5 @@ +/* pmwinmm.h -- system-specific definitions for windows multimedia API */
+
+void pm_winmm_init( void );
+void pm_winmm_term( void );
+
diff --git a/portmidi/pm_win/static.cmake b/portmidi/pm_win/static.cmake new file mode 100644 index 0000000..7fdad18 --- /dev/null +++ b/portmidi/pm_win/static.cmake @@ -0,0 +1,24 @@ +# static.cmake -- change flags to link with static runtime libraries
+#
+# Even when BUILD_SHARED_LIBS is OFF, CMake specifies linking wtih
+# multithread DLL, so you give inconsistent linking instructions
+# resulting in warning messages from MS Visual Studio. If you want
+# a static binary, I've found this approach works to eliminate
+# warnings and make everything static:
+#
+# Changes /MD (multithread DLL) to /MT (multithread static)
+
+if(MSVC)
+ foreach(flag_var
+ CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
+ CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO
+ CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE
+ CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO)
+ if(${flag_var} MATCHES "/MD")
+ string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
+ endif(${flag_var} MATCHES "/MD")
+ endforeach(flag_var)
+
+ message(STATUS
+ "Note: overriding CMAKE_*_FLAGS_* to use Visual C static multithread library")
+endif(MSVC)
diff --git a/portmidi/portmusic_logo.png b/portmidi/portmusic_logo.png Binary files differnew file mode 100644 index 0000000..17a063a --- /dev/null +++ b/portmidi/portmusic_logo.png diff --git a/portmidi/porttime/porttime.c b/portmidi/porttime/porttime.c new file mode 100755 index 0000000..71b06f4 --- /dev/null +++ b/portmidi/porttime/porttime.c @@ -0,0 +1,3 @@ +/* porttime.c -- portable API for millisecond timer */
+
+/* There is no machine-independent implementation code to put here */
diff --git a/portmidi/porttime/porttime.h b/portmidi/porttime/porttime.h new file mode 100755 index 0000000..0a61c5c --- /dev/null +++ b/portmidi/porttime/porttime.h @@ -0,0 +1,103 @@ +/** @file porttime.h portable interface to millisecond timer. */ + +/* CHANGE LOG FOR PORTTIME + 10-Jun-03 Mark Nelson & RBD + boost priority of timer thread in ptlinux.c implementation + */ + +#ifndef PORTMIDI_PORTTIME_H +#define PORTMIDI_PORTTIME_H + +/* Should there be a way to choose the source of time here? */ + +#ifdef WIN32 +#ifndef INT32_DEFINED +// rather than having users install a special .h file for windows, +// just put the required definitions inline here. portmidi.h uses +// these too, so the definitions are (unfortunately) duplicated there +typedef int int32_t; +typedef unsigned int uint32_t; +#define INT32_DEFINED +#endif +#else +#include <stdint.h> // needed for int32_t +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef PMEXPORT +#ifdef _WINDLL +#define PMEXPORT __declspec(dllexport) +#else +#define PMEXPORT +#endif +#endif + +/** @defgroup grp_porttime PortTime: Millisecond Timer + @{ +*/ + +typedef enum { + ptNoError = 0, /* success */ + ptHostError = -10000, /* a system-specific error occurred */ + ptAlreadyStarted, /* cannot start timer because it is already started */ + ptAlreadyStopped, /* cannot stop timer because it is already stopped */ + ptInsufficientMemory /* memory could not be allocated */ +} PtError; /**< @brief @enum PtError PortTime error code; a common return type. + * No error is indicated by zero; errors are indicated by < 0. + */ + +/** real time or time offset in milliseconds. */ +typedef int32_t PtTimestamp; + +/** a function that gets a current time */ +typedef void (PtCallback)(PtTimestamp timestamp, void *userData); + +/** start a real-time clock service. + + @param resolution the timer resolution in ms. The time will advance every + \p resolution ms. + + @param callback a function pointer to be called every resolution ms. + + @param userData is passed to \p callback as a parameter. + + @return #ptNoError on success. See #PtError for other values. +*/ +PMEXPORT PtError Pt_Start(int resolution, PtCallback *callback, void *userData); + +/** stop the timer. + + @return #ptNoError on success. See #PtError for other values. +*/ +PMEXPORT PtError Pt_Stop(void); + +/** test if the timer is running. + + @return TRUE or FALSE +*/ +PMEXPORT int Pt_Started(void); + +/** get the current time in ms. + + @return the current time +*/ +PMEXPORT PtTimestamp Pt_Time(void); + +/** pauses the current thread, allowing other threads to run. + + @param duration the length of the pause in ms. The true duration + of the pause may be rounded to the nearest or next clock tick + as determined by resolution in #Pt_Start(). +*/ +PMEXPORT void Pt_Sleep(int32_t duration); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif // PORTMIDI_PORTTIME_H diff --git a/portmidi/porttime/pthaiku.cpp b/portmidi/porttime/pthaiku.cpp new file mode 100644 index 0000000..9d8de14 --- /dev/null +++ b/portmidi/porttime/pthaiku.cpp @@ -0,0 +1,88 @@ +// pthaiku.cpp - portable timer implementation for Haiku + +#include "porttime.h" +#include <Looper.h> +#include <MessageRunner.h> +#include <OS.h> + +namespace { + const uint32 timerMessage = 'PTTM'; + + struct TimerLooper : BLooper { + TimerLooper() : BLooper() { + } + + + virtual void MessageReceived(BMessage *message) + { + PtCallback *callback; + void *userData; + if (message->what == timerMessage && message->FindPointer("callback", (void**)&callback) == B_OK && message->FindPointer("userData", &userData) == B_OK) { + (*callback)(Pt_Time(), userData); + } + BLooper::MessageReceived(message); + } + }; + + bool time_started_flag = false; + bigtime_t time_offset; + TimerLooper *timerLooper; + BMessageRunner *timerRunner; +} + +extern "C" { + PtError Pt_Start(int resolution, PtCallback *callback, void *userData) + { + if (time_started_flag) return ptAlreadyStarted; + time_offset = system_time(); + if (callback) { + timerLooper = new TimerLooper; + timerLooper->Run(); + BMessenger target(timerLooper); + BMessage message(timerMessage); + message.AddPointer("callback", (void*)callback); + message.AddPointer("userData", userData); + bigtime_t interval = resolution * 1000; + timerRunner = new BMessageRunner(target, &message, interval); + if(timerRunner->InitCheck() != B_OK) { + delete timerRunner; + timerRunner = NULL; + timerLooper->PostMessage(B_QUIT_REQUESTED); + timerLooper = NULL; + return ptHostError; + } + } + time_started_flag = true; + return ptNoError; + } + + + PtError Pt_Stop() + { + if (!time_started_flag) return ptAlreadyStopped; + time_started_flag = false; + delete timerRunner; + timerRunner = NULL; + timerLooper->PostMessage(B_QUIT_REQUESTED); + timerLooper = NULL; + return ptNoError; + } + + + int Pt_Started() + { + return time_started_flag; + } + + + PtTimestamp Pt_Time() + { + return (system_time() - time_offset) / 1000; + } + + + void Pt_Sleep(int32_t duration) + { + snooze(duration * 1000); + } +} diff --git a/portmidi/porttime/ptlinux.c b/portmidi/porttime/ptlinux.c new file mode 100755 index 0000000..c4af5c1 --- /dev/null +++ b/portmidi/porttime/ptlinux.c @@ -0,0 +1,139 @@ +/* ptlinux.c -- portable timer implementation for linux */ + + +/* IMPLEMENTATION NOTES (by Mark Nelson): + +Unlike Windows, Linux has no system call to request a periodic callback, +so if Pt_Start() receives a callback parameter, it must create a thread +that wakes up periodically and calls the provided callback function. +If running as superuser, use setpriority() to renice thread to -20. +One could also set the timer thread to a real-time priority (SCHED_FIFO +and SCHED_RR), but this is dangerous for This is necessary because +if the callback hangs it'll never return. A more serious reason +is that the current scheduler implementation busy-waits instead +of sleeping when realtime threads request a sleep of <=2ms (as a way +to get around the 10ms granularity), which means the thread would never +let anyone else on the CPU. + +CHANGE LOG + +18-Jul-03 Roger Dannenberg -- Simplified code to set priority of timer + thread. Simplified implementation notes. + +*/ +/* stdlib, stdio, unistd, and sys/types were added because they appeared + * in a Gentoo patch, but I'm not sure why they are needed. -RBD + */ +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/types.h> +#include "porttime.h" +#include "time.h" +#include "sys/resource.h" +#include "pthread.h" + +#define TRUE 1 +#define FALSE 0 + +#ifndef CLOCK_MONOTONIC_RAW +#define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC +#endif + +static int time_started_flag = FALSE; +static struct timespec time_offset = {0, 0}; +static pthread_t pt_thread_pid; +static int pt_thread_created = FALSE; + +/* note that this is static data -- we only need one copy */ +typedef struct { + int id; + int resolution; + PtCallback *callback; + void *userData; +} pt_callback_parameters; + +static int pt_callback_proc_id = 0; + +static void *Pt_CallbackProc(void *p) +{ + pt_callback_parameters *parameters = (pt_callback_parameters *) p; + int mytime = 1; + /* to kill a process, just increment the pt_callback_proc_id */ + /* printf("pt_callback_proc_id %d, id %d\n", pt_callback_proc_id, + parameters->id); */ + if (geteuid() == 0) setpriority(PRIO_PROCESS, 0, -20); + while (pt_callback_proc_id == parameters->id) { + /* wait for a multiple of resolution ms */ + struct timeval timeout; + int delay = mytime++ * parameters->resolution - Pt_Time(); + if (delay < 0) delay = 0; + timeout.tv_sec = 0; + timeout.tv_usec = delay * 1000; + select(0, NULL, NULL, NULL, &timeout); + (*(parameters->callback))(Pt_Time(), parameters->userData); + } + /* printf("Pt_CallbackProc exiting\n"); */ + // free(parameters); + return NULL; +} + + +PtError Pt_Start(int resolution, PtCallback *callback, void *userData) +{ + if (time_started_flag) return ptNoError; + /* need this set before process runs: */ + clock_gettime(CLOCK_MONOTONIC_RAW, &time_offset); + if (callback) { + int res; + pt_callback_parameters *parms = (pt_callback_parameters *) + malloc(sizeof(pt_callback_parameters)); + if (!parms) return ptInsufficientMemory; + parms->id = pt_callback_proc_id; + parms->resolution = resolution; + parms->callback = callback; + parms->userData = userData; + res = pthread_create(&pt_thread_pid, NULL, + Pt_CallbackProc, parms); + if (res != 0) return ptHostError; + pt_thread_created = TRUE; + } + time_started_flag = TRUE; + return ptNoError; +} + + +PtError Pt_Stop() +{ + /* printf("Pt_Stop called\n"); */ + pt_callback_proc_id++; + if (pt_thread_created) { + pthread_join(pt_thread_pid, NULL); + pt_thread_created = FALSE; + } + time_started_flag = FALSE; + return ptNoError; +} + + +int Pt_Started() +{ + return time_started_flag; +} + + +PtTimestamp Pt_Time() +{ + long seconds, ms; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC_RAW, &now); + seconds = now.tv_sec - time_offset.tv_sec; + ms = (now.tv_nsec - time_offset.tv_nsec) / 1000000; /* round down */ + return seconds * 1000 + ms; +} + + +void Pt_Sleep(int32_t duration) +{ + usleep(duration * 1000); +} diff --git a/portmidi/porttime/ptmacosx_cf.c b/portmidi/porttime/ptmacosx_cf.c new file mode 100755 index 0000000..a174c86 --- /dev/null +++ b/portmidi/porttime/ptmacosx_cf.c @@ -0,0 +1,140 @@ +/* ptmacosx.c -- portable timer implementation for mac os x */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <pthread.h>
+#include <CoreFoundation/CoreFoundation.h>
+
+#import <mach/mach.h>
+#import <mach/mach_error.h>
+#import <mach/mach_time.h>
+#import <mach/clock.h>
+
+#include "porttime.h"
+
+#define THREAD_IMPORTANCE 30
+#define LONG_TIME 1000000000.0
+
+static int time_started_flag = FALSE;
+static CFAbsoluteTime startTime = 0.0;
+static CFRunLoopRef timerRunLoop;
+
+typedef struct {
+ int resolution;
+ PtCallback *callback;
+ void *userData;
+} PtThreadParams;
+
+
+void Pt_CFTimerCallback(CFRunLoopTimerRef timer, void *info)
+{
+ PtThreadParams *params = (PtThreadParams*)info;
+ (*params->callback)(Pt_Time(), params->userData);
+}
+
+static void* Pt_Thread(void *p)
+{
+ CFTimeInterval timerInterval;
+ CFRunLoopTimerContext timerContext;
+ CFRunLoopTimerRef timer;
+ PtThreadParams *params = (PtThreadParams*)p;
+ //CFTimeInterval timeout;
+
+ /* raise the thread's priority */
+ kern_return_t error;
+ thread_extended_policy_data_t extendedPolicy;
+ thread_precedence_policy_data_t precedencePolicy;
+
+ extendedPolicy.timeshare = 0;
+ error = thread_policy_set(mach_thread_self(), THREAD_EXTENDED_POLICY,
+ (thread_policy_t)&extendedPolicy,
+ THREAD_EXTENDED_POLICY_COUNT);
+ if (error != KERN_SUCCESS) {
+ mach_error("Couldn't set thread timeshare policy", error);
+ }
+
+ precedencePolicy.importance = THREAD_IMPORTANCE;
+ error = thread_policy_set(mach_thread_self(), THREAD_PRECEDENCE_POLICY,
+ (thread_policy_t)&precedencePolicy,
+ THREAD_PRECEDENCE_POLICY_COUNT);
+ if (error != KERN_SUCCESS) {
+ mach_error("Couldn't set thread precedence policy", error);
+ }
+
+ /* set up the timer context */
+ timerContext.version = 0;
+ timerContext.info = params;
+ timerContext.retain = NULL;
+ timerContext.release = NULL;
+ timerContext.copyDescription = NULL;
+
+ /* create a new timer */
+ timerInterval = (double)params->resolution / 1000.0;
+ timer = CFRunLoopTimerCreate(NULL, startTime+timerInterval, timerInterval,
+ 0, 0, Pt_CFTimerCallback, &timerContext);
+
+ timerRunLoop = CFRunLoopGetCurrent();
+ CFRunLoopAddTimer(timerRunLoop, timer, CFSTR("PtTimeMode"));
+
+ /* run until we're told to stop by Pt_Stop() */
+ CFRunLoopRunInMode(CFSTR("PtTimeMode"), LONG_TIME, false);
+
+ CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), timer, CFSTR("PtTimeMode"));
+ CFRelease(timer);
+ free(params);
+
+ return NULL;
+}
+
+PtError Pt_Start(int resolution, PtCallback *callback, void *userData)
+{
+ PtThreadParams *params = (PtThreadParams*)malloc(sizeof(PtThreadParams));
+ pthread_t pthread_id;
+
+ printf("Pt_Start() called\n");
+
+ // /* make sure we're not already playing */
+ if (time_started_flag) return ptAlreadyStarted;
+ startTime = CFAbsoluteTimeGetCurrent();
+
+ if (callback) {
+
+ params->resolution = resolution;
+ params->callback = callback;
+ params->userData = userData;
+
+ pthread_create(&pthread_id, NULL, Pt_Thread, params);
+ }
+
+ time_started_flag = TRUE;
+ return ptNoError;
+}
+
+
+PtError Pt_Stop()
+{
+ printf("Pt_Stop called\n");
+
+ CFRunLoopStop(timerRunLoop);
+ time_started_flag = FALSE;
+ return ptNoError;
+}
+
+
+int Pt_Started()
+{
+ return time_started_flag;
+}
+
+
+PtTimestamp Pt_Time()
+{
+ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
+ return (PtTimestamp) ((now - startTime) * 1000.0);
+}
+
+
+void Pt_Sleep(int32_t duration)
+{
+ usleep(duration * 1000);
+}
diff --git a/portmidi/porttime/ptmacosx_mach.c b/portmidi/porttime/ptmacosx_mach.c new file mode 100755 index 0000000..1390af8 --- /dev/null +++ b/portmidi/porttime/ptmacosx_mach.c @@ -0,0 +1,204 @@ +/* ptmacosx.c -- portable timer implementation for mac os x */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <CoreAudio/HostTime.h>
+
+#import <mach/mach.h>
+#import <mach/mach_error.h>
+#import <mach/mach_time.h>
+#import <mach/clock.h>
+#include <unistd.h>
+#include <AvailabilityMacros.h>
+
+#include "porttime.h"
+#include "sys/time.h"
+#include "pthread.h"
+
+#ifndef NSEC_PER_MSEC
+#define NSEC_PER_MSEC 1000000
+#endif
+#define THREAD_IMPORTANCE 63
+
+// QOS headers are available as of macOS 10.10
+#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
+#include "sys/qos.h"
+#define HAVE_APPLE_QOS 1
+#else
+#undef HAVE_APPLE_QOS
+#endif
+
+static int time_started_flag = FALSE;
+static UInt64 start_time;
+static pthread_t pt_thread_pid;
+
+/* note that this is static data -- we only need one copy */
+typedef struct {
+ int id;
+ int resolution;
+ PtCallback *callback;
+ void *userData;
+} pt_callback_parameters;
+
+static int pt_callback_proc_id = 0;
+
+static void *Pt_CallbackProc(void *p)
+{
+ pt_callback_parameters *parameters = (pt_callback_parameters *) p;
+ int mytime = 1;
+
+ kern_return_t error;
+ thread_extended_policy_data_t extendedPolicy;
+ thread_precedence_policy_data_t precedencePolicy;
+
+ extendedPolicy.timeshare = 0;
+ error = thread_policy_set(mach_thread_self(), THREAD_EXTENDED_POLICY,
+ (thread_policy_t)&extendedPolicy,
+ THREAD_EXTENDED_POLICY_COUNT);
+ if (error != KERN_SUCCESS) {
+ mach_error("Couldn't set thread timeshare policy", error);
+ }
+
+ precedencePolicy.importance = THREAD_IMPORTANCE;
+ error = thread_policy_set(mach_thread_self(), THREAD_PRECEDENCE_POLICY,
+ (thread_policy_t)&precedencePolicy,
+ THREAD_PRECEDENCE_POLICY_COUNT);
+ if (error != KERN_SUCCESS) {
+ mach_error("Couldn't set thread precedence policy", error);
+ }
+
+ // Most important, set real-time constraints.
+
+ // Define the guaranteed and max fraction of time for the audio thread.
+ // These "duty cycle" values can range from 0 to 1. A value of 0.5
+ // means the scheduler would give half the time to the thread.
+ // These values have empirically been found to yield good behavior.
+ // Good means that audio performance is high and other threads won't starve.
+ const double kGuaranteedAudioDutyCycle = 0.75;
+ const double kMaxAudioDutyCycle = 0.85;
+
+ // Define constants determining how much time the audio thread can
+ // use in a given time quantum. All times are in milliseconds.
+
+ // About 128 frames @44.1KHz
+ const double kTimeQuantum = 2.9;
+
+ // Time guaranteed each quantum.
+ const double kAudioTimeNeeded = kGuaranteedAudioDutyCycle * kTimeQuantum;
+
+ // Maximum time each quantum.
+ const double kMaxTimeAllowed = kMaxAudioDutyCycle * kTimeQuantum;
+
+ // Get the conversion factor from milliseconds to absolute time
+ // which is what the time-constraints call needs.
+ mach_timebase_info_data_t tb_info;
+ mach_timebase_info(&tb_info);
+ double ms_to_abs_time =
+ ((double)tb_info.denom / (double)tb_info.numer) * 1000000;
+
+ thread_time_constraint_policy_data_t time_constraints;
+ time_constraints.period = (uint32_t)(kTimeQuantum * ms_to_abs_time);
+ time_constraints.computation = (uint32_t)(kAudioTimeNeeded * ms_to_abs_time);
+ time_constraints.constraint = (uint32_t)(kMaxTimeAllowed * ms_to_abs_time);
+ time_constraints.preemptible = 0;
+
+ error = thread_policy_set(mach_thread_self(),
+ THREAD_TIME_CONSTRAINT_POLICY,
+ (thread_policy_t)&time_constraints,
+ THREAD_TIME_CONSTRAINT_POLICY_COUNT);
+ if (error != KERN_SUCCESS) {
+ mach_error("Couldn't set thread precedence policy", error);
+ }
+
+ /* to kill a process, just increment the pt_callback_proc_id */
+ /* printf("pt_callback_proc_id %d, id %d\n", pt_callback_proc_id,
+ parameters->id); */
+ while (pt_callback_proc_id == parameters->id) {
+ /* wait for a multiple of resolution ms */
+ UInt64 wait_time;
+ int delay = mytime++ * parameters->resolution - Pt_Time();
+ PtTimestamp timestamp;
+ if (delay < 0) delay = 0;
+ wait_time = AudioConvertNanosToHostTime((UInt64)delay * NSEC_PER_MSEC);
+ wait_time += AudioGetCurrentHostTime();
+ mach_wait_until(wait_time);
+ timestamp = Pt_Time();
+ (*(parameters->callback))(timestamp, parameters->userData);
+ }
+ free(parameters);
+ return NULL;
+}
+
+
+PtError Pt_Start(int resolution, PtCallback *callback, void *userData)
+{
+ if (time_started_flag) return ptAlreadyStarted;
+ start_time = AudioGetCurrentHostTime();
+
+ if (callback) {
+ int res;
+ pt_callback_parameters *parms;
+
+ parms = (pt_callback_parameters *) malloc(sizeof(pt_callback_parameters));
+ if (!parms) return ptInsufficientMemory;
+ parms->id = pt_callback_proc_id;
+ parms->resolution = resolution;
+ parms->callback = callback;
+ parms->userData = userData;
+
+#ifdef HAVE_APPLE_QOS
+ pthread_attr_t qosAttribute;
+ pthread_attr_init(&qosAttribute);
+ pthread_attr_set_qos_class_np(&qosAttribute,
+ QOS_CLASS_USER_INTERACTIVE, 0);
+
+ res = pthread_create(&pt_thread_pid, &qosAttribute, Pt_CallbackProc,
+ parms);
+#else
+ res = pthread_create(&pt_thread_pid, NULL, Pt_CallbackProc, parms);
+#endif
+
+ struct sched_param sp;
+ memset(&sp, 0, sizeof(struct sched_param));
+ sp.sched_priority = sched_get_priority_max(SCHED_RR);
+ if (pthread_setschedparam(pthread_self(), SCHED_RR, &sp) == -1) {
+ return ptHostError;
+ }
+
+ if (res != 0) return ptHostError;
+ }
+
+ time_started_flag = TRUE;
+ return ptNoError;
+}
+
+
+PtError Pt_Stop(void)
+{
+ /* printf("Pt_Stop called\n"); */
+ pt_callback_proc_id++;
+ pthread_join(pt_thread_pid, NULL);
+ time_started_flag = FALSE;
+ return ptNoError;
+}
+
+
+int Pt_Started(void)
+{
+ return time_started_flag;
+}
+
+
+PtTimestamp Pt_Time(void)
+{
+ UInt64 clock_time, nsec_time;
+ clock_time = AudioGetCurrentHostTime() - start_time;
+ nsec_time = AudioConvertHostTimeToNanos(clock_time);
+ return (PtTimestamp)(nsec_time / NSEC_PER_MSEC);
+}
+
+
+void Pt_Sleep(int32_t duration)
+{
+ usleep(duration * 1000);
+}
diff --git a/portmidi/porttime/ptwinmm.c b/portmidi/porttime/ptwinmm.c new file mode 100755 index 0000000..5204659 --- /dev/null +++ b/portmidi/porttime/ptwinmm.c @@ -0,0 +1,70 @@ +/* ptwinmm.c -- portable timer implementation for win32 */
+
+
+#include "porttime.h"
+#include "windows.h"
+#include "time.h"
+
+
+TIMECAPS caps;
+
+static long time_offset = 0;
+static int time_started_flag = FALSE;
+static long time_resolution;
+static MMRESULT timer_id;
+static PtCallback *time_callback;
+
+void CALLBACK winmm_time_callback(UINT uID, UINT uMsg, DWORD_PTR dwUser,
+ DWORD_PTR dw1, DWORD_PTR dw2)
+{
+ (*time_callback)(Pt_Time(), (void *) dwUser);
+}
+
+
+PMEXPORT PtError Pt_Start(int resolution, PtCallback *callback, void *userData)
+{
+ if (time_started_flag) return ptAlreadyStarted;
+ timeBeginPeriod(resolution);
+ time_resolution = resolution;
+ time_offset = timeGetTime();
+ time_started_flag = TRUE;
+ time_callback = callback;
+ if (callback) {
+ timer_id = timeSetEvent(resolution, 1, winmm_time_callback,
+ (DWORD_PTR) userData, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
+ if (!timer_id) return ptHostError;
+ }
+ return ptNoError;
+}
+
+
+PMEXPORT PtError Pt_Stop()
+{
+ if (!time_started_flag) return ptAlreadyStopped;
+ if (time_callback && timer_id) {
+ timeKillEvent(timer_id);
+ time_callback = NULL;
+ timer_id = 0;
+ }
+ time_started_flag = FALSE;
+ timeEndPeriod(time_resolution);
+ return ptNoError;
+}
+
+
+PMEXPORT int Pt_Started()
+{
+ return time_started_flag;
+}
+
+
+PMEXPORT PtTimestamp Pt_Time()
+{
+ return timeGetTime() - time_offset;
+}
+
+
+PMEXPORT void Pt_Sleep(int32_t duration)
+{
+ Sleep(duration);
+}
@@ -0,0 +1,531 @@ +/* TinyMidiLoader - v0.7 - Minimalistic midi parsing library - https://github.com/schellingb/TinySoundFont
+ no warranty implied; use at your own risk
+ Do this:
+ #define TML_IMPLEMENTATION
+ before you include this file in *one* C or C++ file to create the implementation.
+ // i.e. it should look like this:
+ #include ...
+ #include ...
+ #define TML_IMPLEMENTATION
+ #include "tml.h"
+
+ [OPTIONAL] #define TML_NO_STDIO to remove stdio dependency
+ [OPTIONAL] #define TML_MALLOC, TML_REALLOC, and TML_FREE to avoid stdlib.h
+ [OPTIONAL] #define TML_MEMCPY to avoid string.h
+
+ LICENSE (ZLIB)
+
+ Copyright (C) 2017, 2018, 2020 Bernhard Schelling
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+*/
+
+#ifndef TML_INCLUDE_TML_INL
+#define TML_INCLUDE_TML_INL
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Define this if you want the API functions to be static
+#ifdef TML_STATIC
+#define TMLDEF static
+#else
+#define TMLDEF extern
+#endif
+
+// Channel message type
+enum TMLMessageType
+{
+ TML_NOTE_OFF = 0x80, TML_NOTE_ON = 0x90, TML_KEY_PRESSURE = 0xA0, TML_CONTROL_CHANGE = 0xB0, TML_PROGRAM_CHANGE = 0xC0, TML_CHANNEL_PRESSURE = 0xD0, TML_PITCH_BEND = 0xE0, TML_SET_TEMPO = 0x51
+};
+
+// Midi controller numbers
+enum TMLController
+{
+ TML_BANK_SELECT_MSB, TML_MODULATIONWHEEL_MSB, TML_BREATH_MSB, TML_FOOT_MSB = 4, TML_PORTAMENTO_TIME_MSB, TML_DATA_ENTRY_MSB, TML_VOLUME_MSB,
+ TML_BALANCE_MSB, TML_PAN_MSB = 10, TML_EXPRESSION_MSB, TML_EFFECTS1_MSB, TML_EFFECTS2_MSB, TML_GPC1_MSB = 16, TML_GPC2_MSB, TML_GPC3_MSB, TML_GPC4_MSB,
+ TML_BANK_SELECT_LSB = 32, TML_MODULATIONWHEEL_LSB, TML_BREATH_LSB, TML_FOOT_LSB = 36, TML_PORTAMENTO_TIME_LSB, TML_DATA_ENTRY_LSB, TML_VOLUME_LSB,
+ TML_BALANCE_LSB, TML_PAN_LSB = 42, TML_EXPRESSION_LSB, TML_EFFECTS1_LSB, TML_EFFECTS2_LSB, TML_GPC1_LSB = 48, TML_GPC2_LSB, TML_GPC3_LSB, TML_GPC4_LSB,
+ TML_SUSTAIN_SWITCH = 64, TML_PORTAMENTO_SWITCH, TML_SOSTENUTO_SWITCH, TML_SOFT_PEDAL_SWITCH, TML_LEGATO_SWITCH, TML_HOLD2_SWITCH,
+ TML_SOUND_CTRL1, TML_SOUND_CTRL2, TML_SOUND_CTRL3, TML_SOUND_CTRL4, TML_SOUND_CTRL5, TML_SOUND_CTRL6,
+ TML_SOUND_CTRL7, TML_SOUND_CTRL8, TML_SOUND_CTRL9, TML_SOUND_CTRL10, TML_GPC5, TML_GPC6, TML_GPC7, TML_GPC8,
+ TML_PORTAMENTO_CTRL, TML_FX_REVERB = 91, TML_FX_TREMOLO, TML_FX_CHORUS, TML_FX_CELESTE_DETUNE, TML_FX_PHASER,
+ TML_DATA_ENTRY_INCR, TML_DATA_ENTRY_DECR, TML_NRPN_LSB, TML_NRPN_MSB, TML_RPN_LSB, TML_RPN_MSB,
+ TML_ALL_SOUND_OFF = 120, TML_ALL_CTRL_OFF, TML_LOCAL_CONTROL, TML_ALL_NOTES_OFF, TML_OMNI_OFF, TML_OMNI_ON, TML_POLY_OFF, TML_POLY_ON
+};
+
+// A single MIDI message linked to the next message in time
+typedef struct tml_message
+{
+ // Time of the message in milliseconds
+ unsigned int time;
+
+ // Type (see TMLMessageType) and channel number
+ unsigned char type, channel;
+
+ // 2 byte of parameter data based on the type:
+ // - key, velocity for TML_NOTE_ON and TML_NOTE_OFF messages
+ // - key, key_pressure for TML_KEY_PRESSURE messages
+ // - control, control_value for TML_CONTROL_CHANGE messages (see TMLController)
+ // - program for TML_PROGRAM_CHANGE messages
+ // - channel_pressure for TML_CHANNEL_PRESSURE messages
+ // - pitch_bend for TML_PITCH_BEND messages
+ union
+ {
+ #ifdef _MSC_VER
+ #pragma warning(push)
+ #pragma warning(disable:4201) //nonstandard extension used: nameless struct/union
+ #elif defined(__GNUC__)
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wpedantic" //ISO C++ prohibits anonymous structs
+ #endif
+
+ struct { union { char key, control, program, channel_pressure; }; union { char velocity, key_pressure, control_value; }; };
+ struct { unsigned short pitch_bend; };
+
+ #ifdef _MSC_VER
+ #pragma warning( pop )
+ #elif defined(__GNUC__)
+ #pragma GCC diagnostic pop
+ #endif
+ };
+
+ // The pointer to the next message in time following this event
+ struct tml_message* next;
+} tml_message;
+
+// The load functions will return a pointer to a struct tml_message.
+// Normally the linked list gets traversed by following the next pointers.
+// Make sure to keep the pointer to the first message to free the memory.
+// On error the tml_load* functions will return NULL most likely due to an
+// invalid MIDI stream (or if the file did not exist in tml_load_filename).
+
+#ifndef TML_NO_STDIO
+// Directly load a MIDI file from a .mid file path
+TMLDEF tml_message* tml_load_filename(const char* filename);
+#endif
+
+// Load a MIDI file from a block of memory
+TMLDEF tml_message* tml_load_memory(const void* buffer, int size);
+
+// Get infos about this loaded MIDI file, returns the note count
+// NULL can be passed for any output value pointer if not needed.
+// used_channels: Will be set to how many channels play notes
+// (i.e. 1 if channel 15 is used but no other)
+// used_programs: Will be set to how many different programs are used
+// total_notes: Will be set to the total number of note on messages
+// time_first_note: Will be set to the time of the first note on message
+// time_length: Will be set to the total time in milliseconds
+TMLDEF int tml_get_info(tml_message* first_message, int* used_channels, int* used_programs, int* total_notes, unsigned int* time_first_note, unsigned int* time_length);
+
+// Read the tempo (microseconds per quarter note) value from a message with the type TML_SET_TEMPO
+TMLDEF int tml_get_tempo_value(tml_message* set_tempo_message);
+
+// Free all the memory of the linked message list (can also call free() manually)
+TMLDEF void tml_free(tml_message* f);
+
+// Stream structure for the generic loading
+struct tml_stream
+{
+ // Custom data given to the functions as the first parameter
+ void* data;
+
+ // Function pointer will be called to read 'size' bytes into ptr (returns number of read bytes)
+ int (*read)(void* data, void* ptr, unsigned int size);
+};
+
+// Generic Midi loading method using the stream structure above
+TMLDEF tml_message* tml_load(struct tml_stream* stream);
+
+// If this library is used together with TinySoundFont, tsf_stream (equivalent to tml_stream) can also be used
+struct tsf_stream;
+TMLDEF tml_message* tml_load_tsf_stream(struct tsf_stream* stream);
+
+#ifdef __cplusplus
+}
+#endif
+
+// end header
+// ---------------------------------------------------------------------------------------------------------
+#endif //TML_INCLUDE_TML_INL
+
+#ifdef TML_IMPLEMENTATION
+
+#if !defined(TML_MALLOC) || !defined(TML_FREE) || !defined(TML_REALLOC)
+# include <stdlib.h>
+# define TML_MALLOC malloc
+# define TML_FREE free
+# define TML_REALLOC realloc
+#endif
+
+#if !defined(TML_MEMCPY)
+# include <string.h>
+# define TML_MEMCPY memcpy
+#endif
+
+#ifndef TML_NO_STDIO
+# include <stdio.h>
+#endif
+
+#define TML_NULL 0
+
+////crash on errors and warnings to find broken midi files while debugging
+//#define TML_ERROR(msg) *(int*)0 = 0xbad;
+//#define TML_WARN(msg) *(int*)0 = 0xf00d;
+
+////print errors and warnings
+//#define TML_ERROR(msg) printf("ERROR: %s\n", msg);
+//#define TML_WARN(msg) printf("WARNING: %s\n", msg);
+
+#ifndef TML_ERROR
+#define TML_ERROR(msg)
+#endif
+
+#ifndef TML_WARN
+#define TML_WARN(msg)
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef TML_NO_STDIO
+static int tml_stream_stdio_read(FILE* f, void* ptr, unsigned int size) { return (int)fread(ptr, 1, size, f); }
+TMLDEF tml_message* tml_load_filename(const char* filename)
+{
+ struct tml_message* res;
+ struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_stdio_read };
+ #if __STDC_WANT_SECURE_LIB__
+ FILE* f = TML_NULL; fopen_s(&f, filename, "rb");
+ #else
+ FILE* f = fopen(filename, "rb");
+ #endif
+ if (!f) { TML_ERROR("File not found"); return 0; }
+ stream.data = f;
+ res = tml_load(&stream);
+ fclose(f);
+ return res;
+}
+#endif
+
+struct tml_stream_memory { const char* buffer; unsigned int total, pos; };
+static int tml_stream_memory_read(struct tml_stream_memory* m, void* ptr, unsigned int size) { if (size > m->total - m->pos) size = m->total - m->pos; TML_MEMCPY(ptr, m->buffer+m->pos, size); m->pos += size; return size; }
+TMLDEF struct tml_message* tml_load_memory(const void* buffer, int size)
+{
+ struct tml_stream stream = { TML_NULL, (int(*)(void*,void*,unsigned int))&tml_stream_memory_read };
+ struct tml_stream_memory f = { 0, 0, 0 };
+ f.buffer = (const char*)buffer;
+ f.total = size;
+ stream.data = &f;
+ return tml_load(&stream);
+}
+
+struct tml_track
+{
+ unsigned int Idx, End, Ticks;
+};
+
+struct tml_tempomsg
+{
+ unsigned int time;
+ unsigned char type, Tempo[3];
+ tml_message* next;
+};
+
+struct tml_parser
+{
+ unsigned char *buf, *buf_end;
+ int last_status, message_array_size, message_count;
+};
+
+enum TMLSystemType
+{
+ TML_TEXT = 0x01, TML_COPYRIGHT = 0x02, TML_TRACK_NAME = 0x03, TML_INST_NAME = 0x04, TML_LYRIC = 0x05, TML_MARKER = 0x06, TML_CUE_POINT = 0x07,
+ TML_EOT = 0x2f, TML_SMPTE_OFFSET = 0x54, TML_TIME_SIGNATURE = 0x58, TML_KEY_SIGNATURE = 0x59, TML_SEQUENCER_EVENT = 0x7f,
+ TML_SYSEX = 0xf0, TML_TIME_CODE = 0xf1, TML_SONG_POSITION = 0xf2, TML_SONG_SELECT = 0xf3, TML_TUNE_REQUEST = 0xf6, TML_EOX = 0xf7, TML_SYNC = 0xf8,
+ TML_TICK = 0xf9, TML_START = 0xfa, TML_CONTINUE = 0xfb, TML_STOP = 0xfc, TML_ACTIVE_SENSING = 0xfe, TML_SYSTEM_RESET = 0xff
+};
+
+static int tml_readbyte(struct tml_parser* p)
+{
+ return (p->buf == p->buf_end ? -1 : *(p->buf++));
+}
+
+static int tml_readvariablelength(struct tml_parser* p)
+{
+ unsigned int res = 0, i = 0;
+ unsigned char c;
+ for (; i != 4; i++)
+ {
+ if (p->buf == p->buf_end) { TML_WARN("Unexpected end of file"); return -1; }
+ c = *(p->buf++);
+ if (c & 0x80) res = ((res | (c & 0x7F)) << 7);
+ else return (int)(res | c);
+ }
+ TML_WARN("Invalid variable length byte count"); return -1;
+}
+
+static int tml_parsemessage(tml_message** f, struct tml_parser* p)
+{
+ int deltatime = tml_readvariablelength(p), status = tml_readbyte(p);
+ tml_message* evt;
+
+ if (deltatime & 0xFFF00000) deltatime = 0; //throw away delays that are insanely high for malformatted midis
+ if (status < 0) { TML_WARN("Unexpected end of file"); return -1; }
+ if ((status & 0x80) == 0)
+ {
+ // Invalid, use same status as before
+ if ((p->last_status & 0x80) == 0) { TML_WARN("Undefined status and invalid running status"); return -1; }
+ p->buf--;
+ status = p->last_status;
+ }
+ else p->last_status = status;
+
+ if (p->message_array_size == p->message_count)
+ {
+ //start allocated memory size of message array at 64, double each time until 8192, then add 1024 entries until done
+ p->message_array_size += (!p->message_array_size ? 64 : (p->message_array_size > 4096 ? 1024 : p->message_array_size));
+ *f = (tml_message*)TML_REALLOC(*f, p->message_array_size * sizeof(tml_message));
+ if (!*f) { TML_ERROR("Out of memory"); return -1; }
+ }
+ evt = *f + p->message_count;
+
+ //check what message we have
+ if ((status == TML_SYSEX) || (status == TML_EOX)) //sysex
+ {
+ //sysex messages are not handled
+ p->buf += tml_readvariablelength(p);
+ if (p->buf > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
+ evt->type = 0;
+ }
+ else if (status == 0xFF) //meta events
+ {
+ int meta_type = tml_readbyte(p), buflen = tml_readvariablelength(p);
+ unsigned char* metadata = p->buf;
+ if (meta_type < 0) { TML_WARN("Unexpected end of file"); return -1; }
+ if (buflen > 0 && (p->buf += buflen) > p->buf_end) { TML_WARN("Unexpected end of file"); p->buf = p->buf_end; return -1; }
+
+ switch (meta_type)
+ {
+ case TML_EOT:
+ if (buflen != 0) { TML_WARN("Invalid length for EndOfTrack event"); return -1; }
+ if (!deltatime) return TML_EOT; //no need to store this message
+ evt->type = TML_EOT;
+ break;
+
+ case TML_SET_TEMPO:
+ if (buflen != 3) { TML_WARN("Invalid length for SetTempo meta event"); return -1; }
+ evt->type = TML_SET_TEMPO;
+ ((struct tml_tempomsg*)evt)->Tempo[0] = metadata[0];
+ ((struct tml_tempomsg*)evt)->Tempo[1] = metadata[1];
+ ((struct tml_tempomsg*)evt)->Tempo[2] = metadata[2];
+ break;
+
+ default:
+ evt->type = 0;
+ }
+ }
+ else //channel message
+ {
+ int param;
+ if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+ evt->key = (param & 0x7f);
+ evt->channel = (status & 0x0f);
+ switch (evt->type = (status & 0xf0))
+ {
+ case TML_NOTE_OFF:
+ case TML_NOTE_ON:
+ case TML_KEY_PRESSURE:
+ case TML_CONTROL_CHANGE:
+ if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+ evt->velocity = (param & 0x7f);
+ break;
+
+ case TML_PITCH_BEND:
+ if ((param = tml_readbyte(p)) < 0) { TML_WARN("Unexpected end of file"); return -1; }
+ evt->pitch_bend = ((param & 0x7f) << 7) | evt->key;
+ break;
+
+ case TML_PROGRAM_CHANGE:
+ case TML_CHANNEL_PRESSURE:
+ evt->velocity = 0;
+ break;
+
+ default: //ignore system/manufacture messages
+ evt->type = 0;
+ break;
+ }
+ }
+
+ if (deltatime || evt->type)
+ {
+ evt->time = deltatime;
+ p->message_count++;
+ }
+ return evt->type;
+}
+
+TMLDEF tml_message* tml_load(struct tml_stream* stream)
+{
+ int num_tracks, division, trackbufsize = 0;
+ unsigned char midi_header[14], *trackbuf = TML_NULL;
+ struct tml_message* messages = TML_NULL;
+ struct tml_track *tracks, *t, *tracksEnd;
+ struct tml_parser p = { TML_NULL, TML_NULL, 0, 0, 0 };
+
+ // Parse MIDI header
+ if (stream->read(stream->data, midi_header, 14) != 14) { TML_ERROR("Unexpected end of file"); return messages; }
+ if (midi_header[0] != 'M' || midi_header[1] != 'T' || midi_header[2] != 'h' || midi_header[3] != 'd' ||
+ midi_header[7] != 6 || midi_header[9] > 2) { TML_ERROR("Doesn't look like a MIDI file: invalid MThd header"); return messages; }
+ if (midi_header[12] & 0x80) { TML_ERROR("File uses unsupported SMPTE timing"); return messages; }
+ num_tracks = (int)(midi_header[10] << 8) | midi_header[11];
+ division = (int)(midi_header[12] << 8) | midi_header[13]; //division is ticks per beat (quarter-note)
+ if (num_tracks <= 0 && division <= 0) { TML_ERROR("Doesn't look like a MIDI file: invalid track or division values"); return messages; }
+
+ // Allocate temporary tracks array for parsing
+ tracks = (struct tml_track*)TML_MALLOC(sizeof(struct tml_track) * num_tracks);
+ tracksEnd = &tracks[num_tracks];
+ for (t = tracks; t != tracksEnd; t++) t->Idx = t->End = t->Ticks = 0;
+
+ // Read all messages for all tracks
+ for (t = tracks; t != tracksEnd; t++)
+ {
+ unsigned char track_header[8];
+ int track_length;
+ if (stream->read(stream->data, track_header, 8) != 8) { TML_WARN("Unexpected end of file"); break; }
+ if (track_header[0] != 'M' || track_header[1] != 'T' || track_header[2] != 'r' || track_header[3] != 'k')
+ { TML_WARN("Invalid MTrk header"); break; }
+
+ // Get size of track data and read into buffer (allocate bigger buffer if needed)
+ track_length = track_header[7] | (track_header[6] << 8) | (track_header[5] << 16) | (track_header[4] << 24);
+ if (track_length < 0) { TML_WARN("Invalid MTrk header"); break; }
+ if (trackbufsize < track_length) { TML_FREE(trackbuf); trackbuf = (unsigned char*)TML_MALLOC(trackbufsize = track_length); }
+ if (stream->read(stream->data, trackbuf, track_length) != track_length) { TML_WARN("Unexpected end of file"); break; }
+
+ t->Idx = p.message_count;
+ for (p.buf_end = (p.buf = trackbuf) + track_length; p.buf != p.buf_end;)
+ {
+ int type = tml_parsemessage(&messages, &p);
+ if (type == TML_EOT || type < 0) break; //file end or illegal data encountered
+ }
+ if (p.buf != p.buf_end) { TML_WARN( "Track length did not match data length"); }
+ t->End = p.message_count;
+ }
+ TML_FREE(trackbuf);
+
+ // Change message time signature from delta ticks to actual msec values and link messages ordered by time
+ if (p.message_count)
+ {
+ tml_message *PrevMessage = TML_NULL, *Msg, *MsgEnd, Swap;
+ unsigned int ticks = 0, tempo_ticks = 0; //tick counter and value at last tempo change
+ int step_smallest, msec, tempo_msec = 0; //msec value at last tempo change
+ double ticks2time = 500000 / (1000.0 * division); //milliseconds per tick
+
+ // Loop through all messages over all tracks ordered by time
+ for (step_smallest = 0; step_smallest != 0x7fffffff; ticks += step_smallest)
+ {
+ step_smallest = 0x7fffffff;
+ msec = tempo_msec + (int)((ticks - tempo_ticks) * ticks2time);
+ for (t = tracks; t != tracksEnd; t++)
+ {
+ if (t->Idx == t->End) continue;
+ for (Msg = &messages[t->Idx], MsgEnd = &messages[t->End]; Msg != MsgEnd && t->Ticks + Msg->time == ticks; Msg++, t->Idx++)
+ {
+ t->Ticks += Msg->time;
+ if (Msg->type == TML_SET_TEMPO)
+ {
+ unsigned char* Tempo = ((struct tml_tempomsg*)Msg)->Tempo;
+ ticks2time = ((Tempo[0]<<16)|(Tempo[1]<<8)|Tempo[2])/(1000.0 * division);
+ tempo_msec = msec;
+ tempo_ticks = ticks;
+ }
+ if (Msg->type)
+ {
+ Msg->time = msec;
+ if (PrevMessage) { PrevMessage->next = Msg; PrevMessage = Msg; }
+ else { Swap = *Msg; *Msg = *messages; *messages = Swap; PrevMessage = messages; }
+ }
+ }
+ if (Msg != MsgEnd && t->Ticks + Msg->time > ticks)
+ {
+ int step = (int)(t->Ticks + Msg->time - ticks);
+ if (step < step_smallest) step_smallest = step;
+ }
+ }
+ }
+ if (PrevMessage) PrevMessage->next = TML_NULL;
+ else p.message_count = 0;
+ }
+ TML_FREE(tracks);
+
+ if (p.message_count == 0)
+ {
+ TML_FREE(messages);
+ messages = TML_NULL;
+ }
+
+ return messages;
+}
+
+TMLDEF tml_message* tml_load_tsf_stream(struct tsf_stream* stream)
+{
+ return tml_load((struct tml_stream*)stream);
+}
+
+TMLDEF int tml_get_info(tml_message* Msg, int* out_used_channels, int* out_used_programs, int* out_total_notes, unsigned int* out_time_first_note, unsigned int* out_time_length)
+{
+ int used_programs = 0, used_channels = 0, total_notes = 0;
+ unsigned int time_first_note = 0xffffffff, time_length = 0;
+ unsigned char channels[16] = { 0 }, programs[128] = { 0 };
+ for (;Msg; Msg = Msg->next)
+ {
+ time_length = Msg->time;
+ if (Msg->type == TML_PROGRAM_CHANGE && !programs[(int)Msg->program]) { programs[(int)Msg->program] = 1; used_programs++; }
+ if (Msg->type != TML_NOTE_ON) continue;
+ if (time_first_note == 0xffffffff) time_first_note = time_length;
+ if (!channels[Msg->channel]) { channels[Msg->channel] = 1; used_channels++; }
+ total_notes++;
+ }
+ if (time_first_note == 0xffffffff) time_first_note = 0;
+ if (out_used_channels ) *out_used_channels = used_channels;
+ if (out_used_programs ) *out_used_programs = used_programs;
+ if (out_total_notes ) *out_total_notes = total_notes;
+ if (out_time_first_note) *out_time_first_note = time_first_note;
+ if (out_time_length ) *out_time_length = time_length;
+ return total_notes;
+}
+
+TMLDEF int tml_get_tempo_value(tml_message* msg)
+{
+ unsigned char* Tempo;
+ if (!msg || msg->type != TML_SET_TEMPO) return 0;
+ Tempo = ((struct tml_tempomsg*)msg)->Tempo;
+ return ((Tempo[0]<<16)|(Tempo[1]<<8)|Tempo[2]);
+}
+
+TMLDEF void tml_free(tml_message* f)
+{
+ TML_FREE(f);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //TML_IMPLEMENTATION
|
