Skip to main content

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;
};
MethodPurposeWhen to Call
StartCapture()Begin audio capture (all supported channels)Before recording
StartCapture(Channels)Begin capture for specific channelsBefore recording
StopCapture()Stop audio captureAfter recording
GetVolume()Get current audio level (0.0-1.0)For volume indicators
GetSourceName()Get source identifierDebugging, UI labels
GetSupportedChannels()Get channel bitmaskCapability detection

Audio Delegate Signature

DECLARE_DELEGATE_ThreeParams(
    FOnRenderAudioDelegate,
    TArrayView<const float>,  // PCM samples
    int32,                    // Number of channels
    ELCKAudioChannel          // Source channel type
);

Parameters Explained

ParameterTypeDescription
PCMTArrayView<const float>Interleaved audio samples, 32-bit float format (-1.0 to 1.0)
Channelsint32Number of audio channels (1 = mono, 2 = stereo)
SourceChannelELCKAudioChannelAudio type: Game, Microphone, or VoiceChat

Audio Data Format

PropertyValue
Sample format32-bit float
Range-1.0 to 1.0
LayoutInterleaved (e.g., [L, R, L, R, ...] for stereo)
Typical channels2 (stereo)
Sample rateSource-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:
SourceGameMicrophoneVoiceChatNotes
LCKUnrealAudioBuilt-in Unreal audio
LCKFMODFMOD Studio game audio
LCKWwiseWwise game audio
LCKOboeAndroid low-latency mic
LCKVivoxVivox 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

ApproachUse WhenNotes
Fire immediatelyEncoder is the only listenerLCK encoder is thread-safe
Marshal to game threadMultiple listeners, UI updatesSafer 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