/* sysex.c -- example program showing how to send and receive sysex
    messages

   Messages are stored in a file using 2-digit hexadecimal numbers,
   one per byte, separated by blanks, with up to 32 numbers per line:
   F0 14 A7 4B ...

 */

#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
#include "portmidi.h"
#include "porttime.h"
#include "string.h"
#ifdef WIN32
// need to get declaration for Sleep()
#include "windows.h"
#else
#include <unistd.h>
#define Sleep(n) usleep(n * 1000)
#endif

// enable some extra printing
#ifndef VERBOSE
#define VERBOSE 0
#endif

#define MIDI_SYSEX 0xf0
#define MIDI_EOX 0xf7

#define STRING_MAX 80

#ifndef true
#define true 1
#define false 0
#endif

int latency = 0;

/* read a number from console */
/**/
int get_number(const char *prompt)
{
    int n = 0, i;
    fputs(prompt, stdout);
    while (n != 1) {
        n = scanf("%d", &i);
        while (getchar() != '\n') ;
    }
    return i;
}


/* loopback test -- send/rcv from 2 to 1000 bytes of random midi data */
/**/
void loopback_test()
{
    int outp;
    int inp;
    PmStream *midi_in;
    PmStream *midi_out;
    unsigned char msg[1024];
    int32_t len;
    int i;
    int data;
    PmEvent event;
    int shift;
    long total_bytes = 0;
    int32_t begin_time;

    Pt_Start(1, 0, 0);
    
    printf("Connect a midi cable from an output port to an input port.\n");
    printf("This test will send random data via sysex message from output\n");
    printf("to input and check that the correct data was received.\n");
    outp = get_number("Type output device number: ");
    /* Open output with 1ms latency -- when latency is non-zero, the Win32
       implementation supports sending sysex messages incrementally in a 
       series of buffers. This is nicer than allocating a big buffer for the
       message, and it also seems to work better. Either way works.
     */
    while ((latency = get_number(
                     "Latency in milliseconds (0 to send data immediatedly,\n"
                     "  >0 to send timestamped messages): ")) < 0);
    Pm_OpenOutput(&midi_out, outp, NULL, 0, NULL, NULL, latency);
    inp = get_number("Type input device number: ");
    /* since we are going to send and then receive, make sure the input buffer
       is large enough for the entire message */
    Pm_OpenInput(&midi_in, inp, NULL, 512, NULL, NULL);

    srand((unsigned int) Pt_Time()); /* seed for random numbers */

    begin_time = Pt_Time();
    while (total_bytes < 100000) {
        PmError count;
        int32_t start_time;
        int error_position = -1; /* 0; -1; -1 for continuous */ 
        int expected = 0;
        int actual = 0;
        /* this modification will run until an error is detected */
        /* set error_position above to 0 for interactive, -1 for */
        /* continuous */
        if (error_position >= 0) {
            int c;
            printf("Type return to send message, q to quit: ");
            while ((c = getchar()) != '\n') {
                if (c == 'q') goto cleanup;
            }
        }

        /* compose the message */
        len = rand() % 998 + 2; /* len only counts data bytes */
        msg[0] = (char) MIDI_SYSEX; /* start of SYSEX message */
        /* data bytes go from 1 to len */
        for (i = 0; i < len; i++) {
/* pick whether data is sequential or random... (docs say random) */
#define DATA_EXPR (i+1)
// #define DATA_EXPR rand()
            msg[i + 1] = DATA_EXPR & 0x7f; /* MIDI data */
        }
        /* final EOX goes in len+1, total of len+2 bytes in msg */
        msg[len + 1] = (char) MIDI_EOX;

        /* sanity check: before we send, there should be no queued data */
        count = Pm_Read(midi_in, &event, 1);

        if (count != 0) {
            printf("Before sending anything, a MIDI message was found in\n");
            printf("the input buffer. Please try again.\n");
            break;
        }

        /* send the message two ways: 1) Pm_WriteSysEx, 2) Pm_Write */
        if (total_bytes & 1) {
            printf("Sending %d byte sysex msg via Pm_WriteSysEx.\n", len + 2);
            Pm_WriteSysEx(midi_out, 0, msg);
        } else {
            PmEvent event = {0, 0};
            int bits = 0;
            printf("Sending %d byte sysex msg via Pm_Write(s).\n", len + 2);
            for (i = 0; i < len + 2; i++) {
                event.message |= (msg[i] << bits);
                bits += 8;
                if (bits == 32) {  /* full message - send it */
                    Pm_Write(midi_out, &event, 1);
                    bits = 0;
                    event.message = 0;
                }
            }
            if (bits > 0) {  /* last message is partially full */
                Pm_Write(midi_out, &event, 1);
            }
        }

        /* receive the message and compare to msg[] */
        data = 0;
        shift = 0;
        i = 0;
        start_time = Pt_Time();
        if (VERBOSE) {
            printf("start_time %d\n", start_time);
        }
        error_position = -1;
        /* allow up to 2 seconds for transmission */
        while (data != MIDI_EOX && start_time + 2000 > Pt_Time()) {
            count = Pm_Read(midi_in, &event, 1);
            if (count == 0) {
                Sleep(1); /* be nice: give some CPU time to the system */
                continue; /* continue polling for input */
            }
            if (VERBOSE) {
                printf("read %08x ", event.message);
                fflush(stdout);
            }
            /* compare 4 bytes of data until you reach an eox */
            for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
                data = (event.message >> shift) & 0xFF;
                if (data != msg[i] && error_position < 0) {
                    error_position = i;
                    expected = msg[i];
                    actual = data;
                }
                i++;
            }
        }
        if (error_position >= 0) {
            printf("Error at time %d byte %d: sent %x recd %x.\n", Pt_Time(),
                   error_position, expected, actual);
            break;
        } else if (i != len + 2) {
            printf("Error at time %d: byte %d not received.\n", Pt_Time(), i);
            break;
        } else {
            int seconds = (Pt_Time() - begin_time) / 1000;
            if (seconds == 0) seconds = 1;
            printf("Correctly received %d byte sysex message.\n", i);
            total_bytes += i;
            printf("Cummulative bytes/sec: %d, %d%% done.\n", 
                   (int) (total_bytes / seconds),
                   (int) (100 * total_bytes / 100000));
        }
    }
cleanup:
    Pm_Close(midi_out);
    Pm_Close(midi_in);
    return;
}


/* send_multiple test -- send many sysex messages */
/**/
void send_multiple_test()
{
    int outp;
    int length;
    int num_msgs;
    PmStream *midi_out;
    unsigned char msg[1024];
    int i;
    PtTimestamp start_time;
    PtTimestamp stop_time;

    Pt_Start(1, 0, 0);
    
    printf("This is for performance testing. You should be sending to this\n");
    printf("program running the receive multiple test. Do NOT send to\n");
    printf("a synthesizer or you risk reprogramming it\n");
    outp = get_number("Type output device number: ");
    while ((latency = get_number(
                     "Latency in milliseconds (0 to send data immediatedly,\n"
                     "  >0 to send timestamped messages): ")) < 0);
    Pm_OpenOutput(&midi_out, outp, NULL, 0, NULL, NULL, latency);
    while ((length = get_number("Message length (7 - 1024): ")) < 7 ||
           length > 1024) ;
    while ((num_msgs = get_number("Number of messages: ")) < 1);
    /* latency, length, and num_msgs should now all be valid */
    /* compose the message except for sequence number in first 5 bytes */
    msg[0] = (char) MIDI_SYSEX;
    for (i = 6; i < length - 1; i++) {
        msg[i] = i % 128; /* this is just filler */
    }
    msg[length - 1] = (char) MIDI_EOX;

    start_time = Pt_Time();
    /* send the messages */
    for (i = num_msgs; i > 0; i--) {
        /* insert sequence number into first 5 data bytes */
        /* sequence counts down to zero */
        int j;
        int count = i;
        /* 7 bits of message count i goes into each data byte */
        for (j = 1; j <= 5; j++) {
            msg[j] = count & 127;
            count >>= 7;
        }
        /* send the message */
        Pm_WriteSysEx(midi_out, 0, msg);
    }
    stop_time = Pt_Time();
    Pm_Close(midi_out);
    return;
}

#define MAX_MSG_LEN 1024
static unsigned char receive_msg[MAX_MSG_LEN];
static int receive_msg_index;
static int receive_msg_length;
static int receive_msg_count;
static int receive_msg_error;
static int receive_msg_messages;
static PmStream *receive_msg_midi_in;
static int receive_poll_running;

/* receive_poll -- callback function to check for midi input */
/**/
void receive_poll(PtTimestamp timestamp, void *userData)
{
    PmError count;
    PmEvent event;
    int shift;
    int data = 0;
    int i;
    
    if (!receive_poll_running) return; /* wait until midi device is opened */
    shift = 0;
    while (data != MIDI_EOX) {
        count = Pm_Read(receive_msg_midi_in, &event, 1);
        if (count == 0) return;

        /* compare 4 bytes of data until you reach an eox */
        for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
            receive_msg[receive_msg_index++] = data = 
                (event.message >> shift) & 0xFF;
            if (receive_msg_index >= MAX_MSG_LEN) {
                printf("error: incoming sysex too long\n");
                goto error;
            }
        }
    }
    /* check the message */
    if (receive_msg_length == 0) {
        receive_msg_length = receive_msg_index;
    }
    if (receive_msg_length != receive_msg_index) {
        printf("error: incoming sysex wrong length\n");
        goto error;
    }
    if (receive_msg[0] != MIDI_SYSEX) {
        printf("error: incoming sysex missing status byte\n");
        goto error;
    }
    /* get and check the count */
    count = 0;
    for (i = 0; i < 5; i++) {
        count += receive_msg[i + 1] << (7 * i);
    }
    if (receive_msg_count == -1) {
        receive_msg_count = count;
        receive_msg_messages = count;
    }
    if (receive_msg_count != count) {
        printf("error: incoming sysex has wrong count\n");
        goto error;
    }
    for (i = 6; i < receive_msg_index - 1; i++) {
        if (receive_msg[i] != i % 128) {
            printf("error: incoming sysex has bad data\n");
            goto error;
        }
    }
    if (receive_msg[receive_msg_length - 1] != MIDI_EOX) goto error;
    receive_msg_index = 0; /* get ready for next message */
    receive_msg_count--;
    return;
 error:
    receive_msg_error = 1;
    return;
}


/* receive_multiple_test -- send/rcv from 2 to 1000 bytes of random midi data */
/**/
void receive_multiple_test()
{
    PmError err;
    int inp;
    
    printf("This test expects to receive data sent by the send_multiple test\n");
    printf("The test will check that correct data is received.\n");

    /* Important: start PortTime first -- if it is not started first, it will
       be started by PortMidi, and then our attempt to open again will fail */
    receive_poll_running = false;
    if ((err = Pt_Start(1, receive_poll, 0))) {
        printf("PortTime error code: %d\n", err);
        goto cleanup;
    }
    inp = get_number("Type input device number: ");
    Pm_OpenInput(&receive_msg_midi_in, inp, NULL, 512, NULL, NULL);
    receive_msg_index = 0;
    receive_msg_length = 0;
    receive_msg_count = -1;
    receive_msg_error = 0;
    receive_poll_running = true;
    while ((!receive_msg_error) && (receive_msg_count != 0)) {
#ifdef WIN32
        Sleep(1000);
#else
        sleep(1); /* block and wait */
#endif
    }
    if (receive_msg_error) {
        printf("Receive_multiple test encountered an error\n");
    } else {
        printf("Receive_multiple test successfully received %d sysex messages\n", 
               receive_msg_messages);
    }
cleanup:
    receive_poll_running = false;
    Pm_Close(receive_msg_midi_in);
    Pt_Stop();
    return;
}


#define is_real_time_msg(msg) ((0xF0 & Pm_MessageStatus(msg)) == 0xF8)


void receive_sysex()
{
    char line[80];
    FILE *f;
    PmStream *midi;
    int shift = 0;
    int data = 0;
    int bytes_on_line = 0;
    PmEvent msg;

    /* determine which output device to use */
    int i = get_number("Type input device number: ");

    /* open input device */
    Pm_OpenInput(&midi, i, NULL, 512, NULL, NULL);
    printf("Midi Input opened, type file for sysex data: ");

    /* open file */
    if (!fgets(line, STRING_MAX, stdin)) return;  /* no more stdin? */
    /* remove the newline character */
    if (strlen(line) > 0) line[strlen(line) - 1] = 0;
    f = fopen(line, "w");
    if (!f) {
        printf("Could not open %s\n", line);
        Pm_Close(midi);
        return;
    }

    printf("Ready to receive a sysex message\n");

    /* read data and write to file */
    while (data != MIDI_EOX) {
        PmError count;
        count = Pm_Read(midi, &msg, 1);
        /* CAUTION: this causes busy waiting. It would be better to 
           be in a polling loop to avoid being compute bound. PortMidi
           does not support a blocking read since this is so seldom
           useful.
         */
        if (count == 0) continue;
        /* ignore real-time messages */
        if (is_real_time_msg(Pm_MessageStatus(msg.message))) continue;

        /* write 4 bytes of data until you reach an eox */
        for (shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
            data = (msg.message >> shift) & 0xFF;
            /* if this is a status byte that's not MIDI_EOX, the sysex
               message is incomplete and there is no more sysex data */
            if (data & 0x80 && data != MIDI_EOX) break;
            fprintf(f, "%2x ", data);
            if (++bytes_on_line >= 16) {
                fprintf(f, "\n");
                bytes_on_line = 0;
            }
        }
    }
    fclose(f);
    Pm_Close(midi);
}


void send_sysex()
{
    char line[80];
    FILE *f;
    PmStream *midi;
    int data;
    int shift = 0;
    PmEvent msg;

	/* determine which output device to use */
    int i = get_number("Type output device number: ");
    while ((latency = get_number(
                     "Latency in milliseconds (0 to send data immediatedly,\n"
                     "  >0 to send timestamped messages): ")) < 0);

    msg.timestamp = 0; /* no need for timestamp */

	/* open output device */
    Pm_OpenOutput(&midi, i, NULL, 0, NULL, NULL, latency);
	printf("Midi Output opened, type file with sysex data: ");

    /* open file */
    if (!fgets(line, STRING_MAX, stdin)) return;  /* no more stdin? */
    /* remove the newline character */
    if (strlen(line) > 0) line[strlen(line) - 1] = 0;
    f = fopen(line, "r");
    if (!f) {
        printf("Could not open %s\n", line);
        Pm_Close(midi);
        return;
    }

    /* read file and send data */
    msg.message = 0;
    while (1) {
        /* get next byte from file */

        if (fscanf(f, "%x", &data) == 1) {
            /* printf("read %x, ", data); */
            /* OR byte into message at proper offset */
            msg.message |= (data << shift);
            shift += 8;
        }
        /* send the message if it's full (shift == 32) or if we are at end */
        if (shift == 32 || data == MIDI_EOX) {
            /* this will send sysex data 4 bytes at a time -- it would
               be much more efficient to send multiple PmEvents at once
               but this method is simpler. See Pm_WriteSysEx for a more
               efficient code example.
             */
            Pm_Write(midi, &msg, 1);
            msg.message = 0;
            shift = 0;
        }
        if (data == MIDI_EOX) { /* end of message */
            fclose(f);
            Pm_Close(midi);
            return;
        }
    }
}


int main()
{
    int i;
    
    /* list device information */
    for (i = 0; i < Pm_CountDevices(); i++) {
        const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
        printf("%d: %s, %s", i, info->interf, info->name);
        if (info->input) printf(" (input)");
        if (info->output) printf(" (output)");
        printf("\n");
    }
    while (1) {
        char cmd;
        printf("Type r to receive sysex, s to send,"
               " l for loopback test, m to send multiple,"
               " n to receive multiple, q to quit: ");
        cmd = getchar();
        while (getchar() != '\n') ;
        switch (cmd) {
          case 'r':
            receive_sysex();
            break;
          case 's':
            send_sysex();
            break;
          case 'l':
            loopback_test();
            break;
          case 'm':
            send_multiple_test();
            break;
          case 'n':
            receive_multiple_test();
            break;
          case 'q':
            exit(0);
          default:
            break;
        }
    }
    return 0;
}
