/*
============================================================
Task 4 - Clap to Light
============================================================
Group of 3 -> includes the confidence / match-quality addition.
BEHAVIOUR
---------
1. On boot, previously stored rhythms are loaded from EEPROM.
2. If none are stored yet (first run), the Arduino automatically
enters PROGRAMMING MODE:
- For each of the 3 rhythms (LED 1, 2, 3 in turn):
* the corresponding LED lights up
* you clap a rhythm (at least 2 claps, e.g. clap-clap...clap)
* the Arduino records the TIMING GAPS between claps (not just
the count - this is what makes it a "rhythm")
* it plays the captured rhythm back to you on the buzzer
- after all 3 rhythms are captured they are saved to EEPROM.
3. In RECOGNITION MODE the Arduino continuously listens. When you
clap a pattern, it compares the timing of your claps against the
3 stored rhythms and lights the LED of the best match, printing
something like:
"Rhythm 2 detected with 82% confidence"
to the Serial Monitor (9600 baud).
4. Press BTN_PROGRAM at any time to re-record all 3 rhythms.
5. Hold BTN_CLEAR while powering on to wipe the stored rhythms.
WHY NOT JUST "COUNT CLAPS"?
----------------------------
Each rhythm is stored as the sequence of TIME INTERVALS between
consecutive claps (in ms), e.g. clap-clap-pause-clap becomes
[180ms, 650ms]. Two rhythms with the same number of claps but a
different timing pattern are recognised as different rhythms.
CONFIDENCE / MATCH QUALITY (3-student addition)
-------------------------------------------------
To let the same rhythm be clapped slightly faster or slower and
still match, each interval is normalised by the total duration of
that rhythm (so the intervals of one rhythm always sum to 1.0).
The confidence is then derived from the average difference between
the normalised, live-clapped pattern and each stored pattern -
a perfect match -> 100%, a very different pattern -> 0%.
WIRING
-------
Sound sensor DO -> D2 (VCC->5V, GND->GND)
Buzzer + -> D3 (-> GND)
LED 1 -> D8 (+ resistor -> GND)
LED 2 -> D9 (+ resistor -> GND)
LED 3 -> D10 (+ resistor -> GND)
Button PROGRAM -> D4 (other leg -> GND, uses INPUT_PULLUP)
Button CLEAR -> D5 (other leg -> GND, uses INPUT_PULLUP)
*/
#include <EEPROM.h>
#include <math.h>
// ---------------------- Pin definitions ----------------------
const uint8_t SOUND_SENSOR_PIN = 2;
const uint8_t BUZZER_PIN = 3;
const uint8_t LED_PINS[3] = {8, 9, 10};
const uint8_t BTN_PROGRAM = 4;
const uint8_t BTN_CLEAR = 5;
// Confirmed on our hardware: sensor idles HIGH, drops to LOW on a clap.
const uint8_t CLAP_ACTIVE_STATE = LOW;
// ---------------------- Timing constants ----------------------
const uint16_t CLAP_DEBOUNCE_MS = 120; // ignore sensor noise right after a clap
const uint16_t CLAP_GAP_TIMEOUT = 1500; // silence after which a rhythm is "done"
const uint32_t FIRST_CLAP_TIMEOUT = 8000; // how long recognition mode waits for a first clap
const uint8_t MAX_CLAPS = 8;
const uint8_t NUM_RHYTHMS = 3;
const uint8_t EEPROM_MAGIC = 0xA5;
const uint16_t EEPROM_ADDR = 0;
const float MATCH_THRESHOLD = 50.0; // minimum confidence (%) accepted as a match
// ---------------------- Data structure for one rhythm ----------------------
struct Rhythm {
uint8_t numClaps; // 2..MAX_CLAPS
uint16_t intervals[MAX_CLAPS - 1]; // ms gaps between consecutive claps
};
struct RhythmBank {
uint8_t magic;
Rhythm rhythms[NUM_RHYTHMS];
};
RhythmBank bank;
// =====================================================================
void setup() {
Serial.begin(9600);
pinMode(SOUND_SENSOR_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(BTN_PROGRAM, INPUT_PULLUP);
pinMode(BTN_CLEAR, INPUT_PULLUP);
for (uint8_t i = 0; i < 3; i++) pinMode(LED_PINS[i], OUTPUT);
if (digitalRead(BTN_CLEAR) == LOW) {
Serial.println(F("Clearing stored rhythms..."));
EEPROM.update(EEPROM_ADDR, 0xFF); // invalidate stored magic byte
}
loadBankFromEEPROM();
if (bank.magic != EEPROM_MAGIC) {
Serial.println(F("No valid rhythms stored. Entering programming mode."));
programAllRhythms();
} else {
Serial.println(F("Loaded stored rhythms. Entering recognition mode."));
}
}
// =====================================================================
void loop() {
if (digitalRead(BTN_PROGRAM) == LOW) {
delay(30); // debounce the button itself
if (digitalRead(BTN_PROGRAM) == LOW) {
while (digitalRead(BTN_PROGRAM) == LOW) { /* wait for release */ }
programAllRhythms();
}
}
recognizeOnce();
}
// =====================================================================
// ---------------------- Clap detection primitive ----------------------
// Waits up to timeoutMs for ONE clap (edge into CLAP_ACTIVE_STATE).
// Returns true and sets 'when' to millis() at detection, false on timeout.
bool waitForClap(uint32_t timeoutMs, uint32_t &when) {
uint32_t start = millis();
bool wasActive = (digitalRead(SOUND_SENSOR_PIN) == CLAP_ACTIVE_STATE);
while (millis() - start < timeoutMs) {
bool isActive = (digitalRead(SOUND_SENSOR_PIN) == CLAP_ACTIVE_STATE);
if (isActive && !wasActive) {
when = millis();
// ride out the noisy tail of the same clap
uint32_t debounceStart = millis();
while (millis() - debounceStart < CLAP_DEBOUNCE_MS) { /* wait */ }
return true;
}
wasActive = isActive;
}
return false;
}
// ---------------------- Capture a full rhythm ----------------------
// Blocks until the first clap (or firstClapTimeout expires -> returns false),
// then keeps listening until CLAP_GAP_TIMEOUT of silence or MAX_CLAPS is hit.
bool captureRhythm(Rhythm &r, uint32_t firstClapTimeout) {
uint32_t timestamps[MAX_CLAPS];
uint8_t count = 0;
uint32_t when;
if (!waitForClap(firstClapTimeout, when)) return false;
timestamps[count++] = when;
while (count < MAX_CLAPS) {
if (waitForClap(CLAP_GAP_TIMEOUT, when)) {
timestamps[count++] = when;
} else {
break; // silence -> rhythm finished
}
}
if (count < 2) return false; // a single clap is not a rhythm
r.numClaps = count;
for (uint8_t i = 0; i < count - 1; i++) {
r.intervals[i] = (uint16_t)(timestamps[i + 1] - timestamps[i]);
}
return true;
}
// ---------------------- Playback on the buzzer ----------------------
void playRhythm(const Rhythm &r) {
const uint16_t CLAP_TONE_HZ = 1000;
const uint16_t CLAP_TONE_MS = 90;
tone(BUZZER_PIN, CLAP_TONE_HZ, CLAP_TONE_MS);
delay(CLAP_TONE_MS);
for (uint8_t i = 0; i < (uint8_t)(r.numClaps - 1); i++) {
int32_t gap = (int32_t)r.intervals[i] - CLAP_TONE_MS;
if (gap < 20) gap = 20;
delay(gap);
tone(BUZZER_PIN, CLAP_TONE_HZ, CLAP_TONE_MS);
delay(CLAP_TONE_MS);
}
noTone(BUZZER_PIN);
}
// ---------------------- Programming mode ----------------------
void programAllRhythms() {
for (uint8_t idx = 0; idx < NUM_RHYTHMS; idx++) {
setLED(idx, true); // shows which rhythm/LED slot is being programmed
Serial.print(F("Programming rhythm "));
Serial.print(idx + 1);
Serial.println(F(" - clap the pattern now."));
Rhythm r;
bool ok = false;
while (!ok) {
ok = captureRhythm(r, 15000UL); // generous timeout while programming
if (!ok) {
Serial.println(F("No valid rhythm detected (need at least 2 claps). Try again."));
}
}
Serial.println(F("Playing back what was captured..."));
delay(400);
playRhythm(r);
bank.rhythms[idx] = r;
setLED(idx, false);
delay(500);
}
bank.magic = EEPROM_MAGIC;
saveBankToEEPROM();
Serial.println(F("All 3 rhythms saved. Ready to recognize claps."));
}
// ---------------------- Recognition mode ----------------------
void recognizeOnce() {
Rhythm live;
if (!captureRhythm(live, FIRST_CLAP_TIMEOUT)) return; // nobody clapped, keep looping
int8_t bestIdx = -1;
float bestConfidence = -1;
for (uint8_t i = 0; i < NUM_RHYTHMS; i++) {
float confidence = matchConfidence(live, bank.rhythms[i]);
if (confidence > bestConfidence) {
bestConfidence = confidence;
bestIdx = i;
}
}
if (bestIdx >= 0 && bestConfidence >= MATCH_THRESHOLD) {
Serial.print(F("Rhythm "));
Serial.print(bestIdx + 1);
Serial.print(F(" detected with "));
Serial.print(bestConfidence, 0);
Serial.println(F("% confidence."));
setLED(bestIdx, true);
delay(2000);
setLED(bestIdx, false);
} else {
Serial.print(F("No rhythm matched confidently (best guess "));
Serial.print(bestConfidence < 0 ? 0 : bestConfidence, 0);
Serial.println(F("%)."));
blinkAllBriefly();
}
}
// ---------------------- Confidence / match quality ----------------------
// Intervals are normalised by their own sum so the same rhythm clapped
// faster or slower still matches (only the relative spacing matters).
// Clap count must match exactly, otherwise confidence is 0.
float matchConfidence(const Rhythm &a, const Rhythm &b) {
if (a.numClaps != b.numClaps) return 0;
uint8_t n = a.numClaps - 1;
if (n == 0) return 0;
float sumA = 0, sumB = 0;
for (uint8_t i = 0; i < n; i++) { sumA += a.intervals[i]; sumB += b.intervals[i]; }
if (sumA == 0 || sumB == 0) return 0;
float totalError = 0;
for (uint8_t i = 0; i < n; i++) {
float fa = a.intervals[i] / sumA;
float fb = b.intervals[i] / sumB;
totalError += fabs(fa - fb);
}
float avgError = totalError / n; // 0 = perfect match, larger = worse
// 0.5 average normalised error is treated as "essentially no match" (0%)
float confidence = 100.0 * (1.0 - (avgError / 0.5));
if (confidence < 0) confidence = 0;
if (confidence > 100) confidence = 100;
return confidence;
}
// ---------------------- Small helpers ----------------------
void setLED(uint8_t idx, bool state) {
digitalWrite(LED_PINS[idx], state ? HIGH : LOW);
}
void blinkAllBriefly() {
for (uint8_t i = 0; i < 3; i++) digitalWrite(LED_PINS[i], HIGH);
delay(150);
for (uint8_t i = 0; i < 3; i++) digitalWrite(LED_PINS[i], LOW);
}
// ---------------------- EEPROM persistence ----------------------
void saveBankToEEPROM() {
EEPROM.put(EEPROM_ADDR, bank);
}
void loadBankFromEEPROM() {
EEPROM.get(EEPROM_ADDR, bank);
}