initial revision
[swftools.git] / lib / lame / id3tag.c
diff --git a/lib/lame/id3tag.c b/lib/lame/id3tag.c
new file mode 100644 (file)
index 0000000..f3fecd9
--- /dev/null
@@ -0,0 +1,532 @@
+/*
+ * id3tag.c -- Write ID3 version 1 and 2 tags.
+ *
+ * Copyright (C) 2000 Don Melton.
+ *
+ * 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.
+ */
+
+/*
+ * HISTORY: This source file is part of LAME (see http://www.mp3dev.org/mp3/)
+ * and was originally adapted by Conrad Sanderson <c.sanderson@me.gu.edu.au>
+ * from mp3info by Ricardo Cerqueira <rmc@rccn.net> to write only ID3 version 1
+ * tags.  Don Melton <don@blivet.com> COMPLETELY rewrote it to support version
+ * 2 tags and be more conformant to other standards while remaining flexible.
+ *
+ * NOTE: See http://id3.org/ for more information about ID3 tag formats.
+ */
+
+/* $Id: id3tag.c,v 1.1 2002/04/28 17:30:19 kramm Exp $ */
+
+#include "config_static.h"
+
+#ifdef STDC_HEADERS
+# include <stddef.h>
+# include <stdlib.h>
+# include <string.h>
+#else
+# ifndef HAVE_STRCHR
+#  define strchr index
+#  define strrchr rindex
+# endif
+char *strchr (), *strrchr ();
+# ifndef HAVE_MEMCPY
+#  define memcpy(d, s, n) bcopy ((s), (d), (n))
+#  define memmove(d, s, n) bcopy ((s), (d), (n))
+# endif
+#endif
+
+#include "lame.h"
+#include "id3tag.h"
+#include "util.h"
+#include "bitstream.h"
+
+#ifdef WITH_DMALLOC
+#include <dmalloc.h>
+#endif
+
+static const char *const genre_names[] =
+{
+    /*
+     * NOTE: The spelling of these genre names is identical to those found in
+     * Winamp and mp3info.
+     */
+    "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+    "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+    "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+    "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
+    "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
+    "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "Alt. Rock",
+    "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
+    "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial",
+    "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
+    "Cult", "Gangsta Rap", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
+    "Native American", "Cabaret", "New Wave", "Psychedelic", "Rave",
+    "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz",
+    "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk",
+    "Folk/Rock", "National Folk", "Swing", "Fast-Fusion", "Bebob", "Latin",
+    "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock",
+    "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
+    "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech",
+    "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass",
+    "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+    "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet",
+    "Punk Rock", "Drum Solo", "A Cappella", "Euro-House", "Dance Hall",
+    "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
+    "BritPop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap",
+    "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
+    "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
+    "Synthpop"
+};
+
+#define GENRE_NAME_COUNT \
+    ((int)(sizeof genre_names / sizeof (const char *const)))
+
+static const int genre_alpha_map [] = {
+    123, 34, 74, 73, 99, 20, 40, 26, 145, 90, 116, 41, 135, 85, 96, 138, 89, 0,
+    107, 132, 65, 88, 104, 102, 97, 136, 61, 141, 32, 1, 112, 128, 57, 140, 2,
+    139, 58, 3, 125, 50, 22, 4, 55, 127, 122, 120, 98, 52, 48, 54, 124, 25, 84,
+    80, 115, 81, 119, 5, 30, 36, 59, 126, 38, 49, 91, 6, 129, 79, 137, 7, 35,
+    100, 131, 19, 33, 46, 47, 8, 29, 146, 63, 86, 71, 45, 142, 9, 77, 82, 64,
+    133, 10, 66, 39, 11, 103, 12, 75, 134, 13, 53, 62, 109, 117, 23, 108, 92,
+    67, 93, 43, 121, 15, 68, 14, 16, 76, 87, 118, 17, 78, 143, 114, 110, 69, 21,
+    111, 95, 105, 42, 37, 24, 56, 44, 101, 83, 94, 106, 147, 113, 18, 51, 130,
+    144, 60, 70, 31, 72, 27, 28
+};
+
+#define GENRE_ALPHA_COUNT ((int)(sizeof genre_alpha_map / sizeof (int)))
+
+void
+id3tag_genre_list(void (*handler)(int, const char *, void *), void *cookie)
+{
+    if (handler) {
+        int i;
+        for (i = 0; i < GENRE_NAME_COUNT; ++i) {
+            if (i < GENRE_ALPHA_COUNT) {
+                int j = genre_alpha_map[i];
+                handler(j, genre_names[j], cookie);
+            }
+        }
+    }
+}
+
+#define GENRE_NUM_UNKNOWN 255
+
+void
+id3tag_init(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    memset(&gfc->tag_spec, 0, sizeof gfc->tag_spec);
+    gfc->tag_spec.genre = GENRE_NUM_UNKNOWN;
+}
+
+
+
+void
+id3tag_add_v2(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    gfc->tag_spec.flags &= ~V1_ONLY_FLAG;
+    gfc->tag_spec.flags |= ADD_V2_FLAG;
+}
+
+void
+id3tag_v1_only(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    gfc->tag_spec.flags &= ~(ADD_V2_FLAG | V2_ONLY_FLAG);
+    gfc->tag_spec.flags |= V1_ONLY_FLAG;
+}
+
+void
+id3tag_v2_only(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    gfc->tag_spec.flags &= ~V1_ONLY_FLAG;
+    gfc->tag_spec.flags |= V2_ONLY_FLAG;
+}
+
+void
+id3tag_space_v1(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    gfc->tag_spec.flags &= ~V2_ONLY_FLAG;
+    gfc->tag_spec.flags |= SPACE_V1_FLAG;
+}
+
+void
+id3tag_pad_v2(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    gfc->tag_spec.flags &= ~V1_ONLY_FLAG;
+    gfc->tag_spec.flags |= PAD_V2_FLAG;
+}
+
+void
+id3tag_set_title(lame_global_flags *gfp, const char *title)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (title && *title) {
+        gfc->tag_spec.title = title;
+        gfc->tag_spec.flags |= CHANGED_FLAG;
+    }
+}
+
+void
+id3tag_set_artist(lame_global_flags *gfp, const char *artist)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (artist && *artist) {
+        gfc->tag_spec.artist = artist;
+        gfc->tag_spec.flags |= CHANGED_FLAG;
+    }
+}
+
+void
+id3tag_set_album(lame_global_flags *gfp, const char *album)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (album && *album) {
+        gfc->tag_spec.album = album;
+        gfc->tag_spec.flags |= CHANGED_FLAG;
+    }
+}
+
+void
+id3tag_set_year(lame_global_flags *gfp, const char *year)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (year && *year) {
+        int num = atoi(year);
+        if (num < 0) {
+            num = 0;
+        }
+        /* limit a year to 4 digits so it fits in a version 1 tag */
+        if (num > 9999) {
+            num = 9999;
+        }
+        if (num) {
+            gfc->tag_spec.year = num;
+            gfc->tag_spec.flags |= CHANGED_FLAG;
+        }
+    }
+}
+
+void
+id3tag_set_comment(lame_global_flags *gfp, const char *comment)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (comment && *comment) {
+        gfc->tag_spec.comment = comment;
+        gfc->tag_spec.flags |= CHANGED_FLAG;
+    }
+}
+
+void
+id3tag_set_track(lame_global_flags *gfp, const char *track)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (track && *track) {
+        int num = atoi(track);
+        if (num < 0) {
+            num = 0;
+        }
+        /* limit a track to 255 so it fits in a version 1 tag even though CD
+         * audio doesn't allow more than 99 tracks */
+        if (num > 255) {
+            num = 255;
+        }
+        if (num) {
+            gfc->tag_spec.track = num;
+            gfc->tag_spec.flags |= CHANGED_FLAG;
+        }
+    }
+}
+
+/* would use real "strcasecmp" but it isn't portable */
+static int
+local_strcasecmp(const char *s1, const char *s2)
+{
+    unsigned char c1;
+    unsigned char c2;
+    do {
+        c1 = tolower(*s1);
+        c2 = tolower(*s2);
+        if (!c1) {
+            break;
+        }
+        ++s1;
+        ++s2;
+    } while (c1 == c2);
+    return c1 - c2;
+}
+
+int
+id3tag_set_genre(lame_global_flags *gfp, const char *genre)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if (genre && *genre) {
+        char *str;
+        int num = strtol(genre, &str, 10);
+        /* is the input a string or a valid number? */
+        if (*str) {
+            int i;
+            for (i = 0; i < GENRE_NAME_COUNT; ++i) {
+                if (!local_strcasecmp(genre, genre_names[i])) {
+                    num = i;
+                    break;
+                }
+            }
+            if (i == GENRE_NAME_COUNT) {
+                return -1;
+            }
+        } else if ((num < 0) || (num >= GENRE_NAME_COUNT)) {
+            return -1;
+        }
+        gfc->tag_spec.genre = num;
+        gfc->tag_spec.flags |= CHANGED_FLAG;
+    }
+    return 0;
+}
+
+static unsigned char *
+set_4_byte_value(unsigned char *bytes, unsigned long value)
+{
+    int index;
+    for (index = 3; index >= 0; --index) {
+        bytes[index] = value & 0xfful;
+        value >>= 8;
+    }
+    return bytes + 4;
+}
+
+#define FRAME_ID(a, b, c, d) \
+    ( ((unsigned long)(a) << 24) \
+    | ((unsigned long)(b) << 16) \
+    | ((unsigned long)(c) <<  8) \
+    | ((unsigned long)(d) <<  0) )
+#define TITLE_FRAME_ID FRAME_ID('T', 'I', 'T', '2')
+#define ARTIST_FRAME_ID FRAME_ID('T', 'P', 'E', '1')
+#define ALBUM_FRAME_ID FRAME_ID('T', 'A', 'L', 'B')
+#define YEAR_FRAME_ID FRAME_ID('T', 'Y', 'E', 'R')
+#define COMMENT_FRAME_ID FRAME_ID('C', 'O', 'M', 'M')
+#define TRACK_FRAME_ID FRAME_ID('T', 'R', 'C', 'K')
+#define GENRE_FRAME_ID FRAME_ID('T', 'C', 'O', 'N')
+
+static unsigned char *
+set_frame(unsigned char *frame, unsigned long id, const char *text,
+    size_t length)
+{
+    if (length) {
+        frame = set_4_byte_value(frame, id);
+        /* Set frame size = total size - header size.  Frame header and field
+         * bytes include 2-byte header flags, 1 encoding descriptor byte, and
+         * for comment frames: 3-byte language descriptor and 1 content
+         * descriptor byte */
+        frame = set_4_byte_value(frame, ((id == COMMENT_FRAME_ID) ? 5 : 1)
+                + length);
+        /* clear 2-byte header flags */
+        *frame++ = 0;
+        *frame++ = 0;
+        /* clear 1 encoding descriptor byte to indicate ISO-8859-1 format */
+        *frame++ = 0;
+        if (id == COMMENT_FRAME_ID) {
+            /* use id3lib-compatible bogus language descriptor */
+            *frame++ = 'X';
+            *frame++ = 'X';
+            *frame++ = 'X';
+            /* clear 1 byte to make content descriptor empty string */
+            *frame++ = 0;
+        }
+        while (length--) {
+            *frame++ = *text++;
+        }
+    }
+    return frame;
+}
+
+int
+id3tag_write_v2(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if ((gfc->tag_spec.flags & CHANGED_FLAG)
+            && !(gfc->tag_spec.flags & V1_ONLY_FLAG)) {
+        /* calculate length of four fields which may not fit in verion 1 tag */
+        size_t title_length = gfc->tag_spec.title
+            ? strlen(gfc->tag_spec.title) : 0;
+        size_t artist_length = gfc->tag_spec.artist
+            ? strlen(gfc->tag_spec.artist) : 0;
+        size_t album_length = gfc->tag_spec.album
+            ? strlen(gfc->tag_spec.album) : 0;
+        size_t comment_length = gfc->tag_spec.comment
+            ? strlen(gfc->tag_spec.comment) : 0;
+        /* write tag if explicitly requested or if fields overflow */
+        if ((gfc->tag_spec.flags & (ADD_V2_FLAG | V2_ONLY_FLAG))
+                || (title_length > 30)
+                || (artist_length > 30) || (album_length > 30)
+                || (comment_length > 30)
+                || (gfc->tag_spec.track && (comment_length > 28))) {
+            size_t tag_size;
+            char year[5];
+            size_t year_length;
+            char track[3];
+            size_t track_length;
+            char genre[6];
+            size_t genre_length;
+            unsigned char *tag;
+            unsigned char *p;
+            size_t adjusted_tag_size;
+            unsigned int index;
+            /* calulate size of tag starting with 10-byte tag header */
+            tag_size = 10;
+            if (title_length) {
+                /* add 10-byte frame header, 1 encoding descriptor byte ... */
+                tag_size += 11 + title_length;
+            }
+            if (artist_length) {
+                tag_size += 11 + artist_length;
+            }
+            if (album_length) {
+                tag_size += 11 + album_length;
+            }
+            if (gfc->tag_spec.year) {
+                year_length = sprintf(year, "%d", gfc->tag_spec.year);
+                tag_size += 11 + year_length;
+            } else {
+                year_length = 0;
+            }
+            if (comment_length) {
+                /* add 10-byte frame header, 1 encoding descriptor byte,
+                 * 3-byte language descriptor, 1 content descriptor byte ... */
+                tag_size += 15 + comment_length;
+            }
+            if (gfc->tag_spec.track) {
+                track_length = sprintf(track, "%d", gfc->tag_spec.track);
+                tag_size += 11 + track_length;
+            } else {
+                track_length = 0;
+            }
+            if (gfc->tag_spec.genre != GENRE_NUM_UNKNOWN) {
+                genre_length = sprintf(genre, "(%d)", gfc->tag_spec.genre);
+                tag_size += 11 + genre_length;
+            } else {
+                genre_length = 0;
+            }
+            if (gfc->tag_spec.flags & PAD_V2_FLAG) {
+                /* add 128 bytes of padding */
+                tag_size += 128;
+            }
+            tag = (unsigned char *)malloc(tag_size);
+            if (!tag) {
+                return -1;
+            }
+            p = tag;
+            /* set tag header starting with file identifier */
+            *p++ = 'I'; *p++ = 'D'; *p++ = '3';
+            /* set version number word */
+            *p++ = 3; *p++ = 0;
+            /* clear flags byte */
+            *p++ = 0;
+            /* calculate and set tag size = total size - header size */
+            adjusted_tag_size = tag_size - 10;
+            /* encode adjusted size into four bytes where most significant 
+             * bit is clear in each byte, for 28-bit total */
+            *p++ = (adjusted_tag_size >> 21) & 0x7fu;
+            *p++ = (adjusted_tag_size >> 14) & 0x7fu;
+            *p++ = (adjusted_tag_size >> 7) & 0x7fu;
+            *p++ = adjusted_tag_size & 0x7fu;
+
+            /*
+             * NOTE: The remainder of the tag (frames and padding, if any)
+             * are not "unsynchronized" to prevent false MPEG audio headers
+             * from appearing in the bitstream.  Why?  Well, most players
+             * and utilities know how to skip the ID3 version 2 tag by now
+             * even if they don't read its contents, and it's actually
+             * very unlikely that such a false "sync" pattern would occur
+             * in just the simple text frames added here.
+             */
+
+            /* set each frame in tag */
+            p = set_frame(p, TITLE_FRAME_ID, gfc->tag_spec.title, title_length);
+            p = set_frame(p, ARTIST_FRAME_ID, gfc->tag_spec.artist,
+                    artist_length);
+            p = set_frame(p, ALBUM_FRAME_ID, gfc->tag_spec.album, album_length);
+            p = set_frame(p, YEAR_FRAME_ID, year, year_length);
+            p = set_frame(p, COMMENT_FRAME_ID, gfc->tag_spec.comment,
+                    comment_length);
+            p = set_frame(p, TRACK_FRAME_ID, track, track_length);
+            p = set_frame(p, GENRE_FRAME_ID, genre, genre_length);
+            /* clear any padding bytes */
+            memset(p, 0, tag_size - (p - tag));
+            /* write tag directly into bitstream at current position */
+            for (index = 0; index < tag_size; ++index) {
+                add_dummy_byte(gfp, tag[index]);
+            }
+            free(tag);
+            return tag_size;
+        }
+    }
+    return 0;
+}
+
+static unsigned char *
+set_text_field(unsigned char *field, const char *text, size_t size, int pad)
+{
+    while (size--) {
+        if (text && *text) {
+            *field++ = *text++;
+        } else {
+            *field++ = pad;
+        }
+    }
+    return field;
+}
+
+int
+id3tag_write_v1(lame_global_flags *gfp)
+{
+    lame_internal_flags *gfc = gfp->internal_flags;
+    if ((gfc->tag_spec.flags & CHANGED_FLAG)
+            && !(gfc->tag_spec.flags & V2_ONLY_FLAG)) {
+        unsigned char tag[128];
+        unsigned char *p = tag;
+        int pad = (gfc->tag_spec.flags & SPACE_V1_FLAG) ? ' ' : 0;
+        char year[5];
+        unsigned int index;
+        /* set tag identifier */
+        *p++ = 'T'; *p++ = 'A'; *p++ = 'G';
+        /* set each field in tag */
+        p = set_text_field(p, gfc->tag_spec.title, 30, pad);
+        p = set_text_field(p, gfc->tag_spec.artist, 30, pad);
+        p = set_text_field(p, gfc->tag_spec.album, 30, pad);
+        sprintf(year, "%d", gfc->tag_spec.year);
+        p = set_text_field(p, gfc->tag_spec.year ? year : NULL, 4, pad);
+        /* limit comment field to 28 bytes if a track is specified */
+        p = set_text_field(p, gfc->tag_spec.comment, gfc->tag_spec.track
+                ? 28 : 30, pad);
+        if (gfc->tag_spec.track) {
+            /* clear the next byte to indicate a version 1.1 tag */
+            *p++ = 0;
+            *p++ = gfc->tag_spec.track;
+        }
+        *p++ = gfc->tag_spec.genre;
+        /* write tag directly into bitstream at current position */
+        for (index = 0; index < 128; ++index) {
+            add_dummy_byte(gfp, tag[index]);
+        }
+        return 128;
+    }
+    return 0;
+}