mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-03 16:09:39 +00:00
1323 lines
33 KiB
C
1323 lines
33 KiB
C
/*
|
|
* 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;
|
|
|
|
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));
|
|
|
|
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;
|
|
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;
|
|
}
|
|
}
|