Skip to content

AudioEngine

Core class for the NES-like audio subsystem.

Description

AudioEngine manages the audio channels (Pulse, Triangle, Noise), mixes their output, and provides the audio stream to the backend. It implements a NES-like audio system with 4 fixed channels: 2 Pulse channels, 1 Triangle channel, and 1 Noise channel.

The engine is event-driven: you trigger sound effects via playEvent(), and the engine automatically manages channel allocation and playback.

Namespace

namespace pixelroot32::audio {
    class AudioEngine {
        // ...
    };
}

Inheritance

  • Base class: None (standalone class)
  • Used by: Engine (manages audio engine instance)

Constructors

AudioEngine(const AudioConfig& config)

Constructs the AudioEngine with the given configuration.

Parameters: - config (const AudioConfig&): Configuration struct containing the backend and parameters (sample rate, etc.)

Example:

#include "audio/AudioEngine.h"
#include "audio/AudioConfig.h"

pixelroot32::audio::AudioConfig audioConfig;
audioConfig.backend = &audioBackend;  // Platform-specific backend
audioConfig.sampleRate = 22050;       // 22.05 kHz for retro feel

pixelroot32::audio::AudioEngine audioEngine(audioConfig);
audioEngine.init();

Public Methods

void init()

Initializes the audio subsystem and the backend.

Returns: - void

Notes: - Must be called after construction and before use - Initializes the platform-specific audio backend - Safe to call multiple times (idempotent) - Typically called automatically by Engine::init()

Example:

AudioEngine audioEngine(audioConfig);
audioEngine.init();  // Initialize before use

void update(unsigned long deltaTime)

Updates the audio state based on game time.

Parameters: - deltaTime (unsigned long): Time elapsed since last frame in milliseconds

Returns: - void

Notes: - Should be called from the main game loop (typically via Engine::update()) - Updates channel lifetimes and durations - Automatically stops channels when their duration expires - Must be called every frame for proper audio timing

Example:

void update(unsigned long deltaTime) override {
    // Update audio (called automatically by Engine)
    engine.getAudioEngine().update(deltaTime);

    // Your game logic...
}

void generateSamples(int16_t* stream, int length)

Fills the provided buffer with mixed audio samples.

Parameters: - stream (int16_t*): Pointer to the buffer to fill - length (int): Number of samples to generate

Returns: - void

Notes: - This method is typically called by the AudioBackend from an audio callback or task - Not usually called directly by game code - Generates 16-bit signed integer PCM samples - Mixes all active channels into a mono stream

Advanced Usage:

// Typically not called directly, but if implementing custom backend:
int16_t buffer[512];
audioEngine.generateSamples(buffer, 512);

void playEvent(const AudioEvent& event)

Triggers a one-shot sound effect.

Parameters: - event (const AudioEvent&): The audio event to play

Returns: - void

Notes: - Automatically finds an available channel of the correct type - If no channel is available, the event may be dropped (no error) - Events are fire-and-forget (no need to track playback) - Use for sound effects, not background music

Example:

// Play a jump sound
pixelroot32::audio::AudioEvent jumpSound{};
jumpSound.type = pixelroot32::audio::WaveType::PULSE;
jumpSound.frequency = 800.0f;
jumpSound.duration = 0.1f;
jumpSound.volume = 0.7f;
jumpSound.duty = 0.5f;

auto& audio = engine.getAudioEngine();
audio.playEvent(jumpSound);

// Play an explosion sound
pixelroot32::audio::AudioEvent explosion{};
explosion.type = pixelroot32::audio::WaveType::NOISE;
explosion.frequency = 1000.0f;
explosion.duration = 0.3f;
explosion.volume = 0.9f;

audio.playEvent(explosion);

void setMasterVolume(float volume)

Sets the master volume for all audio output.

Parameters: - volume (float): Volume level (0.0 = silent, 1.0 = full volume)

Returns: - void

Notes: - Affects all channels and events - Clamped to [0.0, 1.0] range - Use for volume control menus or mute functionality

Example:

auto& audio = engine.getAudioEngine();
audio.setMasterVolume(0.5f);  // 50% volume
audio.setMasterVolume(0.0f);  // Mute
audio.setMasterVolume(1.0f);  // Full volume

float getMasterVolume() const

Gets the current master volume.

Returns: - float: Current master volume (0.0 to 1.0)

Example:

float currentVolume = audioEngine.getMasterVolume();

Audio Channels

The engine manages 4 fixed channels:

  1. Channel 0: Pulse wave
  2. Channel 1: Pulse wave
  3. Channel 2: Triangle wave
  4. Channel 3: Noise wave

Notes: - Channels are automatically allocated when playing events - If all channels of a type are busy, new events may be dropped - Background music typically uses one channel (via MusicPlayer)

Usage Example

#include "audio/AudioEngine.h"
#include "audio/AudioConfig.h"

class MyScene : public pixelroot32::core::Scene {
private:
    void playJumpSound() {
        auto& audio = engine.getAudioEngine();

        pixelroot32::audio::AudioEvent sound{};
        sound.type = pixelroot32::audio::WaveType::PULSE;
        sound.frequency = 800.0f;
        sound.duration = 0.1f;
        sound.volume = 0.7f;
        sound.duty = 0.5f;

        audio.playEvent(sound);
    }

    void playHitSound() {
        auto& audio = engine.getAudioEngine();

        pixelroot32::audio::AudioEvent sound{};
        sound.type = pixelroot32::audio::WaveType::NOISE;
        sound.frequency = 500.0f;
        sound.duration = 0.05f;
        sound.volume = 0.5f;

        audio.playEvent(sound);
    }

    void update(unsigned long deltaTime) override {
        Scene::update(deltaTime);

        // Audio is updated automatically by Engine
        // Just play events when needed
        if (playerJumped) {
            playJumpSound();
            playerJumped = false;
        }
    }
};

Performance Considerations

  • Channel limit: Only 4 channels total; plan sound effects accordingly
  • Event dropping: If all channels are busy, new events are silently dropped
  • Update frequency: update() must be called every frame for proper timing
  • Sample generation: generateSamples() is called by backend at audio rate (not game rate)

ESP32 Considerations

  • Sample rate: Lower sample rates (11025 Hz) use less CPU and memory
  • Backend choice: DAC backend is simpler but lower quality than I2S
  • Buffer size: Larger buffers reduce underruns but increase latency
  • Channel management: Limit simultaneous sounds to avoid channel conflicts

See Also