Yes — there’s a very strong clue in your state logic, and I think the root cause is not MQS or the mixer at all.
The problem is almost certainly caused by using:
wavPlayer.togglePlayPause();
combined with this logic:
if (wavPlayer.isPlaying() || isSongPaused)
and especially this:
if (internalAudioState == AUDIO_BUFFERING && wavPlayer.isPlaying()) {
internalAudioState = AUDIO_PLAYING;
}
On Teensy Audio, AudioPlaySdWav::isPlaying() does
not reliably mean “actively outputting audio”.
When paused with togglePlayPause(), the internal decoder state can still look “playing”.
That means your scheduler can accidentally interpret a paused track as ended or transition-ready and start another play() while the old decoder instance is still alive.
That exactly matches your symptom:
- first track works
- repeated pause/unpause works
- second track starts
- shortly after, beacon pause happens
- multiple WAV streams suddenly overlap
- eventually all finish naturally and things stabilize again
That means old WAV playback instances were never actually stopped/reset cleanly.
The smoking gun
This section is dangerous:
if (!isSongPaused) {
if (internalAudioState == AUDIO_PLAYING && !isTransmitting) {
if (!wavPlayer.isPlaying() && (currentMillis - lastPlayingMillis > 200)) {
internalAudioState = AUDIO_IDLE;
}
}
if (totalTracksFound > 0 && initialBeaconFired &&
(internalAudioState == AUDIO_IDLE) &&
!isTransmitting && !trackTransitionLock) {
logMessage("[INFO] Track ended natively. Advancing sequence pointer...");
startNextTrack();
}
}
During pause/unpause timing windows, internalAudioState and wavPlayer.isPlaying() can drift out of sync.
Then:
startNextTrack();
gets called while the previous WAV engine is still alive.
The Teensy WAV player is not designed for repeated toggle/start races like this.
Why it only happens on track 2
Your first track begins after a fresh startup:
startNextTrack();
No pause history yet.
Then you do many:
togglePlayPause();
cycles.
Eventually:
- decoder buffers
- SD reads
- internal playback flags
become desynchronized.
When the next track transition happens:
wavPlayer.play(...)
is issued while previous playback state is still partially active.
Result:
- overlapping audio streams
- decoder chaos
- several files sounding simultaneously
Biggest issue: togglePlayPause()
This function is notoriously fragile on Teensy Audio.
Especially with:
- SD playback
- rapid pause/unpause
- state machines
- track transitions
I would strongly recommend removing it entirely.
Much safer design
Instead of pausing the WAV player:
Option 1 (best)
Leave WAV playback running continuously and mute it in the mixer.
You already HAVE the correct architecture for this.
Instead of:
wavPlayer.togglePlayPause();
isSongPaused = true;
do:
audioMixer.gain(0, 0.0f);
audioMixer.gain(2, 0.0f);
Then restore after Morse:
audioMixer.gain(0, currentSettings.mixerGain);
audioMixer.gain(2, currentSettings.mixerGain);
This avoids touching the WAV decoder entirely.
This is the cleanest and most robust solution.
Your WAV stream keeps advancing silently during CW.
For a foxhunt beacon, that is usually perfectly acceptable.
If you MUST resume exactly where paused
Then do NOT use togglePlayPause().
Instead:
- stop playback
- store filename + position
- restart from offset
But Teensy AudioPlaySdWav does not support seeking cleanly.
So the practical answer is:
use mixer muting instead of pausing
Another bug
This line is also suspicious:
delay(150);
inside your Morse state machine.
Never put delays inside audio state management on Teensy audio.
Even short delays can interfere with SD refill timing.
You also have:
delay(50);
delay(25);
around playback transitions.
Those are risky.
Another subtle issue
This:
AudioConnection patchCord5(audioMixer, 0, mqsOutput, 0);
AudioConnection patchCord6(audioMixer, 0, mqsOutput, 1);
feeds the same mixer output to both MQS channels.
That’s fine for mono.
But you connected WAV stereo inputs like this:
patchCord1(wavPlayer, 0, audioMixer, 0);
patchCord2(wavPlayer, 1, audioMixer, 2);
and then collapse both into one mono mixer output.
That is okay, but note:
- mixer channel 0 = left
- mixer channel 2 = right
If gains are ever mismanaged during transitions, you can get apparent layering/phasing artifacts that sound like “multiple songs”.
Still, the real issue is almost certainly the playback state race.
What I would change immediately
REMOVE ALL togglePlayPause()
Replace:
wavPlayer.togglePlayPause();
with mixer muting only.
Replace pause logic
Instead of:
if (initialBeaconFired && wavPlayer.isPlaying() && !isSongPaused) {
wavPlayer.togglePlayPause();
isSongPaused = true;
}
use:
if (initialBeaconFired && wavPlayer.isPlaying()) {
audioMixer.gain(0, 0.0f);
audioMixer.gain(2, 0.0f);
}
Then after Morse:
audioMixer.gain(0, currentSettings.mixerGain);
audioMixer.gain(2, currentSettings.mixerGain);
And delete:
isSongPaused
entirely.
Also simplify state logic
You can likely eliminate:
AUDIO_BUFFERING
and maybe even:
internalAudioState
completely.
Just use:
wavPlayer.isPlaying()
plus your own:
isTransmitting
flag.
Right now you have too many overlapping state variables:
- isSongPaused
- internalAudioState
- isTransmitting
- trackTransitionLock
- wavPlayer.isPlaying()
That creates race-condition territory.
Most likely exact failure sequence
I suspect this happens:
- Track 1 nearly ends
- Pause toggled
- Resume toggled
- isPlaying() briefly false
- Scheduler thinks track ended
- startNextTrack() fires
- old decoder not fully stopped
- new play starts
- more pauses/resumes occur
- multiple decoder streams overlap
That matches your timestamps perfectly.
Final recommendation
For embedded audio reliability on Teensy:
- NEVER pause SD WAV playback with togglePlayPause()
- Use mixer gain muting instead
- Avoid delay() in audio control paths
- Reduce redundant playback state variables
I’d bet a beer that removing togglePlayPause() fixes this immediately.