Page 1 of 1

[SOLVED] Changing frequency while synthesising sine wave

Posted: Mon Apr 19, 2021 8:20 am
by busboy
Hello,

I've been stuck for days on changing the frequency of a sine wave oscillator over a period of time, and I'm completely stumped.

Image

I've tried it many number of ways, but what always ends up happening is that the frequency over the lerp of its duration is never correct, it always either overshoots by the end, or goes too low and comes back up. You can see this very easily by entering hz values that are really high or really low in the frequency table.

I've tested every part of the program for the cause of the error, I've tried other sine oscillators (I'm currently using one that was shared by zorg in https://love2d.org/forums/viewtopic.php?t=82944), I've tried replacing variables with constants every step of the way, I've tried pretty much everything.

I've put together a some code in love.draw() to better illustrate what's going on in the wave form, and you can press space at any time to hear the sound.

As you can see in the frequency table, the sound should first descend from 130 to 65 hz over a second, then hold on 65hz for a second, then ascend again to 130 over the final second. But the middle second is the only one that works correctly, for some reason, everything else acts totally erratically and I can't understand why.

I've included my main.lua as code below, and my love game as an attachment. Thank you!

Code: Select all

sound = {}

sound.rate = 44100  --sample rate
sound.bits = 16     --bit rate
sound.channel = 1
sound.initialPhase = 0

function lerp(a, b, t) return a + (b - a) * t end -- linear interpolation

function love.load()

    love.window.setMode(1200, 200, { vsync = true, highdpi = true, resizable = true })

    --frequency table
    frequency = {130.81278265029931, 65.406391325149656, 65.406391325149656, 130.81278265029931}
    --time table
    seconds = {0, 1, 2, 3}

    --tone is the sound data, sound_table is the x points of the waveform
    tone, sound_table = sound.get()

    qs = love.audio.newQueueableSource(tone:getSampleRate(), tone:getBitDepth(), tone:getChannelCount())
end

function love.keypressed(key)--, scancode, isrepeat)

    if key == "space" then
        qs:queue(tone)
        qs:play()
    end
end


function love.draw()
    -- draw waveform
    if sound_table then -- draw a line dividing each second
        love.graphics.setColor(255, 255, 255, 0.2)
        for i=1, #seconds do
            love.graphics.line(300*seconds[i], 0, 300*seconds[i], love.graphics.getHeight())
        end

        love.graphics.setColor(255, 255, 255, 1) --draw the waveform as dots
        for i=1, #sound_table-1 do
            love.graphics.points(100*seconds[1] + (100*(i-1))/sound.rate*3, (100*sound_table[i])+(100))
        end
    end

end

-- Constructor for a sine wave generator.
sine = function(generator)
    local tau = math.pi*2
    local generator = generator
    local increment = 1.0 / generator.rate --/ generator.channels
    local phase = generator.initialPhase
    return function(freq)
        phase = phase + increment
        generator.phase = phase
        local x = phase * freq
        -- 2 ops: 1 mul, 1 trig
        return math.sin(tau * x)
    end
end

function sound.get()

    local sound_table = {}
    
    local length = (seconds[#seconds]-seconds[1]) * sound.rate

    -- initialising sample
    local sound_data = love.sound.newSoundData(length, sound.rate, sound.bits, sound.channel)

    local oscillator = sine(sound)

    -- writing to sample
    local amplitude = 0.5

    for i = 1, #seconds-1 do
        
        from = (seconds[i]-seconds[1]) * sound.rate
        till = (seconds[i+1]-seconds[1]) * sound.rate - 1
        from, till = math.floor(from), math.floor(till) --rounding down the samples, just in case

        for s = from, till do

            now = lerp(frequency[i], frequency[i+1], (s-from)/(till-from))

            sample = oscillator(now) * amplitude
            sound_data:setSample(s, sample)
            table.insert(sound_table, sample)
        end
    end

    return sound_data, sound_table

end

--manual, deconstructed loop below
--[[
function sound.get(args, ...)

    local sound_table = {}
    
    local length = 3 * sound.rate

    -- creating an empty sample
    local sound_data = love.sound.newSoundData(length, sound.rate, sound.bits, sound.channel)

    local oscillator = sine(sound)

    s = 0

    -- filling the sample with values
    local amplitude = 0.5

        for i = 0, 44100 do

            s = i
            now = lerp(frequency[1], frequency[2], i/44100)
            
            --if i == 2 and s == from and (s-from)/(till-from) == 0 then debug1 = now end
            --if i == 1 and s == till and (s-from)/(till-from) == 1 then debug2 = now end

            sample = oscillator(now) * amplitude
            sound_data:setSample(s, sample)
            table.insert(sound_table, sample)
        end

        for i = 44100, 88200 do

            s = i

            now = lerp(frequency[2], frequency[3], (i-44100)/(88200-44100))
            
            --if i == 2 and s == from and (s-from)/(till-from) == 0 then debug1 = now end
            --if i == 1 and s == till and (s-from)/(till-from) == 1 then debug2 = now end

            sample = oscillator(now) * amplitude
            sound_data:setSample(s, sample)
            table.insert(sound_table, sample)
        end

        for i = 88200, 132300-1 do

            s = i

            now = lerp(frequency[3], frequency[4], (i - 88200)/(132300-88200))
            
            --if i == 2 and s == from and (s-from)/(till-from) == 0 then debug1 = now end
            --if i == 1 and s == till and (s-from)/(till-from) == 1 then debug2 = now end

            sample = oscillator(now) * amplitude
            sound_data:setSample(s, sample)
            table.insert(sound_table, sample)
        end

    return sound_data, sound_table

end]]

Re: Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 3:47 am
by zorg
Haven't had time yet to look at it too much, but one thing you might try is to limit the phase in the returned generator function to the range/domain (i get these mixed up) of [0,1] with something like:

Code: Select all

phase = (phase + increment) % 1.0
...that said, i haven't noticed you dividing x by the sample rate either, that should happen if you want the correct frequencies to play.

Re: Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 4:38 am
by busboy
Thanks for your reply!
zorg wrote: Tue Apr 20, 2021 3:47 am Haven't had time yet to look at it too much, but one thing you might try is to limit the phase in the returned generator function to the range/domain (i get these mixed up) of [0,1] with something like:

Code: Select all

phase = (phase + increment) % 1.0
Unfortunately all this seemed to do is affect the continuity of the signal from one frequency point to another. Your sine oscillator not having one of these limits was also what kept it from clicking like all the others I tried.

without phase limit:
Image

with phase limit:
Image
zorg wrote: Tue Apr 20, 2021 3:47 am ...that said, i haven't noticed you dividing x by the sample rate either, that should happen if you want the correct frequencies to play.
Would that just be replacing the x declaration in the returned function with

Code: Select all

local x = (phase * freq)/generator.rate
Because that just flattens the line.
Image

I'm not sure I fully understand, I apologise.

Re: Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 5:34 am
by zorg
>Would that just be replacing the x declaration in the returned function with <code>
Yes, it would... i feel like something else is amiss, but again, i'd need to dwelve into it, and due to me working today, that's not gonna happen.
I did notice that you did multiply with sound.rate in your code, so that might be why it could have worked before... i just feel that that's in the wrong place.

Re: Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 10:56 am
by pgimeno
This seems to work:

Code: Select all

    return function(freq)
        phase = phase + tau * increment * freq
        generator.phase = phase
        return math.sin(phase)
    end
Why is it different, I haven't stopped to analyse yet. By the way, you can save a multiplication by pre-calculating tau * increment.

Edit: Intuitively, the phase shift should be affected by frequency, and in your original formulation you don't have that; the phase is always the same for a given instant in time, which isn't right. Your original formulation works for a fixed frequency, but not for an arbitrarily changing frequency.

Re: Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 2:49 pm
by busboy
pgimeno wrote: Tue Apr 20, 2021 10:56 am

Code: Select all

    return function(freq)
        phase = phase + tau * increment * freq
        generator.phase = phase
        return math.sin(phase)
    end
This is dead on, thank you so much.

Re: [SOLVED] Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 3:44 pm
by darkfrei
How it works?

Re: [SOLVED] Changing frequency while synthesising sine wave

Posted: Tue Apr 20, 2021 7:14 pm
by zorg
I checked the post of mine from the thread you linked, and i got it wrong there as well... guess i'll take the blame fair and square. :D