/* * midiparse - midi parser plugin for gstreamer * * Copyright 2013 Wim Taymans * * 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 #endif #include #include #include #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 "); 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; } }