Make a tone
This program is a small keyboard with no sound files. Hold a key, and the program works out each sample with a sine function. You learn that sound is only numbers that your code can make.
Press and hold a key in the demo. Make sure that your sound is on.
The full program
/* A small keyboard. Hold a key to play a sine wave that the program makes. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define NOTE_COUNT 8
#define NO_NOTE -1
#define SAMPLE_RATE 44100
#define CHUNK_SAMPLES 1024
#define KEEP_QUEUED (CHUNK_SAMPLES * 2)
#define LOUDNESS 0.25f
#define FADE_PER_SAMPLE 0.002f
#define KEY_WIDTH 64.0f
#define KEY_HEIGHT 240.0f
#define KEY_GAP 8.0f
#define KEYS_X 20.0f
#define KEYS_Y 120.0f
/* The frequencies, in hertz, of a C major scale. */
static const float frequencies[NOTE_COUNT] = {
261.63f, 293.66f, 329.63f, 349.23f, 392.00f, 440.00f, 493.88f, 523.25f
};
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_AudioStream *stream = NULL;
static int active_note = NO_NOTE;
static float frequency = 440.0f;
static float phase = 0.0f; /* The position in the wave, from 0 to 1. */
static float volume = 0.0f; /* The present loudness. */
/* Write the next samples of the wave. The phase and volume carry on from the
last chunk, so that the wave has no jump between chunks. */
static void make_samples(float *samples, int count)
{
float target = (active_note == NO_NOTE) ? 0.0f : LOUDNESS;
float step = frequency / (float)SAMPLE_RATE;
for (int i = 0; i < count; i++) {
if (volume < target) {
volume = SDL_min(volume + FADE_PER_SAMPLE, target);
} else if (volume > target) {
volume = SDL_max(volume - FADE_PER_SAMPLE, target);
}
samples[i] = volume * SDL_sinf(phase * 2.0f * SDL_PI_F);
phase += step;
if (phase >= 1.0f) {
phase -= 1.0f;
}
}
}
/* Keep a small amount of sound queued. More makes the keys feel slow. */
static void feed_audio(void)
{
while (SDL_GetAudioStreamQueued(stream) < KEEP_QUEUED * (int)sizeof(float)) {
float samples[CHUNK_SAMPLES];
make_samples(samples, CHUNK_SAMPLES);
SDL_PutAudioStreamData(stream, samples, sizeof(samples));
}
}
static SDL_FRect key_rect(int index)
{
SDL_FRect rect = { KEYS_X + (float)index * (KEY_WIDTH + KEY_GAP), KEYS_Y, KEY_WIDTH, KEY_HEIGHT };
return rect;
}
static int key_at(float x, float y)
{
SDL_FPoint point = { x, y };
for (int i = 0; i < NOTE_COUNT; i++) {
SDL_FRect rect = key_rect(i);
if (SDL_PointInRectFloat(&point, &rect)) {
return i;
}
}
return NO_NOTE;
}
static void press_key(int note)
{
if (note != NO_NOTE) {
active_note = note;
frequency = frequencies[note];
}
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
SDL_AudioSpec spec = { SDL_AUDIO_F32, 1, SAMPLE_RATE };
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Make a tone", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&window, &renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(renderer, 1);
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;
}
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) {
press_key(key_at(event->button.x, event->button.y));
} else if (event->type == SDL_EVENT_MOUSE_BUTTON_UP) {
active_note = NO_NOTE;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
feed_audio();
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
for (int i = 0; i < NOTE_COUNT; i++) {
SDL_FRect rect = key_rect(i);
if (i == active_note) {
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
}
SDL_RenderFillRect(renderer, &rect);
}
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
SDL_DestroyAudioStream(stream);
}/* A small keyboard. Hold a key to play a sine wave that the program makes. */
#include <SDL.h>
#include <stdbool.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define NOTE_COUNT 8
#define NO_NOTE -1
#define SAMPLE_RATE 44100
#define CHUNK_SAMPLES 1024
#define KEEP_QUEUED (CHUNK_SAMPLES * 2)
#define LOUDNESS 0.25f
#define FADE_PER_SAMPLE 0.002f
#define KEY_WIDTH 64.0f
#define KEY_HEIGHT 240.0f
#define KEY_GAP 8.0f
#define KEYS_X 20.0f
#define KEYS_Y 120.0f
/* The frequencies, in hertz, of a C major scale. */
static const float frequencies[NOTE_COUNT] = {
261.63f, 293.66f, 329.63f, 349.23f, 392.00f, 440.00f, 493.88f, 523.25f
};
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_AudioDeviceID device = 0;
static bool running = true;
static int active_note = NO_NOTE;
static float frequency = 440.0f;
static float phase = 0.0f; /* The position in the wave, from 0 to 1. */
static float volume = 0.0f; /* The present loudness. */
/* Write the next samples of the wave. The phase and volume carry on from the
last chunk, so that the wave has no jump between chunks. */
static void make_samples(float *samples, int count)
{
float target = (active_note == NO_NOTE) ? 0.0f : LOUDNESS;
float step = frequency / (float)SAMPLE_RATE;
for (int i = 0; i < count; i++) {
if (volume < target) {
volume = SDL_min(volume + FADE_PER_SAMPLE, target);
} else if (volume > target) {
volume = SDL_max(volume - FADE_PER_SAMPLE, target);
}
samples[i] = volume * SDL_sinf(phase * 2.0f * (float)M_PI);
phase += step;
if (phase >= 1.0f) {
phase -= 1.0f;
}
}
}
/* Keep a small amount of sound queued. More makes the keys feel slow. */
static void feed_audio(void)
{
while (SDL_GetQueuedAudioSize(device) < KEEP_QUEUED * sizeof(float)) {
float samples[CHUNK_SAMPLES];
make_samples(samples, CHUNK_SAMPLES);
SDL_QueueAudio(device, samples, sizeof(samples));
}
}
static SDL_FRect key_rect(int index)
{
SDL_FRect rect = { KEYS_X + (float)index * (KEY_WIDTH + KEY_GAP), KEYS_Y, KEY_WIDTH, KEY_HEIGHT };
return rect;
}
static int key_at(float x, float y)
{
SDL_FPoint point = { x, y };
for (int i = 0; i < NOTE_COUNT; i++) {
SDL_FRect rect = key_rect(i);
if (SDL_PointInFRect(&point, &rect)) {
return i;
}
}
return NO_NOTE;
}
static void press_key(int note)
{
if (note != NO_NOTE) {
active_note = note;
frequency = frequencies[note];
}
}
static void frame(void)
{
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = false;
} else if (event.type == SDL_MOUSEBUTTONDOWN) {
press_key(key_at((float)event.button.x, (float)event.button.y));
} else if (event.type == SDL_MOUSEBUTTONUP) {
active_note = NO_NOTE;
}
}
feed_audio();
SDL_SetRenderDrawColor(renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(renderer);
for (int i = 0; i < NOTE_COUNT; i++) {
SDL_FRect rect = key_rect(i);
if (i == active_note) {
SDL_SetRenderDrawColor(renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
} else {
SDL_SetRenderDrawColor(renderer, 235, 236, 240, SDL_ALPHA_OPAQUE);
}
SDL_RenderFillRectF(renderer, &rect);
}
SDL_RenderPresent(renderer);
}
int main(int argc, char *argv[])
{
SDL_AudioSpec spec;
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, "Make a tone");
SDL_RenderSetVSync(renderer, 1);
SDL_zero(spec);
spec.freq = SAMPLE_RATE;
spec.format = AUDIO_F32SYS;
spec.channels = 1;
spec.samples = CHUNK_SAMPLES;
device = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
if (device == 0) {
SDL_Log("SDL_OpenAudioDevice failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_PauseAudioDevice(device, 0);
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(frame, 0, 1);
#else
while (running) {
frame();
}
#endif
SDL_CloseAudioDevice(device);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}Pitch is frequency
A sine wave is the simplest sound. Its pitch depends on how many cycles it completes each second. This is its frequency, in hertz. The A above middle C is 440 hertz, so the wave repeats 440 times each second.
Make the samples
samples[i] = volume * SDL_sinf(phase * 2.0f * SDL_PI_F); samples[i] = volume * SDL_sinf(phase * 2.0f * (float)M_PI);The phase is the position in one cycle, from 0 to 1. For each sample, it moves forward by the frequency divided by the sample rate. A higher note takes larger steps, so it completes more cycles each second.
The samples are float values from -1.0 to 1.0. The program keeps them well below 1.0, because a full-scale sine wave is very loud.
Keep the wave continuous
phase += step;
if (phase >= 1.0f) {
phase -= 1.0f;
}The program makes sound in chunks, but the speaker needs one unbroken wave. So phase is a global, and each chunk starts where the last one ended. A chunk that restarts at phase 0 makes a jump in the wave at each join, and you hear a buzz.
A sudden change in volume also makes a click. So the program moves volume toward its target in small steps. This is a very short fade in and fade out.
Feed the queue
while (SDL_GetAudioStreamQueued(stream) < KEEP_QUEUED * (int)sizeof(float)) { while (SDL_GetQueuedAudioSize(device) < KEEP_QUEUED * sizeof(float)) {The program runs this test in each frame. If the queue is low, it adds a chunk. The amount in the queue is a compromise. A small queue answers your key press quickly, but a slow frame can empty it, and you hear a gap. A large queue is safe, but the sound comes late.
Here the queue holds about 46 milliseconds of sound, and each frame takes about 16 milliseconds. So one slow frame does no harm.
One note at a time
The program holds one frequency, so it plays one note at a time. To play a chord, you add several sine waves together, and you lower the volume so that the total stays in range.
Try this
- Replace the sine with a square wave. If the phase is below 0.5, use
0.5. If it is not, use-0.5. The sound is buzzy and old. - Add a second wave at twice the frequency, at half the volume. The tone sounds richer.
- Play a short tune from an array of notes and lengths, with a timer.
- Use the arrow keys to slide the frequency up and down.
What you learned
- A sine wave at a fixed frequency makes a pure tone.
- Each sample comes from the phase, and the phase must carry on between chunks.
- A small fade stops a click.
- The queue length is a balance between delay and gaps.