From 988ea2cffc36d891d5b4752484c741a98eddede3 Mon Sep 17 00:00:00 2001 From: Magnus Holmgren Date: Wed, 27 Jul 2005 11:54:33 +0000 Subject: Added support for ID3V2 ReplayGain tags (as written by Foobar). Generalized the replaygain tag parsing a bit, to cut down the code size (APE tags should use this as well, but as it requires larger changes, it will have to wait for another commit). Also fixed a bug in the ID3V2 parser; ISO-8859-1 strings could confuse the main parsing loop (causing bufferpos to come out of sync). git-svn-id: svn://svn.rockbox.org/rockbox/trunk@7243 a1c6a512-1295-4272-9138-f99709370657 --- apps/codecs/mpa.c | 3 +- apps/metadata.c | 38 ++-------- apps/screens.c | 8 +-- firmware/export/id3.h | 4 +- firmware/export/replaygain.h | 4 ++ firmware/id3.c | 166 ++++++++++++++++++++++++++++--------------- firmware/replaygain.c | 108 +++++++++++++++++++++++++++- 7 files changed, 235 insertions(+), 96 deletions(-) diff --git a/apps/codecs/mpa.c b/apps/codecs/mpa.c index 2dcc71922e..fb478222a4 100644 --- a/apps/codecs/mpa.c +++ b/apps/codecs/mpa.c @@ -118,6 +118,7 @@ enum codec_status codec_start(struct codec_api* api) frequency_divider = 441; ci->configure(DSP_SET_FREQUENCY, (int *)ci->id3->frequency); + codec_set_replaygain(ci->id3); ci->request_buffer(&size, ci->id3->first_frame_offset); ci->advance_buffer(size); @@ -144,7 +145,7 @@ enum codec_status codec_start(struct codec_api* api) samplecount = ci->id3->length * frequency_divider / 10; samplesdone = ci->id3->elapsed * frequency_divider / 10; } - + /* This is the decoding loop. */ while (1) { ci->yield(); diff --git a/apps/metadata.c b/apps/metadata.c index 409bdf88c5..bbbfe07d66 100644 --- a/apps/metadata.c +++ b/apps/metadata.c @@ -528,7 +528,7 @@ static bool get_apetag_info (struct mp3entry *entry, int fd) if (rem_space > 1 && get_apetag_item (&temp_apetag, "replaygain_track_gain", temp_buffer, rem_space)) { - entry->track_gain = get_replaygain (entry->track_gain_str = temp_buffer); + entry->track_gain = get_replaygain (entry->track_gain_string = temp_buffer); str_space = strlen (temp_buffer) + 1; temp_buffer += str_space; rem_space -= str_space; @@ -536,7 +536,7 @@ static bool get_apetag_info (struct mp3entry *entry, int fd) if (rem_space > 1 && get_apetag_item (&temp_apetag, "replaygain_album_gain", temp_buffer, rem_space)) { - entry->album_gain = get_replaygain (entry->album_gain_str = temp_buffer); + entry->album_gain = get_replaygain (entry->album_gain_string = temp_buffer); str_space = strlen (temp_buffer) + 1; temp_buffer += str_space; rem_space -= str_space; @@ -910,37 +910,13 @@ static bool get_vorbis_comments (struct mp3entry *entry, int fd) } else if (strncasecmp(temp, "TRACKNUMBER=", 12) == 0) { name_length = 11; p = &(entry->track_string); - } else if ((strncasecmp(temp, "RG_RADIO=", 9) == 0) - && !entry->track_gain) { - entry->track_gain = get_replaygain(&temp[9]); - name_length = 8; - p = &(entry->track_gain_str); - } else if (strncasecmp(temp, "REPLAYGAIN_TRACK_GAIN=", 22) == 0) { - entry->track_gain = get_replaygain(&temp[22]); - name_length = 21; - p = &(entry->track_gain_str); - } else if ((strncasecmp(temp, "RG_AUDIOPHILE=", 14) == 0) - && !entry->album_gain) { - entry->album_gain = get_replaygain(&temp[14]); - name_length = 13; - p = &(entry->album_gain_str); - } else if (strncasecmp(temp, "REPLAYGAIN_ALBUM_GAIN=", 22) == 0) { - entry->album_gain = get_replaygain(&temp[22]); - name_length = 21; - p = &(entry->album_gain_str); - } else if ((strncasecmp(temp, "RG_PEAK=", 8) == 0) - && !entry->track_peak) { - entry->track_peak = get_replaypeak(&temp[8]); - p = NULL; - } else if (strncasecmp(temp, "REPLAYGAIN_TRACK_PEAK=", 22) == 0) { - entry->track_peak = get_replaypeak(&temp[22]); - p = NULL; - } else if (strncasecmp(temp, "REPLAYGAIN_ALBUM_PEAK=", 22) == 0) { - entry->album_peak = get_replaypeak(&temp[22]); - p = NULL; } else { + int value_length = parse_replaygain(temp, NULL, entry, buffer, + buffer_remaining); + buffer_remaining -= value_length; + buffer += value_length; p = NULL; - } + } if (p) { comment_length -= (name_length + 1); diff --git a/apps/screens.c b/apps/screens.c index 86d11a73a4..2c7d5fe9b1 100644 --- a/apps/screens.c +++ b/apps/screens.c @@ -1388,15 +1388,15 @@ bool browse_id3(void) #if CONFIG_HWCODEC == MASNONE case 11: lcd_puts(0, 0, str(LANG_ID3_TRACK_GAIN)); - lcd_puts(0, 1, id3->track_gain_str - ? id3->track_gain_str + lcd_puts(0, 1, id3->track_gain_string + ? id3->track_gain_string : (char*) str(LANG_ID3_NO_GAIN)); break; case 12: lcd_puts(0, 0, str(LANG_ID3_ALBUM_GAIN)); - lcd_puts(0, 1, id3->album_gain_str - ? id3->album_gain_str + lcd_puts(0, 1, id3->album_gain_string + ? id3->album_gain_string : (char*) str(LANG_ID3_NO_GAIN)); break; #endif diff --git a/firmware/export/id3.h b/firmware/export/id3.h index 348b17e191..abb354b233 100644 --- a/firmware/export/id3.h +++ b/firmware/export/id3.h @@ -119,8 +119,8 @@ struct mp3entry { /* replaygain support */ #if CONFIG_HWCODEC == MASNONE - char* track_gain_str; - char* album_gain_str; + char* track_gain_string; + char* album_gain_string; long track_gain; /* 7.24 signed fixed point. 0 for no gain. */ long album_gain; long track_peak; /* 7.24 signed fixed point. 0 for no peak. */ diff --git a/firmware/export/replaygain.h b/firmware/export/replaygain.h index 09b0776069..e96a7f907a 100644 --- a/firmware/export/replaygain.h +++ b/firmware/export/replaygain.h @@ -20,7 +20,11 @@ #ifndef _REPLAYGAIN_H #define _REPLAYGAIN_H +#include "id3.h" + long get_replaygain(const char* str); long get_replaypeak(const char* str); +long parse_replaygain(const char* key, const char* value, + struct mp3entry* entry, char* buffer, int length); #endif diff --git a/firmware/id3.c b/firmware/id3.c index 7344bf53c7..d783053426 100644 --- a/firmware/id3.c +++ b/firmware/id3.c @@ -41,6 +41,7 @@ #include "id3.h" #include "mp3data.h" #include "system.h" +#include "replaygain.h" #define UNSYNC(b0,b1,b2,b3) (((long)(b0 & 0x7F) << (3*7)) | \ ((long)(b1 & 0x7F) << (2*7)) | \ @@ -163,6 +164,10 @@ char* id3_get_codec(const struct mp3entry* id3) Many ID3 symbolic names come in more than one form. You can add both forms, each referencing the same variable in struct mp3entry. If both forms are present, the last found will be used. + Note that the offset can be zero, in which case no entry will be set + in the mp3entry struct; the frame is still read into the buffer and + the special processing function is called (several times, if there + are several frames with the same name). 4. Alternately, use the TAG_LIST_ENTRY macro with ID3 tag symbolic name, @@ -305,6 +310,34 @@ static int parsegenre( struct mp3entry* entry, char* tag, int bufferpos ) } } +#if CONFIG_HWCODEC == MASNONE +/* parse user defined text, looking for replaygain information. */ +static int parseuser( struct mp3entry* entry, char* tag, int bufferpos ) +{ + char* value = NULL; + int desc_len = strlen(tag); + int value_len = 0; + + if ((tag - entry->id3v2buf + desc_len + 2) < bufferpos) { + /* At least part of the value was read, so we can safely try to + * parse it + */ + + value = tag + desc_len + 1; + value_len = parse_replaygain(tag, value, entry, tag, + bufferpos - (tag - entry->id3v2buf)); + } + + if (value_len) { + bufferpos = tag - entry->id3v2buf + value_len; + } else { + bufferpos = tag - entry->id3v2buf; + } + + return bufferpos; +} +#endif + static const struct tag_resolver taglist[] = { { "TPE1", 4, offsetof(struct mp3entry, artist), NULL }, { "TP1", 3, offsetof(struct mp3entry, artist), NULL }, @@ -319,6 +352,9 @@ static const struct tag_resolver taglist[] = { { "TCOM", 4, offsetof(struct mp3entry, composer), NULL }, { "TCON", 4, offsetof(struct mp3entry, genre_string), &parsegenre }, { "TCO", 3, offsetof(struct mp3entry, genre_string), &parsegenre }, +#if CONFIG_HWCODEC == MASNONE + { "TXXX", 4, 0, &parseuser }, +#endif }; #define TAGLIST_SIZE ((int)(sizeof(taglist) / sizeof(taglist[0]))) @@ -327,12 +363,12 @@ static const struct tag_resolver taglist[] = { string. If it is, we attempt to convert it to a 8-bit ASCII string (for valid 8-bit ASCII characters). If it's not unicode, we leave it alone. At some point we should fully support unicode strings */ -static int unicode_munge(char** string, int *len) { +static int unicode_munge(char* string, int *len) { long tmp; bool le = false; int i; - char *str = *string; - char *outstr = *string; + char *str = string; + char *outstr = string; bool bom = false; int outlen; @@ -343,8 +379,14 @@ static int unicode_munge(char** string, int *len) { /* Type 0x00 is ordinary ISO 8859-1 */ if(str[0] == 0x00) { - (*len)--; - (*string)++; /* Skip the encoding type byte */ + int i = --(*len); + + /* We must move the string to the left */ + while (i--) { + string[0] = string[1]; + string++; + } + return 0; } @@ -352,53 +394,59 @@ static int unicode_munge(char** string, int *len) { if(str[0] == 0x01 || str[0] == 0x02) { (*len)--; str++; - tmp = BYTES2INT(0, 0, str[0], str[1]); - - /* Now check if there is a BOM (zero-width non-breaking space, 0xfeff) - and if it is in little or big endian format */ - if(tmp == 0xfffe) { /* Little endian? */ - bom = true; - le = true; - str += 2; - (*len)-=2; - } - - if(tmp == 0xfeff) { /* Big endian? */ - bom = true; - str += 2; - (*len)-=2; - } - - /* If there is no BOM (which is a specification violation), - let's try to guess it. If one of the bytes is 0x00, it is - probably the most significant one. */ - if(!bom) { - if(str[1] == 0) - le = true; - } - i = 0; - - outlen = *len / 2; - + + /* Handle frames with more than one string (needed for TXXX frames). + */ do { - if(le) { - if(str[1]) - outstr[i++] = '.'; - else - outstr[i++] = str[0]; - } else { - if(str[0]) - outstr[i++] = '.'; - else - outstr[i++] = str[1]; + tmp = BYTES2INT(0, 0, str[0], str[1]); + + /* Now check if there is a BOM (zero-width non-breaking space, 0xfeff) + and if it is in little or big endian format */ + if(tmp == 0xfffe) { /* Little endian? */ + bom = true; + le = true; + str += 2; + (*len)-=2; + } + + if(tmp == 0xfeff) { /* Big endian? */ + bom = true; + str += 2; + (*len)-=2; + } + + /* If there is no BOM (which is a specification violation), + let's try to guess it. If one of the bytes is 0x00, it is + probably the most significant one. */ + if(!bom) { + if(str[1] == 0) + le = true; } + + outlen = *len / 2; + + do { + if(le) { + if(str[1]) + outstr[i++] = '.'; + else + outstr[i++] = str[0]; + } else { + if(str[0]) + outstr[i++] = '.'; + else + outstr[i++] = str[1]; + } + str += 2; + } while((str[0] || str[1]) && (i < outlen)); + str += 2; - } while((str[0] || str[1]) && (i < outlen)); + outstr[i++] = 0; /* Terminate the string */ + } while(i < outlen); - *len = i; + *len = i - 1; - outstr[i] = 0; /* Terminate the string */ return 0; } @@ -686,29 +734,33 @@ static void setid3v2title(int fd, struct mp3entry *entry) for (i=0; ioffset); + char** ptag = tr->offset ? (char**) (((char*)entry) + tr->offset) + : NULL; char* tag; - if( !*ptag && !memcmp( header, tr->tag, tr->tag_length ) ) { - + if( (!ptag || !*ptag) && !memcmp( header, tr->tag, tr->tag_length ) ) { + /* found a tag matching one in tagList, and not yet filled */ + tag = buffer + bufferpos; + if(global_unsynch && version <= ID3_VER_2_3) - bytesread = read_unsynched(fd, buffer + bufferpos, - framelen); + bytesread = read_unsynched(fd, tag, framelen); else - bytesread = read(fd, buffer + bufferpos, framelen); + bytesread = read(fd, tag, framelen); if( bytesread != framelen ) return; size -= bytesread; - *ptag = buffer + bufferpos; - + if(unsynch || (global_unsynch && version >= ID3_VER_2_4)) - bytesread = unsynchronize_frame(*ptag, bytesread); + bytesread = unsynchronize_frame(tag, bytesread); - unicode_munge( ptag, &bytesread ); - tag = *ptag; + unicode_munge( tag, &bytesread ); + + if (ptag) + *ptag = tag; + /* remove trailing spaces */ while ( bytesread > 0 && isspace(tag[bytesread-1])) bytesread--; diff --git a/firmware/replaygain.c b/firmware/replaygain.c index 23a25cc310..542eee6101 100644 --- a/firmware/replaygain.c +++ b/firmware/replaygain.c @@ -20,9 +20,12 @@ #include #include #include +#include #include #include -#include +#include +#include +#include "id3.h" #include "debug.h" /* The fixed point math routines (with the exception of fp_atof) are based @@ -326,3 +329,106 @@ long get_replaypeak(const char* str) return peak; } + +/* Compare two strings, ignoring case, up to the end nil or another end of + * string character. E.g., if eos is '=', "a=" would equal "a". Returns + * true for a match, false otherwise. + * TODO: This should be placed somewhere else, as it could be useful in + * other places too. + */ +static bool str_equal(const char* s1, const char* s2, char eos) +{ + char c1 = 0; + char c2 = 0; + + while (*s1 && *s2 && (*s1 != eos) && (*s2 != eos)) + { + if ((c1 = toupper(*s1)) != (c2 = toupper(*s2))) + { + return false; + } + + s1++; + s2++; + } + + if (c1 == eos) + { + c1 = '\0'; + } + + if (c2 == eos) + { + c2 = '\0'; + } + + return c1 == c2; +} + +/* Check for a ReplayGain tag conforming to the "VorbisGain standard". If + * found, set the mp3entry accordingly. If value is NULL, key is expected + * to be on the "key=value" format, and the comparion/extraction is done + * accordingly. buffer is where to store the text contents of the gain tags; + * up to length bytes (including end nil) can be written. + * Returns number of bytes written to the tag text buffer, or zero if + * no ReplayGain tag was found (or nothing was copied to the buffer for + * other reasons). + */ +long parse_replaygain(const char* key, const char* value, + struct mp3entry* entry, char* buffer, int length) +{ + const char* val = value; + char **p = NULL; + char eos = '\0'; + + if (!val) + { + if (!(val = strchr(key, '='))) + { + return 0; + } + + val++; + eos = '='; + } + + if (str_equal(key, "replaygain_track_gain", eos) + || (str_equal(key, "rg_radio", eos) && !entry->track_gain)) + { + entry->track_gain = get_replaygain(val); + p = &(entry->track_gain_string); + } + else if (str_equal(key, "replaygain_album_gain", eos) + || (str_equal(key, "rg_audiophile", eos) && !entry->album_gain)) + { + entry->album_gain = get_replaygain(val); + p = &(entry->album_gain_string); + } + else if (str_equal(key, "replaygain_track_peak", eos) + || (str_equal(key, "rg_peak", eos) && !entry->track_peak)) + { + entry->track_peak = get_replaypeak(val); + } + else if (str_equal(key, "replaygain_album_peak", eos)) + { + entry->album_peak = get_replaypeak(val); + } + + if (p) + { + int len = strlen(val); + + len = MIN(len, length - 1); + + /* A few characters just isn't interesting... */ + if (len > 1) + { + strncpy(buffer, val, len); + buffer[len] = 0; + *p = buffer; + return len + 1; + } + } + + return 0; +} -- cgit v1.2.3