/*
 * Purpose: Multi channel playback support for devices with multiple stereo engines.
 *
 * Some sound cards don't provide single multi channel output engine. Instead
 * they have multiple stereo output pairs connected to the front, side, 
 * center/LFE and rear speakers. The remux driver is used by such drivers to
 * redistribute multi channel streams to the individual stereo engines.
 */
/*
 * This file is part of Open Sound System
 *
 * Copyright (C) 4Front Technologies 1996-2007.
 *
 * This software is released under CDDL 1.0 source license for operating systems
 * that have their full source code available under the BSD or CDDL licenses.
 *
 * See the COPYING file included in the main directory of this source
 * distribution. Please contact sales@opensound.com for further info.
 */

/*
 * NOTE! This is not the development version of this file. Please download the
 * sources of the latest Open Sound System version from 
 * http://developer.opensound.com/sources if you plan to make any changes
 * to the software.
 */

#include "oss_config.h"

#ifndef USE_REMUX
#error remux.c is not compatible with this architecture (endianess)
#endif

#define HACK_HACK_HACK

#ifdef HACK_HACK_HACK
static int tune = 3;
#endif

#ifdef OSS_BIG_ENDIAN
void
remux_install (char *name, oss_device_t * osdev, int frontdev, int reardev,
	       int center_lfe_dev, int surrounddev)
{
  /* Not compatible with big endian yet */
}

#else

#include "remux.h"

static char *chnames[MAX_SUBDEVS] = {
  "Front",
  "Surr",
  "C&L",
  "Rear"
};

static const int bindings[MAX_SUBDEVS] = {
  DSP_BIND_FRONT,
  DSP_BIND_SURR,
  DSP_BIND_CENTER_LFE,
  DSP_BIND_REAR
};

static remux_devc dev_info = { 0 }, *devc = &dev_info;

/*
 * Audio routines
 */
static void
remux_callback (int dev, int parm)
{
  oss_audio_outputintr (devc->audio_dev, 1);
}

static int
remux_set_rate (int dev, int arg)
{
  remux_devc *devc = audio_engines[dev]->devc;

  return devc->speed =
    audio_engines[devc->physdev[0]]->d->set_rate (devc->physdev[0], arg);
}

static short
remux_set_channels (int dev, short arg)
{
  remux_devc *devc = audio_engines[dev]->devc;

  if (arg == 0)
    return devc->channels;

  arg &= ~1;			/* Make sure we have even number of channels */

  if (arg < 2)
    return devc->channels = 2;
  if (arg > devc->maxchannels)
    return devc->channels = devc->maxchannels;

  return devc->channels = arg;
}

static unsigned int
remux_set_format (int dev, unsigned int arg)
{
  return AFMT_S16_NE;
}

static int
remux_ioctl (int dev, unsigned int cmd, ioctl_arg arg)
{
  return -EINVAL;
}

static void remux_trigger (int dev, int state);

static void
remux_reset (int dev)
{
  remux_trigger (dev, 0);
}

static int
remux_open (int dev, int mode, int open_flags)
{
  remux_devc *devc = audio_engines[dev]->devc;
  adev_p adev = audio_engines[devc->audio_dev];
  oss_native_word flags;
  int i, j, err;

  if (mode & OPEN_READ)
    {
      cmn_err (CE_WARN, "Audio device %d cannot do recording\n", dev);
      /* return -ENOTSUP; */
    }

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);

  if (devc->open_mode != 0)
    {
      MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

      return -EBUSY;
    }
  devc->open_mode = mode;
  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

  for (i = 0; i < devc->n_physdevs; i++)
    {
      adev_p pdev = audio_engines[devc->physdev[i]];

      devc->finfo[i].mode = OPEN_WRITE;
      devc->finfo[i].flags = 0;
      if ((err =
	   oss_audio_open_engine (devc->physdev[i], OSS_DEV_DSP,
				  &devc->finfo[i], 1, OF_SMALLFRAGS,
				  NULL)) < 0)
	{
	  for (j = 0; j < i - 1; j++)
	    oss_audio_release (devc->physdev[j], &devc->finfo[j]);

	  devc->open_mode = 0;
	  return err;
	}


      strcpy (pdev->cmd, chnames[i]);
      pdev->pid = 0;
      pdev->cooked_enable = 0;

      if (pdev->d->bind != NULL)
	{
	  int b = bindings[i];
	  pdev->d->bind (pdev->engine_num, SNDCTL_DSP_BIND_CHANNEL,
			 (ioctl_arg) & b);
	}

      if (pdev->flags & ADEV_FIXEDRATE)
	{
	  adev->flags |= ADEV_FIXEDRATE;
	  adev->fixed_rate = pdev->fixed_rate;
	}
      else
	adev->flags &= ~(ADEV_FIXEDRATE);

    }

  devc->speed = 48000;
  devc->channels = 2;
  adev->cooked_enable = 0;

  return 0;
}

static void
remux_close (int dev, int mode)
{
  int i;
  remux_devc *devc = audio_engines[dev]->devc;

  for (i = 0; i < devc->n_physdevs; i++)
    oss_audio_release (devc->physdev[i], &devc->finfo[i]);
  devc->open_mode = 0;
}

#if 0
static int sinebuf[48] = {

  0, 4276, 8480, 12539, 16383, 19947, 23169, 25995,
  28377, 30272, 31650, 32486, 32767, 32486, 31650, 30272,
  28377, 25995, 23169, 19947, 16383, 12539, 8480, 4276,
  0, -4276, -8480, -12539, -16383, -19947, -23169, -25995,
  -28377, -30272, -31650, -32486, -32767, -32486, -31650, -30272,
  -28377, -25995, -23169, -19947, -16383, -12539, -8480, -4276
};
#endif

static void
remux_output_block (int dev, oss_native_word buf, int count,
		    int fragsize, int intrflag)
{
/*
 * This routine does the actual de-interleaving of stereo pairs to the
 * individual devices. All data movement is done based on sample pairs
 * 2*16=32 bit (2*short=int). Native endianess (AFMT_S16_NE) is assumed.
 */
  adev_p adev = audio_engines[dev], pdev = audio_engines[devc->physdev[0]];
  remux_devc *devc = adev->devc;
  dmap_p dmap = adev->dmap_out, pdmap = pdev->dmap_out;

  int ch, nc, nf = pdmap->nfrags;

  int *inbuf;
  int ptr, pos = 0;

  ptr = ((unsigned long) dmap->byte_counter) % dmap->bytes_in_use;

  inbuf = (int *) (dmap->dmabuf + ptr);

  nc = devc->channels / 2;

  for (ch = 0; ch < nc; ch++)
    {
      int *outbuf;
      int i, ns;

      pdev = audio_engines[devc->physdev[ch]];
      pdmap = pdev->dmap_out;
      if (ch == 0)
	pos =
	  (pdmap->user_counter + pdmap->fragment_size) % pdmap->bytes_in_use;

      outbuf = (int *) (pdmap->dmabuf + pos);

      ns = dmap->fragment_size / (4 * nc);

/*cmn_err(CE_CONT, "Loop %d, pos=%x, ns=%d, fs=%d/%d\n", ch, pos, ns, dmap->fragment_size, pdmap->fragment_size);*/
      for (i = 0; i < ns; i++)
	{
#if 0
	  static int p[MAX_SUBDEVS] = { 0 };

	  short *s = (short *) &outbuf[i];
	  s[0] = s[1] = sinebuf[p[ch]];
	  p[ch] = (p[ch] + 1) % 48;
#else
	  outbuf[i] = inbuf[(i * nc) + ch];
#endif
	}

      pdmap->user_counter += ns * 4;
/*cmn_err(CE_CONT, "User %d, phys %d\n", pdmap->user_counter, pdmap->byte_counter);*/
    }

/*cmn_err(CE_CONT, "%d/%d\n", pdmap->user_counter, pdmap->byte_counter);*/
  if (pdmap->user_counter - pdmap->byte_counter <
      pdmap->fragment_size * (nf / 2))
    {
      oss_audio_outputintr (devc->audio_dev, 1);
    }
}

static void
remux_start_input (int dev, oss_native_word buf, int count,
		   int fragsize, int intrflag)
{
}

static void
remux_trigger (int dev, int state)
{
  remux_devc *devc = audio_engines[dev]->devc;

  int i;

  for (i = 0; i < devc->n_physdevs; i++)
    {
      int pd = devc->physdev[i];
      adev_p pdev = audio_engines[pd];

      pdev->d->trigger (pd, state);
      if (state & PCM_ENABLE_OUTPUT)
	pdev->dmap_out->flags |= DMAP_STARTED;
      else
	pdev->dmap_out->flags &= ~DMAP_STARTED;
    }
}

static int
remux_prepare_for_input (int dev, int bsize, int bcount)
{
  return -EIO;
}

static int
remux_prepare_for_output (int dev, int bsize, int bcount)
{
  remux_devc *devc = audio_engines[dev]->devc;
  adev_p adev = audio_engines[dev];
  int i, err, tmp, nd;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);

  nd = devc->channels / 2;
#ifdef HACK_HACK_HACK
  bsize = 2400;			/* Must be divisible by 3*4 and 4*4 */
#else
  bsize = 240;			/* Must be divisible by 3*4 and 4*4 */
#endif

/*cmn_err(CE_CONT, "Setting up %d channel playback\n", devc->channels);*/

  for (i = 0; i < devc->n_physdevs; i++)
    {
      int pd = devc->physdev[i];
      adev_p pdev = audio_engines[pd];
      dmap_p dmap = pdev->dmap_out;

      tmp = pdev->d->set_format (pd, AFMT_S16_NE);
      if (tmp != AFMT_S16_NE)
	{
	  cmn_err (CE_NOTE, "remux: Bad sample format %x\n", tmp);
	  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
	  return -EIO;
	}

      tmp = pdev->d->set_channels (pd, 2);
      if (tmp != 2)
	{
	  cmn_err (CE_NOTE, "remux: Bad number of channels %d\n", tmp);
	  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
	  return -EIO;
	}

      tmp = pdev->d->set_rate (pd, devc->speed);
      if (tmp != devc->speed)
	{
	  cmn_err (CE_NOTE, "remux: Bad sample rate %d\n", tmp);
	  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
	  return -EIO;
	}

      if (pdev->min_block > 0 && bsize < pdev->min_block)
	bsize = pdev->min_block;
      if (pdev->max_block > 0 && bsize > pdev->max_block)
	bsize = pdev->max_block;

      dmap->fragment_size = bsize;

#ifdef HACK_HACK_HACK
      bcount = 2 * tune;
#else
      bcount = dmap->buffsize / bsize;
#endif

      dmap->bytes_in_use = bcount * bsize;
      dmap->nfrags = bcount;

/*cmn_err(CE_CONT, "Pdev%d %d*%d=%d\n", pd, dmap->nfrags, dmap->fragment_size, dmap->bytes_in_use);*/

      if ((err = pdev->d->prepare_for_output (pd, bsize, bcount)) < 0)
	{
	  cmn_err (CE_WARN,
		   "remux: Preparing device #%d failed, error %d %d\n", pd,
		   err);
	  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
	  return err;
	}

      if (dmap->dmabuf == NULL)
	{
	  cmn_err (CE_WARN, "dmabuf==NULL\n");
	  return -ENOMEM;
	}

      memset (dmap->dmabuf, 0, dmap->buffsize);

      if (i == 0)
	dmap->audio_callback = remux_callback;
      dmap->dma_mode = PCM_ENABLE_OUTPUT;
      dmap->flags |= DMAP_PREPARED;
      dmap->data_rate = devc->speed * 4;
    }

  adev->dmap_out->fragment_size = bsize * nd;
  adev->dmap_out->nfrags =
    adev->dmap_out->bytes_in_use / adev->dmap_out->fragment_size;
  adev->dmap_out->nfrags &= ~1;

  if (adev->dmap_out->nfrags < 2)
    adev->dmap_out->nfrags = 2;
  adev->dmap_out->bytes_in_use =
    adev->dmap_out->fragment_size * adev->dmap_out->nfrags;

/*cmn_err(CE_CONT, "Adev %d*%d=%d\n", adev->dmap_out->nfrags, adev->dmap_out->fragment_size, adev->dmap_out->bytes_in_use);*/
  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

  return 0;
}

static audiodrv_t remux_driver = {
  remux_open,
  remux_close,
  remux_output_block,
  remux_start_input,
  remux_ioctl,
  remux_prepare_for_input,
  remux_prepare_for_output,
  remux_reset,
  NULL,
  NULL,
  NULL,
  NULL,
  remux_trigger,
  remux_set_rate,
  remux_set_format,
  remux_set_channels,
  NULL,
  NULL,
  NULL,
  NULL,
  NULL,				/* remux_alloc_buffer */
  NULL,				/* remux_free_buffer */
  NULL,
  NULL,
  NULL
};

static int
check_physdev (int dev)
{
  adev_p adev = audio_engines[dev];

  if (!(adev->oformat_mask & AFMT_S16_NE))
    {
      cmn_err (CE_NOTE,
	       "remux: Audio device %d doesn't support the 16 bit format\n",
	       dev);
      return 0;
    }

  if (adev->flags & (ADEV_NOOUTPUT))
    {
      cmn_err (CE_NOTE, "remux: Audio device %d doesn't support output\n",
	       dev);
      return 0;
    }

#if 0
  if (!(adev->flags & ADEV_AUTOMODE))
    {
      cmn_err (CE_NOTE, "remux: Audio device %d doesn't support auto-mode\n",
	       dev);
      return 0;
    }
#endif

  return 1;
}

static int
countdevs (int frontdev, int reardev, int center_lfe_dev, int surrounddev)
{
  if (frontdev < 0 || frontdev >= num_audio_engines)
    return 0;
  if (reardev < 0 || reardev >= num_audio_engines)
    return 2;
  if (center_lfe_dev < 0 || center_lfe_dev >= num_audio_engines)
    return 4;
  if (surrounddev < 0 || surrounddev >= num_audio_engines)
    return 6;

  return 8;
}

void
remux_install (char *name, oss_device_t * osdev, int frontdev, int reardev,
	       int center_lfe_dev, int surrounddev)
{
  adev_p adev, pdev;
  int n, i;

  DDB (cmn_err (CE_CONT, "remux install %s: %d, %d, %d, %d\n",
		name, frontdev, reardev, center_lfe_dev, surrounddev));

  devc->osdev = osdev;
  MUTEX_INIT (devc->osdev, devc->mutex, MH_DRV + 2);

  n = countdevs (frontdev, reardev, center_lfe_dev, surrounddev);

  if (n < 4)
    {
      cmn_err (CE_WARN, "remux: Bad devices\n");
      return;
    }

  devc->maxchannels = n;
  devc->n_physdevs = n / 2;
  devc->physdev[0] = frontdev;
  devc->physdev[1] = reardev;
  devc->physdev[2] = center_lfe_dev;
  devc->physdev[3] = surrounddev;

  for (i = 0; i < devc->n_physdevs; i++)
    if (!check_physdev (devc->physdev[i]))
      return;

  pdev = audio_engines[frontdev];

  if ((devc->audio_dev = oss_install_audiodev (OSS_AUDIO_DRIVER_VERSION,
					       devc->osdev,
					       devc->osdev,
					       name,
					       &remux_driver,
					       sizeof (audiodrv_t),
					       ADEV_NOINPUT | ADEV_SPECIAL |
					       ADEV_DISABLE_VIRTUAL |
					       ADEV_NOSRC, AFMT_S16_NE, NULL,
					       -1)) < 0)
    {
      devc->audio_dev = -1;
      return;
    }

  adev = audio_engines[devc->audio_dev];

  adev->devc = devc;
  adev->min_channels = 2;
  adev->max_channels = devc->maxchannels;
  adev->min_rate = pdev->min_rate;
  adev->max_rate = pdev->max_rate;
  adev->rate_source = frontdev;
  adev->caps |= DSP_CH_MULTI;
  if (pdev->caps & PCM_CAP_FREERATE)
    adev->caps |= PCM_CAP_FREERATE;

  devc->open_mode = 0;
}

#endif
