/* FluidSynth - A Software Synthesizer
 *
 * Copyright (C) 2003  Peter Hanappe and others.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA
 */

/*
 * Low-level routines for file output.
 */

#include "fluid_sys.h"
#include "fluid_synth.h"
#include "fluid_settings.h"

#if LIBSNDFILE_SUPPORT
#include <sndfile.h>
#endif

struct _fluid_file_renderer_t
{
    fluid_synth_t *synth;

#if LIBSNDFILE_SUPPORT
    SNDFILE *sndfile;
    float *buf;
#else
    FILE *file;
    short *buf;
#endif

    int period_size;
    int buf_size;
};

#if LIBSNDFILE_SUPPORT

/* Default file type used, if none specified and auto extension search fails */
#define FLUID_FILE_RENDERER_DEFAULT_FILE_TYPE   SF_FORMAT_WAV

/* File audio format names.
 * !! Keep in sync with format_ids[] */
static const char *const format_names[] =
{
    "s8",
    "s16",
    "s24",
    "s32",
    "u8",
    "float",
    "double"
};


/* File audio format IDs.
 * !! Keep in sync with format_names[] */
static const int format_ids[] =
{
    SF_FORMAT_PCM_S8,
    SF_FORMAT_PCM_16,
    SF_FORMAT_PCM_24,
    SF_FORMAT_PCM_32,
    SF_FORMAT_PCM_U8,
    SF_FORMAT_FLOAT,
    SF_FORMAT_DOUBLE
};

/* File endian byte order names.
 * !! Keep in sync with endian_ids[] */
static const char *const endian_names[] =
{
    "auto",
    "little",
    "big",
    "cpu"
};

/* File endian byte order ids.
 * !! Keep in sync with endian_names[] */
static const int endian_ids[] =
{
    SF_ENDIAN_FILE,
    SF_ENDIAN_LITTLE,
    SF_ENDIAN_BIG,
    SF_ENDIAN_CPU
};

static int fluid_file_renderer_parse_options(char *filetype, char *format,
        char *endian, char *filename, SF_INFO *info);
static int fluid_file_renderer_find_file_type(char *extension, int *type);
static int fluid_file_renderer_find_valid_format(SF_INFO *info);

#endif


void
fluid_file_renderer_settings(fluid_settings_t *settings)
{
#if LIBSNDFILE_SUPPORT
    SF_FORMAT_INFO finfo, cmpinfo;
    int major_count;
    int i, i2;
    unsigned int n;

    fluid_settings_register_str(settings, "audio.file.name", "fluidsynth.wav", 0);
    fluid_settings_register_str(settings, "audio.file.type", "auto", 0);
    fluid_settings_register_str(settings, "audio.file.format", "s16", 0);
    fluid_settings_register_str(settings, "audio.file.endian", "auto", 0);

    fluid_settings_add_option(settings, "audio.file.type", "auto");

    sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_count, sizeof(int));

    for(i = 0; i < major_count; i++)
    {
        finfo.format = i;
        sf_command(NULL, SFC_GET_FORMAT_MAJOR, &finfo, sizeof(finfo));

        /* Check for duplicates */
        for(i2 = 0; i2 < i; i2++)
        {
            cmpinfo.format = i2;
            sf_command(NULL, SFC_GET_FORMAT_MAJOR, &cmpinfo, sizeof(cmpinfo));

            if(FLUID_STRCMP(cmpinfo.extension, finfo.extension) == 0)
            {
                break;
            }
        }

        if(i2 == i)
        {
            fluid_settings_add_option(settings, "audio.file.type", finfo.extension);
        }
    }

    for(n = 0; n < FLUID_N_ELEMENTS(format_names); n++)
    {
        fluid_settings_add_option(settings, "audio.file.format", format_names[n]);
    }

    for(n = 0; n < FLUID_N_ELEMENTS(endian_names); n++)
    {
        fluid_settings_add_option(settings, "audio.file.endian", endian_names[n]);
    }

#else

    fluid_settings_register_str(settings, "audio.file.name", "fluidsynth.raw", 0);
    fluid_settings_register_str(settings, "audio.file.type", "raw", 0);
    fluid_settings_add_option(settings, "audio.file.type", "raw");
    fluid_settings_register_str(settings, "audio.file.format", "s16", 0);
    fluid_settings_add_option(settings, "audio.file.format", "s16");
    fluid_settings_register_str(settings, "audio.file.endian", "cpu", 0);
    fluid_settings_add_option(settings, "audio.file.endian", "cpu");
#endif
}

/**
 * Create a new file renderer and open the file.
 * @param synth The synth that creates audio data.
 * @return the new object, or NULL on failure
 * @since 1.1.0
 *
 * NOTE: Available file types and formats depends on if libfluidsynth was
 * built with libsndfile support or not.  If not then only RAW 16 bit output is
 * supported.
 *
 * Uses the following settings from the synth object:
 *   - audio.file.name: Output filename
 *   - audio.file.type: File type, "auto" tries to determine type from filename
 *     extension with fallback to "wav".
 *   - audio.file.format: Audio format
 *   - audio.file.endian: Endian byte order, "auto" for file type's default byte order
 *   - audio.period-size: Size of audio blocks to process
 *   - synth.sample-rate: Sample rate to use
 */
fluid_file_renderer_t *
new_fluid_file_renderer(fluid_synth_t *synth)
{
#if LIBSNDFILE_SUPPORT
    char *type, *format, *endian;
    SF_INFO info;
    double samplerate;
    int retval;
#endif
    char *filename = NULL;
    fluid_file_renderer_t *dev;

    fluid_return_val_if_fail(synth != NULL, NULL);
    fluid_return_val_if_fail(synth->settings != NULL, NULL);

    dev = FLUID_NEW(fluid_file_renderer_t);

    if(dev == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        return NULL;
    }

    FLUID_MEMSET(dev, 0, sizeof(fluid_file_renderer_t));

    dev->synth = synth;
    fluid_settings_getint(synth->settings, "audio.period-size", &dev->period_size);

#if LIBSNDFILE_SUPPORT
    dev->buf_size = 2 * dev->period_size * sizeof(float);
    dev->buf = FLUID_ARRAY(float, 2 * dev->period_size);
#else
    dev->buf_size = 2 * dev->period_size * sizeof(short);
    dev->buf = FLUID_ARRAY(short, 2 * dev->period_size);
#endif

    if(dev->buf == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Out of memory");
        goto error_recovery;
    }

    fluid_settings_dupstr(synth->settings, "audio.file.name", &filename);

    if(filename == NULL)
    {
        FLUID_LOG(FLUID_ERR, "No file name specified");
        goto error_recovery;
    }

#if LIBSNDFILE_SUPPORT
    memset(&info, 0, sizeof(info));

    info.format = FLUID_FILE_RENDERER_DEFAULT_FILE_TYPE | SF_FORMAT_PCM_16;

    fluid_settings_dupstr(synth->settings, "audio.file.type", &type);
    fluid_settings_dupstr(synth->settings, "audio.file.format", &format);
    fluid_settings_dupstr(synth->settings, "audio.file.endian", &endian);

    retval = fluid_file_renderer_parse_options(type, format, endian, filename, &info);

    if(type)
    {
        FLUID_FREE(type);
    }

    if(format)
    {
        FLUID_FREE(format);
    }

    if(endian)
    {
        FLUID_FREE(endian);
    }

    if(!retval)
    {
        goto error_recovery;
    }

    fluid_settings_getnum(synth->settings, "synth.sample-rate", &samplerate);
    info.samplerate = samplerate + 0.5;
    info.channels = 2;

    /* Search for valid format for given file type, if invalid and no format was specified.
     * To handle Ogg/Vorbis and possibly future file types with new formats.
     * Checking if format is SF_FORMAT_PCM_16 isn't a fool proof way to check if
     * format was specified or not (if user specifies "s16" itself), but should suffice. */
    if(!sf_format_check(&info)
            && ((info.format & SF_FORMAT_SUBMASK) != SF_FORMAT_PCM_16
                || !fluid_file_renderer_find_valid_format(&info)))
    {
        FLUID_LOG(FLUID_ERR, "Invalid or unsupported audio file format settings");
        goto error_recovery;
    }

    dev->sndfile = sf_open(filename, SFM_WRITE, &info);

    if(!dev->sndfile)
    {
        FLUID_LOG(FLUID_ERR, "Failed to open audio file '%s' for writing", filename);
        goto error_recovery;
    }

    /* Turn on clipping and normalization of floats (-1.0 - 1.0) */
    sf_command(dev->sndfile, SFC_SET_CLIPPING, NULL, SF_TRUE);
    sf_command(dev->sndfile, SFC_SET_NORM_FLOAT, NULL, SF_TRUE);

#else
    dev->file = FLUID_FOPEN(filename, "wb");

    if(dev->file == NULL)
    {
        FLUID_LOG(FLUID_ERR, "Failed to open the file '%s'", filename);
        goto error_recovery;
    }

#endif

    FLUID_FREE(filename);
    return dev;

error_recovery:

    FLUID_FREE(filename);
    delete_fluid_file_renderer(dev);
    return NULL;
}

/**
 * Set vbr encoding quality (only available with libsndfile support)
 * @param dev File renderer object.
 * @param q The encoding quality, see libsndfile documentation of \c SFC_SET_VBR_ENCODING_QUALITY
 * @return #FLUID_OK if the quality has been successfully set, #FLUID_FAILED otherwise
 * @since 1.1.7
 */
int
fluid_file_set_encoding_quality(fluid_file_renderer_t *dev, double q)
{
#if LIBSNDFILE_SUPPORT

    if(sf_command(dev->sndfile, SFC_SET_VBR_ENCODING_QUALITY, &q, sizeof(double)) == SF_TRUE)
    {
        return FLUID_OK;
    }
    else
#endif
    {
        return FLUID_FAILED;
    }
}

/**
 * Close file and destroy a file renderer object.
 * @param dev File renderer object.
 * @since 1.1.0
 */
void delete_fluid_file_renderer(fluid_file_renderer_t *dev)
{
    fluid_return_if_fail(dev != NULL);

#if LIBSNDFILE_SUPPORT

    if(dev->sndfile != NULL)
    {
        int retval = sf_close(dev->sndfile);

        if(retval != 0)
        {
            FLUID_LOG(FLUID_WARN, "Error closing audio file: %s", sf_error_number(retval));
        }
    }

#else

    if(dev->file != NULL)
    {
        fclose(dev->file);
    }

#endif

    FLUID_FREE(dev->buf);
    FLUID_FREE(dev);
}

/**
 * Write period_size samples to file.
 * @param dev File renderer instance
 * @return #FLUID_OK or #FLUID_FAILED if an error occurred
 * @since 1.1.0
 */
int
fluid_file_renderer_process_block(fluid_file_renderer_t *dev)
{
#if LIBSNDFILE_SUPPORT
    int n;

    fluid_synth_write_float(dev->synth, dev->period_size, dev->buf, 0, 2, dev->buf, 1, 2);

    n = sf_writef_float(dev->sndfile, dev->buf, dev->period_size);

    if(n != dev->period_size)
    {
        FLUID_LOG(FLUID_ERR, "Audio file write error: %s",
                  sf_strerror(dev->sndfile));
        return FLUID_FAILED;
    }

    return FLUID_OK;

#else   /* No libsndfile support */

    size_t res, nmemb = dev->buf_size;

    fluid_synth_write_s16(dev->synth, dev->period_size, dev->buf, 0, 2, dev->buf, 1, 2);

    res = fwrite(dev->buf, 1, nmemb, dev->file);

    if(res < nmemb)
    {
        FLUID_LOG(FLUID_ERR, "Audio output file write error: %s",
                  strerror(errno));
        return FLUID_FAILED;
    }

    return FLUID_OK;
#endif
}


#if LIBSNDFILE_SUPPORT

/**
 * Parse a colon separated format string and configure an SF_INFO structure accordingly.
 * @param filetype File type string (NULL or "auto" to attempt to identify format
 *   by filename extension, with fallback to "wav")
 * @param format File audio format string or NULL to use "s16"
 * @param endian File endian string or NULL to use "auto" which uses the file type's
 *   default endian byte order.
 * @param filename File name (used by "auto" type to determine type, based on extension)
 * @param info Audio file info structure to configure
 * @return TRUE on success, FALSE otherwise
 */
static int
fluid_file_renderer_parse_options(char *filetype, char *format, char *endian,
                                  char *filename, SF_INFO *info)
{
    int type = -1;        /* -1 indicates "auto" type */
    char *s;
    unsigned int i;

    /* If "auto" type, then use extension to search for a match */
    if(!filetype || FLUID_STRCMP(filetype, "auto") == 0)
    {
        type = FLUID_FILE_RENDERER_DEFAULT_FILE_TYPE;
        s = FLUID_STRRCHR(filename, '.');

        if(s && s[1] != '\0')
        {
            if(!fluid_file_renderer_find_file_type(s + 1, &type))
            {
                FLUID_LOG(FLUID_WARN, "Failed to determine audio file type from filename, defaulting to WAV");
            }
        }
    }
    else if(!fluid_file_renderer_find_file_type(filetype, &type))
    {
        FLUID_LOG(FLUID_ERR, "Invalid or unsupported audio file type '%s'", filetype);
        return FALSE;
    }


    info->format = (info->format & ~SF_FORMAT_TYPEMASK) | type;

    /* Look for subtype */
    if(format)
    {
        for(i = 0; i < FLUID_N_ELEMENTS(format_names); i++)
        {
            if(FLUID_STRCMP(format, format_names[i]) == 0)
            {
                break;
            }
        }

        if(i >= FLUID_N_ELEMENTS(format_names))
        {
            FLUID_LOG(FLUID_ERR, "Invalid or unsupported file audio format '%s'", format);
            return FALSE;
        }

        info->format = (info->format & ~SF_FORMAT_SUBMASK) | format_ids[i];
    }

#if LIBSNDFILE_HASVORBIS

    /* Force subformat to vorbis as nothing else would make sense currently */
    if((info->format & SF_FORMAT_TYPEMASK) == SF_FORMAT_OGG)
    {
        info->format = (info->format & ~SF_FORMAT_SUBMASK) | SF_FORMAT_VORBIS;
    }

#endif

    /* Look for endian */
    if(endian)
    {
        for(i = 0; i < FLUID_N_ELEMENTS(endian_names); i++)
        {
            if(FLUID_STRCMP(endian, endian_names[i]) == 0)
            {
                break;
            }
        }

        if(i >= FLUID_N_ELEMENTS(endian_names))
        {
            FLUID_LOG(FLUID_ERR, "Invalid or unsupported endian byte order '%s'", endian);
            return FALSE;
        }

        info->format = (info->format & ~SF_FORMAT_ENDMASK) | endian_ids[i];
    }

    return TRUE;
}

/**
 * Searches for a supported libsndfile file type by extension.
 * @param extension The extension string
 * @param type Location to store the type (unmodified if not found)
 * @return TRUE if found, FALSE otherwise
 */
static int
fluid_file_renderer_find_file_type(char *extension, int *type)
{
    SF_FORMAT_INFO finfo;
    int major_count;
    int i;

    sf_command(NULL, SFC_GET_FORMAT_MAJOR_COUNT, &major_count, sizeof(int));

    for(i = 0; i < major_count; i++)
    {
        finfo.format = i;
        sf_command(NULL, SFC_GET_FORMAT_MAJOR, &finfo, sizeof(finfo));

        if(FLUID_STRCMP(extension, finfo.extension) == 0)
        {
            break;
        }
    }

    if(i < major_count)
    {
        *type = finfo.format;
        return TRUE;
    }

    return FALSE;
}

/* Search for a valid audio format for a given file type */
static int
fluid_file_renderer_find_valid_format(SF_INFO *info)
{
    SF_FORMAT_INFO format_info;
    int count, i;

    sf_command(NULL, SFC_GET_FORMAT_SUBTYPE_COUNT, &count, sizeof(int));

    for(i = 0; i < count; i++)
    {
        format_info.format = i;

        sf_command(NULL, SFC_GET_FORMAT_SUBTYPE, &format_info, sizeof(format_info));

        info->format = (info->format & ~SF_FORMAT_SUBMASK) | format_info.format;

        if(sf_format_check(info))
        {
            return TRUE;
        }
    }

    return FALSE;
}

#endif
