WAV File Synthesis From Scratch - C
Asked Answered
J

1

8

Recently I saw a video lecture in my CS 101 class that inspired me to start playing with the WAV File Format in C. My project today has been creating sounds using a simple mathematical sine function. Despite a couple obstacles, my program can now accept several inputs(frequencies of waves, amplitudes of waves, sampling rate, etc.) and create a wav file containing the specified pitches.

However, when playing these tones on my computer speakers, there is a strange, rhythmic popping sound, which varies with the sampling rate. At higher sampling rates, the frequency of the popping sound increases and turns into an annoying whining sound.

The strange part is that the popping sound is consistent across different computers with the same file.

Below I will post the code that I use to generate the WAV file. Any insights into what might be causing this phenomenon will be appreciated. It's probably just a stupid mistake on my part somewhere. :)

#include <stdio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <math.h>

struct WAVHeader {
    char ChunkID[4];
    uint32_t ChunkSize;
    char RIFFType[4];
};

struct FormatHeader {
    char ChunkID[4];
    uint32_t ChunkSize;
    uint16_t CompressionCode;
    uint16_t Channels;
    uint32_t SampleRate;
    uint32_t AvgBytesPerSec;
    uint16_t BlockAlign;
    uint16_t SigBitsPerSamp;
};

struct DataHeader {
    char ChunkID[4];
    uint32_t ChunkSize;

};


void main(int argc, char * argv[]) {

//Check for valid number of arguments or display help
if(argc < 8) {
    printf("Usage:\n./Tone -l [length] -s [frequency] [amplitude] -o [output-file] -r [sample-rate]\n");
    printf("-l length of tone to produce in seconds\n");    
    printf("-s Creates sine wave. Can be used multiple times. Frequency (Hz) and amplitude (0 - 32767) of each tone. \n");  
    printf("-o File to write to\n");
    printf("-r samples per second (kHz). Note: Must be double highest frequency in tone.\n");   
    return;
}

//Organize arguments
int length, sinf[10], sina[10], samplerate;
memset(sinf, 0, sizeof(int) * 10);
memset(sina, 0, sizeof(int) * 10);
char * output = NULL;
int i = 0;
int count;
for(count = 1; count < argc; count++){
    char first = *argv[count];
    int second = *(argv[count] + 1);    
    if (first == '-') {
        switch (second) {
            case 's':
                sinf[i] = atoi(argv[count+1]);
                sina[i] = atoi(argv[count+2]);
                i++;
                break;
            case 'l':
                length = atoi(argv[count+1]);
                break;
            case 'o':
                output = argv[count+1];
                break;
            case 'r':
                samplerate = atoi(argv[count+1]) * 1000;
                break;
        }
    }
}

//Allocate memory for wav file
size_t size = sizeof(struct WAVHeader) + sizeof(struct FormatHeader) + sizeof(struct DataHeader) + (length * samplerate * 2);
void * buffer = malloc(size);

//Fill buffer with headers
struct WAVHeader * WAV = (struct WAVHeader *)buffer;
struct FormatHeader * Format = (struct FormatHeader *)(WAV + 1);
struct DataHeader * Data = (struct DataHeader *)(Format + 1);

strcpy(WAV->ChunkID, "RIFF");
WAV->ChunkSize = (uint32_t)size - 8;
strcpy(WAV->RIFFType, "WAVE");

strcpy(Format->ChunkID, "fmt ");
Format->ChunkSize = 16;
Format->CompressionCode = 1;
Format->Channels = 1;
Format->SampleRate = (uint32_t)samplerate;
Format->SigBitsPerSamp = 16;
Format->BlockAlign = 2;
Format->AvgBytesPerSec = Format->BlockAlign * samplerate;

strcpy(Data->ChunkID, "data");
Data->ChunkSize = length * samplerate * 2;

//Generate Sound
printf("Generating sound...\n");
short * sound = (short *)(Data + 1);
short total;
float time;
float increment = 1.0/(float)samplerate;
for (time = 0; time < length; time += increment){
    total = 0;
    for (i = 0; i < 10; i++) {
        total += sina[i] * sin((float)sinf[i] * time * (2 * 3.1415926));
    }
    *(sound + (int)(time * samplerate)) = total;
    //printf("Time: %f Value: %hd\n", time, total);
}

//Write buffer to file
FILE * out = fopen(output, "w");
fwrite(buffer, size, 1, out);
printf("Wrote to %s\n", output);

return;

}
Jaconet answered 1/6, 2012 at 3:45 Comment(5)
What happens to samplerate if the -r option is not issued?Tedder
This is not your problem, but you should be using memcpy, rather than strcpy as that copies the trailing '\0'.Aerugo
Nice catch @Michael. I'm surprised that hasn't botched the whole thing up yet. :)Jaconet
Oh. The reason it's not botched up is because I write over it right after I make the mistake. Well. If I can't be good, I might as well be lucky. :DJaconet
btw I also required stdint.h and stdlib.h to compile, (and gcc -lm to link)Annaleeannaliese
R
7

I think this is your core problem:

*(sound + (int)(time * samplerate)) = total;

I suspect that (time*samplerate) doesn't always increase on integer boundaries due to floating point rounding errors. Hence, some sample positions are skipped and/or overwritten due to rounding errors. That's just a guess.

But also, as "time" increases, the multiplication of "time * frequency * 2PI" will overflow within a float. So you should normalize "time" such that it doesn't increase forever.

In any case, I validated this modified loop works (and sounds) just fine:

float TWOPI = 6.28318531f;
unsigned int sample_count = length * samplerate;

for (unsigned int i = 0; i < sample_count; i++)
{
    unsigned int j = i % samplerate; // normalize the sample position so that we don't blow up in the subsequent multiplication
    float f = 0.0f;
    int result;

    for (int x = 0; x < 10; x++)
    {
        f += sina[x] * sin((sinf[x] * j * TWOPI) / samplerate);
    }

    result = (long)f;

    //clamp to 16-bit
    if (result > 32767)
    {
        result = 32767;
    }
    else if (result < -32768)
    {
        result = -32768;
    }

    sound[i] = (short)result;

    //printf("%d\n", sound[i]);

}
Ranchman answered 1/6, 2012 at 5:26 Comment(4)
The loop to 10 is to support up to 10 sines all added together.Aerugo
Oh I see. I updated my answer to include his array loop. And to properly clamp his results before casing the float back to a short.Ranchman
Thank you @Ranchman for your suggestion. I'll try it and see if it cures my ailment.Jaconet
Thanks a ton, Selbie. Everything sounds wonderful. Now I'm off to make music.Jaconet

© 2022 - 2024 — McMap. All rights reserved.