view tools/ppl.c @ 2576:812b16ee49db

I had been living under apparent false impression that "realfft.c" on which the FFT implementation in DMLIB was basically copied from was released in public domain at some point, but it could very well be that it never was. Correct license is (or seems to be) GNU GPL. Thus I removing the code from DMLIB, and profusely apologize to the author, Philip Van Baren. It was never my intention to distribute code based on his original work under a more liberal license than originally intended.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 11 Mar 2022 16:32:50 +0200
parents d56a0e86067a
children 9807ae37ad69
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,   0, "license"         , "Print out this program's license agreement", OPT_NONE },
    {  2, 'v', "verbose"         , "Be more verbose", OPT_NONE },

    { 10,   0, "fs"              , "Fullscreen", OPT_NONE },
    { 12, 'C', "cli"             , "Do not open GUI window", OPT_NONE },
    { 14, 'w', "window"          , "Initial window size/resolution -w 640x480", OPT_ARGREQ },

    { 16, '1', "16bit"           , "16-bit output", OPT_NONE },
    { 18, '8', "8bit"            , "8-bit output", OPT_NONE },
    { 20, 'm', "mono"            , "Mono output", OPT_NONE },
    { 22, 's', "stereo"          , "Stereo output", OPT_NONE },
    { 24, 'f', "freq"            , "Output frequency", OPT_ARGREQ },

    { 28, 'M', "mute"            , "Mute other channels than #", OPT_ARGREQ },
    { 30, 'o', "order"           , "Start from order #", OPT_ARGREQ },
    { 32, '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, 80 - 2);
}


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

        case 1:
            dmPrintLicense(stdout);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

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

        case 12:
            optUseGUI = FALSE;
            break;

        case 14:
            {
                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 16:
            optOutFormat = JSS_AUDIO_S16;
            break;

        case 18:
            optOutFormat = JSS_AUDIO_U8;
            break;

        case 20:
            optOutChannels = JSS_AUDIO_MONO;
            break;

        case 22:
            optOutChannels = JSS_AUDIO_STEREO;
            break;

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

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

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

        case 32:
            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)
{
    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 res = DMERR_OK;
    BOOL muteState = FALSE;

    memset(&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))
        goto out;

    // Open the files
    if (optFilename == NULL || argc < 2)
    {
        argShowHelp();
        res = dmError(DMERR_INVALID_ARGS,
            "No filename specified.\n");
        goto out;
    }

    if ((res = dmf_open_stdio(optFilename, "rb", &file)) != DMERR_OK)
    {
        dmErrorMsg("Error opening file '%s': %s\n",
            optFilename, dmErrorStr(res));
        goto out;
    }

    // Initialize miniJSS
    jssInit();

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

    // Check for errors, we still might have some data tho
    if (res != DMERR_OK)
    {
        dmErrorMsg("Error loading module file: %s\n",
            dmErrorStr(res));
        goto out;
    }

    // Check if we have anything
    if (eng.mod == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "Could not load module file.\n");
        goto out;
    }

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


    // Initialize SDL components
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0)
    {
        res = dmError(DMERR_INIT_FAIL,
            "Could not initialize SDL: %s\n",
            SDL_GetError());
        goto out;
    }
    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)
    {
        res = dmError(DMERR_INIT_FAIL,
            "jvmInit() returned NULL\n");
        goto out;
    }

    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:
            res = dmError(DMERR_NOT_SUPPORTED,
                "Unsupported audio format %d (could not set matching SDL format)\n",
                optOutFormat);
            goto out;
    }

    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)
    {
        res = dmError(DMERR_INIT_FAIL,
            "Couldn't open SDL audio: %s\n",
            SDL_GetError());
        goto out;
    }
    audioInit = TRUE;

    // Initialize player
    if ((eng.plr = jmpInit(eng.dev)) == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "jmpInit() returned NULL\n");
        goto out;
    }

    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
        static const char *engineFontName = "pplfont.fnt";
        res = dmf_open_memio(NULL, engineFontName, engineSetupFont, sizeof(engineSetupFont), &file);
        if (res != DMERR_OK)
        {
            dmErrorMsg("Error opening font file '%s': %s\n",
                engineFontName, dmErrorStr(res));
            goto out;
        }
        res = dmLoadBitmapFont(file, &font);
        dmf_close(file);
        if (res != DMERR_OK)
        {
            dmErrorMsg("Could not load font data from '%s': %s\n",
                engineFontName, dmErrorStr(res));
            goto out;
        }

        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)
        {
            res = dmError(DMERR_INIT_FAIL,
                "Can't create an SDL window: %s\n",
                SDL_GetError());
            goto out;
        }

        SDL_SetWindowTitle(eng.window, dmProgDesc);

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

        if (!dmInitializeVideo())
        {
            res = DMERR_INIT_FAIL;
            goto out;
        }

        //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 out;
                        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 out;

                        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);
    }

out:
    // 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 res;
}