#include <Adafruit_CircuitPlayground.h>
/*
_____ by Michael McCallum and Andrew Parkinson
*/
// ----------------------------------------------------
// pins
// ----------------------------------------------------
const int SOUND_PIN = A4;
const int POT_PIN = A2;
const int BUTTON_PIN = 10;
// ----------------------------------------------------
// game constants
// ----------------------------------------------------
const int NUM_PIXELS = 10;
const int DB_MIN = 30;
const int DB_MAX = 100;
const unsigned long HOLD_TIME_MS = 300;
const unsigned long WIN_SHOW_MS = 1000;
const unsigned long DEBUG_INTERVAL_MS = 120;
const unsigned long OUT_OF_RANGE_GRACE_MS = 220;
// stabilize noisy mic values
const int DB_BIN_SIZE = 3;
const int IN_RANGE_EXIT_EXTRA_DB = 2;
// Pot mapping values
const int MIC_GAIN_MIN_PERCENT = 110;
const int MIC_GAIN_MAX_PERCENT = 260;
const int MIC_GATE_MAX = 16;
const int MIC_GATE_MIN = 2;
const int MIC_ACTIVITY_MAX = 140;
// ----------------------------------------------------
// Main game variables
// ----------------------------------------------------
bool inPlay = false;
bool roundWon = false;
int noiseFloor = 0;
int smoothActivity = 0;
int smoothPot = 0;
int currentDb = DB_MIN;
int rawDb = DB_MIN;
int currentDbTop = DB_MAX;
int currentPotPercent = 0;
int targetCenterDb = 75;
int targetBaseHalfWidth = 3;
int targetLowDb = 65;
int targetHighDb = 85;
int bottomPixel = 0;
int topPixel = 5;
int waterFillPixels = 0;
unsigned long holdStartMs = 0;
unsigned long outOfRangeSinceMs = 0;
unsigned long winShowStartMs = 0;
unsigned long lastDebugMs = 0;
bool inRangeLatched = false;
bool lastButton = HIGH;
// ----------------------------------------------------
// Small helper functions
// ----------------------------------------------------
// keep pixel index between 0 and 9
int wrapPixel(int index) {
if (index >= NUM_PIXELS) {
return index - NUM_PIXELS;
}
if (index < 0) {
return index + NUM_PIXELS;
}
return index;
}
// T=turn off all LEDs
void clearAllPixels() {
for (int i = 0; i < NUM_PIXELS; i++) {
CircuitPlayground.setPixelColor(i, 0, 0, 0);
}
}
// check button press edge (not hold)
bool buttonPressed() {
bool now = digitalRead(BUTTON_PIN);
bool pressed = (lastButton == HIGH && now == LOW);
lastButton = now;
return pressed;
}
// snap dB to small ranges (ex: 31 -> 30, 32 -> 33)
// reduce jitter
int snapDbToRange(int dbValue) {
int offset = dbValue - DB_MIN;
int snappedOffset = ((offset + (DB_BIN_SIZE / 2)) / DB_BIN_SIZE) * DB_BIN_SIZE;
int snappedDb = DB_MIN + snappedOffset;
return constrain(snappedDb, DB_MIN, DB_MAX);
}
// ----------------------------------------------------
// Setup helpers
// ----------------------------------------------------
// read mic in quiet room once
void calibrateMic() {
long total = 0;
for (int i = 0; i < 120; i++) {
total += analogRead(SOUND_PIN);
delay(3);
}
noiseFloor = total / 120;
smoothActivity = 0;
}
// start a new random round
void startNewRound() {
targetCenterDb = random(42, 90);
targetBaseHalfWidth = random(2, 5);
targetLowDb = constrain(targetCenterDb - targetBaseHalfWidth, DB_MIN, DB_MAX);
targetHighDb = constrain(targetCenterDb + targetBaseHalfWidth, DB_MIN, DB_MAX);
bottomPixel = random(0, NUM_PIXELS);
topPixel = wrapPixel(bottomPixel + 5);
holdStartMs = 0;
outOfRangeSinceMs = 0;
waterFillPixels = 0;
roundWon = false;
inRangeLatched = false;
}
// ----------------------------------------------------
// Input + mapping
// ----------------------------------------------------
void readAudioAndMap() {
int potRaw = analogRead(POT_PIN);
// smooth pot so it doesn't jump
smoothPot = (smoothPot * 7 + potRaw) / 8;
currentPotPercent = map(smoothPot, 0, 1023, 0, 100);
// pot changes gain and noise gate
int micGainPercent = map(smoothPot, 0, 1023, MIC_GAIN_MIN_PERCENT, MIC_GAIN_MAX_PERCENT);
int noiseGate = map(smoothPot, 0, 1023, MIC_GATE_MAX, MIC_GATE_MIN);
// pot also changes top dB capture
currentDbTop = map(smoothPot, 0, 1023, 68, DB_MAX);
currentDbTop = constrain(currentDbTop, DB_MIN + 10, DB_MAX);
// pot changes strictness a little
int strictnessAdjust = map(smoothPot, 0, 1023, 1, -1);
int half = constrain(targetBaseHalfWidth + strictnessAdjust, 2, 8);
targetLowDb = constrain(targetCenterDb - half, DB_MIN, DB_MAX);
targetHighDb = constrain(targetCenterDb + half, DB_MIN, DB_MAX);
// read microphone and convert to activity value
int micRaw = analogRead(SOUND_PIN);
int activity = abs(micRaw - noiseFloor);
// smooth mic activity
smoothActivity = (smoothActivity * 7 + activity) / 8;
// ignore tiny noise
int gated = smoothActivity - noiseGate;
if (gated < 0) {
gated = 0;
}
// scale into a game dB range
int scaled = (gated * micGainPercent) / 100;
scaled = constrain(scaled, 0, MIC_ACTIVITY_MAX);
rawDb = map(scaled, 0, MIC_ACTIVITY_MAX, DB_MIN, currentDbTop);
rawDb = constrain(rawDb, DB_MIN, DB_MAX);
// use snapped value for stable gameplay
currentDb = snapDbToRange(rawDb);
}
// In-range check
// Enter with exact range, exit with extra margin
bool checkInRangeStable() {
int enterLow = targetLowDb;
int enterHigh = targetHighDb;
int exitLow = targetLowDb - IN_RANGE_EXIT_EXTRA_DB;
int exitHigh = targetHighDb + IN_RANGE_EXIT_EXTRA_DB;
if (!inRangeLatched) {
if (currentDb >= enterLow && currentDb <= enterHigh) {
inRangeLatched = true;
}
} else {
if (currentDb < exitLow || currentDb > exitHigh) {
inRangeLatched = false;
}
}
return inRangeLatched;
}
// ----------------------------------------------------
// LED drawing
// ----------------------------------------------------
void renderIdle() {
clearAllPixels();
// blue breathing effect while waiting
int pulse = map((millis() / 8) % 255, 0, 254, 8, 45);
for (int i = 0; i < NUM_PIXELS; i++) {
CircuitPlayground.setPixelColor(i, 0, 0, pulse);
}
}
void renderPlay(bool inRange) {
clearAllPixels();
// white target markers
CircuitPlayground.setPixelColor(bottomPixel, 25, 25, 25);
CircuitPlayground.setPixelColor(topPixel, 25, 25, 25);
// blue water fill is based on dB relative to target
int fillPairs = 0;
if (currentDb < targetLowDb) {
fillPairs = map(currentDb, DB_MIN, targetLowDb, 0, 2);
fillPairs = constrain(fillPairs, 0, 2);
} else if (currentDb <= targetHighDb) {
fillPairs = 3;
} else {
int overflowPairs = map(currentDb, targetHighDb, DB_MAX, 0, 2);
overflowPairs = constrain(overflowPairs, 0, 2);
fillPairs = 3 + overflowPairs;
}
// middle area between opposite markers
int leftCenter = wrapPixel(bottomPixel + 2);
int rightCenter = wrapPixel(bottomPixel + 3);
// draw mirrored pairs
for (int ring = 0; ring < fillPairs; ring++) {
int leftPixel = wrapPixel(leftCenter - ring);
int rightPixel = wrapPixel(rightCenter + ring);
CircuitPlayground.setPixelColor(leftPixel, 0, 0, 85);
CircuitPlayground.setPixelColor(rightPixel, 0, 0, 85);
}
waterFillPixels = fillPairs * 2;
// redraw markers on top
CircuitPlayground.setPixelColor(bottomPixel, 25, 25, 25);
CircuitPlayground.setPixelColor(topPixel, 25, 25, 25);
// green = in range, Red = too high
if (inRange) {
CircuitPlayground.setPixelColor(bottomPixel, 0, 90, 0);
CircuitPlayground.setPixelColor(topPixel, 0, 90, 0);
} else if (currentDb > targetHighDb) {
CircuitPlayground.setPixelColor(bottomPixel, 90, 0, 0);
CircuitPlayground.setPixelColor(topPixel, 90, 0, 0);
}
}
void renderWinShow() {
clearAllPixels();
// small animation across opposite pairs
int phase = (millis() / 90) % 5;
for (int level = 0; level < 5; level++) {
int bright = 90 - abs(level - phase) * 25;
bright = constrain(bright, 10, 90);
int a = level;
int b = level + 5;
if (((millis() / 180) % 2) == 0) {
CircuitPlayground.setPixelColor(a, 0, bright, 30);
CircuitPlayground.setPixelColor(b, 0, bright, 30);
} else {
CircuitPlayground.setPixelColor(a, 0, 30, bright);
CircuitPlayground.setPixelColor(b, 0, 30, bright);
}
}
}
void renderWinReady() {
clearAllPixels();
// pulse the two target markers while waiting for button
int pulse = map((millis() / 10) % 255, 0, 254, 10, 55);
CircuitPlayground.setPixelColor(bottomPixel, pulse, pulse, pulse);
CircuitPlayground.setPixelColor(topPixel, pulse, pulse, pulse);
}
// ----------------------------------------------------
// Serial debug
// ----------------------------------------------------
void printDebug(bool inRange) {
if (millis() - lastDebugMs < DEBUG_INTERVAL_MS) {
return;
}
lastDebugMs = millis();
unsigned long holdMs = 0;
if (holdStartMs > 0) {
holdMs = millis() - holdStartMs;
}
Serial.print("state=");
Serial.print(inPlay ? 1 : 0);
Serial.print(", db=");
Serial.print(currentDb);
Serial.print(", rawDb=");
Serial.print(rawDb);
Serial.print(", pot=");
Serial.print(currentPotPercent);
Serial.print("%");
Serial.print(", dbTop=");
Serial.print(currentDbTop);
Serial.print(", target=");
Serial.print(targetLowDb);
Serial.print("-");
Serial.print(targetHighDb);
Serial.print(", pair=");
Serial.print(bottomPixel);
Serial.print("-");
Serial.print(topPixel);
Serial.print(", fill=");
Serial.print(waterFillPixels);
Serial.print(", inRange=");
Serial.print(inRange ? 1 : 0);
Serial.print(", won=");
Serial.print(roundWon ? 1 : 0);
Serial.print(", holdMs=");
Serial.println(holdMs);
}
// ----------------------------------------------------
// Arduino setup + loop
// ----------------------------------------------------
void setup() {
Serial.begin(115200);
CircuitPlayground.begin();
pinMode(BUTTON_PIN, INPUT_PULLUP);
randomSeed(analogRead(A1));
calibrateMic();
// Start pot smoother at current pot position
smoothPot = analogRead(POT_PIN);
}
void loop() {
bool pressed = buttonPressed();
// Always update sensor values each frame
readAudioAndMap();
// IDLE: waiting for player to press button
if (!inPlay) {
renderIdle();
printDebug(false);
if (pressed) {
startNewRound();
inPlay = true;
}
}
// PLAY: active round or win/next-round flow
else {
if (!roundWon) {
bool inRange = checkInRangeStable();
renderPlay(inRange);
printDebug(inRange);
// If in range, keep counting hold time
if (inRange) {
outOfRangeSinceMs = 0;
if (holdStartMs == 0) {
holdStartMs = millis();
}
if (millis() - holdStartMs >= HOLD_TIME_MS) {
roundWon = true;
winShowStartMs = millis();
holdStartMs = 0;
}
}
// If out of range, allow short grace period before reset
else {
if (holdStartMs > 0) {
if (outOfRangeSinceMs == 0) {
outOfRangeSinceMs = millis();
}
if (millis() - outOfRangeSinceMs > OUT_OF_RANGE_GRACE_MS) {
holdStartMs = 0;
outOfRangeSinceMs = 0;
}
}
}
}
// Won this round: show animation then wait for button
else {
if (millis() - winShowStartMs < WIN_SHOW_MS) {
renderWinShow();
} else {
renderWinReady();
if (pressed) {
startNewRound();
}
}
printDebug(false);
}
}
// Small frame delay for stability
delay(16);
}
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.