view tools/ppl.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents e3f0eaf23f4f
children ba696835f66d
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-2018 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;
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 },
    {  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;

        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)
{
    int i;
    for (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 error_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 error_exit;
    }

    // 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 error_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 error_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);

    // 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 error_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 error_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 error_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 error_exit;
    }
    audioInit = TRUE;

    // Initialize player
    if ((eng.plr = jmpInit(eng.dev)) == NULL)
    {
        dmErrorMsg("jmpInit() returned NULL\n");
        goto error_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;
    }

    // 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 error_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 error_exit;
    }

    if (!dmInitializeVideo())
        goto error_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 error_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 error_exit;

                        needUpdate = TRUE;
                        break;
                }
                break;

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


#if 1
        JSS_LOCK(eng.plr);
        JSSPattern *currPattern = eng.plr->pattern;
        int currRow = eng.plr->row;
        if (!eng.plr->isPlaying)
            eng.exitFlag = TRUE;
        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-2018 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;

#endif
        // 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);
        }

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

error_exit:
    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);

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

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

    dmFreeBitmapFont(font);

    if (initSDL)
        SDL_Quit();

    jssClose();

    return 0;
}