diff options
author | Wim Taymans <wim.taymans@collabora.co.uk> | 2013-03-21 06:50:52 +0100 |
---|---|---|
committer | Tim-Philipp Müller <tim@centricular.net> | 2013-07-22 12:03:10 +0100 |
commit | 4194c9f655f69629233674eec840fa99e0932626 (patch) | |
tree | 5c3f18814ee8776fbec067c5b9eb9c8a17aae30c | |
parent | 3a5fa02e27e25c729dc3f5120e2f06348674b563 (diff) | |
download | gstreamer-plugins-bad-4194c9f655f69629233674eec840fa99e0932626.tar.gz |
midiparse: add basic midi parser
Parses midi files and outputs buffers with midi events.
Pick midi parser from master branch.
midi: set rank as marginal
midiparse: small fixes
midiparse: handle midi in RIFF
midiparse: Update duration
midiparse: fix klass
midiparse: duration is max of all track durations
midiparse: small cleanups in error paths
midiparse: don't leak midi data
midiparse: clean up tracks
midiparse: simplify the callbacks
midiparse: implement seeking
midiparse: always seek back to beginning
midi: set midi parser rank primary
midiparser: ensure variable is initialized and fix debug message access
midiparse: Push stream-start event before anything else
midiparse: In pull mode drop SEGMENT, CAPS and STREAM_START events
gst: Add better support for static plugins
https://bugzilla.gnome.org/show_bug.cgi?id=696041
-rw-r--r-- | configure.ac | 2 | ||||
-rw-r--r-- | gst/midi/Makefile.am | 30 | ||||
-rw-r--r-- | gst/midi/midi.c | 58 | ||||
-rw-r--r-- | gst/midi/midiparse.c | 1334 | ||||
-rw-r--r-- | gst/midi/midiparse.h | 90 |
5 files changed, 1514 insertions, 0 deletions
diff --git a/configure.ac b/configure.ac index 5f5c3b23f..5d1aa9fe3 100644 --- a/configure.ac +++ b/configure.ac @@ -540,6 +540,7 @@ AG_GST_CHECK_PLUGIN(jp2kdecimator) AG_GST_CHECK_PLUGIN(jpegformat) AG_GST_CHECK_PLUGIN(librfb) AG_GST_CHECK_PLUGIN(liveadder) +AG_GST_CHECK_PLUGIN(midi) AG_GST_CHECK_PLUGIN(mpegdemux) AG_GST_CHECK_PLUGIN(mpegtsdemux) AG_GST_CHECK_PLUGIN(mpegtsmux) @@ -2234,6 +2235,7 @@ gst/jp2kdecimator/Makefile gst/jpegformat/Makefile gst/librfb/Makefile gst/liveadder/Makefile +gst/midi/Makefile gst/mpegdemux/Makefile gst/mpegtsdemux/Makefile gst/mpegtsmux/Makefile diff --git a/gst/midi/Makefile.am b/gst/midi/Makefile.am new file mode 100644 index 000000000..058a778ee --- /dev/null +++ b/gst/midi/Makefile.am @@ -0,0 +1,30 @@ +plugin_LTLIBRARIES = libgstmidi.la + +libgstmidi_la_SOURCES = midi.c midiparse.c +libgstmidi_la_CFLAGS = \ + $(GST_PLUGINS_BAD_CFLAGS) \ + $(GST_PLUGINS_BASE_CFLAGS) \ + $(GST_BASE_CFLAGS) \ + $(GST_CFLAGS) +libgstmidi_la_LIBADD = \ + $(GST_PLUGINS_BASE_LIBS) -lgsttag-$(GST_API_VERSION) \ + $(GST_BASE_LIBS) \ + $(LIBM) +libgstmidi_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) +libgstmidi_la_LIBTOOLFLAGS = $(GST_PLUGIN_LIBTOOLFLAGS) + +noinst_HEADERS = midiparse.h + +Android.mk: Makefile.am $(BUILT_SOURCES) + androgenizer \ + -:PROJECT libgstmidi -:SHARED libgstmidi \ + -:TAGS eng debug \ + -:REL_TOP $(top_srcdir) -:ABS_TOP $(abs_top_srcdir) \ + -:SOURCES $(libgstmidi_la_SOURCES) \ + -:CFLAGS $(DEFS) $(DEFAULT_INCLUDES) $(libgstmidi_la_CFLAGS) \ + -:LDFLAGS $(libgstmidi_la_LDFLAGS) \ + $(libgstmidi_la_LIBADD) \ + -ldl \ + -:PASSTHROUGH LOCAL_ARM_MODE:=arm \ + LOCAL_MODULE_PATH:='$$(TARGET_OUT)/lib/gstreamer-1.0' \ + > $@ diff --git a/gst/midi/midi.c b/gst/midi/midi.c new file mode 100644 index 000000000..6171359f6 --- /dev/null +++ b/gst/midi/midi.c @@ -0,0 +1,58 @@ +/* -*- Mode: C; tab-width: 2; indent-tabs-mode: t; c-basic-offset: 2 -*- */ +/* GStreamer MIDI plugin initialisation + * Copyright (C) <2013> Wim Taymans <wim.taymans@gmail.com> + * + * 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <gst/tag/tag.h> + +#include <gst/gst-i18n-plugin.h> + +#include "midiparse.h" + +GST_DEBUG_CATEGORY_STATIC (midi_debug); +#define GST_CAT_DEFAULT (midi_debug) + +static gboolean +plugin_init (GstPlugin * plugin) +{ + gboolean ret; + + GST_DEBUG_CATEGORY_INIT (midi_debug, "midi", 0, "MIDI plugin"); + +#ifdef ENABLE_NLS + GST_DEBUG ("binding text domain %s to locale dir %s", GETTEXT_PACKAGE, + LOCALEDIR); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); +#endif + + ret = gst_element_register (plugin, "midiparse", GST_RANK_PRIMARY, + GST_TYPE_MIDI_PARSE); + + return ret; +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + midi, + "Parse MIDI files", + plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/gst/midi/midiparse.c b/gst/midi/midiparse.c new file mode 100644 index 000000000..609d55fc2 --- /dev/null +++ b/gst/midi/midiparse.c @@ -0,0 +1,1334 @@ +/* + * midiparse - midi parser plugin for gstreamer + * + * Copyright 2013 Wim Taymans <wim.taymans@gmail.com> + * + * 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-midiparse + * @see_also: fluidsynth + * + * This element parses midi-files into midi events. You would need a midi + * renderer such as fluidsynth to convert the events into raw samples. + * + * <refsect2> + * <title>Example pipeline</title> + * |[ + * gst-launch-1.0 filesrc location=song.mid ! midiparse ! fluidsynth ! pulsesink + * ]| This example pipeline will parse the midi and render to raw audio which is + * played via pulseaudio. + * </refsect2> + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <gst/gst.h> +#include <string.h> +#include <glib.h> + +#include "midiparse.h" + +GST_DEBUG_CATEGORY_STATIC (gst_midi_parse_debug); +#define GST_CAT_DEFAULT gst_midi_parse_debug + +enum +{ + /* FILL ME */ + LAST_SIGNAL +}; + +enum +{ + PROP_0, + /* FILL ME */ +}; + +#define DEFAULT_TEMPO 500000 /* 120 BPM is the default */ + +typedef struct +{ + guint8 *data; + guint size; + guint offset; + + guint8 running_status; + guint64 pulse; + gboolean eot; + +} GstMidiTrack; + +typedef GstFlowReturn (*GstMidiPushFunc) (GstMidiParse * parse, + GstMidiTrack * track, guint8 event, guint8 * data, guint length, + gpointer user_data); + +static void gst_midi_parse_finalize (GObject * object); + +static gboolean gst_midi_parse_sink_event (GstPad * pad, GstObject * parent, + GstEvent * event); +static gboolean gst_midi_parse_src_event (GstPad * pad, GstObject * parent, + GstEvent * event); + +static GstStateChangeReturn gst_midi_parse_change_state (GstElement * element, + GstStateChange transition); +static gboolean gst_midi_parse_activate (GstPad * pad, GstObject * parent); +static gboolean gst_midi_parse_activatemode (GstPad * pad, GstObject * parent, + GstPadMode mode, gboolean active); + +static void gst_midi_parse_loop (GstPad * sinkpad); +static GstFlowReturn gst_midi_parse_chain (GstPad * sinkpad, GstObject * parent, + GstBuffer * buffer); + +static gboolean gst_midi_parse_src_query (GstPad * pad, GstObject * parent, + GstQuery * query); + +static void gst_midi_parse_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_midi_parse_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); + +static void reset_track (GstMidiTrack * track, GstMidiParse * midiparse); + +static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink", + GST_PAD_SINK, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/midi; audio/riff-midi") + ); + +static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS ("audio/x-midi-event")); + +#define parent_class gst_midi_parse_parent_class +G_DEFINE_TYPE (GstMidiParse, gst_midi_parse, GST_TYPE_ELEMENT); + +/* initialize the plugin's class */ +static void +gst_midi_parse_class_init (GstMidiParseClass * klass) +{ + GObjectClass *gobject_class; + GstElementClass *gstelement_class; + + gobject_class = (GObjectClass *) klass; + gstelement_class = (GstElementClass *) klass; + + gobject_class->finalize = gst_midi_parse_finalize; + gobject_class->set_property = gst_midi_parse_set_property; + gobject_class->get_property = gst_midi_parse_get_property; + + gst_element_class_add_pad_template (gstelement_class, + gst_static_pad_template_get (&src_factory)); + gst_element_class_add_pad_template (gstelement_class, + gst_static_pad_template_get (&sink_factory)); + gst_element_class_set_static_metadata (gstelement_class, "MidiParse", + "Codec/Demuxer/Audio", + "Midi Parser Element", "Wim Taymans <wim.taymans@gmail.com>"); + + GST_DEBUG_CATEGORY_INIT (gst_midi_parse_debug, "midiparse", + 0, "MIDI parser plugin"); + + gstelement_class->change_state = gst_midi_parse_change_state; +} + +/* initialize the new element + * instantiate pads and add them to element + * set functions + * initialize structure + */ +static void +gst_midi_parse_init (GstMidiParse * filter) +{ + filter->sinkpad = gst_pad_new_from_static_template (&sink_factory, "sink"); + + gst_pad_set_activatemode_function (filter->sinkpad, + gst_midi_parse_activatemode); + gst_pad_set_activate_function (filter->sinkpad, gst_midi_parse_activate); + gst_pad_set_event_function (filter->sinkpad, gst_midi_parse_sink_event); + gst_pad_set_chain_function (filter->sinkpad, gst_midi_parse_chain); + gst_element_add_pad (GST_ELEMENT (filter), filter->sinkpad); + + filter->srcpad = gst_pad_new_from_static_template (&src_factory, "src"); + + gst_pad_set_query_function (filter->srcpad, gst_midi_parse_src_query); + gst_pad_set_event_function (filter->srcpad, gst_midi_parse_src_event); + gst_pad_use_fixed_caps (filter->srcpad); + + gst_element_add_pad (GST_ELEMENT (filter), filter->srcpad); + + gst_segment_init (&filter->segment, GST_FORMAT_TIME); + + filter->adapter = gst_adapter_new (); +} + +static void +gst_midi_parse_finalize (GObject * object) +{ + GstMidiParse *midiparse; + + midiparse = GST_MIDI_PARSE (object); + + g_object_unref (midiparse->adapter); + g_free (midiparse->data); + + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static gboolean +gst_midi_parse_src_query (GstPad * pad, GstObject * parent, GstQuery * query) +{ + gboolean res = TRUE; + GstMidiParse *midiparse = GST_MIDI_PARSE (parent); + + switch (GST_QUERY_TYPE (query)) { + case GST_QUERY_DURATION: + gst_query_set_duration (query, GST_FORMAT_TIME, + midiparse->segment.duration); + break; + case GST_QUERY_POSITION: + gst_query_set_position (query, GST_FORMAT_TIME, + midiparse->segment.position); + break; + case GST_QUERY_FORMATS: + gst_query_set_formats (query, 1, GST_FORMAT_TIME); + break; + case GST_QUERY_SEGMENT: + gst_query_set_segment (query, midiparse->segment.rate, + midiparse->segment.format, midiparse->segment.start, + midiparse->segment.stop); + break; + case GST_QUERY_SEEKING: + gst_query_set_seeking (query, midiparse->segment.format, + FALSE, 0, midiparse->segment.duration); + break; + default: + res = gst_pad_query_default (pad, parent, query); + break; + } + + return res; +} + +static gboolean +gst_midi_parse_do_seek (GstMidiParse * midiparse, GstSegment * segment) +{ + /* if seeking backwards, start from 0 else we just let things run and + * have it clip downstream */ + GST_DEBUG_OBJECT (midiparse, "seeking back to 0"); + segment->position = 0; + g_list_foreach (midiparse->tracks, (GFunc) reset_track, midiparse); + midiparse->pulse = 0; + + return TRUE; +} + +static gboolean +gst_midi_parse_perform_seek (GstMidiParse * midiparse, GstEvent * event) +{ + gboolean res = TRUE, tres; + gdouble rate; + GstFormat seek_format; + GstSeekFlags flags; + GstSeekType start_type, stop_type; + gint64 start, stop; + gboolean flush; + gboolean update; + GstSegment seeksegment; + guint32 seqnum; + GstEvent *tevent; + + GST_DEBUG_OBJECT (midiparse, "doing seek: %" GST_PTR_FORMAT, event); + + if (event) { + gst_event_parse_seek (event, &rate, &seek_format, &flags, + &start_type, &start, &stop_type, &stop); + + if (seek_format != GST_FORMAT_TIME) + goto invalid_format; + + flush = flags & GST_SEEK_FLAG_FLUSH; + seqnum = gst_event_get_seqnum (event); + } else { + flush = FALSE; + /* get next seqnum */ + seqnum = gst_util_seqnum_next (); + } + + /* send flush start */ + if (flush) { + tevent = gst_event_new_flush_start (); + gst_event_set_seqnum (tevent, seqnum); + gst_pad_push_event (midiparse->srcpad, tevent); + } else + gst_pad_pause_task (midiparse->srcpad); + + /* grab streaming lock, this should eventually be possible, either + * because the task is paused, our streaming thread stopped + * or because our peer is flushing. */ + GST_PAD_STREAM_LOCK (midiparse->sinkpad); + if (G_UNLIKELY (midiparse->seqnum == seqnum)) { + /* we have seen this event before, issue a warning for now */ + GST_WARNING_OBJECT (midiparse, "duplicate event found %" G_GUINT32_FORMAT, + seqnum); + } else { + midiparse->seqnum = seqnum; + GST_DEBUG_OBJECT (midiparse, "seek with seqnum %" G_GUINT32_FORMAT, seqnum); + } + + /* Copy the current segment info into the temp segment that we can actually + * attempt the seek with. We only update the real segment if the seek succeeds. */ + memcpy (&seeksegment, &midiparse->segment, sizeof (GstSegment)); + + /* now configure the final seek segment */ + if (event) { + gst_segment_do_seek (&seeksegment, rate, seek_format, flags, + start_type, start, stop_type, stop, &update); + } + + /* Else, no seek event passed, so we're just (re)starting the + current segment. */ + GST_DEBUG_OBJECT (midiparse, "segment configured from %" G_GINT64_FORMAT + " to %" G_GINT64_FORMAT ", position %" G_GINT64_FORMAT, + seeksegment.start, seeksegment.stop, seeksegment.position); + + /* do the seek, segment.position contains the new position. */ + res = gst_midi_parse_do_seek (midiparse, &seeksegment); + + /* and prepare to continue streaming */ + if (flush) { + tevent = gst_event_new_flush_stop (TRUE); + gst_event_set_seqnum (tevent, seqnum); + /* send flush stop, peer will accept data and events again. We + * are not yet providing data as we still have the STREAM_LOCK. */ + gst_pad_push_event (midiparse->srcpad, tevent); + } + + /* if the seek was successful, we update our real segment and push + * out the new segment. */ + if (res) { + GST_OBJECT_LOCK (midiparse); + memcpy (&midiparse->segment, &seeksegment, sizeof (GstSegment)); + GST_OBJECT_UNLOCK (midiparse); + + if (seeksegment.flags & GST_SEGMENT_FLAG_SEGMENT) { + GstMessage *message; + + message = gst_message_new_segment_start (GST_OBJECT (midiparse), + seeksegment.format, seeksegment.position); + gst_message_set_seqnum (message, seqnum); + + gst_element_post_message (GST_ELEMENT (midiparse), message); + } + /* for deriving a stop position for the playback segment from the seek + * segment, we must take the duration when the stop is not set */ + if ((stop = seeksegment.stop) == -1) + stop = seeksegment.duration; + + midiparse->segment_pending = TRUE; + midiparse->discont = TRUE; + } + + /* and restart the task in case it got paused explicitly or by + * the FLUSH_START event we pushed out. */ + tres = + gst_pad_start_task (midiparse->sinkpad, + (GstTaskFunction) gst_midi_parse_loop, midiparse->sinkpad, NULL); + if (res && !tres) + res = FALSE; + + /* and release the lock again so we can continue streaming */ + GST_PAD_STREAM_UNLOCK (midiparse->sinkpad); + + return res; + + /* ERROR */ +invalid_format: + { + GST_DEBUG_OBJECT (midiparse, "Unsupported seek format %s", + gst_format_get_name (seek_format)); + return FALSE; + } +} + +static gboolean +gst_midi_parse_src_event (GstPad * pad, GstObject * parent, GstEvent * event) +{ + gboolean res = FALSE; + GstMidiParse *midiparse = GST_MIDI_PARSE (parent); + + GST_DEBUG_OBJECT (pad, "%s event received", GST_EVENT_TYPE_NAME (event)); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_SEEK: + res = gst_midi_parse_perform_seek (midiparse, event); + break; + default: + break; + } + gst_event_unref (event); + + return res; +} + +static gboolean +gst_midi_parse_activate (GstPad * sinkpad, GstObject * parent) +{ + GstQuery *query; + gboolean pull_mode; + + query = gst_query_new_scheduling (); + + if (!gst_pad_peer_query (sinkpad, query)) { + gst_query_unref (query); + goto activate_push; + } + + pull_mode = gst_query_has_scheduling_mode_with_flags (query, + GST_PAD_MODE_PULL, GST_SCHEDULING_FLAG_SEEKABLE); + gst_query_unref (query); + + if (!pull_mode) + goto activate_push; + + GST_DEBUG_OBJECT (sinkpad, "activating pull"); + return gst_pad_activate_mode (sinkpad, GST_PAD_MODE_PULL, TRUE); + +activate_push: + { + GST_DEBUG_OBJECT (sinkpad, "activating push"); + return gst_pad_activate_mode (sinkpad, GST_PAD_MODE_PUSH, TRUE); + } +} + +static gboolean +gst_midi_parse_activatemode (GstPad * pad, GstObject * parent, + GstPadMode mode, gboolean active) +{ + gboolean res; + + switch (mode) { + case GST_PAD_MODE_PUSH: + res = TRUE; + break; + case GST_PAD_MODE_PULL: + if (active) { + res = gst_pad_start_task (pad, (GstTaskFunction) gst_midi_parse_loop, + pad, NULL); + } else { + res = gst_pad_stop_task (pad); + } + break; + default: + res = FALSE; + break; + } + return res; +} + +static gboolean +parse_MThd (GstMidiParse * midiparse, guint8 * data, guint size) +{ + guint16 format, ntracks, division; + gboolean multitrack; + + format = GST_READ_UINT16_BE (data); + switch (format) { + case 0: + multitrack = FALSE; + break; + case 1: + multitrack = TRUE; + break; + default: + case 2: + goto invalid_format; + } + ntracks = GST_READ_UINT16_BE (data + 2); + if (ntracks > 1 && !multitrack) + goto invalid_tracks; + + division = GST_READ_UINT16_BE (data + 4); + if (division & 0x8000) + goto invalid_division; + + GST_DEBUG_OBJECT (midiparse, "format %u, tracks %u, division %u", + format, ntracks, division); + + midiparse->ntracks = ntracks; + midiparse->division = division; + + return TRUE; + +invalid_format: + { + GST_ERROR_OBJECT (midiparse, "unsupported midi format %u", format); + return FALSE; + } +invalid_tracks: + { + GST_ERROR_OBJECT (midiparse, "invalid number of tracks %u for format %u", + ntracks, format); + return FALSE; + } +invalid_division: + { + GST_ERROR_OBJECT (midiparse, "unsupported division"); + return FALSE; + } +} + +static guint +parse_varlen (GstMidiParse * midiparse, guint8 * data, guint size, + gint32 * result) +{ + gint32 res; + gint i; + + res = 0; + for (i = 0; i < 4; i++) { + if (size == 0) + return 0; + + res = (res << 7) | ((data[i]) & 0x7f); + if ((data[i] & 0x80) == 0) { + *result = res; + return i + 1; + } + } + return 0; +} + +static GstFlowReturn +handle_meta_event (GstMidiParse * midiparse, GstMidiTrack * track, guint8 event) +{ + guint8 type; + guint8 *data; + gchar *bytes; + guint size, consumed; + gint32 length; + + track->offset += 1; + + data = track->data + track->offset; + size = track->size - track->offset; + + if (size < 1) + goto short_file; + + type = data[0]; + + consumed = parse_varlen (midiparse, data + 1, size - 1, &length); + if (consumed == 0) + goto short_file; + + data += consumed + 1; + size -= consumed + 1; + + if (size < length) + goto short_file; + + GST_DEBUG_OBJECT (midiparse, "handle meta event type 0x%02x, length %u", + type, length); + + bytes = g_strndup ((const gchar *) data, length); + + switch (type) { + case 0x01: + GST_DEBUG_OBJECT (midiparse, "Text: %s", bytes); + break; + case 0x02: + GST_DEBUG_OBJECT (midiparse, "Copyright: %s", bytes); + break; + case 0x03: + GST_DEBUG_OBJECT (midiparse, "Track Name: %s", bytes); + break; + case 0x04: + GST_DEBUG_OBJECT (midiparse, "Instrument: %s", bytes); + break; + case 0x05: + GST_DEBUG_OBJECT (midiparse, "Lyric: %s", bytes); + break; + case 0x06: + GST_DEBUG_OBJECT (midiparse, "Marker: %s", bytes); + break; + case 0x07: + GST_DEBUG_OBJECT (midiparse, "Cue point: %s", bytes); + break; + case 0x08: + GST_DEBUG_OBJECT (midiparse, "Patch name: %s", bytes); + break; + case 0x09: + GST_DEBUG_OBJECT (midiparse, "MIDI port: %s", bytes); + break; + case 0x2f: + GST_DEBUG_OBJECT (midiparse, "End of track"); + break; + case 0x51: + { + guint32 uspqn = (data[0] << 16) | (data[1] << 8) | data[2]; + midiparse->tempo = (uspqn ? uspqn : DEFAULT_TEMPO); + GST_DEBUG_OBJECT (midiparse, "tempo %u", midiparse->tempo); + break; + } + case 0x54: + GST_DEBUG_OBJECT (midiparse, "SMPTE offset"); + break; + case 0x58: + GST_DEBUG_OBJECT (midiparse, "Time signature"); + break; + case 0x59: + GST_DEBUG_OBJECT (midiparse, "Key signature"); + break; + case 0x7f: + GST_DEBUG_OBJECT (midiparse, "Proprietary event"); + break; + default: + GST_DEBUG_OBJECT (midiparse, "unknown event 0x%02x length %d", type, + length); + break; + } + g_free (bytes); + + track->offset += consumed + length + 1; + + return GST_FLOW_OK; + + /* ERRORS */ +short_file: + { + GST_DEBUG_OBJECT (midiparse, "not enough data"); + return GST_FLOW_ERROR; + } +} + +static GstFlowReturn +handle_sysex_event (GstMidiParse * midiparse, GstMidiTrack * track, + guint8 event, GstMidiPushFunc pushfunc, gpointer user_data) +{ + GstFlowReturn ret; + guint8 *data; + guint size, consumed; + gint32 length; + + track->offset += 1; + + data = track->data + track->offset; + size = track->size - track->offset; + + consumed = parse_varlen (midiparse, data, size, &length); + if (consumed == 0) + goto short_file; + + data += consumed; + size -= consumed; + + if (size < length) + goto short_file; + + GST_DEBUG_OBJECT (midiparse, "handle sysex event 0x%02x, length %u", + event, length); + + if (pushfunc) + ret = pushfunc (midiparse, track, event, data, length, user_data); + else + ret = GST_FLOW_OK; + + track->offset += consumed + length; + + return ret; + + /* ERRORS */ +short_file: + { + GST_DEBUG_OBJECT (midiparse, "not enough data"); + return GST_FLOW_ERROR; + } +} + + +static guint8 +event_from_status (GstMidiParse * midiparse, GstMidiTrack * track, + guint8 status) +{ + if ((status & 0x80) == 0) { + if ((track->running_status & 0x80) == 0) + return 0; + + return track->running_status; + } else { + return status; + } +} + +static gboolean +update_track_position (GstMidiParse * midiparse, GstMidiTrack * track) +{ + gint32 delta_time; + guint8 *data; + guint size, consumed; + + if (track->offset >= track->size) + goto eot; + + data = track->data + track->offset; + size = track->size - track->offset; + + consumed = parse_varlen (midiparse, data, size, &delta_time); + if (consumed == 0) + goto eot; + + track->pulse += delta_time; + track->offset += consumed; + + GST_LOG_OBJECT (midiparse, "updated track to pulse %" G_GUINT64_FORMAT, + track->pulse); + + return TRUE; + + /* ERRORS */ +eot: + { + GST_DEBUG_OBJECT (midiparse, "track ended"); + track->eot = TRUE; + return FALSE; + } +} + +static GstFlowReturn +handle_next_event (GstMidiParse * midiparse, GstMidiTrack * track, + GstMidiPushFunc pushfunc, gpointer user_data) +{ + GstFlowReturn ret = GST_FLOW_OK; + guint8 status, event; + guint length; + guint8 *data; + + data = &track->data[track->offset]; + + status = data[0]; + event = event_from_status (midiparse, track, status); + + GST_LOG_OBJECT (midiparse, "track %p, status 0x%02x, event 0x%02x", track, + status, event); + + switch (event & 0xf0) { + case 0xf0: + switch (event) { + case 0xff: + ret = handle_meta_event (midiparse, track, event); + break; + case 0xf0: + case 0xf7: + ret = + handle_sysex_event (midiparse, track, event, pushfunc, user_data); + break; + default: + goto unhandled_event; + } + length = 0; + break; + case 0xc0: + case 0xd0: + length = 1; + break; + case 0x80: + case 0x90: + case 0xa0: + case 0xb0: + case 0xe0: + length = 2; + break; + default: + goto undefined_status; + } + if (length > 0) { + if (status & 0x80) { + if (pushfunc) + ret = pushfunc (midiparse, track, event, data + 1, length, user_data); + track->offset += length + 1; + } else { + if (pushfunc) + ret = pushfunc (midiparse, track, event, data, length + 1, user_data); + track->offset += length; + } + } + + if (ret == GST_FLOW_OK) { + if (event < 0xF8) + track->running_status = event; + + update_track_position (midiparse, track); + } + return ret; + + /* ERRORS */ +undefined_status: + { + GST_ERROR_OBJECT (midiparse, "Undefined status and invalid running status"); + return GST_FLOW_ERROR; + } +unhandled_event: + { + /* we don't know the size so we can't continue parsing */ + GST_ERROR_OBJECT (midiparse, "unhandled event 0x%08x", event); + return GST_FLOW_ERROR; + } +} + +static void +reset_track (GstMidiTrack * track, GstMidiParse * midiparse) +{ + GST_DEBUG_OBJECT (midiparse, "reset track"); + track->offset = 0; + track->pulse = 0; + track->eot = FALSE; + track->running_status = 0xff; + update_track_position (midiparse, track); +} + +static gboolean +parse_MTrk (GstMidiParse * midiparse, guint8 * data, guint size) +{ + GstMidiTrack *track; + GstClockTime duration; + + /* ignore excess tracks */ + if (midiparse->track_count >= midiparse->ntracks) + return TRUE; + + track = g_slice_new (GstMidiTrack); + track->data = data; + track->size = size; + reset_track (track, midiparse); + + midiparse->tracks = g_list_append (midiparse->tracks, track); + midiparse->track_count++; + + /* now loop over all events and calculate the duration */ + while (!track->eot) { + handle_next_event (midiparse, track, NULL, NULL); + } + + duration = gst_util_uint64_scale (track->pulse, + 1000 * midiparse->tempo, midiparse->division); + + GST_DEBUG_OBJECT (midiparse, "duration %" GST_TIME_FORMAT, + GST_TIME_ARGS (duration)); + + if (duration > midiparse->segment.duration) + midiparse->segment.duration = duration; + + reset_track (track, midiparse); + + return TRUE; +} + +static gboolean +find_midi_chunk (GstMidiParse * midiparse, guint8 * data, guint size, + guint * offset, guint * length) +{ + guint32 type; + + *length = 0; + + if (size < 8) + goto short_chunk; + + type = GST_STR_FOURCC (data); + + if (type == GST_MAKE_FOURCC ('R', 'I', 'F', 'F')) { + guint32 riff_len; + + GST_DEBUG_OBJECT (midiparse, "found RIFF"); + + if (size < 12) + goto short_chunk; + + if (GST_STR_FOURCC (data + 8) != GST_MAKE_FOURCC ('R', 'M', 'I', 'D')) + goto invalid_format; + + riff_len = GST_READ_UINT32_LE (data + 4); + + if (size < riff_len) + goto short_chunk; + + data += 12; + size -= 12; + *offset = 12; + + GST_DEBUG_OBJECT (midiparse, "found RIFF RMID of size %u", riff_len); + + while (TRUE) { + guint32 chunk_type; + guint32 chunk_len; + + if (riff_len < 8) + goto short_chunk; + + chunk_type = GST_STR_FOURCC (data); + chunk_len = GST_READ_UINT32_LE (data + 4); + + riff_len -= 8; + if (riff_len < chunk_len) + goto short_chunk; + + data += 8; + size -= 8; + *offset += 8; + riff_len -= chunk_len; + + if (chunk_type == GST_MAKE_FOURCC ('d', 'a', 't', 'a')) { + *length = chunk_len; + break; + } + + data += chunk_len; + size -= chunk_len; + } + } else { + *offset = 0; + *length = size; + } + return TRUE; + + /* ERRORS */ +short_chunk: + { + GST_LOG_OBJECT (midiparse, "not enough data %u < %u", *length + 8, size); + return FALSE; + } +invalid_format: + { + GST_ERROR_OBJECT (midiparse, "invalid format"); + return FALSE; + } +} + +static guint +gst_midi_parse_chunk (GstMidiParse * midiparse, guint8 * data, guint size) +{ + guint32 type, length = 0; + + if (size < 8) + goto short_chunk; + + length = GST_READ_UINT32_BE (data + 4); + + GST_DEBUG_OBJECT (midiparse, "have type %c%c%c%c, length %u", + data[0], data[1], data[2], data[3], length); + + if (size < length + 8) + goto short_chunk; + + type = GST_STR_FOURCC (data); + + switch (type) { + case GST_MAKE_FOURCC ('M', 'T', 'h', 'd'): + if (!parse_MThd (midiparse, data + 8, length)) + goto invalid_format; + break; + case GST_MAKE_FOURCC ('M', 'T', 'r', 'k'): + if (!parse_MTrk (midiparse, data + 8, length)) + goto invalid_format; + break; + default: + GST_LOG_OBJECT (midiparse, "ignore chunk"); + break; + } + + return length + 8; + + /* ERRORS */ +short_chunk: + { + GST_LOG_OBJECT (midiparse, "not enough data %u < %u", size, length + 8); + return 0; + } +invalid_format: + { + GST_ERROR_OBJECT (midiparse, "invalid format"); + return 0; + } +} + +static GstFlowReturn +gst_midi_parse_parse_song (GstMidiParse * midiparse) +{ + GstCaps *outcaps; + guint8 *data; + guint size, offset, length; + gchar *stream_id; + + GST_DEBUG_OBJECT (midiparse, "Parsing song"); + + gst_segment_init (&midiparse->segment, GST_FORMAT_TIME); + midiparse->segment.duration = 0; + midiparse->pulse = 0; + + size = gst_adapter_available (midiparse->adapter); + data = gst_adapter_take (midiparse->adapter, size); + + midiparse->data = data; + midiparse->tempo = DEFAULT_TEMPO; + + if (!find_midi_chunk (midiparse, data, size, &offset, &length)) + goto invalid_format; + + while (length) { + guint consumed; + + consumed = gst_midi_parse_chunk (midiparse, &data[offset], length); + if (consumed == 0) + goto short_file; + + offset += consumed; + length -= consumed; + } + + GST_DEBUG_OBJECT (midiparse, "song duration %" GST_TIME_FORMAT, + GST_TIME_ARGS (midiparse->segment.duration)); + + stream_id = gst_pad_create_stream_id (midiparse->srcpad, GST_ELEMENT_CAST (midiparse), NULL); + gst_pad_push_event (midiparse->srcpad, gst_event_new_stream_start (stream_id)); + g_free (stream_id); + + outcaps = gst_pad_get_pad_template_caps (midiparse->srcpad); + gst_pad_set_caps (midiparse->srcpad, outcaps); + gst_caps_unref (outcaps); + + midiparse->segment_pending = TRUE; + midiparse->discont = TRUE; + + GST_DEBUG_OBJECT (midiparse, "Parsing song done"); + + return GST_FLOW_OK; + + /* ERRORS */ +short_file: + { + GST_ERROR_OBJECT (midiparse, "not enough data"); + return GST_FLOW_ERROR; + } +invalid_format: + { + GST_ERROR_OBJECT (midiparse, "invalid format"); + return GST_FLOW_ERROR; + } +} + +static GstFlowReturn +play_push_func (GstMidiParse * midiparse, GstMidiTrack * track, + guint8 event, guint8 * data, guint length, gpointer user_data) +{ + GstBuffer *outbuf; + GstMapInfo info; + GstClockTime position; + + outbuf = gst_buffer_new_allocate (NULL, length + 1, NULL); + + gst_buffer_map (outbuf, &info, GST_MAP_WRITE); + info.data[0] = event; + if (length) + memcpy (&info.data[1], data, length); + gst_buffer_unmap (outbuf, &info); + + position = midiparse->segment.position; + GST_BUFFER_PTS (outbuf) = position; + GST_BUFFER_DTS (outbuf) = position; + + GST_DEBUG_OBJECT (midiparse, "pushing %" GST_TIME_FORMAT, + GST_TIME_ARGS (position)); + + if (midiparse->discont) { + GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_DISCONT); + midiparse->discont = FALSE; + } + + return gst_pad_push (midiparse->srcpad, outbuf); +} + +static GstFlowReturn +gst_midi_parse_do_play (GstMidiParse * midiparse) +{ + GstFlowReturn res; + GList *walk; + guint64 pulse, next_pulse = G_MAXUINT64; + GstClockTime position, next_position; + guint64 tick; + + pulse = midiparse->pulse; + position = midiparse->segment.position; + + if (midiparse->segment_pending) { + gst_pad_push_event (midiparse->srcpad, + gst_event_new_segment (&midiparse->segment)); + midiparse->segment_pending = FALSE; + } + + GST_DEBUG_OBJECT (midiparse, "pulse %" G_GUINT64_FORMAT ", position %" + GST_TIME_FORMAT, pulse, GST_TIME_ARGS (position)); + + for (walk = midiparse->tracks; walk; walk = g_list_next (walk)) { + GstMidiTrack *track = walk->data; + + while (!track->eot && track->pulse == pulse) { + res = handle_next_event (midiparse, track, play_push_func, NULL); + if (res != GST_FLOW_OK) + goto error; + } + + if (!track->eot && track->pulse < next_pulse) + next_pulse = track->pulse; + } + + if (next_pulse == G_MAXUINT64) + goto eos; + + tick = position / (10 * GST_MSECOND); + GST_DEBUG_OBJECT (midiparse, "current tick %" G_GUINT64_FORMAT, tick); + + next_position = gst_util_uint64_scale (next_pulse, + 1000 * midiparse->tempo, midiparse->division); + GST_DEBUG_OBJECT (midiparse, "next position %" GST_TIME_FORMAT, + GST_TIME_ARGS (next_position)); + + /* send 10ms ticks to advance the downstream element */ + while (TRUE) { + /* get position of next tick */ + position = ++tick * (10 * GST_MSECOND); + GST_DEBUG_OBJECT (midiparse, "tick %" G_GUINT64_FORMAT + ", position %" GST_TIME_FORMAT, tick, GST_TIME_ARGS (position)); + + if (position >= next_position) + break; + + midiparse->segment.position = position; + res = play_push_func (midiparse, NULL, 0xf9, NULL, 0, NULL); + if (res != GST_FLOW_OK) + goto error; + } + + midiparse->pulse = next_pulse; + midiparse->segment.position = next_position; + + return GST_FLOW_OK; + + /* ERRORS */ +eos: + { + GST_DEBUG_OBJECT (midiparse, "we are EOS"); + return GST_FLOW_EOS; + } +error: + { + GST_DEBUG_OBJECT (midiparse, "have flow result %s", + gst_flow_get_name (res)); + return res; + } +} + +static gboolean +gst_midi_parse_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) +{ + gboolean res; + GstMidiParse *midiparse = GST_MIDI_PARSE (parent); + + GST_DEBUG_OBJECT (pad, "%s event received", GST_EVENT_TYPE_NAME (event)); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_EOS: + midiparse->state = GST_MIDI_PARSE_STATE_PARSE; + /* now start the parsing task */ + res = gst_pad_start_task (midiparse->sinkpad, + (GstTaskFunction) gst_midi_parse_loop, midiparse->sinkpad, NULL); + /* don't forward the event */ + gst_event_unref (event); + break; + case GST_EVENT_CAPS: + case GST_EVENT_STREAM_START: + case GST_EVENT_SEGMENT: + res = TRUE; + gst_event_unref (event); + break; + default: + res = gst_pad_event_default (pad, parent, event); + break; + } + return res; +} + +static GstFlowReturn +gst_midi_parse_chain (GstPad * sinkpad, GstObject * parent, GstBuffer * buffer) +{ + GstMidiParse *midiparse; + + midiparse = GST_MIDI_PARSE (parent); + + /* push stuff in the adapter, we will start doing something in the sink event + * handler when we get EOS */ + gst_adapter_push (midiparse->adapter, buffer); + + return GST_FLOW_OK; +} + +static void +gst_midi_parse_loop (GstPad * sinkpad) +{ + GstMidiParse *midiparse = GST_MIDI_PARSE (GST_PAD_PARENT (sinkpad)); + GstFlowReturn ret; + + switch (midiparse->state) { + case GST_MIDI_PARSE_STATE_LOAD: + { + GstBuffer *buffer = NULL; + + GST_DEBUG_OBJECT (midiparse, "loading song"); + + ret = + gst_pad_pull_range (midiparse->sinkpad, midiparse->offset, -1, + &buffer); + + if (ret == GST_FLOW_EOS) { + GST_DEBUG_OBJECT (midiparse, "Song loaded"); + midiparse->state = GST_MIDI_PARSE_STATE_PARSE; + } else if (ret != GST_FLOW_OK) { + GST_ELEMENT_ERROR (midiparse, STREAM, DECODE, (NULL), + ("Unable to read song")); + goto pause; + } else { + GST_DEBUG_OBJECT (midiparse, "pushing buffer"); + gst_adapter_push (midiparse->adapter, buffer); + midiparse->offset += gst_buffer_get_size (buffer); + } + break; + } + case GST_MIDI_PARSE_STATE_PARSE: + ret = gst_midi_parse_parse_song (midiparse); + if (ret != GST_FLOW_OK) + goto pause; + midiparse->state = GST_MIDI_PARSE_STATE_PLAY; + break; + case GST_MIDI_PARSE_STATE_PLAY: + ret = gst_midi_parse_do_play (midiparse); + if (ret != GST_FLOW_OK) + goto pause; + break; + default: + break; + } + return; + +pause: + { + const gchar *reason = gst_flow_get_name (ret); + GstEvent *event; + + GST_DEBUG_OBJECT (midiparse, "pausing task, reason %s", reason); + gst_pad_pause_task (sinkpad); + if (ret == GST_FLOW_EOS) { + /* perform EOS logic */ + event = gst_event_new_eos (); + gst_pad_push_event (midiparse->srcpad, event); + } else if (ret == GST_FLOW_NOT_LINKED || ret < GST_FLOW_EOS) { + event = gst_event_new_eos (); + /* for fatal errors we post an error message, post the error + * first so the app knows about the error first. */ + GST_ELEMENT_ERROR (midiparse, STREAM, FAILED, + ("Internal data flow error."), + ("streaming task paused, reason %s (%d)", reason, ret)); + gst_pad_push_event (midiparse->srcpad, event); + } + } +} + +static void +free_track (GstMidiTrack * track, GstMidiParse * midiparse) +{ + g_slice_free (GstMidiTrack, track); +} + +static void +gst_midi_parse_reset (GstMidiParse * midiparse) +{ + gst_adapter_clear (midiparse->adapter); + g_free (midiparse->data); + midiparse->data = NULL; + g_list_foreach (midiparse->tracks, (GFunc) free_track, midiparse); + g_list_free (midiparse->tracks); + midiparse->tracks = NULL; + midiparse->track_count = 0; +} + +static GstStateChangeReturn +gst_midi_parse_change_state (GstElement * element, GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstMidiParse *midiparse = GST_MIDI_PARSE (element); + + switch (transition) { + case GST_STATE_CHANGE_NULL_TO_READY: + break; + case GST_STATE_CHANGE_READY_TO_PAUSED: + midiparse->offset = 0; + midiparse->state = GST_MIDI_PARSE_STATE_LOAD; + break; + case GST_STATE_CHANGE_PAUSED_TO_PLAYING: + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + + switch (transition) { + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + break; + case GST_STATE_CHANGE_PAUSED_TO_READY: + gst_midi_parse_reset (midiparse); + break; + case GST_STATE_CHANGE_READY_TO_NULL: + break; + default: + break; + } + + return ret; +} + +static void +gst_midi_parse_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_midi_parse_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + switch (prop_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} diff --git a/gst/midi/midiparse.h b/gst/midi/midiparse.h new file mode 100644 index 000000000..ba3a25ea1 --- /dev/null +++ b/gst/midi/midiparse.h @@ -0,0 +1,90 @@ +/* + * gstmidiparse - midiparse plugin for gstreamer + * + * Copyright 2007 Wouter Paesen <wouter@blue-gate.be> + * Copyright 2013 Wim Taymans <wim.taymans@gmail.be> + * + * 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_MIDIPARSE_H__ +#define __GST_MIDIPARSE_H__ + +#include <gst/gst.h> +#include <gst/base/gstadapter.h> +#include <midiparse.h> + +G_BEGIN_DECLS + +#define GST_TYPE_MIDI_PARSE \ + (gst_midi_parse_get_type()) +#define GST_MIDI_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_MIDI_PARSE,GstMidiParse)) +#define GST_MIDI_PARSE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_MIDI_PARSE,GstMidiParseClass)) +#define GST_IS_MIDI_PARSE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_MIDI_PARSE)) +#define GST_IS_MIDI_PARSE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_MIDI_PARSE)) + +typedef struct _GstMidiParse GstMidiParse; +typedef struct _GstMidiParseClass GstMidiParseClass; + +typedef enum { + GST_MIDI_PARSE_STATE_LOAD, + GST_MIDI_PARSE_STATE_PARSE, + GST_MIDI_PARSE_STATE_PLAY +} GstMidiParseState; + +struct _GstMidiParse +{ + GstElement element; + + GstPad *sinkpad, *srcpad; + + /* input stream properties */ + GstMidiParseState state; + + guint tempo; + guint16 ntracks; + guint16 division; + + GList *tracks; + guint track_count; + + guint64 offset; + GstAdapter *adapter; + guint8 *data; + + /* output data */ + gboolean discont; + GstSegment segment; + gboolean segment_pending; + guint32 seqnum; + + guint64 pulse; +}; + +struct _GstMidiParseClass +{ + GstElementClass parent_class; +}; + +GType gst_midi_parse_get_type (void); + +G_END_DECLS + +#endif /* __GST_MIDI_PARSE_H__ */ |