beep - Generating sounds in C
Introduction
How do you generate sound programmatically? beep
is a simple note-playing program I’ve written using the PortAudio library, written in C. (Ok, the title is a pun: I really mean the C programming language.) It is a minimal working example of sound generation: it is simple enough to understand very quickly, and is easy to play around with.
Try it
A prerequisite to compile and run beep
is that you must have a C compiler, and the PortAudio dev libraries installed. On Ubuntu this can be installed by running something like:
sudo apt-get install portaudio19-dev
(For other operating systems, see PortAudio’s website.)
To download and complile beep
:
git clone git@github.com:notfed/beep.git beep
cd beep
make
To play a 440Hz sine wave, run
./beep 440
Generating sounds in C (low-level)
To make sounds on a computer, we tell our speaker to vibrate. (Obviously.)
/.
/ \
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ \\
/ ||
/ || <- ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
/ ||
/ //
/ //
/ //
/ //
/ //
/ //
/ //
/ //
//
\______________/
v
It works as follows: 44100 times per second—44100 frames per second—our speaker asks our program, “choose any number for v
from -1.0 to 1.0”. We are in power to answer. We can return the same number for every frame, but then the speaker won’t move. We can give it -1,+1,-1,+1,-1+1 (rapidly moving back and forth 44100 times per second) but it will be too high pitched (44kHz) to hear. We can set v
equal to the result of a function the frame number: f(n)=sin(2*PI*440*n/44100)
, and, like mathematical magic, we will hear a clean tone.
That’s how low-level APIs for generating sound usually work, and that’s the API that PortAudio exposes to us. It’s nice that PortAudio offers such a low-level API, because it gives us power. But, in order to generate note-worthy sounds, we’ll want to add a minimal layer of abstraction.
Generating sounds in C (with an abstraction)
Instead of thinking of our interface to the speaker as simply an infinite stream of frames, we might want to think of it as a stream of notes. We can define a “note” as a structure which has a duration, a frequency, a waveform shape, and amplitude (volume). For example, imagine a 1-second-long, 440Hz sine wave, at 100% volume…that’s a note. Ideally, we’d like to be able to play a note with something like the following:
/* set up a note to play */
beep_note note = {
.frequency = freq,
.amplitude = 1.0,
.duration = 1000,
.waveform = beep_waveform_sine
};
/* play the note */
beep_note_play(¬e);
We’ll call a note a beep_note
and define it as:
typedef struct beep_note
{
float frequency;
float amplitude;
float duration;
beep_waveform waveform;
} beep_note;
When we’re actively playing notes, we’ll need to remember which frame we’re on. So, we’ll create an object (beep_head
) to keep track of which note is playing and which frame we’re at. (To keep things simple, for now, we can only play one note at a time; we can’t compose notes.)
typedef struct beep_head
{
beep_note note;
int frame;
} beep_head;
Our note will need a waveform (beep_waveform
). Examples of waveforms are sine, triangle, sawtooth, square—but a waveform could be any math formula. We can think of a waveform as a function which takes a beep_head
as an input and produces a frame value as output:
typedef float (*beep_waveform)(struct beep_head*);
For a sine wave, this will be f(frame)=sin(2*PI*frequency*frame/44100)*amplitude)
. In C:
static float beep_waveform_sine(beep_note_active *active)
{
beep_note *note = &active->note;
return note->amplitude * sin(2 * M_PI * note->frequency * active->frame / 44100);
}
The final code can be found on GitHub.
Future enhancements include adding envelopes, and supporting note composition, but the purpose of this program was to keep things simple.