/** \file   file_drv.c
 * \brief   File based audio input driver.
 *
 * \author  Marco van den Heuvel <blackystardust68@yahoo.com>
 */

/*
 * This file is part of VICE, the Versatile Commodore Emulator.
 * See README for copyright notice.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307  USA.
 *
 */

/* WAV files tested and working:
 - 22050 Hz, 8bit PCM, stereo
 - 11025 Hz, 16bit PCM, mono
 - 22050 Hz, 16bit PCM, mono
 - 23456 Hz, 16bit PCM, mono
 - 44100 Hz, 16bit PCM, mono
 - 11025 Hz, 16bit PCM, stereo
 - 22050 Hz, 16bit PCM, stereo
 - 23456 Hz, 16bit PCM, stereo
 - 44100 Hz, 16bit PCM, stereo
 - 22050 Hz, 24bit PCM, stereo
 - 22050 Hz, 32bit PCM, stereo
 - 22050 Hz, 32bit float, stereo
 - 22050 Hz, 64bit float, stereo
 - 44010 Hz, a-law, stereo
 - 44010 Hz, u-law, stereo
 */

/* VOC files tested and working:
 - 22050 Hz, 8bit PCM, stereo
 - 22050 Hz, 16bit PCM, mono
 - 11025 Hz, 16bit PCM, stereo
 - 22050 Hz, 16bit PCM, stereo
 - 23456 Hz, 16bit PCM, stereo
 - 44100 Hz, 16bit PCM, stereo
 - 22050 Hz, a-law, stereo
 - 22050 Hz, u-law, stereo
 */

/* IFF files tested and working:
 - 22050 Hz, 8bit PCM, mono
 - 11025 Hz, 8bit PCM, stereo
 - 22050 Hz, 8bit PCM, stereo
 - 23456 Hz, 8bit PCM, stereo
 - 44100 Hz, 8bit PCM, stereo
 */

/* AIFF files tested and working:
 - 22050 Hz, 8bit PCM, stereo
 - 22050 Hz, 16bit PCM, mono
 - 11025 Hz, 16bit PCM, stereo
 - 22050 Hz, 16bit PCM, stereo
 - 44100 Hz, 16bit PCM, stereo
 - 22050 Hz, 24bit PCM, stereo
 - 22050 Hz, 32bit PCM, stereo
 */

/* AIFC files tested and working:
 - 22050 Hz, 8bit PCM, stereo
 - 22050 Hz, 32bit float, stereo
 - 22050 Hz, 64bit float, stereo
 - 22050 Hz, a-law, stereo
 - 22050 Hz, u-law, stereo
 */

#include "vice.h"

/* #define DEBUG_FILEDRV 1 */

#include <string.h> /* for memcpy */
#include <math.h>

#ifdef USE_MPG123
#include <mpg123.h>
#endif

#ifdef USE_FLAC
#include <FLAC/stream_decoder.h>
#endif

#ifdef USE_VORBIS
#include <vorbis/vorbisfile.h>
#endif

#include "types.h"

#include "cmdline.h"
#include "file_drv.h"
#include "lib.h"
#include "log.h"
#include "machine.h"
#include "maincpu.h"
#include "resources.h"
#include "sampler.h"
#include "util.h"

static log_t filedrv_log = LOG_ERR;

static char *sample_name = NULL;

/* In the future the filename can be set from either commandline or gui */
#define SAMPLE_NAME "inputsound"

#define AUDIO_TYPE_UNKNOWN  -1
#define AUDIO_TYPE_PCM       0
#define AUDIO_TYPE_FLOAT     1
#define AUDIO_TYPE_ALAW      2
#define AUDIO_TYPE_ULAW      3
#define AUDIO_TYPE_PCM_AMIGA 4
#define AUDIO_TYPE_PCM_BE    5
#define AUDIO_TYPE_FLOAT_BE  6

static unsigned sample_size = 0;
static int sound_sampling_started = 0;

static unsigned int sound_sample_frame_start;
static unsigned int old_frame;
static unsigned int sound_frames_per_sec;
static unsigned int sound_cycles_per_frame;
static unsigned int sound_samples_per_frame;

static int current_channels = 0;

static uint8_t *file_buffer = NULL;
static unsigned int file_pointer = 0;
static unsigned int file_size = 0;

static int sound_audio_type = 0;
static unsigned int sound_audio_channels = 0;
static unsigned int sound_audio_rate = 0;
static unsigned int sound_audio_bits = 0;

/* ---------------------------------------------------------------------- */

static int wav_fmt_extension_bytes = 0;

static uint8_t *sample_buffer1 = NULL;
static uint8_t *sample_buffer2 = NULL;

static int16_t decode_ulaw(uint8_t sample)
{
    int16_t t;

    sample = ~sample;

    t = ((sample & 0xf) << 3) + 0x84;
    t <<= ((unsigned)sample & 0x70) >> 4;

    return ((sample & 0x80) ? (0x84 - t) : (t - 0x84));
}

static int16_t decode_alaw(uint8_t sample)
{
    int16_t t;
    int16_t seg;

    sample ^= 0x55;

    t = (sample & 0xf) << 4;
    seg = ((unsigned)sample & 0x70) >> 4;
    switch (seg) {
        case 0:
            t += 8;
            break;
        case 1:
            t += 0x108;
            break;
        default:
            t += 0x108;
            t <<= seg - 1;
    }
    return ((sample & 0x80) ? t : -t);
}

static int convert_alaw_buffer(int size, int channels)
{
    unsigned int frame_size = sound_audio_bits * sound_audio_channels / 8;
    unsigned int i;
    signed char sample;

    sample_size = size / frame_size;

    sample_buffer1 = lib_malloc(sample_size);
    if (channels == SAMPLER_OPEN_STEREO) {
        if (sound_audio_channels == 2) {
            sample_buffer2 = lib_malloc(sample_size);
        } else {
            sample_buffer2 = sample_buffer1;
        }
    }

    for (i = 0; i < sample_size; ++i) {
        sample = file_buffer[file_pointer + (i * frame_size)];
        sample_buffer1[i] = (uint8_t)(decode_alaw(sample) >> 8) + 0x80;
        if (sound_audio_channels == 2 && channels == SAMPLER_OPEN_STEREO) {
            sample = file_buffer[file_pointer + (i * frame_size) + 1];
            sample_buffer2[i] = (uint8_t)(decode_alaw(sample) >> 4) + 0x80;
        }
    }
    lib_free(file_buffer);
    file_buffer = NULL;
    return 0;
}

static int convert_ulaw_buffer(int size, int channels)
{
    unsigned int frame_size = sound_audio_bits * sound_audio_channels / 8;
    unsigned int i;
    signed char sample;

    sample_size = size / frame_size;

    sample_buffer1 = lib_malloc(sample_size);
    if (channels == SAMPLER_OPEN_STEREO) {
        if (sound_audio_channels == 2) {
            sample_buffer2 = lib_malloc(sample_size);
        } else {
            sample_buffer2 = sample_buffer1;
        }
    }

    for (i = 0; i < sample_size; ++i) {
        sample = file_buffer[file_pointer + (i * frame_size)];
        sample_buffer1[i] = (uint8_t)(decode_ulaw(sample) >> 8) + 0x80;
        if (sound_audio_channels == 2 && channels == SAMPLER_OPEN_STEREO) {
            sample = file_buffer[file_pointer + (i * frame_size) + 1];
            sample_buffer2[i] = (uint8_t)(decode_ulaw(sample) >> 3) + 0x80;
        }
    }
    lib_free(file_buffer);
    file_buffer = NULL;
    return 0;
}

static int convert_pcm_buffer(int size, int channels)
{
    unsigned int frame_size = sound_audio_bits * sound_audio_channels / 8;
    unsigned int i;

    sample_size = size / frame_size;

    sample_buffer1 = lib_malloc(sample_size);
    if (channels == SAMPLER_OPEN_STEREO) {
        if (sound_audio_channels == 2) {
            sample_buffer2 = lib_malloc(sample_size);
        } else {
            sample_buffer2 = sample_buffer1;
        }
    }

    for (i = 0; i < sample_size; ++i) {
        if (sound_audio_type == AUDIO_TYPE_PCM_BE) {
            sample_buffer1[i] = file_buffer[file_pointer + (i * frame_size)];
        } else {
            sample_buffer1[i] = file_buffer[file_pointer + (i * frame_size) + (sound_audio_bits / 8) - 1];
        }
        if (sound_audio_bits != 8 || sound_audio_type == AUDIO_TYPE_PCM_AMIGA || sound_audio_type == AUDIO_TYPE_PCM_BE) {
            sample_buffer1[i] += 0x80;
        }
        if (sound_audio_channels == 2 && channels == SAMPLER_OPEN_STEREO) {
            if (sound_audio_type == AUDIO_TYPE_PCM_BE) {
                sample_buffer2[i] = file_buffer[file_pointer + (i * frame_size) + (frame_size / 2)];
            } else {
                sample_buffer2[i] = file_buffer[file_pointer + (i * frame_size) + (frame_size / 2) + (sound_audio_bits / 8) - 1];
            }
            if (sound_audio_bits != 8 || sound_audio_type == AUDIO_TYPE_PCM_AMIGA || sound_audio_type == AUDIO_TYPE_PCM_BE) {
                sample_buffer2[i] += 0x80;
            }
        }
    }
    lib_free(file_buffer);
    file_buffer = NULL;
    return 0;
}

/* FIXME: endianess */
static int convert_float_buffer(int size, int channels)
{
    unsigned int frame_size = sound_audio_bits * sound_audio_channels / 8;
    unsigned int i;
    unsigned char c[sizeof(float)];
    float f;
    int32_t sample;

    sample_size = size / frame_size;

    sample_buffer1 = lib_malloc(sample_size);
    if (channels == SAMPLER_OPEN_STEREO) {
        if (sound_audio_channels == 2) {
            sample_buffer2 = lib_malloc(sample_size);
        } else {
            sample_buffer2 = sample_buffer1;
        }
    }

    for (i = 0; i < sample_size; ++i) {
        if (sound_audio_type == AUDIO_TYPE_FLOAT_BE) {
            c[3] = file_buffer[file_pointer + (i * frame_size)];
            c[2] = file_buffer[file_pointer + (i * frame_size) + 1];
            c[1] = file_buffer[file_pointer + (i * frame_size) + 2];
            c[0] = file_buffer[file_pointer + (i * frame_size) + 3];
        } else {
            c[0] = file_buffer[file_pointer + (i * frame_size)];
            c[1] = file_buffer[file_pointer + (i * frame_size) + 1];
            c[2] = file_buffer[file_pointer + (i * frame_size) + 2];
            c[3] = file_buffer[file_pointer + (i * frame_size) + 3];
        }
        memcpy(&f, c, sizeof(float));
        f *= 0x7fffffff;
        sample = (int32_t)f;
        sample_buffer1[i] = (uint8_t)((sample >> 24) + 0x80);
        if (sound_audio_channels == 2 && channels == SAMPLER_OPEN_STEREO) {
            if (sound_audio_type == AUDIO_TYPE_FLOAT_BE) {
                c[3] = file_buffer[file_pointer + (i * frame_size) + 4];
                c[2] = file_buffer[file_pointer + (i * frame_size) + 5];
                c[1] = file_buffer[file_pointer + (i * frame_size) + 6];
                c[0] = file_buffer[file_pointer + (i * frame_size) + 7];
            } else {
                c[0] = file_buffer[file_pointer + (i * frame_size) + 4];
                c[1] = file_buffer[file_pointer + (i * frame_size) + 5];
                c[2] = file_buffer[file_pointer + (i * frame_size) + 6];
                c[3] = file_buffer[file_pointer + (i * frame_size) + 7];
            }
            memcpy(&f, c, sizeof(float));
            f *= 0x7fffffff;
            sample = (int32_t)f;
            sample_buffer2[i] = (uint8_t)((sample >> 24) + 0x80);
        }
    }
    lib_free(file_buffer);
    file_buffer = NULL;
    return 0;
}

/* FIXME: endianess */
static int convert_double_buffer(int size, int channels)
{
    unsigned int frame_size = sound_audio_bits * sound_audio_channels / 8;
    unsigned int i;
    unsigned char c[sizeof(double)];
    double f;
    int32_t sample;

    sample_size = size / frame_size;

    sample_buffer1 = lib_malloc(sample_size);
    if (channels == SAMPLER_OPEN_STEREO) {
        if (sound_audio_channels == 2) {
            sample_buffer2 = lib_malloc(sample_size);
        } else {
            sample_buffer2 = sample_buffer1;
        }
    }

    for (i = 0; i < sample_size; ++i) {
        if (sound_audio_type == AUDIO_TYPE_FLOAT_BE) {
            c[7] = file_buffer[file_pointer + (i * frame_size)];
            c[6] = file_buffer[file_pointer + (i * frame_size) + 1];
            c[5] = file_buffer[file_pointer + (i * frame_size) + 2];
            c[4] = file_buffer[file_pointer + (i * frame_size) + 3];
            c[3] = file_buffer[file_pointer + (i * frame_size) + 4];
            c[2] = file_buffer[file_pointer + (i * frame_size) + 5];
            c[1] = file_buffer[file_pointer + (i * frame_size) + 6];
            c[0] = file_buffer[file_pointer + (i * frame_size) + 7];
        } else {
            c[0] = file_buffer[file_pointer + (i * frame_size)];
            c[1] = file_buffer[file_pointer + (i * frame_size) + 1];
            c[2] = file_buffer[file_pointer + (i * frame_size) + 2];
            c[3] = file_buffer[file_pointer + (i * frame_size) + 3];
            c[4] = file_buffer[file_pointer + (i * frame_size) + 4];
            c[5] = file_buffer[file_pointer + (i * frame_size) + 5];
            c[6] = file_buffer[file_pointer + (i * frame_size) + 6];
            c[7] = file_buffer[file_pointer + (i * frame_size) + 7];
        }
        memcpy(&f, c, sizeof(double));
        f *= 0x7fffffff;
        sample = (int32_t)f;
        sample_buffer1[i] = (uint8_t)((sample >> 24) + 0x80);
        if (sound_audio_channels == 2 && channels == SAMPLER_OPEN_STEREO) {
            if (sound_audio_type == AUDIO_TYPE_FLOAT_BE) {
                c[7] = file_buffer[file_pointer + (i * frame_size) + 8];
                c[6] = file_buffer[file_pointer + (i * frame_size) + 9];
                c[5] = file_buffer[file_pointer + (i * frame_size) + 10];
                c[4] = file_buffer[file_pointer + (i * frame_size) + 11];
                c[3] = file_buffer[file_pointer + (i * frame_size) + 12];
                c[2] = file_buffer[file_pointer + (i * frame_size) + 13];
                c[1] = file_buffer[file_pointer + (i * frame_size) + 14];
                c[0] = file_buffer[file_pointer + (i * frame_size) + 15];
            } else {
                c[0] = file_buffer[file_pointer + (i * frame_size) + 8];
                c[1] = file_buffer[file_pointer + (i * frame_size) + 9];
                c[2] = file_buffer[file_pointer + (i * frame_size) + 10];
                c[3] = file_buffer[file_pointer + (i * frame_size) + 11];
                c[4] = file_buffer[file_pointer + (i * frame_size) + 12];
                c[5] = file_buffer[file_pointer + (i * frame_size) + 13];
                c[6] = file_buffer[file_pointer + (i * frame_size) + 14];
                c[7] = file_buffer[file_pointer + (i * frame_size) + 15];
            }
            memcpy(&f, c, sizeof(double));
            f *= 0x7fffffff;
            sample = (int32_t)f;
            sample_buffer2[i] = (uint8_t)((sample >> 24) + 0x80);
        }
    }
    lib_free(file_buffer);
    file_buffer = NULL;
    return 0;
}

/* ---------------------------------------------------------------------- */

static void check_and_skip_chunk(void)
{
    unsigned int size = 0;
    int skip_chunk = 0;

    /* if the current chunk is of type 'LIST' we need to skip it */
    if (file_buffer[file_pointer] == 0x4C && file_buffer[file_pointer + 1] == 0x49 && file_buffer[file_pointer + 2] == 0x53 && file_buffer[file_pointer + 3] == 0x54) {
        skip_chunk = 1;
#if DEBUG_FILEDRV
        log_warning(filedrv_log, "LIST chunk found, skipping");
#endif
    }

    /* if the current chunk is of type 'PEAK' we need to skip it */
    if (file_buffer[file_pointer] == 0x50 && file_buffer[file_pointer + 1] == 0x45 && file_buffer[file_pointer + 2] == 0x41 && file_buffer[file_pointer + 3] == 0x4B) {
        skip_chunk = 1;
#if DEBUG_FILEDRV
        log_warning(filedrv_log, "PEAK chunk found, skipping");
#endif
    }

    /* if the current chunk is of type 'fact' we need to skip it */
    if (file_buffer[file_pointer] == 0x66 && file_buffer[file_pointer + 1] == 0x61 && file_buffer[file_pointer + 2] == 0x63 && file_buffer[file_pointer + 3] == 0x74) {
        skip_chunk = 1;
#if DEBUG_FILEDRV
        log_warning(filedrv_log, "fact chunk found, skipping");
#endif
    }

    if (skip_chunk) {
        file_pointer += 4;
        size = (file_buffer[file_pointer + 3] << 24) | (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
        file_pointer += size + 4;
    }
}

static int valid_wav_ftm_chunk_size(void)
{
    if (file_buffer[file_pointer] == 0x10 && file_buffer[file_pointer + 1] == 0 && file_buffer[file_pointer + 2] == 0 && file_buffer[file_pointer + 3] == 0) {
        wav_fmt_extension_bytes = 0;
        return 1;
    }
    if (file_buffer[file_pointer] == 0x12 && file_buffer[file_pointer + 1] == 0 && file_buffer[file_pointer + 2] == 0 && file_buffer[file_pointer + 3] == 0) {
        wav_fmt_extension_bytes = 2;
        return 1;
    }
    if (file_buffer[file_pointer] == 0x28 && file_buffer[file_pointer + 1] == 0 && file_buffer[file_pointer + 2] == 0 && file_buffer[file_pointer + 3] == 0) {
        wav_fmt_extension_bytes = 24;
        return 1;
    }
    return 0;
}

static int handle_wav_file(int channels)
{
    unsigned int size = 0;
    unsigned int bps = 0;

    if (file_size < 8) {
        log_error(filedrv_log, "file size smaller than 8 bytes.");
        return -1;
    }

    /* sanity check header indicated size with loaded size */
    size = (file_buffer[7] << 24) | (file_buffer[6] << 16) | (file_buffer[5] << 8) | file_buffer[4];
    if (size != file_size - 8) {
        log_error(filedrv_log,
                "header reported size not what was expected: header says: %u, "
                "filesize - 8 is %u.",
                size, file_size - 8);
        return -1;
    }

    /* next needs to be 'WAVE' */
    if (file_buffer[8] != 0x57 || file_buffer[9] != 0x41 || file_buffer[10] != 0x56 || file_buffer[11] != 0x45) {
        log_error(filedrv_log, "WAVE not found at expected header position, found: %X %X %X %X.", file_buffer[8], file_buffer[9], file_buffer[10], file_buffer[11]);
        return -1;
    }

    file_pointer = 12;

    check_and_skip_chunk();

    /* current chunk now needs to be 'fmt ' */
    if (file_buffer[file_pointer] != 0x66 || file_buffer[file_pointer + 1] != 0x6D || file_buffer[file_pointer + 2] != 0x74 || file_buffer[file_pointer + 3] != 0x20) {
        log_error(filedrv_log, "'fmt ' chunk not found in the expected header position, %X %X %X %X", file_buffer[file_pointer], file_buffer[file_pointer + 1], file_buffer[file_pointer + 2], file_buffer[file_pointer + 3]);
        return -1;
    }
    file_pointer += 4;

    /* chunk size needs to be 0x10 */
    if (!valid_wav_ftm_chunk_size()) {
        log_error(filedrv_log, "unexpected chunk size %2X%2X%2X%2X", file_buffer[file_pointer + 3], file_buffer[file_pointer + 2], file_buffer[file_pointer + 1], file_buffer[file_pointer]);
        return -1;
    }
    file_pointer += 4;

    /* get the audio format 1: PCM (8/16/24/32bit), 3: float (32/64bit), 6: Alaw, 7: Ulaw */
    if (file_buffer[file_pointer] == 1 && file_buffer[file_pointer + 1] == 0) {
        sound_audio_type = AUDIO_TYPE_PCM;
    } else if (file_buffer[file_pointer] == 3 && file_buffer[file_pointer + 1] == 0) {
        sound_audio_type = AUDIO_TYPE_FLOAT;
    } else if (file_buffer[file_pointer] == 6 && file_buffer[file_pointer + 1] == 0) {
        sound_audio_type = AUDIO_TYPE_ALAW;
    } else if (file_buffer[file_pointer] == 7 && file_buffer[file_pointer + 1] == 0) {
        sound_audio_type = AUDIO_TYPE_ULAW;
    } else {
        log_error(filedrv_log, "unexpected audio format : %2X%2X", file_buffer[file_pointer + 1], file_buffer[file_pointer]);
        return -1;
    }
    file_pointer += 2;

    /* channels used in the file */
    sound_audio_channels = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (sound_audio_channels == 0 || sound_audio_channels > 2) {
        log_error(filedrv_log, "unexpected amount of audio channels : %u",
                sound_audio_channels);
        return -1;
    }
    file_pointer +=2;

    /* sample rate used in file */
    sound_audio_rate = (file_buffer[file_pointer + 3] << 24) | (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (sound_audio_rate == 0) {
        log_error(filedrv_log, "audio rate is 0");
        return -1;
    }
    file_pointer += 4;

    /* get 1st instance of bits per sample */
    bps = (file_buffer[file_pointer + 3] << 24) | (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    sound_audio_bits = (bps / (sound_audio_rate * sound_audio_channels)) * 8;
    file_pointer += 4;

    /* get 2nd instance of bits per sample */
    bps = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    bps = bps * 8 / sound_audio_channels;
    if (bps != sound_audio_bits) {
        log_error(filedrv_log,
                "First instance of bps does not match second instance: %u %u",
                sound_audio_bits, bps);
        return -1;
    }
    file_pointer += 2;

    /* get real instance of bits per sample */
    bps = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (bps != sound_audio_bits) {
        log_error(filedrv_log,
                "First instance of bps does not match real instance: %u %u",
                sound_audio_bits, bps);
        return -1;
    }
    file_pointer += 2;

    if (wav_fmt_extension_bytes) {
        file_pointer += wav_fmt_extension_bytes;
    }

    check_and_skip_chunk();
    check_and_skip_chunk();
    check_and_skip_chunk();

    /* current chunk now needs to be 'data' */
    if (file_buffer[file_pointer] != 0x64 || file_buffer[file_pointer + 1] != 0x61 || file_buffer[file_pointer + 2] != 0x74 || file_buffer[file_pointer + 3] != 0x61) {
        log_error(filedrv_log, "data chunk not found at expected header position: %X%X%X%X", file_buffer[file_pointer], file_buffer[file_pointer + 1], file_buffer[file_pointer + 2], file_buffer[file_pointer + 3]);
        return -1;
    }
    file_pointer += 4;

    /* get remaining size */
    size = (file_buffer[file_pointer + 3] << 24) | (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (size != file_size - (file_pointer + 4)) {
        log_error(filedrv_log,
                "data chunk size does not match remaining file size: %u %u",
                size, file_size - (file_pointer + 4));
        return -1;
    }
    file_pointer += 4;

    switch (sound_audio_type) {
        case AUDIO_TYPE_PCM:
            return convert_pcm_buffer(size, channels);
        case AUDIO_TYPE_FLOAT:
            switch (sound_audio_bits) {
                case 32:
                    return convert_float_buffer(size, channels);
                case 64:
                    return convert_double_buffer(size, channels);
                default:
                    log_error(filedrv_log, "Unhandled float format : %u",
                            sound_audio_bits);
                    return -1;
            }
        case AUDIO_TYPE_ALAW:
            return convert_alaw_buffer(size, channels);
        case AUDIO_TYPE_ULAW:
            return convert_ulaw_buffer(size, channels);
        default:
            log_error(filedrv_log, "unhandled audio type");
            return -1;
    }
    return -1;
}

static int is_wav_file(void)
{
    if (file_size < 4) {
        return 0;
    }

    /* Check for wav header signature */
    if (file_buffer[0] == 0x52 && file_buffer[1] == 0x49 && file_buffer[2] == 0x46 && file_buffer[3] == 0x46) {
        return 1;
    }
    return 0;
}

/* ---------------------------------------------------------------------- */

static uint8_t *voc_buffer1 = NULL;
static uint8_t *voc_buffer2 = NULL;
static unsigned int voc_buffer_size;

static int voc_handle_sound_1(int channels)
{
    unsigned int size;
    uint8_t fd;
    uint8_t codec;
    unsigned int rem;

    if (file_pointer + 6 > file_size) {
        log_error(filedrv_log, "Voc file size too small");
        return -1;
    }

    ++file_pointer;

    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (file_pointer + size > file_size) {
        log_error(filedrv_log, "Reported voc file size too big: %X %X", file_size, size);
        return -1;
    }
    file_pointer += 3;

    rem = (file_size - file_pointer) % 0x1000000;

    if (rem == size || rem == size - 1 || rem == size + 1) {
        size = (file_size - file_pointer) - 1;
    }

    if (!sound_audio_rate) {
        fd = file_buffer[file_pointer];
        codec = file_buffer[file_pointer + 1];

        switch (codec) {
            case 0:
                sound_audio_type = AUDIO_TYPE_PCM;
                sound_audio_bits = 8;
                break;
            case 4:
                sound_audio_type = AUDIO_TYPE_PCM;
                sound_audio_bits = 16;
                break;
            case 6:
                sound_audio_type = AUDIO_TYPE_ALAW;
                sound_audio_bits = 16;
                break;
            case 7:
                sound_audio_type = AUDIO_TYPE_ULAW;
                sound_audio_bits = 16;
                break;
            default:
                log_error(filedrv_log, "Unhandled voc file codec: %X", codec);
                return -1;
                break;
        }
        sound_audio_rate = 1000000 / (256 - fd);
    }
    file_pointer += 2;
    size -= 2;

    if (voc_buffer1) {
        lib_free(voc_buffer1);
        voc_buffer1 = NULL;
        return -1;
    }

    voc_buffer1 = lib_malloc(size);
    memcpy(voc_buffer1, file_buffer + file_pointer, size);
    file_pointer += size;
    voc_buffer_size = size;

    return 0;
}

static int voc_handle_sound_2(int channels)
{
    unsigned int size;

    if (file_pointer + 6 > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }

    ++file_pointer;

    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (file_pointer + size > file_size) {
        log_error(filedrv_log, "Reported voc sound 2 block size too big: %X %X", size, file_size);
        return -1;
    }
    file_pointer += 5;
    size -= 2;
    
    if (!voc_buffer1) {
        return -1;
    }
    voc_buffer2 = lib_malloc(voc_buffer_size + size);
    memcpy(voc_buffer2, voc_buffer1, voc_buffer_size);
    memcpy(voc_buffer2 + voc_buffer_size, file_buffer + file_pointer, size);
    lib_free(voc_buffer1);
    voc_buffer1 = voc_buffer2;
    voc_buffer2 = NULL;
    voc_buffer_size += size;
    file_pointer += size;

    return 0;
}

static int voc_handle_silence(int channels)
{
    unsigned int size;

    if (file_pointer + 7 > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }

    ++file_pointer;

    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (size != 3) {
        log_error(filedrv_log, "Voc silence block size mismatch: %X", size);
        return -1;
    }

    file_pointer += 3;

    size = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];

    if (sound_audio_type == AUDIO_TYPE_UNKNOWN) {
        log_error(filedrv_log, "Found voc silence block before any information headers");
        return -1;
    }

    if (sound_audio_type == AUDIO_TYPE_PCM) {
        size *= (sound_audio_bits / 8);
    }
    size *= sound_audio_channels;

    voc_buffer2 = lib_malloc(voc_buffer_size + size);

    if (sound_audio_bits == 8) {
        memset(voc_buffer2 + voc_buffer_size, 0x80, size);
    } else {
        memset(voc_buffer2 + voc_buffer_size, 0, size);
    }

    if (voc_buffer1) {
        memcpy(voc_buffer2, voc_buffer1, voc_buffer_size);
        lib_free(voc_buffer1);
    }
    voc_buffer1 = voc_buffer2;
    voc_buffer2 = NULL;
    voc_buffer_size += size;
    file_pointer += 3;
    return 0;
}

static int voc_handle_sound_9(int channels)
{
    unsigned int size;
    uint16_t codec;
    unsigned int rem;

    if (file_pointer + 16 > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }

    ++file_pointer;

    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (size + file_pointer > file_size) {
        log_error(filedrv_log, "Reported voc block 9 size too big: %X %X", size, file_size - file_pointer);
        return -1;
    }

    file_pointer += 3;

    rem = (file_size - file_pointer) % 0x1000000;

    if (rem == size || rem == size - 1 || rem == size + 1) {
        size = (file_size - file_pointer) - 1;
    }

    sound_audio_rate = (file_buffer[file_pointer + 3] << 24) | (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (!sound_audio_rate) {
        log_error(filedrv_log, "Reported voc sound block audio rate is 0");
        return -1;
    }

    file_pointer += 4;
    size -= 4;

    sound_audio_bits = file_buffer[file_pointer];

    switch (sound_audio_bits) {
        case 8:
        case 16:
        case 24:
        case 32:
            break;
        default:
            log_error(filedrv_log, "Reported voc bits per sample is not 8, 16, 24 or 32");
            return -1;
    }

    ++file_pointer;
    --size;

    sound_audio_channels = file_buffer[file_pointer];
    if (sound_audio_channels < 1 || sound_audio_channels > 2) {
        log_error(filedrv_log, "Reported voc channels not 1 or 2");
        return -1;
    }

    ++file_pointer;
    --size;

    codec = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];

    switch (codec) {
        case 0:
        case 4:
            sound_audio_type = AUDIO_TYPE_PCM;
            break;
        case 6:
            sound_audio_type = AUDIO_TYPE_ALAW;
            break;
        case 7:
            sound_audio_type = AUDIO_TYPE_ULAW;
            break;
        default:
            log_error(filedrv_log, "Unknown voc sound block codec");
            return -1;
            break;
    }

    file_pointer += 6;
    size -= 6;

    voc_buffer2 = lib_malloc(voc_buffer_size + size);

    if (voc_buffer1) {
        memcpy(voc_buffer2, voc_buffer1, voc_buffer_size);
        lib_free(voc_buffer1);
    }
    memcpy(voc_buffer2 + voc_buffer_size, file_buffer + file_pointer, size);
    voc_buffer1 = voc_buffer2;
    voc_buffer2 = NULL;
    voc_buffer_size += size;
    file_pointer += size;

    return 0;
}

static int voc_handle_extra_info(int channels)
{
    unsigned int size;
    uint16_t fd;
    uint8_t codec;

    if (file_pointer + 8 > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }

    ++file_pointer;

    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (size != 4) {
        log_error(filedrv_log, "Reported voc extra info block size is not 4");
        return -1;
    }
    file_pointer += 3;

    fd = (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    codec = file_buffer[file_pointer + 2];

    switch (codec) {
        case 0:
            sound_audio_type = AUDIO_TYPE_PCM;
            sound_audio_bits = 8;
            break;
        case 4:
            sound_audio_type = AUDIO_TYPE_PCM;
            sound_audio_bits = 16;
            break;
        case 6:
            sound_audio_type = AUDIO_TYPE_ALAW;
            sound_audio_bits = 16;
            break;
        case 7:
            sound_audio_type = AUDIO_TYPE_ULAW;
            sound_audio_bits = 16;
            break;
        default:
            log_error(filedrv_log, "Reported voc extra info block codec is unknown");
            return -1;
            break;
    }
    sound_audio_channels = file_buffer[file_pointer + 3] + 1;
    sound_audio_rate = 256000000 / (sound_audio_channels * (65536 - fd));
    file_pointer += 4;
    return 0;
}

static int voc_handle_text(void)
{
    unsigned int size;

    if (file_pointer + 4 > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }
    ++file_pointer;
    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    file_pointer += 3;

    if (file_pointer + size > file_size) {
        log_error(filedrv_log, "Reported voc text block size too big");
        return -1;
    }
    if (size) {
        file_pointer += size;
    }
    return 0;
}

static int voc_handle_ignore(unsigned int amount)
{
    unsigned int size;

    if (amount + 4 + file_pointer > file_size) {
        log_error(filedrv_log, "Voc file too small");
        return -1;
    }
    ++file_pointer;
    size = (file_buffer[file_pointer + 2] << 16) | (file_buffer[file_pointer + 1] << 8) | file_buffer[file_pointer];
    if (size != amount) {
        log_error(filedrv_log, "Unexpected voc block size: %X", size);
        return -1;
    }
    file_pointer += 3;
    if (amount) {
        file_pointer += amount;
    }
    return 0;
}

static int handle_voc_file(int channels)
{
    uint16_t version;
    uint16_t check;
    int end_of_stream = 0;
    int err = 0;

    sound_audio_channels = 0;
    sound_audio_rate = 0;
    sound_audio_bits = 0;
    sound_audio_type = AUDIO_TYPE_UNKNOWN;

    if (file_buffer[19] != 0x1A) {
        log_error(filedrv_log, "Voc file $1A signature not found");
        return -1;
    }

    if (file_buffer[20] != 0x1A || file_buffer[21] != 0) {
        log_error(filedrv_log, "Incorrect voc file header length : %X", 
                (unsigned int)(file_buffer[21] << 8) | file_buffer[20]);
        return -1;
    }

    version = (file_buffer[23] << 8) | file_buffer[22];
    check = (file_buffer[25] << 8) | file_buffer[24];

    if (check != ~version + 0x1234) {
        log_error(filedrv_log, "VOC file header checksum incorrect: %4X %4X", check, version);
        return -1;
    }

    file_pointer = 26;

    while (file_pointer <= file_size && !end_of_stream) {
        switch (file_buffer[file_pointer]) {
            case 0:
                end_of_stream = 1;
                break;
            case 1:
                err = voc_handle_sound_1(channels);
                break;
            case 2:
                err = voc_handle_sound_2(channels);
                break;
            case 3:
                err = voc_handle_silence(channels);
                break;
            case 4:
            case 6:
                err = voc_handle_ignore(2);
                break;
            case 5:
                err = voc_handle_text();
                break;
            case 7:
                err = voc_handle_ignore(0);
                break;
            case 8:
                err = voc_handle_extra_info(channels);
                break;
            case 9:
                err = voc_handle_sound_9(channels);
                break;
            default:
                log_error(filedrv_log, "Unknown VOC block type : %2X", file_buffer[file_pointer]);
                return -1;
        }
        if (err) {
            if (voc_buffer1) {
                lib_free(voc_buffer1);
                voc_buffer1 = NULL;
            }
            return -1;
        }
    }

    lib_free(file_buffer);
    file_buffer = NULL;

    file_buffer = voc_buffer1;
    voc_buffer1 = NULL;

    file_pointer = 0;
    file_size = voc_buffer_size;

    switch (sound_audio_type) {
        case AUDIO_TYPE_PCM:
            return convert_pcm_buffer(file_size, channels);
        case AUDIO_TYPE_ALAW:
            return convert_alaw_buffer(file_size, channels);
        case AUDIO_TYPE_ULAW:
            return convert_ulaw_buffer(file_size, channels);
        default:
            log_error(filedrv_log, "unhandled audio type");
            return -1;
    }
    return -1;
}

static int is_voc_file(void)
{
    char header[] = { 0x43, 0x72, 0x65, 0x61, 0x74, 0x69, 0x76, 0x65, 0x20, 0x56, 0x6F, 0x69, 0x63, 0x65, 0x20, 0x46, 0x69, 0x6C, 0x65 };
    size_t i;

    if (file_size < 26) {
        return 0;
    }

    /* Check for voc header signature */
    for (i = 0; i < sizeof(header); ++i) {
        if (file_buffer[i] != header[i]) {
            return 0;
        }
    }
    return 1;
}

/* ---------------------------------------------------------------------- */

static unsigned int iff_samples = 0;

static int iff_handle_chan(void)
{
    uint32_t stereo;

    file_pointer += 4;

    if (file_buffer[file_pointer] != 0 || file_buffer[file_pointer + 1] != 0 || file_buffer[file_pointer + 2] != 0 || file_buffer[file_pointer + 3] != 4) {
        log_error(filedrv_log, "Reported iff chan chunk size is not 4");
        return -1;
    }

    file_pointer += 4;

    stereo = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

    if (stereo == 6 || stereo == 0x6000000) {
        sound_audio_channels = 2;
    }

    file_pointer += 4;

    return 0;
}

static int iff_handle_body(int channels)
{
    uint32_t size;

    file_pointer += 4;

    size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

    if (!size) {
        log_error(filedrv_log, "Reported iff body chunk size is 0");
        return -1;
    }

    file_pointer += 4;

    return convert_pcm_buffer(size, channels);
}

static int handle_iff_file(int channels)
{
    uint32_t size;
    uint32_t header;
    int body_found = 0;
    int err = 0;

    sound_audio_type = AUDIO_TYPE_PCM_AMIGA;
    sound_audio_channels = 1;
    sound_audio_bits = 8;

    size = (file_buffer[4] << 24) | (file_buffer[5] << 16) | (file_buffer[6] << 8) | file_buffer[7];

    if (size != file_size - 8) {
        log_error(filedrv_log, "Reported iff total size mismatch : %X, %X", (unsigned int)size, file_size - 8);
        return -1;
    }

    size = (file_buffer[16] << 24) | (file_buffer[17] << 16) | (file_buffer[18] << 8) | file_buffer[19];
    if (size != 20) {
        log_error(filedrv_log, "Reported iff header size is not 20");
        return -1;
    }

    iff_samples = (file_buffer[20] << 24) | (file_buffer[21] << 16) | (file_buffer[22] << 8) | file_buffer[23];
    if (!iff_samples) {
        log_error(filedrv_log, "Reported iff amount of samples is 0");
        return -1;
    }

    if (file_buffer[24] != 0 || file_buffer[25] != 0 || file_buffer[26] != 0 || file_buffer[27] != 0) {
        return -1;
    }

    if (file_buffer[28] != 0 || file_buffer[29] != 0 || file_buffer[30] != 0 || file_buffer[31] != 0) {
        return -1;
    }

    sound_audio_rate = (file_buffer[32] << 8) | file_buffer[33];
    if (!sound_audio_rate) {
        log_error(filedrv_log, "Reported iff bitrate  is 0");
        return -1;
    }

    if (file_buffer[34] != 1) {
        return -1;
    }

    if (file_buffer[35]) {
        return -1;
    }

    file_pointer = 40;

    while (file_pointer <= file_size && !body_found) {
        if (file_pointer + 8 > file_size) {
            log_error(filedrv_log, "Iff file too small");
            return -1;
        }

        header = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

        switch (header) {
            case 0x424F4459:
                err = iff_handle_body(channels);
                body_found = 1;
                break;
            case 0x4348414E:
                err = iff_handle_chan();
                break;
            default:
                file_pointer += 4;
                size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
                file_pointer += 4;
                if (file_pointer + size > file_size) {
                    log_error(filedrv_log, "Iff file too small");
                    return -1;
                }
                file_pointer += size;
        }
        if (err) {
            return -1;
        }
    }
    if (!body_found) {
        return -1;
    }
    return 0;
}

static int is_iff_file(void)
{
    if (file_size < 16) {
        return 0;
    }

    /* Check for iff header signature */
    if (file_buffer[0] == 0x46 && file_buffer[1] == 0x4F && file_buffer[2] == 0x52 && file_buffer[3] == 0x4D) {
        if (file_buffer[8] == 0x38 && file_buffer[9] == 0x53 && file_buffer[10] == 0x56 && file_buffer[11] == 0x58) {
            if (file_buffer[12] == 0x56 && file_buffer[13] == 0x48 && file_buffer[14] == 0x44 && file_buffer[15] == 0x52) {
                return 1;
            }
        }
    }
    return 0;
}

/* ---------------------------------------------------------------------- */

#define U2F(u) (((double)((long)(u - 2147483647L - 1))) + 2147483648.0)

#ifndef HUGE_VAL
#define HUGE_VAL HUGE
#endif

static double float80tofloat64(unsigned char* bytes)
{
    double f;
    int expon;
    unsigned long hiMant, loMant;

    expon = ((bytes[0] & 0x7F) << 8) | (bytes[1] & 0xFF);
    hiMant = ((unsigned long)(bytes[2] & 0xFF) << 24) | ((unsigned long)(bytes[3] & 0xFF) << 16) | ((unsigned long)(bytes[4] & 0xFF) << 8) | ((unsigned long)(bytes[5] & 0xFF));
    loMant = ((unsigned long)(bytes[6] & 0xFF) << 24) | ((unsigned long)(bytes[7] & 0xFF) << 16) | ((unsigned long)(bytes[8] & 0xFF) << 8) | ((unsigned long)(bytes[9] & 0xFF));

    if (!expon && !hiMant && !loMant) {
        f = 0;
    } else {
        if (expon == 0x7FFF) {
            f = HUGE_VAL;
        } else {
            expon -= 16383;
            f  = ldexp(U2F(hiMant), expon -= 31);
            f += ldexp(U2F(loMant), expon -= 32);
        }
    }

    if (bytes[0] & 0x80) {
        return -f;
    }
    return f;
}

static int aiff_handle_ssnd(int channels)
{
    uint32_t size;
    int i;

    file_pointer += 4;

    size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
    file_pointer += 4;
    if (file_pointer + size > file_size) {
        log_error(filedrv_log, "SSND chunk bigger than remaining size : %X %X", (unsigned int)size, file_size - file_pointer);
        return -1;
    }

    for (i = 0; i < 8; ++i) {
        if (file_buffer[file_pointer + i]) {
            log_error(filedrv_log, "SSND secondary parameters not 0");
            return -1;
        }
    }

    file_pointer += 8;
    size -= 8;

    return convert_pcm_buffer(size, channels);
}

static int aiff_handle_comm(void)
{
    uint32_t size;
    double f64;
    unsigned char f80[10];
    int i;

    file_pointer += 4;

    size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
    if (size != 18) {
        log_error(filedrv_log, "COMM chunk size not 18: %d", (int)size);
        return -1;
    }

    file_pointer += 4;

    sound_audio_channels = (file_buffer[file_pointer] << 8) | file_buffer[file_pointer + 1];
    if (sound_audio_channels < 1 || sound_audio_channels > 2) {
        log_error(filedrv_log, "COMM channels not 1 or 2 : %u",
                sound_audio_channels);
        return -1;
    }

    file_pointer += 6;

    sound_audio_bits = (file_buffer[file_pointer] << 8) | file_buffer[file_pointer + 1];
    switch (sound_audio_bits) {
        case 8:
        case 16:
        case 24:
        case 32:
            break;
        default:
            log_error(filedrv_log, "COMM bits not 8, 16, 24 or 32 : %u",
                    sound_audio_bits);
            return -1;
    }

    file_pointer += 2;

    for (i = 0; i < 10; ++i) {
        f80[i] = file_buffer[file_pointer + i];  
    }

    f64 = float80tofloat64(f80);
    sound_audio_rate = (unsigned int)f64;
    if (!sound_audio_rate) {
        log_error(filedrv_log, "COMM audio rate is 0");
        return -1;
    }

    file_pointer += 10;
    sound_audio_type = AUDIO_TYPE_PCM_BE;

    return 0;
}

static int handle_aiff_file(int channels)
{
    uint32_t size;
    uint32_t header;
    int ssnd_found = 0;
    int err = 0;

    size = (file_buffer[4] << 24) | (file_buffer[5] << 16) | (file_buffer[6] << 8) | file_buffer[7];

    if (size != file_size - 8) {
        log_error(filedrv_log, "AIFF size is wrong : %X %X", (unsigned int)size, file_size - 8);
        return -1;
    }

    file_pointer = 12;

    while (file_pointer <= file_size && !ssnd_found) {
        if (file_pointer + 8 > file_size) {
            log_error(filedrv_log, "Aiff file too small");
            return -1;
        }

        header = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

        switch (header) {
            case 0x53534E44:
#ifdef DEBUG_FILEDRV
                log_warning(filedrv_log, "handling AIFF SSND chunk");
#endif
                err = aiff_handle_ssnd(channels);
                ssnd_found = 1;
                break;
            case 0x434F4D4D:
#ifdef DEBUG_FILEDRV
                log_warning(filedrv_log, "handling AIFF COMM chunk");
#endif
                err = aiff_handle_comm();
                break;
            default:
                file_pointer += 4;
                size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
                if (size % 2) {
                    ++size;
                }
                file_pointer += 4;
                if (file_pointer + size > file_size) {
                    log_error(filedrv_log, "Aiff file too small");
                    return -1;
                }
                file_pointer += size;
        }
        if (err) {
            return -1;
        }
    }
    if (!ssnd_found) {
        return -1;
    }
    return 0;
}

static int is_aiff_file(void)
{
    if (file_size < 12) {
        return 0;
    }

    /* Check for aiff header signature */
    if (file_buffer[0] == 0x46 && file_buffer[1] == 0x4F && file_buffer[2] == 0x52 && file_buffer[3] == 0x4D) {
        if (file_buffer[8] == 0x41 && file_buffer[9] == 0x49 && file_buffer[10] == 0x46 && file_buffer[11] == 0x46) {
            return 1;
        }
    }
    return 0;
}

/* ---------------------------------------------------------------------- */

static int aifc_handle_ssnd(int channels)
{
    uint32_t size;
    int i;

    file_pointer += 4;

    size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
    file_pointer += 4;
    if (file_pointer + size > file_size) {
        log_error(filedrv_log, "SSND chunk bigger than remaining size : %X %X", (unsigned int)size, file_size - file_pointer);
        return -1;
    }

    for (i = 0; i < 8; ++i) {
        if (file_buffer[file_pointer + i]) {
            log_error(filedrv_log, "SSND secondary parameters not 0");
            return -1;
        }
    }

    file_pointer += 8;
    size -= 8;

    switch (sound_audio_type) {
        case AUDIO_TYPE_PCM:
            return convert_pcm_buffer(size, channels);
        case AUDIO_TYPE_FLOAT_BE:
            switch (sound_audio_bits) {
                case 32:
                    return convert_float_buffer(size, channels);
                case 64:
                    return convert_double_buffer(size, channels);
                default:
                    log_error(filedrv_log, "Unhandled float format : %u",
                            sound_audio_bits);
                    return -1;
            }
        case AUDIO_TYPE_ALAW:
            return convert_alaw_buffer(size, channels);
        case AUDIO_TYPE_ULAW:
            return convert_ulaw_buffer(size, channels);
        default:
            log_error(filedrv_log, "unhandled audio type");
            return -1;
    }
    return -1;
}

static int aifc_handle_comm(void)
{
    uint32_t size;
    uint32_t type;
    double f64;
    unsigned char f80[10];
    int i;

    file_pointer += 4;

    size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
    file_pointer += 4;
    if (size + file_pointer > file_size) {
        log_error(filedrv_log, "Aifc file too small");
        return -1;
    }

    sound_audio_channels = (file_buffer[file_pointer] << 8) | file_buffer[file_pointer + 1];
    if (sound_audio_channels < 1 || sound_audio_channels > 2) {
        log_error(filedrv_log, "COMM channels not 1 or 2 : %u",
                sound_audio_channels);
        return -1;
    }

    file_pointer += 6;
    size -= 6;

    sound_audio_bits = (file_buffer[file_pointer] << 8) | file_buffer[file_pointer + 1];
    switch (sound_audio_bits) {
        case 8:
        case 16:
        case 24:
        case 32:
        case 64:
            break;
        default:
            log_error(filedrv_log, "COMM bits not 8, 16, 24, 32 or 64 : %u",
                    sound_audio_bits);
            return -1;
    }

    file_pointer += 2;
    size -= 2;

    for (i = 0; i < 10; ++i) {
        f80[i] = file_buffer[file_pointer + i];  
    }

    f64 = float80tofloat64(f80);
    sound_audio_rate = (unsigned int)f64;
    if (!sound_audio_rate) {
        log_error(filedrv_log, "COMM audio rate is 0");
        return -1;
    }

    file_pointer += 10;
    size -= 10;

    type = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

    switch (type) {
        case 0x616C6177:
            sound_audio_type = AUDIO_TYPE_ALAW;
            break;
        case 0x464C3332:
        case 0x464C3634:
            sound_audio_type = AUDIO_TYPE_FLOAT_BE;
            break;
        case 0x72617720:
            sound_audio_type = AUDIO_TYPE_PCM;
            break;
        case 0x756C6177:
            sound_audio_type = AUDIO_TYPE_ULAW;
            break;
        default:
            log_error(filedrv_log, "Unknown aifc audio type");
            return -1;
    }
    file_pointer += size;

    return 0;
}

static int handle_aifc_file(int channels)
{
    uint32_t size;
    uint32_t header;
    int ssnd_found = 0;
    int err = 0;

    size = (file_buffer[4] << 24) | (file_buffer[5] << 16) | (file_buffer[6] << 8) | file_buffer[7];

    if (size != file_size - 8) {
        log_error(filedrv_log, "AIFC size is wrong : %X %X", (unsigned int)size, file_size - 8);
        return -1;
    }

    file_pointer = 12;

    while (file_pointer <= file_size && !ssnd_found) {
        if (file_pointer + 8 > file_size) {
            log_error(filedrv_log, "Aifc file too small");
            return -1;
        }

        header = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];

        switch (header) {
            case 0x53534E44:
#ifdef DEBUG_FILEDRV
                log_warning(filedrv_log, "handling AIFC SSND chunk");
#endif
                err = aifc_handle_ssnd(channels);
                ssnd_found = 1;
                break;
            case 0x434F4D4D:
#ifdef DEBUG_FILEDRV
                log_warning(filedrv_log, "handling AIFC COMM chunk");
#endif
                err = aifc_handle_comm();
                break;
            default:
                file_pointer += 4;
                size = (file_buffer[file_pointer] << 24) | (file_buffer[file_pointer + 1] << 16) | (file_buffer[file_pointer + 2] << 8) | file_buffer[file_pointer + 3];
                if (size % 2) {
                    ++size;
                }
                file_pointer += 4;
                if (file_pointer + size > file_size) {
                    log_error(filedrv_log, "Aifc file too small");
                    return -1;
                }
                file_pointer += size;
        }
        if (err) {
            return -1;
        }
    }
    if (!ssnd_found) {
        return -1;
    }
    return 0;
}

static int is_aifc_file(void)
{
    if (file_size < 12) {
        return 0;
    }

    /* Check for aifc header signature */
    if (file_buffer[0] == 0x46 && file_buffer[1] == 0x4F && file_buffer[2] == 0x52 && file_buffer[3] == 0x4D) {
        if (file_buffer[8] == 0x41 && file_buffer[9] == 0x49 && file_buffer[10] == 0x46 && file_buffer[11] == 0x43) {
            return 1;
        }
    }
    return 0;
}

/* ---------------------------------------------------------------------- */

#ifdef USE_MPG123
static int mp3_err = MPG123_OK;
static mpg123_handle *mh = NULL;

static int handle_mp3_file(int channels)
{
    int mp3_channels = 0;
    int mp3_encoding = 0;
    long mp3_rate = 0;
    off_t buffer_size = 0;
    size_t done = 0;

    mp3_err = mpg123_getformat(mh, &mp3_rate, &mp3_channels, &mp3_encoding);
    if (mp3_err != MPG123_OK) {
        mpg123_close(mh);
        mpg123_delete(mh);
        mpg123_exit();
        log_error(filedrv_log, "Unknown mp3 format");
        return -1;
    }

    /* Should not happen, but just in case */
    if (mp3_encoding != MPG123_ENC_SIGNED_16) {
        mpg123_close(mh);
        mpg123_delete(mh);
        mpg123_exit();
        log_error(filedrv_log, "Mp3 encoding is not signed 16bit");
        return -1;
    }

    /* lock format */
    mpg123_format_none(mh);
    mpg123_format(mh, mp3_rate, mp3_channels, mp3_encoding);

    mpg123_scan(mh);
    buffer_size = mpg123_length(mh);

    lib_free(file_buffer);
    file_size = buffer_size * 2 * mp3_channels;
    file_buffer = lib_malloc(file_size);

    mp3_err = mpg123_read(mh, file_buffer, file_size, &done);

    mpg123_close(mh);
    mpg123_delete(mh);
    mpg123_exit();

    file_pointer = 0;

    sound_audio_type = AUDIO_TYPE_PCM;
    sound_audio_channels = mp3_channels;
    sound_audio_rate = mp3_rate;
    sound_audio_bits = 16;

    return convert_pcm_buffer(file_size, channels);
}

static int is_mp3_file(void)
{
    mp3_err = mpg123_init();
    if (mp3_err != MPG123_OK) {
        return 0;
    }
    mh = mpg123_new(NULL, &mp3_err);
    if (!mh) {
        mpg123_exit();
        return 0;
    }
    mp3_err = mpg123_open(mh, sample_name);
    if (mp3_err != MPG123_OK) {
        mpg123_delete(mh);
        mpg123_exit();
        return 0;
    }
    return 1;
}
#endif

/* ---------------------------------------------------------------------- */

#ifdef USE_FLAC
static FLAC__uint64 flac_total_samples = 0;
static unsigned int flac_sample_rate = 0;
static unsigned int flac_channels = 0;
static unsigned int flac_bps = 0;

static FLAC__uint32 flac_total_size = 0;
static uint8_t *flac_buffer = NULL;
static unsigned int flac_size = 0;

static void flac_buffer_add(FLAC__uint32 raw)
{
    uint16_t sample = 0;

    switch (flac_bps) {
        case 8:
            sample = (uint16_t)((raw & 0xFF) << 8);
            break;
        case 16:
            sample = (uint16_t)(raw & 0xFFFF);
            break;
        case 24:
            sample = (uint16_t)((raw & 0xFFFF00) >> 8);
            break;
        case 32:
            sample = (uint16_t)((raw & 0xFFFF0000) >> 16);
            break;
    }
    if (flac_size + 2 > flac_total_size) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "flac buffer overflow");
#endif
    } else {
        flac_buffer[flac_size + 1] = (uint8_t)(sample >> 8);
        flac_buffer[flac_size] = (uint8_t)(sample & 0xFF);
        flac_size += 2;
    }
}

static FLAC__StreamDecoderWriteStatus flac_write_callback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data)
{
    size_t i;

    if(frame->header.number.sample_number == 0) {
        flac_total_size = (FLAC__uint32)(flac_total_samples * flac_channels * (flac_bps / 8));

        if (!flac_total_samples) {
            return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }

        if (flac_channels < 1 || flac_channels > 2) {
            return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }

        switch (flac_bps) {
            case 8:
            case 16:
            case 24:
            case 32:
                break;
            default:
                return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }

        if (frame->header.channels != flac_channels) {
            return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }

        flac_buffer = lib_malloc(flac_total_size);
        flac_size = 0;
    }

    if (buffer[0] == NULL) {
        return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
    }

    if (flac_channels == 2) {
        if (buffer [1] == NULL) {
            return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
        }
    }

    for (i = 0; i < frame->header.blocksize; ++i) {
        flac_buffer_add(buffer[0][i]);
        if (flac_channels == 2) {
            flac_buffer_add(buffer[1][i]);
        }
    }

    return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}

static void flac_metadata_callback(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data)
{
    if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
        flac_total_samples = metadata->data.stream_info.total_samples;
        flac_sample_rate = metadata->data.stream_info.sample_rate;
        flac_channels = metadata->data.stream_info.channels;
        flac_bps = metadata->data.stream_info.bits_per_sample;
    }
}

static void flac_error_callback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data)
{
}

static int handle_flac_file(int channels)
{
    FLAC__bool ok = true;
    FLAC__StreamDecoder *decoder = NULL;
    FLAC__StreamDecoderInitStatus init_status;

    decoder = FLAC__stream_decoder_new();
    if (!decoder) {
        log_error(filedrv_log, "Cannot init flac decoder");
        return -1;
    }

    (void)FLAC__stream_decoder_set_md5_checking(decoder, true);

    init_status = FLAC__stream_decoder_init_file(decoder, sample_name, flac_write_callback, flac_metadata_callback, flac_error_callback, NULL);
    if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
        FLAC__stream_decoder_delete(decoder);
        log_error(filedrv_log, "Cannot init flac decoder");
        return -1;
    }

    ok = FLAC__stream_decoder_process_until_end_of_stream(decoder);
    FLAC__stream_decoder_delete(decoder);

    if (!ok) {
        if (flac_buffer) {
            lib_free(flac_buffer);
            flac_buffer = NULL;
        }
        log_error(filedrv_log, "Error during flac stream decode");
        return -1;
    }

    lib_free(file_buffer);
    file_buffer = flac_buffer;
    flac_buffer = NULL;
    file_size = flac_total_size;
    file_pointer = 0;
    sound_audio_type = AUDIO_TYPE_PCM;
    sound_audio_channels = flac_channels;
    sound_audio_rate = flac_sample_rate;
    sound_audio_bits = 16;
    
    return convert_pcm_buffer(file_size, channels);
}

static int is_flac_file(void)
{
    if (file_size < 4) {
        return 0;
    }
    if (file_buffer[0] == 0x66 && file_buffer[1] == 0x4C && file_buffer[2] == 0x61 && file_buffer[3] == 0x43) {
        return 1;
    }
    return 0;
}
#endif

/* ---------------------------------------------------------------------- */

#ifdef USE_VORBIS
static int handle_vorbis_file(int channels)
{
    OggVorbis_File ov;
    int i;
    ogg_int64_t pcmlength;
    uint8_t *vorbis_buffer;
    int dummy;
    int error;
    vorbis_info *vi;

    error = ov_fopen(sample_name, &ov);
    if (error < 0) {
        log_error(filedrv_log, "Unable to open ogg/vorbis file");
        return -1;
    }

    if (!ov_seekable(&ov)) {
        ov_clear(&ov);
        log_error(filedrv_log, "The ogg/vorbis file is not seekable");
        return -1;
    }

    for (i = 0; i < ov.links; i++) {
        vi = ov_info(&ov, i);
        sound_audio_channels = vi->channels;
        if (sound_audio_channels < 1 || sound_audio_channels > 2) {
            ov_clear(&ov);
            log_error(filedrv_log, "The ogg/vorbis file channels is not 1 or 2");
            return -1;
        }
        sound_audio_rate = vi->rate;
        sound_audio_bits = 16;
        sound_audio_type = AUDIO_TYPE_PCM;
    }

    pcmlength = ov_pcm_total(&ov, -1);
    vorbis_buffer = lib_malloc(pcmlength * sound_audio_channels * 2);
    i = 0;
    while (i < pcmlength * sound_audio_channels * 2){
        int ret = ov_read(&ov, (char*)vorbis_buffer + i, (pcmlength * 2 * sound_audio_channels) - i, 0, 2, 1, &dummy);
        if (ret < 0) {
            ov_clear(&ov);
            lib_free(vorbis_buffer);
            vorbis_buffer = NULL;
            log_error(filedrv_log, "Error reading ogg/vorbis stream");
            return -1;
        }
        if (ret) {
            i += ret;
        } else {
            pcmlength = i / (2 * sound_audio_channels);
        }
    }

    ov_clear(&ov);

    lib_free(file_buffer);
    file_buffer = vorbis_buffer;
    file_size = pcmlength * 2 * sound_audio_channels;
    file_pointer = 0;

    return convert_pcm_buffer(file_size, channels);
}

static int is_vorbis_file(void)
{
    if (file_size < 36) {
        return 0;
    }
    if (file_buffer[0] == 0x4F && file_buffer[1] == 0x67 && file_buffer[2] == 0x67 && file_buffer[3] == 0x53) {
        if (file_buffer[29] == 0x76 && file_buffer[30] == 0x6F && file_buffer[31] == 0x72) {
            if (file_buffer[32] == 0x62 && file_buffer[33] == 0x69 && file_buffer[34] == 0x73) {
                return 1;
            }
        }
    }
    return 0;
}
#endif

/* ---------------------------------------------------------------------- */

static int handle_file_type(int channels)
{
    /* Check for wav file */
    if (is_wav_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as a WAVE file, starting parsing.");
#endif
        return handle_wav_file(channels);
    }

    /* Check for voc file */
    if (is_voc_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as a VOC file, starting parsing.");
#endif
        return handle_voc_file(channels);
    }

    /* Check for iff file */
    if (is_iff_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as an IFF file, starting parsing.");
#endif
        return handle_iff_file(channels);
    }

    /* Check for aiff file */
    if (is_aiff_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as an AIFF file, starting parsing.");
#endif
        return handle_aiff_file(channels);
    }

    /* Check for aiff file */
    if (is_aifc_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as an AIFC file, starting parsing.");
#endif
        return handle_aifc_file(channels);
    }

#ifdef USE_FLAC
    /* Check for flac file */
    if (is_flac_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as a FLAC file, starting parsing.");
#endif
        return handle_flac_file(channels);
    }
#endif

#ifdef USE_VORBIS
    /* Check for ogg/vorbis file */
    if (is_vorbis_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as an ogg/vorbis file, starting parsing.");
#endif
        return handle_vorbis_file(channels);
    }
#endif

#ifdef USE_MPG123
    /* Check for mp3 file */
    if (is_mp3_file()) {
#ifdef DEBUG_FILEDRV
        log_warning(filedrv_log, "filetype recognized as an MP3 file, starting parsing.");
#endif
        return handle_mp3_file(channels);
    }
#endif

    log_error(filedrv_log, "filetype was not handled.");
    return -1;
}

/* ---------------------------------------------------------------------- */

static void file_load_sample(int channels)
{
    FILE *sample_file = NULL;
    int err = 0;

    sample_file = fopen(sample_name, "rb");
    if (sample_file) {
        fseek(sample_file, 0, SEEK_END);
        file_size = ftell(sample_file);
        fseek(sample_file, 0, SEEK_SET);
        file_buffer = lib_malloc(file_size);
        if (fread(file_buffer, 1, file_size, sample_file) != file_size) {
            log_warning(filedrv_log, "Unexpected end of data in '%s'.", sample_name);
        }
        fclose(sample_file);
        err = handle_file_type(channels);
        if (!err) {
            sound_sampling_started = 0;
            sound_cycles_per_frame = machine_get_cycles_per_frame();
            sound_frames_per_sec = machine_get_cycles_per_second() / sound_cycles_per_frame;
            sound_samples_per_frame = sound_audio_rate / sound_frames_per_sec;
            current_channels = channels;
        } else {
            lib_free(file_buffer);
            file_buffer = NULL;
            log_error(filedrv_log, "Unknown file type for '%s'.", sample_name);
        }
    } else {
        log_error(filedrv_log, "Cannot open sampler file: '%s'.", sample_name);
    }
}

static void file_free_sample(void)
{
    if (sample_buffer1) {
        if (sample_buffer2) {
            if (sample_buffer1 != sample_buffer2) {
                lib_free(sample_buffer2);
            }
            sample_buffer2 = NULL;
        }
        lib_free(sample_buffer1);
        sample_buffer1 = NULL;
    }
    sound_sampling_started = 0;
}

/* ---------------------------------------------------------------------- */

static int set_sample_name(const char *name, void *param)
{
    if (sample_name != NULL && name != NULL && strcmp(name, sample_name) == 0) {
        return 0;
    }

    if (name != NULL && *name != '\0') {
        if (util_check_filename_access(name) < 0) {
            return -1;
        }
    }

    if (sample_buffer1) {
        file_free_sample();
        util_string_set(&sample_name, name);
        file_load_sample(current_channels);
    } else {
        util_string_set(&sample_name, name);
    }

    return 0;
}

static const resource_string_t resources_string[] = {
    { "SampleName", "", RES_EVENT_NO, NULL,
      &sample_name, set_sample_name, NULL },
    RESOURCE_STRING_LIST_END
};

static int sampler_file_resources_init(void)
{
    return resources_register_string(resources_string);
}


static void sampler_file_resources_shutdown(void)
{
    if (sample_name != NULL) {
        lib_free(sample_name);
    }
}


/* ---------------------------------------------------------------------- */

static const cmdline_option_t cmdline_options[] =
{
    { "-samplename", SET_RESOURCE, CMDLINE_ATTRIB_NEED_ARGS,
      NULL, NULL, "SampleName", NULL,
      "<Name>", "Specify name of sample file" },
    CMDLINE_LIST_END
};

static int sampler_file_cmdline_options_init(void)
{
    return cmdline_register_options(cmdline_options);
}

/* ---------------------------------------------------------------------- */

/* For now channel is ignored */
static uint8_t file_get_sample(int channel)
{
    unsigned int current_frame = 0;
    unsigned int current_cycle = 0;
    unsigned int frame_diff = 0;
    unsigned int frame_sample = 0;

    if (!sample_buffer1) {
        return 0x80;
    }
    if (!sound_sampling_started) {
        sound_sampling_started = 1;
        old_frame = maincpu_clk / sound_cycles_per_frame;
        return sample_buffer1[0];
    }
    current_frame = maincpu_clk / sound_cycles_per_frame;
    current_cycle = maincpu_clk % sound_cycles_per_frame;

    if (current_frame > old_frame) {
        frame_diff = current_frame - old_frame;
        while (frame_diff) {
            --frame_diff;
            ++old_frame;
            sound_sample_frame_start += sound_samples_per_frame;
            if (sound_sample_frame_start >= sample_size) {
                sound_sample_frame_start -= sample_size;
            }
        }
    }
    frame_sample = current_cycle * sound_samples_per_frame / sound_cycles_per_frame;

    return sample_buffer1[(frame_sample + sound_sample_frame_start) % sample_size];
}

static void file_shutdown(void)
{
    if (sample_buffer1) {
        file_free_sample();
    }
}

static void file_reset(void)
{
    if (sample_buffer1) {
        sound_sampling_started = 0;
    }
}

static sampler_device_t file_device =
{
    "media file input",
    file_load_sample,
    file_free_sample,
    file_get_sample,
    file_shutdown,
    sampler_file_resources_init,
    sampler_file_cmdline_options_init,
    file_reset
};

void fileaudio_init(void)
{
    filedrv_log = log_open("Sampler Filedrv");

    sampler_device_register(&file_device, SAMPLER_DEVICE_FILE);
}


void fileaudio_shutdown(void)
{
    sampler_file_resources_shutdown();
}


