見出し画像

User Defined Opcode(UDO) in Csound

User Defined Opcode(UDO)とは、Csoundの既存の命令を組み合わせて、独自の命令(opcode)を作る機能です。Pure Dataでいえばabstractionに相当すると思います。UDOの特徴に、自らを再帰的にcallすることができる、という点があります(recursive UDO)。recursive UDOによってしか実現できない処理もあるのですが、その辺りのことはマニュアルにあまり明確に書かれていないので、解説します。


■UDOの簡単な例

まず、UDOとはどんなものなのかを示すために、簡単な例を挙げます。このUDO「MyUDO」は、2つのa-rateの入力を掛け算して出力する、リングモジュレーターです。この程度の簡単なものであれば、UDOを使う利点はあまりありません。

なお、UDO内の変数はローカル変数となるので、UDOの外からは参照できません。従って、もしUDOの内と外に同名の変数があっても、それらは区別されます。

opcode MyUDO, a, aa
aSig1, aSig2 xin
xout aSig1 * aSig2
endop

instr 1
kEnv linseg 0, 0.1, 1, p3-0.2, 1, 0.1, 0
kFreq line 100, p3, 1000
aSine1 oscili 1, 500, 1
aSine2 oscili 1,  kFreq, 1
aOut MyUDO aSine1, aSine2
outall aOut * 0.7
endin

■UDOの応用例 - 1

UDO「FiltBank」は2入力( a-rateとk-rate)、1出力(a-rate)です。a-rateの入力(aSIg)を3つのバンドパスフィルターに通し、その和を出力します。フィルタの周波数は、k-rateの入力(kFreq)で制御され、kFreq, kFreq+500, kFreq+1000となります。
instr 1では、3種類の異なるパラメータでUDOを実行することで、3つの異なる音群を生成しています。
UDOによりコードの再利用が容易になり、同じような処理の繰り返しを簡潔に記述できます。

opcode FiltBank, a, ak
aSig, kFreq xin
aBPF0 butterbp aSig, kFreq, 50
aBPF1 butterbp aSig, kFreq +500, 50
aBPF2 butterbp aSig, kFreq +500*2, 50
xout aBPF0 + aBPF1 + aBPF2
endop

instr 1
kEnv linseg 0, 1, 1, p3-3, 1, 2, 0
aNoise random -1, 1
kFilterFreq line 100, p3, 1000
kLFO oscili 100, 0.5, 1
aFilt1 FiltBank aNoise, kFilterFreq
aFilt2 FiltBank aNoise, kFilterFreq + kLFO ;variation 1
aFilt3 FiltBank aNoise, 5000 - 5*kFilterFreq ;variation 2
outall (aFilt1 + aFilt2 + aFilt3)*kEnv
endin

■UDOの応用例 - 2

UDOの入力/出力にはarrayも使用可能です。UDOでarrayを入出力する場合、以下の注意点があります。

  • opcode で outtypes、intypes を指定する際、arrayに該当するtypeに"[]"をつける

  • xin で input arguments を指定する際、array名に"[]"をつける。なお、このarrayについては、initで要素数を宣言する必要はない。

下記のUDO「Swap」のSyntaxは
karray Swap kinarray, kndx1, kndx2
で、kinarrayについてknd1, kndx2で指定した2要素を入れ替えてkarrayに返します。このUDOを使った作例を示します。

opcode Swap, k[], k[]kk
kArray[], kIndex1, kIndex2 xin
k1 = kArray[kIndex1]
k2 = kArray[kIndex2]
kArray[kIndex1] = k2
kArray[kIndex2] = k1
xout kArray
endop

■Recursive UDOの簡単な例

再帰的プログラムの例としてよく見かけるのは、階乗( n! = n*(n-1)*(n-2)*…*2*1 )の計算です。Recursive UDOを使って作成すると下記のようになります。UDO「Factorial」の中で、Factorialを再帰的に呼び出しています。
他言語でのプログラム例と見比べることで、処理の流れが理解できるかと思います。

opcode Factorial, i, i
ival xin
if ival <= 1 then
 iout = 1
else
  isub Factorial ival - 1
  iout = ival*isub
endif
xout iout
endop

instr 1
ix init p4
ifac Factorial ix
printf "*** Factorial of %d is %d ***\n", 1, ix, ifac
endin

i1 0 1 10

■Recursive UDOの使い所

冒頭で書いたように、recursive UDOによってしか実現できない処理があります。具体的には、Csoundの多くの命令は、ループ処理を行う場合、recursive UDOを使う必要があります。

例えば、oscilの個数をscoreの中で指定し、instrの中で動的にoscilを発生させたい場合、if/goto、while、loop_itを使ったループ処理では想定通りに動きません(エラーも出ない!)。その理由は、Csound の多くの命令がステートフルで、例えばoscilであれば「今現在のphase」等のデータを内部に保持しているためです。上記の方法でループ処理した場合、ループが回る毎にステートがアップデートされるため、意図した結果が得られません。Recursive UDOを使うとこの問題が生じません。技術的な説明は参考資料[3]に詳しいです。

下記のCSDでは、recursive UDOで正常にループ処理が行われるinstr1、i-rateのループ処理を行うinstr2、k-rateのループ処理を行うinstr3を、順に実行します。意図する出力は、scoreのp5で指定した数のサイン波(ここでは8個)を発生させる、というものですが、while文でループを記述したinstr2、instr3では、そのように動きません。

;recursive UDO
opcode oscilbank, a, kki
kAmplitude, kFrequency, indx xin
aSig2 init 0
aSig oscili kAmplitude, kFrequency, 1
if indx > 1 then
aSig2 oscilbank kAmplitude, kFrequency +100, indx-1
endif
aSig3 = aSig + aSig2
xout aSig3
endop

instr 1
kAmp init 0.1
kFreq init p4 
indx init p5

aOut oscilbank kAmp, kFreq, indx
outall aOut
endin


;NG example -1 : i-rate loop
instr 2
kAmp init 0.1
kFreq init p4 
indx init p5
aOut init 0

while indx >0 do
aSig oscili kAmp, kFreq , 1
aOut += aSig
kFreq += 100
indx -= 1
od
outall aOut
endin

;NG example -2 :  : k-rate loop
instr 3
kAmp init 0.1
kFreq init p4 
kndx init p5
aOut init 0

while kndx >0 do
aSig oscili kAmp, kFreq , 1
aOut += aSig
kFreq += 100
kndx -= 1
od
outall aOut

;clear for next k cycle
kndx = p5
aOut = 0
kFreq = p4
endin

■Recursive UDOの応用例

UDOの応用例 -1のUDO「FiltBank」をrecursive UDOに書き換えたのが下記の「FiltBank2」です。FiltBankではバンドパスフィルターは3つに固定ですが、FiltBank2ではフィルター数が指定できます。
このcsdファイルでは、バンドパスフィルターの数はscoreのp4で指定した8個となります。

opcode FiltBank2, a,aki
aSig, kFreq, icnt xin
aSub init 0
aBPF butterbp aSig, kFreq, 50
if icnt > 1 then
  aSub FiltBank2 aSig, kFreq+500, icnt-1
endif
xout aBPF + aSub
endop

■追記:繰り返し処理はRecursive UDOに置き換えるべきか?

Reference [3]によれば、Recursive UDOよりも単純な繰り返しの方が、処理スピードやメモリ消費の点で有利であるとのことです。「Recursive UDOの使い所」で記載したような、Recursive UDOでしか実現できない処理を中心に使用するのが良いと思います。

In general, iteration is faster and requires less memory than recursion, therefore it is recommended to solve problems using iteration first and only then use recursion when necessary in Csound.

[3] "Control Flow - Part II". CSOUND JOURNAL, 2006, Summer 

■Reference

[1] "opcode - Defines the start of user-defined opcode block". The Canonical Csound Reference Manual, Version 6.18.0
https://csound.com/manual/opcode.html

[2] "USER DEFINED OPCODES". The Csound FLOSS Manual, Version 7.0.1, 03 G.
https://flossmanual.csound.com/csound-language/user-defined-opcodes

[3] Yi S. "Control Flow - Part II". CSOUND JOURNAL, 2006, Summer 
http://www.csounds.com/journal/2006summer/controlFlow_part2.html

Version history
v1.0 : 2023/02/05 公開
v2.0 : 2023/08/11 「追記:繰り返し処理はRecursive UDOに置き換えるべきか?」を追加


この記事が気に入ったらサポートをしてみませんか?