/**
 * OpenAL streamer from Wave file input
 * Copyright (C) 2009 by author.
 * This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 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
 *  Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library 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.
 * Or go to http://www.gnu.org/copyleft/lgpl.html
 */

#include <AL/al.h>
#include <AL/alc.h>
#include <AL/alext.h>

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>


PFNALGENDATABUFFERSEXTPROC palGenDatabuffersEXT;
PFNALDELETEDATABUFFERSEXTPROC palDeleteDatabuffersEXT;
PFNALISDATABUFFEREXTPROC palIsDatabufferEXT;
PFNALDATABUFFERDATAEXTPROC palDatabufferDataEXT;
PFNALDATABUFFERSUBDATAEXTPROC palDatabufferSubDataEXT;
PFNALGETDATABUFFERSUBDATAEXTPROC palGetDatabufferSubDataEXT;
PFNALDATABUFFERFEXTPROC palDatabufferfEXT;
PFNALDATABUFFERFVEXTPROC palDatabufferfvEXT;
PFNALDATABUFFERIEXTPROC palDatabufferiEXT;
PFNALDATABUFFERIVEXTPROC palDatabufferivEXT;
PFNALGETDATABUFFERFEXTPROC palGetDatabufferfEXT;
PFNALGETDATABUFFERFVEXTPROC palGetDatabufferfvEXT;
PFNALGETDATABUFFERIEXTPROC palGetDatabufferiEXT;
PFNALGETDATABUFFERIVEXTPROC palGetDatabufferivEXT;
PFNALSELECTDATABUFFEREXTPROC palSelectDatabufferEXT;
PFNALMAPDATABUFFEREXTPROC palMapDatabufferEXT;
PFNALUNMAPDATABUFFEREXTPROC palUnmapDatabufferEXT;


int main(int argc, char **argv)
{
    ALCdevice *dev;
    ALCcontext *ctx;

    if(argc < 2)
    {
        fprintf(stderr, "Usage: %s <wavefile>\n", argv[0]);
        return 0;
    }

    /* First the standard open-device, create-context, set-context.. */
    dev = alcOpenDevice(NULL);
    if(!dev)
    {
        fprintf(stderr, "Oops\n");
        return 1;
    }
    ctx = alcCreateContext(dev, NULL);
    alcMakeContextCurrent(ctx);
    if(!ctx)
    {
        fprintf(stderr, "Oops2\n");
        return 1;
    }


    if(alIsExtensionPresent("AL_EXTX_sample_buffer_object") == AL_FALSE)
    {
        fprintf(stderr, "Could not find extension AL_EXTX_sample_buffer_object\n");
        return 1;
    }

#define LOAD_FUNC(x) p##x = alGetProcAddress(#x); assert(p##x != NULL)
    LOAD_FUNC(alGenDatabuffersEXT);
    LOAD_FUNC(alDeleteDatabuffersEXT);
    LOAD_FUNC(alIsDatabufferEXT);
    LOAD_FUNC(alDatabufferDataEXT);
    LOAD_FUNC(alDatabufferSubDataEXT);
    LOAD_FUNC(alGetDatabufferSubDataEXT);
    LOAD_FUNC(alDatabufferfEXT);
    LOAD_FUNC(alDatabufferfvEXT);
    LOAD_FUNC(alDatabufferiEXT);
    LOAD_FUNC(alDatabufferivEXT);
    LOAD_FUNC(alGetDatabufferfEXT);
    LOAD_FUNC(alGetDatabufferfvEXT);
    LOAD_FUNC(alGetDatabufferiEXT);
    LOAD_FUNC(alGetDatabufferivEXT);
    LOAD_FUNC(alSelectDatabufferEXT);
    LOAD_FUNC(alMapDatabufferEXT);
    LOAD_FUNC(alUnmapDatabufferEXT);
#undef LOAD_FUNC


    {
        /* The number of buffers and bytes-per-buffer for our stream are set
         * here. The number of buffers should be two or more, and the buffer
         * size should be a multiple of the frame size (by default, OpenAL's
         * largest frame size is 4, however extensions that can add more formats
         * may be larger). Slower systems may need more buffers/larger buffer
         * sizes. */
#define NUM_BUFFERS 3
#define BUFFER_SIZE 4096
        /* These are what we'll use for OpenAL playback */
        ALuint source, buffers[NUM_BUFFERS];
        ALuint databuffer;
        ALuint frequency;
        ALenum format;
        unsigned char *buf, tmp[12];
        FILE *f;

        /* Generate the buffers and sources */
        alGenBuffers(NUM_BUFFERS, buffers);
        alGenSources(1, &source);
        palGenDatabuffersEXT(1, &databuffer);
        if(alGetError() != AL_NO_ERROR)
        {
            fprintf(stderr, "Error generating :(\n");
            return 1;
        }

        palDatabufferDataEXT(databuffer, NULL, BUFFER_SIZE, AL_STREAM_WRITE_EXT);
        if(alGetError() != AL_NO_ERROR)
        {
            fprintf(stderr, "Error allocating :(\n");
            return 1;
        }

        if(strcmp(argv[1], "-") != 0)
        {
            f = fopen(argv[1], "rb");
            if(!f)
            {
                fprintf(stderr, "Error opening :(\n");
                return 1;
            }
        }
        else
            f = stdin;

        /* Allocate the buffer, and read the RIFF-WAVE header. We don't actually
         * need to read it, so just ignore what it writes to the buffer. Note
         * that because this might be written in real-time, the chunk size
         * information may not be filled out. */
        fread(tmp, 1, 12, f);

        /* This is the first .wav file chunk. Check the chunk header to make
         * sure it is the format information. The first four bytes is the
         * indentifier (which we check), and the last four is the chunk size
         * (which we ignore) */
        fread(tmp, 1, 8, f);
        if(tmp[0] != 'f' || tmp[1] != 'm' || tmp[2] != 't' || tmp[3] != ' ')
        {
            /* If this isn't the format info, we can't deal with the file. */
            fprintf(stderr, "Not 'fmt ' :(\n");
            return 1;
        }

        {
            int channels, bits;

            /* Read the wave format type, as a 16-bit little-endian integer.
             * There's no reason this shouldn't be 1. */
            fread(tmp, 1, 2, f);
            if(tmp[1] != 0 || tmp[0] != 1)
            {
                fprintf(stderr, "Not PCM :(\n");
                return 1;
            }

            /* Get the channel count (16-bit little-endian) */
            fread(tmp, 1, 2, f);
            channels  = tmp[1]<<8;
            channels |= tmp[0];

            /* Get the sample frequency (32-bit little-endian) */
            fread(tmp, 1, 4, f);
            frequency  = tmp[3]<<24;
            frequency |= tmp[2]<<16;
            frequency |= tmp[1]<<8;
            frequency |= tmp[0];

            /* The next 6 bytes hold the block size and bytes-per-second. We
             * don't need that info, so just read and ignore it. */
            fread(tmp, 1, 6, f);

            /* Get the bit depth (16-bit little-endian) */
            fread(tmp, 1, 2, f);
            bits  = tmp[1]<<8;
            bits |= tmp[0];

            /* Now convert the given channel count and bit depth into an OpenAL
             * format. We could use extensions to support more formats (eg.
             * surround sound, floating-point samples), but that is beyond the
             * scope of this tutorial */
            format = 0;
            if(bits == 8)
            {
                if(channels == 1)
                    format = AL_FORMAT_MONO8;
                else if(channels == 2)
                    format = AL_FORMAT_STEREO8;
            }
            else if(bits == 16)
            {
                if(channels == 1)
                    format = AL_FORMAT_MONO16;
                else if(channels == 2)
                    format = AL_FORMAT_STEREO16;
            }
            if(!format)
            {
                fprintf(stderr, "Incompatible format (%d, %d) :(\n", channels, bits);
                return 1;
            }
        }

        /* Next up is the data chunk, which will hold the decoded sample data */
        fread(tmp, 1, 8, f);
        if(tmp[0] != 'd' || tmp[1] != 'a' || tmp[2] != 't' || tmp[3] != 'a')
        {
            fprintf(stderr, "Not 'data' :(\n");
            return 1;
        }

        /* Now we have everything we need. To read the data, all we have to do
         * is read from the file handle! Note that the .wav format spec has
         * multibyte sample formats stored as little-endian. If you were on a
         * big-endian machine, you'd have to iterate over the returned data and
         * flip the bytes for those formats before giving it to OpenAL. */
        {
            int ret, i;

            palSelectDatabufferEXT(AL_SAMPLE_SOURCE_EXT, databuffer);
            /* Fill the data buffer with the amount of bytes-per-buffer, and
             * buffer it into OpenAL. This may read (and return) less than the
             * requested amount when it hits the end of the "stream" */
            for(i = 0;i < NUM_BUFFERS;i++)
            {
                buf = palMapDatabufferEXT(databuffer, 0, BUFFER_SIZE, AL_WRITE_ONLY_EXT);
                if(buf)
                {
                    ret = fread(buf, 1, BUFFER_SIZE, f);
                    palUnmapDatabufferEXT(databuffer);
                    buf = NULL;

                    alBufferData(buffers[i], format, (void*)0, ret, frequency);
                }
            }
            palSelectDatabufferEXT(AL_SAMPLE_SOURCE_EXT, 0);

            if((ret=alGetError()) != AL_NO_ERROR)
            {
                fprintf(stderr, "Error loading, %#04x :(\n", ret);
                return 1;
            }

            /* Queue the buffers onto the source, and start playback! */
            alSourceQueueBuffers(source, NUM_BUFFERS, buffers);
            alSourcePlay(source);
            if(alGetError() != AL_NO_ERROR)
            {
                fprintf(stderr, "Error starting :(\n");
                return 1;
            }

            /* While not at the end of the stream... */
            while(!feof(f))
            {
                ALuint buffer;
                ALint val;

                /* Check if OpenAL is done with any of the queued buffers */
                alGetSourcei(source, AL_BUFFERS_PROCESSED, &val);
                if(val <= 0)
                {
                    usleep(1000);
                    continue;
                }

                /* For each processed buffer... */
                palSelectDatabufferEXT(AL_SAMPLE_SOURCE_EXT, databuffer);
                while(val--)
                {
                    /* Pop the oldest queued buffer from the source, fill it
                     * with the new data, then requeue it */
                    alSourceUnqueueBuffers(source, 1, &buffer);

                    buf = palMapDatabufferEXT(databuffer, 0, BUFFER_SIZE, AL_WRITE_ONLY_EXT);
                    if(buf)
                    {
                        /* Read the next chunk of decoded data from the stream */
                        ret = fread(buf, 1, BUFFER_SIZE, f);
                        palUnmapDatabufferEXT(databuffer);
                        buf = NULL;

                        alBufferData(buffer, format, (void*)0, ret, frequency);
                        alSourceQueueBuffers(source, 1, &buffer);
                    }
                    if(alGetError() != AL_NO_ERROR)
                    {
                        fprintf(stderr, "Error buffering :(\n");
                        return 1;
                    }
                }
                palSelectDatabufferEXT(AL_SAMPLE_SOURCE_EXT, 0);
                /* Make sure the source is still playing, and restart it if
                 * needed. */
                alGetSourcei(source, AL_SOURCE_STATE, &val);
                if(val != AL_PLAYING)
                    alSourcePlay(source);
            }
        }

        /* File's done decoding. We can close the pipe and free the data buffer
         * now. */
        if(f != stdin)
            fclose(f);
        {
            ALint val;
            /* Although we got all the data from the file, OpenAL may still be
             * playing the remaining buffers. Wait until it stops. */
            do {
                alGetSourcei(source, AL_SOURCE_STATE, &val);
            } while(val == AL_PLAYING);
        }

        /* Done playing. Delete the source and buffers */
        palDeleteDatabuffersEXT(1, &databuffer);
        alDeleteSources(1, &source);
        alDeleteBuffers(NUM_BUFFERS, buffers);
    }

    /* All done. Close OpenAL and exit. */
    alcMakeContextCurrent(NULL);
    alcDestroyContext(ctx);
    alcCloseDevice(dev);

    return 0;
}

