/* ----------------------------------------------------------------------
    CRM64Pro GDK - Roberto Prieto
    Copyright (C) 2013-2026 MegaStorm Systems

    This software is provided 'as-is', without any express or implied
    warranty. In no event will the authors be held liable for any damages
    arising from the use of this software.

    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software.
    2. Altered source versions must be plainly marked as such, and must not be
       misrepresented as being the original software.
    3. This notice may not be removed or altered from any source distribution.

------------------------------------------------------------------------
                        Tutorial 09: Audio
------------------------------------------------------------------------

    Overview:
    This tutorial extends Tutorial 08 Sprite with audio tracks.
    It removes sprite rendering, adds audio files to Tutorial.cdc, loads them
    from the CDC, and mixes looping music with sound effects.

    Key concepts:
    - Initialize the audio device.
    - Load AudioTrack resources from files.
    - Save AudioTrack resources to a CDC file.
    - Load AudioTrack resources from a CDC file.
    - Play music and sound effects at the same time.
    - Control audio by track and by tag.

    Controls:
    - 1: Fade in looping music.
    - 2: Play WAV sound effect.
    - 3: Play FLAC sound effect.
    - 4: Play OGG voice-style sound.
    - P / R: Pause or resume all tracks.
    - S: Stop all tracks.
    - F: Fade out music.
    - + / -: Change master gain.
    - D: Toggle debug window.
    - Grave: Toggle console.
    - Q / ESC: Quit.

    Loop order:
    - Process input events.
    - Run one fixed logic update.
    - Let Main::update() call the screen render callback.
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"

using namespace CRM64Pro;

#define BASEDIR "Base/"
#ifdef CRM64PRO_PLATFORM_WINDOWS
#define OUTPUTDIR "Win64/"
#elif defined(CRM64PRO_PLATFORM_LINUX)
#define OUTPUTDIR "Linux/"
#elif defined(CRM64PRO_PLATFORM_MACOS)
#define OUTPUTDIR "macOS/"
#endif

#define RESOURCE_AUDIO_MUSIC_FILE BASEDIR"music.mp3"
#define RESOURCE_AUDIO_SFX_FILE BASEDIR"music.wav"
#define RESOURCE_AUDIO_FLAC_FILE BASEDIR"test.flac"
#define RESOURCE_AUDIO_VOICE_FILE BASEDIR"music.ogg"
#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define RESOURCE_CURSOR_CDC_NAME "Tutorial_cursor"
#define RESOURCE_AUDIO_MUSIC_CDC_NAME "Tutorial_audio_music"
#define RESOURCE_AUDIO_SFX_CDC_NAME "Tutorial_audio_sfx"
#define RESOURCE_AUDIO_FLAC_CDC_NAME "Tutorial_audio_flac"
#define RESOURCE_AUDIO_VOICE_CDC_NAME "Tutorial_audio_voice"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"
// Render rate 0 lets the screen render as often as possible. Logic rate controls counters.
static const Sint32 iLogicRate = 20;
static const Sint32 iRenderRate = 0;

// Window size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;

// Values changed while the tutorial runs.
struct TutorialState
{
    Sint32 idBgImage = -1;
    Sint32 idCursor = -1;
    Sint32 idMusic = -1;
    Sint32 idSFX = -1;
    Sint32 idFLAC = -1;
    Sint32 idVoice = -1;
    Sint32 idBuiltInFont = -1;
    Sint32 idDebugWindow = -1;
    float fMouseX = 0.0f;
    float fMouseY = 0.0f;
    float fMasterGain = 1.0f;
    Sint32 iCurrentRenderRate = 0;
    Sint32 iCurrentLogicRate = 0;
    Sint32 iLogicFrames = 0;
    char szLastAction[128] = "Ready";
};

// Helper functions used by the setup log messages.
static void logTaskOk(Log& mLog)
{
    mLog.msg(LL_DEBUG, "OK\n");
}

// Record a failed tutorial task.
static void logTaskFailed(Log& mLog, Sint32 iCode)
{
    mLog.msg(LL_ERROR, "FAILED (Code %d)\n", iCode);
}

// Record a failed tutorial task.
static void logTaskFailed(Log& mLog)
{
    mLog.msg(LL_ERROR, "FAILED\n");
}

// Store the action shown by the tutorial.
static void setLastAction(TutorialState& rState, const char* szAction)
{
    snprintf(rState.szLastAction, sizeof(rState.szLastAction), "%s", szAction);
}

// Update the tutorial state for the current frame.
static void updateLogic(TutorialState& rState)
{
    rState.iLogicFrames++;
}

// Draw text at the requested screen position.
static void renderText(Font* pFont, Sint32 iX, Sint32 iY, const char* szText)
{
    if(!pFont) return;
    pFont->setPosition(iX, iY);
    pFont->render(szText);
}

// Draw the tutorial status information.
static void renderHUD(Font* pBuiltInFont, const TutorialState& rState)
{
    if(!pBuiltInFont) return;

    char szLine[160];
    snprintf(szLine, sizeof(szLine), "Render: %d fps   Logic: %d fps   Master gain: %.1f", rState.iCurrentRenderRate, rState.iCurrentLogicRate, rState.fMasterGain);
    renderText(pBuiltInFont, 10, 10, szLine);

    renderText(pBuiltInFont, 10, 55, "Audio controls");
    renderText(pBuiltInFont, 10, 82, "1  Fade in looping music");
    renderText(pBuiltInFont, 10, 106, "2  Play WAV sound effect");
    renderText(pBuiltInFont, 10, 130, "3  Play FLAC sound effect");
    renderText(pBuiltInFont, 10, 154, "4  Play OGG voice-style sound");
    renderText(pBuiltInFont, 10, 178, "P/R  Pause or resume all tracks");
    renderText(pBuiltInFont, 10, 202, "S  Stop all tracks");
    renderText(pBuiltInFont, 10, 226, "F  Fade out music");
    renderText(pBuiltInFont, 10, 250, "+/-  Change master gain");
    renderText(pBuiltInFont, 10, 274, "D  Toggle debug window");

    snprintf(szLine, sizeof(szLine), "Last action: %s", rState.szLastAction);
    renderText(pBuiltInFont, 10, 330, szLine);
}

// Draw the current frame. CursorMgr draws the selected cursor automatically.
static void renderFrame(Image* pBgImage, Font* pBuiltInFont, const TutorialState& rState)
{
    if(pBgImage) pBgImage->render();
    renderHUD(pBuiltInFont, rState);
}

// Screen render callback.
// Main::update() calls this when the screen is ready to render.
static Sint32 renderWrapper(Sint32, void* pObj)
{
    TutorialState* pState = static_cast<TutorialState*>(pObj);
    if(!pState) return 0;

    Main& mC64 = Main::instance();
    Image* pBgImage = mC64.imageMgr().get(pState->idBgImage);
    Font* pBuiltInFont = mC64.fontMgr().get(pState->idBuiltInFont);
    renderFrame(pBgImage, pBuiltInFont, *pState);

    return 0;
}

// Show or hide the debug window.
static void toggleDebugWindow(DebugWindow* pDebug)
{
    if(!pDebug) return;
    if(pDebug->baseWidget().status() == GS_SHOWN) pDebug->baseWidget().hide();
    else pDebug->baseWidget().show();
}

// Save an audio track to the tutorial archive.
static bool saveAudioTrackToCDC(AudioTrackMgr& mAudioMgr, Log& mLog, Sint32 idTrack, const char* szResourceName)
{
    AudioTrack* pTrack = mAudioMgr.get(idTrack);
    if(idTrack < 0 || pTrack == nullptr)
    {
        logTaskFailed(mLog, idTrack);
        return false;
    }

    if(pTrack->save(OUTPUT_CDC, szResourceName) < 0)
    {
        logTaskFailed(mLog);
        return false;
    }

    logTaskOk(mLog);
    return true;
}

// Close the tracks opened for temporary processing.
static void closeTemporaryAudioTracks(AudioTrackMgr& mAudioMgr)
{
    mAudioMgr.close(0);
    mAudioMgr.setKeepSourceData(false);
}

// Add the audio resources to the tutorial archive.
static bool addAudioToCDC(Main& mC64, Log& mLog)
{
    AudioTrackMgr& mAudioMgr = mC64.audioTrackMgr();

    mLog.msg(LL_INFO, "  Find Tutorial 04 CDC: %s ... ", OUTPUT_CDC);
    if(!mC64.tool().fileExists(OUTPUT_CDC))
    {
        logTaskFailed(mLog);
        return false;
    }
    logTaskOk(mLog);

    // Keep raw source bytes only while packaging the tracks into the CDC.
    mAudioMgr.setKeepSourceData(true);

    mLog.msg(LL_INFO, "  Load music track: %s ... ", RESOURCE_AUDIO_MUSIC_FILE);
    Sint32 idMusicFile = mAudioMgr.loadFromFile(RESOURCE_AUDIO_MUSIC_FILE, RESOURCE_AUDIO_MUSIC_CDC_NAME, ATT_MUSIC, ALM_PRELOAD);
    if(idMusicFile < 0)
    {
        logTaskFailed(mLog, idMusicFile);
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Save music track to CDC ... ");
    if(!saveAudioTrackToCDC(mAudioMgr, mLog, idMusicFile, RESOURCE_AUDIO_MUSIC_CDC_NAME))
    {
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }

    mLog.msg(LL_INFO, "  Load WAV sound effect: %s ... ", RESOURCE_AUDIO_SFX_FILE);
    Sint32 idSFXFile = mAudioMgr.loadFromFile(RESOURCE_AUDIO_SFX_FILE, RESOURCE_AUDIO_SFX_CDC_NAME, ATT_SFX, ALM_PRELOAD);
    if(idSFXFile < 0)
    {
        logTaskFailed(mLog, idSFXFile);
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Save WAV sound effect to CDC ... ");
    if(!saveAudioTrackToCDC(mAudioMgr, mLog, idSFXFile, RESOURCE_AUDIO_SFX_CDC_NAME))
    {
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }

    mLog.msg(LL_INFO, "  Load FLAC sound effect: %s ... ", RESOURCE_AUDIO_FLAC_FILE);
    Sint32 idFLACFile = mAudioMgr.loadFromFile(RESOURCE_AUDIO_FLAC_FILE, RESOURCE_AUDIO_FLAC_CDC_NAME, ATT_SFX, ALM_PRELOAD);
    if(idFLACFile < 0)
    {
        logTaskFailed(mLog, idFLACFile);
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Save FLAC sound effect to CDC ... ");
    if(!saveAudioTrackToCDC(mAudioMgr, mLog, idFLACFile, RESOURCE_AUDIO_FLAC_CDC_NAME))
    {
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }

    mLog.msg(LL_INFO, "  Load OGG voice-style sound: %s ... ", RESOURCE_AUDIO_VOICE_FILE);
    Sint32 idVoiceFile = mAudioMgr.loadFromFile(RESOURCE_AUDIO_VOICE_FILE, RESOURCE_AUDIO_VOICE_CDC_NAME, ATT_VOICE, ALM_PRELOAD);
    if(idVoiceFile < 0)
    {
        logTaskFailed(mLog, idVoiceFile);
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Save OGG voice-style sound to CDC ... ");
    if(!saveAudioTrackToCDC(mAudioMgr, mLog, idVoiceFile, RESOURCE_AUDIO_VOICE_CDC_NAME))
    {
        closeTemporaryAudioTracks(mAudioMgr);
        return false;
    }

    mLog.msg(LL_INFO, "  Close temporary audio tracks ... ");
    closeTemporaryAudioTracks(mAudioMgr);
    logTaskOk(mLog);

    return true;
}

// Load an audio track from the tutorial archive.
static bool loadAudioTrackFromCDC(AudioTrackMgr& mAudioMgr, Log& mLog, Sint32& rIDTrack, const char* szResourceName)
{
    rIDTrack = mAudioMgr.load(OUTPUT_CDC, szResourceName);
    if(rIDTrack < 0 || mAudioMgr.get(rIDTrack) == nullptr)
    {
        logTaskFailed(mLog, rIDTrack);
        return false;
    }
    logTaskOk(mLog);
    return true;
}

// Play a track and record the requested action.
static void playTrack(AudioTrackMgr& mAudioMgr, Log& mLog, TutorialState& rState, Sint32 idTrack, const char* szAction)
{
    AudioTrack* pTrack = mAudioMgr.get(idTrack);
    if(!pTrack)
    {
        mLog.msg(LL_ERROR, "  Audio track not loaded\n");
        setLastAction(rState, "Audio track not loaded");
        return;
    }

    if(pTrack->play() == 0)
    {
        setLastAction(rState, szAction);
        mLog.msg(LL_INFO, "  %s\n", szAction);
    }
    else
    {
        setLastAction(rState, "Playback failed");
        mLog.msg(LL_ERROR, "  Playback failed\n");
    }
}

// Run the tutorial application and release its resources.
int main(int argc, char* argv[])
{
    eConfigRendererDriver eRenderer = CRD_SOFTWARE;
    if(!runParameterReadRender(argc, argv, eRenderer)) return 1;

    // Start CRM64Pro memory tracking before creating engine resources.
    // atexit() releases memory tracking data when the program exits.
    CMem::setStatsLevel(CMem::MSL_NORMAL);
    atexit(CMem::destroy);

    // Main gives access to the CRM64Pro managers.
    Main& mC64 = Main::instance();
    Log& mLog = *mC64.logMgr().get();
    AudioTrackMgr& mAudioMgr = mC64.audioTrackMgr();

    TutorialState state;

    // Write log output to stdout and to a file under validation/bin.
    mLog.init("Tutorial_09_Audio", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_09_Audio.log");

    mLog.msg(LL_INFO, "\nSETUP: screen, timing and audio\n");

    // Initialize the timer before calling Main::update().
    // setRate() receives render frames per second, then logic frames per second.
    mLog.msg(LL_INFO, "  Initialize timer ... ");
    if(!mC64.timer().init())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Set frame rates (render=%d, logic=%d) ... ", iRenderRate, iLogicRate);
    if(!mC64.timer().setRate(iRenderRate, iLogicRate))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    // Add a 1 ms idle delay inside Main::update().
    mC64.configMgr().setMTFriendly(1);

    // Configure and show the default screen.
    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("CRM64Pro - Tutorial 09: Audio");

    mLog.msg(LL_INFO, "  Create screen (%dx%d) ... ", iScreenW, iScreenH);
    if(pScreen->show() < 0)
    {
        mLog.msg(LL_CRITICAL, "FAILED\n");
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Initialize audio device ... ");
    if(!mC64.configMgr().audioInit())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Set render callback ... ");
    if(!pScreen->setRenderCallback([&state](Sint32 iMode) { return renderWrapper(iMode, &state); }))
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nRESOURCES: add audio and load from CDC\n");

    if(!addAudioToCDC(mC64, mLog))
    {
        Main::terminate();
        return -1;
    }

    mLog.msg(LL_INFO, "  Load background image from CDC ... ");
    state.idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    if(state.idBgImage < 0)
    {
        logTaskFailed(mLog, state.idBgImage);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load music from CDC ... ");
    if(!loadAudioTrackFromCDC(mAudioMgr, mLog, state.idMusic, RESOURCE_AUDIO_MUSIC_CDC_NAME))
    {
        Main::terminate();
        return -1;
    }
    mLog.msg(LL_INFO, "  Load WAV sound effect from CDC ... ");
    if(!loadAudioTrackFromCDC(mAudioMgr, mLog, state.idSFX, RESOURCE_AUDIO_SFX_CDC_NAME))
    {
        Main::terminate();
        return -1;
    }
    mLog.msg(LL_INFO, "  Load FLAC sound effect from CDC ... ");
    if(!loadAudioTrackFromCDC(mAudioMgr, mLog, state.idFLAC, RESOURCE_AUDIO_FLAC_CDC_NAME))
    {
        Main::terminate();
        return -1;
    }
    mLog.msg(LL_INFO, "  Load OGG voice-style sound from CDC ... ");
    if(!loadAudioTrackFromCDC(mAudioMgr, mLog, state.idVoice, RESOURCE_AUDIO_VOICE_CDC_NAME))
    {
        Main::terminate();
        return -1;
    }

    mLog.msg(LL_INFO, "  Load cursor from CDC ... ");
    state.idCursor = mC64.cursorMgr().load(OUTPUT_CDC, RESOURCE_CURSOR_CDC_NAME);
    Cursor* pCursor = mC64.cursorMgr().get(state.idCursor);
    if(state.idCursor < 0 || pCursor == nullptr)
    {
        logTaskFailed(mLog, state.idCursor);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Select cursor ... ");
    if(mC64.cursorMgr().select(state.idCursor) < 0 || !mC64.cursorMgr().show())
    {
        logTaskFailed(mLog);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "  Load built-in font ... ");
    state.idBuiltInFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    Font* pBuiltInFont = mC64.fontMgr().get(state.idBuiltInFont);
    if(state.idBuiltInFont < 0 || pBuiltInFont == nullptr)
    {
        logTaskFailed(mLog, state.idBuiltInFont);
        Main::terminate();
        return -1;
    }
    logTaskOk(mLog);

    mLog.msg(LL_INFO, "\nTOOLS: debug window and console\n");

    // The DebugWindow is available, but this tutorial starts with it hidden.
    mLog.msg(LL_INFO, "  Create debug window ... ");
    state.idDebugWindow = mC64.guiMgr().create("Debug", CRM64PRO_GUI_DEBUGWINDOW);
    DebugWindow* pDebug = mC64.guiMgr().getDebugWindow(state.idDebugWindow);
    if(!pDebug)
    {
        logTaskFailed(mLog, state.idDebugWindow);
        Main::terminate();
        return -1;
    }
    pDebug->baseWidget().setPosition(Position(PH_RIGHT, -120), Position(PH_TOP, 10));
    pDebug->addWatch("Mouse X", nullptr, &state.fMouseX);
    pDebug->addWatch("Mouse Y", nullptr, &state.fMouseY);
    pDebug->addWatch("Master gain", nullptr, &state.fMasterGain);
    pDebug->addWatch("Render fps", &state.iCurrentRenderRate);
    pDebug->addWatch("Logic fps", &state.iCurrentLogicRate);
    pDebug->addWatch("Logic frames", &state.iLogicFrames);
    pDebug->baseWidget().hide();
    logTaskOk(mLog);

    // Main creates the default Console. Log output is also sent there.
    Console* pConsole = mC64.guiMgr().getConsole();
    if(pConsole) pConsole->print("Tutorial 09: Audio ready. Press ` to toggle the console.\n");

    mLog.msg(LL_INFO, "\nCONTROLS\n");
    mLog.msg(LL_INFO, "  [Q] / [ESC] Quit\n");
    mLog.msg(LL_INFO, "  [1] Fade in looping music\n");
    mLog.msg(LL_INFO, "  [2] Play WAV sound effect\n");
    mLog.msg(LL_INFO, "  [3] Play FLAC sound effect\n");
    mLog.msg(LL_INFO, "  [4] Play OGG voice-style sound\n");
    mLog.msg(LL_INFO, "  [P] / [R] Pause or resume all tracks\n");
    mLog.msg(LL_INFO, "  [S] Stop all tracks\n");
    mLog.msg(LL_INFO, "  [F] Fade out music\n");
    mLog.msg(LL_INFO, "  [+] / [-] Change master gain\n");
    mLog.msg(LL_INFO, "  [D] Toggle debug window\n");
    mLog.msg(LL_INFO, "  [`] Toggle console\n");
    mLog.msg(LL_INFO, "\nRUNNING\n");

    SDL_Event event;
    bool bRunning = true;
    while(bRunning)
    {
        // Main::update() processes input, runs internal GUI logic and calls the
        // screen render callback when a render frame is ready.
        while(mC64.update(&event))
        {
            if(event.type == SDL_EVENT_QUIT)
            {
                bRunning = false;
            }
            else if(event.type == SDL_EVENT_KEY_DOWN)
            {
                if(event.key.key == SDLK_Q || event.key.key == SDLK_ESCAPE)
                {
                    bRunning = false;
                }
                else if(event.key.key == SDLK_1)
                {
                    AudioTrack* pMusic = mAudioMgr.get(state.idMusic);
                    if(pMusic && pMusic->fadeIn(700, -1) == 0)
                    {
                        setLastAction(state, "Looping music fading in");
                        mLog.msg(LL_INFO, "  Looping music fading in\n");
                    }
                    else
                    {
                        setLastAction(state, "Music playback failed");
                        mLog.msg(LL_ERROR, "  Music playback failed\n");
                    }
                }
                else if(event.key.key == SDLK_2)
                {
                    playTrack(mAudioMgr, mLog, state, state.idSFX, "WAV sound effect played");
                }
                else if(event.key.key == SDLK_3)
                {
                    playTrack(mAudioMgr, mLog, state, state.idFLAC, "FLAC sound effect played");
                }
                else if(event.key.key == SDLK_4)
                {
                    playTrack(mAudioMgr, mLog, state, state.idVoice, "OGG voice-style sound played");
                }
                else if(event.key.key == SDLK_P)
                {
                    mAudioMgr.pauseAll();
                    setLastAction(state, "All tracks paused");
                    mLog.msg(LL_INFO, "  All tracks paused\n");
                }
                else if(event.key.key == SDLK_R)
                {
                    mAudioMgr.resumeAll();
                    setLastAction(state, "All tracks resumed");
                    mLog.msg(LL_INFO, "  All tracks resumed\n");
                }
                else if(event.key.key == SDLK_S)
                {
                    mAudioMgr.stopAll();
                    setLastAction(state, "All tracks stopped");
                    mLog.msg(LL_INFO, "  All tracks stopped\n");
                }
                else if(event.key.key == SDLK_F)
                {
                    if(mAudioMgr.fadeOutTag(ATT_MUSIC, 1200))
                    {
                        setLastAction(state, "Music fading out");
                        mLog.msg(LL_INFO, "  Music fading out\n");
                    }
                    else
                    {
                        setLastAction(state, "Music fade-out failed");
                        mLog.msg(LL_ERROR, "  Music fade-out failed\n");
                    }
                }
                else if(event.key.key == SDLK_PLUS || event.key.key == SDLK_KP_PLUS)
                {
                    state.fMasterGain += 0.1f;
                    if(state.fMasterGain > 2.0f) state.fMasterGain = 2.0f;
                    mC64.configMgr().audioSetMasterGain(state.fMasterGain);
                    setLastAction(state, "Master gain increased");
                    mLog.msg(LL_INFO, "  Master gain: %.1f\n", state.fMasterGain);
                }
                else if(event.key.key == SDLK_MINUS || event.key.key == SDLK_KP_MINUS)
                {
                    state.fMasterGain -= 0.1f;
                    if(state.fMasterGain < 0.0f) state.fMasterGain = 0.0f;
                    mC64.configMgr().audioSetMasterGain(state.fMasterGain);
                    setLastAction(state, "Master gain decreased");
                    mLog.msg(LL_INFO, "  Master gain: %.1f\n", state.fMasterGain);
                }
                else if(event.key.key == SDLK_D)
                {
                    toggleDebugWindow(pDebug);
                }
            }
        }

        if(!bRunning) break;
        state.fMouseX = mC64.cursorMgr().getX();
        state.fMouseY = mC64.cursorMgr().getY();
        state.iCurrentRenderRate = static_cast<Sint32>(mC64.timer().getCurrentRFR());
        state.iCurrentLogicRate = static_cast<Sint32>(mC64.timer().getCurrentLFR());
        updateLogic(state);
    }

    // Close resources owned by this tutorial.
    mAudioMgr.info();
    mLog.msg(LL_INFO, "\nCLEANUP\n");
    mLog.msg(LL_INFO, "  Stop and close audio tracks ... ");
    mAudioMgr.stopAll();
    mAudioMgr.close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close audio device ... ");
    mC64.configMgr().audioClose();
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close debug window ... ");
    mC64.guiMgr().close(state.idDebugWindow);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close fonts ... ");
    mC64.fontMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close cursors ... ");
    mC64.cursorMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close images ... ");
    mC64.imageMgr().close(0);
    logTaskOk(mLog);
    mLog.msg(LL_INFO, "  Close CDC archives ... ");
    mC64.archiveMgr().close(0);
    logTaskOk(mLog);
    Main::terminate();

    return 0;
}
