diff tools/ppl.c @ 652:d9888292f971

Rename again.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 16 Apr 2013 06:04:22 +0300
parents utils/ppl.c@e2ac08228a0f
children 3d813c81f33c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/ppl.c	Tue Apr 16 06:04:22 2013 +0300
@@ -0,0 +1,932 @@
+/*
+ * Cyrbe Pasci Player - A simple SDL-based UI for XM module playing
+ * Programmed and designed by Matti 'ccr' Hamalainen
+ * (C) Copyright 2012 Tecnic Software productions (TNSP)
+ *
+ * Please read file 'COPYING' for information on license and distribution.
+ */
+#include <SDL.h>
+#include "dmlib.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;
+} 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", engineSetupFont, sizeof(engineSetupFont));
+    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;
+}