From b0d3783bf6c0dea7cc525cf9d9041cf56f7ee385 Mon Sep 17 00:00:00 2001 From: Wohlstand Date: Tue, 23 Jul 2019 13:55:50 +0300 Subject: [PATCH] MAD: Backport a better tag skipping Official SDL Mixer changeset: https://hg.libsdl.org/SDL_mixer/rev/4e7795a92332 @sezero, good job! Anyway, I think, why not to put tag processing code into the separate file? I did that because with a tag length I also taking some textual meta-tags are can be retrieved by user to, for example, show what music is playing (SuperTux Kart does that, for example). I would to drop away libID3 and use internal simplified code, however, these tags are usually UTF16-encoded and it's need to decode them into UTF8. --- src/codecs/music_mad.c | 233 +++++++++++++++++++++++++++++++---------- 1 file changed, 179 insertions(+), 54 deletions(-) diff --git a/src/codecs/music_mad.c b/src/codecs/music_mad.c index 1aef94c6..53e54d4b 100644 --- a/src/codecs/music_mad.c +++ b/src/codecs/music_mad.c @@ -26,6 +26,7 @@ #include "mad.h" + /* NOTE: The dithering functions are GPL, which should be fine if your application is GPL (which would need to be true if you enabled libmad support in SDL_mixer). If you're using libmad under the @@ -138,7 +139,7 @@ enum { typedef struct { int play_count; SDL_RWops *src; - Sint64 position_begin; + Sint64 start, length, pos; int freesrc; struct mad_stream stream; struct mad_frame frame; @@ -157,6 +158,35 @@ typedef struct { } MAD_Music; +static size_t MAD_RWread(MAD_Music *music, void *ptr, size_t size, size_t maxnum) { + size_t remaining = (size_t)(music->length - music->pos); + size_t ret; + maxnum *= size; + if (maxnum > remaining) maxnum = remaining; + ret = SDL_RWread(music->src, ptr, 1, maxnum); + music->pos += (Sint64)ret; + return ret; +} + +static Sint64 MAD_RWseek(MAD_Music *music, Sint64 offset, int whence) { + Sint64 ret; + switch (whence) { /* assumes a legal whence value */ + case RW_SEEK_CUR: + offset += music->pos; + break; + case RW_SEEK_END: + offset = music->length + offset; + break; + } + if (offset < 0) return -1; + if (offset > music->length) + offset = music->length; + ret = SDL_RWseek(music->src, music->start + offset, RW_SEEK_SET); + if (ret < 0) return ret; + music->pos = offset; + return (music->pos - music->start); +} + static void read_update_buffer(struct mad_stream *stream, MAD_Music *music); static void calculate_total_time(MAD_Music *music) @@ -167,7 +197,7 @@ static void calculate_total_time(MAD_Music *music) mad_header_init(&header); mad_stream_init(&stream); - SDL_RWseek(music->src, music->position_begin, RW_SEEK_SET); + MAD_RWseek(music, 0, RW_SEEK_SET); while (1) { @@ -202,10 +232,11 @@ static void calculate_total_time(MAD_Music *music) music->status = 0; - SDL_RWseek(music->src, music->position_begin, RW_SEEK_SET); + MAD_RWseek(music, 0, RW_SEEK_SET); } static int MAD_Seek(void *context, double position); +static int skip_tags(MAD_Music *music); static void *MAD_CreateFromRW(SDL_RWops *src, int freesrc) { @@ -219,8 +250,18 @@ static void *MAD_CreateFromRW(SDL_RWops *src, int freesrc) music->src = src; music->volume = MIX_MAX_VOLUME; + music->length = SDL_RWsize(src); + meta_tags_init(&music->tags); - music->position_begin = id3tag_fetchTags(&music->tags, music->src); + music->start = id3tag_fetchTags(&music->tags, music->src); + music->length -= music->start; + + if ((music->start == 0) && skip_tags(music) < 0) { + SDL_free(music); + Mix_SetError("music_mad: corrupt mp3 file."); + return NULL; + } + calculate_total_time(music); mad_stream_init(&music->stream); @@ -255,7 +296,7 @@ static int MAD_Play(void *context, int play_count) /*************************** TAG HANDLING: ******************************/ -static SDL_INLINE SDL_bool is_id3v1(const Uint8 *data, size_t length) +static SDL_INLINE SDL_bool is_id3v1(const unsigned char *data, size_t length) { /* http://id3.org/ID3v1 : 3 bytes "TAG" identifier and 125 bytes tag data */ if (length < 3 || SDL_memcmp(data,"TAG",3) != 0) { @@ -263,7 +304,18 @@ static SDL_INLINE SDL_bool is_id3v1(const Uint8 *data, size_t length) } return SDL_TRUE; } -static SDL_INLINE SDL_bool is_id3v2(const Uint8 *data, size_t length) +static SDL_INLINE SDL_bool is_id3v1ext(const unsigned char *data, size_t length) +{ + /* ID3v1 extended tag: just before ID3v1, always 227 bytes. + * https://www.getid3.org/phpBB3/viewtopic.php?t=1202 + * https://en.wikipedia.org/wiki/ID3v1#Enhanced_tag + * Not an official standard, is only supported by few programs. */ + if (length < 4 || SDL_memcmp(data,"TAG+",4) != 0) { + return SDL_FALSE; + } + return SDL_TRUE; +} +static SDL_INLINE SDL_bool is_id3v2(const unsigned char *data, size_t length) { /* ID3v2 header is 10 bytes: http://id3.org/id3v2.4.0-structure */ /* bytes 0-2: "ID3" identifier */ @@ -282,19 +334,35 @@ static SDL_INLINE SDL_bool is_id3v2(const Uint8 *data, size_t length) } return SDL_TRUE; } -static SDL_INLINE SDL_bool is_apetag(const Uint8 *data, size_t length) +static SDL_INLINE long get_id3v2_len(const unsigned char *data, long length) +{ + /* size is a 'synchsafe' integer (see above) */ + long size = (long)((data[6]<<21) + (data[7]<<14) + (data[8]<<7) + data[9]); + size += 10; /* header size */ + /* ID3v2 header[5] is flags (bits 4-7 only, 0-3 are zero). + * bit 4 set: footer is present (a copy of the header but + * with "3DI" as ident.) */ + if (data[5] & 0x10) { + size += 10; /* footer size */ + } + /* optional padding (always zeroes) */ + while (size < length && data[size] == 0) { + ++size; + } + return size; +} +static SDL_INLINE SDL_bool is_apetag(const unsigned char *data, size_t length) { /* http://wiki.hydrogenaud.io/index.php?title=APEv2_specification - * APEv2 header is 32 bytes: bytes 0-7 ident, bytes 8-11 version, - * bytes 12-17 size. bytes 24-31 are reserved: must be all zeroes. - * APEv1 has no header, so no luck. */ + * 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. */ Uint32 v; if (length < 32 || SDL_memcmp(data,"APETAGEX",8) != 0) { return SDL_FALSE; } - v = (data[11]<<24) | (data[10]<<16) | (data[9]<<8) | data[8]; /* version */ - if (v != 2000U) { + v = (Uint32)((data[11]<<24) | (data[10]<<16) | (data[9]<<8) | data[8]); /* version */ + if (v != 2000U && v != 1000U) { return SDL_FALSE; } v = 0; /* reserved bits : */ @@ -303,48 +371,105 @@ static SDL_INLINE SDL_bool is_apetag(const Uint8 *data, size_t length) } return SDL_TRUE; } +static SDL_INLINE long get_ape_len(const unsigned char *data, long datalen, Uint32 *version) +{ + long size = (long)((data[15]<<24) | (data[14]<<16) | (data[13]<<8) | data[12]); + MIX_UNUSED(datalen); + *version = (Uint32)((data[11]<<24) | (data[10]<<16) | (data[9]<<8) | data[8]); + return size; /* caller will handle the additional v2 header length */ +} -static size_t get_tagsize(const Uint8 *data, size_t length) +static int skip_tags(MAD_Music *music) { - size_t size; - - if (is_id3v1(data, length)) { - return 128; /* fixed length */ - } - if (is_id3v2(data, length)) { - /* size is a 'synchsafe' integer (see above) */ - size = (data[6]<<21) + (data[7]<<14) + (data[8]<<7) + data[9]; - size += 10; /* header size */ - /* ID3v2 header[5] is flags (bits 4-7 only, 0-3 are zero). - * bit 4 set: footer is present (a copy of the header but - * with "3DI" as ident.) */ - if (data[5] & 0x10) { - size += 10; /* footer size */ + long len; size_t readsize; + + readsize = MAD_RWread(music, music->input_buffer, 1, MAD_INPUT_BUFFER_SIZE); + if (!readsize) return -1; + + /* ID3v2 tag is at the start */ + if (is_id3v2(music->input_buffer, readsize)) { + len = get_id3v2_len(music->input_buffer, (long)readsize); + if (len >= music->length) return -1; + music->start += len; + music->length -= len; + MAD_RWseek(music, 0, RW_SEEK_SET); + } + /* APE tag _might_ be at the start: read the header */ + else if (is_apetag(music->input_buffer, readsize)) { + Uint32 v; + len = get_ape_len(music->input_buffer, (long)readsize, &v); + len += 32; /* we're at top: have a header. */ + if (len >= music->length) return -1; + music->start += len; + music->length -= len; + MAD_RWseek(music, 0, RW_SEEK_SET); + } + + /* ID3v1 tag is at the end */ + if (music->length < 128) goto ape; + MAD_RWseek(music, -128, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 128); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 128) return -1; + if (is_id3v1(music->input_buffer, 128)) { + music->length -= 128; + + /* APE tag may be before the ID3v1: read the footer */ + if (music->length < 32) goto end; + MAD_RWseek(music, -32, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 32); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 32) return -1; + if (is_apetag(music->input_buffer, 32)) { + Uint32 v; + len = get_ape_len(music->input_buffer, (long)readsize, &v); + if (v == 2000U) len += 32; /* header */ + if (len >= music->length) return -1; + if (v == 2000U) { /* verify header : */ + MAD_RWseek(music, -len, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 32); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 32) return -1; + if (!is_apetag(music->input_buffer, 32)) return -1; + } + music->length -= len; + goto end; } - /* optional padding (always zeroes) */ - while (size < length && data[size] == 0) { - ++size; + /* extended ID3v1 just before the ID3v1 tag? (unlikely) */ + if (music->length < 227) goto end; + MAD_RWseek(music, -227, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 227); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 227) return -1; + if (is_id3v1ext(music->input_buffer, 227)) { + music->length -= 227; + goto end; } - return size; } - if (is_apetag(data, length)) { - size = (data[15]<<24) | (data[14]<<16) | (data[13]<<8) | data[12]; - size += 32; /* header size */ - return size; + ape: /* APE tag may be at the end: read the footer */ + if (music->length >= 32) { + MAD_RWseek(music, -32, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 32); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 32) return -1; + if (is_apetag(music->input_buffer, 32)) { + Uint32 v; + len = get_ape_len(music->input_buffer, (long)readsize, &v); + if (v == 2000U) len += 32; /* header */ + if (len >= music->length) return -1; + if (v == 2000U) { /* verify header : */ + MAD_RWseek(music, -len, RW_SEEK_END); + readsize = MAD_RWread(music, music->input_buffer, 1, 32); + MAD_RWseek(music, 0, RW_SEEK_SET); + if (readsize != 32) return -1; + if (!is_apetag(music->input_buffer, 32)) return -1; + } + music->length -= len; + } } - return 0; -} -static int consume_tag(struct mad_stream *stream) -{ - /* FIXME: what if the buffer doesn't have the full tag ??? */ - size_t remaining = stream->bufend - stream->next_frame; - size_t tagsize = get_tagsize(stream->this_frame, remaining); - if (tagsize != 0) { - mad_stream_skip(stream, tagsize); - return 0; - } - return -1; + end: + return (music->length > 0)? 0: -1; } /* Reads the next frame from the file. @@ -362,7 +487,7 @@ static void read_update_buffer(struct mad_stream *stream, MAD_Music *music) time. If so, move them down and read more bytes following them. */ if (stream->next_frame != NULL) { - remaining = stream->bufend - stream->next_frame; + remaining = (size_t)(stream->bufend - stream->next_frame); memmove(music->input_buffer, stream->next_frame, remaining); read_start = music->input_buffer + remaining; read_size = MAD_INPUT_BUFFER_SIZE - remaining; @@ -374,7 +499,7 @@ static void read_update_buffer(struct mad_stream *stream, MAD_Music *music) } /* Now read additional bytes from the input file. */ - read_size = SDL_RWread(music->src, read_start, 1, read_size); + read_size = MAD_RWread(music, read_start, 1, read_size); if (read_size == 0) { if ((music->status & (MS_input_eof | MS_input_error)) == 0) { @@ -406,7 +531,6 @@ static SDL_bool read_next_frame(MAD_Music *music) its buffer. */ if (mad_frame_decode(&music->frame, &music->stream)) { if (MAD_RECOVERABLE(music->stream.error)) { - consume_tag(&music->stream); /* consume any ID3 tags */ mad_stream_sync(&music->stream); /* to frame seek mode */ return SDL_FALSE; @@ -460,7 +584,7 @@ static SDL_bool decode_frame(MAD_Music *music) pcm = &music->synth.pcm; if (!music->audiostream) { - music->audiostream = SDL_NewAudioStream(AUDIO_S16, pcm->channels, pcm->samplerate, music_spec.format, music_spec.channels, music_spec.freq); + music->audiostream = SDL_NewAudioStream(AUDIO_S16, (Uint8)pcm->channels, (int)pcm->samplerate, music_spec.format, music_spec.channels, music_spec.freq); if (!music->audiostream) { return SDL_FALSE; } @@ -489,8 +613,7 @@ static SDL_bool decode_frame(MAD_Music *music) } music->sample_position += nsamples; - - result = SDL_AudioStreamPut(music->audiostream, buffer, (nsamples * nchannels * sizeof(Sint16))); + result = SDL_AudioStreamPut(music->audiostream, buffer, (int)(nsamples * nchannels * sizeof(Sint16))); SDL_stack_free(buffer); if (result < 0) { @@ -561,7 +684,8 @@ static int MAD_Seek(void *context, double position) mad_timer_reset(&music->next_frame_start); music->status &= ~MS_error_flags; - SDL_RWseek(music->src, music->position_begin, RW_SEEK_SET); + MAD_RWseek(music, 0, RW_SEEK_SET); + /* Avoid junk chunk be played after seek */ SDL_memset(music->input_buffer, 0, sizeof(music->input_buffer)); } @@ -605,6 +729,7 @@ static const char* MAD_GetMetaTag(void *context, Mix_MusicMetaTag tag_type) static void MAD_Delete(void *context) { MAD_Music *music = (MAD_Music *)context; + mad_stream_finish(&music->stream); mad_frame_finish(&music->frame); mad_synth_finish(&music->synth);