// Copyright 2015 Emilie Gillet.
//
// Author: Emilie Gillet (emilie.o.gillet@gmail.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// 
// See http://creativecommons.org/licenses/MIT/ for more information.
//
// -----------------------------------------------------------------------------
//
// Onset detector.

#ifndef RINGS_DSP_ONSET_DETECTOR_H_
#define RINGS_DSP_ONSET_DETECTOR_H_

#include "stmlib/stmlib.h"

#include <algorithm>

#include "stmlib/dsp/dsp.h"
#include "stmlib/dsp/filter.h"

namespace rings {

using namespace std;
using namespace stmlib;

class ZScorer {
 public:
  ZScorer() { }
  ~ZScorer() { }
  
  void Init(float cutoff) {
    coefficient_ = cutoff;
    mean_ = 0.0f;
    variance_ = 0.00f;
  }
  
  inline float Normalize(float sample) {
    return Update(sample) / Sqrt(variance_);
  }
  
  inline bool Test(float sample, float threshold) {
    float value = Update(sample);
    return value > Sqrt(variance_) * threshold;
  }

  inline bool Test(float sample, float threshold, float absolute_threshold) {
    float value = Update(sample);
    return value > Sqrt(variance_) * threshold && value > absolute_threshold;
  }

 private:
  inline float Update(float sample) {
    float centered = sample - mean_;
    mean_ += coefficient_ * centered;
    variance_ += coefficient_ * (centered * centered - variance_);
    return centered;
  }
  
  float coefficient_;
  float mean_;
  float variance_;
  
  DISALLOW_COPY_AND_ASSIGN(ZScorer);
};

class Compressor {
 public:  
  Compressor() { }
  ~Compressor() { }
  
  void Init(float attack, float decay, float max_gain) {
    attack_ = attack;
    decay_ = decay;
    level_ = 0.0f;
    skew_ = 1.0f / max_gain;
  }
  
  void Process(const float* in, float* out, size_t size) {
    float level = level_;
    while (size--) {
      SLOPE(level, fabs(*in), attack_, decay_);
      *out++ = *in++ / (skew_ + level);
    }
    level_ = level;
  }
 
 private:
  float attack_; 
  float decay_;
  float level_;
  float skew_;
  
  DISALLOW_COPY_AND_ASSIGN(Compressor);
};

class OnsetDetector {
 public:  
  OnsetDetector() { }
  ~OnsetDetector() { }
  
  void Init(
      float low,
      float low_mid,
      float mid_high,
      float decimated_sr,
      float ioi_time) {
    float ioi_f = 1.0f / (ioi_time * decimated_sr);
    compressor_.Init(ioi_f * 10.0f, ioi_f * 0.05f, 40.0f);
    
    low_mid_filter_.Init();
    mid_high_filter_.Init();
    low_mid_filter_.set_f_q<FREQUENCY_DIRTY>(low_mid, 0.5f);
    mid_high_filter_.set_f_q<FREQUENCY_DIRTY>(mid_high, 0.5f);

    attack_[0] = low_mid;
    decay_[0] = low * 0.25f;

    attack_[1] = low_mid;
    decay_[1] = low * 0.25f;

    attack_[2] = low_mid;
    decay_[2] = low * 0.25f;

    fill(&envelope_[0], &envelope_[3], 0.0f);
    fill(&energy_[0], &energy_[3], 0.0f);
    
    z_df_.Init(ioi_f * 0.05f);
    
    inhibit_time_ = static_cast<int32_t>(ioi_time * decimated_sr);
    inhibit_decay_ = 1.0f / (ioi_time * decimated_sr);
    
    inhibit_threshold_ = 0.0f;
    inhibit_counter_ = 0;
    onset_df_ = 0.0f;
  }
  
  bool Process(const float* samples, size_t size) {
    // Automatic gain control.
    compressor_.Process(samples, bands_[0], size);
    
    // Quick and dirty filter bank - split the signal in three bands.
    mid_high_filter_.Split(bands_[0], bands_[1], bands_[2], size);
    low_mid_filter_.Split(bands_[1], bands_[0], bands_[1], size);

    // Compute low-pass energy and onset detection function
    // (derivative of energy) in each band.
    float onset_df = 0.0f;
    float total_energy = 0.0f;
    for (int32_t i = 0; i < 3; ++i) {
      float* s = bands_[i];
      float energy = 0.0f;
      float envelope = envelope_[i];
      size_t increment = 4 >> i;
      for (size_t j = 0; j < size; j += increment) {
        SLOPE(envelope, s[j] * s[j], attack_[i], decay_[i]);
        energy += envelope;
      }
      energy = Sqrt(energy) * float(increment);
      envelope_[i] = envelope;

      float derivative = energy - energy_[i];
      onset_df += derivative + fabs(derivative);
      energy_[i] = energy;
      total_energy += energy;
    }
    
    onset_df_ += 0.05f * (onset_df - onset_df_);
    bool outlier_in_df = z_df_.Test(onset_df_, 1.0f, 0.01f);
    bool exceeds_energy_threshold = total_energy >= inhibit_threshold_;
    bool not_inhibited = !inhibit_counter_;
    bool has_onset = outlier_in_df && exceeds_energy_threshold && not_inhibited;
    
    if (has_onset) {
      inhibit_threshold_ = total_energy * 1.5f;
      inhibit_counter_ = inhibit_time_;
    } else {
      inhibit_threshold_ -= inhibit_decay_ * inhibit_threshold_;
      if (inhibit_counter_) {
        --inhibit_counter_;
      }
    }
    return has_onset;
  }
  
 private:
  Compressor compressor_;
  NaiveSvf low_mid_filter_;
  NaiveSvf mid_high_filter_;
  
  float attack_[3];
  float decay_[3];
  float energy_[3];
  float envelope_[3];
  float onset_df_;
  
  float bands_[3][32];
  
  ZScorer z_df_;
  
  float inhibit_threshold_;
  float inhibit_decay_;
  int32_t inhibit_time_;
  int32_t inhibit_counter_;
  
  DISALLOW_COPY_AND_ASSIGN(OnsetDetector);
};

}  // namespace rings

#endif  // RINGS_DSP_ONSET_DETECTOR_H_
