/* 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 Library General Public License
 * as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA
 */

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

#include <stdio.h>
#include "fluidsynth_priv.h"
#include "fluid_synth.h"
#include "fluid_sys.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[] */
const char *format_names[] = {
  "s8",
  "s16",
  "s24",
  "s32",
  "u8",
  "float",
  "double",
  NULL          /* Terminator */
};
  

/* File audio format IDs.
 * !! Keep in sync with format_names[] */
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[] */
const char *endian_names[] = {
  "auto",
  "little",
  "big",
  "cpu",
  NULL
};

/* File endian byte order ids.
 * !! Keep in sync with endian_names[] */
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);


#else   /* No libsndfile support */


/* File type names. */
const char *type_names[] = {
  "raw",
  NULL          /* Terminator */
};

/* File audio format names. */
const char *format_names[] = {
  "s16",
  NULL          /* Terminator */
};

/* File endian byte order names. */
const char *endian_names[] = {
  "cpu",
  NULL
};

#endif


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

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

  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 (np = format_names; *np; np++)
    fluid_settings_add_option (settings, "audio.file.format", *np);

  for (np = endian_names; *np; np++)
    fluid_settings_add_option (settings, "audio.file.endian", *np);

#else

  fluid_settings_register_str(settings, "audio.file.name", "fluidsynth.raw", 0, NULL, NULL);
  fluid_settings_register_str(settings, "audio.file.type", "raw", 0, NULL, NULL);
  fluid_settings_add_option (settings, "audio.file.type", "raw");
  fluid_settings_register_str(settings, "audio.file.format", "s16", 0, NULL, NULL);
  fluid_settings_add_option (settings, "audio.file.format", "s16");
  fluid_settings_register_str(settings, "audio.file.endian", "cpu", 0, NULL, NULL);
  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 = fopen(filename, "wb");
	if (dev->file == NULL) {
		FLUID_LOG(FLUID_ERR, "Failed to open the file '%s'", filename);
		goto error_recovery;
	}
#endif

	return dev;

 error_recovery:
	if (filename) FLUID_FREE (filename);
	delete_fluid_file_renderer(dev);
	return NULL;
}

/**
 * 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)
{
	if (dev == NULL) {
		return;
	}

#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

	if (dev->buf != NULL) {
		FLUID_FREE(dev->buf);
	}

	FLUID_FREE(dev);
	return;
}

/**
 * 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 */

	int n, offset;

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

	for (offset = 0; offset < dev->buf_size; offset += n) {

		n = fwrite((char*) dev->buf + offset, 1, dev->buf_size - offset, dev->file);
		if (n < 0) {
			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;
  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; format_names[i]; i++)
      if (FLUID_STRCMP (format, format_names[i]) == 0)
        break;

    if (!format_names[i])
    {
      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; endian_names[i]; i++)
      if (FLUID_STRCMP (endian, endian_names[i]) == 0)
        break;

    if (!endian_names[i])
    {
      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 ext_len Length of 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
