LancePutnam / Gamma

Generic (Sound) Synthesis Library
Other
458 stars 54 forks source link

gam::Saw output range: has DC offset and exceeds 1.0 #51

Open younkhg opened 5 years ago

younkhg commented 5 years ago

output of below program is:

min: -0.625428, max: 1.02889

is there a way to normalize this output to [-1.0 : 1.0] ?

#include "Gamma/Domain.h"
#include "Gamma/Oscillator.h"

#include <cfloat>
#include <iostream>

using namespace std;

int main()
{
    gam::Domain::master().spu(44100.0f);
    gam::Saw<> saw;
    saw.freq(440.0f);

    float max_s = -FLT_MAX;
    float min_s = FLT_MAX;

    for (int i = 0; i < 44100; i += 1)
    {
        float s = saw();
        if (s > max_s) max_s = s;
        if (s < min_s) min_s = s;
    }

    cout << "min: " << min_s << ", max: " << max_s << endl;
}
younkhg commented 5 years ago

also tagging @mantaraya36 and @kybr ...

kybr commented 5 years ago

I think that this might be expected behaviour. For a geometrically perfect sawtooth you would expect no DC and strict limits on (-1, 1). Of course, those sound awful.

However, gam::Saw is bandlimited. It is designed to sound like a sawtooth and not introduce aliasing. (I think. I could be wrong.) Consider Synthesis of Quasi-Bandlimited Analog Waveforms Using Frequency Modulation (Schoffhauzer 2005). That implementation of Saw and Square/Pulse does not stay in (-1, 1) and it always has some DC offset. Also, when you look at the waveforms it makes... they don't really look like Saw or Square/Pulse. They sound right though.

younkhg commented 5 years ago

gam::Saw is bandlimited, it seems like it uses BLIT integration (http://musicdsp.org/files/waveforms.txt) I've plotted it too and everything looks right.

I was asking if there's more proper way to map the result to [-1:1] rather than just subtracting some value like 0.5...

dc offset should not matter that much but value above 1.0 seemed weird for me

LancePutnam commented 5 years ago

Yep, it's using BLIT integration. It might be possible to fix it to give the expected range [-1,1], but I'm not exactly sure. I suspect the odd range is because it uses a leaky integrator to kill DC and that may be distorting the wave shape due to its non-linear phase response. You might want to check out DWO::up or DWO::down. They are not exactly band limited, but have far less aliasing than a naive saw and give the expected range.

younkhg commented 5 years ago

Thank you for suggesting DWO! Btw I just did another experiment and I think I can more clarify what was happening.

part of the result from program attached at the bottom is:

at freq: 400
    iteration 0 min: -0.225813, max: 1.02908
    iteration 1 min: -0.425058, max: 0.889006
    iteration 2 min: -0.489829, max: 0.798894
    iteration 3 min: -0.551924, max: 0.74092
    iteration 4 min: -0.575975, max: 0.684907
    iteration 5 min: -0.591454, max: 0.664167
    iteration 6 min: -0.601419, max: 0.65422
    iteration 7 min: -0.607839, max: 0.647812
    iteration 8 min: -0.611977, max: 0.643681
    iteration 9 min: -0.616374, max: 0.641016
    iteration 10    min: -0.617493, max: 0.638173
    iteration 11    min: -0.618222, max: 0.637444
    iteration 12    min: -0.618699, max: 0.636966
    iteration 13    min: -0.619015, max: 0.636649
    iteration 14    min: -0.619227, max: 0.636436
    iteration 15    min: -0.619473, max: 0.636289

It seems like first few thousand samples are off-range before stabilization of the integration. For practical use, it shouldn't matter for many case I guess? Since oscillators usually are not made new in the middle of audio app but predefined in the beginning.

#include "Gamma/Oscillator.h"

#include <cfloat>
#include <iostream>

using namespace std;

void test_saw (float freq)
{
    gam::Saw<> saw;
    saw.freq(freq);

    float max_s = -FLT_MAX;
    float min_s = FLT_MAX;

    cout << "at freq: " << freq << endl;

    for (int i = 0; i < 16; i += 1)
    {
        float max_s = -FLT_MAX;
        float min_s = FLT_MAX;

        for (int j = 0; j < 512; j += 1)
        {
            float s = saw();
            if (s > max_s) max_s = s;
            if (s < min_s) min_s = s;
        }

        cout << "\titeration " << i << "\tmin: " << min_s << ", max: " << max_s << endl;
    }

}

int main ()
{
    gam::Domain::master().spu(44100.0f);

    for (float t = 100.0f; t < 4000.0f; t += 100.0f)
    {
        test_saw(t);
    }

}