617 lines
12 KiB
C
617 lines
12 KiB
C
/*
|
|
* ALSA SEQ < - > JACK MIDI bridge
|
|
*
|
|
* Copyright (c) 2006,2007 Dmitry S. Baikov <c0ff@konstruktiv.org>
|
|
* Copyright (c) 2007,2008,2009,2011 Nedko Arnaudov <nedko@arnaudov.name>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU 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
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <signal.h>
|
|
#include <semaphore.h>
|
|
#include <stdbool.h>
|
|
#include <sys/stat.h>
|
|
#include <pthread.h>
|
|
|
|
#include <alsa/asoundlib.h>
|
|
|
|
#include <jack/jack.h>
|
|
#include <jack/midiport.h>
|
|
#include <jack/ringbuffer.h>
|
|
|
|
#if HAVE_DBUS_1
|
|
# include <dbus/dbus.h>
|
|
#endif
|
|
|
|
#include <getopt.h>
|
|
|
|
#include "list.h"
|
|
#include "structs.h"
|
|
#include "port.h"
|
|
#include "port_thread.h"
|
|
#include "port_hash.h"
|
|
#include "log.h"
|
|
#if HAVE_DBUS_1
|
|
# include "dbus.h"
|
|
#endif
|
|
#include "a2jmidid.h"
|
|
#include "paths.h"
|
|
#include "conf.h"
|
|
#include "jack.h"
|
|
#include "siginfo/siginfo.h"
|
|
#if HAVE_GITVERSION_H
|
|
#include "gitversion.h"
|
|
#endif
|
|
#include "dbus_iface_control.h"
|
|
|
|
#define MAIN_LOOP_SLEEP_INTERVAL 50 // in milliseconds
|
|
|
|
bool g_keep_walking = true;
|
|
bool g_keep_alsa_walking = false;
|
|
bool g_stop_request = false;
|
|
static bool g_started = false;
|
|
struct a2j * g_a2j = NULL;
|
|
size_t g_max_jack_port_name_size;
|
|
bool g_disable_port_uniqueness = false;
|
|
bool g_filter_note_on = true;
|
|
|
|
bool g_a2j_export_hw_ports = false;
|
|
char * g_a2j_jack_server_name = "default";
|
|
|
|
static
|
|
void
|
|
a2j_sigint_handler(
|
|
int i)
|
|
{
|
|
((void)(i)); /* unreferenced parameter */
|
|
g_keep_walking = false;
|
|
}
|
|
|
|
static
|
|
bool
|
|
a2j_stream_init(
|
|
struct a2j * self,
|
|
int dir)
|
|
{
|
|
struct a2j_stream *str = &self->stream[dir];
|
|
|
|
str->new_ports = jack_ringbuffer_create(MAX_PORTS * sizeof(struct a2j_port *));
|
|
if (str->new_ports == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
snd_midi_event_new(MAX_EVENT_SIZE, &str->codec);
|
|
INIT_LIST_HEAD(&str->list);
|
|
|
|
return true;
|
|
}
|
|
|
|
static
|
|
void
|
|
a2j_stream_attach(
|
|
struct a2j_stream * stream_ptr)
|
|
{
|
|
((void)(stream_ptr)); /* unreferenced parameter */
|
|
}
|
|
|
|
static
|
|
void
|
|
a2j_stream_detach(
|
|
struct a2j_stream * stream_ptr)
|
|
{
|
|
struct a2j_port * port_ptr;
|
|
struct list_head * node_ptr;
|
|
|
|
while (!list_empty(&stream_ptr->list))
|
|
{
|
|
node_ptr = stream_ptr->list.next;
|
|
list_del(node_ptr);
|
|
port_ptr = list_entry(node_ptr, struct a2j_port, siblings);
|
|
a2j_info("port deleted: %s", port_ptr->name);
|
|
a2j_port_free(port_ptr);
|
|
}
|
|
}
|
|
|
|
static
|
|
void
|
|
a2j_stream_close(
|
|
struct a2j * self,
|
|
int dir)
|
|
{
|
|
struct a2j_stream *str = &self->stream[dir];
|
|
|
|
if (str->codec)
|
|
snd_midi_event_free(str->codec);
|
|
if (str->new_ports)
|
|
jack_ringbuffer_free(str->new_ports);
|
|
}
|
|
|
|
struct a2j * a2j_new(void)
|
|
{
|
|
int error;
|
|
void * thread_status;
|
|
|
|
struct a2j *self = calloc(1, sizeof(struct a2j));
|
|
a2j_debug("midi: new");
|
|
if (!self)
|
|
{
|
|
a2j_error("calloc() failed to allocate a2j struct");
|
|
goto fail;
|
|
}
|
|
|
|
self->port_add = jack_ringbuffer_create(2 * MAX_PORTS * sizeof(snd_seq_addr_t));
|
|
if (self->port_add == NULL)
|
|
{
|
|
goto free_self;
|
|
}
|
|
|
|
self->port_del = jack_ringbuffer_create(2 * MAX_PORTS * sizeof(struct a2j_port *));
|
|
if (self->port_del == NULL)
|
|
{
|
|
goto free_ringbuffer_add;
|
|
}
|
|
|
|
self->outbound_events = jack_ringbuffer_create(MAX_EVENT_SIZE * 16 * sizeof(struct a2j_delivery_event));
|
|
if (self->outbound_events == NULL)
|
|
{
|
|
goto free_ringbuffer_del;
|
|
}
|
|
|
|
if (!a2j_stream_init(self, A2J_PORT_CAPTURE))
|
|
{
|
|
goto free_ringbuffer_outbound;
|
|
}
|
|
|
|
if (!a2j_stream_init(self, A2J_PORT_PLAYBACK))
|
|
{
|
|
goto close_capture_stream;
|
|
}
|
|
|
|
error = snd_seq_open(&self->seq, "hw", SND_SEQ_OPEN_DUPLEX, 0);
|
|
if (error < 0)
|
|
{
|
|
a2j_error("failed to open alsa seq");
|
|
goto close_playback_stream;
|
|
}
|
|
|
|
error = snd_seq_set_client_name(self->seq, "a2jmidid");
|
|
if (error < 0)
|
|
{
|
|
a2j_error("snd_seq_set_client_name() failed");
|
|
goto close_seq_client;
|
|
}
|
|
|
|
self->port_id = snd_seq_create_simple_port(
|
|
self->seq,
|
|
"port",
|
|
SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_WRITE
|
|
#ifndef DEBUG
|
|
|SND_SEQ_PORT_CAP_NO_EXPORT
|
|
#endif
|
|
,SND_SEQ_PORT_TYPE_APPLICATION);
|
|
if (self->port_id < 0)
|
|
{
|
|
a2j_error("snd_seq_create_simple_port() failed");
|
|
goto close_seq_client;
|
|
}
|
|
|
|
self->client_id = snd_seq_client_id(self->seq);
|
|
if (self->client_id < 0)
|
|
{
|
|
a2j_error("snd_seq_client_id() failed");
|
|
goto close_seq_client;
|
|
}
|
|
|
|
self->queue = snd_seq_alloc_queue(self->seq);
|
|
if (self->queue < 0)
|
|
{
|
|
a2j_error("snd_seq_alloc_queue() failed");
|
|
goto close_seq_client;
|
|
}
|
|
|
|
snd_seq_start_queue(self->seq, self->queue, 0);
|
|
|
|
a2j_stream_attach(self->stream + A2J_PORT_CAPTURE);
|
|
a2j_stream_attach(self->stream + A2J_PORT_PLAYBACK);
|
|
|
|
error = snd_seq_nonblock(self->seq, 1);
|
|
if (error < 0)
|
|
{
|
|
a2j_error("snd_seq_nonblock() failed");
|
|
goto close_seq_client;
|
|
}
|
|
|
|
snd_seq_drop_input(self->seq);
|
|
|
|
a2j_add_ports(&self->stream[A2J_PORT_CAPTURE]);
|
|
a2j_add_ports(&self->stream[A2J_PORT_PLAYBACK]);
|
|
|
|
self->jack_client = a2j_jack_client_create(self, A2J_JACK_CLIENT_NAME, g_a2j_jack_server_name);
|
|
if (self->jack_client == NULL)
|
|
{
|
|
goto free_self;
|
|
}
|
|
|
|
if (sem_init(&self->io_semaphore, 0, 0) < 0)
|
|
{
|
|
a2j_error("can't create IO semaphore");
|
|
goto close_jack_client;
|
|
}
|
|
|
|
if (jack_activate(self->jack_client))
|
|
{
|
|
a2j_error("can't activate jack client");
|
|
goto sem_destroy;
|
|
}
|
|
|
|
g_keep_alsa_walking = true;
|
|
|
|
if (pthread_create(&self->alsa_input_thread, NULL, a2j_alsa_input_thread, self) < 0)
|
|
{
|
|
a2j_error("cannot start ALSA input thread");
|
|
goto sem_destroy;
|
|
}
|
|
|
|
/* wake the poll loop in the alsa input thread so initial ports are fetched */
|
|
error = snd_seq_connect_from(self->seq, self->port_id, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE);
|
|
if (error < 0)
|
|
{
|
|
a2j_error("snd_seq_connect_from() failed");
|
|
goto join_input_thread;
|
|
}
|
|
|
|
if (pthread_create(&self->alsa_output_thread, NULL, a2j_alsa_output_thread, self) < 0)
|
|
{
|
|
a2j_error("cannot start ALSA output thread");
|
|
goto disconnect;
|
|
}
|
|
|
|
return self;
|
|
|
|
disconnect:
|
|
g_keep_alsa_walking = false; /* tell alsa threads to stop */
|
|
snd_seq_disconnect_from(self->seq, self->port_id, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE);
|
|
join_input_thread:
|
|
pthread_join(self->alsa_input_thread, &thread_status);
|
|
sem_destroy:
|
|
sem_destroy(&self->io_semaphore);
|
|
close_jack_client:
|
|
error = jack_client_close(self->jack_client);
|
|
if (error != 0)
|
|
{
|
|
a2j_error("Cannot close jack client");
|
|
}
|
|
close_seq_client:
|
|
snd_seq_close(self->seq);
|
|
close_playback_stream:
|
|
a2j_stream_close(self, A2J_PORT_PLAYBACK);
|
|
close_capture_stream:
|
|
a2j_stream_close(self, A2J_PORT_CAPTURE);
|
|
free_ringbuffer_outbound:
|
|
jack_ringbuffer_free(self->outbound_events);
|
|
free_ringbuffer_del:
|
|
jack_ringbuffer_free(self->port_del);
|
|
free_ringbuffer_add:
|
|
jack_ringbuffer_free(self->port_add);
|
|
free_self:
|
|
free(self);
|
|
fail:
|
|
return NULL;
|
|
}
|
|
|
|
static void a2j_destroy(struct a2j * self)
|
|
{
|
|
int error;
|
|
void * thread_status;
|
|
|
|
a2j_debug("midi: delete");
|
|
|
|
g_keep_alsa_walking = false; /* tell alsa threads to stop */
|
|
|
|
/* do something that we need to do anyway and will wake the input thread, then join */
|
|
snd_seq_disconnect_from(self->seq, self->port_id, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE);
|
|
pthread_join(self->alsa_input_thread, &thread_status);
|
|
|
|
/* wake output thread and join, then destroy the semaphore */
|
|
sem_post(&self->io_semaphore);
|
|
pthread_join(self->alsa_output_thread, &thread_status);
|
|
sem_destroy(&self->io_semaphore);
|
|
|
|
jack_ringbuffer_reset(self->port_add);
|
|
|
|
jack_deactivate(self->jack_client);
|
|
|
|
a2j_stream_detach(self->stream + A2J_PORT_CAPTURE);
|
|
a2j_stream_detach(self->stream + A2J_PORT_PLAYBACK);
|
|
|
|
error = jack_client_close(self->jack_client);
|
|
if (error != 0)
|
|
{
|
|
a2j_error("Cannot close jack client (%d)", error);
|
|
}
|
|
|
|
snd_seq_close(self->seq);
|
|
self->seq = NULL;
|
|
|
|
a2j_stream_close(self, A2J_PORT_PLAYBACK);
|
|
a2j_stream_close(self, A2J_PORT_CAPTURE);
|
|
|
|
jack_ringbuffer_free(self->outbound_events);
|
|
jack_ringbuffer_free(self->port_add);
|
|
jack_ringbuffer_free(self->port_del);
|
|
|
|
free(self);
|
|
}
|
|
|
|
bool a2j_start(void)
|
|
{
|
|
if (g_started)
|
|
{
|
|
a2j_error("Bridge already started");
|
|
return false;
|
|
}
|
|
|
|
a2j_info("Bridge starting...");
|
|
|
|
a2j_info("Using JACK server '%s'", g_a2j_jack_server_name);
|
|
|
|
a2j_info("Hardware ports %s be exported.", g_a2j_export_hw_ports ? "will": "will not");
|
|
|
|
g_a2j = a2j_new();
|
|
if (g_a2j == NULL)
|
|
{
|
|
a2j_error("a2j_new() failed.");
|
|
return false;
|
|
}
|
|
|
|
a2j_info("Bridge started");
|
|
|
|
#if HAVE_DBUS_1
|
|
if (a2j_dbus_is_available())
|
|
{
|
|
a2j_dbus_signal_emit_bridge_started();
|
|
}
|
|
#endif
|
|
|
|
g_started = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool a2j_stop(void)
|
|
{
|
|
if (!g_started)
|
|
{
|
|
a2j_error("Bridge already stopped");
|
|
return false;
|
|
}
|
|
|
|
a2j_info("Bridge stopping...");
|
|
|
|
a2j_destroy(g_a2j);
|
|
g_a2j = NULL;
|
|
|
|
a2j_info("Bridge stopped");
|
|
|
|
g_started = false;
|
|
|
|
#if HAVE_DBUS_1
|
|
if (a2j_dbus_is_available())
|
|
{
|
|
a2j_dbus_signal_emit_bridge_stopped();
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
a2j_is_started(void)
|
|
{
|
|
return g_started;
|
|
}
|
|
|
|
static
|
|
void
|
|
a2j_help(
|
|
const char * self)
|
|
{
|
|
a2j_info("Usage: %s [-j jack-server] [-e | --export-hw] [-u] [-n]", self);
|
|
a2j_info("Defaults:");
|
|
a2j_info("-j default");
|
|
}
|
|
|
|
int
|
|
main(
|
|
int argc,
|
|
char *argv[])
|
|
{
|
|
bool dbus;
|
|
struct stat st;
|
|
char timestamp_str[26];
|
|
|
|
//test_list_sort();
|
|
|
|
st.st_mtime = 0;
|
|
stat(argv[0], &st);
|
|
ctime_r(&st.st_mtime, timestamp_str);
|
|
timestamp_str[24] = 0;
|
|
|
|
g_max_jack_port_name_size = jack_port_name_size();
|
|
|
|
if (!a2j_paths_init())
|
|
{
|
|
goto fail;
|
|
}
|
|
|
|
#if HAVE_DBUS_1
|
|
dbus = argc == 2 && strcmp(argv[1], "dbus") == 0;
|
|
#else
|
|
dbus = false;
|
|
#endif
|
|
|
|
if (!a2j_log_init(dbus))
|
|
{
|
|
goto fail_paths_uninit;
|
|
}
|
|
|
|
if (!dbus)
|
|
{
|
|
struct option long_opts[] = { { "export-hw", 0, 0, 'e' }, { 0, 0, 0, 0 } };
|
|
|
|
int option_index = 0;
|
|
int c;
|
|
while ((c = getopt_long(argc, argv, "j:eu", long_opts, &option_index)) != -1)
|
|
{
|
|
switch (c)
|
|
{
|
|
case 'j':
|
|
g_a2j_jack_server_name = strdup(optarg);
|
|
break;
|
|
case 'e':
|
|
g_a2j_export_hw_ports = true;
|
|
break;
|
|
case 'u':
|
|
g_disable_port_uniqueness = true;
|
|
break;
|
|
case 'n':
|
|
g_filter_note_on = false;
|
|
break;
|
|
default:
|
|
a2j_help(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//a2j_conf_load();
|
|
}
|
|
|
|
if (dbus)
|
|
{
|
|
a2j_info("----------------------------");
|
|
}
|
|
|
|
a2j_info("JACK MIDI <-> ALSA sequencer MIDI bridge, version " A2J_VERSION
|
|
#if (HAVE_GITVERSION_H)
|
|
" (" GIT_VERSION ")"
|
|
#endif
|
|
" built on %s", timestamp_str);
|
|
a2j_info("Copyright 2006,2007 Dmitry S. Baikov");
|
|
a2j_info("Copyright 2007,2008,2009,2011,2012 Nedko Arnaudov");
|
|
|
|
if (dbus)
|
|
{
|
|
a2j_info("----------------------------");
|
|
a2j_info("Activated.");
|
|
}
|
|
else
|
|
{
|
|
a2j_info("");
|
|
}
|
|
|
|
/* setup our SIGSEGV magic that prints nice stack in our logfile */
|
|
if (dbus)
|
|
{
|
|
setup_siginfo();
|
|
}
|
|
|
|
signal(SIGINT, &a2j_sigint_handler);
|
|
signal(SIGTERM, &a2j_sigint_handler);
|
|
|
|
if (dbus)
|
|
{
|
|
#if HAVE_DBUS_1
|
|
if (!a2j_dbus_init())
|
|
{
|
|
a2j_error("a2j_dbus_init() failed.");
|
|
goto fail_uninit_log;
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
if (!a2j_start())
|
|
{
|
|
goto fail_uninit_log;
|
|
}
|
|
|
|
a2j_info("Press ctrl-c to stop the bridge");
|
|
}
|
|
|
|
while (g_keep_walking)
|
|
{
|
|
if (dbus)
|
|
{
|
|
#if HAVE_DBUS_1
|
|
if (!a2j_dbus_run(MAIN_LOOP_SLEEP_INTERVAL))
|
|
{
|
|
a2j_warning("Disconnect message was received from D-Bus.");
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
usleep(MAIN_LOOP_SLEEP_INTERVAL * 1000);
|
|
}
|
|
|
|
if (g_stop_request)
|
|
{
|
|
g_stop_request = false;
|
|
|
|
a2j_stop();
|
|
|
|
if (!dbus)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (g_started)
|
|
{
|
|
a2j_free_ports(g_a2j->port_del);
|
|
a2j_update_ports(g_a2j);
|
|
}
|
|
}
|
|
|
|
if (g_started)
|
|
{
|
|
a2j_stop();
|
|
}
|
|
|
|
#if HAVE_DBUS_1
|
|
if (dbus)
|
|
{
|
|
a2j_dbus_uninit();
|
|
|
|
a2j_info("Deactivated.");
|
|
a2j_info("----------------------------");
|
|
}
|
|
#endif
|
|
|
|
fail_uninit_log:
|
|
a2j_log_uninit();
|
|
|
|
fail_paths_uninit:
|
|
a2j_paths_uninit();
|
|
|
|
fail:
|
|
return 0;
|
|
}
|