Cabbage Logo
Back to Cabbage Site

Subharmonics - brute force or loop?

I tried to progam subharmonic sounds. My first attempt was brute force - simply add all fundamentals:

aSine1 = poscil:a(kAmp/(1^iDamp), kFreq/1 ) // fundamental
aSine2 = poscil:a(kAmp/(2^iDamp), kFreq/2 ) // harmonics
aSine3 = poscil:a(kAmp/(3^iDamp), kFreq/3 )
aSine4 = poscil:a(kAmp/(4^iDamp), kFreq/4 )
aSine5 = poscil:a(kAmp/(5^iDamp), kFreq/5 )
aSine6 = poscil:a(kAmp/(6^iDamp), kFreq/6 )
aSine7 = poscil:a(kAmp/(7^iDamp), kFreq/7 )
aSine8 = poscil:a(kAmp/(8^iDamp), kFreq/8 )
aSine9 = poscil:a(kAmp/(9^iDamp), kFreq/9 )
aSine10 = poscil:a(kAmp/(10^iDamp), kFreq/10)

aSine = aSine1 + aSine2 + aSine3 + aSine4 + aSine5 + etc…

outall (aSine)

iDamp is a damping factor, it might be 1 or 2, to make the subharmonics more or less quieter. So partial 5 gets a volume of only 1/5 (or 1/25) of the fundamental and a frequency of 1/5 of the fundamental. This is, how I understood subharmonics.

This works fine. In a spectrum analyzer, I see a large peak, and more peaks to the left, getting gradually quieter.

But this is brute force, so I tried a more elegant loop, following the example, that I found here. It looks like this:

kIndex = 1
while kIndex < 32 do
aOuts[kIndex-1] = poscil:a(kAmp/(kIndex^iDamp), kFreq/kIndex)
kIndex += 1
od

aRes = sumarray(aOuts)
out aRes

But - big surprise! - it sounds completely different and looks also complete different in an analyzer.

What am I doing wrong?

In Csound, some opcodes, poscil included, maintain an internal state to keep track of their operations. This internal state for oscillators is usually related to their phase. When you run it in a loop Csound will incrementing the phase each time it’s called (this is how I understand it at least!). So in order to do something like this, you need a recursive UDO that will actually create 10 poscils in a loop. Here is a SuperSaw UDO I posted to the forum some time back. It should be enough to get you going:

giSine ftgen 1, 0, 4096, 10, 1

opcode SuperSaw,a,kkiO 
   iDetune random 0, 1
   aVcoBank init 0
   kAmp, kFreq, iNum, iCnt xin 
   aSig vco2 kAmp/iNum,kFreq+iDetune
   iCount = iCnt+1 
   if iCount < iNum then 
      aVcoBank SuperSaw 1/iNum,kFreq+iDetune,iNum,iCount
   endif 
   xout aSig + aVcoBank
endop

;instrument will be triggered by keyboard widget
instr 1
    aOut SuperSaw p5, p4, 10
    outs aOut, aOut
endin
1 Like

why not use adsynth2 opcode?

I didn’t know that opcode, otherwise I would have suggested it too :wink: I guess the advantage of a UDO is you have absolutely control over all aspects of the generated waveform :man_shrugging: Now I’m curious about which approach is more performant. I doubt there is much in it, but I assume the opcode wins.

Thanks to @rorywalsh and @Kzz for support.

Seems, I will prefer the opcode (have no good explanation, it is just a feeling). Will come back to this somewhen later and tell about success.

1 Like

On the weekend, I tested the recursive UDO, and it works fine. Needs still some fine-tuning , e.g. different types of damping of the partials, or selection of all or only odd partials, but I already did that in my first brute force version, it is only a question of time and will :wink: Many thanks to @rorywalsh

The code looks now far more elegant than before.

Took me a while to understand, how Csound passes parameters to subprograms, it has not much to do with what I have learned with PACSAL/FORTRAN/C/PERL… But there are good manuals on the web, like the flossmanual with its many helpful examples and the canonical reference manual.

some off-topic thoughts about Csound syntax

And still struggling wit a-, k-, and i-Variables. I imagine, that every instrument contains some hidden loops: an i-initiliazition part, a k-loop and an nested a-loop deep inside. This idea helps me a bit to understand, what is going on.

Although it is a long time ago, that I used the languages mentioned above, their way of thinking is still hard coded in my brain :wink:

That’s pretty accurate. On each k-boundary an a-rate loop is run to fill the audio buffers. What’s funny is that there are actually no a-rate opcodes, they are all k-rate internally.

1 Like

Your idea piqued my curiosity, I had a go on the weekend using adsynt.

<CsoundSynthesizer>
<CsOptions>
-odac -d
</CsOptions>
;================================
<CsInstruments>

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

opcode subharm_2, ii,ii
  iNumHarm, iDamp xin

  iNumHarm = limit:i(iNumHarm, 1, 512)
  iRealDamp = (iDamp >= 0 ? iDamp : -iDamp)
print iDamp
  iFrqs = ftgen(0, 0, -iNumHarm, -2, 0)
  iAmps = ftgen(0, 0, -iNumHarm, -2, 0)

  iIndex init 1

  while iIndex <= iNumHarm do

    tabw_i(1/iIndex, iIndex - 1, iFrqs)

    if iDamp >= 0 then

        tabw_i(1/iIndex^iRealDamp, iIndex - 1, iAmps)

      else 

        tabw_i(1/iIndex^iRealDamp, iNumHarm - iIndex, iAmps)

    endif

    iIndex += 1

  od

  xout iFrqs, iAmps
ftprint iFrqs
ftprint iAmps
endop

instr create
  iNumHarm init 6
  iDamp init 1
  kFreq = linseg:k(1000, p3/1.75, 220)
  kAmp  init  .36

  giFrqs, giAmps subharm_2, iNumHarm, iDamp

  iWave = ftgentmp(0, 0, 2^17, 10, 1)

  aOut  = adsynt2(kAmp, kFreq, iWave, giFrqs, giAmps, iNumHarm)
  out(aOut)
endin

instr trig
  ; table for notes & random probabilities
  giWave = ftgentmp(0, 0, 1024, 10, 1, 0, .1)
  giTab = ftgentmp(0, 0, 180, -41, \
        46, .3, 48, 1, 51, .3, 53, .4, \
        54, .3, 55, .6, 58, .6, 60, .8, 62, .3)

  iNPS  = 6 ; notes per second
  kTrig = metro(iNPS)
  schedkwhen kTrig, 0, 0, "play", 0, .005, p4, p5
endin

instr play
  iFreq = mtof:i(duserrnd(giTab))
  iAmp  = p4
  aEnv  = transegr:a(.001, .004, 2, iAmp, .24, -2, .001)
  aOut  = adsynt2(1, iFreq, giWave, giFrqs, giAmps, p5)
  out(aOut * aEnv)
endin

</CsInstruments>
;================================
<CsScore>
i"create" 0  8
i"trig"   9  10.6  .65  1 ; no subharmonics
i.       21  .     .53  4  ; with subharmonics
</CsScore>
</CsoundSynthesizer>

I did another version as well, with the adsynth inside the udo.

A few notes - if you use a negative damp value it scales the harmonic amplitudes in reverse (the lowest harmonic will be loudest). Also, when the udo is called, you can create as many harmonics as you like but, since the freq/amp tables are global, individual adsynt occurrences can use only the number of subharmonics desired.

It would be interesting to test if this is more efficient than recursion or not. Also, by writing the freq/amp tables it would be very easy to modify to use only odd or even harm etc. The downside might be changing freq/amp and damping at k-rate as is. Should be easy enough to do with modifications though. In that case adsynt2 might be better at dealing with k-rate table changes, I have no idea.

2 Likes

Is there a reason that there are no a-rate opcodes? Convention? Performance?

I was going to move some code to a UDO and, in the process, change it from k-rate to a-rate to make some of the accounting simpler.

We should be careful when we say there are no a-rate opcodes, as most people refer to any opcodes that produces an a-rate signal as an a-rate opcode. Statements like this might lead to some i-rate Csound users :rofl: (couldn’t resist that one!)

The reason it works like this is performance. Processing a block of samples is typically far more efficient that processing sample by sample. Of course the trade of is latency.

1 Like