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