|
| 1 | +[⬅ Back to Graphics Index](../index.md) | [⬅ Back to Sprite](../Sprite/sprite.md) |
| 2 | + |
| 3 | +# Audio Module |
| 4 | + |
| 5 | +Sound effects playback using SDL2 backend. |
| 6 | + |
| 7 | +> **Note**: Requires SDL2 and the compile flag `-DPYTHONIC_ENABLE_SDL2_AUDIO -lSDL2` |
| 8 | +
|
| 9 | +--- |
| 10 | + |
| 11 | +## Quick Start |
| 12 | + |
| 13 | +```cpp |
| 14 | +#include <pythonic/TerminalGraphics/Audio/Sound.hpp> |
| 15 | +using namespace Pythonic::TG; |
| 16 | + |
| 17 | +SoundBuffer buffer; |
| 18 | +if (buffer.loadFromFile("jump.wav")) |
| 19 | +{ |
| 20 | + Sound sound; |
| 21 | + sound.setBuffer(buffer); |
| 22 | + sound.play(); |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Compilation |
| 29 | + |
| 30 | +The audio module uses SDL2 for cross-platform audio playback. Enable it with: |
| 31 | + |
| 32 | +```bash |
| 33 | +g++ -std=c++20 -Iinclude \ |
| 34 | + -DPYTHONIC_ENABLE_SDL2_AUDIO \ |
| 35 | + -o game game.cpp \ |
| 36 | + -lSDL2 -pthread |
| 37 | +``` |
| 38 | + |
| 39 | +Without `-DPYTHONIC_ENABLE_SDL2_AUDIO`, audio calls compile but do nothing. |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## SoundBuffer |
| 44 | + |
| 45 | +Container for audio sample data. Loads WAV files via SDL2. |
| 46 | + |
| 47 | +### Loading |
| 48 | + |
| 49 | +| Method | Return | Description | |
| 50 | +| -------------------- | ------ | ------------- | |
| 51 | +| `loadFromFile(path)` | `bool` | Load WAV file | |
| 52 | + |
| 53 | +```cpp |
| 54 | +SoundBuffer jumpBuffer; |
| 55 | +if (!jumpBuffer.loadFromFile("sounds/jump.wav")) |
| 56 | +{ |
| 57 | + // Handle error |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +### Properties |
| 62 | + |
| 63 | +| Method | Return | Description | |
| 64 | +| ------------------- | ---------------- | --------------------------- | |
| 65 | +| `getSamples()` | `const int16_t*` | Raw sample data | |
| 66 | +| `getSampleCount()` | `size_t` | Number of samples | |
| 67 | +| `getSampleRate()` | `unsigned` | Sample rate (Hz) | |
| 68 | +| `getChannelCount()` | `unsigned` | Channels (1=mono, 2=stereo) | |
| 69 | +| `getDuration()` | `float` | Audio duration in seconds | |
| 70 | + |
| 71 | +```cpp |
| 72 | +float duration = buffer.getDuration(); |
| 73 | +std::cout << "Sound is " << duration << " seconds\n"; |
| 74 | +``` |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## Sound |
| 79 | + |
| 80 | +Playable sound instance. Multiple sounds can play simultaneously. |
| 81 | + |
| 82 | +### Constructor |
| 83 | + |
| 84 | +| Constructor | Description | |
| 85 | +| --------------- | --------------------- | |
| 86 | +| `Sound()` | Create without buffer | |
| 87 | +| `Sound(buffer)` | Create with buffer | |
| 88 | + |
| 89 | +### Buffer |
| 90 | + |
| 91 | +| Method | Description | |
| 92 | +| ------------------- | ------------------ | |
| 93 | +| `setBuffer(buffer)` | Set sound buffer | |
| 94 | +| `getBuffer()` | Get buffer pointer | |
| 95 | + |
| 96 | +```cpp |
| 97 | +Sound sound; |
| 98 | +sound.setBuffer(jumpBuffer); |
| 99 | +``` |
| 100 | + |
| 101 | +### Playback |
| 102 | + |
| 103 | +| Method | Description | |
| 104 | +| ------------- | ---------------------- | |
| 105 | +| `play()` | Start/restart playback | |
| 106 | +| `pause()` | Pause playback | |
| 107 | +| `stop()` | Stop playback | |
| 108 | +| `getStatus()` | Get current state | |
| 109 | + |
| 110 | +| Status | Description | |
| 111 | +| ---------------- | ----------------- | |
| 112 | +| `Sound::Stopped` | Not playing | |
| 113 | +| `Sound::Playing` | Currently playing | |
| 114 | +| `Sound::Paused` | Paused | |
| 115 | + |
| 116 | +```cpp |
| 117 | +sound.play(); |
| 118 | + |
| 119 | +// Check status |
| 120 | +if (sound.getStatus() == Sound::Playing) |
| 121 | +{ |
| 122 | + // Still playing |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +### Volume |
| 127 | + |
| 128 | +| Method | Description | |
| 129 | +| ------------------- | ------------------------- | |
| 130 | +| `setVolume(volume)` | Set volume (0.0 to 100.0) | |
| 131 | +| `getVolume()` | Get current volume | |
| 132 | + |
| 133 | +```cpp |
| 134 | +sound.setVolume(50.0f); // 50% volume |
| 135 | +``` |
| 136 | + |
| 137 | +### Loop |
| 138 | + |
| 139 | +| Method | Description | |
| 140 | +| --------------- | ---------------------- | |
| 141 | +| `setLoop(bool)` | Enable/disable looping | |
| 142 | +| `getLoop()` | Check if looping | |
| 143 | + |
| 144 | +```cpp |
| 145 | +Sound bgm(musicBuffer); |
| 146 | +bgm.setLoop(true); // Loop forever |
| 147 | +bgm.play(); |
| 148 | +``` |
| 149 | +
|
| 150 | +### Pitch |
| 151 | +
|
| 152 | +| Method | Description | |
| 153 | +| ----------------- | ------------------------ | |
| 154 | +| `setPitch(pitch)` | Set pitch (1.0 = normal) | |
| 155 | +| `getPitch()` | Get current pitch | |
| 156 | +
|
| 157 | +```cpp |
| 158 | +sound.setPitch(0.5f); // Half speed, lower pitch |
| 159 | +sound.setPitch(2.0f); // Double speed, higher pitch |
| 160 | +``` |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## Multiple Sounds |
| 165 | + |
| 166 | +Multiple sounds can play simultaneously via callback-based mixing: |
| 167 | + |
| 168 | +```cpp |
| 169 | +SoundBuffer jumpBuf, coinBuf, hitBuf; |
| 170 | +jumpBuf.loadFromFile("jump.wav"); |
| 171 | +coinBuf.loadFromFile("coin.wav"); |
| 172 | +hitBuf.loadFromFile("hit.wav"); |
| 173 | + |
| 174 | +Sound jump(jumpBuf); |
| 175 | +Sound coin(coinBuf); |
| 176 | +Sound hit(hitBuf); |
| 177 | + |
| 178 | +// All can play at the same time |
| 179 | +jump.play(); |
| 180 | +coin.play(); // Overlaps with jump |
| 181 | +// hit.play() later overlaps with both |
| 182 | +``` |
| 183 | +
|
| 184 | +--- |
| 185 | +
|
| 186 | +## Preloading Sounds |
| 187 | +
|
| 188 | +For latency-free playback, load all sounds at startup: |
| 189 | +
|
| 190 | +```cpp |
| 191 | +// At initialization |
| 192 | +struct GameSounds |
| 193 | +{ |
| 194 | + SoundBuffer jumpBuffer; |
| 195 | + SoundBuffer coinBuffer; |
| 196 | + SoundBuffer hitBuffer; |
| 197 | + SoundBuffer gameOverBuffer; |
| 198 | +
|
| 199 | + Sound jump; |
| 200 | + Sound coin; |
| 201 | + Sound hit; |
| 202 | + Sound gameOver; |
| 203 | +
|
| 204 | + bool load() |
| 205 | + { |
| 206 | + if (!jumpBuffer.loadFromFile("jump.wav")) return false; |
| 207 | + if (!coinBuffer.loadFromFile("coin.wav")) return false; |
| 208 | + if (!hitBuffer.loadFromFile("hit.wav")) return false; |
| 209 | + if (!gameOverBuffer.loadFromFile("gameover.wav")) return false; |
| 210 | +
|
| 211 | + jump.setBuffer(jumpBuffer); |
| 212 | + coin.setBuffer(coinBuffer); |
| 213 | + hit.setBuffer(hitBuffer); |
| 214 | + gameOver.setBuffer(gameOverBuffer); |
| 215 | +
|
| 216 | + return true; |
| 217 | + } |
| 218 | +} sounds; |
| 219 | +
|
| 220 | +// In game code |
| 221 | +sounds.jump.play(); // Instant playback |
| 222 | +``` |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Error Handling |
| 227 | + |
| 228 | +```cpp |
| 229 | +SoundBuffer buffer; |
| 230 | +if (!buffer.loadFromFile("missing.wav")) |
| 231 | +{ |
| 232 | + std::cerr << "Failed to load sound\n"; |
| 233 | + // Continue without sound, or use fallback |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +--- |
| 238 | + |
| 239 | +## Complete Example: Game with Sound Effects |
| 240 | + |
| 241 | +```cpp |
| 242 | +#include <pythonic/TerminalGraphics/TerminalGraphics.hpp> |
| 243 | +using namespace Pythonic::TG; |
| 244 | + |
| 245 | +int main() |
| 246 | +{ |
| 247 | + Canvas canvas(160, 80, RenderMode::Braille); |
| 248 | + |
| 249 | + // Load sounds |
| 250 | + SoundBuffer jumpBuf, scoreBuf; |
| 251 | + jumpBuf.loadFromFile("sounds/jump.wav"); |
| 252 | + scoreBuf.loadFromFile("sounds/score.wav"); |
| 253 | + |
| 254 | + Sound jumpSound(jumpBuf); |
| 255 | + Sound scoreSound(scoreBuf); |
| 256 | + jumpSound.setVolume(80.0f); |
| 257 | + |
| 258 | + // Game state |
| 259 | + float playerY = 40; |
| 260 | + float velocityY = 0; |
| 261 | + int score = 0; |
| 262 | + bool wasSpacePressed = false; |
| 263 | + |
| 264 | + Clock clock; |
| 265 | + |
| 266 | + while (!Keyboard::isKeyPressed(Key::Escape)) |
| 267 | + { |
| 268 | + float dt = clock.restart().asSeconds(); |
| 269 | + |
| 270 | + // Jump on space (one-shot) |
| 271 | + bool spacePressed = Keyboard::isKeyPressed(Key::Space); |
| 272 | + if (spacePressed && !wasSpacePressed) |
| 273 | + { |
| 274 | + velocityY = -200; |
| 275 | + jumpSound.play(); |
| 276 | + } |
| 277 | + wasSpacePressed = spacePressed; |
| 278 | + |
| 279 | + // Physics |
| 280 | + velocityY += 500 * dt; // Gravity |
| 281 | + playerY += velocityY * dt; |
| 282 | + |
| 283 | + // Bounds |
| 284 | + if (playerY > 70) |
| 285 | + { |
| 286 | + playerY = 70; |
| 287 | + velocityY = 0; |
| 288 | + } |
| 289 | + |
| 290 | + // Score (example trigger) |
| 291 | + static float scoreTimer = 0; |
| 292 | + scoreTimer += dt; |
| 293 | + if (scoreTimer >= 2.0f) |
| 294 | + { |
| 295 | + score++; |
| 296 | + scoreSound.play(); |
| 297 | + scoreTimer = 0; |
| 298 | + } |
| 299 | + |
| 300 | + // Draw |
| 301 | + canvas.clear(); |
| 302 | + canvas.fillCircle(80, playerY, 5, Color::Yellow); |
| 303 | + Text::draw(canvas, 5, 2, "Score: " + std::to_string(score)); |
| 304 | + canvas.display(); |
| 305 | + |
| 306 | + sleep(Time::milliseconds(16)); |
| 307 | + } |
| 308 | + |
| 309 | + return 0; |
| 310 | +} |
| 311 | +``` |
| 312 | +
|
| 313 | +--- |
| 314 | +
|
| 315 | +## Technical Notes |
| 316 | +
|
| 317 | +### SDL2 Backend Details |
| 318 | +
|
| 319 | +- Uses callback-based audio mixing for low latency |
| 320 | +- All playing sounds are mixed in real-time in `audioCallback()` |
| 321 | +- Audio device is initialized on first `Sound::play()` |
| 322 | +- Cleanup is automatic via `atexit()` handler |
| 323 | +- Thread-safe: uses mutex for sound list access |
| 324 | +
|
| 325 | +### Supported Formats |
| 326 | +
|
| 327 | +- WAV (via SDL2_LoadWAV) |
| 328 | +- Other formats require SDL2_mixer extension |
| 329 | +
|
| 330 | +--- |
| 331 | +
|
| 332 | +[⬅ Back to Sprite](../Sprite/sprite.md) | [Next: Text Module →](../Text/text.md) |
0 commit comments