| /* GStreamer |
| * Copyright (C) 1999 Erik Walthinsen <omega@cse.ogi.edu> |
| * Copyright (C) 2003,2004 David A. Schleef <ds@schleef.org> |
| * Copyright (C) 2007-2008 Sebastian Dröge <sebastian.droege@collabora.co.uk> |
| * |
| * 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-audioresample |
| * @title: audioresample |
| * |
| * audioresample resamples raw audio buffers to different sample rates using |
| * a configurable windowing function to enhance quality. |
| * |
| * By default, the resampler uses a reduced sinc table, with cubic interpolation filling in |
| * the gaps. This ensures that the table does not become too big. However, the interpolation |
| * increases the CPU usage considerably. As an alternative, a full sinc table can be used. |
| * Doing so can drastically reduce CPU usage (4x faster with 44.1 -> 48 kHz conversions for |
| * example), at the cost of increased memory consumption, plus the sinc table takes longer |
| * to initialize when the element is created. A third mode exists, which uses the full table |
| * unless said table would become too large, in which case the interpolated one is used instead. |
| * |
| * ## Example launch line |
| * |[ |
| * gst-launch-1.0 -v uridecodebin uri=file:///path/to/audio.ogg ! audioconvert ! audioresample ! audio/x-raw, rate=8000 ! autoaudiosink |
| * ]| |
| * Decode an audio file and downsample it to 8Khz and play sound. |
| * To create the Ogg/Vorbis file refer to the documentation of vorbisenc. |
| * This assumes there is an audio sink that will accept/handle 8kHz audio. |
| * |
| */ |
| |
| /* TODO: |
| * - Enable SSE/ARM optimizations and select at runtime |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include <string.h> |
| #include <math.h> |
| |
| #include "gstaudioresample.h" |
| #include <gst/gstutils.h> |
| #include <gst/audio/audio.h> |
| #include <gst/base/gstbasetransform.h> |
| |
| GST_DEBUG_CATEGORY (audio_resample_debug); |
| #define GST_CAT_DEFAULT audio_resample_debug |
| #if !defined(AUDIORESAMPLE_FORMAT_AUTO) |
| GST_DEBUG_CATEGORY_STATIC (GST_CAT_PERFORMANCE); |
| #endif |
| |
| #undef USE_SPEEX |
| |
| #define DEFAULT_QUALITY GST_AUDIO_RESAMPLER_QUALITY_DEFAULT |
| #define DEFAULT_RESAMPLE_METHOD GST_AUDIO_RESAMPLER_METHOD_KAISER |
| #define DEFAULT_SINC_FILTER_MODE GST_AUDIO_RESAMPLER_FILTER_MODE_AUTO |
| #define DEFAULT_SINC_FILTER_AUTO_THRESHOLD (1*1048576) |
| #define DEFAULT_SINC_FILTER_INTERPOLATION GST_AUDIO_RESAMPLER_FILTER_INTERPOLATION_CUBIC |
| |
| enum |
| { |
| PROP_0, |
| PROP_QUALITY, |
| PROP_RESAMPLE_METHOD, |
| PROP_SINC_FILTER_MODE, |
| PROP_SINC_FILTER_AUTO_THRESHOLD, |
| PROP_SINC_FILTER_INTERPOLATION |
| }; |
| |
| #define SUPPORTED_CAPS \ |
| GST_AUDIO_CAPS_MAKE (GST_AUDIO_FORMATS_ALL) \ |
| ", layout = (string) { interleaved, non-interleaved }" |
| |
| static GstStaticPadTemplate gst_audio_resample_sink_template = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS (SUPPORTED_CAPS)); |
| |
| static GstStaticPadTemplate gst_audio_resample_src_template = |
| GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS (SUPPORTED_CAPS)); |
| |
| static void gst_audio_resample_set_property (GObject * object, |
| guint prop_id, const GValue * value, GParamSpec * pspec); |
| static void gst_audio_resample_get_property (GObject * object, |
| guint prop_id, GValue * value, GParamSpec * pspec); |
| |
| /* vmethods */ |
| static gboolean gst_audio_resample_get_unit_size (GstBaseTransform * base, |
| GstCaps * caps, gsize * size); |
| static GstCaps *gst_audio_resample_transform_caps (GstBaseTransform * base, |
| GstPadDirection direction, GstCaps * caps, GstCaps * filter); |
| static GstCaps *gst_audio_resample_fixate_caps (GstBaseTransform * base, |
| GstPadDirection direction, GstCaps * caps, GstCaps * othercaps); |
| static gboolean gst_audio_resample_transform_size (GstBaseTransform * trans, |
| GstPadDirection direction, GstCaps * incaps, gsize insize, |
| GstCaps * outcaps, gsize * outsize); |
| static gboolean gst_audio_resample_set_caps (GstBaseTransform * base, |
| GstCaps * incaps, GstCaps * outcaps); |
| static GstFlowReturn gst_audio_resample_transform (GstBaseTransform * base, |
| GstBuffer * inbuf, GstBuffer * outbuf); |
| static gboolean gst_audio_resample_transform_meta (GstBaseTransform * trans, |
| GstBuffer * outbuf, GstMeta * meta, GstBuffer * inbuf); |
| static GstFlowReturn gst_audio_resample_submit_input_buffer (GstBaseTransform * |
| base, gboolean is_discont, GstBuffer * input); |
| static gboolean gst_audio_resample_sink_event (GstBaseTransform * base, |
| GstEvent * event); |
| static gboolean gst_audio_resample_start (GstBaseTransform * base); |
| static gboolean gst_audio_resample_stop (GstBaseTransform * base); |
| static gboolean gst_audio_resample_query (GstPad * pad, GstObject * parent, |
| GstQuery * query); |
| |
| #define gst_audio_resample_parent_class parent_class |
| G_DEFINE_TYPE (GstAudioResample, gst_audio_resample, GST_TYPE_BASE_TRANSFORM); |
| |
| static void |
| gst_audio_resample_class_init (GstAudioResampleClass * klass) |
| { |
| GObjectClass *gobject_class = (GObjectClass *) klass; |
| GstElementClass *gstelement_class = (GstElementClass *) klass; |
| |
| gobject_class->set_property = gst_audio_resample_set_property; |
| gobject_class->get_property = gst_audio_resample_get_property; |
| |
| g_object_class_install_property (gobject_class, PROP_QUALITY, |
| g_param_spec_int ("quality", "Quality", "Resample quality with 0 being " |
| "the lowest and 10 being the best", |
| GST_AUDIO_RESAMPLER_QUALITY_MIN, GST_AUDIO_RESAMPLER_QUALITY_MAX, |
| DEFAULT_QUALITY, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); |
| |
| g_object_class_install_property (gobject_class, PROP_RESAMPLE_METHOD, |
| g_param_spec_enum ("resample-method", "Resample method to use", |
| "What resample method to use", |
| GST_TYPE_AUDIO_RESAMPLER_METHOD, |
| DEFAULT_RESAMPLE_METHOD, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| g_object_class_install_property (gobject_class, PROP_SINC_FILTER_MODE, |
| g_param_spec_enum ("sinc-filter-mode", "Sinc filter table mode", |
| "What sinc filter table mode to use", |
| GST_TYPE_AUDIO_RESAMPLER_FILTER_MODE, |
| DEFAULT_SINC_FILTER_MODE, |
| G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| |
| g_object_class_install_property (gobject_class, |
| PROP_SINC_FILTER_AUTO_THRESHOLD, |
| g_param_spec_uint ("sinc-filter-auto-threshold", |
| "Sinc filter auto mode threshold", |
| "Memory usage threshold to use if sinc filter mode is AUTO, given in bytes", |
| 0, G_MAXUINT, DEFAULT_SINC_FILTER_AUTO_THRESHOLD, |
| G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| g_object_class_install_property (gobject_class, |
| PROP_SINC_FILTER_INTERPOLATION, |
| g_param_spec_enum ("sinc-filter-interpolation", |
| "Sinc filter interpolation", |
| "How to interpolate the sinc filter table", |
| GST_TYPE_AUDIO_RESAMPLER_FILTER_INTERPOLATION, |
| DEFAULT_SINC_FILTER_INTERPOLATION, |
| G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
| |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &gst_audio_resample_src_template); |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &gst_audio_resample_sink_template); |
| |
| gst_element_class_set_static_metadata (gstelement_class, "Audio resampler", |
| "Filter/Converter/Audio", "Resamples audio", |
| "Sebastian Dröge <sebastian.droege@collabora.co.uk>"); |
| |
| GST_BASE_TRANSFORM_CLASS (klass)->start = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_start); |
| GST_BASE_TRANSFORM_CLASS (klass)->stop = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_stop); |
| GST_BASE_TRANSFORM_CLASS (klass)->transform_size = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_transform_size); |
| GST_BASE_TRANSFORM_CLASS (klass)->get_unit_size = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_get_unit_size); |
| GST_BASE_TRANSFORM_CLASS (klass)->transform_caps = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_transform_caps); |
| GST_BASE_TRANSFORM_CLASS (klass)->fixate_caps = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_fixate_caps); |
| GST_BASE_TRANSFORM_CLASS (klass)->set_caps = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_set_caps); |
| GST_BASE_TRANSFORM_CLASS (klass)->transform = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_transform); |
| GST_BASE_TRANSFORM_CLASS (klass)->sink_event = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_sink_event); |
| GST_BASE_TRANSFORM_CLASS (klass)->transform_meta = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_transform_meta); |
| GST_BASE_TRANSFORM_CLASS (klass)->submit_input_buffer = |
| GST_DEBUG_FUNCPTR (gst_audio_resample_submit_input_buffer); |
| |
| GST_BASE_TRANSFORM_CLASS (klass)->passthrough_on_same_caps = TRUE; |
| } |
| |
| static void |
| gst_audio_resample_init (GstAudioResample * resample) |
| { |
| GstBaseTransform *trans = GST_BASE_TRANSFORM (resample); |
| |
| resample->method = DEFAULT_RESAMPLE_METHOD; |
| resample->quality = DEFAULT_QUALITY; |
| resample->sinc_filter_mode = DEFAULT_SINC_FILTER_MODE; |
| resample->sinc_filter_auto_threshold = DEFAULT_SINC_FILTER_AUTO_THRESHOLD; |
| resample->sinc_filter_interpolation = DEFAULT_SINC_FILTER_INTERPOLATION; |
| |
| gst_base_transform_set_gap_aware (trans, TRUE); |
| gst_pad_set_query_function (trans->srcpad, gst_audio_resample_query); |
| } |
| |
| /* vmethods */ |
| static gboolean |
| gst_audio_resample_start (GstBaseTransform * base) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| |
| resample->need_discont = TRUE; |
| |
| resample->num_gap_samples = 0; |
| resample->num_nongap_samples = 0; |
| resample->t0 = GST_CLOCK_TIME_NONE; |
| resample->in_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->out_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->samples_in = 0; |
| resample->samples_out = 0; |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| gst_audio_resample_stop (GstBaseTransform * base) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| |
| if (resample->converter) { |
| gst_audio_converter_free (resample->converter); |
| resample->converter = NULL; |
| } |
| return TRUE; |
| } |
| |
| static gboolean |
| gst_audio_resample_get_unit_size (GstBaseTransform * base, GstCaps * caps, |
| gsize * size) |
| { |
| GstAudioInfo info; |
| |
| if (!gst_audio_info_from_caps (&info, caps)) |
| goto invalid_caps; |
| |
| *size = GST_AUDIO_INFO_BPF (&info); |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| invalid_caps: |
| { |
| GST_ERROR_OBJECT (base, "invalid caps"); |
| return FALSE; |
| } |
| } |
| |
| static GstCaps * |
| gst_audio_resample_transform_caps (GstBaseTransform * base, |
| GstPadDirection direction, GstCaps * caps, GstCaps * filter) |
| { |
| const GValue *val; |
| GstStructure *s; |
| GstCaps *res; |
| gint i, n; |
| |
| /* transform single caps into input_caps + input_caps with the rate |
| * field set to our supported range. This ensures that upstream knows |
| * about downstream's prefered rate(s) and can negotiate accordingly. */ |
| res = gst_caps_new_empty (); |
| n = gst_caps_get_size (caps); |
| for (i = 0; i < n; i++) { |
| s = gst_caps_get_structure (caps, i); |
| |
| /* If this is already expressed by the existing caps |
| * skip this structure */ |
| if (i > 0 && gst_caps_is_subset_structure (res, s)) |
| continue; |
| |
| /* first, however, check if the caps contain a range for the rate field, in |
| * which case that side isn't going to care much about the exact sample rate |
| * chosen and we should just assume things will get fixated to something sane |
| * and we may just as well offer our full range instead of the range in the |
| * caps. If the rate is not an int range value, it's likely to express a |
| * real preference or limitation and we should maintain that structure as |
| * preference by putting it first into the transformed caps, and only add |
| * our full rate range as second option */ |
| s = gst_structure_copy (s); |
| val = gst_structure_get_value (s, "rate"); |
| if (val == NULL || GST_VALUE_HOLDS_INT_RANGE (val)) { |
| /* overwrite existing range, or add field if it doesn't exist yet */ |
| gst_structure_set (s, "rate", GST_TYPE_INT_RANGE, 1, G_MAXINT, NULL); |
| } else { |
| /* append caps with full range to existing caps with non-range rate field */ |
| gst_caps_append_structure (res, gst_structure_copy (s)); |
| gst_structure_set (s, "rate", GST_TYPE_INT_RANGE, 1, G_MAXINT, NULL); |
| } |
| gst_caps_append_structure (res, s); |
| } |
| |
| if (filter) { |
| GstCaps *intersection; |
| |
| intersection = |
| gst_caps_intersect_full (filter, res, GST_CAPS_INTERSECT_FIRST); |
| gst_caps_unref (res); |
| res = intersection; |
| } |
| |
| return res; |
| } |
| |
| /* Fixate rate to the allowed rate that has the smallest difference */ |
| static GstCaps * |
| gst_audio_resample_fixate_caps (GstBaseTransform * base, |
| GstPadDirection direction, GstCaps * caps, GstCaps * othercaps) |
| { |
| GstStructure *s; |
| gint rate; |
| |
| s = gst_caps_get_structure (caps, 0); |
| if (G_UNLIKELY (!gst_structure_get_int (s, "rate", &rate))) |
| return othercaps; |
| |
| othercaps = gst_caps_truncate (othercaps); |
| othercaps = gst_caps_make_writable (othercaps); |
| s = gst_caps_get_structure (othercaps, 0); |
| gst_structure_fixate_field_nearest_int (s, "rate", rate); |
| |
| return othercaps; |
| } |
| |
| static GstStructure * |
| make_options (GstAudioResample * resample, GstAudioInfo * in, |
| GstAudioInfo * out) |
| { |
| GstStructure *options; |
| |
| options = gst_structure_new_empty ("resampler-options"); |
| if (in != NULL && out != NULL) |
| gst_audio_resampler_options_set_quality (resample->method, |
| resample->quality, in->rate, out->rate, options); |
| |
| gst_structure_set (options, |
| GST_AUDIO_CONVERTER_OPT_RESAMPLER_METHOD, GST_TYPE_AUDIO_RESAMPLER_METHOD, |
| resample->method, |
| GST_AUDIO_RESAMPLER_OPT_FILTER_MODE, GST_TYPE_AUDIO_RESAMPLER_FILTER_MODE, |
| resample->sinc_filter_mode, GST_AUDIO_RESAMPLER_OPT_FILTER_MODE_THRESHOLD, |
| G_TYPE_UINT, resample->sinc_filter_auto_threshold, |
| GST_AUDIO_RESAMPLER_OPT_FILTER_INTERPOLATION, |
| GST_TYPE_AUDIO_RESAMPLER_FILTER_INTERPOLATION, |
| resample->sinc_filter_interpolation, NULL); |
| |
| return options; |
| } |
| |
| static gboolean |
| gst_audio_resample_update_state (GstAudioResample * resample, GstAudioInfo * in, |
| GstAudioInfo * out) |
| { |
| gboolean updated_latency = FALSE; |
| gsize old_latency = -1; |
| GstStructure *options; |
| |
| if (resample->converter == NULL && in == NULL && out == NULL) |
| return TRUE; |
| |
| options = make_options (resample, in, out); |
| |
| if (resample->converter) |
| old_latency = gst_audio_converter_get_max_latency (resample->converter); |
| |
| /* if channels and layout changed, destroy existing resampler */ |
| if (in != NULL && (in->finfo != resample->in.finfo || |
| in->channels != resample->in.channels || |
| in->layout != resample->in.layout) && resample->converter) { |
| gst_audio_converter_free (resample->converter); |
| resample->converter = NULL; |
| } |
| if (resample->converter == NULL) { |
| resample->converter = |
| gst_audio_converter_new (GST_AUDIO_CONVERTER_FLAG_VARIABLE_RATE, in, |
| out, options); |
| if (resample->converter == NULL) |
| goto resampler_failed; |
| } else if (in && out) { |
| gboolean ret; |
| |
| ret = |
| gst_audio_converter_update_config (resample->converter, in->rate, |
| out->rate, options); |
| if (!ret) |
| goto update_failed; |
| } else { |
| gst_structure_free (options); |
| } |
| if (old_latency != -1) |
| updated_latency = |
| old_latency != |
| gst_audio_converter_get_max_latency (resample->converter); |
| |
| if (updated_latency) |
| gst_element_post_message (GST_ELEMENT (resample), |
| gst_message_new_latency (GST_OBJECT (resample))); |
| |
| return TRUE; |
| |
| /* ERRORS */ |
| resampler_failed: |
| { |
| GST_ERROR_OBJECT (resample, "failed to create resampler"); |
| return FALSE; |
| } |
| update_failed: |
| { |
| GST_ERROR_OBJECT (resample, "failed to update resampler"); |
| return FALSE; |
| } |
| } |
| |
| static void |
| gst_audio_resample_reset_state (GstAudioResample * resample) |
| { |
| if (resample->converter) |
| gst_audio_converter_reset (resample->converter); |
| } |
| |
| static gboolean |
| gst_audio_resample_transform_size (GstBaseTransform * base, |
| GstPadDirection direction, GstCaps * caps, gsize size, GstCaps * othercaps, |
| gsize * othersize) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| gboolean ret = TRUE; |
| gint bpf; |
| |
| GST_LOG_OBJECT (base, "asked to transform size %" G_GSIZE_FORMAT |
| " in direction %s", size, direction == GST_PAD_SINK ? "SINK" : "SRC"); |
| |
| /* Number of samples in either buffer is size / (width*channels) -> |
| * calculate the factor */ |
| bpf = GST_AUDIO_INFO_BPF (&resample->in); |
| |
| /* Convert source buffer size to samples */ |
| size /= bpf; |
| |
| if (direction == GST_PAD_SINK) { |
| /* asked to convert size of an incoming buffer */ |
| *othersize = gst_audio_converter_get_out_frames (resample->converter, size); |
| *othersize *= bpf; |
| } else { |
| /* asked to convert size of an outgoing buffer */ |
| *othersize = gst_audio_converter_get_in_frames (resample->converter, size); |
| *othersize *= bpf; |
| } |
| |
| GST_LOG_OBJECT (base, |
| "transformed size %" G_GSIZE_FORMAT " to %" G_GSIZE_FORMAT, |
| size * bpf, *othersize); |
| |
| return ret; |
| } |
| |
| static gboolean |
| gst_audio_resample_set_caps (GstBaseTransform * base, GstCaps * incaps, |
| GstCaps * outcaps) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| GstAudioInfo in, out; |
| |
| GST_LOG ("incaps %" GST_PTR_FORMAT ", outcaps %" |
| GST_PTR_FORMAT, incaps, outcaps); |
| |
| if (!gst_audio_info_from_caps (&in, incaps)) |
| goto invalid_incaps; |
| if (!gst_audio_info_from_caps (&out, outcaps)) |
| goto invalid_outcaps; |
| |
| /* FIXME do some checks */ |
| gst_audio_resample_update_state (resample, &in, &out); |
| |
| resample->in = in; |
| resample->out = out; |
| |
| return TRUE; |
| |
| /* ERROR */ |
| invalid_incaps: |
| { |
| GST_ERROR_OBJECT (base, "invalid incaps"); |
| return FALSE; |
| } |
| invalid_outcaps: |
| { |
| GST_ERROR_OBJECT (base, "invalid outcaps"); |
| return FALSE; |
| } |
| } |
| |
| /* Push history_len zeros into the filter, but discard the output. */ |
| static void |
| gst_audio_resample_dump_drain (GstAudioResample * resample, guint history_len) |
| { |
| gsize out_len, outsize; |
| gpointer out[1]; |
| |
| out_len = |
| gst_audio_converter_get_out_frames (resample->converter, history_len); |
| if (out_len == 0) |
| return; |
| |
| outsize = out_len * resample->out.bpf; |
| |
| out[0] = g_malloc (outsize); |
| gst_audio_converter_samples (resample->converter, 0, NULL, history_len, |
| out, out_len); |
| g_free (out[0]); |
| } |
| |
| static void |
| gst_audio_resample_push_drain (GstAudioResample * resample, guint history_len) |
| { |
| GstBuffer *outbuf; |
| GstFlowReturn res; |
| gint outsize; |
| gsize out_len; |
| GstMapInfo map; |
| gpointer out[1]; |
| |
| g_assert (resample->converter != NULL); |
| |
| /* Don't drain samples if we were reset. */ |
| if (!GST_CLOCK_TIME_IS_VALID (resample->t0)) |
| return; |
| |
| out_len = |
| gst_audio_converter_get_out_frames (resample->converter, history_len); |
| if (out_len == 0) |
| return; |
| |
| outsize = out_len * resample->in.bpf; |
| outbuf = gst_buffer_new_and_alloc (outsize); |
| |
| gst_buffer_map (outbuf, &map, GST_MAP_WRITE); |
| |
| out[0] = map.data; |
| gst_audio_converter_samples (resample->converter, 0, NULL, history_len, |
| out, out_len); |
| |
| gst_buffer_unmap (outbuf, &map); |
| |
| /* time */ |
| if (GST_CLOCK_TIME_IS_VALID (resample->t0)) { |
| GST_BUFFER_TIMESTAMP (outbuf) = resample->t0 + |
| gst_util_uint64_scale_int_round (resample->samples_out, GST_SECOND, |
| resample->out.rate); |
| GST_BUFFER_DURATION (outbuf) = resample->t0 + |
| gst_util_uint64_scale_int_round (resample->samples_out + out_len, |
| GST_SECOND, resample->out.rate) - GST_BUFFER_TIMESTAMP (outbuf); |
| } else { |
| GST_BUFFER_TIMESTAMP (outbuf) = GST_CLOCK_TIME_NONE; |
| GST_BUFFER_DURATION (outbuf) = GST_CLOCK_TIME_NONE; |
| } |
| /* offset */ |
| if (resample->out_offset0 != GST_BUFFER_OFFSET_NONE) { |
| GST_BUFFER_OFFSET (outbuf) = resample->out_offset0 + resample->samples_out; |
| GST_BUFFER_OFFSET_END (outbuf) = GST_BUFFER_OFFSET (outbuf) + out_len; |
| } else { |
| GST_BUFFER_OFFSET (outbuf) = GST_BUFFER_OFFSET_NONE; |
| GST_BUFFER_OFFSET_END (outbuf) = GST_BUFFER_OFFSET_NONE; |
| } |
| /* move along */ |
| resample->samples_out += out_len; |
| resample->samples_in += history_len; |
| |
| GST_LOG_OBJECT (resample, |
| "Pushing drain buffer of %u bytes with timestamp %" GST_TIME_FORMAT |
| " duration %" GST_TIME_FORMAT " offset %" G_GUINT64_FORMAT " offset_end %" |
| G_GUINT64_FORMAT, outsize, |
| GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (outbuf)), |
| GST_TIME_ARGS (GST_BUFFER_DURATION (outbuf)), GST_BUFFER_OFFSET (outbuf), |
| GST_BUFFER_OFFSET_END (outbuf)); |
| |
| res = gst_pad_push (GST_BASE_TRANSFORM_SRC_PAD (resample), outbuf); |
| |
| if (G_UNLIKELY (res != GST_FLOW_OK)) |
| GST_WARNING_OBJECT (resample, "Failed to push drain: %s", |
| gst_flow_get_name (res)); |
| |
| return; |
| } |
| |
| static gboolean |
| gst_audio_resample_sink_event (GstBaseTransform * base, GstEvent * event) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_FLUSH_STOP: |
| gst_audio_resample_reset_state (resample); |
| resample->num_gap_samples = 0; |
| resample->num_nongap_samples = 0; |
| resample->t0 = GST_CLOCK_TIME_NONE; |
| resample->in_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->out_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->samples_in = 0; |
| resample->samples_out = 0; |
| resample->need_discont = TRUE; |
| break; |
| case GST_EVENT_SEGMENT: |
| if (resample->converter) { |
| gsize latency = |
| gst_audio_converter_get_max_latency (resample->converter); |
| gst_audio_resample_push_drain (resample, latency); |
| } |
| gst_audio_resample_reset_state (resample); |
| resample->num_gap_samples = 0; |
| resample->num_nongap_samples = 0; |
| resample->t0 = GST_CLOCK_TIME_NONE; |
| resample->in_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->out_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->samples_in = 0; |
| resample->samples_out = 0; |
| resample->need_discont = TRUE; |
| break; |
| case GST_EVENT_EOS: |
| if (resample->converter) { |
| gsize latency = |
| gst_audio_converter_get_max_latency (resample->converter); |
| gst_audio_resample_push_drain (resample, latency); |
| } |
| gst_audio_resample_reset_state (resample); |
| break; |
| default: |
| break; |
| } |
| |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->sink_event (base, event); |
| } |
| |
| static gboolean |
| gst_audio_resample_check_discont (GstAudioResample * resample, GstBuffer * buf) |
| { |
| guint64 offset; |
| guint64 delta; |
| |
| /* is the incoming buffer a discontinuity? */ |
| if (G_UNLIKELY (GST_BUFFER_IS_DISCONT (buf))) |
| return TRUE; |
| |
| /* no valid timestamps or offsets to compare --> no discontinuity */ |
| if (G_UNLIKELY (!(GST_BUFFER_TIMESTAMP_IS_VALID (buf) && |
| GST_CLOCK_TIME_IS_VALID (resample->t0)))) |
| return FALSE; |
| |
| /* convert the inbound timestamp to an offset. */ |
| offset = |
| gst_util_uint64_scale_int_round (GST_BUFFER_TIMESTAMP (buf) - |
| resample->t0, resample->in.rate, GST_SECOND); |
| |
| /* many elements generate imperfect streams due to rounding errors, so we |
| * permit a small error (up to one sample) without triggering a filter |
| * flush/restart (if triggered incorrectly, this will be audible) */ |
| /* allow even up to more samples, since sink is not so strict anyway, |
| * so give that one a chance to handle this as configured */ |
| delta = ABS ((gint64) (offset - resample->samples_in)); |
| if (delta <= (resample->in.rate >> 5)) |
| return FALSE; |
| |
| GST_WARNING_OBJECT (resample, |
| "encountered timestamp discontinuity of %" G_GUINT64_FORMAT " samples = %" |
| GST_TIME_FORMAT, delta, |
| GST_TIME_ARGS (gst_util_uint64_scale_int_round (delta, GST_SECOND, |
| resample->in.rate))); |
| return TRUE; |
| } |
| |
| static GstFlowReturn |
| gst_audio_resample_process (GstAudioResample * resample, GstBuffer * inbuf, |
| GstBuffer * outbuf) |
| { |
| GstMapInfo in_map, out_map; |
| gsize outsize; |
| guint32 in_len; |
| guint32 out_len; |
| guint filt_len = |
| gst_audio_converter_get_max_latency (resample->converter) * 2; |
| gboolean inbuf_writable; |
| |
| inbuf_writable = gst_buffer_is_writable (inbuf) |
| && gst_buffer_n_memory (inbuf) == 1 |
| && gst_memory_is_writable (gst_buffer_peek_memory (inbuf, 0)); |
| |
| gst_buffer_map (inbuf, &in_map, |
| inbuf_writable ? GST_MAP_READWRITE : GST_MAP_READ); |
| gst_buffer_map (outbuf, &out_map, GST_MAP_WRITE); |
| |
| in_len = in_map.size / resample->in.bpf; |
| out_len = out_map.size / resample->out.bpf; |
| |
| if (GST_BUFFER_FLAG_IS_SET (inbuf, GST_BUFFER_FLAG_GAP)) { |
| resample->num_nongap_samples = 0; |
| if (resample->num_gap_samples < filt_len) { |
| guint zeros_to_push; |
| if (in_len >= filt_len - resample->num_gap_samples) |
| zeros_to_push = filt_len - resample->num_gap_samples; |
| else |
| zeros_to_push = in_len; |
| |
| gst_audio_resample_push_drain (resample, zeros_to_push); |
| in_len -= zeros_to_push; |
| resample->num_gap_samples += zeros_to_push; |
| } |
| |
| { |
| guint num, den; |
| |
| num = resample->in.rate; |
| den = resample->out.rate; |
| |
| if (resample->samples_in + in_len >= filt_len / 2) |
| out_len = |
| gst_util_uint64_scale_int_ceil (resample->samples_in + in_len - |
| filt_len / 2, den, num) - resample->samples_out; |
| else |
| out_len = 0; |
| |
| memset (out_map.data, 0, out_map.size); |
| GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_GAP); |
| resample->num_gap_samples += in_len; |
| } |
| } else { /* not a gap */ |
| if (resample->num_gap_samples > filt_len) { |
| /* push in enough zeros to restore the filter to the right offset */ |
| guint num; |
| |
| num = resample->in.rate; |
| |
| gst_audio_resample_dump_drain (resample, |
| (resample->num_gap_samples - filt_len) % num); |
| } |
| resample->num_gap_samples = 0; |
| if (resample->num_nongap_samples < filt_len) { |
| resample->num_nongap_samples += in_len; |
| if (resample->num_nongap_samples > filt_len) |
| resample->num_nongap_samples = filt_len; |
| } |
| { |
| /* process */ |
| gpointer in[1], out[1]; |
| GstAudioConverterFlags flags; |
| |
| flags = 0; |
| if (inbuf_writable) |
| flags |= GST_AUDIO_CONVERTER_FLAG_IN_WRITABLE; |
| |
| in[0] = in_map.data; |
| out[0] = out_map.data; |
| gst_audio_converter_samples (resample->converter, flags, in, in_len, |
| out, out_len); |
| } |
| } |
| |
| /* time */ |
| if (GST_CLOCK_TIME_IS_VALID (resample->t0)) { |
| GST_BUFFER_TIMESTAMP (outbuf) = resample->t0 + |
| gst_util_uint64_scale_int_round (resample->samples_out, GST_SECOND, |
| resample->out.rate); |
| GST_BUFFER_DURATION (outbuf) = resample->t0 + |
| gst_util_uint64_scale_int_round (resample->samples_out + out_len, |
| GST_SECOND, resample->out.rate) - GST_BUFFER_TIMESTAMP (outbuf); |
| } else { |
| GST_BUFFER_TIMESTAMP (outbuf) = GST_CLOCK_TIME_NONE; |
| GST_BUFFER_DURATION (outbuf) = GST_CLOCK_TIME_NONE; |
| } |
| /* offset */ |
| if (resample->out_offset0 != GST_BUFFER_OFFSET_NONE) { |
| GST_BUFFER_OFFSET (outbuf) = resample->out_offset0 + resample->samples_out; |
| GST_BUFFER_OFFSET_END (outbuf) = GST_BUFFER_OFFSET (outbuf) + out_len; |
| } else { |
| GST_BUFFER_OFFSET (outbuf) = GST_BUFFER_OFFSET_NONE; |
| GST_BUFFER_OFFSET_END (outbuf) = GST_BUFFER_OFFSET_NONE; |
| } |
| /* move along */ |
| resample->samples_out += out_len; |
| resample->samples_in += in_len; |
| |
| gst_buffer_unmap (inbuf, &in_map); |
| gst_buffer_unmap (outbuf, &out_map); |
| |
| outsize = out_len * resample->in.bpf; |
| |
| GST_LOG_OBJECT (resample, |
| "Converted to buffer of %" G_GUINT32_FORMAT |
| " samples (%" G_GSIZE_FORMAT " bytes) with timestamp %" GST_TIME_FORMAT |
| ", duration %" GST_TIME_FORMAT ", offset %" G_GUINT64_FORMAT |
| ", offset_end %" G_GUINT64_FORMAT, out_len, outsize, |
| GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (outbuf)), |
| GST_TIME_ARGS (GST_BUFFER_DURATION (outbuf)), |
| GST_BUFFER_OFFSET (outbuf), GST_BUFFER_OFFSET_END (outbuf)); |
| |
| if (outsize == 0) |
| return GST_BASE_TRANSFORM_FLOW_DROPPED; |
| else |
| return GST_FLOW_OK; |
| } |
| |
| static GstFlowReturn |
| gst_audio_resample_transform (GstBaseTransform * base, GstBuffer * inbuf, |
| GstBuffer * outbuf) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| GstFlowReturn ret; |
| |
| GST_LOG_OBJECT (resample, "transforming buffer of %" G_GSIZE_FORMAT " bytes," |
| " ts %" GST_TIME_FORMAT ", duration %" GST_TIME_FORMAT ", offset %" |
| G_GINT64_FORMAT ", offset_end %" G_GINT64_FORMAT, |
| gst_buffer_get_size (inbuf), GST_TIME_ARGS (GST_BUFFER_TIMESTAMP (inbuf)), |
| GST_TIME_ARGS (GST_BUFFER_DURATION (inbuf)), |
| GST_BUFFER_OFFSET (inbuf), GST_BUFFER_OFFSET_END (inbuf)); |
| |
| /* check for timestamp discontinuities; flush/reset if needed, and set |
| * flag to resync timestamp and offset counters and send event |
| * downstream */ |
| if (G_UNLIKELY (gst_audio_resample_check_discont (resample, inbuf))) { |
| gsize size; |
| gint bpf = GST_AUDIO_INFO_BPF (&resample->in); |
| |
| gst_audio_resample_reset_state (resample); |
| resample->need_discont = TRUE; |
| |
| /* need to recalculate the output size */ |
| size = gst_buffer_get_size (inbuf) / bpf; |
| size = gst_audio_converter_get_out_frames (resample->converter, size); |
| gst_buffer_set_size (outbuf, size * bpf); |
| } |
| |
| /* handle discontinuity */ |
| if (G_UNLIKELY (resample->need_discont)) { |
| resample->num_gap_samples = 0; |
| resample->num_nongap_samples = 0; |
| /* reset */ |
| resample->samples_in = 0; |
| resample->samples_out = 0; |
| GST_DEBUG_OBJECT (resample, "found discontinuity; resyncing"); |
| /* resync the timestamp and offset counters if possible */ |
| if (GST_BUFFER_TIMESTAMP_IS_VALID (inbuf)) { |
| resample->t0 = GST_BUFFER_TIMESTAMP (inbuf); |
| } else { |
| GST_DEBUG_OBJECT (resample, "... but new timestamp is invalid"); |
| resample->t0 = GST_CLOCK_TIME_NONE; |
| } |
| if (GST_BUFFER_OFFSET_IS_VALID (inbuf)) { |
| resample->in_offset0 = GST_BUFFER_OFFSET (inbuf); |
| resample->out_offset0 = |
| gst_util_uint64_scale_int_round (resample->in_offset0, |
| resample->out.rate, resample->in.rate); |
| } else { |
| GST_DEBUG_OBJECT (resample, "... but new offset is invalid"); |
| resample->in_offset0 = GST_BUFFER_OFFSET_NONE; |
| resample->out_offset0 = GST_BUFFER_OFFSET_NONE; |
| } |
| /* set DISCONT flag on output buffer */ |
| GST_DEBUG_OBJECT (resample, "marking this buffer with the DISCONT flag"); |
| GST_BUFFER_FLAG_SET (outbuf, GST_BUFFER_FLAG_DISCONT); |
| resample->need_discont = FALSE; |
| } |
| |
| ret = gst_audio_resample_process (resample, inbuf, outbuf); |
| if (G_UNLIKELY (ret != GST_FLOW_OK)) |
| return ret; |
| |
| GST_DEBUG_OBJECT (resample, "input = samples [%" G_GUINT64_FORMAT ", %" |
| G_GUINT64_FORMAT ") = [%" G_GUINT64_FORMAT ", %" G_GUINT64_FORMAT |
| ") ns; output = samples [%" G_GUINT64_FORMAT ", %" G_GUINT64_FORMAT |
| ") = [%" G_GUINT64_FORMAT ", %" G_GUINT64_FORMAT ") ns", |
| GST_BUFFER_OFFSET (inbuf), GST_BUFFER_OFFSET_END (inbuf), |
| GST_BUFFER_TIMESTAMP (inbuf), GST_BUFFER_TIMESTAMP (inbuf) + |
| GST_BUFFER_DURATION (inbuf), GST_BUFFER_OFFSET (outbuf), |
| GST_BUFFER_OFFSET_END (outbuf), GST_BUFFER_TIMESTAMP (outbuf), |
| GST_BUFFER_TIMESTAMP (outbuf) + GST_BUFFER_DURATION (outbuf)); |
| |
| return GST_FLOW_OK; |
| } |
| |
| static gboolean |
| gst_audio_resample_transform_meta (GstBaseTransform * trans, GstBuffer * outbuf, |
| GstMeta * meta, GstBuffer * inbuf) |
| { |
| const GstMetaInfo *info = meta->info; |
| const gchar *const *tags; |
| |
| tags = gst_meta_api_type_get_tags (info->api); |
| |
| if (!tags || (g_strv_length ((gchar **) tags) == 1 |
| && gst_meta_api_type_has_tag (info->api, |
| g_quark_from_string (GST_META_TAG_AUDIO_STR)))) |
| return TRUE; |
| |
| return FALSE; |
| } |
| |
| static GstFlowReturn |
| gst_audio_resample_submit_input_buffer (GstBaseTransform * base, |
| gboolean is_discont, GstBuffer * input) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (base); |
| |
| if (base->segment.format == GST_FORMAT_TIME) { |
| input = |
| gst_audio_buffer_clip (input, &base->segment, resample->in.rate, |
| resample->in.bpf); |
| |
| if (!input) |
| return GST_FLOW_OK; |
| } |
| |
| return GST_BASE_TRANSFORM_CLASS (parent_class)->submit_input_buffer (base, |
| is_discont, input); |
| } |
| |
| static gboolean |
| gst_audio_resample_query (GstPad * pad, GstObject * parent, GstQuery * query) |
| { |
| GstAudioResample *resample = GST_AUDIO_RESAMPLE (parent); |
| GstBaseTransform *trans; |
| gboolean res = TRUE; |
| |
| trans = GST_BASE_TRANSFORM (resample); |
| |
| switch (GST_QUERY_TYPE (query)) { |
| case GST_QUERY_LATENCY: |
| { |
| GstClockTime min, max; |
| gboolean live; |
| guint64 latency; |
| gint rate = resample->in.rate; |
| gint resampler_latency; |
| |
| if (resample->converter) |
| resampler_latency = |
| gst_audio_converter_get_max_latency (resample->converter); |
| else |
| resampler_latency = 0; |
| |
| if (gst_base_transform_is_passthrough (trans)) |
| resampler_latency = 0; |
| |
| if ((res = |
| gst_pad_peer_query (GST_BASE_TRANSFORM_SINK_PAD (trans), |
| query))) { |
| gst_query_parse_latency (query, &live, &min, &max); |
| |
| GST_DEBUG_OBJECT (resample, "Peer latency: min %" |
| GST_TIME_FORMAT " max %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (min), GST_TIME_ARGS (max)); |
| |
| /* add our own latency */ |
| if (rate != 0 && resampler_latency != 0) |
| latency = gst_util_uint64_scale_round (resampler_latency, |
| GST_SECOND, rate); |
| else |
| latency = 0; |
| |
| GST_DEBUG_OBJECT (resample, "Our latency: %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (latency)); |
| |
| min += latency; |
| if (GST_CLOCK_TIME_IS_VALID (max)) |
| max += latency; |
| |
| GST_DEBUG_OBJECT (resample, "Calculated total latency : min %" |
| GST_TIME_FORMAT " max %" GST_TIME_FORMAT, |
| GST_TIME_ARGS (min), GST_TIME_ARGS (max)); |
| |
| gst_query_set_latency (query, live, min, max); |
| } |
| break; |
| } |
| default: |
| res = gst_pad_query_default (pad, parent, query); |
| break; |
| } |
| return res; |
| } |
| |
| static void |
| gst_audio_resample_set_property (GObject * object, guint prop_id, |
| const GValue * value, GParamSpec * pspec) |
| { |
| GstAudioResample *resample; |
| |
| resample = GST_AUDIO_RESAMPLE (object); |
| |
| switch (prop_id) { |
| case PROP_QUALITY: |
| /* FIXME locking! */ |
| resample->quality = g_value_get_int (value); |
| GST_DEBUG_OBJECT (resample, "new quality %d", resample->quality); |
| gst_audio_resample_update_state (resample, NULL, NULL); |
| break; |
| case PROP_RESAMPLE_METHOD: |
| resample->method = g_value_get_enum (value); |
| gst_audio_resample_update_state (resample, NULL, NULL); |
| break; |
| case PROP_SINC_FILTER_MODE: |
| /* FIXME locking! */ |
| resample->sinc_filter_mode = g_value_get_enum (value); |
| gst_audio_resample_update_state (resample, NULL, NULL); |
| break; |
| case PROP_SINC_FILTER_AUTO_THRESHOLD: |
| /* FIXME locking! */ |
| resample->sinc_filter_auto_threshold = g_value_get_uint (value); |
| gst_audio_resample_update_state (resample, NULL, NULL); |
| break; |
| case PROP_SINC_FILTER_INTERPOLATION: |
| /* FIXME locking! */ |
| resample->sinc_filter_interpolation = g_value_get_enum (value); |
| gst_audio_resample_update_state (resample, NULL, NULL); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| gst_audio_resample_get_property (GObject * object, guint prop_id, |
| GValue * value, GParamSpec * pspec) |
| { |
| GstAudioResample *resample; |
| |
| resample = GST_AUDIO_RESAMPLE (object); |
| |
| switch (prop_id) { |
| case PROP_QUALITY: |
| g_value_set_int (value, resample->quality); |
| break; |
| case PROP_RESAMPLE_METHOD: |
| g_value_set_enum (value, resample->method); |
| break; |
| case PROP_SINC_FILTER_MODE: |
| g_value_set_enum (value, resample->sinc_filter_mode); |
| break; |
| case PROP_SINC_FILTER_AUTO_THRESHOLD: |
| g_value_set_uint (value, resample->sinc_filter_auto_threshold); |
| break; |
| case PROP_SINC_FILTER_INTERPOLATION: |
| g_value_set_enum (value, resample->sinc_filter_interpolation); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static gboolean |
| plugin_init (GstPlugin * plugin) |
| { |
| GST_DEBUG_CATEGORY_INIT (audio_resample_debug, "audioresample", 0, |
| "audio resampling element"); |
| |
| if (!gst_element_register (plugin, "audioresample", GST_RANK_PRIMARY, |
| GST_TYPE_AUDIO_RESAMPLE)) { |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, |
| GST_VERSION_MINOR, |
| audioresample, |
| "Resamples audio", plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, |
| GST_PACKAGE_ORIGIN); |