Fout opcode suppored for Cabbage

Hi!

I’m currently working with Dr. Richard Boulanger on a research project called CStore, which import AI into the Csound.

We’ve run into an issue when trying to export a short preview “one-shot” of a Cabbage-designed instrument (just a few seconds of audio) using the fout opcode.

The problem:

Many plugin-designed .csd files use performance loops or k-rate logic that essentially run indefinitely. To generate one-shot preview audio, I wrote a Python tool that:

  1. parses the .csd
  2. finds the instrument’s score events
  3. rewrites duration values
  4. inserts a fout opcode to export audio (AIFF format)

But in practice:

  • sometimes Csound creates a massively long file (for example, larger than 1 TB)
  • other times it produces an empty file
  • or the rendering fails completely

It seems that the issue might be related to how Cabbage handles performance loops and internal scheduling, but I’m not entirely sure.

My question:

Does Cabbage 2 officially support the fout opcode?
If not, will Cabbage 3 include support for reliable offline exporting or a safe way to render a bounded-duration audio file from a plugin-style .csd?

Our goal is simple:
Inside a “designed-for-plugin” Cabbage .csd, we want to render a few seconds of audio for machine-learning dataset generation, without depending on real-time output or infinite loops.

Any guidance on:

  • correct usage of fout inside Cabbage
  • whether offline export is expected to work
  • or future plans in Cabbage 3 related to this
  • or any other way I can do that function!
    would be incredibly helpful.

Thanks so much!

— Charlie

Hi @CharlieSL1, always nice to hear from Richard’s students. There are no limitations to using fout in Cabbage. It should work just like it doesn’t in plain old Csound. Do you have a simple example of one of these modified csd files? The key to the issue can probably be found there :thinking:

Hi Rory,

Thanks a lot for getting back to me — and for confirming that fout should work the same way in Cabbage as in plain Csound.

Today Dr. B and I spent about 30 minutes debugging, and we got our test file working as a .csd in Cabbage, so fout itself is definitely functional. However, we’re still seeing a few strange behaviours and I’d love your input.

Using the attached/embedded Trapped09 example (adapted from Dr. B’s Trapped collection), we noticed:

  1. If we place fout directly on the local amix signal inside instr Trapped09 , the rendered file can sound noisy / trashy compared to what we hear from the Monitor output.
  2. If instead we record from a Monitor instrument that uses monitorfout on allL, allR , the bounced allMix file sounds correct.
  3. The duration of the file rendered from Trapped09 (local amix ) does not always match the duration of the file rendered from the full mix, even though we’re using the same ip3 / dur values.
  4. For our use case, we’d ideally like a clean way to:
  • trigger the sound once,
  • write it to disk with fout , and then
  • have Cabbage/Csound stop automatically after the render (so we don’t end up with very long or looping renders).

Here is the test .csd we’ve been working with (the problematic fout line is currently commented in instr Trapped09 , and the working one is in instr Monitor ):

<Cabbage> bounds(0, 0, 0, 0) 
form caption("Trapped09") size(650, 250), guiMode("queue") pluginId("def1")

button  bounds(34, 34, 101, 44) channel("trigger") text("Trigger") textColour("white")

hslider bounds(322, 10, 150, 50) channel("dur") range(2, 9, 4, 1, 0.001) text("Dur") textColour("white")
hslider bounds(250, 68, 150, 50) channel("note") range(20, 80, 50, 1, 0.001) text("MIDI NN") textColour("white")
hslider bounds(408, 68, 150, 50) channel("rndNote") range(0, 8, 3, 1, 0.001) text("Rnd NN") textColour("white")

hslider bounds(484, 10, 150, 50) channel("amp") range(0, 1, .6, 1, 0.001) text("Synth Lvl") textColour("white")

hslider bounds(286, 122, 162, 50) channel("rndRate") range(100, 400, 185, 1, 0.001) text("RndRate") textColour("white")
hslider bounds(452, 122, 150, 50) channel("rndAmp") range(0, 10, 3.3, 1, 0.001) text("RndAmp") textColour("white")

hslider bounds(338, 182, 150, 50) channel("delaySend") range(0, 1, .76, 1, 0.001) text("Delay Send") textColour("white")
hslider bounds(490, 182, 150, 50) channel("delayTime") range(.001, 5, .08, 1, 0.001) text("Delay Time") textColour("white")

hslider bounds(22, 184, 150, 50) channel("rvbSend") range(0, 1, .45, 1, 0.001) text("Rvb Send") textColour("white")
hslider bounds(176, 184, 150, 50) channel("rvbPan") range(0, 6, 4, 1, 0.001) text("Rvb Pan") textColour("white")

hslider bounds(160, 10, 150, 50) channel("masterLvl") range(0, 1, 0.9, 1, 0.001) text("Master Lvl") textColour("white")

combobox bounds(120, 104, 100, 25), populate("*.snaps"), channelType("string") automatable(0) channel("combo31") text("Starting-4note", "More Notes Less Vcomb", "More noise & open filter -pan LFO faster", "faster, less notes, more delay", "Slower, More resonance, and RingMod", "Complex PolyRhythms", "Alll Notes, Spinning, No Delay, All FreeVerb") value("1")
filebutton bounds(58, 104, 60, 25), text("Save", "Save"), populate("*.snaps", "test"), mode("named preset") channel("filebutton32")
filebutton bounds(58, 134, 60, 25), text("Remove", "Remove"), populate("*.snaps", "test"), mode("remove preset") channel("filebutton33")
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-odac 
</CsOptions>
<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

garvb          init      0
gadel          init      0

giSine ftgen 1, 0, 8192, 10, 1
giWav2 ftgen 2, 0, 8192, 10, 10, 8, 0, 6, 0, 4, 0, 1
giWav3 ftgen 3, 0, 8192, 10, 10, 0, 5, 5, 0, 4, 3, 0, 1
giWav4 ftgen 4, 0, 8192, 10, 10, 0, 9, 0, 0, 8, 0, 7, 0, 4, 0, 2, 0, 1

gkRevPan init 4

instr 1
    iDur = chnget:i("dur")
    iNote = chnget:i("note") + rnd(chnget:i("rndNote"))
    iFrq = cpsmidinn(iNote)
    iAmp = chnget:i("amp")
    kTrig chnget "trigger"
    if changed(kTrig) == 1 then
        event "i", "Trapped09", 0, iDur, iFrq, iAmp
    endif
endin

instr Trapped09
ip3    = chnget:i("dur")
iamp   = chnget:i("amp")*.2
kNote  = chnget:k("note")+rnd(chnget:i("rndNote"))
kFrq   = cpsmidinn(kNote)*.25

kRndAmp  = chnget:k("rndAmp")
kRndFrq  = chnget:k("rndRate")

k2             randh     kRndAmp, kRndFrq, .1                     
k3             randh     kRndAmp * .98, kRndFrq * .91, .2             
k4             randh     kRndAmp * 1.2, kRndFrq * .96, .3            
k5             randh     kRndAmp * .9, kRndFrq * 1.3     

kenv           linen    iamp, ip3 *.2, ip3, ip3 * .8  

a1             oscil     kenv, kFrq + k2, giSine, .2             
a2             oscil     kenv * .91, (kFrq + .004) + k3, giWav2, .3
a3             oscil     kenv * .85, (kFrq + .006) + k4, giWav3, .5
a4             oscil     kenv * .95, (kFrq + .009) + k5, giWav4, .8

kgate         transeg   1, ip3, 0, 0 
amix           =        (a1 + a2 + a3 + a4) * kgate
aL             =        a1 + a3
aR             =        a2 + a4

              outs      aL * chnget:k("masterLvl"), aR * chnget:k("masterLvl")
; Problem area when enabled:
;              fout "/Applications/CsoundTest/fout_aif.aiff", 14, amix

garvb          =         garvb + (amix * chnget:k("rvbSend")) 
gadel          =         gadel + (amix * chnget:k("delaySend")) 
endin

instr Delay
denorm    gadel
ip3    = chnget:i("dur")
kgate          expseg    1, ip3*.7, 1, ip3*.3, .0001
asig           delay     gadel, chnget:i("delayTime")
asig = asig*kgate
               outs      asig*chnget:k("masterLvl"), asig*chnget:k("masterLvl")
gadel          =         0
endin

instr Reverb 
denorm    garvb                   
k1             oscil     .5, chnget:k("rvbPan"), 1
k2             =         .5 + k1
k3             =         1 - k2
asig           reverb    garvb, 3.1
               outs      (asig * k2) * chnget:k("masterLvl"), ((asig * k3) * (-1)) * chnget:k("masterLvl")
garvb          =         0
endin
             
instr Monitor
allL, allR monitor
fout "/Applications/CsoundTest/fout_allMix.wav", 14, allL, allR
endin

</CsInstruments>
<CsScore>
i 1 0 15
i "Delay" 0 15
i "Reverb" 0 15
i "Monitor" 0 15
i "Trapped09" 0 3
i "Trapped09" 4 3
i "Trapped09" 8 3
</CsScore>
</CsoundSynthesizer>

If you have any suggestions about:

  • best practice for using fout from within a Cabbage plugin (especially inside a single instrument vs from monitor ), and
  • a clean pattern for “render once then stop” in a plugin context,

that would be incredibly helpful for us and for the larger project we’re working on.

Thanks again for your time and for all your work on Cabbage — it’s been central to this project with Dr. B.

Using fout within an instrument is not a problem, so long as theinstrument isn’t triggered multiple times. This is usually why problems arise with this approach. Also, I see that you have 3 instances of Trapped09. This might also cause some issues as the sound file might not be closed. You should probably call ficlose during the release phase of the instrument to ensure it’s properly closed before trying to write again. Closing the sound file after writing should help in the context of rendering to disk too. You can also render faster than realtime in a k-rate loop if I’m not mistaken. During each k-cycle will process ksmps samples, so if you run things in a k-rate loop you should be able to do a kind of offline bounce, but I’ve not tried it.

Hi Rory,

First, Happy New Year!!!

Thanks a lot for the detailed explanation, it helped me narrow the issue down.

I tried adding ficlose to force the file to close during release, but I could not get it to behave reliably. After your note about multiple triggers and multiple instances, I changed the approach and now I only write a single stereo mix bus using a dedicated Monitor instrument:

  • Monitor reads the output via monitor

  • fout "…/Re-CAB-Trapped09_fout_allMix.wav", 14, allL, allR

With this setup, the export does succeed, so the “multiple simultaneous Trapped09 writing to the same file” problem seems gone.

My current biggest blocker is automation: I cannot figure out a way to run this render from Python without manually clicking Play and Stop in the Cabbage GUI. My goal is a command driven workflow where a script triggers the render and exits cleanly, producing the wav, with no GUI interaction.

Below is the exact .csd I’m testing right now (full file):

<Cabbage> bounds(0, 0, 0, 0)
form caption("Trapped09") size(650, 250), guiMode("queue") pluginId("def1")

button  bounds(34, 34, 101, 44) channel("trigger") text("Trigger") textColour("white")

hslider bounds(322, 10, 150, 50) channel("dur") range(2, 9, 4, 1, 0.001) text("Dur") textColour("white")
hslider bounds(250, 68, 150, 50) channel("note") range(20, 80, 50, 1, 0.001) text("MIDI NN") textColour("white")
hslider bounds(408, 68, 150, 50) channel("rndNote") range(0, 8, 3, 1, 0.001) text("Rnd NN") textColour("white")

hslider bounds(484, 10, 150, 50) channel("amp") range(0, 1, .6, 1, 0.001) text("Synth Lvl") textColour("white")

hslider bounds(286, 122, 162, 50) channel("rndRate") range(100, 400, 185, 1, 0.001) text("RndRate") textColour("white")
hslider bounds(452, 122, 150, 50) channel("rndAmp") range(0, 10, 3.3, 1, 0.001) text("RndAmp") textColour("white")

hslider bounds(338, 182, 150, 50) channel("delaySend") range(0, 1, .76, 1, 0.001) text("Delay Send") textColour("white")
hslider bounds(490, 182, 150, 50) channel("delayTime") range(.001, 5, .08, 1, 0.001) text("Delay Time") textColour("white")

hslider bounds(22, 184, 150, 50) channel("rvbSend") range(0, 1, .45, 1, 0.001) text("Rvb Send") textColour("white")
hslider bounds(176, 184, 150, 50) channel("rvbPan") range(0, 6, 4, 1, 0.001) text("Rvb Pan") textColour("white")

hslider bounds(160, 10, 150, 50) channel("masterLvl") range(0, 1, 0.9, 1, 0.001) text("Master Lvl") textColour("white")

combobox bounds(120, 104, 100, 25), populate("*.snaps"), channelType("string") automatable(0) channel("combo31") text("Starting-4note", "More Notes Less Vcomb", "More noise & open filter -pan LFO faster", "faster, less notes, more delay", "Slower, More resonance, and RingMod", "Complex PolyRhythms", "Alll Notes, Spinning, No Delay, All FreeVerb") value("1")
filebutton bounds(58, 104, 60, 25), text("Save", "Save"), populate("*.snaps", "test"), mode("named preset") channel("filebutton32")
filebutton bounds(58, 134, 60, 25), text("Remove", "Remove"), populate("*.snaps", "test"), mode("remove preset") channel("filebutton33")
</Cabbage>

<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>

<CsInstruments>
ksmps = 32
nchnls = 2
0dbfs = 1

garvb init 0
gadel init 0

giSine ftgen 1, 0, 8192, 10, 1
giWav2 ftgen 2, 0, 8192, 10, 10, 8, 0, 6, 0, 4, 0, 1
giWav3 ftgen 3, 0, 8192, 10, 10, 0, 5, 5, 0, 4, 3, 0, 1
giWav4 ftgen 4, 0, 8192, 10, 10, 0, 9, 0, 0, 8, 0, 7, 0, 4, 0, 2, 0, 1

gkRevPan init 4

instr 1
    iDur = chnget:i("dur")
    iNote = chnget:i("frq")+rnd(chnget:i("rndNote"))
    iFrq = cpsmidinn(iNote)
    iAmp = chnget:i("amp")

    kTrig chnget "trigger"
    if changed(kTrig) == 1 then
        event "i", "Trapped09", 0, iDur, iFrq, iAmp
    endif
endin

instr Trapped09
    ip3 = chnget:i("dur")
    iamp = chnget:i("amp")*.2

    kNote = chnget:k("note")+rnd(chnget:i("rndNote"))
    kFrq  = cpsmidinn(kNote)*.25

    kRndAmp = chnget:k("rndAmp")
    kRndFrq = chnget:k("rndRate")

    k2 randh kRndAmp, kRndFrq, .1
    k3 randh kRndAmp * .98, kRndFrq * .91, .2
    k4 randh kRndAmp * 1.2, kRndFrq * .96, .3
    k5 randh kRndAmp * .9, kRndFrq * 1.3

    kenv linen iamp, ip3 *.2, ip3, ip3 * .8

    a1 oscil kenv, kFrq + k2, giSine, .2
    a2 oscil kenv * .91, (kFrq + .004) + k3, giWav2, .3
    a3 oscil kenv * .85, (kFrq + .006) + k4, giWav3, .5
    a4 oscil kenv * .95, (kFrq + .009) + k5, giWav4, .8

    kgate transeg 1, ip3, 0, 0
    amix = (a1 + a2 + a3 + a4) * kgate
    aL = a1 + a3
    aR = a2 + a4

    outs aL * chnget:k("masterLvl"), aR * chnget:k("masterLvl")

    garvb = garvb + (amix * chnget:k("rvbSend"))
    gadel = gadel + (amix * chnget:k("delaySend"))
endin

instr Delay
    denorm gadel
    ip3 = chnget:i("dur")
    kgate expseg 1, ip3*.7, 1, ip3*.3, .0001
    asig delay gadel, chnget:i("delayTime")
    asig = asig * kgate
    outs asig*chnget:k("masterLvl"), asig*chnget:k("masterLvl")
    gadel = 0
endin

instr Reverb
    denorm garvb
    k1 oscil .5, chnget:k("rvbPan"), 1
    k2 = .5 + k1
    k3 = 1 - k2
    asig reverb garvb, 3.1
    outs (asig * k2) * chnget:k("masterLvl"), ((asig * k3) * (-1)) * chnget:k("masterLvl")
    garvb = 0
endin

instr Monitor
    allL, allR monitor
    fout "/Users/lishi/Desktop/Research/CStore/out/Re-CAB-Trapped09_fout_allMix.wav", 14, allL, allR
endin
</CsInstruments>

<CsScore>
i 1 0 15
i "Trapped09" 0 15
i "Delay" 0 15
i "Reverb" 0 15
i "Monitor" 0 15
i "Trapped09" 0 15
i "Trapped09" 16 15
i "Trapped09" 31 15
</CsScore>
</CsoundSynthesizer>

A few things I’m unsure about, and I would really appreciate your guidance on the recommended way:

  1. Is there a supported way to launch a Cabbage .csd render from command line (or via Python subprocess) that behaves like pressing Play, then automatically stops when the score ends?

  2. If the best practice is to bypass Cabbage for offline rendering, would you recommend keeping a “Csound only” version of the file (no section) and running Csound directly to render to disk?

  3. Regarding faster than realtime: do you have a minimal example of the k rate loop approach you mentioned for an offline bounce, or any flags/workflow you recommend in practice?

If it helps, I can also send a minimal repro .csd that shows the current Monitor based render working inside Cabbage, but not yet scriptable from Python.

Thanks again for your time.

Best,
Charlie Shi

Hi @CharlieSL1 glad to hear you made some progress.

No, the only way to do this would be to trigger a new instrument to do the work. Well, you could trigger Csound to run in the background using a Python process, but I would try to stay clear of the Python opcodes in Csound. Some hosts will not allow plugins to run subprocesses, and I have seen problems with this in Live in the past. Allowing plugins to launch arbitrary processes during playback is a bit of a security risk, so it is not surprising that DAWs are starting to guard against it. On the other hand, if this is just for prototyping a proof of concept, you might as well use whatever tools you can to help realize your ideas.

Personally, if I could, I would keep everything in a single .csd file. Your idea of batch processing a collection of instruments to add extra features seems like a neat and well-structured way to do things.

Perhaps this is the missing piece of the puzzle for you. Check out this instrument. All it does is copy a sound file to disk, but it does so in a single k-rate loop. I am using Csound 7 syntax, but it is easy to change to Csound 6. There are no special tricks involved here.


instr 1
    prints("Writing copy of file...\n")
    lenght:i = filelen("C:\\Users\\roryw\\sourcecode\\cabbage3-recipes\\samples/cleanGuitar.wav")*sr
    print(lenght)
    index:k = 0

    while index < lenght do
        sig:a = diskin2("C:\\Users\\roryw\\sourcecode\\cabbage3-recipes\\samples/cleanGuitar.wav", 1, 0, 1) 
        fout("C:\\Users\\roryw\\sourcecode\\cabbage3-recipes\\copy1.wav", 4, sig:a)
        index += 1
    od  

    if(release() ==1) then
        prints("Finished writing file...\n")
        ficlose("C:\\Users\\roryw\\sourcecode\\cabbage3-recipes\\copy2.wav")
    endif
endin

I hope this helps :slight_smile:

Hi Rory,

Thank you again for your advice on using fout and closing the soundfile properly. It was really helpful.

I followed your guidance and was able to export a WAV successfully from my Cabbage/Csound .csd workflow using Python. Your notes about avoiding multiple triggers and calling ficlose in the release phase made the difference.

Really appreciate your time and support.

Best regards,
Li(Charlie) Shi

1 Like