//| 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 "cwVectOps.h"
#include "cwMath.h"
#include "cwDspTypes.h"
#include "cwDsp.h"
#include "cwDspTransforms.h"

namespace cw
{
  namespace dsp
  {
    namespace compressor
    {
      void _ms_to_samples( obj_t*p, ftime_t ms, unsigned& outRef )
      {
        outRef = std::max(1u,(unsigned)floor(ms * p->srate / 1000.0));
      }
    }
  }
}

//----------------------------------------------------------------------------------------------------------------
// compressor
//

cw::rc_t cw::dsp::compressor::create( obj_t*& p, srate_t srate, unsigned procSmpCnt, coeff_t inGain, ftime_t rmsWndMaxMs, ftime_t rmsWndMs, coeff_t threshDb, coeff_t ratio_num, ftime_t atkMs, ftime_t rlsMs, coeff_t outGain, bool bypassFl )
{
  p = mem::allocZ<obj_t>();
      
  p->srate      = srate;
  p->procSmpCnt = procSmpCnt;
  p->threshDb   = threshDb;
  p->ratio_num  = ratio_num;

  set_attack_ms(p,atkMs);
  set_release_ms(p,rlsMs);

  p->inGain     = inGain;
  p->outGain    = outGain;
  p->bypassFl   = bypassFl;

  p->rmsWndAllocCnt = (unsigned)std::max(1.0,floor(rmsWndMaxMs * srate / (1000.0 * procSmpCnt)));
  p->rmsWnd         = mem::allocZ<sample_t>(p->rmsWndAllocCnt);
  set_rms_wnd_ms(p, rmsWndMs );
  p->rmsWndIdx = 0; 

  p->state       = kRlsCompId;
  p->timeConstDb = 10.0;
  p->accumDb     = p->threshDb;

  return kOkRC;
}
    
cw::rc_t cw::dsp::compressor::destroy( obj_t*& p )
{
  mem::release(p->rmsWnd);
  mem::release(p);
  return kOkRC;
}

/*
  The ratio determines to what degree a signal above the threshold is reduced.
  Given a 2:1 ratio, a signal 2dB above the threshold will be reduced to 1db above the threshold.
  Given a 4:1 ratio, a signal 2dB above the threshold will be reduced to 0.25db above the threshold.
  Gain_reduction_db = (thresh - signal) / ratio_numerator  (difference between the threshold and signal level after reduction)
  Gain Coeff = 10^(gain_reduction_db / 20);
 
  Total_reduction_db = signal - threshold + Gain_reduc_db
  (total change in signal level)

  The attack can be viewed as beginning at the threshold and moving to the peak
  over some period of time. In linear terms this will go from 1.0 to the max gain
  reductions. In this case we step from thresh to peak at a fixed rate in dB
  based on the attack time.

  Db:        thresh - [thesh:peak] / ratio_num
  Linear: pow(10, (thresh - [thesh:peak] / ratio_num)/20 );

  During attacks p->accumDb increments toward the p->pkDb.
  During release p->accumDb decrements toward the threshold.
  
  (thresh - accumDb) / ratio_num gives the signal level which will be achieved
  if this value is converted to linear form and applied as a gain coeff.

  See compressor.m
*/
    
cw::rc_t cw::dsp::compressor::exec( obj_t* p, const sample_t* x, sample_t* y, unsigned n, bool enable_fl )
{

  sample_t xx[n];

  vop::mul(xx,x,p->inGain,n); // apply input gain

  p->rmsWnd[ p->rmsWndIdx ] = vop::rms(xx, n);               // calc and store signal RMS
  p->rmsWndIdx              = (p->rmsWndIdx + 1) % p->rmsWndCnt; // advance the RMS storage buffer

  coeff_t rmsLin = vop::mean(p->rmsWnd,p->rmsWndCnt);                   // calc avg RMS
  coeff_t rmsDb  = std::max(-100.0,20 * log10(std::max((coeff_t)0.00001,rmsLin)));  // convert avg RMS to dB
  rmsDb += 100.0;

  // if the compressor is bypassed
  if( p->bypassFl || !enable_fl )
  {
    vop::copy(y,x,n); // copy through - with no input gain
    return kOkRC;
  }

  // if the signal is above the threshold
  if( rmsDb <= p->threshDb )
    p->state = kRlsCompId;
  else
  {
    if( rmsDb > p->pkDb )  
      p->pkDb   = rmsDb;
    
    p->state  = kAtkCompId;    
  }

  switch( p->state )
  {
    case kAtkCompId:                      
      p->accumDb = std::min(p->pkDb, p->accumDb + p->timeConstDb * n / p->atkSmp );
      break;

    case kRlsCompId:
      p->accumDb = std::max(p->threshDb, p->accumDb - p->timeConstDb * n / p->rlsSmp );
      break;
  }

  p->gain = pow(10.0,(p->threshDb - p->accumDb) / (p->ratio_num * 20.0));

  vop::mul(y,xx,p->gain * p->outGain,n);
  
  return kOkRC;
      
}

    
void cw::dsp::compressor::set_attack_ms(  obj_t* p, ftime_t ms )
{
  _ms_to_samples(p,ms,p->atkSmp);
}
    
void cw::dsp::compressor::set_release_ms( obj_t* p, ftime_t ms )
{
  _ms_to_samples(p,ms,p->rlsSmp);
}
    
void cw::dsp::compressor::set_rms_wnd_ms( obj_t* p, ftime_t ms )
{
  p->rmsWndCnt = std::max((unsigned)1,(unsigned)floor(ms * p->srate / (1000.0 * p->procSmpCnt)));

  // do not allow rmsWndCnt to exceed rmsWndAllocCnt
  if( p->rmsWndCnt > p->rmsWndAllocCnt )
    p->rmsWndCnt = p->rmsWndAllocCnt;      
}

//----------------------------------------------------------------------------------------------------------------
// Limiter
//

cw::rc_t cw::dsp::limiter::create( obj_t*& p, srate_t srate, unsigned procSmpCnt, coeff_t thresh, coeff_t igain, coeff_t ogain, bool bypassFl )
{
  p = mem::allocZ<obj_t>();

  p->procSmpCnt = procSmpCnt;
  p->thresh     = thresh;
  p->igain      = igain;
  p->ogain      = ogain;
  return kOkRC;
}

cw::rc_t cw::dsp::limiter::destroy( obj_t*& p )
{
  mem::release(p);
  return kOkRC;
}

cw::rc_t cw::dsp::limiter::exec( obj_t* p, const sample_t* x, sample_t* y, unsigned n )
{
  if( p->bypassFl )
  {
    vop::copy(y,x,n); // copy through - with no input gain
    return kOkRC;
  }
  else
  {
    coeff_t T = p->thresh * p->ogain;
  
    for(unsigned i=0; i<n; ++i)
    {
      sample_t mx = 0.999;
      sample_t s = x[i] < 0.0 ? -mx : mx;
      sample_t v = fabsf(x[i]) * p->igain;
      
      if( v >= mx )
        y[i] = s;
      else
      {
        if( v < p->thresh )
        {
          y[i] = s * T * v/p->thresh;
        }    
        else
        {
          // apply a linear limiting function
          y[i] = s * (T + (1.0f-T) * (v-p->thresh)/(1.0f-p->thresh));
        }
      }
    }
  }
  return kOkRC;
}

//----------------------------------------------------------------------------------------------------------------
// dc-filter
//

cw::rc_t cw::dsp::dc_filter::create( obj_t*& p, srate_t srate, unsigned procSmpCnt, coeff_t gain, bool bypassFl )
{
  p = mem::allocZ<obj_t>();

  p->gain     = gain;
  p->bypassFl = bypassFl;
  p->b0       = 1;
  p->b[0]     = -1;
  p->a[0]     = -0.999;
  p->d[0]     = 0;
  p->d[1]     = 0;
  
  
  return kOkRC;
}

cw::rc_t cw::dsp::dc_filter::destroy( obj_t*& pp )
{
  mem::release(pp);
  return kOkRC;
}

cw::rc_t cw::dsp::dc_filter::exec( obj_t* p, const sample_t* x, sample_t* y, unsigned n )
{

  if( p->bypassFl )
    vop::copy(y,x,n);
  else
    vop::filter<sample_t,coeff_t>(y,n,x,n,p->b0, p->b, p->a,  p->d, 1 );

  return kOkRC;
}

cw::rc_t cw::dsp::dc_filter::set( obj_t* p, coeff_t gain, bool bypassFl )
{
  p->gain = gain;
  p->bypassFl = bypassFl;
  return kOkRC;
}


//----------------------------------------------------------------------------------------------------------------
// Recorder
//

cw::rc_t cw::dsp::recorder::create(  obj_t*& pRef, srate_t srate, ftime_t max_secs, unsigned chN )
{
  obj_t* p     = mem::allocZ<obj_t>();
  p->srate     = srate;
  p->maxFrameN = (unsigned)(max_secs * srate);
  p->chN       = chN;
  p->buf       = mem::allocZ<sample_t>( p->maxFrameN * p->chN );

  pRef = p;

  return kOkRC;
}

cw::rc_t cw::dsp::recorder::destroy( obj_t*& pRef)
{
  obj_t* p = pRef;

  mem::release(p->buf);
  mem::release(p);
  
  pRef = nullptr;
  return kOkRC;
}

cw::rc_t cw::dsp::recorder::exec( obj_t* p, const sample_t* buf, unsigned chN, unsigned frameN )
{
  const sample_t* chA[ chN ];
  for(unsigned i=0; i<chN; ++i)
    chA[i] = buf + (i*frameN);

  return exec(p, chA, chN, frameN );
}

cw::rc_t cw::dsp::recorder::exec( obj_t* p, const sample_t* chA[], unsigned chN, unsigned frameN )
{
  chN = std::min( chN, p->chN );
  
  // 
  if( p->frameIdx + frameN > p->maxFrameN )
    frameN = p->maxFrameN - p->frameIdx;
  
  for(unsigned i=0; i<chN; ++i)
    for(unsigned j=0; j<frameN; ++j )
      p->buf[ i*p->maxFrameN + p->frameIdx + j ] = chA[i][j];

  p->frameIdx += frameN;

  return kOkRC;
}

cw::rc_t cw::dsp::recorder::write(   obj_t* p, const char* fname )
{
  file::handle_t h;
  cw::rc_t rc;
  
  if((rc = file::open(h,fname, file::kWriteFl )) != kOkRC )
  {
    rc = cwLogError(rc,"Recorder file open failed on '%s'.", cwStringNullGuard(fname));
    goto errLabel;
  }

  file::printf(h,"{\n");

  file::printf(h,"\"srate\":%f,\n",p->srate);
  file::printf(h,"\"maxFrameN\":%i,\n",p->frameIdx);

  for(unsigned i=0; i<p->chN; ++i)
  {
    file::printf(h,"\"%i\":[",i);

    for(unsigned j=0; j<p->frameIdx; ++j)
      file::printf(h,"%f%c\n",p->buf[ p->maxFrameN*i + j ], j+1==p->frameIdx ? ' ' : ',');
    
    file::printf(h,"]\n");
  }
  
  file::printf(h,"}\n");

 errLabel:

  if((rc = file::close(h)) != kOkRC )
  {
    rc = cwLogError(rc,"Recorder file close failed on '%s'.", cwStringNullGuard(fname));
    goto errLabel;
  }

  return rc;
}

//----------------------------------------------------------------------------------------------------------------
// Audio Meter
//
namespace cw {
  namespace dsp {
    namespace audio_meter {
      sample_t _sum_square( const sample_t* v, unsigned vn, bool& clipFlRef )
      {
	sample_t sum = 0;
	for(unsigned i=0; i<vn; ++i)
	{
	   sample_t x = v[i]*v[i];
	   sum += x;
	   clipFlRef = x > 1.0;
	   
	}
	return sum;
      }
    }
  }
}

cw::rc_t cw::dsp::audio_meter::create( obj_t*& p, srate_t srate, ftime_t maxWndMs, ftime_t wndMs, coeff_t peakThreshDb )
{
  rc_t rc = kOkRC;

  if( maxWndMs < wndMs )
  {
    cwLogWarning("Audio meter Max. window length (%f ms) is less than requested window length (%f ms). Setting max window length to %f ms.",maxWndMs,wndMs,wndMs);
    maxWndMs = wndMs;
  }
  
  p             = mem::allocZ<obj_t>();
  p->maxWndMs   = maxWndMs;
  p->maxWndSmpN = (unsigned)((maxWndMs * srate)/1000.0);
  p->wndV       = mem::allocZ<sample_t>(p->maxWndSmpN);
  p->srate      = srate;
  p->peakThreshDb = peakThreshDb;
  p->wi         = 0;

  set_window_ms( p, wndMs );
  reset(p);
  
  return rc;
}

cw::rc_t cw::dsp::audio_meter::destroy( obj_t*& pp )
{
  rc_t rc = kOkRC;
  mem::release(pp->wndV);
  mem::release(pp);
  return rc;
}

cw::rc_t cw::dsp::audio_meter::exec( obj_t* p, const sample_t* xV, unsigned xN )
{
  rc_t rc = kOkRC;
  unsigned n = 0;

  // copy the incoming audio samples to the buffer
  while( xN )
  {
    unsigned n0 = std::min( p->maxWndSmpN - p->wi, xN );
    
    vop::copy(p->wndV + p->wi, xV + n, n0 );
    
    n += n0;
    xN -= n0;

    p->wi = (p->wi + n0) % p->maxWndSmpN;
  }

  // get the starting and ending locations of the RMS sub-buffers
  
  unsigned i0 = 0, i1 = 0;
  unsigned n0 = 0, n1 = 0;
  
  if( p->wi >= p->wndSmpN )
  {
    i0 = p->wi - p->wndSmpN;
    n0 = p->wndSmpN;
  }
  else
  {
    i1 = 0;
    n1 = p->wi;
    n0 = p->wndSmpN - n1;
    i0 = p->maxWndSmpN - n0;
  }

  // calc the squared-sum of the RMS buffers
  sample_t sum = _sum_square(p->wndV + i0, n0, p->clipFl);
  if( n1 )
    sum += _sum_square(p->wndV + i1, n1, p->clipFl );

  p->outLin = std::sqrt( sum / (n0+n1) );  // linear RMS
  p->outDb  = ampl_to_db(p->outLin);           // RMS dB

  p->peakFl = p->outDb > p->peakThreshDb;   // set peak flag
  p->clipFl = vop::max(xV,xN) >= 1.0;       // set clip flag

  p->peakCnt += p->peakFl ? 1 : 0;          // count peak violations
  
  return rc;
}

void cw::dsp::audio_meter::reset( obj_t* p )
{
  p->peakFl  = false;
  p->clipFl  = false;
  p->peakCnt = 0;
  p->clipCnt = 0;
}

void cw::dsp::audio_meter::set_window_ms( obj_t* p, ftime_t wndMs )
{
  unsigned wndSmpN  = (unsigned)((wndMs * p->srate)/1000.0);

  if( wndSmpN <= p->maxWndSmpN )
    p->wndSmpN = wndSmpN;
  else
  {
    cwLogWarning("The audio meter window length (%f ms) exceeds the max. window length (%f ms). The window length was reduced to (%f ms).",wndMs,p->maxWndMs,p->maxWndMs);
    p->wndSmpN = p->maxWndSmpN;
  }
  
  
}
