| /* |
| * Copyright (c) 2006 Christophe Fergeau <teuf@gnome.org> |
| * Copyright (c) 2008 Sebastian Dröge <slomo@circular-chaos.org> |
| * |
| * 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. |
| */ |
| |
| /* Xing SDK: http://www.mp3-tech.org/programmer/sources/vbrheadersdk.zip */ |
| |
| |
| /** |
| * SECTION:element-xingmux |
| * |
| * xingmux adds a Xing header to MP3 files. This contains information about the duration and size |
| * of the file and a seek table and is very useful for getting an almost correct duration and better |
| * seeking on VBR MP3 files. |
| * |
| * This element will remove any existing Xing, LAME or VBRI headers from the beginning of the file. |
| * |
| * <refsect2> |
| * <title>Example launch line</title> |
| * |[ |
| * gst-launch-1.0 audiotestsrc num-buffers=1000 ! audioconvert ! lamemp3enc ! xingmux ! filesink location=test.mp3 |
| * gst-launch-1.0 filesrc location=test.mp3 ! xingmux ! filesink location=test2.mp3 |
| * gst-launch-1.0 filesrc location=test.mp3 ! mp3parse ! xingmux ! filesink location=test2.mp3 |
| * ]| |
| * </refsect2> |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <string.h> |
| #include "gstxingmux.h" |
| |
| GST_DEBUG_CATEGORY_STATIC (xing_mux_debug); |
| #define GST_CAT_DEFAULT xing_mux_debug |
| |
| #define gst_xing_mux_parent_class parent_class |
| G_DEFINE_TYPE (GstXingMux, gst_xing_mux, GST_TYPE_ELEMENT); |
| |
| /* Xing Header stuff */ |
| #define GST_XING_FRAME_FIELD (1 << 0) |
| #define GST_XING_BYTES_FIELD (1 << 1) |
| #define GST_XING_TOC_FIELD (1 << 2) |
| #define GST_XING_QUALITY_FIELD (1 << 3) |
| |
| typedef struct _GstXingSeekEntry |
| { |
| gint64 timestamp; |
| gint byte; |
| } GstXingSeekEntry; |
| |
| static inline GstXingSeekEntry * |
| gst_xing_seek_entry_new (void) |
| { |
| return g_slice_new (GstXingSeekEntry); |
| } |
| |
| static inline void |
| gst_xing_seek_entry_free (GstXingSeekEntry * entry) |
| { |
| g_slice_free (GstXingSeekEntry, entry); |
| } |
| |
| static void gst_xing_mux_finalize (GObject * obj); |
| static GstStateChangeReturn |
| gst_xing_mux_change_state (GstElement * element, GstStateChange transition); |
| static GstFlowReturn gst_xing_mux_chain (GstPad * pad, GstObject * parent, |
| GstBuffer * buffer); |
| static gboolean gst_xing_mux_sink_event (GstPad * pad, GstObject * parent, |
| GstEvent * event); |
| |
| static GstStaticPadTemplate gst_xing_mux_sink_template = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/mpeg, " |
| "mpegversion = (int) 1, " "layer = (int) [ 1, 3 ]")); |
| |
| |
| static GstStaticPadTemplate gst_xing_mux_src_template = |
| GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/mpeg, " |
| "mpegversion = (int) 1, " "layer = (int) [ 1, 3 ]")); |
| static const guint mp3types_bitrates[2][3][16] = { |
| { |
| {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448,}, |
| {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,}, |
| {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,} |
| }, |
| { |
| {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256,}, |
| {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160,}, |
| {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160,} |
| }, |
| }; |
| |
| static const guint mp3types_freqs[3][3] = { {44100, 48000, 32000}, |
| {22050, 24000, 16000}, |
| {11025, 12000, 8000} |
| }; |
| |
| static gboolean |
| parse_header (guint32 header, guint * ret_size, guint * ret_spf, |
| gulong * ret_rate) |
| { |
| guint length, spf; |
| gulong samplerate, bitrate, layer, padding; |
| gint lsf, mpg25; |
| |
| if ((header & 0xffe00000) != 0xffe00000) { |
| g_warning ("invalid sync"); |
| return FALSE; |
| } |
| |
| if (((header >> 19) & 3) == 0x01) { |
| g_warning ("invalid MPEG version"); |
| return FALSE; |
| } |
| |
| if (((header >> 17) & 3) == 0x00) { |
| g_warning ("invalid MPEG layer"); |
| return FALSE; |
| } |
| |
| if (((header >> 12) & 0xf) == 0xf || ((header >> 12) & 0xf) == 0x0) { |
| g_warning ("invalid bitrate"); |
| return FALSE; |
| } |
| |
| if (((header >> 10) & 0x3) == 0x3) { |
| g_warning ("invalid sampling rate"); |
| return FALSE; |
| } |
| |
| if (header & 0x00000002) { |
| g_warning ("invalid emphasis"); |
| return FALSE; |
| } |
| |
| if (header & (1 << 20)) { |
| lsf = (header & (1 << 19)) ? 0 : 1; |
| mpg25 = 0; |
| } else { |
| lsf = 1; |
| mpg25 = 1; |
| } |
| |
| layer = 4 - ((header >> 17) & 0x3); |
| |
| bitrate = (header >> 12) & 0xF; |
| bitrate = mp3types_bitrates[lsf][layer - 1][bitrate] * 1000; |
| if (bitrate == 0) |
| return FALSE; |
| |
| samplerate = (header >> 10) & 0x3; |
| samplerate = mp3types_freqs[lsf + mpg25][samplerate]; |
| |
| padding = (header >> 9) & 0x1; |
| |
| switch (layer) { |
| case 1: |
| length = 4 * ((bitrate * 12) / samplerate + padding); |
| break; |
| case 2: |
| length = (bitrate * 144) / samplerate + padding; |
| break; |
| default: |
| case 3: |
| length = (bitrate * 144) / (samplerate << lsf) + padding; |
| break; |
| } |
| |
| if (layer == 1) |
| spf = 384; |
| else if (layer == 2 || lsf == 0) |
| spf = 1152; |
| else |
| spf = 576; |
| |
| if (ret_size) |
| *ret_size = length; |
| if (ret_spf) |
| *ret_spf = spf; |
| if (ret_rate) |
| *ret_rate = samplerate; |
| |
| return TRUE; |
| } |
| |
| static guint |
| get_xing_offset (guint32 header) |
| { |
| guint mpeg_version = (header >> 19) & 0x3; |
| guint channel_mode = (header >> 6) & 0x3; |
| |
| if (mpeg_version == 0x3) { |
| if (channel_mode == 0x3) { |
| return 0x11; |
| } else { |
| return 0x20; |
| } |
| } else { |
| if (channel_mode == 0x3) { |
| return 0x09; |
| } else { |
| return 0x11; |
| } |
| } |
| } |
| |
| static gboolean |
| has_xing_header (guint32 header, GstBuffer * buffer, gsize size) |
| { |
| gboolean ret; |
| GstMapInfo map; |
| guint8 *data; |
| |
| gst_buffer_map (buffer, &map, GST_MAP_READ); |
| data = map.data; |
| data += 4; |
| data += get_xing_offset (header); |
| |
| if (memcmp (data, "Xing", 4) == 0 || |
| memcmp (data, "Info", 4) == 0 || memcmp (data, "VBRI", 4) == 0) |
| ret = TRUE; |
| else |
| ret = FALSE; |
| |
| gst_buffer_unmap (buffer, &map); |
| return ret; |
| } |
| |
| static GstBuffer * |
| generate_xing_header (GstXingMux * xing) |
| { |
| guint8 *xing_flags; |
| guint32 xing_flags_tmp = 0; |
| GstBuffer *xing_header; |
| GstMapInfo map; |
| guchar *data; |
| |
| guint32 header; |
| guint32 header_be; |
| guint size, spf, xing_offset; |
| gulong rate; |
| guint bitrate = 0x00; |
| |
| gint64 duration; |
| gint64 byte_count; |
| |
| header = xing->first_header; |
| |
| /* Set bitrate and choose lowest possible size */ |
| do { |
| bitrate++; |
| |
| header &= 0xffff0fff; |
| header |= bitrate << 12; |
| |
| if (!parse_header (header, &size, &spf, &rate)) { |
| GST_ERROR ("Failed to parse header!"); |
| return NULL; |
| } |
| xing_offset = get_xing_offset (header); |
| } while (size < (4 + xing_offset + 4 + 4 + 4 + 4 + 100) && bitrate < 0xe); |
| |
| if (bitrate == 0xe) { |
| GST_ERROR ("No usable bitrate found!"); |
| return NULL; |
| } |
| |
| xing_header = gst_buffer_new_and_alloc (size); |
| |
| gst_buffer_map (xing_header, &map, GST_MAP_WRITE); |
| data = map.data; |
| memset (data, 0, size); |
| header_be = GUINT32_TO_BE (header); |
| memcpy (data, &header_be, 4); |
| |
| data += 4; |
| data += xing_offset; |
| |
| memcpy (data, "Xing", 4); |
| data += 4; |
| |
| xing_flags = data; |
| data += 4; |
| |
| if (xing->duration != GST_CLOCK_TIME_NONE) { |
| duration = xing->duration; |
| } else { |
| GstFormat fmt = GST_FORMAT_TIME; |
| |
| if (!gst_pad_peer_query_duration (xing->sinkpad, fmt, &duration)) |
| duration = GST_CLOCK_TIME_NONE; |
| } |
| |
| if (duration != GST_CLOCK_TIME_NONE) { |
| guint32 number_of_frames; |
| |
| /* The Xing Header contains a NumberOfFrames field, which verifies to: |
| * Duration = NumberOfFrames *SamplesPerFrame/SamplingRate |
| * SamplesPerFrame and SamplingRate are values for the current frame. |
| */ |
| number_of_frames = gst_util_uint64_scale (duration, rate, GST_SECOND) / spf; |
| number_of_frames += 1; /* Xing Header Frame */ |
| GST_DEBUG ("Setting number of frames to %u", number_of_frames); |
| number_of_frames = GUINT32_TO_BE (number_of_frames); |
| memcpy (data, &number_of_frames, 4); |
| xing_flags_tmp |= GST_XING_FRAME_FIELD; |
| data += 4; |
| } |
| |
| if (xing->byte_count != 0) { |
| byte_count = xing->byte_count; |
| } else { |
| GstFormat fmt = GST_FORMAT_BYTES; |
| |
| if (!gst_pad_peer_query_duration (xing->sinkpad, fmt, &byte_count)) |
| byte_count = 0; |
| if (byte_count == -1) |
| byte_count = 0; |
| } |
| |
| if (byte_count != 0) { |
| guint32 nbytes; |
| |
| if (byte_count > G_MAXUINT32) { |
| GST_DEBUG ("Too large stream: %" G_GINT64_FORMAT " > %u bytes", |
| byte_count, G_MAXUINT32); |
| } else { |
| nbytes = byte_count; |
| GST_DEBUG ("Setting number of bytes to %u", nbytes); |
| nbytes = GUINT32_TO_BE (nbytes); |
| memcpy (data, &nbytes, 4); |
| xing_flags_tmp |= GST_XING_BYTES_FIELD; |
| data += 4; |
| } |
| } |
| |
| if (xing->seek_table != NULL && byte_count != 0 |
| && duration != GST_CLOCK_TIME_NONE) { |
| GList *it; |
| gint percent = 0; |
| |
| xing_flags_tmp |= GST_XING_TOC_FIELD; |
| |
| GST_DEBUG ("Writing seek table"); |
| for (it = xing->seek_table; it != NULL && percent < 100; it = it->next) { |
| GstXingSeekEntry *entry = (GstXingSeekEntry *) it->data; |
| gint64 pos; |
| guchar byte; |
| |
| while ((entry->timestamp * 100) / duration >= percent) { |
| pos = (entry->byte * 256) / byte_count; |
| GST_DEBUG (" %d %% -- %" G_GINT64_FORMAT " 1/256", percent, pos); |
| byte = (guchar) pos; |
| memcpy (data, &byte, 1); |
| data++; |
| percent++; |
| } |
| } |
| |
| if (percent < 100) { |
| guchar b; |
| gint i; |
| |
| memcpy (&b, data - 1, 1); |
| |
| for (i = percent; i < 100; i++) { |
| GST_DEBUG (" %d %% -- %d 1/256", i, b); |
| memcpy (data, &b, 1); |
| data++; |
| } |
| } |
| } |
| |
| GST_DEBUG ("Setting Xing flags to 0x%x\n", xing_flags_tmp); |
| xing_flags_tmp = GUINT32_TO_BE (xing_flags_tmp); |
| memcpy (xing_flags, &xing_flags_tmp, 4); |
| gst_buffer_unmap (xing_header, &map); |
| return xing_header; |
| } |
| |
| static void |
| gst_xing_mux_class_init (GstXingMuxClass * klass) |
| { |
| GObjectClass *gobject_class; |
| GstElementClass *gstelement_class; |
| |
| gobject_class = (GObjectClass *) klass; |
| gstelement_class = (GstElementClass *) klass; |
| |
| gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_xing_mux_finalize); |
| gstelement_class->change_state = |
| GST_DEBUG_FUNCPTR (gst_xing_mux_change_state); |
| |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &gst_xing_mux_src_template); |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &gst_xing_mux_sink_template); |
| |
| GST_DEBUG_CATEGORY_INIT (xing_mux_debug, "xingmux", 0, "Xing Header Muxer"); |
| |
| gst_element_class_set_static_metadata (gstelement_class, "MP3 Xing muxer", |
| "Formatter/Muxer/Metadata", |
| "Adds a Xing header to the beginning of a VBR MP3 file", |
| "Christophe Fergeau <teuf@gnome.org>"); |
| } |
| |
| static void |
| gst_xing_mux_finalize (GObject * obj) |
| { |
| GstXingMux *xing = GST_XING_MUX (obj); |
| |
| if (xing->adapter) { |
| g_object_unref (xing->adapter); |
| xing->adapter = NULL; |
| } |
| |
| if (xing->seek_table) { |
| g_list_foreach (xing->seek_table, (GFunc) gst_xing_seek_entry_free, NULL); |
| g_list_free (xing->seek_table); |
| xing->seek_table = NULL; |
| } |
| |
| G_OBJECT_CLASS (parent_class)->finalize (obj); |
| } |
| |
| static void |
| xing_reset (GstXingMux * xing) |
| { |
| xing->duration = GST_CLOCK_TIME_NONE; |
| xing->byte_count = 0; |
| |
| gst_adapter_clear (xing->adapter); |
| |
| if (xing->seek_table) { |
| g_list_foreach (xing->seek_table, (GFunc) gst_xing_seek_entry_free, NULL); |
| g_list_free (xing->seek_table); |
| xing->seek_table = NULL; |
| } |
| |
| xing->sent_xing = FALSE; |
| } |
| |
| |
| static void |
| gst_xing_mux_init (GstXingMux * xing) |
| { |
| /* pad through which data comes in to the element */ |
| xing->sinkpad = |
| gst_pad_new_from_static_template (&gst_xing_mux_sink_template, "sink"); |
| gst_pad_set_chain_function (xing->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_xing_mux_chain)); |
| gst_pad_set_event_function (xing->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_xing_mux_sink_event)); |
| GST_PAD_SET_PROXY_CAPS (xing->sinkpad); |
| gst_element_add_pad (GST_ELEMENT (xing), xing->sinkpad); |
| |
| /* pad through which data goes out of the element */ |
| xing->srcpad = |
| gst_pad_new_from_static_template (&gst_xing_mux_src_template, "src"); |
| gst_element_add_pad (GST_ELEMENT (xing), xing->srcpad); |
| |
| xing->adapter = gst_adapter_new (); |
| |
| xing_reset (xing); |
| } |
| |
| static GstFlowReturn |
| gst_xing_mux_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) |
| { |
| GstXingMux *xing = GST_XING_MUX (parent); |
| GstFlowReturn ret = GST_FLOW_OK; |
| |
| gst_adapter_push (xing->adapter, buffer); |
| |
| while (gst_adapter_available (xing->adapter) >= 4) { |
| const guchar *data; |
| guint32 header; |
| GstBuffer *outbuf; |
| GstClockTime duration; |
| guint size, spf; |
| gulong rate; |
| GstXingSeekEntry *seek_entry; |
| |
| data = gst_adapter_map (xing->adapter, 4); |
| header = GST_READ_UINT32_BE (data); |
| gst_adapter_unmap (xing->adapter); |
| |
| if (!parse_header (header, &size, &spf, &rate)) { |
| GST_DEBUG ("Lost sync, resyncing"); |
| gst_adapter_flush (xing->adapter, 1); |
| continue; |
| } |
| |
| if (gst_adapter_available (xing->adapter) < size) |
| break; |
| |
| outbuf = gst_adapter_take_buffer (xing->adapter, size); |
| |
| if (!xing->sent_xing) { |
| if (has_xing_header (header, outbuf, size)) { |
| GST_LOG_OBJECT (xing, "Dropping old Xing header"); |
| gst_buffer_unref (outbuf); |
| continue; |
| } else { |
| GstBuffer *xing_header; |
| guint64 xing_header_size; |
| |
| xing->first_header = header; |
| |
| xing_header = generate_xing_header (xing); |
| |
| if (xing_header == NULL) { |
| GST_ERROR ("Can't generate Xing header"); |
| gst_buffer_unref (outbuf); |
| return GST_FLOW_ERROR; |
| } |
| |
| xing_header_size = gst_buffer_get_size (xing_header); |
| |
| if ((ret = gst_pad_push (xing->srcpad, xing_header)) != GST_FLOW_OK) { |
| GST_ERROR_OBJECT (xing, "Failed to push Xing header: %s", |
| gst_flow_get_name (ret)); |
| gst_buffer_unref (xing_header); |
| gst_buffer_unref (outbuf); |
| return ret; |
| } |
| |
| xing->byte_count += xing_header_size; |
| xing->sent_xing = TRUE; |
| } |
| } |
| |
| seek_entry = gst_xing_seek_entry_new (); |
| seek_entry->timestamp = |
| (xing->duration == GST_CLOCK_TIME_NONE) ? 0 : xing->duration; |
| /* Workaround for parsers checking that the first seek table entry is 0 */ |
| seek_entry->byte = (seek_entry->timestamp == 0) ? 0 : xing->byte_count; |
| xing->seek_table = g_list_append (xing->seek_table, seek_entry); |
| |
| duration = gst_util_uint64_scale_ceil (spf, GST_SECOND, rate); |
| |
| GST_BUFFER_TIMESTAMP (outbuf) = |
| (xing->duration == GST_CLOCK_TIME_NONE) ? 0 : xing->duration; |
| GST_BUFFER_DURATION (outbuf) = duration; |
| GST_BUFFER_OFFSET (outbuf) = xing->byte_count; |
| xing->byte_count += gst_buffer_get_size (outbuf); |
| GST_BUFFER_OFFSET_END (outbuf) = xing->byte_count; |
| |
| if (xing->duration == GST_CLOCK_TIME_NONE) |
| xing->duration = duration; |
| else |
| xing->duration += duration; |
| |
| if ((ret = gst_pad_push (xing->srcpad, outbuf)) != GST_FLOW_OK) { |
| GST_ERROR_OBJECT (xing, "Failed to push MP3 frame: %s", |
| gst_flow_get_name (ret)); |
| return ret; |
| } |
| } |
| |
| return ret; |
| } |
| |
| static gboolean |
| gst_xing_mux_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| GstXingMux *xing; |
| gboolean result; |
| |
| xing = GST_XING_MUX (parent); |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_SEGMENT: |
| if (xing->sent_xing) { |
| GST_ERROR ("Already sent Xing header, dropping NEWSEGMENT event!"); |
| gst_event_unref (event); |
| result = FALSE; |
| } else { |
| GstSegment segment; |
| |
| gst_event_copy_segment (event, &segment); |
| |
| if (segment.format == GST_FORMAT_BYTES) { |
| result = gst_pad_push_event (xing->srcpad, event); |
| } else { |
| |
| gst_event_unref (event); |
| gst_segment_init (&segment, GST_FORMAT_BYTES); |
| event = gst_event_new_segment (&segment); |
| |
| result = gst_pad_push_event (xing->srcpad, event); |
| } |
| } |
| break; |
| |
| case GST_EVENT_EOS:{ |
| GstEvent *n_event; |
| |
| GST_DEBUG_OBJECT (xing, "handling EOS event"); |
| |
| if (xing->sent_xing) { |
| GstSegment segment; |
| |
| gst_segment_init (&segment, GST_FORMAT_BYTES); |
| n_event = gst_event_new_segment (&segment); |
| |
| if (G_UNLIKELY (!gst_pad_push_event (xing->srcpad, n_event))) { |
| GST_WARNING |
| ("Failed to seek to position 0 for pushing the Xing header"); |
| } else { |
| GstBuffer *header; |
| GstFlowReturn ret; |
| |
| header = generate_xing_header (xing); |
| |
| if (header == NULL) { |
| GST_ERROR ("Can't generate Xing header"); |
| } else { |
| |
| GST_INFO ("Writing real Xing header to beginning of stream"); |
| |
| if ((ret = gst_pad_push (xing->srcpad, header)) != GST_FLOW_OK) |
| GST_WARNING ("Failed to push updated Xing header: %s\n", |
| gst_flow_get_name (ret)); |
| } |
| } |
| } |
| result = gst_pad_push_event (xing->srcpad, event); |
| break; |
| } |
| default: |
| result = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| |
| return result; |
| } |
| |
| |
| static GstStateChangeReturn |
| gst_xing_mux_change_state (GstElement * element, GstStateChange transition) |
| { |
| GstXingMux *xing; |
| GstStateChangeReturn result; |
| |
| xing = GST_XING_MUX (element); |
| |
| result = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); |
| |
| switch (transition) { |
| case GST_STATE_CHANGE_PAUSED_TO_READY: |
| xing_reset (xing); |
| break; |
| default: |
| break; |
| } |
| |
| return result; |
| } |