Cabbage Logo
Back to Cabbage Site

Changing audio buffer size in ftgen?

If you want to do cross fading you need to have two sources to cross fade against. Right now you are only writing the audio to a single table. I think you need to write the signal to two tables, or use a delay line, and then cross-fade between them. This code is untested, but should give you an idea. Of course you could also do this without a delay and second function table. I’m sure you’ll find the best approach through some experimentation.

opcode Reverse, a, aiik

    setksmps 1

    aIn, iFn, iEnvTable, kLength  xin

    kWritePointer init 0
    kReadPointer init 0

    gkTableLength = ftlen(iFn)*kLength
    tablew aIn, a(kReadPointer), iFn  
    
    aSignal1 table  a(gkTableLength)-a(kReadPointer), iFn
    
    kWritePointer = kWritePointer < gkTableLength ? kWritePointer + 1 : 0
    kReadPointer = kReadPointer < gkTableLength ? kReadPointer + 1 : 0
    
    aEnvPhs1 phasor sr/gkTableLength
    aEnvPhs2 phasor sr/gkTableLength, 0.5
    
    aEnv1 table aEnvPhs1, iEnvTable, 1
    
    aSignal2 vdelay aSignal1, a((gkTableLength/sr)/2), 1000
    aEnv2 table aEnvPhs2, iEnvTable, 1
    xout aSignal1*aEnv1+aSignal2*aEnv2

endop

Hmm, so no matter how I implement the windowing (singular or overlapping), it almost always clicks after adjusting the size (atleast in Reaper, testing with a oscillator).

The code creates no clicks when you load it as a effect in Reaper, but when you adjust the length, it starts clicking at times. I tried converting kTableLength to integer (incase that would be a issue), but that doesn’t help… Also tried doubling the speed of the window phasor incase it would help, but it doesn’t either…

Is there something obvious I am missing?

<Cabbage>
form caption("Reverse") size(400, 300), guiMode("queue"), colour(58, 110, 182), pluginId("def1")
rslider bounds(26, 28, 60, 60) channel("length") range(0, 1, 1, 1, 0.001), text("Length")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

giTableL ftgen  91, 0, 32768*2, 7, 0, 32768*2, 0
giEnvTable ftgen  92, 0, 32768*2, 9, 0.5, 1, 0

opcode Reverse, a, aiik

    setksmps 2

    aIn, iFn, iEnvTable, kLength  xin

    kWritePointer init 0
    kReadPointer init 0

    kTableLength = int(ftlen(iFn)*kLength)
    tablew aIn, a(kReadPointer), iFn  
    aSignal table  kTableLength-a(kReadPointer), iFn
    
    kWritePointer = kWritePointer < kTableLength ? kWritePointer + 1 : 0
    kReadPointer = kReadPointer < kTableLength ? kReadPointer + 1 : 0

    aEnvPhs phasor (sr/(kTableLength*0.5))
    printk 1, k(aEnvPhs)
    aEnv table aEnvPhs, iEnvTable, 1
    xout aSignal*aEnv
;    xout aSignal

endop

instr 1
kLength cabbageGetValue "length"

a1 inch 1
a2 inch 2

aRevL Reverse a1, giTableL, giEnvTable, kLength
;aRevR Reverse a2, giTableR, giEnvTable, kLength

outs aRevL, aRevL
endin

</CsInstruments>
<CsScore>
i1 0 z
</CsScore>
</CsoundSynthesizer>

I think you need to reset the phase of the envelope when you change the number of sample you wish to reverse :thinking:

opcode Reverse, a, aiik

    setksmps 2

    aIn, iFn, iEnvTable, kLength  xin
    kTableLength = int(ftlen(iFn)*kLength)
    
    if changed(kTableLength) == 1 then
       reinit resetPhase
    endif
    
    resetPhase:
    kWritePointer init 0
    kReadPointer init 0
    
    tablew aIn, a(kReadPointer), iFn  
    aSignal table  kTableLength-a(kReadPointer), iFn
    
    kWritePointer = kWritePointer < kTableLength ? kWritePointer + 1 : 0
    kReadPointer = kReadPointer < kTableLength ? kReadPointer + 1 : 0

    aEnvPhs phasor (sr/(kTableLength))
    aEnv table aEnvPhs, iEnvTable, 1
    xout aSignal*aEnv

endop

Nice! You’re totally right!

So here is one for two tables then, first writing to two tables, then crossfading with eachother. If you use the delay method you’re just postponing the same table content right? While if you do something like this, you should be able to write two independent buffers of audio at different times and overlap those two tables. Should be a better solution or?

There is still a slight clicking in this one though. One thing I noticed is that I can’t initialize with a k-rate like this: kWritePointer2 init kTableLength/2, so I put it in the changed if-statement, which I’m thinking could be the culprit.

Also, does setksmps need to be changed?

<Cabbage>
form caption("Reverse") size(400, 300), guiMode("queue"), colour(58, 110, 182), pluginId("def1")
rslider bounds(26, 28, 60, 60) channel("length") range(0, 1, 1, 1, 0.1), text("Length")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

giTableLen = 2^16

giTableL1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableL2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giEnvTable ftgen  0, 0, giTableLen, 9, 0.5, 1, 0

opcode Reverse, a, aiiik

    setksmps 2

    aIn, iFn1, iFn2, iEnvTable, kLength  xin

    kWritePointer1 init 0
    kReadPointer1 init 0
    kWritePointer2 init ftlen(iFn2)/2
    kReadPointer2 init ftlen(iFn2)/2
    
    kTableLength = int(ftlen(iFn1)*kLength)
    
    if changed(kTableLength) == 1 then
       reinit resetPhase
        kWritePointer2 = kTableLength/2
        kReadPointer2 = kTableLength/2
    endif
    
    resetPhase:
    kWritePointer1 init 0
    kReadPointer1 init 0
    
    tablew aIn, a(kReadPointer1), iFn1  
    aSignal1 table  kTableLength-a(kReadPointer1), iFn1
    
    tablew aIn, a(kReadPointer2), iFn2  
    aSignal2 table  kTableLength-a(kReadPointer2), iFn2
    
    kWritePointer1 = kWritePointer1 < kTableLength ? kWritePointer1 + 1 : 0
    kReadPointer1 = kReadPointer1 < kTableLength ? kReadPointer1 + 1 : 0
    
    kWritePointer2 = kWritePointer2 < kTableLength ? kWritePointer2 + 1 : 0
    kReadPointer2 = kReadPointer2 < kTableLength ? kReadPointer2 + 1 : 0
    
    aEnvPhs1 phasor (sr/(kTableLength))
    aEnvPhs2 phasor (sr/(kTableLength)), 0.5

    aEnv1 table aEnvPhs1, iEnvTable, 1
    aEnv2 table aEnvPhs2, iEnvTable, 1
    
    xout  (aSignal1*aEnv1) + (aSignal2*aEnv2)

endop

instr 1
kLength cabbageGetValue "length"

a1 inch 1
a2 inch 2

aRevL Reverse a1, giTableL1, giTableL2, giEnvTable, kLength
aRevR Reverse a2, giTableR1, giTableR2, giEnvTable, kLength

outs aRevL, aRevR
endin

</CsInstruments>
<CsScore>
i1 0 z
</CsScore>
</CsoundSynthesizer>

Or maybe a better solution than doing the crossfading inside the opcode, would be to add a normalized offset parameter? Something like this?

aRevL1 Reverse a1, giTableL1, giEnvTable, kLength, 0 ;Offset = 0
aRevL2 Reverse a1, giTableL2, giEnvTable, kLength, 0.5 ;Offset = 0.5

aRevL1 + aRevL2

Here’s the idea I had with adding the offset parameter. Still some small clicks but it definetly works somewhat okey. Anything that can be improved?

<Cabbage>
form caption("Reverse") size(400, 300), guiMode("queue"), colour(58, 110, 182), pluginId("def1")
rslider bounds(26, 28, 60, 60) channel("length") range(0, 1, 1, 1, 0.1), text("Length")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

giTableLen = 2^16

giTableL1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableL2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giEnvTable ftgen  0, 0, giTableLen, 9, 0.5, 1, 0

opcode Reverse, a, aiiki

    setksmps 2

    aIn, iFn, iEnvTable, kLength, iOffset  xin
    kTableLength = int(ftlen(iFn)*kLength)
    
    if changed(kTableLength) == 1 then
        kWritePointer = 0 + iOffset*kTableLength
        kReadPointer = 0 + iOffset*kTableLength
    endif
    
    tablew aIn, a(kReadPointer), iFn  
    aSignal table  kTableLength-a(kReadPointer), iFn
    
    kWritePointer = kWritePointer < kTableLength ? kWritePointer + 1 : 0
    kReadPointer = kReadPointer < kTableLength ? kReadPointer + 1 : 0

    aEnvPhs phasor (sr/(kTableLength)), iOffset
    aEnv table aEnvPhs, iEnvTable, 1
    xout aSignal*aEnv

endop

instr 1
kLength cabbageGetValue "length"

a1 inch 1
a2 inch 2

aRevL1 Reverse a1, giTableL1, giEnvTable, kLength, 0
aRevL2 Reverse a1, giTableL2, giEnvTable, kLength, 0.5

outs aRevL1 + aRevL2, aRevL1 + aRevL2
endin

</CsInstruments>
<CsScore>
i1 0 z
</CsScore>
</CsoundSynthesizer>

setksmps should be 1. Setting it to two means it will only write on every second sample.

I’ll take a look at the other efforts a little later. Got to roll my sleeves up this morning and blast through the ever increasing Cabbage snag list…

1 Like

[edit] Those clicks will always be present if you don’t reset the read pointer for both the sample table, and the envelope every time you change the table length. When you change the table length you break the synchronisation between the envelope and the sample table. They will no longer be reading from the same positions. This is why you will get clicks. It is also why I used a reinit in the code I posted.

Isn’t that what I do here? I couldn’t use the reinit with k-rate values…

Ah, so I need to reset aEnvPhs too then?

Yup

I didn’t get clicks when setting ksmps to 2, but I guess it was then because it skipped writing that particular sample?

That’s it. You read and write every second sample so you don’t even notice.

I believe I am very close now, but for some damn reason the reinitialization of kOffset to 0 does not seem to work properly. I’m trying to start the vdelayk opcode over again when changing kTableLength, but I can’t get it to work… Do you see anything that is stopping the reinitialization from working properly?

<Cabbage> bounds(0, 0, 0, 0)
form caption("Reverse") size(400, 300), guiMode("queue"), colour(58, 110, 182), pluginId("def1")
rslider bounds(26, 28, 60, 60) channel("length") range(0, 1, 1, 1, 0.1), text("Length")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d --midi-key=4 --midi-velocity-amp=5
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

giTableLen = 2^16

giTableL1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableL2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR1 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giTableR2 ftgen  0, 0, giTableLen, 7, 0, giTableLen, 0
giEnvTable ftgen  0, 0, giTableLen, 9, 0.5, 1, 0

opcode Reverse, a, aiiki

    setksmps 1
    
    kWritePointer init 0
    kReadPointer init 0
    kOffset init 0
    aEnvPhs init 0

    aIn, iFn, iEnvTable, kLength, iOffset  xin
    kTableLength = int(ftlen(iFn)*kLength)
    
    if kOffset != iOffset then
        kOffset vdelayk kOffset + iOffset, (iOffset*(kTableLength/sr)), 10
        
    endif

    if kOffset == iOffset then      
        tablew aIn, a(kReadPointer), iFn  
        aSignal table  kTableLength-a(kReadPointer), iFn
    
        kWritePointer = kWritePointer < kTableLength ? kWritePointer + 1 : 0
        kReadPointer = kReadPointer < kTableLength ? kReadPointer + 1 : 0
        
        aEnvPhs phasor (sr/(kTableLength));, iOffset

        aEnv table aEnvPhs*2, iEnvTable, 1

        xout aSignal*aEnv
        
        if changed(kTableLength) == 1 then
            reinit Reset
        endif
        
        Reset:
        kWritePointer init 0
        kReadPointer init 0
        kOffset init 0
        aEnvPhs init 0
        
    endif

printk2 kOffset
endop

instr 1
kLength cabbageGetValue "length"

a1 inch 1
a2 inch 2

aRevL1 Reverse a1, giTableL1, giEnvTable, kLength, 2
;aRevL2 Reverse a1, giTableL2, giEnvTable, kLength, 0.5

;aRevR1 Reverse a2, giTableR1, giEnvTable, kLength, 1.5
;aRevR2 Reverse a2, giTableR2, giEnvTable, kLength, 0.5

;outs aRevL1, aRevR1
endin

</CsInstruments>
<CsScore>
i1 0 z
</CsScore>
</CsoundSynthesizer>

Although you are resetting the read/write pointers to 0, your phasor’s phase is not being reset. You need to put the Reset label above your phasor if I’m not mistaken…

Thanks. Arrgh you are right of course… :rofl:

opcode Reverse, a, aiiki

    setksmps 1
    
    aIn, iFn, iEnvTable, kLength, iOffset  xin
    kTableLength = int(ftlen(iFn)*kLength)
    
    kWritePointer init 0
    kReadPointer init 0
    kOffset init 0
    aEnvPhs init 0
    
    if changed(kTableLength) == 1 then
            reinit Reset
    endif
    
    Reset:
    kWritePointer init 0
    kReadPointer init 0
    kOffset init 0
    aEnvPhs init 0

    if kOffset != iOffset then
        kOffset vdelayk kOffset + iOffset, (iOffset*(kTableLength/sr)), 10
    endif

    if kOffset == iOffset then      
        tablew aIn, a(kReadPointer), iFn  
        aSignal table  kTableLength-a(kReadPointer), iFn
    
        kWritePointer = kWritePointer < kTableLength ? kWritePointer + 1 : 0
        kReadPointer = kReadPointer < kTableLength ? kReadPointer + 1 : 0
        
        aEnvPhs phasor (sr/(kTableLength))
        aEnv table aEnvPhs*2, iEnvTable, 1
        xout aSignal*aEnv
    endif
endop

Now it works fine, but I have to multiply aEnvPhs by 2 for it to stop clicking, which I don’t fully understand? I thought it would work like this with aEnvPhs running at normal speed? The clicks being muted because the envelope table being at 0 when the clicking occurs?

ReverseImage

Also, anywhere I can place interpolation or upsampling to avoid singular clicks when changing “length”? (Doesn’t really matter that much, but the perfectionist inside me is screaming :wink: )

With this setup your table position will move from the start to the end, and then stay there for some time before it starts moving again. That’s why you get that slightly longer than expected silence after each buffer playback. Try this instead:

aEnv table aEnvPhs*2, iEnvTable, 1, 0, 1

It will give you the same playback speed, but with a wrap around. It should give you the same non-click sound, but without the additional silence.

Thanks, forgot about it, but there is a small issue with doubling aEnvPhs by 2. To do the overlapping tables I now need to set ioffset to 0.25 instead of 0.5, which makes the second table write earlier than what you really want :thinking:

Would you not get away with a simple delay?

You mean delaying the sound inside the opcode? That could work

Yeah, just add a simple delay line that will always be half a cycle? Simple, but maybe effective?