/* 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. 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 /* active is set true when midi processing should start */ int active = FALSE; /* process_midi_exit_flag is set when the timer thread shuts down */ 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; PmTimestamp current_timestamp = 0; int thru_sysex_in_progress = FALSE; int app_sysex_in_progress = FALSE; PmTimestamp last_timestamp = 0; /* time proc parameter for Pm_MidiOpen */ long 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 */ /* if (current_timestamp % 1000 == 0) printf("time %d\n", current_timestamp); */ /* 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) { long status; if (Pm_Read(midi_in, &buffer, 1) == pmBufferOverflow) continue; /* 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 */ long status = Pm_MessageStatus(buffer.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 char line[STRING_MAX]; printf("%s\nType ENTER...", msg); fgets(line, STRING_MAX, stdin); exit(1); } void initialize() /* 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; int id; /* 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(); id = Pm_GetDefaultOutputDeviceID(); info = Pm_GetDeviceInfo(id); if (info == NULL) { printf("Could not open default output device (%d).", id); 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, id, NULL /* driver info */, OUT_QUEUE_SIZE, &midithru_time_proc, NULL /* time info */, 0 /* Latency */); id = Pm_GetDefaultInputDeviceID(); info = Pm_GetDeviceInfo(id); if (info == NULL) { printf("Could not open default input device (%d).", id); exit_with_message(""); } printf("Opening input device %s %s\n", info->interf, info->name); Pm_OpenInput(&midi_in, id, NULL /* driver info */, 0 /* use default input size */, &midithru_time_proc, NULL /* time info */); /* 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; /* determine what type of test to run */ printf("begin PortMidi midithru program...\n"); initialize(); /* set up and start midi processing */ printf("%s\n%s\n", "This program will run for 60 seconds, or until you play middle C,", "echoing all input with a 2 second delay."); 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); */ buffer.timestamp = buffer.timestamp + 2000; /* delay */ Pm_Enqueue(out_queue, &buffer); /* play middle C to break out of loop */ if (Pm_MessageStatus(buffer.message) == 0x90 && Pm_MessageData1(buffer.message) == 60) { goto quit_now; } } } quit_now: finalize(); exit_with_message("finished PortMidi midithru program."); return 0; /* never executed, but keeps the compiler happy */ }