| /* GStreamer |
| * Copyright (C) 2003 Christophe Fergeau <teuf@gnome.org> |
| * Copyright (C) 2008 Jonathan Matthew <jonathan@d14n.org> |
| * Copyright (C) 2008 Sebastian Dröge <sebastian.droege@collabora.co.uk> |
| * |
| * gstflactag.c: plug-in for reading/modifying vorbis comments in flac files |
| * |
| * 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-flactag |
| * @see_also: #flacenc, #flacdec, #GstTagSetter |
| * |
| * The flactag element can change the tag contained within a raw |
| * FLAC stream. Specifically, it modifies the comments header packet |
| * of the FLAC stream. |
| * |
| * Applications can set the tags to write using the #GstTagSetter interface. |
| * Tags contained withing the FLAC bitstream will be picked up |
| * automatically (and merged according to the merge mode set via the tag |
| * setter interface). |
| * |
| * <refsect2> |
| * <title>Example pipelines</title> |
| * |[ |
| * gst-launch-1.0 -v filesrc location=foo.flac ! flactag ! filesink location=bar.flac |
| * ]| This element is not useful with gst-launch, because it does not support |
| * setting the tags on a #GstTagSetter interface. Conceptually, the element |
| * will usually be used in this order though. |
| * </refsect2> |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include <gst/gst.h> |
| #include <gst/gsttagsetter.h> |
| #include <gst/base/gstadapter.h> |
| #include <gst/tag/tag.h> |
| #include <string.h> |
| |
| #include "gstflactag.h" |
| |
| GST_DEBUG_CATEGORY_STATIC (flactag_debug); |
| #define GST_CAT_DEFAULT flactag_debug |
| |
| /* elementfactory information */ |
| static GstStaticPadTemplate flac_tag_src_template = |
| GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/x-flac") |
| ); |
| |
| static GstStaticPadTemplate flac_tag_sink_template = |
| GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("audio/x-flac") |
| ); |
| |
| |
| static void gst_flac_tag_dispose (GObject * object); |
| |
| static GstFlowReturn gst_flac_tag_chain (GstPad * pad, GstObject * parent, |
| GstBuffer * buffer); |
| |
| static GstStateChangeReturn gst_flac_tag_change_state (GstElement * element, |
| GstStateChange transition); |
| |
| static gboolean gst_flac_tag_sink_event (GstPad * pad, GstObject * parent, |
| GstEvent * event); |
| |
| #define gst_flac_tag_parent_class parent_class |
| G_DEFINE_TYPE_WITH_CODE (GstFlacTag, gst_flac_tag, GST_TYPE_ELEMENT, |
| G_IMPLEMENT_INTERFACE (GST_TYPE_TAG_SETTER, NULL)); |
| |
| |
| static void |
| gst_flac_tag_class_init (GstFlacTagClass * klass) |
| { |
| GstElementClass *gstelement_class; |
| GObjectClass *gobject_class; |
| |
| GST_DEBUG_CATEGORY_INIT (flactag_debug, "flactag", 0, "flac tag rewriter"); |
| |
| gstelement_class = (GstElementClass *) klass; |
| gobject_class = (GObjectClass *) klass; |
| |
| gobject_class->dispose = gst_flac_tag_dispose; |
| gstelement_class->change_state = gst_flac_tag_change_state; |
| |
| gst_element_class_set_static_metadata (gstelement_class, "FLAC tagger", |
| "Formatter/Metadata", |
| "Rewrite tags in a FLAC file", "Christophe Fergeau <teuf@gnome.org>"); |
| |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &flac_tag_sink_template); |
| gst_element_class_add_static_pad_template (gstelement_class, |
| &flac_tag_src_template); |
| } |
| |
| static void |
| gst_flac_tag_dispose (GObject * object) |
| { |
| GstFlacTag *tag = GST_FLAC_TAG (object); |
| |
| if (tag->adapter) { |
| g_object_unref (tag->adapter); |
| tag->adapter = NULL; |
| } |
| if (tag->vorbiscomment) { |
| gst_buffer_unref (tag->vorbiscomment); |
| tag->vorbiscomment = NULL; |
| } |
| if (tag->tags) { |
| gst_tag_list_unref (tag->tags); |
| tag->tags = NULL; |
| } |
| |
| G_OBJECT_CLASS (parent_class)->dispose (object); |
| } |
| |
| |
| static void |
| gst_flac_tag_init (GstFlacTag * tag) |
| { |
| /* create the sink and src pads */ |
| tag->sinkpad = |
| gst_pad_new_from_static_template (&flac_tag_sink_template, "sink"); |
| gst_pad_set_chain_function (tag->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_flac_tag_chain)); |
| gst_pad_set_event_function (tag->sinkpad, |
| GST_DEBUG_FUNCPTR (gst_flac_tag_sink_event)); |
| gst_element_add_pad (GST_ELEMENT (tag), tag->sinkpad); |
| |
| tag->srcpad = |
| gst_pad_new_from_static_template (&flac_tag_src_template, "src"); |
| gst_element_add_pad (GST_ELEMENT (tag), tag->srcpad); |
| |
| tag->adapter = gst_adapter_new (); |
| } |
| |
| static gboolean |
| gst_flac_tag_sink_event (GstPad * pad, GstObject * parent, GstEvent * event) |
| { |
| GstFlacTag *tag; |
| gboolean ret; |
| |
| tag = GST_FLAC_TAG (parent); |
| |
| GST_DEBUG_OBJECT (pad, "Received %s event on sinkpad, %" GST_PTR_FORMAT, |
| GST_EVENT_TYPE_NAME (event), event); |
| |
| switch (GST_EVENT_TYPE (event)) { |
| case GST_EVENT_CAPS: |
| /* FIXME: parse and store the caps. Once we parsed and built the headers, |
| * update the "streamheader" field in the caps and send a new caps event |
| */ |
| ret = gst_pad_push_event (tag->srcpad, event); |
| break; |
| default: |
| ret = gst_pad_event_default (pad, parent, event); |
| break; |
| } |
| |
| return ret; |
| } |
| |
| #define FLAC_MAGIC "fLaC" |
| #define FLAC_MAGIC_SIZE (sizeof (FLAC_MAGIC) - 1) |
| |
| static GstFlowReturn |
| gst_flac_tag_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer) |
| { |
| GstFlacTag *tag; |
| GstFlowReturn ret; |
| GstMapInfo map; |
| gsize size; |
| |
| ret = GST_FLOW_OK; |
| tag = GST_FLAC_TAG (parent); |
| |
| gst_adapter_push (tag->adapter, buffer); |
| |
| GST_LOG_OBJECT (pad, "state: %d", tag->state); |
| |
| /* Initial state, we don't even know if we are dealing with a flac file */ |
| if (tag->state == GST_FLAC_TAG_STATE_INIT) { |
| GstBuffer *id_buffer; |
| |
| if (gst_adapter_available (tag->adapter) < sizeof (FLAC_MAGIC)) |
| goto cleanup; |
| |
| id_buffer = gst_adapter_take_buffer (tag->adapter, FLAC_MAGIC_SIZE); |
| GST_DEBUG_OBJECT (tag, "looking for " FLAC_MAGIC " identifier"); |
| if (gst_buffer_memcmp (id_buffer, 0, FLAC_MAGIC, FLAC_MAGIC_SIZE) == 0) { |
| |
| GST_DEBUG_OBJECT (tag, "pushing " FLAC_MAGIC " identifier buffer"); |
| ret = gst_pad_push (tag->srcpad, id_buffer); |
| if (ret != GST_FLOW_OK) |
| goto cleanup; |
| |
| tag->state = GST_FLAC_TAG_STATE_METADATA_BLOCKS; |
| } else { |
| /* FIXME: does that work well with FLAC files containing ID3v2 tags ? */ |
| gst_buffer_unref (id_buffer); |
| GST_ELEMENT_ERROR (tag, STREAM, WRONG_TYPE, (NULL), (NULL)); |
| ret = GST_FLOW_ERROR; |
| } |
| } |
| |
| |
| /* The fLaC magic string has been skipped, try to detect the beginning |
| * of a metadata block |
| */ |
| if (tag->state == GST_FLAC_TAG_STATE_METADATA_BLOCKS) { |
| guint type; |
| gboolean is_last; |
| const guint8 *block_header; |
| |
| g_assert (tag->metadata_block_size == 0); |
| g_assert (tag->metadata_last_block == FALSE); |
| |
| /* The header of a flac metadata block is 4 bytes long: |
| * 1st bit: indicates whether this is the last metadata info block |
| * 7 next bits: 4 if vorbis comment block |
| * 24 next bits: size of the metadata to follow (big endian) |
| */ |
| if (gst_adapter_available (tag->adapter) < 4) |
| goto cleanup; |
| |
| block_header = gst_adapter_map (tag->adapter, 4); |
| |
| is_last = ((block_header[0] & 0x80) == 0x80); |
| type = block_header[0] & 0x7F; |
| size = (block_header[1] << 16) |
| | (block_header[2] << 8) |
| | block_header[3]; |
| gst_adapter_unmap (tag->adapter); |
| |
| /* The 4 bytes long header isn't included in the metadata size */ |
| tag->metadata_block_size = size + 4; |
| tag->metadata_last_block = is_last; |
| |
| GST_DEBUG_OBJECT (tag, |
| "got metadata block: %" G_GSIZE_FORMAT " bytes, type %d, " |
| "is vorbiscomment: %d, is last: %d", |
| size, type, (type == 0x04), is_last); |
| |
| /* Metadata blocks of type 4 are vorbis comment blocks */ |
| if (type == 0x04) { |
| tag->state = GST_FLAC_TAG_STATE_VC_METADATA_BLOCK; |
| } else { |
| tag->state = GST_FLAC_TAG_STATE_WRITING_METADATA_BLOCK; |
| } |
| } |
| |
| |
| /* Reads a metadata block */ |
| if ((tag->state == GST_FLAC_TAG_STATE_WRITING_METADATA_BLOCK) || |
| (tag->state == GST_FLAC_TAG_STATE_VC_METADATA_BLOCK)) { |
| GstBuffer *metadata_buffer; |
| |
| if (gst_adapter_available (tag->adapter) < tag->metadata_block_size) |
| goto cleanup; |
| |
| metadata_buffer = gst_adapter_take_buffer (tag->adapter, |
| tag->metadata_block_size); |
| /* clear the is-last flag, as the last metadata block will |
| * be the vorbis comment block which we will build ourselves. |
| */ |
| gst_buffer_map (metadata_buffer, &map, GST_MAP_READWRITE); |
| map.data[0] &= (~0x80); |
| gst_buffer_unmap (metadata_buffer, &map); |
| |
| if (tag->state == GST_FLAC_TAG_STATE_WRITING_METADATA_BLOCK) { |
| GST_DEBUG_OBJECT (tag, "pushing metadata block buffer"); |
| ret = gst_pad_push (tag->srcpad, metadata_buffer); |
| if (ret != GST_FLOW_OK) |
| goto cleanup; |
| } else { |
| tag->vorbiscomment = metadata_buffer; |
| } |
| tag->metadata_block_size = 0; |
| tag->state = GST_FLAC_TAG_STATE_METADATA_NEXT_BLOCK; |
| } |
| |
| /* This state is mainly used to be able to stop as soon as we read |
| * a vorbiscomment block from the flac file if we are in an only output |
| * tags mode |
| */ |
| if (tag->state == GST_FLAC_TAG_STATE_METADATA_NEXT_BLOCK) { |
| /* Check if in the previous iteration we read a vorbis comment metadata |
| * block, and stop now if the user only wants to read tags |
| */ |
| if (tag->vorbiscomment != NULL) { |
| guint8 id_data[4]; |
| /* We found some tags, try to parse them and notify the other elements |
| * that we encountered some tags |
| */ |
| GST_DEBUG_OBJECT (tag, "emitting vorbiscomment tags"); |
| gst_buffer_extract (tag->vorbiscomment, 0, id_data, 4); |
| tag->tags = gst_tag_list_from_vorbiscomment_buffer (tag->vorbiscomment, |
| id_data, 4, NULL); |
| if (tag->tags != NULL) { |
| gst_pad_push_event (tag->srcpad, |
| gst_event_new_tag (gst_tag_list_ref (tag->tags))); |
| } |
| |
| gst_buffer_unref (tag->vorbiscomment); |
| tag->vorbiscomment = NULL; |
| } |
| |
| /* Skip to next state */ |
| if (tag->metadata_last_block == FALSE) { |
| tag->state = GST_FLAC_TAG_STATE_METADATA_BLOCKS; |
| } else { |
| tag->state = GST_FLAC_TAG_STATE_ADD_VORBIS_COMMENT; |
| } |
| } |
| |
| |
| /* Creates a vorbis comment block from the metadata which was set |
| * on the gstreamer element, and add it to the flac stream |
| */ |
| if (tag->state == GST_FLAC_TAG_STATE_ADD_VORBIS_COMMENT) { |
| GstBuffer *buffer; |
| const GstTagList *user_tags; |
| GstTagList *merged_tags; |
| |
| /* merge the tag lists */ |
| user_tags = gst_tag_setter_get_tag_list (GST_TAG_SETTER (tag)); |
| if (user_tags != NULL) { |
| merged_tags = gst_tag_list_merge (user_tags, tag->tags, |
| gst_tag_setter_get_tag_merge_mode (GST_TAG_SETTER (tag))); |
| } else { |
| merged_tags = gst_tag_list_copy (tag->tags); |
| } |
| |
| if (merged_tags == NULL) { |
| /* If we get a NULL list of tags, we must generate a padding block |
| * which is marked as the last metadata block, otherwise we'll |
| * end up with a corrupted flac file. |
| */ |
| GST_WARNING_OBJECT (tag, "No tags found"); |
| buffer = gst_buffer_new_and_alloc (12); |
| if (buffer == NULL) |
| goto no_buffer; |
| |
| gst_buffer_map (buffer, &map, GST_MAP_WRITE); |
| memset (map.data, 0, map.size); |
| map.data[0] = 0x81; /* 0x80 = Last metadata block, |
| * 0x01 = padding block */ |
| gst_buffer_unmap (buffer, &map); |
| } else { |
| guchar header[4]; |
| guint8 fbit[1]; |
| |
| memset (header, 0, sizeof (header)); |
| header[0] = 0x84; /* 0x80 = Last metadata block, |
| * 0x04 = vorbiscomment block */ |
| buffer = gst_tag_list_to_vorbiscomment_buffer (merged_tags, header, |
| sizeof (header), NULL); |
| GST_DEBUG_OBJECT (tag, "Writing tags %" GST_PTR_FORMAT, merged_tags); |
| gst_tag_list_unref (merged_tags); |
| if (buffer == NULL) |
| goto no_comment; |
| |
| size = gst_buffer_get_size (buffer); |
| if ((size < 4) || ((size - 4) > 0xFFFFFF)) |
| goto comment_too_long; |
| |
| fbit[0] = 1; |
| /* Get rid of the framing bit at the end of the vorbiscomment buffer |
| * if it exists since libFLAC seems to lose sync because of this |
| * bit in gstflacdec |
| */ |
| if (gst_buffer_memcmp (buffer, size - 1, fbit, 1) == 0) { |
| buffer = gst_buffer_make_writable (buffer); |
| gst_buffer_resize (buffer, 0, size - 1); |
| } |
| } |
| |
| /* The 4 byte metadata block header isn't accounted for in the total |
| * size of the metadata block |
| */ |
| gst_buffer_map (buffer, &map, GST_MAP_WRITE); |
| map.data[1] = (((map.size - 4) & 0xFF0000) >> 16); |
| map.data[2] = (((map.size - 4) & 0x00FF00) >> 8); |
| map.data[3] = ((map.size - 4) & 0x0000FF); |
| gst_buffer_unmap (buffer, &map); |
| |
| GST_DEBUG_OBJECT (tag, "pushing %" G_GSIZE_FORMAT " byte vorbiscomment " |
| "buffer", map.size); |
| |
| ret = gst_pad_push (tag->srcpad, buffer); |
| if (ret != GST_FLOW_OK) { |
| goto cleanup; |
| } |
| tag->state = GST_FLAC_TAG_STATE_AUDIO_DATA; |
| } |
| |
| /* The metadata blocks have been read, now we are reading audio data */ |
| if (tag->state == GST_FLAC_TAG_STATE_AUDIO_DATA) { |
| GstBuffer *buffer; |
| guint avail; |
| |
| avail = gst_adapter_available (tag->adapter); |
| if (avail > 0) { |
| buffer = gst_adapter_take_buffer (tag->adapter, avail); |
| ret = gst_pad_push (tag->srcpad, buffer); |
| } |
| } |
| |
| cleanup: |
| GST_LOG_OBJECT (pad, "state: %d, ret: %d", tag->state, ret); |
| return ret; |
| |
| /* ERRORS */ |
| no_buffer: |
| { |
| GST_ELEMENT_ERROR (tag, CORE, TOO_LAZY, (NULL), |
| ("Error creating 12-byte buffer for padding block")); |
| ret = GST_FLOW_ERROR; |
| goto cleanup; |
| } |
| no_comment: |
| { |
| GST_ELEMENT_ERROR (tag, CORE, TAG, (NULL), |
| ("Error converting tag list to vorbiscomment buffer")); |
| ret = GST_FLOW_ERROR; |
| goto cleanup; |
| } |
| comment_too_long: |
| { |
| /* FLAC vorbis comment blocks are limited to 2^24 bytes, |
| * while the vorbis specs allow more than that. Shouldn't |
| * be a real world problem though |
| */ |
| GST_ELEMENT_ERROR (tag, CORE, TAG, (NULL), |
| ("Vorbis comment of size %" G_GSIZE_FORMAT " too long", size)); |
| ret = GST_FLOW_ERROR; |
| goto cleanup; |
| } |
| } |
| |
| static GstStateChangeReturn |
| gst_flac_tag_change_state (GstElement * element, GstStateChange transition) |
| { |
| GstFlacTag *tag; |
| |
| tag = GST_FLAC_TAG (element); |
| |
| switch (transition) { |
| case GST_STATE_CHANGE_NULL_TO_READY: |
| break; |
| case GST_STATE_CHANGE_READY_TO_PAUSED: |
| break; |
| case GST_STATE_CHANGE_PAUSED_TO_PLAYING: |
| /* do something to get out of the chain function faster */ |
| break; |
| case GST_STATE_CHANGE_PLAYING_TO_PAUSED: |
| break; |
| case GST_STATE_CHANGE_PAUSED_TO_READY: |
| gst_adapter_clear (tag->adapter); |
| if (tag->vorbiscomment) { |
| gst_buffer_unref (tag->vorbiscomment); |
| tag->vorbiscomment = NULL; |
| } |
| if (tag->tags) { |
| gst_tag_list_unref (tag->tags); |
| tag->tags = NULL; |
| } |
| tag->metadata_block_size = 0; |
| tag->metadata_last_block = FALSE; |
| tag->state = GST_FLAC_TAG_STATE_INIT; |
| break; |
| case GST_STATE_CHANGE_READY_TO_NULL: |
| break; |
| default: |
| break; |
| } |
| |
| return GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); |
| } |