1 /* midithru.c -- example program implementing background thru processing */
2 
3 /* suppose you want low-latency midi-thru processing, but your application
4    wants to take advantage of the input buffer and timestamped data so that
5    it does not have to operate with very low latency.
6 
7    This program illustrates how to use a timer callback from PortTime to
8    implement a low-latency process that handles midi thru, including correctly
9    merging midi data from the application with midi data from the input port.
10 
11    The main application, which runs in the main program thread, will use an
12    interface similar to that of PortMidi, but since PortMidi does not allow
13    concurrent threads to share access to a stream, the application will
14    call private methods that transfer MIDI messages to and from the timer
15    thread. All PortMidi API calls are made from the timer thread.
16  */
17 
18 /* DESIGN
19 
20 All setup will be done by the main thread. Then, all direct access to
21 PortMidi will be handed off to the timer callback thread.
22 
23 After this hand-off, the main thread will get/send messages via a queue.
24 
25 The goal is to send incoming messages to the midi output while merging
26 any midi data generated by the application. Sysex is a problem here
27 because you cannot insert (merge) a midi message while a sysex is in
28 progress. There are at least three ways to implement midi thru with
29 sysex messages:
30 
31 1) Turn them off. If your application does not need them, turn them off
32    with Pm_SetFilter(midi_in, PM_FILT_ACTIVE | PM_FILT_SYSEX). You will
33    not receive sysex (or active sensing messages), so you will not have
34    to handle them.
35 
36 2) Make them atomic. As you receive sysex messages, copy the data into
37    a (big) buffer. Ideally, expand the buffer as needed -- sysex messages
38    do not have any maximum length. Even more ideally, use a list structure
39    and real-time memory allocation to avoid latency in the timer thread.
40    When a full sysex message is received, send it to the midi output all
41    at once.
42 
43 3) Process sysex incrementally. Send sysex data to midi output as it
44    arrives. Block any non-real-time messages from the application until
45    the sysex message completes. There is the risk that an incomplete
46    sysex message will block messages forever, so implement a 5-second
47    timeout: if no sysex data is seen for 5 seconds, release the block,
48    possibly losing the rest of the sysex message.
49 
50    Application messages must be processed similarly: once started, a
51    sysex message will block MIDI THRU processing. We will assume that
52    the application will not abort a sysex message, so timeouts are not
53    necessary here.
54 
55 This code implements (3).
56 
57 Latency is also an issue. PortMidi requires timestamps to be in
58 non-decreasing order. Since we'll be operating with a low-latency
59 timer thread, we can just set the latency to zero meaning timestamps
60 are ignored by PortMidi. This will allow thru to go through with
61 minimal latency. The application, however, needs to use timestamps
62 because we assume it is high latency (the whole purpose of this
63 example is to illustrate how to get low-latency thru with a high-latency
64 application.) So the callback thread will implement midi timing by
65 observing timestamps. The current timestamp will be available in the
66 global variable current_timestamp.
67 
68 */
69 
70 
71 #include "stdio.h"
72 #include "stdlib.h"
73 #include "string.h"
74 #include "assert.h"
75 #include "portmidi.h"
76 #include "pmutil.h"
77 #include "porttime.h"
78 
79 #define MIDI_SYSEX 0xf0
80 #define MIDI_EOX 0xf7
81 
82 /* active is set true when midi processing should start */
83 int active = FALSE;
84 /* process_midi_exit_flag is set when the timer thread shuts down */
85 int process_midi_exit_flag;
86 
87 PmStream *midi_in;
88 PmStream *midi_out;
89 
90 /* shared queues */
91 #define IN_QUEUE_SIZE 1024
92 #define OUT_QUEUE_SIZE 1024
93 PmQueue *in_queue;
94 PmQueue *out_queue;
95 PmTimestamp current_timestamp = 0;
96 int thru_sysex_in_progress = FALSE;
97 int app_sysex_in_progress = FALSE;
98 PmTimestamp last_timestamp = 0;
99 
100 
101 /* time proc parameter for Pm_MidiOpen */
midithru_time_proc(void * info)102 PmTimestamp midithru_time_proc(void *info)
103 {
104     return current_timestamp;
105 }
106 
107 
108 /* timer interrupt for processing midi data.
109    Incoming data is delivered to main program via in_queue.
110    Outgoing data from main program is delivered via out_queue.
111    Incoming data from midi_in is copied with low latency to  midi_out.
112    Sysex messages from either source block messages from the other.
113  */
process_midi(PtTimestamp timestamp,void * userData)114 void process_midi(PtTimestamp timestamp, void *userData)
115 {
116     PmError result;
117     PmEvent buffer; /* just one message at a time */
118 
119     current_timestamp++; /* update every millisecond */
120     /* if (current_timestamp % 1000 == 0)
121         printf("time %d\n", current_timestamp); */
122 
123     /* do nothing until initialization completes */
124     if (!active) {
125         /* this flag signals that no more midi processing will be done */
126         process_midi_exit_flag = TRUE;
127         return;
128     }
129 
130     /* see if there is any midi input to process */
131     if (!app_sysex_in_progress) {
132         do {
133             result = Pm_Poll(midi_in);
134             if (result) {
135                 int status;
136                 PmError rslt = Pm_Read(midi_in, &buffer, 1);
137                 if (rslt == pmBufferOverflow)
138                     continue;
139                 assert(rslt == 1);
140 
141                 /* record timestamp of most recent data */
142                 last_timestamp = current_timestamp;
143 
144                 /* the data might be the end of a sysex message that
145                    has timed out, in which case we must ignore it.
146                    It's a continuation of a sysex message if status
147                    is actually a data byte (high-order bit is zero). */
148                 status = Pm_MessageStatus(buffer.message);
149                 if (((status & 0x80) == 0) && !thru_sysex_in_progress) {
150                     continue; /* ignore this data */
151                 }
152 
153                 /* implement midi thru */
154                 /* note that you could output to multiple ports or do other
155                    processing here if you wanted
156                  */
157                 /* printf("thru: %x\n", buffer.message); */
158                 Pm_Write(midi_out, &buffer, 1);
159 
160                 /* send the message to the application */
161                 /* you might want to filter clock or active sense messages here
162                    to avoid sending a bunch of junk to the application even if
163                    you want to send it to MIDI THRU
164                  */
165                 Pm_Enqueue(in_queue, &buffer);
166 
167                 /* sysex processing */
168                 if (status == MIDI_SYSEX) thru_sysex_in_progress = TRUE;
169                 else if ((status & 0xF8) != 0xF8) {
170                     /* not MIDI_SYSEX and not real-time, so */
171                     thru_sysex_in_progress = FALSE;
172                 }
173                 if (thru_sysex_in_progress && /* look for EOX */
174                     (((buffer.message & 0xFF) == MIDI_EOX) ||
175                      (((buffer.message >> 8) & 0xFF) == MIDI_EOX) ||
176                      (((buffer.message >> 16) & 0xFF) == MIDI_EOX) ||
177                      (((buffer.message >> 24) & 0xFF) == MIDI_EOX))) {
178                     thru_sysex_in_progress = FALSE;
179                 }
180             }
181         } while (result);
182     }
183 
184 
185     /* see if there is application midi data to process */
186     while (!Pm_QueueEmpty(out_queue)) {
187         /* see if it is time to output the next message */
188         PmEvent *next = (PmEvent *) Pm_QueuePeek(out_queue);
189         assert(next); /* must be non-null because queue is not empty */
190         if (next->timestamp <= current_timestamp) {
191             /* time to send a message, first make sure it's not blocked */
192             int status = Pm_MessageStatus(next->message);
193             if ((status & 0xF8) == 0xF8) {
194                 ; /* real-time messages are not blocked */
195             } else if (thru_sysex_in_progress) {
196                 /* maybe sysex has timed out (output becomes unblocked) */
197                 if (last_timestamp + 5000 < current_timestamp) {
198                     thru_sysex_in_progress = FALSE;
199                 } else break; /* output is blocked, so exit loop */
200             }
201             Pm_Dequeue(out_queue, &buffer);
202             Pm_Write(midi_out, &buffer, 1);
203 
204             /* inspect message to update app_sysex_in_progress */
205             if (status == MIDI_SYSEX) app_sysex_in_progress = TRUE;
206             else if ((status & 0xF8) != 0xF8) {
207                 /* not MIDI_SYSEX and not real-time, so */
208                 app_sysex_in_progress = FALSE;
209             }
210             if (app_sysex_in_progress && /* look for EOX */
211                 (((buffer.message & 0xFF) == MIDI_EOX) ||
212                  (((buffer.message >> 8) & 0xFF) == MIDI_EOX) ||
213                  (((buffer.message >> 16) & 0xFF) == MIDI_EOX) ||
214                  (((buffer.message >> 24) & 0xFF) == MIDI_EOX))) {
215                 app_sysex_in_progress = FALSE;
216             }
217         } else break; /* wait until indicated timestamp */
218     }
219 }
220 
221 
exit_with_message(char * msg)222 void exit_with_message(char *msg)
223 {
224 #define STRING_MAX 80
225     char line[STRING_MAX];
226     printf("%s\nType ENTER...", msg);
227     fgets(line, STRING_MAX, stdin);
228     exit(1);
229 }
230 
231 
initialize()232 void initialize()
233 /* set up midi processing thread and open midi streams */
234 {
235     /* note that it is safe to call PortMidi from the main thread for
236        initialization and opening devices. You should not make any
237        calls to PortMidi from this thread once the midi thread begins.
238        to make PortMidi calls.
239      */
240 
241     /* note that this routine provides minimal error checking. If
242        you use the PortMidi library compiled with PM_CHECK_ERRORS,
243        then error messages will be printed and the program will exit
244        if an error is encountered. Otherwise, you should add some
245        error checking to this code.
246      */
247 
248     const PmDeviceInfo *info;
249     int id;
250 
251     /* make the message queues */
252     in_queue = Pm_QueueCreate(IN_QUEUE_SIZE, sizeof(PmEvent));
253     assert(in_queue != NULL);
254     out_queue = Pm_QueueCreate(OUT_QUEUE_SIZE, sizeof(PmEvent));
255     assert(out_queue != NULL);
256 
257     /* always start the timer before you start midi */
258     Pt_Start(1, &process_midi, 0); /* start a timer with millisecond accuracy */
259     /* the timer will call our function, process_midi() every millisecond */
260 
261     Pm_Initialize();
262 
263     id = Pm_GetDefaultOutputDeviceID();
264     info = Pm_GetDeviceInfo(id);
265     if (info == NULL) {
266         printf("Could not open default output device (%d).", id);
267         exit_with_message("");
268     }
269     printf("Opening output device %s %s\n", info->interf, info->name);
270 
271     /* use zero latency because we want output to be immediate */
272     Pm_OpenOutput(&midi_out,
273                   id,
274                   NULL /* driver info */,
275                   OUT_QUEUE_SIZE,
276                   &midithru_time_proc,
277                   NULL /* time info */,
278                   0 /* Latency */);
279 
280     id = Pm_GetDefaultInputDeviceID();
281     info = Pm_GetDeviceInfo(id);
282     if (info == NULL) {
283         printf("Could not open default input device (%d).", id);
284         exit_with_message("");
285     }
286     printf("Opening input device %s %s\n", info->interf, info->name);
287     Pm_OpenInput(&midi_in,
288                  id,
289                  NULL /* driver info */,
290                  0 /* use default input size */,
291                  &midithru_time_proc,
292                  NULL /* time info */);
293     /* Note: if you set a filter here, then this will filter what goes
294        to the MIDI THRU port. You may not want to do this.
295      */
296     Pm_SetFilter(midi_in, PM_FILT_ACTIVE | PM_FILT_CLOCK);
297 
298     active = TRUE; /* enable processing in the midi thread -- yes, this
299                       is a shared variable without synchronization, but
300                       this simple assignment is safe */
301 
302 }
303 
304 
finalize()305 void finalize()
306 {
307     /* the timer thread could be in the middle of accessing PortMidi stuff */
308     /* to detect that it is done, we first clear process_midi_exit_flag and
309        then wait for the timer thread to set it
310      */
311     process_midi_exit_flag = FALSE;
312     active = FALSE;
313     /* busy wait for flag from timer thread that it is done */
314     while (!process_midi_exit_flag) ;
315     /* at this point, midi thread is inactive and we need to shut down
316      * the midi input and output
317      */
318     Pt_Stop(); /* stop the timer */
319     Pm_QueueDestroy(in_queue);
320     Pm_QueueDestroy(out_queue);
321 
322     Pm_Close(midi_in);
323     Pm_Close(midi_out);
324 
325     Pm_Terminate();
326 }
327 
328 
main(int argc,char * argv[])329 int main(int argc, char *argv[])
330 {
331     PmTimestamp last_time = 0;
332     PmEvent buffer;
333 
334     /* determine what type of test to run */
335     printf("begin PortMidi midithru program...\n");
336 
337     initialize(); /* set up and start midi processing */
338 
339     printf("%s\n%s\n",
340            "This program will run for 60 seconds, or until you play middle C,",
341            "echoing all input with a 2 second delay.");
342 
343     while (current_timestamp < 60000) {
344         /* just to make the point that this is not a low-latency process,
345            spin until half a second has elapsed */
346         last_time = last_time + 500;
347         while (last_time > current_timestamp) ;
348 
349         /* now read data and send it after changing timestamps */
350         while (Pm_Dequeue(in_queue, &buffer) == 1) {
351             /* printf("timestamp %d\n", buffer.timestamp); */
352             /* printf("message %x\n", buffer.message); */
353             buffer.timestamp = buffer.timestamp + 2000; /* delay */
354             Pm_Enqueue(out_queue, &buffer);
355             /* play middle C to break out of loop */
356             if (Pm_MessageStatus(buffer.message) == 0x90 &&
357                 Pm_MessageData1(buffer.message) == 60) {
358                 goto quit_now;
359             }
360         }
361     }
362 quit_now:
363     finalize();
364     exit_with_message("finished PortMidi midithru program.");
365     return 0; /* never executed, but keeps the compiler happy */
366 }
367