initial commit
This commit is contained in:
+465
@@ -0,0 +1,465 @@
|
||||
// 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!
|
||||
//
|
||||
// Author: Tony DiCola
|
||||
// License: MIT (https://opensource.org/licenses/MIT)
|
||||
#include <Adafruit_CircuitPlayground.h>
|
||||
|
||||
// General configuration defines:
|
||||
#define BREATH_THRESHOLD 92 // Peak to peak sound pressure level that
|
||||
// determines if someone is blowing on the board.
|
||||
// You can open the serial console to see the sound
|
||||
// levels and adjust as necessary!
|
||||
|
||||
#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 sound pressure level from the microphone
|
||||
// for a specified number of milliseconds.
|
||||
// While measuring the current NeoPixel animation will be updated too.
|
||||
float measurePeak(uint32_t milliseconds) {
|
||||
float 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 check the sound pressure level 10ms at a time
|
||||
float sample = CircuitPlayground.mic.soundPressureLevel(10);
|
||||
Serial.println(sample);
|
||||
soundMax = max(sample, soundMax);
|
||||
// Be sure to drive the NeoPixel animation too.
|
||||
animatePixels(current);
|
||||
current = millis();
|
||||
}
|
||||
return soundMax;
|
||||
}
|
||||
|
||||
// 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).
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
void 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_FS4, 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_FS4, 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user