Files
UnrealEngine/Engine/Source/Runtime/Apple/AudioMixerAudioUnit/Private/AudioMixerPlatformAudioUnit.cpp
2025-05-18 13:04:45 +08:00

603 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AudioMixerPlatformAudioUnit.h"
#include "AudioMixerPlatformAudioUnitUtils.h"
#include "Modules/ModuleManager.h"
#include "AudioMixer.h"
#include "AudioDevice.h"
#include "AudioMixerDevice.h"
#include "CoreGlobals.h"
#include "CoreMinimal.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/CoreDelegates.h"
#include "Misc/CommandLine.h"
/*
This implementation only depends on the audio units API which allows it to run on MacOS, iOS and tvOS.
For now just assume an iOS configuration (only 2 left and right channels on a single device)
*/
/**
* CoreAudio System Headers
*/
#include <AudioToolbox/AudioToolbox.h>
#include <AudioUnit/AudioUnit.h>
#include <AVFoundation/AVAudioSession.h>
static int32 SuspendCounter = 0;
DECLARE_LOG_CATEGORY_EXTERN(LogAudioMixerAudioUnit, Log, All);
DEFINE_LOG_CATEGORY(LogAudioMixerAudioUnit);
namespace Audio
{
static const int32 DefaultBufferSize = 512;
static const double DefaultSampleRate = 48000.0;
void AUDIOMIXERAUDIOUNIT_API IncrementIOSAudioMixerPlatformSuspendCounter()
{
FMixerPlatformAudioUnit::IncrementSuspendCounter();
}
void AUDIOMIXERAUDIOUNIT_API DecrementIOSAudioMixerPlatformSuspendCounter()
{
FMixerPlatformAudioUnit::DecrementSuspendCounter();
}
FMixerPlatformAudioUnit::FMixerPlatformAudioUnit()
: bSuspended(false)
, bInitialized(false)
, bInCallback(false)
, SubmittedBufferPtr(nullptr)
, RemainingBytesInCurrentSubmittedBuffer(0)
, BytesPerSubmittedBuffer(0)
, GraphSampleRate(DefaultSampleRate)
, NumSamplesPerRenderCallback(0)
, NumSamplesPerDeviceCallback(0)
{
}
FMixerPlatformAudioUnit::~FMixerPlatformAudioUnit()
{
if (bInitialized)
{
TeardownHardware();
}
}
int32 FMixerPlatformAudioUnit::GetNumFrames(const int32 InNumReqestedFrames)
{
return AlignArbitrary(InNumReqestedFrames, 4);
}
bool FMixerPlatformAudioUnit::InitializeHardware()
{
if (bInitialized)
{
return false;
}
bSupportsBackgroundAudio = false;
GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsBackgroundAudio"), bSupportsBackgroundAudio, GEngineIni);
OSStatus Status;
GraphSampleRate = (double) InternalPlatformSettings.SampleRate;
UInt32 BufferSize = (UInt32) GetNumFrames(InternalPlatformSettings.CallbackBufferFrameSize);
const int32 NumChannels = 2;
if (GraphSampleRate == 0)
{
GraphSampleRate = DefaultSampleRate;
}
if (BufferSize == 0)
{
BufferSize = DefaultBufferSize;
}
BytesPerSubmittedBuffer = BufferSize * NumChannels * sizeof(float);
check(BytesPerSubmittedBuffer != 0);
NSError* error;
AVAudioSession* AudioSession = [AVAudioSession sharedInstance];
// this sample rate is currently gotten from AudioSession in GetPlatformSettings, so there should be no issue
bool Success = [AudioSession setPreferredSampleRate:GraphSampleRate error:&error];
if (!Success)
{
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Error setting sample rate."));
}
#if PLATFORM_VISIONOS
Success = [AudioSession setIntendedSpatialExperience:AVAudioSessionSpatialExperienceBypassed options:nil error:&error];
if (!Success)
{
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Error while setting spatial experience to bypass for VisionOS."));
}
#endif
// By calling setPreferredIOBufferDuration, we indicate that we would prefer that the buffer size not change if possible.
float AudioMixerBufferSizeInSec = InternalPlatformSettings.CallbackBufferFrameSize / GraphSampleRate;
Success = [AudioSession setPreferredIOBufferDuration:AudioMixerBufferSizeInSec error: &error];
int32 FinalBufferSize = [AudioSession IOBufferDuration] * GraphSampleRate;
int32 FinalPreferredBufferSize = [AudioSession preferredIOBufferDuration] * GraphSampleRate;
BytesPerSubmittedBuffer = FinalBufferSize * NumChannels * sizeof(float);
check(BytesPerSubmittedBuffer != 0);
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Device Sample Rate: %f"), GraphSampleRate);
check(GraphSampleRate != 0);
Success = [AudioSession setActive:true error:&error];
if (!Success)
{
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Error starting audio session."));
}
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Bytes per submitted buffer: %d"), BytesPerSubmittedBuffer);
// Linear PCM stream format
OutputFormat.mFormatID = kAudioFormatLinearPCM;
OutputFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
OutputFormat.mChannelsPerFrame = 2;
OutputFormat.mBytesPerFrame = sizeof(float) * OutputFormat.mChannelsPerFrame;
OutputFormat.mFramesPerPacket = 1;
OutputFormat.mBytesPerPacket = OutputFormat.mBytesPerFrame * OutputFormat.mFramesPerPacket;
OutputFormat.mBitsPerChannel = 8 * sizeof(float);
OutputFormat.mSampleRate = GraphSampleRate;
Status = NewAUGraph(&AudioUnitGraph);
if (Status != noErr)
{
HandleError(TEXT("Failed to create audio unit graph!"));
return false;
}
AudioComponentDescription UnitDescription;
// Setup audio output unit
UnitDescription.componentType = kAudioUnitType_Output;
//On iOS, we'll use the RemoteIO AudioUnit.
UnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
UnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
UnitDescription.componentFlags = 0;
UnitDescription.componentFlagsMask = 0;
Status = AUGraphAddNode(AudioUnitGraph, &UnitDescription, &OutputNode);
if (Status != noErr)
{
HandleError(TEXT("Failed to initialize audio output node!"), true);
return false;
}
Status = AUGraphOpen(AudioUnitGraph);
if (Status != noErr)
{
HandleError(TEXT("Failed to open audio unit graph"), true);
return false;
}
Status = AUGraphNodeInfo(AudioUnitGraph, OutputNode, nullptr, &OutputUnit);
if (Status != noErr)
{
HandleError(TEXT("Failed to retrieve output unit reference!"), true);
return false;
}
Status = AudioUnitSetProperty(OutputUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&OutputFormat,
sizeof(AudioStreamBasicDescription));
if (Status != noErr)
{
HandleError(TEXT("Failed to set output format!"), true);
return false;
}
AudioStreamInfo.DeviceInfo = GetPlatformDeviceInfo();
AURenderCallbackStruct InputCallback;
InputCallback.inputProc = &AudioRenderCallback;
InputCallback.inputProcRefCon = this;
Status = AUGraphSetNodeInputCallback(AudioUnitGraph,
OutputNode,
0,
&InputCallback);
UE_CLOG(Status != noErr, LogAudioMixerAudioUnit, Error, TEXT("Failed to set input callback for audio output node"));
AudioStreamInfo.StreamState = EAudioOutputStreamState::Closed;
bInitialized = true;
return true;
}
bool FMixerPlatformAudioUnit::CheckAudioDeviceChange()
{
//TODO
return false;
}
bool FMixerPlatformAudioUnit::TeardownHardware()
{
if(!bInitialized)
{
return true;
}
StopAudioStream();
CloseAudioStream();
DisposeAUGraph(AudioUnitGraph);
AudioUnitGraph = nullptr;
OutputNode = -1;
OutputUnit = nullptr;
bInitialized = false;
return true;
}
bool FMixerPlatformAudioUnit::IsInitialized() const
{
return bInitialized;
}
bool FMixerPlatformAudioUnit::GetNumOutputDevices(uint32& OutNumOutputDevices)
{
OutNumOutputDevices = 1;
return true;
}
bool FMixerPlatformAudioUnit::GetOutputDeviceInfo(const uint32 InDeviceIndex, FAudioPlatformDeviceInfo& OutInfo)
{
OutInfo = AudioStreamInfo.DeviceInfo;
return true;
}
bool FMixerPlatformAudioUnit::GetDefaultOutputDeviceIndex(uint32& OutDefaultDeviceIndex) const
{
OutDefaultDeviceIndex = 0;
return true;
}
bool FMixerPlatformAudioUnit::OpenAudioStream(const FAudioMixerOpenStreamParams& Params)
{
if (!bInitialized || AudioStreamInfo.StreamState != EAudioOutputStreamState::Closed)
{
return false;
}
OpenStreamParams = Params;
//todo: AudioStreamInfo.SampleRate = OpenStreamParams.SampleRate;
AudioStreamInfo.Reset();
AudioStreamInfo.OutputDeviceIndex = OpenStreamParams.OutputDeviceIndex;
AudioStreamInfo.NumOutputFrames = OpenStreamParams.NumFrames;
AudioStreamInfo.NumBuffers = OpenStreamParams.NumBuffers;
AudioStreamInfo.AudioMixer = OpenStreamParams.AudioMixer;
AudioStreamInfo.DeviceInfo = GetPlatformDeviceInfo();
// Set up circular buffer between our rendering buffer size and the device's buffer size.
// Since we are only using this circular buffer on a single thread, we do not need to add extra slack.
NumSamplesPerRenderCallback = AudioStreamInfo.NumOutputFrames * AudioStreamInfo.DeviceInfo.NumChannels;
NumSamplesPerDeviceCallback = InternalPlatformSettings.CallbackBufferFrameSize * AudioStreamInfo.DeviceInfo.NumChannels;
// initial circular buffer capacity is zero, so this initializes it.
GrowCircularBufferIfNeeded(NumSamplesPerRenderCallback, NumSamplesPerDeviceCallback);
// Initialize the audio unit graph
OSStatus Status = AUGraphInitialize(AudioUnitGraph);
if (Status != noErr)
{
HandleError(TEXT("Failed to initialize audio graph!"), true);
return false;
}
AudioStreamInfo.StreamState = EAudioOutputStreamState::Open;
FCoreDelegates::ApplicationWillDeactivateDelegate.AddRaw(this, &FMixerPlatformAudioUnit::SuspendContext);
FCoreDelegates::ApplicationHasReactivatedDelegate.AddRaw(this, &FMixerPlatformAudioUnit::ResumeContext);
FCoreDelegates::AudioInterruptionDelegate.AddRaw(this, &FMixerPlatformAudioUnit::InterruptContext);
return true;
}
bool FMixerPlatformAudioUnit::CloseAudioStream()
{
if (!bInitialized || (AudioStreamInfo.StreamState != EAudioOutputStreamState::Open && AudioStreamInfo.StreamState != EAudioOutputStreamState::Stopped))
{
return false;
}
AudioStreamInfo.StreamState = EAudioOutputStreamState::Closed;
FCoreDelegates::AudioInterruptionDelegate.RemoveAll(this);
FCoreDelegates::ApplicationWillDeactivateDelegate.RemoveAll(this);
FCoreDelegates::ApplicationHasReactivatedDelegate.RemoveAll(this);
return true;
}
bool FMixerPlatformAudioUnit::StartAudioStream()
{
if (!bInitialized || (AudioStreamInfo.StreamState != EAudioOutputStreamState::Open && AudioStreamInfo.StreamState != EAudioOutputStreamState::Stopped))
{
return false;
}
BeginGeneratingAudio();
// This will start the render audio callback
OSStatus Status = AUGraphStart(AudioUnitGraph);
if (Status != noErr)
{
HandleError(TEXT("Failed to start audio graph!"), true);
return false;
}
return true;
}
bool FMixerPlatformAudioUnit::StopAudioStream()
{
if(!bInitialized || AudioStreamInfo.StreamState != EAudioOutputStreamState::Running)
{
return false;
}
StopGeneratingAudio();
AUGraphStop(AudioUnitGraph);
return true;
}
bool FMixerPlatformAudioUnit::MoveAudioStreamToNewAudioDevice(const FString& InNewDeviceId)
{
//TODO
return false;
}
FAudioPlatformDeviceInfo FMixerPlatformAudioUnit::GetPlatformDeviceInfo() const
{
FAudioPlatformDeviceInfo DeviceInfo;
AVAudioSession* AudioSession = [AVAudioSession sharedInstance];
double SampleRate = [AudioSession preferredSampleRate];
DeviceInfo.SampleRate = (int32)SampleRate;
DeviceInfo.NumChannels = 2;
DeviceInfo.Format = EAudioMixerStreamDataFormat::Float;
DeviceInfo.OutputChannelArray.SetNum(2);
DeviceInfo.OutputChannelArray[0] = EAudioMixerChannel::FrontLeft;
DeviceInfo.OutputChannelArray[1] = EAudioMixerChannel::FrontRight;
DeviceInfo.bIsSystemDefault = true;
return DeviceInfo;
}
void FMixerPlatformAudioUnit::SubmitBuffer(const uint8* Buffer)
{
if (!Buffer || bIsUsingNullDevice)
{
return;
}
const int32 BytesToSubmitToAudioMixer = NumSamplesPerRenderCallback * sizeof(float);
int32 PushResult = CircularOutputBuffer.Push((const int8*)Buffer, BytesToSubmitToAudioMixer);
check(PushResult == BytesToSubmitToAudioMixer);
}
FString FMixerPlatformAudioUnit::GetDefaultDeviceName()
{
return FString();
}
FAudioPlatformSettings FMixerPlatformAudioUnit::GetPlatformSettings() const
{
InternalPlatformSettings = FAudioPlatformSettings::GetPlatformSettings(FPlatformProperties::GetRuntimeSettingsClassName());
// Check for command line overrides
FString TempString;
// Buffer Size
if(FParse::Value(FCommandLine::Get(), TEXT("-ForceIOSAudioMixerBufferSize="), TempString))
{
InternalPlatformSettings.CallbackBufferFrameSize = FCString::Atoi(*TempString);
}
// NumBuffers
if(FParse::Value(FCommandLine::Get(), TEXT("-ForceIOSAudioMixerNumBuffers="), TempString))
{
InternalPlatformSettings.NumBuffers = FCString::Atoi(*TempString);
}
AVAudioSession* AudioSession = [AVAudioSession sharedInstance];
double PreferredBufferSizeInSec = [AudioSession preferredIOBufferDuration];
double BufferSizeInSec = [AudioSession IOBufferDuration];
double SampleRate = [AudioSession preferredSampleRate];
int32 NumFrames;
if (BufferSizeInSec == 0.0)
{
NumFrames = DefaultBufferSize;
}
else
{
NumFrames = (int32)(SampleRate * BufferSizeInSec);
}
if (FMath::IsNearlyZero(SampleRate))
{
SampleRate = DefaultSampleRate;
}
InternalPlatformSettings.SampleRate = SampleRate;
return InternalPlatformSettings;
}
void FMixerPlatformAudioUnit::IncrementSuspendCounter()
{
if(SuspendCounter == 0)
{
FPlatformAtomics::InterlockedIncrement(&SuspendCounter);
}
}
void FMixerPlatformAudioUnit::DecrementSuspendCounter()
{
if(SuspendCounter > 0)
{
FPlatformAtomics::InterlockedDecrement(&SuspendCounter);
}
}
void FMixerPlatformAudioUnit::ResumeContext()
{
if (SuspendCounter > 0)
{
FPlatformAtomics::InterlockedDecrement(&SuspendCounter);
if (bIsUsingNullDevice)
{
StopRunningNullDevice();
}
if (AudioUnitGraph != NULL)
{
AUGraphStart(AudioUnitGraph);
}
if (OutputUnit != NULL)
{
AudioOutputUnitStart(OutputUnit);
}
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Resuming Audio"));
bSuspended = false;
}
}
void FMixerPlatformAudioUnit::SuspendContext()
{
#if PLATFORM_IOS
if (bSupportsBackgroundAudio)
{
return;
}
#endif
if (SuspendCounter == 0)
{
FPlatformAtomics::InterlockedIncrement(&SuspendCounter);
if(AudioUnitGraph != NULL)
{
AUGraphStop(AudioUnitGraph);
}
if (OutputUnit != NULL)
{
AudioOutputUnitStop(OutputUnit);
}
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Suspending Audio"));
bSuspended = true;
}
}
void FMixerPlatformAudioUnit::HandleError(const TCHAR* InLogOutput, bool bTeardown)
{
UE_LOG(LogAudioMixerAudioUnit, Log, TEXT("%s"), InLogOutput);
if (bTeardown)
{
TeardownHardware();
}
}
void FMixerPlatformAudioUnit::GrowCircularBufferIfNeeded(const int32 InNumSamplesPerRenderCallback, const int32 InNumSamplesPerDeviceCallback)
{
const int32 MaxCircularBufferCapacity = 2 * sizeof(float) * FMath::Max<int32>(NumSamplesPerRenderCallback, NumSamplesPerDeviceCallback);
if (CircularOutputBuffer.GetCapacity() < MaxCircularBufferCapacity)
{
// SetCapacity also zeros-out data
CircularOutputBuffer.SetCapacity(MaxCircularBufferCapacity);
UE_LOG(LogAudioMixerAudioUnit, Display, TEXT("Growing iOS circular buffer to %i bytes."), MaxCircularBufferCapacity);
}
}
void FMixerPlatformAudioUnit::InterruptContext(bool Interrupted)
{
if (Interrupted)
{
StartRunningNullDevice();
}
else
{
StopRunningNullDevice();
}
}
bool FMixerPlatformAudioUnit::PerformCallback(AudioBufferList* OutputBufferData)
{
bInCallback = true;
if (AudioStreamInfo.StreamState == EAudioOutputStreamState::Running)
{
// How many bytes we have left over from previous callback
BytesPerSubmittedBuffer = OutputBufferData->mBuffers[0].mDataByteSize;
uint8* OutputBufferPtr = (uint8*)OutputBufferData->mBuffers[0].mData;
// Check to see if the system has requested a larger callback size
NumSamplesPerDeviceCallback = BytesPerSubmittedBuffer / static_cast<float>(sizeof(float));
GrowCircularBufferIfNeeded(NumSamplesPerRenderCallback, NumSamplesPerDeviceCallback);
while (CircularOutputBuffer.Num() < BytesPerSubmittedBuffer)
{
ReadNextBuffer();
}
int32 PopResult = CircularOutputBuffer.Pop((int8*)OutputBufferPtr, BytesPerSubmittedBuffer);
check(PopResult == BytesPerSubmittedBuffer);
}
else // AudioStreamInfo.StreamState != EAudioOutputStreamState::Running
{
for (uint32 bufferItr = 0; bufferItr < OutputBufferData->mNumberBuffers; ++bufferItr)
{
memset(OutputBufferData->mBuffers[bufferItr].mData, 0, OutputBufferData->mBuffers[bufferItr].mDataByteSize);
}
}
bInCallback = false;
return true;
}
OSStatus FMixerPlatformAudioUnit::AudioRenderCallback(void* RefCon, AudioUnitRenderActionFlags* ActionFlags,
const AudioTimeStamp* TimeStamp, UInt32 BusNumber,
UInt32 NumFrames, AudioBufferList* IOData)
{
// Get the user data and cast to our FMixerPlatformCoreAudio object
FMixerPlatformAudioUnit* me = (FMixerPlatformAudioUnit*) RefCon;
me->PerformCallback(IOData);
return noErr;
}
}