//| Copyright: (C) 2020-2024 Kevin Larke <contact AT larke DOT org> 
//| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
#include "cwCommon.h"
#include "cwLog.h"
#include "cwCommonImpl.h"
#include "cwTest.h"
#include "cwMem.h"
#include "cwFile.h"
#include "cwText.h"
#include "cwObject.h"
#include "cwAudioFile.h"
#include "cwUtility.h"
#include "cwFileSys.h"
#include "cwAudioFileOps.h"
#include "cwMath.h"
#include "cwVectOps.h"
#include "cwDsp.h"
#include "cwAudioTransforms.h"

namespace cw
{
  namespace afop
  {
    typedef struct _cutMixArg_str
    {
      const cutMixArg_t*  arg;
      char*               srcFn;
      unsigned            srcFrmIdx;
      unsigned            srcFrmN;
      unsigned            srcFadeInFrmN;
      unsigned            srcFadeOutFrmN;
      unsigned            dstFrmIdx;
      audiofile::handle_t afH;
      audiofile::info_t   afInfo;
    } _cutMixArg_t;
    
    rc_t _cutAndMixOpen( const char* srcDir, const cutMixArg_t* argL, _cutMixArg_t* xArgL, unsigned argN, unsigned& chN_Ref, double& srate_Ref, unsigned& dstFrmN_Ref, unsigned& maxSrcFrmN_Ref )
    {
      rc_t rc = kOkRC;

      maxSrcFrmN_Ref  = 0;
      chN_Ref         = 0;
      
      for(unsigned i = 0; i<argN; ++i)
      {
        // create the source audio file name
        xArgL[i].srcFn = filesys::makeFn(srcDir, argL[i].srcFn, NULL, NULL);


        // get the audio file info
        if((rc = audiofile::open( xArgL[i].afH, xArgL[i].srcFn, &xArgL[i].afInfo )) != kOkRC )
        {
          rc = cwLogError(rc, "Unable to obtain info for the source audio file: '%s'.", cwStringNullGuard(xArgL[i].srcFn));
          goto errLabel;
        }

        // get the system sample rate from the first file
        if( i == 0 )
          srate_Ref = xArgL[i].afInfo.srate;

        // if the file sample rate does not match the system sample rate
        if( srate_Ref != xArgL[i].afInfo.srate )
        {
          rc = cwLogError(kInvalidArgRC,"'%s' sample rate %f does not match the system sample rate %f.", xArgL[i].srcFn, xArgL[i].afInfo.srate, srate_Ref );
          goto errLabel;
        }

        // verify the source file begin/end time 
        if( argL[i].srcBegSec > argL[i].srcEndSec )
        {
          rc = cwLogError(kInvalidArgRC,"The end time is prior to the begin time on source file '%s'.", xArgL[i].srcFn);
          goto errLabel;
        }

        xArgL[i].arg            = argL + i;
        xArgL[i].srcFrmIdx      = floor(argL[i].srcBegSec     * srate_Ref);
        xArgL[i].srcFrmN        = floor(argL[i].srcEndSec     * srate_Ref) - xArgL[i].srcFrmIdx;
        xArgL[i].srcFadeInFrmN  = floor(argL[i].srcBegFadeSec * srate_Ref);
        xArgL[i].srcFadeOutFrmN = floor(argL[i].srcEndFadeSec * srate_Ref);
        xArgL[i].dstFrmIdx      = floor(argL[i].dstBegSec     * srate_Ref);

        //printf("cm beg:%f end:%f dst:%f gain:%f %s\n", argL[i].srcBegSec, argL[i].srcEndSec, argL[i].dstBegSec, argL[i].gain, argL[i].srcFn );
      
        
        chN_Ref        = std::max( chN_Ref,        xArgL[i].afInfo.chCnt );
        maxSrcFrmN_Ref = std::max( maxSrcFrmN_Ref, xArgL[i].srcFrmN );

        dstFrmN_Ref = std::max( dstFrmN_Ref, xArgL[i].dstFrmIdx + xArgL[i].srcFrmN );
        
      }

    errLabel:
      return rc;
    }

    rc_t _cutAndMixClose( _cutMixArg_t* xArgL, unsigned argN )
    {
      rc_t rc = kOkRC;
      
      for(unsigned i = 0; i<argN; ++i)
      {

        if((rc = audiofile::close( xArgL[i].afH )) != kOkRC )
        {
          rc = cwLogError(kOpFailRC,"'%s' file closed.", xArgL[i].srcFn );
          goto errLabel;
            
        }
          
        mem::release( xArgL[i].srcFn );
      }
      
    errLabel:
      return rc;
    }

    enum { kLinearFadeFl = 0x01, kEqualPowerFadeFl=0x02, kFadeInFl=0x04, kFadeOutFl=0x08 };

    void _fadeOneChannel( float* xV, unsigned frmN, unsigned fadeFrmN, unsigned flags )
    {
      int i0,d,offs;
      
      if( cwIsFlag(flags,kFadeInFl ) )
      {
        // count forward
        i0   = 0;
        d    = 1;
        offs = 0;
      }
      else
      {
        assert( frmN >= fadeFrmN );
        
        // count backward
        i0 = (int)fadeFrmN;
        d    = -1;
        offs = frmN-fadeFrmN;
      }

      // do a linear fade
      if( cwIsFlag(flags,kLinearFadeFl) )
      {
        for(int i = i0,j=0; j<(int)fadeFrmN; i+=d,++j )
        {
          assert(0 <= offs+j && offs+j < (int)frmN );
          xV[offs+j] *= ((float)i) / fadeFrmN;
        }
      }
      else                      // do an equal power fade
      {
        for(int i = i0,j=0; j<(int)fadeFrmN; i+=d,++j )
        {
          assert(0 <= offs+j && offs+j < (int)frmN );
          xV[offs+j] *= std::sqrt(((float)i) / fadeFrmN);
        }
      } 
    }

    
    void _fadeAllChannels( float* chBufL[], unsigned chN, unsigned frmN, unsigned fadeFrmN, unsigned flags )
    {
      fadeFrmN = std::min(frmN,fadeFrmN);

      for(unsigned i=0; i<chN; ++i)        
        _fadeOneChannel(chBufL[i],frmN,fadeFrmN,flags);
    }
  }
}


cw::rc_t     cw::afop::sine( const char* fn, double srate, unsigned bits, double hz, double gain, double secs )
{
  rc_t     rc    = kOkRC;
  unsigned bN    = srate * secs;
  float*   b     = mem::alloc<float>(bN);
  unsigned chCnt = 1;

  unsigned        i;
  for(i=0; i<bN; ++i)
    b[i] = gain * sin(2.0*M_PI*hz*i/srate);

  if((rc = audiofile::writeFileFloat(fn, srate, bits, bN, chCnt, &b)) != kOkRC)
    return rc;

  return rc;
}

cw::rc_t cw::afop::sine( const object_t* cfg )
{
  rc_t        rc;
  double      srate, hz, gain, secs;
  unsigned    bits;
  const char* fn = nullptr;
  
  if((rc = cfg->getv("fn",fn,"srate",srate,"bits",bits,"hz",hz,"gain",gain,"secs",secs)) != kOkRC )
    return cwLogError(kSyntaxErrorRC,"Invalid parameter to audio file sine test.");
  
  char* afn = filesys::expandPath(fn);
  rc        = sine( afn, srate, bits, hz, gain, secs );
  mem::release(afn);
  
  return rc;
}

cw::rc_t cw::afop::clicks(const char* fn, double srate, unsigned bits, double secs, const unsigned* clickSmpOffsArray, unsigned clickSmpOffsN, double clickAmp, double decay, double burstMs )
{
  rc_t     rc    = kOkRC;
  unsigned xN    = srate * secs;
  float*   x     = mem::alloc<float>(xN);
  unsigned bN    = srate * burstMs / 1000.0;
  unsigned chCnt = 1;
  float    b[ bN ];

  for(unsigned i=0; i<bN; ++i)
    b[i] = clickAmp * (float)rand()/RAND_MAX;

  for(unsigned i=0; i<clickSmpOffsN; ++i)
  {
    unsigned smp_idx = clickSmpOffsArray[i] * srate / 1000.0;
    if( smp_idx >= xN )
      return cwLogError(kInvalidArgRC,"Click index %i (%f secs) is greater than the signal length: %i (%f secs).", smp_idx, smp_idx/srate, xN, secs );

    for(unsigned j=0; j<bN && smp_idx+j < xN; ++j)
      x[smp_idx+j] = b[j];
  }

  for(unsigned i=1; i<xN; ++i)    
    x[i] = decay*x[i-1] + (1.0-decay)*x[i];
  
  if((rc = audiofile::writeFileFloat(fn, srate, bits, xN, chCnt, &x)) != kOkRC)
    return rc;

  return rc;
}

cw::rc_t      cw::afop::mix( const char* fnV[], const float* gainV, unsigned srcN, const char* outFn, unsigned outBits )
{
  rc_t rc = kOkRC;
  
  if( srcN == 0 )
    return rc;

  unsigned            maxFrmN = 0;
  unsigned            maxChN  = 0;
  double              srate   = 0;
  audiofile::handle_t hV[ srcN ];
  audiofile::handle_t oH;

  // open each source file and determine the output file audio format
  for(unsigned i=0; i<srcN; ++i)
  {
    audiofile::info_t info;

    if((rc = audiofile::open( hV[i], fnV[i], &info )) != kOkRC )
    {
      rc = cwLogError(kOpFailRC,"Unable to open the audio mix source file '%s'.", fnV[i] );
      goto errLabel;
    }

    if( srate == 0 )
      srate    = info.srate;
    
    if( srate != info.srate )
    {
      rc = cwLogError(kInvalidArgRC,"The sample rate (%f) of '%s' does not match the sample rate (%f) of '%s'.", info.srate, fnV[i], srate, fnV[0] );
      goto errLabel;
    }
        
    if( maxFrmN < info.frameCnt )
      maxFrmN = info.frameCnt;

    if( maxChN < info.chCnt )
      maxChN = info.chCnt;
  }

  // create the output file
  if((rc = audiofile::create( oH, outFn, srate, outBits, maxChN)) != kOkRC )
    goto errLabel;
  else
  {
    const unsigned kBufFrmN = 1024;
    float          ibuf[   maxChN * kBufFrmN ];
    float          obuf[   maxChN * kBufFrmN ];
    float*         iChBuf[ maxChN ];
    float*         oChBuf[ maxChN ];

    // setup the in/out channel buffers
    for(unsigned i=0; i<maxChN; ++i)
    {
      iChBuf[i] = ibuf + (i*kBufFrmN);
      oChBuf[i] = obuf + (i*kBufFrmN);
    }

    // for each frame
    for(unsigned frmIdx=0; frmIdx < maxFrmN; frmIdx += kBufFrmN )
    {
      // zero the mix buf
      memset(obuf,0,sizeof(obuf));

      unsigned maxActualFrmN = 0;

      // for each source
      for(unsigned i=0; i<srcN; ++i)
      {      
        unsigned actualFrmN = 0;
        
        // read a buffer of audio from the ith source.
        if((rc = audiofile::readFloat( hV[i], kBufFrmN, 0, channelCount(hV[i]), iChBuf, &actualFrmN)) != kOkRC )
        {
          rc = cwLogError(kOpFailRC,"Read failed on source '%s'.", name(hV[i]));
          goto errLabel;
        }

        // mix the input buffer into the output buffer
        for(unsigned j=0; j<channelCount(hV[i]); ++j)
          for(unsigned k=0; k<actualFrmN; ++k)
            oChBuf[j][k] += gainV[i] * iChBuf[j][k];

        // track the max. count of samples actually read for this buffer cycle
        if( actualFrmN > maxActualFrmN )
          maxActualFrmN = actualFrmN;

      }

      // write the mixed output buffer
      if((rc = audiofile::writeFloat(oH, maxActualFrmN, maxChN, oChBuf )) != kOkRC )
      {
        rc = cwLogError(kOpFailRC,"Write failed on output file '%s'.", outFn );
        goto errLabel;
      }
    }
  }
  
 errLabel:
  if( rc != kOkRC )
    cwLogError(kOpFailRC,"Mix failed.");

  // close the source audio files
  for(unsigned i=0; i<srcN; ++i)
    audiofile::close(hV[i]);

  audiofile::close(oH);         // close the output file
  
  return rc;
}

cw::rc_t       cw::afop::mix( const object_t* cfg )
{
  rc_t            rc      = kOkRC;
  const object_t* srcL    = nullptr;
  const char*     oFn     = nullptr;
  unsigned        outBits = 16;

  // read the top level cfg record
  if((rc = cfg->getv("outFn",oFn,"outBits",outBits,"srcL",srcL)) != kOkRC )
    goto errLabel;
  else
  {
    char*       outFn = filesys::expandPath(oFn);
    unsigned    srcN  = srcL->child_count();
    const char* fnV[ srcN ];
    float       gainV[ srcN ];

    memset(fnV,0,sizeof(fnV));

    // read each source record
    for(unsigned i=0; i<srcN; ++i)
    {
      const char* fn                                                = nullptr;
      if((rc = srcL->child_ele(i)->getv("gain",gainV[i],"src",fn)) != kOkRC )
        rc                                                          = cwLogError(kSyntaxErrorRC,"Mix source index %i syntax error.");
      else
        fnV[i]                                                      = filesys::expandPath(fn);      
    }

    if( rc == kOkRC )
      rc = mix( fnV, gainV, srcN, outFn, outBits);

    mem::free(outFn);
    for(unsigned i=0; i<srcN; ++i)
      mem::free((char*)fnV[i]);

  }
  
 errLabel:
  return rc;
}

cw::rc_t  cw::afop::selectToFile( const char* srcFn,                 // source audio file
                                  double beg0Sec, double beg1Sec,    // begin cross-fade
                                  double end0Sec, double end1Sec,    // end cross-fade
                                  unsigned outBits,                  // output sample word bits 
                                  const char* outDir,                // output directory
                                  const char* outFn )                // output filename
{
  rc_t                rc     = kOkRC;
  char*               iFn    = filesys::expandPath(srcFn);
  char*               dstDir = filesys::expandPath(outDir);
  char*               oFn    = filesys::makeFn( dstDir, outFn, nullptr, nullptr );
  audiofile::info_t   info;
  audiofile::handle_t iH;
  audiofile::handle_t oH;
  
  //
  if( beg1Sec < beg0Sec )
  {
    rc = cwLogError(kInvalidArgRC,"Invalid fade in time selection. Begin fade time (%f) is greater than end fade time (%f). ", beg0Sec, beg1Sec );
    goto errLabel;
  }

  // 
  if( end1Sec != -1 && end1Sec < end0Sec )
  {
    rc = cwLogError(kInvalidArgRC,"Invalid fade out time selection. Begin fade time (%f) is greater than end fade time (%f). ", end0Sec, end1Sec );
    goto errLabel;
  }

  //
  if( beg1Sec > end0Sec )
  {
    rc = cwLogError(kInvalidArgRC,"Invalid time selection. Begin time (%f) is greater than end time (%f). ", beg1Sec, end0Sec );
    goto errLabel;
  }

  // open the source file
  if((rc = audiofile::open( iH, iFn, &info )) != kOkRC )
    goto errLabel;

  // create the output file
  if((rc = audiofile::create( oH, oFn, info.srate, outBits, info.chCnt)) != kOkRC )
    goto errLabel;
  else
  {
    unsigned       beg0FrmIdx    = (unsigned)floor(beg0Sec * info.srate);
    unsigned       beg1FrmIdx    = (unsigned)floor(beg1Sec * info.srate);
    unsigned       end0FrmIdx    = (unsigned)floor(end0Sec * info.srate);
    unsigned       end1FrmIdx    = end1Sec==-1 ? info.frameCnt : (unsigned)floor(end1Sec * info.srate);
    unsigned       ttlFrmN       = end1FrmIdx - beg0FrmIdx;
    unsigned       actualBufFrmN = 0;
    float*         chBuf[ info.chCnt ];

    beg0FrmIdx = std::max(0u,std::min(beg0FrmIdx,info.frameCnt));
    beg1FrmIdx = std::max(0u,std::min(beg1FrmIdx,info.frameCnt));
    end0FrmIdx = std::max(0u,std::min(end0FrmIdx,info.frameCnt));
    end1FrmIdx = std::max(0u,std::min(end1FrmIdx,info.frameCnt));

    
    cwLogInfo("beg:%f %f end: %f %f : src:%s dst:%s", beg0Sec,beg1Sec,end0Sec,end1Sec,iFn,oFn);
    cwLogInfo("beg:%f %f end: %f %f", beg0FrmIdx/info.srate,beg1FrmIdx/info.srate,end0FrmIdx/info.srate,end1FrmIdx/info.srate);

    // Seek to the start of the read location
    if((rc = audiofile::seek( iH, beg0FrmIdx )) != kOkRC )
      goto errLabel;
    
    
    // set up the read/write channel buffer
    for(unsigned i = 0; i<info.chCnt; ++i)
      chBuf[i] = mem::alloc<float>(ttlFrmN);

    // read the audio file
    if((rc = audiofile::readFloat(  iH, ttlFrmN, 0, info.chCnt, chBuf, &actualBufFrmN)) != kOkRC )
    {
      rc = cwLogError(kOpFailRC,"Read failed on file '%s'.", iFn );
      goto errLabel;
    }
    
    
    // if there is a fade-in then generate it
    if( beg1FrmIdx > beg0FrmIdx )
      _fadeAllChannels( chBuf, info.chCnt, ttlFrmN, beg1FrmIdx-beg0FrmIdx, kLinearFadeFl | kFadeInFl );

    // if there is a fade-out then generate it
    if( end1FrmIdx > end0FrmIdx )
    {
      assert( actualBufFrmN >= (end1FrmIdx - end0FrmIdx) );
      
      float*  fadeChBuf[ info.chCnt ];
      for(unsigned i = 0; i<info.chCnt; ++i)
        fadeChBuf[i] = chBuf[i] + actualBufFrmN - (end1FrmIdx - end0FrmIdx);
      
      _fadeAllChannels( fadeChBuf, info.chCnt, end1FrmIdx-end0FrmIdx, end1FrmIdx-end0FrmIdx, kLinearFadeFl | kFadeOutFl );
    }
    
    // write the buffer to the output file
    if((rc = audiofile::writeFloat(oH, actualBufFrmN, info.chCnt, chBuf )) != kOkRC )
    {
      rc = cwLogError(kOpFailRC,"Write failed on output file '%s'.", oFn );
      goto errLabel;
    }
    
        
  }
    
 errLabel:
  if( rc != kOkRC )
    cwLogError(rc,"selectToFile failed.");

  audiofile::close(oH);
  audiofile::close(iH);
  mem::release(iFn);
  mem::release(dstDir);
  mem::release(oFn);
  return rc;
}

cw::rc_t       cw::afop::selectToFile( const object_t* cfg )
{
  rc_t            rc            = kOkRC;
  const object_t* selectL       = nullptr;
  const char*     oDir          = nullptr;
  const char*     src0Fn        = nullptr;
  char*           markerFn      = nullptr;
  double          fadeInSec     = 0;
  double          fadeOutSec    = 0;
  bool            fadeInPreFl   = false;
  bool            fadeOutPostFl = false;
  unsigned        outBits       = 16;
  file::handle_t  markerFh;

  // read the top level cfg record
  if((rc = cfg->getv("outDir",oDir,"outBits",outBits,"selectL",selectL)) != kOkRC )
    goto errLabel;  

  if((rc = cfg->getv_opt("srcFn",src0Fn,"markerFn",markerFn,"fadeInSec",fadeInSec,"fadeOutSec",fadeOutSec,"fadeInPreFl",fadeInPreFl,"fadeOutPreFl",fadeOutPostFl)) != kOkRC )
      goto errLabel;

  
  if( markerFn != nullptr )
  {    
    if((rc = file::open(markerFh, markerFn, file::kWriteFl )) != kOkRC )
    {
      rc = cwLogError(rc,"Marker file '%s' create failed.", cwStringNullGuard(markerFn));
      goto errLabel;
    }
  }
      
  for(unsigned i=0; i<selectL->child_count(); ++i)
  {
    double      begSec = 0;
    double      endSec = -1;
    const char* dstFn = nullptr;
    const char* src1Fn  = nullptr;

    if((rc = selectL->child_ele(i)->getv("begSec",begSec,"endSec",endSec,"dst",dstFn)) != kOkRC )
    {
      rc = cwLogError(kSyntaxErrorRC,"Argument error in 'select to file' index %i syntax error.");
      goto errLabel;
    }

        
    if((rc = selectL->child_ele(i)->getv_opt("src",src1Fn)) != kOkRC )
    {
      rc = cwLogError(kSyntaxErrorRC,"Optional argument parse error in 'select to file' index %i syntax error.");
      goto errLabel;
    }

    if( src0Fn == nullptr && src1Fn == nullptr )
    {
      rc = cwLogError(kInvalidArgRC,"selection at index %i does not have a source file.",i);
      goto errLabel;
    }

    double beg0Sec = fadeInPreFl     ? std::max(0.0,begSec-fadeInSec)  : begSec;
    double beg1Sec = fadeInPreFl     ? begSec                          : begSec + fadeInSec;
    double end0Sec = fadeOutPostFl   ? std::max(0.0,endSec - fadeOutSec) : endSec;
    double end1Sec = fadeOutPostFl   ? endSec : endSec+fadeOutSec;

    beg1Sec = std::max(beg0Sec,beg1Sec);
    end1Sec = std::max(end0Sec,end1Sec);

    if( markerFh.isValid() )
    {
      file::printf(markerFh,"%f\t%f\t%i\n",begSec,begSec,i+1);
    }
        
    if((rc = selectToFile( src1Fn==nullptr ? src0Fn : src1Fn, beg0Sec, beg1Sec, end0Sec, end1Sec, outBits, oDir, dstFn )) != kOkRC )
      goto errLabel;
  }
  
 errLabel:

  if( markerFh.isValid() )
  {
    file::close(markerFh);
  }
  return rc;
}


cw::rc_t cw::afop::cutAndMix( const char* dstFn, unsigned dstBits, const char* srcDir, const cutMixArg_t* argL, unsigned argN )
{
  rc_t         rc         = kOkRC;
  unsigned     dstChN     = 0;
  double       dstSrate   = 0;
  unsigned     dstFrmN    = 0;
  unsigned     maxSrcFrmN = 0;
  float*       dstV       = nullptr;
  float*       srcV       = nullptr;
  _cutMixArg_t xArgL[ argN ];
  memset( &xArgL, 0, sizeof(xArgL));

  // open each of the source files
  if((rc = _cutAndMixOpen( srcDir, argL, xArgL, argN, dstChN, dstSrate, dstFrmN, maxSrcFrmN )) != kOkRC )
    goto errLabel;
  else
  {
    float* dstChBufL[ dstChN ];
    float* srcChBufL[ dstChN ];
    
    dstV = mem::allocZ<float>(dstFrmN*dstChN); // output signal buffer
    srcV  = mem::alloc<float>(maxSrcFrmN*dstChN); // source signal buffer

    // create the src read/ dst write buffer
    for(unsigned i=0; i<dstChN; ++i)
    {
      dstChBufL[i] = dstV + (i*dstFrmN);
      srcChBufL[i] = srcV + (i*maxSrcFrmN);
    }
  
    // for each source file
    for(unsigned i = 0; i<argN; ++i)
    {
      unsigned chIdx         = 0;
      unsigned actualFrmN    = 0;
      unsigned srcFrmN       = xArgL[i].srcFrmN;
      unsigned srcChN        = xArgL[i].afInfo.chCnt;


      // read the source segment
      if((rc = audiofile::getFloat( xArgL[i].srcFn, xArgL[i].srcFrmIdx, srcFrmN, chIdx, srcChN, srcChBufL, &actualFrmN, nullptr)) != kOkRC )
      {
        cwLogError(rc,"Error reading source audio file '%s'.", xArgL[i].srcFn );
        goto errLabel;
      }

      srcFrmN     = std::min( srcFrmN, actualFrmN ); // Track the true size of the source buffer.
      
      // Apply the fade in and out functions.
      _fadeAllChannels(srcChBufL, srcChN, srcFrmN, xArgL[i].srcFadeInFrmN,  kFadeInFl  | kLinearFadeFl );
      _fadeAllChannels(srcChBufL, srcChN, srcFrmN, xArgL[i].srcFadeOutFrmN, kFadeOutFl | kLinearFadeFl );
      
      // sum into the source signal into the output buffer
      for(unsigned j   = 0; j<srcChN; ++j)
        for(unsigned k = 0; k<srcFrmN; ++k)
        {
          assert( xArgL[i].dstFrmIdx + k < dstFrmN );
          dstChBufL[j][ xArgL[i].dstFrmIdx + k ] += xArgL[i].arg->gain * srcChBufL[j][k];
        }
      
    }


    // write the output file
    if((rc = audiofile::writeFileFloat(  dstFn, dstSrate, dstBits, dstFrmN, dstChN, dstChBufL)) != kOkRC )
    {
      cwLogError(rc,"Output file ('%s') write failed.", cwStringNullGuard(dstFn));
      goto errLabel;
    }
  }
  

 errLabel:
  _cutAndMixClose( xArgL, argN );

  mem::release(dstV);
  mem::release(srcV);
  if( rc != kOkRC )
    cwLogError(rc,"Cross-fade failed.");
  
  return rc;
}

cw::rc_t cw::afop::cutAndMix( const object_t* cfg )
{
  rc_t            rc       = kOkRC;
  const char*     srcDir   = nullptr;
  const char*     dstFn    = nullptr;
  unsigned        dstBits  = 16;
  char*           afSrcDir = nullptr;
  char*           afDstFn     = nullptr;  
  double          crossFadeSec = 0;
  const object_t* argNodeL    = nullptr;
  unsigned        i;
  
  // read the top level cfg record
  if((rc = cfg->getv("dstFn",dstFn,
                     "dstBits",dstBits,
                     "srcDir",srcDir,
                     "crossFadeSec",crossFadeSec,
                     "argL",argNodeL)) != kOkRC )
    goto errLabel;

  if( argNodeL == nullptr )
  {
    rc = cwLogError(kInvalidArgRC,"No crossfades were specified.");
  }
  else
  {
    unsigned argN = argNodeL->child_count(); 
    cutMixArg_t argL[ argN ];
        
    // for each source file
    for(i=0; i<argNodeL->child_count(); ++i)
    {
      const object_t* o = argNodeL->child_ele(i);

      // parse the non-optional parameters
      if((rc = o->getv("srcBegSec", argL[i].srcBegSec,
                       "srcEndSec", argL[i].srcEndSec,
                       "srcFn", argL[i].srcFn  )) != kOkRC )
      {
        rc = cwLogError(kInvalidArgRC,"Invalid crossfade argument at argument index %i.",i);
      }
      else
      {        
        argL[i].dstBegSec     = argL[i].srcBegSec; // By default the src is moved to the same location 
        argL[i].srcBegFadeSec = crossFadeSec;      // By default the beg/end fade is the global fade time. 
        argL[i].srcEndFadeSec = crossFadeSec;
        argL[i].gain          = 1;

        // parse the optional parameters
        if((rc = o->getv_opt("dstBegSec", argL[i].dstBegSec,
                             "srcBegFadeSec", argL[i].srcBegFadeSec,
                             "srcEndFadeSec", argL[i].srcEndFadeSec,
                             "gain", argL[i].gain  )) != kOkRC )
        {
          rc = cwLogError(kInvalidArgRC,"Invalid crossfade optional argument at argument index %i.",i);
        }
      }

      //printf("cm beg:%f end:%f dst:%f gain:%f %s\n", argL[i].srcBegSec, argL[i].srcEndSec, argL[i].dstBegSec, argL[i].gain, argL[i].srcFn );
      
    }

    afSrcDir = filesys::expandPath(srcDir);
    afDstFn  = filesys::expandPath(dstFn);

    // call cross-fader
    if((rc = cutAndMix( afDstFn, dstBits, afSrcDir, argL, argN )) != kOkRC )
    {
      rc = cwLogError(rc,"");
      goto errLabel;
    }

  }

 errLabel:
  mem::release(afSrcDir);
  mem::release(afDstFn);
  if( rc != kOkRC )
    rc = cwLogError(rc,"Cut and mix failed.");
  return rc;
}

cw::rc_t cw::afop::parallelMix( const char* dstFn, unsigned dstBits, const char* srcDir, const parallelMixArg_t* argL, unsigned argN )
{
  
  double fadeInSec = 0;
  cutMixArg_t cmArgL[ argN ];
  double dstBegSec = 0;
  memset(&cmArgL,0,sizeof(cmArgL));
  
  for(unsigned i=0; i<argN; ++i)
  {
    cmArgL[i].srcFn          = argL[i].srcFn;
    cmArgL[i].srcBegSec      = argL[i].srcBegSec;
    cmArgL[i].srcEndSec      = argL[i].srcEndSec + argL[i].fadeOutSec;
    cmArgL[i].srcBegFadeSec  = fadeInSec;
    cmArgL[i].srcEndFadeSec  = argL[i].fadeOutSec;
    //cmArgL[i].dstBegSec      = dstBegSec;
    cmArgL[i].dstBegSec      = argL[i].srcBegSec - argL[0].srcBegSec;
    cmArgL[i].gain           = argL[i].gain;

    dstBegSec               += argL[i].srcEndSec - argL[i].srcBegSec;
    fadeInSec                = argL[i].fadeOutSec;
  }

  return cutAndMix( dstFn, dstBits, srcDir, cmArgL, argN );
}

cw::rc_t cw::afop::parallelMix( const object_t* cfg )
{
  rc_t            rc       = kOkRC;
  const char*     srcDir   = nullptr;
  const char*     dstFn    = nullptr;
  unsigned        dstBits  = 16;
  char*           afSrcDir = nullptr;
  char*           afDstFn     = nullptr;  
  const object_t* argNodeL    = nullptr;
  unsigned        i;

  // read the top level cfg record
  if((rc = cfg->getv("dstFn",dstFn,
                     "dstBits",dstBits,
                     "srcDir",srcDir,
                     "argL",argNodeL)) != kOkRC )
    goto errLabel;

  if( argNodeL == nullptr )
  {
    rc = cwLogError(kInvalidArgRC,"No crossfades were specified.");
  }
  else
  {
    unsigned argN = argNodeL->child_count(); 
    parallelMixArg_t argL[ argN ];
        
    // for each source file
    for(i=0; i<argNodeL->child_count(); ++i)
    {
      const object_t* o = argNodeL->child_ele(i);

      // parse the non-optional parameters
      if((rc = o->getv("srcBegSec", argL[i].srcBegSec, "srcEndSec", argL[i].srcEndSec, "fadeOutSec", argL[i].fadeOutSec, "srcFn", argL[i].srcFn  )) != kOkRC )
      {
        rc = cwLogError(kInvalidArgRC,"Invalid xform app argument at argument index %i.",i);
      }
      else
      {
        argL[i].gain = 1;
        
        // parse the optional parameters
        if((rc = o->getv_opt("gain", argL[i].gain  )) != kOkRC )
        {
          rc = cwLogError(kInvalidArgRC,"Invalid xform app optional argument at argument index %i.",i);
        }
      }

      //printf("beg:%f end:%f fade:%f %s\n", argL[i].srcBegSec, argL[i].srcEndSec, argL[i].fadeOutSec, argL[i].srcFn );
      
    }

    afSrcDir = filesys::expandPath(srcDir);
    afDstFn  = filesys::expandPath(dstFn);

    // call cross-fader
    if((rc = parallelMix( afDstFn, dstBits, afSrcDir, argL, argN )) != kOkRC )
      goto errLabel;

  }

 errLabel:
  mem::release(afSrcDir);
  mem::release(afDstFn);
  if( rc != kOkRC )
    rc = cwLogError(rc,"Parallel-mix failed.");
  return rc;
}

cw::rc_t cw::afop::transformApp( const object_t* cfg )
{
  rc_t            rc        = kOkRC;
  const char*     srcDir    = nullptr;
  const char*     dryFn     = nullptr;
  const char*     dstPreFn  = nullptr;
  const char*     dstRevFn  = nullptr;
  unsigned        dstBits   = 16;
  const object_t* argNodeL  = nullptr;
  bool            irEnableFl=false;
  const char*     irFn      = nullptr;
  double          irScale   = 1;

  char* expSrcDir   = nullptr;
  char* expDryFn    = nullptr;
  char* expDstPreFn = nullptr;  
  char* expDstRevFn = nullptr;  
  char* expIrFn     = nullptr;
  
  unsigned        i;

  // read the top level cfg record
  if((rc = cfg->getv("dstPreFn",dstPreFn,
                     "dstRevFn",dstRevFn,
                     "dstBits",dstBits,
                     "srcDir",srcDir,
                     "argL",argNodeL,
                     "dryFn",dryFn,
                     "irEnableFl",irEnableFl,
                     "irFn",irFn,
                     "irScale",irScale)) != kOkRC )
    goto errLabel;


  expSrcDir   = filesys::expandPath(srcDir);
  expDryFn    = filesys::expandPath(dryFn);
  expDstPreFn = filesys::expandPath(dstPreFn);
  expDstRevFn = filesys::expandPath(dstRevFn);


  if( argNodeL == nullptr )
  {
    rc = cwLogError(kInvalidArgRC,"No crossfades were specified.");
  }
  else
  {
    unsigned argN = argNodeL->child_count() * 2; 
    parallelMixArg_t argL[ argN ];
        
    // for each source file
    for(i=0; i<argNodeL->child_count(); ++i)
    {
      const object_t* o = argNodeL->child_ele(i);

      unsigned j = i*2;

      // parse the non-optional parameters
      if((rc = o->getv("srcBegSec", argL[j].srcBegSec, "srcEndSec", argL[j].srcEndSec, "fadeOutSec", argL[j].fadeOutSec, "srcFn", argL[j].srcFn  )) != kOkRC )
      {
        rc = cwLogError(kInvalidArgRC,"Invalid xform app argument at argument index %i.",i);
      }
      else
      {
        argL[j].gain = 1;
        
        // parse the optional parameters
        if((rc = o->getv_opt("wetGain", argL[j].gain  )) != kOkRC )
        {
          rc = cwLogError(kInvalidArgRC,"Invalid xform app optional argument at argument index %i.",i);
        }
      }

      // expand the source file name
      argL[j].srcFn  = filesys::makeFn(expSrcDir, argL[j].srcFn, NULL, NULL);

      // form the dry file name
      argL[j+1]       = argL[j];
      argL[j+1].srcFn = expDryFn;
      argL[j+1].gain  = 1.0 - argL[j].gain;
    }

    // call cross-fader
    rc = parallelMix( expDstPreFn, dstBits, "", argL, argN );

    for(unsigned i=0; i<argN; i+=2)
      mem::release( const_cast<char*&>(argL[i].srcFn));

    if( rc == kOkRC && irEnableFl )
    {
      expIrFn     = filesys::expandPath(irFn);
      rc = convolve( expDstRevFn, dstBits, expDstPreFn, expIrFn, irScale );        
    }
  }

 errLabel:

  
  
  mem::release(expSrcDir);
  mem::release(expDryFn);
  mem::release(expDstPreFn);
  mem::release(expDstRevFn);
  mem::release(expIrFn);
  
  if( rc != kOkRC )
    rc = cwLogError(rc,"Transform-app failed.");
  return rc;
  
}



cw::rc_t cw::afop::convolve( const char* dstFn, unsigned dstBits, const char* srcFn, const char* impulseResponseFn, float irScale )
{
  rc_t              rc     = kOkRC;
  float**           hChBuf = nullptr;
  unsigned          hChN   = 0;
  unsigned          hFrmN  = 0;
  double            hSrate = 0;
  float**           xChBuf = nullptr;
  unsigned          xChN   = 0;
  unsigned          xFrmN  = 0;
  audiofile::info_t info;

  audiofile::reportInfo(impulseResponseFn);
  audiofile::reportInfo(srcFn);

  // read the impulse response audio file
  if((rc = audiofile::allocFloatBuf(impulseResponseFn, hChBuf, hChN, hFrmN, info)) != kOkRC )
  {
    rc = cwLogError(rc,"The impulse audio file read failed on '%s'.", cwStringNullGuard(impulseResponseFn));
    goto errLabel;
  }

  hSrate = info.srate;

  // read the source audio file
  if((rc = audiofile::allocFloatBuf(srcFn, xChBuf, xChN, xFrmN, info)) != kOkRC )
  {
    rc = cwLogError(rc,"The source audio file read failed on '%s'.", cwStringNullGuard(srcFn));
    goto errLabel;
  }

  // the sample rate of impulse response and src audio signals must be the same
  if( hSrate != info.srate )
  {
    rc = cwLogError(kInvalidArgRC,"The soure file sample rate %f does not match the impulse response sample rate %f.",info.srate,hSrate);    
  }
  else
  {
    // allocate the output buffer
    float*   yChBuf[ xChN ];
    unsigned yFrmN = xFrmN + hFrmN;
    for(unsigned i=0; i<xChN; ++i)
      yChBuf[i] = mem::allocZ<float>( yFrmN );

    //printf("xFrmN:%i xChN:%i hFrmN:%i hChN:%i yFrmN:%i\n",xFrmN,xChN,hFrmN,hChN,yFrmN);

    // scale the impulse response
    for(unsigned i=0; i<hChN; ++i)
      vop::mul( hChBuf[i], irScale, hFrmN );

    // for each source channel
    for(unsigned i=0; i<xChN  && rc == kOkRC; ++i)
    {
      unsigned j = i >= hChN ? 0 : i; // select the impulse response channel

      // convolve this channel with the impulse response and store into the output buffer
      rc = dsp::convolve::apply( xChBuf[i], xFrmN, hChBuf[j], hFrmN, yChBuf[i], yFrmN );
    }

    // write the output file.
    if( rc == kOkRC )
      rc = audiofile::writeFileFloat( dstFn, info.srate, dstBits, yFrmN, xChN, yChBuf);
       

    // release the output buffer
    for(unsigned i=0; i<xChN; ++i)
      mem::release( yChBuf[i] );    
  }
  
 errLabel:
  if(rc != kOkRC )
    cwLogError(rc,"Audio file convolve failed.");
  
  audiofile::freeFloatBuf(hChBuf, hChN );
  audiofile::freeFloatBuf(xChBuf, xChN );

  return rc;
}

cw::rc_t cw::afop::convolve( const object_t* cfg )
{
  rc_t            rc       = kOkRC;
  const char*     srcFn    = nullptr;
  const char*     dstFn    = nullptr;
  const char*     irFn     = nullptr;
  float           irScale   = 1.0;
  unsigned        dstBits  = 16;
  

  // read the top level cfg record
  if((rc = cfg->getv("dstFn",dstFn,
                     "dstBits",dstBits,
                     "srcFn",srcFn,
                     "irFn",irFn,
                     "irScale",irScale)) != kOkRC )
  {
    cwLogError(rc,"convolve() arg. parse failed.");
  }
  else
  {
    char* sFn = filesys::expandPath(srcFn);
    char* dFn = filesys::expandPath(dstFn);
    char* iFn = filesys::expandPath(irFn);

    rc = convolve( dFn, dstBits, sFn, iFn, irScale );
    
    mem::release(sFn);
    mem::release(dFn);
    mem::release(iFn);
  }

  
  return rc;
}

cw::rc_t cw::afop::generate( const object_t* cfg )
{
  rc_t rc = kOkRC;

  const char*     dstFn    = nullptr;
  char*           outFn    = nullptr;
  unsigned        dstBits  = 16;
  double          dstSrate = 48000;
  double          dstSecs  = 1.0;
  const char*     opLabel  = nullptr;
  const object_t* sineNode = nullptr;
  const object_t* clickNode= nullptr;
  
  // read the top level cfg record
  if((rc = cfg->getv("dstFn",dstFn,
                     "dstBits",dstBits,
                     "dstSrate",dstSrate,
                     "dstSecs",dstSecs,
                     "op",opLabel,
                     "sine",sineNode,
                     "click",clickNode)) != kOkRC )
  {
    cwLogError(rc,"generate() arg. parse failed.");
  }
  else
  {

    outFn = filesys::expandPath(dstFn);
    
    if( textCompare(opLabel,"sine") == 0 )
    {
      double gain = 1.0;
      double hz   = 100.0;
      if((rc = sineNode->getv("gain",gain,"hertz",hz)) != kOkRC )
      {
        rc = cwLogError(rc,"Error parsing generator 'sine' parameters.");
        goto errLabel;
      }
      else
      {
        if((rc = sine( dstFn, dstSrate, dstBits, hz, gain, dstSecs)) != kOkRC )
        {
          rc = cwLogError(rc,"Sine generator failed..");
          goto errLabel;          
        }
      }
    }
    
    if( textCompare(opLabel,"click") == 0 )
    {
      double          gain  = 1.0;
      double          decay = 0.7;
      const object_t* msL   = nullptr;

      if((rc = clickNode->getv("gain",gain,"decay",decay,"msL",msL)) != kOkRC )
      {
        rc = cwLogError(rc,"Error parsing generator 'click' parameters.");
        goto errLabel;
      }
      else
      {
        unsigned msA_N = msL->child_count();
        unsigned msA[ msA_N ];
        
        for(unsigned i=0; i<msA_N; ++i)
          if((rc = msL->child_ele(i)->value( msA[i] )) != kOkRC )
          {
            rc = cwLogError(rc,"Error parsing generator 'click' msL[] values.");
            goto errLabel;
          }

        if((rc = clicks( outFn, dstSrate, dstBits, dstSecs, msA, msA_N, gain, -decay )) != kOkRC )
        {
          rc = cwLogError(rc,"Error generating click audio file.");
          goto errLabel;
        }

      }
    }
  }
  
 errLabel:

  mem::release(outFn);
  
  return rc;
}


cw::rc_t cw::afop::test( const object_t* cfg )
{
  rc_t rc = kOkRC;

  typedef rc_t (*func_t)( const object_t* cfg );
  typedef struct map_str {
    const char* label;
    func_t func;
  } map_t;
  
  map_t mapA[] = {
    { "sine", sine },
    { "mix", mix },
    { "select_to_file",selectToFile },
    { "cut_and_mix",cutAndMix },
    { "parallel_mix",parallelMix },
    { "convolve", convolve },
    { "xfade_convolve", transformApp },
    { "generate", generate }
  };

  unsigned mapN = sizeof(mapA)/sizeof(mapA[0]);
  
  for(unsigned i=0; i<cfg->child_count(); ++i)
  {
    const object_t* pair      = cfg->child_ele(i);
    const map_t*    m         = std::find_if( mapA, mapA+mapN, [pair](const map_t& m){ return textIsEqual(m.label,pair->pair_label()); });
    bool            enable_fl = false;

    if( m == mapA+mapN )
    {
      rc = cwLogError(kInvalidArgRC,"The audio file operation '%s' is not valid.",pair->pair_label());
      goto errLabel;
    }

    if((rc = pair->pair_value()->getv_opt("enable_fl",enable_fl)) != kOkRC )
    {
      goto errLabel;
    }

    if( enable_fl )
    {
      if((rc = m->func( pair->pair_value() )) != kOkRC )
      {
        goto errLabel;
      }
    }
    
  }
  
errLabel:
  return rc;
}
