/* FAudioGMS - Game Maker FAudio bindings in C
 *
 * Copyright (c) 2021 Evan Hemsley
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from
 * the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 * claim that you wrote the original software. If you use this software in a
 * product, an acknowledgment in the product documentation would be
 * appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and must not be
 * misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source distribution.
 *
 * Evan "cosmonaut" Hemsley <evan@moonside.games>
 *
 */

#include "SDL.h"

#define DISABLE_XNASONG

#include "FAudioGMS.h"

#include "FAPOBase.h"
#include "FAudioFX.h"
#include "F3DAudio.h"

#include "FAudio.h"

#define DR_WAV_IMPLEMENTATION
#define DRWAV_MALLOC(sz)                   SDL_malloc((sz))
#define DRWAV_REALLOC(p, sz)               SDL_realloc((p), (sz))
#define DRWAV_FREE(p)                      SDL_free((p))
#define DRWAV_COPY_MEMORY(dst, src, sz)    SDL_memcpy((dst), (src), (sz))
#define DRWAV_ZERO_MEMORY(p, sz)           SDL_memset((p), 0, (sz))
#include "../lib/dr_wav.h"

/* stb vorbis defines. this is kind of a mess but oh well */

#include "../lib/FAudio/src/FAudio_internal.h"

#define malloc FAudio_malloc
#define realloc FAudio_realloc
#define free FAudio_free
#ifdef memset /* Thanks, Apple! */
#undef memset
#endif
#define memset SDL_memset
#ifdef memcpy /* Thanks, Apple! */
#undef memcpy
#endif
#define memcpy SDL_memcpy
#define memcmp FAudio_memcmp

#define pow FAudio_pow
#define log(x) FAudio_log(x)
#define sin(x) FAudio_sin(x)
#define cos(x) FAudio_cos(x)
#define floor FAudio_floor
#define abs(x) FAudio_abs(x)
#define ldexp(v, e) FAudio_ldexp((v), (e))
#define exp(x) FAudio_exp(x)

#define qsort FAudio_qsort

#ifdef assert
#undef assert
#endif

#define assert FAudio_assert

#define FILE FAudioIOStream
#ifdef SEEK_SET
#undef SEEK_SET
#endif
#ifdef SEEK_END
#undef SEEK_END
#endif
#ifdef EOF
#undef EOF
#endif
#define SEEK_SET FAUDIO_SEEK_SET
#define SEEK_END FAUDIO_SEEK_END
#define EOF FAUDIO_EOF
#define fopen(path, mode) FAudio_fopen(path)
#define fopen_s(io, path, mode) (!(*io = FAudio_fopen(path)))
#define fclose(io) FAudio_close(io)
#define fread(dst, size, count, io) io->read(io->data, dst, size, count)
#define fseek(io, offset, whence) io->seek(io->data, offset, whence)
#define ftell(io) io->seek(io->data, 0, FAUDIO_SEEK_CUR)

#define STB_VORBIS_NO_PUSHDATA_API 1
#define STB_VORBIS_NO_INTEGER_CONVERSION 1
#include "../lib/FAudio/src/stb_vorbis.h"

#include <stdio.h>

static inline void Log(char *string)
{
    printf("%s\n", string);
    fflush(stdout);
}

typedef struct IdStack
{
    uint32_t* array;
    uint32_t count;
    uint32_t capacity;
} IdStack;

static inline void IdStack_Init(IdStack *stack)
{
    stack->array = NULL;
    stack->capacity = 0;
    stack->count = 0;
}

static inline void IdStack_Push(IdStack *stack, uint32_t id)
{
    if (stack->count >= stack->capacity)
    {
        stack->array = SDL_realloc(stack->array, (stack->capacity + 1) * sizeof(uint32_t));
        stack->capacity += 1;
    }

    stack->array[stack->count] = id;
    stack->count += 1;
}

static inline uint32_t IdStack_Pop(IdStack* stack)
{
    stack->count -= 1;
    return stack->array[stack->count];
}

typedef enum FAudioGMS_SoundState
{
    SoundState_Playing,
    SoundState_Paused,
    SoundState_Stopped
} FAudioGMS_SoundState;

typedef struct FAudioGMS_Voice
{
    FAudioVoice* handle;
    FAudioVoiceSends sends;

    uint8_t effectChainAttached;
    FAudioSubmixVoice* effectVoice;
    FAudioVoiceSends effectSends;
    float effectGain;
} FAudioGMS_Voice;

typedef struct FAudioGMS_StaticSound
{
    uint32_t id;
    FAudioBuffer buffer;
    uint32_t channels;
    uint32_t samplesPerSecond;
    uint32_t lengthInSeconds;
} FAudioGMS_StaticSound;

typedef struct FAudioGMS_StreamingSound
{
    stb_vorbis* fileHandle;
    stb_vorbis_info info;
    float* streamBuffer;
    uint32_t streamBufferSize;
    uint32_t mostRecentBufferOffset; /* used for calculating track position */
	uint8_t isFinalBuffer; /* used to detect end of playback */
} FAudioGMS_StreamingSound;

typedef struct FAudioGMS_SoundInstance FAudioGMS_SoundInstance;

struct FAudioGMS_SoundInstance
{
    uint32_t id;
    FAudioGMS_Voice voice;
    FAudioWaveFormatEx format;
    uint8_t loop; /* bool */
    FAudioGMS_SoundState soundState;
    F3DAUDIO_DSP_SETTINGS dspSettings;

    float pan;
    float pitch;
    float volume;
    float lowPassFilter;
    float highPassFilter;
    float bandPassFilter;

    uint8_t adjustingVolumeOverTime;
    float targetVolume;
    float volumeDelta;

    uint8_t isStatic;
    uint8_t destroyOnFinish;

    uint8_t is3D;
    F3DAUDIO_EMITTER* emitter; /* must not be NULL if is3D */
    float stereoAzimuth[2];

	uint8_t isGlobalPaused;

	uint32_t playBegin;
	uint32_t playLength;

	FAudioGMS_SoundInstance *queuedSoundInstance;

    union
    {
        FAudioGMS_StaticSound *staticSound; /* static sounds are loaded separately, so they do not belong to the instance */
        FAudioGMS_StreamingSound streamingSound;
    } soundData;
};

typedef enum FAudioGMS_EffectType
{
    EffectType_Reverb
} FAudioGMS_EffectType;

typedef union FAudioGMS_EffectParameters
{
    FAudioFXReverbParameters reverbParameters;
} FAudioGMS_EffectParameters;

typedef struct FAudioGMS_EffectChain
{
    uint32_t id;
    uint32_t effectCount;
    FAudioGMS_EffectType *effectTypes; /* length equal to effectCount */
    union FAudioGMS_EffectParameters *effectParameters; /* length equal to effectCount */
} FAudioGMS_EffectChain;

static const float SPEED_OF_SOUND = 343.5f;
static const float DOPPLER_SCALE = 1.0f;

typedef struct FAudioGMS_Device
{
    FAudio* handle;
    F3DAUDIO_HANDLE handle3D;

    FAudioDeviceDetails deviceDetails;
    FAudioMasteringVoice *masteringVoice;

    FAudioGMS_Voice fauxMasteringVoice;

    F3DAUDIO_LISTENER listener;
    float spatialDistanceScale;

    FAudioVoiceCallback voiceCallbacks;

    FAudioGMS_StaticSound **staticSounds;
    uint32_t staticSoundCount;
    IdStack staticSoundIndexStack;

    FAudioGMS_SoundInstance **soundInstances;
    uint32_t soundInstanceCount;
    IdStack soundInstanceIndexStack;

    FAudioGMS_EffectChain** effectChains;
    uint32_t effectChainCount;
    IdStack effectChainIndexStack;

    double timestep;
} FAudioGMS_Device;

static FAudioGMS_Device* device = NULL;

/* Game Maker doesn't let us control execution order on clean up so we have this stupid macro to help us not crash on exit */
#define RETURN_ON_NULL_DEVICE(x) if (device == NULL) { return x; }

static inline FAudioGMS_StaticSound* FAudioGMS_INTERNAL_LookupStaticSound(uint32_t id)
{
    if (id >= 0 && id < device->staticSoundCount)
    {
        return device->staticSounds[id];
    }
    else
    {
        Log("Invalid StaticSound ID!");
        return NULL;
    }
}

static inline FAudioGMS_SoundInstance* FAudioGMS_INTERNAL_LookupSoundInstance(uint32_t id)
{
    if (id >= 0 && id < device->soundInstanceCount)
    {
        return device->soundInstances[id];
    }
    else
    {
        Log("Invalid SoundInstance ID!");
        return NULL;
    }
}

static inline FAudioGMS_EffectChain* FAudioGMS_INTERNAL_LookupEffectChain(uint32_t id)
{
    if (id >= 0 && id < device->effectChainCount)
    {
        return device->effectChains[id];
    }
    else
    {
        Log("Invalid EffectChain ID!");
        return NULL;
    }
}

/* Forward declarations to avoid some annoying BS */

static void FAudioGMS_INTERNAL_SoundInstance_AddBuffer(FAudioGMS_SoundInstance* instance);
static void FAudioGMS_INTERNAL_SoundInstance_Play(FAudioGMS_SoundInstance* instance);
static void FAudioGMS_INTERNAL_SoundInstance_Stop(FAudioGMS_SoundInstance* instance);

static void FAudioGMS_INTERNAL_OnBufferEndCallback(FAudioVoiceCallback* callback, FAudioGMS_SoundInstance *instance)
{
	if (instance->isStatic)
	{
		FAudioGMS_INTERNAL_SoundInstance_Stop(instance);
	}
	else
	{
		if (instance->soundData.streamingSound.isFinalBuffer)
		{
			FAudioGMS_INTERNAL_SoundInstance_Stop(instance);
		}
		else
		{
			FAudioGMS_INTERNAL_SoundInstance_AddBuffer(instance);
		}
	}

	if (instance->soundState == SoundState_Stopped)
	{
		if (instance->queuedSoundInstance != NULL)
		{
			FAudioGMS_INTERNAL_SoundInstance_Play(instance->queuedSoundInstance);
			instance->queuedSoundInstance = NULL;
		}
	}
}

void FAudioGMS_Init(double spatialDistanceScale, double timestep)
{
    device = SDL_malloc(sizeof(FAudioGMS_Device));

    uint32_t result = FAudioCreate(&device->handle, 0, FAUDIO_DEFAULT_PROCESSOR);

    if (result != 0)
    {
        Log("Failed to create device! Bailing!");
        SDL_free(device);
        device = NULL;
        return;
    }

    /* Find a suitable device */

    uint32_t deviceCount;
    FAudio_GetDeviceCount(device->handle, &deviceCount);

    if (deviceCount == 0)
    {
        Log("No audio devices found! Bailing!");
        FAudio_Release(device->handle);
        SDL_free(device);
        device = NULL;
        return;
    }

    FAudioDeviceDetails deviceDetails;

    uint32_t i = 0;
    for (i = 0; i < deviceCount; i += 1)
    {
        FAudio_GetDeviceDetails(
            device->handle,
            i,
            &deviceDetails);

        if ((deviceDetails.Role & FAudioDefaultGameDevice) == FAudioDefaultGameDevice)
        {
            device->deviceDetails = deviceDetails;
            break;
        }
    }

    if (i == deviceCount)
    {
        i = 0; /* whatever we'll just use the first one i guess */
        FAudio_GetDeviceDetails(
            device->handle,
            i,
            &deviceDetails);

        device->deviceDetails = deviceDetails;
    }

    if (FAudio_CreateMasteringVoice(
        device->handle,
        &device->masteringVoice,
        FAUDIO_DEFAULT_CHANNELS,
        FAUDIO_DEFAULT_SAMPLERATE,
        0,
        i,
        NULL
    ) != 0)
    {
        Log("No mastering voice found! Bailing!");
        FAudio_Release(device->handle);
        SDL_free(device);
        device = NULL;
        return;
    }

    device->fauxMasteringVoice.sends.SendCount = 1;
    device->fauxMasteringVoice.sends.pSends = SDL_malloc(sizeof(FAudioSendDescriptor));
    device->fauxMasteringVoice.sends.pSends[0].Flags = 0;
    device->fauxMasteringVoice.sends.pSends[0].pOutputVoice = device->masteringVoice;

    if (FAudio_CreateSubmixVoice(
        device->handle,
        &device->fauxMasteringVoice.handle,
        device->deviceDetails.OutputFormat.Format.nChannels,
        device->deviceDetails.OutputFormat.Format.nSamplesPerSec,
        0,
        0,
        &device->fauxMasteringVoice.sends,
        NULL
    ) != 0)
    {
        Log("Failed to create faux mastering voice! Bailing!");
        FAudioVoice_DestroyVoice(device->masteringVoice);
        FAudio_Release(device->handle);
        SDL_free(device);
        device = NULL;
        return;
    }

    device->fauxMasteringVoice.effectChainAttached = 0;
    device->fauxMasteringVoice.effectGain = 0;
    device->fauxMasteringVoice.effectVoice = NULL;
    device->fauxMasteringVoice.effectSends.SendCount = 0;
    device->fauxMasteringVoice.effectSends.pSends = NULL;

    device->spatialDistanceScale = spatialDistanceScale;

    F3DAudioInitialize(
        device->deviceDetails.OutputFormat.dwChannelMask,
        SPEED_OF_SOUND,
        device->handle3D
    );

    device->listener.OrientFront.x = 0;
    device->listener.OrientFront.y = 0;
    device->listener.OrientFront.z = 1;
    device->listener.OrientTop.x = 0;
    device->listener.OrientTop.y = 1;
    device->listener.OrientTop.z = 0;
    device->listener.Position.x = 0;
    device->listener.Position.y = 0;
    device->listener.Position.z = 0;
    device->listener.Velocity.x = 0;
    device->listener.Velocity.y = 0;
    device->listener.Velocity.z = 0;
    device->listener.pCone = NULL;

    device->voiceCallbacks.OnBufferEnd = FAudioGMS_INTERNAL_OnBufferEndCallback;
    device->voiceCallbacks.OnBufferStart = NULL;
    device->voiceCallbacks.OnLoopEnd = NULL;
    device->voiceCallbacks.OnStreamEnd = NULL;
    device->voiceCallbacks.OnVoiceError = NULL;
    device->voiceCallbacks.OnVoiceProcessingPassEnd = NULL;
    device->voiceCallbacks.OnVoiceProcessingPassStart = NULL;

    device->staticSounds = NULL;
    device->staticSoundCount = 0;
    IdStack_Init(&device->staticSoundIndexStack);

    device->soundInstances = NULL;
    device->soundInstanceCount = 0;
    IdStack_Init(&device->soundInstanceIndexStack);

    device->effectChains = NULL;
    device->effectChainCount = 0;
    IdStack_Init(&device->effectChainIndexStack);

    device->timestep = timestep;

    Log("FAudio initialized successfully!");
    printf("Device: %ls\n", device->deviceDetails.DisplayName);
    fflush(stdout);
}

/* Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs */
static void SetPanMatrixCoefficients(FAudioGMS_SoundInstance *instance)
{
    /* Two major things to notice:
    * 1. The spec assumes any speaker count >= 2 has Front Left/Right.
    * 2. Stereo panning is WAY more complicated than you think.
    *    The main thing is that hard panning does NOT eliminate an
    *    entire channel; the two channels are blended on each side.
    * -flibit
    */

    float* outputMatrix = (float*) instance->dspSettings.pMatrixCoefficients;
    if (instance->dspSettings.SrcChannelCount == 1)
    {
        if (instance->dspSettings.DstChannelCount == 1)
        {
            outputMatrix[0] = 1.0f;
        }
        else
        {
            outputMatrix[0] = (instance->pan > 0.0f) ? (1.0f - instance->pan) : 1.0f;
            outputMatrix[1] = (instance->pan < 0.0f) ? (1.0f  + instance->pan) : 1.0f;
        }
    }
    else
    {
        if (instance->dspSettings.DstChannelCount == 1)
        {
            outputMatrix[0] = 1.0f;
            outputMatrix[1] = 1.0f;
        }
        else
        {
            if (instance->pan <= 0.0f)
            {
                // Left speaker blends left/right channels
                outputMatrix[0] = 0.5f * instance->pan + 1.0f;
                outputMatrix[1] = 0.5f * -instance->pan;
                // Right speaker gets less of the right channel
                outputMatrix[2] = 0.0f;
                outputMatrix[3] = instance->pan + 1.0f;
            }
            else
            {
                // Left speaker gets less of the left channel
                outputMatrix[0] = -instance->pan + 1.0f;
                outputMatrix[1] = 0.0f;
                // Right speaker blends right/left channels
                outputMatrix[2] = 0.5f * instance->pan;
                outputMatrix[3] = 0.5f * -instance->pan + 1.0f;
            }
        }
    }
}

double FAudioGMS_StaticSound_LoadWAV(char *filePath)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    drwav_uint64 frameCount;

    FAudioGMS_StaticSound *sound = SDL_malloc(sizeof(FAudioGMS_StaticSound));
    float *pSampleData = drwav_open_file_and_read_pcm_frames_f32(filePath, &sound->channels, &sound->samplesPerSecond, &frameCount, NULL);
    if (pSampleData == NULL)
    {
        Log("Error opening WAV file: ");
        Log(filePath);
        SDL_free(sound);
        return -1;
    }

    sound->buffer.AudioBytes = (uint32_t)(frameCount * sound->channels * sizeof(float));
    sound->buffer.Flags = 0;
    sound->buffer.LoopBegin = 0;
    sound->buffer.LoopCount = 0;
    sound->buffer.LoopLength = 0;
	sound->buffer.PlayBegin = 0;
    sound->buffer.PlayLength = frameCount;
    sound->buffer.pAudioData = (uint8_t*) pSampleData;
    sound->buffer.pContext = NULL;

    sound->lengthInSeconds = frameCount / sound->samplesPerSecond;

    if (device->staticSoundIndexStack.count > 0)
    {
        sound->id = IdStack_Pop(&device->staticSoundIndexStack);
    }
    else
    {
        sound->id = device->staticSoundCount;

        device->staticSounds = SDL_realloc(device->staticSounds, (device->staticSoundCount + 1) * sizeof(FAudioGMS_StaticSound*));
        device->staticSoundCount += 1;
    }

    device->staticSounds[sound->id] = sound;

    return (double)sound->id;
}

static void FAudioGMS_INTERNAL_SoundInstance_SetPan(FAudioGMS_SoundInstance* instance, float pan)
{
    pan = SDL_max(-1.0, SDL_min(1.0, pan));
    instance->pan = pan;
    SetPanMatrixCoefficients(instance);

    FAudioVoice_SetOutputMatrix(
        instance->voice.handle,
        device->fauxMasteringVoice.handle,
        instance->dspSettings.SrcChannelCount,
        instance->dspSettings.DstChannelCount,
        instance->dspSettings.pMatrixCoefficients,
        0
    );
}

static void FAudioGMS_INTERNAL_SoundInstance_UpdatePitch(FAudioGMS_SoundInstance* instance)
{
    float doppler = 1.0f;

    if (!instance->is3D || DOPPLER_SCALE == 0.0f)
    {
        doppler = 1.0f;
    }
    else
    {
        doppler = instance->dspSettings.DopplerFactor * DOPPLER_SCALE;
    }

    FAudioSourceVoice_SetFrequencyRatio(
        instance->voice.handle,
        SDL_powf(2.0f, instance->pitch - 1) * doppler, /* FAudio expects pitch range to be -1.0 to 1.0 while GM uses 0.0 to 2.0 so we adjust here */
        0
    );
}

static void FAudioGMS_INTERNAL_SoundInstance_SetPitch(FAudioGMS_SoundInstance* instance, float pitch)
{
    pitch = SDL_max(0.0, SDL_min(2.0, pitch));
    instance->pitch = pitch;
    FAudioGMS_INTERNAL_SoundInstance_UpdatePitch(instance);
}

static void FAudioGMS_INTERNAL_SoundInstance_SetVolume(FAudioGMS_SoundInstance* instance, float volume)
{
    instance->volume = volume;
    FAudioVoice_SetVolume(instance->voice.handle, volume, 0);
}

static FAudioGMS_SoundInstance* FAudioGMS_INTERNAL_SoundInstance_Init(
    uint32_t channelCount,
    uint32_t samplesPerSecond,
    uint8_t isStatic
) {
    FAudioGMS_SoundInstance* instance = SDL_malloc(sizeof(FAudioGMS_SoundInstance));

    instance->voice.handle = NULL;

    instance->isStatic = isStatic;
    instance->loop = 0;
    instance->destroyOnFinish = 0;
	instance->isGlobalPaused = 0;

    instance->format.wFormatTag = FAUDIO_FORMAT_IEEE_FLOAT;
    instance->format.wBitsPerSample = 32;
    instance->format.nChannels = channelCount;
    instance->format.nBlockAlign = (uint16_t)(4 * channelCount);
    instance->format.nSamplesPerSec = samplesPerSecond;
    instance->format.nAvgBytesPerSec = instance->format.nBlockAlign * samplesPerSecond;

    instance->voice.sends.SendCount = 1;
    instance->voice.sends.pSends = SDL_malloc(sizeof(FAudioSendDescriptor));
    instance->voice.sends.pSends[0].Flags = 0;
    instance->voice.sends.pSends[0].pOutputVoice = device->fauxMasteringVoice.handle;

    FAudio_CreateSourceVoice(
        device->handle,
        &instance->voice.handle,
        &instance->format,
        FAUDIO_VOICE_USEFILTER,
        FAUDIO_DEFAULT_FREQ_RATIO,
        isStatic ? NULL : &device->voiceCallbacks,
        &instance->voice.sends,
        NULL);

    if (instance->voice.handle == NULL)
    {
        Log("SoundInstance failed to initialize!");
        SDL_free(instance);
        return NULL;
    }

    instance->dspSettings.DopplerFactor = 1.0f;
    instance->dspSettings.SrcChannelCount = channelCount;
    instance->dspSettings.DstChannelCount = device->deviceDetails.OutputFormat.Format.nChannels;

    uint32_t memsize = 4 * instance->dspSettings.SrcChannelCount * instance->dspSettings.DstChannelCount;
    instance->dspSettings.pMatrixCoefficients = SDL_malloc(memsize);
    SDL_memset(instance->dspSettings.pMatrixCoefficients, 0, memsize);

    instance->voice.effectChainAttached = 0;
    instance->voice.effectVoice = NULL;
    instance->voice.effectGain = 0;

    instance->lowPassFilter = 0.0f;
    instance->highPassFilter = 0.0f;
    instance->bandPassFilter = 0.0f;

    FAudioGMS_INTERNAL_SoundInstance_SetPan(instance, 0);
    FAudioGMS_INTERNAL_SoundInstance_SetPitch(instance, 1);
    FAudioGMS_INTERNAL_SoundInstance_SetVolume(instance, 1);

    instance->soundState = SoundState_Stopped;

    instance->adjustingVolumeOverTime = 0;
    instance->volumeDelta = 0;
    instance->targetVolume = 1;

    instance->is3D = 0;
    instance->emitter = NULL;
    instance->stereoAzimuth[0] = 0.0f;
    instance->stereoAzimuth[1] = 0.0f;

	instance->playBegin = 0;
	instance->playLength = 0;

	instance->queuedSoundInstance = NULL;

    if (device->soundInstanceIndexStack.count > 0)
    {
        instance->id = IdStack_Pop(&device->soundInstanceIndexStack);
    }
    else
    {
        instance->id = device->soundInstanceCount;

        device->soundInstances = SDL_realloc(device->soundInstances, (device->soundInstanceCount + 1) * sizeof(FAudioGMS_SoundInstance*));
        device->soundInstanceCount += 1;
    }

    device->soundInstances[instance->id] = instance;

    return instance;
}

static FAudioGMS_SoundInstance* FAudioGMS_INTERNAL_SoundInstance_CreateFromStaticSound(
    FAudioGMS_StaticSound* staticSound
) {
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_SoundInstance_Init(
        staticSound->channels,
        staticSound->samplesPerSecond,
        1
    );

    instance->isStatic = 1;
    instance->soundData.staticSound = staticSound;

    return instance;
}

double FAudioGMS_StaticSound_CreateSoundInstance(double staticSoundID)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_StaticSound* staticSound = FAudioGMS_INTERNAL_LookupStaticSound((uint32_t)staticSoundID);

    if (staticSound != NULL)
    {
        FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_SoundInstance_CreateFromStaticSound(staticSound);
        return instance->id;
    }
    else
    {
        Log("Invalid static sound ID!");
        return -1.0;
    }
}

static void FAudioGMS_INTERNAL_SoundInstance_SetLowPassFilter(FAudioGMS_SoundInstance* instance, float lowPassFilter, float Q)
{
    FAudioFilterParameters p;
    p.Type = FAudioLowPassFilter;
    p.Frequency = SDL_max(0.0, SDL_min(1.0, lowPassFilter));
    p.OneOverQ = 1.0f / Q;

    FAudioVoice_SetFilterParameters(instance->voice.handle, &p, 0);

    instance->lowPassFilter = lowPassFilter;
}

void FAudioGMS_SoundInstance_SetLowPassFilter(double soundInstanceID, double lowPassFilter, double Q)
{
    FAudioGMS_SoundInstance *instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_SetLowPassFilter(instance, lowPassFilter, Q);
    }
}

static void FAudioGMS_INTERNAL_SoundInstance_SetHighPassFilter(FAudioGMS_SoundInstance* instance, float highPassFilter, float Q)
{
    FAudioFilterParameters p;
    p.Type = FAudioHighPassFilter;
    p.Frequency = SDL_max(0.0, SDL_min(1.0, highPassFilter));
    p.OneOverQ = 1.0f / Q;

    FAudioVoice_SetFilterParameters(instance->voice.handle, &p, 0);

    instance->highPassFilter = highPassFilter;
}

void FAudioGMS_SoundInstance_SetHighPassFilter(double soundInstanceID, double highPassFilter, double Q)
{
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_SetHighPassFilter(instance, highPassFilter, Q);
    }
}

static void FAudioGMS_INTERNAL_SoundInstance_SetBandPassFilter(FAudioGMS_SoundInstance* instance, float bandPassFilter, float Q)
{
    FAudioFilterParameters p;
    p.Type = FAudioBandPassFilter;
    p.Frequency = SDL_max(0.0, SDL_min(1.0, bandPassFilter));
    p.OneOverQ = 1.0f / Q;

    FAudioVoice_SetFilterParameters(instance->voice.handle, &p, 0);

    instance->bandPassFilter = bandPassFilter;
}

void FAudioGMS_SoundInstance_SetBandPassFilter(double soundInstanceID, double bandPassFilter, double Q)
{
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_SetBandPassFilter(instance, bandPassFilter, Q);
    }
}

void FAudioGMS_SoundInstance_QueueSoundInstance(double soundInstanceID, double queueSoundInstanceID)
{
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);
	FAudioGMS_SoundInstance* queueInstance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)queueSoundInstanceID);

    if (instance != NULL && queueInstance != NULL)
    {
		instance->queuedSoundInstance = queueInstance;
	}
	else
	{
		if (instance == NULL)
		{
			Log("QueueSoundInstace: Invalid instance ID!");
		}
		else
		{
			Log("QueueSoundInstance: Invalid queue sound instance ID!");
		}
	}
}

static void FAudioGMS_INTERNAL_Apply3D(FAudioGMS_SoundInstance* instance)
{
    F3DAUDIO_EMITTER* emitter = instance->emitter;

    if (emitter == NULL)
    {
        Log("Sound instance does not have 3D data! Did you forget to initialize?");
        return;
    }

    F3DAudioCalculate(
        device->handle3D,
        &device->listener,
        emitter,
        F3DAUDIO_CALCULATE_MATRIX | F3DAUDIO_CALCULATE_DOPPLER,
        &instance->dspSettings
    );

    FAudioGMS_INTERNAL_SoundInstance_UpdatePitch(instance);
    FAudioVoice_SetOutputMatrix(
        instance->voice.handle,
        device->fauxMasteringVoice.handle,
        instance->dspSettings.SrcChannelCount,
        instance->dspSettings.DstChannelCount,
        instance->dspSettings.pMatrixCoefficients,
        0
    );
}

static void FAudioGMS_INTERNAL_SoundInstance_AddBuffer(FAudioGMS_SoundInstance* instance)
{
    uint32_t defaultRequestedSampleCount = instance->format.nSamplesPerSec / 4;
	uint32_t requestedSampleCount = defaultRequestedSampleCount;

	if (instance->playLength != 0)
	{
		uint32_t distanceToEndPoint = (instance->playBegin + instance->playLength) - stb_vorbis_get_sample_offset(instance->soundData.streamingSound.fileHandle);
		requestedSampleCount = SDL_min(requestedSampleCount, distanceToEndPoint);
	}

    uint32_t requiredStagingBufferSize = requestedSampleCount * instance->format.nChannels * sizeof(float);
    if (instance->soundData.streamingSound.streamBufferSize < requiredStagingBufferSize)
    {
        instance->soundData.streamingSound.streamBuffer = SDL_realloc(instance->soundData.streamingSound.streamBuffer, requiredStagingBufferSize);
		instance->soundData.streamingSound.streamBufferSize = requiredStagingBufferSize;
    }

    instance->soundData.streamingSound.mostRecentBufferOffset = stb_vorbis_get_sample_offset(instance->soundData.streamingSound.fileHandle);

    /* NOTE: this function returns samples per channel, not total samples */
    uint32_t sampleCount = stb_vorbis_get_samples_float_interleaved(
        instance->soundData.streamingSound.fileHandle,
        instance->format.nChannels,
        instance->soundData.streamingSound.streamBuffer,
        requestedSampleCount * instance->format.nChannels
    );

    FAudioBuffer buffer;
    buffer.AudioBytes = sampleCount * instance->format.nChannels * sizeof(float);
    buffer.pAudioData = (uint8_t*) instance->soundData.streamingSound.streamBuffer;
    buffer.PlayBegin = 0;
    buffer.PlayLength = sampleCount;

    buffer.Flags = 0;
    buffer.LoopBegin = 0;
    buffer.LoopCount = 0;
    buffer.LoopLength = 0;
    buffer.pContext = instance; /* context for OnBufferEnd callback */

    FAudioSourceVoice_SubmitSourceBuffer(instance->voice.handle, &buffer, NULL);

	instance->soundData.streamingSound.isFinalBuffer = 0;

	if (sampleCount < defaultRequestedSampleCount)
	{
		if (instance->loop)
		{
			stb_vorbis_seek_frame(instance->soundData.streamingSound.fileHandle, instance->playBegin);
		}
		else
		{
			instance->soundData.streamingSound.isFinalBuffer = 1;
		}
	}
}

double FAudioGMS_StreamingSound_LoadOGG(char* filePath)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    int error = 0;
    stb_vorbis *fileHandle = stb_vorbis_open_filename(filePath, &error, NULL);

    if (error != 0)
    {
        Log("Error opening OGG file!");
        Log(filePath);
        return -1;
    }

    stb_vorbis_info info = stb_vorbis_get_info(fileHandle);

    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_SoundInstance_Init(
        info.channels,
        info.sample_rate,
        0
    );

    instance->soundData.streamingSound.fileHandle = fileHandle;
    instance->soundData.streamingSound.info = info;
    instance->soundData.streamingSound.streamBuffer = NULL;
    instance->soundData.streamingSound.streamBufferSize = 0;
    instance->soundData.streamingSound.mostRecentBufferOffset = 0;
	instance->soundData.streamingSound.isFinalBuffer = 0;

    FAudioGMS_INTERNAL_SoundInstance_AddBuffer(instance);

    return instance->id;
}

static void FAudioGMS_INTERNAL_SoundInstance_AddEmitter(FAudioGMS_SoundInstance* instance, float x, float y, float z)
{
    instance->is3D = 1;

    instance->emitter = SDL_malloc(sizeof(F3DAUDIO_EMITTER));

    /* defaults based on XNA behavior */
    instance->emitter->pCone = NULL;
    instance->emitter->ChannelRadius = 1.0f;
    instance->emitter->pChannelAzimuths = instance->stereoAzimuth;
    instance->emitter->pVolumeCurve = NULL;
    instance->emitter->pLFECurve = NULL;
    instance->emitter->pLPFDirectCurve = NULL;
    instance->emitter->pLPFReverbCurve = NULL;
    instance->emitter->pReverbCurve = NULL;
    instance->emitter->InnerRadius = 0;
    instance->emitter->InnerRadiusAngle = 0;

    instance->emitter->ChannelCount = instance->dspSettings.SrcChannelCount;
    instance->emitter->CurveDistanceScaler = device->spatialDistanceScale;
    instance->emitter->DopplerScaler = 1.0f;

    instance->emitter->Position.x = x;
    instance->emitter->Position.y = y;
    instance->emitter->Position.z = z;

    instance->emitter->Velocity.x = 0;
    instance->emitter->Velocity.y = 0;
    instance->emitter->Velocity.z = 0;

    instance->emitter->OrientTop.x = 0;
    instance->emitter->OrientTop.y = 1;
    instance->emitter->OrientTop.z = 0;

    instance->emitter->OrientFront.x = 0;
    instance->emitter->OrientFront.y = 0;
    instance->emitter->OrientFront.z = -1;

    FAudioGMS_INTERNAL_Apply3D(instance);
}

static void FAudioGMS_INTERNAL_SoundInstance_Play(FAudioGMS_SoundInstance* instance)
{
    if (instance->soundState == SoundState_Playing)
    {
        return;
    }

    if (instance->isStatic)
    {
		instance->soundData.staticSound->buffer.pContext = instance;

        if (instance->loop)
        {
            instance->soundData.staticSound->buffer.LoopCount = FAUDIO_LOOP_INFINITE;
            instance->soundData.staticSound->buffer.LoopBegin = instance->playBegin;
            instance->soundData.staticSound->buffer.LoopLength = instance->playLength;
        }
        else
        {
            instance->soundData.staticSound->buffer.LoopCount = 0;
            instance->soundData.staticSound->buffer.LoopBegin = 0;
            instance->soundData.staticSound->buffer.LoopLength = 0;
        }

		if (instance->soundState == SoundState_Paused)
		{
			FAudioSourceVoice_FlushSourceBuffers(instance->voice.handle);

			instance->soundData.staticSound->buffer.PlayBegin = instance->voice.handle->src.curBufferOffset;
			instance->soundData.staticSound->buffer.PlayLength = instance->playLength;
		}
		else
		{
			instance->soundData.staticSound->buffer.PlayBegin = instance->playBegin;
			instance->soundData.staticSound->buffer.PlayLength = instance->playLength;
		}

        FAudioSourceVoice_SubmitSourceBuffer(instance->voice.handle, &instance->soundData.staticSound->buffer, NULL);
    }

    FAudioSourceVoice_Start(instance->voice.handle, 0, 0);
    instance->soundState = SoundState_Playing;
}

void FAudioGMS_SoundInstance_Play(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_Play(instance);
    }
}

static void FAudioGMS_INTERNAL_SoundInstance_Pause(FAudioGMS_SoundInstance* instance)
{
	if (instance != NULL)
    {
        if (instance->soundState == SoundState_Playing)
        {
            FAudioSourceVoice_Stop(instance->voice.handle, 0, 0); /* this actually just pauses lol */
            instance->soundState = SoundState_Paused;
        }
    }
    else
    {
        Log("SoundInstance_Pause: Invalid sound instance ID! Did you destroy this instance?");
    }
}

void FAudioGMS_SoundInstance_Pause(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);
	FAudioGMS_INTERNAL_SoundInstance_Pause(instance);
}

static void FAudioGMS_INTERNAL_SoundInstance_Stop(FAudioGMS_SoundInstance* instance)
{
    if (instance != NULL)
    {
        instance->soundState = SoundState_Stopped; /* set this before so flush buffers doesn't trigger buffer add callback */

        FAudioSourceVoice_Stop(instance->voice.handle, 0, 0);
        FAudioSourceVoice_FlushSourceBuffers(instance->voice.handle);

        if (!instance->isStatic)
        {
            stb_vorbis_seek_frame(instance->soundData.streamingSound.fileHandle, instance->playBegin); /* back to the start */
            FAudioGMS_INTERNAL_SoundInstance_AddBuffer(instance); /* preload so we dont stutter on play */
        }
    }
    else
    {
        Log("SoundInstance_Stop: Invalid sound instance ID! Did you destroy this instance?");
    }
}

void FAudioGMS_SoundInstance_Stop(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);
    FAudioGMS_INTERNAL_SoundInstance_Stop(instance);
}

void FAudioGMS_SoundInstance_SetLoop(double soundInstanceID, double loop)
{
	RETURN_ON_NULL_DEVICE()
	FAudioGMS_SoundInstance *instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

	if (instance != NULL)
	{
		instance->loop = (uint8_t)loop;

		if (instance->isStatic && instance->soundState == SoundState_Playing)
		{
			/* We need to pause and play so that static buffers get resubmitted */
			FAudioGMS_INTERNAL_SoundInstance_Pause(instance);
			FAudioGMS_INTERNAL_SoundInstance_Play(instance);
		}
	}
}

void FAudioGMS_SoundInstance_SetPan(double soundInstanceID, double pan)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL && !instance->is3D)
    {
        FAudioGMS_INTERNAL_SoundInstance_SetPan(instance, pan);
    }
}

void FAudioGMS_SoundInstance_SetPitch(double soundInstanceID, double pitch)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_SetPitch(instance, pitch);
    }
}

void FAudioGMS_SoundInstance_SetVolume(double soundInstanceID, double volume)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        instance->adjustingVolumeOverTime = 0; /* override volume adjustment over time */
        FAudioGMS_INTERNAL_SoundInstance_SetVolume(instance, volume);
    }
}

void FAudioGMS_SoundInstance_Set3DPosition(double soundInstanceID, double x, double y, double z)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        if (instance->is3D)
        {
            instance->emitter->Position.x = x;
            instance->emitter->Position.y = y;
            instance->emitter->Position.z = z;

            FAudioGMS_INTERNAL_Apply3D(instance);
        }
        else
        {
            FAudioGMS_INTERNAL_SoundInstance_AddEmitter(instance, x, y, z);
        }
    }
}

void FAudioGMS_SoundInstance_Set3DVelocity(double soundInstanceID, double xVelocity, double yVelocity, double zVelocity)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        if (!instance->is3D)
        {
            Log("Not a 3D sound!");
            return;
        }

        instance->emitter->Velocity.x = xVelocity;
        instance->emitter->Velocity.y = yVelocity;
        instance->emitter->Velocity.z = zVelocity;
    }
}

/* FIXME: this will die horribly if position is greater than total length */
void FAudioGMS_SoundInstance_SetTrackPositionInSeconds(double soundInstanceID, double trackPositionInSeconds)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        uint32_t sampleFrame = instance->soundData.staticSound->samplesPerSecond * trackPositionInSeconds;

        FAudioGMS_SoundState currentState = instance->soundState;
        if (currentState == SoundState_Playing)
        {
            FAudioSourceVoice_Stop(instance->voice.handle, 0, 0);
            FAudioSourceVoice_FlushSourceBuffers(instance->voice.handle);
        }

        if (instance->isStatic)
        {
            instance->soundData.staticSound->buffer.PlayBegin = sampleFrame;
        }
        else
        {
            stb_vorbis_seek(instance->soundData.streamingSound.fileHandle, sampleFrame);
            FAudioGMS_INTERNAL_SoundInstance_AddBuffer(instance);
        }

        if (currentState == SoundState_Playing)
        {
            FAudioGMS_INTERNAL_SoundInstance_Play(instance);
        }
    }
}

static uint32_t FAudioGMS_INTERNAL_SoundInstance_GetTrackPositionInSampleFrames(FAudioGMS_SoundInstance* instance)
{
	if (instance != NULL)
	{
		if (instance->isStatic)
		{
			return instance->voice.handle->src.curBufferOffset / sizeof(float);
		}
		else
		{
			return instance->soundData.streamingSound.mostRecentBufferOffset + instance->voice.handle->src.curBufferOffset;
		}
	}

	Log("Invalid sound instance!");
	return 0;
}

void FAudioGMS_SoundInstance_SetPlayRegion(double soundInstanceID, double startInMilliseconds, double endInMilliseconds)
{
	RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

	if (instance != NULL)
	{
		uint32_t playBeginSampleFrame = instance->format.nSamplesPerSec * (startInMilliseconds / 1000);
		uint32_t playEndSampleFrame = instance->format.nSamplesPerSec * (endInMilliseconds / 1000);
		uint32_t playLength = playEndSampleFrame - playBeginSampleFrame;

		if (playLength <= 0)
		{
			Log("Play end is less than or equal to play start! Bailing!");
			return;
		}

		instance->playBegin = playBeginSampleFrame;
		instance->playLength = playLength;

		uint32_t currentFrame = FAudioGMS_INTERNAL_SoundInstance_GetTrackPositionInSampleFrames(instance);

		if (
			currentFrame < instance->playBegin ||
			(currentFrame > instance->playBegin + instance->playLength)
		) {
			/* we are outside the play region */
			if (instance->isStatic && instance->soundState == SoundState_Playing)
			{
				FAudioGMS_INTERNAL_SoundInstance_Stop(instance);
				FAudioGMS_INTERNAL_SoundInstance_Play(instance);
			}
			else if (!instance->isStatic)
			{
				FAudioSourceVoice_FlushSourceBuffers(instance->voice.handle);
				stb_vorbis_seek_frame(instance->soundData.streamingSound.fileHandle, instance->playBegin);
				FAudioGMS_INTERNAL_SoundInstance_AddBuffer(instance);
			}
		}
		else
		{
			/* we are inside the play region */
			if (instance->isStatic && instance->soundState != SoundState_Stopped)
			{
				FAudioGMS_INTERNAL_SoundInstance_Pause(instance);
				FAudioGMS_INTERNAL_SoundInstance_Play(instance);
			}
		}
	}
}

void FAudioGMS_SoundInstance_SetVolumeOverTime(double soundInstanceID, double volume, double milliseconds)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);\

    if (instance != NULL)
    {
        instance->adjustingVolumeOverTime = 1;
        instance->targetVolume = volume;
        instance->volumeDelta = (volume - instance->volume) / ((milliseconds / 1000) / device->timestep);
    }
}

double FAudioGMS_SoundInstance_GetPitch(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        return instance->pitch;
    }
    else
    {
        Log("Invalid sound instance!");
        return -1;
    }
}

double FAudioGMS_SoundInstance_GetVolume(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        return instance->volume;
    }
    else
    {
        Log("Invalid sound instance!");
        return -1;
    }
}

double FAudioGMS_SoundInstance_GetTrackLengthInSeconds(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        if (instance->isStatic)
        {
            return instance->soundData.staticSound->lengthInSeconds;
        }
        else
        {
            return stb_vorbis_stream_length_in_seconds(instance->soundData.streamingSound.fileHandle);
        }
    }
    else
    {
        Log("Invalid sound instance!");
        return -1;
    }
}

double FAudioGMS_SoundInstance_GetTrackPositionInSeconds(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
		return FAudioGMS_INTERNAL_SoundInstance_GetTrackPositionInSampleFrames(instance) / instance->format.nSamplesPerSec;
    }
    else
    {
        Log("Invalid sound instance!");
        return -1.0;
    }
}

void FAudioGMS_SetListenerPosition(double x, double y, double z)
{
    RETURN_ON_NULL_DEVICE()
    device->listener.Position.x = x;
    device->listener.Position.y = y;
    device->listener.Position.z = z;
}

void FAudioGMS_SetListenerVelocity(double xVelocity, double yVelocity, double zVelocity)
{
    RETURN_ON_NULL_DEVICE()
    device->listener.Velocity.x = xVelocity;
    device->listener.Velocity.y = yVelocity;
    device->listener.Velocity.z = zVelocity;
}

static void FAudioGMS_INTERNAL_SoundInstance_Destroy(FAudioGMS_SoundInstance* instance)
{
    if (instance != NULL)
    {
        device->soundInstances[instance->id] = NULL;
        IdStack_Push(&device->soundInstanceIndexStack, instance->id);
        FAudioGMS_INTERNAL_SoundInstance_Stop(instance);

        if (instance->voice.effectChainAttached)
        {
            FAudioVoice_DestroyVoice(instance->voice.effectVoice);
            SDL_free(instance->voice.effectSends.pSends);
        }

        SDL_free(instance->voice.sends.pSends);
        FAudioVoice_DestroyVoice(instance->voice.handle);

        if (!instance->isStatic)
        {
            SDL_free(instance->soundData.streamingSound.streamBuffer);
            stb_vorbis_close(instance->soundData.streamingSound.fileHandle);
        }
        if (instance->is3D)
        {
            SDL_free(instance->emitter);
        }
        SDL_free(instance->dspSettings.pMatrixCoefficients);
        SDL_free(instance);
    }
}

void FAudioGMS_SoundInstance_Destroy(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_SoundInstance_Destroy(instance);
    }
}

void FAudioGMS_SoundInstance_DestroyWhenFinished(double soundInstanceID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance* instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        instance->destroyOnFinish = 1;
    }
}

/* NOTE: this will die horribly if a sound is playing */
static void FAudioGMS_INTERNAL_StaticSound_Destroy(FAudioGMS_StaticSound* sound)
{
    if (sound != NULL)
    {
        device->staticSounds[sound->id] = NULL;
        IdStack_Push(&device->staticSoundIndexStack, sound->id);
        SDL_free((void*)sound->buffer.pAudioData);
        SDL_free(sound);
    }
}

void FAudioGMS_StaticSound_Destroy(double staticSoundID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_StaticSound *sound = FAudioGMS_INTERNAL_LookupStaticSound((uint32_t)staticSoundID);
    FAudioGMS_INTERNAL_StaticSound_Destroy(sound);
}

double FAudioGMS_EffectChain_Create()
{
    RETURN_ON_NULL_DEVICE(-1.0)
    FAudioGMS_EffectChain* effectChain = SDL_malloc(sizeof(FAudioGMS_EffectChain));

    effectChain->effectCount = 0;
    effectChain->effectTypes = NULL;
    effectChain->effectParameters = NULL;

    if (device->effectChainIndexStack.count > 0)
    {
        effectChain->id = IdStack_Pop(&device->effectChainIndexStack);
    }
    else
    {
        effectChain->id = device->effectChainCount;

        device->effectChains = SDL_realloc(device->effectChains, (device->effectChainCount + 1) * sizeof(FAudioGMS_EffectChain*));
        device->effectChainCount += 1;
    }

    device->effectChains[effectChain->id] = effectChain;

    return effectChain->id;
}

static void FAudioGMS_INTERNAL_EffectChain_AddReverb(FAudioGMS_EffectChain* effectChain, FAudioFXReverbParameters *reverbParameters)
{
    effectChain->effectCount += 1;

    effectChain->effectTypes = SDL_realloc(effectChain->effectTypes, effectChain->effectCount * sizeof(FAudioGMS_EffectType));
    effectChain->effectTypes[effectChain->effectCount - 1] = EffectType_Reverb;

    effectChain->effectParameters = SDL_realloc(effectChain->effectParameters, effectChain->effectCount * sizeof(FAudioGMS_EffectParameters));
    SDL_memcpy(&effectChain->effectParameters[effectChain->effectCount - 1], reverbParameters, sizeof(FAudioFXReverbParameters));
}

void FAudioGMS_EffectChain_AddDefaultReverb(double effectChainID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_EffectChain *effectChain = FAudioGMS_INTERNAL_LookupEffectChain((uint32_t)effectChainID);

    if (effectChain != NULL)
    {
        FAudioFXReverbParameters reverbParams;

        reverbParams.WetDryMix = 100.0f;
        reverbParams.ReflectionsDelay = 7;
        reverbParams.ReverbDelay = 11;
        reverbParams.RearDelay = FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
        reverbParams.PositionLeft = FAUDIOFX_REVERB_DEFAULT_POSITION;
        reverbParams.PositionRight = FAUDIOFX_REVERB_DEFAULT_POSITION;
        reverbParams.PositionMatrixLeft = FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
        reverbParams.PositionMatrixRight = FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
        reverbParams.EarlyDiffusion = 15;
        reverbParams.LateDiffusion = 15;
        reverbParams.LowEQGain = 8;
        reverbParams.LowEQCutoff = 4;
        reverbParams.HighEQGain = 8;
        reverbParams.HighEQCutoff = 6;
        reverbParams.RoomFilterFreq = 5000.0f;
        reverbParams.RoomFilterMain = -10.0f;
        reverbParams.RoomFilterHF = -1.0f;
        reverbParams.ReflectionsGain = -26.0200005f;
        reverbParams.ReverbGain = 10.0f;
        reverbParams.DecayTime = 1.49000001f;
        reverbParams.Density = 100.0f;
        reverbParams.RoomSize = FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE;

        FAudioGMS_INTERNAL_EffectChain_AddReverb(effectChain, &reverbParams);
    }
}

/* no room filter params because game maker only supports 16 arguments lmao */
void FAudioGMS_EffectChain_AddReverb(
    double effectChainID,
    double wetDryMix,
    double reflectionsDelay,
    double reverbDelay,
    double earlyDiffusion,
    double lateDiffusion,
    double lowEQGain,
    double lowEQCutoff,
    double highEQGain,
    double highEQCutoff,
    double reflectionsGain,
    double reverbGain,
    double decayTime,
    double density,
    double roomSize
) {
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_EffectChain* effectChain = FAudioGMS_INTERNAL_LookupEffectChain((uint32_t)effectChainID);

    if (effectChain != NULL)
    {
        FAudioFXReverbParameters reverbParams;

        reverbParams.WetDryMix = wetDryMix;
        reverbParams.ReflectionsDelay = reflectionsDelay;
        reverbParams.ReverbDelay = reverbDelay;
        reverbParams.RearDelay = FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
        reverbParams.PositionLeft = FAUDIOFX_REVERB_DEFAULT_POSITION;
        reverbParams.PositionRight = FAUDIOFX_REVERB_DEFAULT_POSITION;
        reverbParams.PositionMatrixLeft = FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
        reverbParams.PositionMatrixRight = FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
        reverbParams.EarlyDiffusion = earlyDiffusion;
        reverbParams.LateDiffusion = lateDiffusion;
        reverbParams.LowEQGain = lowEQGain;
        reverbParams.LowEQCutoff = lowEQCutoff;
        reverbParams.HighEQGain = highEQGain;
        reverbParams.HighEQCutoff = highEQCutoff;
        reverbParams.RoomFilterFreq = 5000.0f;
        reverbParams.RoomFilterMain = -10.0f;
        reverbParams.RoomFilterHF = -1.0f;
        reverbParams.ReflectionsGain = reflectionsGain;
        reverbParams.ReverbGain = reverbGain;
        reverbParams.DecayTime = decayTime;
        reverbParams.Density = density;
        reverbParams.RoomSize = roomSize;

        FAudioGMS_INTERNAL_EffectChain_AddReverb(effectChain, &reverbParams);
    }
}

static void FAudioGMS_INTERNAL_Voice_SetEffectGain(FAudioGMS_Voice *voice, float effectGain)
{
    if (voice->effectChainAttached)
    {
        float* outputMatrix = SDL_stack_alloc(float, voice->effectSends.SendCount);

        outputMatrix[0] = effectGain;
        if (voice->effectSends.SendCount == 2)
        {
            outputMatrix[1] = effectGain;
        }

        FAudioVoice_SetOutputMatrix(
            voice->handle,
            voice->effectVoice,
            voice->effectSends.SendCount,
            1,
            outputMatrix,
            0);

        voice->effectGain = effectGain;

        SDL_stack_free(outputMatrix);
    }
}

void FAudioGMS_SoundInstance_SetEffectGain(double soundInstanceID, double effectGain)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance *instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);

    if (instance != NULL)
    {
        FAudioGMS_INTERNAL_Voice_SetEffectGain(&instance->voice, effectGain);
    }
}

static FAudioEffectChain* FAudioGMS_INTERNAL_CreateFAudioEffectChain(FAudioGMS_EffectChain* effectChain)
{
    FAudioEffectChain* fAudioEffectChain = SDL_malloc(sizeof(FAudioEffectChain));
    uint32_t i;

    fAudioEffectChain->EffectCount = effectChain->effectCount;
    fAudioEffectChain->pEffectDescriptors = SDL_malloc(fAudioEffectChain->EffectCount * sizeof(FAudioEffectDescriptor));

    FAPO* reverb;

    for (i = 0; i < effectChain->effectCount; i += 1)
    {
        switch (effectChain->effectTypes[i])
        {
        case EffectType_Reverb:
            FAudioCreateReverb(&reverb, 0);

            FAudioEffectDescriptor* reverbDescriptor = &fAudioEffectChain->pEffectDescriptors[i];
            reverbDescriptor->InitialState = 1;
            reverbDescriptor->OutputChannels = device->deviceDetails.OutputFormat.Format.nChannels == 6 ? 6 : 1;
            reverbDescriptor->pEffect = reverb;
            break;

        default:
            Log("Unknown effect type! Something is very wrong!");
            return NULL;
        }
    }

    return fAudioEffectChain;
}

static void FAudioGMS_INTERNAL_SetEffectChain(FAudioGMS_Voice* voice, FAudioGMS_EffectChain* effectChain, float effectGain)
{
    uint32_t i;

    if (voice->effectChainAttached)
    {
        /* This frees the effect chain on the voice */
        FAudioVoice_SetEffectChain(voice->effectVoice, NULL);
        SDL_free(voice->effectSends.pSends);
    }
    else
    {
        voice->sends.SendCount = 2;
        voice->sends.pSends = SDL_realloc(voice->sends.pSends, 2 * sizeof(FAudioSendDescriptor));
    }

    FAudioEffectChain* fAudioEffectChain = FAudioGMS_INTERNAL_CreateFAudioEffectChain(effectChain);

    voice->effectSends.SendCount = 1;
    voice->effectSends.pSends = SDL_malloc(sizeof(FAudioSendDescriptor));

    voice->effectSends.pSends[0].Flags = 0;

    if (voice == &device->fauxMasteringVoice)
    {
        voice->effectSends.pSends[0].pOutputVoice = device->masteringVoice;
    }
    else
    {
        voice->effectSends.pSends[0].pOutputVoice = device->fauxMasteringVoice.handle;
    }

    FAudio_CreateSubmixVoice(
        device->handle,
        &voice->effectVoice,
        1, /* FIXME: is this correct? */
        device->deviceDetails.OutputFormat.Format.nSamplesPerSec,
        0,
        0,
        &voice->effectSends,
        fAudioEffectChain);

    /* Copy the effect params */
    for (i = 0; i < effectChain->effectCount; i += 1)
    {
        uint32_t parametersSize;
        void* parameters;

        switch (effectChain->effectTypes[i])
        {
        case EffectType_Reverb:
            parametersSize = sizeof(FAudioFXReverbParameters);
            parameters = &effectChain->effectParameters[i].reverbParameters;
            break;

        default:
            Log("Unknown effect type! Something is very wrong!");
        }

        FAudioVoice_SetEffectParameters(
            voice->effectVoice,
            i,
            parameters,
            parametersSize,
            0);
    }

    /* Set the instance voice to go through both faux mastering and effect voice for wet/dry */
    voice->sends.pSends[0].Flags = 0;
    if (voice == &device->fauxMasteringVoice)
    {
        voice->sends.pSends[0].pOutputVoice = device->masteringVoice;
    }
    else
    {
        voice->sends.pSends[0].pOutputVoice = device->fauxMasteringVoice.handle;
    }
    voice->sends.pSends[1].Flags = 0;
    voice->sends.pSends[1].pOutputVoice = voice->effectVoice;

    voice->effectChainAttached = 1;

    FAudioVoice_SetOutputVoices(
        voice->handle,
        &voice->sends);

    FAudioGMS_INTERNAL_Voice_SetEffectGain(voice, effectGain);

    /* All the effect parameters are copied to the voice so we free here */
    for (i = 0; i < effectChain->effectCount; i += 1)
    {
        FAPOBase_Release((FAPOBase*)fAudioEffectChain->pEffectDescriptors[i].pEffect);
    }
    SDL_free(fAudioEffectChain->pEffectDescriptors);
    SDL_free(fAudioEffectChain);
}

void FAudioGMS_SoundInstance_SetEffectChain(double soundInstanceID, double effectChainID, double effectGain)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_SoundInstance *instance = FAudioGMS_INTERNAL_LookupSoundInstance((uint32_t)soundInstanceID);
    FAudioGMS_EffectChain *effectChain = FAudioGMS_INTERNAL_LookupEffectChain((uint32_t)effectChainID);

    if (instance != NULL && effectChain != NULL)
    {
        FAudioGMS_INTERNAL_SetEffectChain(&instance->voice, effectChain, effectGain);
    }
}

void FAudioGMS_SetMasteringEffectChain(double effectChainID, double effectGain)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_EffectChain* effectChain = FAudioGMS_INTERNAL_LookupEffectChain((uint32_t)effectChainID);

    if (effectChain != NULL)
    {
        FAudioGMS_INTERNAL_SetEffectChain(&device->fauxMasteringVoice, effectChain, effectGain);
    }
}

void FAudioGMS_SetMasteringEffectGain(double effectGain)
{
    RETURN_ON_NULL_DEVICE()
    if (device->fauxMasteringVoice.effectChainAttached)
    {
        FAudioGMS_INTERNAL_Voice_SetEffectGain(&device->fauxMasteringVoice, effectGain);
    }
}

static void FAudioGMS_INTERNAL_EffectChain_Destroy(FAudioGMS_EffectChain *effectChain)
{
    if (effectChain != NULL)
    {
        device->effectChains[effectChain->id] = NULL;
        IdStack_Push(&device->effectChainIndexStack, effectChain->id);

        SDL_free(effectChain->effectParameters);
        SDL_free(effectChain->effectTypes);
        SDL_free(effectChain);
    }
}

void FAudioGMS_EffectChain_Destroy(double effectChainID)
{
    RETURN_ON_NULL_DEVICE()
    FAudioGMS_EffectChain *effectChain = FAudioGMS_INTERNAL_LookupEffectChain((uint32_t)effectChainID);

    if (effectChain != NULL)
    {
        FAudioGMS_INTERNAL_EffectChain_Destroy(effectChain);
    }
}

void FAudioGMS_Update()
{
    RETURN_ON_NULL_DEVICE()
    uint32_t i;

    for (i = 0; i < device->soundInstanceCount; i += 1)
    {
        FAudioGMS_SoundInstance* instance = device->soundInstances[i];

        if (instance != NULL)
        {
            if (instance->is3D)
            {
                FAudioGMS_INTERNAL_Apply3D(instance);
            }

            if (instance->adjustingVolumeOverTime)
            {
                float volume = instance->volume + instance->volumeDelta;
                if (instance->volumeDelta > 0)
                {
                    if (volume >= instance->targetVolume)
                    {
                        volume = instance->targetVolume;
                        instance->adjustingVolumeOverTime = 0;
                    }
                }
                else if (instance->volumeDelta < 0)
                {
                    if (volume <= instance->targetVolume)
                    {
                        volume = instance->targetVolume;
                        instance->adjustingVolumeOverTime = 0;
                    }
                }
                else
                {
                    instance->adjustingVolumeOverTime = 0;
                }

                FAudioGMS_INTERNAL_SoundInstance_SetVolume(instance, volume);
            }

            if (instance->destroyOnFinish)
            {
                FAudioVoiceState state;
                FAudioSourceVoice_GetState(instance->voice.handle, &state, FAUDIO_VOICE_NOSAMPLESPLAYED);

                if (state.BuffersQueued == 0)
                {
                    FAudioGMS_INTERNAL_SoundInstance_Destroy(instance);
                }
            }
        }
    }
}

void FAudioGMS_PauseAll()
{
	RETURN_ON_NULL_DEVICE()
	uint32_t i;

	for (i = 0; i < device->soundInstanceCount; i += 1)
	{
		FAudioGMS_SoundInstance *instance = device->soundInstances[i];
		if (instance != NULL)
		{
			instance->isGlobalPaused = 1;
			FAudioGMS_INTERNAL_SoundInstance_Pause(instance);
		}
	}
}

void FAudioGMS_ResumeAll()
{
	RETURN_ON_NULL_DEVICE()
	uint32_t i;

	for (i = 0; i < device->soundInstanceCount; i += 1)
	{
		FAudioGMS_SoundInstance *instance = device->soundInstances[i];
		if (instance != NULL && instance->isGlobalPaused)
		{
			instance->isGlobalPaused = 0;
			FAudioGMS_INTERNAL_SoundInstance_Play(instance);
		}
	}
}

void FAudioGMS_StopAll()
{
    RETURN_ON_NULL_DEVICE()
    uint32_t i;

    for (i = 0; i < device->soundInstanceCount; i += 1)
    {
        FAudioGMS_INTERNAL_SoundInstance_Stop(device->soundInstances[i]);
    }
}

void FAudioGMS_Destroy()
{
    RETURN_ON_NULL_DEVICE()
    uint32_t i;

    for (i = 0; i < device->soundInstanceCount; i += 1)
    {
        FAudioGMS_INTERNAL_SoundInstance_Destroy(device->soundInstances[i]);
    }

    for (i = 0; i < device->effectChainCount; i += 1)
    {
        FAudioGMS_INTERNAL_EffectChain_Destroy(device->effectChains[i]);
    }

    for (i = 0; i < device->staticSoundCount; i += 1)
    {
        FAudioGMS_INTERNAL_StaticSound_Destroy(device->staticSounds[i]);
    }

    FAudioVoice_DestroyVoice(device->fauxMasteringVoice.handle);
    SDL_free(device->fauxMasteringVoice.sends.pSends);

    FAudio_Release(device->handle);
    SDL_free(device);
    device = NULL;

    Log("FAudio cleaned up successfully!");
}