/* sndio backend for FluidSynth - A Software Synthesizer
 *
 * Copyright (c) 2008 Jacob Meuser <jakemsr@sdf.lonestar.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


/* fluid_sndio.c
 *
 * Driver for the sndio audio access library
 */

#include "fluid_synth.h"
#include "fluid_adriver.h"
#include "fluid_midi.h"
#include "fluid_mdriver.h"
#include "fluid_settings.h"

#if SNDIO_SUPPORT

#include <sndio.h>

#include <sys/time.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>


/** fluid_sndio_audio_driver_t
 *
 * This structure should not be accessed directly. Use audio port
 * functions instead.
 */
typedef struct {
  fluid_audio_driver_t driver;
  fluid_synth_t* synth;
  fluid_audio_callback_t read;
  void* buffer;
  pthread_t thread;
  int cont;
  struct sio_hdl *hdl;
  struct sio_par par;
  int buffer_size;
  int buffer_byte_size;
  fluid_audio_func_t callback;
  void* data;
  float* buffers[2];
} fluid_sndio_audio_driver_t;

typedef struct {
  fluid_midi_driver_t driver;
  struct mio_hdl *hdl;
  pthread_t thread;
  int status;
  fluid_midi_parser_t *parser;
} fluid_sndio_midi_driver_t;

int delete_fluid_sndio_audio_driver(fluid_audio_driver_t* p);

/* local utilities */
static void* fluid_sndio_audio_run(void* d);
static void* fluid_sndio_audio_run2(void* d);


void
fluid_sndio_audio_driver_settings(fluid_settings_t* settings)
{
  fluid_settings_register_str(settings, "audio.sndio.device", "default", 0, NULL, NULL);
}

/*
 * new_fluid_sndio_audio_driver
 */
fluid_audio_driver_t*
new_fluid_sndio_audio_driver(fluid_settings_t* settings, fluid_synth_t* synth)
{
  fluid_sndio_audio_driver_t* dev = NULL;
  double sample_rate;
  int periods, period_size;
  char* devname;
  pthread_attr_t attr;
  int err;

  dev = FLUID_NEW(fluid_sndio_audio_driver_t);
  if (dev == NULL) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    return NULL;
  }
  FLUID_MEMSET(dev, 0, sizeof(fluid_sndio_audio_driver_t));

  fluid_settings_getint(settings, "audio.periods", &periods);
  fluid_settings_getint(settings, "audio.period-size", &period_size);
  fluid_settings_getnum(settings, "synth.sample-rate", &sample_rate);

  dev->hdl = NULL;
  dev->synth = synth;
  dev->callback = NULL;
  dev->data = NULL;
  dev->cont = 1;

  if (!fluid_settings_getstr(settings, "audio.sndio.device", &devname)) {
    devname = NULL;
  }

  dev->hdl = sio_open(devname, SIO_PLAY, 0);
  if (dev->hdl == NULL) {
    FLUID_LOG(FLUID_ERR, "sndio could not be opened for writing");
    goto error_recovery;
  }

  sio_initpar(&dev->par);

  if (fluid_settings_str_equal(settings, "audio.sample-format", "16bits")) {
    dev->par.bits = 16;
    dev->par.le = SIO_LE_NATIVE;
    dev->read = fluid_synth_write_s16;
  } else {
    FLUID_LOG(FLUID_ERR, "Unknown sample format");
    goto error_recovery;
  }

  dev->par.appbufsz = period_size * periods;
  dev->par.round = period_size;

  dev->par.pchan = 2;
  dev->par.rate = sample_rate;

  if (!sio_setpar(dev->hdl, &dev->par)) {
    FLUID_LOG(FLUID_ERR, "Couldn't set sndio audio parameters");
    goto error_recovery;
  }

  if (!sio_getpar(dev->hdl, &dev->par)) {
    FLUID_LOG(FLUID_ERR, "Couldn't get sndio audio parameters");
    goto error_recovery;
  } else if (dev->par.pchan != 2 || dev->par.rate != sample_rate ||
      dev->par.bits != 16) {
    FLUID_LOG(FLUID_ERR, "Couldn't set sndio audio parameters as desired");
    goto error_recovery;
  }

  dev->buffer_size = dev->par.round;
  dev->buffer_byte_size = dev->par.round * dev->par.bps * dev->par.pchan;

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

  if (!sio_start(dev->hdl)) {
    FLUID_LOG(FLUID_ERR, "Couldn't start sndio");
    goto error_recovery;
  }

  if (pthread_attr_init(&attr)) {
    FLUID_LOG(FLUID_ERR, "Couldn't initialize audio thread attributes");
    goto error_recovery;
  }

  err = pthread_create(&dev->thread, &attr, fluid_sndio_audio_run, (void*) dev);
  if (err) {
    FLUID_LOG(FLUID_ERR, "Couldn't create audio thread");
    goto error_recovery;
  }

  return (fluid_audio_driver_t*) dev;

error_recovery:
  delete_fluid_sndio_audio_driver((fluid_audio_driver_t*) dev);
  return NULL;
}

fluid_audio_driver_t*
new_fluid_sndio_audio_driver2(fluid_settings_t* settings, fluid_audio_func_t func, void* data)
{
  fluid_sndio_audio_driver_t* dev = NULL;
  double sample_rate;
  int periods, period_size;
  char* devname;
  pthread_attr_t attr;
  int err;

  dev = FLUID_NEW(fluid_sndio_audio_driver_t);
  if (dev == NULL) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    return NULL;
  }
  FLUID_MEMSET(dev, 0, sizeof(fluid_sndio_audio_driver_t));

  fluid_settings_getint(settings, "audio.periods", &periods);
  fluid_settings_getint(settings, "audio.period-size", &period_size);
  fluid_settings_getnum(settings, "synth.sample-rate", &sample_rate);

  dev->hdl = NULL;
  dev->synth = NULL;
  dev->read = NULL;
  dev->callback = func;
  dev->data = data;
  dev->cont = 1;

  if (!fluid_settings_getstr(settings, "audio.sndio.device", &devname)) {
    devname = NULL;
  }

  dev->hdl = sio_open(devname, SIO_PLAY, 0);
  if (dev->hdl == NULL) {
    FLUID_LOG(FLUID_ERR, "sndio could not be opened for writing");
    goto error_recovery;
  }

  sio_initpar(&dev->par);

  dev->par.appbufsz = period_size * periods;
  dev->par.round = period_size;

  dev->par.bits = 16;
  dev->par.le = SIO_LE_NATIVE;
  dev->par.pchan = 2;
  dev->par.rate = sample_rate;

  if (!sio_setpar(dev->hdl, &dev->par)){
    FLUID_LOG(FLUID_ERR, "Can't configure sndio parameters");
    goto error_recovery;
  }

  if (!sio_getpar(dev->hdl, &dev->par)) {
    FLUID_LOG(FLUID_ERR, "Couldn't get sndio audio parameters");
    goto error_recovery;
  } else if (dev->par.pchan != 2 || dev->par.rate != sample_rate ||
      dev->par.bits != 16) {
    FLUID_LOG(FLUID_ERR, "Couldn't set sndio audio parameters as desired");
    goto error_recovery;
  }

  dev->buffer_size = dev->par.round;
  dev->buffer_byte_size = dev->par.round * dev->par.bps * dev->par.pchan;

  /* allocate the buffers. FIXME!!! don't use interleaved samples */
  dev->buffer = FLUID_MALLOC(dev->buffer_byte_size);
  if (dev->buffer == NULL) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    goto error_recovery;
  }
  dev->buffers[0] = FLUID_ARRAY(float, dev->buffer_size);
  dev->buffers[1] = FLUID_ARRAY(float, dev->buffer_size);
  if ((dev->buffer == NULL) || (dev->buffers[0] == NULL) || (dev->buffers[1] == NULL)) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    goto error_recovery;
  }

  if (!sio_start(dev->hdl)) {
    FLUID_LOG(FLUID_ERR, "Couldn't start sndio");
    goto error_recovery;
  }

  if (pthread_attr_init(&attr)) {
    FLUID_LOG(FLUID_ERR, "Couldn't initialize audio thread attributes");
    goto error_recovery;
  }

  err = pthread_create(&dev->thread, &attr, fluid_sndio_audio_run2, (void*) dev);
  if (err) {
    FLUID_LOG(FLUID_ERR, "Couldn't create audio2 thread");
    goto error_recovery;
  }

  return (fluid_audio_driver_t*) dev;

error_recovery:
  delete_fluid_sndio_audio_driver((fluid_audio_driver_t*) dev);
  return NULL;
}

/*
 * delete_fluid_sndio_audio_driver
 */
int
delete_fluid_sndio_audio_driver(fluid_audio_driver_t* p)
{
  fluid_sndio_audio_driver_t* dev = (fluid_sndio_audio_driver_t*) p;

  if (dev == NULL) {
    return FLUID_OK;
  }
  dev->cont = 0;
  if (dev->thread) {
    if (pthread_join(dev->thread, NULL)) {
      FLUID_LOG(FLUID_ERR, "Failed to join the audio thread");
      return FLUID_FAILED;
    }
  }
  if (dev->hdl) {
    sio_close(dev->hdl);
  }
  if (dev->buffer != NULL) {
    FLUID_FREE(dev->buffer);
  }
  FLUID_FREE(dev);
  return FLUID_OK;
}

/*
 * fluid_sndio_audio_run
 */
void*
fluid_sndio_audio_run(void* d)
{
  fluid_sndio_audio_driver_t* dev = (fluid_sndio_audio_driver_t*) d;
  fluid_synth_t* synth = dev->synth;
  void* buffer = dev->buffer;
  int len = dev->buffer_size;

  /* it's as simple as that: */
  while (dev->cont)
  {
    dev->read (synth, len, buffer, 0, 2, buffer, 1, 2);
    sio_write (dev->hdl, buffer, dev->buffer_byte_size);
  }

  FLUID_LOG(FLUID_DBG, "Audio thread finished");

  pthread_exit(NULL);

  return 0; /* not reached */
}


/*
 * fluid_sndio_audio_run
 */
void*
fluid_sndio_audio_run2(void* d)
{
  fluid_sndio_audio_driver_t* dev = (fluid_sndio_audio_driver_t*) d;
  short* buffer = (short*) dev->buffer;
  float* left = dev->buffers[0];
  float* right = dev->buffers[1];
  int buffer_size = dev->buffer_size;
  int dither_index = 0;

  FLUID_LOG(FLUID_DBG, "Audio thread running");

  /* it's as simple as that: */
  while (dev->cont)
  {
    (*dev->callback)(dev->data, buffer_size, 0, NULL, 2, dev->buffers);

    fluid_synth_dither_s16 (&dither_index, buffer_size, left, right,
			    buffer, 0, 2, buffer, 1, 2);

    sio_write (dev->hdl, buffer, dev->buffer_byte_size);
  }

  FLUID_LOG(FLUID_DBG, "Audio thread finished");

  pthread_exit(NULL);

  return 0; /* not reached */
}

void fluid_sndio_midi_driver_settings(fluid_settings_t* settings)
{
  fluid_settings_register_str(settings, "midi.sndio.device", "default", 0, NULL, NULL);
}

int
delete_fluid_sndio_midi_driver(fluid_midi_driver_t *addr)
{
  int err;
  fluid_sndio_midi_driver_t *dev = (fluid_sndio_midi_driver_t *)addr;

  if (dev == NULL) {
    return FLUID_OK;
  }
  dev->status = FLUID_MIDI_DONE;

  /* cancel the thread and wait for it before cleaning up */
  if (dev->thread) {
    err = pthread_cancel(dev->thread);
    if (err) {
      FLUID_LOG(FLUID_ERR, "Failed to cancel the midi thread");
      return FLUID_FAILED;
    }
    if (pthread_join(dev->thread, NULL)) {
      FLUID_LOG(FLUID_ERR, "Failed to join the midi thread");
      return FLUID_FAILED;
    }
  }
  if (dev->hdl != NULL) {
    mio_close(dev->hdl);
  }
  if (dev->parser != NULL) {
    delete_fluid_midi_parser(dev->parser);
  }
  FLUID_FREE(dev);
  return FLUID_OK;
}

void *
fluid_sndio_midi_run(void *addr)
{
  int n, i;
  fluid_midi_event_t* evt;
  fluid_sndio_midi_driver_t *dev = (fluid_sndio_midi_driver_t *)addr;
#define MIDI_BUFLEN (3125 / 10)
  unsigned char buffer[MIDI_BUFLEN];

  /* make sure the other threads can cancel this thread any time */
  if (pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL)) {
    FLUID_LOG(FLUID_ERR, "Failed to set the cancel state of the midi thread");
    pthread_exit(NULL);
  }
  if (pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL)) {
    FLUID_LOG(FLUID_ERR, "Failed to set the cancel state of the midi thread");
    pthread_exit(NULL);
  }

  /* go into a loop until someone tells us to stop */
  dev->status = FLUID_MIDI_LISTENING;

  while (dev->status == FLUID_MIDI_LISTENING) {

    /* read new data */
    n = mio_read(dev->hdl, buffer, MIDI_BUFLEN);
    if (n == 0 && mio_eof(dev->hdl)) {
      FLUID_LOG(FLUID_ERR, "Failed to read the midi input");
      dev->status = FLUID_MIDI_DONE;
    }

    /* let the parser convert the data into events */
    for (i = 0; i < n; i++) {
      evt = fluid_midi_parser_parse(dev->parser, buffer[i]);
      if (evt != NULL) {
	/* send the event to the next link in the chain */
	(*dev->driver.handler)(dev->driver.data, evt);
      }
    }
  }
  pthread_exit(NULL);
}

int
fluid_sndio_midi_driver_status(fluid_midi_driver_t *addr)
{
  fluid_sndio_midi_driver_t *dev = (fluid_sndio_midi_driver_t *)addr;
  return dev->status;
}


fluid_midi_driver_t *
new_fluid_sndio_midi_driver(fluid_settings_t *settings,
			       handle_midi_event_func_t handler, void *data)
{
  int err;
  fluid_sndio_midi_driver_t *dev;
  char *device;

  /* not much use doing anything */
  if (handler == NULL) {
    FLUID_LOG(FLUID_ERR, "Invalid argument");
    return NULL;
  }

  /* allocate the device */
  dev = FLUID_NEW(fluid_sndio_midi_driver_t);
  if (dev == NULL) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    return NULL;
  }
  FLUID_MEMSET(dev, 0, sizeof(fluid_sndio_midi_driver_t));
  dev->hdl = NULL;

  dev->driver.handler = handler;
  dev->driver.data = data;

  /* allocate one event to store the input data */
  dev->parser = new_fluid_midi_parser();
  if (dev->parser == NULL) {
    FLUID_LOG(FLUID_ERR, "Out of memory");
    goto error_recovery;
  }

  /* get the device name. if none is specified, use the default device. */
  if (!fluid_settings_getstr(settings, "midi.sndio.device", &device)) {
	device = NULL;
  }

  /* open the default hardware device. only use midi in. */
  dev->hdl = mio_open(device, MIO_IN, 0);
  if (dev->hdl == NULL) {
    FLUID_LOG(FLUID_ERR, "Couldn't open sndio midi device");
    goto error_recovery;
  }

  dev->status = FLUID_MIDI_READY;

  err = pthread_create(&dev->thread, NULL, fluid_sndio_midi_run, (void *)dev);
  if (err) {
    FLUID_LOG(FLUID_PANIC, "Couldn't create the midi thread.");
    goto error_recovery;
  }
  return (fluid_midi_driver_t *) dev;

 error_recovery:
  delete_fluid_sndio_midi_driver((fluid_midi_driver_t *)dev);
  return NULL;
}

#endif /*#if SNDIO_SUPPORT */
