/**********************************************************************

  Audacity: A Digital Audio Editor

  ClickRemoval.cpp

  Craig DeForest

  Clicks are identified as small regions of high amplitude compared
  to the surrounding chunk of sound.  Anything sufficiently tall compared
  to a large (2048 sample) window around it, and sufficiently narrow,
  is considered to be a click.

  The structure was largely stolen from Domonic Mazzoni's NoiseRemoval
  module, and reworked for the new effect.

  This file is intended to become part of Audacity.  You may modify
  and/or distribute it under the same terms as Audacity itself.

**********************************************************************/

#include "../Audacity.h"

#include <math.h>

#if defined(__WXMSW__) && !defined(__CYGWIN__)
#include <float.h>
#define finite(x) _finite(x)
#endif

#ifdef __MACOSX__
#include <math.h>
#endif

#ifdef __MACOS9__
#include <fp.h>
#define finite(x) isfinite(x)
#endif

#include <wx/msgdlg.h>
#include <wx/textdlg.h>
#include <wx/brush.h>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/statbox.h>
#include <wx/intl.h>

#include "ClickRemoval.h"
#include "../Envelope.h"
// #include "../FFT.h"
#include "../WaveTrack.h"
#include "../Prefs.h"

EffectClickRemoval::EffectClickRemoval()
{
   windowSize = 8192;
//   mThresholdLevel = 200;
//   mClickWidth = 20;
   sep=2049;

   Init();
}

EffectClickRemoval::~EffectClickRemoval()
{
}

#define MAX_THRESHOLD 900
#define MAX_CLICK_WIDTH 40
bool EffectClickRemoval::Init()
{
   mThresholdLevel = gPrefs->Read(wxT("/CsPresets/ClickThresholdLevel"), 200);
   if ((mThresholdLevel < 0) || (mThresholdLevel > MAX_THRESHOLD)) {  // corrupted Prefs?
      mThresholdLevel = 0;  //Off-skip
      gPrefs->Write(wxT("/CsPresets/ClickThresholdLevel"), mThresholdLevel);
   }
   mClickWidth = gPrefs->Read(wxT("/CsPresets/ClickWidth"), 20);
   if ((mClickWidth < 0) || (mClickWidth > MAX_CLICK_WIDTH)) {  // corrupted Prefs?
      mClickWidth = 0;  //Off-skip
      gPrefs->Write(wxT("/CsPresets/ClickWidth"), mClickWidth);
   }
   return true;
}

bool EffectClickRemoval::CheckWhetherSkipEffect()
{
   bool rc = ((mClickWidth == 0) || (mThresholdLevel == 0));
   return rc;
}

bool EffectClickRemoval::PromptUser()
{
   ClickRemovalDialog dlog(this, mParent, -1, _("Click and Pop Removal"));
   dlog.m_pSlider_threshold->SetValue(mThresholdLevel);
   dlog.m_pSlider_width->SetValue(mClickWidth);
   //   dlog.m_pSlider_sep->SetValue(sep);
   dlog.m_pButton_RemoveClicks->SetDefault();
   dlog.m_pButton_RemoveClicks->SetFocus();

   dlog.CentreOnParent();
   dlog.ShowModal();

   if (dlog.GetReturnCode() == 0)
      return false;

   mThresholdLevel = dlog.m_pSlider_threshold->GetValue();
   mClickWidth = dlog.m_pSlider_width->GetValue();
   //   sep = dlog.m_pSlider_sep->GetValue();
   gPrefs->Write(wxT("/CsPresets/ClickThresholdLevel"), mThresholdLevel);
   gPrefs->Write(wxT("/CsPresets/ClickWidth"), mClickWidth);

   return true;
}
bool EffectClickRemoval::TransferParameters( Shuttle & shuttle )
{  
   shuttle.TransferInt(wxT("Threshold"),mThresholdLevel,0);
   shuttle.TransferInt(wxT("Width"),mClickWidth,0);
   return true;
}

bool EffectClickRemoval::Process()
{
   TrackListIterator iter(mWaveTracks);
   WaveTrack *track = (WaveTrack *) iter.First();
   int count = 0;
   while (track) {
      double trackStart = track->GetStartTime();
      double trackEnd = track->GetEndTime();
      double t0 = mT0 < trackStart? trackStart: mT0;
      double t1 = mT1 > trackEnd? trackEnd: mT1;

      if (t1 > t0) {
         longSampleCount start = track->TimeToLongSamples(t0);
         longSampleCount end = track->TimeToLongSamples(t1);
         sampleCount len = (sampleCount)(end - start);

         if (!ProcessOne(count, track, start, len))
            return false;
      }

      track = (WaveTrack *) iter.Next();
      count++;
   }
   return true;
}

bool EffectClickRemoval::ProcessOne(int count, WaveTrack * track,
                                    longSampleCount start, sampleCount len)
{
   bool rc = true;
   sampleCount s = 0;
   sampleCount idealBlockLen = track->GetMaxBlockSize() * 4;

   if (idealBlockLen % windowSize != 0)
      idealBlockLen += (windowSize - (idealBlockLen % windowSize));

   float *buffer = new float[idealBlockLen];

   float *datawindow = new float[windowSize];

   int i;

   while((s < len)&&((len-s)>(windowSize/2))) {
      sampleCount block = idealBlockLen;
      if (s + block > len)
         block = len - s;

      track->Get((samplePtr) buffer, floatSample, start + s, block);

      for(i=0; i<(block-windowSize/2); i+=windowSize/2) {
         int wcopy = windowSize;
         if (i + wcopy > block)
            wcopy = block - i;

         int j;
         for(j=0; j<wcopy; j++)
            datawindow[j] = buffer[i+j];
         for(j=wcopy; j<windowSize; j++)
            datawindow[j] = 0;

   RemoveClicks(windowSize, datawindow);

   for(j=0; j<wcopy; j++)
     buffer[i+j] = datawindow[j];

      }

      track->Set((samplePtr) buffer, floatSample, start + s, block);

      s += block;

      if (TrackProgress(count, s / (double) len)) {
         rc = false;
         break;
      }
   }

   delete[] buffer;
   delete[] datawindow;

   return true;
}

void EffectClickRemoval::RemoveClicks(sampleCount len, float *buffer)
{
   int i;
   int j;
   int left = 0;

   float msw;
   int ww;
   int s2 = sep/2;
   float *ms_seq = new float[len];
   float *b2 = new float[len];

   for( i=0; i<len; i++)
      b2[i] = buffer[i]*buffer[i];

   /* Shortcut for rms - multiple passes through b2, accumulating
    * as we go.
    */
   for(i=0;i<len;i++)
      ms_seq[i]=b2[i];

   for(i=1; i < sep; i *= 2) {
      for(j=0;j<len-i; j++)
         ms_seq[j] += ms_seq[j+i];
      }
   /* Cheat by truncating sep to next-lower power of two... */
      sep = i;

      for( i=0; i<len-sep; i++ ) {
         ms_seq[i] /= sep;
      }
      /* ww runs from about 4 to mClickWidth.  wrc is the reciprocal;
       * chosen so that integer roundoff doesn't clobber us.
       */
      int wrc;
      for(wrc=mClickWidth/4; wrc>=1; wrc /= 2) {
         ww = mClickWidth/wrc;

         for( i=0; i<len-sep; i++ ){
            msw = 0;
            for( j=0; j<ww; j++) {
               msw += b2[i+s2+j];
            }
            msw /= ww;

            if(msw >= mThresholdLevel * ms_seq[i]/10) {
               if( left == 0 ) {
                  left = i+s2;
               }
            } else {
               if(left != 0 && i-left+s2 <= ww*2) {
                  float lv = buffer[left];
                  float rv = buffer[i+ww+s2];
                  for(j=left; j<i+ww+s2; j++) {
                     buffer[j]= (rv*(j-left) + lv*(i+ww+s2-j))/(float)(i+ww+s2-left);
                     b2[j] = buffer[j]*buffer[j];
                  }
                  left=0;
               } else if(left != 0) {
               left = 0;
            }
         }
      }
   }
   delete[] ms_seq;
   delete[] b2;
}


// WDR: class implementations

//----------------------------------------------------------------------------
// ClickRemovalDialog
//----------------------------------------------------------------------------

// WDR: event table for ClickRemovalDialog

//enum {
//  ID_BUTTON_PREVIEW=10002
//};

BEGIN_EVENT_TABLE(ClickRemovalDialog,wxDialog)
  EVT_BUTTON(wxID_OK, ClickRemovalDialog::OnRemoveClicks)
  EVT_BUTTON(wxID_CANCEL, ClickRemovalDialog::OnCancel)
//  EVT_BUTTON(ID_BUTTON_PREVIEW, ClickRemovalDialog::OnPreview)
END_EVENT_TABLE()

ClickRemovalDialog::ClickRemovalDialog(EffectClickRemoval * effect,
                          wxWindow *parent, wxWindowID id,
                          const wxString &title,
                          const wxPoint &position,
                          const wxSize& size,
                          long style ) :
   wxDialog( parent, id, title, position, size, style )
{
   m_pEffect = effect;

   // NULL out the control members until the controls are created.
   m_pButton_GetProfile = NULL;
   m_pSlider_threshold = NULL;
//   m_pButton_Preview = NULL;
   m_pButton_RemoveClicks = NULL;

   this->MakeClickRemovalDialog(true);
}

// WDR: handler implementations for NoiseRemovalDialog
/*
void ClickRemovalDialog::OnPreview(wxCommandEvent &event)
{
  // Save & restore parameters around Preview, because we didn't do OK.
  int oldLevel = m_pEffect->mThresholdLevel;
  int oldWidth = m_pEffect->mClickWidth;
  int oldSep = m_pEffect->sep;

  m_pEffect->mThresholdLevel = m_pSlider->GetValue();
  m_pEffect->mClickWidth = m_pSlider_width->GetValue();
  //  m_pEffect->sep = m_pSlider_sep->GetValue();


  m_pEffect->Preview();

  m_pEffect->sep   = oldSep;
  m_pEffect->mClickWidth = oldWidth;
  m_pEffect->mThresholdLevel = oldLevel;
}
*/
void ClickRemovalDialog::OnRemoveClicks( wxCommandEvent &event )
{
   EndModal(2);
}

void ClickRemovalDialog::OnCancel(wxCommandEvent &event)
{
   EndModal(0);
}

wxSizer *ClickRemovalDialog::MakeClickRemovalDialog(bool call_fit /* = true */,
                                   bool set_sizer /* = true */)
{
   wxBoxSizer *mainSizer = new wxBoxSizer( wxVERTICAL );
   wxStaticBoxSizer *group;
   wxControl *item;

   item = new wxStaticText(this, -1,
                   _("Click and Pop Removal by Craig DeForest"), wxDefaultPosition,
                   wxDefaultSize, wxALIGN_CENTRE );
   mainSizer->Add(item, 0, wxALIGN_CENTRE|wxALL, 5);


   group = new wxStaticBoxSizer(new wxStaticBox(this, -1,
                                                _("Settings")), wxVERTICAL);

   item = new wxStaticText(this, -1,
                           _("\nSelect threshold (lower is more sensitive)"),
                           wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT );
   group->Add(item, 0, wxALIGN_CENTRE|wxALL, 5 );

   m_pSlider_threshold = new wxSlider(this, -1, 8, 0, MAX_THRESHOLD,  wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL);
   group->Add(m_pSlider_threshold, 1, wxEXPAND|wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxTOP, 5 );

   wxBoxSizer *hSizer = new wxBoxSizer(wxHORIZONTAL);
   item = new wxStaticText(this, -1, _("Off"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   hSizer->Add(10, 10, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT | wxBOTTOM, 5);
   item = new wxStaticText(this, -1, _("Medium"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   hSizer->Add(10, 10, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT | wxBOTTOM, 5);
   item = new wxStaticText(this, -1, _("Max"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   group->Add(hSizer, 1, wxEXPAND|wxALIGN_CENTRE|wxALL, 5 );

   hSizer = new wxBoxSizer(wxHORIZONTAL);

   item = new wxStaticText(this,-1,
         _("\n\nMax spike width (higher is more sensitive)"),
         wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
   group->Add(item,0,wxALIGN_CENTER|wxALL,5);

   m_pSlider_width = new wxSlider(this, -1, 8, 0, MAX_CLICK_WIDTH, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL);
   group->Add(m_pSlider_width, 1, wxEXPAND|wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxTOP, 5 );


   //   item = new wxStaticText(this,-1,
   //        _("\nNeighborhood"),
   //        wxDefaultPosition, wxDefaultSize, wxALIGN_LEFT);
   //   group->Add(item,0,wxALIGN_CENTER|wxALL,5);
   //   m_pSlider_sep = new wxSlider(this,-1,8,1,4100,wxDefaultPosition,wxDefaultSize,wxSL_HORIZONTAL);
   //   group->Add(m_pSlider_sep, 1, wxEXPAND|wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxTOP, 5 );

   hSizer = new wxBoxSizer(wxHORIZONTAL);
   item = new wxStaticText(this, -1, _("Off"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   hSizer->Add(10, 10, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT | wxBOTTOM, 5);
   item = new wxStaticText(this, -1, _("Medium"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   hSizer->Add(10, 10, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT | wxBOTTOM, 5);
   item = new wxStaticText(this, -1, _("Max"));
   hSizer->Add(item, 0, wxALIGN_CENTRE|wxLEFT | wxRIGHT | wxBOTTOM, 5 );
   group->Add(hSizer, 1, wxEXPAND|wxALIGN_CENTRE|wxALL, 5 );

   hSizer = new wxBoxSizer(wxHORIZONTAL);
/*
   m_pButton_Preview = new wxButton(this, ID_BUTTON_PREVIEW, m_pEffect->GetPreviewName());
   hSizer->Add(m_pButton_Preview, 0, wxALIGN_LEFT | wxALL, 5);

   hSizer->Add(25, 5); // horizontal spacer
*/
   m_pButton_RemoveClicks = new wxButton(this, wxID_OK, _("Remove clicks"), wxDefaultPosition, wxDefaultSize, 0 );
   hSizer->Add(m_pButton_RemoveClicks, 0, wxALIGN_RIGHT | wxALL, 5 );

   group->Add(hSizer, 0, wxALIGN_CENTER | wxALL, 5 );

   mainSizer->Add( group, 0, wxALIGN_CENTRE|wxALL, 5 );

   item = new wxButton( this, wxID_CANCEL, _("Cancel"), wxDefaultPosition, wxDefaultSize, 0 );
   mainSizer->Add(item, 0, wxALIGN_CENTRE|wxALL, 5 );

   if (set_sizer) {
      this->SetAutoLayout( TRUE );
      this->SetSizer( mainSizer );
      if (call_fit) {
         mainSizer->Fit( this );
         mainSizer->SetSizeHints( this );
      }
   }
   return mainSizer;
}
