1 /* GStreamer taglib-based ID3v2 muxer
2  * Copyright (C) 2006 Christophe Fergeau <teuf@gnome.org>
3  * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this library; if not, write to the
17  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20 
21 /**
22  * SECTION:element-id3v2mux
23  * @see_also: #GstID3Demux, #GstTagSetter
24  *
25  * This element adds ID3v2 tags to the beginning of a stream using the taglib
26  * library. More precisely, the tags written are ID3 version 2.4.0 tags (which
27  * means in practice that some hardware players or outdated programs might not
28  * be able to read them properly).
29  *
30  * Applications can set the tags to write using the #GstTagSetter interface.
31  * Tags sent by upstream elements will be picked up automatically (and merged
32  * according to the merge mode set via the tag setter interface).
33  *
34  * <refsect2>
35  * <title>Example pipelines</title>
36  * |[
37  * gst-launch-1.0 -v filesrc location=foo.ogg ! decodebin ! audioconvert ! lame ! id3v2mux ! filesink location=foo.mp3
38  * ]| A pipeline that transcodes a file from Ogg/Vorbis to mp3 format with an
39  * ID3v2 that contains the same as the the Ogg/Vorbis file. Make sure the
40  * Ogg/Vorbis file actually has comments to preserve.
41  * |[
42  * gst-launch-1.0 -m filesrc location=foo.mp3 ! id3demux ! fakesink silent=TRUE 2&gt; /dev/null | grep taglist
43  * ]| Verify that tags have been written.
44  * </refsect2>
45  */
46 
47 #ifdef HAVE_CONFIG_H
48 #include <config.h>
49 #endif
50 
51 #include "gstid3v2mux.h"
52 
53 #include <string.h>
54 
55 #include <textidentificationframe.h>
56 #include <uniquefileidentifierframe.h>
57 #include <attachedpictureframe.h>
58 #include <relativevolumeframe.h>
59 #include <commentsframe.h>
60 #include <unknownframe.h>
61 #include <id3v2synchdata.h>
62 #include <id3v2tag.h>
63 #include <gst/tag/tag.h>
64 
65 using namespace TagLib;
66 
67 GST_DEBUG_CATEGORY_STATIC (gst_id3v2_mux_debug);
68 #define GST_CAT_DEFAULT gst_id3v2_mux_debug
69 
70 static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
71     GST_PAD_SRC,
72     GST_PAD_ALWAYS,
73     GST_STATIC_CAPS ("application/x-id3"));
74 
75 static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
76     GST_PAD_SINK,
77     GST_PAD_ALWAYS,
78     GST_STATIC_CAPS ("ANY"));
79 
80 G_DEFINE_TYPE (GstId3v2Mux, gst_id3v2_mux, GST_TYPE_TAG_MUX);
81 
82 static GstBuffer *gst_id3v2_mux_render_tag (GstTagMux * mux,
83     const GstTagList * taglist);
84 static GstBuffer *gst_id3v2_mux_render_end_tag (GstTagMux * mux,
85     const GstTagList * taglist);
86 
87 static void
gst_id3v2_mux_class_init(GstId3v2MuxClass * klass)88 gst_id3v2_mux_class_init (GstId3v2MuxClass * klass)
89 {
90   GstElementClass *element_class = GST_ELEMENT_CLASS (klass);
91 
92   GST_TAG_MUX_CLASS (klass)->render_start_tag =
93       GST_DEBUG_FUNCPTR (gst_id3v2_mux_render_tag);
94   GST_TAG_MUX_CLASS (klass)->render_end_tag =
95       GST_DEBUG_FUNCPTR (gst_id3v2_mux_render_end_tag);
96 
97   gst_element_class_add_static_pad_template (element_class, &sink_template);
98   gst_element_class_add_static_pad_template (element_class, &src_template);
99 
100   gst_element_class_set_static_metadata (element_class,
101       "TagLib-based ID3v2 Muxer", "Formatter/Metadata",
102       "Adds an ID3v2 header to the beginning of MP3 files using taglib",
103       "Christophe Fergeau <teuf@gnome.org>");
104 
105   GST_DEBUG_CATEGORY_INIT (gst_id3v2_mux_debug, "id3v2mux", 0,
106       "taglib-based ID3v2 tag muxer");
107 }
108 
109 static void
gst_id3v2_mux_init(GstId3v2Mux * id3v2mux)110 gst_id3v2_mux_init (GstId3v2Mux * id3v2mux)
111 {
112   /* nothing to do */
113 }
114 
115 #if 0
116 static void
117 add_one_txxx_tag (ID3v2::Tag * id3v2tag, const gchar * key, const gchar * val)
118 {
119   ID3v2::UserTextIdentificationFrame * frame;
120 
121   if (val == NULL)
122     return;
123 
124   GST_DEBUG ("Setting %s to %s", key, val);
125   frame = new ID3v2::UserTextIdentificationFrame (String::UTF8);
126   id3v2tag->addFrame (frame);
127   frame->setDescription (key);
128   frame->setText (val);
129 }
130 #endif
131 
132 typedef void (*GstId3v2MuxAddTagFunc) (ID3v2::Tag * id3v2tag,
133     const GstTagList * list,
134     const gchar * tag, guint num_tags, const gchar * data);
135 
136 static void
add_encoder_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * unused)137 add_encoder_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
138     const gchar * tag, guint num_tags, const gchar * unused)
139 {
140   TagLib::StringList string_list;
141   guint n;
142 
143   /* ENCODER_VERSION is either handled with the ENCODER tag or not at all */
144   if (strcmp (tag, GST_TAG_ENCODER_VERSION) == 0)
145     return;
146 
147   for (n = 0; n < num_tags; ++n) {
148     gchar *encoder = NULL;
149 
150     if (gst_tag_list_get_string_index (list, tag, n, &encoder) && encoder) {
151       guint encoder_version;
152       gchar *s;
153 
154       if (gst_tag_list_get_uint_index (list, GST_TAG_ENCODER_VERSION, n,
155               &encoder_version) && encoder_version > 0) {
156         s = g_strdup_printf ("%s %u", encoder, encoder_version);
157       } else {
158         s = g_strdup (encoder);
159       }
160 
161       GST_LOG ("encoder[%u] = '%s'", n, s);
162       string_list.append (String (s, String::UTF8));
163       g_free (s);
164       g_free (encoder);
165     }
166   }
167 
168   if (!string_list.isEmpty ()) {
169     ID3v2::TextIdentificationFrame * f;
170 
171     f = new ID3v2::TextIdentificationFrame ("TSSE", String::UTF8);
172     id3v2tag->addFrame (f);
173     f->setText (string_list);
174   } else {
175     GST_WARNING ("Empty list for tag %s, skipping", tag);
176   }
177 }
178 
179 static void
add_date_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * unused)180 add_date_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
181     const gchar * tag, guint num_tags, const gchar * unused)
182 {
183   TagLib::StringList string_list;
184   guint n;
185 
186   GST_LOG ("Adding date frame");
187 
188   for (n = 0; n < num_tags; ++n) {
189     GDate *date = NULL;
190 
191     if (gst_tag_list_get_date_index (list, tag, n, &date) && date != NULL) {
192       GDateYear year;
193       gchar *s;
194 
195       year = g_date_get_year (date);
196       if (year > 500 && year < 2100) {
197         s = g_strdup_printf ("%u", year);
198         GST_LOG ("%s[%u] = '%s'", tag, n, s);
199         string_list.append (String (s, String::UTF8));
200         g_free (s);
201       } else {
202         GST_WARNING ("invalid year %u, skipping", year);
203       }
204 
205       g_date_free (date);
206     }
207   }
208 
209   if (!string_list.isEmpty ()) {
210     ID3v2::TextIdentificationFrame * f;
211 
212     f = new ID3v2::TextIdentificationFrame ("TDRC", String::UTF8);
213     id3v2tag->addFrame (f);
214     f->setText (string_list);
215   } else {
216     GST_WARNING ("Empty list for tag %s, skipping", tag);
217   }
218 }
219 
220 static void
add_count_or_num_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)221 add_count_or_num_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
222     const gchar * tag, guint num_tags, const gchar * frame_id)
223 {
224   static const struct
225   {
226     const gchar *gst_tag;
227     const gchar *corr_count;    /* corresponding COUNT tag (if number) */
228     const gchar *corr_num;      /* corresponding NUMBER tag (if count) */
229   } corr[] = {
230     {
231     GST_TAG_TRACK_NUMBER, GST_TAG_TRACK_COUNT, NULL}, {
232     GST_TAG_TRACK_COUNT, NULL, GST_TAG_TRACK_NUMBER}, {
233     GST_TAG_ALBUM_VOLUME_NUMBER, GST_TAG_ALBUM_VOLUME_COUNT, NULL}, {
234     GST_TAG_ALBUM_VOLUME_COUNT, NULL, GST_TAG_ALBUM_VOLUME_NUMBER}
235   };
236   guint idx;
237 
238   for (idx = 0; idx < G_N_ELEMENTS (corr); ++idx) {
239     if (strcmp (corr[idx].gst_tag, tag) == 0)
240       break;
241   }
242 
243   g_assert (idx < G_N_ELEMENTS (corr));
244   g_assert (frame_id && strlen (frame_id) == 4);
245 
246   if (corr[idx].corr_num == NULL) {
247     guint number;
248 
249     /* number tag */
250     if (gst_tag_list_get_uint_index (list, tag, 0, &number)) {
251       ID3v2::TextIdentificationFrame * frame;
252       gchar *tag_str;
253       guint count;
254 
255       if (gst_tag_list_get_uint_index (list, corr[idx].corr_count, 0, &count))
256         tag_str = g_strdup_printf ("%u/%u", number, count);
257       else
258         tag_str = g_strdup_printf ("%u", number);
259 
260       GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
261       frame = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8);
262       id3v2tag->addFrame (frame);
263       frame->setText (tag_str);
264       g_free (tag_str);
265     }
266   } else if (corr[idx].corr_count == NULL) {
267     guint count;
268 
269     /* count tag */
270     if (gst_tag_list_get_uint_index (list, corr[idx].corr_num, 0, &count)) {
271       GST_DEBUG ("%s handled with %s, skipping", tag, corr[idx].corr_num);
272     } else if (gst_tag_list_get_uint_index (list, tag, 0, &count)) {
273       ID3v2::TextIdentificationFrame * frame;
274       gchar *tag_str;
275 
276       tag_str = g_strdup_printf ("0/%u", count);
277       GST_DEBUG ("Setting %s to %s (frame_id = %s)", tag, tag_str, frame_id);
278       frame = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8);
279       id3v2tag->addFrame (frame);
280       frame->setText (tag_str);
281       g_free (tag_str);
282     }
283   }
284 
285   if (num_tags > 1) {
286     GST_WARNING ("more than one %s, can only handle one", tag);
287   }
288 }
289 
290 static void
add_unique_file_id_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * unused)291 add_unique_file_id_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
292     const gchar * tag, guint num_tags, const gchar * unused)
293 {
294   const gchar *origin = "http://musicbrainz.org";
295   gchar *id_str = NULL;
296 
297   if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
298     ID3v2::UniqueFileIdentifierFrame * frame;
299 
300     GST_LOG ("Adding %s (%s): %s", tag, origin, id_str);
301     frame = new ID3v2::UniqueFileIdentifierFrame (origin, id_str);
302     id3v2tag->addFrame (frame);
303     g_free (id_str);
304   }
305 }
306 
307 static void
add_musicbrainz_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * data)308 add_musicbrainz_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
309     const gchar * tag, guint num_tags, const gchar * data)
310 {
311   static const struct
312   {
313     const gchar gst_tag[28];
314     const gchar spec_id[28];
315     const gchar realworld_id[28];
316   } mb_ids[] = {
317     {
318     GST_TAG_MUSICBRAINZ_ARTISTID, "MusicBrainz Artist Id",
319           "musicbrainz_artistid"}, {
320     GST_TAG_MUSICBRAINZ_ALBUMID, "MusicBrainz Album Id", "musicbrainz_albumid"}, {
321     GST_TAG_MUSICBRAINZ_ALBUMARTISTID, "MusicBrainz Album Artist Id",
322           "musicbrainz_albumartistid"}, {
323     GST_TAG_MUSICBRAINZ_TRMID, "MusicBrainz TRM Id", "musicbrainz_trmid"}, {
324     GST_TAG_CDDA_MUSICBRAINZ_DISCID, "MusicBrainz DiscID",
325           "musicbrainz_discid"}, {
326       /* the following one is more or less made up, there seems to be little
327        * evidence that any popular application is actually putting this info
328        * into TXXX frames; the first one comes from a musicbrainz wiki 'proposed
329        * tags' page, the second one is analogue to the vorbis/ape/flac tag. */
330     GST_TAG_CDDA_CDDB_DISCID, "CDDB DiscID", "discid"}
331   };
332   guint i, idx;
333 
334   idx = (guint8) data[0];
335   g_assert (idx < G_N_ELEMENTS (mb_ids));
336 
337   for (i = 0; i < num_tags; ++i) {
338     ID3v2::UserTextIdentificationFrame * frame;
339     gchar *id_str;
340 
341     if (gst_tag_list_get_string_index (list, tag, 0, &id_str) && id_str) {
342       GST_DEBUG ("Setting '%s' to '%s'", mb_ids[idx].spec_id, id_str);
343 
344       /* add two frames, one with the ID the musicbrainz.org spec mentions
345        * and one with the ID that applications use in the real world */
346       frame = new ID3v2::UserTextIdentificationFrame (String::Latin1);
347       id3v2tag->addFrame (frame);
348       frame->setDescription (mb_ids[idx].spec_id);
349       frame->setText (id_str);
350 
351       frame = new ID3v2::UserTextIdentificationFrame (String::Latin1);
352       id3v2tag->addFrame (frame);
353       frame->setDescription (mb_ids[idx].realworld_id);
354       frame->setText (id_str);
355 
356       g_free (id_str);
357     }
358   }
359 }
360 
361 static void
add_id3v2frame_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)362 add_id3v2frame_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
363     const gchar * tag, guint num_tags, const gchar * frame_id)
364 {
365   ID3v2::FrameFactory * factory = ID3v2::FrameFactory::instance ();
366   guint i;
367 
368   for (i = 0; i < num_tags; ++i) {
369     ID3v2::Frame * frame;
370     const GValue *val;
371     GstBuffer *buf;
372     GstSample *sample;
373 
374     val = gst_tag_list_get_value_index (list, tag, i);
375     sample = (GstSample *) g_value_get_boxed (val);
376 
377     if (sample && (buf = gst_sample_get_buffer (sample)) &&
378         gst_sample_get_caps (sample)) {
379       GstStructure *s;
380       gint version = 0;
381 
382       s = gst_caps_get_structure (gst_sample_get_caps (sample), 0);
383       if (s && gst_structure_get_int (s, "version", &version) && version > 0) {
384         GstMapInfo map;
385 
386         gst_buffer_map (buf, &map, GST_MAP_READ);
387         GST_DEBUG ("Injecting ID3v2.%u frame %u/%u of length %" G_GSIZE_FORMAT " and type %"
388             GST_PTR_FORMAT, version, i, num_tags, map.size, s);
389 
390         frame = factory->createFrame (ByteVector ((const char *) map.data,
391                 map.size), (TagLib::uint) version);
392         if (frame)
393           id3v2tag->addFrame (frame);
394 
395         gst_buffer_unmap (buf, &map);
396       }
397     }
398   }
399 }
400 
401 static void
add_image_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * unused)402 add_image_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
403     const gchar * tag, guint num_tags, const gchar * unused)
404 {
405   guint n;
406 
407   for (n = 0; n < num_tags; ++n) {
408     const GValue *val;
409     GstSample *sample;
410     GstBuffer *image;
411 
412     GST_DEBUG ("image %u/%u", n + 1, num_tags);
413 
414     val = gst_tag_list_get_value_index (list, tag, n);
415     sample = (GstSample *) g_value_get_boxed (val);
416 
417     if (GST_IS_SAMPLE (sample) && (image = gst_sample_get_buffer (sample)) &&
418         GST_IS_BUFFER (image) && gst_buffer_get_size (image) > 0 &&
419         gst_sample_get_caps (sample) != NULL &&
420         !gst_caps_is_empty (gst_sample_get_caps (sample))) {
421       const gchar *mime_type;
422       GstStructure *s;
423 
424       s = gst_caps_get_structure (gst_sample_get_caps (sample), 0);
425       mime_type = gst_structure_get_name (s);
426       if (mime_type != NULL) {
427         ID3v2::AttachedPictureFrame * frame;
428         const gchar *desc = NULL;
429         GstMapInfo map;
430         const GstStructure *info_struct;
431 
432         info_struct = gst_sample_get_info (sample);
433         if (!info_struct
434             || !gst_structure_has_name (info_struct, "GstTagImageInfo"))
435           info_struct = NULL;
436 
437         if (strcmp (mime_type, "text/uri-list") == 0)
438           mime_type = "-->";
439 
440         frame = new ID3v2::AttachedPictureFrame ();
441 
442         gst_buffer_map (image, &map, GST_MAP_READ);
443 
444         GST_DEBUG ("Attaching picture of %" G_GSIZE_FORMAT " bytes and mime type %s",
445             map.size, mime_type);
446 
447         id3v2tag->addFrame (frame);
448         frame->setPicture (ByteVector ((const char *) map.data, map.size));
449         frame->setTextEncoding (String::UTF8);
450         frame->setMimeType (mime_type);
451 
452         gst_buffer_unmap (image, &map);
453 
454         if (info_struct)
455           desc = gst_structure_get_string (info_struct, "image-description");
456 
457         frame->setDescription ((desc) ? desc : "");
458 
459         if (strcmp (tag, GST_TAG_PREVIEW_IMAGE) == 0) {
460           frame->setType (ID3v2::AttachedPictureFrame::FileIcon);
461         } else {
462           int image_type = ID3v2::AttachedPictureFrame::Other;
463 
464           if (info_struct) {
465             if (gst_structure_get (info_struct, "image-type",
466                     GST_TYPE_TAG_IMAGE_TYPE, &image_type, (void *) NULL)) {
467               if (image_type > 0 && image_type <= 18) {
468                 image_type += 2;
469               } else {
470                 image_type = ID3v2::AttachedPictureFrame::Other;
471               }
472             }
473           }
474 
475           frame->setType ((TagLib::ID3v2::AttachedPictureFrame::Type) image_type);
476         }
477       }
478     } else {
479       GST_WARNING ("NULL image or no caps on image sample (%p, caps=%"
480           GST_PTR_FORMAT ")", sample,
481           (sample) ? gst_sample_get_caps (sample) : NULL);
482     }
483   }
484 }
485 
486 static void
add_comment_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * unused)487 add_comment_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
488     const gchar * tag, guint num_tags, const gchar * unused)
489 {
490   TagLib::StringList string_list;
491   guint n;
492 
493   GST_LOG ("Adding comment frames");
494   for (n = 0; n < num_tags; ++n) {
495     gchar *s = NULL;
496 
497     if (gst_tag_list_get_string_index (list, tag, n, &s) && s != NULL) {
498       ID3v2::CommentsFrame * f;
499       gchar *desc = NULL, *val = NULL, *lang = NULL;
500 
501       f = new ID3v2::CommentsFrame (String::UTF8);
502 
503       if (strcmp (tag, GST_TAG_COMMENT) == 0 ||
504           !gst_tag_parse_extended_comment (s, &desc, &lang, &val, TRUE)) {
505         /* create dummy description to allow for multiple comment frames
506          * (taglib will drop comment frames if descriptions are not unique) */
507         desc = g_strdup_printf ("c%u", n);
508         val = g_strdup (s);
509       }
510 
511       GST_LOG ("%s[%u] = '%s' (%s|%s|%s)", tag, n, s, GST_STR_NULL (desc),
512           GST_STR_NULL (lang), GST_STR_NULL (val));
513 
514       f->setDescription (desc);
515       f->setText (val);
516       if (lang) {
517         f->setLanguage (lang);
518       }
519 
520       g_free (lang);
521       g_free (desc);
522       g_free (val);
523 
524       id3v2tag->addFrame (f);
525     }
526     g_free (s);
527   }
528 }
529 
530 static void
add_text_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)531 add_text_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
532     const gchar * tag, guint num_tags, const gchar * frame_id)
533 {
534   ID3v2::TextIdentificationFrame * f;
535   TagLib::StringList string_list;
536   guint n;
537 
538   GST_LOG ("Adding '%s' frame", frame_id);
539   for (n = 0; n < num_tags; ++n) {
540     gchar *s = NULL;
541 
542     if (gst_tag_list_get_string_index (list, tag, n, &s) && s != NULL) {
543       GST_LOG ("%s: %s[%u] = '%s'", frame_id, tag, n, s);
544       string_list.append (String (s, String::UTF8));
545       g_free (s);
546     }
547   }
548 
549   if (!string_list.isEmpty ()) {
550     f = new ID3v2::TextIdentificationFrame (frame_id, String::UTF8);
551     id3v2tag->addFrame (f);
552     f->setText (string_list);
553   } else {
554     GST_WARNING ("Empty list for tag %s, skipping", tag);
555   }
556 }
557 
558 static void
add_uri_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)559 add_uri_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
560     const gchar * tag, guint num_tags, const gchar * frame_id)
561 {
562   gchar *url = NULL;
563 
564   g_assert (frame_id != NULL);
565 
566   /* URI tags are limited to one of each per taglist */
567   if (gst_tag_list_get_string_index (list, tag, 0, &url) && url != NULL) {
568     guint url_len;
569 
570     url_len = strlen (url);
571     if (url_len > 0 && gst_uri_is_valid (url)) {
572       ID3v2::FrameFactory * factory = ID3v2::FrameFactory::instance ();
573       ID3v2::Frame * frame;
574       char *data;
575 
576       data = g_new0 (char, 10 + url_len);
577 
578       memcpy (data, frame_id, 4);
579       memcpy (data + 4, ID3v2::SynchData::fromUInt (url_len).data (), 4);
580       memcpy (data + 10, url, url_len);
581       ByteVector bytes (data, 10 + url_len);
582 
583       g_free (data);
584 
585       frame = factory->createFrame (bytes, (TagLib::uint) 4);
586       if (frame) {
587         id3v2tag->addFrame (frame);
588 
589         GST_LOG ("%s: %s = '%s'", frame_id, tag, url);
590       }
591     } else {
592       GST_WARNING ("Tag %s does not contain a valid URI (%s)", tag, url);
593     }
594 
595     g_free (url);
596   }
597 }
598 
599 static void
add_relative_volume_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)600 add_relative_volume_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
601     const gchar * tag, guint num_tags, const gchar * frame_id)
602 {
603   const char *gain_tag_name;
604   const char *peak_tag_name;
605   gdouble peak_val;
606   gdouble gain_val;
607   ID3v2::RelativeVolumeFrame * frame;
608 
609   frame = new ID3v2::RelativeVolumeFrame ();
610 
611   /* figure out tag names and the identification string to use */
612   if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
613       strcmp (tag, GST_TAG_TRACK_GAIN) == 0) {
614     gain_tag_name = GST_TAG_TRACK_GAIN;
615     peak_tag_name = GST_TAG_TRACK_PEAK;
616     frame->setIdentification ("track");
617     GST_DEBUG ("adding track relative-volume frame");
618   } else {
619     gain_tag_name = GST_TAG_ALBUM_GAIN;
620     peak_tag_name = GST_TAG_ALBUM_PEAK;
621     frame->setIdentification ("album");
622     GST_DEBUG ("adding album relative-volume frame");
623   }
624 
625   /* find the value for the paired tag (gain, if this is peak, and
626    * vice versa).  if both tags exist, only write the frame when
627    * we're processing the peak tag.
628    */
629   if (strcmp (tag, GST_TAG_TRACK_PEAK) == 0 ||
630       strcmp (tag, GST_TAG_ALBUM_PEAK) == 0) {
631     ID3v2::RelativeVolumeFrame::PeakVolume encoded_peak;
632     short peak_int;
633 
634     gst_tag_list_get_double (list, tag, &peak_val);
635 
636     if (gst_tag_list_get_tag_size (list, gain_tag_name) > 0) {
637       gst_tag_list_get_double (list, gain_tag_name, &gain_val);
638       GST_DEBUG ("setting volume adjustment %g", gain_val);
639       frame->setVolumeAdjustment (gain_val);
640     }
641 
642     /* copying mutagen: always write as 16 bits for sanity. */
643     peak_int = (short)(peak_val * G_MAXSHORT);
644     encoded_peak.bitsRepresentingPeak = 16;
645     encoded_peak.peakVolume = ByteVector::fromShort(peak_int, true);
646     GST_DEBUG ("setting peak value %g", peak_val);
647     frame->setPeakVolume(encoded_peak);
648 
649   } else {
650     gst_tag_list_get_double (list, tag, &gain_val);
651     GST_DEBUG ("setting volume adjustment %g", gain_val);
652     frame->setVolumeAdjustment (gain_val);
653 
654     if (gst_tag_list_get_tag_size (list, peak_tag_name) != 0) {
655       GST_DEBUG ("both gain and peak tags exist, not adding frame this time around");
656       delete frame;
657       return;
658     }
659   }
660 
661   id3v2tag->addFrame (frame);
662 }
663 
664 static void
add_bpm_tag(ID3v2::Tag * id3v2tag,const GstTagList * list,const gchar * tag,guint num_tags,const gchar * frame_id)665 add_bpm_tag (ID3v2::Tag * id3v2tag, const GstTagList * list,
666     const gchar * tag, guint num_tags, const gchar * frame_id)
667 {
668   gdouble bpm;
669 
670   if (gst_tag_list_get_double_index (list, tag, 0, &bpm)) {
671     ID3v2::TextIdentificationFrame * frame;
672     gchar *tag_str;
673 
674     tag_str = g_strdup_printf ("%u", (guint)bpm);
675 
676     GST_DEBUG ("Setting %s to %s", tag, tag_str);
677     frame = new ID3v2::TextIdentificationFrame ("TBPM", String::UTF8);
678     id3v2tag->addFrame (frame);
679     frame->setText (tag_str);
680     g_free (tag_str);
681   }
682 }
683 
684 /* id3demux produces these for frames it cannot parse */
685 #define GST_ID3_DEMUX_TAG_ID3V2_FRAME "private-id3v2-frame"
686 
687 static const struct
688 {
689   const gchar *gst_tag;
690   const GstId3v2MuxAddTagFunc func;
691   const gchar data[5];
692 } add_funcs[] = {
693   {
694   GST_TAG_ARTIST, add_text_tag, "TPE1"}, {
695   GST_TAG_ALBUM_ARTIST, add_text_tag, "TPE2"}, {
696   GST_TAG_TITLE, add_text_tag, "TIT2"}, {
697   GST_TAG_ALBUM, add_text_tag, "TALB"}, {
698   GST_TAG_COPYRIGHT, add_text_tag, "TCOP"}, {
699   GST_TAG_COMPOSER, add_text_tag, "TCOM"}, {
700   GST_TAG_GENRE, add_text_tag, "TCON"}, {
701   GST_TAG_COMMENT, add_comment_tag, ""}, {
702   GST_TAG_EXTENDED_COMMENT, add_comment_tag, ""}, {
703   GST_TAG_DATE, add_date_tag, ""}, {
704   GST_TAG_IMAGE, add_image_tag, ""}, {
705   GST_TAG_PREVIEW_IMAGE, add_image_tag, ""}, {
706   GST_ID3_DEMUX_TAG_ID3V2_FRAME, add_id3v2frame_tag, ""}, {
707   GST_TAG_MUSICBRAINZ_ARTISTID, add_musicbrainz_tag, "\000"}, {
708   GST_TAG_MUSICBRAINZ_ALBUMID, add_musicbrainz_tag, "\001"}, {
709   GST_TAG_MUSICBRAINZ_ALBUMARTISTID, add_musicbrainz_tag, "\002"}, {
710   GST_TAG_MUSICBRAINZ_TRMID, add_musicbrainz_tag, "\003"}, {
711   GST_TAG_CDDA_MUSICBRAINZ_DISCID, add_musicbrainz_tag, "\004"}, {
712   GST_TAG_CDDA_CDDB_DISCID, add_musicbrainz_tag, "\005"}, {
713   GST_TAG_MUSICBRAINZ_TRACKID, add_unique_file_id_tag, ""}, {
714   GST_TAG_ARTIST_SORTNAME, add_text_tag, "TSOP"}, {
715   GST_TAG_ALBUM_SORTNAME, add_text_tag, "TSOA"}, {
716   GST_TAG_TITLE_SORTNAME, add_text_tag, "TSOT"}, {
717   GST_TAG_TRACK_NUMBER, add_count_or_num_tag, "TRCK"}, {
718   GST_TAG_TRACK_COUNT, add_count_or_num_tag, "TRCK"}, {
719   GST_TAG_ALBUM_VOLUME_NUMBER, add_count_or_num_tag, "TPOS"}, {
720   GST_TAG_ALBUM_VOLUME_COUNT, add_count_or_num_tag, "TPOS"}, {
721   GST_TAG_ENCODER, add_encoder_tag, ""}, {
722   GST_TAG_ENCODER_VERSION, add_encoder_tag, ""}, {
723   GST_TAG_COPYRIGHT_URI, add_uri_tag, "WCOP"}, {
724   GST_TAG_LICENSE_URI, add_uri_tag, "WCOP"}, {
725   GST_TAG_TRACK_PEAK, add_relative_volume_tag, ""}, {
726   GST_TAG_TRACK_GAIN, add_relative_volume_tag, ""}, {
727   GST_TAG_ALBUM_PEAK, add_relative_volume_tag, ""}, {
728   GST_TAG_ALBUM_GAIN, add_relative_volume_tag, ""}, {
729   GST_TAG_BEATS_PER_MINUTE, add_bpm_tag, ""}
730 };
731 
732 
733 static void
foreach_add_tag(const GstTagList * list,const gchar * tag,gpointer userdata)734 foreach_add_tag (const GstTagList * list, const gchar * tag, gpointer userdata)
735 {
736   ID3v2::Tag * id3v2tag = (ID3v2::Tag *) userdata;
737   TagLib::StringList string_list;
738   guint num_tags, i;
739 
740   num_tags = gst_tag_list_get_tag_size (list, tag);
741 
742   GST_LOG ("Processing tag %s (num=%u)", tag, num_tags);
743 
744   if (num_tags > 1 && gst_tag_is_fixed (tag)) {
745     GST_WARNING ("Multiple occurences of fixed tag '%s', ignoring some", tag);
746     num_tags = 1;
747   }
748 
749   for (i = 0; i < G_N_ELEMENTS (add_funcs); ++i) {
750     if (strcmp (add_funcs[i].gst_tag, tag) == 0) {
751       add_funcs[i].func (id3v2tag, list, tag, num_tags, add_funcs[i].data);
752       break;
753     }
754   }
755 
756   if (i == G_N_ELEMENTS (add_funcs)) {
757     GST_WARNING ("Unsupported tag '%s' - not written", tag);
758   }
759 }
760 
761 static GstBuffer *
gst_id3v2_mux_render_tag(GstTagMux * mux,const GstTagList * taglist)762 gst_id3v2_mux_render_tag (GstTagMux * mux, const GstTagList * taglist)
763 {
764   ID3v2::Tag id3v2tag;
765   ByteVector rendered_tag;
766   GstBuffer *buf;
767   guint tag_size;
768 
769   /* write all strings as UTF-8 by default */
770   TagLib::ID3v2::FrameFactory::instance ()->
771       setDefaultTextEncoding (TagLib::String::UTF8);
772 
773   /* Render the tag */
774   gst_tag_list_foreach (taglist, foreach_add_tag, &id3v2tag);
775 
776 #if 0
777   /* Do we want to add our own signature to the tag somewhere? */
778   {
779     gchar *tag_producer_str;
780 
781     tag_producer_str = g_strdup_printf ("(GStreamer id3v2mux %s, using "
782         "taglib %u.%u)", VERSION, TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION);
783     add_one_txxx_tag (id3v2tag, "tag_encoder", tag_producer_str);
784     g_free (tag_producer_str);
785   }
786 #endif
787 
788   rendered_tag = id3v2tag.render ();
789   tag_size = rendered_tag.size ();
790 
791   GST_LOG_OBJECT (mux, "tag size = %d bytes", tag_size);
792 
793   /* Create buffer with tag */
794   buf = gst_buffer_new_and_alloc (tag_size);
795   gst_buffer_fill (buf, 0, rendered_tag.data (), tag_size);
796 
797   return buf;
798 }
799 
800 static GstBuffer *
gst_id3v2_mux_render_end_tag(GstTagMux * mux,const GstTagList * taglist)801 gst_id3v2_mux_render_end_tag (GstTagMux * mux, const GstTagList * taglist)
802 {
803   return NULL;
804 }
805