I’m building a game using an arduino mega and a Tsunami Super WAV Trigger (Qwiic).
This is the idea:
- The sd card contains 9 tracks in 3 categories (the tracks are all in the cards root directory), labeled 001.wav - 009.wav. There’s also a seperate track called 010.wav.
- There’s one large illuminated start button and 9 smaller bi-colour pushbuttons (red/green).
- At startup, the game is in standby mode, which means that the led in the startbutton is on and everything else is switched off.
- When the start button is pushed, it’s led is turned off.
- One track from each category is randomly picked and then these 3 tracks are played in a loop simultaniously. (the track from the first category is played less loud than the other two).
- The player has to guess which sounds he or she hears by pushing the buttons (each button corresponds with one of the tracks).
- If he guesses wrong, the red led in the button lights up for 2.5 seconds and then turns off again.
- If he guesses right, the green led turns on and stays on.
- The track associated with that button fades out and then stops.
- If it was one of the tracks from the second or third category, the volume of the track from the first category is increased.
- When all three sounds have been guessed correctly, track 10 is played and the led in the start button flashes.
- Then the game goes back in standby mode (the game also goes in stanby mode after 30 seconds of inactivity).
It all seems to work quite well, but there’s one problem: sometimes when a track is guessed correctly and it should fade out and stop playing, it seems to “hang”.
When I keep pushing buttons until all tracks have been guessed correctly, the sound stops (the rgb led on the Tsunami flashes blue). But as soon as I start the game again, the same track starts playing immediately (still hanging). I have to do a powercycle to get things up and running again.
Does anyone have any idea what could be causing this and what I can do to fix it?
Here’s a video I took showing the problem (this is just my prototype, so I’m using breadboard buttons and led’s): https://youtu.be/bTh67hEeKm0
Here’s my code:
#include <Tsunami.h>
#include <avr/wdt.h>
#include <avr/io.h>
Tsunami tsunami;
// -------------------- USER CONFIG --------------------
static const int TRACK_COUNT = 9;
static const int GROUP1_MIN = 1, GROUP1_MAX = 3;
static const int GROUP2_MIN = 4, GROUP2_MAX = 6;
static const int GROUP3_MIN = 7, GROUP3_MAX = 9;
static const uint32_t RED_ON_MS = 2500;
static const uint32_t INACTIVITY_MS = 30000;
static const uint32_t WIN_DELAY_MS = 2500;
static const uint32_t WIN_BLINK_MS = 250;
// Success track settings
static const int SUCCESS_TRACK = 10; // expects file “010_*.wav” on SD
static const int SUCCESS_GAIN_DB = 0; // -70..+10
static const bool SUCCESS_LOCK = true; // true = not subject to voice stealing
static const int FADE_OUT_MS = 1200;
static const int FADE_TARGET_DB = -70;
static const int TSUNAMI_OUT = 1;
static const uint32_t DEBOUNCE_MS = 30;
// Mix (dB)
static const int GAIN_GROUP1_START_DB = -5;
static const int GAIN_GROUP23_START_DB = 0;
static const int GAIN_GROUP1_BOOST_STEP_DB = +5;
static const int GAIN_GROUP1_MAX_DB = +6;
static const int BOOST_FADE_MS = 500;
// Debug
static const uint32_t SERIAL_BAUD = 115200;
static const bool DEBUG_DEFAULT_ON = true;
static const uint32_t HEARTBEAT_MS = 5000;
// Boot hardening
static const uint32_t PULLUP_SETTLE_MS = 20;
static const uint32_t BOOT_IGNORE_MS = 300;
// Watchdog
static const bool WATCHDOG_ENABLED = true;
static const uint8_t WATCHDOG_TIMEOUT = WDTO_2S;
// -------------------- PIN MAP --------------------
static const uint8_t startButtonPin = A1;
static const uint8_t startLedPin = A2;
static const uint8_t buttonPins[TRACK_COUNT] = { 45,46,47,48,49,50,51,52,53 };
static const uint8_t greenLedPins[TRACK_COUNT] = { 22,20,15,2,4,6,8,10,12 };
static const uint8_t redLedPins[TRACK_COUNT] = { 21,16,14,3,5,7,9,11,13 };
// -------------------------------------------------------------
enum GameState { STANDBY, PLAYING, WIN_DELAY };
GameState state = STANDBY;
int selected[3] = {0, 0, 0};
bool solved[3] = {false, false, false};
uint32_t redOffAt[TRACK_COUNT];
uint8_t lastStableRead[TRACK_COUNT + 1];
uint8_t lastInstantRead[TRACK_COUNT + 1];
uint32_t lastChangeAt[TRACK_COUNT + 1];
uint32_t lastActivityAt = 0;
int group1GainCurrentDb = GAIN_GROUP1_START_DB;
bool debugOn = DEBUG_DEFAULT_ON;
uint32_t bootIgnoreUntil = 0;
// WIN_DELAY timing
uint32_t winDelayUntil = 0;
// Blink state during WIN_DELAY
uint32_t nextWinBlinkAt = 0;
bool winBlinkLedOn = false;
// Forced-stop scheduler (tracks 1..9)
uint32_t forceStopAtMs[TRACK_COUNT + 1];
bool forceStopPending[TRACK_COUNT + 1];
// Heartbeat
uint32_t nextHeartbeatAt = 0;
// -------------------- WATCHDOG HELPERS --------------------
void watchdogInitAndReport() {
bool wasWdtReset = (MCUSR & _BV(WDRF));
MCUSR &= ~_BV(WDRF);
if (WATCHDOG_ENABLED) {
wdt_disable(); wdt_enable(WATCHDOG_TIMEOUT);} else {
wdt_disable();}
if (debugOn) {
Serial.print(F("\[WDT\] ")); if (WATCHDOG_ENABLED) { Serial.print(F("ENABLED timeout=")); Serial.println((int)WATCHDOG_TIMEOUT); } else { Serial.println(F("DISABLED")); } if (wasWdtReset) Serial.println(F("\[WDT\] Previous reset cause: WATCHDOG")); else Serial.println(F("\[WDT\] Previous reset cause: not watchdog (or unknown)"));}
}
inline void watchdogPet() {
if (WATCHDOG_ENABLED) wdt_reset();
}
// -------------------- DEBUG HELPERS --------------------
void printHelp() {
Serial.println();
Serial.println(F(“=== Debug Commands ===”));
Serial.println(F(“d : toggle debug on/off”));
Serial.println(F(“s : print current state/selection”));
Serial.println(F(“?/h: show this help”));
Serial.println();
}
void debugPrintState() {
Serial.print(F("[STATE] "));
if (state == STANDBY) Serial.print(F(“STANDBY”));
else if (state == PLAYING) Serial.print(F(“PLAYING”));
else Serial.print(F(“WIN_DELAY”));
Serial.print(F(" | sel: "));
Serial.print(selected[0]); Serial.print(F(“,”));
Serial.print(selected[1]); Serial.print(F(“,”));
Serial.print(selected[2]);
Serial.print(F(" | solved: "));
Serial.print(solved[0]); Serial.print(F(“,”));
Serial.print(solved[1]); Serial.print(F(“,”));
Serial.print(solved[2]);
Serial.print(F(" | g1Gain="));
Serial.print(group1GainCurrentDb);
Serial.println(F(" dB"));
}
void handleSerialDebug() {
while (Serial.available() > 0) {
char c = (char)Serial.read(); if (c == '\\n' || c == '\\r' || c == ' ') continue; if (c == 'd' || c == 'D') { debugOn = !debugOn; Serial.print(F("Debug is now ")); Serial.println(debugOn ? F("ON") : F("OFF")); if (debugOn) { debugPrintState(); nextHeartbeatAt = millis() + HEARTBEAT_MS; } } else if (c == 's' || c == 'S') { debugPrintState(); } else if (c == '?' || c == 'h' || c == 'H') { printHelp(); } else { Serial.print(F("Unknown command: ")); Serial.println(c); Serial.println(F("Send '?' for help.")); }}
}
void dbgBtn(uint8_t idx, int track, const __FlashStringHelper* result) {
if (!debugOn) return;
Serial.print(F(“[BTN] idx=”));
Serial.print(idx);
Serial.print(F(" track="));
Serial.print(track);
Serial.print(F(" → "));
Serial.println(result);
}
void serviceHeartbeat() {
if (!debugOn) return;
uint32_t now = millis();
if ((int32_t)(now - nextHeartbeatAt) >= 0) {
Serial.print(F("\[HB\] t=")); Serial.print(now / 1000); Serial.print(F("s | state=")); Serial.print(state == STANDBY ? F("STANDBY") : (state == PLAYING ? F("PLAYING") : F("WIN_DELAY"))); Serial.print(F(" | sel=")); Serial.print(selected\[0\]); Serial.print(F(",")); Serial.print(selected\[1\]); Serial.print(F(",")); Serial.print(selected\[2\]); Serial.print(F(" | solved=")); Serial.print(solved\[0\]); Serial.print(F(",")); Serial.print(solved\[1\]); Serial.print(F(",")); Serial.print(solved\[2\]); Serial.println(); nextHeartbeatAt = now + HEARTBEAT_MS;}
}
// -------------------- HELPERS --------------------
static inline int randInRangeInclusive(int a, int b) {
return a + (int)random((long)(b - a + 1));
}
void setAllGreensOff() { for (int i = 0; i < TRACK_COUNT; i++) digitalWrite(greenLedPins[i], LOW); }
void setAllRedsOff() { for (int i = 0; i < TRACK_COUNT; i++) digitalWrite(redLedPins[i], LOW); }
void updateRedTimers() {
uint32_t now = millis();
for (int i = 0; i < TRACK_COUNT; i++) {
if (redOffAt\[i\] != 0 && (int32_t)(now - redOffAt\[i\]) >= 0) { digitalWrite(redLedPins\[i\], LOW); redOffAt\[i\] = 0; }}
}
void redOnThenOffLater(uint8_t idx) {
digitalWrite(redLedPins[idx], HIGH);
redOffAt[idx] = millis() + RED_ON_MS;
}
int selectedIndexForTrack(int track) {
if (track == selected[0]) return 0;
if (track == selected[1]) return 1;
if (track == selected[2]) return 2;
return -1;
}
bool allSolved() {
return solved[0] && solved[1] && solved[2];
}
void syncButtonStatesAtStartup() {
uint32_t now = millis();
for (uint8_t i = 0; i < TRACK_COUNT; i++) {
uint8_t v = digitalRead(buttonPins\[i\]); lastStableRead\[i\] = v; lastInstantRead\[i\] = v; lastChangeAt\[i\] = now;}
uint8_t sv = digitalRead(startButtonPin);
lastStableRead[9] = sv;
lastInstantRead[9] = sv;
lastChangeAt[9] = now;
if (debugOn) {
Serial.print(F("\[BOOT\] Debounce synced. Start pin=")); Serial.println(sv == HIGH ? F("HIGH (released)") : F("LOW (pressed?)"));}
}
bool debouncedPressed(uint8_t logicalIndex, uint8_t pin) {
uint32_t now = millis();
uint8_t inst = digitalRead(pin);
if (inst != lastInstantRead[logicalIndex]) {
lastInstantRead\[logicalIndex\] = inst; lastChangeAt\[logicalIndex\] = now;}
if ((now - lastChangeAt[logicalIndex]) >= DEBOUNCE_MS) {
if (lastStableRead\[logicalIndex\] != inst) { lastStableRead\[logicalIndex\] = inst; if (inst == LOW) return true; }}
return false;
}
void applyStartingGains() {
group1GainCurrentDb = GAIN_GROUP1_START_DB;
tsunami.trackGain(selected[0], group1GainCurrentDb);
tsunami.trackGain(selected[1], GAIN_GROUP23_START_DB);
tsunami.trackGain(selected[2], GAIN_GROUP23_START_DB);
}
void boostGroup1SmoothIfActive() {
if (solved[0]) return;
int nextGain = group1GainCurrentDb + GAIN_GROUP1_BOOST_STEP_DB;
if (nextGain > GAIN_GROUP1_MAX_DB) nextGain = GAIN_GROUP1_MAX_DB;
if (nextGain != group1GainCurrentDb) {
group1GainCurrentDb = nextGain; tsunami.trackFade(selected\[0\], group1GainCurrentDb, BOOST_FADE_MS, false);}
}
void clearAllLoopFlags() {
for (int t = 1; t <= TRACK_COUNT; t++) tsunami.trackLoop(t, false);
}
void setSelectedLoopFlagsOn() {
tsunami.trackLoop(selected[0], true);
tsunami.trackLoop(selected[1], true);
tsunami.trackLoop(selected[2], true);
}
void playSuccessTrack() {
tsunami.trackLoop(SUCCESS_TRACK, false);
tsunami.trackGain(SUCCESS_TRACK, SUCCESS_GAIN_DB);
tsunami.trackPlayPoly(SUCCESS_TRACK, TSUNAMI_OUT, SUCCESS_LOCK);
if (debugOn) {
Serial.print(F("\[SUCCESS\] track ")); Serial.print(SUCCESS_TRACK); Serial.println(F(" started"));}
}
// -------- Forced-stop scheduler --------
void initForceStopScheduler() {
for (int t = 0; t <= TRACK_COUNT; t++) {
forceStopAtMs\[t\] = 0; forceStopPending\[t\] = false;}
}
void cancelAllForcedStops() {
for (int t = 1; t <= TRACK_COUNT; t++) {
forceStopAtMs\[t\] = 0; forceStopPending\[t\] = false;}
}
void scheduleTrackStopAfterFade(int track, uint32_t fadeMs) {
if (track < 1 || track > TRACK_COUNT) return;
forceStopPending[track] = true;
forceStopAtMs[track] = millis() + fadeMs + 30;
if (debugOn) {
Serial.print(F("\[STOP-SCHED\] track ")); Serial.print(track); Serial.print(F(" in ")); Serial.print(fadeMs); Serial.println(F(" ms"));}
}
void serviceForcedStops() {
uint32_t now = millis();
for (int t = 1; t <= TRACK_COUNT; t++) {
if (forceStopPending\[t\] && (int32_t)(now - forceStopAtMs\[t\]) >= 0) { tsunami.trackLoop(t, false); tsunami.trackStop(t); forceStopPending\[t\] = false; forceStopAtMs\[t\] = 0; if (debugOn) { Serial.print(F("\[STOP\] forced trackStop on ")); Serial.println(t); } }}
}
// -------------------- STATE HANDLERS --------------------
void enterStandby(const __FlashStringHelper* reason) {
tsunami.stopAllTracks();
clearAllLoopFlags();
cancelAllForcedStops();
state = STANDBY;
// ONLY start LED should be on in standby:
setAllGreensOff();
setAllRedsOff();
for (int i = 0; i < TRACK_COUNT; i++) redOffAt[i] = 0;
digitalWrite(startLedPin, HIGH);
selected[0] = selected[1] = selected[2] = 0;
solved[0] = solved[1] = solved[2] = false;
group1GainCurrentDb = GAIN_GROUP1_START_DB;
winDelayUntil = 0;
nextWinBlinkAt = 0;
winBlinkLedOn = false;
if (debugOn) {
Serial.print(F("\[MODE\] STANDBY (")); Serial.print(reason); Serial.println(F(")"));}
}
void enterWinDelay() {
state = WIN_DELAY;
winDelayUntil = millis() + WIN_DELAY_MS;
winBlinkLedOn = true;
digitalWrite(startLedPin, HIGH);
nextWinBlinkAt = millis() + WIN_BLINK_MS;
playSuccessTrack();
if (debugOn) {
Serial.print(F("\[MODE\] WIN_DELAY for ")); Serial.print(WIN_DELAY_MS); Serial.print(F(" ms, blink ")); Serial.print(WIN_BLINK_MS); Serial.println(F(" ms"));}
}
void updateWinBlink() {
if (state != WIN_DELAY) return;
uint32_t now = millis();
if ((int32_t)(now - nextWinBlinkAt) >= 0) {
winBlinkLedOn = !winBlinkLedOn; digitalWrite(startLedPin, winBlinkLedOn ? HIGH : LOW); nextWinBlinkAt = now + WIN_BLINK_MS;}
}
void startRound() {
digitalWrite(startLedPin, LOW);
setAllGreensOff();
setAllRedsOff();
for (int i = 0; i < TRACK_COUNT; i++) redOffAt[i] = 0;
cancelAllForcedStops();
selected[0] = randInRangeInclusive(GROUP1_MIN, GROUP1_MAX);
selected[1] = randInRangeInclusive(GROUP2_MIN, GROUP2_MAX);
selected[2] = randInRangeInclusive(GROUP3_MIN, GROUP3_MAX);
solved[0] = solved[1] = solved[2] = false;
clearAllLoopFlags();
setSelectedLoopFlagsOn();
tsunami.trackLoad(selected[0], TSUNAMI_OUT, false);
tsunami.trackLoad(selected[1], TSUNAMI_OUT, false);
tsunami.trackLoad(selected[2], TSUNAMI_OUT, false);
applyStartingGains();
tsunami.resumeAllInSync();
state = PLAYING;
lastActivityAt = millis();
if (debugOn) {
Serial.print(F("\[MODE\] PLAYING | selected: ")); Serial.print(selected\[0\]); Serial.print(F(",")); Serial.print(selected\[1\]); Serial.print(F(",")); Serial.println(selected\[2\]); Serial.println(F("\[INFO\] Selected tracks will loop until stopped."));}
}
void handleCorrectPress(uint8_t idx, int track, int selIdx) {
digitalWrite(greenLedPins[idx], HIGH);
solved[selIdx] = true;
dbgBtn(idx, track, F(“CORRECT”));
if (selIdx == 1 || selIdx == 2) {
boostGroup1SmoothIfActive();}
tsunami.trackLoop(track, false);
tsunami.trackFade(track, FADE_TARGET_DB, FADE_OUT_MS, false);
scheduleTrackStopAfterFade(track, FADE_OUT_MS);
lastActivityAt = millis();
if (allSolved()) {
enterWinDelay();}
}
void handleWrongPress(uint8_t idx, int track) {
dbgBtn(idx, track, F(“WRONG”));
redOnThenOffLater(idx);
lastActivityAt = millis();
}
// -------------------- SETUP / LOOP --------------------
void setup() {
wdt_disable();
Serial.begin(SERIAL_BAUD);
delay(50);
pinMode(startButtonPin, INPUT_PULLUP);
pinMode(startLedPin, OUTPUT);
for (int i = 0; i < TRACK_COUNT; i++) {
pinMode(buttonPins\[i\], INPUT_PULLUP); pinMode(greenLedPins\[i\], OUTPUT); pinMode(redLedPins\[i\], OUTPUT); redOffAt\[i\] = 0;}
initForceStopScheduler();
delay(PULLUP_SETTLE_MS);
syncButtonStatesAtStartup();
bootIgnoreUntil = millis() + BOOT_IGNORE_MS;
randomSeed(analogRead(A0));
tsunami.start();
tsunami.stopAllTracks();
tsunami.setReporting(true);
clearAllLoopFlags();
if (debugOn) {
Serial.println(F("Tsunami game starting...")); Serial.print(F("\[BOOT\] Ignoring inputs for ")); Serial.print(BOOT_IGNORE_MS); Serial.println(F(" ms")); printHelp();}
nextHeartbeatAt = millis() + HEARTBEAT_MS;
watchdogInitAndReport();
watchdogPet();
enterStandby(F(“boot”));
}
void loop() {
watchdogPet();
handleSerialDebug();
tsunami.update();
serviceForcedStops();
updateRedTimers();
serviceHeartbeat();
if ((int32_t)(millis() - bootIgnoreUntil) < 0) return;
// WIN_DELAY handling
if (state == WIN_DELAY) {
watchdogPet(); updateWinBlink(); if ((int32_t)(millis() - winDelayUntil) >= 0) { enterStandby(F("win delay done")); } return;}
// START button (allowed in STANDBY/PLAYING)
if (debouncedPressed(9, startButtonPin)) {
if (debugOn) Serial.println(F("\[START\] pressed")); if (state == STANDBY) startRound(); else lastActivityAt = millis();}
// --------- IMPORTANT RULES (explicit) ----------
// Input buttons are ONLY handled during PLAYING.
// In STANDBY and WIN_DELAY they do nothing (no red flashes).
if (state != PLAYING) {
return;}
// 9 input buttons (PLAYING only)
for (uint8_t i = 0; i < TRACK_COUNT; i++) {
if (debouncedPressed(i, buttonPins\[i\])) { int track = (int)i + 1; int selIdx = selectedIndexForTrack(track); if (selIdx >= 0) { if (!solved\[selIdx\]) { handleCorrectPress(i, track, selIdx); } else { // Already solved -> ignore (no red LED) dbgBtn(i, track, F("IGNORED (already solved)")); lastActivityAt = millis(); } } else { // Not selected -> wrong handleWrongPress(i, track); } }}
// Inactivity timeout
if ((millis() - lastActivityAt) >= INACTIVITY_MS) {
enterStandby(F("inactivity"));}
}