/* Copyright (C) 2010 Devin Anderson This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* * This program is used to measure MIDI latency and jitter. It writes MIDI * messages to one port and calculates how long it takes before it reads the * same MIDI message over another port. It was written to calculate the * latency and jitter of hardware and JACK hardware drivers, but might have * other practical applications. * * The latency results of the program include the latency introduced by the * JACK system. Because JACK has sample accurate MIDI, the same latency * imposed on audio is also imposed on MIDI going through the system. Make * sure you take this into account before complaining to me or (*especially*) * other JACK developers about reported MIDI latency. * * The jitter results are a little more interesting. The program attempts to * calculate 'average jitter' and 'peak jitter', as defined here: * * http://openmuse.org/transport/fidelity.html * * It also outputs a jitter plot, which gives you a more specific idea about * the MIDI jitter for the ports you're testing. This is useful for catching * extreme jitter values, and for analyzing the amount of truth in the * technical specifications for your MIDI interface(s). :) * * This program is loosely based on 'alsa-midi-latency-test' in the ALSA test * suite. * * To port this program to non-POSIX platforms, you'll have to include * implementations for semaphores and command-line argument handling. */ #include #include #include #include #include #include #include #include #include #include #ifdef WIN32 #include #include #else #include #endif #define ABS(x) (((x) >= 0) ? (x) : (-(x))) #ifdef WIN32 typedef HANDLE semaphore_t; #else typedef sem_t *semaphore_t; #endif const char *ERROR_MSG_TIMEOUT = "timed out while waiting for MIDI message"; const char *ERROR_RESERVE = "could not reserve MIDI event on port buffer"; const char *ERROR_SHUTDOWN = "the JACK server has been shutdown"; const char *SOURCE_EVENT_RESERVE = "jack_midi_event_reserve"; const char *SOURCE_PROCESS = "handle_process"; const char *SOURCE_SHUTDOWN = "handle_shutdown"; const char *SOURCE_SIGNAL_SEMAPHORE = "signal_semaphore"; const char *SOURCE_WAIT_SEMAPHORE = "wait_semaphore"; char *alias1; char *alias2; jack_client_t *client; semaphore_t connect_semaphore; volatile int connections_established; const char *error_message; const char *error_source; jack_nframes_t highest_latency; jack_time_t highest_latency_time; jack_latency_range_t in_latency_range; jack_port_t *in_port; semaphore_t init_semaphore; jack_nframes_t last_activity; jack_time_t last_activity_time; jack_time_t *latency_time_values; jack_nframes_t *latency_values; jack_nframes_t lowest_latency; jack_time_t lowest_latency_time; jack_midi_data_t *message_1; jack_midi_data_t *message_2; int messages_received; int messages_sent; size_t message_size; jack_latency_range_t out_latency_range; jack_port_t *out_port; semaphore_t process_semaphore; volatile sig_atomic_t process_state; char *program_name; jack_port_t *remote_in_port; jack_port_t *remote_out_port; size_t samples; const char *target_in_port_name; const char *target_out_port_name; int timeout; jack_nframes_t total_latency; jack_time_t total_latency_time; int unexpected_messages; int xrun_count; #ifdef WIN32 char semaphore_error_msg[1024]; #endif static void output_error(const char *source, const char *message); static void output_usage(void); static void set_process_error(const char *source, const char *message); static int signal_semaphore(semaphore_t semaphore); static jack_port_t * update_connection(jack_port_t *remote_port, int connected, jack_port_t *local_port, jack_port_t *current_port, const char *target_name); static int wait_semaphore(semaphore_t semaphore, int block); static semaphore_t create_semaphore(int id) { semaphore_t semaphore; #ifdef WIN32 semaphore = CreateSemaphore(NULL, 0, 2, NULL); #elif defined (__APPLE__) char name[128]; sprintf(name, "midi_sem_%d", id); semaphore = sem_open(name, O_CREAT, 0777, 0); if (semaphore == (sem_t *) SEM_FAILED) { semaphore = NULL; } #else semaphore = malloc(sizeof(semaphore_t)); if (semaphore != NULL) { if (sem_init(semaphore, 0, 0)) { free(semaphore); semaphore = NULL; } } #endif return semaphore; } static void destroy_semaphore(semaphore_t semaphore, int id) { #ifdef WIN32 CloseHandle(semaphore); #else sem_destroy(semaphore); #ifdef __APPLE__ { char name[128]; sprintf(name, "midi_sem_%d", id); sem_close(semaphore); sem_unlink(name); } #else free(semaphore); #endif #endif } static void die(const char *source, const char *error_message) { output_error(source, error_message); output_usage(); exit(EXIT_FAILURE); } static const char * get_semaphore_error(void) { #ifdef WIN32 DWORD error = GetLastError(); if (! FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), semaphore_error_msg, 1024, NULL)) { snprintf(semaphore_error_msg, 1023, "Unknown OS error code '%ld'", error); } return semaphore_error_msg; #else return strerror(errno); #endif } static void handle_info(const char *message) { /* Suppress info */ } static void handle_port_connection_change(jack_port_id_t port_id_1, jack_port_id_t port_id_2, int connected, void *arg) { jack_port_t *port_1; jack_port_t *port_2; if ((remote_in_port != NULL) && (remote_out_port != NULL)) { return; } port_1 = jack_port_by_id(client, port_id_1); port_2 = jack_port_by_id(client, port_id_2); /* The 'update_connection' call is not RT-safe. It calls 'jack_port_get_connections' and 'jack_free'. This might be a problem with JACK 1, as this callback runs in the process thread in JACK 1. */ if (port_1 == in_port) { remote_in_port = update_connection(port_2, connected, in_port, remote_in_port, target_in_port_name); } else if (port_2 == in_port) { remote_in_port = update_connection(port_1, connected, in_port, remote_in_port, target_in_port_name); } else if (port_1 == out_port) { remote_out_port = update_connection(port_2, connected, out_port, remote_out_port, target_out_port_name); } else if (port_2 == out_port) { remote_out_port = update_connection(port_1, connected, out_port, remote_out_port, target_out_port_name); } if ((remote_in_port != NULL) && (remote_out_port != NULL)) { connections_established = 1; if (! signal_semaphore(connect_semaphore)) { /* Sigh ... */ die("post_semaphore", get_semaphore_error()); } if (! signal_semaphore(init_semaphore)) { /* Sigh ... */ die("post_semaphore", get_semaphore_error()); } } } static int handle_process(jack_nframes_t frames, void *arg) { jack_midi_data_t *buffer; jack_midi_event_t event; jack_nframes_t event_count; jack_nframes_t event_time; jack_nframes_t frame; size_t i; jack_nframes_t last_frame_time; jack_midi_data_t *message; jack_time_t microseconds; void *port_buffer; jack_time_t time; jack_midi_clear_buffer(jack_port_get_buffer(out_port, frames)); switch (process_state) { case 0: /* State: initializing */ switch (wait_semaphore(init_semaphore, 0)) { case -1: set_process_error(SOURCE_WAIT_SEMAPHORE, get_semaphore_error()); /* Fallthrough on purpose */ case 0: return 0; } highest_latency = 0; lowest_latency = 0; messages_received = 0; messages_sent = 0; process_state = 1; total_latency = 0; total_latency_time = 0; unexpected_messages = 0; xrun_count = 0; jack_port_get_latency_range(remote_in_port, JackCaptureLatency, &in_latency_range); jack_port_get_latency_range(remote_out_port, JackPlaybackLatency, &out_latency_range); goto send_message; case 1: /* State: processing */ port_buffer = jack_port_get_buffer(in_port, frames); event_count = jack_midi_get_event_count(port_buffer); last_frame_time = jack_last_frame_time(client); for (i = 0; i < event_count; i++) { jack_midi_event_get(&event, port_buffer, i); message = (messages_received % 2) ? message_2 : message_1; if ((event.size == message_size) && (! memcmp(message, event.buffer, message_size * sizeof(jack_midi_data_t)))) { goto found_message; } unexpected_messages++; } microseconds = jack_frames_to_time(client, last_frame_time) - last_activity_time; if ((microseconds / 1000000) >= timeout) { set_process_error(SOURCE_PROCESS, ERROR_MSG_TIMEOUT); } break; found_message: event_time = last_frame_time + event.time; frame = event_time - last_activity; time = jack_frames_to_time(client, event_time) - last_activity_time; if ((! highest_latency) || (frame > highest_latency)) { highest_latency = frame; highest_latency_time = time; } if ((! lowest_latency) || (frame < lowest_latency)) { lowest_latency = frame; lowest_latency_time = time; } latency_time_values[messages_received] = time; latency_values[messages_received] = frame; total_latency += frame; total_latency_time += time; messages_received++; if (messages_received == samples) { process_state = 2; if (! signal_semaphore(process_semaphore)) { /* Sigh ... */ die(SOURCE_SIGNAL_SEMAPHORE, get_semaphore_error()); } break; } send_message: frame = (jack_nframes_t) ((((double) rand()) / RAND_MAX) * frames); if (frame >= frames) { frame = frames - 1; } port_buffer = jack_port_get_buffer(out_port, frames); buffer = jack_midi_event_reserve(port_buffer, frame, message_size); if (buffer == NULL) { set_process_error(SOURCE_EVENT_RESERVE, ERROR_RESERVE); break; } message = (messages_sent % 2) ? message_2 : message_1; memcpy(buffer, message, message_size * sizeof(jack_midi_data_t)); last_activity = jack_last_frame_time(client) + frame; last_activity_time = jack_frames_to_time(client, last_activity); messages_sent++; case 2: /* State: finished - do nothing */ case -1: /* State: error - do nothing */ case -2: /* State: signalled - do nothing */ ; } return 0; } static void handle_shutdown(void *arg) { set_process_error(SOURCE_SHUTDOWN, ERROR_SHUTDOWN); } static void handle_signal(int sig) { process_state = -2; if (! signal_semaphore(connect_semaphore)) { /* Sigh ... */ die(SOURCE_SIGNAL_SEMAPHORE, get_semaphore_error()); } if (! signal_semaphore(process_semaphore)) { /* Sigh ... */ die(SOURCE_SIGNAL_SEMAPHORE, get_semaphore_error()); } } static int handle_xrun(void *arg) { xrun_count++; return 0; } static void output_error(const char *source, const char *message) { fprintf(stderr, "%s: %s: %s\n", program_name, source, message); } static void output_usage(void) { fprintf(stderr, "Usage: %s [options] [out-port-name in-port-name]\n\n" "\t-h, --help print program usage\n" "\t-m, --message-size=size set size of MIDI messages to send " "(default: 3)\n" "\t-s, --samples=n number of MIDI messages to send " "(default: 1024)\n" "\t-t, --timeout=seconds message timeout (default: 5)\n\n", program_name); } static unsigned long parse_positive_number_arg(char *s, char *name) { char *end_ptr; unsigned long result; errno = 0; result = strtoul(s, &end_ptr, 10); if (errno) { die(name, strerror(errno)); } if (*s == '\0') { die(name, "argument value cannot be empty"); } if (*end_ptr != '\0') { die(name, "invalid value"); } if (! result) { die(name, "must be a positive number"); } return result; } static int register_signal_handler(void (*func)(int)) { #ifdef WIN32 if (signal(SIGABRT, func) == SIG_ERR) { return 0; } #else if (signal(SIGQUIT, func) == SIG_ERR) { return 0; } if (signal(SIGHUP, func) == SIG_ERR) { return 0; } #endif if (signal(SIGINT, func) == SIG_ERR) { return 0; } if (signal(SIGTERM, func) == SIG_ERR) { return 0; } return 1; } static void set_process_error(const char *source, const char *message) { error_source = source; error_message = message; process_state = -1; if (! signal_semaphore(process_semaphore)) { /* Sigh ... */ output_error(source, message); die(SOURCE_SIGNAL_SEMAPHORE, get_semaphore_error()); } } static int signal_semaphore(semaphore_t semaphore) { #ifdef WIN32 return ReleaseSemaphore(semaphore, 1, NULL); #else return ! sem_post(semaphore); #endif } static jack_port_t * update_connection(jack_port_t *remote_port, int connected, jack_port_t *local_port, jack_port_t *current_port, const char *target_name) { if (connected) { if (current_port) { return current_port; } if (target_name) { char *aliases[2]; if (! strcmp(target_name, jack_port_name(remote_port))) { return remote_port; } aliases[0] = alias1; aliases[1] = alias2; switch (jack_port_get_aliases(remote_port, aliases)) { case -1: /* Sigh ... */ die("jack_port_get_aliases", "Failed to get port aliases"); case 2: if (! strcmp(target_name, alias2)) { return remote_port; } /* Fallthrough on purpose */ case 1: if (! strcmp(target_name, alias1)) { return remote_port; } /* Fallthrough on purpose */ case 0: return NULL; } /* This shouldn't happen. */ assert(0); } return remote_port; } if (! strcmp(jack_port_name(remote_port), jack_port_name(current_port))) { const char **port_names; if (target_name) { return NULL; } port_names = jack_port_get_connections(local_port); if (port_names == NULL) { return NULL; } /* If a connected port is disconnected and other ports are still connected, then we take the first port name in the array and use it as our remote port. It's a dumb implementation. */ current_port = jack_port_by_name(client, port_names[0]); jack_free(port_names); if (current_port == NULL) { /* Sigh */ die("jack_port_by_name", "failed to get port by name"); } } return current_port; } static int wait_semaphore(semaphore_t semaphore, int block) { #ifdef WIN32 DWORD result = WaitForSingleObject(semaphore, block ? INFINITE : 0); switch (result) { case WAIT_OBJECT_0: return 1; case WAIT_TIMEOUT: return 0; } return -1; #else if (block) { while (sem_wait(semaphore)) { if (errno != EINTR) { return -1; } } } else { while (sem_trywait(semaphore)) { switch (errno) { case EAGAIN: return 0; case EINTR: continue; default: return -1; } } } return 1; #endif } int main(int argc, char **argv) { int jitter_plot[101]; int latency_plot[101]; int long_index = 0; struct option long_options[] = { {"help", 0, NULL, 'h'}, {"message-size", 1, NULL, 'm'}, {"samples", 1, NULL, 's'}, {"timeout", 1, NULL, 't'} }; size_t name_arg_count; size_t name_size; char *option_string = "hm:s:t:"; int show_usage = 0; connections_established = 0; error_message = NULL; message_size = 3; program_name = argv[0]; remote_in_port = 0; remote_out_port = 0; samples = 1024; timeout = 5; for (;;) { signed char c = getopt_long(argc, argv, option_string, long_options, &long_index); switch (c) { case 'h': show_usage = 1; break; case 'm': message_size = parse_positive_number_arg(optarg, "message-size"); break; case 's': samples = parse_positive_number_arg(optarg, "samples"); break; case 't': timeout = parse_positive_number_arg(optarg, "timeout"); break; default: { char *s = "'- '"; s[2] = c; die(s, "invalid switch"); } case -1: if (show_usage) { output_usage(); exit(EXIT_SUCCESS); } goto parse_port_names; case 1: /* end of switch :) */ ; } } parse_port_names: name_arg_count = argc - optind; switch (name_arg_count) { case 2: target_in_port_name = argv[optind + 1]; target_out_port_name = argv[optind]; break; case 0: target_in_port_name = 0; target_out_port_name = 0; break; default: output_usage(); return EXIT_FAILURE; } name_size = jack_port_name_size(); alias1 = malloc(name_size * sizeof(char)); if (alias1 == NULL) { error_message = strerror(errno); error_source = "malloc"; goto show_error; } alias2 = malloc(name_size * sizeof(char)); if (alias2 == NULL) { error_message = strerror(errno); error_source = "malloc"; goto free_alias1; } latency_values = malloc(sizeof(jack_nframes_t) * samples); if (latency_values == NULL) { error_message = strerror(errno); error_source = "malloc"; goto free_alias2; } latency_time_values = malloc(sizeof(jack_time_t) * samples); if (latency_time_values == NULL) { error_message = strerror(errno); error_source = "malloc"; goto free_latency_values; } message_1 = malloc(message_size * sizeof(jack_midi_data_t)); if (message_1 == NULL) { error_message = strerror(errno); error_source = "malloc"; goto free_latency_time_values; } message_2 = malloc(message_size * sizeof(jack_midi_data_t)); if (message_2 == NULL) { error_message = strerror(errno); error_source = "malloc"; goto free_message_1; } switch (message_size) { case 1: message_1[0] = 0xf6; message_2[0] = 0xfe; break; case 2: message_1[0] = 0xc0; message_1[1] = 0x00; message_2[0] = 0xd0; message_2[1] = 0x7f; break; case 3: message_1[0] = 0x80; message_1[1] = 0x00; message_1[2] = 0x00; message_2[0] = 0x90; message_2[1] = 0x7f; message_2[2] = 0x7f; break; default: message_1[0] = 0xf0; memset(message_1 + 1, 0, (message_size - 2) * sizeof(jack_midi_data_t)); message_1[message_size - 1] = 0xf7; message_2[0] = 0xf0; memset(message_2 + 1, 0x7f, (message_size - 2) * sizeof(jack_midi_data_t)); message_2[message_size - 1] = 0xf7; } client = jack_client_open(program_name, JackNullOption, NULL); if (client == NULL) { error_message = "failed to open JACK client"; error_source = "jack_client_open"; goto free_message_2; } in_port = jack_port_register(client, "in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); if (in_port == NULL) { error_message = "failed to register MIDI-in port"; error_source = "jack_port_register"; goto close_client; } out_port = jack_port_register(client, "out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); if (out_port == NULL) { error_message = "failed to register MIDI-out port"; error_source = "jack_port_register"; goto unregister_in_port; } if (jack_set_process_callback(client, handle_process, NULL)) { error_message = "failed to set process callback"; error_source = "jack_set_process_callback"; goto unregister_out_port; } if (jack_set_xrun_callback(client, handle_xrun, NULL)) { error_message = "failed to set xrun callback"; error_source = "jack_set_xrun_callback"; goto unregister_out_port; } if (jack_set_port_connect_callback(client, handle_port_connection_change, NULL)) { error_message = "failed to set port connection callback"; error_source = "jack_set_port_connect_callback"; goto unregister_out_port; } jack_on_shutdown(client, handle_shutdown, NULL); jack_set_info_function(handle_info); process_state = 0; connect_semaphore = create_semaphore(0); if (connect_semaphore == NULL) { error_message = get_semaphore_error(); error_source = "create_semaphore"; goto unregister_out_port; } init_semaphore = create_semaphore(1); if (init_semaphore == NULL) { error_message = get_semaphore_error(); error_source = "create_semaphore"; goto destroy_connect_semaphore;; } process_semaphore = create_semaphore(2); if (process_semaphore == NULL) { error_message = get_semaphore_error(); error_source = "create_semaphore"; goto destroy_init_semaphore; } if (jack_activate(client)) { error_message = "could not activate client"; error_source = "jack_activate"; goto destroy_process_semaphore; } if (name_arg_count) { if (jack_connect(client, jack_port_name(out_port), target_out_port_name)) { error_message = "could not connect MIDI out port"; error_source = "jack_connect"; goto deactivate_client; } if (jack_connect(client, target_in_port_name, jack_port_name(in_port))) { error_message = "could not connect MIDI in port"; error_source = "jack_connect"; goto deactivate_client; } } if (! register_signal_handler(handle_signal)) { error_message = strerror(errno); error_source = "register_signal_handler"; goto deactivate_client; } printf("Waiting for connections ...\n"); if (wait_semaphore(connect_semaphore, 1) == -1) { error_message = get_semaphore_error(); error_source = "wait_semaphore"; goto deactivate_client; } if (connections_established) { printf("Waiting for test completion ...\n\n"); if (wait_semaphore(process_semaphore, 1) == -1) { error_message = get_semaphore_error(); error_source = "wait_semaphore"; goto deactivate_client; } } if (! register_signal_handler(SIG_DFL)) { error_message = strerror(errno); error_source = "register_signal_handler"; goto deactivate_client; } if (process_state == 2) { double average_latency = ((double) total_latency) / samples; double average_latency_time = total_latency_time / samples; size_t i; double latency_plot_offset = floor(((double) lowest_latency_time) / 100.0) / 10.0; double sample_rate = (double) jack_get_sample_rate(client); jack_nframes_t total_jitter = 0; jack_time_t total_jitter_time = 0; for (i = 0; i <= 100; i++) { jitter_plot[i] = 0; latency_plot[i] = 0; } for (i = 0; i < samples; i++) { double latency_time_value = (double) latency_time_values[i]; double latency_plot_time = (latency_time_value / 1000.0) - latency_plot_offset; double jitter_time = ABS(average_latency_time - latency_time_value); if (latency_plot_time >= 10.0) { (latency_plot[100])++; } else { (latency_plot[(int) (latency_plot_time * 10.0)])++; } if (jitter_time >= 10000.0) { (jitter_plot[100])++; } else { (jitter_plot[(int) (jitter_time / 100.0)])++; } total_jitter += ABS(average_latency - ((double) latency_values[i])); total_jitter_time += jitter_time; } printf("Reported out-port latency: %.2f-%.2f ms (%u-%u frames)\n" "Reported in-port latency: %.2f-%.2f ms (%u-%u frames)\n" "Average latency: %.2f ms (%.2f frames)\n" "Lowest latency: %.2f ms (%u frames)\n" "Highest latency: %.2f ms (%u frames)\n" "Peak MIDI jitter: %.2f ms (%u frames)\n" "Average MIDI jitter: %.2f ms (%.2f frames)\n", (out_latency_range.min / sample_rate) * 1000.0, (out_latency_range.max / sample_rate) * 1000.0, out_latency_range.min, out_latency_range.max, (in_latency_range.min / sample_rate) * 1000.0, (in_latency_range.max / sample_rate) * 1000.0, in_latency_range.min, in_latency_range.max, average_latency_time / 1000.0, average_latency, lowest_latency_time / 1000.0, lowest_latency, highest_latency_time / 1000.0, highest_latency, (highest_latency_time - lowest_latency_time) / 1000.0, highest_latency - lowest_latency, (total_jitter_time / 1000.0) / samples, ((double) total_jitter) / samples); printf("\nJitter Plot:\n"); for (i = 0; i < 100; i++) { if (jitter_plot[i]) { printf("%.1f - %.1f ms: %d\n", ((float) i) / 10.0, ((float) (i + 1)) / 10.0, jitter_plot[i]); } } if (jitter_plot[100]) { printf(" > 10 ms: %d\n", jitter_plot[100]); } printf("\nLatency Plot:\n"); for (i = 0; i < 100; i++) { if (latency_plot[i]) { printf("%.1f - %.1f ms: %d\n", latency_plot_offset + (((float) i) / 10.0), latency_plot_offset + (((float) (i + 1)) / 10.0), latency_plot[i]); } } if (latency_plot[100]) { printf(" > %.1f ms: %d\n", latency_plot_offset + 10.0, latency_plot[100]); } } deactivate_client: jack_deactivate(client); printf("\nMessages sent: %d\nMessages received: %d\n", messages_sent, messages_received); if (unexpected_messages) { printf("Unexpected messages received: %d\n", unexpected_messages); } if (xrun_count) { printf("Xruns: %d\n", xrun_count); } destroy_process_semaphore: destroy_semaphore(process_semaphore, 2); destroy_init_semaphore: destroy_semaphore(init_semaphore, 1); destroy_connect_semaphore: destroy_semaphore(connect_semaphore, 0); unregister_out_port: jack_port_unregister(client, out_port); unregister_in_port: jack_port_unregister(client, in_port); close_client: jack_client_close(client); free_message_2: free(message_2); free_message_1: free(message_1); free_latency_time_values: free(latency_time_values); free_latency_values: free(latency_values); free_alias2: free(alias2); free_alias1: free(alias1); if (error_message != NULL) { show_error: output_error(error_source, error_message); exit(EXIT_FAILURE); } return EXIT_SUCCESS; }