#define MUSIC_DEFINITION
#include "Music.h"

#include <SDL.h>
#include <FAudio.h>
#include <physfsrwops.h>

#include "Alloc.h"
#include "BinaryBlob.h"
#include "FileSystemUtils.h"
#include "Game.h"
#include "Graphics.h"
#include "Map.h"
#include "Script.h"
#include "Unused.h"
#include "UtilityClass.h"
#include "Vlogging.h"

#include <vector>

/* stb_vorbis */

#define malloc SDL_malloc
#define realloc SDL_realloc
#define free VVV_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 SDL_memcmp

#define pow SDL_pow
#define log(x) SDL_log(x)
#define sin(x) SDL_sin(x)
#define cos(x) SDL_cos(x)
#define floor SDL_floor
#define abs(x) SDL_abs(x)
#define ldexp(v, e) SDL_scalbn((v), (e))
#define exp(x) SDL_exp(x)

#define qsort SDL_qsort

#define assert SDL_assert

#define FILE SDL_RWops
#ifdef SEEK_SET
#undef SEEK_SET
#endif
#ifdef SEEK_CUR
#undef SEEK_CUR
#endif
#ifdef SEEK_END
#undef SEEK_END
#endif
#ifdef EOF
#undef EOF
#endif
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
#define EOF -1
#define fopen(path, mode) SDL_RWFromFile(path, mode)
#define fopen_s(io, path, mode) (!(*io = fopen(path, mode)))
#define fclose(io) SDL_RWclose(io)
#define fread(dst, size, count, io) SDL_RWread(io, dst, size, count)
#define fseek(io, offset, whence) SDL_RWseek(io, offset, whence)
#define ftell(io) SDL_RWtell(io)

#define FAudio_alloca(x) SDL_stack_alloc(uint8_t, x)
#define FAudio_dealloca(x) SDL_stack_free(x)

#define STB_VORBIS_NO_PUSHDATA_API 1
#define STB_VORBIS_NO_INTEGER_CONVERSION 1
#include <stb_vorbis.h>

/* End stb_vorbis include */

#define VVV_MAX_VOLUME 128
#define VVV_MAX_CHANNELS 8

class SoundTrack;
class MusicTrack;
static std::vector<SoundTrack> soundTracks;
static std::vector<MusicTrack> musicTracks;

static FAudio* faudioctx = NULL;
static FAudioMasteringVoice* masteringvoice = NULL;

class SoundTrack
{
public:
    SoundTrack(const char* fileName)
    {
        unsigned char* mem;
        size_t length;
        voice_index = -1;

        FILESYSTEM_loadAssetToMemory(fileName, &mem, &length);
        if (mem == NULL)
        {
            vlog_error("Unable to load sound file %s", fileName);
            SDL_assert(0 && "Sound file missing!");
            return;
        }

        SDL_zerop(this);
        if (length >= 4 && SDL_memcmp(mem, "OggS", 4) == 0)
        {
            LoadOGG(fileName, mem, length);
            callbacks.OnBufferStart = &SoundTrack::refillReserve;
            callbacks.OnBufferEnd = &SoundTrack::swapBuffers;
        }
        else
        {
            LoadWAV(fileName, mem, length);
        }
    }

    void LoadWAV(const char* fileName, unsigned char* mem, const size_t length)
    {
        SDL_AudioSpec spec;
        SDL_RWops *fileIn;
        fileIn = SDL_RWFromConstMem(mem, length);
        if (SDL_LoadWAV_RW(fileIn, 1, &spec, &wav_buffer, &wav_length) == NULL)
        {
            vlog_error("Unable to load WAV file %s", fileName);
            goto end;
        }
        format.nChannels = spec.channels;
        format.nSamplesPerSec = spec.freq;
        format.wFormatTag = FAUDIO_FORMAT_PCM;
        format.wBitsPerSample = SDL_AUDIO_BITSIZE(spec.format);
        format.nBlockAlign = format.nChannels * format.wBitsPerSample;
        format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
        format.cbSize = 0;
        valid = true;
end:
        VVV_free(mem);
    }

    void LoadOGG(const char* fileName, unsigned char* mem, const size_t length)
    {
        int err;
        stb_vorbis_info vorbis_info;
        vorbis = stb_vorbis_open_memory(mem, length, &err, NULL);
        if (vorbis == NULL)
        {
            vlog_error("Unable to create Vorbis handle for %s, error %d", fileName, err);
            VVV_free(mem);
            return;
        }
        vorbis_info = stb_vorbis_get_info(vorbis);
        format.wFormatTag = FAUDIO_FORMAT_IEEE_FLOAT;
        format.wBitsPerSample = sizeof(float) * 8;
        format.nChannels = vorbis_info.channels;
        format.nSamplesPerSec = vorbis_info.sample_rate;
        format.nBlockAlign = format.nChannels * format.wBitsPerSample;
        format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
        format.cbSize = 0;

        channels = format.nChannels;
        size = format.nAvgBytesPerSec / 20;

        decoded_buf_playing = (Uint8*) SDL_malloc(size);
        decoded_buf_reserve = (Uint8*) SDL_malloc(size);

        ogg_file = mem;
        valid = true;
    }

    void Dispose(void)
    {
        VVV_free(wav_buffer);

        VVV_free(decoded_buf_playing);
        VVV_free(decoded_buf_reserve);
        VVV_freefunc(stb_vorbis_close, vorbis);
        VVV_free(ogg_file);
    }

    void Play(void)
    {
        if (!valid)
        {
            return;
        }

        for (int i = 0; i < VVV_MAX_CHANNELS; i++)
        {
            FAudioVoiceState voicestate;
            FAudioSourceVoice_GetState(voices[i], &voicestate, 0);
            if (voicestate.BuffersQueued == 0)
            {
                if (SDL_memcmp(&voice_formats[i], &format, sizeof(format)) != 0)
                {
                    VVV_freefunc(FAudioVoice_DestroyVoice, voices[i]);
                    if (vorbis != NULL)
                    {
                        FAudio_CreateSourceVoice(faudioctx, &voices[i], &format, 0, 2.0f, &callbacks, NULL, NULL);
                    }
                    else
                    {
                        FAudio_CreateSourceVoice(faudioctx, &voices[i], &format, 0, 2.0f, NULL, NULL, NULL);
                    }
                    voice_formats[i] = format;
                }
                FAudioBuffer faudio_buffer = {
                    FAUDIO_END_OF_STREAM, /* Flags */
                    wav_length * 8, /* AudioBytes */
                    wav_buffer, /* AudioData */
                    0, /* playbegin */
                    0, /* playlength */
                    0, /* LoopBegin */
                    0, /* LoopLength */
                    0, /* LoopCount */
                    NULL
                };
                if (vorbis != NULL)
                {
                    stb_vorbis_seek_start(vorbis);
                    faudio_buffer.PlayLength = stb_vorbis_get_samples_float_interleaved(
                        vorbis,
                        channels,
                        (float*) decoded_buf_playing,
                        size / sizeof(float)
                    );
                    faudio_buffer.AudioBytes = size;
                    faudio_buffer.pAudioData = decoded_buf_playing;
                    faudio_buffer.pContext = this;
                }
                if (FAudioSourceVoice_SubmitSourceBuffer(voices[i], &faudio_buffer, NULL))
                {
                    vlog_error("Unable to queue sound buffer");
                    voice_index = -1;
                    return;
                }
                FAudioVoice_SetVolume(voices[i], volume, FAUDIO_COMMIT_NOW);
                if (FAudioSourceVoice_Start(voices[i], 0, FAUDIO_COMMIT_NOW))
                {
                    vlog_error("Unable to start voice processing");
                    voice_index = -1;
                }
                voice_index = i;
                return;
            }
        }
    }

    static void Init(int audio_rate)
    {
        if (voices == NULL)
        {
            voices = (FAudioSourceVoice**) SDL_malloc(sizeof(FAudioSourceVoice*) * VVV_MAX_CHANNELS);
            for (int i = 0; i < VVV_MAX_CHANNELS; i++)
            {
                FAudioWaveFormatEx format;
                format.nChannels = 1; /* Assume 1 for SoundTracks. Will be recreated if mismatched during play */
                format.nSamplesPerSec = audio_rate;
                format.wFormatTag = FAUDIO_FORMAT_PCM;
                format.wBitsPerSample = 16;
                format.nBlockAlign = format.nChannels * format.wBitsPerSample;
                format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
                format.cbSize = 0;
                voice_formats[i] = format;
                if (FAudio_CreateSourceVoice(faudioctx, &voices[i], &format, 0, 2.0f, NULL, NULL, NULL))
                {
                    vlog_error("Unable to create source voice no. %i", i);
                    return;
                }
            }
        }
    }

    static void Pause(void)
    {
        for (size_t i = 0; i < VVV_MAX_CHANNELS; i++)
        {
            FAudioSourceVoice_Stop(voices[i], 0, FAUDIO_COMMIT_NOW);
        }
    }

    static void Resume(void)
    {
        for (size_t i = 0; i < VVV_MAX_CHANNELS; i++)
        {
            FAudioSourceVoice_Start(voices[i], 0, FAUDIO_COMMIT_NOW);
        }
    }

    static void Destroy(void)
    {
        if (voices != NULL)
        {
            for (int i = 0; i < VVV_MAX_CHANNELS; i++)
            {
                VVV_freefunc(FAudioVoice_DestroyVoice, voices[i]);
            }
            VVV_free(voices);
        }
    }

    static void SetVolume(int soundVolume)
    {
        volume = (float) soundVolume / VVV_MAX_VOLUME;
        for (size_t i = 0; i < VVV_MAX_CHANNELS; i++)
        {
            FAudioVoice_SetVolume(voices[i], volume, FAUDIO_COMMIT_NOW);
        }
    }

    static void refillReserve(FAudioVoiceCallback* callback, void* ctx)
    {
        bool inbounds;
        SoundTrack* t = (SoundTrack*) ctx;
        FAudioBuffer faudio_buffer;
        SDL_zero(faudio_buffer);
        UNUSED(callback);
        faudio_buffer.PlayLength = stb_vorbis_get_samples_float_interleaved(t->vorbis, t->channels, (float*) t->decoded_buf_reserve, t->size / sizeof(float));
        faudio_buffer.AudioBytes = t->size;
        faudio_buffer.pAudioData = t->decoded_buf_reserve;
        faudio_buffer.pContext = t;
        if (faudio_buffer.PlayLength == 0)
        {
            return;
        }

        inbounds = t->voice_index >= 0 && t->voice_index < VVV_MAX_CHANNELS;
        if (!inbounds)
        {
            return;
        }

        FAudioSourceVoice_SubmitSourceBuffer(voices[t->voice_index], &faudio_buffer, NULL);
    }

    static void swapBuffers(FAudioVoiceCallback* callback, void* ctx)
    {
        SoundTrack* t = (SoundTrack*) ctx;
        Uint8* tmp = t->decoded_buf_playing;
        UNUSED(callback);
        t->decoded_buf_playing = t->decoded_buf_reserve;
        t->decoded_buf_reserve = tmp;
    }

    Uint8 *wav_buffer;
    Uint32 wav_length;
    FAudioWaveFormatEx format;
    int voice_index;

    unsigned char* ogg_file;
    stb_vorbis* vorbis;
    int channels;
    Uint32 size;
    Uint8* decoded_buf_playing;
    Uint8* decoded_buf_reserve;
    FAudioVoiceCallback callbacks;

    bool valid;

    static FAudioSourceVoice** voices;
    static FAudioWaveFormatEx voice_formats[VVV_MAX_CHANNELS];
    static float volume;
};
FAudioSourceVoice** SoundTrack::voices = NULL;
FAudioWaveFormatEx SoundTrack::voice_formats[VVV_MAX_CHANNELS];
float SoundTrack::volume = 0.0f;

class MusicTrack
{
public:
    MusicTrack(SDL_RWops *rw)
    {
        SDL_zerop(this);
        read_buf = (Uint8*) SDL_malloc(rw->size(rw));
        SDL_RWread(rw, read_buf, rw->size(rw), 1);
        int err;
        stb_vorbis_info vorbis_info;
        stb_vorbis_comment vorbis_comment;
        vorbis = stb_vorbis_open_memory(read_buf, rw->size(rw), &err, NULL);
        if (vorbis == NULL)
        {
            vlog_error("Unable to create Vorbis handle, error %d", err);
            VVV_free(read_buf);
            goto end;
        }
        vorbis_info = stb_vorbis_get_info(vorbis);
        format.wFormatTag = FAUDIO_FORMAT_IEEE_FLOAT;
        format.wBitsPerSample = sizeof(float) * 8;
        format.nChannels = vorbis_info.channels;
        format.nSamplesPerSec = vorbis_info.sample_rate;
        format.nBlockAlign = format.nChannels * format.wBitsPerSample;
        format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
        format.cbSize = 0;

        channels = format.nChannels;
        size = format.nAvgBytesPerSec / 20;

        decoded_buf_playing = (Uint8*) SDL_malloc(size);
        decoded_buf_reserve = (Uint8*) SDL_malloc(size);

        loopbegin = 0;
        looplength = 0;
        vorbis_comment = stb_vorbis_get_comment(vorbis);
        parseComments(this, vorbis_comment.comment_list, vorbis_comment.comment_list_length);
        valid = true;

end:
        SDL_RWclose(rw);
    }

    void Dispose(void)
    {
        VVV_freefunc(stb_vorbis_close, vorbis);
        VVV_free(read_buf);
        VVV_free(decoded_buf_playing);
        VVV_free(decoded_buf_reserve);
        if (!IsHalted())
        {
            VVV_freefunc(FAudioVoice_DestroyVoice, musicVoice);
        }
    }

    bool Play(bool loop)
    {
        if (!valid)
        {
            return false;
        }

        shouldloop = loop;
        sample_pos = 0;
        stb_vorbis_seek_start(vorbis);

        Halt();

        SDL_zero(callbacks);
        callbacks.OnBufferStart = &MusicTrack::refillReserve;
        callbacks.OnBufferEnd = &MusicTrack::swapBuffers;
        FAudio_CreateSourceVoice(faudioctx, &musicVoice, &format, 0, 2.0f, &callbacks, NULL, NULL);

        FAudioBuffer faudio_buffer;
        SDL_zero(faudio_buffer);
        if (looplength == 0)
        {
            faudio_buffer.PlayLength = stb_vorbis_get_samples_float_interleaved(vorbis, channels, (float*) decoded_buf_playing, size / sizeof(float));
        }
        else
        {
            int samples_read = stb_vorbis_get_samples_float_interleaved(vorbis, channels, (float*) decoded_buf_playing, size / sizeof(float));
            faudio_buffer.PlayLength = SDL_min(samples_read, (loopbegin + looplength) - sample_pos);
        }
        faudio_buffer.AudioBytes = size;
        faudio_buffer.pAudioData = decoded_buf_playing;
        faudio_buffer.pContext = this;
        sample_pos += faudio_buffer.PlayLength;
        if (FAudioSourceVoice_SubmitSourceBuffer(musicVoice, &faudio_buffer, NULL))
        {
            vlog_error("Unable to queue sound buffer");
            return false;
        }
        Resume();
        return true;
    }

    static void Halt(void)
    {
        if (!IsHalted())
        {
            FAudioSourceVoice_FlushSourceBuffers(musicVoice);
            VVV_freefunc(FAudioVoice_DestroyVoice, musicVoice);
            paused = true;
        }
    }

    static bool IsHalted(void)
    {
        return musicVoice == NULL;
    }

    static void Pause(void)
    {
        if (!IsHalted())
        {
            FAudioSourceVoice_Stop(musicVoice, 0, FAUDIO_COMMIT_NOW);
            paused = true;
        }
    }

    static bool IsPaused(void)
    {
        return paused || IsHalted();
    }

    static void Resume(void)
    {
        if (!IsHalted())
        {
            FAudioSourceVoice_Start(musicVoice, 0, FAUDIO_COMMIT_NOW);
            paused = false;
        }
    }

    static void SetVolume(int musicVolume)
    {
        float adj_vol = (float) musicVolume / VVV_MAX_VOLUME;
        if (!IsHalted())
        {
            FAudioVoice_SetVolume(musicVoice, adj_vol, FAUDIO_COMMIT_NOW);
        }
    }

    stb_vorbis* vorbis;
    int channels;
    Uint32 size;
    int loopbegin;
    int looplength;
    int sample_pos; //stb_vorbis offset not yet functional on pulldata API. TODO Replace when fixed

    FAudioVoiceCallback callbacks;
    FAudioWaveFormatEx format;

    Uint8* decoded_buf_playing;
    Uint8* decoded_buf_reserve;
    Uint8* read_buf;
    bool shouldloop;
    bool valid;

    static bool paused;
    static FAudioSourceVoice* musicVoice;

    static void refillReserve(FAudioVoiceCallback* callback, void* ctx)
    {
        MusicTrack* t = (MusicTrack*) ctx;
        FAudioBuffer faudio_buffer;
        SDL_zero(faudio_buffer);
        UNUSED(callback);
        if (t->looplength == 0)
        {
            faudio_buffer.PlayLength = stb_vorbis_get_samples_float_interleaved(t->vorbis, t->channels, (float*) t->decoded_buf_reserve, t->size / sizeof(float));
        }
        else
        {
            int samples_read = stb_vorbis_get_samples_float_interleaved(t->vorbis, t->channels, (float*) t->decoded_buf_reserve, t->size / sizeof(float));
            faudio_buffer.PlayLength = SDL_min(samples_read, (t->loopbegin + t->looplength) - t->sample_pos);
        }
        faudio_buffer.AudioBytes = t->size;
        faudio_buffer.pAudioData = t->decoded_buf_reserve;
        faudio_buffer.pContext = t;
        if (faudio_buffer.PlayLength == 0)
        {
            if (t->shouldloop)
            {
                stb_vorbis_seek(t->vorbis, t->loopbegin);
                t->sample_pos = t->loopbegin;
                if (t->looplength != 0)
                {
                    int samples_read = stb_vorbis_get_samples_float_interleaved(t->vorbis, t->channels, (float*) t->decoded_buf_reserve, t->size / sizeof(float));
                    faudio_buffer.PlayLength = SDL_min(samples_read, (t->loopbegin + t->looplength) - t->sample_pos);
                }
                else
                {
                    faudio_buffer.PlayLength = stb_vorbis_get_samples_float_interleaved(t->vorbis, t->channels, (float*) t->decoded_buf_reserve, t->size / sizeof(float));
                }
                if (faudio_buffer.PlayLength == 0)
                {
                    return;
                }
            }
            else
            {
                return;
            }
        }
        t->sample_pos += faudio_buffer.PlayLength;
        FAudioSourceVoice_SubmitSourceBuffer(musicVoice, &faudio_buffer, NULL);
    }

    static void swapBuffers(FAudioVoiceCallback* callback, void* ctx)
    {
        MusicTrack* t = (MusicTrack*) ctx;
        Uint8* tmp = t->decoded_buf_playing;
        UNUSED(callback);
        t->decoded_buf_playing = t->decoded_buf_reserve;
        t->decoded_buf_reserve = tmp;
    }

    /* Lifted from SDL_mixer, we used it in 2.3 and previous */
    static void parseComments(
        MusicTrack* t, char** comments, const int comment_list_length
    ) {
        int loopend = 0;
        for (int i = 0; i < comment_list_length; i++)
        {
            char* param = SDL_strdup(comments[i]);
            if (param == NULL)
            {
                vlog_error(
                    "Could not allocate memory to parse '%s'. Ignoring comments.",
                    comments[i]
                );
                break;
            }
            char* argument = param;
            char* value = SDL_strchr(param, '=');
            if (value == NULL)
            {
                value = param + SDL_strlen(param);
            }
            else
            {
                *(value++) = '\0';
            }

            /* Want to match LOOP-START, LOOP_START, etc. Remove - or _ from
            * string if it is present at position 4. */
            char buf[5];
            SDL_strlcpy(buf, argument, sizeof(buf));
            if (SDL_strcasecmp(buf, "LOOP") == 0
            && ((argument[4] == '_') || (argument[4] == '-')))
            {
                SDL_memmove(argument + 4, argument + 5, SDL_strlen(argument) - 4);
            }

            if (SDL_strcasecmp(argument, "LOOPSTART") == 0)
            {
                t->loopbegin = _Mix_ParseTime(value, t->format.nSamplesPerSec);
            }
            else if (SDL_strcasecmp(argument, "LOOPLENGTH") == 0)
            {
                t->looplength = SDL_strtoll(value, NULL, 10);
            }
            else if (SDL_strcasecmp(argument, "LOOPEND") == 0)
            {
                loopend = _Mix_ParseTime(value, t->format.nSamplesPerSec);
            }

            if (t->loopbegin < 0 || t->looplength < 0 || loopend < 0)
            {
                vlog_warn(
                    "A track loop comment had a negative value. "
                    "Ignoring all comments for the track."
                );
                t->loopbegin = 0;
                t->looplength = 0;
                loopend = 0;
                VVV_free(param);
                break;
            }

            VVV_free(param);
        }
        if (loopend != 0)
        {
            t->looplength = loopend - t->loopbegin;
        }
    }

    static int _Mix_ParseTime(char* time, const long samplerate_hz)
    {
        char* num_start = time;
        char* p;
        Sint64 result = 0;
        int val;

        /* Time is directly expressed as a sample position */
        if (SDL_strchr(time, ':') == NULL)
        {
            return SDL_strtoll(time, NULL, 10);
        }

        for (p = time; *p != '\0'; ++p)
        {
            if (*p == '.' || *p == ':')
            {
                const char c = *p;
                *p = '\0';
                val = SDL_atoi(num_start);
                if (val < 0)
                {
                    return -1;
                }
                result = result * 60 + val;
                num_start = p + 1;
                *p = c;
            }

            if (*p == '.')
            {
                const double val_f = SDL_atof(p);
                if (val_f < 0)
                {
                    return -1;
                }
                return result * samplerate_hz + (Sint64) (val_f * samplerate_hz);
            }
        }

        val = SDL_atoi(num_start);
        if (val < 0)
        {
            return -1;
        }
        return (result * 60 + val) * samplerate_hz;
    }
};
bool MusicTrack::paused = false;
FAudioSourceVoice* MusicTrack::musicVoice = NULL;

musicclass::musicclass(void)
{
    safeToProcessMusic= false;
    m_doFadeInVol = false;
    m_doFadeOutVol = false;
    musicVolume = 0;

    user_music_volume = USER_VOLUME_MAX;
    user_sound_volume = USER_VOLUME_MAX;

    currentsong = -1;
    haltedsong = -1;
    nicechange = -1;
    nicefade = false;
    quick_fade = true;

    usingmmmmmm = false;
}

void musicclass::init(void)
{
    if (FAudioCreate(&faudioctx, 0, FAUDIO_DEFAULT_PROCESSOR))
    {
        vlog_error("Unable to initialize FAudio");
        return;
    }
    if (FAudio_CreateMasteringVoice(faudioctx, &masteringvoice, 2, 44100, 0, 0, NULL))
    {
        vlog_error("Unable to create mastering voice");
        return;
    }

    SoundTrack::Init(44100);

    soundTracks.push_back(SoundTrack( "sounds/jump.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/jump2.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/hurt.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/souleyeminijingle.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/coin.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/save.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crumble.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/vanish.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/blip.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/preteleport.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/teleport.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew1.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew2.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew3.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew4.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew5.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crew6.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/terminal.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/gamesaved.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crashing.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/blip2.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/countdown.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/go.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/crash.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/combine.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/newrecord.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/trophy.wav" ));
    soundTracks.push_back(SoundTrack( "sounds/rescue.wav" ));

#ifdef VVV_COMPILEMUSIC
    binaryBlob musicWriteBlob;
#define FOREACH_TRACK(blob, track_name) blob.AddFileToBinaryBlob("data/" track_name);
    TRACK_NAMES(musicWriteBlob)
#undef FOREACH_TRACK

    musicWriteBlob.writeBinaryBlob("data/BinaryMusic.vvv");
    musicWriteBlob.clear();
#endif

    num_mmmmmm_tracks = 0;
    num_pppppp_tracks = 0;

    if (!mmmmmm_blob.unPackBinary("mmmmmm.vvv"))
    {
        if (pppppp_blob.unPackBinary("vvvvvvmusic.vvv"))
        {
            vlog_info("Loading music from PPPPPP blob...");

            mmmmmm = false;
            usingmmmmmm=false;

            int index;
            SDL_RWops* rw;

#define TRACK_LOAD_BLOB(blob, track_name) \
    index = blob.getIndex("data/" track_name); \
    if (index >= 0 && index < blob.max_headers) \
    { \
        rw = SDL_RWFromConstMem(blob.getAddress(index), blob.getSize(index)); \
        if (rw == NULL) \
        { \
            vlog_error("Unable to read music file header: %s", SDL_GetError()); \
        } \
        else \
        { \
            musicTracks.push_back(MusicTrack(rw)); \
        } \
    }

#define FOREACH_TRACK(blob, track_name) TRACK_LOAD_BLOB(blob, track_name)

            TRACK_NAMES(pppppp_blob)

#undef FOREACH_TRACK
        }
        else
        {
            vlog_info("Loading music from loose files...");

            SDL_RWops* rw;

#define FOREACH_TRACK(_, track_name) \
    rw = PHYSFSRWOPS_openRead(track_name); \
    if (rw == NULL) \
    { \
        vlog_error("Unable to read loose music file: %s", SDL_GetError()); \
    } \
    else \
    { \
        musicTracks.push_back(MusicTrack(rw)); \
    }

            TRACK_NAMES(_)

#undef FOREACH_TRACK
        }
    }
    else
    {
        vlog_info("Loading PPPPPP and MMMMMM blobs...");

        mmmmmm = true;
        int index;
        SDL_RWops* rw;

#define FOREACH_TRACK(blob, track_name) TRACK_LOAD_BLOB(blob, track_name)

        TRACK_NAMES(mmmmmm_blob)

        num_mmmmmm_tracks += musicTracks.size();

        size_t index_ = 0;
        while (mmmmmm_blob.nextExtra(&index_))
        {
            rw = SDL_RWFromConstMem(mmmmmm_blob.getAddress(index_), mmmmmm_blob.getSize(index_));
            musicTracks.push_back(MusicTrack( rw ));

            num_mmmmmm_tracks++;
            index_++;
        }

        bool ohCrap = pppppp_blob.unPackBinary("vvvvvvmusic.vvv");
        SDL_assert(ohCrap && "Music not found!");

    TRACK_NAMES(pppppp_blob)

#undef FOREACH_TRACK
#undef TRACK_LOAD_BLOB
    }

    num_pppppp_tracks += musicTracks.size() - num_mmmmmm_tracks;

    SDL_RWops* rw;
    size_t index_ = 0;
    while (pppppp_blob.nextExtra(&index_))
    {
        rw = SDL_RWFromConstMem(pppppp_blob.getAddress(index_), pppppp_blob.getSize(index_));
        musicTracks.push_back(MusicTrack( rw ));

        num_pppppp_tracks++;
        index_++;
    }
}

void musicclass::destroy(void)
{
    for (size_t i = 0; i < soundTracks.size(); ++i)
    {
        soundTracks[i].Dispose();
    }
    soundTracks.clear();
    SoundTrack::Destroy();

    for (size_t i = 0; i < musicTracks.size(); ++i)
    {
        musicTracks[i].Dispose();
    }
    musicTracks.clear();

    pppppp_blob.clear();
    mmmmmm_blob.clear();
    VVV_freefunc(FAudioVoice_DestroyVoice, masteringvoice);
    VVV_freefunc(FAudio_Release, faudioctx);
}

void musicclass::play(int t)
{
    if (mmmmmm && usingmmmmmm)
    {
        // Don't conjoin this if-statement with the above one...
        if (num_mmmmmm_tracks > 0)
        {
            t %= num_mmmmmm_tracks;
        }
    }
    else if (num_pppppp_tracks > 0)
    {
        t %= num_pppppp_tracks;
    }

    if (mmmmmm && !usingmmmmmm)
    {
        t += num_mmmmmm_tracks;
    }

    safeToProcessMusic = true;

    if (currentsong == t && !m_doFadeOutVol)
    {
        return;
    }

    currentsong = t;
    haltedsong = -1;

    if (t == -1)
    {
        return;
    }

    if (!INBOUNDS_VEC(t, musicTracks))
    {
        vlog_error("play() out-of-bounds!");
        currentsong = -1;
        return;
    }

    if (currentsong == Music_PATHCOMPLETE ||
        currentsong == Music_PLENARY ||
        (!map.custommode && (currentsong == Music_PATHCOMPLETE + num_mmmmmm_tracks
                             || currentsong == Music_PLENARY + num_mmmmmm_tracks)))
    {
        // No fade in or repeat
        if (musicTracks[t].Play(false))
        {
            m_doFadeInVol = false;
            m_doFadeOutVol = false;
            musicVolume = VVV_MAX_VOLUME;
            MusicTrack::SetVolume(musicVolume);
        }
    }
    else
    {
        if (m_doFadeOutVol)
        {
            // We're already fading out
            nicechange = t;
            nicefade = true;
            currentsong = -1;

            if (quick_fade)
            {
                fadeMusicVolumeOut(500); // fade out quicker
            }
            else
            {
                quick_fade = true;
            }
        }
        else if (musicTracks[t].Play(true))
        {
            m_doFadeInVol = false;
            m_doFadeOutVol = false;
            fadeMusicVolumeIn(3000);
        }
    }
}

void musicclass::resume(void)
{
    if (currentsong == -1)
    {
        currentsong = haltedsong;
        haltedsong = -1;
    }
    MusicTrack::Resume();
}

void musicclass::resumefade(const int fadein_ms)
{
    resume();
    fadeMusicVolumeIn(fadein_ms);
}

void musicclass::fadein(void)
{
    resumefade(3000); // 3000 ms fadein
}

void musicclass::pause(void)
{
    MusicTrack::Pause();
}

void musicclass::haltdasmusik(void)
{
    haltdasmusik(false);
}

void musicclass::haltdasmusik(const bool from_fade)
{
    /* Just pauses music. This is intended. */
    pause();
    haltedsong = currentsong;
    currentsong = -1;
    m_doFadeInVol = false;
    m_doFadeOutVol = false;
    if (!from_fade)
    {
        nicefade = false;
        nicechange = -1;
    }
}

void musicclass::silencedasmusik(void)
{
    musicVolume = 0;
    m_doFadeInVol = false;
    m_doFadeOutVol = false;
}

struct FadeState
{
    int start_volume;
    int end_volume;
    int duration_ms;
    int step_ms;
};

static struct FadeState fade;

enum FadeCode
{
    Fade_continue,
    Fade_finished
};

static enum FadeCode processmusicfade(struct FadeState* state, int* volume)
{
    int range;
    int new_volume;

    if (state->duration_ms == 0 /* Fast path. */
    || state->start_volume == state->end_volume /* Fast path. */
    || state->step_ms >= state->duration_ms /* We're finished. */)
    {
        *volume = state->end_volume;
        state->step_ms = 0;
        return Fade_finished;
    }

    range = state->end_volume - state->start_volume;
    new_volume = range * state->step_ms / state->duration_ms;
    new_volume += state->start_volume;

    *volume = new_volume;

    state->step_ms += game.get_timestep();

    return Fade_continue;
}

void musicclass::fadeMusicVolumeIn(int ms)
{
    if (halted())
    {
        return;
    }

    m_doFadeInVol = true;
    m_doFadeOutVol = false;

    /* Ensure it starts at 0 */
    musicVolume = 0;

    /* Fix 1-frame glitch */
    MusicTrack::SetVolume(0);

    fade.step_ms = 0;
    fade.duration_ms = ms;
    fade.start_volume = 0;
    fade.end_volume = VVV_MAX_VOLUME;
}

void musicclass::fadeMusicVolumeOut(const int fadeout_ms)
{
    if (halted())
    {
        return;
    }

    m_doFadeInVol = false;
    m_doFadeOutVol = true;

    fade.step_ms = 0;
    /* Duration is proportional to current volume. */
    fade.duration_ms = fadeout_ms * musicVolume / VVV_MAX_VOLUME;
    fade.start_volume = musicVolume;
    fade.end_volume = 0;
}

void musicclass::fadeout(const bool quick_fade_ /*= true*/)
{
    fadeMusicVolumeOut(quick_fade_ ? 500 : 2000);
    quick_fade = quick_fade_;
}

void musicclass::processmusicfadein(void)
{
    enum FadeCode fade_code = processmusicfade(&fade, &musicVolume);
    if (fade_code == Fade_finished)
    {
        m_doFadeInVol = false;
    }
}

void musicclass::processmusicfadeout(void)
{
    enum FadeCode fade_code = processmusicfade(&fade, &musicVolume);
    if (fade_code == Fade_finished)
    {
        musicVolume = 0;
        m_doFadeOutVol = false;
        haltdasmusik(true);
    }
}

void musicclass::processmusic(void)
{
    if(!safeToProcessMusic)
    {
        return;
    }

    if(m_doFadeInVol)
    {
        processmusicfadein();
    }

    if (m_doFadeOutVol)
    {
        processmusicfadeout();
    }

    /* This needs to come after processing fades */
    if (nicefade && halted())
    {
        play(nicechange);
        nicechange = -1;
        nicefade = false;
    }
}


void musicclass::niceplay(int t)
{
    /* important: do nothing if the correct song is playing! */
    if ((!mmmmmm && currentsong != t)
    || (mmmmmm && usingmmmmmm && currentsong != t)
    || (mmmmmm && !usingmmmmmm && currentsong != t + num_mmmmmm_tracks))
    {
        if (currentsong != -1)
        {
            fadeout(false);
        }
        nicefade = true;
    }
    nicechange = t;
}

static const int areamap[] = {
    4, 3, 3, 3, 3, 3, 3, 3, 4,-2, 4, 4, 4,12,12,12,12,12,12,12,
    4, 3, 3, 3, 3, 3, 3, 4, 4,-2, 4, 4, 4, 4,12,12,12,12,12,12,
    4, 4, 4, 4, 3, 4, 4, 4, 4,-2, 4, 4, 4, 4,12,12,12,12,12,12,
    4, 4, 4, 4, 3, 4, 4, 4, 4,-2, 4, 4, 1, 1, 1, 1,12,12,12,12,
    4, 4, 3, 3, 3, 4, 4, 4, 4,-2,-2,-2, 1, 1, 1, 1, 4, 4, 4, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 1, 1, 1, 1, 1, 1,11,11,-1, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 1, 1, 1, 1, 1, 1, 1,11,11,11,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 1, 1, 1, 1, 1, 1, 1, 1, 1,11,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 4, 4, 4, 1, 1, 1, 1, 1, 1, 3,
    4, 4, 4, 4, 4, 4, 4, 4,-2,-2, 4, 4, 4, 1, 1, 1, 1, 1, 1, 4,
    4, 4,-1,-1,-1, 4, 4, 4, 4,-2, 4, 4, 4, 1, 1, 1, 1, 1, 1, 4,
    4, 4,-1,-1,-1, 4, 4, 4, 4,-2, 4, 1, 1, 1, 1, 1, 1, 1, 1, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 4, 1, 1, 1, 1, 1, 1, 4, 1, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 4, 1, 1, 1, 1, 1, 1, 4, 1, 4,
    4, 4, 4, 4, 4, 4, 4, 4, 4,-2, 4,-1,-3, 4, 4, 4, 4, 4, 1, 4,
    4, 4, 4, 4, 4, 3, 3, 3, 4,-2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    4, 4, 3, 3, 3, 3, 3, 3, 4,-2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    4, 3, 3, 3, 3, 3, 3, 3, 4,-2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    3, 3, 3, 3, 3, 4, 4, 3, 4,-2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
    3, 3, 3, 3, 3, 4, 4, 3, 4,-2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
};

SDL_COMPILE_TIME_ASSERT(areamap, SDL_arraysize(areamap) == 20 * 20);

void musicclass::changemusicarea(int x, int y)
{
    int room;
    int track;

    if (script.running)
    {
        return;
    }

    room = musicroom(x, y);

    if (!INBOUNDS_ARR(room, areamap))
    {
        SDL_assert(0 && "Music map index out-of-bounds!");
        return;
    }

    track = areamap[room];

    switch (track)
    {
    case -1:
        /* Don't change music. */
        return;
    case -2:
        /* Special case: Tower music, changes with Flip Mode. */
        if (graphics.setflipmode)
        {
            track = 9; /* ecroF evitisoP */
        }
        else
        {
            track = 2; /* Positive Force */
        }
        break;
    case -3:
        /* Special case: start of Space Station 2. */
        if (game.intimetrial)
        {
            track = 1; /* Pushing Onwards */
        }
        else
        {
            track = 4; /* Passion for Exploring */
        }
        break;
    }

    niceplay(track);
}

void musicclass::playef(int t)
{
    if (!INBOUNDS_VEC(t, soundTracks))
    {
        return;
    }
    soundTracks[t].Play();
}

void musicclass::pauseef(void)
{
    SoundTrack::Pause();
}

void musicclass::resumeef(void)
{
    SoundTrack::Resume();
}

bool musicclass::halted(void)
{
    return MusicTrack::IsPaused();
}

void musicclass::updatemutestate(void)
{
    if (game.muted)
    {
        MusicTrack::SetVolume(0);
        SoundTrack::SetVolume(0);
    }
    else
    {
        SoundTrack::SetVolume(VVV_MAX_VOLUME * user_sound_volume / USER_VOLUME_MAX);

        if (game.musicmuted)
        {
            MusicTrack::SetVolume(0);
        }
        else
        {
            MusicTrack::SetVolume(musicVolume * user_music_volume / USER_VOLUME_MAX);
        }
    }
}
