/* GStreamer * * gst-aseq-appsrc: a GStreamer appsrc for the ALSA MIDI sequencer API * * Copyright (C) 2014 Antonio Ospite * * 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. */ /* Code _heavily_ inspired to aseqdump and amidicat: * http://git.alsa-project.org/?p=alsa-utils.git;a=tree;f=seq/aseqdump * http://krellan.com/amidicat/ */ #include #include #include #include #include #include #define DEFAULT_BUFSIZE 65536 #define DEFAULT_CLIENT_NAME "gst-aseq-appsrc" #define DEFAULT_TICK_PERIOD_MS 10 #define DEFAULT_POLL_TIMEOUT_MS (DEFAULT_TICK_PERIOD_MS / 2) GST_DEBUG_CATEGORY (mysrc_debug); #define GST_CAT_DEFAULT mysrc_debug typedef struct _App App; struct _App { GstElement *pipeline; GstElement *appsrc; GMainLoop *loop; 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; }; App s_app; static int init_seq (App * app) { int ret; ret = snd_seq_open (&app->seq, "default", SND_SEQ_OPEN_DUPLEX, 0); if (ret < 0) { GST_ERROR ("Cannot open sequencer - %s", snd_strerror (ret)); goto error; } ret = snd_seq_set_client_name (app->seq, DEFAULT_CLIENT_NAME); if (ret < 0) { GST_ERROR ("Cannot set client name - %s", snd_strerror (ret)); goto error_seq_close; } return 0; error_seq_close: snd_seq_close (app->seq); error: return ret; } /* Parses one or more port addresses from the string */ static int parse_ports (const char *arg, App * app) { gchar **ports_list; guint i; int ret = 0; GST_DEBUG ("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); app->port_count = g_strv_length (ports_list); app->seq_ports = g_try_new (snd_seq_addr_t, app->port_count); if (!app->seq_ports) { GST_ERROR ("Out of memory"); ret = -ENOMEM; goto out_free_ports_list; } for (i = 0; i < (guint)app->port_count; i++) { gchar *port_name = ports_list[i]; ret = snd_seq_parse_address (app->seq, &app->seq_ports[i], port_name); if (ret < 0) { GST_ERROR ("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 (app->seq_ports); out_free_ports_list: g_strfreev (ports_list); return ret; } static int create_port (App * app) { int ret; ret = snd_seq_create_simple_port (app->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 ("Cannot create port - %s", snd_strerror (ret)); return ret; } static void connect_ports (App * app) { int i; int ret; for (i = 0; i < app->port_count; ++i) { ret = snd_seq_connect_from (app->seq, 0, app->seq_ports[i].client, app->seq_ports[i].port); if (ret < 0) /* warning */ GST_WARNING ("Cannot connect from port %d:%d - %s", app->seq_ports[i].client, app->seq_ports[i].port, snd_strerror (ret)); } } static void push_buffer (App * app, gpointer data, guint size) { GstBuffer *buffer; GstClockTime time; int ret; gpointer local_data; /* read the next chunk */ buffer = gst_buffer_new (); time = app->tick * DEFAULT_TICK_PERIOD_MS * GST_MSECOND; GST_BUFFER_DTS (buffer) = time; GST_BUFFER_PTS (buffer) = time; GST_BUFFER_OFFSET (buffer) = time; GST_BUFFER_DURATION (buffer) = DEFAULT_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_DEBUG ("feed buffer %p, tick %" G_GUINT64_FORMAT " size: %u", (gpointer) buffer, app->tick, size); g_signal_emit_by_name (app->appsrc, "push-buffer", buffer, &ret); gst_buffer_unref (buffer); app->tick += 1; } static void push_tick_buffer (App * app) { app->buffer[0] = 0xf9; push_buffer (app, app->buffer, 1); } /* This method is called by the need-data signal callback, we feed data into the * appsrc. */ static void feed_data (GstElement * appsrc, guint size, App * app) { long size_ev = 0; int err; int ret; snd_seq_poll_descriptors (app->seq, app->pfds, app->npfds, POLLIN); ret = poll (app->pfds, app->npfds, DEFAULT_POLL_TIMEOUT_MS); if (ret < 0) { GST_ERROR ("ERROR in poll: %s", strerror (errno)); } else if (ret == 0) { push_tick_buffer (app); } else { do { snd_seq_event_t *event; err = snd_seq_event_input (app->seq, &event); if (err < 0) break; if (event) { size_ev = snd_midi_event_decode (app->parser, app->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 ("Warning: Received non-MIDI message"); push_tick_buffer (app); } else { GST_ERROR ("Error decoding event from ALSA to output: %s", strerror (-size_ev)); break; } } else { push_buffer (app, app->buffer, size_ev); } } } while (err > 0); } return; } /* this callback is called when pipeline has constructed a source object to read * from. Since we provided the appsrc:// uri to pipeline, this will be the * appsrc that we must handle. We set up a signals to push data into appsrc. */ static void found_source (GObject * object, GObject * orig, GParamSpec * pspec, App * app) { /* get a handle to the appsrc */ g_object_get (orig, pspec->name, &app->appsrc, NULL); GST_DEBUG ("got appsrc %p", (gpointer) app->appsrc); /* configure the appsrc, we will push a buffer to appsrc when it needs more * data */ g_signal_connect (app->appsrc, "need-data", G_CALLBACK (feed_data), app); } static gboolean bus_message (GstBus * bus, GstMessage * message, App * app) { GST_DEBUG ("got message %s", gst_message_type_get_name (GST_MESSAGE_TYPE (message))); switch (GST_MESSAGE_TYPE (message)) { case GST_MESSAGE_ERROR: { GError *error = NULL; gchar *dbg_info = NULL; gst_message_parse_error (message, &error, &dbg_info); g_printerr ("ERROR from element %s: %s", GST_OBJECT_NAME (message->src), error->message); g_printerr ("Debugging info: %s", (dbg_info) ? dbg_info : "none"); g_error_free (error); g_free (dbg_info); g_main_loop_quit (app->loop); break; } case GST_MESSAGE_EOS: g_main_loop_quit (app->loop); break; default: break; } return TRUE; } static int app_init (App * app, char *ports) { int ret; app->tick = 0; ret = init_seq (app); if (ret < 0) goto err; if (ports) { ret = parse_ports (ports, app); if (ret < 0) goto err_seq_close; } ret = create_port (app); if (ret < 0) goto err_free_ports; connect_ports (app); ret = snd_seq_nonblock (app->seq, 1); if (ret < 0) { GST_ERROR ("Cannot set nonblock mode - %s", snd_strerror (ret)); goto err_free_ports; } snd_midi_event_new (DEFAULT_BUFSIZE, &app->parser); snd_midi_event_init (app->parser); snd_midi_event_reset_decode (app->parser); snd_midi_event_no_status (app->parser, 1); app->buffer = malloc (DEFAULT_BUFSIZE); if (app->buffer == NULL) { ret = -ENOMEM; goto err_free_parser; } app->npfds = snd_seq_poll_descriptors_count (app->seq, POLLIN); app->pfds = malloc (sizeof (*app->pfds) * app->npfds); if (app->pfds == NULL) { ret = -ENOMEM; goto err_free_buffer; } return 0; err_free_buffer: free (app->buffer); err_free_parser: snd_midi_event_free (app->parser); err_free_ports: g_free (app->seq_ports); err_seq_close: snd_seq_close (app->seq); err: return ret; } static void app_finalize (App * app) { /* free the resources */ free (app->pfds); free (app->buffer); snd_midi_event_free (app->parser); g_free (app->seq_ports); snd_seq_close (app->seq); } static gboolean on_sigint (gpointer user_data) { GMainLoop *loop = (GMainLoop *) user_data; g_message ("Caught SIGINT. Initiating shutdown."); g_main_loop_quit (loop); return FALSE; } int main (int argc, char *argv[]) { App *app = &s_app; GstBus *bus; GstCaps *caps; int ret; GOptionContext *ctx; GError *err = NULL; gchar *ports = NULL; GOptionEntry options[] = { {"ports", 'p', 0, G_OPTION_ARG_STRING, &ports, "Comma separated list of sequencer ports", "client:port,..."}, {NULL} }; ctx = g_option_context_new (NULL); g_option_context_add_main_entries (ctx, options, NULL); g_option_context_add_group (ctx, gst_init_get_option_group ()); if (!g_option_context_parse (ctx, &argc, &argv, &err)) { if (err) g_printerr ("Error initializing: %s\n", GST_STR_NULL (err->message)); else g_printerr ("Error initializing: Unknown error!\n"); exit (1); } g_option_context_free (ctx); gst_init (&argc, &argv); GST_DEBUG_CATEGORY_INIT (mysrc_debug, "mysrc", 0, "ALSA MIDI sequencer appsrc pipeline"); ret = app_init (app, ports); if (ret < 0) return ret; free (ports); if (app->port_count > 0) printf ("Waiting for data.\n"); else printf ("Waiting for data at port %d:0.\n", snd_seq_client_id (app->seq)); /* create a mainloop to get messages */ app->loop = g_main_loop_new (NULL, FALSE); app->pipeline = gst_parse_launch ("appsrc name=mysource ! fluiddec ! audioconvert ! autoaudiosink", NULL); g_assert (app->pipeline); bus = gst_pipeline_get_bus (GST_PIPELINE (app->pipeline)); g_assert (bus); /* add watch for messages */ gst_bus_add_watch (bus, (GstBusFunc) bus_message, app); /* get the appsrc */ app->appsrc = gst_bin_get_by_name (GST_BIN (app->pipeline), "mysource"); g_assert (app->appsrc); g_assert (GST_IS_APP_SRC (app->appsrc)); g_signal_connect (app->appsrc, "need-data", G_CALLBACK (feed_data), app); g_object_set (app->appsrc, "format", GST_FORMAT_TIME, NULL); g_object_set (app->appsrc, "is-live", TRUE, NULL); /* set the caps on the source */ caps = gst_caps_new_simple ("audio/x-midi-event", NULL, NULL); gst_app_src_set_caps (GST_APP_SRC (app->appsrc), caps); gst_caps_unref (caps); /* get notification when the source is created so that we get a handle to it * and can configure it */ g_signal_connect (app->pipeline, "deep-notify::source", (GCallback) found_source, app); /* go to playing and wait in a mainloop. */ gst_element_set_state (app->pipeline, GST_STATE_PLAYING); /* this mainloop is stopped when we receive an error or EOS, or on SIGINT */ g_unix_signal_add (SIGINT, on_sigint, app->loop); g_main_loop_run (app->loop); g_main_loop_unref (app->loop); GST_DEBUG ("stopping"); gst_element_set_state (app->pipeline, GST_STATE_NULL); gst_object_unref (app->appsrc); gst_object_unref (bus); gst_object_unref (app->pipeline); app_finalize (app); return 0; }