gstreamer/ext/gme/gstgme.c
Sebastian Dröge 782965be9f gmedec: Use GstAdapter instead of many buffer joins
This reduces the number of reallocations and memcpys drastically.

Also free the input data as soon as it's complete and passed to GME
as it's not needed anymore.
2009-08-07 14:47:30 +02:00

546 lines
15 KiB
C

/* Copyright (C) 2004-2005,2009 Michael Pyne <mpyne at kde org>
* Copyright (C) 2004-2006 Chris Lee <clee at kde org>
* Copyright (C) 2007 Brian Koropoff <bkoropoff at 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., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gstgme.h"
#include <string.h>
#include <glib/gprintf.h>
#include <glib.h>
static GstStaticPadTemplate sink_factory =
GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
GST_STATIC_CAPS ("audio/x-ay; "
"audio/x-gbs; "
"audio/x-gym; "
"audio/x-hes; "
"audio/x-kss; "
"audio/x-nsf; " "audio/x-sap; " "audio/x-spc; " "audio/x-vgm"));
static GstStaticPadTemplate src_factory =
GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS,
GST_STATIC_CAPS ("audio/x-raw-int, "
"endianness = (int) BYTE_ORDER, "
"signed = (boolean) TRUE, "
"width = (int) 16, "
"depth = (int) 16, " "rate = (int) 32000, " "channels = (int) 2"));
GST_BOILERPLATE (GstGmeDec, gst_gme_dec, GstElement, GST_TYPE_ELEMENT);
static GstFlowReturn gst_gme_dec_chain (GstPad * pad, GstBuffer * buffer);
static gboolean gst_gme_dec_sink_event (GstPad * pad, GstEvent * event);
static gboolean gst_gme_dec_src_event (GstPad * pad, GstEvent * event);
static gboolean gst_gme_dec_src_query (GstPad * pad, GstQuery * query);
static GstStateChangeReturn gst_gme_dec_change_state (GstElement * element,
GstStateChange transition);
static void gst_gme_play (GstPad * pad);
static void gst_gme_dec_dispose (GObject * object);
static gboolean gme_setup (GstGmeDec * gme);
static gboolean
gme_negotiate (GstGmeDec * gme)
{
GstCaps *allowed, *caps;
GstStructure *structure;
gint width = 16, depth = 16;
gboolean sign;
int rate = 32000;
int channels = 2;
allowed = gst_pad_get_allowed_caps (gme->srcpad);
if (!allowed) {
GST_DEBUG_OBJECT (gme, "couldn't get allowed caps");
return FALSE;
}
GST_DEBUG_OBJECT (gme, "allowed caps: %" GST_PTR_FORMAT, allowed);
structure = gst_caps_get_structure (allowed, 0);
gst_structure_get_int (structure, "width", &width);
gst_structure_get_int (structure, "depth", &depth);
if (width && depth && width != depth) {
GST_DEBUG_OBJECT (gme, "width %d and depth %d are different", width, depth);
gst_caps_unref (allowed);
return FALSE;
}
gst_structure_get_boolean (structure, "signed", &sign);
gst_structure_get_int (structure, "rate", &rate);
gst_structure_get_int (structure, "channels", &channels);
caps = gst_caps_new_simple ("audio/x-raw-int",
"endianness", G_TYPE_INT, G_BYTE_ORDER,
"signed", G_TYPE_BOOLEAN, TRUE,
"width", G_TYPE_INT, width,
"depth", G_TYPE_INT, depth,
"rate", G_TYPE_INT, rate, "channels", G_TYPE_INT, channels, NULL);
gst_pad_set_caps (gme->srcpad, caps);
gst_caps_unref (caps);
gst_caps_unref (allowed);
return TRUE;
}
static void
gst_gme_dec_base_init (gpointer g_class)
{
GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
gst_element_class_set_details_simple (element_class,
"Gaming console music file decoder", "Codec/Audio/Decoder",
"Uses libgme to emulate a gaming console sound processors",
"Chris Lee <clee@kde.org>, Brian Koropoff <bkoropoff@gmail.com>, "
"Michael Pyne <mpyne@kde.org>, Sebastian Dröge <sebastian.droege@collabora.co.uk>");
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&sink_factory));
gst_element_class_add_pad_template (element_class,
gst_static_pad_template_get (&src_factory));
}
static void
gst_gme_dec_class_init (GstGmeDecClass * klass)
{
GstElementClass *element_class = (GstElementClass *) klass;
GObjectClass *gobject_class = (GObjectClass *) klass;
element_class->change_state = GST_DEBUG_FUNCPTR (gst_gme_dec_change_state);
gobject_class->dispose = gst_gme_dec_dispose;
}
static const GstQueryType *
gst_gme_dec_src_query_type (GstPad * pad)
{
static const GstQueryType query_types[] = {
GST_QUERY_DURATION,
GST_QUERY_POSITION,
(GstQueryType) 0
};
return query_types;
}
static void
gst_gme_dec_init (GstGmeDec * gme, GstGmeDecClass * klass)
{
gme->sinkpad = gst_pad_new_from_static_template (&sink_factory, "sink");
/* gst_pad_set_query_function (gme->sinkpad, NULL); */
gst_pad_set_event_function (gme->sinkpad, gst_gme_dec_sink_event);
gst_pad_set_chain_function (gme->sinkpad, gst_gme_dec_chain);
gst_element_add_pad (GST_ELEMENT (gme), gme->sinkpad);
gme->srcpad = gst_pad_new_from_static_template (&src_factory, "src");
gst_pad_set_event_function (gme->srcpad, gst_gme_dec_src_event);
gst_pad_set_query_function (gme->srcpad, gst_gme_dec_src_query);
gst_pad_set_query_type_function (gme->srcpad, gst_gme_dec_src_query_type);
gst_pad_use_fixed_caps (gme->srcpad);
gst_element_add_pad (GST_ELEMENT (gme), gme->srcpad);
gme->adapter = gst_adapter_new ();
gme->player = NULL;
gme->total_duration = GST_CLOCK_TIME_NONE;
gme->initialized = FALSE;
}
static void
gst_gme_dec_dispose (GObject * object)
{
GstGmeDec *gme = GST_GME_DEC (object);
if (gme->adapter) {
gst_object_unref (gme->adapter);
gme->adapter = NULL;
}
}
static GstFlowReturn
gst_gme_dec_chain (GstPad * pad, GstBuffer * buffer)
{
GstGmeDec *gme = GST_GME_DEC (gst_pad_get_parent (pad));
/* Accumulate GME data until end-of-stream, then commence playback. */
gst_adapter_push (gme->adapter, buffer);
gst_object_unref (gme);
return GST_FLOW_OK;
}
static gboolean
gst_gme_dec_sink_event (GstPad * pad, GstEvent * event)
{
GstGmeDec *gme = GST_GME_DEC (gst_pad_get_parent (pad));
gboolean result = TRUE;
gboolean forward = FALSE;
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_EOS:
/* we get EOS when we loaded the complete file, now try to initialize the
* decoding */
if (!(result = gme_setup (gme))) {
/* can't start, post an ERROR and push EOS downstream */
GST_ELEMENT_ERROR (gme, STREAM, DEMUX, (NULL),
("can't start playback"));
forward = TRUE;
}
break;
default:
break;
}
if (forward)
result = gst_pad_push_event (gme->srcpad, event);
else
gst_event_unref (event);
gst_object_unref (gme);
return result;
}
static gboolean
gst_gme_dec_src_event (GstPad * pad, GstEvent * event)
{
GstGmeDec *gme = GST_GME_DEC (gst_pad_get_parent (pad));
gboolean result = FALSE;
switch (GST_EVENT_TYPE (event)) {
case GST_EVENT_SEEK:
{
gdouble rate;
GstFormat format;
GstSeekFlags flags;
GstSeekType start_type, stop_type;
gint64 start, stop;
gboolean flush;
gst_event_parse_seek (event, &rate, &format, &flags, &start_type, &start,
&stop_type, &stop);
gst_event_unref (event);
if (format != GST_FORMAT_TIME) {
GST_DEBUG_OBJECT (gme, "seeking is only supported in TIME format");
break;
}
if (start_type != GST_SEEK_TYPE_SET || stop_type != GST_SEEK_TYPE_NONE) {
GST_DEBUG_OBJECT (gme, "unsupported seek type");
break;
}
if (stop_type == GST_SEEK_TYPE_NONE)
stop = GST_CLOCK_TIME_NONE;
if (start_type == GST_SEEK_TYPE_SET) {
guint64 cur = gme_tell (gme->player) * GST_MSECOND;
guint64 dest = (guint64) start;
if (gme->total_duration != GST_CLOCK_TIME_NONE)
dest = CLAMP (dest, 0, gme->total_duration);
else
dest = MAX (0, dest);
if (dest == cur)
break;
flush = (flags & GST_SEEK_FLAG_FLUSH) == GST_SEEK_FLAG_FLUSH;
if (flush) {
gst_pad_push_event (gme->srcpad, gst_event_new_flush_start ());
} else {
gst_pad_stop_task (gme->srcpad);
}
GST_PAD_STREAM_LOCK (gme->srcpad);
if (flags & GST_SEEK_FLAG_SEGMENT) {
gst_element_post_message (GST_ELEMENT (gme),
gst_message_new_segment_start (GST_OBJECT (gme), format, cur));
}
if (flush) {
gst_pad_push_event (gme->srcpad, gst_event_new_flush_stop ());
}
if (stop == GST_CLOCK_TIME_NONE
&& gme->total_duration != GST_CLOCK_TIME_NONE)
stop = gme->total_duration;
gst_pad_push_event (gme->srcpad, gst_event_new_new_segment (FALSE, rate,
GST_FORMAT_TIME, dest, stop, dest));
gme->seekpoint = dest / GST_MSECOND; /* nsecs to msecs */
gme->seeking = TRUE;
gst_pad_start_task (gme->srcpad, (GstTaskFunction) gst_gme_play,
gme->srcpad);
GST_PAD_STREAM_UNLOCK (gme->srcpad);
result = TRUE;
}
break;
}
default:
result = gst_pad_push_event (gme->sinkpad, event);
break;
}
gst_object_unref (gme);
return result;
}
static gboolean
gst_gme_dec_src_query (GstPad * pad, GstQuery * query)
{
GstGmeDec *gme = GST_GME_DEC (gst_pad_get_parent (pad));
gboolean result = TRUE;
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_DURATION:
{
GstFormat format;
gst_query_parse_duration (query, &format, NULL);
if (!gme->initialized || format != GST_FORMAT_TIME
|| gme->total_duration == GST_CLOCK_TIME_NONE) {
result = FALSE;
break;
}
gst_query_set_duration (query, GST_FORMAT_TIME, gme->total_duration);
break;
}
case GST_QUERY_POSITION:
{
GstFormat format;
gst_query_parse_position (query, &format, NULL);
if (!gme->initialized || format != GST_FORMAT_TIME) {
result = FALSE;
break;
}
gst_query_set_position (query, GST_FORMAT_TIME,
(gint64) gme_tell (gme->player) * GST_MSECOND);
break;
}
default:
result = gst_pad_query_default (pad, query);
break;
}
gst_object_unref (gme);
return result;
}
static void
gst_gme_play (GstPad * pad)
{
GstGmeDec *gme = GST_GME_DEC (gst_pad_get_parent (pad));
GstFlowReturn flow_return;
GstBuffer *out;
gboolean seeking = gme->seeking;
gme_err_t gme_err = NULL;
const int NUM_SAMPLES = 1600; /* 4 bytes (stereo 16-bit) per sample */
if (!seeking) {
out = gst_buffer_new_and_alloc (NUM_SAMPLES * 4);
gst_buffer_set_caps (out, GST_PAD_CAPS (pad));
GST_BUFFER_TIMESTAMP (out) = gme_tell (gme->player) * GST_MSECOND;
gme_err =
gme_play (gme->player, NUM_SAMPLES * 2,
(short *) GST_BUFFER_DATA (out));
if (gme_err) {
GST_ELEMENT_ERROR (gme, STREAM, DEMUX, (NULL), (gme_err));
gst_pad_pause_task (pad);
gst_pad_push_event (pad, gst_event_new_eos ());
gst_object_unref (gme);
return;
}
} else {
gme_seek (gme->player, gme->seekpoint);
gme->seeking = FALSE;
out = gst_buffer_new ();
gst_buffer_set_caps (out, GST_PAD_CAPS (pad));
}
if ((flow_return = gst_pad_push (gme->srcpad, out)) != GST_FLOW_OK) {
GST_DEBUG_OBJECT (gme, "pausing task, reason %s",
gst_flow_get_name (flow_return));
gst_pad_pause_task (pad);
if (GST_FLOW_IS_FATAL (flow_return) || flow_return == GST_FLOW_NOT_LINKED) {
gst_pad_push_event (pad, gst_event_new_eos ());
}
}
if (gme_track_ended (gme->player)) {
gst_pad_pause_task (pad);
gst_pad_push_event (pad, gst_event_new_eos ());
}
gst_object_unref (gme);
return;
}
static gboolean
gme_setup (GstGmeDec * gme)
{
gme_info_t *info;
gme_err_t gme_err = NULL;
GstTagList *taglist;
guint64 total_duration;
GstBuffer *buffer;
if (!gst_adapter_available (gme->adapter) || !gme_negotiate (gme)) {
return FALSE;
}
buffer =
gst_adapter_take_buffer (gme->adapter,
gst_adapter_available (gme->adapter));
gme_err =
gme_open_data (GST_BUFFER_DATA (buffer), GST_BUFFER_SIZE (buffer),
&gme->player, 32000);
gst_buffer_unref (buffer);
if (gme_err || !gme->player) {
if (gme->player) {
gme_delete (gme->player);
gme->player = NULL;
}
GST_ELEMENT_ERROR (gme, STREAM, DEMUX, (NULL), (gme_err));
return FALSE;
}
gme_err = gme_track_info (gme->player, &info, 0);
taglist = gst_tag_list_new ();
if (info->song && *info->song)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_TITLE,
info->song, NULL);
if (info->author && *info->author)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_ARTIST,
info->author, NULL);
/* Prefer the name of the official soundtrack over the name of the game (since this is
* how track numbers are derived)
*/
if (info->game && *info->game)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_ALBUM, info->game,
NULL);
if (info->comment && *info->comment)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_COMMENT,
info->comment, NULL);
if (info->dumper && *info->dumper)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_CONTACT,
info->dumper, NULL);
if (info->copyright && *info->copyright)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_COPYRIGHT,
info->copyright, NULL);
if (info->system && *info->system)
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE, GST_TAG_ENCODER,
info->system, NULL);
gme->total_duration = total_duration =
gst_util_uint64_scale_int (info->play_length, GST_MSECOND, 1);
gst_tag_list_add (taglist, GST_TAG_MERGE_REPLACE,
GST_TAG_DURATION, total_duration, NULL);
gst_element_found_tags_for_pad (GST_ELEMENT (gme), gme->srcpad, taglist);
g_free (info);
#ifdef HAVE_LIBGME_ACCURACY
/* TODO: Is it worth it to make this optional? */
gme_enable_accuracy (gme->player, 1);
#endif
gme_start_track (gme->player, 0);
gst_pad_push_event (gme->srcpad, gst_event_new_new_segment (FALSE, 1.0,
GST_FORMAT_TIME, 0, -1, 0));
gst_pad_start_task (gme->srcpad, (GstTaskFunction) gst_gme_play, gme->srcpad);
gme->initialized = TRUE;
gme->seeking = FALSE;
gme->seekpoint = 0;
return gme->initialized;
}
static GstStateChangeReturn
gst_gme_dec_change_state (GstElement * element, GstStateChange transition)
{
GstStateChangeReturn result;
GstGmeDec *dec;
dec = GST_GME_DEC (element);
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
dec->total_duration = GST_CLOCK_TIME_NONE;
break;
default:
break;
}
result = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
if (result == GST_STATE_CHANGE_FAILURE)
return result;
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
gst_adapter_clear (dec->adapter);
break;
default:
break;
}
return result;
}
static gboolean
plugin_init (GstPlugin * plugin)
{
return gst_element_register (plugin, "gmedec", GST_RANK_PRIMARY,
GST_TYPE_GME_DEC);
}
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
GST_VERSION_MINOR,
"gmedec",
"GME Audio Decoder",
plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN);