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 (Retro/Low-Cost)¶
The internal DAC is ideal for rapid prototyping or "Game Boy" style sounds.
PAM8302A Connection:
| PAM8302A | ESP32 | Notes |
|---|---|---|
| VCC | 5V (or 3.3V*) | |
| GND | GND | |
| A+ (IN+) | GPIO25 (DAC1) | Or GPIO26 (DAC2) |
| A- (IN-) | GND | |
| SPK+ | Speaker + | |
| SPK- | Speaker - |
#include <drivers/esp32/ESP32_DAC_AudioBackend.h>
const int DAC_PIN = 25; // GPIO 25 or 26
// 11025 Hz is recommended for the internal DAC
pixelroot32::drivers::esp32::ESP32_DAC_AudioBackend audioBackend(DAC_PIN, 11025);
pixelroot32::audio::AudioConfig audioConfig(&audioBackend, audioBackend.getSampleRate());
Limitation: The internal DAC has an 8-bit resolution, producing a constant background noise ("hiss"). The driver operates in software mode to avoid hardware conflicts.
ESP32: External I2S DAC (Recommended)¶
For clean and professional audio quality, use an I2S module like the MAX98357A.
MAX98357A Connection:
| MAX98357A | ESP32 | Notes |
|---|---|---|
| VIN | 5V | Recommended for higher power |
| GND | GND | |
| BCLK | GPIO26 | Bit Clock |
| LRC (WS) | GPIO25 | Word Select |
| DIN | GPIO22 | Data In |
| SD | 3.3V | Or GPIO control for Mute |
| GAIN | GND / NC | Configurable per datasheet |
| SPK+ | Speaker + | |
| SPK- | Speaker - |
#include <drivers/esp32/ESP32_I2S_AudioBackend.h>
const int I2S_BCLK = 26;
const int I2S_LRCK = 25;
const int I2S_DOUT = 22;
pixelroot32::drivers::esp32::ESP32_I2S_AudioBackend audioBackend(
I2S_BCLK, I2S_LRCK, I2S_DOUT, 22050
);
pixelroot32::audio::AudioConfig audioConfig(&audioBackend, 22050);
Native (PC): SDL2¶
#include <drivers/native/SDL2_AudioBackend.h>
pixelroot32::drivers::native::SDL2_AudioBackend audioBackend(22050, 1024);
pixelroot32::audio::AudioConfig audioConfig(&audioBackend, 22050);
Architecture: Decoupled and Automatic Hardware Adaptation¶
PixelRoot32 uses a high-performance audio architecture that completely decouples audio processing from the main game loop and automatically adapts to your hardware's capabilities.
- Automatic Core Management (ESP32):
- Dual-Core: The audio subsystem automatically pins itself to Core 0, while your game logic and rendering run on Core 1. This prevents audio "stuttering" even when the game is performing heavy calculations.
- Single-Core (ESP32-S2/C3/etc.): The engine detects the single-core configuration and runs the audio task with high priority on the only available core, letting FreeRTOS handle the multitasking efficiently.
- Sample-Accurate Timing: Unlike many engines that update audio once per frame, PixelRoot32 uses a sample-based sequencer. This ensures that music and sound effects have perfect timing, independent of the game's frame rate.
- Lock-Free Communication: When you call
playEvent()ormusicPlayer.play(), the engine enqueues a command into a thread-safe, lock-free queue. The audio core picks up these commands asynchronously.
The engine handles all hardware detection and task pinning automatically via the PlatformCapabilities system. You never have to call an update() method for audio in your game code.
PlatformCapabilities and Task Pinning¶
The engine uses the PlatformCapabilities structure to determine the optimal threading strategy for your specific hardware.
hasDualCore: Indicates if the hardware supports parallel execution of game and audio tasks.audioCoreId/mainCoreId: Specifies which CPU core is assigned to each subsystem. On dual-core ESP32, audio typically runs on Core 0 while the game loop runs on Core 1.audioPriority: The task priority assigned to the audio sequencer to ensure glitch-free playback.
You can inspect the detected capabilities through the engine:
const auto& caps = engine.getPlatformCapabilities();
if (caps.hasDualCore) {
// Audio is running on a dedicated core (Core 0 by default)
}
Sound Effects¶
Sound effects are created using AudioEvent structures and played through the AudioEngine.
AudioEvent Structure¶
#include <audio/AudioTypes.h>
pixelroot32::audio::AudioEvent soundEffect{};
soundEffect.type = pixelroot32::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
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.25f;
audio.playEvent(jumpSound);
Common Sound Effects¶
Here are some example sound effects you can use:
namespace SoundEffects {
// Jump sound
inline pixelroot32::audio::AudioEvent jump() {
pixelroot32::audio::AudioEvent evt{};
evt.type = pixelroot32::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 pixelroot32::audio::AudioEvent coin() {
pixelroot32::audio::AudioEvent evt{};
evt.type = pixelroot32::audio::WaveType::PULSE;
evt.frequency = 1500.0f;
evt.duration = 0.12f;
evt.volume = 0.8f;
evt.duty = 0.5f;
return evt;
}
// Explosion
inline pixelroot32::audio::AudioEvent explosion() {
pixelroot32::audio::AudioEvent evt{};
evt.type = pixelroot32::audio::WaveType::NOISE;
evt.frequency = 200.0f;
evt.duration = 0.3f;
evt.volume = 0.9f;
return evt;
}
// Hit/damage
inline pixelroot32::audio::AudioEvent hit() {
pixelroot32::audio::AudioEvent evt{};
evt.type = pixelroot32::audio::WaveType::NOISE;
evt.frequency = 300.0f;
evt.duration = 0.15f;
evt.volume = 0.6f;
return evt;
}
}
// Usage
audio.playEvent(SoundEffects::jump());
Background Music¶
📖 For comprehensive MusicPlayer guide with advanced examples, see MusicPlayer Integration Guide
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 pixelroot32::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 pixelroot32::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 pixelroot32::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 pixelroot32::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¶
- Automatic Protection: The non-linear mixer now prevents most digital clipping automatically.
- Check for hardware-specific issues (bad cables, poor power supply).
- Reduce sample rate (ESP32 DAC works better at 11025 Hz).
- Verify that you are not manually scaling volumes above 1.0.
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: