| /* -*- c-basic-offset: 2 -*- |
| * GStreamer |
| * Copyright (C) 1999-2001 Erik Walthinsen <omega@cse.ogi.edu> |
| * |
| * 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-speed |
| * |
| * Plays an audio stream at a different speed (by resampling the audio). |
| * |
| * Do not use this element. Either use the 'pitch' element, or do a seek with |
| * a non-1.0 rate parameter, this will have the same effect as using the speed |
| * element (but relies on the decoder/demuxer to handle this correctly, also |
| * requires a fairly up-to-date gst-plugins-base, as of February 2007). |
| * |
| * <refsect2> |
| * <title>Example launch line</title> |
| * |[ |
| * gst-launch filesrc location=test.ogg ! decodebin ! audioconvert ! speed speed=1.5 ! audioconvert ! audioresample ! autoaudiosink |
| * ]| Plays an .ogg file at 1.5x speed. |
| * </refsect2> |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| # include "config.h" |
| #endif |
| |
| #include <string.h> |
| #include <math.h> |
| #include <gst/gst.h> |
| #include <gst/audio/audio.h> |
| |
| #include "gstspeed.h" |
| |
| GST_DEBUG_CATEGORY_STATIC (speed_debug); |
| #define GST_CAT_DEFAULT speed_debug |
| |
| enum |
| { |
| PROP_0, |
| PROP_SPEED |
| }; |
| |
| /* assumption here: sizeof (gfloat) = 4 */ |
| #define GST_SPEED_AUDIO_CAPS \ |
| "audio/x-raw, " \ |
| "format = {" GST_AUDIO_NE (F32) ", " GST_AUDIO_NE (S16) "}, " \ |
| "rate = (int) [ 1, MAX ], " \ |
| "channels = (int) [ 1, MAX ]" |
| |
| static GstStaticPadTemplate gst_speed_sink_template = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS (GST_SPEED_AUDIO_CAPS) |
| ); |
| |
| static GstStaticPadTemplate gst_speed_src_template = |
| GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS (GST_SPEED_AUDIO_CAPS) |
| ); |
| |
| static void speed_set_property (GObject * object, guint prop_id, |
| const GValue * value, GParamSpec * pspec); |
| static void speed_get_property (GObject * object, guint prop_id, GValue * value, |
| GParamSpec * pspec); |
| |
| static gboolean speed_parse_caps (GstSpeed * filter, const GstCaps * caps); |
| |
| static GstFlowReturn speed_chain (GstPad * pad, GstObject * parent, |
| GstBuffer * buf); |
| |
| static GstStateChangeReturn speed_change_state (GstElement * element, |
| GstStateChange transition); |
| static gboolean speed_sink_event (GstPad * pad, GstObject * parent, |
| GstEvent * event); |
| static gboolean speed_src_event (GstPad * pad, GstObject * parent, |
| GstEvent * event); |
| |
| G_DEFINE_TYPE (GstSpeed, gst_speed, GST_TYPE_ELEMENT); |
| |
| static gboolean |
| speed_setcaps (GstPad * pad, GstCaps * caps) |
| { |
| GstSpeed *filter; |
| gboolean ret; |
| |
| filter = GST_SPEED (gst_pad_get_parent (pad)); |
| |
| ret = speed_parse_caps (filter, caps); |
| |
| gst_object_unref (filter); |
| |
| return ret; |
| } |
| |
| static gboolean |
| speed_parse_caps (GstSpeed * filter, const GstCaps * caps) |
| { |
| g_return_val_if_fail (filter != NULL, FALSE); |
| g_return_val_if_fail (caps != NULL, FALSE); |
| |
| if (!gst_audio_info_from_caps (&filter->info, caps)) |
| return FALSE; |
| |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| speed_src_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| GstSpeed *filter; |
| gboolean ret = FALSE; |
| |
| filter = GST_SPEED (parent); |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_SEEK:{ |
| gdouble rate; |
| GstFormat format; |
| GstSeekFlags flags; |
| GstSeekType start_type, stop_type; |
| gint64 start, stop; |
| |
| 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 (filter, "only support seeks in TIME format"); |
| break; |
| } |
| |
| if (start_type != GST_SEEK_TYPE_NONE && start != -1) { |
| start *= filter->speed; |
| } |
| |
| if (stop_type != GST_SEEK_TYPE_NONE && stop != -1) { |
| stop *= filter->speed; |
| } |
| |
| event = gst_event_new_seek (rate, format, flags, start_type, start, |
| stop_type, stop); |
| |
| GST_LOG ("sending seek event: %" GST_PTR_FORMAT, |
| gst_event_get_structure (event)); |
| |
| ret = gst_pad_send_event (GST_PAD_PEER (filter->sinkpad), event); |
| break; |
| } |
| default: |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static gboolean |
| gst_speed_convert (GstSpeed * filter, GstFormat src_format, gint64 src_value, |
| GstFormat * dest_format, gint64 * dest_value) |
| { |
| gboolean ret = TRUE; |
| guint scale = 1; |
| |
| if (src_format == *dest_format) { |
| *dest_value = src_value; |
| return TRUE; |
| } |
| |
| switch (src_format) { |
| case GST_FORMAT_BYTES: |
| switch (*dest_format) { |
| case GST_FORMAT_DEFAULT: |
| if (GST_AUDIO_INFO_BPF (&filter->info) == 0) { |
| ret = FALSE; |
| break; |
| } |
| *dest_value = src_value / GST_AUDIO_INFO_BPF (&filter->info); |
| break; |
| case GST_FORMAT_TIME: |
| { |
| gint byterate = |
| GST_AUDIO_INFO_BPF (&filter->info) * |
| GST_AUDIO_INFO_RATE (&filter->info); |
| |
| if (byterate == 0) { |
| ret = FALSE; |
| break; |
| } |
| *dest_value = src_value * GST_SECOND / byterate; |
| break; |
| } |
| default: |
| ret = FALSE; |
| } |
| break; |
| case GST_FORMAT_DEFAULT: |
| switch (*dest_format) { |
| case GST_FORMAT_BYTES: |
| *dest_value = src_value * GST_AUDIO_INFO_BPF (&filter->info); |
| break; |
| case GST_FORMAT_TIME: |
| if (GST_AUDIO_INFO_RATE (&filter->info) == 0) { |
| ret = FALSE; |
| break; |
| } |
| *dest_value = |
| src_value * GST_SECOND / GST_AUDIO_INFO_RATE (&filter->info); |
| break; |
| default: |
| ret = FALSE; |
| } |
| break; |
| case GST_FORMAT_TIME: |
| switch (*dest_format) { |
| case GST_FORMAT_BYTES: |
| scale = GST_AUDIO_INFO_BPF (&filter->info); |
| /* fallthrough */ |
| case GST_FORMAT_DEFAULT: |
| *dest_value = |
| src_value * scale * GST_AUDIO_INFO_RATE (&filter->info) / |
| GST_SECOND; |
| break; |
| default: |
| ret = FALSE; |
| } |
| break; |
| default: |
| ret = FALSE; |
| } |
| |
| return ret; |
| |
| } |
| |
| static gboolean |
| speed_src_query (GstPad * pad, GstObject * parent, GstQuery * query) |
| { |
| gboolean ret = TRUE; |
| GstSpeed *filter; |
| |
| filter = GST_SPEED (parent); |
| |
| switch (GST_QUERY_TYPE (query)) { |
| case GST_QUERY_POSITION: |
| { |
| GstFormat format; |
| GstFormat rformat = GST_FORMAT_TIME; |
| gint64 cur; |
| GstFormat conv_format = GST_FORMAT_TIME; |
| |
| /* save requested format */ |
| gst_query_parse_position (query, &format, NULL); |
| |
| /* query peer for current position in time */ |
| gst_query_set_position (query, GST_FORMAT_TIME, -1); |
| |
| if (!gst_pad_peer_query_position (filter->sinkpad, rformat, &cur)) { |
| GST_LOG_OBJECT (filter, "TIME query on peer pad failed, trying BYTES"); |
| rformat = GST_FORMAT_BYTES; |
| if (!gst_pad_peer_query_position (filter->sinkpad, rformat, &cur)) { |
| GST_LOG_OBJECT (filter, "BYTES query on peer pad failed too"); |
| goto error; |
| } |
| } |
| |
| if (rformat == GST_FORMAT_BYTES) |
| GST_LOG_OBJECT (filter, |
| "peer pad returned current=%" G_GINT64_FORMAT " bytes", cur); |
| else if (rformat == GST_FORMAT_TIME) |
| GST_LOG_OBJECT (filter, "peer pad returned time=%" G_GINT64_FORMAT, |
| cur); |
| |
| /* convert to time format */ |
| if (!gst_speed_convert (filter, rformat, cur, &conv_format, &cur)) { |
| ret = FALSE; |
| break; |
| } |
| |
| /* adjust for speed factor */ |
| cur /= filter->speed; |
| |
| /* convert to time format */ |
| if (!gst_speed_convert (filter, conv_format, cur, &format, &cur)) { |
| ret = FALSE; |
| break; |
| } |
| gst_query_set_position (query, format, cur); |
| |
| GST_LOG_OBJECT (filter, |
| "position query: we return %" G_GUINT64_FORMAT " (format %u)", cur, |
| format); |
| |
| break; |
| } |
| case GST_QUERY_DURATION: |
| { |
| GstFormat format; |
| GstFormat rformat = GST_FORMAT_TIME; |
| gint64 end; |
| GstFormat conv_format = GST_FORMAT_TIME; |
| |
| /* save requested format */ |
| gst_query_parse_duration (query, &format, NULL); |
| |
| /* query peer for total length in time */ |
| gst_query_set_duration (query, GST_FORMAT_TIME, -1); |
| |
| if (!gst_pad_peer_query_duration (filter->sinkpad, rformat, &end)) { |
| GST_LOG_OBJECT (filter, "TIME query on peer pad failed, trying BYTES"); |
| rformat = GST_FORMAT_BYTES; |
| if (!gst_pad_peer_query_duration (filter->sinkpad, rformat, &end)) { |
| GST_LOG_OBJECT (filter, "BYTES query on peer pad failed too"); |
| goto error; |
| } |
| } |
| |
| if (rformat == GST_FORMAT_BYTES) |
| GST_LOG_OBJECT (filter, |
| "peer pad returned total=%" G_GINT64_FORMAT " bytes", end); |
| else if (rformat == GST_FORMAT_TIME) |
| GST_LOG_OBJECT (filter, "peer pad returned time=%" G_GINT64_FORMAT, |
| end); |
| |
| /* convert to time format */ |
| if (!gst_speed_convert (filter, rformat, end, &conv_format, &end)) { |
| ret = FALSE; |
| break; |
| } |
| |
| /* adjust for speed factor */ |
| end /= filter->speed; |
| |
| /* convert to time format */ |
| if (!gst_speed_convert (filter, conv_format, end, &format, &end)) { |
| ret = FALSE; |
| break; |
| } |
| |
| gst_query_set_duration (query, format, end); |
| |
| GST_LOG_OBJECT (filter, |
| "duration query: we return %" G_GUINT64_FORMAT " (format %u)", end, |
| format); |
| |
| break; |
| } |
| default: |
| ret = FALSE; |
| break; |
| } |
| |
| return ret; |
| |
| error: |
| |
| gst_object_unref (filter); |
| GST_DEBUG ("error handling query"); |
| return FALSE; |
| } |
| |
| static void |
| gst_speed_class_init (GstSpeedClass * klass) |
| { |
| GObjectClass *gobject_class = (GObjectClass *) klass; |
| GstElementClass *gstelement_class = (GstElementClass *) klass; |
| |
| gobject_class->set_property = speed_set_property; |
| gobject_class->get_property = speed_get_property; |
| gstelement_class->change_state = speed_change_state; |
| |
| g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_SPEED, |
| g_param_spec_float ("speed", "speed", "speed", |
| 0.1, 40.0, 1.0, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); |
| |
| gst_element_class_set_static_metadata (gstelement_class, "Speed", |
| "Filter/Effect/Audio", |
| "Set speed/pitch on audio/raw streams (resampler)", |
| "Andy Wingo <apwingo@eos.ncsu.edu>, " |
| "Tim-Philipp Müller <tim@centricular.net>"); |
| |
| gst_element_class_add_pad_template (gstelement_class, |
| gst_static_pad_template_get (&gst_speed_src_template)); |
| gst_element_class_add_pad_template (gstelement_class, |
| gst_static_pad_template_get (&gst_speed_sink_template)); |
| } |
| |
| static void |
| gst_speed_init (GstSpeed * filter) |
| { |
| filter->sinkpad = |
| gst_pad_new_from_static_template (&gst_speed_sink_template, "sink"); |
| gst_pad_set_chain_function (filter->sinkpad, speed_chain); |
| gst_element_add_pad (GST_ELEMENT (filter), filter->sinkpad); |
| gst_pad_set_event_function (filter->sinkpad, speed_sink_event); |
| GST_PAD_SET_PROXY_CAPS (filter->sinkpad); |
| |
| filter->srcpad = |
| gst_pad_new_from_static_template (&gst_speed_src_template, "src"); |
| gst_pad_set_query_function (filter->srcpad, speed_src_query); |
| gst_element_add_pad (GST_ELEMENT (filter), filter->srcpad); |
| gst_pad_set_event_function (filter->srcpad, speed_src_event); |
| GST_PAD_SET_PROXY_CAPS (filter->srcpad); |
| |
| filter->offset = 0; |
| filter->timestamp = 0; |
| } |
| |
| static inline guint |
| speed_chain_int16 (GstSpeed * filter, GstBuffer * in_buf, GstBuffer * out_buf, |
| guint c, guint in_samples) |
| { |
| gint16 *in_data, *out_data; |
| gfloat interp, lower, i_float; |
| guint i, j; |
| GstMapInfo in_info, out_info; |
| |
| gst_buffer_map (in_buf, &in_info, GST_MAP_READ); |
| gst_buffer_map (out_buf, &out_info, GST_MAP_WRITE); |
| |
| in_data = (gint16 *) in_info.data + c; |
| out_data = (gint16 *) out_info.data + c; |
| |
| lower = in_data[0]; |
| i_float = 0.5 * (filter->speed - 1.0); |
| i = (guint) ceil (i_float); |
| j = 0; |
| |
| while (i < in_samples) { |
| interp = i_float - floor (i_float); |
| |
| out_data[j * GST_AUDIO_INFO_CHANNELS (&filter->info)] = |
| lower * (1 - interp) + |
| in_data[i * GST_AUDIO_INFO_CHANNELS (&filter->info)] * interp; |
| |
| lower = in_data[i * GST_AUDIO_INFO_CHANNELS (&filter->info)]; |
| |
| i_float += filter->speed; |
| i = (guint) ceil (i_float); |
| |
| ++j; |
| } |
| |
| gst_buffer_unmap (in_buf, &in_info); |
| gst_buffer_unmap (out_buf, &out_info); |
| return j; |
| } |
| |
| static inline guint |
| speed_chain_float32 (GstSpeed * filter, GstBuffer * in_buf, GstBuffer * out_buf, |
| guint c, guint in_samples) |
| { |
| gfloat *in_data, *out_data; |
| gfloat interp, lower, i_float; |
| guint i, j; |
| GstMapInfo in_info, out_info; |
| |
| gst_buffer_map (in_buf, &in_info, GST_MAP_WRITE); |
| gst_buffer_map (out_buf, &out_info, GST_MAP_WRITE); |
| |
| in_data = (gfloat *) in_info.data + c; |
| out_data = (gfloat *) out_info.data + c; |
| |
| lower = in_data[0]; |
| i_float = 0.5 * (filter->speed - 1.0); |
| i = (guint) ceil (i_float); |
| j = 0; |
| |
| while (i < in_samples) { |
| interp = i_float - floor (i_float); |
| |
| out_data[j * GST_AUDIO_INFO_CHANNELS (&filter->info)] = |
| lower * (1 - interp) + |
| in_data[i * GST_AUDIO_INFO_CHANNELS (&filter->info)] * interp; |
| |
| lower = in_data[i * GST_AUDIO_INFO_CHANNELS (&filter->info)]; |
| |
| i_float += filter->speed; |
| i = (guint) ceil (i_float); |
| |
| ++j; |
| } |
| gst_buffer_unmap (in_buf, &in_info); |
| gst_buffer_unmap (out_buf, &out_info); |
| return j; |
| } |
| |
| |
| static gboolean |
| speed_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| GstSpeed *filter = GST_SPEED (parent); |
| gboolean ret = FALSE; |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_SEGMENT:{ |
| gdouble rate; |
| GstFormat format; |
| gint64 start_value, stop_value, base; |
| const GstSegment *segment; |
| GstSegment seg; |
| |
| gst_event_parse_segment (event, &segment); |
| |
| rate = segment->rate; |
| format = segment->format; |
| start_value = segment->start; |
| stop_value = segment->stop; |
| base = segment->base; |
| |
| gst_event_unref (event); |
| |
| if (format != GST_FORMAT_TIME) { |
| GST_WARNING_OBJECT (filter, "newsegment event not in TIME format!"); |
| break; |
| } |
| |
| g_assert (filter->speed > 0); |
| |
| if (start_value >= 0) |
| start_value /= filter->speed; |
| if (stop_value >= 0) |
| stop_value /= filter->speed; |
| base /= filter->speed; |
| |
| /* this would only really be correct if we clipped incoming data */ |
| filter->timestamp = start_value; |
| |
| /* set to NONE so it gets reset later based on the timestamp when we have |
| * the samplerate */ |
| filter->offset = GST_BUFFER_OFFSET_NONE; |
| |
| gst_segment_init (&seg, GST_FORMAT_TIME); |
| seg.rate = rate; |
| seg.start = start_value; |
| seg.stop = stop_value; |
| seg.time = segment->time; |
| ret = gst_pad_push_event (filter->srcpad, gst_event_new_segment (&seg)); |
| |
| break; |
| } |
| case GST_EVENT_CAPS: |
| { |
| GstCaps *caps; |
| |
| gst_event_parse_caps (event, &caps); |
| ret = speed_setcaps (pad, caps); |
| if (!ret) { |
| gst_event_unref (event); |
| return ret; |
| } |
| } |
| /* Fallthrough so that the caps event gets forwarded */ |
| default: |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| return ret; |
| } |
| |
| static GstFlowReturn |
| speed_chain (GstPad * pad, GstObject * parent, GstBuffer * in_buf) |
| { |
| GstBuffer *out_buf; |
| GstSpeed *filter = GST_SPEED (parent); |
| guint c, in_samples, out_samples, out_size; |
| GstFlowReturn flow; |
| gsize size; |
| |
| if (G_UNLIKELY (filter->offset == GST_BUFFER_OFFSET_NONE)) { |
| filter->offset = gst_util_uint64_scale_int (filter->timestamp, |
| GST_AUDIO_INFO_RATE (&filter->info), GST_SECOND); |
| } |
| |
| /* buffersize has to be aligned to a frame */ |
| out_size = ceil ((gfloat) gst_buffer_get_size (in_buf) / filter->speed); |
| out_size = ((out_size + GST_AUDIO_INFO_BPF (&filter->info) - 1) / |
| GST_AUDIO_INFO_BPF (&filter->info)) * GST_AUDIO_INFO_BPF (&filter->info); |
| |
| out_buf = gst_buffer_new_and_alloc (out_size); |
| |
| in_samples = gst_buffer_get_size (in_buf) / |
| GST_AUDIO_INFO_BPF (&filter->info); |
| |
| out_samples = 0; |
| |
| for (c = 0; c < GST_AUDIO_INFO_CHANNELS (&filter->info); ++c) { |
| if (GST_AUDIO_INFO_IS_INTEGER (&filter->info)) |
| out_samples = speed_chain_int16 (filter, in_buf, out_buf, c, in_samples); |
| else |
| out_samples = |
| speed_chain_float32 (filter, in_buf, out_buf, c, in_samples); |
| } |
| |
| size = out_samples * GST_AUDIO_INFO_BPF (&filter->info); |
| gst_buffer_set_size (out_buf, size); |
| |
| GST_BUFFER_OFFSET (out_buf) = filter->offset; |
| GST_BUFFER_TIMESTAMP (out_buf) = filter->timestamp; |
| |
| filter->offset += size / GST_AUDIO_INFO_BPF (&filter->info); |
| filter->timestamp = gst_util_uint64_scale_int (filter->offset, GST_SECOND, |
| GST_AUDIO_INFO_RATE (&filter->info)); |
| |
| /* make sure it's at least nominally a perfect stream */ |
| GST_BUFFER_DURATION (out_buf) = |
| filter->timestamp - GST_BUFFER_TIMESTAMP (out_buf); |
| flow = gst_pad_push (filter->srcpad, out_buf); |
| |
| if (G_UNLIKELY (flow != GST_FLOW_OK)) |
| GST_DEBUG_OBJECT (filter, "flow: %s", gst_flow_get_name (flow)); |
| |
| gst_buffer_unref (in_buf); |
| return flow; |
| } |
| |
| static void |
| speed_set_property (GObject * object, guint prop_id, const GValue * value, |
| GParamSpec * pspec) |
| { |
| GstSpeed *filter = GST_SPEED (object); |
| |
| switch (prop_id) { |
| case PROP_SPEED: |
| filter->speed = g_value_get_float (value); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| |
| } |
| |
| static void |
| speed_get_property (GObject * object, guint prop_id, GValue * value, |
| GParamSpec * pspec) |
| { |
| GstSpeed *filter = GST_SPEED (object); |
| |
| switch (prop_id) { |
| case PROP_SPEED: |
| g_value_set_float (value, filter->speed); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| |
| } |
| |
| static GstStateChangeReturn |
| speed_change_state (GstElement * element, GstStateChange transition) |
| { |
| GstSpeed *speed = GST_SPEED (element); |
| |
| switch (transition) { |
| case GST_STATE_CHANGE_READY_TO_PAUSED: |
| speed->offset = GST_BUFFER_OFFSET_NONE; |
| speed->timestamp = 0; |
| gst_audio_info_init (&speed->info); |
| break; |
| default: |
| break; |
| } |
| |
| return GST_ELEMENT_CLASS (gst_speed_parent_class)->change_state (element, |
| transition); |
| } |
| |
| static gboolean |
| plugin_init (GstPlugin * plugin) |
| { |
| GST_DEBUG_CATEGORY_INIT (speed_debug, "speed", 0, "speed element"); |
| |
| return gst_element_register (plugin, "speed", GST_RANK_NONE, GST_TYPE_SPEED); |
| } |
| |
| GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, |
| GST_VERSION_MINOR, |
| speed, |
| "Set speed/pitch on audio/raw streams (resampler)", |
| plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN) |