CRM64Pro Tutorial 09

Audio playback.

Package several audio formats, mix looping music with sound effects, and control playback by track or tag.

  • Beginner
  • AudioTrackMgr
  • CDC resources
  • Mixing

Overview

This tutorial extends Tutorial 08 with AudioTrack resources. It packages MP3, WAV, FLAC and OGG files into Tutorial.cdc, loads them back, and mixes looping music with sound effects.

The example also demonstrates track tags, fading, global playback controls and mixer master gain.

Final result

CRM64Pro Tutorial 09 audio controls
Runtime controls

1 fades in looping music; 2, 3 and 4 play WAV, FLAC and OGG tracks; P/R pause or resume all tracks; S stops all tracks; F fades out music; plus/minus changes master gain; D toggles debug; grave toggles the console; and Q or ESC exits.

Prerequisites

  • CRM64Pro GDK installed and configured with a supported C++17 compiler.
  • Tutorial package downloaded and fully extracted, preserving its folder structure.
  • Run Tutorials 02 and 04 so Tutorial.cdc contains the background and cursor.
  • Included assets available under bin/Base/: music.mp3, music.wav, music.flac, music.ogg.
  • A working audio output device and write access to the output directory.

What you will learn

  • Initialize and close the audio device.
  • Load and save AudioTrack resources.
  • Retain source bytes temporarily for CDC packaging.
  • Use music, sound-effect and voice tags.
  • Fade, pause, resume and stop playback.
  • Change mixer master gain.

Step by step

Step 1: Initialize the audio device

Open the mixer before loading or playing any tracks.

mLog.msg(LL_INFO, "  Initialize audio device ... ");
if(!mC64.configMgr().audioInit())
{
    logTaskFailed(mLog);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);

Step 2: Verify the shared archive

The audio resources extend the archive created earlier. Source bytes are retained only while external tracks are packaged.

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

Step 3: Save a loaded track

AudioTrack::save() needs the retained source bytes. The helper validates each track before writing it.

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

Step 4: Package tracks by format and tag

Each file receives a stable CDC name and a playback tag. These examples show the music and voice resources; WAV and FLAC follow the same source pattern.

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

Step 5: Release temporary source data

Close the packaging tracks and restore the default memory policy before loading runtime copies.

// Close the tracks opened for temporary processing.
static void closeTemporaryAudioTracks(AudioTrackMgr& mAudioMgr)
{
    mAudioMgr.close(0);
    mAudioMgr.setKeepSourceData(false);
}
mLog.msg(LL_INFO, "  Close temporary audio tracks ... ");
closeTemporaryAudioTracks(mAudioMgr);
logTaskOk(mLog);

Step 6: Load runtime tracks from CDC

The helper loads each named resource and confirms that its track is available.

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

Step 7: Play individual tracks

Music fades in with infinite looping. Other controls call play() through a result-checking helper.

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

Step 8: Control tracks globally and by tag

The manager controls all tracks together, while the music tag limits fade-out to music.

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

Step 9: Change master gain

Plus and minus clamp mixer gain to the documented 0.0-2.0 range.

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

Step 10: Finish the loop cleanly

A quit request exits before another state update.

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

Step 11: Stop playback before closing audio

Tracks are stopped and closed before the mixer device.

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

Complete source

Use the source file as the authoritative version of this tutorial.

  • View Tutorial_09_Audio.cpp
  • Assets: music.mp3, music.wav, music.flac, music.ogg
  • Input/output archive: Tutorial.cdc
  • Resources: Tutorial_audio_music, _sfx, _flac, _voice
  • Log: Tutorial_09_Audio.log

Previous tutorial

Create sprites and animation states.

Go to Tutorial 08: Sprite

Tutorial index

Back to tutorials

Next tutorial

Create and control particle emitters.

Go to Tutorial 10: Particles