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