Skip to content

Audio

PixelRoot32 includes a complete NES-like audio system with 4 channels for sound effects and background music. This guide shows you how to add sound and music to your games.

Audio Configuration

Before using audio, you need to configure an AudioBackend. This is done when creating the Engine:

ESP32: Internal DAC

#include <drivers/esp32/ESP32_DAC_AudioBackend.h>

const int DAC_PIN = 25; // GPIO 25 or 26
pr32::drivers::esp32::ESP32_DAC_AudioBackend audioBackend(DAC_PIN, 11025);

pr32::audio::AudioConfig audioConfig(&audioBackend, audioBackend.getSampleRate());

ESP32: External I2S DAC

#include <drivers/esp32/ESP32_I2S_AudioBackend.h>

const int I2S_BCLK = 26;  // Bit clock
const int I2S_LRCK = 25;  // Left/Right clock
const int I2S_DOUT = 22;  // Data out

pr32::drivers::esp32::ESP32_I2S_AudioBackend audioBackend(
    I2S_BCLK, I2S_LRCK, I2S_DOUT, 22050
);

pr32::audio::AudioConfig audioConfig(&audioBackend, 22050);

Native (PC): SDL2

#include <drivers/native/SDL2_AudioBackend.h>

pr32::drivers::native::SDL2_AudioBackend audioBackend(22050, 1024);

pr32::audio::AudioConfig audioConfig(&audioBackend, 22050);

Sound Effects

Sound effects are created using AudioEvent structures and played through the AudioEngine.

AudioEvent Structure

#include <audio/AudioTypes.h>

pr32::audio::AudioEvent soundEffect{};
soundEffect.type = pr32::audio::WaveType::PULSE;  // Waveform type
soundEffect.frequency = 1500.0f;                  // Frequency in Hz
soundEffect.duration = 0.12f;                      // Duration in seconds
soundEffect.volume = 0.8f;                         // Volume (0.0 to 1.0)
soundEffect.duty = 0.5f;                           // Duty cycle (for PULSE only)

Wave Types

PixelRoot32 supports three wave types:

  • PULSE: Square wave with variable duty cycle
  • Duty cycles: 0.125 (thin), 0.25 (classic NES), 0.5 (symmetric), 0.75 (fat)
  • Good for: Beeps, jumps, UI sounds, leads

  • TRIANGLE: Triangle wave (fixed volume/duty)

  • Softer, smoother sound
  • Good for: Bass lines, pads, background tones

  • NOISE: Pseudo-random noise

  • Harsh, chaotic sound
  • Good for: Explosions, hits, impacts, drums

Playing Sound Effects

// Get the audio engine
auto& audio = engine.getAudioEngine();

// Create and play a sound
pr32::audio::AudioEvent jumpSound{};
jumpSound.type = pr32::audio::WaveType::PULSE;
jumpSound.frequency = 800.0f;
jumpSound.duration = 0.1f;
jumpSound.volume = 0.7f;
jumpSound.duty = 0.25f;

audio.playEvent(jumpSound);

Common Sound Effects

Here are some example sound effects you can use:

namespace SoundEffects {
    // Jump sound
    inline pr32::audio::AudioEvent jump() {
        pr32::audio::AudioEvent evt{};
        evt.type = pr32::audio::WaveType::PULSE;
        evt.frequency = 600.0f;
        evt.duration = 0.1f;
        evt.volume = 0.7f;
        evt.duty = 0.25f;
        return evt;
    }

    // Coin/collect sound
    inline pr32::audio::AudioEvent coin() {
        pr32::audio::AudioEvent evt{};
        evt.type = pr32::audio::WaveType::PULSE;
        evt.frequency = 1500.0f;
        evt.duration = 0.12f;
        evt.volume = 0.8f;
        evt.duty = 0.5f;
        return evt;
    }

    // Explosion
    inline pr32::audio::AudioEvent explosion() {
        pr32::audio::AudioEvent evt{};
        evt.type = pr32::audio::WaveType::NOISE;
        evt.frequency = 200.0f;
        evt.duration = 0.3f;
        evt.volume = 0.9f;
        return evt;
    }

    // Hit/damage
    inline pr32::audio::AudioEvent hit() {
        pr32::audio::AudioEvent evt{};
        evt.type = pr32::audio::WaveType::NOISE;
        evt.frequency = 300.0f;
        evt.duration = 0.15f;
        evt.volume = 0.6f;
        return evt;
    }
}

// Usage
audio.playEvent(SoundEffects::jump());

Background Music

Background music uses the MusicPlayer system, which sequences notes over time.

Music Notes

Music is built from MusicNote structures:

#include <audio/AudioMusicTypes.h>

using namespace pr32::audio;

MusicNote note{};
note.note = Note::C;        // Musical note (C, D, E, F, G, A, B, or Rest)
note.octave = 4;            // Octave (0-8)
note.duration = 0.2f;       // Duration in seconds
note.volume = 0.7f;         // Volume (0.0 to 1.0)

Instrument Presets

For convenience, use predefined instrument presets:

using namespace pr32::audio;

// Available presets:
// - INSTR_PULSE_LEAD: Main lead pulse (octave 4)
// - INSTR_PULSE_BASS: Bass pulse (octave 3)
// - INSTR_PULSE_CHIP_HIGH: High-pitched chiptune (octave 5)
// - INSTR_TRIANGLE_PAD: Soft triangle pad (octave 4)

Creating a Melody

#include <audio/AudioMusicTypes.h>

using namespace pr32::audio;

// Define melody notes
static const MusicNote MELODY_NOTES[] = {
    makeNote(INSTR_PULSE_LEAD, Note::C, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::E, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::G, 0.25f),
    makeRest(0.10f),  // Rest (silence)
    makeNote(INSTR_PULSE_LEAD, Note::C, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::E, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::G, 0.25f),
    makeRest(0.10f),
};

// Create music track
static const MusicTrack GAME_MUSIC = {
    MELODY_NOTES,                              // notes array
    sizeof(MELODY_NOTES) / sizeof(MusicNote),  // note count
    true,                                       // loop
    WaveType::PULSE,                           // channel type
    0.5f                                       // duty cycle
};

Playing Music

// Get the music player
auto& music = engine.getMusicPlayer();

// Play a track
music.play(GAME_MUSIC);

// Control playback
music.stop();   // Stop playback
music.pause();  // Pause (time doesn't advance)
music.resume(); // Resume after pause

// Check status
if (music.isPlaying()) {
    // Music is currently playing
}

Music in Scene

Typically, you start music in your scene's init():

void MyGameScene::init() override {
    // Start background music
    engine.getMusicPlayer().play(GAME_MUSIC);

    // ... rest of initialization
}

Master Volume

Control overall volume without changing individual sounds:

auto& audio = engine.getAudioEngine();

// Set master volume (0.0 to 1.0)
audio.setMasterVolume(0.5f); // 50% volume

// Get current volume
float currentVolume = audio.getMasterVolume();

Complete Example

Here's a complete example combining sound effects and music:

#include <core/Scene.h>
#include <audio/AudioTypes.h>
#include <audio/AudioMusicTypes.h>

using namespace pr32::audio;

// Background music
static const MusicNote GAME_MELODY[] = {
    makeNote(INSTR_PULSE_LEAD, Note::C, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::E, 0.20f),
    makeNote(INSTR_PULSE_LEAD, Note::G, 0.25f),
    makeRest(0.10f),
};

static const MusicTrack BACKGROUND_MUSIC = {
    GAME_MELODY,
    sizeof(GAME_MELODY) / sizeof(MusicNote),
    true,  // loop
    WaveType::PULSE,
    0.5f
};

class AudioExampleScene : public pixelroot32::core::Scene {
public:
    void init() override {
        // Start background music
        engine.getMusicPlayer().play(BACKGROUND_MUSIC);

        // Set master volume
        engine.getAudioEngine().setMasterVolume(0.8f);
    }

    void update(unsigned long deltaTime) override {
        auto& input = engine.getInputManager();
        auto& audio = engine.getAudioEngine();

        // Play sound effect on button press
        if (input.isButtonPressed(4)) { // Button A
            AudioEvent jumpSound{};
            jumpSound.type = WaveType::PULSE;
            jumpSound.frequency = 800.0f;
            jumpSound.duration = 0.1f;
            jumpSound.volume = 0.7f;
            jumpSound.duty = 0.25f;

            audio.playEvent(jumpSound);
        }

        Scene::update(deltaTime);
    }

    void draw(pixelroot32::graphics::Renderer& renderer) override {
        Scene::draw(renderer);
    }
};

Designing NES-like Sounds

Frequency Guidelines

  • Low frequencies (200-400 Hz): Bass, impacts, explosions
  • Mid frequencies (400-1000 Hz): Main sounds, jumps, UI
  • High frequencies (1000-2000 Hz): Beeps, coins, pickups
  • Very high (2000+ Hz): Sharp sounds, alerts

Duration Guidelines

  • Short (0.05-0.1s): UI clicks, small effects
  • Medium (0.1-0.2s): Jumps, hits, pickups
  • Long (0.2-0.5s): Explosions, power-ups, transitions

Duty Cycle (PULSE only)

  • 0.125: Thin, sharp, piercing
  • 0.25: Classic NES lead sound
  • 0.5: Symmetric, full, fat
  • 0.75: Very fat, bass-like

Best Practices

Sound Design

  • Keep sounds short: Long sounds can overlap and cause issues
  • Use appropriate volumes: 0.6-0.8 is usually good for effects
  • Vary frequencies: Don't use the same frequency for everything
  • Test on hardware: ESP32 audio may sound different than PC

Music

  • Use one channel for music: Leave other channels for SFX
  • Keep melodies simple: Complex melodies can be hard to follow
  • Loop seamlessly: End your melody where it can loop naturally
  • Consider tempo: Faster games need faster music

Performance

  • Limit simultaneous sounds: Only 4 channels total
  • Music uses one channel: Plan your SFX accordingly
  • Don't spam sounds: Too many sounds can cause audio glitches
  • Use master volume: Easier than adjusting individual sounds

Common Patterns

Sound Effect Helper Function

void playJumpSound() {
    auto& audio = engine.getAudioEngine();
    AudioEvent evt{};
    evt.type = WaveType::PULSE;
    evt.frequency = 600.0f;
    evt.duration = 0.1f;
    evt.volume = 0.7f;
    evt.duty = 0.25f;
    audio.playEvent(evt);
}

Music State Management

class GameScene : public Scene {
    bool musicStarted = false;

    void init() override {
        // Don't start music here if scene can be re-initialized
    }

    void update(unsigned long deltaTime) override {
        if (!musicStarted) {
            engine.getMusicPlayer().play(GAME_MUSIC);
            musicStarted = true;
        }
        Scene::update(deltaTime);
    }
};

Troubleshooting

No Sound

  • Check audio backend is configured correctly
  • Verify sample rate matches backend
  • Check master volume is not 0
  • Ensure audio is initialized (engine.init())

Distorted Sound

  • Lower volume levels
  • Reduce sample rate (ESP32 DAC works better at 11025 Hz)
  • Check for too many simultaneous sounds
  • Verify hardware connections (ESP32)

Music Not Playing

  • Check music.isPlaying() status
  • Ensure track is properly defined
  • Verify MusicPlayer is updated (happens automatically)
  • Check that music channel is not being used by SFX

Next Steps

Now that you can add audio, learn about: - NES Audio Reference - Advanced audio techniques - Physics and Collisions - Make objects interact - User Interface - Create menus and HUDs


See also: - API Reference - AudioEngine - API Reference - MusicPlayer - API Reference - Audio Types - Manual - Audio Overview