/*
  WDL - convoengine.cpp
  Copyright (C) 2006 and later Cockos Incorporated

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
  

*/

#ifdef _WIN32
#include <windows.h>
#endif
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "convoengine.h"

#include "denormal.h"

//#define TIMING
#include "timing.c"

#define CONVOENGINE_SILENCE_THRESH 1.0e-12 // -240dB
#define CONVOENGINE_IMPULSE_SILENCE_THRESH 1.0e-15 // -300dB

static void WDL_CONVO_CplxMul2(WDL_FFT_COMPLEX *c, WDL_FFT_COMPLEX *a, WDL_CONVO_IMPULSEBUFCPLXf *b, int n)
{
  WDL_FFT_REAL t1, t2, t3, t4, t5, t6, t7, t8;
  if (n<2 || (n&1)) return;

  do {
    t1 = a[0].re * b[0].re;
    t2 = a[0].im * b[0].im;
    t3 = a[0].im * b[0].re;
    t4 = a[0].re * b[0].im;
    t5 = a[1].re * b[1].re;
    t6 = a[1].im * b[1].im;
    t7 = a[1].im * b[1].re;
    t8 = a[1].re * b[1].im;
    t1 -= t2;
    t3 += t4;
    t5 -= t6;
    t7 += t8;
    c[0].re = t1;
    c[1].re = t5;
    c[0].im = t3;
    c[1].im = t7;
    a += 2;
    b += 2;
    c += 2;
  } while (n -= 2);
}
static void WDL_CONVO_CplxMul3(WDL_FFT_COMPLEX *c, WDL_FFT_COMPLEX *a, WDL_CONVO_IMPULSEBUFCPLXf *b, int n)
{
  WDL_FFT_REAL t1, t2, t3, t4, t5, t6, t7, t8;
  if (n<2 || (n&1)) return;

  do {
    t1 = a[0].re * b[0].re;
    t2 = a[0].im * b[0].im;
    t3 = a[0].im * b[0].re;
    t4 = a[0].re * b[0].im;
    t5 = a[1].re * b[1].re;
    t6 = a[1].im * b[1].im;
    t7 = a[1].im * b[1].re;
    t8 = a[1].re * b[1].im;
    t1 -= t2;
    t3 += t4;
    t5 -= t6;
    t7 += t8;
    c[0].re += t1;
    c[1].re += t5;
    c[0].im += t3;
    c[1].im += t7;
    a += 2;
    b += 2;
    c += 2;
  } while (n -= 2);
}

static bool CompareQueueToBuf(WDL_FastQueue *q, const void *data, int len)
{
  int offs=0;
  while (len>0)
  {
    void *td=NULL;
    int sz=q->GetPtr(offs,&td);
    if (sz<1) return true; // not enough data = not equal!
    if (sz>len) sz=len;
    
    int i=sz/sizeof(WDL_FFT_REAL);
    WDL_FFT_REAL *a1=(WDL_FFT_REAL*)td;
    WDL_FFT_REAL *b1=(WDL_FFT_REAL*)data;
    while (i--) 
    {
      if (fabs(*a1-*b1)>CONVOENGINE_SILENCE_THRESH) return true;
      a1++;
      b1++;
    }

    data = ((char *)data)+sz;
    offs+=sz;
    len-=sz;
  }
  return false;
}


WDL_ConvolutionEngine::WDL_ConvolutionEngine()
{
  WDL_fft_init();
  m_fft_size=0;
  m_impdata.Add(new ImpChannelInfo);
  m_impulse_len=0;
  m_proc_nch=0;
}

WDL_ConvolutionEngine::~WDL_ConvolutionEngine()
{
  m_impdata.Empty(true);
  m_proc.Empty(true);
}

int WDL_ConvolutionEngine::SetImpulse(WDL_ImpulseBuffer *impulse, int fft_size, int impulse_sample_offset, int max_imp_size, bool forceBrute)
{
  int impulse_len=0;
  int x;
  int nch=impulse->GetNumChannels();
  for (x = 0; x < nch; x ++)
  {
    int l=impulse->impulses[x].GetSize()-impulse_sample_offset;
    if (max_imp_size && l>max_imp_size) l=max_imp_size;
    if (impulse_len < l) impulse_len=l;
  }

  if (nch>1) // detect mono signals pretending to be multichannel
  {
    for (x = 1; x < nch; x ++)
    {
      if (impulse->impulses[x].GetSize()!=impulse->impulses[0].GetSize()||
          memcmp(impulse->impulses[x].Get(),impulse->impulses[0].Get(),
            impulse->impulses[0].GetSize()*sizeof(WDL_FFT_REAL)))
            break;
    }
    if (x >= nch) nch=1;
  }

  m_impulse_len=impulse_len;
  m_proc_nch=-1;

  while (m_impdata.GetSize() > nch)
    m_impdata.Delete(m_impdata.GetSize()-1,true);
  while (m_impdata.GetSize() < nch)
    m_impdata.Add(new ImpChannelInfo);

  if (forceBrute)
  {
    m_fft_size=0;

    // save impulse
    for (x = 0; x < m_impdata.GetSize(); x ++)
    {
      WDL_FFT_REAL *imp=impulse->impulses[x].Get()+impulse_sample_offset;
      int lenout=impulse->impulses[x].GetSize()-impulse_sample_offset;  
      if (max_imp_size && lenout>max_imp_size) lenout=max_imp_size;

      WDL_CONVO_IMPULSEBUFf *impout=m_impdata.Get(x)->imp.Resize(lenout)+lenout;
      while (lenout-->0) *--impout = (WDL_CONVO_IMPULSEBUFf) *imp++;
    }

    for (x = 0; x < m_proc.GetSize(); x ++)
    {
      ProcChannelInfo *inf = m_proc.Get(x);
      inf->samplesin.Clear();
      inf->samplesin2.Clear();
      inf->samplesout.Clear();
    }

    return 0;
  }

 
  if (fft_size<=0)
  {
    int msz=fft_size<=-16? -fft_size*2 : 32768;

    fft_size=32;
    while (fft_size < impulse_len*2 && fft_size < msz) fft_size*=2;
  }

  m_fft_size=fft_size;

  int impchunksize=fft_size/2;
  int nblocks=(impulse_len+impchunksize-1)/impchunksize;
  //wdl_log("il=%d, ffts=%d, cs=%d, nb=%d\n",impulse_len,fft_size,impchunksize,nblocks);

  const bool smallerSizeMode=sizeof(WDL_CONVO_IMPULSEBUFf)!=sizeof(WDL_FFT_REAL);
 
  WDL_FFT_REAL scale=(WDL_FFT_REAL) (1.0/fft_size);
  for (x = 0; x < m_impdata.GetSize(); x ++)
  {
    WDL_FFT_REAL *imp=impulse->impulses[x].Get()+impulse_sample_offset;

    WDL_FFT_REAL *imp2=x < m_impdata.GetSize()-1 ? impulse->impulses[x+1].Get()+impulse_sample_offset : NULL;

    WDL_CONVO_IMPULSEBUFf *impout=m_impdata.Get(x)->imp.Resize((nblocks+!!smallerSizeMode)*fft_size*2);
    char *zbuf=m_impdata.Get(x)->zflag.Resize(nblocks);
    int lenout=impulse->impulses[x].GetSize()-impulse_sample_offset;  
    if (max_imp_size && lenout>max_imp_size) lenout=max_imp_size;
      
    int bl;
    for (bl = 0; bl < nblocks; bl ++)
    {

      int thissz=lenout;
      if (thissz > impchunksize) thissz=impchunksize;

      lenout -= thissz;
      int i=0;    
      WDL_FFT_REAL mv=0.0;
      WDL_FFT_REAL mv2=0.0;
      WDL_FFT_REAL *imptmp = (WDL_FFT_REAL *)impout; //-V615

      for (; i < thissz; i ++)
      {
        WDL_FFT_REAL v=*imp++;
        WDL_FFT_REAL v2=(WDL_FFT_REAL)fabs(v);
        if (v2 > mv) mv=v2;

        imptmp[i*2]=denormal_filter_aggressive(v * scale);

        if (imp2)
        {
          v=*imp2++;
          v2=(WDL_FFT_REAL)fabs(v);
          if (v2>mv2) mv2=v2;
          imptmp[i*2+1]=denormal_filter_aggressive(v*scale);
        }
        else imptmp[i*2+1]=0.0;
      }
      for (; i < fft_size; i ++)
      {
        imptmp[i*2]=0.0;
        imptmp[i*2+1]=0.0;
      }
      if (mv>CONVOENGINE_IMPULSE_SILENCE_THRESH||mv2>CONVOENGINE_IMPULSE_SILENCE_THRESH)
      {
        *zbuf++=mv>CONVOENGINE_IMPULSE_SILENCE_THRESH ? 2 : 1; // 1 means only second channel has content
        WDL_fft((WDL_FFT_COMPLEX*)impout,fft_size,0);

        if (smallerSizeMode)
        {
          int x,n=fft_size*2;
          for(x=0;x<n;x++) impout[x]=(WDL_CONVO_IMPULSEBUFf)imptmp[x];
        }
      }
      else *zbuf++=0;

      impout+=fft_size*2;
    }
  }
  return m_fft_size/2;
}


void WDL_ConvolutionEngine::Reset() // clears out any latent samples
{
  for (int x = 0; x < m_proc.GetSize(); x ++)
  {
    ProcChannelInfo *inf = m_proc.Get(x);
    inf->samplesin.Clear();
    inf->samplesin2.Clear();
    inf->samplesout.Clear();
    inf->hist_pos = 0;
    memset(inf->samplehist_zflag.Get(),0,inf->samplehist_zflag.GetSize());
    memset(inf->samplehist.Get(),0,inf->samplehist.GetSize()*sizeof(WDL_FFT_REAL));
    memset(inf->overlaphist.Get(),0,inf->overlaphist.GetSize()*sizeof(WDL_FFT_REAL));
  }
}

void WDL_ConvolutionEngine::Add(WDL_FFT_REAL **bufs, int len, int nch)
{
  while (m_proc.GetSize() < nch) m_proc.Add(new ProcChannelInfo);
  while (m_proc.GetSize() > nch) m_proc.Delete(m_proc.GetSize()-1,true);

  if (m_fft_size<1)
  {
    m_proc_nch=nch;

    for (int ch = 0; ch < nch; ch ++)
    {
      int wch = ch % m_impdata.GetSize();
      WDL_CONVO_IMPULSEBUFf *imp=m_impdata.Get(wch)->imp.Get();
      int imp_len = m_impdata.Get(wch)->imp.GetSize();
      ProcChannelInfo *pinf = m_proc.Get(ch);

      if (imp_len>0) 
      {
        if (pinf->samplesin2.Available()<imp_len*(int)sizeof(WDL_FFT_REAL))
        {
          int sza=imp_len*sizeof(WDL_FFT_REAL)-pinf->samplesin2.Available();
          memset(pinf->samplesin2.Add(NULL,sza),0,sza);
        }
        WDL_FFT_REAL *psrc;
        
        if (bufs && bufs[ch])
          psrc=(WDL_FFT_REAL*)pinf->samplesin2.Add(bufs[ch],len*sizeof(WDL_FFT_REAL));
        else
        {
          psrc=(WDL_FFT_REAL*)pinf->samplesin2.Add(NULL,len*sizeof(WDL_FFT_REAL));
          memset(psrc,0,len*sizeof(WDL_FFT_REAL));
        }

        WDL_FFT_REAL *pout=(WDL_FFT_REAL*)pinf->samplesout.Add(NULL,len*sizeof(WDL_FFT_REAL));
        int x;
        int len1 = len&~1;
        for (x=0; x < len1 ; x += 2)
        {
          int i=imp_len;
          double sum=0.0,sum2=0.0;
          WDL_FFT_REAL *sp=psrc+x-imp_len + 1;
          WDL_CONVO_IMPULSEBUFf *ip=imp;
          int j=i/4; i&=3;
          while (j--) // produce 2 samples, 4 impulse samples at a time
          {
            double a = ip[0],b=ip[1],aa=ip[2],bb=ip[3];
            double c = sp[1],d=sp[2],cc=sp[3];
            sum+=a * sp[0] + b * c + aa * d + bb * cc;
            sum2+=a * c + b * d + aa * cc + bb * sp[4];
            ip+=4;
            sp+=4;
          }

          while (i--)
          {
            double a = *ip++;
            sum+=a * sp[0];
            sum2+=a * sp[1];
            sp++;
          }
          pout[x]=(WDL_FFT_REAL) sum;
          pout[x+1]=(WDL_FFT_REAL) sum2;
        }
        for(;x<len;x++) // any odd samples left
        {
          int i=imp_len;
          double sum=0.0;
          WDL_FFT_REAL *sp=psrc+x-imp_len + 1;
          WDL_CONVO_IMPULSEBUFf *ip=imp;
          int j=i/4; i&=3;
          while (j--)
          {
            sum+=ip[0] * sp[0] + ip[1] * sp[1] + ip[2] * sp[2] + ip[3] * sp[3];
            ip+=4;
            sp+=4;
          }

          while (i--) sum+=*ip++ * *sp++;
          pout[x]=(WDL_FFT_REAL) sum;
        }
        pinf->samplesin2.Advance(len*sizeof(WDL_FFT_REAL));
        pinf->samplesin2.Compact();
      }
      else
      {
        if (bufs && bufs[ch]) pinf->samplesout.Add(bufs[ch],len*sizeof(WDL_FFT_REAL));
        else
        {
          memset(pinf->samplesout.Add(NULL,len*sizeof(WDL_FFT_REAL)),0,len*sizeof(WDL_FFT_REAL));
        }
      }

    }
    return;
  }


  int impchunksize=m_fft_size/2;
  int nblocks=(m_impulse_len+impchunksize-1)/impchunksize;

  if (m_proc_nch != nch)
  {
    m_proc_nch=nch;

    int mso=0;
    for (int ch = 0; ch < nch; ch ++)
    {
      ProcChannelInfo *pinf = m_proc.Get(ch);
      pinf->hist_pos = 0;
      int so=pinf->samplesin.Available() + pinf->samplesout.Available();
      if (so>mso) mso=so;

      if (m_impulse_len<1||!nblocks) 
      {
        if (pinf->samplesin.Available())
        {
          int s=pinf->samplesin.Available();
          void *buf=pinf->samplesout.Add(NULL,s);
          pinf->samplesin.GetToBuf(0,buf,s);
          pinf->samplesin.Clear();
        }
      }

      if (so < mso)
      {
        memset(pinf->samplesout.Add(NULL,mso-so),0,mso-so);
      }
      
      const int sz=nblocks*m_fft_size;

      memset(pinf->samplehist_zflag.Resize(nblocks),0,nblocks);
      pinf->samplehist.Resize(sz*2);
      pinf->overlaphist.Resize(m_fft_size/2);
      memset(pinf->samplehist.Get(),0,pinf->samplehist.GetSize()*sizeof(WDL_FFT_REAL));
      memset(pinf->overlaphist.Get(),0,pinf->overlaphist.GetSize()*sizeof(WDL_FFT_REAL));
    }
  }

  if (m_impulse_len<1||!nblocks) 
  {
    for (int ch = 0; ch < nch; ch ++)
    {
      ProcChannelInfo *pinf = m_proc.Get(ch);
      if (bufs && bufs[ch])
        pinf->samplesout.Add(bufs[ch],len*sizeof(WDL_FFT_REAL));
      else
        memset(pinf->samplesout.Add(NULL,len*sizeof(WDL_FFT_REAL)),0,len*sizeof(WDL_FFT_REAL));
    }
    // pass through
    return;
  }

  for (int ch = 0; ch < nch; ch ++)
  {
    ProcChannelInfo *pinf = m_proc.Get(ch);
    if (!pinf->samplehist.GetSize()||!pinf->overlaphist.GetSize()) continue;
    pinf->samplesin.Add(bufs ? bufs[ch] : NULL,len*sizeof(WDL_FFT_REAL));
  }
}

void WDL_ConvolutionEngine::AddSilenceToOutput(int len)
{  
  for (int ch = 0; ch < m_proc_nch; ch++)
  {
    ProcChannelInfo *pinf = m_proc.Get(ch);
    memset(pinf->samplesout.Add(NULL,len*sizeof(WDL_FFT_REAL)),0,len*sizeof(WDL_FFT_REAL));
  }
}

int WDL_ConvolutionEngine::Avail(int want)
{
  if (m_fft_size<1)
  {
    ProcChannelInfo *pinf = m_proc.Get(0);
    return pinf ? pinf->samplesout.Available()/sizeof(WDL_FFT_REAL) : 0;
  }

  const int sz=m_fft_size/2;
  const int chunksize=m_fft_size/2;
  const int nblocks=(m_impulse_len+chunksize-1)/chunksize;
  // clear combining buffer
  WDL_FFT_REAL *workbuf2 = m_combinebuf.Resize(m_fft_size*4); // temp space

  for (int ch = 0; ch < m_proc_nch; ch ++)
  {
    ProcChannelInfo *pinf = m_proc.Get(ch);
    ProcChannelInfo *pinf2 = (!(ch&1) && ch+1 < m_proc_nch) ? m_proc.Get(ch+1) : NULL;

    if (!pinf->samplehist.GetSize()||!pinf->overlaphist.GetSize()) continue;
    int srcc=ch % m_impdata.GetSize();

    bool allow_mono_input_mode=true;
    bool mono_impulse_mode=false;

    if (m_impdata.GetSize()==1 && pinf2 &&
        pinf2->samplehist.GetSize()&&pinf2->overlaphist.GetSize() &&
        pinf->samplesin.Available()==pinf2->samplesin.Available() &&
        pinf->samplesout.Available()==pinf2->samplesout.Available()
        )
    { // 2x processing mode
      mono_impulse_mode=true;
      allow_mono_input_mode=false;
    }


    const int in_needed=sz;

    // useSilentList[x] = 1 for mono signal, 2 for stereo, 0 for silent
    char *useSilentList=pinf->samplehist_zflag.GetSize()==nblocks ? pinf->samplehist_zflag.Get() : NULL;
    while (pinf->samplesin.Available()/(int)sizeof(WDL_FFT_REAL) >= sz &&
           pinf->samplesout.Available() < want*(int)sizeof(WDL_FFT_REAL))
    {
      int histpos;
      if ((histpos=++pinf->hist_pos) >= nblocks) histpos=pinf->hist_pos=0;

      // get samples from input, to history
      WDL_FFT_REAL *optr = pinf->samplehist.Get()+histpos*m_fft_size*2;

      pinf->samplesin.GetToBuf(0,optr+sz,in_needed*sizeof(WDL_FFT_REAL));
      pinf->samplesin.Advance(in_needed*sizeof(WDL_FFT_REAL));


      bool mono_input_mode=false;

      bool nonzflag=false;
      if (mono_impulse_mode)
      {
        if (++pinf2->hist_pos >= nblocks) pinf2->hist_pos=0;
        pinf2->samplesin.GetToBuf(0,workbuf2,sz*sizeof(WDL_FFT_REAL));
        pinf2->samplesin.Advance(sz*sizeof(WDL_FFT_REAL));
        for (int i = 0; i < sz; i ++) // unpack samples
        {
          WDL_FFT_REAL f = optr[i*2]=denormal_filter_aggressive(optr[sz+i]);
          if (!nonzflag && (f<-CONVOENGINE_SILENCE_THRESH || f>CONVOENGINE_SILENCE_THRESH)) nonzflag=true;
          f=optr[i*2+1]=denormal_filter_aggressive(workbuf2[i]);
          if (!nonzflag && (f<-CONVOENGINE_SILENCE_THRESH || f>CONVOENGINE_SILENCE_THRESH)) nonzflag=true;
        }
      }
      else
      {
        if (allow_mono_input_mode && 
          pinf2 &&
          srcc<m_impdata.GetSize()-1 &&
          !CompareQueueToBuf(&pinf2->samplesin,optr+sz,sz*sizeof(WDL_FFT_REAL))
          )
        {
          mono_input_mode=true;
        }
        else 
        {
          allow_mono_input_mode=false;
        }

        for (int i = 0; i < sz; i ++) // unpack samples
        {
          WDL_FFT_REAL f=optr[i*2]=denormal_filter_aggressive(optr[sz+i]);
          optr[i*2+1]=0.0;
          if (!nonzflag && (f<-CONVOENGINE_SILENCE_THRESH || f>CONVOENGINE_SILENCE_THRESH)) nonzflag=true;
        }
      }

      for (int i = 1; mono_input_mode && i < nblocks; i ++) // start @ 1, since hist[histpos] is no longer used for here
      {
        int srchistpos = histpos-i;
        if (srchistpos < 0) srchistpos += nblocks;
        if (!useSilentList || useSilentList[srchistpos]==2) mono_input_mode=false;
        else if (useSilentList[srchistpos]==0)
        {
          // left channel was marked as being silent, disable mono if right channel is non-silent
          int srchistpos2 = pinf2->hist_pos + 1 - i; // +1 is because hist_pos has not yet been updated (happens below)
          if (srchistpos2 < 0) srchistpos2 += nblocks;
          if (WDL_NOT_NORMALLY(pinf2->samplehist_zflag.GetSize() != nblocks) ||
                pinf2->samplehist_zflag.Get()[srchistpos2]) mono_input_mode = false;
        }
      }

      if (nonzflag||!useSilentList) memset(optr+sz*2,0,sz*2*sizeof(WDL_FFT_REAL));


#ifdef WDLCONVO_ZL_ACCOUNTING
      m_zl_fftcnt++;
#endif

      if (nonzflag) WDL_fft((WDL_FFT_COMPLEX*)optr,m_fft_size,0);

      if (useSilentList) useSilentList[histpos]=nonzflag ? (mono_input_mode ? 1 : 2) : 0;
    
      int mzfl=2;
      if (mono_input_mode)
      {
        mzfl=1;

        pinf2->samplesin.Advance(sz*sizeof(WDL_FFT_REAL));

        // save a valid copy in sample hist incase we switch from mono to stereo
        if (++pinf2->hist_pos >= nblocks) pinf2->hist_pos=0;
        if (pinf2->samplehist_zflag.GetSize()==nblocks)
          pinf2->samplehist_zflag.Get()[pinf2->hist_pos] = nonzflag ? 1 : 0;

        WDL_FFT_REAL *optr2 = pinf2->samplehist.Get()+pinf2->hist_pos*m_fft_size*2;
        memcpy(optr2,optr,m_fft_size*2*sizeof(WDL_FFT_REAL));
      }

      int applycnt=0;
      char *useImpSilentList=m_impdata.Get(srcc)->zflag.GetSize() == nblocks ? m_impdata.Get(srcc)->zflag.Get() : NULL;

      WDL_CONVO_IMPULSEBUFf *impulseptr=m_impdata.Get(srcc)->imp.Get();
      for (int i = 0; i < nblocks; i ++, impulseptr+=m_fft_size*2)
      {
        int srchistpos = histpos-i;
        if (srchistpos < 0) srchistpos += nblocks;

        if (useImpSilentList && useImpSilentList[i]<mzfl) continue;
        if (useSilentList && !useSilentList[srchistpos]) continue; // silent block

        WDL_FFT_REAL *samplehist=pinf->samplehist.Get() + m_fft_size*srchistpos*2;

        if (applycnt++) // add to output
          WDL_CONVO_CplxMul3((WDL_FFT_COMPLEX*)workbuf2,(WDL_FFT_COMPLEX*)samplehist,(WDL_CONVO_IMPULSEBUFCPLXf*)impulseptr,m_fft_size);   
        else // replace output
          WDL_CONVO_CplxMul2((WDL_FFT_COMPLEX*)workbuf2,(WDL_FFT_COMPLEX*)samplehist,(WDL_CONVO_IMPULSEBUFCPLXf*)impulseptr,m_fft_size);  

      }
      if (!applycnt)
        memset(workbuf2,0,m_fft_size*2*sizeof(WDL_FFT_REAL));
      else
        WDL_fft((WDL_FFT_COMPLEX*)workbuf2,m_fft_size,1);

      WDL_FFT_REAL *olhist=pinf->overlaphist.Get(); // errors from last time
      WDL_FFT_REAL *p1=workbuf2,*p3=workbuf2+m_fft_size,*p1o=workbuf2;

      if (mono_impulse_mode||mono_input_mode)
      {
        WDL_FFT_REAL *p2o=workbuf2+m_fft_size*2;
        WDL_FFT_REAL *olhist2=pinf2->overlaphist.Get(); // errors from last time
        int s=sz/2;
        while (s--)
        {
          p2o[0] = p1[1]+olhist2[0];
          p2o[1] = p1[3]+olhist2[1];
          p1o[0] = p1[0]+olhist[0];
          p1o[1] = p1[2]+olhist[1];
          p1o+=2;
          p2o+=2;
          p1+=4;

          olhist[0]=p3[0];
          olhist[1]=p3[2];
          olhist2[0]=p3[1];
          olhist2[1]=p3[3];
          p3+=4;

          olhist+=2;
          olhist2+=2;
        }
        // add samples to output
        pinf->samplesout.Add(workbuf2,sz*sizeof(WDL_FFT_REAL));
        pinf2->samplesout.Add(workbuf2+m_fft_size*2,sz*sizeof(WDL_FFT_REAL));
      }
      else
      {
        int s=sz/2;
        while (s--)
        {
          p1o[0] = p1[0]+olhist[0];
          p1o[1] = p1[2]+olhist[1];
          p1o+=2;
          p1+=4;

          olhist[0]=p3[0];
          olhist[1]=p3[2];
          p3+=4;

          olhist+=2;
        }
        // add samples to output
        pinf->samplesout.Add(workbuf2,sz*sizeof(WDL_FFT_REAL));
      }
    } // while available

    if (mono_impulse_mode) ch++;
  }

  int mv = want;
  for (int ch=0;ch<m_proc_nch;ch++)
  {
    const ProcChannelInfo *pinf = m_proc.Get(ch);
    int v = pinf ? pinf->samplesout.Available()/sizeof(WDL_FFT_REAL) : 0;
    if (!ch || v<mv)mv=v;
  }
  return mv;
}

WDL_FFT_REAL **WDL_ConvolutionEngine::Get() 
{
  WDL_FFT_REAL **ret = m_get_tmpptrs.ResizeOK(m_proc_nch,false);
  if (WDL_NORMALLY(ret))
    for (int x = 0; x < m_proc_nch; x ++) ret[x]=(WDL_FFT_REAL *)m_proc.Get(x)->samplesout.Get();
  return ret;
}

void WDL_ConvolutionEngine::Advance(int len)
{
  for (int ch = 0; ch < m_proc_nch; ch ++)
  {
    ProcChannelInfo *pinf = m_proc.Get(ch);
    pinf->samplesout.Advance(len*sizeof(WDL_FFT_REAL));
    pinf->samplesout.Compact();
  }
}



/****************************************************************
**  low latency version
*/

WDL_ConvolutionEngine_Div::WDL_ConvolutionEngine_Div()
{
  timingInit();
  for (int x = 0; x < 2; x ++) m_sout.Add(new WDL_Queue);
  m_need_feedsilence=true;
}

int WDL_ConvolutionEngine_Div::SetImpulse(WDL_ImpulseBuffer *impulse, int maxfft_size, int known_blocksize, int max_imp_size, int impulse_offset, int latency_allowed)
{
  m_need_feedsilence=true;

  m_engines.Empty(true);
  if (maxfft_size<0)maxfft_size=-maxfft_size;
  maxfft_size*=2;
  if (!maxfft_size || maxfft_size>32768) maxfft_size=32768;


  const int MAX_SIZE_FOR_BRUTE=64;

  int fftsize = MAX_SIZE_FOR_BRUTE;
  int impulsechunksize = MAX_SIZE_FOR_BRUTE;

  if (known_blocksize && !(known_blocksize&(known_blocksize-1)) && known_blocksize>MAX_SIZE_FOR_BRUTE*2)
  {
    fftsize=known_blocksize/2;
    impulsechunksize=known_blocksize/2;
  }
  if (latency_allowed*2 > fftsize)
  {
    int x = 16;
    while (x <= latency_allowed) x*=2;
    if (x>32768) x=32768;
    fftsize=impulsechunksize=x;
  }

  int offs=0;
  int samplesleft=impulse->impulses[0].GetSize()-impulse_offset;
  if (max_imp_size>0 && samplesleft>max_imp_size) samplesleft=max_imp_size;

  do
  {
    WDL_ConvolutionEngine *eng=new WDL_ConvolutionEngine;

    bool wantBrute = !latency_allowed && !offs;
    if (impulsechunksize*(wantBrute ? 2 : 3) >= samplesleft) impulsechunksize=samplesleft; // early-out, no point going to a larger FFT (since if we did this, we wouldnt have enough samples for a complete next pass)
    if (fftsize>=maxfft_size) { impulsechunksize=samplesleft; fftsize=maxfft_size; } // if FFTs are as large as possible, finish up

    eng->SetImpulse(impulse,fftsize,offs+impulse_offset,impulsechunksize, wantBrute);
    eng->m_zl_delaypos = offs;
    eng->m_zl_dumpage=0;
    m_engines.Add(eng);

#ifdef WDLCONVO_ZL_ACCOUNTING
    wdl_log("ce%d: offs=%d, len=%d, fftsize=%d\n",m_engines.GetSize(),offs,impulsechunksize,fftsize);
#endif

    samplesleft -= impulsechunksize;
    offs+=impulsechunksize;

#if 1 // this seems about 10% faster (maybe due to better cache use from less sized ffts used?)
    impulsechunksize=offs*3;
    fftsize=offs*2;
#else
    impulsechunksize=fftsize;

    fftsize*=2;
#endif
  }
  while (samplesleft > 0);
  
  return GetLatency();
}

int WDL_ConvolutionEngine_Div::GetLatency()
{
  return m_engines.GetSize() ? m_engines.Get(0)->GetLatency() : 0;
}


void WDL_ConvolutionEngine_Div::Reset()
{
  int x;
  for (x = 0; x < m_engines.GetSize(); x ++)
  {
    WDL_ConvolutionEngine *eng=m_engines.Get(x);
    eng->Reset();
  }
  for (x = 0; x < m_sout.GetSize(); x ++)
  {
    m_sout.Get(x)->Clear();
  }

  m_need_feedsilence=true;
}

WDL_ConvolutionEngine_Div::~WDL_ConvolutionEngine_Div()
{
  timingPrint();
  m_engines.Empty(true);
  m_sout.Empty(true);
}

void WDL_ConvolutionEngine_Div::Add(WDL_FFT_REAL **bufs, int len, int nch)
{
  while (m_sout.GetSize() < nch)
    m_sout.Add(new WDL_Queue);
  while (m_sout.GetSize() > nch)
    m_sout.Delete(m_sout.GetSize()-1,true);

  bool ns=m_need_feedsilence;
  m_need_feedsilence=false;

  int x;
  for (x = 0; x < m_engines.GetSize(); x ++)
  {
    WDL_ConvolutionEngine *eng=m_engines.Get(x);
    if (ns)
    {
      eng->m_zl_dumpage = (x>0 && x < m_engines.GetSize()-1) ? (eng->GetLatency()/4) : 0; // reduce max number of ffts per block by staggering them

      if (eng->m_zl_dumpage>0)
        eng->Add(NULL,eng->m_zl_dumpage,nch); // added silence to input (to control when fft happens)
    }

    eng->Add(bufs,len,nch);

    if (ns) eng->AddSilenceToOutput(eng->m_zl_delaypos); // add silence to output (to delay output to its correct time)

  }
}
WDL_FFT_REAL **WDL_ConvolutionEngine_Div::Get() 
{
  WDL_FFT_REAL **ret = m_get_tmpptrs.ResizeOK(m_sout.GetSize(),false);
  if (WDL_NORMALLY(ret))
    for (int x = 0; x < m_sout.GetSize(); x ++) ret[x]=(WDL_FFT_REAL *)m_sout.Get(x)->Get();
  return ret;
}

void WDL_ConvolutionEngine_Div::Advance(int len)
{
  int x;
  for (x = 0; x < m_sout.GetSize(); x ++)
  {
    WDL_Queue *q = m_sout.Get(x);
    q->Advance(len*sizeof(WDL_FFT_REAL));
    q->Compact();
  }
}

int WDL_ConvolutionEngine_Div::Avail(int wantSamples)
{
  timingEnter(1);
  int wso=wantSamples;
  int x;
#ifdef WDLCONVO_ZL_ACCOUNTING
  int cnt=0;
  static int maxcnt=-1;
  int h=0;
#endif
  for (x = 0; x < m_engines.GetSize(); x ++)
  {
    WDL_ConvolutionEngine *eng=m_engines.Get(x);
#ifdef WDLCONVO_ZL_ACCOUNTING
    eng->m_zl_fftcnt=0;
#endif
    int a=eng->Avail(wso+eng->m_zl_dumpage) - eng->m_zl_dumpage;
#ifdef WDLCONVO_ZL_ACCOUNTING
    cnt += !!eng->m_zl_fftcnt;

#if 0
    if (eng->m_zl_fftcnt)
      h|=1<<x;
    
    if (eng->m_zl_fftcnt && x==m_engines.GetSize()-1 && cnt>1)
    {
      wdl_log("fft flags=%08x (%08x=max)\n",h,1<<x);
    }
#endif
#endif
    if (a < wantSamples) wantSamples=a;
  }

#ifdef WDLCONVO_ZL_ACCOUNTING
  static DWORD lastt=0;
  if (cnt>maxcnt)maxcnt=cnt;
  if (GetTickCount()>lastt+1000)
  {
    lastt=GetTickCount();
    wdl_log("maxcnt=%d\n",maxcnt);
    maxcnt=-1;
  }
#endif
  if (wantSamples>0)
  {
    const int add_sz = wantSamples*sizeof(WDL_FFT_REAL);
    for (x =0; x < m_sout.GetSize(); x ++)
    {
      void *add = m_sout.Get(x)->Add(NULL,add_sz);
      if (WDL_NORMALLY(add != NULL)) memset(add,0,add_sz);
    }

    for (x = 0; x < m_engines.GetSize(); x ++)
    {
      WDL_ConvolutionEngine *eng=m_engines.Get(x);
      if (eng->m_zl_dumpage>0) { eng->Advance(eng->m_zl_dumpage); eng->m_zl_dumpage=0; }

      WDL_FFT_REAL **p=eng->Get();
      if (p)
      {
        int i;
        for (i =0; i < m_sout.GetSize(); i ++)
        {
          WDL_Queue *q = m_sout.Get(i);
          const int qsz = q->Available();
          if (WDL_NORMALLY(qsz >= add_sz))
          {
            WDL_FFT_REAL *o=(WDL_FFT_REAL *)((char *)q->Get() + qsz - add_sz);
            const WDL_FFT_REAL *in=p[i];
            int j=wantSamples;
            while (j-->0) *o++ += *in++;
          }
        }
      }
      eng->Advance(wantSamples);
    }
  }
  timingLeave(1);

  WDL_Queue *q0 = m_sout.Get(0);
  int av=WDL_NORMALLY(q0 != NULL) ? (int) (q0->Available()/sizeof(WDL_FFT_REAL)) : 0;
  return av>wso ? wso : av;
}


#ifdef WDL_TEST_CONVO

#include <stdio.h>

int main(int argc, char **argv)
{
  if (argc!=5)
  {
    printf("usage: convoengine fftsize implen oneoffs pingoffs\n");
    return -1;
  }

  int fftsize=atoi(argv[1]);
  int implen = atoi(argv[2]);
  int oneoffs = atoi(argv[3]);
  int pingoffs=atoi(argv[4]);

  if (implen < 1 || oneoffs < 0 || oneoffs >= implen || pingoffs < 0)
  {
    printf("invalid parameters\n");
    return -1;
  }

  WDL_ImpulseBuffer imp;
  imp.nch=1;
  memset(imp.impulses[0].Resize(implen),0,implen*sizeof(WDL_FFT_REAL));
  imp.impulses[0].Get()[oneoffs]=1.0;


#if WDL_TEST_CONVO==2
  WDL_ConvolutionEngine_Div engine;
#else
  WDL_ConvolutionEngine engine;
#endif
  engine.SetImpulse(&imp,fftsize);
  WDL_TypedBuf<WDL_FFT_REAL> m_tmpbuf;
  memset(m_tmpbuf.Resize(pingoffs+1),0,pingoffs*sizeof(WDL_FFT_REAL));
  m_tmpbuf.Get()[pingoffs]=1.0;
  WDL_FFT_REAL *p=m_tmpbuf.Get();
  engine.Add(&p,pingoffs+1,1);
  
  p=m_tmpbuf.Resize(4096);
  memset(p,0,m_tmpbuf.GetSize()*sizeof(WDL_FFT_REAL));

  int avail;
  while ((avail=engine.Avail(pingoffs+oneoffs + 8192)) < pingoffs+oneoffs + 8192)
  {
    engine.Add(&p,4096,1);
  }
  WDL_FFT_REAL **output = engine.Get();
  if (!output || !*output)
  {
    printf("cant get output\n");
    return -1;
  }
  int x;
  for (x = 0; x < avail; x ++)
  {
    WDL_FFT_REAL val=output[0][x];
    WDL_FFT_REAL expval = (x==pingoffs+oneoffs) ? 1.0:0.0;
    if (fabs(val-expval)>0.000000001)
    {
      printf("%d: %.4fdB - %f %f\n",x,log10(max(val,0.000000000001))*20.0 - log10(max(expval,0.000000000001))*20.0,val,expval);
    }
  }

  return 0;
}

#endif


int WDL_ImpulseBuffer::SetLength(int samples)
{
  const int nch = impulses.list.GetSize();
  if (!nch) return 0;
  for (int x=0;x<nch;x++)
  {
    int cursz=impulses[x].GetSize();
    if (cursz!=samples) 
    {
      impulses[x].Resize(samples,false);

      if (impulses[x].GetSize()!=samples) // validate length!
      {
        // ERROR! FREE ALL!
        for(x=0;x<impulses.list.GetSize();x++) impulses[x].Resize(0);
        return 0;
      }
    }

    if (cursz<samples)
      memset(impulses[x].Get()+cursz,0,(samples-cursz)*sizeof(WDL_FFT_REAL));     
  }
  return impulses[0].GetSize();
}


void WDL_ImpulseBuffer::SetNumChannels(int usench, bool duplicateExisting)
{
  if (usench<1) usench=1;

  const int old_nch = impulses.list.GetSize();
  if (usench > old_nch)
  {
    while (impulses.list.GetSize() < usench)
      impulses.list.Add(new WDL_TypedBuf<WDL_FFT_REAL>);

    const int len = SetLength(GetLength());

    int x,ax=0;
    if (duplicateExisting && len>0 && old_nch>0) for(x=old_nch;x<usench;x++)
    {
      memcpy(impulses[x].Get(),impulses[ax].Get(),len*sizeof(WDL_FFT_REAL)); // duplicate channels
      if (++ax>=old_nch) ax=0;
    }
  }
  else while (usench<impulses.list.GetSize())
  {
    impulses.list.Delete(impulses.list.GetSize()-1,true);
  }
}
