Unintended repeating of sound when playback is stopped

(Please note that I am not good at English, so the following is a translation from DeepL.)

Hi JUCE developers,

We develop applications like DAWs.
We use juce::AudioProcessorGraph for the audio internals.
Each track is considered as one AudioProcessor and is connected in parallel.
In this graph, audio tracks are defined as a separate AudioProcessorGraph and a new AudioProcessor is added for each audio file, as multiple audio files are loaded in one track.
When trying to play such a graph structure, only on macOS default audio devices the sound repeats unintentionally when playback is stopped.
The sound is repeated 2 or 3 times for about 0.2 seconds around the stop position.

When I ran Windows with Bootcamp on the same PC, I could play the same audio device without any problems.
Also, with an external audio interface (*I tried with a UR22C), I was able to play it on both Windows and macOS without any problems.
The phenomenon seems to be limited to Apple product audio devices as it was reproduced on multiple macOS devices (mac mini & macbook).
We also confirmed that the structure without sub-graphs plays without problems.

A sample project where the defects occur is attached.
(*Please note that there are unnecessary implementations as the necessary code has been extracted from the product under development).
TransportBugSample.zip (7.0 MB)

Is there a solution? Please advise.

When you say the sound is repeated you mean that the few last played milli seconds are played in a loop for 0.2 seconds?
To me that sounds a lot like the audio device is just playing the audio buffers previously calculated since your application is not producing audio anymore when it is being stopped. But I’m not perfectly sure why this is happening while shutting down the audio device.

I think what I’d try first is reduce the load it takes to shut down the device. E.g. make sure you don’t have to free up huge amounts of RAM inside releaseResources. I’d also try to comment out your updateTime and updateState stuff. You are scheduling a blocking call on the message thread (that’s not good, you should use atomics). Maybe this gets stuck while your are shutting down.

When you say the sound is repeated you mean that the few last played milli seconds are played in a loop for 0.2 seconds?

No.
It is difficult to explain, so I took a video of the problem occurring.
PXL_20220322_082755924.mov.zip (6.8 MB)
My English is not good enough to explain it well, so I hope this video will help you understand the problem.

Yes, that’s exactly what I was expecting. I’d try the steps I already outlined.

OK.
However, the assumptions differ from the hypotheses proposed.
In this case, the device is not shutting down when playback is stopped.
Therefore, releaseResources() is not called either.
It may be possible that one extra block is played due to the playhead not being exclusive, but it is unlikely that the audio output will be as good as in this case.

It is not possible that one extra block is played. What you are hearing is that the audio device is expecting your application to render audio but your application is not doing so. Therefore, the audio device plays what your application left in the memory.
I suspect that it is happening, because your are calling blocking methods from your audio thread. Like the last line in ProcessBlock. Try removing all the listener calling from your audio thread and see what happens.

Try removing all the listener calling from your audio thread and see what happens.

I removed the listener calling, but nothing improved.

Hm, then it’s a little hard to pin point the problem just from looking at it. I’d recommend commenting stuff out until you’re not having the problem anymore. But from what I’m seeing you will be having massiv problems with the multi threading stuff. E.g. the listener calling or the missing atomics in the AudioPlayhead.
You could also try profiling your application with Instruments and check where your application is hanging.

Thanks for the advice. Improvements will proceed.

However, we still don’t have any clues about the glitches you posted, so we are looking forward to your feedback.
Or is there some bug on the JUCE side that we would like the JUCE team to check?

I hardly doubt that that’s a JUCE bug. It’s perfectly reasonable that that kind of behaviour appears differently on every machine depending on operating system and hardware. Your windows machines might be just faster to lock and unlock your mutex or you were playing with a hight IO buffer size, so your application were allowed to take its sweet time blocking the audio thread.

If the is a JUCE bug, you should be able to come up with a much more simpler program. Maybe reduce it down to one additional class besides Main and MainComponent.

    juce::int64 time_in_samples_;
    bool is_playing_;
    double sampling_rate_;

All three variables should potentially be atomics when they are accessed from more that one thread and at least one thread does a write operation. At least for is_playing_ that is the case.

play_control_listenters_.call(
        [this](PlayControlListener& l) { l.PlayPositionChanged(audio_play_head_.GetTimeInSeconds()); });

You are calling listeners from the audio thread (that itself is not so big of a deal) but don’t pay attention to what you are doing in the listeners. One of them takes a MessageManagerLock. The message thread might be insanely busy. It handles all the UI stuff, every click, every mouse movement. Don’t lock, unlock, try_lock a mutex on the audio thread, as that is a system call and the OS might be busy doing other stuff than multi threading your application.

I’d suggest to read the book “C++ Concurrency in Action” to get ahead on those problems.

Try removing all the listener calling from your audio thread and see what happens.

I removed the listener calling, but nothing improved.

We have confirmed that the problem occurs even if the listener call process is commented out. I mentioned that in a comment yesterday, remember?

Also, the bug happened even if you make the three variables you pointed out atomic.

There are certainly inadequacies in my program, but that is another story.
I am trying to find out the cause of this bug which is dependent on the audio device and OS combination.

Your comment seems to be only to point out the inadequacies of the program.
Please let me focus on identifying the cause of the bug I have posted." The “correct” programming design is outside the scope of this topic.

Unfortunately, it’s not possible to completely separate the two. Problems with thread safety can cause problems elsewhere in your program in unpredictable ways. Here are a couple of examples:

If you run your program with ThreadSanitizer enabled this will help you locate some places where you have data races.

I’m fairly confident this will be a timing issue where processBlock is being called when you don’t expect it to be.

1 Like

Thanks for your advice.

Attached is the code with the following two modifications.

  • Remove calls to listeners from AudioController
  • ScopedLock was added to all functions of the HostAudioPlayHead class to control concurrent access from other threads.
    TransportBugSampleImproved.zip (7.0 MB)

But, nothing improved.
I tried ThreadSanitizer in xcode, but it did not output anything and did not break the debugger.
I also tried AddressSanitizer and UndefinedBehaviorSanitizer and they performed without problems.

Are there any other possible causes?

ScopedLock was added to all functions of the HostAudioPlayHead class to control concurrent access from other threads.

That sounds like it might be solving only half of the problem. Without locking in the audio thread as well there’s no guarantee that the two will be synchronised. When you press the stop button, how are making sure that no audio is being played from that exact moment?

My understanding is that ScopedLock can be used to synchronize calls from all threads, including audio threads, but am I using it wrong?

My understanding is that ScopedLock can be used to synchronize calls from all threads, including audio threads

That is correct. But if you are only using it to synchronise access to methods on HostAudioPlayHead, how are you also synchronising what is happening on the audio thread? What happens if the audio thread is part way through processing a buffer when a HostAudioPlayHead method is called?

The most probable cause is still a timing issue.

OK, I understand.
In the case of my implementation, I need another Lock in the AudioController class.

A further revision is attached.
This should not change the value of playhead during the execution of proccessBlock().
However, the result of the execution is still the same: the sound around the stop position repeats several times.
TransportBugSampleImproved2.zip (7.0 MB)

No. That won’t work. You need a shared lock to synchronise access. Independent locks will make no difference.

I think this implementation is fine, since I designed the HostAudioPlayHead to be changed only via the AudioController class.
At least in this code, we can guarantee that the value of playhead will not change during the execution of processBlock, right?