/* 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()