Skip to content

Audio

AudioManager provides simple playback for sound effects and background music.

Supported APIs:

  • Desktop: OpenAL (vendored OpenAL Soft preferred when available)
  • Web: Emscripten OpenAL
  • iOS: statically linked vendored OpenAL Soft
  • Android: statically linked vendored OpenAL Soft with its Oboe backend

Supported file formats: - .wav - .ogg - .mp3 - .flac

Sample Usage

auto* audio = context()->audioManager();
if (audio) {
    audio->playCooldown = 0.08f;                 // optional cooldown per sound path
    const auto sfx = audio->play("audio/cat_meow.ogg");
    const auto loopSfx = audio->play("audio/wind.ogg", true);  // looping SFX instance
    audio->playBgm("audio/bgm.mp3", 0.8f, 1.0f);   // volume, pitch
    audio->setBgmPlaybackSpeed(1.1f);

    audio->mute();
    audio->unmute();

    audio->stop(sfx);
    audio->stop(loopSfx);                         // stop a specific SFX instance
    if (audio->isBgmPlaying()) {
        audio->stopBgm();
    }
}

playCooldown throttles repeated play() calls for the same path (in seconds). Set it to 0.0f to disable. play() returns an AudioHandle; when playback does not start it returns AudioManager::kInvalidAudioHandle. mute() silences all audio without interrupting playback. unmute() restores audio output.

Layered Mute State

Snowpulse tracks independent mute reasons so one subsystem cannot accidentally unmute another:

  • AudioMuteSource::Applicationmute() / unmute() and game settings
  • AudioMuteSource::Advertisement — game-side ad flow
  • AudioMuteSource::Portal — portal audio preference
  • AudioMuteSource::PortalPause — temporary portal pause
  • AudioMuteSource::SystemLifecycle — Activity/app suspension
  • AudioMuteSource::AudioFocus — Android focus loss
audio->setMuted(snowpulse::AudioMuteSource::Advertisement, true);
// ...show an ad...
audio->setMuted(snowpulse::AudioMuteSource::Advertisement, false);

if (audio->isMuted(snowpulse::AudioMuteSource::Portal)) {
    // The host portal still requires silence.
}

isMuted() without an argument reports whether any source is active. mute() and unmute() only change the Application source. CrazyGames, Playgama, and YouTube portal adapters update their own portal sources.

Loading, Unloading, and Output Latency

play() and playBgm() load files on demand. Use load(path) to warm the decoded-audio cache, unload(path) to release one entry, and unloadAll() to clear it. Stop any active playback that depends on an entry before unloading it.

preferredOutputBufferFrames(frames) requests a backend buffer size and must be called before AudioManager::init(). Call outputBufferFrames() after initialization to inspect the effective size:

bool init() override {
    context()->audioManager()->preferredOutputBufferFrames(256);
    if (!snowpulse::App::init()) {
        return false;
    }
    std::printf("Audio buffer: %d frames\n",
                context()->audioManager()->outputBufferFrames());
    return true;
}

The backend may choose a different effective value. This is a latency/CPU tuning control, not a portable guarantee.

On Android, audio is loaded from packaged assets rather than extracted files. Short effects use cached OpenAL buffers and BGM feeds queued streaming buffers from a seekable decoder. Activity pause/resume and Android audio focus are independent mute sources; Oboe reopens output across Bluetooth/headset route changes. Keep long audio extensions in Gradle's noCompress list.

Physical volume buttons are handled by Android rather than Snowpulse input. While the game Activity is foregrounded, they target the media/music stream and display the normal system volume overlay. This device volume is independent of AudioManager::mute(), per-sound gain, and the layered mute sources above.

Desktop Recording

On macOS 13 or newer, the desktop MP4 recorder captures the final process audio produced by OpenAL, including BGM, sound effects, volume, pitch, looping, and source stopping. It encodes that mix as 48 kHz stereo AAC. This uses ScreenCaptureKit and therefore requests macOS Screen Recording permission on the first audio-enabled recording.

Audio capture is enabled inside an opt-in recorder configuration. Disable it when video-only output or permission-free recording is preferred:

auto recorder = context()->desktopRecorderConfig();
recorder.captureAudio = false;
context()->desktopRecorderConfig(recorder);

Denied permission and macOS 11-12 automatically fall back to video-only MP4.