/*
    rtalsa.c:

    Copyright (C) 2002 Istvan Varga

    This file is part of Csound.

    The Csound 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.

    Csound 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 Csound; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
    02111-1307 USA
*/

/*                          rtalsa.c             RTAUDIO.C for Linux  */

/*  This module is included when RTAUDIO is defined at compile time.
    It provides an interface between Csound realtime record/play calls
    and the device-driver code that controls the actual hardware.
    This is the alsa 0.9 version
    The audio device used is called csound-dev, and should be defined
    in a .asoundrc
    Access is interleaved only; for non-interleaved access you have to
    setup an .asoundrc file using the plughw interface
*/

#include "cs.h"
#include "soundio.h"
#define ALSA_PCM_NEW_HW_PARAMS_API
#include <alsa/asoundlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#define DSP_NAME    "csound-dev"

#ifdef PIPES
#define _pclose pclose
#endif

int setsndparms(snd_pcm_t *handle, int format, int nchanls, unsigned int *sr,
                snd_pcm_uframes_t *periodsize, snd_pcm_uframes_t *buffersize)
{
    int alsa_format;
    snd_pcm_hw_params_t *hwparams;
    snd_pcm_sw_params_t *swparams;
    int err;

    /* set sample size/format */
    switch ( format ) {
    case AE_UNCH:  /* unsigned char - standard Linux 8-bit format */
      alsa_format = SND_PCM_FORMAT_U8;
      break;
    case AE_CHAR:  /* signed char. - probably not supported by Linux */
      alsa_format = SND_PCM_FORMAT_S8;
      break;
    case AE_ULAW: /* assuming u_law == mu_law */
      alsa_format = SND_PCM_FORMAT_MU_LAW;
      break;
    case AE_ALAW:
      alsa_format = SND_PCM_FORMAT_A_LAW;
      break;
#ifdef LINUX_BE
    case AE_SHORT:
      alsa_format = SND_PCM_FORMAT_S16_BE;
      break;
    case AE_FLOAT:
      alsa_format = SND_PCM_FORMAT_FLOAT_BE;
      break;
    case AE_24INT:         /*not fully implemented yet, is this true 24 bit or packed in 32? no idea */
      alsa_format = SND_PCM_FORMAT_S24_BE;
      break;
    case AE_DOUBLE:
      alsa_format = SND_PCM_FORMAT_FLOAT64_BE;
      break;
#else
    case AE_SHORT:
      alsa_format = SND_PCM_FORMAT_S16_LE;
      break;
    case AE_FLOAT:
      alsa_format = SND_PCM_FORMAT_FLOAT_LE;
      break;
    case AE_24INT:         /*not fully implemented yet, is this true 24 bit or packed in 32? no idea */
      alsa_format = SND_PCM_FORMAT_S24_LE;
      break;
    case AE_DOUBLE:
      alsa_format = SND_PCM_FORMAT_FLOAT64_LE;
      break;
#endif
    default:
      sprintf(errmsg,"unknown sample format");
      warning(errmsg);
      return -1;
    }


    /* set up HW params */
    snd_pcm_hw_params_malloc(&hwparams);
    err = snd_pcm_hw_params_any(handle, hwparams);
    if (err < 0) {
      sprintf(errmsg,
              "Broken configuration for this PCM: no configurations available\n");
      warning(errmsg);
      return -1;
    }

    err = snd_pcm_hw_params_set_access(handle, hwparams,
                                       SND_PCM_ACCESS_RW_INTERLEAVED);
    if (err < 0) {
      sprintf(errmsg,"interleaved access type not available\n");
      warning(errmsg);
      return -1;
    }

    err = snd_pcm_hw_params_set_format(handle, hwparams, alsa_format);
    if (err < 0) {
      warning(Str(X_1312,"unable to set requested sample format on soundcard"));
      return -1;
    }

    err = snd_pcm_hw_params_set_channels(handle, hwparams, nchanls);
    if (err < 0) {
      sprintf(errmsg,"Channels count non available");
      warning(errmsg);
      return -1;
    }

    err = snd_pcm_hw_params_set_rate_near(handle,hwparams, sr, 0);
    if (err < 0) {
      warning(Str(X_1313, "unable to set sample rate on soundcard"));
      return -1;
    }

    err = snd_pcm_hw_params_set_buffer_size_near(handle, hwparams, buffersize);
    if (err < 0) {
      sprintf(errmsg,"cannot set buffersize\n");
      warning(errmsg);
      return -1;
    }


    err = snd_pcm_hw_params_set_period_size_near(handle, hwparams, periodsize, 0);
    if (err < 0) {
      sprintf(errmsg,"cannot set software buffer size\n");
      warning(errmsg);
      return -1;
    }

    err = snd_pcm_hw_params(handle, hwparams);
    if (err < 0) {
      sprintf(errmsg,"Unable to install hw params\n");
      warning(errmsg);
      return -1;
    }

    snd_pcm_hw_params_free(hwparams);

    /* set up software parameters */

    snd_pcm_sw_params_malloc(&swparams);
    err = snd_pcm_sw_params_current(handle, swparams);
    if (err < 0) {
      sprintf(errmsg,"Unable to acces software parameters\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params_set_avail_min(handle,swparams,*periodsize);
    if (err < 0) {
      sprintf(errmsg,"Unable to set min availablei number of frames\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params_set_start_threshold(handle,
                                                swparams, *periodsize * 2);
    if (err < 0) {
      sprintf(errmsg,"Unable to set start threshold\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params_set_stop_threshold(handle, swparams, *buffersize);
    if (err < 0) {
      sprintf(errmsg,"Unable to set stop threshold\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params_set_xfer_align(handle, swparams, 1);
    if (err < 0) {
      sprintf(errmsg,"Unable to set transfer align parameter\n");
      warning(errmsg);
      return -1;
    }

    err = snd_pcm_sw_params_set_silence_size(handle, swparams, 0);
    if (err < 0) {
      sprintf(errmsg,"Unable to set silence size parameter\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params_set_sleep_min(handle, swparams, 0);
    if (err < 0) {
      sprintf(errmsg,"Unable to set sleep parameter\n");
      warning(errmsg);
      return -1;
    }
    err = snd_pcm_sw_params(handle, swparams);
    if (err < 0) {
      sprintf(errmsg,"Unable to set sw parameters\n");
      warning(errmsg);
      return -1;
    }
    snd_pcm_sw_params_free(swparams);

    return 0;
}

static snd_pcm_t *capture_handle = NULL, *playback_handle = NULL;

int xrun_recovery(snd_pcm_t *handle, int err)
{
   /* this could be improved with time stamping, so that the length of the xrun
      can be communicated to the user, OTOH this is probably a quicker recovery */
    err = snd_pcm_prepare(handle);
    if (err < 0) return -1;
    return 0;
}

void recopen_(int nchanls, int dsize, MYFLT sr, int scale)
     /* open for audio input */
{
    snd_pcm_uframes_t periodsize,buffersize;
    int err;
    unsigned int sample_rate;
    err=snd_pcm_open(&capture_handle, DSP_NAME, SND_PCM_STREAM_CAPTURE,0);
    if (err<0) {
      err=snd_pcm_open(&capture_handle, "default",SND_PCM_STREAM_CAPTURE,0);
      if (err<0) {
        sprintf(errmsg, "Opening CAPTURE channel: %s\n", snd_strerror(err));
        die(errmsg);
      }
      else {
        sprintf(errmsg,
                "using the DEFAULT alsa device, you have not set up an "
                "csound-dev in your .asoundrc\n");
        warning(errmsg);
      }
    }

    /* determine the period and buffer size */
    /*  period size is -b buffer_size is -B*/

    periodsize = O.inbufsamps/nchanls;
    buffersize = O.oMaxLag;
    if (buffersize==0) buffersize=IODACSAMPS;
    sample_rate= (unsigned int) sr;
    err = setsndparms(capture_handle,O.informat,nchanls,
                      &sample_rate,&periodsize,&buffersize);
    if (err < 0) {
      sprintf(errmsg,"unable to set parameters for capture\n");
      die(errmsg);
    }

    if (sample_rate != sr) {
      sprintf(errmsg,"sample rate does not match, set to %i instead of %f\n",
              sample_rate,sr);
      warning(errmsg);
    }
    if (buffersize != O.oMaxLag) {
      sprintf(errmsg,
              "hw-buffer size rate does not match, set to %ul instead of %ul\n",
              (unsigned int) buffersize,O.oMaxLag);
      warning(errmsg);
      O.oMaxLag = buffersize;
    }
    if (periodsize != O.inbufsamps/nchanls) {
      sprintf(errmsg,
              "sw-buffer size rate does not match, set to %ul instead of %ul\n",
              (unsigned int) periodsize,O.inbufsamps/nchanls);
      warning(errmsg);
      O.inbufsamps = periodsize * nchanls;
    }
    return;
}

void playopen_(int nchanls, int dsize, MYFLT sr, int scale)
{
    snd_pcm_uframes_t periodsize,buffersize;
    int err;
    unsigned int sample_rate;

    err=snd_pcm_open(&playback_handle, DSP_NAME, SND_PCM_STREAM_PLAYBACK,0);
    if (err<0) {
      err=snd_pcm_open(&playback_handle, "default",SND_PCM_STREAM_PLAYBACK,0);
      if (err<0) {
        sprintf(errmsg, "Opening CAPTURE channel: %s\n", snd_strerror(err));
        die(errmsg);
      }
      else {
        sprintf(errmsg,
                "using the DEFAULT alsa device, you haven't set up an "
                "csound-dev in your .asoundrc\n");
        warning(errmsg);
      }
    }

    /* determine the period and buffer size */
    /*  period size is -b buffer_size is -B*/

    periodsize = O.outbufsamps/nchnls;
    buffersize = O.oMaxLag;
    if (buffersize==0) buffersize=IODACSAMPS;
    sample_rate = (unsigned int) sr;
    err=setsndparms(playback_handle,O.informat,nchanls,
                    &sample_rate, &periodsize, &buffersize);
    if (err<0) {
      sprintf(errmsg,"unable to set parameters for capture\n");
      die(errmsg);
    }

    if (sample_rate != sr) {
      sprintf(errmsg,"sample rate does not match, set to %i instead of %f\n",
              sample_rate,sr);
      warning(errmsg);
    }

    if (buffersize != O.oMaxLag) {
      sprintf(errmsg,
              "hw-buffer size rate does not match, set to %ul instead of %ul\n",
              (unsigned int) buffersize,O.oMaxLag);
      warning(errmsg);
      O.oMaxLag = buffersize;
    }

    if (periodsize != O.outbufsamps/nchanls) {
      sprintf(errmsg,
              "sw-buffer size rate does not match, set to %i instead of %i\n",
              (unsigned int) periodsize,O.outbufsamps/nchanls);
      warning(errmsg);
      O.outbufsamps = periodsize * nchanls;
    }
}

int rtrecord_(char *inbuf, int nbytes) /* get samples from ADC */
{
    snd_pcm_sframes_t read = snd_pcm_bytes_to_frames(capture_handle,nbytes);
    if ( (read = snd_pcm_readi(capture_handle, (void *) inbuf, read)) < 0 ) {
      if (xrun_recovery(capture_handle,read) < 0) {
        sprintf(errmsg," unrecoverable error while reading audio input\n");
        die(errmsg);
      }
      read = 0;
    }
    return(snd_pcm_frames_to_bytes(capture_handle,read));
}

void rtplay_(char *outbuf, int nbytes) /* put samples to DAC  */
{
    snd_pcm_sframes_t written;
    snd_pcm_sframes_t frames_to_write =
      snd_pcm_bytes_to_frames(playback_handle,nbytes);

    written = snd_pcm_writei(playback_handle, outbuf, frames_to_write);
    if (written < frames_to_write) {
      sprintf(errmsg,"could not write all bytes requested\n");
      warning(errmsg);
      if (written < 0) {
        if (xrun_recovery(playback_handle,written) < 0) {
          sprintf(errmsg,"unrecoverable error while writing audio output\n");
          die(errmsg);
        }
      }
      nrecs++;
    }
    return;
}

void rtclose_(void)              /* close the I/O device entirely  */
{                                /* called only when both complete */
    if (capture_handle) {
      snd_pcm_drain(capture_handle);
      snd_pcm_close(capture_handle);
    }

    if (playback_handle) {
      snd_pcm_drain(playback_handle);
      snd_pcm_close(playback_handle);
    }

#ifdef PIPES
    if (O.Linename && O.Linename[0]=='|') _pclose(Linepipe);
    else
#endif
      if (O.Linename && strcmp(O.Linename, "stdin")!=0) close(Linefd);
}

