I’m not sure where the bug is, but it’s probably not in sound concurrency manager. The rest of the function in the code you quoted is:
void FSoundConcurrencyManager::RemoveActiveSound(FActiveSound* ActiveSound)
{
if (!ActiveSound->ConcurrencyGroupID)
{
return;
}
// Remove this sound from it's concurrency list
FConcurrencyGroup* ConcurrencyGroup = ConcurrencyGroups.Find(ActiveSound->ConcurrencyGroupID);
check(ConcurrencyGroup);
TArray<FActiveSound*>& ActiveSounds = ConcurrencyGroup->GetActiveSounds();
check(ActiveSounds.Num() > 0);
ActiveSounds.Remove(ActiveSound);
}
In other words the active sound that’s getting removed is the active sound in the concurrency group. The corresponding add function is:
void FConcurrencyGroup::AddActiveSound(FActiveSound* ActiveSound)
{
check(ConcurrencyGroupID != 0);
ActiveSounds.Add(ActiveSound);
ActiveSound->ConcurrencyGroupID = ConcurrencyGroupID;
ActiveSound->ConcurrencyGeneration = Generation++;
}
Playing a sound without a concurrency group will simply bypass the check in the beginning of a playsound call’s life in:
void FAudioDevice::AddNewActiveSound(const FActiveSound& NewActiveSound)
There, before a sound is allowed to create an FActiveSound, it checks the concurrency manager in FSoundConcurrencyManager::CreateNewActiveSound:
FActiveSound* FSoundConcurrencyManager::CreateNewActiveSound(const FActiveSound& NewActiveSound)
{
check(NewActiveSound.GetSound());
// If there are no concurrency settings associated then there is no limit on this sound
const FSoundConcurrencySettings* Concurrency = NewActiveSound.GetSoundConcurrencySettingsToApply();
// If there was no concurrency or the setting was zero, then we will always play this sound.
if (!Concurrency)
{
FActiveSound* ActiveSound = new FActiveSound(NewActiveSound);
ActiveSound->SetAudioDevice(AudioDevice);
return ActiveSound;
}
check(Concurrency->MaxCount > 0);
uint32 ConcurrencyObjectID = NewActiveSound.GetSoundConcurrencyObjectID();
if (ConcurrencyObjectID == 0)
{
return HandleConcurrencyEvaluationOverride(NewActiveSound);
}
else
{
return HandleConcurrencyEvaluation(NewActiveSound);
}
}
Note that if there is no concurrency settings on the sound, it simply creates a new active sound and returns that. No concurrency evaluation, no adding to a concurrency group, etc.
When an active sound finishes, FAudioDevice::RemoveActiveSound is called:
void FAudioDevice::RemoveActiveSound(FActiveSound* ActiveSound)
{
check(IsInAudioThread());
ConcurrencyManager.RemoveActiveSound(ActiveSound);
// Perform the notification
if (ActiveSound->GetAudioComponentID() > 0)
{
UAudioComponent::PlaybackCompleted(ActiveSound->GetAudioComponentID(), false);
}
const int32 NumRemoved = ActiveSounds.Remove(ActiveSound);
check(NumRemoved == 1);
}
Notice that the concurrency manager is called (the function that you suspect is a leak/bug) here. But we’ve seen that if an active sound doesn’t have a concurrency group, that function does nothing. Then it performs a notification on the audio component that it’s done (if it has an audio component). Then it actually removes the active sound from the real list in FAudioDevice.
The “ActiveSounds” member of FAudioDevice is the one that prevents the leak and is the list that is evaluated every frame to determine WaveInstances (and does the evaluation of sound cue graphs, etc) in FAudioDevice::GetSortedActiveWaveInstances.
Edit:
For completeness, since the above implies that RemoveActiveSound is intended to be the final clean up and clearly there hasn’t been a delete ActiveSound call. The active sound lifetime is finished in:
void FAudioDevice::ProcessingPendingActiveSoundStops(bool bForceDelete)
{
// Process the PendingSoundsToDelete. These may have
// had their deletion deferred due to an async operation
for (int32 i = PendingSoundsToDelete.Num() - 1; i >= 0; --i)
{
FActiveSound* ActiveSound = PendingSoundsToDelete*;
if (bForceDelete || ActiveSound->CanDelete())
{
ActiveSound->bAsyncOcclusionPending = false;
PendingSoundsToDelete.RemoveAtSwap(i, 1, false);
delete ActiveSound;
}
}
// Stop any pending active sounds that need to be stopped
for (FActiveSound* ActiveSound : PendingSoundsToStop)
{
check(ActiveSound);
ActiveSound->Stop();
// If we can delete the active sound now, then delete it
if (bForceDelete || ActiveSound->CanDelete())
{
ActiveSound->bAsyncOcclusionPending = false;
delete ActiveSound;
}
else
{
// There was an async operation pending. We need to defer deleting this sound
PendingSoundsToDelete.Add(ActiveSound);
}
}
PendingSoundsToStop.Reset();
}
Basically, what happens is that when an active sound is called to stop, it doesn’t immediately stop it due to a number of reasons. But instead appends the ActiveSounds to a Pending list:
void FAudioDevice::AddSoundToStop(FActiveSound* SoundToStop)
{
check(IsInAudioThread());
const uint64 AudioComponentID = SoundToStop->GetAudioComponentID();
if (AudioComponentID > 0)
{
AudioComponentIDToActiveSoundMap.Remove(AudioComponentID);
}
check(SoundToStop);
bool bIsAlreadyInSet = false;
PendingSoundsToStop.Add(SoundToStop, &bIsAlreadyInSet);
if (bIsAlreadyInSet)
{
UE_LOG(LogAudio, Verbose, TEXT("Stopping sound which was already in the process of stopping"));
}
}
Which is then processed in the above function. ActiveSound->Stop() is the point at which it’s removed from the internal list of ActiveSounds. The primary reason why we might not be able to delete the ActiveSound (why we need a PendingSoundsToDelete list) is if there is an async trace call pending for occlusion. The primary reason why we can’t immediately stop an active sound when its stopped is to prevent stack overflows with BP delegate functions which attempt to play more sounds in the same concurrency group of limit 1, which immediately trigger a stop, which trigger another play call, which trigger a stop, etc. I discovered that bug when I first implemented concurrency. It was a bug that had actually been possible before the new sound concurrency group feature.