aboutsummaryrefslogtreecommitdiff
path: root/pd/portmidi/pm_test/midithru.c
blob: 270246fbf578861ebdb966d7bf7eadfb10a64ad5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
/* 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 */
}