// gcc -o jack_midi_dump -Wall midi_dump.c -ljack -pthread

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <inttypes.h>
#include <jack/jack.h>
#include <jack/midiport.h>
#include <jack/ringbuffer.h>

#ifdef __MINGW32__
#include <pthread.h>
#endif

#ifndef WIN32
#include <signal.h>
#include <pthread.h>
#include <sys/mman.h>
#endif

static jack_port_t* port;
static jack_ringbuffer_t *rb = NULL;
static pthread_mutex_t msg_thread_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t data_ready = PTHREAD_COND_INITIALIZER;

static int keeprunning = 1;
static uint64_t monotonic_cnt = 0;

#define RBSIZE 100
#define MSG_BUFFER_SIZE 4096

typedef struct {
	uint8_t  buffer[MSG_BUFFER_SIZE];
	uint32_t size;
	uint32_t tme_rel;
	uint64_t tme_mon;
} midimsg;

static void
describe (midimsg* event)
{
	if (event->size == 0) {
		return;
	}

	uint8_t type = event->buffer[0] & 0xf0;
	uint8_t channel = event->buffer[0] & 0xf;

	switch (type) {
		case 0x90:
			assert (event->size == 3);
			printf (" note on  (channel %2d): pitch %3d, velocity %3d", channel, event->buffer[1], event->buffer[2]);
			break;
		case 0x80:
			assert (event->size == 3);
			printf (" note off (channel %2d): pitch %3d, velocity %3d", channel, event->buffer[1], event->buffer[2]);
			break;
		case 0xb0:
			assert (event->size == 3);
			printf (" control change (channel %2d): controller %3d, value %3d", channel, event->buffer[1], event->buffer[2]);
			break;
		default:
			break;
	}
}

int
process (jack_nframes_t frames, void* arg)
{
	void* buffer;
	jack_nframes_t N;
	jack_nframes_t i;

	buffer = jack_port_get_buffer (port, frames);
	assert (buffer);
	
	N = jack_midi_get_event_count (buffer);
	for (i = 0; i < N; ++i) {
		jack_midi_event_t event;
		int r;
		r = jack_midi_event_get (&event, buffer, i);

		if (r != 0) {continue;}

		if (event.size > MSG_BUFFER_SIZE) {
			fprintf(stderr, "Error: MIDI message was too large, skipping event. Max. allowed size: %d bytes\n", MSG_BUFFER_SIZE);
		}
		else if (jack_ringbuffer_write_space (rb) >= sizeof(midimsg)) {
			midimsg m;
			m.tme_mon = monotonic_cnt;
			m.tme_rel = event.time;
			m.size    = event.size;
			memcpy (m.buffer, event.buffer, event.size);
			jack_ringbuffer_write (rb, (void *) &m, sizeof(midimsg));
		}
		else {
			fprintf (stderr, "Error: ringbuffer was full, skipping event.\n");
		}
	}

	monotonic_cnt += frames;

	if (pthread_mutex_trylock (&msg_thread_lock) == 0) {
		pthread_cond_signal (&data_ready);
		pthread_mutex_unlock (&msg_thread_lock);
	}

	return 0;
}

static void wearedone(int sig) {
	fprintf(stderr, "Shutting down\n");
	keeprunning = 0;
	/* main loop might be blocked by data_ready when jack server dies. */
	if (pthread_mutex_trylock (&msg_thread_lock) == 0) {
		pthread_cond_signal (&data_ready);
		pthread_mutex_unlock (&msg_thread_lock);
	}
}

static void usage (int status) {
	printf ("jack_midi_dump - JACK MIDI Monitor.\n\n");
	printf ("Usage: jack_midi_dump [ OPTIONS ] [CLIENT-NAME]\n\n");
	printf ("Options:\n\
  -a        use absolute timestamps relative to application start\n\
  -h        display this help and exit\n\
  -r        use relative timestamps to previous MIDI event\n\
\n");
	printf ("\n\
This tool listens for MIDI events on a JACK MIDI port and prints\n\
the message to stdout.\n\
\n\
If no client name is given it defaults to 'midi-monitor'.\n\
\n\
See also: jackd(1)\n\
\n");
	exit (status);
}

int
main (int argc, char* argv[])
{
	jack_client_t* client;
	char const default_name[] = "midi-monitor";
	char const * client_name;
	int time_format = 0;
	int r;

	int cn = 1;

	if (argc > 1) {
		if      (!strcmp (argv[1], "-a")) { time_format = 1; cn = 2; }
		else if (!strcmp (argv[1], "-r")) { time_format = 2; cn = 2; }
		else if (!strcmp (argv[1], "-h")) { usage (EXIT_SUCCESS); }
		else if (argv[1][0] == '-')       { usage (EXIT_FAILURE); }
	}

	if (argc > cn) {
		client_name = argv[cn];
	} else {
		client_name = default_name;
	}

	client = jack_client_open (client_name, JackNullOption, NULL);
	if (client == NULL) {
		fprintf (stderr, "Could not create JACK client.\n");
		exit (EXIT_FAILURE);
	}

	rb = jack_ringbuffer_create (RBSIZE * sizeof(midimsg));

	jack_set_process_callback (client, process, 0);

	port = jack_port_register (client, "input", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);
	if (port == NULL) {
		fprintf (stderr, "Could not register port.\n");
		exit (EXIT_FAILURE);
	}

#ifndef WIN32
	if (mlockall (MCL_CURRENT | MCL_FUTURE)) {
		fprintf (stderr, "Warning: Can not lock memory.\n");
	}
#endif

	r = jack_activate (client);
	if (r != 0) {
		fprintf (stderr, "Could not activate client.\n");
		exit (EXIT_FAILURE);
	}

#ifdef WIN32
	signal(SIGINT, wearedone);
	signal(SIGABRT, wearedone);
	signal(SIGTERM, wearedone);
#else
	signal(SIGQUIT, wearedone);
	signal(SIGTERM, wearedone);
	signal(SIGHUP, wearedone);
	signal(SIGINT, wearedone);
#endif

	pthread_mutex_lock (&msg_thread_lock);

	uint64_t prev_event = 0;
	while (keeprunning) {
		const int mqlen = jack_ringbuffer_read_space (rb) / sizeof(midimsg);
		int i;
		for (i=0; i < mqlen; ++i) {
			size_t j;
			midimsg m;
			jack_ringbuffer_read(rb, (char*) &m, sizeof(midimsg));

			switch(time_format) {
				case 1:
					printf ("%7"PRId64":", m.tme_rel + m.tme_mon);
					break;
				case 2:
					printf ("%+6"PRId64":", m.tme_rel + m.tme_mon - prev_event);
					break;
				default:
					printf ("%4d:", m.tme_rel);
					break;
			}
			for (j = 0; j < m.size && j < sizeof(m.buffer); ++j) {
				printf (" %02x", m.buffer[j]);
			}

			describe (&m);
			printf("\n");
			prev_event = m.tme_rel + m.tme_mon;
		}
		fflush (stdout);
		pthread_cond_wait (&data_ready, &msg_thread_lock);
	}
	pthread_mutex_unlock (&msg_thread_lock);

	jack_deactivate (client);
	jack_client_close (client);
	jack_ringbuffer_free (rb);

	return 0;
}
