| /* |
| * 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 |
| * @title: midiparse |
| * @see_also: fluiddec |
| * |
| * This element parses midi-files into midi events. You would need a midi |
| * renderer such as fluidsynth to convert the events into raw samples. |
| * |
| * ## Example pipeline |
| * |[ |
| * gst-launch-1.0 filesrc location=song.mid ! midiparse ! fluiddec ! pulsesink |
| * ]| This example pipeline will parse the midi and render to raw audio which is |
| * played via pulseaudio. |
| * |
| */ |
| |
| #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_static_pad_template (gstelement_class, &src_factory); |
| gst_element_class_add_static_pad_template (gstelement_class, &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 (); |
| |
| filter->have_group_id = FALSE; |
| filter->group_id = G_MAXUINT; |
| } |
| |
| 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:{ |
| GstFormat format; |
| gint64 start, stop; |
| |
| format = midiparse->segment.format; |
| |
| start = |
| gst_segment_to_stream_time (&midiparse->segment, format, |
| midiparse->segment.start); |
| if ((stop = midiparse->segment.stop) == -1) |
| stop = midiparse->segment.duration; |
| else |
| stop = gst_segment_to_stream_time (&midiparse->segment, format, stop); |
| |
| gst_query_set_segment (query, midiparse->segment.rate, format, start, |
| stop); |
| res = TRUE; |
| 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; |
| GstEvent *event; |
| 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); |
| event = |
| gst_pad_get_sticky_event (midiparse->sinkpad, GST_EVENT_STREAM_START, 0); |
| if (event) { |
| if (gst_event_parse_group_id (event, &midiparse->group_id)) |
| midiparse->have_group_id = TRUE; |
| else |
| midiparse->have_group_id = FALSE; |
| gst_event_unref (event); |
| } else if (!midiparse->have_group_id) { |
| midiparse->have_group_id = TRUE; |
| midiparse->group_id = gst_util_group_id_next (); |
| } |
| event = gst_event_new_stream_start (stream_id); |
| if (midiparse->have_group_id) |
| gst_event_set_group_id (event, midiparse->group_id); |
| gst_pad_push_event (midiparse->srcpad, event); |
| 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_FLOW_ERROR (midiparse, 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; |
| midiparse->have_group_id = FALSE; |
| midiparse->group_id = G_MAXUINT; |
| } |
| |
| 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; |
| } |
| } |