// Copyright (c), Firelight Technologies Pty, Ltd. 2012-2024.

#include "FMODStudioModule.h"
#include "FMODSettings.h"
#include "FMODAudioComponent.h"
#include "FMODBlueprintStatics.h"
#include "FMODAssetTable.h"
#include "FMODFileCallbacks.h"
#include "FMODUtils.h"
#include "FMODEvent.h"
#include "FMODListener.h"
#include "FMODSnapshotReverb.h"

#include "FMODAudioLinkModule.h"
#if WITH_EDITOR
#include "FMODAudioLinkEditorModule.h"
#endif

#include "Async/Async.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/App.h"
#include "Misc/CommandLine.h"
#include "Misc/CoreDelegates.h"
#include "Engine/Engine.h"
#include "Engine/LocalPlayer.h"
#include "Engine/GameViewportClient.h"
#include "GameFramework/PlayerController.h"
#include "Containers/Ticker.h"
#include "Misc/Paths.h"
#include "Runtime/Media/Public/IMediaClock.h"
#include "Runtime/Media/Public/IMediaClockSink.h"
#include "Runtime/Media/Public/IMediaModule.h"
#include "TimerManager.h"

#include "fmod_studio.hpp"
#include "fmod_errors.h"
#include "FMODStudioPrivatePCH.h"

#include <atomic>

#ifdef FMOD_PLATFORM_HEADER
#include "FMODPlatform.h"
#endif

#if PLATFORM_IOS || PLATFORM_TVOS
#include <AVFoundation/AVAudioSession.h>
#endif

#define LOCTEXT_NAMESPACE "FMODStudio"

DEFINE_LOG_CATEGORY(LogFMOD);

DECLARE_STATS_GROUP(TEXT("FMOD"), STATGROUP_FMOD, STATCAT_Advanced);
DECLARE_FLOAT_COUNTER_STAT(TEXT("FMOD CPU - Mixer"), STAT_FMOD_CPUMixer, STATGROUP_FMOD);
DECLARE_FLOAT_COUNTER_STAT(TEXT("FMOD CPU - Studio"), STAT_FMOD_CPUStudio, STATGROUP_FMOD);
DECLARE_MEMORY_STAT(TEXT("FMOD Memory - Current"), STAT_FMOD_Current_Memory, STATGROUP_FMOD);
DECLARE_MEMORY_STAT(TEXT("FMOD Memory - Max"), STAT_FMOD_Max_Memory, STATGROUP_FMOD);
DECLARE_DWORD_COUNTER_STAT(TEXT("FMOD Channels - Total"), STAT_FMOD_Total_Channels, STATGROUP_FMOD);
DECLARE_DWORD_COUNTER_STAT(TEXT("FMOD Channels - Real"), STAT_FMOD_Real_Channels, STATGROUP_FMOD);

const TCHAR *FMODSystemContextNames[EFMODSystemContext::Max] = {
    TEXT("Auditioning"), TEXT("Runtime"), TEXT("Editor"),
};

void *F_CALL FMODMemoryAlloc(unsigned int size, FMOD_MEMORY_TYPE type, const char *sourcestr)
{
    return FMemory::Malloc(size);
}
void *F_CALL FMODMemoryRealloc(void *ptr, unsigned int size, FMOD_MEMORY_TYPE type, const char *sourcestr)
{
    return FMemory::Realloc(ptr, size);
}
void F_CALL FMODMemoryFree(void *ptr, FMOD_MEMORY_TYPE type, const char *sourcestr)
{
    FMemory::Free(ptr);
}

struct FFMODSnapshotEntry
{
    FFMODSnapshotEntry(UFMODSnapshotReverb *InSnapshot = nullptr, FMOD::Studio::EventInstance *InInstance = nullptr)
        : Snapshot(InSnapshot)
        , Instance(InInstance)
        , StartTime(0.0)
        , FadeDuration(0.0f)
        , FadeIntensityStart(0.0f)
        , FadeIntensityEnd(0.0f)
    {
    }

    float CurrentIntensity() const
    {
        double CurrentTime = FApp::GetCurrentTime();
        if (StartTime + FadeDuration <= CurrentTime)
        {
            return FadeIntensityEnd;
        }
        else
        {
            float Factor = (CurrentTime - StartTime) / FadeDuration;
            return FMath::Lerp<float>(FadeIntensityStart, FadeIntensityEnd, Factor);
        }
    }

    void FadeTo(float Target, float Duration)
    {
        float StartIntensity = CurrentIntensity();

        StartTime = FApp::GetCurrentTime();
        FadeDuration = Duration;
        FadeIntensityStart = StartIntensity;
        FadeIntensityEnd = Target;
    }

    UFMODSnapshotReverb *Snapshot;
    FMOD::Studio::EventInstance *Instance;
    double StartTime;
    float FadeDuration;
    float FadeIntensityStart;
    float FadeIntensityEnd;
};

class FFMODStudioSystemClockSink : public IMediaClockSink
{
public:
    DECLARE_DELEGATE(FUpdateListenerPosition);

    FFMODStudioSystemClockSink(FMOD::Studio::System *SystemIn)
        : System(SystemIn)
        , LastResult(FMOD_OK)
    {
    }

    virtual void TickRender(FTimespan DeltaTime, FTimespan Timecode) override
    {
        if (System)
        {
            if (UpdateListenerPosition.IsBound())
            {
                UpdateListenerPosition.Execute();
            }

            LastResult = System->update();
        }
    }

    void SetUpdateListenerPositionDelegate(FUpdateListenerPosition UpdateListenerPositionIn) { UpdateListenerPosition = UpdateListenerPositionIn; }

    void OnDestroyStudioSystem() { System = nullptr; }

    FMOD::Studio::System *System;
    FMOD_RESULT LastResult;
    FUpdateListenerPosition UpdateListenerPosition;
};

class FFMODStudioModule : public IFMODStudioModule
{
    TUniquePtr<FFMODAudioLinkModule> FMODAudioLinkModule;
#if WITH_EDITOR
    TUniquePtr<FFMODAudioLinkEditorModule> FMODAudioLinkEditorModule;
#endif
public:
    /** IModuleInterface implementation */
    FFMODStudioModule()
        : AuditioningInstance(nullptr)
        , ListenerCount(1)
        , bSimulating(false)
        , bIsInPIE(false)
        , bUseSound(true)
        , bListenerMoved(true)
        , bAllowLiveUpdate(true)
        , LowLevelLibHandle(nullptr)
        , StudioLibHandle(nullptr)
        , bMixerPaused(false)
        , MemPool(nullptr)
    {
        for (int i = 0; i < EFMODSystemContext::Max; ++i)
        {
            StudioSystem[i] = nullptr;
            bBanksLoaded[i] = false;
        }
    }

    void HandleApplicationWillDeactivate()
    {
        AsyncTask(ENamedThreads::GameThread, [&]() { SetSystemPaused(true); });
    }

    void HandleApplicationHasReactivated()
    {
        AsyncTask(ENamedThreads::GameThread, [&]() { SetSystemPaused(false); });
    }

    virtual void StartupModule() override;
    virtual void ShutdownModule() override;

    FString GetDllPath(const TCHAR *ShortName, bool bExplicitPath, bool bUseLibPrefix);
    void *LoadDll(const TCHAR *ShortName);

    bool LoadLibraries();

    void LoadBanks(EFMODSystemContext::Type Type);
    void UnloadBanks(EFMODSystemContext::Type Type);

#if WITH_EDITOR
    FSimpleMulticastDelegate PreEndPIEDelegate;
    FSimpleMulticastDelegate &PreEndPIEEvent() override { return PreEndPIEDelegate; };
    virtual void PreEndPIE() override;
    void ReloadBanks();
    void LoadEditorBanks();
    void UnloadEditorBanks();
#endif

    void CreateStudioSystem(EFMODSystemContext::Type Type);
    void DestroyStudioSystem(EFMODSystemContext::Type Type);

    bool Tick(float DeltaTime);

    void UpdateListeners();
    void UpdateWorldListeners(UWorld *World, int *ListenerIndex);

    virtual FMOD::Studio::System *GetStudioSystem(EFMODSystemContext::Type Context) override;
    virtual FMOD::Studio::EventDescription *GetEventDescription(const UFMODEvent *Event, EFMODSystemContext::Type Type) override;
    virtual FMOD::Studio::EventInstance *CreateAuditioningInstance(const UFMODEvent *Event) override;
    virtual void StopAuditioningInstance() override;

    virtual void SetListenerPosition(int ListenerIndex, UWorld *World, const FTransform &ListenerTransform, float DeltaSeconds) override;
    virtual void FinishSetListenerPosition(int ListenerCount) override;

    virtual const FFMODListener &GetNearestListener(const FVector &Location) override;

    virtual bool HasListenerMoved() override;

    virtual void SetSystemPaused(bool paused) override;

    virtual void SetInPIE(bool bInPIE, bool simulating) override;

    virtual UFMODAsset *FindAssetByName(const FString &Name) override;
    virtual UFMODEvent *FindEventByName(const FString &Name) override;
    virtual FString GetBankPath(const UFMODBank &Bank) override;
    virtual void GetAllBankPaths(TArray<FString> &Paths, bool IncludeMasterBank) const override;

    virtual TArray<FString> GetFailedBankLoads(EFMODSystemContext::Type Context) override { return FailedBankLoads[Context]; }

    virtual TArray<FString> GetRequiredPlugins() override { return RequiredPlugins; }

    virtual void AddRequiredPlugin(const FString &Plugin)
    {
        if (!RequiredPlugins.Contains(Plugin))
        {
            RequiredPlugins.Add(Plugin);
        }
    }

    virtual bool UseSound() override { return bUseSound; }

    virtual bool LoadPlugin(EFMODSystemContext::Type Context, const TCHAR *ShortName) override;

    virtual void LogError(int result, const char *function) override;

    virtual bool AreBanksLoaded() override;

    virtual bool SetLocale(const FString& Locale) override;

    virtual FString GetLocale() override;

    virtual FString GetDefaultLocale() override;

    void ResetInterpolation();

#if PLATFORM_IOS || PLATFORM_TVOS
    void InitializeAudioSession();
#endif

    /** The studio system handle. */
    FMOD::Studio::System *StudioSystem[EFMODSystemContext::Max];
    FMOD::Studio::EventInstance *AuditioningInstance;

    /** The delegate to be invoked when this profiler manager ticks. */
    FTickerDelegate OnTick;

    /** IMediaClockSink wrappers for Studio Systems */
    TSharedPtr<FFMODStudioSystemClockSink, ESPMode::ThreadSafe> ClockSinks[EFMODSystemContext::Max];

    /** Handle for registered TickDelegate. */
    FTSTicker::FDelegateHandle TickDelegateHandle;

    /** Table of assets with name and guid */
    FFMODAssetTable AssetTable;

    /** List of failed bank files */
    TArray<FString> FailedBankLoads[EFMODSystemContext::Max];

    /** List of required plugins we found when loading banks. */
    TArray<FString> RequiredPlugins;

/** Listener information */
#if FMOD_VERSION >= 0x00010600
    static const int MAX_LISTENERS = FMOD_MAX_LISTENERS;
#else
    static const int MAX_LISTENERS = 1;
#endif
    FFMODListener Listeners[MAX_LISTENERS];
    int ListenerCount;

    /** Current snapshot applied via reverb zones*/
    TArray<FFMODSnapshotEntry> ReverbSnapshots;

    /** True if simulating */
    bool bSimulating;

    /** True if in PIE */
    bool bIsInPIE;

    /** True if we want sound enabled */
    bool bUseSound;

    /** True if we the listener has moved and may have changed audio settings*/
    bool bListenerMoved;

    /** True if we allow live update */
    bool bAllowLiveUpdate;

    bool bBanksLoaded[EFMODSystemContext::Max];

    /** Dynamic library */
    FString BaseLibPath;
    void *LowLevelLibHandle;
    void *StudioLibHandle;

    /** True if the mixer has been paused by application deactivation */
    std::atomic<bool> bMixerPaused;

    /** You can also supply a pool of memory for FMOD to work with and it will do so with no extra calls to malloc or free. */
    void *MemPool;

    bool bLoadAllSampleData;
};

IMPLEMENT_MODULE(FFMODStudioModule, FMODStudio)

void FFMODStudioModule::LogError(int result, const char *function)
{
    FString ErrorStr(ANSI_TO_TCHAR(FMOD_ErrorString((FMOD_RESULT)result)));
    FString FunctionStr(ANSI_TO_TCHAR(function));
    UE_LOG(LogFMOD, Error, TEXT("'%s' returned '%s'"), *FunctionStr, *ErrorStr);
}

bool FFMODStudioModule::LoadPlugin(EFMODSystemContext::Type Context, const TCHAR *ShortName)
{
    UE_LOG(LogFMOD, Log, TEXT("Loading plugin '%s'"), ShortName);

    static const int ATTEMPT_COUNT = 2;
    static const TCHAR *AttemptPrefixes[ATTEMPT_COUNT] = {
        TEXT(""),
        TEXT("64")
    };

    FMOD::System *LowLevelSystem = nullptr;
    verifyfmod(StudioSystem[Context]->getCoreSystem(&LowLevelSystem));

    FMOD_RESULT PluginLoadResult;

    for (int useLib = 0; useLib < 2; ++useLib)
    {
        for (int attempt = 0; attempt < 2; ++attempt)
        {
            // Try to load combinations of suffix and lib prefix for relevant platforms
            FString AttemptName = FString(ShortName) + AttemptPrefixes[attempt];
            FString PluginPath = GetDllPath(*AttemptName, true, useLib != 0);

            UE_LOG(LogFMOD, Log, TEXT("Trying to load plugin file at location: %s"), *PluginPath);

            unsigned int Handle = 0;
            PluginLoadResult = LowLevelSystem->loadPlugin(TCHAR_TO_UTF8(*PluginPath), &Handle, 0);
            if (PluginLoadResult == FMOD_OK)
            {
                UE_LOG(LogFMOD, Log, TEXT("Loaded plugin %s"), ShortName);
                return true;
            }
        }
    }
    UE_LOG(LogFMOD, Error, TEXT("Failed to load plugin '%s', sounds may not play"), ShortName);
    return false;
}

void *FFMODStudioModule::LoadDll(const TCHAR *ShortName)
{
    FString LibPath = GetDllPath(ShortName, false, true);

    void *Handle = nullptr;
    UE_LOG(LogFMOD, Log, TEXT("FFMODStudioModule::LoadDll: Loading %s"), *LibPath);
    // Unfortunately Unreal's platform loading code hasn't been implemented on all platforms so we wrap it
#ifdef FMOD_PLATFORM_LOAD_DLL
    Handle = FMODPlatformLoadDll(*LibPath);
#else
    Handle = FPlatformProcess::GetDllHandle(*LibPath);
#endif

#if WITH_EDITOR
    if (!Handle && !FApp::IsUnattended())
    {
        FString Message = TEXT("Couldn't load FMOD DLL ") + LibPath;
        FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *Message, TEXT("Error"));
    }
#endif
    if (!Handle)
    {
        UE_LOG(LogFMOD, Error, TEXT("Failed to load FMOD DLL '%s', FMOD sounds will not play!"), *LibPath);
    }
    return Handle;
}

FString FFMODStudioModule::GetDllPath(const TCHAR *ShortName, bool bExplicitPath, bool bUseLibPrefix)
{
    const TCHAR *LibPrefixName = (bUseLibPrefix ? TEXT("lib") : TEXT(""));
#ifdef FMOD_PLATFORM_HEADER
    return FMODPlatform_GetDllPath(ShortName, bExplicitPath, bUseLibPrefix);
#elif PLATFORM_MAC
    return FString::Printf(TEXT("%s/Mac/%s%s.dylib"), *BaseLibPath, LibPrefixName, ShortName);
#elif PLATFORM_ANDROID
    return FString::Printf(TEXT("%s%s.so"), LibPrefixName, ShortName);
#elif PLATFORM_LINUX
    return FString::Printf(TEXT("%s%s.so"), LibPrefixName, ShortName);
#elif PLATFORM_WINDOWS
    return FString::Printf(TEXT("%s/Win64/%s.dll"), *BaseLibPath, ShortName);
#else
    UE_LOG(LogFMOD, Error, TEXT("Unsupported platform for dynamic libs"));
    return "";
#endif
}

bool FFMODStudioModule::LoadLibraries()
{
#if PLATFORM_IOS || PLATFORM_TVOS || PLATFORM_ANDROID || PLATFORM_LINUX || PLATFORM_MAC || defined(FMOD_DONT_LOAD_LIBRARIES)
    return true; // Nothing to do on those platforms
#else
    UE_LOG(LogFMOD, Verbose, TEXT("FFMODStudioModule::LoadLibraries"));

#if defined(FMODSTUDIO_LINK_DEBUG)
    FString ConfigName = TEXT("D");
#elif defined(FMODSTUDIO_LINK_LOGGING)
    FString ConfigName = TEXT("L");
#elif defined(FMODSTUDIO_LINK_RELEASE)
    FString ConfigName = TEXT("");
#else
#error FMODSTUDIO_LINK not defined
#endif

    FString LowLevelName = FString(TEXT("fmod")) + ConfigName;
    FString StudioName = FString(TEXT("fmodstudio")) + ConfigName;
    LowLevelLibHandle = LoadDll(*LowLevelName);
    StudioLibHandle = LoadDll(*StudioName);
    return (LowLevelLibHandle != nullptr && StudioLibHandle != nullptr);
#endif
}

void FFMODStudioModule::StartupModule()
{
    UE_LOG(LogFMOD, Log, TEXT("FFMODStudioModule startup"));
    BaseLibPath = IPluginManager::Get().FindPlugin(TEXT("FMODStudio"))->GetBaseDir() + TEXT("/Binaries");
    UE_LOG(LogFMOD, Log, TEXT("Lib path = '%s'"), *BaseLibPath);

    if (FParse::Param(FCommandLine::Get(), TEXT("nosound")) || FApp::IsBenchmarking() || IsRunningDedicatedServer() || IsRunningCommandlet())
    {
        bUseSound = false;
        UE_LOG(LogFMOD, Log, TEXT("Disabling FMOD Runtime."));
    }

    if (FParse::Param(FCommandLine::Get(), TEXT("noliveupdate")))
    {
        bAllowLiveUpdate = false;
    }

    if (LoadLibraries())
    {
        verifyfmod(FMOD::Debug_Initialize(FMOD_DEBUG_LEVEL_WARNING, FMOD_DEBUG_MODE_CALLBACK, FMODLogCallback));

        const UFMODSettings &Settings = *GetDefault<UFMODSettings>();
        int32 size = Settings.GetMemoryPoolSize();

        if (!GIsEditor && size > 0)
        {
            MemPool = FMemory::Malloc(size);
            verifyfmod(FMOD::Memory_Initialize(MemPool, size, nullptr, nullptr, nullptr));
        }
        else
        {
            verifyfmod(FMOD::Memory_Initialize(0, 0, FMODMemoryAlloc, FMODMemoryRealloc, FMODMemoryFree));
        }

#if defined(FMOD_PLATFORM_HEADER)
        verifyfmod(FMODPlatformSystemSetup());
#endif

        AcquireFMODFileSystem();

        if (GIsEditor)
        {
            AssetTable.Load();
            AssetTable.SetLocale(GetDefaultLocale());
            CreateStudioSystem(EFMODSystemContext::Auditioning);
            CreateStudioSystem(EFMODSystemContext::Editor);
        }
        else
        {
            SetInPIE(true, false);
        }

        // Load AudioLink module
        bool bFMODAudioLinkEnabled = Settings.bFMODAudioLinkEnabled;
        if (bFMODAudioLinkEnabled)
        {
            UE_LOG(LogFMOD, Log, TEXT("FFMODAudioLinkModule startup"));
            FMODAudioLinkModule = MakeUnique<FFMODAudioLinkModule>();
#if WITH_EDITOR
            UE_LOG(LogFMOD, Log, TEXT("FFMODAudioLinkEditorModule startup"));
            FMODAudioLinkEditorModule = MakeUnique<FFMODAudioLinkEditorModule>();
#endif
        }
    }

    OnTick = FTickerDelegate::CreateRaw(this, &FFMODStudioModule::Tick);
    TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(OnTick);
}

inline FMOD_SPEAKERMODE ConvertSpeakerMode(EFMODSpeakerMode::Type Mode)
{
    switch (Mode)
    {
        case EFMODSpeakerMode::Stereo:
            return FMOD_SPEAKERMODE_STEREO;
        case EFMODSpeakerMode::Surround_5_1:
            return FMOD_SPEAKERMODE_5POINT1;
        case EFMODSpeakerMode::Surround_7_1:
            return FMOD_SPEAKERMODE_7POINT1;
        case EFMODSpeakerMode::Surround_7_1_4:
            return FMOD_SPEAKERMODE_7POINT1POINT4;
        default:
            check(0);
            return FMOD_SPEAKERMODE_DEFAULT;
    };
}

inline FMOD_OUTPUTTYPE ConvertOutputType(EFMODOutput::Type output)
{
    switch (output)
    {
    case EFMODOutput::TYPE_AUTODETECT:
        return FMOD_OUTPUTTYPE_AUTODETECT;
    case EFMODOutput::TYPE_NOSOUND:
        return FMOD_OUTPUTTYPE_NOSOUND;
    case EFMODOutput::TYPE_WASAPI:
        return FMOD_OUTPUTTYPE_WASAPI;
    case EFMODOutput::TYPE_ASIO:
        return FMOD_OUTPUTTYPE_ASIO;
    case EFMODOutput::TYPE_PULSEAUDIO:
        return FMOD_OUTPUTTYPE_PULSEAUDIO;
    case EFMODOutput::TYPE_ALSA:
        return FMOD_OUTPUTTYPE_ALSA;
    case EFMODOutput::TYPE_COREAUDIO:
        return FMOD_OUTPUTTYPE_COREAUDIO;
    case EFMODOutput::TYPE_AUDIOTRACK:
        return FMOD_OUTPUTTYPE_AUDIOTRACK;
    case EFMODOutput::TYPE_OPENSL:
        return FMOD_OUTPUTTYPE_OPENSL;
    case EFMODOutput::TYPE_AUDIOOUT:
        return FMOD_OUTPUTTYPE_AUDIOOUT;
    case EFMODOutput::TYPE_AUDIO3D:
        return FMOD_OUTPUTTYPE_AUDIO3D;
    case EFMODOutput::TYPE_NNAUDIO:
        return FMOD_OUTPUTTYPE_NNAUDIO;
    case EFMODOutput::TYPE_WINSONIC:
        return FMOD_OUTPUTTYPE_WINSONIC;
    case EFMODOutput::TYPE_AAUDIO:
        return FMOD_OUTPUTTYPE_AAUDIO;
    default:
        return FMOD_OUTPUTTYPE_AUTODETECT;
    }
}

void FFMODStudioModule::CreateStudioSystem(EFMODSystemContext::Type Type)
{
    DestroyStudioSystem(Type);
    if (!bUseSound)
    {
        return;
    }

    UE_LOG(LogFMOD, Verbose, TEXT("CreateStudioSystem for context %s"), FMODSystemContextNames[Type]);

    const UFMODSettings &Settings = *GetDefault<UFMODSettings>();
    bLoadAllSampleData = Settings.bLoadAllSampleData;

    FMOD_STUDIO_INITFLAGS StudioInitFlags = FMOD_STUDIO_INIT_NORMAL;
    FMOD_INITFLAGS InitFlags = FMOD_INIT_NORMAL;

#if (defined(FMODSTUDIO_LINK_DEBUG) || defined(FMODSTUDIO_LINK_LOGGING))
    bool liveUpdateEnabledForType = ((Type == EFMODSystemContext::Auditioning) && Settings.bEnableEditorLiveUpdate) ||
                                    ((Type == EFMODSystemContext::Runtime) && Settings.bEnableLiveUpdate);
    if (liveUpdateEnabledForType && bAllowLiveUpdate)
    {
        UE_LOG(LogFMOD, Verbose, TEXT("Enabling live update"));
        StudioInitFlags |= FMOD_STUDIO_INIT_LIVEUPDATE;
    }

    if (Settings.bEnableMemoryTracking && Type == EFMODSystemContext::Runtime)
    {
        StudioInitFlags |= FMOD_STUDIO_INIT_MEMORY_TRACKING;
    }

#endif
    if (Type == EFMODSystemContext::Auditioning || Type == EFMODSystemContext::Editor)
    {
        StudioInitFlags |= FMOD_STUDIO_INIT_ALLOW_MISSING_PLUGINS;
    }

    verifyfmod(FMOD::Studio::System::create(&StudioSystem[Type]));
    FMOD::System *lowLevelSystem = nullptr;
    verifyfmod(StudioSystem[Type]->getCoreSystem(&lowLevelSystem));

    FTCHARToUTF8 WavWriterDestUTF8(*Settings.WavWriterPath);
    void *InitData = nullptr;
    FMOD_OUTPUTTYPE outputType;
    if (Type == EFMODSystemContext::Runtime && Settings.WavWriterPath.Len() > 0)
    {
        UE_LOG(LogFMOD, Log, TEXT("Running with Wav Writer: %s"), *Settings.WavWriterPath);
        outputType = FMOD_OUTPUTTYPE_WAVWRITER;
        InitData = (void *)WavWriterDestUTF8.Get();
    }
    else
    {
        outputType = ConvertOutputType(Settings.GetOutputType());
    }
    verifyfmod(lowLevelSystem->setOutput(outputType));

    int DriverIndex = 0;
    if (!Settings.InitialOutputDriverName.IsEmpty())
    {
        int DriverCount = 0;
        verifyfmod(lowLevelSystem->getNumDrivers(&DriverCount));
        for (int id = 0; id < DriverCount; ++id)
        {
            char DriverNameUTF8[256] = {};
            verifyfmod(lowLevelSystem->getDriverInfo(id, DriverNameUTF8, sizeof(DriverNameUTF8), 0, 0, 0, 0));
            FString DriverName(UTF8_TO_TCHAR(DriverNameUTF8));
            UE_LOG(LogFMOD, Log, TEXT("Driver %d: %s"), id, *DriverName);
            if (DriverName.Contains(Settings.InitialOutputDriverName))
            {
                UE_LOG(LogFMOD, Log, TEXT("Selected driver %d"), id);
                DriverIndex = id;
            }
        }
        verifyfmod(lowLevelSystem->setDriver(DriverIndex));
    }
    int SampleRate = Settings.GetSampleRate();

    if (Settings.bMatchHardwareSampleRate)
    {
        int DefaultSampleRate = 0;
        verifyfmod(lowLevelSystem->getSoftwareFormat(&DefaultSampleRate, 0, 0));
        int SystemSampleRate = 0;
        verifyfmod(lowLevelSystem->getDriverInfo(DriverIndex, nullptr, 0, nullptr, &SystemSampleRate, nullptr, nullptr));
        UE_LOG(LogFMOD, Log, TEXT("Default sample rate = %d"), DefaultSampleRate);
        UE_LOG(LogFMOD, Log, TEXT("System sample rate = %d"), SystemSampleRate);
        if (DefaultSampleRate >= 44100 && DefaultSampleRate <= 48000 && SystemSampleRate >= 44100 && SystemSampleRate <= 48000)
        {
            UE_LOG(LogFMOD, Log, TEXT("Matching system sample rate %d"), SystemSampleRate);
            SampleRate = SystemSampleRate;
        }
    }

    if (outputType == FMOD_OUTPUTTYPE_AUDIO3D || outputType == FMOD_OUTPUTTYPE_AUDIOOUT || outputType == FMOD_OUTPUTTYPE_WINSONIC)
    {
        SampleRate = 48000;
        UE_LOG(LogFMOD, Log, TEXT("Overriding SampleRate to 48KHz for 3D Audio."));
    }

    FMOD_SPEAKERMODE OutputMode = ConvertSpeakerMode(Settings.GetSpeakerMode());

    verifyfmod(lowLevelSystem->setSoftwareFormat(SampleRate, OutputMode, 0));
    verifyfmod(lowLevelSystem->setSoftwareChannels(Settings.GetRealChannelCount()));
    AttachFMODFileSystem(lowLevelSystem, Settings.FileBufferSize);

    if (Settings.DSPBufferLength > 0 && Settings.DSPBufferCount > 0)
    {
        verifyfmod(lowLevelSystem->setDSPBufferSize(Settings.DSPBufferLength, Settings.DSPBufferCount));
    }

    FMOD_ADVANCEDSETTINGS advSettings = { 0 };
    advSettings.cbSize = sizeof(FMOD_ADVANCEDSETTINGS);
    advSettings.vol0virtualvol = Settings.Vol0VirtualLevel;

    TMap<TEnumAsByte<EFMODCodec::Type>, int32> Codecs = Settings.GetCodecs();
    advSettings.maxXMACodecs    = Codecs.Contains(EFMODCodec::XMA)      ? Codecs[EFMODCodec::XMA]       : 0;
    advSettings.maxVorbisCodecs = Codecs.Contains(EFMODCodec::VORBIS)   ? Codecs[EFMODCodec::VORBIS]    : 0;
    advSettings.maxAT9Codecs    = Codecs.Contains(EFMODCodec::AT9)      ? Codecs[EFMODCodec::AT9]       : 0;
    advSettings.maxFADPCMCodecs = Codecs.Contains(EFMODCodec::FADPCM)   ? Codecs[EFMODCodec::FADPCM]    : 0;
    advSettings.maxOpusCodecs   = Codecs.Contains(EFMODCodec::OPUS)     ? Codecs[EFMODCodec::OPUS]      : 0;

    if (Type == EFMODSystemContext::Runtime)
    {
        advSettings.profilePort = Settings.LiveUpdatePort;
    }
    else if (Type == EFMODSystemContext::Auditioning)
    {
        advSettings.profilePort = Settings.EditorLiveUpdatePort;
    }
    advSettings.randomSeed = FMath::Rand();
    verifyfmod(lowLevelSystem->setAdvancedSettings(&advSettings));

    if (Settings.bEnableAPIErrorLogging)
    {
        verifyfmod(lowLevelSystem->setCallback(FMODErrorCallback, FMOD_SYSTEM_CALLBACK_ERROR));
    }

    FMOD_STUDIO_ADVANCEDSETTINGS advStudioSettings = { 0 };
    advStudioSettings.cbsize = sizeof(advStudioSettings);
    advStudioSettings.studioupdateperiod = Settings.StudioUpdatePeriod;

    if (!Settings.StudioBankKey.IsEmpty())
    {
        advStudioSettings.encryptionkey = TCHAR_TO_UTF8(*Settings.StudioBankKey);
    }

    verifyfmod(StudioSystem[Type]->setAdvancedSettings(&advStudioSettings));

    verifyfmod(StudioSystem[Type]->initialize(Settings.TotalChannelCount, StudioInitFlags, InitFlags, InitData));

    for (FString PluginName : Settings.PluginFiles)
    {
        if (!PluginName.IsEmpty())
            LoadPlugin(Type, *PluginName);
    }

    if (Type == EFMODSystemContext::Runtime)
    {
        // Add interrupt callbacks for Mobile
#if PLATFORM_IOS || PLATFORM_TVOS
        InitializeAudioSession();
#endif

        FCoreDelegates::ApplicationWillDeactivateDelegate.AddRaw(this, &FFMODStudioModule::HandleApplicationWillDeactivate);
        FCoreDelegates::ApplicationHasReactivatedDelegate.AddRaw(this, &FFMODStudioModule::HandleApplicationHasReactivated);
    }

    IMediaModule *MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media");

    if (MediaModule != nullptr)
    {
        ClockSinks[Type] = MakeShared<FFMODStudioSystemClockSink, ESPMode::ThreadSafe>(StudioSystem[Type]);

        if (Type == EFMODSystemContext::Runtime)
        {
            ClockSinks[Type]->SetUpdateListenerPositionDelegate(FFMODStudioSystemClockSink::FUpdateListenerPosition::CreateRaw(this, &FFMODStudioModule::UpdateListeners));
        }

        MediaModule->GetClock().AddSink(ClockSinks[Type].ToSharedRef());
    }
}

void FFMODStudioModule::DestroyStudioSystem(EFMODSystemContext::Type Type)
{
    UE_LOG(LogFMOD, Verbose, TEXT("DestroyStudioSystem for context %s"), FMODSystemContextNames[Type]);

    if (ClockSinks[Type].IsValid())
    {
        // Calling through the shared ptr enforces thread safety with the media clock
        ClockSinks[Type]->OnDestroyStudioSystem();

        IMediaModule *MediaModule = FModuleManager::LoadModulePtr<IMediaModule>("Media");

        if (MediaModule != nullptr)
        {
            MediaModule->GetClock().RemoveSink(ClockSinks[Type].ToSharedRef());
        }

        ClockSinks[Type].Reset();
    }

    if (StudioSystem[Type])
    {
        FMOD::Studio::Bus* mBus;
        StudioSystem[Type]->getBus("bus:/", &mBus);
        mBus->setMute(true);
        StudioSystem[Type]->flushCommands();

        verifyfmod(StudioSystem[Type]->release());
        StudioSystem[Type] = nullptr;
    }
}

void FFMODStudioModule::UnloadBanks(EFMODSystemContext::Type Type)
{
    if (StudioSystem[Type])
    {
        verifyfmod(StudioSystem[Type]->unloadAll());
        bBanksLoaded[Type] = false;
    }
}

bool FFMODStudioModule::Tick(float DeltaTime)
{
    if (ClockSinks[EFMODSystemContext::Auditioning].IsValid())
    {
        verifyfmod(ClockSinks[EFMODSystemContext::Auditioning]->LastResult);
    }
    if (ClockSinks[EFMODSystemContext::Runtime].IsValid())
    {
        FMOD_STUDIO_CPU_USAGE Usage = {};
        FMOD_CPU_USAGE UsageCore = {};
        StudioSystem[EFMODSystemContext::Runtime]->getCPUUsage(&Usage, &UsageCore);
        SET_FLOAT_STAT(STAT_FMOD_CPUMixer, UsageCore.dsp);
        SET_FLOAT_STAT(STAT_FMOD_CPUStudio, Usage.update);

        int currentAlloc, maxAlloc;
        FMOD::Memory_GetStats(&currentAlloc, &maxAlloc, false);
        SET_MEMORY_STAT(STAT_FMOD_Current_Memory, currentAlloc);
        SET_MEMORY_STAT(STAT_FMOD_Max_Memory, maxAlloc);

        int channels, realChannels;
        FMOD::System *lowlevel;
        StudioSystem[EFMODSystemContext::Runtime]->getCoreSystem(&lowlevel);
        lowlevel->getChannelsPlaying(&channels, &realChannels);
        SET_DWORD_STAT(STAT_FMOD_Real_Channels, realChannels);
        SET_DWORD_STAT(STAT_FMOD_Total_Channels, channels);

        verifyfmod(ClockSinks[EFMODSystemContext::Runtime]->LastResult);
    }
    if (ClockSinks[EFMODSystemContext::Editor].IsValid())
    {
        verifyfmod(ClockSinks[EFMODSystemContext::Editor]->LastResult);
    }
    return true;
}

void FFMODStudioModule::UpdateListeners()
{
    int ListenerIndex = 0;
    bListenerMoved = false;

#if WITH_EDITOR
    if (bSimulating)
    {
        return;
    }

    if (GEngine)
    {
        // Every PIE session has its own world and local player controller(s), iterate all of them
        for (auto ContextIt = GEngine->GetWorldContexts().CreateConstIterator(); ContextIt; ++ContextIt)
        {
            const FWorldContext &PieContext = *ContextIt;

            // We need to update the listener for all PIE worlds and all standalone game worlds. Since this code is only built WITH_EDITOR
            // EWorldType::Game means a standalone game world in this scope.

            if (PieContext.WorldType == EWorldType::PIE || PieContext.WorldType == EWorldType::Game)
            {
                if (PieContext.GameViewport)
                {
                    UpdateWorldListeners(PieContext.GameViewport->GetWorld(), &ListenerIndex);
                }
            }
        }
    }
#else
    if (GEngine && GEngine->GameViewport)
    {
        UpdateWorldListeners(GEngine->GameViewport->GetWorld(), &ListenerIndex);
    }
#endif

    FinishSetListenerPosition(ListenerIndex);
}

void FFMODStudioModule::UpdateWorldListeners(UWorld *World, int *ListenerIndex)
{
    if (!World)
    {
        return;
    }

    float DeltaSeconds = World->GetDeltaSeconds();

    for (auto Iterator = GEngine->GetLocalPlayerIterator(World); Iterator; ++Iterator)
    {
        ULocalPlayer *LocalPlayer = *Iterator;

        if (LocalPlayer && LocalPlayer->PlayerController)
        {
            APlayerController *PlayerController = LocalPlayer->PlayerController;
            FVector Location;
            FVector ProjFront;
            FVector ProjRight;
            PlayerController->GetAudioListenerPosition(Location, ProjFront, ProjRight);
            FVector ProjUp = FVector::CrossProduct(ProjFront, ProjRight);

            FTransform ListenerTransform(FRotationMatrix::MakeFromXY(ProjFront, ProjRight));
            ListenerTransform.SetTranslation(Location);
            ListenerTransform.NormalizeRotation();

            SetListenerPosition(*ListenerIndex, World, ListenerTransform, DeltaSeconds);

            (*ListenerIndex)++;
        }
    }
}

bool FFMODStudioModule::HasListenerMoved()
{
    return bListenerMoved;
}

void FFMODStudioModule::ResetInterpolation()
{
    for (FFMODListener &Listener : Listeners)
    {
        Listener = FFMODListener();
    }
}

const FFMODListener &FFMODStudioModule::GetNearestListener(const FVector &Location)
{
    float BestDistSq = FLT_MAX;
    int BestListener = 0;
    for (int i = 0; i < ListenerCount; ++i)
    {
        const float DistSq = FVector::DistSquared(Location, Listeners[i].Transform.GetTranslation());
        if (DistSq < BestDistSq)
        {
            BestListener = i;
            BestDistSq = DistSq;
        }
    }
    return Listeners[BestListener];
}

// Partially copied from FAudioDevice::SetListener
void FFMODStudioModule::SetListenerPosition(int ListenerIndex, UWorld *World, const FTransform &ListenerTransform, float DeltaSeconds)
{
    FMOD::Studio::System *System = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
    if (System && ListenerIndex < MAX_LISTENERS)
    {
        // Expand number of listeners dynamically
        if (ListenerIndex >= ListenerCount)
        {
            Listeners[ListenerIndex] = FFMODListener();
            ListenerCount = ListenerIndex + 1;
            verifyfmod(System->setNumListeners(ListenerCount));
        }

        FVector ListenerPos = ListenerTransform.GetTranslation();

        FInteriorSettings *InteriorSettings =
            (FInteriorSettings *)alloca(sizeof(FInteriorSettings)); // FinteriorSetting::FInteriorSettings() isn't exposed (possible UE4 bug???)
        AAudioVolume *Volume = World->GetAudioSettings(ListenerPos, NULL, InteriorSettings);

        Listeners[ListenerIndex].Velocity =
            DeltaSeconds > 0.f ? (ListenerTransform.GetTranslation() - Listeners[ListenerIndex].Transform.GetTranslation()) / DeltaSeconds :
                                 FVector::ZeroVector;

        Listeners[ListenerIndex].Transform = ListenerTransform;

        Listeners[ListenerIndex].ApplyInteriorSettings(Volume, *InteriorSettings);

        // We are using a direct copy of the inbuilt transforms but the directions come out wrong.
        // Several of the audio functions use GetFront() for right, so we do the same here.
        const FVector Up = Listeners[ListenerIndex].GetUp();
        const FVector Right = Listeners[ListenerIndex].GetFront();
        const FVector Forward = Right ^ Up;

        FMOD_3D_ATTRIBUTES Attributes = { { 0 } };
        Attributes.position = FMODUtils::ConvertWorldVector(ListenerPos);
        Attributes.forward = FMODUtils::ConvertUnitVector(Forward);
        Attributes.up = FMODUtils::ConvertUnitVector(Up);
        Attributes.velocity = FMODUtils::ConvertWorldVector(Listeners[ListenerIndex].Velocity);
        verifyfmod(System->setListenerAttributes(ListenerIndex, &Attributes));
        bListenerMoved = true;
    }
}

void FFMODStudioModule::FinishSetListenerPosition(int NumListeners)
{
    FMOD::Studio::System *System = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
    if (!System || NumListeners < 1)
    {
        return;
    }

    if (System && NumListeners < ListenerCount)
    {
        ListenerCount = NumListeners;
        verifyfmod(System->setNumListeners(ListenerCount));
    }

    for (int i = 0; i < ListenerCount; ++i)
    {
        Listeners[i].UpdateCurrentInteriorSettings();
    }

    // Apply a reverb snapshot from the listener position(s)
    TWeakObjectPtr<AAudioVolume> BestVolume = nullptr;
    for (int i = 0; i < ListenerCount; ++i)
    {
        AAudioVolume *CandidateVolume = Listeners[i].Volume;

        if (BestVolume == nullptr || (IsValid(CandidateVolume) && BestVolume.IsValid() && CandidateVolume->GetPriority() > BestVolume->GetPriority()))
        {
            BestVolume = CandidateVolume;
        }
    }
    UFMODSnapshotReverb *NewSnapshot = nullptr;

    if (BestVolume.IsValid() && BestVolume->GetReverbSettings().bApplyReverb)
    {
        NewSnapshot = Cast<UFMODSnapshotReverb>(BestVolume->GetReverbSettings().ReverbEffect);
    }

    if (NewSnapshot != nullptr)
    {
        FString NewSnapshotName = FMODUtils::LookupNameFromGuid(System, NewSnapshot->AssetGuid);
        UE_LOG(LogFMOD, Verbose, TEXT("Starting new snapshot '%s'"), *NewSnapshotName);

        // Try to steal old entry
        FFMODSnapshotEntry SnapshotEntry;
        int SnapshotEntryIndex = -1;
        for (int i = 0; i < ReverbSnapshots.Num(); ++i)
        {
            if (ReverbSnapshots[i].Snapshot == NewSnapshot)
            {
                UE_LOG(LogFMOD, Verbose, TEXT("Re-using old entry with intensity %f"), ReverbSnapshots[i].CurrentIntensity());
                SnapshotEntryIndex = i;
                break;
            }
        }
        // Create new instance
        if (SnapshotEntryIndex == -1)
        {
            UE_LOG(LogFMOD, Verbose, TEXT("Creating new instance"));

            FMOD::Studio::ID Guid = FMODUtils::ConvertGuid(NewSnapshot->AssetGuid);
            FMOD::Studio::EventInstance *NewInstance = nullptr;
            FMOD::Studio::EventDescription *EventDesc = nullptr;
            System->getEventByID(&Guid, &EventDesc);
            if (EventDesc)
            {
                EventDesc->createInstance(&NewInstance);
                if (NewInstance)
                {
                    NewInstance->setParameterByName("Intensity", 0.0f);
                    NewInstance->start();
                }
            }

            SnapshotEntryIndex = ReverbSnapshots.Num();
            ReverbSnapshots.Push(FFMODSnapshotEntry(NewSnapshot, NewInstance));
        }
        // Fade up
        if (ReverbSnapshots[SnapshotEntryIndex].FadeIntensityEnd == 0.0f)
        {
            ReverbSnapshots[SnapshotEntryIndex].FadeTo(BestVolume->GetReverbSettings().Volume, BestVolume->GetReverbSettings().FadeTime);
        }
    }
    // Fade out all other entries
    for (int i = 0; i < ReverbSnapshots.Num(); ++i)
    {
        UE_LOG(LogFMOD, Verbose, TEXT("Ramping intensity (%f,%f) -> %f"), ReverbSnapshots[i].FadeIntensityStart, ReverbSnapshots[i].FadeIntensityEnd,
            ReverbSnapshots[i].CurrentIntensity());
        ReverbSnapshots[i].Instance->setParameterByName("Intensity", 100.0f * ReverbSnapshots[i].CurrentIntensity());

        if (ReverbSnapshots[i].Snapshot != NewSnapshot)
        {
            // Start fading out if needed
            if (ReverbSnapshots[i].FadeIntensityEnd != 0.0f)
            {
                ReverbSnapshots[i].FadeTo(0.0f, ReverbSnapshots[i].FadeDuration);
            }
            // Finish fading out and remove
            else if (ReverbSnapshots[i].CurrentIntensity() == 0.0f)
            {
                UE_LOG(LogFMOD, Verbose, TEXT("Removing snapshot"));

                ReverbSnapshots[i].Instance->stop(FMOD_STUDIO_STOP_ALLOWFADEOUT);
                ReverbSnapshots[i].Instance->release();
                ReverbSnapshots.RemoveAt(i);
                --i; // removed entry, redo current index for next one
            }
        }
    }
}

void FFMODStudioModule::SetInPIE(bool bInPIE, bool simulating)
{
    bIsInPIE = bInPIE;
    bSimulating = simulating;
    bListenerMoved = true;
    ResetInterpolation();

    FMOD_DEBUG_FLAGS flags;

    if (bInPIE)
    {
        if (StudioSystem[EFMODSystemContext::Auditioning])
        {
            // We currently don't tear down auditioning system but we do stop the playing event.
            if (AuditioningInstance)
            {
                AuditioningInstance->stop(FMOD_STUDIO_STOP_IMMEDIATE);
                AuditioningInstance = nullptr;
            }
            // Also make sure banks are finishing loading so they aren't grabbing file handles.
            StudioSystem[EFMODSystemContext::Auditioning]->flushCommands();
        }

        // TODO: Stop sounds for the Editor system? What should happen if the user previews a sequence with transport
        // controls then starts a PIE session? What does happen?

        AssetTable.Load();
        AssetTable.SetLocale(GetDefaultLocale());

        ListenerCount = 1;
        CreateStudioSystem(EFMODSystemContext::Runtime);
        LoadBanks(EFMODSystemContext::Runtime);

        const UFMODSettings &Settings = *GetDefault<UFMODSettings>();
        flags = Settings.LoggingLevel;
    }
    else
    {
        ReverbSnapshots.Reset();
        DestroyStudioSystem(EFMODSystemContext::Runtime);
        flags = FMOD_DEBUG_LEVEL_WARNING;
    }

    verifyfmod(FMOD::Debug_Initialize(flags, FMOD_DEBUG_MODE_CALLBACK, FMODLogCallback));

}

UFMODAsset *FFMODStudioModule::FindAssetByName(const FString &Name)
{
    return AssetTable.GetAssetByStudioPath(Name);
}

UFMODEvent *FFMODStudioModule::FindEventByName(const FString &Name)
{
    UFMODAsset *Asset = FindAssetByName(Name);
    return Cast<UFMODEvent>(Asset);
}

FString FFMODStudioModule::GetBankPath(const UFMODBank &Bank)
{
    FString BankPath = AssetTable.GetBankPath(Bank);

    if (!BankPath.IsEmpty())
    {
        const UFMODSettings &Settings = *GetDefault<UFMODSettings>();
        BankPath = Settings.GetFullBankPath() / BankPath;
    }

    return BankPath;
}

void FFMODStudioModule::GetAllBankPaths(TArray<FString> &Paths, bool IncludeMasterBank) const
{
    AssetTable.GetAllBankPaths(Paths, IncludeMasterBank);
}

void FFMODStudioModule::SetSystemPaused(bool paused)
{
    if (StudioSystem[EFMODSystemContext::Runtime])
    {
        bool expected = !paused;

        if (bMixerPaused.compare_exchange_strong(expected, paused))
        {
            FMOD::System *LowLevelSystem = nullptr;
            verifyfmod(StudioSystem[EFMODSystemContext::Runtime]->getCoreSystem(&LowLevelSystem));

            // Resume mixer before making calls for Android in particular
            if (!paused)
            {
                LowLevelSystem->mixerResume();
            }

            FMOD::ChannelGroup *MasterChannelGroup = nullptr;
            verifyfmod(LowLevelSystem->getMasterChannelGroup(&MasterChannelGroup));
            verifyfmod(MasterChannelGroup->setPaused(paused));

            if (paused)
            {
                LowLevelSystem->mixerSuspend();
            }
        }
    }
}

#if WITH_EDITOR
void FFMODStudioModule::PreEndPIE()
{
    UE_LOG(LogFMOD, Verbose, TEXT("PreEndPIE"));
    if (PreEndPIEDelegate.IsBound())
    {
        PreEndPIEDelegate.Broadcast();
    }
}
#endif

void FFMODStudioModule::ShutdownModule()
{
    UE_LOG(LogFMOD, Verbose, TEXT("FFMODStudioModule shutdown"));

    DestroyStudioSystem(EFMODSystemContext::Auditioning);
    DestroyStudioSystem(EFMODSystemContext::Runtime);
    DestroyStudioSystem(EFMODSystemContext::Editor);

    if (FMODAudioLinkModule)
    {
        FMODAudioLinkModule.Reset();
    }
#if WITH_EDITOR
    if (FMODAudioLinkEditorModule)
    {
        FMODAudioLinkEditorModule.Reset();
    }
#endif

    if (StudioLibHandle && LowLevelLibHandle)
    {
        ReleaseFMODFileSystem();
    }

    if (MemPool)
        FMemory::Free(MemPool);

    if (UObjectInitialized())
    {
        // Unregister tick function.
        FTSTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle);
    }

    UE_LOG(LogFMOD, Verbose, TEXT("FFMODStudioModule unloading dynamic libraries"));
    if (StudioLibHandle)
    {
        FPlatformProcess::FreeDllHandle(StudioLibHandle);
        StudioLibHandle = nullptr;
    }
    if (LowLevelLibHandle)
    {
        FPlatformProcess::FreeDllHandle(LowLevelLibHandle);
        LowLevelLibHandle = nullptr;
    }
    UE_LOG(LogFMOD, Verbose, TEXT("FFMODStudioModule finished unloading"));
}

struct NamedBankEntry
{
    NamedBankEntry()
        : Bank(nullptr)
    {
    }
    NamedBankEntry(const FString &InName, FMOD::Studio::Bank *InBank, FMOD_RESULT InResult)
        : Name(InName)
        , Bank(InBank)
        , Result(InResult)
    {
    }

    FString Name;
    FMOD::Studio::Bank *Bank;
    FMOD_RESULT Result;
};

bool FFMODStudioModule::AreBanksLoaded()
{
    for (int i = 0; i < EFMODSystemContext::Max; ++i)
    {
        if (bBanksLoaded[i])
        {
            return true;
        }
    }
    return false;
}

bool FFMODStudioModule::SetLocale(const FString& LocaleName)
{
    const UFMODSettings &Settings = *GetDefault<UFMODSettings>();

    for (const FFMODProjectLocale& Locale : Settings.Locales)
    {
        if (Locale.LocaleName == LocaleName)
        {
            AssetTable.SetLocale(Locale.LocaleCode);
            return true;
        }
    }

    UE_LOG(LogFMOD, Error, TEXT("No project locale named '%s' has been defined."), *LocaleName);
    return false;
}

FString FFMODStudioModule::GetLocale()
{
    return AssetTable.GetLocale();
}

FString FFMODStudioModule::GetDefaultLocale()
{
    FString LocaleCode = "";
    const UFMODSettings& Settings = *GetDefault<UFMODSettings>();

    if (Settings.Locales.Num() > 0)
    {
        LocaleCode = Settings.Locales[0].LocaleCode;

        for (int32 i = 0; i < Settings.Locales.Num(); ++i)
        {
            if (Settings.Locales[i].bDefault)
            {
                LocaleCode = Settings.Locales[i].LocaleCode;
                break;
            }
        }
    }
    return LocaleCode;
}

void FFMODStudioModule::LoadBanks(EFMODSystemContext::Type Type)
{
    const UFMODSettings &Settings = *GetDefault<UFMODSettings>();

    FailedBankLoads[Type].Reset();
    if (Type == EFMODSystemContext::Auditioning || Type == EFMODSystemContext::Editor)
    {
        RequiredPlugins.Reset();
    }

    if (StudioSystem[Type] != nullptr && Settings.IsBankPathSet())
    {
        UE_LOG(LogFMOD, Verbose, TEXT("LoadBanks for context %s"), FMODSystemContextNames[Type]);

        /*
            Queue up all banks to load asynchronously then wait at the end.
        */
        bool bLoadAllBanks = ((Type == EFMODSystemContext::Auditioning) || (Type == EFMODSystemContext::Editor) || Settings.bLoadAllBanks);
        bool bLoadSampleData = ((Type == EFMODSystemContext::Runtime) && Settings.bLoadAllSampleData);
        bool bLockAllBuses = ((Type == EFMODSystemContext::Runtime) && Settings.bLockAllBuses);
        FMOD_STUDIO_LOAD_BANK_FLAGS BankFlags = (bLockAllBuses ? FMOD_STUDIO_LOAD_BANK_NORMAL : FMOD_STUDIO_LOAD_BANK_NONBLOCKING);
        FMOD_RESULT Result = FMOD_OK;
        TArray<NamedBankEntry> BankEntries;

        // Always load the master bank at startup
        FMOD::Studio::Bank *MasterBank = nullptr;

        if (AssetTable.GetMasterBankPath().IsEmpty())
        {
            FString MasterBankFilename = Settings.GetMasterBankFilename();
            UE_LOG(LogFMOD, Warning, TEXT("Master bank (%s) not found."), *MasterBankFilename);
            FailedBankLoads[Type].Add(FString::Printf(TEXT("Could not find master bank (%s). Check project settings."), *MasterBankFilename));
        }
        else
        {
            FString MasterBankPath = Settings.GetFullBankPath() / AssetTable.GetMasterBankPath();
            UE_LOG(LogFMOD, Verbose, TEXT("Loading master bank: %s"), *MasterBankPath);
            Result = StudioSystem[Type]->loadBankFile(TCHAR_TO_UTF8(*MasterBankPath), BankFlags, &MasterBank);
            BankEntries.Add(NamedBankEntry(MasterBankPath, MasterBank, Result));
        }

        if (Result == FMOD_OK && !AssetTable.GetMasterAssetsBankPath().IsEmpty())
        {
            FMOD::Studio::Bank *MasterAssetsBank = nullptr;
            FString MasterAssetsBankPath = Settings.GetFullBankPath() / AssetTable.GetMasterAssetsBankPath();
            if (FPaths::FileExists(MasterAssetsBankPath))
            {
                Result = StudioSystem[Type]->loadBankFile(TCHAR_TO_UTF8(*MasterAssetsBankPath), BankFlags, &MasterAssetsBank);
                BankEntries.Add(NamedBankEntry(MasterAssetsBankPath, MasterAssetsBank, Result));
            }
        }

        if (Result == FMOD_OK)
        {
            // Auditioning needs string bank to get back full paths from events
            // Runtime could do without it, but if we load it we can look up guids to names which is helpful
            if (MasterBank)
            {
                FString StringsBankPath = Settings.GetFullBankPath() / AssetTable.GetMasterStringsBankPath();
                UE_LOG(LogFMOD, Verbose, TEXT("Loading strings bank: %s"), *StringsBankPath);
                FMOD::Studio::Bank *StringsBank = nullptr;
                Result = StudioSystem[Type]->loadBankFile(TCHAR_TO_UTF8(*StringsBankPath), BankFlags, &StringsBank);
                BankEntries.Add(NamedBankEntry(StringsBankPath, StringsBank, Result));
            }

            // Optionally load all banks in the directory
            if (bLoadAllBanks)
            {
                UE_LOG(LogFMOD, Verbose, TEXT("Loading all banks"));
                TArray<FString> BankFiles;
                AssetTable.GetAllBankPaths(BankFiles, false);
                for (const FString &OtherFile : BankFiles)
                {
                    if (Settings.SkipLoadBankName.Len() && OtherFile.Contains(Settings.SkipLoadBankName))
                    {
                        UE_LOG(LogFMOD, Log, TEXT("Skipping bank: %s"), *OtherFile);
                        continue;
                    }
                    UE_LOG(LogFMOD, Log, TEXT("Loading bank: %s"), *OtherFile);

                    FMOD::Studio::Bank *OtherBank;
                    Result = StudioSystem[Type]->loadBankFile(TCHAR_TO_UTF8(*OtherFile), BankFlags, &OtherBank);
                    BankEntries.Add(NamedBankEntry(OtherFile, OtherBank, Result));
                }
            }

            // Optionally lock all buses to make sure they are created
            if (MasterBank && bLockAllBuses)
            {
                UE_LOG(LogFMOD, Verbose, TEXT("Locking all buses"));
                int BusCount = 0;
                verifyfmod(MasterBank->getBusCount(&BusCount));
                if (BusCount != 0)
                {
                    TArray<FMOD::Studio::Bus *> BusList;
                    BusList.AddZeroed(BusCount);
                    verifyfmod(MasterBank->getBusList(BusList.GetData(), BusCount, &BusCount));
                    BusList.SetNum(BusCount);
                    for (int BusIdx = 0; BusIdx < BusCount; ++BusIdx)
                    {
                        verifyfmod(BusList[BusIdx]->lockChannelGroup());
                    }
                }
            }
        }

        // Wait for all banks to load.
        StudioSystem[Type]->flushCommands();

        for (NamedBankEntry &Entry : BankEntries)
        {
            if (Entry.Result == FMOD_OK)
            {
                FMOD_STUDIO_LOADING_STATE BankLoadingState = FMOD_STUDIO_LOADING_STATE_ERROR;
                Entry.Result = Entry.Bank->getLoadingState(&BankLoadingState);
                if (BankLoadingState == FMOD_STUDIO_LOADING_STATE_ERROR)
                {
                    Entry.Bank->unload();
                    Entry.Bank = nullptr;
                }
                else if (bLoadSampleData)
                {
                    verifyfmod(Entry.Bank->loadSampleData());
                }
            }
            if (Entry.Bank == nullptr || Entry.Result != FMOD_OK)
            {
                FString ErrorMessage;
                if (!FPaths::FileExists(Entry.Name))
                {
                    ErrorMessage = "File does not exist";
                }
                else
                {
                    ErrorMessage = UTF8_TO_TCHAR(FMOD_ErrorString(Entry.Result));
                }
                UE_LOG(LogFMOD, Warning, TEXT("Failed to load bank: %s (%s)"), *Entry.Name, *ErrorMessage);
                FailedBankLoads[Type].Add(FString::Printf(TEXT("%s (%s)"), *FPaths::GetBaseFilename(Entry.Name), *ErrorMessage));
            }
        }
    }

    bBanksLoaded[Type] = true;
}

#if WITH_EDITOR
void FFMODStudioModule::ReloadBanks()
{
    UE_LOG(LogFMOD, Verbose, TEXT("Refreshing auditioning system"));
    bool bReloadAuditioningBanks = 0;
    bool bReloadEditorBanks = 0;
    if (bBanksLoaded[EFMODSystemContext::Auditioning])
    {
        StopAuditioningInstance();
        UnloadBanks(EFMODSystemContext::Auditioning);
        bReloadAuditioningBanks = true;
    }
    if (bBanksLoaded[EFMODSystemContext::Editor])
    {
        UnloadBanks(EFMODSystemContext::Editor);
        bReloadEditorBanks = true;
    }

    AssetTable.Load();

    if (bReloadAuditioningBanks)
    {
        LoadBanks(EFMODSystemContext::Auditioning);
    }
    if (bReloadEditorBanks)
    {
        LoadBanks(EFMODSystemContext::Editor);
    }
}

void FFMODStudioModule::LoadEditorBanks()
{
    LoadBanks(EFMODSystemContext::Editor);
}

void FFMODStudioModule::UnloadEditorBanks()
{
    UnloadBanks(EFMODSystemContext::Editor);
}
#endif

FMOD::Studio::System *FFMODStudioModule::GetStudioSystem(EFMODSystemContext::Type Context)
{
    if (Context == EFMODSystemContext::Max)
    {
        Context = (bIsInPIE ? EFMODSystemContext::Runtime : EFMODSystemContext::Auditioning);
    }
    return StudioSystem[Context];
}

FMOD::Studio::EventDescription *FFMODStudioModule::GetEventDescription(const UFMODEvent *Event, EFMODSystemContext::Type Context)
{
    if (Context == EFMODSystemContext::Max)
    {
        Context = (bIsInPIE ? EFMODSystemContext::Runtime : EFMODSystemContext::Auditioning);
    }
    if (StudioSystem[Context] != nullptr && IsValid(Event) && Event->AssetGuid.IsValid())
    {
        FMOD::Studio::ID Guid = FMODUtils::ConvertGuid(Event->AssetGuid);
        FMOD::Studio::EventDescription *EventDesc = nullptr;
        StudioSystem[Context]->getEventByID(&Guid, &EventDesc);
        return EventDesc;
    }
    return nullptr;
}

FMOD::Studio::EventInstance *FFMODStudioModule::CreateAuditioningInstance(const UFMODEvent *Event)
{
    StopAuditioningInstance();
    if (IsValid(Event))
    {
        FMOD::Studio::EventDescription *EventDesc = GetEventDescription(Event, EFMODSystemContext::Auditioning);
        if (EventDesc)
        {
            FMOD_RESULT Result = EventDesc->createInstance(&AuditioningInstance);
            if (Result == FMOD_OK)
            {
                return AuditioningInstance;
            }
        }
    }
    return nullptr;
}

void FFMODStudioModule::StopAuditioningInstance()
{
    if (AuditioningInstance)
    {
        // Don't bother checking for errors just in case auditioning is already shutting down
        AuditioningInstance->stop(FMOD_STUDIO_STOP_ALLOWFADEOUT);
        AuditioningInstance->release();
        AuditioningInstance = nullptr;
    }
}

#if PLATFORM_IOS || PLATFORM_TVOS
static bool gIsSuspended = false;
static bool gNeedsReset = false;

void FFMODStudioModule::InitializeAudioSession()
{
    [[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionInterruptionNotification object:nil queue:nil usingBlock:^(NSNotification *notification)
    {
        AVAudioSessionInterruptionType type = (AVAudioSessionInterruptionType)[[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
        if (type == AVAudioSessionInterruptionTypeBegan)
        {
            UE_LOG(LogFMOD, Log, TEXT("Interruption Began"));
            // Ignore deprecated warnings regarding AVAudioSessionInterruptionReasonAppWasSuspended and
            // AVAudioSessionInterruptionWasSuspendedKey, we protect usage for the versions where they are available
            #pragma clang diagnostic push
            #pragma clang diagnostic ignored "-Wdeprecated-declarations"
            
            // If the audio session was deactivated while the app was in the background, the app receives the
            // notification when relaunched. Identify this reason for interruption and ignore it.
            if (@available(iOS 16.0, tvOS 14.5, *))
            {
                // Delayed suspend-in-background notifications no longer exist, this must be a real interruption
            }
            #if !PLATFORM_TVOS // tvOS never supported "AVAudioSessionInterruptionReasonAppWasSuspended"
            else if (@available(iOS 14.5, *))
            {
                if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionReasonKey] intValue] == AVAudioSessionInterruptionReasonAppWasSuspended)
                {
                    return; // Ignore delayed suspend-in-background notification
                }
            }
            #endif
            else
            {
                if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionWasSuspendedKey] boolValue])
                {
                    return; // Ignore delayed suspend-in-background notification
                }
            }
            
            SetSystemPaused(true);
            gIsSuspended = true;
            #pragma clang diagnostic pop
        }
        else if (type == AVAudioSessionInterruptionTypeEnded)
        {
            UE_LOG(LogFMOD, Log, TEXT("Interruption Ended"));
            NSError *ActiveError = nil;
            if (![[AVAudioSession sharedInstance] setActive:TRUE error:&ActiveError])
            {
                // Interruption like Siri can prevent session activation, wait for did-become-active notification
                UE_LOG(LogFMOD, Error, TEXT("Failed to set audio session to active = %d [Error = %s]"), TRUE, *FString([ActiveError description]));
                return;
            }
            
            SetSystemPaused(false);
            gIsSuspended = false;
        }
    }];

    [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *notification)
    {
        // If the Media Services were reset while we were in the background we need to reset
        // To reset we need to suspend so that we can resume.
        if (gNeedsReset)
        {
            SetSystemPaused(true);
            gIsSuspended = true;
        }

        NSError *ActiveError = nil;
        if (![[AVAudioSession sharedInstance] setActive:TRUE error:&ActiveError])
        {
            if ([ActiveError code] == AVAudioSessionErrorCodeCannotStartPlaying)
            {
                // Interruption like Screen Time can prevent session activation, but will not trigger an interruption-ended notification.
                // There is no other callback or trigger to hook into after this point, we are not in the background and there is no other audio playing.
                // Our only option is to have a sleep loop until the Audio Session can be activated again.
                while (![[AVAudioSession sharedInstance] setActive:TRUE error:nil])
                {
                    FPlatformProcess::Sleep(0.02f);
                }
            }
            else
            {
                // Interruption like Siri can prevent session activation, wait for interruption-ended notification.
                UE_LOG(LogFMOD, Warning, TEXT("UIApplicationDidBecomeActiveNotification: Failed to set audio session to active = %d [Error = %s]"), TRUE, *FString([ActiveError description]));
                return;
            }
        }

        // It's possible the system missed sending us an interruption end, so recover here
        if (gIsSuspended)
        {
            SetSystemPaused(false);
            gNeedsReset = false;
            gIsSuspended = false;
        }
    }];
    
    [[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionMediaServicesWereResetNotification object:nil queue:nil usingBlock:^(NSNotification *notification)
    {
        if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground || gIsSuspended)
        {
            // Received the reset notification while in the background, need to reset the AudioUnit when we come back to foreground.
            gNeedsReset = true;
        }
        else
        {
            // In the foregound but something chopped the media services, need to do a reset.
            SetSystemPaused(true);
            SetSystemPaused(false);
        }
    }];
}
#endif

#undef LOCTEXT_NAMESPACE
