summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/metadata.c292
1 files changed, 292 insertions, 0 deletions
diff --git a/apps/metadata.c b/apps/metadata.c
index 2e39ab1..540e62c 100644
--- a/apps/metadata.c
+++ b/apps/metadata.c
@@ -25,6 +25,7 @@
#include "mp3_playback.h"
#include "mp3data.h"
#include "logf.h"
+#include "atoi.h"
/* Simple file type probing by looking filename extension. */
int probe_file_format(const char *filename)
@@ -85,6 +86,8 @@ unsigned short a52_441framesizes[]=
/* Get metadata for track - return false if parsing showed problems with the
file that would prevent playback. */
+static bool get_apetag_info (struct mp3entry *entry, int fd);
+
bool get_metadata(struct track_info* track, int fd, const char* trackname,
bool v1first) {
unsigned long totalsamples,bytespersample,channels,bitspersample,numbytes;
@@ -341,6 +344,7 @@ bool get_metadata(struct track_info* track, int fd, const char* trackname,
(track->id3.length / 8);
}
+ get_apetag_info (&track->id3, fd); /* use any apetag info we find */
lseek (fd, 0, SEEK_SET);
strncpy (track->id3.path, trackname, sizeof (track->id3.path));
track->taginfo_ready = true;
@@ -411,3 +415,291 @@ bool get_metadata(struct track_info* track, int fd, const char* trackname,
return true;
}
+
+/************************* APE TAG HANDLING CODE ****************************/
+
+/*
+ * This is a first pass at APEv2 tag handling. I'm not sure if this should
+ * reside here, but I wanted to modify as little as possible since I don't
+ * have a feel for the complete system. It may be that APEv2 tags should be
+ * added to the ID3 handling code in the firmware directory. APEv2 tags are
+ * used in WavPack files and Musepack files by default, however they are
+ * also used in MP3 files sometimes (by Foobar2000). Also, WavPack files can
+ * also use ID3v1 tags (but not ID3v2), so it seems like some universal tag
+ * handler might be a reasonable approach.
+ *
+ * This code does not currently handle APEv1 tags, but I believe that this
+ * is not a problem because they were only used in Monkey's Audio files which
+ * will probably never be playable in RockBox (and certainly not by this CPU).
+ */
+
+#define APETAG_HEADER_FORMAT "8LLLL"
+#define APETAG_HEADER_LENGTH 32
+#define APETAG_DATA_LIMIT 4096
+
+struct apetag_header {
+ char id [8];
+ long version, length, item_count, flags;
+ char res [8];
+};
+
+static struct apetag {
+ struct apetag_header header;
+ char data [APETAG_DATA_LIMIT];
+} temp_apetag;
+
+static int get_apetag_item (struct apetag *tag,
+ const char *item,
+ char *value,
+ int size);
+
+static int load_apetag (int fd, struct apetag *tag);
+static void UTF8ToAnsi (unsigned char *pUTF8);
+
+/*
+ * This function searches the specified file for an APEv2 tag and uses any
+ * information found there to populate the appropriate fields in the specified
+ * mp3entry structure. A temporary buffer is used to hold the tag during this
+ * operation. For now, the actual string data that needs to be held during the
+ * life of the track entry is stored in the "id3v2buf" field (which should not
+ * be used for any file that has an APEv2 tag). This limits the total space
+ * for the artist, title, album, composer and genre strings to 300 characters.
+ */
+
+static bool get_apetag_info (struct mp3entry *entry, int fd)
+{
+ int rem_space = sizeof (entry->id3v2buf), str_space;
+ char *temp_buffer = entry->id3v2buf;
+
+ if (rem_space <= 1 || !load_apetag (fd, &temp_apetag))
+ return false;
+
+ if (get_apetag_item (&temp_apetag, "year", temp_buffer, rem_space))
+ entry->year = atoi (temp_buffer);
+
+ if (get_apetag_item (&temp_apetag, "track", temp_buffer, rem_space))
+ entry->tracknum = atoi (temp_buffer);
+
+ if (get_apetag_item (&temp_apetag, "artist", temp_buffer, rem_space)) {
+ UTF8ToAnsi (entry->artist = temp_buffer);
+ str_space = strlen (temp_buffer) + 1;
+ temp_buffer += str_space;
+ rem_space -= str_space;
+ }
+
+ if (rem_space > 1 &&
+ get_apetag_item (&temp_apetag, "title", temp_buffer, rem_space)) {
+ UTF8ToAnsi (entry->title = temp_buffer);
+ str_space = strlen (temp_buffer) + 1;
+ temp_buffer += str_space;
+ rem_space -= str_space;
+ }
+
+ if (rem_space > 1 &&
+ get_apetag_item (&temp_apetag, "album", temp_buffer, rem_space)) {
+ UTF8ToAnsi (entry->album = temp_buffer);
+ str_space = strlen (temp_buffer) + 1;
+ temp_buffer += str_space;
+ rem_space -= str_space;
+ }
+
+ if (rem_space > 1 &&
+ get_apetag_item (&temp_apetag, "genre", temp_buffer, rem_space)) {
+ UTF8ToAnsi (entry->genre_string = temp_buffer);
+ str_space = strlen (temp_buffer) + 1;
+ temp_buffer += str_space;
+ rem_space -= str_space;
+ }
+
+ if (rem_space > 1 &&
+ get_apetag_item (&temp_apetag, "composer", temp_buffer, rem_space))
+ UTF8ToAnsi (entry->composer = temp_buffer);
+
+ return true;
+}
+
+/*
+ * Helper function to convert little-endian structures to easily usable native
+ * format using a format string (this does nothing on a little-endian machine).
+ */
+
+static void little_endian_to_native (void *data, char *format)
+{
+ unsigned char *cp = (unsigned char *) data;
+ long temp;
+
+ while (*format) {
+ switch (*format) {
+ case 'L':
+ temp = cp [0] + ((long) cp [1] << 8) + ((long) cp [2] << 16) + ((long) cp [3] << 24);
+ * (long *) cp = temp;
+ cp += 4;
+ break;
+
+ case 'S':
+ temp = cp [0] + (cp [1] << 8);
+ * (short *) cp = (short) temp;
+ cp += 2;
+ break;
+
+ default:
+ if (*format >= '0' && *format <= '9')
+ cp += *format - '0';
+
+ break;
+ }
+
+ format++;
+ }
+}
+
+/*
+ * Attempt to obtain the named string-type item from the specified APEv2 tag.
+ * The tag value will be copied to "value" (including an appended terminating
+ * NULL) and the length of the string (including the NULL) will be returned.
+ * If the data will not fit in the specified "size" then it will be truncated
+ * early (but still terminated). If the specified item is not found then 0 is
+ * returned and written to the first character of "value". If "value" is
+ * passed in as NULL, then the specified size is ignored and the actual size
+ * required to store the value is returned.
+ *
+ * Note that this function does not work on binary tag data; only UTF-8
+ * encoded strings. However, numeric data (like ReplayGain) is usually stored
+ * as strings.
+ *
+ * Also, APEv2 tags may have multiple values for a given item and these will
+ * all be copied to "value" with NULL separators (this is why the total data
+ * size is returned). Of course, it is possible to ignore any additional
+ * values by simply using up to the first NULL.
+ */
+
+static int get_apetag_item (struct apetag *tag,
+ const char *item,
+ char *value,
+ int size)
+{
+ if (value && size)
+ *value = 0;
+
+ if (tag->header.id [0] == 'A') {
+ char *p = tag->data;
+ char *q = p + tag->header.length - APETAG_HEADER_LENGTH;
+ int i;
+
+ for (i = 0; i < tag->header.item_count; ++i) {
+ int vsize, flags, isize;
+
+ vsize = * (long *) p; p += 4;
+ flags = * (long *) p; p += 4;
+ isize = strlen (p);
+
+ little_endian_to_native (&vsize, "L");
+ little_endian_to_native (&flags, "L");
+
+ if (p + isize + vsize + 1 > q)
+ break;
+
+ if (isize && vsize && !stricmp (item, p) && !(flags & 6)) {
+
+ if (value) {
+ if (vsize + 1 > size)
+ vsize = size - 1;
+
+ memcpy (value, p + isize + 1, vsize);
+ value [vsize] = 0;
+ }
+
+ return vsize + 1;
+ }
+ else
+ p += isize + vsize + 1;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * Attempt to load an APEv2 tag from the specified file into the specified
+ * structure. If the APEv2 tag will not fit into the predefined data size,
+ * then the tag is not loaded. A return value of TRUE indicates success.
+ */
+
+static int load_apetag (int fd, struct apetag *tag)
+{
+ if (lseek (fd, -APETAG_HEADER_LENGTH, SEEK_END) == -1 ||
+ read (fd, &tag->header, APETAG_HEADER_LENGTH) != APETAG_HEADER_LENGTH ||
+ strncmp (tag->header.id, "APETAGEX", 8)) {
+ tag->header.id [0] = 0;
+ return false;
+ }
+
+ little_endian_to_native (&tag->header, APETAG_HEADER_FORMAT);
+
+ if (tag->header.version == 2000 && tag->header.item_count &&
+ tag->header.length > APETAG_HEADER_LENGTH &&
+ tag->header.length < APETAG_DATA_LIMIT) {
+
+ int data_size = tag->header.length - APETAG_HEADER_LENGTH;
+
+ if (lseek (fd, -tag->header.length, SEEK_END) == -1 ||
+ read (fd, tag->data, data_size) != data_size) {
+ tag->header.id [0] = 0;
+ return false;
+ }
+ else
+ return true;
+ }
+
+ tag->header.id [0] = 0;
+ return false;
+}
+
+/*
+ * This is a *VERY* boneheaded attempt to convert UTF-8 unicode character
+ * strings to ANSI. It simply maps the 16-bit Unicode characters that are
+ * less than 0x100 directly to an 8-bit value, and turns all the rest into
+ * question marks. This can be done "in-place" because the resulting string
+ * can only get smaller.
+ */
+
+static void UTF8ToAnsi (unsigned char *pUTF8)
+{
+ unsigned char *pAnsi = pUTF8;
+ unsigned short widechar = 0;
+ int trail_bytes = 0;
+
+ while (*pUTF8) {
+ if (*pUTF8 & 0x80) {
+ if (*pUTF8 & 0x40) {
+ if (trail_bytes) {
+ trail_bytes = 0;
+ *pAnsi++ = widechar < 0x100 ? widechar : '?';
+ }
+ else {
+ char temp = *pUTF8;
+
+ while (temp & 0x80) {
+ trail_bytes++;
+ temp <<= 1;
+ }
+
+ widechar = temp >> trail_bytes--;
+ }
+ }
+ else if (trail_bytes) {
+ widechar = (widechar << 6) | (*pUTF8 & 0x3f);
+
+ if (!--trail_bytes)
+ *pAnsi++ = widechar < 0x100 ? widechar : '?';
+ }
+ }
+ else
+ *pAnsi++ = *pUTF8;
+
+ pUTF8++;
+ }
+
+ *pAnsi = 0;
+}
+