970 lines
29 KiB
C
970 lines
29 KiB
C
/*
|
|
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 <assert.h>
|
|
#include <errno.h>
|
|
#include <math.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <getopt.h>
|
|
|
|
#include <jack/jack.h>
|
|
#include <jack/midiport.h>
|
|
|
|
#ifdef WIN32
|
|
#include <windows.h>
|
|
#include <unistd.h>
|
|
#else
|
|
#include <semaphore.h>
|
|
#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;
|
|
}
|