/* GStreamer ID3v2 tag writer * * Copyright (C) 2006 Christophe Fergeau * Copyright (C) 2006-2009 Tim-Philipp Müller * Copyright (C) 2009 Pioneers of the Inevitable * * 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 #include GST_DEBUG_CATEGORY_EXTERN (gst_id3_mux_debug); #define GST_CAT_DEFAULT gst_id3_mux_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 * id3_mux_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, gboolean * wrote_tag); static void latin1_convert (const GstTagList * list, const gchar * tag, guint8 * dst, int maxlen, gboolean * wrote_tag) { gchar *str; gsize len; gchar *latin1; if (!gst_tag_list_get_string (list, tag, &str) || str == NULL) 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 != NULL && *latin1 != '\0') { len = MIN (len, maxlen); memcpy (dst, latin1, len); *wrote_tag = TRUE; g_free (latin1); } g_free (str); } static void date_v1_convert (const GstTagList * list, const gchar * tag, guint8 * dst, int maxlen, gboolean * wrote_tag) { 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); *wrote_tag = TRUE; 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, gboolean * wrote_tag) { gchar *str; int genreidx = -1; guint i, max; /* We only support one genre */ if (!gst_tag_list_get_string_index (list, tag, 0, &str) || str == NULL) 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; *wrote_tag = TRUE; } g_free (str); } static void track_number_convert (const GstTagList * list, const gchar * tag, guint8 * dst, int maxlen, gboolean * wrote_tag) { 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; *wrote_tag = TRUE; } } 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 * id3_mux_render_v1_tag (GstTagMux * mux, GstTagList * taglist) { GstBuffer *buf = gst_buffer_new_and_alloc (ID3_V1_TAG_SIZE); guint8 *data = GST_BUFFER_DATA (buf); gboolean wrote_tag = FALSE; int i; memset (data, 0, ID3_V1_TAG_SIZE); data[0] = 'T'; data[1] = 'A'; data[2] = 'G'; /* Genre #0 stands for 'Blues', so init genre field to an invalid number */ data[127] = 255; 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, &wrote_tag); } if (!wrote_tag) { GST_WARNING_OBJECT (mux, "no ID3v1 tag written (no suitable tags found)"); gst_buffer_unref (buf); return NULL; } gst_buffer_set_caps (buf, GST_PAD_CAPS (mux->srcpad)); return buf; }