// Copyright Epic Games, Inc. All Rights Reserved. #include "Components/SynthComponent.h" #include "AudioDevice.h" #include "AudioMixerLog.h" #include "Engine/World.h" #include "Sound/AudioSettings.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(SynthComponent) USynthSound::USynthSound(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } void USynthSound::Init(USynthComponent* InSynthComponent, const int32 InNumChannels, const int32 InSampleRate, const int32 InCallbackSize) { check(InSynthComponent); OwningSynthComponent = InSynthComponent; VirtualizationMode = EVirtualizationMode::PlayWhenSilent; NumChannels = InNumChannels; NumSamplesToGeneratePerCallback = InCallbackSize; bCanProcessAsync = true; Duration = INDEFINITELY_LOOPING_DURATION; bLooping = true; SampleRate = InSampleRate; } void USynthSound::StartOnAudioDevice(FAudioDevice* InAudioDevice) { } void USynthSound::OnBeginGenerate() { if (ensure(OwningSynthComponent.IsValid())) { OwningSynthComponent->OnBeginGenerate(); } } int32 USynthSound::OnGeneratePCMAudio(TArray& OutAudio, int32 NumSamples) { LLM_SCOPE(ELLMTag::AudioSynthesis); OutAudio.Reset(); // If running with audio mixer, the output audio buffer will be in floats already OutAudio.AddZeroed(NumSamples * sizeof(float)); // Mark pending kill can null this out on the game thread in rare cases. if (!OwningSynthComponent.IsValid()) { return 0; } return OwningSynthComponent->OnGeneratePCMAudio((float*)OutAudio.GetData(), NumSamples); } void USynthSound::OnEndGenerate() { // Mark pending kill can null this out on the game thread in rare cases. if (OwningSynthComponent.IsValid()) { OwningSynthComponent->OnEndGenerate(); } } ISoundGeneratorPtr USynthSound::CreateSoundGenerator(const FSoundGeneratorInitParams& InParams) { if (OwningSynthComponent.IsValid()) { return OwningSynthComponent->CreateSoundGeneratorInternal(InParams); } return nullptr; } Audio::EAudioMixerStreamDataFormat::Type USynthSound::GetGeneratedPCMDataFormat() const { // Only audio mixer supports return float buffers return Audio::EAudioMixerStreamDataFormat::Float; } USynthComponent::USynthComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { bAutoActivate = false; bStopWhenOwnerDestroyed = true; bEnableBusSends = true; bEnableBaseSubmix = true; bEnableSubmixSends = true; bNeverNeedsRenderUpdate = true; bUseAttachParentBound = true; // Avoid CalcBounds() when transform changes. bIsSynthPlaying = false; bIsInitialized = false; bIsUISound = false; bAlwaysPlay = true; Synth = nullptr; PreferredBufferLength = DEFAULT_PROCEDURAL_SOUNDWAVE_BUFFER_SIZE; #if WITH_EDITORONLY_DATA bVisualizeComponent = false; #endif } void USynthComponent::OnAudioComponentEnvelopeValue(const UAudioComponent* InAudioComponent, const USoundWave* SoundWave, const float EnvelopeValue) { if (OnAudioEnvelopeValue.IsBound()) { OnAudioEnvelopeValue.Broadcast(EnvelopeValue); } if (OnAudioEnvelopeValueNative.IsBound()) { OnAudioEnvelopeValueNative.Broadcast(InAudioComponent, EnvelopeValue); } } void USynthComponent::BeginDestroy() { Super::BeginDestroy(); Stop(); } void USynthComponent::Activate(bool bReset) { if (bReset || ShouldActivate()) { Start(); if (IsActive()) { OnComponentActivated.Broadcast(this, bReset); } } } void USynthComponent::Deactivate() { if (ShouldActivate() == false) { Stop(); if (!IsActive()) { OnComponentDeactivated.Broadcast(this); } } } void USynthComponent::Initialize(int32 SampleRateOverride) { // This will try to create the audio component if it hasn't yet been created CreateAudioComponent(); // Try to get a proper sample rate int32 SampleRate = SampleRateOverride; if (SampleRate == INDEX_NONE) { // Check audio device if we've not explicitly been told what sample rate to use FAudioDevice* AudioDevice = GetAudioDevice(); if (AudioDevice) { SampleRate = AudioDevice->SampleRate; } } // Only allow initialization if we have a proper sample rate if (SampleRate != INDEX_NONE) { #if SYNTH_GENERATOR_TEST_TONE NumChannels = 2; TestSineLeft.Init(SampleRate, 440.0f, 0.5f); TestSineRight.Init(SampleRate, 220.0f, 0.5f); #else // Initialize the synth component Init(SampleRate); if (NumChannels < 0 || NumChannels > 8) { UE_LOG(LogAudioMixer, Error, TEXT("Synthesis component '%s' has set an invalid channel count '%d'."), *GetName(), NumChannels); } NumChannels = FMath::Clamp(NumChannels, 1, 8); #endif if (!Synth) { Synth = NewObject(); } // Copy sound base data to the sound Synth->SourceEffectChain = SourceEffectChain; Synth->SoundSubmixObject = SoundSubmix; Synth->SoundSubmixSends = SoundSubmixSends; Synth->BusSends = BusSends; Synth->PreEffectBusSends = PreEffectBusSends; Synth->bEnableBusSends = bEnableBusSends; Synth->bEnableBaseSubmix = bEnableBaseSubmix; Synth->bEnableSubmixSends = bEnableSubmixSends; Synth->Init(this, NumChannels, SampleRate, PreferredBufferLength); // Retrieve the synth component's audio device vs the audio component's if (FAudioDevice* AudioDevice = GetAudioDevice()) { Synth->StartOnAudioDevice(AudioDevice); } } } UAudioComponent* USynthComponent::GetAudioComponent() { return AudioComponent; } void USynthComponent::CreateAudioComponent() { if (!AudioComponent) { // Create the audio component which will be used to play the procedural sound wave AudioComponent = NewObject(this, NAME_None, RF_Transactional | RF_Transient | RF_TextExportTransient); AudioComponent->CreationMethod = CreationMethod; AudioComponent->OnAudioSingleEnvelopeValueNative.AddUObject(this, &USynthComponent::OnAudioComponentEnvelopeValue); if (!AudioComponent->GetAttachParent() && !AudioComponent->IsAttachedTo(this)) { AActor* Owner = GetOwner(); // If the media component has no owner or the owner doesn't have a world if (!Owner || !Owner->GetWorld()) { // Attempt to retrieve the synth component's world and register the audio component with it // This ensures that the synth component plays on the correct world in cases where there isn't an owner if (UWorld* World = GetWorld()) { AudioComponent->RegisterComponentWithWorld(World); AudioComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform); } else { AudioComponent->SetupAttachment(this); } } else { AudioComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform); AudioComponent->RegisterComponent(); } } } if (AudioComponent) { AudioComponent->bAutoActivate = false; AudioComponent->bStopWhenOwnerDestroyed = true; AudioComponent->bShouldRemainActiveIfDropped = true; AudioComponent->Mobility = EComponentMobility::Movable; #if WITH_EDITORONLY_DATA AudioComponent->bVisualizeComponent = false; #endif // Set defaults to be the same as audio component defaults AudioComponent->EnvelopeFollowerAttackTime = EnvelopeFollowerAttackTime; AudioComponent->EnvelopeFollowerReleaseTime = EnvelopeFollowerReleaseTime; AudioComponent->bAlwaysPlay = bAlwaysPlay; } } void USynthComponent::OnRegister() { CreateAudioComponent(); Super::OnRegister(); } void USynthComponent::OnUnregister() { // Route OnUnregister event. Super::OnUnregister(); // Don't stop audio and clean up component if owner has been destroyed (default behaviour). This function gets // called from AActor::ClearComponents when an actor gets destroyed which is not usually what we want for one- // shot sounds. AActor* Owner = GetOwner(); if (!Owner || bStopWhenOwnerDestroyed) { Stop(); } // Make sure the audio component is destroyed during unregister if (AudioComponent && !AudioComponent->IsBeingDestroyed()) { if (Owner && Owner->GetWorld()) { AudioComponent->DetachFromComponent(FDetachmentTransformRules::KeepRelativeTransform); AudioComponent->UnregisterComponent(); } AudioComponent->DestroyComponent(); AudioComponent = nullptr; } // Clear out the synth component's reference to the sound generator or it will leak until it gets GC'd // Normally this is ok to wait till GC but some derived synths might need for the handle to be released SoundGenerator.Reset(); } void USynthComponent::EndPlay(const EEndPlayReason::Type Reason) { Super::EndPlay(Reason); if (GetOwner() && (Reason == EEndPlayReason::LevelTransition || Reason == EEndPlayReason::RemovedFromWorld || Reason == EEndPlayReason::Destroyed)) { // If our world or sublevel is going away, stop immediately to prevent the containing world/level from being leaked via hard references from the audio device. Stop(); } } USoundClass* USynthComponent::GetSoundClass() { if (SoundClass) { return SoundClass; } const UAudioSettings* AudioSettings = GetDefault(); if (ensure(AudioSettings)) { return AudioSettings->GetDefaultSoundClass(); } return nullptr; } bool USynthComponent::IsReadyForOwnerToAutoDestroy() const { const bool bIsAudioComponentReadyForDestroy = !AudioComponent || (AudioComponent && !AudioComponent->IsPlaying()); const bool bIsSynthSoundReadyForDestroy = !Synth || !Synth->IsGeneratingAudio(); return bIsAudioComponentReadyForDestroy && bIsSynthSoundReadyForDestroy; } #if WITH_EDITOR void USynthComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { if (IsActive()) { // If this is an auto destroy component we need to prevent it from being auto-destroyed since we're really just restarting it const bool bWasAutoDestroy = bAutoDestroy; bAutoDestroy = false; Stop(); bAutoDestroy = bWasAutoDestroy; Start(); } Super::PostEditChangeProperty(PropertyChangedEvent); } #endif //WITH_EDITOR #if WITH_EDITORONLY_DATA void USynthComponent::PostLoad() { Super::PostLoad(); if (bOutputToBusOnly_DEPRECATED) { bEnableBusSends = true; bEnableBaseSubmix = !bOutputToBusOnly_DEPRECATED; bEnableSubmixSends = !bOutputToBusOnly_DEPRECATED; bOutputToBusOnly_DEPRECATED = false; } } #endif //WITH_EDITORONLY_DATA void USynthComponent::Serialize(FArchive& Ar) { Super::Serialize(Ar); #if WITH_EDITORONLY_DATA if (Ar.IsLoading()) { if (ConcurrencySettings_DEPRECATED != nullptr) { ConcurrencySet.Add(ConcurrencySettings_DEPRECATED); ConcurrencySettings_DEPRECATED = nullptr; } } #endif // WITH_EDITORONLY_DATA } void USynthComponent::PumpPendingMessages() { TFunction Command; while (CommandQueue.Dequeue(Command)) { Command(); } } FAudioDevice* USynthComponent::GetAudioDevice() const { // If the synth component has a world, that means it was already registed with that world if (UWorld* World = GetWorld()) { return World->AudioDeviceHandle.GetAudioDevice(); } // Otherwise, retrieve the audio component's audio device (probably from it's owner) if (AudioComponent) { return AudioComponent->GetAudioDevice(); } // No audio device return nullptr; } int32 USynthComponent::OnGeneratePCMAudio(float* GeneratedPCMData, int32 NumSamples) { LLM_SCOPE(ELLMTag::AudioSynthesis); PumpPendingMessages(); check(NumSamples > 0); // Only call into the synth if we're actually playing, otherwise, we'll write out zero's if (bIsSynthPlaying) { return OnGenerateAudio(GeneratedPCMData, NumSamples); } return NumSamples; } void USynthComponent::Start() { // Only need to start if we're not already active if (IsActive()) { return; } // We will also ensure that this synth was initialized before attempting to play. Initialize(); // If there is no Synth USoundBase, we can't start. This can happen if start is called in a cook, a server, or // if the audio engine is set to "noaudio". // TODO: investigate if this should be handled elsewhere before this point if (!Synth) { return; } if (AudioComponent) { // Copy the attenuation and concurrency data from the synth component to the audio component AudioComponent->AttenuationSettings = AttenuationSettings; AudioComponent->bOverrideAttenuation = bOverrideAttenuation; AudioComponent->bIsUISound = bIsUISound; AudioComponent->bIsPreviewSound = bIsPreviewSound; AudioComponent->bAllowSpatialization = bAllowSpatialization; AudioComponent->ConcurrencySet = ConcurrencySet; AudioComponent->AttenuationOverrides = AttenuationOverrides; AudioComponent->SoundClassOverride = SoundClass; AudioComponent->EnvelopeFollowerAttackTime = EnvelopeFollowerAttackTime; AudioComponent->EnvelopeFollowerReleaseTime = EnvelopeFollowerReleaseTime; AudioComponent->ModulationRouting = ModulationRouting; // Copy sound base data to the sound Synth->AttenuationSettings = AttenuationSettings; Synth->SourceEffectChain = SourceEffectChain; Synth->SoundSubmixObject = SoundSubmix; Synth->SoundSubmixSends = SoundSubmixSends; // Set the audio component's sound to be our procedural sound wave AudioComponent->SetSound(Synth); AudioComponent->Play(0); SetActiveFlag(AudioComponent->IsActive()); bIsSynthPlaying = true; } } void USynthComponent::Stop() { if (IsActive()) { if (AudioComponent) { AudioComponent->Stop(); if (FAudioDevice* AudioDevice = AudioComponent->GetAudioDevice()) { AudioDevice->StopSoundsUsingResource(Synth); } } SetActiveFlag(false); } } bool USynthComponent::IsPlaying() const { return AudioComponent && AudioComponent->IsPlaying(); } void USynthComponent::SetVolumeMultiplier(float VolumeMultiplier) { if (AudioComponent) { AudioComponent->SetVolumeMultiplier(VolumeMultiplier); } } void USynthComponent::SetSubmixSend(USoundSubmixBase* Submix, float SendLevel) { if (AudioComponent) { AudioComponent->SetSubmixSend(Submix, SendLevel); } } void USynthComponent::SetSourceBusSendPreEffect(USoundSourceBus* SoundSourceBus, float SourceBusSendLevel) { if (AudioComponent) { AudioComponent->SetSourceBusSendPreEffect(SoundSourceBus, SourceBusSendLevel); } } void USynthComponent::SetSourceBusSendPostEffect(USoundSourceBus* SoundSourceBus, float SourceBusSendLevel) { if (AudioComponent) { AudioComponent->SetSourceBusSendPostEffect(SoundSourceBus, SourceBusSendLevel); } } void USynthComponent::SetAudioBusSendPreEffect(UAudioBus* AudioBus, float AudioBusSendLevel) { if (AudioComponent) { AudioComponent->SetAudioBusSendPreEffect(AudioBus, AudioBusSendLevel); } } void USynthComponent::SetAudioBusSendPostEffect(UAudioBus* AudioBus, float AudioBusSendLevel) { if (AudioComponent) { AudioComponent->SetAudioBusSendPostEffect(AudioBus, AudioBusSendLevel); } } void USynthComponent::SetLowPassFilterEnabled(bool InLowPassFilterEnabled) { if (AudioComponent) { AudioComponent->SetLowPassFilterEnabled(InLowPassFilterEnabled); } } void USynthComponent::SetLowPassFilterFrequency(float InLowPassFilterFrequency) { if (AudioComponent) { AudioComponent->SetLowPassFilterFrequency(InLowPassFilterFrequency); } } void USynthComponent::SetOutputToBusOnly(bool bInOutputToBusOnly) { if (AudioComponent) { AudioComponent->SetOutputToBusOnly(bInOutputToBusOnly); } } void USynthComponent::FadeIn(float FadeInDuration, float FadeVolumeLevel/* = 1.0f*/, float StartTime/* = 0.0f*/, const EAudioFaderCurve FadeCurve/* = EAudioFaderCurve::Linear*/) const { if(AudioComponent) { AudioComponent->FadeIn(FadeInDuration, FadeVolumeLevel, StartTime, FadeCurve); } } void USynthComponent::FadeOut(float FadeOutDuration, float FadeVolumeLevel, const EAudioFaderCurve FadeCurve/* = EAudioFaderCurve::Linear*/) const { if(AudioComponent) { AudioComponent->FadeOut(FadeOutDuration, FadeVolumeLevel, FadeCurve); } } void USynthComponent::AdjustVolume(float AdjustVolumeDuration, float AdjustVolumeLevel, const EAudioFaderCurve FadeCurve/* = EAudioFaderCurve::Linear*/) const { if(AudioComponent) { AudioComponent->AdjustVolume(AdjustVolumeDuration, AdjustVolumeLevel, FadeCurve); } } void USynthComponent::SetModulationRouting(const TSet& Modulators, const EModulationDestination Destination, const EModulationRouting RoutingMethod) { if (AudioComponent) { AudioComponent->SetModulationRouting(Modulators, Destination, RoutingMethod); } } TSet USynthComponent::GetModulators(const EModulationDestination Destination) { if (AudioComponent) { return AudioComponent->GetModulators(Destination); } return TSet(); } void USynthComponent::SynthCommand(TFunction Command) { if (SoundGenerator.IsValid()) { UE_LOG(LogAudioMixer, Error, TEXT("Synthesis component '%s' has implemented a sound generator interface. Do not call SynthCommand on the USynthComponent)."), *GetName()); } else { CommandQueue.Enqueue(MoveTemp(Command)); } } ISoundGeneratorPtr USynthComponent::CreateSoundGeneratorInternal(const FSoundGeneratorInitParams& InParams) { LLM_SCOPE(ELLMTag::AudioSynthesis); return SoundGenerator = CreateSoundGenerator(InParams); }