diff --git a/ext/alsa/Makefile.am b/ext/alsa/Makefile.am index 9a2225784f..7f885c5be6 100644 --- a/ext/alsa/Makefile.am +++ b/ext/alsa/Makefile.am @@ -5,6 +5,7 @@ libgstalsa_la_SOURCES = \ gstalsaplugin.c \ gstalsasink.c \ gstalsasrc.c \ + gstalsamidisrc.c \ gstalsa.c libgstalsa_la_CFLAGS = \ @@ -24,4 +25,5 @@ noinst_HEADERS = \ gstalsa.h \ gstalsadeviceprobe.h \ gstalsasrc.h \ - gstalsasink.h + gstalsasink.h \ + gstalsamidisrc.h diff --git a/ext/alsa/gstalsamidisrc.c b/ext/alsa/gstalsamidisrc.c new file mode 100644 index 0000000000..645d0b0371 --- /dev/null +++ b/ext/alsa/gstalsamidisrc.c @@ -0,0 +1,521 @@ +/* GStreamer + * Copyright (C) 2014 Antonio Ospite + * + * gstalsamidisrc.c: Source element for ALSA MIDI sequencer events + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +/** + * SECTION:element-alsamidisrc + * @see_also: #GstPushSrc + * + * The alsamidisrc element is an element that fetches ALSA MIDI sequencer + * events and makes them available to elements understanding + * audio/x-midi-events in their sink pads. + * + * It can be used to generate notes from a MIDI input device. + * + * + * Example launch line + * |[ + * gst-launch -v alsamidisrc ports=129:0 ! fluiddec ! audioconvert ! autoaudiosink + * ]| This pipeline will listen for events from the sequencer device at port 129:0, + * and generate notes using the fluiddec element. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "gstalsamidisrc.h" + +GST_DEBUG_CATEGORY_STATIC (gst_alsa_midi_src_debug); +#define GST_CAT_DEFAULT gst_alsa_midi_src_debug + +/* + * The MIDI specification declares some status bytes undefined: + * + * - 0xF4 System common - Undefined (Reserved) + * - 0xF5 System common - Undefined (Reserved) + * - 0xF9 System real-time - Undefined (Reserved) + * - 0xFD System real-time - Undefined (Reserved) + * + * See: http://www.midi.org/techspecs/midimessages.php#2 + * + * Some other documents define status 0xf9 as a tick message with a period of + * 10ms: + * + * - http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/midispec/tick.htm + * - http://www.sequencer.de/synth/index.php/MIDI_Format#0xf9_-_MIDI_Tick + * + * Even if non-standard it looks like this convention is quite widespread. + * + * For instance Fluidsynth uses 0xF9 as a "midi tick" message: + * http://sourceforge.net/p/fluidsynth/code-git/ci/master/tree/fluidsynth/src/midi/fluid_midi.h#l62 + * + * And then so does the midiparse element in order to be compatible with + * Fluidsynth and the fluiddec element. + * + * Do the same to behave like midiparse. + */ +#define MIDI_TICK 0xf9 +#define MIDI_TICK_PERIOD_MS 10 + +/* Functions specific to the Alsa MIDI sequencer API */ + +#define DEFAULT_BUFSIZE 65536 +#define DEFAULT_CLIENT_NAME "alsamidisrc" +#define DEFAULT_POLL_TIMEOUT_MS (MIDI_TICK_PERIOD_MS / 2) + +static int +init_seq (GstAlsaMidiSrc * alsamidisrc) +{ + int ret; + + ret = snd_seq_open (&alsamidisrc->seq, "default", SND_SEQ_OPEN_DUPLEX, 0); + if (ret < 0) { + GST_ERROR_OBJECT (alsamidisrc, "Cannot open sequencer - %s", + snd_strerror (ret)); + goto error; + } + + ret = snd_seq_set_client_name (alsamidisrc->seq, DEFAULT_CLIENT_NAME); + if (ret < 0) { + GST_ERROR_OBJECT (alsamidisrc, "Cannot set client name - %s", + snd_strerror (ret)); + goto error_seq_close; + } + + return 0; + +error_seq_close: + snd_seq_close (alsamidisrc->seq); +error: + return ret; +} + +/* Parses one or more port addresses from the string */ +static int +parse_ports (const char *arg, GstAlsaMidiSrc * alsamidisrc) +{ + gchar **ports_list; + guint i; + int ret = 0; + + GST_DEBUG_OBJECT (alsamidisrc, "ports: %s", arg); + + /* + * Assume that ports are separated by commas. + * + * Commas are used instead of spaces because those are valid in client + * names. + */ + ports_list = g_strsplit (arg, ",", 0); + + alsamidisrc->port_count = g_strv_length (ports_list); + alsamidisrc->seq_ports = g_try_new (snd_seq_addr_t, alsamidisrc->port_count); + if (!alsamidisrc->seq_ports) { + GST_ERROR_OBJECT (alsamidisrc, "Out of memory"); + ret = -ENOMEM; + goto out_free_ports_list; + } + + for (i = 0; i < alsamidisrc->port_count; i++) { + gchar *port_name = ports_list[i]; + + ret = snd_seq_parse_address (alsamidisrc->seq, &alsamidisrc->seq_ports[i], + port_name); + if (ret < 0) { + GST_ERROR_OBJECT (alsamidisrc, "Invalid port %s - %s", port_name, + snd_strerror (ret)); + goto error_free_seq_ports; + } + } + + goto out_free_ports_list; + +error_free_seq_ports: + g_free (alsamidisrc->seq_ports); +out_free_ports_list: + g_strfreev (ports_list); + return ret; +} + +static int +create_port (GstAlsaMidiSrc * alsamidisrc) +{ + int ret; + + ret = snd_seq_create_simple_port (alsamidisrc->seq, DEFAULT_CLIENT_NAME, + SND_SEQ_PORT_CAP_WRITE | + SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION); + if (ret < 0) + GST_ERROR_OBJECT (alsamidisrc, "Cannot create port - %s", + snd_strerror (ret)); + + return ret; +} + +static void +connect_ports (GstAlsaMidiSrc * alsamidisrc) +{ + int i; + int ret; + + for (i = 0; i < alsamidisrc->port_count; ++i) { + ret = + snd_seq_connect_from (alsamidisrc->seq, 0, + alsamidisrc->seq_ports[i].client, alsamidisrc->seq_ports[i].port); + if (ret < 0) + /* Issue a warning and try the other ports */ + GST_WARNING_OBJECT (alsamidisrc, "Cannot connect from port %d:%d - %s", + alsamidisrc->seq_ports[i].client, alsamidisrc->seq_ports[i].port, + snd_strerror (ret)); + } +} + +/* GStreamer specific functions */ + +static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-midi-event")); + +#define DEFAULT_PORTS NULL + +enum +{ + PROP_0, + PROP_PORTS, + PROP_LAST, +}; + +#define _do_init \ + GST_DEBUG_CATEGORY_INIT (gst_alsa_midi_src_debug, "alsamidisrc", 0, "alsamidisrc element"); +#define gst_alsa_midi_src_parent_class parent_class +G_DEFINE_TYPE_WITH_CODE (GstAlsaMidiSrc, gst_alsa_midi_src, GST_TYPE_PUSH_SRC, + _do_init); + +static void gst_alsa_midi_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_alsa_midi_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static gboolean gst_alsa_midi_src_start (GstBaseSrc * basesrc); +static gboolean gst_alsa_midi_src_stop (GstBaseSrc * basesrc); + +static GstFlowReturn +gst_alsa_midi_src_create (GstPushSrc * src, GstBuffer ** buf); + +static void +gst_alsa_midi_src_class_init (GstAlsaMidiSrcClass * klass) +{ + GObjectClass *gobject_class; + GstElementClass *gstelement_class; + GstBaseSrcClass *gstbase_src_class; + GstPushSrcClass *gstpush_src_class; + + gobject_class = G_OBJECT_CLASS (klass); + gstelement_class = GST_ELEMENT_CLASS (klass); + gstbase_src_class = GST_BASE_SRC_CLASS (klass); + gstpush_src_class = GST_PUSH_SRC_CLASS (klass); + + gobject_class->set_property = gst_alsa_midi_src_set_property; + gobject_class->get_property = gst_alsa_midi_src_get_property; + + g_object_class_install_property (gobject_class, PROP_PORTS, + g_param_spec_string ("ports", "Ports", + "Comma separated list of sequencer ports (e.g. client:port,...)", + DEFAULT_PORTS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + gst_element_class_set_static_metadata (gstelement_class, + "AlsaMidi Source", + "Source", + "Push ALSA MIDI sequencer events around", "Antonio Ospite "); + gst_element_class_add_pad_template (gstelement_class, + gst_static_pad_template_get (&srctemplate)); + + gstbase_src_class->start = GST_DEBUG_FUNCPTR (gst_alsa_midi_src_start); + gstbase_src_class->stop = GST_DEBUG_FUNCPTR (gst_alsa_midi_src_stop); + gstpush_src_class->create = GST_DEBUG_FUNCPTR (gst_alsa_midi_src_create); +} + +static void +gst_alsa_midi_src_init (GstAlsaMidiSrc * alsamidisrc) +{ + alsamidisrc->ports = DEFAULT_PORTS; + + gst_base_src_set_format (GST_BASE_SRC (alsamidisrc), GST_FORMAT_TIME); + gst_base_src_set_live (GST_BASE_SRC (alsamidisrc), TRUE); +} + +static void +gst_alsa_midi_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAlsaMidiSrc *src; + + src = GST_ALSA_MIDI_SRC (object); + + switch (prop_id) { + case PROP_PORTS: + g_free (src->ports); + src->ports = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_alsa_midi_src_get_property (GObject * object, guint prop_id, GValue * value, + GParamSpec * pspec) +{ + GstAlsaMidiSrc *src; + + g_return_if_fail (GST_IS_ALSA_MIDI_SRC (object)); + + src = GST_ALSA_MIDI_SRC (object); + + switch (prop_id) { + case PROP_PORTS: + g_value_set_string (value, src->ports); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GstBuffer * +prepare_buffer (GstAlsaMidiSrc * alsamidisrc, gpointer data, guint size) +{ + GstClockTime time; + gpointer local_data; + GstBuffer *buffer; + + buffer = gst_buffer_new (); + + time = alsamidisrc->tick * MIDI_TICK_PERIOD_MS * GST_MSECOND; + + GST_BUFFER_DTS (buffer) = time; + GST_BUFFER_PTS (buffer) = time; + GST_BUFFER_OFFSET (buffer) = time; + GST_BUFFER_DURATION (buffer) = MIDI_TICK_PERIOD_MS * GST_MSECOND; + + local_data = g_memdup (data, size); + + gst_buffer_append_memory (buffer, + gst_memory_new_wrapped (0, local_data, size, 0, size, local_data, + g_free)); + + GST_MEMDUMP_OBJECT (alsamidisrc, "MIDI data:", local_data, size); + + alsamidisrc->tick += 1; + + return buffer; +} + +static void +push_buffer (GstAlsaMidiSrc * alsamidisrc, gpointer data, guint size, + GstBufferList * buffer_list) +{ + gst_buffer_list_add (buffer_list, prepare_buffer (alsamidisrc, data, size)); +} + +static void +push_tick_buffer (GstAlsaMidiSrc * alsamidisrc, GstBufferList * buffer_list) +{ + alsamidisrc->buffer[0] = MIDI_TICK; + push_buffer (alsamidisrc, alsamidisrc->buffer, 1, buffer_list); +} + +static GstFlowReturn +gst_alsa_midi_src_create (GstPushSrc * src, GstBuffer ** buf) +{ + GstAlsaMidiSrc *alsamidisrc; + GstBufferList *buffer_list; + long size_ev = 0; + int err; + int ret; + guint len; + + alsamidisrc = GST_ALSA_MIDI_SRC (src); + + buffer_list = gst_buffer_list_new (); + + snd_seq_poll_descriptors (alsamidisrc->seq, alsamidisrc->pfds, + alsamidisrc->npfds, POLLIN); + + /* + * The file descriptors are polled with a timeout _less_ than 10ms (the MIDI + * tick period) in order not to loose events because of possible overlaps + * with MIDI ticks. + * + * If the polling times out (no new events) then a MIDI-tick event gets + * generated in order to keep the pipeline alive and progressing. + * + * If new events are present, then they are decoded and queued in + * a buffer_list. One buffer per event will be queued, all with different + * timestamps (see the prepare_buffer() function); maybe this can be + * optimized but a as a proof-of-concept mechanism it works OK. + */ + ret = poll (alsamidisrc->pfds, alsamidisrc->npfds, DEFAULT_POLL_TIMEOUT_MS); + if (ret < 0) { + GST_ERROR_OBJECT (alsamidisrc, "ERROR in poll: %s", strerror (errno)); + } else if (ret == 0) { + push_tick_buffer (alsamidisrc, buffer_list); + } else { + /* There are events available */ + do { + snd_seq_event_t *event; + err = snd_seq_event_input (alsamidisrc->seq, &event); + if (err < 0) + break; /* Processed all events */ + + if (event) { + size_ev = + snd_midi_event_decode (alsamidisrc->parser, alsamidisrc->buffer, + DEFAULT_BUFSIZE, event); + if (size_ev < 0) { + /* ENOENT indicates an event that is not a MIDI message, silently skip it */ + if (-ENOENT == size_ev) { + GST_WARNING_OBJECT (alsamidisrc, + "Warning: Received non-MIDI message"); + push_tick_buffer (alsamidisrc, buffer_list); + } else { + GST_ERROR_OBJECT (alsamidisrc, + "Error decoding event from ALSA to output: %s", + strerror (-size_ev)); + goto error; + } + } else { + push_buffer (alsamidisrc, alsamidisrc->buffer, size_ev, buffer_list); + } + } + } while (err > 0); + } + + len = gst_buffer_list_length (buffer_list); + if (len == 0) + goto error; + + /* Pop the _last_ buffer in the list */ + *buf = gst_buffer_copy (gst_buffer_list_get (buffer_list, len - 1)); + gst_buffer_list_remove (buffer_list, len - 1, 1); + --len; + + /* + * If there are no more buffers left, free the list, otherwise push all the + * _previous_ buffers left in the list. + * + * The one popped above will be pushed last when this function returns. + */ + if (len == 0) + gst_buffer_list_unref (buffer_list); + else + gst_pad_push_list (GST_BASE_SRC (src)->srcpad, buffer_list); + + return GST_FLOW_OK; + +error: + gst_buffer_list_unref (buffer_list); + return GST_FLOW_ERROR; +} + +static gboolean +gst_alsa_midi_src_start (GstBaseSrc * basesrc) +{ + GstAlsaMidiSrc *alsamidisrc; + int ret; + + alsamidisrc = GST_ALSA_MIDI_SRC (basesrc); + + alsamidisrc->tick = 0; + alsamidisrc->port_count = 0; + + ret = init_seq (alsamidisrc); + if (ret < 0) + goto err; + + if (alsamidisrc->ports) { + ret = parse_ports (alsamidisrc->ports, alsamidisrc); + if (ret < 0) + goto error_seq_close; + } + + ret = create_port (alsamidisrc); + if (ret < 0) + goto error_free_seq_ports; + + connect_ports (alsamidisrc); + + ret = snd_seq_nonblock (alsamidisrc->seq, 1); + if (ret < 0) { + GST_ERROR_OBJECT (alsamidisrc, "Cannot set nonblock mode - %s", + snd_strerror (ret)); + goto error_free_seq_ports; + } + + snd_midi_event_new (DEFAULT_BUFSIZE, &alsamidisrc->parser); + snd_midi_event_init (alsamidisrc->parser); + snd_midi_event_reset_decode (alsamidisrc->parser); + + snd_midi_event_no_status (alsamidisrc->parser, 1); + + alsamidisrc->buffer = g_try_malloc (DEFAULT_BUFSIZE); + if (alsamidisrc->buffer == NULL) + goto error_free_parser; + + alsamidisrc->npfds = + snd_seq_poll_descriptors_count (alsamidisrc->seq, POLLIN); + alsamidisrc->pfds = + g_try_malloc (sizeof (*alsamidisrc->pfds) * alsamidisrc->npfds); + if (alsamidisrc->pfds == NULL) + goto error_free_buffer; + + return TRUE; + +error_free_buffer: + g_free (alsamidisrc->buffer); +error_free_parser: + snd_midi_event_free (alsamidisrc->parser); +error_free_seq_ports: + g_free (alsamidisrc->seq_ports); +error_seq_close: + snd_seq_close (alsamidisrc->seq); +err: + return FALSE; +} + +static gboolean +gst_alsa_midi_src_stop (GstBaseSrc * basesrc) +{ + GstAlsaMidiSrc *alsamidisrc; + + alsamidisrc = GST_ALSA_MIDI_SRC (basesrc); + + g_free (alsamidisrc->pfds); + g_free (alsamidisrc->buffer); + snd_midi_event_free (alsamidisrc->parser); + g_free (alsamidisrc->seq_ports); + snd_seq_close (alsamidisrc->seq); + + return TRUE; +} diff --git a/ext/alsa/gstalsamidisrc.h b/ext/alsa/gstalsamidisrc.h new file mode 100644 index 0000000000..d30a0c5781 --- /dev/null +++ b/ext/alsa/gstalsamidisrc.h @@ -0,0 +1,77 @@ +/* GStreamer + * Copyright (C) 2014 Antonio Ospite + * + * gstalsamidisrc.h: Source element for ALSA MIDI sequencer events + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __GST_ALSA_MIDI_SRC_H__ +#define __GST_ALSA_MIDI_SRC_H__ + +#include +#include + +#include + +G_BEGIN_DECLS +#define GST_TYPE_ALSA_MIDI_SRC \ + (gst_alsa_midi_src_get_type()) +#define GST_ALSA_MIDI_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_ALSA_MIDI_SRC,GstAlsaMidiSrc)) +#define GST_ALSA_MIDI_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_ALSA_MIDI_SRC,GstAlsaMidiSrcClass)) +#define GST_IS_ALSA_MIDI_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_ALSA_MIDI_SRC)) +#define GST_IS_ALSA_MIDI_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_ALSA_MIDI_SRC)) + +typedef struct _GstAlsaMidiSrc GstAlsaMidiSrc; +typedef struct _GstAlsaMidiSrcClass GstAlsaMidiSrcClass; + +/** + * GstAlsaMidiSrc: + * + * Opaque #GstAlsaMidiSrc data structure. + */ +struct _GstAlsaMidiSrc +{ + GstPushSrc element; + + gchar *ports; + + /*< private > */ + snd_seq_t *seq; + int port_count; + snd_seq_addr_t *seq_ports; + snd_midi_event_t *parser; + unsigned char *buffer; + + struct pollfd *pfds; + int npfds; + + guint64 tick; +}; + +struct _GstAlsaMidiSrcClass +{ + GstPushSrcClass parent_class; +}; + +G_GNUC_INTERNAL GType gst_alsa_midi_src_get_type (void); + +G_END_DECLS +#endif /* __GST_ALSA_MIDI_SRC_H__ */ diff --git a/ext/alsa/gstalsaplugin.c b/ext/alsa/gstalsaplugin.c index 6a7e42bb7c..6c1a67cc90 100644 --- a/ext/alsa/gstalsaplugin.c +++ b/ext/alsa/gstalsaplugin.c @@ -25,6 +25,7 @@ #include "gstalsasink.h" #include "gstalsasrc.h" +#include "gstalsamidisrc.h" #include @@ -66,6 +67,9 @@ plugin_init (GstPlugin * plugin) if (!gst_element_register (plugin, "alsasink", GST_RANK_PRIMARY, GST_TYPE_ALSA_SINK)) return FALSE; + if (!gst_element_register (plugin, "alsamidisrc", GST_RANK_PRIMARY, + GST_TYPE_ALSA_MIDI_SRC)) + return FALSE; GST_DEBUG_CATEGORY_INIT (alsa_debug, "alsa", 0, "alsa plugins");