The Idea

I wanted some royalty free sound assets for a game I’m making. They needed to be completely royalty free so that I could commit them in an open-source git repo. Then I thought, why not generate them myself.

The Basics of Sound

Sound is a vibration that travels through a medium, usually air, and is perceived by our ears. In the digital realm, sound is represented as a series of samples. These samples capture the amplitude of the sound wave at regular intervals. The rate at which these samples are taken is called the sample rate. A common sample rate is 44,100 samples per second (or 44.1 kHz), which is used in CDs.

Sinusoidal Waves and Sound Generation

While there are a lot of complexities into what makes up a sounds. Let’s start with the basics.

The simplest form of a sound wave is a sine wave. It’s smooth, periodic, and can represent pure tones. By varying the frequency of a sine wave, we can produce different musical notes. For instance, the note “A” has a frequency of 440 Hz, meaning it oscillates 440 times per second.

WAV Files: Storing the Sound

Given we are working in the digital realm, we need a way of storing these sound waves / samples. For this project we will save the sounds as .wav files.

WAV is a popular format for storing uncompressed audio data. A WAV file has a specific structure:

  1. RIFF Header: Identifies the file as a WAV format.
  2. Format Sub-chunk (fmt): Describes the audio data’s format.
  3. Data Sub-chunk: Contains the actual audio samples.

The beauty of WAV files is their simplicity and versatility. They can store audio data with varying bit depths and sample rates, making them suitable for various applications.

The Code

Now we have a plan: create a sine wave of a particular note (frequency) and store the samples in a WAV file. The next step is to build this out in code.

In the future we might want to connect up a MIDI keyboard to this application. So let’s use the C programming language to keep it low level and fast.

For our program we will start with three basic functions:

  1. Get User Input: The program will take in a desired frequency (e.g., 440 Hz for the note “A”) and an output filename.
  2. Create WAV File: The program will construct the necessary WAV headers and write the generated waveform data to produce a valid WAV file.
  3. Generate a Waveform: Using the sine function, the program will generate a waveform corresponding to the given frequency.

Follow along

I’m not going to be putting this together step-by step, so to see the full code in action, please grab a copy of this file from my GitHub repo:

https://github.com/danielhookins/soundgen/blob/1743292465573a2a62d1843c1e1f2a00c45def49/main.c

1. Get User Input

For now let’s keep this simple and just take some command line arguments.

  1. FREQUENCY is a double eg. 440.0
  2. output_filename is a pointer to the first character of the filename string literal

We also provide a usage example.

// Get command line arguments
if (argc != 3) {
    printf("Usage: %s <frequency> <output_filename>\n", argv[0]);
    return 1;
}

double FREQUENCY = atof(argv[1]);
char *output_filename = argv[2];

2. Create the WAV File

The WAV Header

A WAV file starts with a header that contains several fields. These fields include:

  • ChunkID: Typically the characters “RIFF”, indicating the file is a RIFF type.
  • ChunkSize: The size of the entire file.
  • Format: The characters “WAVE”, denoting the WAV format.
  • Subchunk1ID: The characters “fmt “, indicating the format sub-chunk.
  • … and several more fields that describe the audio data’s format and size.

The makeWavHeader Function

Let’s dissect the makeWavHeader function:

WavHeader makeWavHeader(int sampleRate, short numChannels, short bitsPerSample) {
    ...
}

The function takes in three parameters:

  • sampleRate: The number of samples per second (e.g., 44100 for CD quality).
  • numChannels: The number of audio channels (1 for mono, 2 for stereo).
  • bitsPerSample: The number of bits used for each sample (e.g., 16 bits).

Crafting the Header

  1. Basic Calculations: The function starts by calculating essential values like chunkSize, subchunk2Size, byteRate, and blockAlign based on the provided parameters. These values are crucial for audio players to understand the audio data’s layout and size.

  2. Setting Identifiers: The function then sets various identifier fields like chunkID to “RIFF”, format to “WAVE”, subchunk1ID to “fmt “, and subchunk2ID to “data”. These identifiers are like signposts, guiding the audio player through the file’s structure.

  3. Final Touches: Some additional fields, like subchunk1Size and audioFormat, are set to standard values. For instance, audioFormat is set to 1, indicating uncompressed PCM audio.

  4. Returning the Crafted Header Finally, the function returns the crafted WavHeader structure, ready to be written to a WAV file.

Writing the WAV Header

We can open a new file for writing (based on the filename provided) and write the header to it:

// Open file for writing
FILE *file = fopen(output_filename, "wb");
if (!file) {
    printf("Could not open file %s for writing\n", output_filename);
    return 1;
}

// Write WAV header
WavHeader header = makeWavHeader(SAMPLE_RATE, 1, 16);
fwrite(&header, sizeof(header), 1, file);

3. Generate a Waveform

Now we’re ready to write in the sound data.

// Write audio samples
for (int i = 0; i < SAMPLE_RATE * DURATION; i++) {
    double time = (double)i / SAMPLE_RATE;
    short sample = (short)(VOLUME * sin(FREQUENCY * TWO_PI * time));
    fwrite(&sample, sizeof(sample), 1, file);
}

The for loop is designed to iterate over each sample that we want to generate. The number of samples is determined by multiplying the SAMPLE_RATE (number of samples per second) with the DURATION (length of the audio in seconds).

For each iteration, we calculate the time for the current sample:

double time = (double)i / SAMPLE_RATE;

This gives us a time value in seconds, ranging from 0 to DURATION.

The heart of the sound generation happens in this line:

short sample = (short)(VOLUME * sin(FREQUENCY * TWO_PI * time));
  • We use the sine function to generate a waveform. The sine function oscillates between -1 and 1, producing a smooth, periodic wave.
  • FREQUENCY * TWO_PI * time calculates the phase of the sine wave, ensuring it oscillates at the desired frequency.
  • Multiplying by VOLUME scales the amplitude of the wave, controlling how loud the generated sound will be.

Finally, the generated sample is written to the output file:

fwrite(&sample, sizeof(sample), 1, file);

This writes the sample value to the file in binary format.

Compile it!

We’re now ready to compile it.

Be sure to compile the program using the lm falg, so that the math library is linked.

gcc main.c -o sounds.exe -lm

You should then be able to run the program to generate a WAV file of a perfect note. Here is an example C4 (Middle C) note:

sounds.exe 261.63 c4.wav

Next we will look at making more interesting sounds and add some character / timbre to our notes.