/*
 * audio_amp.c
 * Copyright (C) 2002 P.Everett <peter@everett9981.freeserve.co.uk>
 *
 * This file is part of KinoAMP, a free RISCOS MPEG program stream decoder.
 *
 * KinoAMP 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.
 *
 * KinoAMP 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "kernel.h"
#include "swis.h"

#include "audiocodec.h"
#include "config.h"
#include "amplayer.h"
#include "playlist.h"
#include "ka_codecs.h"
#include "ka_mem.h"
#include "ka_log.h"
#include "timer1.h"

#define FUDGE_FACTOR 15000 // audio delay correction

#define BIT_31 ((int) 0x80000000)

// Define the AMPlayer stream input fifo buffers
#define A_FIFO_NUM 32
#define FIFO_SIZE 4096

typedef struct
{
  uint32_t pts;
  int usage;               // reset by AMPlayer after data read
  int meta;                // points to meta data or zero if none
  int len;                 // length of data following
  char data[FIFO_SIZE-16]; // data
} am_block;

typedef struct
{
  ka_audiocodec_t hdr;
  int handle;              // AMPlayer stream handle
  int instance;            // AMPlayer instance
  int sound_area;
  int percent2mpeg;
  int buff_usage;
  int done;
  am_block* am_start;
  am_block* am_write;
  am_block* am_end;
  am_block* fifo;
} data_t;

//#define OP_BUFF 393216  // 2.23 secs of 44.1k
#define OP_BUFF 262144  // 1.49 secs of 44.1k
//#define OP_BUFF 131072  // 743ms of 44.1k

static int amp_allow(const ka_aparams_t* params)
{
  switch(params->typ)
  {
    case KA_ABLOCK_MPEG:
      return 1;
    break;
    default:
      return 0;
  }
}

/**
 * Allocates and initializes codec data, links to player module.
 *
 * @param  ppcodec  Pointer to fill with a pointer to the codec's data.
 *
 * @returns Pointer to codec data if OK, else NULL and error block is filled.
 */
static ka_audiocodec_t* amp_open(ka_error_t* pErrorBlock)
{
  _kernel_swi_regs regs;
  int rc;
  data_t* pcodec;

  // Check that we have the required modules
  rc = rm_ensure(pErrorBlock, "SharedSound", 024, "System:Modules.SSound");
  if (!rc) rc = rm_ensure(pErrorBlock, "AMPlayer", 136, "System:Modules.Audio.MP3.AMPlayer");
  if (rc)
    return NULL;

  if ((pcodec = ka_mem_calloc(sizeof(*pcodec))) == NULL)
  {
    ka_error_fill(pErrorBlock, ka_error_nomem);
    return NULL;
  }

  pcodec->hdr.vptr = &kav_amp_codec;
  pcodec->hdr.pErrorBlock = pErrorBlock;

  do // so we can use breaks
  {
    // Create a dynamic area for the audio fifo
    regs.r[0] = 0; // create area
    regs.r[1] = -1;
    regs.r[2] = sizeof(*pcodec->fifo) * A_FIFO_NUM;
    regs.r[3] = -1;
    regs.r[4] = 128; // user cannot modify (green bar)
    regs.r[5] = sizeof(*pcodec->fifo) * A_FIFO_NUM;
    regs.r[6] = NULL;
    regs.r[7] = NULL;
    regs.r[8] = (int)"KinoAMP Sound";
    if (_kernel_swi(OS_DynamicArea, &regs, &regs))
    {
      ka_error_fill(pErrorBlock, "error12"); // (Cannot allocate audio buffer)
      break; // cannot continue with initialisation
    }
    pcodec->sound_area = regs.r[1];
    pcodec->fifo = (am_block*) regs.r[3];
    pcodec->am_start = pcodec->fifo;
    pcodec->am_end = pcodec->fifo + A_FIFO_NUM;

    // Create an AMPlayer instance
    regs.r[0] = 0;
    regs.r[1] = 1;
    regs.r[2] = (int)"Soundtrack";
    if (_kernel_swi(AMPlayer_Instance, &regs, &regs))
    {
      ka_error_fill(pErrorBlock, "error13"); // (Cannot create AMPlayer instance)
      break; // cannot continue with initialisation
    }
    pcodec->instance = regs.r[0];

    // Write output buffer size
    regs.r[0] = 1 + BIT_31;
    regs.r[1] = OP_BUFF;
    regs.r[8] = pcodec->instance;
    _kernel_swi(AMPlayer_Control, &regs, &regs);
  }
  while(0);

  // Errors occured?
  if (!pcodec->instance)
  {
    if (pcodec->sound_area)
    {
      // free buffer
      regs.r[0] = 1;
      regs.r[1] = pcodec->sound_area;
      _kernel_swi(OS_DynamicArea, &regs, &regs);
    }

    ka_mem_free(pcodec);
    return NULL;
  }

  return &pcodec->hdr;
}

/**
 * Unlinks from the player module and releases the codec's data.
 *
 * @param  p  Pointer to the codec's data.
 */
static void amp_close(ka_audiocodec_t** p)
{
  data_t* pcodec = (data_t*) *p;
  *p = NULL;

  if (!pcodec)
    return;

  // Destroy the instance
  if (pcodec->instance)
    _swix(AMPlayer_Instance, _INR(0,2), 0, 2, pcodec->instance);

  // Release the memory
  if (pcodec->sound_area)
    _swix(OS_DynamicArea, _INR(0,1), 1, pcodec->sound_area);

  ka_mem_free(pcodec);
}

/**
 * Connects to the player module and checks the description
 * of the type of audio packets that we will provide it.
 *
 * @param  p      Pointer to the codec's data.
 * @param  params Type of audio packet to decode.
 *
 * @returns 0 if OK, else error block is filled.
 */
static int amp_start(ka_audiocodec_t* p, const ka_aparams_t* params)
{
  data_t* pcodec = (data_t*) p;
  char *stream_name = strrchr(playlist_getcurrentfilm(),'.') + 1;
  int i;

  // Only MPEG AUDIO is supported
  if (params->typ != KA_ABLOCK_MPEG)
  {
    if (config.debug & (cfg_printwarnings | cfg_printaudiostats))
      ka_log(ka_log_error | ka_log_audio, "Audio format not supported by AMPlayer");

    ka_error_fill(pcodec->hdr.pErrorBlock, "error33:%s %s", ka_block_to_name(params->typ), "AMPlayer");
    return -1;
  }

  // Initialise audio fifo buffers
  for (i = 0; i < A_FIFO_NUM; i++)
  {
    pcodec->fifo[i].pts = 0;
    pcodec->fifo[i].usage = 0;
  }
  pcodec->am_write = pcodec->am_start;
  pcodec->done = 0;

  // Open a stream
  if (_swix(AMPlayer_StreamOpen, _IN(0)|_IN(1)|_IN(8)|_OUT(0)
           , 2 + BIT_31, stream_name, pcodec->instance
           , &pcodec->handle))
  {
    ka_error_fill(pcodec->hdr.pErrorBlock, "error14"); // (Cannot open audio stream)
    return -1;
  }

  return 0;
}

/**
 * Stops the replay and decoding,
 * closes the connection with the player module.
 *
 * @param  p     Pointer to the codec's data.
 */
static void amp_stop(ka_audiocodec_t* p)
{
  data_t* pcodec = (data_t*) p;

  if (pcodec->handle)
  {
    // Inform AMPlayer that there will be no more data
    _swix(AMPlayer_StreamClose, _INR(0,1)|_IN(8)
         , 0 + BIT_31, pcodec->handle, pcodec->instance);
    pcodec->handle = 0;

    // Ask AMPlayer to release the buffers
    _swix(AMPlayer_Stop, _IN(0)|_IN(8), 0 + BIT_31, pcodec->instance);
  }
}

/**
 * Tries to update audio information block (parameters and position).
 * Sets info->pts if a new position is provided for audio/video sync.
 *
 * @param  p     Pointer to the codec's data.
 * @param  info  Pointer to the audio information block to update.
 *
 * @returns One or more of the following flags.
 *          AUDIOCODEC_STATE_READY     if the information was successully updated
 *                                     and player is ready to play.
 *          AUDIOCODEC_STATE_NOWORK    if the audio was entirely decoded
 *          AUDIOCODEC_STATE_COMPLETED if the audio was entirely decoded and no future input.
 *          AUDIOCODEC_STATE_INERROR   if the player is in error.
 */
static int amp_getpos(ka_audiocodec_t* p, audio_info_t* info)
{
  data_t* pcodec = (data_t*) p;
  frame_info_t* frame_info = NULL;
  file_info_t* file_info = NULL;
  int delay1;
  am_block* am_read;
  int state = 0;

  info->pts = 0;

  // Find oldest buffer in use
  am_read = pcodec->am_write;
  // Previous buffer
  if (--am_read < pcodec->am_start)
    am_read = pcodec->am_end - 1;
  // Till previous buffer is not in use
  while (am_read->usage && (am_read != pcodec->am_write))
    if (--am_read < pcodec->am_start)
      am_read = pcodec->am_end - 1;
  // Next buffer is in use
  if (++am_read >= pcodec->am_end)
    am_read = pcodec->am_start;

  // Get audio parameters
  if (_swix(AMPlayer_Info, _IN(0)|_IN(8)|_OUT(2)|_OUT(3)
           , 0 + BIT_31, pcodec->instance
           , &file_info, &frame_info))
    return AUDIOCODEC_STATE_INERROR;

  if (!file_info || !file_info->buff_usage)
  {
    state |= AUDIOCODEC_STATE_NOWORK;
    if (pcodec->done)
      state |= AUDIOCODEC_STATE_COMPLETED;
  }

  if (frame_info && file_info)
  {
    state |= AUDIOCODEC_STATE_READY;

    if (!info->channels && frame_info->channels && frame_info->samp_rate)
    {
      snprintf(info->name, sizeof(info->name), "MPEG %s Layer %d", (char*) &frame_info->version, frame_info->layer);
      info->samp_rate = frame_info->samp_rate;
      info->bitrate   = frame_info->bitrate;
      info->channels  = frame_info->channels;
      // In next line, AMPlayer always produces 2 channels, and 16 bit audio is assumed.
      pcodec->percent2mpeg = (OP_BUFF*(timer_getrate()/100/2/2)) / info->samp_rate;
    }

    pcodec->buff_usage = file_info->buff_usage;
    delay1 = pcodec->buff_usage * pcodec->percent2mpeg; // output buffer delay in mpeg clocks
  }
  else
  {
    pcodec->buff_usage = 0;
    delay1 = 0;
  }

  // Try to estimate position from oldest buffer in use
  // guard about nearly filled buffers which causes sync problems
  if ((am_read != pcodec->am_write)
  &&  (pcodec->buff_usage < 90)
  &&  (am_read->pts != 0))
  {
    // We now at least up to this pts is already decoded
    // and how much of this still remains to be played in the output buffer
    info->pts = am_read->pts - delay1 + FUDGE_FACTOR;

    if (config.debug & cfg_printaudiostats)
    {
      ka_log(ka_log_note | ka_log_audio, "buff %d%%, pos %10u (delay %d, read %10u)"
         , pcodec->buff_usage, info->pts, delay1, am_read->pts);
    }
  }

  return state;
}

/**
 * Tries to push the audio packet into the player's input buffers.
 *
 * @param  p     Pointer to the codec's data.
 * @param  block Pointer to audio block or NULL to signal no more blocks are to follow.
 *
 * @returns EAUDIOCODEC_DECODE_TRYLATER  if the buffer is full and the packet should be added later.
 *          EAUDIOCODEC_DECODE_PROCESSED if the packed was successully added.
 *          EAUDIOCODEC_DECODE_INERROR   if the player is in error.
 */
static ka_eaudiocodec_decode amp_decode(ka_audiocodec_t* p, ka_block_t* block)
{
  data_t* pcodec = (data_t*) p;
  int len;

  if (!block)
  {
    pcodec->done = 1;
    return EAUDIOCODEC_DECODE_PROCESSED;
  }
  else
    pcodec->done = 0;

  // Buffer full
  if (pcodec->am_write->usage)
    return EAUDIOCODEC_DECODE_TRYLATER;

  // Output buffer probably filled to much to be able
  // to always decode an input buffer in one go
  // causes problem with position determination
  if (pcodec->buff_usage > 70)
    return EAUDIOCODEC_DECODE_TRYLATER;

  // Data too long for FIFO buffer? Split it
  len = block->eod - block->sod;
  if (len > (FIFO_SIZE-16))
    len = FIFO_SIZE-16;

  // Copy data
  pcodec->am_write->pts = block->pts;
  pcodec->am_write->usage = 1;
  pcodec->am_write->meta = 0;
  pcodec->am_write->len = len;
  memcpy(&pcodec->am_write->data, block->sod, len);

  // Pass buffer location to AMPlayer
  _swix(AMPlayer_StreamGiveData, _INR(0,2)|_IN(8)
       , 0 + BIT_31, pcodec->handle, &pcodec->am_write->usage, pcodec->instance);

  // Point to next buffer for next call
  if (++pcodec->am_write == pcodec->am_end)
    pcodec->am_write = pcodec->am_start;

  // If data was splitted, try to call recursively till all buffer is processed
  if ((block->eod - block->sod) > len)
  {
    block->pts = 0;
    block->sod += len;
    return amp_decode(p, block);
  }

  return EAUDIOCODEC_DECODE_PROCESSED;
}

/**
 * Starts or pauses the replay.
 *
 * @param  p      Pointer to the codec's data.
 * @param  pause  1 to pause, 0 to resume.
 */
static void amp_pause(ka_audiocodec_t* p, int pause)
{
  data_t* pcodec = (data_t*) p;

  if (pcodec->instance)
    _swix(AMPlayer_Pause, _IN(0)|_IN(8)
         , !pause + BIT_31, pcodec->instance);
}

/**
 * Alters or read the audio volume.
 *
 * @param  p     Pointer to the codec's data.
 * @param  vol   Volume to set [0 - 127] or -1 to just read current value
 *
 * @returns new volume.
 */
static int amp_volume(ka_audiocodec_t* p, int vol)
{
  data_t* pcodec = (data_t*) p;
  int rvol = 127;

  if (vol == -1)
  {
    _swix(AMPlayer_Control, _INR(0,1)|_OUT(1), 0, -1, &rvol);
    return rvol;
  }

  _swix(AMPlayer_Control, _INR(0,1)|_IN(8)|_OUT(1)
       , 0 + BIT_31, vol, pcodec->instance
       , &rvol);

  return rvol;
}

const kav_audiocodec_t kav_amp_codec =
{ .name = "AMPlayer"
, .allow  = amp_allow
, .open   = amp_open
, .close  = amp_close
, .start  = amp_start
, .stop   = amp_stop
, .getPos = amp_getpos
, .decode = amp_decode
, .pause  = amp_pause
, .volume = amp_volume
};
