view ppl.c @ 510:43ea59887c69

Start work on making C64 formats encoding possible by changing DMDecodeOps to DMEncDecOps and adding fields and op enums for custom encode functions, renaming, etc. Split generic op sanity checking into a separate function in preparation for its use in generic encoding function.
author Matti Hamalainen <ccr@tnsp.org>
date Mon, 19 Nov 2012 15:06:01 +0200
parents 1bdd2af756ec
children 61af51348ea4
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 20012 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "jss.h"
#include "jssmod.h"
#include "jssmix.h"
#include "jssplr.h"

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

#include "pplfont.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;
} 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;


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


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)
                    {
                        dmError("Invalid width or height: %d x %d\n", w, h);
                        return FALSE;
                    }
                    engine.optScrWidth = w;
                    engine.optScrHeight = h;
                }
                else 
                {
                    dmError("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:
            dmError("Unknown option '%s'.\n", currArg);
            return FALSE;
    }
    
    return TRUE;
}


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


void dmDrawBMTextConstQ(SDL_Surface *screen, DMBitmapFont *font, int mode, int xc, int yc, const char *fmt)
{
    const char *ptr = fmt;
    DMUnscaledBlitFunc blit = NULL;

    while (*ptr)
    {
        int ch = *ptr++;
        SDL_Surface *glyph;

        if (ch == '_')
        {
            xc += 4;
            continue;
        }
        
        if (ch >= 0 && ch < font->nglyphs && (glyph = font->glyphs[ch]) != NULL)
        {
            if (blit == NULL)
                blit = dmGetUnscaledBlitFunc(glyph->format, screen->format, mode);
            
            blit(glyph, xc, yc, screen);
            xc += font->width;
        }
        else
            xc += font->width;
    }
}


void dmDrawBMTextVAQ(SDL_Surface *screen, DMBitmapFont *font, int mode, int xc, int yc, const char *fmt, va_list ap)
{
    char tmp[512];
    vsnprintf(tmp, sizeof(tmp), fmt, ap);
    dmDrawBMTextConstQ(screen, font, mode, xc, yc, tmp);
}


void dmDrawBMTextQ(SDL_Surface *screen, DMBitmapFont *font, int mode, int xc, int yc, const char *fmt, ...)
{
    va_list ap;
    
    va_start(ap, fmt);
    dmDrawBMTextVAQ(screen, font, mode, xc, yc, fmt, ap);
    va_end(ap);
}


Uint32 dmCol(float r, float g, 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)
    {
        dmError("Can't SDL_SetVideoMode(): %s\n", SDL_GetError());
        return FALSE;
    }

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

    return TRUE;
}


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_GETH(chn->chVolume);
    int pitch = screen->pitch / sizeof(Uint32);
    int len = FP_GETH(chn->chSize);
    DMFixedPoint offs = chn->chPos;
    Uint32 coln = dmCol(0.0, 0.8, 0.0), colx = dmCol(1.0, 0, 0);
    Uint32 *pix = screen->pixels;
    Sint16 *data = chn->chData;


    dmFillBox3D(screen, x0, y0, x1, y1,
        (chn->chMute ? dmCol(0.3,0.1,0.1) : dmCol(0,0,0)),
        nchannel == engine.actChannel ? colx : col.box2,
        nchannel == engine.actChannel ? colx : col.box1);

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

    if (chn->chDirection)
    {
        for (xc = x0 + 1; xc < x1 - 1; xc++)
        {
            if (FP_GETH(offs) >= len)
                break;
            Sint16 val = ym + (data[FP_GETH(offs)] * yh * vol) / (65535 * 255);
            pix[xc + val * pitch] = coln;
            FP_ADD(offs, chn->chDeltaO);
        }
    }
    else
    {
        for (xc = x0 + 1; xc < x1 - 1; xc++)
        {
            if (FP_GETH(offs) < 0)
                break;
            Sint16 val = ym + (data[FP_GETH(offs)] * yh * vol) / (65535 * 255);
            pix[xc + val * pitch] = coln;
            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 (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;
            
        if (engine.actChannel == nchannel + choffs)
        {
            dmFillRect(screen, bx0+1, qy0 + 1, bx1-1, qy1 - 1, col.activeChannel);
        }
        else
        {
            dmFillRect(screen, bx0+1, qy0 + 1, bx1-1, qy1 - 1, 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;

    memset(&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, TRUE))
        exit(1);

    // Open the files
    if (optFilename == NULL)
    {
        dmError("No filename specified.\n");
        return 1;
    }
    
    if ((file = dmf_create_stdio(optFilename, "rb")) == NULL)
    {
        int err = dmGetErrno();
        dmError("Error opening file '%s', %d: (%s)\n",
            optFilename, err, dmErrorStr(err));
        return 1;
    }

    // Initialize miniJSS
    jssInit();

    // Read module file
    dmMsg(1, "Reading file: %s\n", optFilename);
#ifdef JSS_SUP_XM
    dmMsg(2, "* Trying XM...\n");
    result = jssLoadXM(file, &engine.mod);
#endif
#ifdef JSS_SUP_JSSMOD
    if (result != 0)
    {
        size_t bufgot, bufsize = dmfsize(file);
        Uint8 *buf = dmMalloc(bufsize);
        dmfseek(file, 0L, SEEK_SET);
        dmMsg(2, "* Trying JSSMOD (%d bytes, %p)...\n", bufsize, buf);
        if ((bufgot = dmfread(buf, 1, bufsize, file)) != bufsize)
        {
            dmf_close(file);
            dmError("Error reading file (not enough data %d), #%d: %s\n",
                bufgot, dmferror(file), dmErrorStr(dmferror(file)));
            goto error_exit;
        }
        result = jssLoadJSSMOD(buf, bufsize, &engine.mod);
        dmFree(buf);
    }
#endif
    dmf_close(file);

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

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

    // Get font
//    file = dmf_create_stdio("fnsmall.fnt", "rb");
    file = dmf_create_memio(NULL, "pplfont.fnt", dmPlayerFont, sizeof(dmPlayerFont));
    if (file == NULL)
    {
        dmError("Error opening font file 'pplfont.fnt'.\n");
        goto error_exit;
    }
    result = dmLoadBitmapFont(file, &font);
    dmf_close(file);
    if (result != DMERR_OK)
    {
        dmError("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)
    {
        dmError("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)
    {
        dmError("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:
            dmError("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)
    {
        dmError("Couldn't open SDL audio: %s\n",
            SDL_GetError());
        goto error_exit;
    }
    audioInit = TRUE;
    
    // Initialize player
    if ((engine.plr = jmpInit(engine.dev)) == NULL)
    {
        dmError("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, 64);

    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)
                        {
                            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)
        {
            dmError("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 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->nrows,
            engine.plr->order, 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;
}