// Circuit Playground Birthday Candles
//
// All the NeoPixels will flicker like candles and the speaker will
// play the tune to Happy Birthday continuously.  Blow on the board
// to slowly blow out all the candles and hear a victory song!
// Controls:
//  - Slide switch: Move this to +/on to hear music or -/off to disable sound.
//  - Left button: Hold this down and press reset to use a rainbow flicker color.
//  - Right button: Hold this down and press reset to use a simple solid color (no flicker).
//  - If neither left or right button are pressed at startup then a flame-like
//    flickering animation will be used.
// After all the candles are blown out press reset to reset and start over!
//
// NOTE: This currently works with only Circuit Playground Classic!
//       Express support is coming soon!
// Author: Tony DiCola
// License: MIT (https://opensource.org/licenses/MIT)
#include <Adafruit_CircuitPlayground.h>

// General configuration defines:
#define BREATH_THRESHOLD  900   // Peak to peak value from the sound sensor that
                                // determines if someone is blowing on the board.
                                // Use a very large value close to 1023 (but not
                                // over) where the higher the value the more
                                // forceful/loudly you need to blow on the board.

#define FLAME_LIFE_MS     200   // Amount of time (in milliseconds) that each
                                // candle flame takes to blow out.  Increase this
                                // to make it harder/slower to blow them all out
                                // and decrease it to make it easier/faster.

#define FLAME_HUE         35    // Primary hue of the flame.  This is a value in
                                // degrees from 0.0 to 360.0, see HSV color space
                                // for different hue values:
                                //   https://en.wikipedia.org/wiki/HSL_and_HSV#/media/File:Hsl-hsv_models.svg
                                // The value 35 degrees is a nice orange in the
                                // middle of red and yellow hues that will look like
                                // a flame flickering as the hues animate.
                                // For the flicker effect the pixels will cycle 
                                // around hues that are +/-10 degrees of this value.
                                // For the solid effect the pixels will be set
                                // to this hue (at full saturation and value).
                                // Rainbow effect ignores this hue config and
                                // cycles through all of them.

#define LIT_CANDLES       10    // The number of candles to start with lit.  By
                                // default all 10 candles/pixels will be lit but
                                // adjust down to light less for a young kid's
                                // birthday.

// A few music note frequencies as defined in this tone example:
//   https://www.arduino.cc/en/Tutorial/toneMelody
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988

// Define note durations.  You only need to adjust the whole note
// time and other notes will be subdivided from it directly.
#define WHOLE         2200       // Length of time in milliseconds of a whole note (i.e. a full bar).
#define HALF          WHOLE/2
#define QUARTER       HALF/2
#define EIGHTH        QUARTER/2
#define EIGHTH_TRIPLE QUARTER/3
#define SIXTEENTH     EIGHTH/2

// Color gamma correction table, this makes the hues of the NeoPixels
// a little more accurate by accounting for our eye's non-linear color
// sensitivity.  See this great guide for more details:
//   https://learn.adafruit.com/led-tricks-gamma-correction/the-issue
const uint8_t PROGMEM gamma8[] = {
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
    1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
    2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
    5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
   10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
   17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
   25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
   37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
   51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
   69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
   90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
  115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
  144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
  177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
  215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 };


// Global program state.
int lit = LIT_CANDLES;           // How many candles are still lit.  Once all are extinguished this falls to -1.
float frequencies[10] = {0};     // Random frequencies and phases will be generated for each pixel to
float phases[10] = {0};          // define an organic but random looking flicker effect.
enum AnimationType { SOLID, FLICKER, RAINBOW };  // Which type of animation is currently running.
AnimationType animation = FLICKER;


// Play a note of the specified frequency and for the specified duration.
// Hold is an optional bool that specifies if this note should be held a
// little longer, i.e. for eigth notes that are tied together.
// While waiting for a note to play the waitBreath delay function is used
// so breath detection and pixel animation continues to run.  No tones
// will play if the slide switch is in the -/off position or all the
// candles have been blown out.
void playNote(int frequency, int duration, bool hold=false, bool measure=true) {
  // Check if the slide switch is off or the candles have been blown out
  // and stop immediately without playing anything.
  if (!CircuitPlayground.slideSwitch() || lit < 0) {
    return;
  }
  if (hold) {
    // For a note that's held play it a little longer than the specified duration
    // so it blends into the next tone (but there's still a small delay to 
    // hear the next note).
    CircuitPlayground.playTone(frequency, duration + duration/32, false);
  }
  else {
    // For a note that isn't held just play it for the specified duration.
    CircuitPlayground.playTone(frequency, duration, false);
  }
  // Use waitBreath instead of Arduino's delay because breath detection and
  // pixel animation needs to occur while music plays.  However skip this logic
  // if not asked for (measure = false, only needed when playing celebration
  // song so as to not continue waiting for breaths).
  if (measure) {
    waitBreath(duration + duration/16);
  }
  else {
    delay(duration + duration/16);
  }
}

// Delay for the specified number of milliseconds while at the same time
// listening for a continuous loud sound on from the microphone, i.e. someone
// blowing directly on it, and animating the NeoPixels.  When a breath is detected
// the number of lit candles will gradually decrease.  Once the number of lit
// candles hits zero then a celebration tune is played and everything stops.
void waitBreath(uint32_t milliseconds) {
  float peakToPeak = measurePeak(milliseconds);
  // Serial.println(peakToPeak);
  while (peakToPeak >= BREATH_THRESHOLD) {
    // Decrement the number of lit candles and keep it from going below
    // the value -1 (a sentinel that indicates all the candles are blown out
    // and no music playback, etc. should occur anymore).
    lit = max(lit-1, -1);
    // For the simple solid color animation (i.e. no flickering) only update
    // the pixels when the lit pixel count changes.  This allows the tone
    // playback to sound better because the pixels don't need to be updated
    // during delays and music note playback (the pixel writing messes with
    // interrupts that drive tone playback and cause scratchier sounding tones).
    if (animation == SOLID) {
      showLitSolid();
    }
    // Check if we just hit 0 candles lit up, i.e. they're all blown out.
    // Turn off the pixels and play a little celebration tune when it 
    // happens, then indicate the candles are blown out with the -1 sentinel value.
    if (lit == 0) {
      CircuitPlayground.clearPixels();
      celebrateSong();
      lit = -1;
    }
    // Keep measuring the peak to peak sound value for a period of time
    // that it takes to blow out another candle.
    peakToPeak = measurePeak(FLAME_LIFE_MS);
  }
}

// Song to play when the candles are blown out.
void celebrateSong() {
  // Play a little charge melody, from:
  //  https://en.wikipedia.org/wiki/Charge_(fanfare)
  // Note the explicit boolean parameters in particular the measure=false
  // at the end.  This means the notes will play without any breath measurement
  // logic.  Without this false value playNote will try to keep waiting for candles
  // to blow out during the celebration song!
  playNote(NOTE_G4, EIGHTH_TRIPLE, true, false);
  playNote(NOTE_C5, EIGHTH_TRIPLE, true, false);
  playNote(NOTE_E5, EIGHTH_TRIPLE, false, false);
  playNote(NOTE_G5, EIGHTH, true, false);
  playNote(NOTE_E5, SIXTEENTH, false);
  playNote(NOTE_G5, HALF, false);
}

// Measure the peak to peak (distance between min and max values)
// values from the sound sensor for a specified number of milliseconds.
// While measuring the current NeoPixel animation will be updated too.
float measurePeak(uint32_t milliseconds) {
  uint16_t soundMin = 1023;  // Keep track of the min and max sound sensor values.
  uint16_t soundMax = 0;
  // Loop until the specified number of milliseconds have ellapsed.
  uint32_t start = millis();
  uint32_t current = start;
  while ((current - start) < milliseconds) {
    // Inside the loop update the min and max sensor values.
    uint16_t sample = CircuitPlayground.soundSensor();
    soundMin = min(sample, soundMin);
    soundMax = max(sample, soundMax);
    // Be sure to drive the NeoPixel animation too.
    animatePixels(current);
    current = millis();
  }
  // Finally compute the peak to peak distance between min and max and return it.
  float peakToPeak = soundMax - soundMin;
  return peakToPeak;
}

// Perform a frame of animation on the NeoPixels.
// Current is the current monotonically increasing time in milliseconds.
void animatePixels(uint32_t current) {
  switch (animation) {
    case FLICKER:
      showLitFlicker(current);
      break;
    case RAINBOW:
      showLitRainbow(current);
      break;
    // Ignore the SOLID case as it has no animation.
    // This makes the audio smoother since it doesn't get interrupted by
    // NeoPixel writing like the other animations.  The pixels are instead
    // changed only once when the number of lit candles changes (see the
    // waitBreath function's loop).
  }
}

// Helper to change the color of a NeoPixel on the Circuit Playground board.
// Will automatically convert from HSV color space to RGB and apply gamma
// correction.
float setPixelHSV(int i, float h, float s, float v) {
  // Convert HSV to RGB
  float r, g, b = 0.0;
  HSVtoRGB(&r, &g, &b, h, s, v);
  // Lookup gamma correct RGB colors (also convert from 0...1.0 RGB range to 0...255 byte range).
  uint8_t r1 = pgm_read_byte(&gamma8[int(r*255.0)]);
  uint8_t g1 = pgm_read_byte(&gamma8[int(g*255.0)]);
  uint8_t b1 = pgm_read_byte(&gamma8[int(b*255.0)]);
  // Set the color of the pixel.
  CircuitPlayground.strip.setPixelColor(i, r1, g1, b1);
}

// Rainbow candle flicker animation.
// Uses a sine wave with random frequency and phase (computed ahead of time in setup)
// to smoothly modulate the hue of each NeoPixel candle flame over time.
void showLitRainbow(uint32_t current) {
  // Convert time from milliseconds to seconds.
  float t = current/1000.0;
  // Loop through each pixel and compute its color.
  for (int i=0; i<10; ++i) {
    if (i < lit) {
      // This pixel should be lit, so compute its hue from the sine wave
      // equation and set the color accordingly.  Notice the frequency
      // is scaled down by 10 to 'slow down' the rainbow flicker animation.
      // This lets the same random frequencies be shared between fast candle
      // flame effects and this slower rainbow flicker effect.
      float x  = sin(2.0*PI*frequencies[i]/10.0*t + phases[i]);
      // Interpolate the sine wave between all 360 degree hue values.
      float h = lerp(x, -1.0, 1.0, 0.0, 360.0);
      setPixelHSV(i, h, 1.0, 1.0);
    }
    else {
      // This pixel has been blown out, just turn it off.
      setPixelHSV(i, 0, 0, 0);
    }
  }
  CircuitPlayground.strip.show();
}

// Flickering candle animation effect.
// Uses a noisey sine wave to modulate the hue of each pixel within a range
// of flame colors.  The sine wave has some low and high frequency components
// which give it an organice and seemingly random appearance.
void showLitFlicker(uint32_t current) {
  // First determine the low and high bounds of the flicker hues.
  // These are +/- 10 degrees of the specified target hue and will
  // wrap around to the start/end as appropriate.
  float lowHue = fmod(FLAME_HUE - 10.0, 360.0);
  float highHue = fmod(FLAME_HUE + 10.0, 360.0);
  // Convert time from milliseconds to seconds.
  float t = current/1000.0;
  // Loop through each pixel and compute its color.
  for (int i=0; i<10; ++i) {
    if (i < lit) {
      // This pixel should be lit, so compute its hue by composing
      // a low frequency / slowly changing sine wave with a high
      // frequency / fast changing cosine wave.  This means the candle will
      // pulse and jump around in an organice but random looking way.
      // The frequencies and phases of the waves are randomly generated at
      // startup in the setup function.
      // Low frequency wave is a sine wave with random freuqency between 1 and 4,
      // and offset by a random phase to keep pixels from all starting at the same
      // color:
      float lowFreq  = sin(2.0*PI*frequencies[i]*t + phases[i]);
      // High frequency is a faster changing cosine wave that uses a different
      // pixel's random frequency.
      float highFreq = cos(3.0*PI*frequencies[(i+5)%10]*t);
      // Add the low and high frequency waves together, then interpolate their value
      // to a hue that's +/-20% of the configured target hue.
      float h = lerp(lowFreq+highFreq, -2.0, 2.0, lowHue, highHue);
      setPixelHSV(i, h, 1.0, 1.0);
    }
    else {
      // This pixel has been blown out, just turn it off.
      setPixelHSV(i, 0, 0, 0);
    }
  }
  CircuitPlayground.strip.show();
}

// Simple solid lit candle effect.
// No animation, each pixel is lit with the specified flame hue until they're all blown out.
void showLitSolid() {
  for (int i=0; i<10; ++i) {
    if (i < lit) {
      // This pixel should be lit.
      setPixelHSV(i, FLAME_HUE, 1.0, 1.0);
    }
    else {
      // This pixel has been blown out, just turn it off.
      setPixelHSV(i, 0, 0, 0);
    }
  }
  CircuitPlayground.strip.show();
}

// HSV to RGB color space conversion function taken directly from:
//  https://www.cs.rit.edu/~ncs/color/t_convert.html
void HSVtoRGB( float *r, float *g, float *b, float h, float s, float v )
{
  int i;
  float f, p, q, t;
  if( s == 0 ) {
    // achromatic (grey)
    *r = *g = *b = v;
    return;
  }
  h /= 60;      // sector 0 to 5
  i = floor( h );
  f = h - i;      // factorial part of h
  p = v * ( 1 - s );
  q = v * ( 1 - s * f );
  t = v * ( 1 - s * ( 1 - f ) );
  switch( i ) {
    case 0:
      *r = v;
      *g = t;
      *b = p;
      break;
    case 1:
      *r = q;
      *g = v;
      *b = p;
      break;
    case 2:
      *r = p;
      *g = v;
      *b = t;
      break;
    case 3:
      *r = p;
      *g = q;
      *b = v;
      break;
    case 4:
      *r = t;
      *g = p;
      *b = v;
      break;
    default:    // case 5:
      *r = v;
      *g = p;
      *b = q;
      break;
  }
}

// Linear interpolation of value y within y0...y1 given x and x0...x1.
float lerp(float x, float x0, float x1, float y0, float y1) {
  return y0 + (x-x0)*((y1-y0)/(x1-x0));
}

void setup() {
  // Initialize serial output and Circuit Playground library.
  Serial.begin(115200);
  CircuitPlayground.begin();
  // Check if a button is being pressed at startup and change the
  // animation mode accordingly.
  if (CircuitPlayground.leftButton()) {
    // Rainbow animation on left button press at startup.
    animation = RAINBOW;
  }
  else if (CircuitPlayground.rightButton()) {
    // Solid color animation on right button press at startup.
    animation = SOLID;
    // Since the solid animation doesn't update every frame, bootstrap it by
    // turning all the pixels on at the start.  As candles are blown out the
    // pixels will be updated (see the waitBreath function's loop).
    showLitSolid();
  }
  else {
    // Otherwise default to flicker animation.
    animation = FLICKER;
  }
  // Read the sound sensor and use it to initialize the random number generator.
  randomSeed(CircuitPlayground.soundSensor());
  // Precompute random frequency and phase values for each pixel.
  // This gives the flicker an organic but random looking appearance.
  for (int i=0; i<10; ++i) {
    // Generate random floating point frequency values between 1.0 and 4.0.
    frequencies[i] = random(1000, 4000)/1000.0;
    // Generate random floating point phase values between 0 and 2*PI.
    phases[i] = random(1000)/1000.0 * 2.0 * PI;
  }
}

void loop() {
  // Play happy birthday tune, from:
  //  http://www.irish-folk-songs.com/happy-birthday-tin-whistle-sheet-music.html#.WXFJMtPytBw
  // Inside each playNote call it will play a note and drive the NeoPixel animation
  // and check for a breath against the sound sensor.  Once all the candles are blown out
  // the playNote calls will stop playing music.
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_E4, QUARTER);       // Bar 1
  playNote(NOTE_D4, QUARTER);
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_F4, HALF);          // Bar 2
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_E4, QUARTER);       // Bar 3
  playNote(NOTE_D4, QUARTER);
  playNote(NOTE_A4, QUARTER);
  playNote(NOTE_G4, HALF);          // Bar 4
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_D5, QUARTER);       // Bar 5
  playNote(NOTE_B4, QUARTER);
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_F4, QUARTER);       // Bar 6
  playNote(NOTE_E4, QUARTER);
  playNote(NOTE_C5, EIGHTH, true);
  playNote(NOTE_C5, EIGHTH);
  playNote(NOTE_B4, QUARTER);       // Bar 7
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_A4, QUARTER);
  playNote(NOTE_G4, HALF);          // Bar 8
  // One second pause before repeating the loop and playing
  // the tune again.  Use waitBreath instead of delay so the
  // pixel animation and breath check continues to happen.
  waitBreath(1000);
}

