#include "SND_seq.h"

#include "SND_bank.h"
#include "SND_exChannel.h"
#include "SND_lfo.h"
#include "SND_main.h"
#include "SND_util.h"
#include "SND_work.h"
#include "code32.h"

#define SND_TIMER_RATE 240

#define SND_TRACK_MUTE_MODE_UNMUTE 0
#define SND_TRACK_MUTE_MODE_MUTE 1
#define SND_TRACK_MUTE_MODE_MUTE_RELEASE 2
#define SND_TRACK_MUTE_MODE_MUTE_STOP 3

enum SNDSeqProc
{
    SND_PROC_SKIP_NOTES = 0,
    SND_PROC_PLAY_NOTES = 1,
};

enum SNDSeqVal
{
    SND_SEQ_VAL_U8 = 0,
    SND_SEQ_VAL_U16 = 1,
    SND_SEQ_VAL_VLV = 2,
    SND_SEQ_VAL_RAN = 3,
    SND_SEQ_VAL_VAR = 4,
};

static struct
{
    const u8 *begin;
    const u8 *end;
    union
    {
        u8 buf[16];
        u32 buf32[4];
    };
} sSeqCache;
static int sPrintEnabled;

static void PlayerSeqMain(struct SNDPlayer *player);
static void PlayerUpdateChannel(struct SNDPlayer *player);
static void PlayerStop(struct SNDPlayer *player);
static void PlayerInit(struct SNDPlayer *player, struct SNDBankData *bank);
static int AllocateTrack(void);
static void TrackInit(struct SNDTrack *track);
static void TrackStart(struct SNDTrack *track, const void *seq, u32 offset);
static void SeqCacheFetch(const u8 *addr);
static u8 SeqReadByte(const u8 *addr);
static u16 TrackReadU16(struct SNDTrack *track);
static struct SNDTrack *PlayerGetTrack(struct SNDPlayer *player, int track);
static void TrackReleaseChannels(struct SNDTrack *track, struct SNDPlayer *player, int release);
static void TrackFreeChannels(struct SNDTrack *track);
static BOOL PlayerStepTicks(struct SNDPlayer *player, u32 ticks);
static void TrackMute(struct SNDTrack *track, struct SNDPlayer *player, int muteMode);
static s16 *PlayerGetVariablePointer(const struct SNDPlayer *player, int var);
static void TrackUpdateChannel(struct SNDTrack *track, struct SNDPlayer *player, int release);

void SND_SeqInit(void)
{
    for (int i = 0; i < SND_PLAYER_COUNT; i++)
    {
        struct SNDPlayer *ply = &SNDi_Work.players[i];

        ply->flags.active = FALSE;
        ply->playerId = (u8)i;
    }

    for (int i = 0; i < SND_TRACK_COUNT; i++)
    {
        struct SNDTrack *trk = &SNDi_Work.tracks[i];

        trk->flags.active = FALSE;
    }
}

void SND_SeqMain(BOOL step)
{
    struct SNDPlayer *ply;
    int i;
    u32 playerStatus = 0;

    for (i = 0; i < SND_PLAYER_COUNT; i++)
    {
        ply = &SNDi_Work.players[i];

        if (!ply->flags.active)
            continue;

        if (ply->flags.prepared)
        {
            if (step && !ply->flags.paused)
                PlayerSeqMain(ply);
            PlayerUpdateChannel(ply);
        }

        if (ply->flags.active)
            playerStatus |= 1 << i;
    }

    if (SNDi_SharedWork)
        SNDi_SharedWork->playerStatus = playerStatus;
}

void SND_PrepareSeq(int player, const void *seq, u32 offset, struct SNDBankData *bankData)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    if (ply->flags.active)
        PlayerStop(ply);

    PlayerInit(ply, bankData);

    int allocTrkIdx = AllocateTrack();

    if (allocTrkIdx < 0)
        return;

    struct SNDTrack *trk = &SNDi_Work.tracks[allocTrkIdx];
    TrackInit(trk);
    TrackStart(trk, seq, offset);
    ply->tracks[0] = (u8)allocTrkIdx;
    SeqCacheFetch(trk->cur);

    u8 cmd = SeqReadByte(trk->cur);

    trk->cur++;

    if (cmd != 0xFE)
    {
        trk->cur--;
    }
    else
    {
        int track;
        u16 trackMask;

        for (trackMask = (u16)(TrackReadU16(trk) >> 1), track = 1; trackMask != 0;
             track++, trackMask >>= 1)
        {
            if (trackMask & 1)
            {
                allocTrkIdx = AllocateTrack();
                if (allocTrkIdx < 0)
                    break;
                TrackInit(&SNDi_Work.tracks[allocTrkIdx]);
                ply->tracks[track] = (u8)allocTrkIdx;
            }
        }
    }

    ply->flags.active = TRUE;
    ply->flags.prepared = FALSE;

    if (SNDi_SharedWork)
    {
        SNDi_SharedWork->playerStatus |= 1 << player;
    }
}

void SND_StartPreparedSeq(int player)
{
    SNDi_Work.players[player].flags.prepared = TRUE;
}

void SND_StartSeq(int player, const void *seq, u32 offset, struct SNDBankData *bankData)
{
    SND_PrepareSeq(player, seq, offset, bankData);
    SND_StartPreparedSeq(player);
}

void SND_StopSeq(int player)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    if (ply->flags.active)
    {
        PlayerStop(ply);

        if (SNDi_SharedWork)
        {
            SNDi_SharedWork->playerStatus &= ~(1 << player);
        }
    }
}

void SND_PauseSeq(int player, BOOL flag)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    ply->flags.paused = flag;

    if (flag)
    {
        for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
        {
            struct SNDTrack *trk = PlayerGetTrack(ply, i);

            if (trk)
            {
                TrackReleaseChannels(trk, ply, 127);
                TrackFreeChannels(trk);
            }
        }
    }
}

void SND_SkipSeq(int player, u32 ticks)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
    {
        struct SNDTrack *trk = PlayerGetTrack(ply, i);

        if (trk)
        {
            TrackReleaseChannels(trk, ply, 127);
            TrackFreeChannels(trk);
        }
    }

    SND_StopIntervalTimer();

    u32 i;
    for (i = 0; i < ticks; i++)
    {
        if (PlayerStepTicks(ply, 0))
        {
            PlayerStop(ply);
            break;
        }
    }

    SND_StartIntervalTimer();

    if (SNDi_SharedWork)
    {
        SNDi_SharedWork->players[ply->playerId].tickCounter += i;
    }
}

void SND_SetTrackMute(int player, u32 trackMask, int muteMode)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER && trackMask != 0; i++, trackMask >>= 1)
    {
        if (trackMask & 1)
        {
            struct SNDTrack *trk = PlayerGetTrack(ply, i);

            if (trk)
            {
                TrackMute(trk, ply, muteMode);
            }
        }
    }
}

void SND_SetTrackAllocatableChannel(int player, u32 trackMask, u32 channelMask)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER && trackMask != 0; i++, trackMask >>= 1)
    {
        if (trackMask & 1)
        {
            struct SNDTrack *trk = PlayerGetTrack(ply, i);

            if (trk)
            {
                trk->channelMask = (u16)channelMask;
                trk->flags.channelMask = TRUE;
            }
        }
    }
}

void SND_InvalidateSeq(const void *start, const void *end)
{
    struct SNDPlayer *ply;
    struct SNDTrack *trk;
    int i;
    int j;

    for (i = 0; i < SND_PLAYER_COUNT; i++)
    {
        ply = &SNDi_Work.players[i];

        if (!ply->flags.active)
            continue;

        for (j = 0; j < SND_TRACK_COUNT_PER_PLAYER; j++)
        {
            trk = PlayerGetTrack(ply, j);

            if (!trk)
                continue;

            if (start <= trk->cur && trk->cur <= end)
            {
                PlayerStop(ply);
                break;
            }
        }
    }
}

void SND_InvalidateBank(const void *start, const void *end)
{
    for (int i = 0; i < SND_PLAYER_COUNT; i++)
    {
        struct SNDPlayer *ply = &SNDi_Work.players[i];

        if (ply->flags.active && start <= ply->bank && ply->bank <= end)
            PlayerStop(ply);
    }
}

void SNDi_SetPlayerParam(int player, u32 offset, u32 data, int size)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    switch (size)
    {
        case 1:
            *(u8 *)((u8 *)ply + offset) = (u8)data;
            break;
        case 2:
            *(u16 *)((u8 *)ply + offset) = (u16)data;
            break;
        case 4:
            *(u32 *)((u8 *)ply + offset) = (u32)data;
            break;
    }
}

void SNDi_SetTrackParam(int player, u32 trackMask, u32 offset, u32 data, int size)
{
    struct SNDPlayer *ply = &SNDi_Work.players[player];

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER && trackMask != 0; i++, trackMask >>= 1)
    {
        if (!(trackMask & 1))
            continue;

        struct SNDTrack *trk = PlayerGetTrack(ply, i);

        if (!trk)
            continue;

        switch (size)
        {
            case 1:
                *(u8 *)((u8 *)trk + offset) = (u8)data;
                break;
            case 2:
                *(u16 *)((u8 *)trk + offset) = (u16)data;
                break;
            case 4:
                *(u32 *)((u8 *)trk + offset) = (u32)data;
                break;
        }
    }
}

static void SeqCacheFetch(const u8 *addr)
{
    addr = (const u8 *)((u32)addr & ~3);
    sSeqCache.begin = addr;
    sSeqCache.end = sSeqCache.begin + 16;

    const u32 *src = (const u32 *)addr;

    sSeqCache.buf32[0] = src[0];
    sSeqCache.buf32[1] = src[1];
    sSeqCache.buf32[2] = src[2];
    sSeqCache.buf32[3] = src[3];
}

static u8 SeqReadByte(const u8 *addr)
{
    if (addr < sSeqCache.begin || addr >= sSeqCache.end)
        SeqCacheFetch(addr);

    return sSeqCache.buf[(u32)addr - (u32)sSeqCache.begin];
}

static inline u8 TrackReadU8(struct SNDTrack *track)
{
    u8 retval = SeqReadByte(track->cur);
    track->cur++;
    return retval;
}

static u16 TrackReadU16(struct SNDTrack *track)
{
    int retval;

    retval = TrackReadU8(track);
    retval |= TrackReadU8(track) << 8;

    return (u16)retval;
}

static u32 TrackReadU24(struct SNDTrack *track)
{
    u32 retval;

    retval = TrackReadU8(track);
    retval |= TrackReadU8(track) << 8;
    retval |= TrackReadU8(track) << 16;

    return retval;
}

static int TrackReadVLV(struct SNDTrack *track)
{
    int retval = 0;
    int b;

    do
    {
        b = TrackReadU8(track);
        retval = (retval << 7) | (b & 0x7F);
    } while (b & 0x80);

    return retval;
}

static int TrackParseValue(struct SNDTrack *track, struct SNDPlayer *player, int valueType)
{
    int retval;
    int hi;
    int lo;
    int ran;
    s16 *var;

    // BUG: undefined behavior if invalid valueType is passed (uninitialized return value)

    switch (valueType)
    {
        case SND_SEQ_VAL_U8:
            retval = TrackReadU8(track);
            break;
        case SND_SEQ_VAL_U16:
            retval = TrackReadU16(track);
            break;
        case SND_SEQ_VAL_VLV:
            retval = TrackReadVLV(track);
            break;
        case SND_SEQ_VAL_VAR:
            var = PlayerGetVariablePointer(player, TrackReadU8(track));
            if (var)
                retval = *var;
            break;
        case SND_SEQ_VAL_RAN:
            lo = TrackReadU16(track) << 16;
            hi = (s16)TrackReadU16(track);
            ran = SND_CalcRandom();
            retval = hi - (lo >> 16);
            retval += 1;
            retval = (ran * retval) >> 16;
            retval += lo >> 16;
            break;
    }

    return retval;
}

static void TrackInit(struct SNDTrack *track)
{
    track->base = NULL;
    track->cur = NULL;

    track->flags.noteWait = TRUE;
    track->flags.muted = FALSE;
    track->flags.tie = FALSE;
    track->flags.noteFinishWait = FALSE;
    track->flags.portamento = FALSE;
    track->flags.cmp = TRUE;
    track->flags.channelMask = FALSE;

    track->callStackDepth = 0;
    track->program = 0;
    track->priority = 64;
    track->volume = 127;
    track->expression = 127;
    track->extFader = 0;
    track->pan = 0;
    track->extPan = 0;
    track->pitchBend = 0;
    track->extPitch = 0;
    track->envAttack = 255;
    track->envDecay = 255;
    track->envSustain = 255;
    track->envRelease = 255;
    track->panRange = 127;
    track->bendRange = 2;
    track->portamentoKey = 60;
    track->portamentoTime = 0;
    track->sweepPitch = 0;
    track->transpose = 0;
    track->channelMask = 0xFFFF;
    SND_InitLfoParam(&track->mod);
    track->wait = 0;
    track->channelLLHead = NULL;
}

static void TrackStart(struct SNDTrack *track, const void *seq, u32 offset)
{
    track->base = (const u8 *)seq;
    track->cur = &track->base[offset];
}

static void PlayerInit(struct SNDPlayer *player, struct SNDBankData *bank)
{
    player->flags.paused = FALSE;
    player->bank = bank;
    player->tempo = 120;
    player->tempoRatio = 256;
    player->tempoCounter = SND_TIMER_RATE;
    player->volume = 0x7F;
    player->extFader = 0;
    player->prio = 64;

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
    {
        player->tracks[i] = 0xFF;
    }

    if (SNDi_SharedWork)
    {
        SNDi_SharedWork->players[player->playerId].tickCounter = 0;

        for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
        {
            SNDi_SharedWork->players[player->playerId].localVars[i] = -1;
        }
    }
}

static void TrackReleaseChannels(struct SNDTrack *track, struct SNDPlayer *player, int release)
{
    TrackUpdateChannel(track, player, 0);

    for (struct SNDExChannel *chn = track->channelLLHead; chn; chn = chn->channelLLNext)
    {
        if (SND_IsExChannelActive(chn))
        {
            if (release >= 0)
                SND_SetExChannelRelease(chn, release & 0xFF);
            chn->priority = 1;
            SND_ReleaseExChannel(chn);
        }
    }
}

static void TrackFreeChannels(struct SNDTrack *track)
{
    for (struct SNDExChannel *chn = track->channelLLHead; chn; chn = chn->channelLLNext)
    {
        SND_FreeExChannel(chn);
    }

    track->channelLLHead = NULL;
}

static void PlayerSeqMain(struct SNDPlayer *player)
{
    int ticks = 0;
    int tempoInc;
    int i;

    while (player->tempoCounter >= SND_TIMER_RATE)
    {
        player->tempoCounter -= SND_TIMER_RATE;
        ticks++;
    }

    for (i = 0; i < ticks; i++)
    {
        if (PlayerStepTicks(player, SND_PROC_PLAY_NOTES))
        {
            PlayerStop(player);
            break;
        }
    }

    if (SNDi_SharedWork)
    {
        SNDi_SharedWork->players[player->playerId].tickCounter += i;
    }

    tempoInc = player->tempo;
    tempoInc *= player->tempoRatio;
    tempoInc >>= 8;

    player->tempoCounter += tempoInc;
}

static struct SNDTrack *PlayerGetTrack(struct SNDPlayer *player, int track)
{
    if (track > (SND_TRACK_COUNT_PER_PLAYER - 1))
        return NULL;

    if (player->tracks[track] == 0xFF)
        return NULL;

    return &SNDi_Work.tracks[player->tracks[track]];
}

static void TrackStop(struct SNDTrack *track, struct SNDPlayer *player)
{
    TrackReleaseChannels(track, player, -1);
    TrackFreeChannels(track);
}

static void PlayerStopTrack(struct SNDPlayer *player, int trackIdx)
{
    struct SNDTrack *track = PlayerGetTrack(player, trackIdx);

    if (track == NULL)
        return;

    TrackStop(track, player);
    SNDi_Work.tracks[player->tracks[trackIdx]].flags.active = FALSE;
    player->tracks[trackIdx] = 0xFF;
}

static void PlayerStop(struct SNDPlayer *player)
{
    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
    {
        PlayerStopTrack(player, i);
    }
    player->flags.active = FALSE;
}

static void ChannelCallback(struct SNDExChannel *chn, int status, void *track_)
{
    struct SNDExChannel *cur;
    struct SNDTrack *track;

    track = (struct SNDTrack *)track_;

    if (status == 1)
    {
        chn->priority = 0;
        SND_FreeExChannel(chn);
    }

    if (track->channelLLHead == chn)
    {
        track->channelLLHead = chn->channelLLNext;
    }
    else
    {
        cur = track->channelLLHead;

        while (cur->channelLLNext)
        {
            if (cur->channelLLNext == chn)
            {
                cur->channelLLNext = chn->channelLLNext;
                return;
            }
            cur = cur->channelLLNext;
        }
    }
}

static void TrackUpdateChannel(struct SNDTrack *track, struct SNDPlayer *player, int release)
{
    int vol;
    int fader;
    int pan;
    int pitch;

    vol = SNDi_DecibelSquareTable[track->volume] + SNDi_DecibelSquareTable[track->expression] +
          SNDi_DecibelSquareTable[player->volume];

    fader = track->extFader + player->extFader;

    pitch = track->pitchBend;
    pitch *= track->bendRange << 6;
    pitch >>= 7;
    pitch += track->extPitch;

    pan = track->pan;

    if (track->panRange != 127)
        pan = (pan * track->panRange + 0x40) >> 7;

    pan += track->extPan;

    if (vol < -0x8000)
        vol = -0x8000;

    if (fader < -0x8000)
        fader = -0x8000;

    if (pan < -128)
        pan = -128;
    else if (pan > 127)
        pan = 127;

    for (struct SNDExChannel *chn = track->channelLLHead; chn != NULL; chn = chn->channelLLNext)
    {
        chn->userDecay2 = (s16)fader;

        if (chn->envStatus == 3)
            continue;

        chn->userDecay = (s16)vol;
        chn->userPitch = (s16)pitch;
        chn->userPan = (s8)pan;
        chn->panRange = track->panRange;
        chn->lfo.param = track->mod;

        if (chn->length == 0 && release != 0)
        {
            chn->priority = 1;
            SND_ReleaseExChannel(chn);
        }
    }
}

static void PlayerUpdateChannel(struct SNDPlayer *player)
{
    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
    {
        struct SNDTrack *trk = PlayerGetTrack(player, i);
        if (trk)
        {
            TrackUpdateChannel(trk, player, 1);
        }
    }
}

static void TrackPlayNote(
    struct SNDTrack *track, struct SNDPlayer *player, int midiKey, int velocity, int length)
{
    struct SNDExChannel *chn = NULL;

    if (track->flags.tie)
    {
        chn = track->channelLLHead;
        if (chn)
        {
            chn->midiKey = (u8)midiKey;
            chn->velocity = (u8)velocity;
        }
    }

    if (chn == NULL)
    {
        struct SNDInstData inst;
        if (!SND_ReadInstData(player->bank, track->program, midiKey, &inst))
            return;

        u32 allowedChannels;

        // get bitmask with allocatable channels based on channel type
        switch (inst.type)
        {
            case SND_INST_PCM:
            case SND_INST_DIRECTPCM:
                // all channels support PCM
                allowedChannels = 0xFFFF;
                break;
            case SND_INST_PSG:
                // only channels 8, 9, 10, 11, 12, 13 support PSG
                allowedChannels = 0x3F00;
                break;
            case SND_INST_NOISE:
                // only channels 14 and 15 support noise
                allowedChannels = 0xC000;
                break;
            default:
                return;
        }

        allowedChannels &= track->channelMask;

        chn = SND_AllocExChannel(allowedChannels,
            player->prio + track->priority,
            track->flags.channelMask,
            ChannelCallback,
            track);
        if (chn == NULL)
            return;

        if (!SND_NoteOn(
                chn, midiKey, velocity, track->flags.tie ? -1 : length, player->bank, &inst))
        {
            chn->priority = 0;
            SND_FreeExChannel(chn);
            return;
        }

        chn->channelLLNext = track->channelLLHead;
        track->channelLLHead = chn;
    }

    if (track->envAttack != 0xFF)
        SND_SetExChannelAttack(chn, track->envAttack);

    if (track->envDecay != 0xFF)
        SND_SetExChannelDecay(chn, track->envDecay);

    if (track->envSustain != 0xFF)
        SND_SetExChannelSustain(chn, track->envSustain);

    if (track->envRelease != 0xFF)
        SND_SetExChannelRelease(chn, track->envRelease);

    chn->sweepPitch = track->sweepPitch;
    if (track->flags.portamento)
        chn->sweepPitch += (s16)((track->portamentoKey - midiKey) << 6);

    if (track->portamentoTime == 0)
    {
        chn->sweepLength = length;
        chn->flags.autoSweep = FALSE;
    }
    else
    {
        int swp = track->portamentoTime * track->portamentoTime;
        swp *= chn->sweepPitch < 0 ? -chn->sweepPitch : chn->sweepPitch;
        swp >>= 11;
        chn->sweepLength = swp;
    }

    chn->sweepCounter = 0;
}

static int TrackStepTicks(
    struct SNDTrack *track, struct SNDPlayer *player, int trackIdx, u32 playNotes)
{
    (void)trackIdx;

    struct SNDExChannel *chn;
    u8 cmd;
    enum SNDSeqVal valueType;
    BOOL specialValueType;
    BOOL runCmd;
    s32 length;
    int midiKey;

    s32 par;

    for (chn = track->channelLLHead; chn; chn = chn->channelLLNext)
    {
        if (chn->length > 0)
            chn->length--;

        if (!chn->flags.autoSweep && chn->sweepCounter < chn->sweepLength)
            chn->sweepCounter++;
    }

    if (track->flags.noteFinishWait)
    {
        if (track->channelLLHead)
            return 0;
        track->flags.noteFinishWait = FALSE;
    }

    if (track->wait > 0)
    {
        track->wait--;
        if (track->wait > 0)
            return 0;
    }

    SeqCacheFetch(track->cur);

    while (track->wait == 0 && !track->flags.noteFinishWait)
    {
        specialValueType = FALSE;
        runCmd = TRUE;

        cmd = TrackReadU8(track);

        if (cmd == 0xA2)
        {
            cmd = TrackReadU8(track);
            runCmd = track->flags.cmp;
        }

        if (cmd == 0xA0)
        {
            cmd = TrackReadU8(track);
            valueType = SND_SEQ_VAL_RAN;
            specialValueType = TRUE;
        }

        if (cmd == 0xA1)
        {
            cmd = TrackReadU8(track);
            valueType = SND_SEQ_VAL_VAR;
            specialValueType = TRUE;
        }

        if ((cmd & 0x80) == 0)
        {
            par = TrackReadU8(track);

            length = TrackParseValue(track, player, specialValueType ? valueType : SND_SEQ_VAL_VLV);
            midiKey = cmd + track->transpose;

            if (!runCmd)
                continue;

            if (midiKey < 0)
                midiKey = 0;
            else if (midiKey > 127)
            {
                midiKey = 127;
            }

            if (!track->flags.muted && playNotes != 0)
            {
                TrackPlayNote(track, player, midiKey, par, (length > 0) ? length : -1);
            }

            track->portamentoKey = (u8)midiKey;

            if (track->flags.noteWait)
            {
                track->wait = length;
                if (length == 0)
                {
                    track->flags.noteFinishWait = TRUE;
                }
            }

            continue;
        }

        switch (cmd & 0xF0)
        {
            case 0x80:
                par =
                    TrackParseValue(track, player, specialValueType ? valueType : SND_SEQ_VAL_VLV);
                if (!runCmd)
                    break;

                switch (cmd)
                {
                    case 0x80:
                        track->wait = par;
                        break;
                    case 0x81:
                        if (par < 0x10000)
                            track->program = (u16)par;
                        break;
                }
                break;
            case 0x90:
                switch (cmd)
                {
                    case 0x93:
                    {
                        u32 off;
                        struct SNDTrack *newTrack;

                        par = TrackReadU8(track);
                        off = TrackReadU24(track);
                        if (!runCmd)
                            break;

                        newTrack = PlayerGetTrack(player, par);
                        if (newTrack && newTrack != track)
                        {
                            TrackStop(newTrack, player);
                            TrackStart(newTrack, track->base, off);
                        }
                    }
                    break;
                    case 0x94:
                    {
                        u32 off = TrackReadU24(track);
                        if (!runCmd)
                            break;
                        track->cur = &track->base[off];
                    }
                    break;
                    case 0x95:
                    {
                        u32 off = TrackReadU24(track);
                        if (!runCmd)
                            break;

                        if (track->callStackDepth < SND_TRACK_MAX_CALL)
                        {
                            track->posCallStack[track->callStackDepth] = track->cur;
                            track->callStackDepth++;
                            track->cur = &track->base[off];
                        }
                        break;
                    }
                }
                break;
            case 0xC0:
            case 0xD0:
            {
                union
                {
                    u8 _u8;
                    s8 _s8;
                } par;
                par._u8 = (u8)TrackParseValue(
                    track, player, specialValueType ? valueType : SND_SEQ_VAL_U8);
                if (!runCmd)
                    break;

                switch (cmd)
                {
                    case 0xC1:
                        track->volume = par._u8;
                        break;
                    case 0xD5:
                        track->expression = par._u8;
                        break;
                    case 0xC2:
                        player->volume = par._u8;
                        break;
                    case 0xC5:
                        track->bendRange = par._u8;
                        break;
                    case 0xC6:
                        track->priority = par._u8;
                        break;
                    case 0xC7:
                        track->flags.noteWait = par._u8;
                        break;
                    case 0xCF:
                        track->portamentoTime = par._u8;
                        break;
                    case 0xCA:
                        track->mod.depth = par._u8;
                        break;
                    case 0xCB:
                        track->mod.speed = par._u8;
                        break;
                    case 0xCC:
                        track->mod.target = par._u8;
                        break;
                    case 0xCD:
                        track->mod.range = par._u8;
                        break;
                    case 0xD0:
                        track->envAttack = par._u8;
                        break;
                    case 0xD1:
                        track->envDecay = par._u8;
                        break;
                    case 0xD2:
                        track->envSustain = par._u8;
                        break;
                    case 0xD3:
                        track->envRelease = par._u8;
                        break;
                    case 0xD4:
                        if (track->callStackDepth < SND_TRACK_MAX_CALL)
                        {
                            track->posCallStack[track->callStackDepth] = track->cur;
                            track->loopCount[track->callStackDepth] = par._u8;
                            track->callStackDepth++;
                        }
                        break;
                    case 0xC8:
                        track->flags.tie = par._u8;
                        TrackReleaseChannels(track, player, -1);
                        TrackFreeChannels(track);
                        break;
                    case 0xD7:
                        TrackMute(track, player, par._u8);
                        break;
                    case 0xC9:
                        track->portamentoKey = (u8)(par._u8 + track->transpose);
                        track->flags.portamento = TRUE;
                        break;
                    case 0xCE:
                        track->flags.portamento = par._u8;
                        break;
                    case 0xC3:
                        track->transpose = par._s8;
                        break;
                    case 0xC4:
                        track->pitchBend = par._s8;
                        break;
                    case 0xC0:
                        track->pan = (s8)(par._u8 - 0x40);
                        break;
                    case 0xD6:
                    {
                        s16 *varPtr;

                        if (sPrintEnabled)
                        {
                            varPtr = PlayerGetVariablePointer(player, par._u8);
                        }
                    }
                    break;
                }
            }
            break;
            case 0xE0:
            {
                s16 par = (s16)TrackParseValue(
                    track, player, specialValueType ? valueType : SND_SEQ_VAL_U16);
                if (!runCmd)
                    break;

                switch (cmd)
                {
                    case 0xE3:
                        track->sweepPitch = par;
                        break;
                    case 0xE1:
                        player->tempo = (u16)par;
                        break;
                    case 0xE0:
                        track->mod.delay = (u16)par;
                        break;
                }
            }
            break;
            case 0xB0:
            {
                int varNum = TrackReadU8(track);

                s16 par = (s16)TrackParseValue(
                    track, player, specialValueType ? valueType : SND_SEQ_VAL_U16);
                s16 *varPtr = PlayerGetVariablePointer(player, varNum);

                if (!runCmd)
                    break;

                if (varPtr == NULL)
                    break;

                switch (cmd)
                {
                    case 0xB0:
                        *varPtr = par;
                        break;
                    case 0xB1:
                        *varPtr += par;
                        break;
                    case 0xB2:
                        *varPtr -= par;
                        break;
                    case 0xB3:
                        *varPtr *= par;
                        break;
                    case 0xB4:
                        if (par != 0)
                            *varPtr /= par;
                        break;
                    case 0xB5:
                        if (par >= 0)
                            *varPtr <<= par;
                        else
                            *varPtr >>= -par;
                        break;
                    case 0xB6:
                    {
                        BOOL neg = FALSE;
                        if (par < 0)
                        {
                            neg = TRUE;
                            par = (s16)(-par);
                        }
                        s32 random = SND_CalcRandom();
                        random = (random * (par + 1)) >> 16;
                        if (neg)
                            random = -random;
                        *varPtr = (s16)random;
                    }
                    break;
                    case 0xB7:
                        break;
                    case 0xB8:
                        track->flags.cmp = *varPtr == par;
                        break;
                    case 0xB9:
                        track->flags.cmp = *varPtr >= par;
                        break;
                    case 0xBA:
                        track->flags.cmp = *varPtr > par;
                        break;
                    case 0xBB:
                        track->flags.cmp = *varPtr <= par;
                        break;
                    case 0xBC:
                        track->flags.cmp = *varPtr < par;
                        break;
                    case 0xBD:
                        track->flags.cmp = *varPtr != par;
                        break;
                }
            }
            break;
            case 0xF0:
                if (!runCmd)
                    break;

                switch (cmd)
                {
                    case 0xFD:
                        if (track->callStackDepth != 0)
                        {
                            track->callStackDepth--;
                            track->cur = track->posCallStack[track->callStackDepth];
                        }
                        break;
                    case 0xFC:
                    {
                        if (track->callStackDepth == 0)
                            break;

                        // gosh, this was nasty to figure out
                        u8 loopCount = track->loopCount[track->callStackDepth - 1];
                        if (loopCount != 0)
                        {
                            loopCount--;
                            if (loopCount == 0)
                            {
                                track->callStackDepth--;
                                break;
                            }
                        }
                        track->loopCount[track->callStackDepth - 1] = loopCount;
                        track->cur = track->posCallStack[track->callStackDepth - 1];
                    }
                    break;
                    case 0xFE:
                        break;
                    case 0xFF:
                        return -1;
                }
                break;
        }
    }

    return 0;
}

static BOOL PlayerStepTicks(struct SNDPlayer *player, u32 ticks)
{
    BOOL isPlaying = FALSE;

    for (int i = 0; i < SND_TRACK_COUNT_PER_PLAYER; i++)
    {
        struct SNDTrack *trk = PlayerGetTrack(player, i);

        if (trk && trk->cur)
        {
            if (TrackStepTicks(trk, player, i, ticks))
                PlayerStopTrack(player, i);
            else
                isPlaying = TRUE;
        }
    }

    return !isPlaying;
}

static s16 *PlayerGetVariablePointer(const struct SNDPlayer *player, int var)
{
    if (SNDi_SharedWork == NULL)
        return NULL;
    else if (var < 16)
    {
        return &SNDi_SharedWork->players[player->playerId].localVars[var];
    }
    else
    {
        return &SNDi_SharedWork->globalVars[var - 16];
    }
}

static int AllocateTrack(void)
{
    for (int i = 0; i < SND_TRACK_COUNT; i++)
    {
        if (!SNDi_Work.tracks[i].flags.active)
        {
            SNDi_Work.tracks[i].flags.active = TRUE;
            return i;
        }
    }

    return -1;
}

static void TrackMute(struct SNDTrack *track, struct SNDPlayer *player, int muteMode)
{
    switch (muteMode)
    {
        case SND_TRACK_MUTE_MODE_UNMUTE:
            track->flags.muted = FALSE;
            break;
        case SND_TRACK_MUTE_MODE_MUTE:
            track->flags.muted = TRUE;
            break;
        case SND_TRACK_MUTE_MODE_MUTE_RELEASE:
            track->flags.muted = TRUE;
            TrackReleaseChannels(track, player, -1);
            break;
        case SND_TRACK_MUTE_MODE_MUTE_STOP:
            track->flags.muted = TRUE;
            TrackReleaseChannels(track, player, 127);
            TrackFreeChannels(track);
            break;
    }
}
