view tools/ppl.c @ 1556:8f06c23e197d last_SDL1

Cosmetics.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 13 May 2018 05:59:42 +0300
parents 813244726b32
children 5e5f75b45f8d
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-2015 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmlib.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_Surface *screen;
    SDL_Event event;

    int optScrWidth, optScrHeight, optVFlags, optScrDepth;

    int actChannel;
    BOOL pauseFlag;

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

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:
            engine.optVFlags |= SDL_FULLSCREEN;
            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;
                    }
                    engine.optScrWidth = w;
                    engine.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("Unknown option '%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(engine.screen, 255.0f * r, 255.0f * g, 255.0f * b);
}


BOOL dmInitializeVideo()
{
    SDL_FreeSurface(engine.screen);

    engine.screen = SDL_SetVideoMode(
        engine.optScrWidth, engine.optScrHeight, engine.optScrDepth,
        engine.optVFlags | SDL_RESIZABLE | SDL_SWSURFACE | SDL_HWPALETTE);

    if (engine.screen == NULL)
    {
        dmErrorMsg("Can't SDL_SetVideoMode(): %s\n", SDL_GetError());
        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 == engine.actChannel ? col.red : col.box2,
        nchannel == engine.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 (engine.actChannel < qwidth / 2)
        choffs = 0;
    else
    if (engine.actChannel >= pat->nchannels - qwidth/2)
        choffs = pat->nchannels - qwidth;
    else
        choffs = engine.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,
            (engine.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(engine.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 < engine.mod->nchannels; i++)
        jvmMute(engine.dev, i, mute);
}


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

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

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

    dmInitProg("CBP", "Cyrbe Basci Player", "0.1", 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_create_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, &engine.mod, TRUE);
#endif
#ifdef JSS_SUP_JSSMOD
    dmfreset(file);
    if (result != DMERR_OK)
    {
        dmMsg(1, "* Trying JSSMOD ...\n");
        result = jssLoadJSSMOD(file, &engine.mod, TRUE);
        dmfreset(file);
        if (result == DMERR_OK)
            result = jssLoadJSSMOD(file, &engine.mod, FALSE);
    }
    else
    {
        dmMsg(2, "* Trying XM...\n");
        result = jssLoadXM(file, &engine.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(engine.mod)) != DMERR_OK)
    {
        dmErrorMsg("Could not convert module for playing, %d: %s\n",
            result, dmErrorStr(result));
        goto error_exit;
    }

    // Get font
    result = dmf_create_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;
    }

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

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

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

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

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

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

    jvmSetCallback(engine.dev, jmpExec, engine.plr);
    jmpSetModule(engine.plr, engine.mod);
    jmpPlayOrder(engine.plr, optStartOrder);
    jvmSetGlobalVol(engine.dev, 255);

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

    // Initialize video
    if (!dmInitializeVideo())
        goto error_exit;

    SDL_WM_SetCaption(dmProgDesc, dmProgName);

    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 (!engine.exitFlag)
    {
        currTick = SDL_GetTicks();
        BOOL force = (currTick - prevTick > 500), updated = FALSE;

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

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

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

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

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

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

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

                    case SDLK_f:
                        engine.optVFlags ^= SDL_FULLSCREEN;
                        if (!dmInitializeVideo())
                            goto error_exit;
                        force = TRUE;
                        break;

                    default:
                        break;
                }

                break;

            case SDL_VIDEORESIZE:
                engine.optScrWidth = engine.event.resize.w;
                engine.optScrHeight = engine.event.resize.h;

                if (!dmInitializeVideo())
                    goto error_exit;

                break;

            case SDL_VIDEOEXPOSE:
                break;

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


#if 1
        JSS_LOCK(engine.plr);
        JSSPattern *currPattern = engine.plr->pattern;
        int currRow = engine.plr->row;
        if (!engine.plr->isPlaying)
            engine.exitFlag = TRUE;
        JSS_UNLOCK(engine.plr);

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

        // Draw frame
        if (SDL_MUSTLOCK(engine.screen) != 0 && SDL_LockSurface(engine.screen) != 0)
        {
            dmErrorMsg("Can't lock surface.\n");
            goto error_exit;
        }

        if (force)
        {
            dmClearSurface(engine.screen, col.boxBg);

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

            dmDrawBMTextQ(engine.screen, font, DMD_TRANSPARENT, 5, 5 + 12 + 11,
                "Song: '%s'",
                engine.mod->moduleName);

            dmDisplayPattern(engine.screen, 5, 40,
                engine.screen->w - 6, engine.screen->h * 0.8,
                currPattern, currRow);

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

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

        if (force)
            prevTick = currTick;

#endif
        // Flip screen
        if (SDL_MUSTLOCK(engine.screen) != 0)
            SDL_UnlockSurface(engine.screen);

        if (updated)
            SDL_Flip(engine.screen);

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

error_exit:
    if (engine.screen)
        SDL_FreeSurface(engine.screen);

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

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

    dmFreeBitmapFont(font);

    if (initSDL)
        SDL_Quit();

    jssClose();

    return 0;
}