Cabbage Logo
Back to Cabbage Site

New to cabbage, used csound ages ago

I just learned about Cabbage today and am considering diving in for developing a looping audio plugin. I used CSound decades ago for synthesis, but audio processing will be new to me. Wish me luck!

I was trying to get this working, but using live audio: 05D08_custom_delay_line.csd from https://flossmanual.csound.com/sound-modification/delay-and-feedback

My version using live audio sounds like the sample rate is reduced on the delayed signal. Is that because the array isn’t processing at audio rates?

I can swap it out for one of the canned delay functions, but I’d like to understand what’s going wrong. Thanks!

Hi Bryan. Without seeing your code there’s no way to tell what’s going on but the delay should sound just like the live input. There is no audio sample rate reduction occurring.

In the version that sounds like the sample rate is reduced “ksmps” is set to 32. When I reduce it to 1, then it sounds normal.

Bryan

<Cabbage>
form caption("Untitled") size(400, 300), guiMode("queue") pluginId("def1")
rslider bounds(296, 162, 100, 100), channel("gain"), range(0, 1, 0, 1, .01), text("Gain"), trackerColour("lime"), outlineColour(0, 0, 0, 50), textColour("black")
rslider bounds(80, 80, 100, 100), channel("input"), range(0, 1, 0, 1, .01), text("Input"), trackerColour("lime"), outlineColour(0, 0, 0, 50), textColour("black")
rslider bounds(162, 80, 100, 100), channel("feedback"), range(0, 1, 0, 1, .01), text("Feedback"), trackerColour("lime"), outlineColour(0, 0, 0, 50), textColour("black")

</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d
</CsOptions>
<CsInstruments>
; Initialize the global variables. 
ksmps = 1
nchnls = 2
0dbfs = 1


instr 1
kInput cabbageGetValue "input"
kGain cabbageGetValue "gain"
kFeedback cabbageGetValue "feedback"

a1 inch 1
a2 inch 2


;; 4 second delay
  idel_size = (4) * sr
  kdelay_line[] init idel_size
  kread_ptr init 1
  kwrite_ptr init 0

  kindx = 0
  while (kindx < ksmps) do
    kdelay_line[kwrite_ptr] = kInput*a1 + kFeedback*kdelay_line[kread_ptr]
    adel[kindx] = kdelay_line[kread_ptr]

    kwrite_ptr = (kwrite_ptr + 1) % idel_size
    kread_ptr = (kread_ptr + 1) % idel_size

    kindx += 1
  od


outs a1*kGain + adel*kGain, a1*kGain + adel*kGain
endin

</CsInstruments>
<CsScore>
;causes Csound to run for about 7000 years...
f0 z
;starts instrument 1 and runs it for a week
i1 0 [60*60*24*7] 
</CsScore>
</CsoundSynthesizer>

You’re using k-rate arrays to hold audio samples. You’ve already worked this out but setting ksmps to 1 will work as each k sample will represent one audio sample. But if ksmps is anything but 1, you will only be writing every ksmps sample to your array. The typical approach in this context is to use a UDO to handle the processing as, you can set a local ksmps there using setksmps. This way your UDO will run at a lower ksmps than your orchestra.

FWIW, you can leave ksmps set to 1 in your orchestra too, but you might find it eats up a little too much CPU.

1 Like

I was also able to get it to sound “normal” with this approach in the while loop (ksmps set back to 32):

kdelay_line[kwrite_ptr] = kInput*a1[kindx] + kFeedback*kdelay_line[kread_ptr]

I’m having an issue now where Logic Pro doesn’t load my AU effect. No idea if that is related or a different user error.

I can’t use inch so no way for me to directly compare. However, take this code for example:

<CsoundSynthesizer>
<CsOptions>
-odac
; -o 0_array_delay.wav
</CsOptions>
<CsInstruments>
sr = 44100
ksmps = 32
nchnls = 2
0dbfs  = 1

instr CustomDelayLine

  ;; 0.5 second delay
  idel_size = 0.5 * sr
  kdelay_line[] init idel_size
  kread_ptr init 1
  kwrite_ptr init 1 ; 0

  aenv = expseg:a(.001, .004, 1, .2, .001)
  asig = noise(.8, 0) * aenv

  kindx = 0
  while (kindx < ksmps) do
    kdelay_line[kwrite_ptr] = asig[kindx]
    adel[kindx] = kdelay_line[kread_ptr]

    kwrite_ptr = (kwrite_ptr + 1) % idel_size
    kread_ptr = (kread_ptr + 1) % idel_size

    kindx += 1
  od
printk2 kindx
  out(asig, adel)
endin

</CsInstruments>
<CsScore>
i1 0 1
</CsScore>
</CsoundSynthesizer>
;example by Steven Yi

In this case, and perhaps Rory you can correct me if I’m mistaken, the k-rate arrays are still still sample accurate as stated in the FLOSS manual (“an advanced insight into sample-by-sample processing in Csound”). I believe it is incorrect to suggest only every ksmps one audio sample is written to the array. It appears that every 1 ksmps cycle 32 audio samples are in fact written to the array.

You’ll see in the code that in one channel (L) is the direct audio. In the other channel is the array output. I used white noise to demonstrate. Viewed in a spectrum analyzer, there is no frequency loss. And when zoomed in the array output is literally sample accurate to the original white noise (see pic). So I can’t understand why you might experience an issue. Not sure what I might be missing here as in the case that if at kspms = 32 only one audio sample was written then that would be visible - there would be visible stepping and it would no longer be sample accurate as it appears to be.

1 Like

you’re right @ST_Music, my initial reaction is always don’t mix k-rate and a-rate, but in this case, considering the loop is happening inline it should work fine.

@Bryan_T you need to codesign your plugin to get it to work in Logic. There is a lot of info about this on the forum.

1 Like

I think the fix @Bryan_T posted now does the same thing as yours @ST_Music. This was the problem line:

kdelay_line[kwrite_ptr] = kInput*a1 + kFeedback*kdelay_line[kread_ptr]

As I understand it, a1 will also return the first sample in the vector. As you’ve both shown, if you want to access samples within the vector in a k-rate loop, you have to use the array syntax on the a-rate variable. Or? I’m just trying to get this clear in my own head :rofl:

1 Like

@rorywalsh To be honest, I’m not sure this offers any benefit to simply using a delay opcode and, if one wants, feeding the signal back on itself (see FLOSS ex. 05D03_delay_feedback_2.csd)

I’d be curious to know.

1 Like

I agree. Using a delay opcode would be simpler, and offer better performance. I guess building delays like this from first principles is a good way to understand how they work. That’s probably why it’s included in the FLOSS manual.

2 Likes

Thanks for all of the discussion! I’m glad the error was simple and my fault.

I’m using this approach for a few reasons. It is closest to the methods in another book I have about plugin design in C++. And this is just the first step in what will be a complex looping plugin that is built on multiple delay lines. I suspect I’ll want access to the inner workings for filtering, changing direction/speed, and pulling out grains. But I’ll definitely keep the built-in delay opcodes in mind.

I’m not sure why my plug-in isn’t showing up in logic. I had no issues with an earlier version working. Probably user error as I figure out the workflow of exporting and adding to Logic.

Thanks!

1 Like

I figured out the issue in Logic. I hadn’t given the plugin a unique ID. Lesson learned.

Here is a quick improv I did in Logic with solo guitar and the simplest version of my looper: https://on.soundcloud.com/tyNQs6QrnCZbmDZ28

I’m beyond excited about the possibilities here. I learned about Cabbage yesterday and I’ve already created a working, musical tool that can be the basis for a much bigger project. So cool!

1 Like

Sounds very nice. The options are endless once you start creating your own plugins :slight_smile:

One version of what I’m hoping to do will have four (for example) delay lines of (potentially) different lengths. Should I plan to allocate the memory for these when the plug-in is launched? Basically, specify a maximum length and then truncate it based on user input? Or is there a more efficient way to think about it?

Related, I might want to “hide” the delay lines that aren’t used until they are needed. I could see doing that via the GUI (greying out controls that aren’t used, for example). But I could also see dynamically creating the delay lines and controls. The latter might not be feasible.

Thanks!

1 Like

The suggestion I got from chatgpt was to use two instruments. The first defines the effect a bit generically. The second creates/destroys instances of the effect. The cabbage stuff is pre-populated for the four instances, with some control over the appearance of the controls if the effect doesn’t exist.

Does that sound like it might work?

Sounds like a reasonable plan. You just have to make sure each instance has its own unique memory resources. For example, if you are writing to function tables, you need to make sure each instance isn’t trying to read/write from the same one :+1:

The million-dollar question: are there any examples out there of creating/destroying instances of an instrument/effect dynamically? ChatGPT tried its best, but it was a mess.

Here’s the sort of thing ChatGPT ultimately produced:

<Cabbage>
form caption("Dynamic Delay Effect") size(600, 400), colour(200, 200, 200), pluginId("del1")

button bounds(10, 10, 150, 40) channel("createInstance") text("Create Instance", "Create Instance")
button bounds(170, 10, 150, 40) channel("destroyInstance") text("Destroy Instance", "Destroy Instance")

; Predefined controls for up to 4 instances
groupbox bounds(10, 60, 580, 100) text("Instance 1") identChannel("group1") visible(0)
hslider bounds(20, 80, 260, 40) channel("delayTime1") range(0.01, 1, 0.5, 0.01) text("Delay Time")
hslider bounds(300, 80, 260, 40) channel("feedback1") range(0, 1, 0.5, 0.01) text("Feedback")

groupbox bounds(10, 170, 580, 100) text("Instance 2") identChannel("group2") visible(0)
hslider bounds(20, 190, 260, 40) channel("delayTime2") range(0.01, 1, 0.5, 0.01) text("Delay Time")
hslider bounds(300, 190, 260, 40) channel("feedback2") range(0, 1, 0.5, 0.01) text("Feedback")

groupbox bounds(10, 280, 580, 100) text("Instance 3") identChannel("group3") visible(0)
hslider bounds(20, 300, 260, 40) channel("delayTime3") range(0.01, 1, 0.5, 0.01) text("Delay Time")
hslider bounds(300, 300, 260, 40) channel("feedback3") range(0, 1, 0.5, 0.01) text("Feedback")

groupbox bounds(10, 390, 580, 100) text("Instance 4") identChannel("group4") visible(0)
hslider bounds(20, 410, 260, 40) channel("delayTime4") range(0.01, 1, 0.5, 0.01) text("Delay Time")
hslider bounds(300, 410, 260, 40) channel("feedback4") range(0, 1, 0.5, 0.01) text("Feedback")

</Cabbage>

<CsoundSynthesizer>
<CsOptions>
-odac     ; For Real-time audio out
</CsOptions>

<CsInstruments>

; Initialize the global variables
sr = 44100
ksmps = 32
nchnls = 2
0dbfs = 1

; Array to keep track of active instances
gkActiveInstances[] init 4
gkNumActiveInstances init 0

instr 1
    iInstance = p4
    SDelayTime sprintfk "delayTime%d", iInstance
    SFeedback sprintfk "feedback%d", iInstance

    kDelayTime chnget SDelayTime
    kFeedback chnget SFeedback

    aInL, aInR ins

    aDelL vdelay aInL, kDelayTime, 44100
    aDelR vdelay aInR, kDelayTime, 44100

    aOutL = aInL + (aDelL * kFeedback)
    aOutR = aInR + (aDelR * kFeedback)

    outs aOutL, aOutR
endin

instr 2
    iNumInstances init 4
    kTrigger chnget "createInstance"
    kDestroy chnget "destroyInstance"

    if (kTrigger == 1 && gkNumActiveInstances < iNumInstances) then
        gkActiveInstances[gkNumActiveInstances] = gkNumActiveInstances + 1
        event "i", 1, 0, -1, gkActiveInstances[gkNumActiveInstances]
        SGroup sprintfk "group%d", gkNumActiveInstances + 1
        chnset "visible(1)", SGroup
        gkNumActiveInstances += 1
    endif

    if (kDestroy == 1 && gkNumActiveInstances > 0) then
        gkNumActiveInstances -= 1
        SGroup sprintfk "group%d", gkNumActiveInstances + 1
        chnset "visible(0)", SGroup
        turnoff2 1, 1, gkActiveInstances[gkNumActiveInstances]
        gkActiveInstances[gkNumActiveInstances] = 0
    endif
endin

</CsInstruments>

<CsScore>
f 0 3600    ; Run for an hour
i 2 0 -1    ; Start the master instrument
</CsScore>

</CsoundSynthesizer>

There’s probably a few ways to approach this. Here’s a short example. The buttons here are not latched.

<Cabbage>
form caption("Dynamic Delay Effect") size(600, 400), colour(200, 200, 200), pluginId("del1")

button bounds(10, 10, 150, 40) channel("add") latched(0) text("Create Instance", "Create Instance")
button bounds(170, 10, 150, 40) channel("sub") latched(0) text("Destroy Instance", "Destroy Instance")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>
<CsInstruments>

sr = 48000
ksmps = 32
nchnls = 1
0dbfs  = 1

instr trig
; add instance
  kAdd chnget "add"
  kTrig trigger kAdd, .5, 0
; remove instance
  kSub chnget "sub"
  kStop trigger kSub, .5, 0

  kInstances init 0

  if  kTrig == 1 && kInstances != 4 then
      event("i", "play", 0, 10^6)
      kInstances += 1
  endif
;  printk2 kInstances
  if  kStop == 1 then
; add release time to turnoff2
; to avoid clicks
      turnoff2("play", 2, 1)
      kInstances -= 1
  endif

endin

instr play
  iFreq = random:i(300, 500)
; add release time to envelope
; to avoid clicks
  aEnv  = linsegr:a(1, p3, 1, 1, 0)
  aOut  = poscil(.2, iFreq) * aEnv
  out(aOut)
endin 

</CsInstruments>
<CsScore>
i"trig"  0 z ; run indefinitely
</CsScore>
</CsoundSynthesizer>

Note turnoff2 has a release & there is a release envelope on the play instr set for the same length, to avoid clicks (fade out). Also turnoff2 will stop the most recently triggered instance, there are other options.

The != 4 may seem counterintuitive but this limits to 4 instances as the += is inside the if statement (see printk2 kInstances).

I noticed you only had one create & one destroy button so this concept should work fine(? works ok for me). If you were to get ambitious and have 4 seperate buttons (one to control each instance seperately) you’d probably want to consider a different approach, like using latched buttons and fractional instrument numbers or seperate instr for each delay.

1 Like