/* agcPI

   By: Andrew Tuline

   Date: Jan, 2020

   An Automatic Gain Control for a microphone on an Arduino. Actually, it's really not 
   working yet, but I'm spending time on it.

   This version uses (err should be using) a PI (proportional integral) control loop.

   See: https://en.wikipedia.org/wiki/PID_controller

   It also includes a ghetto peak detection capability, which is displayed on led[0].

*/

#include <FastLED.h>

#define MIC_PIN   A0                                          // Nano or A0 on ESP8266
uint8_t squelch = 7;                                          // Anything below this is background noise, so we'll make it '0'.
int sample;                                                   // Current sample.
float sampleAvg = 0;                                          // Can be used for smoothing signals.
float micLev = 0;                                             // Used to convert returned value to have '0' as minimum.
uint8_t maxVol = 11;                                          // Reasonable value for constant volume (above average) for 'peak detector'.
bool samplePeak = 0;                                          // Boolean flag for peak detected. Responding routine must reset this flag.

uint8_t targetAgc = 60;                                       // This is our setPoint at ~20% of max for the adjusted output.
float kp = 2, ki = 4;                                         // Proportional and Integral tuning constants. Kept as floating point in case we find better tuning.
int err;                                                      // Current offset from our target.
int minn = -20000, maxx = 20000;                              // Keep everything in check with these values.
int samplePI;                                                 // Sensitivity is calculated by the PI routine.
float samplePIAvg;                                            // Calculated and stored average calculated sample is used for closed loop feedback.


#define LED_DT 6
#define NUM_LEDS 48

struct CRGB leds[NUM_LEDS];


void setup() {
  Serial.begin(115200);
//  analogReference(EXTERNAL);                                  // Nano or other 5V AVR8 when using a 3.3V microphone.
  LEDS.addLeds<WS2812, LED_DT, GRB>(leds, NUM_LEDS);
} // setup()


void loop() {
  getSample();                                                // Sample the microphone.
  agcPI();                                                   // Calculated the PI adjusted value as samplePI.
  ledShow();                                                 // Calculate LED's.
  FastLED.show();
} // loop()



void ledShow() {
  // leds[0-15] are for peak detection.
  // leds[16-31] are for the AGC.
  // leds[32-48] are for regular sound.

  fadeToBlackBy(leds, NUM_LEDS, 2);                         // 8 bit, 1 = slow, 255 = fast
  fadeToBlackBy(leds, 1, 128);                              // This is the peak detection LED.

  if (samplePeak) leds[random8(16)] = CRGB::Red;           // Peak detection only on top row.
  leds[random8(16) + 16] = CHSV(samplePI, 255, samplePI);    // AGC sound.
  leds[random8(16) + 32] = CHSV(sample, 255, sample);        // Regular sample.

  Serial.print(targetAgc); Serial.print("\t"); Serial.print(sampleAvg); Serial.print("\t"); Serial.println(samplePIAvg);    // Let's print and compare average vs PI average.

} // ledShow()



void getSample() {

  int16_t micIn;                                              // Current sample starts with negative values and large values, which is why it's 16 bit signed.
  static long peakTime;                                       // How long was the peak.

  micIn = analogRead(MIC_PIN);                                // Poor man's analog Read.
  micLev = ((micLev * 31) + micIn) / 32;                      // Smooth it out over the last 32 samples for automatic centering.
  micIn -= micLev;                                            // Let's center it to 0 now.
  micIn = abs(micIn);                                         // And get the absolute value of each sample.
  sample = (micIn <= squelch) ? 0 : (sample + micIn) / 2;     // Using a ternary operator, the resultant sample is either 0 or it's a bit smoothed out with the last sample.
  sampleAvg = ((sampleAvg * 31) + sample) / 32;               // Smooth it out over the last 32 samples.


// This section just deal with peak detection, and is not related to the PID loop.
  if (millis() > (peakTime+50)) samplePeak = 0;                        // Reset peak if it's been > 50ms since the last one.
  if (sample > (sampleAvg + maxVol) && millis() > (peakTime + 100)) {  // Set peak if it's been > 100ms but IS a peak.
    samplePeak = 1;
    peakTime = millis();
  }
}  // getSample()



void agcPI() {                                                // A PI Control loop to automatically adjust sound sensitivity.

  float sensitivity;                                          // Sensitivity is calculated by the PI routine.
  static float startt = 0;                                    // Accumulated (or integral) value for Ki.

  err = targetAgc - samplePIAvg;                              // Calculate the average error from the target. Is continuously going up. This should be closed loop.
  startt = constrain(startt + err, minn, maxx);               // Calculate summation (Integral) error, but constrain it.

  sensitivity = kp * err + ki * startt;                       // Sensitivity is the direct (Proportional) error plus the above Integral error.
  if (sensitivity <= 3000) sensitivity = 3000;                // Minimum sensitity multiplier should be 1. Andrew Tuline. I don't want divide by 0.
  samplePI = abs(sample * sensitivity / 3000);
  if (samplePI > 255) samplePI = 255;

  samplePIAvg = ((samplePIAvg * 15) + samplePI) / 16;         // Smooth it out over the last 16 samples and use as feedback for the (now closed) PI loop.

  //------------ Oscilloscope output ---------------------------
  //  Serial.print(sample); Serial.print(" ");
  //  Serial.print(sampleAvg); Serial.print(" ");
  //  Serial.print(micLev); Serial.print(" ");
  //  Serial.print(err); Serial.print(" ");
  //  Serial.print(startt); Serial.print(" ");
  //  Serial.print(samplePI); Serial.print(" ");
  //  Serial.print(samplePIAvg); Serial.print(" ");
  //  Serial.print(sensitivity/3000); Serial.print(" ");
  //  Serial.print(targetAgc); Serial.print(" ");
  // Serial.print(0); Serial.print(" ");
  // Serial.println(" ");

} // agcPI()