audio – Sawtooth waveform generation


Wrote a function that fills a float array with a sawtooth wave of a given frequency. Sounds bad, especially high frequencies. Apparently, this is due to the fact that I incorrectly take into account the accumulating fractional samples. But I can't think of how to take them into account correctly?

void sawtooth(double upPercent, float volume, float freq,
    uint sampleRate, ArrayRange<float> outSamples)
    const uint fullPeriodsCount=(uint)(outSamples.Count()*freq/sampleRate);
    const double samplesPerPeriod=sampleRate/freq;
    const double samplesPerUp=samplesPerPeriod*upPercent;
    const double samplesPerDown=samplesPerPeriod-samplesPerUp;
    const uint intSamplesPerUp=(uint)samplesPerUp;
    const uint intSamplesPerDown=(uint)samplesPerDown;
    const double fractSamplesPerUp=fract(samplesPerUp);
    const double fractSamplesPerDown=fract(samplesPerDown);

    double upFraction=0, downFraction=0;
    uint pos=0;
    double v=-volume, du=2*volume/samplesPerUp, dd=2*volume/samplesPerDown;
    for(uint k=0; k<fullPeriodsCount; k++)
        for(uint s=0; s<intSamplesPerUp; s++)
             outSamples[pos++]=(float)v, v+=du;
        if(upFraction>=1) {upFraction--; outSamples[pos++]=(float)v; v+=du;}

        for(uint s=0; s<intSamplesPerDown; s++)
            outSamples[pos++]=(float)v, v-=dd;
        if(downFraction>=1) {downFraction--; outSamples[pos++]=(float)v; v-=dd;}

Here upPercent is the rise time divided by the entire period of the wave. It is also important that this function is as fast as possible. I have a correct implementation, but it is 6 times slower than this but is wrong.


Through thought, trial and error, I came to this:

double fraction=fractSamplesPerDown/2;
//Цикл ниже начинается от минимального значения сигнала, а нам нужно начать с нулевого, чтобы не было скачка. Сделаем половину спада перед циклом
for(uint s=1; s<samplesPerDown/2; s++) inOutSamples[pos++]=(float)v, v-=dd;

for(int k=0; k<fullPeriodsCount; k++)
    for(uint s=0; s<intSamplesPerUp; s++)
        inOutSamples[pos++]=(float)v, v+=du;
    if(fraction>=1) fraction--, inOutSamples[pos++]=(float)v;

    for(uint s=0; s<intSamplesPerDown; s++)
        inOutSamples[pos++]=(float)v, v-=dd;
    if(fraction>=1) fraction--, inOutSamples[pos++]=(float)v;

At a sampling rate of 11 kHz, it sounds similar to my previous implementation, but the sound is still not entirely clear. But the 7-fold increase in performance more than paid off for the transition to 48 kHz, and at this frequency everything is already fine. So, in principle, this option suits me, but if suddenly someone has ideas on how to do it even better, then write. Naturally, this code can still be optimized later, I have not even started this yet.

I hope it will be useful to someone, otherwise I myself could not find this algorithm anywhere.

Scroll to Top