snd_mp3tags: let it consume all the tags at file end.

We do not know the order of ape, or lyrics3, or musicmatch tags,
so we loop until we consume all, scanning for each tag type once.
I don't yet care about freaky broken mp3 files with double tags.

<rant> MP3 standard has no metadata format, so everyone invented
their own thing, even with extensions, until ID3v2 became dominant:
Hence the impossible mess there.</rant>

Also remove inline directive from a few detection procedures there.

(updates from my code in SDL_mixer.)

git-svn-id: svn://svn.code.sf.net/p/quakespasm/code/trunk/quakespasm@1664 af15c1b1-3010-417e-b628-4374ebc0bcbd
This commit is contained in:
Ozkan Sezer 2019-12-23 21:35:16 +00:00
parent 309ba8f7b6
commit a1eff43d80

View file

@ -1,5 +1,5 @@
/* MP3 TAGS STUFF -- put together using public specs. /* MP3 TAGS STUFF -- put together using public specs.
* Copyright (C) 2018-2019 O.Sezer <sezero@users.sourceforge.net> * Copyright (C) 2018-2019 O. Sezer <sezero@users.sourceforge.net>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -23,14 +23,14 @@
#include "snd_codec.h" #include "snd_codec.h"
#include "q_ctype.h" #include "q_ctype.h"
static inline qboolean tag_is_id3v1(const unsigned char *data, size_t length) { static inline qboolean is_id3v1(const unsigned char *data, long length) {
/* http://id3.org/ID3v1 : 3 bytes "TAG" identifier and 125 bytes tag data */ /* http://id3.org/ID3v1 : 3 bytes "TAG" identifier and 125 bytes tag data */
if (length < 3 || memcmp(data,"TAG",3) != 0) { if (length < 3 || memcmp(data,"TAG",3) != 0) {
return false; return false;
} }
return true; return true;
} }
static inline qboolean tag_is_id3v2(const unsigned char *data, size_t length) { static qboolean is_id3v2(const unsigned char *data, size_t length) {
/* ID3v2 header is 10 bytes: http://id3.org/id3v2.4.0-structure */ /* ID3v2 header is 10 bytes: http://id3.org/id3v2.4.0-structure */
/* bytes 0-2: "ID3" identifier */ /* bytes 0-2: "ID3" identifier */
if (length < 10 || memcmp(data,"ID3",3) != 0) { if (length < 10 || memcmp(data,"ID3",3) != 0) {
@ -48,7 +48,7 @@ static inline qboolean tag_is_id3v2(const unsigned char *data, size_t length) {
} }
return true; return true;
} }
static inline long get_id3v2_len(const unsigned char *data, long length) { static long get_id3v2_len(const unsigned char *data, long length) {
/* size is a 'synchsafe' integer (see above) */ /* size is a 'synchsafe' integer (see above) */
long size = (long)((data[6]<<21) + (data[7]<<14) + (data[8]<<7) + data[9]); long size = (long)((data[6]<<21) + (data[7]<<14) + (data[8]<<7) + data[9]);
size += 10; /* header size */ size += 10; /* header size */
@ -64,7 +64,7 @@ static inline long get_id3v2_len(const unsigned char *data, long length) {
} }
return size; return size;
} }
static inline qboolean tag_is_apetag(const unsigned char *data, size_t length) { static qboolean is_apetag(const unsigned char *data, size_t length) {
/* http://wiki.hydrogenaud.io/index.php?title=APEv2_specification /* http://wiki.hydrogenaud.io/index.php?title=APEv2_specification
* Header/footer is 32 bytes: bytes 0-7 ident, bytes 8-11 version, * Header/footer is 32 bytes: bytes 0-7 ident, bytes 8-11 version,
* bytes 12-17 size. bytes 24-31 are reserved: must be all zeroes. */ * bytes 12-17 size. bytes 24-31 are reserved: must be all zeroes. */
@ -83,7 +83,7 @@ static inline qboolean tag_is_apetag(const unsigned char *data, size_t length) {
} }
return true; return true;
} }
static inline long get_ape_len(const unsigned char *data) { static long get_ape_len(const unsigned char *data) {
unsigned int flags, version; unsigned int flags, version;
long size = (long)((data[15]<<24) | (data[14]<<16) | (data[13]<<8) | data[12]); long size = (long)((data[15]<<24) | (data[14]<<16) | (data[13]<<8) | data[12]);
version = (unsigned)((data[11]<<24) | (data[10]<<16) | (data[9]<<8) | data[8]); version = (unsigned)((data[11]<<24) | (data[10]<<16) | (data[9]<<8) | data[8]);
@ -91,7 +91,7 @@ static inline long get_ape_len(const unsigned char *data) {
if (version == 2000U && (flags & (1U<<31))) size += 32; /* header present. */ if (version == 2000U && (flags & (1U<<31))) size += 32; /* header present. */
return size; return size;
} }
static inline int is_lyrics3tag(const unsigned char *data, size_t length) { static inline int is_lyrics3tag(const unsigned char *data, long length) {
/* http://id3.org/Lyrics3 /* http://id3.org/Lyrics3
* http://id3.org/Lyrics3v2 */ * http://id3.org/Lyrics3v2 */
if (length < 15) return 0; if (length < 15) return 0;
@ -99,7 +99,7 @@ static inline int is_lyrics3tag(const unsigned char *data, size_t length) {
if (memcmp(data+6,"LYRICSEND",9) == 0) return 1; /* v1 */ if (memcmp(data+6,"LYRICSEND",9) == 0) return 1; /* v1 */
return 0; return 0;
} }
static inline long get_lyrics3v1_len(snd_stream_t *stream) { static long get_lyrics3v1_len(snd_stream_t *stream) {
const char *p; long i, len; const char *p; long i, len;
char buf[5104]; char buf[5104];
/* needs manual search: http://id3.org/Lyrics3 */ /* needs manual search: http://id3.org/Lyrics3 */
@ -115,19 +115,19 @@ static inline long get_lyrics3v1_len(snd_stream_t *stream) {
if (i < 0) return -1; if (i < 0) return -1;
return len - (long)(p - buf) + 9 /* footer */; return len - (long)(p - buf) + 9 /* footer */;
} }
static inline long get_lyrics3v2_len(const unsigned char *data, size_t length) { static inline long get_lyrics3v2_len(const unsigned char *data, long length) {
/* 6 bytes before the end marker is size in decimal format - /* 6 bytes before the end marker is size in decimal format -
* does not include the 9 bytes end marker and size field. */ * does not include the 9 bytes end marker and size field. */
if (length != 6) return 0; if (length != 6) return 0;
return strtol((const char *)data, NULL, 10) + 15; return strtol((const char *)data, NULL, 10) + 15;
} }
static inline qboolean verify_lyrics3v2(const unsigned char *data, size_t length) { static inline qboolean verify_lyrics3v2(const unsigned char *data, long length) {
if (length < 11) return false; if (length < 11) return false;
if (memcmp(data,"LYRICSBEGIN",11) == 0) return true; if (memcmp(data,"LYRICSBEGIN",11) == 0) return true;
return false; return false;
} }
#define MMTAG_PARANOID #define MMTAG_PARANOID
static inline qboolean is_musicmatch(const unsigned char *data, long length) { static qboolean is_musicmatch(const unsigned char *data, long length) {
/* From docs/musicmatch.txt in id3lib: https://sourceforge.net/projects/id3lib/ /* From docs/musicmatch.txt in id3lib: https://sourceforge.net/projects/id3lib/
Overall tag structure: Overall tag structure:
@ -170,7 +170,7 @@ static inline qboolean is_musicmatch(const unsigned char *data, long length) {
#endif #endif
return true; return true;
} }
static inline long get_musicmatch_len(snd_stream_t *stream) { static long get_musicmatch_len(snd_stream_t *stream) {
const int metasizes[4] = { 7868, 7936, 8004, 8132 }; const int metasizes[4] = { 7868, 7936, 8004, 8132 };
const unsigned char syncstr[10] = {'1','8','2','7','3','6','4','5',0,0}; const unsigned char syncstr[10] = {'1','8','2','7','3','6','4','5',0,0};
unsigned char buf[256]; unsigned char buf[256];
@ -237,20 +237,106 @@ static inline long get_musicmatch_len(snd_stream_t *stream) {
return len + 256; /* header is present. */ return len + 256; /* header is present. */
} }
static int probe_id3v1(snd_stream_t *stream, unsigned char *buf) {
if (stream->fh.length >= 128) {
FS_fseek(&stream->fh, -128, SEEK_END);
if (FS_fread(buf, 1, 128, &stream->fh) != 128)
return -1;
if (is_id3v1(buf, 128)) {
stream->fh.length -= 128;
Con_DPrintf("MP3: skipped %ld bytes ID3v1 tag\n", 128L);
return 1;
/* FIXME: handle possible double-ID3v1 tags? */
}
}
return 0;
}
static int probe_mmtag(snd_stream_t *stream, unsigned char *buf) {
long len;
if (stream->fh.length >= 68) {
FS_fseek(&stream->fh, -48, SEEK_END);
if (FS_fread(buf, 1, 48, &stream->fh) != 48)
return -1;
if (is_musicmatch(buf, 48)) {
len = get_musicmatch_len(stream);
if (len < 0) return -1;
if (len >= stream->fh.length) return -1;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes MusicMatch tag\n", len);
return 1;
}
}
return 0;
}
static int probe_apetag(snd_stream_t *stream, unsigned char *buf) {
long len;
if (stream->fh.length >= 32) {
FS_fseek(&stream->fh, -32, SEEK_END);
if (FS_fread(buf, 1, 32, &stream->fh) != 32)
return -1;
if (is_apetag(buf, 32)) {
len = get_ape_len(buf);
if (len >= stream->fh.length) return -1;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes APE tag\n", len);
return 1;
}
}
return 0;
}
static int probe_lyrics3(snd_stream_t *stream, unsigned char *buf) {
long len;
if (stream->fh.length >= 15) {
FS_fseek(&stream->fh, -15, SEEK_END);
if (FS_fread(buf, 1, 15, &stream->fh) != 15)
return -1;
len = is_lyrics3tag(buf, 15);
if (len == 2) {
len = get_lyrics3v2_len(buf, 6);
if (len >= stream->fh.length) return -1;
if (len < 15) return -1;
FS_fseek(&stream->fh, -len, SEEK_END);
if (FS_fread(buf, 1, 11, &stream->fh) != 11)
return -1;
if (!verify_lyrics3v2(buf, 11)) return -1;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes Lyrics3 tag\n", len);
return 1;
}
else if (len == 1) {
len = get_lyrics3v1_len(stream);
if (len < 0) return -1;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes Lyrics3 tag\n", len);
return 1;
}
}
return 0;
}
int mp3_skiptags(snd_stream_t *stream) int mp3_skiptags(snd_stream_t *stream)
{ {
unsigned char buf[128]; unsigned char buf[128];
long len; size_t readsize; long len; size_t readsize;
int c_id3, c_ape, c_lyr, c_mm;
int rc = -1; int rc = -1;
/* failsafe */ /* failsafe */
long oldlength = stream->fh.length; long oldlength = stream->fh.length;
long oldstart = stream->fh.start; long oldstart = stream->fh.start;
/* MP3 standard has no metadata format, so everyone invented
* their own thing, even with extensions, until ID3v2 became
* dominant: Hence the impossible mess here.
*
* Note: I don't yet care about freaky broken mp3 files with
* double tags. -- O.S.
*/
readsize = FS_fread(buf, 1, 128, &stream->fh); readsize = FS_fread(buf, 1, 128, &stream->fh);
if (!readsize || FS_ferror(&stream->fh)) goto fail; if (!readsize || FS_ferror(&stream->fh)) goto fail;
/* ID3v2 tag is at the start */ /* ID3v2 tag is at the start */
if (tag_is_id3v2(buf, readsize)) { if (is_id3v2(buf, readsize)) {
len = get_id3v2_len(buf, (long)readsize); len = get_id3v2_len(buf, (long)readsize);
if (len >= stream->fh.length) goto fail; if (len >= stream->fh.length) goto fail;
stream->fh.start += len; stream->fh.start += len;
@ -259,7 +345,7 @@ int mp3_skiptags(snd_stream_t *stream)
} }
/* APE tag _might_ be at the start (discouraged /* APE tag _might_ be at the start (discouraged
* but not forbidden, either.) read the header. */ * but not forbidden, either.) read the header. */
else if (tag_is_apetag(buf, readsize)) { else if (is_apetag(buf, readsize)) {
len = get_ape_len(buf); len = get_ape_len(buf);
if (len >= stream->fh.length) goto fail; if (len >= stream->fh.length) goto fail;
stream->fh.start += len; stream->fh.start += len;
@ -267,71 +353,38 @@ int mp3_skiptags(snd_stream_t *stream)
Con_DPrintf("MP3: skipped %ld bytes APE tag\n", len); Con_DPrintf("MP3: skipped %ld bytes APE tag\n", len);
} }
/* it's not impossible that _old_ MusicMatch tag
* placing itself after ID3v1. */
if ((c_mm = probe_mmtag(stream, buf)) < 0) {
goto fail;
}
/* ID3v1 tag is at the end */ /* ID3v1 tag is at the end */
if (stream->fh.length >= 128) { if ((c_id3 = probe_id3v1(stream, buf)) < 0) {
FS_fseek(&stream->fh, -128, SEEK_END); goto fail;
readsize = FS_fread(buf, 1, 128, &stream->fh);
if (readsize != 128) goto fail;
if (tag_is_id3v1(buf, 128)) {
stream->fh.length -= 128;
Con_DPrintf("MP3: skipped %ld bytes ID3v1 tag\n", 128L);
/* FIXME: handle possible double-ID3v1 tags? */
}
} }
/* we do not know the order of ape or lyrics3
/* do we know whether ape or lyrics3 is the first? * or musicmatch tags, hence the loop here.. */
* well, we don't: we need to handle that later... */ c_ape = 0;
c_lyr = 0;
/* check for the _old_ MusicMatch tag at end. */ for (;;) {
if (stream->fh.length >= 68) { if (!c_lyr) {
FS_fseek(&stream->fh, -48, SEEK_END); /* care about mp3s with double Lyrics3 tags? */
readsize = FS_fread(buf, 1, 48, &stream->fh); if ((c_lyr = probe_lyrics3(stream, buf)) < 0)
if (readsize != 48) goto fail; goto fail;
if (is_musicmatch(buf, 48)) { if (c_lyr) continue;
len = get_musicmatch_len(stream);
if (len < 0) goto fail;
if (len >= stream->fh.length) goto fail;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes MusicMatch tag\n", len);
} }
} if (!c_mm) {
if ((c_mm = probe_mmtag(stream, buf)) < 0)
/* APE tag may be at the end: read the footer */ goto fail;
if (stream->fh.length >= 32) { if (c_mm) continue;
FS_fseek(&stream->fh, -32, SEEK_END);
readsize = FS_fread(buf, 1, 32, &stream->fh);
if (readsize != 32) goto fail;
if (tag_is_apetag(buf, 32)) {
len = get_ape_len(buf);
if (len >= stream->fh.length) goto fail;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes APE tag\n", len);
} }
} if (!c_ape) {
if ((c_ape = probe_apetag(stream, buf)) < 0)
if (stream->fh.length >= 15) { goto fail;
FS_fseek(&stream->fh, -15, SEEK_END); if (c_ape) continue;
readsize = FS_fread(buf, 1, 15, &stream->fh);
if (readsize != 15) goto fail;
len = is_lyrics3tag(buf, 15);
if (len == 2) {
len = get_lyrics3v2_len(buf, 6);
if (len >= stream->fh.length) goto fail;
if (len < 15) goto fail;
FS_fseek(&stream->fh, -len, SEEK_END);
readsize = FS_fread(buf, 1, 11, &stream->fh);
if (readsize != 11) goto fail;
if (!verify_lyrics3v2(buf, 11)) goto fail;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes Lyrics3 tag\n", len);
} }
else if (len == 1) { break;
len = get_lyrics3v1_len(stream); } /* for (;;) */
if (len < 0) goto fail;
stream->fh.length -= len;
Con_DPrintf("MP3: skipped %ld bytes Lyrics3 tag\n", len);
}
}
rc = (stream->fh.length > 0)? 0 : -1; rc = (stream->fh.length > 0)? 0 : -1;
fail: fail: