//=========================================================
//  MusE
//  Linux Music Editor
//    $Id: alsaaudio.cpp,v 1.1.1.1 2003/10/29 10:06:12 wschweer Exp $
//  (C) Copyright 2000 Werner Schweer (ws@seh.de)
//=========================================================

#include "config.h"

#include <alsa/asoundlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <qstring.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <math.h>

#include "globals.h"
#include "audio.h"
#include "alsaaudio.h"
#include "thread.h"

AlsaAudioDevice* alsaAudioDevice;

//---------------------------------------------------------
//   xrun
//    error handler
//---------------------------------------------------------

bool xrun(const char* txt, snd_pcm_t* handle)
      {
      snd_pcm_status_t *status;
      int res;

      snd_pcm_status_alloca(&status);
      if ((res = snd_pcm_status(handle, status))<0) {
            fprintf(stderr, "ALSA: status error: %s", snd_strerror(res));
            exit(EXIT_FAILURE);
            }
      int state = snd_pcm_status_get_state(status);
      if (state == SND_PCM_STATE_XRUN) {
            if (debugMsg) {
                  struct timeval now, diff, tstamp;
                  gettimeofday(&now, 0);
                  snd_pcm_status_get_trigger_tstamp(status, &tstamp);
                  timersub(&now, &tstamp, &diff);
                  fprintf(stderr, "ALSA: %s xrun >= %.3f ms\n", txt, diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
                  }
            if ((res = snd_pcm_prepare(handle)) < 0) {
                  fprintf(stderr, "ALSA: xrun: prepare error: %s", snd_strerror(res));
                  exit(EXIT_FAILURE);
                  }
            return false;     // ok, data should be accepted again
            }
      else {
            fprintf(stderr, "ALSA: %s xrun: read/write error in state %d: %s\n",
               txt, state, snd_strerror(res));
            return true;
            }
      }

//---------------------------------------------------------
//   setParams
//---------------------------------------------------------

static void setParams(snd_pcm_t* handle, bool capture)
      {
      //-----------------------------------------------
      //   HW parameters

      snd_pcm_hw_params_t* hwparams;
      snd_pcm_hw_params_alloca(&hwparams);

      int err = snd_pcm_hw_params_any(handle, hwparams);
      if (err < 0) {
            fprintf(stderr, "ALSA: Broken configuration for this PCM: no configurations available");
            exit(EXIT_FAILURE);
            }

      err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
      if (err < 0) {
            fprintf(stderr, "ALSA: Access type not available");
            exit(EXIT_FAILURE);
            }
      err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE);
      if (err < 0) {
            fprintf(stderr, "ALSA: Sample format non available");
            exit(EXIT_FAILURE);
            }
      err = snd_pcm_hw_params_set_channels(handle, hwparams, 2);
      if (err < 0) {
            fprintf(stderr, "ALSA: Channels count non available");
            exit(EXIT_FAILURE);
            }

      int dir;
      int exactRate = snd_pcm_hw_params_set_rate_near(handle, hwparams, sampleRate, &dir);
      if (exactRate != sampleRate) {
            fprintf(stderr, "ALSA: samplerate %d not available, using %d\n",
               sampleRate, exactRate);
//            sampleRate = exactRate;    // DEBUG
            }

      //
      // buffer
      //
      int cnt = 2;
      int bs = snd_pcm_hw_params_set_buffer_size_near(handle, hwparams, segmentSize * cnt);
      if (bs != segmentSize * cnt) {
            printf("ALSA: set buffer size, req: %d got: %d\n", segmentSize*cnt, bs);
            segmentSize = bs / cnt;
            }

      int ps = snd_pcm_hw_params_set_period_size_near(handle, hwparams, segmentSize, 0);
      if (ps != segmentSize)
            printf("ALSA: set period size, req: %d got: %d\n", segmentSize, ps);

      err = snd_pcm_hw_params(handle, hwparams);
      if (err < 0) {
            fprintf(stderr, "ALSA: Unable to install hw params for %s:\n", capture ? "capture" : "playback");
            fprintf(stderr, "  channels 2  sampleRate %d  format SND_PCM_FORMAT_S16_LE\n",
               sampleRate);
            fprintf(stderr, "  buffer size %d, period size %d\n", segmentSize*cnt, segmentSize);
            exit(EXIT_FAILURE);
            }

      //-----------------------------------------------
      //   SW parameters

      snd_pcm_sw_params_t *sw_params;
      snd_pcm_sw_params_alloca(&sw_params);
      if ((err = snd_pcm_sw_params_current(handle, sw_params)) < 0) {
            fprintf (stderr, "cannot initialize software parameters structure (%s)\n",
               snd_strerror (err));
            exit (1);
            }
      if ((err = snd_pcm_sw_params_set_avail_min(handle, sw_params, segmentSize)) < 0) {
            fprintf (stderr, "cannot set minimum available count (%s)\n",
               snd_strerror (err));
            exit (1);
            }
      if ((err = snd_pcm_sw_params_set_start_threshold(handle, sw_params, 0L)) < 0) {
            fprintf (stderr, "cannot set start mode (%s)\n",
               snd_strerror (err));
            exit (1);
            }
      if ((err = snd_pcm_sw_params_set_stop_threshold(handle, sw_params, segmentSize*cnt)) < 0) {
            fprintf (stderr, "cannot set start mode (%s)\n",
               snd_strerror (err));
            exit (1);
            }
      if ((err = snd_pcm_sw_params(handle, sw_params)) < 0) {
            fprintf (stderr, "cannot set software parameters (%s)\n",
               snd_strerror (err));
            exit (1);
            }
      }

//---------------------------------------------------------
//   processAudioRead
//---------------------------------------------------------

void processAudioRead(void* p, void*)
      {
      AlsaAudioDevice* dev = (AlsaAudioDevice*)p;
      dev->readCallback();
      }

//---------------------------------------------------------
//   readCallback
//---------------------------------------------------------

void AlsaAudioDevice::readCallback()
      {
      short ibuffer[inPorts * segmentSize];

      short* ip  = ibuffer;
      unsigned long frames = segmentSize;
      for (;;) {
            int rv = snd_pcm_readi(captureHandle, ip, segmentSize);
            if (rv == -EAGAIN)
                  ;
            else if (rv == -EPIPE) {
                  if (xrun("Capture", captureHandle)) {
                        for (int i = 0; i < inPorts; ++i)
                              memset(readBuffer[i], 0, sizeof(float) * segmentSize);
                        return;
                        }
                  }
            else if (rv < 0) {
                  fprintf(stderr, "AlsaAudioDevice: read(%p, %p, %lu) failed: %s\n",
                    captureHandle, ip, frames, snd_strerror(rv));
                  exit(1);
                  }
            else {
                  frames -= rv;
                  ip     += rv*2;
                  }
            if (frames == 0)
                  break;
            if (debugMsg)
                  printf("Alsa read: wait %lu %u\n", frames, segmentSize);
            snd_pcm_wait(captureHandle, 50);
            }

      //---------------------------------------------------
      //  transform int->float, deinterlace
      //---------------------------------------------------

      for (int k = 0; k < inPorts; ++k) {
            float* dst = readBuffer[k];
            for (int i = 0; i < segmentSize; i++) {
                  *dst++ = float(ibuffer[i * inPorts + k]) / 32767.0;
                  }
            }
      }

//---------------------------------------------------------
//   read
//---------------------------------------------------------

void AlsaAudioDevice::read(int ports, unsigned long nframes, float** buffer)
      {
      for (int i = 0; i < ports; ++i)
            memcpy(buffer[i], readBuffer[i], sizeof(float) * nframes);
      }

//---------------------------------------------------------
//   write
//---------------------------------------------------------

void AlsaAudioDevice::write(int ports, unsigned long nframes, float** buffer)
      {
      if (playbackHandle == 0)
            return;

      short obuffer[ports * nframes];

      //---------------------------------------------------
      //  transform float->int, interlace
      //---------------------------------------------------

      for (int k = 0; k < ports; ++k) {
            float* src = buffer[k];
            for (unsigned i = 0; i < nframes; i++) {
                  int val  = lrint(*src++ * 32767.0);
                  // clip
                  if (val > 32767)
                        val = 32767;
                  else if (val <= -32768)
                        val = -32768;
                  obuffer[i * ports + k] = val;
                  }
            }
      int n     = nframes;
      short* wp = obuffer;

      while (n > 0) {
            int r = snd_pcm_writei(playbackHandle, wp, n);
            if (r == -EAGAIN) {
                  printf("Alsa: EAGAIN\n");
                  }
            else if (r == -EPIPE) {
                  xrun("Playback", playbackHandle);
                  continue;
                  }
            else if (r < 0) {
                  fprintf(stderr, "AlsaAudioDevice: write(0x%x, 0x%x, %d) failed: %s\n",
                    unsigned(playbackHandle), unsigned(buffer), n, snd_strerror(r));
                  exit(1);
                  }
            else {
                  wp += r*2;
                  n -= r;
                  samplesWritten += r;
                  }
            if (n) {
                  if (debugMsg)
                        printf("Alsa: wait\n");
                  snd_pcm_wait(playbackHandle, 50);
                  }
            }
      }

//---------------------------------------------------------
//   playPos
//---------------------------------------------------------

int AlsaAudioDevice::curPlayPos() const
      {
      snd_pcm_sframes_t delay;
      snd_pcm_delay(playbackHandle, &delay);
      return samplesWritten - delay;
      }

//---------------------------------------------------------
//   initAlsaAudio
//    collect infos about audio devices
//    return true if no soundcard found
//---------------------------------------------------------

void initAlsaAudio()
      {
      if (debugMsg)
            fprintf(stderr, "init alsa audio\n");
      int card = -1;
      if (snd_card_next(&card) < 0 || card < 0) {
            fprintf(stderr, "ALSA: no soundcard found\n");
            alsaAudioDevice = 0;
            return;
            }
      alsaAudioDevice = new AlsaAudioDevice();
      bool bad = alsaAudioDevice->init();
      if (bad) {
            delete alsaAudioDevice;
            alsaAudioDevice = 0;
            }
      }

//---------------------------------------------------------
//   init
//    return true if no soundcard found
//---------------------------------------------------------

bool AlsaAudioDevice::init()
      {
      snd_ctl_card_info_t* info;
      snd_ctl_card_info_alloca(&info);
      snd_pcm_info_t* pcminfo;
      snd_pcm_info_alloca(&pcminfo);

      for (int card = -1;;) {
            int err = 0;
            if (snd_card_next(&card) < 0) {
                  fprintf(stderr, "ALSA: snd_card_next: %s\n",
                     snd_strerror(err));
                  break;
                  }
            if (card < 0)
                  break;
            snd_ctl_t* handle;
            char name[32];
            sprintf(name, "hw:%d", card);
            if ((err = snd_ctl_open(&handle, name, SND_CTL_NONBLOCK)) < 0) {
                  fprintf(stderr, "ALSA: control open (%d): %s\n",
                     card, snd_strerror(err));
                  break;
                  }
            if ((err = snd_ctl_card_info(handle, info)) < 0) {
                  fprintf(stderr, "ALSA: control hardware info %d: %s\n",
                     card, snd_strerror(err));
                  snd_ctl_close(handle);
                  break;
                  }
            int dev = -1;
            for (;;) {
                  if (snd_ctl_pcm_next_device(handle, &dev) < 0)
                        fprintf(stderr, "ALSA: pcm next device: %s\n",
                           snd_strerror(err));
                  if (dev < 0)
                        break;
                  int flags = 0;
                  snd_pcm_info_set_device(pcminfo, dev);
                  snd_pcm_info_set_subdevice(pcminfo, 0);
                  snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
                  const char* rname = snd_pcm_info_get_name(pcminfo);
                  if (snd_ctl_pcm_info(handle, pcminfo) >= 0) {
                        flags |= 1;
                        if (_outputPort.isEmpty())
                              _outputPort = name;
                        }
                  snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_CAPTURE);
                  if (snd_ctl_pcm_info(handle, pcminfo) >= 0) {
                        flags |= 2;
                        if (_inputPort.isEmpty())
                              _inputPort = name;
                        }
                  if (flags) {
                        AlsaPort* d = new AlsaPort(card, dev, QString(rname), flags);
                        portList.push_back(d);
                        }
                  }
            snd_ctl_close(handle);
            }
      bool alsaNotFound = portList.empty();
      if (alsaNotFound)
            printf("No ALSA audio port found\n");
//      else
//            printf("Alsa port found %s - %s\n",
//               _inputPort.latin1(), _outputPort.latin1());
      return alsaNotFound;
      }

//---------------------------------------------------------
//   setPort
//---------------------------------------------------------

void AlsaAudioDevice::setOutputPort(const QString& s)
      {
      _outputPort = s;
      }

void AlsaAudioDevice::setInputPort(const QString& s)
      {
      _inputPort = s;
      }

//---------------------------------------------------------
//   ports
//---------------------------------------------------------

std::list<QString> AlsaAudioDevice::outputPorts()
      {
      std::list<QString> clientList;
      for (std::list<AlsaPort*>::iterator i = portList.begin(); i != portList.end(); ++i) {
            clientList.push_back((*i)->name);
            }
      return clientList;
      }

std::list<QString> AlsaAudioDevice::inputPorts()
      {
      std::list<QString> clientList;
      for (std::list<AlsaPort*>::iterator i = portList.begin(); i != portList.end(); ++i) {
            clientList.push_back((*i)->name);
            }
      return clientList;
      }

void AlsaAudioDevice::activate(bool)
      {
      }

//---------------------------------------------------------
//   start
//---------------------------------------------------------

void AlsaAudioDevice::start()
      {
      if (readBuffer) {
            for (int i = 0; i < inPorts; ++i)
                  delete[] readBuffer[i];
            delete[] readBuffer;
            }
      readBuffer = new float*[inPorts];
      for (int i = 0; i < inPorts; ++i) {
            readBuffer[i] = new float[segmentSize];
            memset(readBuffer[i], 0, sizeof(float)*segmentSize);
            }

      samplesWritten = 0;

      //---------------------------------------------------
      //  setup playback stream
      //---------------------------------------------------

      int err = snd_pcm_open(&playbackHandle, _outputPort.latin1(),
                   SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
      if (err < 0) {
            fprintf(stderr, "ALSA: device <%s> open error: %s\n",
               _outputPort.latin1(), snd_strerror(err));
	      return;
            }
      setParams(playbackHandle, false);

      clearPollFd();

      //---------------------------------------------------
      //  setup capture stream
      //---------------------------------------------------

      err = snd_pcm_open(&captureHandle, _inputPort.latin1(),
               SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
      if (err < 0) {
            fprintf(stderr, "ALSA: device <%s> open error: %s\n",
               _outputPort.latin1(), snd_strerror(err));
            captureHandle = 0; // to make sure
            }
      else {
            setParams(captureHandle, true);
            err = snd_pcm_link(playbackHandle, captureHandle);
            if (err < 0) {
                  fprintf(stderr, "ALSA: link error: %s\n", snd_strerror(err));
                  }
            int n = snd_pcm_poll_descriptors_count(captureHandle);
            struct pollfd fd[n];
            snd_pcm_poll_descriptors(captureHandle, fd, n);
            for (int i = 0; i < n; ++i)
                  addPollFd(fd[i].fd, POLLIN, processAudioRead, this, 0);
            }

      int n = snd_pcm_poll_descriptors_count(playbackHandle);
      struct pollfd fd[n];
      snd_pcm_poll_descriptors(playbackHandle, fd, n);
      for (int i = 0; i < n; ++i)
            addPollFd(fd[i].fd, POLLOUT, processAudio1, 0, 0);
      Thread::start(this);
      }

//---------------------------------------------------------
//   threadStart
//---------------------------------------------------------

void AlsaAudioDevice::threadStart(void*)
      {
      //---------------------------------------------------
      //    prepare audio streams
      //---------------------------------------------------

      int err;
      if (captureHandle) {
            err = snd_pcm_prepare(captureHandle);
            if (err <0) {
                  fprintf(stderr, "ALSA: prepare capture stream error: %s",
                     snd_strerror(err));
                  }
            }
      int res = snd_pcm_prepare(playbackHandle);
      if (res <0) {
            fprintf(stderr, "ALSA: prepare playback stream error: %s",
               snd_strerror(res));
            exit(1);
            }
      }

//---------------------------------------------------------
//   stop
//---------------------------------------------------------

void AlsaAudioDevice::stop()
      {
      Thread::stop(true);
      if (captureHandle) {
	      int err = snd_pcm_close(captureHandle);
            if (err < 0)
	            printf("Alsa close capture: %s\n", snd_strerror(err));
            }
      if (playbackHandle) {
	      int err = snd_pcm_close(playbackHandle);
            if (err < 0)
	            printf("Alsa close playback: %s\n", snd_strerror(err));
            }
      captureHandle  = 0;
      playbackHandle = 0;
      }

//---------------------------------------------------------
//   AlsaAudioDevice
//---------------------------------------------------------

AlsaAudioDevice::AlsaAudioDevice()
   : Thread(realTimeScheduling ? ::realTimePriority / 2 : 0, "Alsa")
      {
      playbackHandle = 0;
      captureHandle  = 0;
      inPorts        = 2;
      outPorts       = 2;
      readBuffer     = 0;
      }

//---------------------------------------------------------
//   ~AlsaAudioDevice
//---------------------------------------------------------

AlsaAudioDevice::~AlsaAudioDevice()
      {
      }

