/*
 * audio-alsa.c: A plugin for the Video Disk Recorder
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: audio-alsa.c,v 1.8 2007/12/25 13:41:13 lucke Exp $
 */
#include "audio-alsa.h"

#define ALSA_PCM_NEW_HW_PARAMS_API
#define ALSA_PCM_NEW_SW_PARAMS_API
#include <alsa/asoundlib.h>

#define PCM_FMT SND_PCM_FORMAT_S16_LE

//#define AUDIODEB(out...) {printf("AUDIO-ALSA[%04d]:",(int)(getTimeMilis() % 10000));printf(out);}

#ifndef AUDIODEB
#define AUDIODEB(out...)
#endif


/*--------------------------------------------------------------------------
*/
cAlsaAudioOut::cAlsaAudioOut() {
    int err;

    if (strlen(setupStore->alsaDevice) == 0)
      strcpy (setupStore->alsaDevice, "default");
    dsyslog("[softdevice-audio] Opening alsa device %s",setupStore->alsaDevice);
    device = setupStore->alsaDevice;

    if (strlen(setupStore->alsaAC3Device) == 0)
      strcpy (setupStore->alsaAC3Device, "hw:0,1");
    dsyslog("[softdevice-audio] Using alsa AC3 device %s",
            setupStore->alsaAC3Device);
    ac3Device = setupStore->alsaAC3Device;

    paused=false;
    ac3PassThrough = false;
    ac3SpdifPro = false;

    if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
      esyslog("[softdevice-audio] Playback open error: %s, %s FATAL exiting",
              device, snd_strerror(err));
      exit(1);
    }
    oldContext.channels = currContext.channels=0;
    oldContext.samplerate = currContext.samplerate=48000;
    dsyslog("[softdevice-audio] Device opened! Ready to play");
    scale_Factor=0x7FFF;
}

/* ----------------------------------------------------------------------------
 */
cAlsaAudioOut::~cAlsaAudioOut()
{
  if (handle)
    snd_pcm_close(handle);
  handle = NULL;
}

/* ----------------------------------------------------------------------------
 */
void cAlsaAudioOut::Suspend()
{
  if (handle)
    snd_pcm_close(handle);
  handle = NULL;
}

/* ----------------------------------------------------------------------------
 */
bool cAlsaAudioOut::Resume() {
   int err;
   AUDIODEB("Device %s\n",device);
   if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK,SND_PCM_NONBLOCK )) < 0) {
     esyslog("[softdevice-audio] Playback open error: %s, %s ",
             device, snd_strerror(err));
     return false;
   }
   //force setting of the parameters after resume
   currContext.channels=0;
   return true;
};

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

void cAlsaAudioOut::Write(uchar *Data, int Length)
{
    int     err;
    size_t  size;

  AUDIODEB("Write %p %d\n",Data,Length);

  if (!handle)
          return;

  handleMutex.Lock();
  if (ac3PassThrough)
  {
    ac3PassThrough = false;
    SetAC3PassThroughMode(ac3PassThrough);
  }
  handleMutex.Unlock();

  if (!currContext.channels)
    size = Length / (2 * 2);
  else
    size = Length/(2*currContext.channels);

  // change the volume without mixer
  if (!setupStore->useMixer)
    Scale((int16_t*)Data,Length/2,scale_Factor);

  while (size) {
    while (paused) usleep(1000); // block
    AUDIODEB("pcm_mmap_writei  handle %p Data %p size %d\n",handle,Data,size);
    err = snd_pcm_mmap_writei(handle,Data, size);
    AUDIODEB("pcm_mmap_writeei return value %d Data %p size %d \n",
                   err,Data,size);
    if (err == -EAGAIN || (err >= 0 && (size_t)err < size)) {
      AUDIODEB("wait \n");
      snd_pcm_wait(handle, 1000);
    } else if (err == -EPIPE) {
      AUDIODEB("Xrun \n");
      Xrun();
      dsyslog("[softdevice-audio]: xrun");
    } else if (err == -ESTRPIPE) {
      //suspend();
      AUDIODEB("Suspend \n");
      dsyslog("[softdevice-audio]: Suspend");
    } else if (err == -EINTR) {
      dsyslog ("[softdevice-audio]: EINTR");
      AUDIODEB("EINTR \n");
      return;
    } else if (err < 0) {
      dsyslog("[softdevice-audio]: write error: %s FATAL exiting",
              snd_strerror(err));
      exit(EXIT_FAILURE);
    }

    if (err > 0 ) {
      if (!currContext.channels)
        Data += err * 2 * 2;
      else
        Data += err * 2 * currContext.channels;
      size -=err;
    };
  }
  AUDIODEB("Write end\n");
}

/* ----------------------------------------------------------------------------
 */
bool
cAlsaAudioOut::SetAC3PassThroughMode(bool on)
{
  if (on)
  {
    oldContext = currContext;

    if (handle)
      snd_pcm_close(handle);
    handle = NULL;

    // open S/P-DIF
    if (ac3pt.SpdifInitAC3(&handle, ac3Device, ac3SpdifPro) == 0)
    {
      currContext.channels = 0;
      return true;
    }
    currContext = oldContext;
    esyslog("[softdevice-audio] AC3 open error:");
  }
  return false;
}

/* ----------------------------------------------------------------------------
 */
void cAlsaAudioOut::WriteAC3(uchar *Data, int Length)
{
  handleMutex.Lock();
  if (!ac3PassThrough)
  {
    ac3PassThrough = true;
    SetAC3PassThroughMode(ac3PassThrough);
  }
  handleMutex.Unlock();

  ac3pt.SpdifBurstAC3 (&handle, Data, Length);
}

/* ----------------------------------------------------------------------------
 */
void cAlsaAudioOut::Pause(void) {
    //    snd_pcm_pause(handle,1);
    dsyslog("[softdevice-audio]: Should pause now");
    paused=true;
}

/* ----------------------------------------------------------------------------
 */
void cAlsaAudioOut::Play(void) {
//    snd_pcm_pause(handle,0);
    paused=false;
}

/* ----------------------------------------------------------------------------
 */
int cAlsaAudioOut::GetDelay(void) {
    int               res = 0;
    snd_pcm_sframes_t r;

  handleMutex.Lock();
  if (!handle) {
          handleMutex.Unlock();
          return 0;
  };

  if ( snd_pcm_state(handle) != SND_PCM_STATE_RUNNING ) {
          handleMutex.Unlock();
          return 0;
  };

  if (!snd_pcm_delay(handle, &r) &&
      currContext.samplerate) {
    // successfully got delay
    res = (long) r * 10000 / currContext.samplerate;
    AUDIODEB("GetDelay %d\n",res);
  }
  handleMutex.Unlock();
  return res;
}

/* ----------------------------------------------------------------------------
 * I/O error handler
 */
void cAlsaAudioOut::Xrun(void)
{
    snd_pcm_status_t  *status;
    int               res;

  snd_pcm_status_alloca(&status);
  //printf("alsa-audio: Xrun\n");
  if ((res = snd_pcm_status(handle, status))<0) {
    esyslog("[softdevice-audio]: Xrun status error: %s FATAL exiting",
    snd_strerror(res));
    exit(EXIT_FAILURE);
  }
  if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
      struct timeval now, diff, tstamp;
    gettimeofday(&now, 0);
    snd_pcm_status_get_trigger_tstamp(status, &tstamp);
    timersub(&now, &tstamp, &diff);
    softlog->Log(SOFT_LOG_DEBUG, 0,
                 "[softdevice-audio]: Xrun (at least %.3f ms long)\n",
                 diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
    if ((res = snd_pcm_prepare(handle))<0) {
      esyslog("[softdevice-audio]: Xrun prepare error: %s FATAL exiting",
      snd_strerror(res));
      exit(EXIT_FAILURE);
    }
    return;		/* ok, data should be accepted again */
  }

  //  error("read/write error, state = %s", snd_pcm_state_name(snd_pcm_status_get_state(status)));
  esyslog("[softdevice-audio]: read/write error FATAL exiting");
  exit(EXIT_FAILURE);
}

/* ----------------------------------------------------------------------------
 */
int cAlsaAudioOut::SetParams(SampleContext &context) {
      int   err;
      char  *ch_dev = NULL;

    // not needed to set again
    if (currContext.samplerate == context.samplerate &&
        currContext.channels == context.channels ) {
      context=currContext;
      return 0;
    };
    //printf("alsa-audio: SetParams\n");
    currContext=context;

    handleMutex.Lock();

    if (handle)
      snd_pcm_close(handle);
    handle = NULL;

    if (context.channels < 10) {
      sprintf (chDevice, "CH%d", currContext.channels);
      device = chDevice;
      if ((err = snd_pcm_open(&handle,
                              device,
                              SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
        esyslog("[softdevice-audio] Playback reopen error: %s, %s",
                chDevice, snd_strerror(err));
        handle = NULL;
      }
    }

    if (!handle) {
      device = setupStore->alsaDevice;
      if ((err = snd_pcm_open(&handle,
                              device,
                              SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
        esyslog("[softdevice-audio] Playback reopen error: "
                "%s, %s FATAL exiting",
                device, snd_strerror(err));

        handle = NULL;
        handleMutex.Unlock();
        return -1;
      }
    }

    dsyslog ("[softdevice-audio] samplerate: %dHz, channels: #%d",
             currContext.samplerate, currContext.channels);
    snd_pcm_hw_params_t *params;
    snd_pcm_sw_params_t *swparams;
    snd_pcm_uframes_t xfer_align;
    snd_pcm_hw_params_alloca(&params);
    snd_pcm_sw_params_alloca(&swparams);

    err = snd_pcm_hw_params_any(handle, params);
    if (err < 0) {
      esyslog("[softdevice-audio] Broken config for this PCM: no configurations available");
      handleMutex.Unlock();
      exit(EXIT_FAILURE);
    }

    snd_pcm_access_mask_t *mask = (snd_pcm_access_mask_t *)alloca(snd_pcm_access_mask_sizeof());
    snd_pcm_access_mask_none(mask);
    snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED);
    snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED);
    snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_COMPLEX);
    err = snd_pcm_hw_params_set_access_mask(handle, params, mask);
    if (err < 0) {
      esyslog("[softdevice-audio] Access type not available NO AUDIO!");
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    err = snd_pcm_hw_params_set_format(handle, params, PCM_FMT);
    if (err < 0) {
      esyslog("[softdevice-audio] Sample format non available NO AUDIO!");
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    err = snd_pcm_hw_params_set_channels(handle, params,currContext.channels);
    if (err < 0) {
      esyslog("[softdevice-audio] Channels count non available NO AUDIO");
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    err = snd_pcm_hw_params_set_rate_near(handle, params, &currContext.samplerate, 0);
    assert(err >= 0);
    if (currContext.samplerate != context.samplerate ) {
      esyslog("[softdevice-audio] Rate %d Hz is not possible (and using instead %d Hz is not implemented) NO AUDIO!",context.samplerate,currContext.samplerate);
      //put back requested samplerate, so that we don't try again
      currContext.samplerate = context.samplerate;
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    // set period size
    snd_pcm_uframes_t bufferSize;
    snd_pcm_uframes_t periodSize;
    //periodSize = 8;
    periodSize = 4608 / 4;
    err = snd_pcm_hw_params_set_period_size_near(handle, params, &periodSize, 0);
    if ( err < 0)  {
      esyslog("[softdevice-audio] Failed to set period size! NO AUDIO!");
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    snd_pcm_uframes_t buffersize = 2 * 4608;

    err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffersize);
    if ( err < 0 ) {
      esyslog("[softdevice-audio] Failed to set buffer size! NO AUDIO");
      handleMutex.Unlock();
      Suspend();
      return -1;
    }

    err = snd_pcm_hw_params(handle, params);
    if (err < 0) {
      esyslog("[softdevice-audio] Unable to install hw params: FATAL exiting");
      handleMutex.Unlock();
      exit(EXIT_FAILURE);
    }

    snd_pcm_hw_params_get_period_size(params, &periodSize, 0);
    snd_pcm_hw_params_get_buffer_size(params, &bufferSize);
    if (periodSize == bufferSize) {
      esyslog("[softdevice-audio] Can't use period equal to buffer size (%lu == %lu) FATAL exiting",
              periodSize, bufferSize);
      handleMutex.Unlock();
      exit(EXIT_FAILURE);
    }
    dsyslog("[softdevice-audio] Device '%s', Samplerate %d Channels %d",
            device,currContext.samplerate, currContext.channels);
    dsyslog("[softdevice-audio] Period size %lu Buffer size %lu",
            periodSize, bufferSize);

    currContext.buffer_size=bufferSize;
    currContext.period_size=periodSize;

    snd_pcm_sw_params_current(handle, swparams);
    err = snd_pcm_sw_params_get_xfer_align(swparams, &xfer_align);
    if (err < 0) {
      esyslog("[softdevice-audio] Unable to obtain xfer align FATAL exiting");
      handleMutex.Unlock();
      exit(EXIT_FAILURE);
    }
    dsyslog("[softdevice-audio] Hardware initialized");
    handleMutex.Unlock();

    context=currContext;
    return 0;
}

/* ----------------------------------------------------------------------------
 */
void cAlsaAudioOut::SetVolume (int vol)
{
  if (!setupStore->useMixer) {
    scale_Factor = CalcScaleFactor(vol);
    //printf("vol %d scale_Factor 0x%04x\n",vol,scale_Factor);
    //scale_Factor = vol;
  } else {
      int                   err;
      long                  mixerMin, mixerMax,
                            setVol;
      double                mixerRange,
                            volPercent;
      char                  *mName = "PCM",
                            *cardName = "default";
      snd_mixer_t           *mHandle;
      snd_mixer_elem_t      *mElem;
      snd_mixer_selem_id_t  *sId;

    snd_mixer_selem_id_alloca(&sId);
    snd_mixer_selem_id_set_name(sId, mName);
    if ((err = snd_mixer_open(&mHandle, 0)) < 0) {
        static int once = 0;
      if (!once) {
        dsyslog("[softdevice-audio]: cannot open mixer: %s (%s)",
                mName,snd_strerror(err));
        once = 1;
      }
      return;
    }

    snd_mixer_attach(mHandle, cardName);
    snd_mixer_selem_register(mHandle, NULL, NULL);
    snd_mixer_load(mHandle);
    mElem = snd_mixer_find_selem(mHandle,sId);
    /* ------------------------------------------------------------------------
     * some cards don't have any master volume. so check return value
     */
    if (mElem) {
      snd_mixer_selem_get_playback_volume_range(mElem,&mixerMin,&mixerMax);
      mixerRange = mixerMax - mixerMin;
      volPercent = (double) vol / 255.0;
      setVol = (int) (((double)mixerMin+(mixerRange*volPercent))+0.5);
      snd_mixer_selem_set_playback_volume(mElem,
                                          SND_MIXER_SCHN_FRONT_LEFT,
                                          setVol);
      snd_mixer_selem_set_playback_volume(mElem,
                                          SND_MIXER_SCHN_FRONT_RIGHT,
                                          setVol);
      if (snd_mixer_selem_has_playback_switch(mElem)) {
        snd_mixer_selem_set_playback_switch_all(mElem, (vol) ? 1 : 0);
      }
    }

    snd_mixer_close(mHandle);
  }
}

