Skip to content
Advertisement

Writing a wave generator with SDL

I’ve coded a simple sequencer in C with SDL 1.2 and SDL_mixer(to play .wav file). It works well and I want to add some audio synthesis to this program. I’ve look up the and I found this sinewave code using SDL2(https://github.com/lundstroem/synth-samples-sdl2/blob/master/src/synth_samples_sdl2_2.c)

Here’s how the sinewave is coded in the program:

static void build_sine_table(int16_t *data, int wave_length)
 {
    /* 
        Build sine table to use as oscillator:
        Generate a 16bit signed integer sinewave table with 1024 samples.
        This table will be used to produce the notes.
        Different notes will be created by stepping through
        the table at different intervals (phase).
    */

    double phase_increment = (2.0f * pi) / (double)wave_length;
    double current_phase = 0;
    for(int i = 0; i < wave_length; i++) {
        int sample = (int)(sin(current_phase) * INT16_MAX);
        data[i] = (int16_t)sample;
        current_phase += phase_increment;
    }
}

static double get_pitch(double note) {

    /*
        Calculate pitch from note value.
        offset note by 57 halfnotes to get correct pitch from the range we have chosen for the notes.
    */
    double p = pow(chromatic_ratio, note - 57);
    p *= 440;
    return p;
}

static void audio_callback(void *unused, Uint8 *byte_stream, int byte_stream_length) {

    /*
        This function is called whenever the audio buffer needs to be filled to allow
        for a continuous stream of audio.
        Write samples to byteStream according to byteStreamLength.
        The audio buffer is interleaved, meaning that both left and right channels exist in the same
        buffer.
    */

    // zero the buffer
    memset(byte_stream, 0, byte_stream_length);

    if(quit) {
        return;
    }

    // cast buffer as 16bit signed int.
    Sint16 *s_byte_stream = (Sint16*)byte_stream;

    // buffer is interleaved, so get the length of 1 channel.
    int remain = byte_stream_length / 2;

    // split the rendering up in chunks to make it buffersize agnostic.
    long chunk_size = 64;
    int iterations = remain/chunk_size;
    for(long i = 0; i < iterations; i++) {
        long begin = i*chunk_size;
        long end = (i*chunk_size) + chunk_size;
        write_samples(s_byte_stream, begin, end, chunk_size);
    }
}

static void write_samples(int16_t *s_byteStream, long begin, long end, long length) {

    if(note > 0) {
        double d_sample_rate = sample_rate;
        double d_table_length = table_length;
        double d_note = note;

        /*
            get correct phase increment for note depending on sample rate and table length.
        */
        double phase_increment = (get_pitch(d_note) / d_sample_rate) * d_table_length;

        /*
            loop through the buffer and write samples.
        */
        for (int i = 0; i < length; i+=2) {
            phase_double += phase_increment;
            phase_int = (int)phase_double;
            if(phase_double >= table_length) {
                double diff = phase_double - table_length;
                phase_double = diff;
                phase_int = (int)diff;
            }

            if(phase_int < table_length && phase_int > -1) {
                if(s_byteStream != NULL) {
                    int16_t sample = sine_wave_table[phase_int];
                    sample *= 0.6; // scale volume.
                    s_byteStream[i+begin] = sample; // left channel
                    s_byteStream[i+begin+1] = sample; // right channel
                }
            }
        }
    }
}

I don’t understand how I could change the sinewave formula to genrate other waveform like square/triangle/saw ect…

EDIT: Because I forgot to explain it, here’s what I tried. I followed the example I’ve seen on this video series(https://www.youtube.com/watch?v=tgamhuQnOkM). The source code of the method provided by the video is on github, and the wave generation code is looking like this:

double w(double dHertz)
{
    return dHertz * 2.0 * PI;
}

// General purpose oscillator


double osc(double dHertz, double dTime, int nType = OSC_SINE)
{
    switch (nType)
    {
    case OSC_SINE: // Sine wave bewteen -1 and +1
        return sin(w(dHertz) * dTime);

    case OSC_SQUARE: // Square wave between -1 and +1
        return sin(w(dHertz) * dTime) > 0 ? 1.0 : -1.0;

    case OSC_TRIANGLE: // Triangle wave between -1 and +1
        return asin(sin(w(dHertz) * dTime)) * (2.0 / PI);
}

Because the C++ code here uses windows soun api I could not copy/paste this method to make it work on the piece of code I’ve found using SDL2. So I tried to this in order to obtain a square wave:

static void build_sine_table(int16_t *data, int wave_length)
 {
    double phase_increment = ((2.0f * pi) / (double)wave_length) > 0 ? 1.0 : -1.0;
    double current_phase = 0;
    for(int i = 0; i < wave_length; i++) {
        int sample = (int)(sin(current_phase) * INT16_MAX);
        data[i] = (int16_t)sample;
        current_phase += phase_increment;
    }
}

This didn’t gave me a square wave but more a saw wave. Here’s what I tried to get a triangle wave:

static void build_sine_table(int16_t *data, int wave_length)
 {
    double phase_increment = (2.0f * pi) / (double)wave_length;
    double current_phase = 0;
    for(int i = 0; i < wave_length; i++) {
        int sample = (int)(asin(sin(current_phase) * INT16_MAX)) * (2 / pi);
        data[i] = (int16_t)sample;
        current_phase += phase_increment;
    }
}

This also gave me another type of waveform, not triangle.

Advertisement

Answer

You’d replace the sin function call with call to one of the following:

// this is a helper function only
double normalize(double phase)
{
  double cycles = phase/(2.0*M_PI);
  phase -= trunc(cycles) * 2.0 * M_PI;
  if (phase < 0) phase += 2.0*M_PI;
  return phase;
}

double square(double phase)
{ return (normalize(phase) < M_PI) ? 1.0 : -1.0; }

double sawtooth(double phase)
{ return -1.0 + normalize(phase) / M_PI; }

double triangle(double phase)
{
  phase = normalize(phase);
  if (phase >= M_PI)
    phase = 2*M_PI - phase;
  return -1.0 + 2.0 * phase / M_PI;
}

You’d be building tables just like you did for the sine, except they’d be the square, sawtooth and triangle tables, respectively.

Advertisement