summaryrefslogtreecommitdiffstats
path: root/gst/id3tag/id3tag.c
diff options
context:
space:
mode:
authorMichael Smith <msmith@songbirdnest.com>2009-05-21 13:15:46 -0700
committerMichael Smith <msmith@songbirdnest.com>2009-05-21 13:15:46 -0700
commitfe38f53572ff91b63a3dc5a5a5ed78e13040d16c (patch)
treeda2e7e402b107f5690a78df21b63e377ac692291 /gst/id3tag/id3tag.c
parentb9ac26713b9c4097373082a47c960d314a19119a (diff)
downloadgst-plugins-bad-fe38f53572ff91b63a3dc5a5a5ed78e13040d16c.tar.gz
gst-plugins-bad-fe38f53572ff91b63a3dc5a5a5ed78e13040d16c.tar.bz2
gst-plugins-bad-fe38f53572ff91b63a3dc5a5a5ed78e13040d16c.zip
id3tag: Add new id3 tagging plugin, supports v1, v2.3, and v2.4.
By default, does v1 and v2.3, but there are properties to select. Will hopefully replace id3mux, id3v2mux, in the not-too-distant future.
Diffstat (limited to 'gst/id3tag/id3tag.c')
-rw-r--r--gst/id3tag/id3tag.c1194
1 files changed, 1194 insertions, 0 deletions
diff --git a/gst/id3tag/id3tag.c b/gst/id3tag/id3tag.c
new file mode 100644
index 00000000..0e040f7e
--- /dev/null
+++ b/gst/id3tag/id3tag.c
@@ -0,0 +1,1194 @@
+/* GStreamer ID3v2 tag writer
+ *
+ * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org>
+ * Copyright (C) 2006-2009 Tim-Philipp Müller <tim centricular net>
+ * Copyright (C) 2009 Pioneers of the Inevitable <songbird@songbirdnest.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "id3tag.h"
+#include <string.h>
+
+#include <gst/tag/tag.h>
+
+GST_DEBUG_CATEGORY_EXTERN (gst_id3tag_debug);
+#define GST_CAT_DEFAULT gst_id3tag_debug
+
+#define ID3V2_APIC_PICTURE_OTHER 0
+#define ID3V2_APIC_PICTURE_FILE_ICON 1
+
+/* ======================================================================== */
+
+typedef GString GstByteWriter;
+
+static inline GstByteWriter *
+gst_byte_writer_new (guint size)
+{
+ return (GstByteWriter *) g_string_sized_new (size);
+}
+
+static inline guint
+gst_byte_writer_get_length (GstByteWriter * w)
+{
+ return ((GString *) w)->len;
+}
+
+static inline void
+gst_byte_writer_write_bytes (GstByteWriter * w, const guint8 * data, guint len)
+{
+ g_string_append_len ((GString *) w, (const gchar *) data, len);
+}
+
+static inline void
+gst_byte_writer_write_uint8 (GstByteWriter * w, guint8 val)
+{
+ guint8 data[1];
+
+ GST_WRITE_UINT8 (data, val);
+ gst_byte_writer_write_bytes (w, data, 1);
+}
+
+static inline void
+gst_byte_writer_write_uint16 (GstByteWriter * w, guint16 val)
+{
+ guint8 data[2];
+
+ GST_WRITE_UINT16_BE (data, val);
+ gst_byte_writer_write_bytes (w, data, 2);
+}
+
+static inline void
+gst_byte_writer_write_uint32 (GstByteWriter * w, guint32 val)
+{
+ guint8 data[4];
+
+ GST_WRITE_UINT32_BE (data, val);
+ gst_byte_writer_write_bytes (w, data, 4);
+}
+
+static inline void
+gst_byte_writer_write_uint32_syncsafe (GstByteWriter * w, guint32 val)
+{
+ guint8 data[4];
+
+ data[0] = (guint8) ((val >> 21) & 0x7f);
+ data[1] = (guint8) ((val >> 14) & 0x7f);
+ data[2] = (guint8) ((val >> 7) & 0x7f);
+ data[3] = (guint8) ((val >> 0) & 0x7f);
+ gst_byte_writer_write_bytes (w, data, 4);
+}
+
+static void
+gst_byte_writer_copy_bytes (GstByteWriter * w, guint8 * dest, guint offset,
+ gint size)
+{
+ guint length;
+
+ length = gst_byte_writer_get_length (w);
+
+ if (size == -1)
+ size = length - offset;
+
+#if GLIB_CHECK_VERSION(2,16,0)
+ g_warn_if_fail (length >= (offset + size));
+#endif
+
+ memcpy (dest, w->str + offset, MIN (size, length - offset));
+}
+
+static inline void
+gst_byte_writer_free (GstByteWriter * w)
+{
+ g_string_free (w, TRUE);
+}
+
+/* ======================================================================== */
+
+/*
+typedef enum {
+ GST_ID3V2_FRAME_FLAG_NONE = 0,
+ GST_ID3V2_FRAME_FLAG_
+} GstID3v2FrameMsgFlags;
+*/
+
+typedef struct
+{
+ gchar id[5];
+ guint32 len; /* Length encoded in the header; this is the
+ total length - header size */
+ guint16 flags;
+ GstByteWriter *writer;
+ gboolean dirty; /* TRUE if frame header needs updating */
+} GstId3v2Frame;
+
+typedef struct
+{
+ GArray *frames;
+ guint major_version; /* The 3 in v2.3.0 */
+} GstId3v2Tag;
+
+typedef void (*GstId3v2AddTagFunc) (GstId3v2Tag * tag, const GstTagList * list,
+ const gchar * gst_tag, guint num_tags, const gchar * data);
+
+#define ID3V2_ENCODING_UTF8 0x03
+
+static gboolean id3v2_tag_init (GstId3v2Tag * tag, guint major_version);
+static void id3v2_tag_unset (GstId3v2Tag * tag);
+
+static void id3v2_frame_init (GstId3v2Frame * frame,
+ const gchar * frame_id, guint16 flags);
+static void id3v2_frame_unset (GstId3v2Frame * frame);
+static void id3v2_frame_finish (GstId3v2Tag * tag, GstId3v2Frame * frame);
+static guint id3v2_frame_get_size (GstId3v2Tag * tag, GstId3v2Frame * frame);
+
+static void id3v2_tag_add_text_frame (GstId3v2Tag * tag,
+ const gchar * frame_id, gchar ** strings, int num_strings);
+
+static gboolean
+id3v2_tag_init (GstId3v2Tag * tag, guint major_version)
+{
+ if (major_version != 3 && major_version != 4)
+ return FALSE;
+
+ tag->major_version = major_version;
+ tag->frames = g_array_new (TRUE, TRUE, sizeof (GstId3v2Frame));
+
+ return TRUE;
+}
+
+static void
+id3v2_tag_unset (GstId3v2Tag * tag)
+{
+ guint i;
+
+ for (i = 0; i < tag->frames->len; ++i)
+ id3v2_frame_unset (&g_array_index (tag->frames, GstId3v2Frame, i));
+
+ g_array_free (tag->frames, TRUE);
+ memset (tag, 0, sizeof (GstId3v2Tag));
+}
+
+#ifndef GST_ROUND_UP_1024
+#define GST_ROUND_UP_1024(num) (((num)+1023)&~1023)
+#endif
+
+static GstBuffer *
+id3v2_tag_to_buffer (GstId3v2Tag * tag)
+{
+ GstByteWriter *w;
+ GstBuffer *buf;
+ guint8 *dest;
+ guint i, size, offset, size_frames = 0;
+
+ GST_DEBUG ("Creating buffer for ID3v2 tag containing %d frames",
+ tag->frames->len);
+
+ for (i = 0; i < tag->frames->len; ++i) {
+ GstId3v2Frame *frame = &g_array_index (tag->frames, GstId3v2Frame, i);
+
+ id3v2_frame_finish (tag, frame);
+ size_frames += id3v2_frame_get_size (tag, frame);
+ }
+
+ size = GST_ROUND_UP_1024 (10 + size_frames);
+
+ w = gst_byte_writer_new (10);
+ gst_byte_writer_write_uint8 (w, 'I');
+ gst_byte_writer_write_uint8 (w, 'D');
+ gst_byte_writer_write_uint8 (w, '3');
+ gst_byte_writer_write_uint8 (w, tag->major_version);
+ gst_byte_writer_write_uint8 (w, 0); /* micro version */
+ gst_byte_writer_write_uint8 (w, 0); /* flags */
+ gst_byte_writer_write_uint32_syncsafe (w, size - 10);
+
+ buf = gst_buffer_new_and_alloc (size);
+ dest = GST_BUFFER_DATA (buf);
+ gst_byte_writer_copy_bytes (w, dest, 0, 10);
+ offset = 10;
+
+ for (i = 0; i < tag->frames->len; ++i) {
+ GstId3v2Frame *frame = &g_array_index (tag->frames, GstId3v2Frame, i);
+
+ gst_byte_writer_copy_bytes (frame->writer, dest + offset, 0, -1);
+ offset += id3v2_frame_get_size (tag, frame);
+ }
+
+ /* Zero out any additional space in our buffer as padding. */
+ memset (dest + offset, 0, size - offset);
+
+ gst_byte_writer_free (w);
+
+ return buf;
+}
+
+static inline void
+id3v2_frame_write_bytes (GstId3v2Frame * frame, const guint8 * data, guint len)
+{
+ gst_byte_writer_write_bytes (frame->writer, data, len);
+ frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint8 (GstId3v2Frame * frame, guint8 val)
+{
+ gst_byte_writer_write_uint8 (frame->writer, val);
+ frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint16 (GstId3v2Frame * frame, guint16 val)
+{
+ gst_byte_writer_write_uint16 (frame->writer, val);
+ frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint32 (GstId3v2Frame * frame, guint32 val)
+{
+ gst_byte_writer_write_uint32 (frame->writer, val);
+ frame->dirty = TRUE;
+}
+
+static inline void
+id3v2_frame_write_uint32_syncsafe (GstId3v2Frame * frame, guint32 val)
+{
+ guint8 data[4];
+
+ data[0] = (guint8) ((val >> 21) & 0x7f);
+ data[1] = (guint8) ((val >> 14) & 0x7f);
+ data[2] = (guint8) ((val >> 7) & 0x7f);
+ data[3] = (guint8) ((val >> 0) & 0x7f);
+ gst_byte_writer_write_bytes (frame->writer, data, 4);
+ frame->dirty = TRUE;
+}
+
+static void
+id3v2_frame_init (GstId3v2Frame * frame, const gchar * frame_id, guint16 flags)
+{
+ g_assert (strlen (frame_id) == 4); /* we only handle 2.3.0/2.4.0 */
+ memcpy (frame->id, frame_id, 4 + 1);
+ frame->flags = flags;
+ frame->len = 0;
+ frame->writer = gst_byte_writer_new (64);
+ id3v2_frame_write_bytes (frame, (const guint8 *) frame->id, 4);
+ id3v2_frame_write_uint32 (frame, 0); /* size, set later */
+ id3v2_frame_write_uint16 (frame, frame->flags);
+}
+
+static void
+id3v2_frame_finish (GstId3v2Tag * tag, GstId3v2Frame * frame)
+{
+ if (frame->dirty) {
+ frame->len = frame->writer->len - 10;
+ GST_LOG ("[%s] %u bytes", frame->id, frame->len);
+ if (tag->major_version == 3) {
+ GST_WRITE_UINT32_BE (frame->writer->str + 4, frame->len);
+ } else {
+ /* Version 4 uses a syncsafe int here */
+ GST_WRITE_UINT8 (frame->writer->str + 4, (frame->len >> 21) & 0x7f);
+ GST_WRITE_UINT8 (frame->writer->str + 5, (frame->len >> 14) & 0x7f);
+ GST_WRITE_UINT8 (frame->writer->str + 6, (frame->len >> 7) & 0x7f);
+ GST_WRITE_UINT8 (frame->writer->str + 7, (frame->len >> 0) & 0x7f);
+ }
+ frame->dirty = FALSE;
+ }
+}
+
+static guint
+id3v2_frame_get_size (GstId3v2Tag * tag, GstId3v2Frame * frame)
+{
+ id3v2_frame_finish (tag, frame);
+ return gst_byte_writer_get_length (frame->writer);
+}
+
+static void
+id3v2_frame_unset (GstId3v2Frame * frame)
+{
+ gst_byte_writer_free (frame->writer);
+ memset (frame, 0, sizeof (GstId3v2Frame));
+}
+
+static void
+id3v2_tag_add_text_frame (GstId3v2Tag * tag, const gchar * frame_id,
+ gchar ** strings_utf8, int num_strings)
+{
+ GstId3v2Frame frame;
+ guint len, i;
+
+ if (num_strings < 1 || strings_utf8 == NULL || strings_utf8[0] == NULL) {
+ GST_LOG ("Not adding text frame, no strings");
+ return;
+ }
+
+ id3v2_frame_init (&frame, frame_id, 0);
+ id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+
+ GST_LOG ("Adding text frame %s with %d strings", frame_id, num_strings);
+
+ for (i = 0; i < num_strings; ++i) {
+ len = strlen (strings_utf8[i]);
+ g_return_if_fail (g_utf8_validate (strings_utf8[i], len, NULL));
+
+ /* write NUL terminator as well */
+ id3v2_frame_write_bytes (&frame, (const guint8 *) strings_utf8[i], len + 1);
+
+ /* only v2.4.0 supports multiple strings per frame (according to the
+ * earlier specs tag readers should just ignore everything after the first
+ * string, but we probably shouldn't write anything there, just in case
+ * tag readers that only support the old version are not expecting
+ * more data after the first string) */
+ if (tag->major_version < 4)
+ break;
+ }
+
+ if (i < num_strings - 1) {
+ GST_WARNING ("Only wrote one of multiple string values for text frame %s "
+ "- ID3v2 supports multiple string values only since v2.4.0, but writing"
+ "v2.%u.0 tag", frame_id, tag->major_version);
+ }
+
+ g_array_append_val (tag->frames, frame);
+}
+
+/* ====================================================================== */
+
+static void
+add_text_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+ gchar **strings;
+ guint n, i;
+
+ GST_LOG ("Adding '%s' frame", frame_id);
+
+ strings = g_new0 (gchar *, num_tags + 1);
+ for (n = 0, i = 0; n < num_tags; ++n) {
+ if (gst_tag_list_get_string_index (list, tag, n, &strings[i]) &&
+ strings[i] != NULL) {
+ GST_LOG ("%s: %s[%u] = '%s'", frame_id, tag, i, strings[i]);
+ ++i;
+ }
+ }
+
+ if (strings[0] != NULL) {
+ id3v2_tag_add_text_frame (id3v2tag, frame_id, strings, i);
+ } else {
+ GST_WARNING ("Empty list for tag %s, skipping", tag);
+ }
+
+ g_strfreev (strings);
+}
+
+static void
+add_id3v2frame_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ guint i;
+
+ for (i = 0; i < num_tags; ++i) {
+ const GValue *val;
+ GstBuffer *buf;
+
+ val = gst_tag_list_get_value_index (list, tag, i);
+ buf = (GstBuffer *) gst_value_get_mini_object (val);
+
+ if (buf && GST_BUFFER_CAPS (buf)) {
+ GstStructure *s;
+ gint version = 0;
+
+ s = gst_caps_get_structure (GST_BUFFER_CAPS (buf), 0);
+ /* We can only add it if this private buffer is for the same ID3 version,
+ because we don't understand the contents at all. */
+ if (s && gst_structure_get_int (s, "version", &version) &&
+ version == id3v2tag->major_version) {
+ GstId3v2Frame frame;
+ gchar frame_id[5];
+ guint16 flags;
+ guint8 *data = GST_BUFFER_DATA (buf);
+ gint size = GST_BUFFER_SIZE (buf);
+
+ if (size < 10) /* header size */
+ return;
+
+ /* We only reach here if the frame version matches the muxer. Since the
+ muxer only does v2.3 or v2.4, the frame must be one of those - and
+ so the frame header is the same format */
+ memcpy (frame_id, data, 4);
+ frame_id[4] = 0;
+ flags = GST_READ_UINT16_BE (data + 8);
+
+ id3v2_frame_init (&frame, frame_id, flags);
+ id3v2_frame_write_bytes (&frame, data + 10, size - 10);
+
+ g_array_append_val (id3v2tag->frames, frame);
+ GST_DEBUG ("Added unparsed tag with %d bytes", size);
+ } else {
+ GST_WARNING ("Discarding unrecognised ID3 tag for different ID3 "
+ "version");
+ }
+ }
+ }
+}
+
+static void
+add_text_tag_v4 (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+ if (id3v2tag->major_version == 4)
+ add_text_tag (id3v2tag, list, tag, num_tags, frame_id);
+ else {
+ GST_WARNING ("Cannot serialise tag '%s' in ID3v2.%d", frame_id,
+ id3v2tag->major_version);
+ }
+}
+
+static void
+add_count_or_num_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+ static const struct
+ {
+ const gchar *gst_tag;
+ const gchar *corr_count; /* corresponding COUNT tag (if number) */
+ const gchar *corr_num; /* corresponding NUMBER tag (if count) */
+ } corr[] = {
+ {
+ GST_TAG_TRACK_NUMBER, GST_TAG_TRACK_COUNT, NULL}, {
+ GST_TAG_TRACK_COUNT, NULL, GST_TAG_TRACK_NUMBER}, {
+ GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT, NULL}, {
+ GST_TAG_ALBUM_VOLUME_COUNT, NULL, GST_TAG_ALBUM_VOLUME_NUMBER}
+ };
+ guint idx;
+
+ for (idx = 0; idx < G_N_ELEMENTS (corr); ++idx) {
+ if (strcmp (corr[idx].gst_tag, tag) == 0)
+ break;
+ }
+
+ g_assert (idx < G_N_ELEMENTS (corr));
+ g_assert (frame_id && strlen (frame_id) == 4);
+
+ if (corr[idx].corr_num == NULL) {
+ guint number;
+
+ /* number tag */
+ if (gst_tag_list_get_uint_index (list, tag, 0, &number)) {
+ gchar *tag_str;
+ guint count;
+
+ if (gst_tag_list_get_uint_index (list, corr[idx].corr_count, 0, &count))
+ tag_str = g_strdup_printf ("%u/%u", number, count);
+ else
+ tag_str = g_strdup_printf ("%u", number);
+
+ GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
+
+ id3v2_tag_add_text_frame (id3v2tag, frame_id, &tag_str, 1);
+ g_free (tag_str);
+ }
+ } else if (corr[idx].corr_count == NULL) {
+ guint count;
+
+ /* count tag */
+ if (gst_tag_list_get_uint_index (list, corr[idx].corr_num, 0, &count)) {
+ GST_DEBUG ("%s handled with %s, skipping", tag, corr[idx].corr_num);
+ } else if (gst_tag_list_get_uint_index (list, tag, 0, &count)) {
+ gchar *tag_str = g_strdup_printf ("0/%u", count);
+ GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
+
+ id3v2_tag_add_text_frame (id3v2tag, frame_id, &tag_str, 1);
+ g_free (tag_str);
+ }
+ }
+
+ if (num_tags > 1) {
+ GST_WARNING ("more than one %s, can only handle one", tag);
+ }
+}
+
+static void
+add_comment_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ guint n;
+
+ GST_LOG ("Adding comment frames");
+ for (n = 0; n < num_tags; ++n) {
+ gchar *s = NULL;
+
+ if (gst_tag_list_get_string_index (list, tag, n, &s) && s != NULL) {
+ gchar *desc = NULL, *val = NULL, *lang = NULL;
+ int desclen, vallen;
+ GstId3v2Frame frame;
+
+ id3v2_frame_init (&frame, "COMM", 0);
+ id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+
+ if (strcmp (tag, GST_TAG_COMMENT) == 0 ||
+ !gst_tag_parse_extended_comment (s, &desc, &lang, &val, TRUE)) {
+ /* create dummy description fields */
+ desc = g_strdup ("Comment");
+ val = g_strdup (s);
+ }
+
+ /* If we don't have a valid language, match what taglib does for
+ unknown languages */
+ if (!lang || strlen (lang) < 3)
+ lang = g_strdup ("XXX");
+
+ desclen = strlen (desc);
+ g_return_if_fail (g_utf8_validate (desc, desclen, NULL));
+ vallen = strlen (val);
+ g_return_if_fail (g_utf8_validate (val, vallen, NULL));
+
+ GST_LOG ("%s[%u] = '%s' (%s|%s|%s)", tag, n, s, GST_STR_NULL (desc),
+ GST_STR_NULL (lang), GST_STR_NULL (val));
+
+ id3v2_frame_write_bytes (&frame, (const guint8 *) lang, 3);
+ /* write description and value, each including NULL terminator */
+ id3v2_frame_write_bytes (&frame, (const guint8 *) desc, desclen + 1);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) val, vallen + 1);
+
+ g_free (lang);
+ g_free (desc);
+ g_free (val);
+
+ g_array_append_val (id3v2tag->frames, frame);
+ }
+ g_free (s);
+ }
+}
+
+static void
+add_image_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ guint n;
+
+ for (n = 0; n < num_tags; ++n) {
+ const GValue *val;
+ GstBuffer *image;
+
+ GST_DEBUG ("image %u/%u", n + 1, num_tags);
+
+ val = gst_tag_list_get_value_index (list, tag, n);
+ image = (GstBuffer *) gst_value_get_mini_object (val);
+
+ if (GST_IS_BUFFER (image) && GST_BUFFER_SIZE (image) > 0 &&
+ GST_BUFFER_CAPS (image) != NULL &&
+ !gst_caps_is_empty (GST_BUFFER_CAPS (image))) {
+ const gchar *mime_type;
+ GstStructure *s;
+
+ s = gst_caps_get_structure (GST_BUFFER_CAPS (image), 0);
+ mime_type = gst_structure_get_name (s);
+ if (mime_type != NULL) {
+ const gchar *desc;
+ GstId3v2Frame frame;
+
+ /* APIC frame specifies "-->" if we're providing a URL to the image
+ rather than directly embedding it */
+ if (strcmp (mime_type, "text/uri-list") == 0)
+ mime_type = "-->";
+
+ GST_DEBUG ("Attaching picture of %u bytes and mime type %s",
+ GST_BUFFER_SIZE (image), mime_type);
+
+ id3v2_frame_init (&frame, "APIC", 0);
+ id3v2_frame_write_uint8 (&frame, ID3V2_ENCODING_UTF8);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) mime_type,
+ strlen (mime_type) + 1);
+
+ /* FIXME set image type properly from caps */
+ if (strcmp (tag, GST_TAG_PREVIEW_IMAGE) == 0)
+ id3v2_frame_write_uint8 (&frame, ID3V2_APIC_PICTURE_FILE_ICON);
+ else
+ id3v2_frame_write_uint8 (&frame, ID3V2_APIC_PICTURE_OTHER);
+
+ desc = gst_structure_get_string (s, "image-description");
+ if (!desc)
+ desc = "";
+ id3v2_frame_write_bytes (&frame, (const guint8 *) desc,
+ strlen (desc) + 1);
+
+ g_array_append_val (id3v2tag->frames, frame);
+ }
+ } else {
+ GST_WARNING ("NULL image or no caps on image buffer (%p, caps=%"
+ GST_PTR_FORMAT ")", image, (image) ? GST_BUFFER_CAPS (image) : NULL);
+ }
+ }
+}
+
+static void
+add_musicbrainz_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * data)
+{
+ static const struct
+ {
+ const gchar gst_tag[28];
+ const gchar spec_id[28];
+ const gchar realworld_id[28];
+ } mb_ids[] = {
+ {
+ GST_TAG_MUSICBRAINZ_ARTISTID, "MusicBrainz Artist Id",
+ "musicbrainz_artistid"}, {
+ GST_TAG_MUSICBRAINZ_ALBUMID, "MusicBrainz Album Id", "musicbrainz_albumid"}, {
+ GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "MusicBrainz Album Artist Id",
+ "musicbrainz_albumartistid"}, {
+ GST_TAG_MUSICBRAINZ_TRMID, "MusicBrainz TRM Id", "musicbrainz_trmid"}, {
+ GST_TAG_CDDA_MUSICBRAINZ_DISCID, "MusicBrainz DiscID",
+ "musicbrainz_discid"}, {
+ /* the following one is more or less made up, there seems to be little
+ * evidence that any popular application is actually putting this info
+ * into TXXX frames; the first one comes from a musicbrainz wiki 'proposed
+ * tags' page, the second one is analogue to the vorbis/ape/flac tag. */
+ GST_TAG_CDDA_CDDB_DISCID, "CDDB DiscID", "discid"}
+ };
+ guint i, idx;
+
+ idx = (guint8) data[0];
+ g_assert (idx < G_N_ELEMENTS (mb_ids));
+
+ for (i = 0; i < num_tags; ++i) {
+ gchar *id_str;
+
+ if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
+ /* add two frames, one with the ID the musicbrainz.org spec mentions
+ * and one with the ID that applications use in the real world */
+ GstId3v2Frame frame1, frame2;
+
+ GST_DEBUG ("Setting '%s' to '%s'", mb_ids[idx].spec_id, id_str);
+
+ id3v2_frame_init (&frame1, "TXXX", 0);
+ id3v2_frame_write_uint8 (&frame1, ID3V2_ENCODING_UTF8);
+ id3v2_frame_write_bytes (&frame1, (const guint8 *) mb_ids[idx].spec_id,
+ strlen (mb_ids[idx].spec_id) + 1);
+ id3v2_frame_write_bytes (&frame1, (const guint8 *) id_str,
+ strlen (id_str) + 1);
+ g_array_append_val (id3v2tag->frames, frame1);
+
+ id3v2_frame_init (&frame2, "TXXX", 0);
+ id3v2_frame_write_uint8 (&frame2, ID3V2_ENCODING_UTF8);
+ id3v2_frame_write_bytes (&frame2,
+ (const guint8 *) mb_ids[idx].realworld_id,
+ strlen (mb_ids[idx].realworld_id) + 1);
+ id3v2_frame_write_bytes (&frame2, (const guint8 *) id_str,
+ strlen (id_str) + 1);
+ g_array_append_val (id3v2tag->frames, frame2);
+
+ g_free (id_str);
+ }
+ }
+}
+
+static void
+add_unique_file_id_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ const gchar *origin = "http://musicbrainz.org";
+ gchar *id_str = NULL;
+
+ if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
+ GstId3v2Frame frame;
+
+ GST_LOG ("Adding %s (%s): %s", tag, origin, id_str);
+
+ id3v2_frame_init (&frame, "UFID", 0);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) origin,
+ strlen (origin) + 1);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) id_str,
+ strlen (id_str) + 1);
+ g_array_append_val (id3v2tag->frames, frame);
+
+ g_free (id_str);
+ }
+}
+
+static void
+add_date_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ guint n;
+ guint i = 0;
+ const gchar *frame_id;
+ gchar **strings;
+
+ if (id3v2tag->major_version == 3)
+ frame_id = "TYER";
+ else
+ frame_id = "TDRC";
+
+ GST_LOG ("Adding date frame");
+
+ strings = g_new0 (gchar *, num_tags + 1);
+ for (n = 0; n < num_tags; ++n) {
+ GDate *date = NULL;
+
+ if (gst_tag_list_get_date_index (list, tag, n, &date) && date != NULL) {
+ GDateYear year;
+ gchar *s;
+
+ year = g_date_get_year (date);
+ if (year > 500 && year < 2100) {
+ s = g_strdup_printf ("%u", year);
+ GST_LOG ("%s[%u] = '%s'", tag, n, s);
+ strings[i] = s;
+ i++;
+ } else {
+ GST_WARNING ("invalid year %u, skipping", year);
+ }
+
+ g_date_free (date);
+ }
+ }
+
+ if (strings[0] != NULL) {
+ id3v2_tag_add_text_frame (id3v2tag, frame_id, strings, i);
+ } else {
+ GST_WARNING ("Empty list for tag %s, skipping", tag);
+ }
+
+ g_strfreev (strings);
+}
+
+static void
+add_encoder_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ guint n;
+ gchar **strings;
+ int i = 0;
+
+ /* ENCODER_VERSION is either handled with the ENCODER tag or not at all */
+ if (strcmp (tag, GST_TAG_ENCODER_VERSION) == 0)
+ return;
+
+ strings = g_new0 (gchar *, num_tags + 1);
+ for (n = 0; n < num_tags; ++n) {
+ gchar *encoder = NULL;
+
+ if (gst_tag_list_get_string_index (list, tag, n, &encoder) && encoder) {
+ guint encoder_version;
+ gchar *s;
+
+ if (gst_tag_list_get_uint_index (list, GST_TAG_ENCODER_VERSION, n,
+ &encoder_version) && encoder_version > 0) {
+ s = g_strdup_printf ("%s %u", encoder, encoder_version);
+ } else {
+ s = g_strdup (encoder);
+ }
+
+ GST_LOG ("encoder[%u] = '%s'", n, s);
+ strings[i] = s;
+ i++;
+ g_free (encoder);
+ }
+ }
+
+ if (strings[0] != NULL) {
+ id3v2_tag_add_text_frame (id3v2tag, "TSSE", strings, i);
+ } else {
+ GST_WARNING ("Empty list for tag %s, skipping", tag);
+ }
+
+ g_strfreev (strings);
+}
+
+static void
+add_uri_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * frame_id)
+{
+ gchar *url = NULL;
+
+ g_assert (frame_id != NULL);
+
+ /* URI tags are limited to one of each per taglist */
+ if (gst_tag_list_get_string_index (list, tag, 0, &url) && url != NULL) {
+ guint url_len;
+
+ url_len = strlen (url);
+ if (url_len > 0 && gst_uri_is_valid (url)) {
+ GstId3v2Frame frame;
+
+ id3v2_frame_init (&frame, frame_id, 0);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) url, strlen (url) + 1);
+ g_array_append_val (id3v2tag->frames, frame);
+ } else {
+ GST_WARNING ("Tag %s does not contain a valid URI (%s)", tag, url);
+ }
+
+ g_free (url);
+ }
+}
+
+static void
+add_relative_volume_tag (GstId3v2Tag * id3v2tag, const GstTagList * list,
+ const gchar * tag, guint num_tags, const gchar * unused)
+{
+ const char *gain_tag_name;
+ const char *peak_tag_name;
+ gdouble peak_val;
+ gdouble gain_val;
+ const char *identification;
+ guint16 peak_int;
+ gint16 gain_int;
+ guint8 peak_bits;
+ GstId3v2Frame frame;
+ gchar *frame_id;
+
+ /* figure out tag names and the identification string to use */
+ if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
+ strcmp (tag, GST_TAG_TRACK_GAIN) == 0) {
+ gain_tag_name = GST_TAG_TRACK_GAIN;
+ peak_tag_name = GST_TAG_TRACK_PEAK;
+ identification = "track";
+ GST_DEBUG ("adding track relative-volume frame");
+ } else {
+ gain_tag_name = GST_TAG_ALBUM_GAIN;
+ peak_tag_name = GST_TAG_ALBUM_PEAK;
+ identification = "album";
+
+ if (id3v2tag->major_version == 3) {
+ GST_WARNING ("Cannot store replaygain album gain data in ID3v2.3");
+ return;
+ }
+ GST_DEBUG ("adding album relative-volume frame");
+ }
+
+ /* find the value for the paired tag (gain, if this is peak, and
+ * vice versa). if both tags exist, only write the frame when
+ * we're processing the peak tag.
+ */
+ if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
+ strcmp (tag, GST_TAG_ALBUM_PEAK) == 0) {
+
+ gst_tag_list_get_double (list, tag, &peak_val);
+
+ if (gst_tag_list_get_tag_size (list, gain_tag_name) > 0) {
+ gst_tag_list_get_double (list, gain_tag_name, &gain_val);
+ GST_DEBUG ("setting volume adjustment %g", gain_val);
+ gain_int = (gint16) (gain_val * 512.0);
+ } else
+ gain_int = 0;
+
+ /* copying mutagen: always write as 16 bits for sanity. */
+ peak_int = (short) (peak_val * G_MAXSHORT);
+ peak_bits = 16;
+ } else {
+ gst_tag_list_get_double (list, tag, &gain_val);
+ GST_DEBUG ("setting volume adjustment %g", gain_val);
+
+ gain_int = (gint16) (gain_val * 512.0);
+ peak_bits = 0;
+ peak_int = 0;
+
+ if (gst_tag_list_get_tag_size (list, peak_tag_name) != 0) {
+ GST_DEBUG
+ ("both gain and peak tags exist, not adding frame this time around");
+ return;
+ }
+ }
+
+ if (id3v2tag->major_version == 4) {
+ /* 2.4: Use RVA2 tag */
+ frame_id = "RVA2";
+ } else {
+ /* 2.3: Use XRVA tag - this is experimental, but useful in the real world.
+ This version only officially supports the 'RVAD' tag, but that appears
+ to not be widely implemented in reality. */
+ frame_id = "XRVA";
+ }
+
+ id3v2_frame_init (&frame, frame_id, 0);
+ id3v2_frame_write_bytes (&frame, (const guint8 *) identification,
+ strlen (identification) + 1);
+ id3v2_frame_write_uint8 (&frame, 0x01); /* Master volume */
+ id3v2_frame_write_uint16 (&frame, gain_int);
+ id3v2_frame_write_uint8 (&frame, peak_bits);
+ if (peak_bits)
+ id3v2_frame_write_uint16 (&frame, peak_int);
+
+ g_array_append_val (id3v2tag->frames, frame);
+}
+
+/* id3demux produces these for frames it cannot parse */
+#define GST_ID3_DEMUX_TAG_ID3V2_FRAME "private-id3v2-frame"
+
+static const struct
+{
+ const gchar *gst_tag;
+ const GstId3v2AddTagFunc func;
+ const gchar *data;
+} add_funcs[] = {
+ {
+ /* Simple text tags */
+ GST_TAG_ARTIST, add_text_tag, "TPE1"}, {
+ GST_TAG_TITLE, add_text_tag, "TIT2"}, {
+ GST_TAG_ALBUM, add_text_tag, "TALB"}, {
+ GST_TAG_COPYRIGHT, add_text_tag, "TCOP"}, {
+ GST_TAG_COMPOSER, add_text_tag, "TCOM"}, {
+ GST_TAG_GENRE, add_text_tag, "TCON"}, {
+
+ /* Private frames */
+ GST_ID3_DEMUX_TAG_ID3V2_FRAME, add_id3v2frame_tag, NULL}, {
+
+ /* Track and album numbers */
+ GST_TAG_TRACK_NUMBER, add_count_or_num_tag, "TRCK"}, {
+ GST_TAG_TRACK_COUNT, add_count_or_num_tag, "TRCK"}, {
+ GST_TAG_ALBUM_VOLUME_NUMBER, add_count_or_num_tag, "TPOS"}, {
+ GST_TAG_ALBUM_VOLUME_COUNT, add_count_or_num_tag, "TPOS"}, {
+
+ /* Comment tags */
+ GST_TAG_COMMENT, add_comment_tag, NULL}, {
+ GST_TAG_EXTENDED_COMMENT, add_comment_tag, NULL}, {
+
+ /* Images */
+ GST_TAG_IMAGE, add_image_tag, NULL}, {
+ GST_TAG_PREVIEW_IMAGE, add_image_tag, NULL}, {
+
+ /* Misc user-defined text tags for IDs (and UFID frame) */
+ GST_TAG_MUSICBRAINZ_ARTISTID, add_musicbrainz_tag, "\000"}, {
+ GST_TAG_MUSICBRAINZ_ALBUMID, add_musicbrainz_tag, "\001"}, {
+ GST_TAG_MUSICBRAINZ_ALBUMARTISTID, add_musicbrainz_tag, "\002"}, {
+ GST_TAG_MUSICBRAINZ_TRMID, add_musicbrainz_tag, "\003"}, {
+ GST_TAG_CDDA_MUSICBRAINZ_DISCID, add_musicbrainz_tag, "\004"}, {
+ GST_TAG_CDDA_CDDB_DISCID, add_musicbrainz_tag, "\005"}, {
+ GST_TAG_MUSICBRAINZ_TRACKID, add_unique_file_id_tag, NULL}, {
+
+ /* Info about encoder */
+ GST_TAG_ENCODER, add_encoder_tag, NULL}, {
+ GST_TAG_ENCODER_VERSION, add_encoder_tag, NULL}, {
+
+ /* URIs */
+ GST_TAG_COPYRIGHT_URI, add_uri_tag, "WCOP"}, {
+ GST_TAG_LICENSE_URI, add_uri_tag, "WCOP"}, {
+
+ /* Up to here, all the frame ids and contents have been the same between
+ versions 2.3 and 2.4. The rest of them differ... */
+ /* Date (in ID3v2.3, this is a TYER tag. In v2.4, it's a TDRC tag */
+ GST_TAG_DATE, add_date_tag, NULL}, {
+
+ /* Replaygain data (not really supported in 2.3, we use an experimental
+ tag there) */
+ GST_TAG_TRACK_PEAK, add_relative_volume_tag, NULL}, {
+ GST_TAG_TRACK_GAIN, add_relative_volume_tag, NULL}, {
+ GST_TAG_ALBUM_PEAK, add_relative_volume_tag, NULL}, {
+ GST_TAG_ALBUM_GAIN, add_relative_volume_tag, NULL}, {
+
+ /* Sortable version of various tags. These are all v2.4 ONLY */
+ GST_TAG_ARTIST_SORTNAME, add_text_tag_v4, "TSOP"}, {
+ GST_TAG_ALBUM_SORTNAME, add_text_tag_v4, "TSOA"}, {
+ GST_TAG_TITLE_SORTNAME, add_text_tag_v4, "TSOT"}
+};
+
+static void
+foreach_add_tag (const GstTagList * list, const gchar * tag, gpointer userdata)
+{
+ GstId3v2Tag *id3v2tag = (GstId3v2Tag *) userdata;
+ guint num_tags, i;
+
+ num_tags = gst_tag_list_get_tag_size (list, tag);
+
+ GST_LOG ("Processing tag %s (num=%u)", tag, num_tags);
+
+ if (num_tags > 1 && gst_tag_is_fixed (tag)) {
+ GST_WARNING ("Multiple occurences of fixed tag '%s', ignoring some", tag);
+ num_tags = 1;
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (add_funcs); ++i) {
+ if (strcmp (add_funcs[i].gst_tag, tag) == 0) {
+ add_funcs[i].func (id3v2tag, list, tag, num_tags, add_funcs[i].data);
+ break;
+ }
+ }
+
+ if (i == G_N_ELEMENTS (add_funcs)) {
+ GST_WARNING ("Unsupported tag '%s' - not written", tag);
+ }
+}
+
+GstBuffer *
+gst_id3mux_render_v2_tag (GstTagMux * mux, GstTagList * taglist, int version)
+{
+ GstId3v2Tag tag;
+ GstBuffer *buf;
+
+ if (!id3v2_tag_init (&tag, version)) {
+ GST_WARNING_OBJECT (mux, "Unsupported version %d", version);
+ return NULL;
+ }
+
+ /* Render the tag */
+ gst_tag_list_foreach (taglist, foreach_add_tag, &tag);
+
+#if 0
+ /* Do we want to add our own signature to the tag somewhere? */
+ {
+ gchar *tag_producer_str;
+
+ tag_producer_str = g_strdup_printf ("(GStreamer id3v2mux %s, using "
+ "taglib %u.%u)", VERSION, TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION);
+ add_one_txxx_tag (id3v2tag, "tag_encoder", tag_producer_str);
+ g_free (tag_producer_str);
+ }
+#endif
+
+ /* Create buffer with tag */
+ buf = id3v2_tag_to_buffer (&tag);
+ gst_buffer_set_caps (buf, GST_PAD_CAPS (mux->srcpad));
+ GST_LOG_OBJECT (mux, "tag size = %d bytes", GST_BUFFER_SIZE (buf));
+
+ id3v2_tag_unset (&tag);
+
+ return buf;
+}
+
+#define ID3_V1_TAG_SIZE 128
+
+typedef void (*GstId3v1WriteFunc) (const GstTagList * list,
+ const gchar * gst_tag, guint8 * dst, int len);
+
+static void
+latin1_convert (const GstTagList * list, const gchar * tag,
+ guint8 * dst, int maxlen)
+{
+ gchar *str;
+ gsize len;
+ gchar *latin1;
+
+ if (!gst_tag_list_get_string (list, tag, &str))
+ return;
+
+ /* Convert to Latin-1 (ISO-8859-1), replacing unrepresentable characters
+ with '?' */
+ latin1 = g_convert_with_fallback (str, -1, "ISO-8859-1", "UTF-8", "?",
+ NULL, &len, NULL);
+
+ if (latin1) {
+ len = MIN (len, maxlen);
+ memcpy (dst, latin1, len);
+ g_free (latin1);
+ }
+
+ g_free (str);
+}
+
+static void
+date_v1_convert (const GstTagList * list, const gchar * tag,
+ guint8 * dst, int maxlen)
+{
+ GDate *date;
+
+ /* Only one date supported */
+ if (gst_tag_list_get_date_index (list, tag, 0, &date) && date != NULL) {
+ GDateYear year = g_date_get_year (date);
+ /* Check for plausible year */
+ if (year > 500 && year < 2100) {
+ gchar str[5];
+ g_snprintf (str, 5, "%.4u", year);
+ memcpy (dst, str, 4);
+ } else {
+ GST_WARNING ("invalid year %u, skipping", year);
+ }
+
+ g_date_free (date);
+ }
+}
+
+static void
+genre_v1_convert (const GstTagList * list, const gchar * tag,
+ guint8 * dst, int maxlen)
+{
+ gchar *str;
+ int genreidx = -1;
+ guint i, max;
+
+ /* We only support one genre */
+ if (!gst_tag_list_get_string_index (list, tag, 0, &str))
+ return;
+
+ max = gst_tag_id3_genre_count ();
+
+ for (i = 0; i < max; i++) {
+ const gchar *genre = gst_tag_id3_genre_get (i);
+ if (g_str_equal (str, genre)) {
+ genreidx = i;
+ break;
+ }
+ }
+
+ if (genreidx >= 0 && genreidx <= 127)
+ *dst = (guint8) genreidx;
+
+ g_free (str);
+}
+
+static void
+track_number_convert (const GstTagList * list, const gchar * tag,
+ guint8 * dst, int maxlen)
+{
+ guint tracknum;
+
+ /* We only support one track number */
+ if (!gst_tag_list_get_uint_index (list, tag, 0, &tracknum))
+ return;
+
+ if (tracknum <= 127)
+ *dst = (guint8) tracknum;
+}
+
+static const struct
+{
+ const gchar *gst_tag;
+ const gint offset;
+ const gint length;
+ const GstId3v1WriteFunc func;
+} v1_funcs[] = {
+ {
+ GST_TAG_TITLE, 3, 30, latin1_convert}, {
+ GST_TAG_ARTIST, 33, 30, latin1_convert}, {
+ GST_TAG_ALBUM, 63, 30, latin1_convert}, {
+ GST_TAG_DATE, 93, 4, date_v1_convert}, {
+ GST_TAG_COMMENT, 97, 28, latin1_convert}, {
+ /* Note: one-byte gap here */
+ GST_TAG_TRACK_NUMBER, 126, 1, track_number_convert}, {
+ GST_TAG_GENRE, 127, 1, genre_v1_convert}
+};
+
+GstBuffer *
+gst_id3mux_render_v1_tag (GstTagMux * mux, GstTagList * taglist)
+{
+ GstBuffer *buf = gst_buffer_new_and_alloc (ID3_V1_TAG_SIZE);
+ guint8 *data = GST_BUFFER_DATA (buf);
+ int i;
+
+ memset (data, 0, ID3_V1_TAG_SIZE);
+
+ data[0] = 'T';
+ data[1] = 'A';
+ data[2] = 'G';
+
+ for (i = 0; i < G_N_ELEMENTS (v1_funcs); i++) {
+ v1_funcs[i].func (taglist, v1_funcs[i].gst_tag, data + v1_funcs[i].offset,
+ v1_funcs[i].length);
+ }
+
+ gst_buffer_set_caps (buf, GST_PAD_CAPS (mux->srcpad));
+ return buf;
+}