Play a sound
This program has two pads, and each one plays a short sound. Click a pad, and make sure that your sound is on. You learn what digital sound is, and how SDL sends it to the speakers.
The full program
/* Click a pad to play a sound from a WAV file. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SOUND_COUNT 2
#define NO_PAD -1
typedef struct {
Uint8 *data;
Uint32 length; /* bytes */
SDL_FRect pad;
SDL_Color color;
} Sound;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_AudioStream *stream = NULL;
static SDL_AudioSpec spec;
static Sound sounds[SOUND_COUNT] = {
{ NULL, 0, { 80.0f, 160.0f, 220.0f, 160.0f }, { 255, 157, 92, SDL_ALPHA_OPAQUE } },
{ NULL, 0, { 340.0f, 160.0f, 220.0f, 160.0f }, { 70, 120, 210, SDL_ALPHA_OPAQUE } }
};
static bool load_sound(Sound *sound, const char *path)
{
if (!SDL_LoadWAV(path, &spec, &sound->data, &sound->length)) {
SDL_Log("SDL_LoadWAV failed: %s", SDL_GetError());
return false;
}
return true;
}
/* Replace anything that is still playing, then add the new sound. */
static void play_sound(const Sound *sound)
{
SDL_ClearAudioStream(stream);
SDL_PutAudioStreamData(stream, sound->data, (int)sound->length);
}
static int pad_at(float x, float y)
{
SDL_FPoint point = { x, y };
for (int i = 0; i < SOUND_COUNT; i++) {
if (SDL_PointInRectFloat(&point, &sounds[i].pad)) {
return i;
}
}
return NO_PAD;
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Play a sound", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&window, &renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
/* Both files have the same format, so they can share one stream. */
if (!load_sound(&sounds[0], "assets/coin.wav") ||
!load_sound(&sounds[1], "assets/drop.wav")) {
return SDL_APP_FAILURE;
}
stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);
if (stream == NULL) {
SDL_Log("SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
/* A new stream starts paused. */
SDL_ResumeAudioStreamDevice(stream);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
SDL_ConvertEventToRenderCoordinates(renderer, event);
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
int pad = pad_at(event->button.x, event->button.y);
if (pad != NO_PAD) {
play_sound(&sounds[pad]);
}
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
for (int i = 0; i < SOUND_COUNT; i++) {
const SDL_Color *c = &sounds[i].color;
SDL_SetRenderDrawColor(renderer, c->r, c->g, c->b, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(renderer, &sounds[i].pad);
}
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
SDL_DestroyAudioStream(stream);
for (int i = 0; i < SOUND_COUNT; i++) {
SDL_free(sounds[i].data);
}
}/* Click a pad to play a sound from a WAV file. */
#include <SDL.h>
#include <stdbool.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SOUND_COUNT 2
#define NO_PAD -1
typedef struct {
Uint8 *data;
Uint32 length; /* bytes */
SDL_FRect pad;
SDL_Color color;
} Sound;
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_AudioDeviceID device = 0;
static SDL_AudioSpec spec;
static bool running = true;
static Sound sounds[SOUND_COUNT] = {
{ NULL, 0, { 80.0f, 160.0f, 220.0f, 160.0f }, { 255, 157, 92, SDL_ALPHA_OPAQUE } },
{ NULL, 0, { 340.0f, 160.0f, 220.0f, 160.0f }, { 70, 120, 210, SDL_ALPHA_OPAQUE } }
};
static bool load_sound(Sound *sound, const char *path)
{
if (SDL_LoadWAV(path, &spec, &sound->data, &sound->length) == NULL) {
SDL_Log("SDL_LoadWAV failed: %s", SDL_GetError());
return false;
}
return true;
}
/* Replace anything that is still playing, then add the new sound. */
static void play_sound(const Sound *sound)
{
SDL_ClearQueuedAudio(device);
SDL_QueueAudio(device, sound->data, sound->length);
}
static int pad_at(float x, float y)
{
SDL_FPoint point = { x, y };
for (int i = 0; i < SOUND_COUNT; i++) {
if (SDL_PointInFRect(&point, &sounds[i].pad)) {
return i;
}
}
return NO_PAD;
}
static void frame(void)
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
} else if (event.type == SDL_MOUSEBUTTONDOWN) {
int pad = pad_at((float)event.button.x, (float)event.button.y);
if (pad != NO_PAD) {
play_sound(&sounds[pad]);
}
}
}
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
for (int i = 0; i < SOUND_COUNT; i++) {
const SDL_Color *c = &sounds[i].color;
SDL_SetRenderDrawColor(renderer, c->r, c->g, c->b, SDL_ALPHA_OPAQUE);
SDL_RenderFillRectF(renderer, &sounds[i].pad);
}
SDL_RenderPresent(renderer);
}
static void clean_up(void)
{
if (device != 0) {
SDL_CloseAudioDevice(device);
}
for (int i = 0; i < SOUND_COUNT; i++) {
SDL_FreeWAV(sounds[i].data);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0, &window, &renderer) != 0) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_SetWindowTitle(window, "Play a sound");
SDL_RenderSetVSync(renderer, 1);
/* Both files have the same format, so they can share one device. */
if (!load_sound(&sounds[0], "assets/coin.wav") ||
!load_sound(&sounds[1], "assets/drop.wav")) {
clean_up();
return 1;
}
device = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
if (device == 0) {
SDL_Log("SDL_OpenAudioDevice failed: %s", SDL_GetError());
clean_up();
return 1;
}
/* A new device starts paused. A 0 here means do not pause. */
SDL_PauseAudioDevice(device, 0);
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(frame, 0, 1);
#else
while (running) {
frame();
}
#endif
clean_up();
return 0;
}Save coin.wav and drop.wav in your assets folder.
What digital sound is
A sound is a wave of air pressure. To store it, a computer measures the wave many times each second, and it keeps each measurement as a number. One measurement is a sample. These files hold 44,100 samples for each second of sound.
The speaker does the opposite: it follows the list of numbers and moves the air. So playing a sound means giving a list of numbers to the audio device, fast enough that it never runs out.
Load a WAV file
A WAV file holds samples with little or no compression, and SDL can read it with no extra library.
if (!SDL_LoadWAV(path, &spec, &sound->data, &sound->length)) { if (SDL_LoadWAV(path, &spec, &sound->data, &sound->length) == NULL) {The function gives you three things. data is a block of samples that SDL allocated, length is its size in bytes, and spec describes the format. The format has the sample rate, the number of channels, and the size of each sample.
For MP3 or Ogg files, you need the separate SDL_mixer library.
Send sound to the device
SDL3 uses an audio stream, a queue for samples that also converts their format.
stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, NULL, NULL);This opens the default device and connects a stream to it. A new stream starts paused, so the next call starts it:
SDL_ResumeAudioStreamDevice(stream);To play a sound, you put its samples in the stream:
SDL_ClearAudioStream(stream);
SDL_PutAudioStreamData(stream, sound->data, (int)sound->length); device = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);The NULL asks for the default device. A new device starts paused, and a 0 in the next call starts it:
SDL_PauseAudioDevice(device, 0);To play a sound, you add its samples to the queue of the device:
SDL_ClearQueuedAudio(device);
SDL_QueueAudio(device, sound->data, sound->length);The first line removes any sound that is still waiting, so a new click takes over at once. Without it, sounds play one after another, and a fast series of clicks falls behind.
The audio device runs on its own, on a separate thread. Your function adds the samples and returns at once, so the frame does not wait for the sound to end.
Sounds in the browser
A browser does not let a page make sound until the user clicks or presses a key. This stops pages from playing sound as they load. Your first sound must come after a click, as it does in this demo.
Free the sound
SDL allocated the samples, so SDL_AppQuit frees them with SDL_free. It also destroys the stream.
SDL allocated the samples, so the program frees them with SDL_FreeWAV. It also closes the device.
Build for the browser
emcc main.c -sUSE_SDL=3 --preload-file assets -o index.htmlemcc main.c -sUSE_SDL=2 --preload-file assets -o index.htmlTry this
- Make a sound play at the end of a point in Pong, and on each paddle hit.
- Remove the line that clears the queue, then click quickly. The sounds now play one after another.
- Find or record your own WAV file, and use it. Make sure that you have the right to use it.
What you learned
- Digital sound is a long list of numbers called samples.
- SDL reads WAV files with no extra library.
- The audio device takes samples from a queue, on its own thread.
- A browser allows sound only after the user acts.