Mercurial > hg > dmlib
view dmsimple.c @ 781:e15e0469499a
Add initial code for simulating audio playback while in no-sound situation.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Tue, 30 Jul 2013 15:29:19 +0300 |
parents | b87c7fc646f9 |
children | 2f32e178854a |
line wrap: on
line source
/* * dmlib * -- Demo engine "player" code * Programmed and designed by Matti 'ccr' Hamalainen * (C) Copyright 2012-2013 Tecnic Software productions (TNSP) */ #include <SDL.h> #include "dmengine.h" #include "dmargs.h" #include "dmtext.h" #include "dmimage.h" #include "setupfont.h" #include "setupimage.h" #include "setupmenubar.h" static const char *engineSetupDataName = "SetupData.txt"; static const char *engineSetupImageName = "SetupImage.png"; static const char *engineSetupMenuBarName = "SetupMenuBar.png"; static const char *engineSetupFontName = "SetupFont.dmf"; static DMEngineData engine; static DMOptArg optList[] = { { 0, '?', "help", "Show this help", OPT_NONE }, { 1, 'v', "verbose", "Be more verbose", OPT_NONE }, }; const int optListN = sizeof(optList) / sizeof(optList[0]); static void argShowHelp() { dmPrintBanner(stdout, dmProgName, "[options]"); dmArgsPrintHelp(stdout, optList, optListN); } static BOOL argHandleOpt(const int optN, char *optArg, char *currArg) { (void) optArg; switch (optN) { case 0: argShowHelp(); exit(0); break; case 1: dmVerbosity++; break; default: dmError("Unknown option '%s'.\n", currArg); return FALSE; } return TRUE; } static void engineAudioCallback(void *userdata, Uint8 *stream, int len) { (void) userdata; dmMutexLock(engine.audioStreamMutex); engine.audioStreamBuf = stream; engine.audioStreamLen = len; if (engine.paused) { memset(stream, 0, len); } else switch (engine.optAudioSetup) { #ifdef DM_USE_JSS case DM_ASETUP_JSS: if (engine.jssDev != NULL) jvmRenderAudio(engine.jssDev, stream, len / jvmGetSampleSize(engine.jssDev)); break; #endif #ifdef DM_USE_TREMOR case DM_ASETUP_TREMOR: if (engine.audioPos + len >= engine.audioRes->resSize) engine.exitFlag = TRUE; else { memcpy(stream, engine.audioRes->resData + engine.audioPos, len); engine.audioPos += len; } break; #endif default: break; } dmMutexUnlock(engine.audioStreamMutex); } static void engineAudioThreadFunc(void *userdata) { do { engineAudioCallback(userdata, engine.audioSimBuf, engine.audioSimBufSize); SDL_Delay(engine.audioSimDelay); } while (!engine.audioSimDone); } static int engineShowProgress(int loaded, int total) { int dx = 60, dh = 20, dw = engine.screen->w - (2 * dx), dy = (engine.screen->h - dh) / 2; if (SDL_MUSTLOCK(engine.screen) != 0 && SDL_LockSurface(engine.screen) != 0) return DMERR_INIT_FAIL; // Draw the progress bar dmClearSurface(engine.screen, dmMapRGBA(engine.screen, 0,0,0,0)); dmFillRect(engine.screen, dx, dy, dx+dw, dy+dh, dmMapRGB(engine.screen, 255,255,255)); dmFillRect(engine.screen, dx+1, dy+1, dx+dw-1, dy+dh-1, dmMapRGB(engine.screen, 0,0,0)); if (total > 0) { dmFillRect(engine.screen, dx+3, dy+3, dx + 3 + ((dw - 3) * loaded) / total, dy + dh - 3, dmMapRGB(engine.screen, 200,200,200)); } // Flip screen if (SDL_MUSTLOCK(engine.screen) != 0) SDL_UnlockSurface(engine.screen); SDL_Flip(engine.screen); return DMERR_OK; } static int engineLoadResources() { int err, loaded = 0, total = 0; BOOL first = TRUE; do { // Show a nice progress bar while loading if ((err = engineShowProgress(loaded, total)) != DMERR_OK) return err; err = dmResourcesPreload(engine.resources, first, &loaded, &total); first = FALSE; } while (err == DMERR_PROGRESS); return err; } static BOOL engineGenInitializeVideo(int width, int height, int depth, Uint32 flags) { dmPrint(1, "Initializing SDL video %d x %d x %dbpp, flags=0x%08x\n", width, height, depth, flags); SDL_FreeSurface(engine.screen); engine.screen = SDL_SetVideoMode(width, height, depth, flags); if (engine.screen == NULL) { dmError("Can't SDL_SetVideoMode(): %s\n", SDL_GetError()); return FALSE; } SDL_WM_SetCaption(dmProgDesc, dmProgName); return TRUE; } static BOOL engineInitializeVideo() { return engineGenInitializeVideo(engine.optVidWidth, engine.optVidHeight, engine.optVidDepth, engine.optVFlags); } typedef struct { int w, h, aspect; } DMModeEntry; DMModeEntry *engineModeList = NULL; int nengineModeList = 0, aengineModeList = 0; static int engineModeSort(const void *pa, const void *pb) { DMModeEntry *va = (DMModeEntry *) pa, *vb = (DMModeEntry *) pb; return (va->w - vb->w); } int engineAddModeToList(int w, int h) { DMModeEntry *mode; int i; int aspect = engineGetVideoAspect(w, h); if (aspect <= 0) return DMERR_INVALID_ARGS; // Check if the mode is already in our list for (i = 0; i < nengineModeList; i++) { mode = &engineModeList[i]; if (mode->w == w && mode->h == h) return DMERR_OK; } // Check if the mode fits our criteria switch (engine.optVidSetup) { case DM_VSETUP_ASPECT: if (aspect != engine.optVidAspect) return DMERR_OK; break; case DM_VSETUP_ANY: break; } // Reallocate array if needed if (nengineModeList + 1 >= aengineModeList) { aengineModeList += 16; engineModeList = dmRealloc(engineModeList, sizeof(DMModeEntry) * aengineModeList); if (engineModeList == NULL) return DMERR_MALLOC; } // Store mode = &engineModeList[nengineModeList]; mode->w = w; mode->h = h; mode->aspect = engineGetVideoAspect(w, h); nengineModeList++; return DMERR_OK; } int engineParseSetupConfig(const char *filename) { DMResource *file = NULL; int res; char buf[128]; if ((res = dmf_open(engine.resources, filename, &file)) != DMERR_OK) return res; while (dmfgets(buf, sizeof(buf), file) != NULL) { ssize_t pos; // Trim line ending for (pos = strlen(buf) - 1; pos >= 0 && isspace(buf[pos]); pos--) buf[pos] = 0; // Find start of the line for (pos = 0; isspace(buf[pos]); pos++); // Skip empty lines and comments if (buf[pos] == 0 || buf[pos] == '#') continue; char *str = buf+pos; if (sscanf(str, "menuPos %f %f", &engine.setupMenuPos.x, &engine.setupMenuPos.y) != 2 && sscanf(str, "menuDim %f %f", &engine.setupMenuDim.x, &engine.setupMenuDim.y) != 2 && sscanf(str, "text1Pos %f %f", &engine.setupText1Pos.x, &engine.setupText1Pos.y) != 2 && sscanf(str, "menuCenter %d", &engine.setupMenuCenter) != 1 && sscanf(str, "textCondensed %d", &engine.setupTextCondensed) != 1 && sscanf(str, "textFullscreen %s", engine.setupTextFullscreen) != 1 && sscanf(str, "textWindowed %s", engine.setupTextWindowed) != 1 && sscanf(str, "textPrefix %s", engine.setupTextPrefix) != 1 ) { dmError("Syntax error in configuration:\n%s\n", buf); res = DMERR_INVALID_DATA; goto out; } } out: dmf_close(file); return res; } static inline DMFloat vsX(DMVector vec) { return (DMFloat) engine.screen->w * vec.x; } static inline DMFloat vsY(DMVector vec) { return (DMFloat) engine.screen->h * vec.y; } int engineVideoSetup() { DMBitmapFont *menuFont = NULL; DMResource *file = NULL; SDL_Surface *menuBgImage = NULL, *menuBarImage = NULL; int result, menuState = -1; BOOL menuFullScreen = TRUE; // Compute a list of valid modes if (!engineGenInitializeVideo(DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT, engine.optVidDepth, engine.optVFlags)) goto out; SDL_Rect **modes = SDL_ListModes(engine.screen->format, engine.screen->flags | SDL_FULLSCREEN); if (modes == (SDL_Rect**) 0) { dmError("No compatible video resolutions/depths available at all. Bailing out.\n"); goto out; } if (modes != (SDL_Rect**) -1) { int i; for (i = 0; modes[i] != NULL; i++) engineAddModeToList(modes[i]->w, modes[i]->h); } if (nengineModeList == 0) { dmError("Umm, no modes found.\n"); goto out; } qsort(engineModeList, nengineModeList, sizeof(engineModeList[0]), engineModeSort); // Open video temporarily if (!engineGenInitializeVideo(DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT, 32, SDL_SWSURFACE | SDL_DOUBLEBUF)) goto out; // Get setup data engine.setupMenuPos.x = 0.18750f; engine.setupMenuPos.y = 0.41666f; engine.setupMenuDim.x = 0.625f; engine.setupMenuDim.y = 0.41666f; engine.setupText1Pos.x = 0.3f; engine.setupText1Pos.y = 0.7f; strcpy(engine.setupTextFullscreen , "FULLSCREEN"); strcpy(engine.setupTextWindowed , " WINDOWED "); strcpy(engine.setupTextPrefix , "USE LEFT/RIGHT ARROW TO TOGGLE : "); if (engineParseSetupConfig(engineSetupDataName) != DMERR_OK) goto out; // Fetch and decompress setup image, try regular resources first if ((result = dmf_open(engine.resources, engineSetupImageName, &file)) == DMERR_OK || (result = dmf_create_memio(NULL, engineSetupImageName, engineSetupImage, sizeof(engineSetupImage), &file)) == DMERR_OK) { menuBgImage = dmLoadImage(file); dmf_close(file); } if ((result = dmf_open(engine.resources, engineSetupMenuBarName, &file)) == DMERR_OK || (result = dmf_create_memio(NULL, engineSetupMenuBarName, engineSetupMenuBar, sizeof(engineSetupMenuBar), &file)) == DMERR_OK) { menuBarImage = dmLoadImage(file); dmf_close(file); } if (menuBgImage == NULL || menuBarImage == NULL) { dmError("Could not instantiate setup screen images, %d: %s\n", result, dmErrorStr(result)); goto out; } if (menuBgImage->w != DM_VSETUP_WIDTH || menuBgImage->h != DM_VSETUP_HEIGHT) { dmError("Setup screen background image does not match " "required dimensions (%dx%d vs %dx%d)\n", menuBgImage->w, menuBgImage->h, DM_VSETUP_WIDTH, DM_VSETUP_HEIGHT); goto out; } // Load up the bitmap font if ((result = dmf_open(engine.resources, engineSetupFontName, &file)) == DMERR_OK || (result = dmf_create_memio(NULL, engineSetupFontName, engineSetupFont, sizeof(engineSetupFont), &file)) == DMERR_OK) { result = dmLoadBitmapFont(file, &menuFont); dmf_close(file); } if (result != DMERR_OK) { dmError("Could not instantiate setup screen font, %d: %s\n", result, dmErrorStr(result)); goto out; } SDL_Surface *tmp = dmConvertScaledSurface(menuBgImage, engine.screen->format, engine.screen->flags, engine.screen->w, engine.screen->h); if (tmp == NULL) { dmError("Could not convert setup screen background image.\n"); goto out; } SDL_FreeSurface(menuBgImage); menuBgImage = tmp; SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); // Enter the main loop of the menu char menuStr[256]; int menuOffset = 0, menuIndex = 0, menuEntryHeight = menuFont->height + 2, menuHeight = vsY(engine.setupMenuDim) / menuEntryHeight; menuState = 0; engine.startTime = SDL_GetTicks(); while (!menuState) { while (SDL_PollEvent(&engine.event)) switch (engine.event.type) { case SDL_KEYDOWN: switch (engine.event.key.keysym.sym) { case SDLK_ESCAPE: menuState = -1; break; case SDLK_RETURN: menuState = 1; break; case SDLK_UP: if (menuIndex > 0) menuIndex--; else if (menuOffset > 0) menuOffset--; break; case SDLK_DOWN: if (menuIndex < menuHeight - 1 && menuOffset + menuIndex < nengineModeList - 1) menuIndex++; else if (menuOffset + menuIndex < nengineModeList - 1) menuOffset++; break; case SDLK_LEFT: menuFullScreen = FALSE; break; case SDLK_RIGHT: menuFullScreen = TRUE; break; case SDLK_SPACE: menuFullScreen = !menuFullScreen; break; default: break; } break; case SDL_QUIT: menuState = -1; break; } // Draw frame engine.frameTime = SDL_GetTicks(); if (SDL_MUSTLOCK(engine.screen) != 0 && SDL_LockSurface(engine.screen) != 0) { dmError("Can't lock surface.\n"); goto out; } // Render the menu dmDirectBlitSurface(menuBgImage, engine.screen); // XXX/TODO: Some hardcoded bits here ... float t = engineGetTimeDT(&engine); int index, entry; for (index = 0, entry = menuOffset; entry < nengineModeList && index < menuHeight; index++, entry++) { DMModeEntry *mode = &engineModeList[entry]; if (entry == menuOffset + menuIndex) { dmScaledBlitSurface32to32TransparentGA(menuBarImage, vsX(engine.setupMenuPos), vsY(engine.setupMenuPos) + (index * menuEntryHeight), vsX(engine.setupMenuDim), menuEntryHeight + 4, engine.screen, 200 + sin(t * 10.0) * 50); } snprintf(menuStr, sizeof(menuStr), "%4d X %-4d - %d:%d", mode->w, mode->h, mode->aspect / 1000, mode->aspect % 1000); DMFloat posX = engine.setupMenuCenter ? 2.0f + (vsX(engine.setupMenuDim) - menuFont->width * strlen(menuStr)) / 2.0f : 2.0f; dmDrawBMTextConst( engine.screen, menuFont, engine.setupTextCondensed, DMD_TRANSPARENT, vsX(engine.setupMenuPos) + posX, vsY(engine.setupMenuPos) + (index * menuEntryHeight), menuStr); } snprintf(menuStr, sizeof(menuStr), "%s%s", engine.setupTextPrefix, menuFullScreen ? engine.setupTextFullscreen : engine.setupTextWindowed); dmDrawBMTextConst( engine.screen, menuFont, engine.setupTextCondensed, DMD_TRANSPARENT, vsX(engine.setupText1Pos), vsY(engine.setupText1Pos), menuStr); // Flip screen if (SDL_MUSTLOCK(engine.screen) != 0) SDL_UnlockSurface(engine.screen); SDL_Flip(engine.screen); SDL_Delay(25); } // Okay, we accepted the selection if (menuState == 1) { DMModeEntry *mode = &engineModeList[menuOffset + menuIndex]; engine.optVidNative = mode->w == engine.optVidWidth && mode->h == engine.optVidHeight; engine.optVidWidth = mode->w; engine.optVidHeight = mode->h; engine.optVidAspect = mode->aspect; if (menuFullScreen) engine.optVFlags |= SDL_FULLSCREEN; } out: SDL_FreeSurface(menuBgImage); SDL_FreeSurface(menuBarImage); dmFreeBitmapFont(menuFont); return menuState; } int engineGetVideoAspect(int width, int height) { if (width > 0 && height > 0) return (width * 1000) / height; else return 1; } int main(int argc, char *argv[]) { int err; BOOL initSDL = FALSE; memset(&engine, 0, sizeof(engine)); // Pre-initialization if ((err = demoPreInit(&engine)) != DMERR_OK) goto error_exit; if (!dmArgsProcess(argc, argv, optList, optListN, argHandleOpt, NULL, FALSE)) return DMERR_INIT_FAIL; dmPrint(0, "%s\n" "%s\n", dmProgDesc, dmProgAuthor); dmPrint(0, "Using libSDL, " #ifdef DM_USE_PACKFS "zlib, " #endif #ifdef DM_USE_TREMOR "Tremor Vorbis codec" #endif " and modified stb_image.\n" "See README.txt for more information.\n"); // Initialize resource subsystem dmPrint(1, "Initializing resources subsystem.\n"); if ((err = dmResourcesInit(&engine.resources, engine.optPackFilename, engine.optDataPath, engine.optResFlags, engineClassifier)) != DMERR_OK) { dmError("Could not initialize resource manager, #%d: %s.\n", err, dmErrorStr(err)); goto error_exit; } // Initialize SDL components dmPrint(1, "Initializing libSDL.\n"); 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; // Present video mode selector if (engine.optVidAspect <= 0) engine.optVidAspect = engineGetVideoAspect(engine.optVidWidth, engine.optVidHeight); if (engine.optVidSetup) { if ((err = engineVideoSetup()) <= 0) goto error_exit; } // Initialize audio parts if (engine.optAfmt.freq == 0 && engine.optAfmt.channels == 0) { engine.optAfmt.freq = 44100; engine.optAfmt.format = AUDIO_S16SYS; engine.optAfmt.channels = 2; } if (engine.optAfmt.samples == 0) engine.optAfmt.samples = engine.optAfmt.freq / 16; switch (engine.optAudioSetup) { case DM_ASETUP_JSS: #ifdef DM_USE_JSS jssInit(); switch (engine.optAfmt.format) { case AUDIO_S16SYS: engine.jssFormat = JSS_AUDIO_S16; break; case AUDIO_U16SYS: engine.jssFormat = JSS_AUDIO_U16; break; case AUDIO_S8: engine.jssFormat = JSS_AUDIO_S8; break; case AUDIO_U8: engine.jssFormat = JSS_AUDIO_U8; break; } dmPrint(1, "Initializing miniJSS mixer with fmt=%d, chn=%d, freq=%d\n", engine.jssFormat, engine.optAfmt.channels, engine.optAfmt.freq); if ((engine.jssDev = jvmInit(engine.jssFormat, engine.optAfmt.channels, engine.optAfmt.freq, JMIX_AUTO)) == NULL) { dmError("jvmInit() returned NULL, voi perkele.\n"); goto error_exit; } if ((engine.jssPlr = jmpInit(engine.jssDev)) == NULL) { dmError("jmpInit() returned NULL\n"); goto error_exit; } #else dmError("miniJSS support not included.\n"); #endif break; case DM_ASETUP_TREMOR: #ifndef DM_USE_TREMOR dmError("Tremor support not included.\n"); #endif break; } // Initialize SDL audio dmPrint(1, "Trying to init SDL audio with: fmt=%d, chn=%d, freq=%d\n", engine.optAfmt.format, engine.optAfmt.channels, engine.optAfmt.freq); engine.optAfmt.callback = engineAudioCallback; engine.audioStreamMutex = dmCreateMutex(); if (SDL_OpenAudio(&engine.optAfmt, NULL) < 0) { int sampleSize; // We'll let this pass, as we want to support no-sound. dmError("Couldn't open SDL audio, falling back to no sound: %s\n", SDL_GetError()); // Set up simulated audio thread sampleSize = engine.optAfmt.channels; switch (engine.optAfmt.format) { case AUDIO_S16SYS: case AUDIO_U16SYS: sampleSize *= 2; break; } engine.audioSimDelay = 1000 / 50; engine.audioSimBufSize = (engine.optAfmt.freq * sampleSize) / 50; engine.audioSimBuf = dmMalloc(engine.audioSimBufSize); engine.audioSimThread = SDL_CreateThread(engineAudioThreadFunc, NULL); } // Initialize SDL video if (engine.demoInitPreVideo != NULL && (err = engine.demoInitPreVideo(&engine)) != DMERR_OK) { dmError("demoInitPreVideo() failed, #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } if (!engineInitializeVideo()) goto error_exit; if (engine.demoInitPostVideo != NULL && (err = engine.demoInitPostVideo(&engine)) != DMERR_OK) { dmError("demoInitPostVideo() failed, #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } // Hide cursor SDL_ShowCursor(SDL_DISABLE); // Load resources dmPrint(1, "Loading resources, please wait...\n"); if ((err = engineLoadResources()) != DMERR_OK) { dmError("Error loading resources, #%d: %s.\n", err, dmErrorStr(err)); goto error_exit; } // Final initializations if ((err = engine.demoInit(&engine)) != DMERR_OK) { dmError("Failure in demoInit(), #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } // Initialize effects if ((err = engineInitializeEffects(&engine)) != DMERR_OK) { dmError("Effects initialization failed, #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } // Use a timeline, if set #ifdef DM_USE_TIMELINE if (engine.timeline != NULL) { if ((err = dmLoadTimeline(engine.timeline, &engine.tl)) != DMERR_OK) { dmError("Error loading timeline, #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } if ((err = dmPrepareTimeline(engine.tl, engine.ptl)) != DMERR_OK) { dmError("Error creating prepared timeline, #%d: %s\n", err, dmErrorStr(err)); goto error_exit; } } #endif dmPrint(1, "Starting up.\n"); SDL_LockAudio(); SDL_PauseAudio(0); SDL_UnlockAudio(); engine.startTime = SDL_GetTicks(); while (!engine.exitFlag) { 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; break; case SDLK_f: engine.optVFlags ^= SDL_FULLSCREEN; if (!engineInitializeVideo()) goto error_exit; break; case SDLK_RETURN: if (engine.event.key.keysym.mod & KMOD_ALT) { engine.optVFlags ^= SDL_FULLSCREEN; if (!engineInitializeVideo()) goto error_exit; } break; default: break; } break; case SDL_VIDEOEXPOSE: break; case SDL_QUIT: engine.exitFlag = TRUE; break; } // Draw frame engine.frameTime = SDL_GetTicks(); if (engine.pauseFlag != engine.paused) { engine.paused = engine.pauseFlag; engine.pauseTime = engineGetTick(&engine); } if (engine.paused) { engine.startTime = engine.frameTime - engine.pauseTime; } if (SDL_MUSTLOCK(engine.screen) != 0 && SDL_LockSurface(engine.screen) != 0) { dmError("Can't lock surface.\n"); goto error_exit; } // Call main tick if (engine.demoRender != NULL) { if ((err = engine.demoRender(&engine)) != DMERR_OK) goto error_exit; } #ifdef DM_USE_TIMELINE else { if ((err = dmExecuteTimeline(engine.ptl, engine.screen, engineGetTick(&engine))) != DMERR_OK) goto error_exit; } #endif // Flip screen if (SDL_MUSTLOCK(engine.screen) != 0) SDL_UnlockSurface(engine.screen); SDL_Flip(engine.screen); SDL_Delay(engine.paused ? 100 : 20); engine.frameCount++; } // Print benchmark results engine.endTime = SDL_GetTicks(); dmPrint(1, "%d frames in %d ms, fps = %1.3f\n", engine.frameCount, engine.endTime - engine.startTime, (float) (engine.frameCount * 1000.0f) / (float) (engine.endTime - engine.startTime)); error_exit: dmPrint(1, "Shutting down.\n"); SDL_ShowCursor(SDL_ENABLE); SDL_LockAudio(); SDL_PauseAudio(1); #ifdef DM_USE_JSS if (engine.optAudioSetup == DM_ASETUP_JSS) { jmpClose(engine.jssPlr); jvmClose(engine.jssDev); jssClose(); } #endif if (engine.audioSimThread != NULL) { dmMutexLock(engine.audioStreamMutex); engine.audioSimDone = TRUE; dmMutexUnlock(engine.audioStreamMutex); SDL_WaitThread(engine.audioSimThread, NULL); } SDL_UnlockAudio(); if (engine.audioStreamMutex != NULL) dmDestroyMutex(engine.audioStreamMutex); #ifdef DM_USE_TIMELINE dmFreeTimeline(engine.tl); dmFreePreparedTimelineData(engine.ptl); #endif engineShutdownEffects(&engine); dmResourcesClose(engine.resources); if (engine.demoShutdown != NULL) engine.demoShutdown(&engine); if (initSDL) SDL_Quit(); if (engine.demoQuit != NULL) engine.demoQuit(&engine); return 0; }