Files
UnrealEngine/Engine/Source/Runtime/AudioMixer/Private/Effects/AudioMixerSubmixEffectDynamicsProcessor.cpp
2025-05-18 13:04:45 +08:00

541 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SubmixEffects/AudioMixerSubmixEffectDynamicsProcessor.h"
#include "AudioBusSubsystem.h"
#include "AudioDeviceManager.h"
#include "AudioMixerDevice.h"
#include "AudioMixerSubmix.h"
#include "ProfilingDebugging/CsvProfiler.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AudioMixerSubmixEffectDynamicsProcessor)
// Link to "Audio" profiling category
CSV_DECLARE_CATEGORY_MODULE_EXTERN(AUDIOMIXERCORE_API, Audio);
DEFINE_STAT(STAT_AudioMixerSubmixDynamics);
static int32 bBypassSubmixDynamicsProcessor = 0;
FAutoConsoleVariableRef CVarBypassDynamicsProcessor(
TEXT("au.Submix.Effects.DynamicsProcessor.Bypass"),
bBypassSubmixDynamicsProcessor,
TEXT("If non-zero, bypasses all submix dynamics processors currently active.\n"),
ECVF_Default);
FSubmixEffectDynamicsProcessor::FSubmixEffectDynamicsProcessor()
{
DeviceCreatedHandle = FAudioDeviceManagerDelegates::OnAudioDeviceCreated.AddRaw(this, &FSubmixEffectDynamicsProcessor::OnDeviceCreated);
DeviceDestroyedHandle = FAudioDeviceManagerDelegates::OnAudioDeviceDestroyed.AddRaw(this, &FSubmixEffectDynamicsProcessor::OnDeviceDestroyed);
}
FSubmixEffectDynamicsProcessor::~FSubmixEffectDynamicsProcessor()
{
ResetKey();
FAudioDeviceManagerDelegates::OnAudioDeviceCreated.Remove(DeviceCreatedHandle);
FAudioDeviceManagerDelegates::OnAudioDeviceDestroyed.Remove(DeviceDestroyedHandle);
}
Audio::FDeviceId FSubmixEffectDynamicsProcessor::GetDeviceId() const
{
return DeviceId;
}
void FSubmixEffectDynamicsProcessor::Init(const FSoundEffectSubmixInitData& InitData)
{
static const int32 ProcessorScratchNumChannels = 8;
DynamicsProcessor.Init(InitData.SampleRate, ProcessorScratchNumChannels);
DeviceId = InitData.DeviceID;
if (USubmixEffectDynamicsProcessorPreset* ProcPreset = Cast<USubmixEffectDynamicsProcessorPreset>(Preset.Get()))
{
switch (ProcPreset->Settings.KeySource)
{
case ESubmixEffectDynamicsKeySource::AudioBus:
{
if (UAudioBus* AudioBus = ProcPreset->Settings.ExternalAudioBus)
{
KeySource.Update(ESubmixEffectDynamicsKeySource::AudioBus, AudioBus->GetUniqueID(), static_cast<int32>(AudioBus->AudioBusChannels) + 1);
}
}
break;
case ESubmixEffectDynamicsKeySource::Submix:
{
if (USoundSubmix* Submix = ProcPreset->Settings.ExternalSubmix)
{
KeySource.Update(ESubmixEffectDynamicsKeySource::Submix, Submix->GetUniqueID());
}
}
break;
default:
{
// KeySource is this effect's submix/input, so do nothing
}
break;
}
}
}
void FSubmixEffectDynamicsProcessor::ResetKey()
{
KeySource.Reset();
}
void FSubmixEffectDynamicsProcessor::OnPresetChanged()
{
GET_EFFECT_SETTINGS(SubmixEffectDynamicsProcessor);
bBypass = Settings.bBypass;
switch (Settings.DynamicsProcessorType)
{
default:
case ESubmixEffectDynamicsProcessorType::Compressor:
DynamicsProcessor.SetProcessingMode(Audio::EDynamicsProcessingMode::Compressor);
break;
case ESubmixEffectDynamicsProcessorType::Limiter:
DynamicsProcessor.SetProcessingMode(Audio::EDynamicsProcessingMode::Limiter);
break;
case ESubmixEffectDynamicsProcessorType::Expander:
DynamicsProcessor.SetProcessingMode(Audio::EDynamicsProcessingMode::Expander);
break;
case ESubmixEffectDynamicsProcessorType::Gate:
DynamicsProcessor.SetProcessingMode(Audio::EDynamicsProcessingMode::Gate);
break;
case ESubmixEffectDynamicsProcessorType::UpwardsCompressor:
DynamicsProcessor.SetProcessingMode(Audio::EDynamicsProcessingMode::UpwardsCompressor);
break;
}
switch (Settings.PeakMode)
{
default:
case ESubmixEffectDynamicsPeakMode::MeanSquared:
DynamicsProcessor.SetPeakMode(Audio::EPeakMode::MeanSquared);
break;
case ESubmixEffectDynamicsPeakMode::RootMeanSquared:
DynamicsProcessor.SetPeakMode(Audio::EPeakMode::RootMeanSquared);
break;
case ESubmixEffectDynamicsPeakMode::Peak:
DynamicsProcessor.SetPeakMode(Audio::EPeakMode::Peak);
break;
}
DynamicsProcessor.SetLookaheadMsec(Settings.LookAheadMsec);
DynamicsProcessor.SetAttackTime(Settings.AttackTimeMsec);
DynamicsProcessor.SetReleaseTime(Settings.ReleaseTimeMsec);
DynamicsProcessor.SetThreshold(Settings.ThresholdDb);
DynamicsProcessor.SetRatio(Settings.Ratio);
DynamicsProcessor.SetKneeBandwidth(Settings.KneeBandwidthDb);
DynamicsProcessor.SetInputGain(Settings.InputGainDb);
DynamicsProcessor.SetOutputGain(Settings.OutputGainDb);
DynamicsProcessor.SetAnalogMode(Settings.bAnalogMode);
DynamicsProcessor.SetKeyAudition(Settings.bKeyAudition);
DynamicsProcessor.SetKeyGain(Settings.KeyGainDb);
DynamicsProcessor.SetKeyHighshelfCutoffFrequency(Settings.KeyHighshelf.Cutoff);
DynamicsProcessor.SetKeyHighshelfEnabled(Settings.KeyHighshelf.bEnabled);
DynamicsProcessor.SetKeyHighshelfGain(Settings.KeyHighshelf.GainDb);
DynamicsProcessor.SetKeyLowshelfCutoffFrequency(Settings.KeyLowshelf.Cutoff);
DynamicsProcessor.SetKeyLowshelfEnabled(Settings.KeyLowshelf.bEnabled);
DynamicsProcessor.SetKeyLowshelfGain(Settings.KeyLowshelf.GainDb);
static_assert(static_cast<int32>(ESubmixEffectDynamicsChannelLinkMode::Count) == static_cast<int32>(Audio::EDynamicsProcessorChannelLinkMode::Count), "Enumerations must match");
DynamicsProcessor.SetChannelLinkMode(static_cast<Audio::EDynamicsProcessorChannelLinkMode>(Settings.LinkMode));
UpdateKeyFromSettings(Settings);
}
Audio::FMixerDevice* FSubmixEffectDynamicsProcessor::GetMixerDevice()
{
if (FAudioDeviceManager* DeviceManager = FAudioDeviceManager::Get())
{
return static_cast<Audio::FMixerDevice*>(DeviceManager->GetAudioDeviceRaw(DeviceId));
}
return nullptr;
}
bool FSubmixEffectDynamicsProcessor::UpdateKeySourcePatch()
{
using namespace Audio;
// Default (input as key) does not use source patch, so don't
// continue checking or updating state.
if (KeySource.GetType() == ESubmixEffectDynamicsKeySource::Default)
{
return false;
}
// Valid object
const uint32 ObjectId = KeySource.GetObjectId();
if (ObjectId == INDEX_NONE)
{
return false;
}
// Retrieving/mutating the MixerDevice is only safe during OnProcessAudio calls if
// it is not called during Teardown. The DynamicsProcessor should be Reset via
// the OnDeviceDestroyed callback (prior to FAudioDevice::Teardown), so this call
// should never be hit during Teardown.
FMixerDevice* MixerDevice = GetMixerDevice();
if (!MixerDevice)
{
return false;
}
// Need to determine if our current patch is still valid.
// This is handled differently per-type. We return true for valid.
// If it's not-valid, we must try and (re)create a patch
switch (KeySource.GetType())
{
case ESubmixEffectDynamicsKeySource::AudioBus:
{
if (UAudioBusSubsystem* AudioBusSubsystem = MixerDevice->GetSubsystem<UAudioBusSubsystem>())
{
// Check patch is still valid and not stale.
// AudioBusses can't change format under us (yet), so just check its validity.
if (KeySource.Patch.IsValid() && !KeySource.Patch->IsInputStale())
{
return true;
}
const int32 NumFrames = MixerDevice->GetNumOutputFrames();
const int32 NumChannels = KeySource.GetNumChannels();
const FAudioBusKey BusKey(ObjectId);
// Make sure the bus is started. (if it's started already this does nothing).
const FString SubmixEffectDynamicsKeySourceAudioBusName = FString::Format(TEXT("_SubmixEffectDynamicsKeySourceId_{0}"), { ObjectId });
AudioBusSubsystem->StartAudioBus(BusKey, SubmixEffectDynamicsKeySourceAudioBusName, NumChannels, /*bInIsAutomatic=*/false);
// Add/Recreate a new patch to this bus
KeySource.Patch = AudioBusSubsystem->AddPatchOutputForAudioBus(BusKey, NumFrames, NumChannels);
// Always set the keyed channels even if we fail to create the patch.
// As the nop case requires these to be set.
DynamicsProcessor.SetKeyNumChannels(NumChannels);
// If the patch is created and got valid inputs (non-stale) we consider it valid.
return KeySource.Patch.IsValid() && !KeySource.Patch->IsInputStale();
}
}
break;
case ESubmixEffectDynamicsKeySource::Submix:
{
if (const FMixerSubmixPtr SubmixPtr = MixerDevice->FindSubmixInstanceByObjectId(ObjectId); SubmixPtr.IsValid())
{
const int32 NumSubmixChannels = SubmixPtr->GetNumOutputChannels();
// Is patch still valid? Check that we haven't changed channel count.
// This can happen after we've swapped devices.
if (KeySource.Patch.IsValid() && !KeySource.Patch->IsInputStale() && KeySource.GetNumChannels() == NumSubmixChannels)
{
// Looks good.
return true;
}
// We store the num channels the submix key source is running at.
KeySource.SetNumChannels(NumSubmixChannels);
// Add/Recreate a new patch to this submix.
KeySource.Patch = MixerDevice->AddPatchForSubmix(ObjectId, 1.0f /* PatchGain */);
// Always set the keyed channels even if we fail to create the patch.
// As the nop case requires these to be set.
DynamicsProcessor.SetKeyNumChannels(NumSubmixChannels);
// If the patch is created and got valid inputs (non-stale) we consider it valid.
return KeySource.Patch.IsValid() && !KeySource.Patch->IsInputStale();
}
}
break;
case ESubmixEffectDynamicsKeySource::Default:
default:
{
checkNoEntry();
}
break;
}
// KeySource invalid.
return false;
}
void FSubmixEffectDynamicsProcessor::OnProcessAudio(const FSoundEffectSubmixInputData& InData, FSoundEffectSubmixOutputData& OutData)
{
CSV_SCOPED_TIMING_STAT(Audio, SubmixDynamics);
SCOPE_CYCLE_COUNTER(STAT_AudioMixerSubmixDynamics);
TRACE_CPUPROFILER_EVENT_SCOPE(FSubmixEffectDynamicsProcessor::OnProcessAudio);
ensure(InData.NumChannels == OutData.NumChannels);
const Audio::FAlignedFloatBuffer& InBuffer = *InData.AudioBuffer;
Audio::FAlignedFloatBuffer& OutBuffer = *OutData.AudioBuffer;
if (bBypassSubmixDynamicsProcessor || bBypass)
{
FMemory::Memcpy(OutBuffer.GetData(), InBuffer.GetData(), sizeof(float) * InBuffer.Num());
return;
}
AudioExternal.Reset();
// Update the output channels if necessary.
if (InData.NumChannels != DynamicsProcessor.GetNumChannels())
{
DynamicsProcessor.SetNumChannels(InData.NumChannels);
}
// If set to default, enforce num key channels to always be number of input channels.
// If either unset or KeySource was changed between frames back to 'Default', NumKeyChannels
// could be stale or left as initialized number of scratch channels.
if (KeySource.GetType() == ESubmixEffectDynamicsKeySource::Default)
{
if (InData.NumChannels != DynamicsProcessor.GetKeyNumChannels())
{
DynamicsProcessor.SetKeyNumChannels(InData.NumChannels);
}
}
else
{
// Update our key source. (this validates the existing patch and create new one if necessary)
// NOTE: This call can adjust the DynamicsProcessor.NumKeyChannels, so query that after updating.
const bool bPatchIsValid = UpdateKeySourcePatch();
if (!bPatchIsValid)
{
// If the patch was not created, we emulate a patch by pumping in
// silence with an equal number of channels as the input audio.
// The key channel count needs to get updated here as it may not have
// been updated to a valid value inside of `UpdateKeySourcePatch()`
if (InData.NumChannels != DynamicsProcessor.GetKeyNumChannels())
{
DynamicsProcessor.SetKeyNumChannels(InData.NumChannels);
}
}
const int32 NumKeyChannels = DynamicsProcessor.GetKeyNumChannels();
const int32 NumKeySamples = InData.NumFrames * NumKeyChannels;
// Make enough space to handle all input samples that we're expecting.
// These zeros will act as input if we don't get enough samples.
AudioExternal.AddZeroed(NumKeySamples);
if (bPatchIsValid)
{
// There's a chance of a patch racing with us here and becoming stale, which would return INDEX_NONE (-1) samples here.
// This shouldn't matter, but we should be careful of ensuring against >= 0 samples popped.
const int32 NumSamplesPopped = KeySource.Patch->PopAudio(AudioExternal.GetData(), AudioExternal.Num(), true /* bUseLatestAudio */);
ensure(NumSamplesPopped <= AudioExternal.Num());
}
}
// No key assigned (Uses input buffer as key)
if (KeySource.GetType() == ESubmixEffectDynamicsKeySource::Default)
{
DynamicsProcessor.ProcessAudio(InBuffer.GetData(), InData.NumChannels * InData.NumFrames, OutBuffer.GetData());
}
// Key assigned
else
{
DynamicsProcessor.ProcessAudio(InBuffer.GetData(), InData.NumChannels * InData.NumFrames, OutBuffer.GetData(), AudioExternal.GetData());
}
}
void FSubmixEffectDynamicsProcessor::UpdateKeyFromSettings(const FSubmixEffectDynamicsProcessorSettings& InSettings)
{
uint32 ObjectId = INDEX_NONE;
int32 SourceNumChannels = 0;
switch (InSettings.KeySource)
{
case ESubmixEffectDynamicsKeySource::AudioBus:
{
if (InSettings.ExternalAudioBus)
{
ObjectId = InSettings.ExternalAudioBus->GetUniqueID();
SourceNumChannels = static_cast<int32>(InSettings.ExternalAudioBus->AudioBusChannels) + 1;
}
}
break;
case ESubmixEffectDynamicsKeySource::Submix:
{
if (InSettings.ExternalSubmix)
{
ObjectId = InSettings.ExternalSubmix->GetUniqueID();
}
}
break;
default:
{
}
break;
}
KeySource.Update(InSettings.KeySource, ObjectId, SourceNumChannels);
}
void FSubmixEffectDynamicsProcessor::OnDeviceCreated(Audio::FDeviceId InDeviceId)
{
if (InDeviceId == DeviceId)
{
GET_EFFECT_SETTINGS(SubmixEffectDynamicsProcessor);
UpdateKeyFromSettings(Settings);
FAudioDeviceManagerDelegates::OnAudioDeviceCreated.Remove(DeviceCreatedHandle);
}
}
void FSubmixEffectDynamicsProcessor::OnDeviceDestroyed(Audio::FDeviceId InDeviceId)
{
if (InDeviceId == DeviceId)
{
// Reset the key on device destruction to avoid reinitializing
// it during FAudioDevice::Teardown via ProcessAudio.
ResetKey();
FAudioDeviceManagerDelegates::OnAudioDeviceDestroyed.Remove(DeviceDestroyedHandle);
}
}
void USubmixEffectDynamicsProcessorPreset::OnInit()
{
switch (Settings.KeySource)
{
case ESubmixEffectDynamicsKeySource::AudioBus:
{
SetAudioBus(Settings.ExternalAudioBus);
}
break;
case ESubmixEffectDynamicsKeySource::Submix:
{
SetExternalSubmix(Settings.ExternalSubmix);
}
break;
default:
{
}
break;
}
}
#if WITH_EDITOR
void USubmixEffectDynamicsProcessorPreset::PostEditChangeChainProperty(struct FPropertyChangedChainEvent& InChainEvent)
{
if (InChainEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(FSubmixEffectDynamicsProcessorSettings, KeySource))
{
switch (Settings.KeySource)
{
case ESubmixEffectDynamicsKeySource::AudioBus:
{
Settings.ExternalSubmix = nullptr;
}
break;
case ESubmixEffectDynamicsKeySource::Submix:
{
Settings.ExternalAudioBus = nullptr;
}
break;
case ESubmixEffectDynamicsKeySource::Default:
default:
{
Settings.ExternalSubmix = nullptr;
Settings.ExternalAudioBus = nullptr;
static_assert(static_cast<int32>(ESubmixEffectDynamicsKeySource::Count) == 3, "Possible missing KeySource switch case coverage");
}
break;
}
}
Super::PostEditChangeChainProperty(InChainEvent);
}
#endif // WITH_EDITOR
void USubmixEffectDynamicsProcessorPreset::Serialize(FStructuredArchive::FRecord Record)
{
FArchive& UnderlyingArchive = Record.GetUnderlyingArchive();
if (UnderlyingArchive.IsLoading())
{
if (Settings.bChannelLinked_DEPRECATED)
{
Settings.LinkMode = ESubmixEffectDynamicsChannelLinkMode::Average;
Settings.bChannelLinked_DEPRECATED = 0;
}
}
Super::Serialize(Record);
}
void USubmixEffectDynamicsProcessorPreset::ResetKey()
{
EffectCommand<FSubmixEffectDynamicsProcessor>([](FSubmixEffectDynamicsProcessor& Instance)
{
Instance.ResetKey();
});
}
void USubmixEffectDynamicsProcessorPreset::SetAudioBus(UAudioBus* InAudioBus)
{
int32 BusChannels = 0;
if (InAudioBus)
{
BusChannels = static_cast<int32>(InAudioBus->AudioBusChannels) + 1;
SetKey(ESubmixEffectDynamicsKeySource::AudioBus, InAudioBus, BusChannels);
}
else
{
ResetKey();
}
}
void USubmixEffectDynamicsProcessorPreset::SetExternalSubmix(USoundSubmix* InSubmix)
{
if (InSubmix)
{
SetKey(ESubmixEffectDynamicsKeySource::Submix, InSubmix);
}
else
{
ResetKey();
}
}
void USubmixEffectDynamicsProcessorPreset::SetKey(ESubmixEffectDynamicsKeySource InKeySource, UObject* InObject, int32 InNumChannels)
{
if (InObject)
{
EffectCommand<FSubmixEffectDynamicsProcessor>([this, ObjectId = InObject->GetUniqueID(), InKeySource, InNumChannels](FSubmixEffectDynamicsProcessor& Instance)
{
Instance.KeySource.Update(InKeySource, ObjectId, InNumChannels);
});
}
}
void USubmixEffectDynamicsProcessorPreset::SetSettings(const FSubmixEffectDynamicsProcessorSettings& InSettings)
{
UpdateSettings(InSettings);
IterateEffects<FSubmixEffectDynamicsProcessor>([&](FSubmixEffectDynamicsProcessor& Instance)
{
Instance.UpdateKeyFromSettings(InSettings);
});
}