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