What Problem Does This Solve?
LCK needs to capture audio from multiple sources:
- Game audio (Unreal, FMOD, Wwise)
- Microphone input
- Voice chat (Vivox)
ILCKAudioSource provides a unified interface so all audio plugins work the same way. This lets you:
- Mix multiple audio sources together
- Switch audio middleware without changing code
- Create custom audio sources
- Route audio to the encoder
When to Use This
Read this if:
- Building a custom audio source plugin
- Integrating new audio middleware
- Understanding how audio flows through LCK
- Debugging audio capture issues
Skip this if: You’re just using the default audio plugins (UnrealAudio, FMOD, Wwise). They already implement this interface.
Critical: Single Delegate Warning
IMPORTANT: OnAudioDataDelegate is a SINGLE delegate, not multicast.Use BindLambda(), NOT AddLambda().Each bind replaces the previous binding. This is by design—audio data goes to one destination (the encoder).
Correct Usage ✅
// CORRECT: Use BindLambda (replaces any existing binding)
Source->OnAudioDataDelegate.BindLambda([](
TArrayView<const float> PCM,
int32 Channels,
ELCKAudioChannel SourceChannel)
{
// Process audio data
});
Incorrect Usage ❌
// WRONG: AddLambda doesn't exist on single delegates
Source->OnAudioDataDelegate.AddLambda(...); // Compilation error
// WRONG: AddDynamic doesn't exist on single delegates
Source->OnAudioDataDelegate.AddDynamic(...); // Compilation error
Interface Definition
class ILCKAudioSource : public IModularFeature,
public TSharedFromThis<ILCKAudioSource>
{
public:
static FName GetModularFeatureName()
{
return TEXT("LCKAudioSource");
}
// Audio data callback (single delegate - use BindLambda)
FOnRenderAudioDelegate OnAudioDataDelegate;
// Control methods
virtual bool StartCapture() noexcept = 0;
virtual bool StartCapture(TLCKAudioChannelsMask Channels) noexcept = 0;
virtual void StopCapture() noexcept = 0;
// Query methods
virtual float GetVolume() const noexcept = 0;
virtual const FString& GetSourceName() const noexcept = 0;
TLCKAudioChannelsMask GetSupportedChannels() const noexcept;
protected:
TLCKAudioChannelsMask SupportedChannels;
};
| Method | Purpose | When to Call |
|---|
StartCapture() | Begin audio capture (all supported channels) | Before recording |
StartCapture(Channels) | Begin capture for specific channels | Before recording |
StopCapture() | Stop audio capture | After recording |
GetVolume() | Get current audio level (0.0-1.0) | For volume indicators |
GetSourceName() | Get source identifier | Debugging, UI labels |
GetSupportedChannels() | Get channel bitmask | Capability detection |
Audio Delegate Signature
DECLARE_DELEGATE_ThreeParams(
FOnRenderAudioDelegate,
TArrayView<const float>, // PCM samples
int32, // Number of channels
ELCKAudioChannel // Source channel type
);
Parameters Explained
| Parameter | Type | Description |
|---|
| PCM | TArrayView<const float> | Interleaved audio samples, 32-bit float format (-1.0 to 1.0) |
| Channels | int32 | Number of audio channels (1 = mono, 2 = stereo) |
| SourceChannel | ELCKAudioChannel | Audio type: Game, Microphone, or VoiceChat |
| Property | Value |
|---|
| Sample format | 32-bit float |
| Range | -1.0 to 1.0 |
| Layout | Interleaved (e.g., [L, R, L, R, ...] for stereo) |
| Typical channels | 2 (stereo) |
| Sample rate | Source-dependent (usually 48000 Hz) |
Example PCM data (stereo):
float PCM[] = {
0.5f, -0.3f, // Frame 0: Left = 0.5, Right = -0.3
0.2f, 0.1f, // Frame 1: Left = 0.2, Right = 0.1
-0.4f, 0.6f // Frame 2: Left = -0.4, Right = 0.6
};
Supported Channels
Each audio source declares which channels it can capture:
| Source | Game | Microphone | VoiceChat | Notes |
|---|
| LCKUnrealAudio | ✅ | ✅ | ❌ | Built-in Unreal audio |
| LCKFMOD | ✅ | ❌ | ❌ | FMOD Studio game audio |
| LCKWwise | ✅ | ❌ | ❌ | Wwise game audio |
| LCKOboe | ❌ | ✅ | ❌ | Android low-latency mic |
| LCKVivox | ❌ | ✅ | ✅ | Vivox voice chat (mic out, voice in) |
Check supported channels:
TLCKAudioChannelsMask SupportedChannels = Source->GetSupportedChannels();
// Check if source supports microphone
if (EnumHasAnyFlags(SupportedChannels, ELCKAudioChannel::Microphone))
{
UE_LOG(LogLCK, Log, TEXT("Source can capture microphone"));
}
// Check multiple channels
if (EnumHasAllFlags(SupportedChannels,
ELCKAudioChannel::Game | ELCKAudioChannel::Microphone))
{
UE_LOG(LogLCK, Log, TEXT("Source can capture both game audio and mic"));
}
Finding Audio Sources
Audio sources are discovered via Unreal’s modular features system:
// Get all registered audio sources
TArray<ILCKAudioSource*> GetAllAudioSources()
{
return IModularFeatures::Get()
.GetModularFeatureImplementations<ILCKAudioSource>(
ILCKAudioSource::GetModularFeatureName()
);
}
// Find sources that support a specific channel
TArray<ILCKAudioSource*> GetSourcesForChannel(ELCKAudioChannel Channel)
{
TArray<ILCKAudioSource*> Result;
for (ILCKAudioSource* Source : GetAllAudioSources())
{
if (EnumHasAnyFlags(Source->GetSupportedChannels(), Channel))
{
Result.Add(Source);
}
}
return Result;
}
// Example: Find all game audio sources
TArray<ILCKAudioSource*> GameAudioSources = GetSourcesForChannel(ELCKAudioChannel::Game);
for (ILCKAudioSource* Source : GameAudioSources)
{
UE_LOG(LogLCK, Log, TEXT("Game audio source: %s"), *Source->GetSourceName());
}
Audio Mixing with FLCKAudioMix
FLCKAudioMix combines multiple audio sources into a single stereo output:
class FLCKAudioMix
{
public:
void AddSource(TSharedPtr<ILCKAudioSource> Source);
void RemoveSource(TSharedPtr<ILCKAudioSource> Source);
// Get mixed stereo audio for specified channels
TArray<float> StereoMix(TLCKAudioChannelsMask Channels);
private:
TArray<TWeakPtr<ILCKAudioSource>> Sources;
FCriticalSection Mutex;
};
Usage Example
// Create mixer
FLCKAudioMix AudioMix;
// Add game audio source
TSharedPtr<ILCKAudioSource> GameAudio = GetUnrealAudioSource();
AudioMix.AddSource(GameAudio);
// Add microphone source
TSharedPtr<ILCKAudioSource> Microphone = GetMicrophoneSource();
AudioMix.AddSource(Microphone);
// Get mixed audio (Game + Microphone)
TLCKAudioChannelsMask Channels =
ELCKAudioChannel::Game | ELCKAudioChannel::Microphone;
TArray<float> MixedAudio = AudioMix.StereoMix(Channels);
// Pass to encoder
Encoder->EncodeAudio(MixedAudio);
Implementing a Custom Audio Source
Step 1: Create Source Class
class FMyAudioSource : public ILCKAudioSource
{
public:
FMyAudioSource()
{
// Declare which channels this source supports
SupportedChannels = ELCKAudioChannel::Game;
SourceName = TEXT("MyAudioSource");
}
// ILCKAudioSource interface
virtual bool StartCapture() noexcept override
{
return StartCapture(SupportedChannels);
}
virtual bool StartCapture(TLCKAudioChannelsMask Channels) noexcept override
{
// Only start if requested channels are supported
if (!EnumHasAllFlags(SupportedChannels, Channels))
{
UE_LOG(LogLCK, Warning, TEXT("Requested channels not supported"));
return false;
}
// Initialize audio capture
bIsCapturing = true;
// Start your audio callback thread/system here
StartAudioThread();
return true;
}
virtual void StopCapture() noexcept override
{
bIsCapturing = false;
StopAudioThread();
}
virtual float GetVolume() const noexcept override
{
return CurrentVolume;
}
virtual const FString& GetSourceName() const noexcept override
{
return SourceName;
}
private:
FString SourceName;
float CurrentVolume = 0.0f;
bool bIsCapturing = false;
void StartAudioThread() { /* Your implementation */ }
void StopAudioThread() { /* Your implementation */ }
};
Step 2: Fire Audio Delegate
When you have audio data, fire the delegate:
void FMyAudioSource::OnAudioCallback(float* Buffer, int32 NumSamples, int32 NumChannels)
{
if (!bIsCapturing || !OnAudioDataDelegate.IsBound())
{
return;
}
// Calculate volume (RMS)
float Sum = 0.0f;
for (int32 i = 0; i < NumSamples; ++i)
{
Sum += Buffer[i] * Buffer[i];
}
CurrentVolume = FMath::Sqrt(Sum / NumSamples);
// Fire delegate with audio data
TArrayView<const float> PCMData(Buffer, NumSamples);
OnAudioDataDelegate.ExecuteIfBound(
PCMData,
NumChannels,
ELCKAudioChannel::Game
);
}
Step 3: Register as Modular Feature
class FMyAudioModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
// Create audio source
AudioSource = MakeShared<FMyAudioSource>();
// Register with modular features
IModularFeatures::Get().RegisterModularFeature(
ILCKAudioSource::GetModularFeatureName(),
AudioSource.Get()
);
UE_LOG(LogLCK, Log, TEXT("MyAudioSource registered"));
}
virtual void ShutdownModule() override
{
// Unregister
if (AudioSource.IsValid())
{
IModularFeatures::Get().UnregisterModularFeature(
ILCKAudioSource::GetModularFeatureName(),
AudioSource.Get()
);
}
}
private:
TSharedPtr<FMyAudioSource> AudioSource;
};
IMPLEMENT_MODULE(FMyAudioModule, MyAudioPlugin)
Thread Safety
Audio callbacks often come from different threads (audio thread, render thread). If you need to access game thread objects (UObject, UI), use AsyncTask.
Thread-Safe Audio Callback
void FMyAudioSource::OnAudioCallback(float* Buffer, int32 NumSamples)
{
// This may be called from audio thread, render thread, or any thread
// Option 1: Fire delegate immediately (receiver must be thread-safe)
if (OnAudioDataDelegate.IsBound())
{
TArrayView<const float> PCMData(Buffer, NumSamples);
OnAudioDataDelegate.Execute(PCMData, 2, ELCKAudioChannel::Game);
}
// Option 2: Marshal to game thread (safer for UI updates)
TArray<float> AudioCopy(Buffer, NumSamples);
AsyncTask(ENamedThreads::GameThread, [this, AudioCopy = MoveTemp(AudioCopy)]()
{
if (OnAudioDataDelegate.IsBound())
{
OnAudioDataDelegate.Execute(AudioCopy, 2, ELCKAudioChannel::Game);
}
});
}
When to Use Each Approach
| Approach | Use When | Notes |
|---|
| Fire immediately | Encoder is the only listener | LCK encoder is thread-safe |
| Marshal to game thread | Multiple listeners, UI updates | Safer but adds latency |
Complete Example: Custom Microphone Source
class FCustomMicSource : public ILCKAudioSource
{
public:
FCustomMicSource()
{
SupportedChannels = ELCKAudioChannel::Microphone;
SourceName = TEXT("CustomMicrophone");
}
virtual bool StartCapture(TLCKAudioChannelsMask Channels) noexcept override
{
if (!EnumHasAnyFlags(Channels, ELCKAudioChannel::Microphone))
{
return false;
}
// Initialize microphone capture (platform-specific)
#if PLATFORM_WINDOWS
InitializeWASAPI();
#elif PLATFORM_ANDROID
InitializeOboe();
#endif
bIsCapturing = true;
return true;
}
virtual void StopCapture() noexcept override
{
bIsCapturing = false;
ShutdownCapture();
}
virtual float GetVolume() const noexcept override
{
return CurrentVolume;
}
virtual const FString& GetSourceName() const noexcept override
{
return SourceName;
}
private:
FString SourceName;
float CurrentVolume = 0.0f;
bool bIsCapturing = false;
void OnMicrophoneData(float* Buffer, int32 NumSamples)
{
if (!bIsCapturing)
{
return;
}
// Calculate RMS volume
float Sum = 0.0f;
for (int32 i = 0; i < NumSamples; ++i)
{
Sum += Buffer[i] * Buffer[i];
}
CurrentVolume = FMath::Sqrt(Sum / NumSamples);
// Fire delegate
if (OnAudioDataDelegate.IsBound())
{
TArrayView<const float> PCM(Buffer, NumSamples);
OnAudioDataDelegate.Execute(PCM, 1, ELCKAudioChannel::Microphone);
}
}
#if PLATFORM_WINDOWS
void InitializeWASAPI() { /* WASAPI setup */ }
#elif PLATFORM_ANDROID
void InitializeOboe() { /* Oboe setup */ }
#endif
void ShutdownCapture() { /* Cleanup */ }
};
Debugging Audio Sources
List All Sources
void ListAudioSources()
{
TArray<ILCKAudioSource*> Sources = IModularFeatures::Get()
.GetModularFeatureImplementations<ILCKAudioSource>(
ILCKAudioSource::GetModularFeatureName()
);
UE_LOG(LogLCK, Log, TEXT("Found %d audio sources:"), Sources.Num());
for (ILCKAudioSource* Source : Sources)
{
TLCKAudioChannelsMask Channels = Source->GetSupportedChannels();
UE_LOG(LogLCK, Log, TEXT(" - %s"), *Source->GetSourceName());
UE_LOG(LogLCK, Log, TEXT(" Supports Game: %s"),
EnumHasAnyFlags(Channels, ELCKAudioChannel::Game) ? TEXT("Yes") : TEXT("No"));
UE_LOG(LogLCK, Log, TEXT(" Supports Mic: %s"),
EnumHasAnyFlags(Channels, ELCKAudioChannel::Microphone) ? TEXT("Yes") : TEXT("No"));
UE_LOG(LogLCK, Log, TEXT(" Supports VoiceChat: %s"),
EnumHasAnyFlags(Channels, ELCKAudioChannel::VoiceChat) ? TEXT("Yes") : TEXT("No"));
}
}
Monitor Audio Levels
void MonitorAudioLevels()
{
TArray<ILCKAudioSource*> Sources = GetAllAudioSources();
for (ILCKAudioSource* Source : Sources)
{
float Volume = Source->GetVolume();
UE_LOG(LogLCK, Log, TEXT("%s volume: %.2f"),
*Source->GetSourceName(), Volume);
}
}
Key Takeaways
Single delegate — Use BindLambda, not AddLambda
Modular features — Audio sources register at module startup
Channel bitmask — Sources declare what they support (Game, Mic, VoiceChat)
Thread safety — Audio callbacks may come from any thread
Audio format — 32-bit float, interleaved, -1.0 to 1.0 range
FLCKAudioMix — Combines multiple sources into one output