view decoders/ogg.c @ 474:c66080364dff

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.
author Ryan C. Gordon <icculus@icculus.org>
date Sat, 08 May 2004 08:19:50 +0000
parents 30f96c853462
children 3e705c9180e5
line wrap: on
line source

/*
 * SDL_sound -- An abstract sound format decoding API.
 * Copyright (C) 2001  Ryan C. Gordon.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * Ogg Vorbis decoder for SDL_sound.
 *
 * This driver handles .OGG audio files, and depends on libvorbisfile to
 *  do the actual decoding work. libvorbisfile is part of libvorbis, which
 *  is part of the Ogg Vorbis project.
 *
 *   Ogg Vorbis: http://www.xiph.org/ogg/vorbis/
 *   vorbisfile documentation: http://www.xiph.org/ogg/vorbis/doc/vorbisfile/
 *
 * Please see the file COPYING in the source's root directory.
 *
 *  This file written by Ryan C. Gordon. (icculus@clutteredmind.org)
 */

#if HAVE_CONFIG_H
#  include <config.h>
#endif

#ifdef SOUND_SUPPORTS_OGG

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#include "SDL_sound.h"

#define __SDL_SOUND_INTERNAL__
#include "SDL_sound_internal.h"

#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>


static int OGG_init(void);
static void OGG_quit(void);
static int OGG_open(Sound_Sample *sample, const char *ext);
static void OGG_close(Sound_Sample *sample);
static Uint32 OGG_read(Sound_Sample *sample);
static int OGG_rewind(Sound_Sample *sample);
static int OGG_seek(Sound_Sample *sample, Uint32 ms);

static const char *extensions_ogg[] = { "OGG", NULL };
const Sound_DecoderFunctions __Sound_DecoderFunctions_OGG =
{
    {
        extensions_ogg,
        "Ogg Vorbis audio through VorbisFile",
        "Ryan C. Gordon <icculus@clutteredmind.org>",
        "http://www.icculus.org/SDL_sound/"
    },

    OGG_init,       /*   init() method */
    OGG_quit,       /*   quit() method */
    OGG_open,       /*   open() method */
    OGG_close,      /*  close() method */
    OGG_read,       /*   read() method */
    OGG_rewind,     /* rewind() method */
    OGG_seek        /*   seek() method */
};


static int OGG_init(void)
{
    return(1);  /* always succeeds. */
} /* OGG_init */


static void OGG_quit(void)
{
    /* it's a no-op. */
} /* OGG_quit */



    /*
     * These are callbacks from vorbisfile that let them read data from
     *  a RWops...
     */

static size_t RWops_ogg_read(void *ptr, size_t size, size_t nmemb, void *datasource)
{
    return((size_t) SDL_RWread((SDL_RWops *) datasource, ptr, size, nmemb));
} /* RWops_ogg_read */

static int RWops_ogg_seek(void *datasource, ogg_int64_t offset, int whence)
{
    return(SDL_RWseek((SDL_RWops *) datasource, offset, whence));
} /* RWops_ogg_seek */

static int RWops_ogg_close(void *datasource)
{
    /* do nothing; SDL_sound will delete the RWops at a higher level. */
    return(0);  /* this is success in fclose(), so I guess that's okay. */
} /* RWops_ogg_close */

static long RWops_ogg_tell(void *datasource)
{
    return((long) SDL_RWtell((SDL_RWops *) datasource));
} /* RWops_ogg_tell */

static const ov_callbacks RWops_ogg_callbacks =
{
    RWops_ogg_read,
    RWops_ogg_seek,
    RWops_ogg_close,
    RWops_ogg_tell
};


    /* Return a human readable version of an VorbisFile error code... */
#if (defined DEBUG_CHATTER)
static const char *ogg_error(int errnum)
{
    switch(errnum)
    {
        case OV_EREAD:
            return("i/o error");
        case OV_ENOTVORBIS:
            return("not a vorbis file");
        case OV_EVERSION:
            return("Vorbis version mismatch");
        case OV_EBADHEADER:
            return("invalid Vorbis bitstream header");
        case OV_EFAULT:
            return("internal logic fault in Vorbis library");
    } /* switch */

    return("unknown error");
} /* ogg_error */
#endif

static __inline__ void output_ogg_comments(OggVorbis_File *vf)
{
#if (defined DEBUG_CHATTER)
    int i;
    vorbis_comment *vc = ov_comment(vf, -1);

    if (vc == NULL)
        return;

    SNDDBG(("OGG: vendor == [%s].\n", vc->vendor));
    for (i = 0; i < vc->comments; i++)
    {
        SNDDBG(("OGG: user comment [%s].\n", vc->user_comments[i]));
    } /* for */
#endif
} /* output_ogg_comments */


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;

    vf = (OggVorbis_File *) malloc(sizeof (OggVorbis_File));
    BAIL_IF_MACRO(vf == NULL, ERR_OUT_OF_MEMORY, 0);

    rc = ov_open_callbacks(internal->rw, vf, NULL, 0, RWops_ogg_callbacks);
    if (rc != 0)
    {
        SNDDBG(("OGG: can't grok data. reason: [%s].\n", ogg_error(rc)));
        free(vf);
        BAIL_MACRO("OGG: Not valid Ogg Vorbis data.", 0);
    } /* if */

    info = ov_info(vf, -1);
    if (info == NULL)
    {
        ov_clear(vf);
        free(vf);
        BAIL_MACRO("OGG: failed to retrieve bitstream info", 0);
    } /* if */

    SNDDBG(("OGG: Accepting data stream.\n"));

    output_ogg_comments(vf);
    SNDDBG(("OGG: bitstream version == (%d).\n", info->version));
    SNDDBG(("OGG: bitstream channels == (%d).\n", info->channels));
    SNDDBG(("OGG: bitstream sampling rate == (%ld).\n", info->rate));
    SNDDBG(("OGG: seekable == {%s}.\n", ov_seekable(vf) ? "TRUE" : "FALSE"));
    SNDDBG(("OGG: number of logical bitstreams == (%ld).\n", ov_streams(vf)));
    SNDDBG(("OGG: serial number == (%ld).\n", ov_serialnumber(vf, -1)));
    SNDDBG(("OGG: total seconds of sample == (%f).\n", ov_time_total(vf, -1)));

    internal->decoder_private = vf;
    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,
     *  and these bitstreams may be in different formats, we might be
     *  converting two or three times: once in vorbisfile, once again in
     *  SDL_sound, and perhaps a third time to get it to the sound device's
     *  format. That's wickedly inefficient.
     *
     * To combat this a little, if the user specified a desired format, we
     *  claim that to be the "actual" format of the collection of logical
     *  bitstreams. This means that VorbisFile will do a conversion as
     *  necessary, and SDL_sound will not. If the user didn't specify a
     *  desired format, then we pretend the "actual" format is something that
     *  OGG files are apparently commonly encoded in.
     */
    sample->actual.format = (sample->desired.format == 0) ?
                             AUDIO_S16LSB : sample->desired.format;
    return(1);
} /* OGG_open */


static void OGG_close(Sound_Sample *sample)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    OggVorbis_File *vf = (OggVorbis_File *) internal->decoder_private;
    ov_clear(vf);
    free(vf);
} /* OGG_close */

/* Note: According to the Vorbis documentation:
 * "ov_read() will  decode at most one vorbis packet per invocation, 
 * so the value returned will generally be less than length."
 * Due to this, for buffer sizes like 16384, SDL_Sound was always getting
 * an underfilled buffer and always setting the EAGAIN flag.
 * Since the SDL_Sound API implies that the entire buffer
 * should be filled unless EOF, additional code has been added
 * to this function to call ov_read() until the buffer is filled.
 * However, there may still be some corner cases where the buffer
 * cannot be entirely filled. So be aware.
 */
static Uint32 OGG_read(Sound_Sample *sample)
{
    int rc;
    int bitstream;
	Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    OggVorbis_File *vf = (OggVorbis_File *) internal->decoder_private;
    
    rc = ov_read(vf, internal->buffer, internal->buffer_size,
            ((sample->actual.format & 0x1000) ? 1 : 0), /* bigendian? */
            ((sample->actual.format & 0xFF) / 8), /* bytes per sample point */
            ((sample->actual.format & 0x8000) ? 1 : 0), /* signed data? */
            &bitstream);

        /* Make sure the read went smoothly... */
    if (rc == 0)
        sample->flags |= SOUND_SAMPLEFLAG_EOF;
        
    else if (rc < 0)
        sample->flags |= SOUND_SAMPLEFLAG_ERROR;

    /* If the buffer isn't filled, keep trying to fill it
     * until no more data can be grabbed */
    else if ((Uint32) rc < internal->buffer_size)
	{
        /* Creating a pointer to the buffer that denotes where to start
         * writing new data. */
        Uint8* buffer_start_point = NULL;
        int total_bytes_read = rc;
        int bytes_remaining = internal->buffer_size - rc;

        /* Keep grabbing data until something prevents
         * us from getting more. (Could be EOF,
         * packets are too large to fit in remaining
         * space, or an error.)
         */
        while( (rc > 0) && (bytes_remaining > 0) )
        {
        /* Set buffer pointer to end of last write */
        /* All the messiness is to get rid of the warning for
         * dereferencing a void*
         */
            buffer_start_point = &(((Uint8*)internal->buffer)[total_bytes_read]);
            rc = ov_read(vf, buffer_start_point, bytes_remaining,
                ((sample->actual.format & 0x1000) ? 1 : 0), /* bigendian? */
                ((sample->actual.format & 0xFF) / 8), /* bytes per sample point */
                ((sample->actual.format & 0x8000) ? 1 : 0), /* signed data? */
                &bitstream);
            /* Make sure rc > 0 because we don't accidently want
             * to change the counters if there was an error
             */
            if(rc > 0)
            {
                total_bytes_read += rc;
                bytes_remaining = bytes_remaining - rc;
            }
	    }
        /* I think the minimum read size is 2, though I'm
         * not sure about this. (I've hit cases where I
         * couldn't read less than 4.) What I don't want to do is
         * accidently claim we hit EOF when the reason rc == 0
         * is because the requested amount of data was smaller
         * than the minimum packet size.
         * For now, I will be conservative
         * and not set the EOF flag, and let the next call to 
         * this function figure it out.
         * I think the ERROR flag is safe to set because 
         * it looks like OGG simply returns 0 if the 
         * read size is too small. 
         * And in most cases for sensible buffer sizes, 
         * this fix will fill the buffer,
         * so we can set the EAGAIN flag without worrying
         * that it will always be set.
         */
        if(rc < 0)
	    {
            sample->flags |= SOUND_SAMPLEFLAG_ERROR;
        }
        else if(rc == 0)
        {
            /* Do nothing for now until there is a better solution */
            /* sample->flags |= SOUND_SAMPLEFLAG_EOF; */
        }

        /* Test for a buffer underrun. It should occur less frequently
         * now, but it still may happen and not necessarily mean
         * anything useful. */
        if ((Uint32) total_bytes_read < internal->buffer_size)
        {
            sample->flags |= SOUND_SAMPLEFLAG_EAGAIN;
        }
        /* change rc to the total bytes read so function
         * can return the correct value.
         */
        rc = total_bytes_read;
    }

    return((Uint32) rc);
} /* OGG_read */


static int OGG_rewind(Sound_Sample *sample)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    OggVorbis_File *vf = (OggVorbis_File *) internal->decoder_private;

    BAIL_IF_MACRO(ov_raw_seek(vf, 0) < 0, ERR_IO_ERROR, 0);
    return(1);
} /* OGG_rewind */


static int OGG_seek(Sound_Sample *sample, Uint32 ms)
{
    Sound_SampleInternal *internal = (Sound_SampleInternal *) sample->opaque;
    OggVorbis_File *vf = (OggVorbis_File *) internal->decoder_private;
    double timepos = (((double) ms) / 1000.0);
    BAIL_IF_MACRO(ov_time_seek(vf, timepos) < 0, ERR_IO_ERROR, 0);
    return(1);
} /* OGG_seek */

#endif /* SOUND_SUPPORTS_OGG */


/* end of ogg.c ... */