/* -*- c++ -*- */
/*
 * Copyright 2004 Free Software Foundation, Inc.
 * 
 * This file is part of GNU Radio
 * 
 * GNU Radio is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 * 
 * GNU Radio 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with GNU Radio; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <audio_alsa_sink.h>
#include <gr_io_signature.h>
#include <gr_prefs.h>
#include <stdio.h>
#include <iostream>
#include <stdexcept>
#include <gri_alsa.h>


static bool CHATTY_DEBUG = false;


static snd_pcm_format_t acceptable_formats[] = {
  // these are in our preferred order...
  SND_PCM_FORMAT_S32,
  SND_PCM_FORMAT_S16
};

#define NELEMS(x) (sizeof(x)/sizeof(x[0]))


static std::string 
default_device_name ()
{
  return gr_prefs::singleton()->get_string("audio_alsa", "default_output_device", "hw:0,0");
}

static double
default_period_time ()
{
  return std::max(0.001, gr_prefs::singleton()->get_double("audio_alsa", "period_time", 0.010));
}

static int
default_nperiods ()
{
  return std::max(2L, gr_prefs::singleton()->get_long("audio_alsa", "nperiods", 4));
}

// ----------------------------------------------------------------

audio_alsa_sink_sptr
audio_alsa_make_sink (int sampling_rate,
		      const std::string dev,
		      bool ok_to_block)
{
  return audio_alsa_sink_sptr (new audio_alsa_sink (sampling_rate, dev,
						    ok_to_block));
}

audio_alsa_sink::audio_alsa_sink (int sampling_rate,
				  const std::string device_name,
				  bool ok_to_block)
  : gr_sync_block ("audio_alsa_sink",
		   gr_make_io_signature (0, 0, 0),
		   gr_make_io_signature (0, 0, 0)),
    d_sampling_rate (sampling_rate),
    d_device_name (device_name.empty() ? default_device_name() : device_name),
    d_pcm_handle (0),
    d_hw_params ((snd_pcm_hw_params_t *)(new char[snd_pcm_hw_params_sizeof()])),
    d_sw_params ((snd_pcm_sw_params_t *)(new char[snd_pcm_sw_params_sizeof()])),
    d_nperiods (default_nperiods()),
    d_period_time_us ((unsigned int) (default_period_time() * 1e6)),
    d_period_size (0),
    d_buffer_size_bytes (0), d_buffer (0),
    d_worker (0), d_special_case_mono_to_stereo (false),
    d_nunderuns (0), d_nsuspends (0)
{
  CHATTY_DEBUG = gr_prefs::singleton()->get_bool("audio_alsa", "verbose", false);

  int  	error;
  int	dir;

  // open the device for playback
  error = snd_pcm_open(&d_pcm_handle, d_device_name.c_str (),
		       SND_PCM_STREAM_PLAYBACK, 0);
  if (error < 0){
    fprintf (stderr, "audio_alsa_sink[%s]: %s\n",
	     d_device_name.c_str(), snd_strerror(error));
    throw std::runtime_error ("audio_alsa_sink");
  }

  // Fill params with a full configuration space for a PCM.
  error = snd_pcm_hw_params_any(d_pcm_handle, d_hw_params);
  if (error < 0)
    bail ("broken configuration for playback", error);


  if (CHATTY_DEBUG)
    gri_alsa_dump_hw_params (d_pcm_handle, d_hw_params, stdout);


  // now that we know how many channels the h/w can handle, set input signature
  unsigned int umin_chan, umax_chan;
  snd_pcm_hw_params_get_channels_min (d_hw_params, &umin_chan);
  snd_pcm_hw_params_get_channels_max (d_hw_params, &umax_chan);
  int min_chan = std::min (umin_chan, 1000U);
  int max_chan = std::min (umax_chan, 1000U);

  // As a special case, if the hw's min_chan is two, we'll accept
  // a single input and handle the duplication ourselves.

  if (min_chan == 2){
    min_chan = 1;
    d_special_case_mono_to_stereo = true;
  }
  set_input_signature (gr_make_io_signature (min_chan, max_chan,
					     sizeof (float)));
  
  // fill in portions of the d_hw_params that we know now...

  // Specify the access methods we implement
  // For now, we only handle RW_INTERLEAVED...
  snd_pcm_access_mask_t *access_mask;
  snd_pcm_access_mask_alloca (&access_mask);
  snd_pcm_access_mask_none (access_mask);
  snd_pcm_access_mask_set (access_mask, SND_PCM_ACCESS_RW_INTERLEAVED);
  // snd_pcm_access_mask_set (access_mask, SND_PCM_ACCESS_RW_NONINTERLEAVED);

  if ((error = snd_pcm_hw_params_set_access_mask (d_pcm_handle,
						  d_hw_params, access_mask)) < 0)
    bail ("failed to set access mask", error);


  // set sample format
  if (!gri_alsa_pick_acceptable_format (d_pcm_handle, d_hw_params,
					acceptable_formats,
					NELEMS (acceptable_formats),
					&d_format,
					"audio_alsa_sink",
					CHATTY_DEBUG))
    throw std::runtime_error ("audio_alsa_sink");
  

  // sampling rate
  unsigned int orig_sampling_rate = d_sampling_rate;
  if ((error = snd_pcm_hw_params_set_rate_near (d_pcm_handle, d_hw_params,
						&d_sampling_rate, 0)) < 0)
    bail ("failed to set rate near", error);
  
  if (orig_sampling_rate != d_sampling_rate){
    fprintf (stderr, "audio_alsa_sink[%s]: unable to support sampling rate %d\n",
	     snd_pcm_name (d_pcm_handle), orig_sampling_rate);
    fprintf (stderr, "  card requested %d instead.\n", d_sampling_rate);
  }

  /*
   * ALSA transfers data in units of "periods".
   * We indirectly determine the underlying buffersize by specifying
   * the number of periods we want (typically 4) and the length of each
   * period in units of time (typically 1ms).
   */
  unsigned int min_nperiods, max_nperiods;
  snd_pcm_hw_params_get_periods_min (d_hw_params, &min_nperiods, &dir);
  snd_pcm_hw_params_get_periods_max (d_hw_params, &max_nperiods, &dir);
  //fprintf (stderr, "alsa_sink: min_nperiods = %d, max_nperiods = %d\n",
  // min_nperiods, max_nperiods);

  unsigned int orig_nperiods = d_nperiods;
  d_nperiods = std::min (std::max (min_nperiods, d_nperiods), max_nperiods);

  // adjust period time so that total buffering remains more-or-less constant
  d_period_time_us = (d_period_time_us * orig_nperiods) / d_nperiods;

  error = snd_pcm_hw_params_set_periods (d_pcm_handle, d_hw_params,
					 d_nperiods, 0);
  if (error < 0)
    bail ("set_periods failed", error);

  dir = 0;
  error = snd_pcm_hw_params_set_period_time_near (d_pcm_handle, d_hw_params,
						  &d_period_time_us, &dir);
  if (error < 0)
    bail ("set_period_time_near failed", error);

  dir = 0;
  error = snd_pcm_hw_params_get_period_size (d_hw_params,
					     &d_period_size, &dir);
  if (error < 0)
    bail ("get_period_size failed", error);
  
  set_output_multiple (d_period_size);
}


bool
audio_alsa_sink::check_topology (int ninputs, int noutputs)
{
  // ninputs is how many channels the user has connected.
  // Now we can finish up setting up the hw params...

  int nchan = ninputs;
  int err;

  // FIXME check_topology may be called more than once.
  // Ensure that the pcm is in a state where we can still mess with the hw_params

  bool special_case = nchan == 1 && d_special_case_mono_to_stereo;
  if (special_case)
    nchan = 2;
  
  err = snd_pcm_hw_params_set_channels (d_pcm_handle, d_hw_params, nchan);

  if (err < 0){
    output_error_msg ("set_channels failed", err);
    return false;
  }

  // set the parameters into the driver...
  err = snd_pcm_hw_params(d_pcm_handle, d_hw_params);
  if (err < 0){
    output_error_msg ("snd_pcm_hw_params failed", err);
    return false;
  }

  // get current s/w params
  err = snd_pcm_sw_params_current (d_pcm_handle, d_sw_params);
  if (err < 0)
    bail ("snd_pcm_sw_params_current", err);
  
  // Tell the PCM device to wait to start until we've filled
  // it's buffers half way full.  This helps avoid audio underruns.

  err = snd_pcm_sw_params_set_start_threshold(d_pcm_handle,
					      d_sw_params,
					      d_nperiods * d_period_size / 2);
  if (err < 0)
    bail ("snd_pcm_sw_params_set_start_threshold", err);

  // store the s/w params
  err = snd_pcm_sw_params (d_pcm_handle, d_sw_params);
  if (err < 0)
    bail ("snd_pcm_sw_params", err);

  d_buffer_size_bytes =
    d_period_size * nchan * snd_pcm_format_size (d_format, 1);

  d_buffer = new char [d_buffer_size_bytes];

  if (CHATTY_DEBUG)
    fprintf (stdout, "audio_alsa_sink[%s]: sample resolution = %d bits\n",
	     snd_pcm_name (d_pcm_handle),
	     snd_pcm_hw_params_get_sbits (d_hw_params));

  switch (d_format){
  case SND_PCM_FORMAT_S16:
    if (special_case)
      d_worker = &audio_alsa_sink::work_s16_1x2;
    else
      d_worker = &audio_alsa_sink::work_s16;
    break;

  case SND_PCM_FORMAT_S32:
    if (special_case)
      d_worker = &audio_alsa_sink::work_s32_1x2;
    else
      d_worker = &audio_alsa_sink::work_s32;
    break;

  default:
    assert (0);
  }

  return true;
}

audio_alsa_sink::~audio_alsa_sink ()
{
  if (snd_pcm_state (d_pcm_handle) == SND_PCM_STATE_RUNNING)
    snd_pcm_drop (d_pcm_handle);

  snd_pcm_close(d_pcm_handle);
  delete [] ((char *) d_hw_params);
  delete [] ((char *) d_sw_params);
  delete [] d_buffer;
}

int
audio_alsa_sink::work (int noutput_items,
		       gr_vector_const_void_star &input_items,
		       gr_vector_void_star &output_items)
{
  assert ((noutput_items % d_period_size) == 0);

  // this is a call through a pointer to a method...
  return (this->*d_worker)(noutput_items, input_items, output_items);
}

/*
 * Work function that deals with float to S16 conversion
 */
int
audio_alsa_sink::work_s16 (int noutput_items,
			   gr_vector_const_void_star &input_items,
			   gr_vector_void_star &output_items)
{
  typedef gr_int16	sample_t;	// the type of samples we're creating
  static const int NBITS = 16;		// # of bits in a sample
  
  unsigned int nchan = input_items.size ();
  const float **in = (const float **) &input_items[0];
  sample_t *buf = (sample_t *) d_buffer;
  int bi;
  int n;

  unsigned int sizeof_frame = nchan * sizeof (sample_t);
  assert (d_buffer_size_bytes == d_period_size * sizeof_frame);

  for (n = 0; n < noutput_items; n += d_period_size){

    // process one period of data
    bi = 0;
    for (unsigned int i = 0; i < d_period_size; i++){
      for (unsigned int chan = 0; chan < nchan; chan++){
	buf[bi++] = (sample_t) (in[chan][i] * (float) ((1L << (NBITS-1)) - 1));
      }
    }

    // update src pointers
    for (unsigned int chan = 0; chan < nchan; chan++)
      in[chan] += d_period_size;

    if (!write_buffer (buf, d_period_size, sizeof_frame))  
      return -1;	// No fixing this problem.  Say we're done.
  }

  return n;
}


/*
 * Work function that deals with float to S32 conversion
 */
int
audio_alsa_sink::work_s32 (int noutput_items,
			   gr_vector_const_void_star &input_items,
			   gr_vector_void_star &output_items)
{
  typedef gr_int32	sample_t;	// the type of samples we're creating
  static const int NBITS = 32;		// # of bits in a sample
  
  unsigned int nchan = input_items.size ();
  const float **in = (const float **) &input_items[0];
  sample_t *buf = (sample_t *) d_buffer;
  int bi;
  int n;

  unsigned int sizeof_frame = nchan * sizeof (sample_t);
  assert (d_buffer_size_bytes == d_period_size * sizeof_frame);

  for (n = 0; n < noutput_items; n += d_period_size){

    // process one period of data
    bi = 0;
    for (unsigned int i = 0; i < d_period_size; i++){
      for (unsigned int chan = 0; chan < nchan; chan++){
	buf[bi++] = (sample_t) (in[chan][i] * (float) ((1L << (NBITS-1)) - 1));
      }
    }

    // update src pointers
    for (unsigned int chan = 0; chan < nchan; chan++)
      in[chan] += d_period_size;

    if (!write_buffer (buf, d_period_size, sizeof_frame))  
      return -1;	// No fixing this problem.  Say we're done.
  }

  return n;
}

/*
 * Work function that deals with float to S16 conversion and
 * mono to stereo kludge.
 */
int
audio_alsa_sink::work_s16_1x2 (int noutput_items,
			       gr_vector_const_void_star &input_items,
			       gr_vector_void_star &output_items)
{
  typedef gr_int16	sample_t;	// the type of samples we're creating
  static const int NBITS = 16;		// # of bits in a sample
  
  assert (input_items.size () == 1);
  static const unsigned int nchan = 2;
  const float **in = (const float **) &input_items[0];
  sample_t *buf = (sample_t *) d_buffer;
  int bi;
  int n;

  unsigned int sizeof_frame = nchan * sizeof (sample_t);
  assert (d_buffer_size_bytes == d_period_size * sizeof_frame);

  for (n = 0; n < noutput_items; n += d_period_size){

    // process one period of data
    bi = 0;
    for (unsigned int i = 0; i < d_period_size; i++){
      sample_t t = (sample_t) (in[0][i] * (float) ((1L << (NBITS-1)) - 1));
      buf[bi++] = t;
      buf[bi++] = t;
    }

    // update src pointers
    in[0] += d_period_size;

    if (!write_buffer (buf, d_period_size, sizeof_frame))  
      return -1;	// No fixing this problem.  Say we're done.
  }

  return n;
}

/*
 * Work function that deals with float to S32 conversion and
 * mono to stereo kludge.
 */
int
audio_alsa_sink::work_s32_1x2 (int noutput_items,
			       gr_vector_const_void_star &input_items,
			       gr_vector_void_star &output_items)
{
  typedef gr_int32	sample_t;	// the type of samples we're creating
  static const int NBITS = 32;		// # of bits in a sample
  
  assert (input_items.size () == 1);
  static unsigned int nchan = 2;
  const float **in = (const float **) &input_items[0];
  sample_t *buf = (sample_t *) d_buffer;
  int bi;
  int n;

  unsigned int sizeof_frame = nchan * sizeof (sample_t);
  assert (d_buffer_size_bytes == d_period_size * sizeof_frame);

  for (n = 0; n < noutput_items; n += d_period_size){

    // process one period of data
    bi = 0;
    for (unsigned int i = 0; i < d_period_size; i++){
      sample_t t = (sample_t) (in[0][i] * (float) ((1L << (NBITS-1)) - 1));
      buf[bi++] = t;
      buf[bi++] = t;
    }

    // update src pointers
    in[0] += d_period_size;

    if (!write_buffer (buf, d_period_size, sizeof_frame))  
      return -1;	// No fixing this problem.  Say we're done.
  }

  return n;
}

bool
audio_alsa_sink::write_buffer (const void *vbuffer,
			       unsigned nframes, unsigned sizeof_frame)
{
  const unsigned char *buffer = (const unsigned char *) vbuffer;

  while (nframes > 0){
    int r = snd_pcm_writei (d_pcm_handle, buffer, nframes);
    if (r == -EAGAIN)
      continue;			// try again

    else if (r == -EPIPE){	// underrun
      d_nunderuns++;
      fputs ("aU", stderr);
      if ((r = snd_pcm_prepare (d_pcm_handle)) < 0){
	output_error_msg ("snd_pcm_prepare failed. Can't recover from underrun", r);
	return false;
      }
      continue;			// try again
    }

    else if (r == -ESTRPIPE){	// h/w is suspended (whatever that means)
				// This is apparently related to power management
      d_nsuspends++;
      if ((r = snd_pcm_resume (d_pcm_handle)) < 0){
	output_error_msg ("failed to resume from suspend", r);
	return false;
      }
      continue;			// try again
    }

    else if (r < 0){
      output_error_msg ("snd_pcm_writei failed", r);
      return false;
    }

    nframes -= r;
    buffer += r * sizeof_frame;
  }

  return true;
}


void
audio_alsa_sink::output_error_msg (const char *msg, int err)
{
  fprintf (stderr, "audio_alsa_sink[%s]: %s: %s\n",
	   snd_pcm_name (d_pcm_handle), msg,  snd_strerror (err));
}

void
audio_alsa_sink::bail (const char *msg, int err) throw (std::runtime_error)
{
  output_error_msg (msg, err);
  throw std::runtime_error ("audio_alsa_sink");
}