| /* GStreamer taglib-based ID3v2 muxer |
| * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org> |
| * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net> |
| * |
| * 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-id3v2mux |
| * @see_also: #GstID3Demux, #GstTagSetter |
| * |
| * This element adds ID3v2 tags to the beginning of a stream using the taglib |
| * library. More precisely, the tags written are ID3 version 2.4.0 tags (which |
| * means in practice that some hardware players or outdated programs might not |
| * be able to read them properly). |
| * |
| * Applications can set the tags to write using the #GstTagSetter interface. |
| * Tags sent by upstream elements 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.ogg ! decodebin ! audioconvert ! lame ! id3v2mux ! filesink location=foo.mp3 |
| * ]| A pipeline that transcodes a file from Ogg/Vorbis to mp3 format with an |
| * ID3v2 that contains the same as the the Ogg/Vorbis file. Make sure the |
| * Ogg/Vorbis file actually has comments to preserve. |
| * |[ |
| * gst-launch-1.0 -m filesrc location=foo.mp3 ! id3demux ! fakesink silent=TRUE 2> /dev/null | grep taglist |
| * ]| Verify that tags have been written. |
| * </refsect2> |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include "gstid3v2mux.h" |
| |
| #include <string.h> |
| |
| #include <textidentificationframe.h> |
| #include <uniquefileidentifierframe.h> |
| #include <attachedpictureframe.h> |
| #include <relativevolumeframe.h> |
| #include <commentsframe.h> |
| #include <unknownframe.h> |
| #include <id3v2synchdata.h> |
| #include <id3v2tag.h> |
| #include <gst/tag/tag.h> |
| |
| using namespace TagLib; |
| |
| GST_DEBUG_CATEGORY_STATIC (gst_id3v2_mux_debug); |
| #define GST_CAT_DEFAULT gst_id3v2_mux_debug |
| |
| static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src", |
| GST_PAD_SRC, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("application/x-id3")); |
| |
| static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink", |
| GST_PAD_SINK, |
| GST_PAD_ALWAYS, |
| GST_STATIC_CAPS ("ANY")); |
| |
| G_DEFINE_TYPE (GstId3v2Mux, gst_id3v2_mux, GST_TYPE_TAG_MUX); |
| |
| static GstBuffer *gst_id3v2_mux_render_tag (GstTagMux * mux, |
| const GstTagList * taglist); |
| static GstBuffer *gst_id3v2_mux_render_end_tag (GstTagMux * mux, |
| const GstTagList * taglist); |
| |
| static void |
| gst_id3v2_mux_class_init (GstId3v2MuxClass * klass) |
| { |
| GstElementClass *element_class = GST_ELEMENT_CLASS (klass); |
| |
| GST_TAG_MUX_CLASS (klass)->render_start_tag = |
| GST_DEBUG_FUNCPTR (gst_id3v2_mux_render_tag); |
| GST_TAG_MUX_CLASS (klass)->render_end_tag = |
| GST_DEBUG_FUNCPTR (gst_id3v2_mux_render_end_tag); |
| |
| gst_element_class_add_static_pad_template (element_class, &sink_template); |
| gst_element_class_add_static_pad_template (element_class, &src_template); |
| |
| gst_element_class_set_static_metadata (element_class, |
| "TagLib-based ID3v2 Muxer", "Formatter/Metadata", |
| "Adds an ID3v2 header to the beginning of MP3 files using taglib", |
| "Christophe Fergeau <teuf@gnome.org>"); |
| |
| GST_DEBUG_CATEGORY_INIT (gst_id3v2_mux_debug, "id3v2mux", 0, |
| "taglib-based ID3v2 tag muxer"); |
| } |
| |
| static void |
| gst_id3v2_mux_init (GstId3v2Mux * id3v2mux) |
| { |
| /* nothing to do */ |
| } |
| |
| #if 0 |
| static void |
| add_one_txxx_tag (ID3v2::Tag * id3v2tag, const gchar * key, const gchar * val) |
| { |
| ID3v2::UserTextIdentificationFrame * frame; |
| |
| if (val == NULL) |
| return; |
| |
| GST_DEBUG ("Setting %s to %s", key, val); |
| frame = new ID3v2::UserTextIdentificationFrame (String::UTF8); |
| id3v2tag->addFrame (frame); |
| frame->setDescription (key); |
| frame->setText (val); |
| } |
| #endif |
| |
| typedef void (*GstId3v2MuxAddTagFunc) (ID3v2::Tag * id3v2tag, |
| const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * data); |
| |
| static void |
| add_encoder_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * unused) |
| { |
| TagLib::StringList string_list; |
| guint n; |
| |
| /* ENCODER_VERSION is either handled with the ENCODER tag or not at all */ |
| if (strcmp (tag, GST_TAG_ENCODER_VERSION) == 0) |
| return; |
| |
| 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); |
| string_list.append (String (s, String::UTF8)); |
| g_free (s); |
| g_free (encoder); |
| } |
| } |
| |
| if (!string_list.isEmpty ()) { |
| ID3v2::TextIdentificationFrame * f; |
| |
| f = new ID3v2::TextIdentificationFrame ("TSSE", String::UTF8); |
| id3v2tag->addFrame (f); |
| f->setText (string_list); |
| } else { |
| GST_WARNING ("Empty list for tag %s, skipping", tag); |
| } |
| } |
| |
| static void |
| add_date_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * unused) |
| { |
| TagLib::StringList string_list; |
| guint n; |
| |
| GST_LOG ("Adding date frame"); |
| |
| 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); |
| string_list.append (String (s, String::UTF8)); |
| g_free (s); |
| } else { |
| GST_WARNING ("invalid year %u, skipping", year); |
| } |
| |
| g_date_free (date); |
| } |
| } |
| |
| if (!string_list.isEmpty ()) { |
| ID3v2::TextIdentificationFrame * f; |
| |
| f = new ID3v2::TextIdentificationFrame ("TDRC", String::UTF8); |
| id3v2tag->addFrame (f); |
| f->setText (string_list); |
| } else { |
| GST_WARNING ("Empty list for tag %s, skipping", tag); |
| } |
| } |
| |
| static void |
| add_count_or_num_tag (ID3v2::Tag * 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)) { |
| ID3v2::TextIdentificationFrame * frame; |
| 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); |
| frame = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8); |
| id3v2tag->addFrame (frame); |
| frame->setText (tag_str); |
| 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)) { |
| ID3v2::TextIdentificationFrame * frame; |
| gchar *tag_str; |
| |
| tag_str = g_strdup_printf ("0/%u", count); |
| GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id); |
| frame = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8); |
| id3v2tag->addFrame (frame); |
| frame->setText (tag_str); |
| g_free (tag_str); |
| } |
| } |
| |
| if (num_tags > 1) { |
| GST_WARNING ("more than one %s, can only handle one", tag); |
| } |
| } |
| |
| static void |
| add_unique_file_id_tag (ID3v2::Tag * 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) { |
| ID3v2::UniqueFileIdentifierFrame * frame; |
| |
| GST_LOG ("Adding %s (%s): %s", tag, origin, id_str); |
| frame = new ID3v2::UniqueFileIdentifierFrame (origin, id_str); |
| id3v2tag->addFrame (frame); |
| g_free (id_str); |
| } |
| } |
| |
| static void |
| add_musicbrainz_tag (ID3v2::Tag * 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) { |
| ID3v2::UserTextIdentificationFrame * frame; |
| gchar *id_str; |
| |
| if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) { |
| GST_DEBUG ("Setting '%s' to '%s'", mb_ids[idx].spec_id, 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 */ |
| frame = new ID3v2::UserTextIdentificationFrame (String::Latin1); |
| id3v2tag->addFrame (frame); |
| frame->setDescription (mb_ids[idx].spec_id); |
| frame->setText (id_str); |
| |
| frame = new ID3v2::UserTextIdentificationFrame (String::Latin1); |
| id3v2tag->addFrame (frame); |
| frame->setDescription (mb_ids[idx].realworld_id); |
| frame->setText (id_str); |
| |
| g_free (id_str); |
| } |
| } |
| } |
| |
| static void |
| add_id3v2frame_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * frame_id) |
| { |
| ID3v2::FrameFactory * factory = ID3v2::FrameFactory::instance (); |
| guint i; |
| |
| for (i = 0; i < num_tags; ++i) { |
| ID3v2::Frame * frame; |
| const GValue *val; |
| GstBuffer *buf; |
| GstSample *sample; |
| |
| val = gst_tag_list_get_value_index (list, tag, i); |
| sample = (GstSample *) g_value_get_boxed (val); |
| |
| if (sample && (buf = gst_sample_get_buffer (sample)) && |
| gst_sample_get_caps (sample)) { |
| GstStructure *s; |
| gint version = 0; |
| |
| s = gst_caps_get_structure (gst_sample_get_caps (sample), 0); |
| if (s && gst_structure_get_int (s, "version", &version) && version > 0) { |
| GstMapInfo map; |
| |
| gst_buffer_map (buf, &map, GST_MAP_READ); |
| GST_DEBUG ("Injecting ID3v2.%u frame %u/%u of length %" G_GSIZE_FORMAT " and type %" |
| GST_PTR_FORMAT, version, i, num_tags, map.size, s); |
| |
| frame = factory->createFrame (ByteVector ((const char *) map.data, |
| map.size), (TagLib::uint) version); |
| if (frame) |
| id3v2tag->addFrame (frame); |
| |
| gst_buffer_unmap (buf, &map); |
| } |
| } |
| } |
| } |
| |
| static void |
| add_image_tag (ID3v2::Tag * 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; |
| GstSample *sample; |
| GstBuffer *image; |
| |
| GST_DEBUG ("image %u/%u", n + 1, num_tags); |
| |
| val = gst_tag_list_get_value_index (list, tag, n); |
| sample = (GstSample *) g_value_get_boxed (val); |
| |
| if (GST_IS_SAMPLE (sample) && (image = gst_sample_get_buffer (sample)) && |
| GST_IS_BUFFER (image) && gst_buffer_get_size (image) > 0 && |
| gst_sample_get_caps (sample) != NULL && |
| !gst_caps_is_empty (gst_sample_get_caps (sample))) { |
| const gchar *mime_type; |
| GstStructure *s; |
| |
| s = gst_caps_get_structure (gst_sample_get_caps (sample), 0); |
| mime_type = gst_structure_get_name (s); |
| if (mime_type != NULL) { |
| ID3v2::AttachedPictureFrame * frame; |
| const gchar *desc = NULL; |
| GstMapInfo map; |
| const GstStructure *info_struct; |
| |
| info_struct = gst_sample_get_info (sample); |
| if (!info_struct |
| || !gst_structure_has_name (info_struct, "GstTagImageInfo")) |
| info_struct = NULL; |
| |
| if (strcmp (mime_type, "text/uri-list") == 0) |
| mime_type = "-->"; |
| |
| frame = new ID3v2::AttachedPictureFrame (); |
| |
| gst_buffer_map (image, &map, GST_MAP_READ); |
| |
| GST_DEBUG ("Attaching picture of %" G_GSIZE_FORMAT " bytes and mime type %s", |
| map.size, mime_type); |
| |
| id3v2tag->addFrame (frame); |
| frame->setPicture (ByteVector ((const char *) map.data, map.size)); |
| frame->setTextEncoding (String::UTF8); |
| frame->setMimeType (mime_type); |
| |
| gst_buffer_unmap (image, &map); |
| |
| if (info_struct) |
| desc = gst_structure_get_string (info_struct, "image-description"); |
| |
| frame->setDescription ((desc) ? desc : ""); |
| |
| if (strcmp (tag, GST_TAG_PREVIEW_IMAGE) == 0) { |
| frame->setType (ID3v2::AttachedPictureFrame::FileIcon); |
| } else { |
| int image_type = ID3v2::AttachedPictureFrame::Other; |
| |
| if (info_struct) { |
| if (gst_structure_get (info_struct, "image-type", |
| GST_TYPE_TAG_IMAGE_TYPE, &image_type, NULL)) { |
| if (image_type > 0 && image_type <= 18) { |
| image_type += 2; |
| } else { |
| image_type = ID3v2::AttachedPictureFrame::Other; |
| } |
| } |
| } |
| |
| frame->setType ((TagLib::ID3v2::AttachedPictureFrame::Type) image_type); |
| } |
| } |
| } else { |
| GST_WARNING ("NULL image or no caps on image sample (%p, caps=%" |
| GST_PTR_FORMAT ")", sample, |
| (sample) ? gst_sample_get_caps (sample) : NULL); |
| } |
| } |
| } |
| |
| static void |
| add_comment_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * unused) |
| { |
| TagLib::StringList string_list; |
| 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) { |
| ID3v2::CommentsFrame * f; |
| gchar *desc = NULL, *val = NULL, *lang = NULL; |
| |
| f = new ID3v2::CommentsFrame (String::UTF8); |
| |
| if (strcmp (tag, GST_TAG_COMMENT) == 0 || |
| !gst_tag_parse_extended_comment (s, &desc, &lang, &val, TRUE)) { |
| /* create dummy description to allow for multiple comment frames |
| * (taglib will drop comment frames if descriptions are not unique) */ |
| desc = g_strdup_printf ("c%u", n); |
| val = g_strdup (s); |
| } |
| |
| GST_LOG ("%s[%u] = '%s' (%s|%s|%s)", tag, n, s, GST_STR_NULL (desc), |
| GST_STR_NULL (lang), GST_STR_NULL (val)); |
| |
| f->setDescription (desc); |
| f->setText (val); |
| if (lang) { |
| f->setLanguage (lang); |
| } |
| |
| g_free (lang); |
| g_free (desc); |
| g_free (val); |
| |
| id3v2tag->addFrame (f); |
| } |
| g_free (s); |
| } |
| } |
| |
| static void |
| add_text_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * frame_id) |
| { |
| ID3v2::TextIdentificationFrame * f; |
| TagLib::StringList string_list; |
| guint n; |
| |
| GST_LOG ("Adding '%s' frame", frame_id); |
| for (n = 0; n < num_tags; ++n) { |
| gchar *s = NULL; |
| |
| if (gst_tag_list_get_string_index (list, tag, n, &s) && s != NULL) { |
| GST_LOG ("%s: %s[%u] = '%s'", frame_id, tag, n, s); |
| string_list.append (String (s, String::UTF8)); |
| g_free (s); |
| } |
| } |
| |
| if (!string_list.isEmpty ()) { |
| f = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8); |
| id3v2tag->addFrame (f); |
| f->setText (string_list); |
| } else { |
| GST_WARNING ("Empty list for tag %s, skipping", tag); |
| } |
| } |
| |
| static void |
| add_uri_tag (ID3v2::Tag * 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)) { |
| ID3v2::FrameFactory * factory = ID3v2::FrameFactory::instance (); |
| ID3v2::Frame * frame; |
| char *data; |
| |
| data = g_new0 (char, 10 + url_len); |
| |
| memcpy (data, frame_id, 4); |
| memcpy (data + 4, ID3v2::SynchData::fromUInt (url_len).data (), 4); |
| memcpy (data + 10, url, url_len); |
| ByteVector bytes (data, 10 + url_len); |
| |
| g_free (data); |
| |
| frame = factory->createFrame (bytes, (TagLib::uint) 4); |
| if (frame) { |
| id3v2tag->addFrame (frame); |
| |
| GST_LOG ("%s: %s = '%s'", frame_id, tag, url); |
| } |
| } else { |
| GST_WARNING ("Tag %s does not contain a valid URI (%s)", tag, url); |
| } |
| |
| g_free (url); |
| } |
| } |
| |
| static void |
| add_relative_volume_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * frame_id) |
| { |
| const char *gain_tag_name; |
| const char *peak_tag_name; |
| gdouble peak_val; |
| gdouble gain_val; |
| ID3v2::RelativeVolumeFrame * frame; |
| |
| frame = new ID3v2::RelativeVolumeFrame (); |
| |
| /* 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; |
| frame->setIdentification ("track"); |
| GST_DEBUG ("adding track relative-volume frame"); |
| } else { |
| gain_tag_name = GST_TAG_ALBUM_GAIN; |
| peak_tag_name = GST_TAG_ALBUM_PEAK; |
| frame->setIdentification ("album"); |
| 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) { |
| ID3v2::RelativeVolumeFrame::PeakVolume encoded_peak; |
| short peak_int; |
| |
| 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); |
| frame->setVolumeAdjustment (gain_val); |
| } |
| |
| /* copying mutagen: always write as 16 bits for sanity. */ |
| peak_int = (short)(peak_val * G_MAXSHORT); |
| encoded_peak.bitsRepresentingPeak = 16; |
| encoded_peak.peakVolume = ByteVector::fromShort(peak_int, true); |
| GST_DEBUG ("setting peak value %g", peak_val); |
| frame->setPeakVolume(encoded_peak); |
| |
| } else { |
| gst_tag_list_get_double (list, tag, &gain_val); |
| GST_DEBUG ("setting volume adjustment %g", gain_val); |
| frame->setVolumeAdjustment (gain_val); |
| |
| 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"); |
| delete frame; |
| return; |
| } |
| } |
| |
| id3v2tag->addFrame (frame); |
| } |
| |
| static void |
| add_bpm_tag (ID3v2::Tag * id3v2tag, const GstTagList * list, |
| const gchar * tag, guint num_tags, const gchar * frame_id) |
| { |
| gdouble bpm; |
| |
| if (gst_tag_list_get_double_index (list, tag, 0, &bpm)) { |
| ID3v2::TextIdentificationFrame * frame; |
| gchar *tag_str; |
| |
| tag_str = g_strdup_printf ("%u", (guint)bpm); |
| |
| GST_DEBUG ("Setting %s to %s", tag, tag_str); |
| frame = new ID3v2::TextIdentificationFrame ("TBPM", String::UTF8); |
| id3v2tag->addFrame (frame); |
| frame->setText (tag_str); |
| g_free (tag_str); |
| } |
| } |
| |
| /* 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 GstId3v2MuxAddTagFunc func; |
| const gchar data[5]; |
| } add_funcs[] = { |
| { |
| GST_TAG_ARTIST, add_text_tag, "TPE1"}, { |
| GST_TAG_ALBUM_ARTIST, add_text_tag, "TPE2"}, { |
| 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"}, { |
| GST_TAG_COMMENT, add_comment_tag, ""}, { |
| GST_TAG_EXTENDED_COMMENT, add_comment_tag, ""}, { |
| GST_TAG_DATE, add_date_tag, ""}, { |
| GST_TAG_IMAGE, add_image_tag, ""}, { |
| GST_TAG_PREVIEW_IMAGE, add_image_tag, ""}, { |
| GST_ID3_DEMUX_TAG_ID3V2_FRAME, add_id3v2frame_tag, ""}, { |
| 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, ""}, { |
| GST_TAG_ARTIST_SORTNAME, add_text_tag, "TSOP"}, { |
| GST_TAG_ALBUM_SORTNAME, add_text_tag, "TSOA"}, { |
| GST_TAG_TITLE_SORTNAME, add_text_tag, "TSOT"}, { |
| 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"}, { |
| GST_TAG_ENCODER, add_encoder_tag, ""}, { |
| GST_TAG_ENCODER_VERSION, add_encoder_tag, ""}, { |
| GST_TAG_COPYRIGHT_URI, add_uri_tag, "WCOP"}, { |
| GST_TAG_LICENSE_URI, add_uri_tag, "WCOP"}, { |
| GST_TAG_TRACK_PEAK, add_relative_volume_tag, ""}, { |
| GST_TAG_TRACK_GAIN, add_relative_volume_tag, ""}, { |
| GST_TAG_ALBUM_PEAK, add_relative_volume_tag, ""}, { |
| GST_TAG_ALBUM_GAIN, add_relative_volume_tag, ""}, { |
| GST_TAG_BEATS_PER_MINUTE, add_bpm_tag, ""} |
| }; |
| |
| |
| static void |
| foreach_add_tag (const GstTagList * list, const gchar * tag, gpointer userdata) |
| { |
| ID3v2::Tag * id3v2tag = (ID3v2::Tag *) userdata; |
| TagLib::StringList string_list; |
| 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); |
| } |
| } |
| |
| static GstBuffer * |
| gst_id3v2_mux_render_tag (GstTagMux * mux, const GstTagList * taglist) |
| { |
| ID3v2::Tag id3v2tag; |
| ByteVector rendered_tag; |
| GstBuffer *buf; |
| guint tag_size; |
| |
| /* write all strings as UTF-8 by default */ |
| TagLib::ID3v2::FrameFactory::instance ()-> |
| setDefaultTextEncoding (TagLib::String::UTF8); |
| |
| /* Render the tag */ |
| gst_tag_list_foreach (taglist, foreach_add_tag, &id3v2tag); |
| |
| #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 |
| |
| rendered_tag = id3v2tag.render (); |
| tag_size = rendered_tag.size (); |
| |
| GST_LOG_OBJECT (mux, "tag size = %d bytes", tag_size); |
| |
| /* Create buffer with tag */ |
| buf = gst_buffer_new_and_alloc (tag_size); |
| gst_buffer_fill (buf, 0, rendered_tag.data (), tag_size); |
| |
| return buf; |
| } |
| |
| static GstBuffer * |
| gst_id3v2_mux_render_end_tag (GstTagMux * mux, const GstTagList * taglist) |
| { |
| return NULL; |
| } |