Songs playing on top of each other while only one is wanted.

PimN

Member
I'm building an audio source for an amateur Radio foxhunt with a Teensy4.1 using the MQS outputs.

In short it should play songs from a specified directory on the build in SD Card in an endless loop. At a configurable interval (of say 30s) the audio from the current song gets paused and after a short delay the Teensy generated morse code is fed to the MQS pins. Then after a short pause the song is un-paused and continues to play.
This works fine until, a short interval before a Morse ID transmission, the 2nd song is started. Then I hear multiple songs on top of each other. Its a bit hard to tell but at least I hear > 4 songs from the used directory being played at the same time.
Gradually all these songs end, interrupted by morse code at the ID interval and everything works as desired after that. i.e. playing one song after each other in an endless loop.

I've been at it for some days but can not find the cause. Used Google Gemini a lot but no luck there either.

Anyone any hints?

I've attached the code and the generated log up to the moment the audio chaos starts.

At 16:52:12 the 2nd song /FOXHUNT/SONGS/CQSERENA.WAV starts resulting in audio chaos.
Then at 16:52:15 the pause leading up to the Morse ID is started. Morse is played, short pause, and song (s) again, etc.
The chaos dwindles down until the songs being played simultaneously have all ended.

Anyone any hints?

Pim, PA2PIM
 

Attachments

  • PA2PIM-107_Fox_CTRL_w_morse-20260520-1617.ino
    32.9 KB · Views: 11
  • PA2PIM-107 v4_5_0-ALPHA_LOG.txt
    8.2 KB · Views: 9
I put everything into ChatGPT and got an answer which make sense to me after looking at the code by myself. There could be a problem with togglePlayPause and your playback state variables.

full text:

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:

  1. Track 1 nearly ends
  2. Pause toggled
  3. Resume toggled
  4. isPlaying() briefly false
  5. Scheduler thinks track ended
  6. startNextTrack() fires
  7. old decoder not fully stopped
  8. new play starts
  9. more pauses/resumes occur
  10. 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.
 
Hi Tom,
Tnx for yr efforts.
Re: " I’d bet a beer that removing togglePlayPause() fixes this immediately." is lost by ChatGPT.

The culprit was Meta Data still present in song #2.
I had used Audacity to convert the songs to the required 16b 44k1 standard and removing the Meta Data.
I must have made a mistake there.
Now how Meta Data in a .WAV file can trigger starting multiple songs is beyond me.

As to the rest of the ChatGPT Final recommendation:
  • NEVER pause SD WAV playback with togglePlayPause() and Use mixer gain muting instead
    • There is a legal requirement to ID the station at least every 600s. In a hunt it is usually once a minute.
    • "Hunters" must hear every part of the songs because they get asked questions about them. It's about the only way of asking questions which are very hard to look up on the Internet. And asking questions is part of the fun to hunt down these transmitters.
    • TogglePlayPause() is not recommended but no alternative is given. If I could start a song at an offset I could note the elapsed time, use ducking, play whatever announcement I want, start playing again a bit before the noted elapsed time and ramp up the volume. In the end then everything was played at the nominal audio level.
    • Next " feature" to implement will be to play, in addition to the Morse ID, also a short announcement .wav file or using Text to speak (tts) for it. With that I can warn the hunters that the hunt is nearly over, battery is going down, etc, etc, Tts is preferred since it's more flexible then needing .WAV files.
  • The other recommendations i will look into. I like Belts and Braces in code but might have stuck in too much and then it becomes overhead.
 
Back
Top