Most decoders now report total sample play time, now. Technically, this
authorRyan C. Gordon <icculus@icculus.org>
Sat, 08 May 2004 08:19:50 +0000
changeset 474 c66080364dff
parent 473 1edb89260487
child 475 f0b8865577db
Most decoders now report total sample play time, now. Technically, this
breaks binary compatibility with the 1.0 branch, since it extends the
Sound_Sample struct, but most (all?) programs are just passing pointers
allocated by SDL_sound around, and might be okay.

Source-level compatibility is not broken...yet! :)

--ryan.


-------- Original Message --------
Subject: SDL_sound patch: Finding total length of time of sound file.
Date: Sun, 26 Jan 2003 09:31:17 -0800 (PST)

Hi Ryan,

I am working with Eric Wing and helping him modify
SDL_sound. AS part of our efforts in improving and
enhancing SDL_sound, we like to submit this patch. We
modified the codecs to find the total time of a sound
file. Below is the explanation of the patch. The
patch is appended as an attachment to this email.


* MOTIVATION:
We needed the ability to get the total play time of a
sample (And we noticed that we're not the only ones).
Since SDL_sound blocks direct access to the specific
decoders, there is no way for a user to know this
information short of decoding the whole thing.
Because of this, we believe this will be a useful
addition, even though the accuracy may not be perfect
(subject to each decoder) or the information may not
always be available.


* CONTRIBUTORS:
Wesley Leong (modified the majority of the codecs and
verified the results)
Eric Wing (showed everyone how to do modify codec,
modified mikmod)
Wang Lam (modified a handful of codecs, researched
into specs and int overflow)
Ahilan Anantha (modified a few codecs and helped with
integer math)


* GENERAL ISSUES:
We chose the value to be milliseconds as an Sint32.
Milliseconds because that's what Sound_Seek takes as a
parameter and -1 to allow for instances/codecs where
the value could not be determined. We are
not sure if this is the final convention you want, so
we are willing to work with you on this.

We also expect the total_time field to be set on open
and never again modified by SDL_sound. Users may
access it directly much like the sample buffer and
buffer_size. We thought about recomputing the time
on DecodeAll, but since users may seek or decode small
chunks first, not all the data may be there. So this
is better done by the user. This may be good
information to document.

Currently, all the main codecs are implemented except
for QuickTime.
CHANGELOG
SDL_sound.h
decoders/aiff.c
decoders/au.c
decoders/flac.c
decoders/midi.c
decoders/mikmod.c
decoders/modplug.c
decoders/mpglib.c
decoders/mpglib/mpg123_sdlsound.h
decoders/mpglib/mpglib_common.c
decoders/ogg.c
decoders/quicktime.c
decoders/raw.c
decoders/shn.c
decoders/smpeg.c
decoders/timidity/playmidi.c
decoders/timidity/readmidi.c
decoders/timidity/timidity.c
decoders/timidity/timidity.h
decoders/voc.c
decoders/wav.c
--- a/CHANGELOG	Sat May 08 03:14:42 2004 +0000
+++ b/CHANGELOG	Sat May 08 08:19:50 2004 +0000
@@ -2,6 +2,10 @@
  * CHANGELOG.
  */
 
+05082004 - Started 1.1.0 development branch with code to determine the total
+           length of a sample (thanks to Wesley, Eric, Wang, and Ahilan!)...
+           this patch was originally committed to CVS on 10252003, but it is
+           getting re-merged in the svn repository.
 05072004 - Backed out some commits, converted repository to Subversion, and
            branched off to a 1.1.0 development tree. Changed MikMod URL...old
            one is now a porn site.  :(
--- a/SDL_sound.h	Sat May 08 03:14:42 2004 +0000
+++ b/SDL_sound.h	Sat May 08 08:19:50 2004 +0000
@@ -185,6 +185,7 @@
     void *buffer;  /**< Decoded sound data lands in here. */
     Uint32 buffer_size;  /**< Current size of (buffer), in bytes (Uint8). */
     Sound_SampleFlags flags;  /**< Flags relating to this sample. */
+    Sint32 total_time;  /**< Total length of song or track */
 } Sound_Sample;
 
 
--- a/decoders/aiff.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/aiff.c	Sat May 08 08:19:50 2004 +0000
@@ -476,6 +476,11 @@
     sample->actual.channels = (Uint8) c.numChannels;
     sample->actual.rate = c.sampleRate;
 
+    /* Really, sample->total_time = (c.numSampleFrames*1000) c.sampleRate */
+    sample->total_time = (c.numSampleFrames / c.sampleRate) * 1000;
+    sample->total_time += (c.numSampleFrames % c.sampleRate) 
+                       *  1000 / c.sampleRate;
+
     if (c.sampleSize <= 8)
     {
         sample->actual.format = AUDIO_S8;
--- a/decoders/au.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/au.c	Sat May 08 08:19:50 2004 +0000
@@ -166,7 +166,7 @@
 {
     Sound_SampleInternal *internal = sample->opaque;
     SDL_RWops *rw = internal->rw;
-    int skip, hsize, i;
+    int skip, hsize, i, bytes_per_second;
     struct au_file_hdr hdr;
     struct audec *dec;
     char c;
@@ -242,7 +242,14 @@
     {
         free(dec);
         BAIL_MACRO("AU: Not an .AU stream.", 0);
-    } /* else */
+    } /* else */    
+
+    bytes_per_second = ( ( dec->encoding == AU_ENC_LINEAR_16 ) ? 2 : 1 )
+        * sample->actual.rate * sample->actual.channels ;
+    sample->total_time = ((dec->remaining == -1) ? (-1) :
+			  ( ( dec->remaining / bytes_per_second ) * 1000 ) +
+			  ( ( dec->remaining % bytes_per_second ) * 1000 /
+                          bytes_per_second ) );
 
     sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
     dec->total = dec->remaining;
--- a/decoders/flac.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/flac.c	Sat May 08 08:19:50 2004 +0000
@@ -247,6 +247,17 @@
         f->sample->actual.channels = metadata->data.stream_info.channels;
         f->sample->actual.rate = metadata->data.stream_info.sample_rate;
 
+	if (metadata->data.stream_info.sample_rate == 0 ||
+            metadata->data.stream_info.total_samples == 0)
+	  f->sample->total_time = -1;
+	else {
+	    f->sample->total_time = (metadata->data.stream_info.total_samples)
+                / metadata->data.stream_info.sample_rate * 1000;
+	    f->sample->total_time += (metadata->data.stream_info.total_samples
+                % metadata->data.stream_info.sample_rate) * 1000
+                / metadata->data.stream_info.sample_rate;
+	}
+
         if (metadata->data.stream_info.bits_per_sample > 8)
             f->sample->actual.format = AUDIO_S16MSB;
         else
--- a/decoders/midi.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/midi.c	Sat May 08 08:19:50 2004 +0000
@@ -112,6 +112,7 @@
     sample->actual.rate = 44100;
     sample->actual.format = AUDIO_S16SYS;
     
+    sample->total_time = Timidity_GetSongLength(song);
     sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
     return(1); /* we'll handle this data. */
 } /* MIDI_open */
--- a/decoders/mikmod.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/mikmod.c	Sat May 08 08:19:50 2004 +0000
@@ -202,7 +202,7 @@
 
 static void _mm_delete_rwops_reader(MREADER *reader)
 {
-        /* SDL_sound will delete the RWops and sample at a higher level... */
+    /* SDL_sound will delete the RWops and sample at a higher level... */
     if (reader != NULL)
         free(reader);
 } /* _mm_delete_rwops_reader */
@@ -213,16 +213,18 @@
 {
     MikMod_RegisterDriver(&drv_nos);
     
-    /* Quick and dirty hack to prevent an infinite loop problem
-     * found when using SDL_mixer and SDL_sound together and 
-     * both have MikMod compiled in. So, check to see if
-     * MikMod has already been registered first before calling
-     * RegisterAllLoaders()
-     */
-    if(MikMod_InfoLoader() == NULL)
+        /*
+         * Quick and dirty hack to prevent an infinite loop problem
+         *  found when using SDL_mixer and SDL_sound together and
+         *  both have MikMod compiled in. So, check to see if
+         *  MikMod has already been registered first before calling
+         *  RegisterAllLoaders()
+         */
+    if (MikMod_InfoLoader() == NULL)
     {
         MikMod_RegisterAllLoaders();
-    }  
+    } /* if */
+
         /*
          * Both DMODE_SOFT_MUSIC and DMODE_16BITS should be set by default,
          * so this is just for clarity. I haven't experimented with any of
@@ -251,7 +253,9 @@
     Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
     MREADER *reader;
     MODULE *module;
-
+    Uint32 i; /* temp counter for time computation */
+    double segment_time = 0.0; /* temp holder for time */
+    
     reader = _mm_new_rwops_reader(sample);
     BAIL_IF_MACRO(reader == NULL, ERR_OUT_OF_MEMORY, 0);
     module = Player_LoadGeneric(reader, 64, 0);
@@ -276,10 +280,110 @@
 
     sample->flags = SOUND_SAMPLEFLAG_NONE;
 
+    /*
+     *   module->sngtime = current song time in 2^-10 seconds
+     *   sample->total_time = (module->sngtime * 1000) / (1<<10)
+     */
+    sample->total_time = (module->sngtime * 1000) / (1<<10);
+
     SNDDBG(("MIKMOD: Name: %s\n", module->songname));
     SNDDBG(("MIKMOD: Type: %s\n", module->modtype));
     SNDDBG(("MIKMOD: Accepting data stream\n"));
 
+
+    /* 
+     * This is a quick and dirty way for getting the play time
+     * of a file. This will often be wrong because the tracker format
+     * allows for so much. If you want a better one, use ModPlug,
+     * demand that the Mikmod people write better functionality,
+     * or write a more complicated version of the code below.
+     *
+     * There are two dumb ways to compute the length. The really
+     * dumb way is to look at the header and take the initial
+     * speed/tempo  values. However, speed values can change throughout
+     * the file. The slightly smarter way is to iterate through
+     * all the positions and add up each segment's time based
+     * on the idea that each segment will give us its own
+     * speed value. The hope is that this is more accurate.
+     * However, this demands that the file be seekable
+     * and that we can change the position of the sample.
+     * Depending on the assumptions of SDL_sound, this block
+     * of code should be enabled or disabled. If disabled,
+     * you still can make the computations doing the first method.
+     * For now, we will assume it's acceptable to seek a Mod file
+     * since this is essentially how Modplug also does it.
+     *
+     * Keep in mind that this will be incorrect for loops, jumps, short
+     * patterns and other features.
+     */
+    sample->flags |= SOUND_SAMPLEFLAG_CANSEEK;
+
+    /* 
+     * For each position (which corresponds to a particular pattern),
+     * get the speed values and compute the time length of the segment
+     */
+    sample->total_time = 0;
+    for (i = 0; i < module->numpos; i++)
+    {
+        Player_SetPosition(i);
+        /* Must call update, or the speed values won't get reset */
+        MikMod_Update();
+        /* Now the magic formula:
+         * Multiply the number of positions by the
+         * Number of rows (usually 64 but can be different) by the
+         * time it takes to read one row (1/50)
+         * by the speed by the 
+         * magic reference beats per minute / the beats per minute
+         * 
+         * We're using positions instead of patterns because in our
+         * test cases, this seems to be the correct value for the 
+         * number of sections you hear during normal playback.
+         * They typically map to a fewer number of patterns
+         * where some patterns get replayed multiple times
+         * in a song (think chorus). Since we're in a for-loop,
+         * the multiplication is implicit while we're adding
+         * all the segments.
+         * 
+         * From a tracker format spec, it seems that 64 rows
+         * is the normal (00-3F), but I've seen songs that 
+         * either have less or are jumping positions in the
+         * middle of a pattern. It looks like Mikmod might
+         * reveal this number for us.
+         *
+         * According to the spec, it seems that a speed of 1
+         * corresponds to reading 1 row in 50 ticks. However,
+         * I'm not sure if ticks are real seconds or this
+         * notion of second units: 
+         * Assuming that it's just normal seconds, we get 1/50 = 0.02.
+         *
+         * The current speed and current tempo (beats per minute) 
+         * we can just grab. However, we need a magic number 
+         * to figure out what the tempo is based on. Our primitive
+         * stopwatch results and intuition seem to imply 120-130bpm 
+         * is the magic number. Looking at the majority of tracker
+         * files I have, 125 seems to be the common value. Furthermore
+         * most (if not all) of my Future Crew .S3M (Scream Tracker 3)
+         * files also use 125. Since they invented that format, 
+         * I'll also assume that's the base number.
+         */
+        if(module->bpm == 0)
+        {
+            /* 
+             * Should never get here, but I don't want any
+             * divide by zero errors
+             */
+            continue;
+        } /* if */
+        segment_time += (module->numrow * .02 * module->sngspd *
+                          125.0 / module->bpm);
+    } /* for */
+    /* Now convert to milliseconds and store the value */
+    sample->total_time = (Sint32)(segment_time * 1000); 
+
+    /* Reset the sample to the beginning */
+    Player_SetPosition(0);
+    MikMod_Update();
+
     return(1); /* we'll handle this data. */
 } /* MIKMOD_open */
 
@@ -322,20 +426,76 @@
 
 static int MIKMOD_seek(Sound_Sample *sample, Uint32 ms)
 {
-#if 0
     Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
     MODULE *module = (MODULE *) internal->decoder_private;
+    double last_time = 0.0;
+    double current_time = 0.0;
+    double target_time;
+    Uint32 i; 
 
         /*
          * Heaven may know what the argument to Player_SetPosition() is.
          * I, however, haven't the faintest idea.
          */
     Player_Start(module);
-    Player_SetPosition(ms);
+
+        /*
+         * Mikmod only lets you seek to the beginning of a pattern.
+         * This means we'll get very coarse grain seeks. The 
+         * value we pass to SetPosition is a value between 0 
+         * and the number of positions in the file. The
+         * dumb approach would be to take our total_time that
+         * we've already calculated and divide it up by the 
+         * number of positions and seek to the position that results.
+         * However, because songs can alter their speed/tempo during
+         * playback, different patterns in the song can take 
+         * up different amounts of time. So the slightly
+         * smarter approach is to repeat what was done in the
+         * total_time computation and traverse through the file
+         * until we find the closest position.
+         * The follwing is basically cut and paste from the 
+         * open function.
+         */
+    if (ms == 0)  /* Check end conditions to simplify things */
+    {
+        Player_SetPosition(0);
+        return(1);
+    } /* if */
+
+    if (ms >= sample->total_time)
+        Player_SetPosition(module->numpos);
+
+    /* Convert time to seconds (double) to make comparisons easier */
+    target_time = ms / 1000.0;
+    
+    for (i = 0; i < module->numpos; i++)
+    {
+        Player_SetPosition(i);
+        /* Must call update, or the speed values won't get reset */
+        MikMod_Update();
+        /* Divide by zero check */
+        if(module->bpm == 0)
+            continue;
+        last_time = current_time;
+        /* See the notes in the open function about the formula */
+        current_time += (module->numrow * .02 
+            * module->sngspd * 125.0 / module->bpm);
+        if(target_time <= current_time)
+            break; /* We now have our interval, so break out */
+    } /* for */
+    
+    if( (target_time-last_time) > (current_time-target_time) )
+    {
+        /* The target time is closer to the higher position, so go there */
+        Player_SetPosition(i+1);
+    } /* if */
+    else
+    {
+        /* The target time is closer to the lower position, so go there */
+        Player_SetPosition(i);
+    } /* else */
+
     return(1);
-#else
-    BAIL_MACRO("MIKMOD: Seeking not implemented", 0);
-#endif
 } /* MIKMOD_seek */
 
 #endif /* SOUND_SUPPORTS_MIKMOD */
--- a/decoders/modplug.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/modplug.c	Sat May 08 08:19:50 2004 +0000
@@ -270,6 +270,7 @@
     if (modplug_mutex != NULL)
         SDL_UnlockMutex(modplug_mutex);
 
+    sample->total_time = ModPlug_GetLength(module);
     SNDDBG(("MODPLUG: [%d ms] %s\n",
             ModPlug_GetLength(module), ModPlug_GetName(module)));
 
--- a/decoders/mpglib.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/mpglib.c	Sat May 08 08:19:50 2004 +0000
@@ -175,6 +175,33 @@
     sample->actual.format = AUDIO_S16SYS;
     sample->flags = SOUND_SAMPLEFLAG_NONE;
 
+    {
+      /* Finds approximate length of the song. */
+        Uint32 pos, total_byte_size;
+        char tag_buffer[4];
+        memset(tag_buffer, '\0', sizeof(tag_buffer));
+
+        /* Get size of file first */
+        pos = SDL_RWtell(internal->rw);
+        total_byte_size = SDL_RWseek(internal->rw, -128, SEEK_END);
+        if( total_byte_size <= 0 ) {
+            BAIL_MACRO("MPGLIB: Not an MP3 stream.", 0);
+        }
+        if ( SDL_RWread(internal->rw, tag_buffer, 1, 3) != 3) {
+            BAIL_MACRO("MPGLIB: Cannot identify  TAG section.", 0);
+	}
+        if ( strncmp(tag_buffer, "TAG", 3) != 0 ) {
+            /* ENDING TAG NOT FOUND */
+            total_byte_size = SDL_RWseek(internal->rw,0, SEEK_END);
+        }
+        if (SDL_RWseek(internal->rw, pos, SEEK_SET) != pos) {
+            BAIL_MACRO("MPGLIB: Cannot go back to save spot in file.", 0);
+	}
+        sample->total_time = total_byte_size / mpg->mp.fr.bitrate * 8.0;
+        sample->total_time += (total_byte_size % mpg->mp.fr.bitrate) * 8.0
+            / mpg->mp.fr.bitrate;
+    }
+
     return(1); /* we'll handle this data. */
 } /* MPGLIB_open */
 
--- a/decoders/mpglib/mpg123_sdlsound.h	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/mpglib/mpg123_sdlsound.h	Sat May 08 08:19:50 2004 +0000
@@ -90,6 +90,8 @@
     /* layer2 stuff */
     int II_sblimit;
     void *alloc;
+
+    int bitrate;
 };
 
 struct parameter {
--- a/decoders/mpglib/mpglib_common.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/mpglib/mpglib_common.c	Sat May 08 08:19:50 2004 +0000
@@ -146,6 +146,8 @@
       default:
         BAIL_MACRO("MPGLIB: Unknown layer type.", 0);
     }
+
+    fr->bitrate = (int) tabsel_123[fr->lsf][(fr->lay - 1)][fr->bitrate_index];
     return 1;
 }
 
--- a/decoders/ogg.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/ogg.c	Sat May 08 08:19:50 2004 +0000
@@ -171,6 +171,7 @@
 static int OGG_open(Sound_Sample *sample, const char *ext)
 {
     int rc;
+    double total_time;
     OggVorbis_File *vf;
     vorbis_info *info;
     Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
@@ -209,6 +210,12 @@
     sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
     sample->actual.rate = (Uint32) info->rate;
     sample->actual.channels = (Uint8) info->channels;
+    total_time = ov_time_total(vf, -1);
+    if (OV_EINVAL == total_time)
+      sample->total_time = -1;
+    else
+      sample->total_time = (Sint32)(total_time * 1000.0 + 0.5);
+
 
     /*
      * Since we might have more than one logical bitstream in the OGG file,
--- a/decoders/quicktime.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/quicktime.c	Sat May 08 08:19:50 2004 +0000
@@ -547,6 +547,8 @@
   instance = QT_open_internal(sample, ext);
   internal->decoder_private = (void*)instance;
 
+  sample->total_time = -1;  /* return -1 for total time of song for now */ 
+
   return(instance != NULL);
     
 } /* QT_open */
--- a/decoders/raw.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/raw.c	Sat May 08 08:19:50 2004 +0000
@@ -95,6 +95,10 @@
 
 static int RAW_open(Sound_Sample *sample, const char *ext)
 {
+    Sound_SampleInternal *internal = sample->opaque;
+    SDL_RWops *rw = internal->rw;
+    Uint32 pos, sample_rate;
+  
         /*
          * We check this explicitly, since we have no other way to
          *  determine whether we should handle this data or not.
@@ -122,6 +126,18 @@
     memcpy(&sample->actual, &sample->desired, sizeof (Sound_AudioInfo));
     sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
 
+    if ( (pos = SDL_RWseek(internal->rw, 0, SEEK_END) ) <= 0) {
+        BAIL_MACRO("RAW: cannot seek the end of the file \"RAW\".", 0);
+    }
+    if ( SDL_RWseek(internal->rw, 0, SEEK_SET) ) {
+        BAIL_MACRO("RAW: cannot reset file \"RAW\".", 0);
+    }
+
+    sample_rate =  (sample->actual.rate * sample->actual.channels
+      * ( (sample->actual.format & 0x0018) >> 3) );
+    sample->total_time = ( pos ) / sample_rate * 1000;
+    sample->total_time += (pos % sample_rate) * 1000 / sample_rate;
+
     return(1); /* we'll handle this data. */
 } /* RAW_open */
 
--- a/decoders/shn.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/shn.c	Sat May 08 08:19:50 2004 +0000
@@ -551,6 +551,7 @@
     Uint16 u16;
     Uint32 u32;
     Sint32 cklen;
+    Uint32 bytes_per_second;
 
     BAIL_IF_MACRO(!uvar_get(SHN_VERBATIM_CKSIZE_SIZE, shn, rw, &cklen), NULL, 0);
 
@@ -572,13 +573,15 @@
     sample->actual.rate = u32;
 
     BAIL_IF_MACRO(!verb_ReadLE32(shn, rw, &u32), NULL, 0); /* bytespersec */
+    bytes_per_second = u32;
     BAIL_IF_MACRO(!verb_ReadLE16(shn, rw, &u16), NULL, 0); /* blockalign */
     BAIL_IF_MACRO(!verb_ReadLE16(shn, rw, &u16), NULL, 0); /* bitspersample */
 
     BAIL_IF_MACRO(!verb_ReadLE32(shn, rw, &u32), NULL, 0); /* 'data' header */
     BAIL_IF_MACRO(u32 != dataID,  "SHN: No 'data' header.", 0);
     BAIL_IF_MACRO(!verb_ReadLE32(shn, rw, &u32), NULL, 0); /* chunksize */
-
+    sample->total_time = u32 / bytes_per_second * 1000;
+    sample->total_time += (u32 % bytes_per_second) * 1000 / bytes_per_second;
     return(1);
 } /* parse_riff_header */
 
--- a/decoders/smpeg.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/smpeg.c	Sat May 08 08:19:50 2004 +0000
@@ -212,6 +212,7 @@
     sample->actual.rate = spec.freq;
     sample->actual.channels = spec.channels;
     sample->flags = SOUND_SAMPLEFLAG_CANSEEK;
+    sample->total_time = smpeg_info.total_time * 1000;
     internal->decoder_private = smpeg;
 
     SMPEG_play(smpeg);
--- a/decoders/timidity/playmidi.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/timidity/playmidi.c	Sat May 08 08:19:50 2004 +0000
@@ -662,6 +662,15 @@
     skip_to(song, (ms * song->rate) / 1000);
 }
 
+Uint32 Timidity_GetSongLength(MidiSong *song)
+{
+  MidiEvent *last_event = &song->events[song->groomed_event_count - 1];
+  /* We want last_event->time * 1000 / song->rate */
+  Uint32 retvalue = (last_event->time / song->rate) * 1000;
+  retvalue       += (last_event->time % song->rate) * 1000 / song->rate;
+  return retvalue;
+}
+
 int Timidity_PlaySome(MidiSong *song, void *stream, Sint32 len)
 {
   Sint32 start_sample, end_sample, samples;
--- a/decoders/timidity/readmidi.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/timidity/readmidi.c	Sat May 08 08:19:50 2004 +0000
@@ -488,7 +488,7 @@
   lp->type=ME_EOT;
   our_event_count++;
   free_midi_list(song);
-  
+
   *eventsp=our_event_count;
   *samplesp=st;
   return groomed_list;
--- a/decoders/timidity/timidity.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/timidity/timidity.c	Sat May 08 08:19:50 2004 +0000
@@ -438,7 +438,6 @@
 MidiSong *Timidity_LoadDLSSong(SDL_RWops *rw, DLS_Patches *patches, SDL_AudioSpec *audio)
 {
   MidiSong *song;
-  Sint32 events;
   int i;
 
   if (rw == NULL)
@@ -514,7 +513,8 @@
   song->lost_notes = 0;
   song->cut_notes = 0;
 
-  song->events = read_midi_file(song, &events, &song->samples);
+  song->events = read_midi_file(song, &(song->groomed_event_count),
+      &song->samples);
 
   /* The RWops can safely be closed at this point, but let's make that the
    * responsibility of the caller.
--- a/decoders/timidity/timidity.h	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/timidity/timidity.h	Sat May 08 08:19:50 2004 +0000
@@ -153,6 +153,7 @@
     Sint32 current_sample;
     Sint32 event_count;
     Sint32 at;
+    Sint32 groomed_event_count;
 } MidiSong;
 
 /* Some of these are not defined in timidity.c but are here for convenience */
@@ -167,6 +168,7 @@
 extern MidiSong *Timidity_LoadSong(SDL_RWops *rw, SDL_AudioSpec *audio);
 extern void Timidity_Start(MidiSong *song);
 extern void Timidity_Seek(MidiSong *song, Uint32 ms);
+extern Uint32 Timidity_GetSongLength(MidiSong *song); /* returns millseconds */
 extern void Timidity_FreeSong(MidiSong *song);
 extern void Timidity_Exit(void);
 
--- a/decoders/voc.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/voc.c	Sat May 08 08:19:50 2004 +0000
@@ -196,6 +196,7 @@
     Uint32 new_rate_long;
     Uint8 trash[6];
     Uint16 period;
+    Uint32 bytes_per_second;
     int i;
 
     v->silent = 0;
@@ -241,6 +242,12 @@
                 v->extended = 0;
                 v->rest = sblen - 2;
                 v->size = ST_SIZE_BYTE;
+
+                bytes_per_second = sample->actual.rate
+                    * sample->actual.channels;
+                sample->total_time += ( v->rest ) / bytes_per_second * 1000;
+		sample->total_time += (v->rest % bytes_per_second) * 1000
+		    / bytes_per_second;
                 return 1;
 
             case VOC_DATA_16:
@@ -272,8 +279,13 @@
 
                 if (!voc_readbytes(src, v, trash, sizeof (Uint8) * 6))
                     return 0;
+                v->rest = sblen - 12;
 
-                v->rest = sblen - 12;
+		bytes_per_second = ((v->size == ST_SIZE_WORD) ? (2) : (1)) *
+		    sample->actual.rate * v->channels;
+                sample->total_time += v->rest / bytes_per_second * 1000;
+                sample->total_time += ( v->rest % bytes_per_second ) * 1000
+		    / bytes_per_second;
                 return 1;
 
             case VOC_CONT:
@@ -302,6 +314,9 @@
                     v->rate = uc;
                 v->rest = period;
                 v->silent = 1;
+
+		sample->total_time += (period) / (v->rate) * 1000;
+		sample->total_time += (period % v->rate) * 1000 / v->rate; 
                 return 1;
 
             case VOC_LOOP:
--- a/decoders/wav.c	Sat May 08 03:14:42 2004 +0000
+++ b/decoders/wav.c	Sat May 08 08:19:50 2004 +0000
@@ -722,8 +722,11 @@
     fmt->data_starting_offset = SDL_RWtell(rw);
     fmt->sample_frame_size = ( ((sample->actual.format & 0xFF) / 8) *
                                sample->actual.channels );
+    internal->decoder_private = (void *) w;
 
-    internal->decoder_private = (void *) w;
+    sample->total_time = (fmt->total_bytes / fmt->dwAvgBytesPerSec) * 1000;
+    sample->total_time += (fmt->total_bytes % fmt->dwAvgBytesPerSec)
+                       *  1000 / fmt->dwAvgBytesPerSec;
 
     sample->flags = SOUND_SAMPLEFLAG_NONE;
     if (fmt->seek_sample != NULL)