/*
 * This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the Do What The Fuck You Want
 * To Public License, Version 2, as published by Sam Hocevar. See
 * http://sam.zoy.org/wtfpl/COPYING for more details.
 */

/* ChangeLog:
 * 1 - Initial program
 * 2 - Changed getAVAudioData to not always grab another packet before decoding
 *     to prevent buffering more compressed data than needed
 */

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

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

/* Opaque handles to files and streams. The main app doesn't need to concern
 * itself with the internals */
typedef struct MyFile *FilePtr;
typedef struct MyStream *StreamPtr;

/**** Helper function ****/

/* If in C++, you must manually extern "C" {} around these */
#include <ffmpeg/avcodec.h>
#include <ffmpeg/avformat.h>

struct MyStream {
    AVCodecContext *CodecCtx;
    int StreamIdx;

    char *Data;
    size_t DataSize;
    size_t DataSizeMax;
    char *DecodedData;
    size_t DecodedDataSize;

    FilePtr parent;
};

struct MyFile {
    AVFormatContext *FmtCtx;
    StreamPtr *Streams;
    size_t StreamsSize;
};

/* This opens a file with ffmpeg and sets up the streams' information */
FilePtr openAVFile(const char *fname)
{
    static int done = 0;
    FilePtr file;

    /* We need to make sure ffmpeg is initialized. Optionally silence warning
     * output from the lib */
    if(!done) {av_register_all();
    av_log_set_level(AV_LOG_ERROR);}
    done = 1;

    file = calloc(1, sizeof(*file));
    if(file && av_open_input_file(&file->FmtCtx, fname, NULL, 0, NULL) == 0)
    {
        /* After opening, we must search for the stream information because not
         * all formats will have it in stream headers (eg. system MPEG streams)
         */
        if(av_find_stream_info(file->FmtCtx) >= 0)
            return file;
        av_close_input_file(file->FmtCtx);
    }
    free(file);
    return NULL;
}

/* This closes/frees an opened file and any of its streams. Pretty self-
 * explanitory... */
void closeAVFile(FilePtr file)
{
    size_t i;

    if(!file) return;

    for(i = 0;i < file->StreamsSize;i++)
    {
        avcodec_close(file->Streams[i]->CodecCtx);
        free(file->Streams[i]->Data);
        free(file->Streams[i]->DecodedData);
        free(file->Streams[i]);
    }
    free(file->Streams);

    av_close_input_file(file->FmtCtx);
    free(file);
}

/* This retrieves a handle for the given audio stream number (generally 0, but
 * some files can have multiple audio streams in one file) */
StreamPtr getAVAudioStream(FilePtr file, int streamnum)
{
    unsigned int i;
    if(!file) return NULL;
    for(i = 0;i < file->FmtCtx->nb_streams;i++)
    {
        if(file->FmtCtx->streams[i]->codec->codec_type != CODEC_TYPE_AUDIO)
            continue;

        if(streamnum == 0)
        {
            StreamPtr stream;
            AVCodec *codec;
            void *temp;
            size_t j;

            /* Found the requested stream. Check if a handle to this stream
             * already exists and return it if it does */
            for(j = 0;j < file->StreamsSize;j++)
            {
                if(file->Streams[j]->StreamIdx == (int)i)
                    return file->Streams[j];
            }

            /* Doesn't yet exist. Now allocate a new stream object and fill in
             * its info */
            stream = calloc(1, sizeof(*stream));
            if(!stream) return NULL;

            stream->parent = file;
            stream->CodecCtx = file->FmtCtx->streams[i]->codec;
            stream->StreamIdx = i;

            /* Try to find the codec for the given codec ID, and open it */
            codec = avcodec_find_decoder(stream->CodecCtx->codec_id);
            if(!codec || avcodec_open(stream->CodecCtx, codec) < 0)
            {
                free(stream);
                return NULL;
            }

            /* Allocate space for the decoded data to be stored in before it
             * gets passed to the app */
            stream->DecodedData = malloc(AVCODEC_MAX_AUDIO_FRAME_SIZE);
            if(!stream->DecodedData)
            {
                avcodec_close(stream->CodecCtx);
                free(stream);
                return NULL;
            }

            /* Append the new stream object to the stream list. The original
             * pointer will remain valid if realloc fails, so we need to use
             * another pointer to watch for errors and not leak memory */
            temp = realloc(file->Streams, (file->StreamsSize+1) *
                                          sizeof(*file->Streams));
            if(!temp)
            {
                avcodec_close(stream->CodecCtx);
                free(stream->DecodedData);
                free(stream);
                return NULL;
            }
            file->Streams = temp;
            file->Streams[file->StreamsSize++] = stream;
            return stream;
        }
        streamnum--;
    }
    return NULL;
}

/* Returns information about the given audio stream. Currently, ffmpeg always
 * decodes audio (even 8-bit PCM) to 16-bit PCM. Returns 0 on success. */
int getAVAudioInfo(StreamPtr stream, int *rate, int *channels, int *bits)
{
    if(!stream || stream->CodecCtx->codec_type != CODEC_TYPE_AUDIO)
        return 1;

    if(rate) *rate = stream->CodecCtx->sample_rate;
    if(channels) *channels = stream->CodecCtx->channels;
    if(bits) *bits = 16;

    return 0;
}

/* Used by getAV*Data to search for more compressed data, and buffer it in the
 * correct stream. It won't buffer data for streams that the app doesn't have a
 * handle for. */
static void getNextPacket(FilePtr file, int streamidx)
{
    AVPacket packet;
    while(av_read_frame(file->FmtCtx, &packet) >= 0)
    {
        StreamPtr *iter = file->Streams;
        size_t i;
        /* Check each stream the user has a handle for, looking for the one
         * this packet belongs to */
        for(i = 0;i < file->StreamsSize;i++,iter++)
        {
            if((*iter)->StreamIdx == packet.stream_index)
            {
                size_t idx = (*iter)->DataSize;

                /* Found the stream. Grow the input data buffer as needed to
                 * hold the new packet's data. Additionally, some ffmpeg codecs
                 * need some padding so they don't overread the allocated
                 * buffer */
                if(idx+packet.size > (*iter)->DataSizeMax)
                {
                    void *temp = realloc((*iter)->Data, idx+packet.size +
                                                 FF_INPUT_BUFFER_PADDING_SIZE);
                    if(!temp) break;
                    (*iter)->Data = temp;
                    (*iter)->DataSizeMax = idx+packet.size;
                }

                /* Copy the packet and free it */
                memcpy(&(*iter)->Data[idx], packet.data, packet.size);
                (*iter)->DataSize += packet.size;

                /* Return if this stream is what we needed a packet for */
                if(streamidx == (*iter)->StreamIdx)
                {
                    av_free_packet(&packet);
                    return;
                }
                break;
            }
        }
        /* Free the packet and look for another */
        av_free_packet(&packet);
    }
}

/* The "meat" function. Decodes audio and writes, at most, length bytes into
 * the provided data buffer. Will only return less for end-of-stream or error
 * conditions. Returns the number of bytes written. */
int getAVAudioData(StreamPtr stream, void *data, int length)
{
    int dec = 0;

    if(!stream || stream->CodecCtx->codec_type != CODEC_TYPE_AUDIO)
        return 0;

    while(dec < length)
    {
        /* If there's any pending decoded data, deal with it first */
        if(stream->DecodedDataSize > 0)
        {
            /* Get the amount of bytes remaining to be written, and clamp to
             * the amount of decoded data we have */
            size_t rem = length-dec;
            if(rem > stream->DecodedDataSize)
                rem = stream->DecodedDataSize;

            /* Copy the data to the app's buffer and increment */
            memcpy(data, stream->DecodedData, rem);
            data = (char*)data + rem;
            dec += rem;

            /* If there's any decoded data left, move it to the front of the
             * buffer for next time */
            if(rem < stream->DecodedDataSize)
                memmove(stream->DecodedData, &stream->DecodedData[rem],
                        stream->DecodedDataSize - rem);
            stream->DecodedDataSize -= rem;
        }

        /* Check if we need to get more decoded data */
        if(stream->DecodedDataSize == 0)
        {
            size_t insize;
            int size;
            int len;

            insize = stream->DataSize;
            if(insize == 0)
            {
                getNextPacket(stream->parent, stream->StreamIdx);
                /* If there's no more input data, break and return what we have */
                if(insize == stream->DataSize)
                    break;
                insize = stream->DataSize;
                memset(&stream->Data[insize], 0, FF_INPUT_BUFFER_PADDING_SIZE);
            }

            /* Clear the input padding bits */
            /* Decode some data, and check for errors */
            size = AVCODEC_MAX_AUDIO_FRAME_SIZE;
            while((len=avcodec_decode_audio2(stream->CodecCtx,
                                         (int16_t*)stream->DecodedData, &size,
                                         (uint8_t*)stream->Data, insize)) == 0)
            {
                if(size > 0)
                    break;
                getNextPacket(stream->parent, stream->StreamIdx);
                if(insize == stream->DataSize)
                    break;
                insize = stream->DataSize;
                memset(&stream->Data[insize], 0, FF_INPUT_BUFFER_PADDING_SIZE);
            }

            if(len < 0)
                break;

            if(len > 0)
            {
                /* If any input data is left, move it to the start of the
                 * buffer, and decrease the buffer size */
                size_t rem = insize-len;
                if(rem)
                    memmove(stream->Data, &stream->Data[len], rem);
                stream->DataSize = rem;
            }
            /* Set the output buffer size */
            stream->DecodedDataSize = size;
        }
    }

    /* Return the number of bytes we were able to get */
    return dec;
}

/**** The main app ****/

/* Create a simple signal handler for SIGINT so ctrl-c cleanly exits. */
static volatile int quitnow = 0;
static void handle_sigint(int signum)
{
    (void)signum;
    quitnow = 1;
}

/* Define the number of buffers and bytes-per-buffer to use. 3 buffers is a
 * good amount (one playing, one ready to play, another being filled). The
 * buffer size must be a multiple of the frame size (of which we can have 1, 2,
 * 4, 6, 8, and 12-byte frame sizes). 16KB to 32KB is a good size per buffer.
 * Adding another buffer or two to increase the overall length wouldn't be a
 * bad idea of you wanted more skip protection */
#define NUM_BUFFERS 3
#define BUFFER_SIZE 19200

int main(int argc, char **argv)
{
    /* Here are the buffers and source to play out through OpenAL with */
    ALuint buffers[NUM_BUFFERS];
    ALuint source;

    ALint state; /* This will hold the state of the source */
    ALbyte *data; /* A temp data buffer for getAVAudioData to write to and pass
                   * to OpenAL with */
    int count; /* The number of bytes read from getAVAudioData */
    int i; /* An iterator for looping over the filenames */

    /* Print out usage if no file was specified */
    if(argc < 2)
    {
        fprintf(stderr, "Usage: %s <filenames...>\n", argv[0]);
        return 1;
    }

    /* Set up our signal handler to run on SIGINT (ctrl-c) */
    if(signal(SIGINT, handle_sigint) == SIG_ERR)
    {
        fprintf(stderr, "Unable to set handler for SIGINT!\n");
        return 1;
    }

    data = malloc(BUFFER_SIZE);
    if(!data)
    {
        fprintf(stderr, "Out of memory allocating temp buffer!\n");
        return 1;
    }

    /* Initialize ALUT with default settings */
    if(alutInit(NULL, NULL) == AL_FALSE)
    {
        free(data);
        fprintf(stderr, "Could not initialize ALUT (%s)!\n",
                alutGetErrorString(alutGetError()));
        return 1;
    }

    /* Generate the buffers and source */
    alGenBuffers(NUM_BUFFERS, buffers);
    if(alGetError() != AL_NO_ERROR)
    {
        alutExit();
        free(data);
        fprintf(stderr, "Could not create buffers...\n");
        return 1;
    }
    alGenSources(1, &source);
    if(alGetError() != AL_NO_ERROR)
    {
        alDeleteBuffers(NUM_BUFFERS, buffers);
        alutExit();
        free(data);
        fprintf(stderr, "Could not create source...\n");
        return 1;
    }

    /* Set parameters so mono sources won't distance attenuate */
    alSourcei(source, AL_SOURCE_RELATIVE, AL_TRUE);
    alSourcei(source, AL_ROLLOFF_FACTOR, 0);
    if(alGetError() != AL_NO_ERROR)
    {
        alDeleteSources(1, &source);
        alDeleteBuffers(NUM_BUFFERS, buffers);
        alutExit();
        free(data);
        fprintf(stderr, "Could not set source parameters...\n");
        return 1;
    }

    /* Play each file listed on the command line */
    for(i = 1;i < argc && !quitnow;i++)
    {
        static ALenum old_format;
        static int old_rate;
        /* Handles for the audio stream */
        FilePtr file;
        StreamPtr stream;
        /* The format of the output stream */
        ALenum format = 0;
        int channels;
        int bits;
        int rate;
        /* The base time to use when determining the playback time from the
         * source. */
        int basetime = 0;

        /* Open the file and get the first stream from it */
        file = openAVFile(argv[i]);
        stream = getAVAudioStream(file, 0);
        if(!stream)
        {
            closeAVFile(file);
            fprintf(stderr, "Could not open audio in %s\n", argv[i]);
            continue;
        }
        /* Get the stream format, and figure out the OpenAL format. We use the
         * AL_EXT_MCFORMATS extension to provide output of 4 and 5.1 audio
         * streams */
        if(getAVAudioInfo(stream, &rate, &channels, &bits) != 0)
        {
            closeAVFile(file);
            fprintf(stderr, "Error getting audio info for %s\n", argv[i]);
            continue;
        }

        if(bits == 8)
        {
            if(channels == 1) format = AL_FORMAT_MONO8;
            if(channels == 2) format = AL_FORMAT_STEREO8;
            if(alIsExtensionPresent("AL_EXT_MCFORMATS"))
            {
                if(channels == 4) format = alGetEnumValue("AL_FORMAT_QUAD8");
                if(channels == 6) format = alGetEnumValue("AL_FORMAT_51CHN8");
            }
        }
        if(bits == 16)
        {
            if(channels == 1) format = AL_FORMAT_MONO16;
            if(channels == 2) format = AL_FORMAT_STEREO16;
            if(alIsExtensionPresent("AL_EXT_MCFORMATS"))
            {
                if(channels == 4) format = alGetEnumValue("AL_FORMAT_QUAD16");
                if(channels == 6) format = alGetEnumValue("AL_FORMAT_51CHN16");
            }
        }

        if(format == 0)
        {
            closeAVFile(file);
            fprintf(stderr, "Unhandled format (%d channels, %d bits) for %s",
                    channels, bits, argv[i]);
            continue;
        }

        /* If the format of the last file matches the current one, we can skip
         * the initial load and let the processing loop take over (gap-less
         * playback!) */
        count = 1;
        if(format != old_format || rate != old_rate)
        {
            int j;

            old_format = format;
            old_rate = rate;
            /* Wait for the last song to finish playing */
            do {
                alutSleep(0.01);
                alGetSourcei(source, AL_SOURCE_STATE, &state);
            } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);
            /* Rewind the source position and clear the buffer queue */
            alSourceRewind(source);
            alSourcei(source, AL_BUFFER, 0);

            /* Fill and queue the buffers */
            for(j = 0;j < NUM_BUFFERS;j++)
            {
                /* Make sure we get some data to give to the buffer */
                count = getAVAudioData(stream, data, BUFFER_SIZE);
                if(count <= 0) break;

                /* Buffer the data with OpenAL and queue the buffer onto the
                 * source */
                alBufferData(buffers[j], format, data, count, rate);
                alSourceQueueBuffers(source, 1, &buffers[j]);
            }
            if(alGetError() != AL_NO_ERROR)
            {
                closeAVFile(file);
                fprintf(stderr, "Error buffering initial data...\n");
                continue;
            }

            /* Now start playback! */
            alSourcePlay(source);
            if(alGetError() != AL_NO_ERROR)
            {
                closeAVFile(file);
                fprintf(stderr, "Error starting playback...\n");
                continue;
            }
        }
        else
        {
            /* When skipping the initial load of a file (because the previous
             * one is using the same exact format), set the base time to the
             * negative of the queued buffers. This is so the timing will be
             * from the beginning of this file, which won't start playing until
             * the next buffer to get queued does */
            basetime = -NUM_BUFFERS;
        }

        fprintf(stderr, "\rPlaying %s (%d-bit, %d channels, %dhz)\n", argv[i],
                bits, channels, rate);
        while(count > 0 && !quitnow)
        {
            /* Check if any buffers on the source are finished playing */
            ALint processed = 0;
            alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed);
            if(processed == 0)
            {
                /* All buffers are full. Check if the source is still playing.
                 * If not, restart it, otherwise, print the time and rest */
                alGetSourcei(source, AL_SOURCE_STATE, &state);
                if(alGetError() != AL_NO_ERROR)
                {
                    fprintf(stderr, "Error checking source state...\n");
                    break;
                }
                if(state != AL_PLAYING)
                {
                    alSourcePlay(source);
                    if(alGetError() != AL_NO_ERROR)
                    {
                        closeAVFile(file);
                        fprintf(stderr, "Error restarting playback...\n");
                        break;
                    }
                }
                else
                {
                    ALint offset;
                    alGetSourcei(source, AL_SAMPLE_OFFSET, &offset);
                    /* Add the base time to the offset. Each count of basetime
                     * represents one buffer, which is BUFFER_SIZE in bytes */
                    offset += basetime * (BUFFER_SIZE/channels*8/bits);
                    fprintf(stderr, "\rTime: %d:%05.02f", offset/rate/60,
                            (offset%(rate*60))/(float)rate);
                    alutSleep(0.01);
                }
                continue;
            }
            /* Read the next chunk of data and refill the oldest buffer */
            count = getAVAudioData(stream, data, BUFFER_SIZE);
            if(count > 0)
            {
                ALuint buf = 0;
                alSourceUnqueueBuffers(source, 1, &buf);
                if(buf != 0)
                {
                    alBufferData(buf, format, data, count, rate);
                    alSourceQueueBuffers(source, 1, &buf);
                    /* For each successfully unqueued buffer, increment the
                     * base time. The retrieved sample offset for timing is
                     * relative to the start of the buffer queue, so for every
                     * buffer that gets unqueued we need to increment the base
                     * time to keep the reported time accurate and not fall
                     * backwards */
                    basetime++;
                }
                if(alGetError() != AL_NO_ERROR)
                {
                    fprintf(stderr, "Error buffering data...\n");
                    break;
                }
            }
        }

        /* All done with this file. Close it and go to the next */
        closeAVFile(file);
    }
    fprintf(stderr, "\nDone.\n");

    /* All data has been streamed in. Wait until the source stops playing it */
    do {
        alutSleep(0.01);
        alGetSourcei(source, AL_SOURCE_STATE, &state);
    } while(alGetError() == AL_NO_ERROR && state == AL_PLAYING);

    /* All files done. Delete the source and buffers, and close OpenAL */
    alDeleteSources(1, &source);
    alDeleteBuffers(NUM_BUFFERS, buffers);

    alutExit();
    free(data);

    return 0;
}
