mirror of
https://github.com/mamedev/mame.git
synced 2024-11-16 07:48:32 +01:00
366 lines
14 KiB
C
366 lines
14 KiB
C
/* 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 */
|
|
PmTimestamp 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) {
|
|
int status;
|
|
PmError rslt = Pm_Read(midi_in, &buffer, 1);
|
|
if (rslt == pmBufferOverflow)
|
|
continue;
|
|
assert(rslt == 1);
|
|
|
|
/* 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 */
|
|
int status = Pm_MessageStatus(next->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 */
|
|
}
|