view tools/ppl.c @ 2305:3bcad61594b4

Cleanup.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 08 Jul 2019 10:28:00 +0300
parents 2c90e455f006
children b7cd5dd0b82e
line wrap: on
line source

/*
 * Cyrbe Pasci Player - A simple SDL-based UI for XM module playing
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2012-2019 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmlib.h"
#include <SDL.h>
#include "libgutil.h"

#include "jss.h"
#include "jssmod.h"
#include "jssmix.h"
#include "jssplr.h"

#include "dmargs.h"
#include "dmimage.h"
#include "dmtext.h"

#include "setupfont.h"


struct
{
    BOOL exitFlag;

    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Texture *texture;
    SDL_Surface *screen;
    SDL_Event event;

    int optScrWidth, optScrHeight, optVFlags, optScrDepth;

    int actChannel;
    BOOL pauseFlag;

    JSSModule *mod;
    JSSMixer *dev;
    JSSPlayer *plr;
    SDL_AudioSpec afmt;
} eng;

struct
{
    Uint32
        boxBg, inboxBg,
        box1, box2,
        viewDiv,
        activeRow, activeChannel,
        scope, muted, black, red;
} col;


DMBitmapFont *font = NULL;

char    *optFilename = NULL;
int     optOutFormat = JSS_AUDIO_S16,
        optOutChannels = 2,
        optOutFreq = 48000,
        optMuteOChannels = -1,
        optStartOrder = 0;
BOOL    optUsePlayTime = FALSE,
        optUseGUI = TRUE;
size_t  optPlayTime;


static const DMOptArg optList[] =
{
    {  0, '?', "help",     "Show this help", OPT_NONE },
    {  1, 'v', "verbose",  "Be more verbose", OPT_NONE },
    {  2,   0, "fs",       "Fullscreen", OPT_NONE },
    { 12, 'C', "cli",      "Do not open GUI window", OPT_NONE },
    {  3, 'w', "window",   "Initial window size/resolution -w 640x480", OPT_ARGREQ },

    {  4, '1', "16bit",    "16-bit output", OPT_NONE },
    {  5, '8', "8bit",     "8-bit output", OPT_NONE },
    {  6, 'm', "mono",     "Mono output", OPT_NONE },
    {  7, 's', "stereo",   "Stereo output", OPT_NONE },
    {  8, 'f', "freq",     "Output frequency", OPT_ARGREQ },

    {  9, 'M', "mute",     "Mute other channels than #", OPT_ARGREQ },
    { 10, 'o', "order",    "Start from order #", OPT_ARGREQ },
    { 11, 't', "time",     "Play for # seconds", OPT_ARGREQ },
};

const int optListN = sizeof(optList) / sizeof(optList[0]);


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options] <module>");
    dmArgsPrintHelp(stdout, optList, optListN, 0);
}


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    switch (optN) {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            dmVerbosity++;
            break;

        case 2:
            eng.optVFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
            break;

        case 3:
            {
                int w, h;
                if (sscanf(optArg, "%dx%d", &w, &h) == 2)
                {
                    if (w < 320 || h < 200 || w > 3200 || h > 3200)
                    {
                        dmErrorMsg("Invalid width or height: %d x %d\n", w, h);
                        return FALSE;
                    }
                    eng.optScrWidth = w;
                    eng.optScrHeight = h;
                }
                else
                {
                    dmErrorMsg("Invalid size argument '%s'.\n", optArg);
                    return FALSE;
                }
            }
            break;

        case 4:
            optOutFormat = JSS_AUDIO_S16;
            break;

        case 5:
            optOutFormat = JSS_AUDIO_U8;
            break;

        case 6:
            optOutChannels = JSS_AUDIO_MONO;
            break;

        case 7:
            optOutChannels = JSS_AUDIO_STEREO;
            break;

        case 8:
            optOutFreq = atoi(optArg);
            break;

        case 9:
            optMuteOChannels = atoi(optArg);
            break;

        case 10:
            optStartOrder = atoi(optArg);
            break;

        case 11:
            optPlayTime = atoi(optArg);
            optUsePlayTime = TRUE;
            break;

        case 12:
            optUseGUI = FALSE;
            break;

        default:
            dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (!optFilename)
        optFilename = currArg;
    else
    {
        dmErrorMsg("Too many filename arguments '%s'\n", currArg);
        return FALSE;
    }

    return TRUE;
}


static inline Uint32 dmCol(const float r, const float g, const float b)
{
    return dmMapRGB(eng.screen, 255.0f * r, 255.0f * g, 255.0f * b);
}


BOOL dmInitializeVideo()
{
    SDL_DestroyTexture(eng.texture);
    SDL_FreeSurface(eng.screen);

    if ((eng.texture = SDL_CreateTexture(eng.renderer,
        SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING,
        eng.optScrWidth, eng.optScrHeight)) == NULL)
    {
        dmErrorMsg("Could not create SDL texture.\n");
        return FALSE;
    }

    if ((eng.screen = SDL_CreateRGBSurfaceWithFormat(0,
        eng.optScrWidth, eng.optScrHeight,
        32, SDL_PIXELFORMAT_RGBA32)) == NULL)
    {
        dmErrorMsg("Could not create SDL surface.\n");
        return FALSE;
    }

    col.black          = dmCol(0  , 0  , 0);
    col.inboxBg        = dmCol(0.6, 0.5, 0.2);
    col.boxBg          = dmCol(0.7, 0.6, 0.3);
    col.box1           = dmCol(1.0, 0.9, 0.6);
    col.box2           = dmCol(0.3, 0.3, 0.15);
    col.viewDiv        = dmCol(0  , 0  , 0);
    col.activeRow      = dmCol(0.5, 0.4, 0.1);
    col.activeChannel  = dmCol(0.6, 0.8, 0.2);
    col.muted          = dmCol(0.3, 0.1, 0.1);
    col.scope          = dmCol(0  , 0.8, 0);
    col.red            = dmCol(1  , 0  , 0);

    return TRUE;
}


//
// XXX TODO: To display actual continuous sample data for channel,
// we would need to have separate "FIFO" buffers for each, updating
// them with new data from incoming channel data.
//
void dmDisplayChn(SDL_Surface *screen, int x0, int y0, int x1, int y1, int nchannel, JSSChannel *chn)
{
    int yh = y1 - y0 - 2;
    if (yh < 10 || chn == NULL)
        return;

    int xc, ym = y0 + (y1 - y0) / 2, vol = FP_GETH32(chn->chVolume);
    DMFixedPoint offs = chn->chPos;
    int len = FP_GETH32(chn->chSize);
    Uint32 pitch = screen->pitch / sizeof(Uint32);
    Uint32 *pix = screen->pixels;
    Sint16 *data = chn->chData;

    dmFillBox3D(screen, x0, y0, x1, y1,
        (chn->chMute ? col.muted : col.black),
        nchannel == eng.actChannel ? col.red : col.box2,
        nchannel == eng.actChannel ? col.red : col.box1);

    if (chn->chData == NULL || !chn->chPlaying)
        return;

    if (chn->chDirection)
    {
        for (xc = x0 + 1; xc < x1 - 1; xc++)
        {
            if (FP_GETH32(offs) >= len)
                break;
            Sint16 val = ym + (data[FP_GETH32(offs)] * yh * vol) / (65535 * 255);
            pix[xc + val * pitch] = col.scope;
            FP_ADD(offs, chn->chDeltaO);
        }
    }
    else
    {
        for (xc = x0 + 1; xc < x1 - 1; xc++)
        {
            if (FP_GETH32(offs) < 0)
                break;
            Sint16 val = ym + (data[FP_GETH32(offs)] * yh * vol) / (65535 * 255);
            pix[xc + val * pitch] = col.scope;
            FP_SUB(offs, chn->chDeltaO);
        }
    }
}


void dmDisplayChannels(SDL_Surface *screen, int x0, int y0, int x1, int y1, JSSMixer *dev)
{
    int nchannel, qx, qy,
        qwidth = x1 - x0,
        qheight = y1 - y0,
        nwidth = jsetNChannels,
        nheight = 1;

    if (qheight < 40)
        return;

    while (qwidth / nwidth <= 60 && qheight / nheight >= 40)
    {
        nheight++;
        nwidth /= nheight;
    }

//    fprintf(stderr, "%d x %d\n", nwidth, nheight);

    if (qheight / nheight <= 40)
    {
        nwidth = qwidth / 60;
        nheight = qheight / 40;
    }

    qwidth /= nwidth;
    qheight /= nheight;

    for (nchannel = qy = 0; qy < nheight && nchannel < jsetNChannels; qy++)
    {
        for (qx = 0; qx < nwidth && nchannel < jsetNChannels; qx++)
        {
            int xc = x0 + qx * qwidth,
                yc = y0 + qy * qheight;

            dmDisplayChn(screen, xc + 1, yc + 1,
                xc + qwidth - 1, yc + qheight - 1,
                nchannel, &dev->channels[nchannel]);

            nchannel++;
        }
    }
}


static const char patNoteTable[12][3] =
{
    "C-", "C#", "D-",
    "D#", "E-", "F-",
    "F#", "G-", "G#",
    "A-", "A#", "B-"
};


#define jmpNMODEffectTable (36)
static const char jmpMODEffectTable[jmpNMODEffectTable] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

static const char jmpHexTab[16] = "0123456789ABCDEF";

static inline char dmHexVal(int v)
{
    return jmpHexTab[v & 15];
}


void dmPrintNote(SDL_Surface *screen, int xc, int yc, JSSNote *n)
{
    char text[32];
    char *ptr = text;

    switch (n->note)
    {
        case jsetNotSet:
            strcpy(ptr, "..._");
            break;
        case jsetNoteOff:
            strcpy(ptr, "===_");
            break;
        default:
            sprintf(ptr, "%s%i_",
                patNoteTable[n->note % 12],
                n->note / 12);
            break;
    }

    ptr += 4;

    if (n->instrument != jsetNotSet)
    {
        int v = n->instrument + 1;
        *ptr++ = dmHexVal(v >> 4);
        *ptr++ = dmHexVal(v);
    }
    else
    {
        *ptr++ = '.';
        *ptr++ = '.';
    }
    *ptr++ = '_';

    if (n->volume == jsetNotSet)
    {
        *ptr++ = '.';
        *ptr++ = '.';
    }
    else
    if (n->volume >= 0x00 && n->volume <= 0x40)
    {
        *ptr++ = dmHexVal(n->volume >> 4);
        *ptr++ = dmHexVal(n->volume);
    }
    else
    {
        char c;
        switch (n->volume & 0xf0)
        {
            case 0x50: c = '-'; break;
            case 0x60: c = '+'; break;
            case 0x70: c = '/'; break;
            case 0x80: c = '\\'; break;
            case 0x90: c = 'S'; break;
            case 0xa0: c = 'V'; break;
            case 0xb0: c = 'P'; break;
            case 0xc0: c = '<'; break;
            case 0xd0: c = '>'; break;
            case 0xe0: c = 'M'; break;
            default:   c = '?'; break;
        }
        *ptr++ = c;
        *ptr++ = dmHexVal(n->volume);
    }
    *ptr++ = '_';

    if (n->effect >= 0 && n->effect < jmpNMODEffectTable)
        *ptr++ = jmpMODEffectTable[n->effect];
    else
        *ptr++ = (n->effect == jsetNotSet ? '.' : '?');

    if (n->param != jsetNotSet)
    {
        *ptr++ = dmHexVal(n->param >> 4);
        *ptr++ = dmHexVal(n->param);
    }
    else
    {
        *ptr++ = '.';
        *ptr++ = '.';
    }

    *ptr = 0;

    dmDrawBMTextConstQ(screen, font, DMD_TRANSPARENT, xc, yc, text);
}


void dmDisplayPattern(SDL_Surface *screen, int x0, int y0, int x1, int y1, JSSPattern *pat, int row)
{
    int cwidth = (font->width * 10 + 3 * 4 + 5),
        lwidth = 6 + font->width * 3,
        qy0 = y0 + font->height + 2,
        qy1 = y1 - font->height - 2,
        qwidth  = ((x1 - x0 - lwidth) / cwidth),
        qheight = ((qy1 - qy0 - 4) / (font->height + 1)),
        nrow, nchannel, yc, choffs,
        midrow = qheight / 2;

    if (pat == NULL)
        return;

    if (qwidth > pat->nchannels)
        qwidth = pat->nchannels;

    if (eng.actChannel < qwidth / 2)
        choffs = 0;
    else
    if (eng.actChannel >= pat->nchannels - qwidth/2)
        choffs = pat->nchannels - qwidth;
    else
        choffs = eng.actChannel - qwidth/2;

    dmDrawBox3D(screen, x0 + lwidth, qy0, x1, qy1, col.box2, col.box1);

    for (nchannel = 0; nchannel < qwidth; nchannel++)
    {
        int bx0 = x0 + lwidth + 1 + nchannel * cwidth,
            bx1 = bx0 + cwidth;

        dmFillRect(screen, bx0+1, qy0+1, bx1-1, qy1-1,
            (eng.actChannel == nchannel + choffs) ? col.activeChannel : col.inboxBg);
    }

    yc = qy0 + 2 + (font->height + 1) * midrow;
    dmFillRect(screen, x0 + lwidth + 1, yc - 1, x1 - 1, yc + font->height, col.activeRow);

    for (nchannel = 0; nchannel < qwidth; nchannel++)
    {
        int bx0 = x0 + lwidth + 1 + nchannel * cwidth,
            bx1 = bx0 + cwidth;

        dmDrawVLine(screen, qy0 + 1, qy1 - 1, bx1, col.viewDiv);

        if (jvmGetMute(eng.dev, nchannel + choffs))
        {
            dmDrawBMTextConstQ(screen, font, DMD_TRANSPARENT,
                bx0 + (cwidth - font->width * 5) / 2, qy1 + 3, "MUTED");
        }

        dmDrawBMTextQ(screen, font, DMD_TRANSPARENT,
            bx0 + (cwidth - font->width * 3) / 2, y0 + 1, "%3d",
            nchannel + choffs);
    }

    for (nrow = 0; nrow < qheight; nrow++)
    {
        int crow = nrow - midrow + row;
        yc = qy0 + 2 + (font->height + 1) * nrow;

        if (crow >= 0 && crow < pat->nrows)
        {
            dmDrawBMTextQ(screen, font, DMD_TRANSPARENT, x0, yc, "%03d", crow);

            for (nchannel = 0; nchannel < qwidth; nchannel++)
            {
                if (choffs + nchannel >= pat->nchannels)
                    break;

                dmPrintNote(screen, x0 + lwidth + 4 + nchannel * cwidth, yc,
                    pat->data + (pat->nchannels * crow) + choffs + nchannel);
            }
        }
    }
}


void audioCallback(void *userdata, Uint8 *stream, int len)
{
    JSSMixer *d = (JSSMixer *) userdata;

    if (d != NULL)
    {
        jvmRenderAudio(d, stream, len / jvmGetSampleSize(d));
    }
}


void dmMuteChannels(BOOL mute)
{
    for (int i = 0; i < eng.mod->nchannels; i++)
        jvmMute(eng.dev, i, mute);
}


int main(int argc, char *argv[])
{
    BOOL initSDL = FALSE, audioInit = FALSE;
    DMResource *file = NULL;
    int result = -1;
    BOOL muteState = FALSE;

    dmMemset(&eng, 0, sizeof(eng));

    eng.optScrWidth = 640;
    eng.optScrHeight = 480;
    eng.optScrDepth = 32;

    dmInitProg("CBP", "Cyrbe Basci Player", "0.2", NULL, NULL);

    // Parse arguments
    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        exit(1);

    // Open the files
    if (optFilename == NULL)
    {
        dmErrorMsg("No filename specified.\n");
        return 1;
    }

    if ((result = dmf_open_stdio(optFilename, "rb", &file)) != DMERR_OK)
    {
        dmErrorMsg("Error opening file '%s', %d: (%s)\n",
            optFilename, result, dmErrorStr(result));
        return 1;
    }

    // Initialize miniJSS
    jssInit();

    // Read module file
    dmMsg(1, "Reading file: %s\n", optFilename);
#ifdef JSS_SUP_XM
    result = jssLoadXM(file, &eng.mod, TRUE);
#endif
#ifdef JSS_SUP_JSSMOD
    dmfreset(file);
    if (result != DMERR_OK)
    {
        dmMsg(1, "* Trying JSSMOD ...\n");
        result = jssLoadJSSMOD(file, &eng.mod, TRUE);
        dmfreset(file);
        if (result == DMERR_OK)
            result = jssLoadJSSMOD(file, &eng.mod, FALSE);
    }
    else
    {
        dmMsg(2, "* Trying XM...\n");
        result = jssLoadXM(file, &eng.mod, FALSE);
    }
#endif
    dmf_close(file);

    if (result != DMERR_OK)
    {
        dmErrorMsg("Error loading module file, %d: %s\n",
            result, dmErrorStr(result));
        goto exit;
    }

    // Try to convert it
    if ((result = jssConvertModuleForPlaying(eng.mod)) != DMERR_OK)
    {
        dmErrorMsg("Could not convert module for playing, %d: %s\n",
            result, dmErrorStr(result));
        goto exit;
    }


    // Initialize SDL components
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0)
    {
        dmErrorMsg("Could not initialize SDL: %s\n", SDL_GetError());
        goto exit;
    }
    initSDL = TRUE;


    // Initialize mixing device
    dmMsg(2, "Initializing miniJSS mixer with: %d, %d, %d\n",
        optOutFormat, optOutChannels, optOutFreq);

    eng.dev = jvmInit(optOutFormat, optOutChannels, optOutFreq, JMIX_AUTO);
    if (eng.dev == NULL)
    {
        dmErrorMsg("jvmInit() returned NULL\n");
        goto exit;
    }

    switch (optOutFormat)
    {
        case JSS_AUDIO_S16: eng.afmt.format = AUDIO_S16SYS; break;
        case JSS_AUDIO_U16: eng.afmt.format = AUDIO_U16SYS; break;
        case JSS_AUDIO_S8:  eng.afmt.format = AUDIO_S8; break;
        case JSS_AUDIO_U8:  eng.afmt.format = AUDIO_U8; break;
        default:
            dmErrorMsg("Unsupported audio format %d (could not set matching SDL format)\n",
                optOutFormat);
            goto exit;
    }

    eng.afmt.freq     = optOutFreq;
    eng.afmt.channels = optOutChannels;
    eng.afmt.samples  = optOutFreq / 16;
    eng.afmt.callback = audioCallback;
    eng.afmt.userdata = (void *) eng.dev;

    // Open the audio device
    if (SDL_OpenAudio(&eng.afmt, NULL) < 0)
    {
        dmErrorMsg("Couldn't open SDL audio: %s\n",
            SDL_GetError());
        goto exit;
    }
    audioInit = TRUE;

    // Initialize player
    if ((eng.plr = jmpInit(eng.dev)) == NULL)
    {
        dmErrorMsg("jmpInit() returned NULL\n");
        goto exit;
    }

    jvmSetCallback(eng.dev, jmpExec, eng.plr);
    jmpSetModule(eng.plr, eng.mod);
    jmpPlayOrder(eng.plr, optStartOrder);
    jvmSetGlobalVol(eng.dev, 200);

    if (optMuteOChannels >= 0 && optMuteOChannels < eng.mod->nchannels)
    {
        dmMuteChannels(TRUE);
        jvmMute(eng.dev, optMuteOChannels, FALSE);
        eng.actChannel = optMuteOChannels;
        muteState = TRUE;
    }

    if (optUseGUI)
    {
        // Get font
        result = dmf_open_memio(NULL, "pplfont.fnt", engineSetupFont, sizeof(engineSetupFont), &file);
        if (result != DMERR_OK)
        {
            dmErrorMsg("Error opening font file 'pplfont.fnt', #%d: %s\n",
                result, dmErrorStr(result));
            goto exit;
        }
        result = dmLoadBitmapFont(file, &font);
        dmf_close(file);
        if (result != DMERR_OK)
        {
            dmErrorMsg("Could not load font from file, %d: %s\n",
                result, dmErrorStr(result));
            goto exit;
        }

        SDL_Color pal[DMFONT_NPALETTE];
        for (int n = 0; n < DMFONT_NPALETTE; n++)
        {
            pal[n].r = pal[n].g = pal[n].b = 0;
            pal[n].a = n > 0 ? 255 : 0;
        }
        dmSetBitmapFontPalette(font, pal, 0, DMFONT_NPALETTE);

        // Open window
        if ((eng.window = SDL_CreateWindow(dmProgName,
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
            eng.optScrWidth, eng.optScrHeight,
            eng.optVFlags | SDL_WINDOW_RESIZABLE
            //| SDL_WINDOW_HIDDEN
            )) == NULL)
        {
            dmErrorMsg("Can't create an SDL window: %s\n", SDL_GetError());
            goto exit;
        }

        SDL_SetWindowTitle(eng.window, dmProgDesc);

        if ((eng.renderer = SDL_CreateRenderer(eng.window, -1, SDL_RENDERER_PRESENTVSYNC)) == NULL)
        {
            dmErrorMsg("Can't create an SDL renderer: %s\n", SDL_GetError());
            goto exit;
        }

        if (!dmInitializeVideo())
            goto exit;

        //SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
        //SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
    }

    // okay, main loop here ... "play" module and print out info
    SDL_LockAudio();
    SDL_PauseAudio(0);
    SDL_UnlockAudio();

    int currTick, prevTick = 0, prevRow = -1;
    while (!eng.exitFlag)
    {
        currTick = SDL_GetTicks();
        BOOL needUpdate = (currTick - prevTick > 500),
             needRender = FALSE;

        while (SDL_PollEvent(&eng.event))
        switch (eng.event.type)
        {
            case SDL_KEYDOWN:
                switch (eng.event.key.keysym.sym)
                {
                    case SDLK_ESCAPE:
                    case SDLK_q:
                        eng.exitFlag = TRUE;
                        break;

                    case SDLK_SPACE:
                        eng.pauseFlag = !eng.pauseFlag;
                        SDL_PauseAudio(eng.pauseFlag);
                        break;

                    case SDLK_LEFT:
                        if (eng.actChannel > 0)
                        {
                            eng.actChannel--;
                            needUpdate = TRUE;
                        }
                        break;

                    case SDLK_RIGHT:
                        if (eng.actChannel < eng.mod->nchannels - 1)
                        {
                            eng.actChannel++;
                            needUpdate = TRUE;
                        }
                        break;

                    case SDLK_m:
                        if (eng.event.key.keysym.mod & KMOD_SHIFT)
                        {
                            muteState = !muteState;
                            dmMuteChannels(muteState);
                        }
                        else
                        if (eng.event.key.keysym.mod & KMOD_CTRL)
                        {
                            dmMuteChannels(FALSE);
                        }
                        else
                        {
                            jvmMute(eng.dev, eng.actChannel, !jvmGetMute(eng.dev, eng.actChannel));
                        }
                        needUpdate = TRUE;
                        break;

                    case SDLK_PAGEUP:
                        JSS_LOCK(eng.dev);
                        JSS_LOCK(eng.plr);
                        jmpChangeOrder(eng.plr, dmClamp(eng.plr->order - 1, 0, eng.mod->norders));
                        JSS_UNLOCK(eng.plr);
                        JSS_UNLOCK(eng.dev);
                        needUpdate = TRUE;
                        break;

                    case SDLK_PAGEDOWN:
                        JSS_LOCK(eng.dev);
                        JSS_LOCK(eng.plr);
                        jmpChangeOrder(eng.plr, dmClamp(eng.plr->order + 1, 0, eng.mod->norders));
                        JSS_UNLOCK(eng.plr);
                        JSS_UNLOCK(eng.dev);
                        needUpdate = TRUE;
                        break;

                    case SDLK_f:
                        eng.optVFlags ^= SDL_WINDOW_FULLSCREEN_DESKTOP;
                        if (SDL_SetWindowFullscreen(eng.window, eng.optVFlags) != 0)
                            goto exit;
                        needUpdate = TRUE;
                        break;

                    default:
                        break;
                }

                break;

            case SDL_WINDOWEVENT:
                switch (eng.event.window.event)
                {
                    case SDL_WINDOWEVENT_EXPOSED:
                        needUpdate = TRUE;
                        break;

                    case SDL_WINDOWEVENT_RESIZED:
                        eng.optScrWidth  = eng.event.window.data1;
                        eng.optScrHeight = eng.event.window.data2;
                        if (!dmInitializeVideo())
                            goto exit;

                        needUpdate = TRUE;
                        break;
                }
                break;

            case SDL_QUIT:
                eng.exitFlag = TRUE;
                break;
        }

        // Check for end of song
        JSS_LOCK(eng.plr);
        if (!eng.plr->isPlaying)
            eng.exitFlag = TRUE;
        JSS_UNLOCK(eng.plr);

        if (optUseGUI)
        {
            // Check if we need to update screen
            JSS_LOCK(eng.plr);
            JSSPattern *currPattern = eng.plr->pattern;
            int currRow = eng.plr->row;
            JSS_UNLOCK(eng.plr);

            if (currRow != prevRow || needUpdate)
            {
                prevRow = currRow;
                needUpdate = TRUE;
            }

            // Draw frame
            if (needUpdate)
            {
                dmClearSurface(eng.screen, col.boxBg);

                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5,
                    "%s v%s by ccr/TNSP - (c) Copyright 2012-2019 TNSP",
                    dmProgDesc, dmProgVersion);

                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2),
                    "Song: '%s'",
                    eng.mod->moduleName);

                dmDisplayPattern(eng.screen, 5, 5 + (font->height + 2) * 3 + 4,
                    eng.screen->w - 6,
                    eng.screen->h * 0.8,
                    currPattern, currRow);

                JSS_LOCK(eng.plr);
                dmDrawBMTextQ(eng.screen, font, DMD_TRANSPARENT, 5, 5 + (font->height + 2) * 2,
                "Tempo: %3d | Speed: %3d | Row: %3d/%-3d | Order: %3d/%-3d | Pattern: %3d/%-3d",
                eng.plr->tempo, eng.plr->speed,
                eng.plr->row, (eng.plr->pattern != NULL) ? eng.plr->pattern->nrows : 0,
                eng.plr->order + 1, eng.mod->norders,
                eng.plr->npattern, eng.mod->npatterns);
                JSS_UNLOCK(eng.plr);
                needRender = TRUE;
            }

            if (needUpdate || currTick - prevTick >= (eng.pauseFlag ? 100 : 20))
            {
                JSS_LOCK(eng.dev);
                dmDisplayChannels(eng.screen, 5, eng.screen->h * 0.8 + 5,
                    eng.screen->w - 5, eng.screen->h - 5, eng.dev);
                JSS_UNLOCK(eng.dev);
                needRender = TRUE;
            }

            if (needUpdate)
                prevTick = currTick;

            // Flip screen
            if (needRender)
            {
                SDL_Surface dst;
                SDL_LockTexture(eng.texture, NULL, &dst.pixels, &dst.pitch);

                if (dst.pitch != eng.screen->pitch)
                    eng.exitFlag = TRUE;
                else
                    memcpy(dst.pixels, eng.screen->pixels, eng.screen->h * dst.pitch);

                SDL_UnlockTexture(eng.texture);

                //SDL_RenderClear(eng.renderer);
                SDL_RenderCopy(eng.renderer, eng.texture, NULL, NULL);
                SDL_RenderPresent(eng.renderer);
            }
        } // optUseGUI

        SDL_Delay(eng.pauseFlag ? 100 : 30);
    }

exit:
    // Cleanup
    if (optUseGUI)
    {
        dmMsg(1, "GUI shutdown.\n");
        if (eng.texture != NULL)
            SDL_DestroyTexture(eng.texture);

        if (eng.renderer != NULL)
            SDL_DestroyRenderer(eng.renderer);

        if (eng.window != NULL)
            SDL_DestroyWindow(eng.window);

        if (eng.screen != NULL)
            SDL_FreeSurface(eng.screen);

        dmFreeBitmapFont(font);
    }

    dmMsg(1, "Audio shutdown.\n");
    if (audioInit)
    {
        SDL_LockAudio();
        SDL_PauseAudio(1);
        SDL_UnlockAudio();
        SDL_CloseAudio();
    }

    jmpClose(eng.plr);
    jvmClose(eng.dev);
    jssFreeModule(eng.mod);

    dmMsg(1, "Shut down SDL.\n");
    if (initSDL)
        SDL_Quit();

    jssClose();

    return 0;
}