Tsunami - track sometimes "hangs" when it's told to fade out and stop

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"));

}

}

Whilst doing some more research, I read in the example sketch that is in the documentation for controlling the Tsunami over serial, to use tracks that are at least 10-20 seconds long.
I don’t know if this is only important when using the example sketch, but I had 2 tracks that were under 10 seconds long.
So I altered them using Audacity (since I’m looping the tracks anyway, I just copied the track a couple of times and pasted them all together).
This looks to have solved the problem. I’ll have to do some prolonged testing to be absolutely sure, but things are looking good.

Thanks for posting the video. The interesting thing here is that Tsunami continues to behave normally, in that it’s clearly responding to serial commands, controlling the LED correctly, and I can even hear the volume changes - so nothing has crashed, and interrupts are happening. But what it sounds like to me is that the microSD card has stopped working altogether, so the DMA buffer is never getting updated with new data. You’re hearing the approx 3ms DAC circular buffer just repeating the same data.

There should be no problem with files shorter than 10 seconds, so not sure why that seemed to fix it. Did you change the microSD card?

Yes, it seems like the Tsunami and the arduino are still responding and behaving as they should. It’s just the fact that the track hangs and keeps being played. Do you think it could be a problem with the SD card? I didn’t change anything (except for exchanging the short files with the longer versions). If I’m not mistaken, it’s a (new) 32 GB Philips Ultra Pro card, so maybe not the best, but certainly not the worst quality either.
My guess was, that maybe the looping was clashing with the fading of the track. I’ll do some more testing on monday and hope this fixed it.

Based on the symptoms, I suspect your microSD card is causing the issue. When you exchanged the files, you also changed what sectors of the card are being used. Consumer grade cards can and do fail. The easiest experiment, if it happens again, is to try using a different card.

Ah, ok. Is there a brand / type of card you can recommend? This is going in a museum exhibit, so I’d rather be sure that this isn’t likely to happen again.

Customers who are using the WAV Trigger and Tsunami in commercial applications claim to have had consistently good results with the white SanDisk Industrial cards. You can find the 8GB and 16GB versions on Amazon.

Thanks, I’ll see if I can source one.