Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

600 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "VoiceCodecOpus.h"
#include "Voice.h"
#include <CoreAudio/CoreAudio.h>
#include <AudioToolbox/AudioToolbox.h>
#include <AudioUnit/AudioUnit.h>
/** Maximum buffer size for storing raw uncompressed audio from the system */
#define MAX_UNCOMPRESSED_VOICE_BUFFER_SIZE 30 * 1024
/** Number of hardware buffers per uncompressed CoreAudio buffer */
#define NUM_HARDWARE_BUFFERS_PER_UNCOMPRESSED 6
/**
* Implementation of voice capture using CoreAudio
*/
class FVoiceCaptureCoreAudio : public IVoiceCapture
{
public:
FVoiceCaptureCoreAudio()
: StreamComponent(nullptr)
, StreamConverter(nullptr)
, VoiceCaptureState(EVoiceCaptureState::UnInitialized)
, BufferSize(0)
, InputLatency(0)
, ReadableBytes(0)
, WriteBuffer(0)
, ReadBuffer(0)
, ReadOffset(0)
{
}
~FVoiceCaptureCoreAudio()
{
Shutdown();
}
static OSStatus InputProc(void* RefCon,
AudioUnitRenderActionFlags* IOActionFlags,
const AudioTimeStamp* InTimeStamp,
UInt32 InBusNumber,
UInt32 InNumberFrames,
AudioBufferList* IOData)
{
FVoiceCaptureCoreAudio* This = static_cast<FVoiceCaptureCoreAudio*>(RefCon);
if ( This && This->IsCapturing() )
{
AudioBufferList BufferList;
FMemory::Memzero(BufferList);
BufferList.mNumberBuffers = 1;
BufferList.mBuffers[0].mNumberChannels = This->OutputDesc.mChannelsPerFrame;
AudioUnitRenderActionFlags Flags = 0;
if ( AudioUnitRender(This->StreamComponent, &Flags, InTimeStamp, InBusNumber, InNumberFrames, &BufferList) == 0 )
{
if ( This->IsCapturing() && This->ReadBuffer != This->WriteBuffer )
{
AudioBuffer& WriteBuffer = This->BufferList[This->WriteBuffer];
uint32 WritableSize = BufferList.mBuffers[0].mDataByteSize;
uint8 const* InputData = (uint8 const*)BufferList.mBuffers[0].mData;
while ( WritableSize )
{
uint32 MaxWritableSize = FMath::Min(This->BufferSize - WriteBuffer.mDataByteSize, WritableSize);
uint8* WriteData = (uint8*)(WriteBuffer.mData) + WriteBuffer.mDataByteSize;
FMemory::Memcpy(WriteData, InputData, MaxWritableSize);
InputData += MaxWritableSize;
WritableSize -= MaxWritableSize;
WriteBuffer.mDataByteSize += MaxWritableSize;
uint32 ReadIndex = (This->WriteBuffer + 1) % This->BufferList.Num();
if( WriteBuffer.mDataByteSize == This->BufferSize )
{
This->WriteBuffer = ReadIndex;
FPlatformAtomics::InterlockedAdd(&This->ReadableBytes, WriteBuffer.mDataByteSize);
This->VoiceCaptureState = This->VoiceCaptureState == EVoiceCaptureState::NoData ? EVoiceCaptureState::Ok : This->VoiceCaptureState;
if ( This->ReadBuffer == ReadIndex )
{
break; // Clip
}
}
}
}
}
else
{
This->VoiceCaptureState = EVoiceCaptureState::Error;
}
}
return noErr;
}
// IVoiceCapture
virtual bool Init(const FString& DeviceName, int32 InSampleRate, int32 InNumChannels) override
{
check(VoiceCaptureState == EVoiceCaptureState::UnInitialized);
if (InSampleRate < 8000 || InSampleRate > 48000)
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Voice capture doesn't support %d hz"), InSampleRate);
return false;
}
if (InNumChannels < 0 || InNumChannels > 2)
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Voice capture only supports 1 or 2 channels"));
return false;
}
uint32 PropSize = sizeof(AudioDeviceID);
AudioDeviceID InputDevice = 0;
AudioObjectPropertyAddress Address = { kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain };
if ( AudioObjectGetPropertyData(kAudioObjectSystemObject, &Address, 0, nullptr, &PropSize, &InputDevice) != 0 || InputDevice == kAudioDeviceUnknown )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't get Default CoreAudio input device"));
return false;
}
StreamDesc.mSampleRate = InSampleRate;
StreamDesc.mFormatID = kAudioFormatLinearPCM;
StreamDesc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
StreamDesc.mBytesPerPacket = InNumChannels == 1 ? 2 : 4;
StreamDesc.mBytesPerFrame = InNumChannels == 1 ? 2 : 4;
StreamDesc.mFramesPerPacket = 1;
StreamDesc.mBitsPerChannel = 16;
StreamDesc.mChannelsPerFrame = InNumChannels;
StreamDesc.mReserved = 0;
AudioComponentDescription ComponentDesc;
ComponentDesc.componentType = kAudioUnitType_Output;
ComponentDesc.componentSubType = kAudioUnitSubType_HALOutput;
ComponentDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
ComponentDesc.componentFlags = 0;
ComponentDesc.componentFlagsMask = 0;
AudioComponent Component = AudioComponentFindNext(nullptr, & ComponentDesc);
if ( !Component || AudioComponentInstanceNew(Component, &StreamComponent) != 0 || !StreamComponent || AudioUnitInitialize(StreamComponent) != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't get CoreAudio input component"));
return false;
}
uint32 EnableInput = 1;
uint32 EnableOutput = 0;
OSStatus Error = AudioUnitSetProperty(StreamComponent, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &EnableOutput, sizeof(EnableOutput));
if ( Error != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio I/O settings"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
Error = AudioUnitSetProperty(StreamComponent, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &EnableInput, sizeof(EnableInput));
if ( Error != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio I/O settings"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
Error = AudioUnitSetProperty(StreamComponent, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &InputDevice, sizeof(InputDevice));
if ( Error != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio I/O settings"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
AURenderCallbackStruct InputCb;
InputCb.inputProc = &FVoiceCaptureCoreAudio::InputProc;
InputCb.inputProcRefCon = this;
PropSize = sizeof(NativeDesc);
FMemory::Memzero(NativeDesc);
if ( AudioUnitSetProperty(StreamComponent, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &InputCb, sizeof(InputCb)) != 0
|| AudioUnitGetProperty(StreamComponent, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &NativeDesc, &PropSize) != 0)
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio input component"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
OutputDesc = StreamDesc;
OutputDesc.mSampleRate = NativeDesc.mSampleRate;
if ( AudioUnitSetProperty(StreamComponent, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, (void *)&OutputDesc, sizeof(OutputDesc)) != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio input component format"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
// create an audio converter
if ( StreamDesc.mSampleRate != OutputDesc.mSampleRate )
{
if ( AudioConverterNew(&OutputDesc, &StreamDesc, &StreamConverter) != 0 || !StreamConverter )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't configure CoreAudio input format converter"));
AudioComponentInstanceDispose(StreamComponent);
return false;
}
}
PropSize = sizeof(UInt32);
UInt32 SafetyOffset = 0;
Address = { kAudioDevicePropertySafetyOffset, kAudioDevicePropertyScopeInput, 0 };
if ( AudioObjectGetPropertyData(InputDevice, &Address, 0, nullptr, &PropSize, &SafetyOffset) != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't get CoreAudio input latency"));
AudioComponentInstanceDispose(StreamComponent);
if(StreamConverter)
{
AudioConverterDispose(StreamConverter);
StreamConverter = nullptr;
}
return false;
}
SafetyOffset *= StreamDesc.mBytesPerFrame;
PropSize = sizeof(UInt32);
BufferSize = 0;
Address = { kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyScopeInput, 0 };
if ( AudioObjectGetPropertyData(InputDevice, &Address, 0, nullptr, &PropSize, &BufferSize) != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't get CoreAudio input latency"));
AudioComponentInstanceDispose(StreamComponent);
if(StreamConverter)
{
AudioConverterDispose(StreamConverter);
StreamConverter = nullptr;
}
return false;
}
// Make the buffer size sufficiently large to prevent underflow when decoding.
BufferSize *= (StreamDesc.mBytesPerFrame * NUM_HARDWARE_BUFFERS_PER_UNCOMPRESSED);
InputLatency = SafetyOffset + BufferSize;
check(InputLatency < MAX_UNCOMPRESSED_VOICE_BUFFER_SIZE);
uint32 NumBuffers = (uint32)ceilf(MAX_UNCOMPRESSED_VOICE_BUFFER_SIZE / (float)BufferSize);
BufferList.AddZeroed(NumBuffers);
WriteBuffer = 0;
for ( uint32 i = 0; i < BufferList.Num(); i++ )
{
BufferList[i].mData = FMemory::Malloc(BufferSize);
if ( !BufferList[i].mData )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Couldn't allocate CoreAudio Buffer List backing store"));
for ( uint32 j = i; j > 0; j-- )
{
FMemory::Free(BufferList[j-1].mData);
}
if(StreamConverter)
{
AudioConverterDispose(StreamConverter);
StreamConverter = nullptr;
}
AudioComponentInstanceDispose(StreamComponent);
return false;
}
}
VoiceCaptureState = EVoiceCaptureState::NotCapturing;
return true;
}
virtual void Shutdown() override
{
switch (VoiceCaptureState)
{
case EVoiceCaptureState::Ok:
case EVoiceCaptureState::NoData:
case EVoiceCaptureState::Stopping:
case EVoiceCaptureState::BufferTooSmall:
case EVoiceCaptureState::Error:
Stop();
case EVoiceCaptureState::NotCapturing:
check(StreamComponent);
if(StreamConverter)
{
AudioConverterDispose(StreamConverter);
StreamConverter = nullptr;
}
AudioComponentInstanceDispose(StreamComponent);
StreamComponent = nullptr;
for ( uint32 i = 0; i < BufferList.Num(); i++ )
{
FMemory::Free(BufferList[i].mData);
}
VoiceCaptureState = EVoiceCaptureState::UnInitialized;
break;
case EVoiceCaptureState::UnInitialized:
break;
default:
check(false);
break;
}
}
virtual bool Start() override
{
check(StreamComponent && VoiceCaptureState == EVoiceCaptureState::NotCapturing);
check(WriteBuffer == 0 && BufferList.Num());
for ( uint32 i = 0; i < BufferList.Num(); i++ )
{
BufferList[i].mDataByteSize = 0;
FMemory::Memzero(BufferList[i].mData, BufferSize);
}
WriteBuffer = 0;
ReadBuffer = BufferList.Num() - 1;
ReadOffset = 0;
ReadableBytes = 0;
OSStatus Error = AudioOutputUnitStart(StreamComponent);
if ( Error == 0 )
{
VoiceCaptureState = EVoiceCaptureState::Ok;
return true;
}
else
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Failed to start capture %d"), Error);
return false;
}
}
virtual void Stop() override
{
check(IsCapturing());
OSStatus Error = AudioOutputUnitStop(StreamComponent);
if ( Error != 0 )
{
UE_LOG(LogVoiceCapture, Warning, TEXT("Failed to stop capture %d"), Error);
}
VoiceCaptureState = EVoiceCaptureState::NotCapturing;
WriteBuffer = 0;
ReadBuffer = 0;
ReadOffset = 0;
ReadableBytes = 0;
for ( uint32 i = 0; i < BufferList.Num(); i++ )
{
BufferList[i].mDataByteSize = 0;
FMemory::Memzero(BufferList[i].mData, BufferSize);
}
}
virtual bool ChangeDevice(const FString& DeviceName, int32 SampleRate, int32 NumChannels)
{
/** NYI */
return false;
}
virtual bool IsCapturing() override
{
return StreamComponent && VoiceCaptureState > EVoiceCaptureState::NotCapturing;
}
virtual EVoiceCaptureState::Type GetCaptureState(uint32& OutAvailableVoiceData) const override
{
if (VoiceCaptureState != EVoiceCaptureState::UnInitialized &&
VoiceCaptureState != EVoiceCaptureState::NotCapturing)
{
OutAvailableVoiceData = ReadableBytes >= InputLatency ? (ReadableBytes - InputLatency) : 0;
}
else
{
OutAvailableVoiceData = 0;
}
return VoiceCaptureState;
}
virtual EVoiceCaptureState::Type GetVoiceData(uint8* OutVoiceBuffer, uint32 InVoiceBufferSize, uint32& OutAvailableVoiceData) override
{
EVoiceCaptureState::Type State = VoiceCaptureState;
if (VoiceCaptureState != EVoiceCaptureState::UnInitialized &&
VoiceCaptureState != EVoiceCaptureState::NotCapturing)
{
GetCaptureState(OutAvailableVoiceData);
if ( OutAvailableVoiceData > 0 )
{
uint32 ConvertedVoiceData = 0;
if ( InVoiceBufferSize >= OutAvailableVoiceData )
{
check(ReadOffset <= BufferSize);
uint32 BytesToRead = OutAvailableVoiceData;
uint8* Data = OutVoiceBuffer;
while (BytesToRead)
{
uint32 CurrentRead = FMath::Min(FMath::Min(BufferList[ReadBuffer].mDataByteSize - ReadOffset, BytesToRead), BufferSize);
uint32 CurrentWrite = 0;
State = CopyBuffer(ReadBuffer, Data, ReadOffset, &CurrentRead, &CurrentWrite);
if ( State == EVoiceCaptureState::Ok )
{
Data += CurrentWrite;
ConvertedVoiceData += CurrentWrite;
BytesToRead = ( CurrentRead != 0 ) ? BytesToRead - CurrentRead : 0;
ReadOffset += CurrentRead;
if( ReadOffset == BufferList[ReadBuffer].mDataByteSize )
{
BufferList[ReadBuffer].mDataByteSize = 0;
FMemory::Memzero(BufferList[ReadBuffer].mData, BufferSize);
ReadBuffer = (ReadBuffer + 1) % BufferList.Num();
ReadOffset = 0;
}
check(ReadOffset <= BufferSize);
}
else
{
break;
}
}
int32 BytesRead = OutAvailableVoiceData;
FPlatformAtomics::InterlockedAdd(&ReadableBytes, -BytesRead);
OutAvailableVoiceData = ConvertedVoiceData;
}
else
{
State = EVoiceCaptureState::BufferTooSmall;
}
}
else
{
VoiceCaptureState = EVoiceCaptureState::NoData;
State = EVoiceCaptureState::Ok;
}
}
else
{
OutAvailableVoiceData = 0;
}
return State;
}
virtual int32 GetBufferSize() const
{
/** NYI */
return 0;
}
virtual void DumpState() const
{
/** NYI */
UE_LOG(LogVoiceCapture, Display, TEXT("NYI"));
}
private:
struct FAudioFileIO
{
uint32 SrcReadFrames;
AudioBufferList* SrcBuffer;
uint32 SrcBufferSize;
uint32 SrcBytesPerFrame;
};
static OSStatus ConvertInputFormat(AudioConverterRef AudioConverter,
UInt32* IONumberDataPackets,
AudioBufferList* IOData,
AudioStreamPacketDescription** OutDataPacketDescription,
void* InUserData)
{
FAudioFileIO* Input = (FAudioFileIO*)InUserData;
if (Input && Input->SrcBuffer)
{
*IOData = *Input->SrcBuffer;
int32 Num = *IONumberDataPackets;
if ((Input->SrcReadFrames * (Input->SrcBytesPerFrame)) < Input->SrcBufferSize)
{
if ((Num * Input->SrcBytesPerFrame) < IOData->mBuffers[0].mDataByteSize)
{
IOData->mBuffers[0].mDataByteSize = Num * Input->SrcBytesPerFrame;
}
else
{
*IONumberDataPackets = IOData->mBuffers[0].mDataByteSize / Input->SrcBytesPerFrame;
}
Input->SrcReadFrames += *IONumberDataPackets;
}
else
{
*IONumberDataPackets = 0;
IOData->mBuffers[0].mDataByteSize = 0;
return eofErr;
}
}
return noErr;
}
EVoiceCaptureState::Type CopyBuffer(uint8 InReadBuffer, uint8* OutVoiceBuffer, uint32 InReadOffset, uint32* ReadLen, uint32* WriteLen)
{
EVoiceCaptureState::Type State = VoiceCaptureState;
if (*ReadLen)
{
uint8* Data = (uint8*)(BufferList[InReadBuffer].mData) + InReadOffset;
if (StreamDesc.mSampleRate == OutputDesc.mSampleRate)
{
FMemory::Memcpy(OutVoiceBuffer, Data, *ReadLen);
*WriteLen = *ReadLen;
State = EVoiceCaptureState::Ok;
}
else
{
UInt32 FramesToCopy = (*ReadLen / OutputDesc.mBytesPerFrame);
AudioBufferList OutputBuffer;
OutputBuffer.mNumberBuffers = 1;
OutputBuffer.mBuffers[0].mNumberChannels = StreamDesc.mChannelsPerFrame;
OutputBuffer.mBuffers[0].mDataByteSize = *ReadLen;
OutputBuffer.mBuffers[0].mData = OutVoiceBuffer;
AudioBufferList InputBuffer;
InputBuffer.mNumberBuffers = 1;
InputBuffer.mBuffers[0].mNumberChannels = StreamDesc.mChannelsPerFrame;
InputBuffer.mBuffers[0].mDataByteSize = BufferList[InReadBuffer].mDataByteSize;
InputBuffer.mBuffers[0].mData = Data;
FAudioFileIO inUserData;
inUserData.SrcBuffer = &InputBuffer;
inUserData.SrcBytesPerFrame = StreamDesc.mBytesPerFrame;
inUserData.SrcBufferSize = *ReadLen;
inUserData.SrcReadFrames = 0;
OSStatus result = AudioConverterFillComplexBuffer(StreamConverter, &FVoiceCaptureCoreAudio::ConvertInputFormat, &inUserData, &FramesToCopy, &OutputBuffer, nullptr);
if (result == 0 || result == eofErr)
{
State = EVoiceCaptureState::Ok;
*ReadLen = inUserData.SrcReadFrames * StreamDesc.mBytesPerFrame;
*WriteLen = FramesToCopy * OutputDesc.mBytesPerFrame;
}
else
{
State = EVoiceCaptureState::Error;
VoiceCaptureState = EVoiceCaptureState::Error;
}
}
}
return State;
}
private:
/** Descriptor for stream format */
AudioStreamBasicDescription StreamDesc;
/** Descriptor for native HW format */
AudioStreamBasicDescription NativeDesc;
/** Descriptor for HW output format */
AudioStreamBasicDescription OutputDesc;
/** Input AudioUnit component instance */
AudioComponentInstance StreamComponent;
/** Sample rate converter instance */
AudioConverterRef StreamConverter;
/** State of capture device */
EVoiceCaptureState::Type VoiceCaptureState;
/** Outstanding CoreAudio buffer lists */
TArray<AudioBuffer> BufferList;
/** Input device buffer size */
uint32 BufferSize;
/** Input device latency */
uint32 InputLatency;
/** Current readable bytes */
int32 ReadableBytes;
/** Buffer CoreAudio should write to */
uint8 WriteBuffer;
/** Buffer game should read from */
uint8 ReadBuffer;
/** Current offset into ReadBuffer */
uint32 ReadOffset;
};
bool InitVoiceCapture()
{
return true;
}
void ShutdownVoiceCapture()
{
}
IVoiceCapture* CreateVoiceCaptureObject(const FString& DeviceName, int32 SampleRate, int32 NumChannels)
{
IVoiceCapture* Capture = new FVoiceCaptureCoreAudio;
if ( !Capture || !Capture->Init(DeviceName, SampleRate, NumChannels) )
{
delete Capture;
Capture = nullptr;
}
return Capture;
}