/* fastrcv.c -- send many MIDI messages very fast. 
 *
 * This is a stress test created to explore reports of
 * pm_write() call blocking (forever) on Linux when
 * sending very dense MIDI sequences.
 *
 * Modified 8 Aug 2017 with -n to send expired timestamps
 * to test a theory about why Linux ALSA hangs in Audacity.
 *
 * Roger B. Dannenberg, Aug 2017
 */

#include "portmidi.h"
#include "porttime.h"
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "assert.h"

#define INPUT_BUFFER_SIZE 1000  /* big to avoid losing any input */
#define DEVICE_INFO NULL
#define DRIVER_INFO NULL
#define TIME_PROC ((PmTimeProcPtr) Pt_Time)
#define TIME_INFO NULL
#define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */

#define STRING_MAX 80 /* used for console input */
// need to get declaration for Sleep()
#ifdef WIN32
#include "windows.h"
#else
#include <unistd.h>
#define Sleep(n) usleep(n * 1000)
#endif


int deviceno = -9999;
int verbose = FALSE;


static void prompt_and_exit(void)
{
    printf("type ENTER...");
    while (getchar() != '\n') ;
    /* this will clean up open ports: */
    exit(-1);
}


static PmError checkerror(PmError err)
{
    if (err == pmHostError) {
        /* it seems pointless to allocate memory and copy the string,
         * so I will do the work of Pm_GetHostErrorText directly
         */
        char errmsg[80];
        Pm_GetHostErrorText(errmsg, 80);
        printf("PortMidi found host error...\n  %s\n", errmsg);
        prompt_and_exit();
    } else if (err < 0) {
        printf("PortMidi call failed...\n  %s\n", Pm_GetErrorText(err));
        prompt_and_exit();
    }
    return err;
}


/* 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;
}


void fastrcv_test(void)
{
    PmStream * midi;
    PmError status, length;
    PmEvent buffer[1];
    PmTimestamp start;
    /* every 10ms read all messages, keep counts */
    /* every 1000ms, print report */
    int msgcnt = 0;
    /* expect repeating sequence of 60 through 71, alternating on/off */
    int expected_pitch = 60;
    int expected_on = TRUE;
    int report_time;
    PmTimestamp last_timestamp = -1;
    PmTimestamp last_delta = -1;

    /* It is recommended to start timer before PortMidi */
    TIME_START;

    /* open output device */
    if (deviceno == Pm_CountDevices()) {
        int id = Pm_CreateVirtualInput("fastrcv", NULL, DEVICE_INFO);
        if (id < 0) checkerror(id);  /* error reporting */
        checkerror(Pm_OpenInput(&midi, id, DRIVER_INFO,
                                INPUT_BUFFER_SIZE, TIME_PROC, TIME_INFO));
    } else {
        Pm_OpenInput(&midi, deviceno, DRIVER_INFO, INPUT_BUFFER_SIZE, 
                     TIME_PROC, TIME_INFO);
    }
    printf("Midi Input opened.\n");

    /* wait a sec after printing previous line */
    start = Pt_Time() + 1000;
    while (start > Pt_Time()) {
        Sleep(10);
    }

    report_time = Pt_Time() + 1000;  /* report every 1s */
    while (TRUE) {
        PmTimestamp now = Pt_Time();
        status = Pm_Poll(midi);
        if (status == TRUE) {
            length = Pm_Read(midi, buffer, 1);
            if (length > 0) {
                int status = Pm_MessageStatus(buffer[0].message);
                if (status == 0x80) {  /* convert NoteOff to NoteOn, vel=0 */
                    status = 0x90;
                    buffer[0].message = Pm_Message(status, 
                            Pm_MessageData1(buffer[0].message), 0);
                }
                /* only listen to NOTEON messages */
                if (status == 0x90) {
                    int pitch = Pm_MessageData1(buffer[0].message);
                    int vel = Pm_MessageData2(buffer[0].message);
                    int is_on = (vel > 0);
                    if (verbose) {
                        printf("Note pitch %d vel %d\n", pitch, vel);
                    }
                    msgcnt++;
                    if (pitch != expected_pitch || expected_on != is_on) {
                        printf("Unexpected note-on: pitch %d vel %d, "
                               "expected: pitch %d Note%s\n", pitch, vel,
                               expected_pitch, (expected_on ? "On" : "Off"));
                    }
                    if (is_on) {
                        expected_on = FALSE;
                        expected_pitch = pitch;
                    } else {
                        expected_on = TRUE;
                        expected_pitch = (pitch + 1) % 72;
                        if (expected_pitch < 60) expected_pitch = 60;
                    }
                    if (last_timestamp >= 0) {
                        last_delta = buffer[0].timestamp - last_timestamp;
                    }
                    last_timestamp = buffer[0].timestamp;
                }
            }
        }
        if (now >= report_time) {
            printf("%d msgs/sec", msgcnt);
            /* if available, print the last timestamp and last delta time */
            if (last_timestamp >= 0) {
                printf(" last timestamp %d", (int) last_timestamp);
                last_timestamp = -1;
            }
            if (last_delta >= 0) {
                printf(" last delta time %d", (int) last_delta);
                last_delta = -1;
            }
            printf("\n");
            report_time += 1000;
            msgcnt = 0;
        }
    }
}


void show_usage(void)
{
    printf("Usage: fastrcv [-h] [-v] [-d device], where\n"
           "device is the PortMidi device number,\n"
           "-h means help,\n"
           "-v means verbose (print messages)\n");
}

int main(int argc, char *argv[])
{
    int default_in;
    int default_out;
    char *deflt;

    int i = 0;
    int test_input = 0, test_output = 0, test_both = 0;
    int stream_test = 0;
    int device_valid = FALSE;
    
    if (sizeof(void *) == 8) 
        printf("Apparently this is a 64-bit machine.\n");
    else if (sizeof(void *) == 4) 
        printf ("Apparently this is a 32-bit machine.\n");
    
    if (argc <= 1) {
        show_usage();
    } else {
        for (i = 1; i < argc; i++) {
            if (strcmp(argv[i], "-h") == 0) {
                show_usage();
            } else if (strcmp(argv[i], "-v") == 0) {
                verbose = TRUE;
            } else if (strcmp(argv[i], "-d") == 0) {
                i = i + 1;
                deviceno = atoi(argv[i]);
                printf("Device will be %d\n", deviceno);
            } else {
                show_usage();
            }
        }
    }

    /* list device information */
    default_in = Pm_GetDefaultInputDeviceID();
    default_out = Pm_GetDefaultOutputDeviceID();
    for (i = 0; i < Pm_CountDevices(); i++) {
        const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
        if (!info->output) {
            printf("%d: %s, %s", i, info->interf, info->name);
            if (i == deviceno) {
                device_valid = TRUE;
                deflt = "selected ";
            } else if (i == default_out) {
                deflt = "default ";
            } else {
                deflt = "";
            }                      
            printf(" (%sinput)\n", deflt);
        }
    }
    printf("%d: Create virtual port named \"fastrcv\"", i);
    if (i == deviceno) {
        device_valid = TRUE;
        deflt = "selected ";
    } else {
        deflt = "";
    }        
    printf(" (%sinput)\n", deflt);
    
    if (!device_valid) {
        deviceno = get_number("Input device number: ");
    }

    fastrcv_test();
    return 0;
}
