SDL_mixer: music_mad.c: Added the support for the Tell and Duration

From ca629cdba7e3a9475afdb2c39bbd2bde2dc548ab Mon Sep 17 00:00:00 2001
From: Wohlstand <[EMAIL REDACTED]>
Date: Thu, 18 Feb 2021 10:00:37 +0300
Subject: [PATCH] music_mad.c: Added the support for the Tell and Duration

This update I developed a while ago for the MixerX fork
---
 src/codecs/music_mad.c | 204 +++++++++++++++++++++++++++++++++++++++--
 1 file changed, 194 insertions(+), 10 deletions(-)

diff --git a/src/codecs/music_mad.c b/src/codecs/music_mad.c
index 8a16140..1ab9f10 100644
--- a/src/codecs/music_mad.c
+++ b/src/codecs/music_mad.c
@@ -150,9 +150,164 @@ typedef struct {
     unsigned short last_nchannels;
     unsigned int last_samplerate;
 
+    double total_length;
+    int sample_rate;
+    int sample_position;
+
     unsigned char input_buffer[MAD_INPUT_BUFFER_SIZE + MAD_BUFFER_GUARD];
 } MAD_Music;
 
+static void read_update_buffer(struct mad_stream *stream, MAD_Music *music);
+
+static double extract_length(struct mad_header *header, struct mad_stream *stream, Sint64 file_size)
+{
+    int mpeg_version = 0;
+    int xing_offset = 0;
+    Uint32 samples_per_frame = 0;
+    Uint32 frames_count = 0;
+    unsigned char const *frames_count_raw;
+
+    /* There are two methods to compute duration:
+     * - Using Xing/Info/VBRI headers
+     * - Rely on filesize and first size of frame in condition of CRB
+     * https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header#VBRHeaders
+     */
+
+    if (!stream->this_frame || !stream->next_frame ||
+        stream->next_frame <= stream->this_frame ||
+        (stream->next_frame - stream->this_frame) < 48) {
+        return -1.0; /* Too small buffer to get any necessary headers */
+    }
+
+    mpeg_version = (stream->this_frame[1] >> 3) & 0x03;
+
+    switch(mpeg_version) {
+    case 0x03: /* MPEG1 */
+        if (header->mode == MAD_MODE_SINGLE_CHANNEL) {
+            xing_offset = 4 + 17;
+        } else {
+            xing_offset = 4 + 32;
+        }
+        break;
+    default:  /* MPEG2 and MPEG2.5 */
+        if (header->mode == MAD_MODE_SINGLE_CHANNEL) {
+            xing_offset = 4 + 17;
+        } else {
+            xing_offset = 4 + 9;
+        }
+        break;
+    }
+
+    switch(header->layer)
+    {
+    case MAD_LAYER_I:
+        samples_per_frame = 384;
+        break;
+    case MAD_LAYER_II:
+        samples_per_frame = 1152;
+        break;
+    case MAD_LAYER_III:
+        if (mpeg_version == 0x03) {
+            samples_per_frame = 1152;
+        } else {
+            samples_per_frame = 576;
+        }
+        break;
+    default:
+        return -1.0;
+    }
+
+    if (SDL_memcmp(stream->this_frame + xing_offset, "Xing", 4) == 0 ||
+        SDL_memcmp(stream->this_frame + xing_offset, "Info", 4) == 0) {
+        /* Xing header to get the count of frames for VBR */
+        frames_count_raw = stream->this_frame + xing_offset + 8;
+        frames_count = ((Uint32)frames_count_raw[0] << 24) +
+                       ((Uint32)frames_count_raw[1] << 16) +
+                       ((Uint32)frames_count_raw[2] << 8) +
+                       ((Uint32)frames_count_raw[3]);
+    }
+    else if (SDL_memcmp(stream->this_frame + xing_offset, "VBRI", 4) == 0) {
+        /* VBRI header to get the count of frames for VBR */
+        frames_count_raw = stream->this_frame + xing_offset + 14;
+        frames_count = ((Uint32)frames_count_raw[0] << 24) +
+                       ((Uint32)frames_count_raw[1] << 16) +
+                       ((Uint32)frames_count_raw[2] << 8) +
+                       ((Uint32)frames_count_raw[3]);
+    } else {
+        /* To get a count of frames for CBR, divide the file size with a size of one frame */
+        frames_count = (Uint32)(file_size / (stream->next_frame - stream->this_frame));
+    }
+
+    return (double)(frames_count * samples_per_frame) / header->samplerate;
+}
+
+static int calculate_total_time(MAD_Music *music)
+{
+    mad_timer_t time = mad_timer_zero;
+    struct mad_header header;
+    struct mad_stream stream;
+    SDL_bool is_first_frame = SDL_TRUE;
+    int ret = 0;
+
+    mad_header_init(&header);
+    mad_stream_init(&stream);
+
+    while (1)
+    {
+        read_update_buffer(&stream, music);
+
+        if (mad_header_decode(&header, &stream) == -1) {
+            if (MAD_RECOVERABLE(stream.error)) {
+                if ((music->status & MS_input_error) == 0) {
+                    continue;
+                }
+                if (is_first_frame) {
+                    ret = -1;
+                }
+                break;
+            } else if (stream.error == MAD_ERROR_BUFLEN) {
+                if ((music->status & MS_input_error) == 0) {
+                    continue;
+                }
+                if (is_first_frame) {
+                    ret = -1;
+                }
+                break;
+            } else {
+                Mix_SetError("mad_frame_decode() failed, corrupt stream?");
+                music->status |= MS_decode_error;
+                if (is_first_frame) {
+                    ret = -1;
+                }
+                break;
+            }
+        }
+
+        music->sample_rate = (int)header.samplerate;
+        mad_timer_add(&time, header.duration);
+
+        if (is_first_frame) {
+            music->total_length = extract_length(&header, &stream, music->mp3file.length);
+            if (music->total_length > 0.0) {
+                break; /* Duration has been recognized */
+            }
+            is_first_frame = SDL_FALSE;
+            /* Otherwise, do the full scan of MP3 file to retrieve a duration */
+        }
+    }
+
+    if (!is_first_frame) {
+        music->total_length = (double)(mad_timer_count(time, (enum mad_units)music->sample_rate)) / (double)music->sample_rate;
+    }
+    mad_stream_finish(&stream);
+    mad_header_finish(&header);
+    SDL_memset(music->input_buffer, 0, sizeof(music->input_buffer));
+
+    music->status = 0;
+
+    MP3_RWseek(&music->mp3file, 0, RW_SEEK_SET);
+    return ret;
+}
 
 static int MAD_Seek(void *context, double position);
 
@@ -177,6 +332,12 @@ static void *MAD_CreateFromRW(SDL_RWops *src, int freesrc)
         return NULL;
     }
 
+    if (calculate_total_time(music) < 0) {
+        SDL_free(music);
+        Mix_SetError("music_mad: corrupt mp3 file (bad stream.)");
+        return NULL;
+    }
+
     mad_stream_init(&music->stream);
     mad_frame_init(&music->frame);
     mad_synth_init(&music->synth);
@@ -210,10 +371,10 @@ static int MAD_Play(void *context, int play_count)
 /* Reads the next frame from the file.
    Returns true on success or false on failure.
  */
-static SDL_bool read_next_frame(MAD_Music *music)
+static void read_update_buffer(struct mad_stream *stream, MAD_Music *music)
 {
-    if (music->stream.buffer == NULL ||
-        music->stream.error == MAD_ERROR_BUFLEN) {
+    if (stream->buffer == NULL ||
+        stream->error == MAD_ERROR_BUFLEN) {
         size_t read_size;
         size_t remaining;
         unsigned char *read_start;
@@ -221,9 +382,9 @@ static SDL_bool read_next_frame(MAD_Music *music)
         /* There might be some bytes in the buffer left over from last
            time.    If so, move them down and read more bytes following
            them. */
-        if (music->stream.next_frame != NULL) {
-            remaining = music->stream.bufend - music->stream.next_frame;
-            SDL_memmove(music->input_buffer, music->stream.next_frame, remaining);
+        if (stream->next_frame != NULL) {
+            remaining = (size_t)(stream->bufend - stream->next_frame);
+            SDL_memmove(music->input_buffer, stream->next_frame, remaining);
             read_start = music->input_buffer + remaining;
             read_size = MAD_INPUT_BUFFER_SIZE - remaining;
 
@@ -249,10 +410,18 @@ static SDL_bool read_next_frame(MAD_Music *music)
         }
 
         /* Now feed those bytes into the libmad stream. */
-        mad_stream_buffer(&music->stream, music->input_buffer,
+        mad_stream_buffer(stream, music->input_buffer,
                                             read_size + remaining);
-        music->stream.error = MAD_ERROR_NONE;
+        stream->error = MAD_ERROR_NONE;
     }
+}
+
+/* Reads the next frame from the file.
+   Returns true on success or false on failure.
+ */
+static SDL_bool read_next_frame(MAD_Music *music)
+{
+    read_update_buffer(&music->stream, music);
 
     /* Now ask libmad to extract a frame from the data we just put in
        its buffer. */
@@ -345,6 +514,7 @@ static SDL_bool decode_frame(MAD_Music *music)
         }
     }
 
+    music->sample_position += nsamples;
     result = SDL_AudioStreamPut(music->audiostream, buffer, (int)(nsamples * nchannels * sizeof(Sint16)));
     SDL_stack_free(buffer);
 
@@ -405,6 +575,8 @@ static int MAD_Seek(void *context, double position)
     int_part = (int)position;
     mad_timer_set(&target, (unsigned long)int_part, (unsigned long)((position - int_part) * 1000000), 1000000);
 
+    music->sample_position = (int)(position * music->sample_rate);
+
     if (mad_timer_compare(music->next_frame_start, target) > 0) {
         /* In order to seek backwards in a VBR file, we have to rewind and
            start again from the beginning.    This isn't necessary if the
@@ -439,6 +611,18 @@ static int MAD_Seek(void *context, double position)
     return 0;
 }
 
+static double MAD_Tell(void *context)
+{
+    MAD_Music *music = (MAD_Music *)context;
+    return (double)music->sample_position / (double)music->sample_rate;
+}
+
+static double MAD_Duration(void *context)
+{
+    MAD_Music *music = (MAD_Music *)context;
+    return music->total_length;
+}
+
 static void MAD_Delete(void *context)
 {
     MAD_Music *music = (MAD_Music *)context;
@@ -475,8 +659,8 @@ Mix_MusicInterface Mix_MusicInterface_MAD =
     MAD_GetAudio,
     NULL,   /* Jump */
     MAD_Seek,
-    NULL,   /* Tell */
-    NULL,   /* Duration */
+    MAD_Tell,
+    MAD_Duration,
     NULL,   /* LoopStart */
     NULL,   /* LoopEnd */
     NULL,   /* LoopLength */