/*  VLSI Logic VS1001 MPEG Decoder

    14.09.2001: tk, the DMA is now disabled during SCI accesses.
                    (see datasheet: After using the Serial Command
                     Interface, it is not allowed to send SCI or SDI
                     data for 5 microseconds)
    30.08.2001: tk, small changes for final VS1001k revision.
                    - GetBitrate() removed.
                    - GetSamplerate() removed.
                    These functions can be simulated by parsing
                    the mp3 header anyways.
    15.06.2001: tk, initial implementation.

    Copyright (c)2000 by Thomas Kindler, thomas.kindler@gmx.de

    This program 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 of
    the License, or (at your option) any later version. Read the
    full License at http://www.gnu.org/copyleft for more details.
*/

// include files -----
//
#include <stdlib.h>
#include <stdio.h>
#include "sc12/amd186.h"
#include "sc12/hwapi.h"
#include "sc12/rtos.h"
#include "misc.h"
#include "vs1001.h"

// Some VS1001 settings, so we can restore
// them after a reset.
//
static int  lastVolLeft, lastVolRight;
static long lastFrequency;
static bool lastBassBoost;

// old DMA terminal count handler
//
static void interrupt (*oldDmaHandler)(...);

// user defined callback function
//
static void (*callbackFunction)(void *);
static void  *callbackParam;


static void SCI_Write(unsigned data)
{
  for (int i=15; i>=0; i--) {
    if (data & 1<<i)
      AMD_PioHigh(AMD_PIOPIN[SCI_SIPIO]);
    else
      AMD_PioLow(AMD_PIOPIN[SCI_SIPIO]);
    AMD_PioHigh(AMD_PIOPIN[SCI_SCLKPIO]);
    AMD_PioLow(AMD_PIOPIN[SCI_SCLKPIO]);
  }
}


static unsigned SCI_Read()
{
  unsigned data = 0;
  for (int i=15; i>=0; i--) {
    AMD_PioHigh(AMD_PIOPIN[SCI_SCLKPIO]);
    if (AMD_GetPio(AMD_PIOPIN[SCI_SOPIO]))
      data |= 1<<i;
    AMD_PioLow(AMD_PIOPIN[SCI_SCLKPIO]);
  }
  return data;
}


static unsigned SCI_ReadReg(int address)
{
  AMD_EnableDrq(SDI_DMACHANNEL, 0);
  AMD_PioLow(AMD_PIOPIN[SCI_SCLKPIO]);
  AMD_PioLow(AMD_PIOPIN[SCI_XCSPIO]);
  SCI_Write( 0x0300 | address );
  unsigned data = SCI_Read();
  AMD_PioHigh(AMD_PIOPIN[SCI_XCSPIO]);
  usleep(5);
  AMD_EnableDrq(SDI_DMACHANNEL, 1);
  return data;
}


static void SCI_WriteReg(int address, unsigned data)
{
  AMD_EnableDrq(SDI_DMACHANNEL, 0);
  AMD_PioLow(AMD_PIOPIN[SCI_SCLKPIO]);
  AMD_PioLow(AMD_PIOPIN[SCI_XCSPIO]);
  SCI_Write( 0x0200 | address );
  SCI_Write( data );
  AMD_PioHigh(AMD_PIOPIN[SCI_XCSPIO]);
  usleep(5);
  AMD_EnableDrq(SDI_DMACHANNEL, 1);
}


/**
 * Interrupt handler for DMA channel terminal count.
 * This handler will call the user specified callback
 * function, after the playback of a data block has
 * finished.
 */
static void interrupt DmaHandler(...)
{
  if (callbackFunction)
    callbackFunction(callbackParam);
  oldDmaHandler();
}


/**
 * Check asynchronous playback status.
 *
 * \returns true if playing, false if stopped.
 */
bool VS_IsPlaying()
{
  DmaInfo  dma;

  // check DMA status
  //
  AMD_GetDmaInfo(SDI_DMACHANNEL, &dma);
  return dma.control & DCON_ST ? true : false;
}


/**
 * Asynchronously play a block of MPEG data. You can either specify
 * a callback function, or poll the transfer status using VS_IsPlaying()
 *
 * \note according to the VS1001 documentation, you should keep an
 *       eye on the data rate. If DREQ is either always asserted, or
 *       always inactive, the decoder most probably choked on some
 *       bad mpeg-stream.
 *
 * \param data      Pointer to mpeg data
 * \param len       Length of data block
 * \param function  function to call after transfer has finished.
                    May be NULL.
 * \param param     user defined parameter that is passed to the
 *                  callback function. May be NULL.
 */
void VS_PlayMpegAsync( char *data, unsigned len,
                       void (*function)(void *),
                       void  *param )
{
  // set up DMA channel
  //
  DmaInfo  dma;
  dma.control = DCON_DSYNC | DCON_SMIO | DCON_SINC | DCON_TC | DCON_INT;
  dma.length  = len;
  dma.srcMem  = data;
  dma.dstPort = SDI_DATAREG;

  callbackFunction = function;
  callbackParam    = param;

  // enable DMA request pin
  //
  AMD_StartDma(SDI_DMACHANNEL, &dma);
  AMD_EnableDrq(SDI_DMACHANNEL, true);
}


/**
 * Play a block of MPEG Data, and wait for completion.
 *
 * \note  you should call VS_Reset after a failed transfer.
 *        Most probably the VS1001 choked on some bad mpeg
 *        stream, and will continue to work after an reset.
 *
 * \param   data  Pointer to mpeg data
 * \param   len   Length of data block
 *
 * \todo    (re)implement timeout checking, using
 *          RTX_Get_System_Ticks()
 */
int VS_PlayMpegSync(char *data, unsigned len)
{
  VS_PlayMpegAsync(data, len, NULL, 0);
  while (VS_IsPlaying());

  return VS_OK;
}


/**
 * Send a stream of zeros to the VS1001. This function
 * can be used to flush the internal bitstream buffer
 * of the 1001.
 *
 * \param  length  number of zeros to write.
 */
void SDI_WriteZeros(unsigned length)
{
  char zeros[256] = {0};
  int  block;

  while (length > 0) {
    block = min(length, sizeof(zeros));
    VS_PlayMpegSync(zeros, block);
    length -= block;;
  }
}


/**
 * Stop playback of current stream.
 *
 */
void VS_Stop()
{
  // stop DMA channel
  AMD_StopDma(SDI_DMACHANNEL);
}


/**
 * Set the output volume for both channels.
 * 255 is the maximum volume, 0 the minimum. Note that
 * a volume setting of 0 will enter the analog powersave
 * mode, and causes a slight snapping sound. If you want
 * to avoid this, set the volume to 1 instead of 0.
 *
 * \param  left  left volume  0..255
 * \param  right right volume 0..255
 */
void VS_SetVolume(int left, int right)
{
  lastVolLeft = left;
  lastVolRight = right;
  SCI_WriteReg(SCI_VOL, ((255-left)<<8) | (255-right));
}


/**
 * Get MPEG Header information.
 * Returns the current header for the stream being decoded.
 * Right after reset, 0 will be returned, indicating that no
 * data has been found yet.
 *
 * \param   pointer to MpegHeader structure to fill out
 */
void VS_GetMPEGHeader(unsigned long *header)
{
  ((unsigned *)header)[0] = SCI_ReadReg(SCI_HDAT0);
  ((unsigned *)header)[1] = SCI_ReadReg(SCI_HDAT1);
}


/**
 * Set Clock frequency.
 * This function needs to be called if the chip is running at
 * something else than 24.576MHz. The Clock frequency may
 * be between  0 .. 65.534 MHz, although hardware limits the
 * highest possible speed.
 *
 * \note   the clock doubler is activated for frequencies
 *         <18,432 MHz. This should be reasonable for all
 *         operation conditions.
 * \param  frequency  Clock frequency in Hz
 */
void VS_SetClockFreq(long frequency)
{
  lastFrequency = frequency;
  if (frequency < 18432000)
    SCI_WriteReg(SCI_CLOCKF, (frequency/2000) | 0x8000);
  else
    SCI_WriteReg(SCI_CLOCKF, (frequency/2000));
}


/**
 * Enable bass and treble enhancement (VS1001k only).
 *
 * \param onoff  enable or disable enhancements
 */
void VS_SetBassBoost(bool onoff)
{
  lastBassBoost = onoff;
  if (onoff)
    SCI_WriteReg(SCI_MODE, SCI_ReadReg(SCI_MODE) | SM_BASS);
  else
    SCI_WriteReg(SCI_MODE, SCI_ReadReg(SCI_MODE) & ~SM_BASS);
}


/**
 * Test SDI Interface - generate short beeps.
 * \note  the test will be very loud. Do not keep your
 *        headphones on while conducting this test.
 */
void VS_SDITest()
{
  char beepOn[] = { 0x53, 0xef, 0x6e, 0x3e, 0, 0, 0, 0 };
  char beepOff[]= { 0x45, 0x78, 0x69, 0x74, 0, 0, 0, 0 };

  printf("Testing SDI interface");
  for (int i=0; i<25; i++) {
    VS_PlayMpegSync(beepOn, sizeof(beepOn));
    RTX_Sleep_Time(100);
    VS_PlayMpegSync(beepOff, sizeof(beepOff));
    RTX_Sleep_Time(100);
    printf(".");
  }
  printf(" ok.\n");
}


/**
 * Test SCI Interface - generate snapping sounds.
 *
 */
void VS_SCITest()
{
  printf("Testing SCI interface");
  for (int i=0; i<25; i++) {
    VS_SetVolume(0, 0);
    RTX_Sleep_Time(100);
    VS_SetVolume(255,255);
    RTX_Sleep_Time(100);
    printf(".");
  }
  printf(" ok.\n");
}


/**
 * Dump all SCI register contents.
 *
 */
void VS_Dump()
{
  printf("VS1001 SCI registers ---\n");
  printf("  MODE    = %04x\n", SCI_ReadReg(SCI_MODE));
  printf("  STATUS  = %04x\n", SCI_ReadReg(SCI_STATUS));
  printf("  CLOCKF  = %04x\n", SCI_ReadReg(SCI_CLOCKF));
  printf("  DECTIME = %04x\n", SCI_ReadReg(SCI_DECTIME));
  printf("  AUDATA  = %04x\n", SCI_ReadReg(SCI_AUDATA));
  printf("  HDAT0   = %04x\n", SCI_ReadReg(SCI_HDAT0));
  printf("  HDAT1   = %04x\n", SCI_ReadReg(SCI_HDAT1));
  printf("  A1ADDR  = %04x\n", SCI_ReadReg(SCI_A1ADDR));
  printf("  VOL     = %04x\n", SCI_ReadReg(SCI_VOL));
  printf("  A1CTRL0 = %04x\n", SCI_ReadReg(SCI_A1CTRL0));
  printf("  A1CTRL1 = %04x\n", SCI_ReadReg(SCI_A1CTRL1));
  printf("  A1CTRL2 = %04x\n", SCI_ReadReg(SCI_A1CTRL2));
}


/**
 * Initiate a software reset.
 * All previous VS1001 settings are automatically restored by
 * this function.
 *
 */
void VS_Reset()
{
  AMD_EnableDrq(SDI_DMACHANNEL, 0);
  SCI_WriteReg(SCI_MODE, SM_RESET);
  usleep(500);
  if (SCI_ReadReg(SCI_MODE) == SM_RESET)
    printf( "OMFG: VS1001 Software reset failed.\n"
            "  Why don't they implement a watchdog timer,\n"
            "  or a reset bit that *works*?! Go and write\n"
            "  some mail to info@vlsi.fi :)\n"
          );
  VS_SetClockFreq(lastFrequency);
  VS_SetVolume(lastVolLeft, lastVolRight);
  VS_SetBassBoost(lastBassBoost);
  SDI_WriteZeros(2);
}


/**
 * Set up hardware resources for the VS1001 interface.
 *
 */
void VS_Init()
{
  printf("SCI_Init(): initializing VS1001...\n");

  // enable SCI PIOs
  //
  pfe_enable_bus(0xff, true);
  pfe_enable_pcs(SDI_DATAREG >> 8);

  pfe_enable_pio(SCI_XCSPIO,  PIO_O1  );
  pfe_enable_pio(SCI_SCLKPIO, PIO_O0  );
  pfe_enable_pio(SCI_SIPIO,   PIO_O0  );
  pfe_enable_pio(SCI_SOPIO,   PIO_IPU );

  // install DMA terminal count interrupt
  //
  oldDmaHandler = getvect(10);
  setvect(10, DmaHandler);

  // initialize settings, and reset the VS1001
  //
  lastVolLeft   = 255;
  lastVolRight  = 255;
  lastFrequency = VS_CLOCK;
  lastBassBoost = true;
  VS_Reset();
}


/**
 * Free all resources used by the VS1001 interface.
 *
 */
void VS_Done()
{
  // disable DMA channel
  //
  VS_Stop();

  if (oldDmaHandler)
    setvect(10, oldDmaHandler);

  // disable PIOs
  //
  /**
    \bug: strange, if you uncomment these lines, the
    terminal count interrupt won't be called next time.
    (or may it be, that the DRQ pin stops working?!?)
  */
  /*  pfe_enable_pio(SCI_XCSPIO,  PIO_IPU );
  pfe_enable_pio(SCI_SCLKPIO, PIO_IPU );
  pfe_enable_pio(SCI_SIPIO,   PIO_IPU );
  pfe_enable_pio(SCI_SOPIO,   PIO_IPU );
  pfe_enable_pio(SDI_DRQPIO,  PIO_IPU );  */
}
