// Copyright Epic Games, Inc. All Rights Reserved. #include "AudioMixerBlueprintLibrary.h" #include "Algo/Transform.h" #include "Async/Async.h" #include "AudioBusSubsystem.h" #include "AudioCompressionSettingsUtils.h" #include "AudioDevice.h" #include "AudioDeviceManager.h" #include "AudioMixerDevice.h" #include "AudioMixerSubmix.h" #include "ContentStreaming.h" #include "CoreMinimal.h" #include "DSP/ConstantQ.h" #include "DSP/SpectrumAnalyzer.h" #include "Engine/World.h" #include "Sound/SoundEffectPreset.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(AudioMixerBlueprintLibrary) // This is our global recording task: static TUniquePtr RecordingData; FAudioOutputDeviceInfo::FAudioOutputDeviceInfo(const Audio::FAudioPlatformDeviceInfo& InDeviceInfo) : Name(InDeviceInfo.Name) , DeviceId(InDeviceInfo.DeviceId) , NumChannels(InDeviceInfo.NumChannels) , SampleRate(InDeviceInfo.SampleRate) , Format(EAudioMixerStreamDataFormatType(InDeviceInfo.Format)) , bIsSystemDefault(InDeviceInfo.bIsSystemDefault) , bIsCurrentDevice(false) { for (EAudioMixerChannel::Type i : InDeviceInfo.OutputChannelArray) { OutputChannelArray.Emplace(EAudioMixerChannelType(i)); } } FString UAudioMixerBlueprintLibrary::Conv_AudioOutputDeviceInfoToString(const FAudioOutputDeviceInfo& InDeviceInfo) { FString output = FString::Printf(TEXT("Device Name: %s, \nDevice Id: %s, \nNum Channels: %u, \nSample Rate: %u, \nFormat: %s, \nIs System Default: %u, \n"), *InDeviceInfo.Name, *InDeviceInfo.DeviceId, InDeviceInfo.NumChannels, InDeviceInfo.SampleRate, *DataFormatAsString(EAudioMixerStreamDataFormatType(InDeviceInfo.Format)), InDeviceInfo.bIsSystemDefault); output.Append("Output Channel Array: \n"); for (int32 i = 0; i < InDeviceInfo.NumChannels; ++i) { if (i < InDeviceInfo.OutputChannelArray.Num()) { output += FString::Printf(TEXT(" %d: %s \n"), i, ToString(InDeviceInfo.OutputChannelArray[i])); } } return output; } FString DataFormatAsString(EAudioMixerStreamDataFormatType type) { switch (type) { case EAudioMixerStreamDataFormatType::Unknown: return FString("Unknown"); case EAudioMixerStreamDataFormatType::Float: return FString("Float"); case EAudioMixerStreamDataFormatType::Int16: return FString("Int16"); case EAudioMixerStreamDataFormatType::Unsupported: return FString("Unsupported"); default: return FString("Invalid Format Type"); } } void UAudioMixerBlueprintLibrary::AddMasterSubmixEffect(const UObject* WorldContextObject, USoundEffectSubmixPreset* SubmixEffectPreset) { if (!SubmixEffectPreset) { UE_LOG(LogAudioMixer, Warning, TEXT("AddMasterSubmixEffect was passed invalid submix effect preset")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { FSoundEffectSubmixInitData InitData; InitData.SampleRate = MixerDevice->GetSampleRate(); InitData.DeviceID = MixerDevice->DeviceID; InitData.PresetSettings = nullptr; InitData.ParentPresetUniqueId = SubmixEffectPreset->GetUniqueID(); // Immediately create a new sound effect base here before the object becomes potentially invalidated TSoundEffectSubmixPtr SoundEffectSubmix = USoundEffectPreset::CreateInstance(InitData, *SubmixEffectPreset); SoundEffectSubmix->SetEnabled(true); MixerDevice->AddMasterSubmixEffect(SoundEffectSubmix); } } void UAudioMixerBlueprintLibrary::RemoveMasterSubmixEffect(const UObject* WorldContextObject, USoundEffectSubmixPreset* SubmixEffectPreset) { if (!SubmixEffectPreset) { UE_LOG(LogAudioMixer, Warning, TEXT("RemoveMasterSubmixEffect was passed invalid submix effect preset")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { // Get the unique id for the preset object on the game thread. Used to refer to the object on audio render thread. uint32 SubmixPresetUniqueId = SubmixEffectPreset->GetUniqueID(); MixerDevice->RemoveMasterSubmixEffect(SubmixPresetUniqueId); } } void UAudioMixerBlueprintLibrary::ClearMasterSubmixEffects(const UObject* WorldContextObject) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->ClearMasterSubmixEffects(); } } int32 UAudioMixerBlueprintLibrary::AddSubmixEffect(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, USoundEffectSubmixPreset* SubmixEffectPreset) { if (!SubmixEffectPreset || !InSoundSubmix) { return 0; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { FSoundEffectSubmixInitData InitData; InitData.DeviceID = MixerDevice->DeviceID; InitData.SampleRate = MixerDevice->GetSampleRate(); InitData.ParentPresetUniqueId = SubmixEffectPreset->GetUniqueID(); TSoundEffectSubmixPtr SoundEffectSubmix = USoundEffectPreset::CreateInstance(InitData, *SubmixEffectPreset); SoundEffectSubmix->SetEnabled(true); return MixerDevice->AddSubmixEffect(InSoundSubmix, SoundEffectSubmix); } return 0; } void UAudioMixerBlueprintLibrary::RemoveSubmixEffectPreset(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, USoundEffectSubmixPreset* InSubmixEffectPreset) { RemoveSubmixEffect(WorldContextObject, InSoundSubmix, InSubmixEffectPreset); } void UAudioMixerBlueprintLibrary::RemoveSubmixEffect(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, USoundEffectSubmixPreset* InSubmixEffectPreset) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { uint32 SubmixPresetUniqueId = InSubmixEffectPreset->GetUniqueID(); MixerDevice->RemoveSubmixEffect(InSoundSubmix, SubmixPresetUniqueId); } } void UAudioMixerBlueprintLibrary::RemoveSubmixEffectPresetAtIndex(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, int32 SubmixChainIndex) { RemoveSubmixEffectAtIndex(WorldContextObject, InSoundSubmix, SubmixChainIndex); } void UAudioMixerBlueprintLibrary::RemoveSubmixEffectAtIndex(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, int32 SubmixChainIndex) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->RemoveSubmixEffectAtIndex(InSoundSubmix, SubmixChainIndex); } } void UAudioMixerBlueprintLibrary::ReplaceSoundEffectSubmix(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, int32 SubmixChainIndex, USoundEffectSubmixPreset* SubmixEffectPreset) { ReplaceSubmixEffect(WorldContextObject, InSoundSubmix, SubmixChainIndex, SubmixEffectPreset); } void UAudioMixerBlueprintLibrary::ReplaceSubmixEffect(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, int32 SubmixChainIndex, USoundEffectSubmixPreset* SubmixEffectPreset) { if (!SubmixEffectPreset || !InSoundSubmix) { return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { FSoundEffectSubmixInitData InitData; InitData.SampleRate = MixerDevice->GetSampleRate(); TSoundEffectSubmixPtr SoundEffectSubmix = USoundEffectPreset::CreateInstance(InitData, *SubmixEffectPreset); SoundEffectSubmix->SetEnabled(true); MixerDevice->ReplaceSoundEffectSubmix(InSoundSubmix, SubmixChainIndex, SoundEffectSubmix); } } void UAudioMixerBlueprintLibrary::ClearSubmixEffects(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->ClearSubmixEffects(InSoundSubmix); } } void UAudioMixerBlueprintLibrary::SetSubmixEffectChainOverride(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, TArray InSubmixEffectPresetChain, float InFadeTimeSec) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { TArray NewSubmixEffectPresetChain; for (USoundEffectSubmixPreset* SubmixEffectPreset : InSubmixEffectPresetChain) { if (SubmixEffectPreset) { FSoundEffectSubmixInitData InitData; InitData.DeviceID = MixerDevice->DeviceID; InitData.SampleRate = MixerDevice->GetSampleRate(); InitData.ParentPresetUniqueId = SubmixEffectPreset->GetUniqueID(); TSoundEffectSubmixPtr SoundEffectSubmix = USoundEffectPreset::CreateInstance(InitData, *SubmixEffectPreset); SoundEffectSubmix->SetEnabled(true); NewSubmixEffectPresetChain.Add(SoundEffectSubmix); } } if (NewSubmixEffectPresetChain.Num() > 0) { MixerDevice->SetSubmixEffectChainOverride(InSoundSubmix, NewSubmixEffectPresetChain, InFadeTimeSec); } } } void UAudioMixerBlueprintLibrary::ClearSubmixEffectChainOverride(const UObject* WorldContextObject, USoundSubmix* InSoundSubmix, float InFadeTimeSec) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->ClearSubmixEffectChainOverride(InSoundSubmix, InFadeTimeSec); } } void UAudioMixerBlueprintLibrary::StartRecordingOutput(const UObject* WorldContextObject, float ExpectedDuration, USoundSubmix* SubmixToRecord) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->StartRecording(SubmixToRecord, ExpectedDuration); } else { UE_LOG(LogAudioMixer, Error, TEXT("Output recording is an audio mixer only feature.")); } } USoundWave* UAudioMixerBlueprintLibrary::StopRecordingOutput(const UObject* WorldContextObject, EAudioRecordingExportType ExportType, const FString& Name, FString Path, USoundSubmix* SubmixToRecord, USoundWave* ExistingSoundWaveToOverwrite) { if (RecordingData.IsValid()) { UE_LOG(LogAudioMixer, Warning, TEXT("Abandoning existing write operation. If you'd like to export multiple submix recordings at the same time, use Start/Finish Recording Submix Output instead.")); } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { float SampleRate; float ChannelCount; // call the thing here. Audio::FAlignedFloatBuffer& RecordedBuffer = MixerDevice->StopRecording(SubmixToRecord, ChannelCount, SampleRate); if (RecordedBuffer.Num() == 0) { UE_LOG(LogAudioMixer, Warning, TEXT("No audio data. Did you call Start Recording Output?")); return nullptr; } // Pack output data into a TSampleBuffer and record out: RecordingData.Reset(new Audio::FAudioRecordingData()); RecordingData->InputBuffer = Audio::TSampleBuffer(RecordedBuffer, ChannelCount, SampleRate); switch (ExportType) { case EAudioRecordingExportType::SoundWave: { USoundWave* ResultingSoundWave = RecordingData->Writer.SynchronouslyWriteSoundWave(RecordingData->InputBuffer, &Name, &Path); RecordingData.Reset(); return ResultingSoundWave; } case EAudioRecordingExportType::WavFile: { RecordingData->Writer.BeginWriteToWavFile(RecordingData->InputBuffer, Name, Path, [SubmixToRecord]() { if (SubmixToRecord && SubmixToRecord->OnSubmixRecordedFileDone.IsBound()) { SubmixToRecord->OnSubmixRecordedFileDone.Broadcast(nullptr); } // I'm gonna try this, but I do not feel great about it. RecordingData.Reset(); }); break; } default: break; } } else { UE_LOG(LogAudioMixer, Error, TEXT("Output recording is an audio mixer only feature.")); } return nullptr; } void UAudioMixerBlueprintLibrary::PauseRecordingOutput(const UObject* WorldContextObject, USoundSubmix* SubmixToPause) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->PauseRecording(SubmixToPause); } else { UE_LOG(LogAudioMixer, Error, TEXT("Output recording is an audio mixer only feature.")); } } void UAudioMixerBlueprintLibrary::ResumeRecordingOutput(const UObject* WorldContextObject, USoundSubmix* SubmixToResume) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->ResumeRecording(SubmixToResume); } else { UE_LOG(LogAudioMixer, Error, TEXT("Output recording is an audio mixer only feature.")); } } void UAudioMixerBlueprintLibrary::StartAnalyzingOutput(const UObject* WorldContextObject, USoundSubmix* SubmixToAnalyze, EFFTSize FFTSize, EFFTPeakInterpolationMethod InterpolationMethod, EFFTWindowType WindowType, float HopSize, EAudioSpectrumType AudioSpectrumType) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { FSoundSpectrumAnalyzerSettings Settings = USoundSubmix::GetSpectrumAnalyzerSettings(FFTSize, InterpolationMethod, WindowType, HopSize, AudioSpectrumType); MixerDevice->StartSpectrumAnalysis(SubmixToAnalyze, Settings); } else { UE_LOG(LogAudioMixer, Error, TEXT("Spectrum Analysis is an audio mixer only feature.")); } } void UAudioMixerBlueprintLibrary::StopAnalyzingOutput(const UObject* WorldContextObject, USoundSubmix* SubmixToStopAnalyzing) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->StopSpectrumAnalysis(SubmixToStopAnalyzing); } else { UE_LOG(LogAudioMixer, Error, TEXT("Spectrum Analysis is an audio mixer only feature.")); } } TArray UAudioMixerBlueprintLibrary::MakeMusicalSpectralAnalysisBandSettings(int32 InNumNotes, EMusicalNoteName InStartingMusicalNote, int32 InStartingOctave, int32 InAttackTimeMsec, int32 InReleaseTimeMsec) { // Make values sane. InNumNotes = FMath::Clamp(InNumNotes, 0, 10000); InStartingOctave = FMath::Clamp(InStartingOctave, -1, 10); InAttackTimeMsec = FMath::Clamp(InAttackTimeMsec, 0, 10000); InReleaseTimeMsec = FMath::Clamp(InReleaseTimeMsec, 0, 10000); // Some assumptions here on what constitutes "music". 12 notes, equal temperament. const float BandsPerOctave = 12.f; // This QFactor makes the bandwidth equal to the difference in frequency between adjacent notes. const float QFactor = 1.f / (FMath::Pow(2.f, 1.f / BandsPerOctave) - 1.f); // Base note index off of A4 which we know to be 440 hz. // Make note relative to A int32 NoteIndex = static_cast(InStartingMusicalNote) - static_cast(EMusicalNoteName::A); // Make relative to 4th octave of A NoteIndex += 12 * (InStartingOctave - 4); const float StartingFrequency = 440.f * FMath::Pow(2.f, static_cast(NoteIndex) / 12.f); TArray BandSettingsArray; for (int32 i = 0; i < InNumNotes; i++) { FSoundSubmixSpectralAnalysisBandSettings BandSettings; BandSettings.BandFrequency = Audio::FPseudoConstantQ::GetConstantQCenterFrequency(i, StartingFrequency, BandsPerOctave); BandSettings.QFactor = QFactor; BandSettings.AttackTimeMsec = InAttackTimeMsec; BandSettings.ReleaseTimeMsec = InReleaseTimeMsec; BandSettingsArray.Add(BandSettings); } return BandSettingsArray; } TArray UAudioMixerBlueprintLibrary::MakeFullSpectrumSpectralAnalysisBandSettings(int32 InNumBands, float InMinimumFrequency, float InMaximumFrequency, int32 InAttackTimeMsec, int32 InReleaseTimeMsec) { // Make inputs sane. InNumBands = FMath::Clamp(InNumBands, 0, 10000); InMinimumFrequency = FMath::Clamp(InMinimumFrequency, 20.0f, 20000.0f); InMaximumFrequency = FMath::Clamp(InMaximumFrequency, InMinimumFrequency, 20000.0f); InAttackTimeMsec = FMath::Clamp(InAttackTimeMsec, 0, 10000); InReleaseTimeMsec = FMath::Clamp(InReleaseTimeMsec, 0, 10000); // Calculate CQT settings needed to space bands. const float NumOctaves = FMath::Loge(InMaximumFrequency / InMinimumFrequency) / FMath::Loge(2.f); const float BandsPerOctave = static_cast(InNumBands) / FMath::Max(NumOctaves, 0.01f); const float QFactor = 1.f / (FMath::Pow(2.f, 1.f / FMath::Max(BandsPerOctave, 0.01f)) - 1.f); TArray BandSettingsArray; for (int32 i = 0; i < InNumBands; i++) { FSoundSubmixSpectralAnalysisBandSettings BandSettings; BandSettings.BandFrequency = Audio::FPseudoConstantQ::GetConstantQCenterFrequency(i, InMinimumFrequency, BandsPerOctave); BandSettings.QFactor = QFactor; BandSettings.AttackTimeMsec = InAttackTimeMsec; BandSettings.ReleaseTimeMsec = InReleaseTimeMsec; BandSettingsArray.Add(BandSettings); } return BandSettingsArray; } TArray UAudioMixerBlueprintLibrary::MakePresetSpectralAnalysisBandSettings(EAudioSpectrumBandPresetType InBandPresetType, int32 InNumBands, int32 InAttackTimeMsec, int32 InReleaseTimeMsec) { float MinimumFrequency = 20.f; float MaximumFrequency = 20000.f; // Likely all these are debatable. What we are shooting for is the most active frequency // ranges, so when an instrument plays a significant amount of spectral energy from that // instrument will show up in the frequency range. switch (InBandPresetType) { case EAudioSpectrumBandPresetType::KickDrum: MinimumFrequency = 40.f; MaximumFrequency = 100.f; break; case EAudioSpectrumBandPresetType::SnareDrum: MinimumFrequency = 150.f; MaximumFrequency = 4500.f; break; case EAudioSpectrumBandPresetType::Voice: MinimumFrequency = 300.f; MaximumFrequency = 3000.f; break; case EAudioSpectrumBandPresetType::Cymbals: MinimumFrequency = 6000.f; MaximumFrequency = 16000.f; break; // More presets can be added. The possibilities are endless. } return MakeFullSpectrumSpectralAnalysisBandSettings(InNumBands, MinimumFrequency, MaximumFrequency, InAttackTimeMsec, InReleaseTimeMsec); } void UAudioMixerBlueprintLibrary::GetMagnitudeForFrequencies(const UObject* WorldContextObject, const TArray& Frequencies, TArray& Magnitudes, USoundSubmix* SubmixToAnalyze /*= nullptr*/) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->GetMagnitudesForFrequencies(SubmixToAnalyze, Frequencies, Magnitudes); } else { UE_LOG(LogAudioMixer, Error, TEXT("Getting magnitude for frequencies is an audio mixer only feature.")); } } void UAudioMixerBlueprintLibrary::GetPhaseForFrequencies(const UObject* WorldContextObject, const TArray& Frequencies, TArray& Phases, USoundSubmix* SubmixToAnalyze /*= nullptr*/) { if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { MixerDevice->GetPhasesForFrequencies(SubmixToAnalyze, Frequencies, Phases); } else { UE_LOG(LogAudioMixer, Error, TEXT("Output recording is an audio mixer only feature.")); } } void UAudioMixerBlueprintLibrary::AddSourceEffectToPresetChain(const UObject* WorldContextObject, USoundEffectSourcePresetChain* PresetChain, FSourceEffectChainEntry Entry) { if (!PresetChain) { UE_LOG(LogAudioMixer, Warning, TEXT("AddSourceEffectToPresetChain was passed invalid preset chain")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { TArray Chain; uint32 PresetChainId = PresetChain->GetUniqueID(); if (!MixerDevice->GetCurrentSourceEffectChain(PresetChainId, Chain)) { Chain = PresetChain->Chain; } Chain.Add(Entry); MixerDevice->UpdateSourceEffectChain(PresetChainId, Chain, PresetChain->bPlayEffectChainTails); } } void UAudioMixerBlueprintLibrary::RemoveSourceEffectFromPresetChain(const UObject* WorldContextObject, USoundEffectSourcePresetChain* PresetChain, int32 EntryIndex) { if (!PresetChain) { UE_LOG(LogAudioMixer, Warning, TEXT("RemoveSourceEffectFromPresetChain was passed invalid preset chain")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { TArray Chain; uint32 PresetChainId = PresetChain->GetUniqueID(); if (!MixerDevice->GetCurrentSourceEffectChain(PresetChainId, Chain)) { Chain = PresetChain->Chain; } if (EntryIndex >= 0 && EntryIndex < Chain.Num()) { Chain.RemoveAt(EntryIndex); MixerDevice->UpdateSourceEffectChain(PresetChainId, Chain, PresetChain->bPlayEffectChainTails); } } } void UAudioMixerBlueprintLibrary::SetBypassSourceEffectChainEntry(const UObject* WorldContextObject, USoundEffectSourcePresetChain* PresetChain, int32 EntryIndex, bool bBypassed) { if (!PresetChain) { UE_LOG(LogAudioMixer, Warning, TEXT("SetBypassSourceEffectChainEntry was passed invalid preset chain")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { TArray Chain; uint32 PresetChainId = PresetChain->GetUniqueID(); if (!MixerDevice->GetCurrentSourceEffectChain(PresetChainId, Chain)) { Chain = PresetChain->Chain; } if (EntryIndex >= 0 && EntryIndex < Chain.Num()) { Chain[EntryIndex].bBypass = bBypassed; MixerDevice->UpdateSourceEffectChain(PresetChainId, Chain, PresetChain->bPlayEffectChainTails); } } } int32 UAudioMixerBlueprintLibrary::GetNumberOfEntriesInSourceEffectChain(const UObject* WorldContextObject, USoundEffectSourcePresetChain* PresetChain) { if (!PresetChain) { UE_LOG(LogAudioMixer, Warning, TEXT("GetNumberOfEntriesInSourceEffectChain was passed invalid preset chain")); return 0; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { TArray Chain; uint32 PresetChainId = PresetChain->GetUniqueID(); if (!MixerDevice->GetCurrentSourceEffectChain(PresetChainId, Chain)) { return PresetChain->Chain.Num(); } return Chain.Num(); } return 0; } void UAudioMixerBlueprintLibrary::PrimeSoundForPlayback(USoundWave* SoundWave, const FOnSoundLoadComplete OnLoadCompletion) { if (!SoundWave) { UE_LOG(LogAudioMixer, Warning, TEXT("Prime Sound For Playback called with a null SoundWave pointer.")); } else { IStreamingManager::Get().GetAudioStreamingManager().RequestChunk(SoundWave->CreateSoundWaveProxy(), 1, [OnLoadCompletion, SoundWave](EAudioChunkLoadResult InResult) { AsyncTask(ENamedThreads::GameThread, [OnLoadCompletion, SoundWave, InResult]() { if (InResult == EAudioChunkLoadResult::Completed || InResult == EAudioChunkLoadResult::AlreadyLoaded) { OnLoadCompletion.ExecuteIfBound(SoundWave, false); } else { OnLoadCompletion.ExecuteIfBound(SoundWave, true); } }); }); } } void UAudioMixerBlueprintLibrary::PrimeSoundCueForPlayback(USoundCue* SoundCue) { if (SoundCue) { SoundCue->PrimeSoundCue(); } } float UAudioMixerBlueprintLibrary::TrimAudioCache(float InMegabytesToFree) { uint64 NumBytesToFree = (uint64) (((double)InMegabytesToFree) * 1024.0 * 1024.0); uint64 NumBytesFreed = IStreamingManager::Get().GetAudioStreamingManager().TrimMemory(NumBytesToFree); return (float)(((double) NumBytesFreed / 1024) / 1024.0); } void UAudioMixerBlueprintLibrary::StartAudioBus(const UObject* WorldContextObject, UAudioBus* AudioBus) { if (!AudioBus) { UE_LOG(LogAudioMixer, Error, TEXT("Start Audio Bus called with an invalid Audio Bus.")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { uint32 AudioBusId = AudioBus->GetUniqueID(); int32 NumChannels = (int32)AudioBus->AudioBusChannels + 1; UAudioBusSubsystem* AudioBusSubsystem = MixerDevice->GetSubsystem(); check(AudioBusSubsystem); AudioBusSubsystem->StartAudioBus(Audio::FAudioBusKey(AudioBusId), AudioBus->GetPathName(), NumChannels, false); } else { UE_LOG(LogAudioMixer, Error, TEXT("Audio buses are an audio mixer only feature. Please run the game with audio mixer enabled for this feature.")); } } void UAudioMixerBlueprintLibrary::StopAudioBus(const UObject* WorldContextObject, UAudioBus* AudioBus) { if (!AudioBus) { UE_LOG(LogAudioMixer, Error, TEXT("Stop Audio Bus called with an invalid Audio Bus.")); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { uint32 AudioBusId = AudioBus->GetUniqueID(); UAudioBusSubsystem* AudioBusSubsystem = MixerDevice->GetSubsystem(); check(AudioBusSubsystem); AudioBusSubsystem->StopAudioBus(Audio::FAudioBusKey(AudioBusId)); } else { UE_LOG(LogAudioMixer, Error, TEXT("Audio buses are an audio mixer only feature. Please run the game with audio mixer enabled for this feature.")); } } bool UAudioMixerBlueprintLibrary::IsAudioBusActive(const UObject* WorldContextObject, UAudioBus* AudioBus) { if (!AudioBus) { UE_LOG(LogAudioMixer, Error, TEXT("Is Audio Bus Active called with an invalid Audio Bus.")); return false; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { uint32 AudioBusId = AudioBus->GetUniqueID(); UAudioBusSubsystem* AudioBusSubsystem = MixerDevice->GetSubsystem(); check(AudioBusSubsystem); return AudioBusSubsystem->IsAudioBusActive(Audio::FAudioBusKey(AudioBusId)); } else { UE_LOG(LogAudioMixer, Error, TEXT("Audio buses are an audio mixer only feature. Please run the game with audio mixer enabled for this feature.")); return false; } } void UAudioMixerBlueprintLibrary::RegisterAudioBusToSubmix(const UObject* WorldContextObject, USoundSubmix* SoundSubmix, UAudioBus* AudioBus) { if (!SoundSubmix) { UE_LOG(LogAudioMixer, Error, TEXT("RegisterAudioBusToSubmix called with an invalid Submix.")); return; } if (!AudioBus) { UE_LOG(LogAudioMixer, Error, TEXT("RegisterAudioBusToSubmix called with an invalid Audio Bus.")); return; } if (!IsInAudioThread()) { //Send this over to the audio thread, with the same settings FAudioThread::RunCommandOnAudioThread([WorldContextObject, SoundSubmix, AudioBus]() { RegisterAudioBusToSubmix(WorldContextObject, SoundSubmix, AudioBus); }); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { Audio::FMixerSubmixPtr MixerSubmixPtr = MixerDevice->GetSubmixInstance(SoundSubmix).Pin(); if (MixerSubmixPtr.IsValid()) { UAudioBusSubsystem* AudioBusSubsystem = MixerDevice->GetSubsystem(); check(AudioBusSubsystem); const Audio::FAudioBusKey AudioBusKey = Audio::FAudioBusKey(AudioBus->GetUniqueID()); MixerSubmixPtr->RegisterAudioBus(AudioBusKey, AudioBusSubsystem->AddPatchInputForAudioBus(AudioBusKey, MixerDevice->GetNumOutputFrames(), AudioBus->GetNumChannels())); } else { UE_LOG(LogAudioMixer, Error, TEXT("Submix not found in audio mixer.")); } } else { UE_LOG(LogAudioMixer, Error, TEXT("Audio mixer device not found.")); } } void UAudioMixerBlueprintLibrary::UnregisterAudioBusFromSubmix(const UObject* WorldContextObject, USoundSubmix* SoundSubmix, UAudioBus* AudioBus) { if (!SoundSubmix) { UE_LOG(LogAudioMixer, Error, TEXT("UnregisterAudioBusToSubmix called with an invalid Submix.")); return; } if (!AudioBus) { UE_LOG(LogAudioMixer, Error, TEXT("UnregisterAudioBusToSubmix called with an invalid Audio Bus.")); return; } if (!IsInAudioThread()) { //Send this over to the audio thread, with the same settings FAudioThread::RunCommandOnAudioThread([WorldContextObject, SoundSubmix, AudioBus]() { UnregisterAudioBusFromSubmix(WorldContextObject, SoundSubmix, AudioBus); }); return; } if (Audio::FMixerDevice* MixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject)) { Audio::FMixerSubmixPtr MixerSubmixPtr = MixerDevice->GetSubmixInstance(SoundSubmix).Pin(); if (MixerSubmixPtr.IsValid()) { const Audio::FAudioBusKey AudioBusKey(AudioBus->GetUniqueID()); MixerSubmixPtr->UnregisterAudioBus(AudioBusKey); } else { UE_LOG(LogAudioMixer, Error, TEXT("Submix not found in audio mixer.")); } } else { UE_LOG(LogAudioMixer, Error, TEXT("Audio mixer device not found.")); } } void UAudioMixerBlueprintLibrary::GetAvailableAudioOutputDevices(const UObject* WorldContextObject, const FOnAudioOutputDevicesObtained& OnObtainDevicesEvent) { if (!OnObtainDevicesEvent.IsBound()) { return; } if (!IsInAudioThread()) { //Send this over to the audio thread, with the same settings FAudioThread::RunCommandOnAudioThread([WorldContextObject, OnObtainDevicesEvent]() { GetAvailableAudioOutputDevices(WorldContextObject, OnObtainDevicesEvent); }); return; } TArray OutputDeviceInfos; //The array of audio device info to return //Verifies its safe to access the audio mixer device interface Audio::FMixerDevice* AudioMixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject); if (AudioMixerDevice) { if (Audio::IAudioMixerPlatformInterface* MixerPlatform = AudioMixerDevice->GetAudioMixerPlatform()) { if (Audio::IAudioPlatformDeviceInfoCache* DeviceInfoCache = MixerPlatform->GetDeviceInfoCache()) { TArray AllDevices = DeviceInfoCache->GetAllActiveOutputDevices(); Algo::Transform(AllDevices, OutputDeviceInfos, [](auto& i) -> FAudioOutputDeviceInfo { return { i }; }); } else { uint32 NumOutputDevices = 0; MixerPlatform->GetNumOutputDevices(NumOutputDevices); OutputDeviceInfos.Reserve(NumOutputDevices); FAudioOutputDeviceInfo CurrentOutputDevice = MixerPlatform->GetPlatformDeviceInfo(); for (uint32 i = 0; i < NumOutputDevices; ++i) { Audio::FAudioPlatformDeviceInfo DeviceInfo; MixerPlatform->GetOutputDeviceInfo(i, DeviceInfo); FAudioOutputDeviceInfo NewInfo(DeviceInfo); NewInfo.bIsCurrentDevice = (NewInfo.DeviceId == CurrentOutputDevice.DeviceId); OutputDeviceInfos.Emplace(MoveTemp(NewInfo)); } } } } //Send data through delegate on game thread FAudioThread::RunCommandOnGameThread([OnObtainDevicesEvent, OutputDeviceInfos]() { OnObtainDevicesEvent.ExecuteIfBound(OutputDeviceInfos); }); } void UAudioMixerBlueprintLibrary::GetCurrentAudioOutputDeviceName(const UObject* WorldContextObject, const FOnMainAudioOutputDeviceObtained& OnObtainCurrentDeviceEvent) { if (!OnObtainCurrentDeviceEvent.IsBound()) { return; } if (!IsInAudioThread()) { //Send this over to the audio thread, with the same settings FAudioThread::RunCommandOnAudioThread([WorldContextObject, OnObtainCurrentDeviceEvent]() { GetCurrentAudioOutputDeviceName(WorldContextObject, OnObtainCurrentDeviceEvent); }); return; } TArray toReturn; //The array of audio device info to return //Verifies its safe to access the audio mixer device interface Audio::FMixerDevice* AudioMixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject); if (AudioMixerDevice) { Audio::IAudioMixerPlatformInterface* MixerPlatform = AudioMixerDevice->GetAudioMixerPlatform(); if (MixerPlatform) { //Send data through delegate on game thread FString CurrentDeviceName = MixerPlatform->GetCurrentDeviceName(); FAudioThread::RunCommandOnGameThread([OnObtainCurrentDeviceEvent, CurrentDeviceName]() { OnObtainCurrentDeviceEvent.ExecuteIfBound(CurrentDeviceName); }); } } } void UAudioMixerBlueprintLibrary::SwapAudioOutputDevice(const UObject* WorldContextObject, const FString& NewDeviceId, const FOnCompletedDeviceSwap& OnCompletedDeviceSwap) { if (!OnCompletedDeviceSwap.IsBound()) { return; } if (!IsInAudioThread()) { //Send this over to the audio thread, with the same settings FAudioThread::RunCommandOnAudioThread([WorldContextObject, NewDeviceId, OnCompletedDeviceSwap]() { SwapAudioOutputDevice(WorldContextObject, NewDeviceId, OnCompletedDeviceSwap); }); return; } //Verifies its safe to access the audio mixer device interface Audio::FMixerDevice* AudioMixerDevice = FAudioDeviceManager::GetAudioMixerDeviceFromWorldContext(WorldContextObject); if (AudioMixerDevice) { Audio::IAudioMixerPlatformInterface* MixerPlatform = AudioMixerDevice->GetAudioMixerPlatform(); //Send message to swap device if (MixerPlatform) { bool result = MixerPlatform->RequestDeviceSwap(NewDeviceId, /*force*/ false, TEXT("UAudioMixerBlueprintLibrary::SwapAudioOutputDevice")); FAudioOutputDeviceInfo CurrentOutputDevice = MixerPlatform->GetPlatformDeviceInfo(); //Send data through delegate on game thread FSwapAudioOutputResult SwapResult; SwapResult.CurrentDeviceId = CurrentOutputDevice.DeviceId; SwapResult.RequestedDeviceId = NewDeviceId; SwapResult.Result = result ? ESwapAudioOutputDeviceResultState::Success : ESwapAudioOutputDeviceResultState::Failure; FAudioThread::RunCommandOnGameThread([OnCompletedDeviceSwap, SwapResult]() { OnCompletedDeviceSwap.ExecuteIfBound(SwapResult); }); } } }