csoundで曲を作るプロセスとrubyによるスコア生成に関するメモ
色々素敵なcsoundのフロントエンドがあるけれど、どうもそれには馴染めないところがあるので手作業が続く中、仕方なくプログラムを作って作業をすることになりました。また、楽器や他の部分についてルールを自分なりに作っておけば後から再利用可能な楽器にすることができるなど分かってきました。
このアプローチはblueでも取られているようですが不勉強なので良く分かりませんが、まあ自分なりにやってみつつblueも試してみようと(試せるのなら)思います。
作曲のプロセス
曲を作るには「リズムを作ってから」とか、「フレーズを作ってから」など色々なスタイルがあると思いますが、結局小節のかたまりを動かしたり繰り返したりするので、csoundのスコアファイルを手編集することに限界があるように思います。最初はemacsでマクロ処理をしていたがかなり面倒でした。だからblueをはじめ色々なcsoundのフロントエンド環境が開発されている野田と思います。しかし、問題は使い方が(僕には)今ひとつ分かりずらいところでいつもくじけます。あきらめて手編集するけどもにもそれでも限界が出てくる。泣けてきますよね。
rubyによるスコア編集用プログラム
パーカッション(だけではないが)の繰り返しに関するエディットをするためにはプログラミングが非常に有効になる(のは明白)。連休前の金曜日にrubyを調べてblockというのが、スコア処理に合いそうだったので(こじつけ)はじめてrubyでプログラムを作ってみた。
最初コマンド引数から入ろうと思ったがいきなりくじけた。だからコマンドラインで、何かを指定するとかそういうレベルに達していないです。しかし、自己弁護をすると、スコアの処理は時系列に進むのがより自然では?というか、僕はそうしている。
例えば基本的なスコアを作ったらp3に揺らぎを与えたあとに全体を3回繰り返して、そのスコアのp6のパンを時間を見ながら三角関数で振るとか。だから、コンパイルの必要のないインタプリタのプログラムの中に直接書いてしまうのが現時点での最善の方法だろう(思い込み)。
例えば、cMaskでは下記(cmaskのマニュアルより転載)のように別ファイルでプログラム内容を指定している。ので、あたらずとも遠からずと希望(cMaskはマニュアルだけでまだ使ったことないから分からないけど)。
{ f1 0 8193 10 1 ;text in braces take place ;in the score file unchanged i3 0 20 } f 0 10 ;a field from 0 to 10 seconds p1 const 1 ;instrument 1 p2 range .01 .2 prec 3 ;onsets between 10 and 200 ms ;precision: 3 digits after ;the decimal point p3 rnd exp 1 ;durations between 0.5 ;and 1 seconds mask .5 1 ;and exponential distribution prec 2 p4 item heap (120 125 400 355) ;permutations of 4 values p5 rnd uni ;uniform distributed numbers mask (3 100 8 50) 200 map 1 ;between 100 and 200 at first and ;between 50 and 200 in the end p6 osc cos (0 .5 10 5) ;faster and faster oscillating ;values in the range {0...1}
で、以下のようなものを作った*1。ruby 1.8.6で動作を確認しました。
# # scproc.rb # - Yet Another SCore PROCessor 2007/09/25 # # - release: alpha 0.01! # - contact: a9a9qq@gmail.com # # class Score INSTR = 1 START = 2 DUR = 3 NOTE = 4 AMP = 5 def initialize @INSTR=1 @START=2 @DUR=3 @score=[] @startTime=99999.99 @endTime=0 @column=0 @scoreFile="" @@debug=true end def _scanminmax(param,min,max) min=99999 max=-99999 @score.each do |qq| min=qq[param-1] if (qq[param-1]<min) max=qq[param-1] if (qq[param-1]<max) end end def _scanStartEndTime() @startTime=10000.0 @endTime=0.0 @score.each do |qq| @startTime=qq[1].to_f if (qq[1].to_f<@startTime.to_f)&&(qq[1].to_f>=0.0) @endTime =qq[1].to_f if (qq[1].to_f>@endTime.to_f) end #print "@startTime=", @startTime, "\n" if @@debug #print "@endTime= ", @endTime, "\n" if @@debug end def timeShift(time) @score.each do |qq| qq[1]=qq[1].to_f+time.to_f end _scanStartEndTime() end def repString(param, str) @score.each do |qq| qq[param-1]=str end end def swapParam(m,n) @score.each do |qq| a=qq[m] qq[m]=qq[n] qq[n]=a end _scanStartEndTime() if (m==@START)||(n==@START) end def insertColumn(n) end def opBloc(param, &bloc) @score.each do |qq| qq[param-1]=bloc.call( qq[param-1] ,qq[@START-1]) end _scanStartEndTime() if param.to_i==@START.to_i end def repFract(param, min,max) dif=(max-min).abs @score.each do |qq| r=rand*dif+min qq[param-1]=r.to_f end _scanStartEndTime() if param.to_i==@START.to_i end def addFract(param,min,max) dif=(max-min).abs @score.each do |qq| r=rand*dif+min qq[param-1]=qq[param-1].to_f+r.to_f print "qq[param-1]=",qq[param-1],"\n" if qq[param-1].to_f<0 qq[param-1]=0 if (qq[param-1].to_f<0) end _scanStartEndTime() if param.to_i==@START.to_i end def addTimeInterp(param, min, max, stime=@startTime, etime=@endTime) dt=etime-stime dx=max-min rate=dx.to_f/dt.to_f @score.each do |qq| qq[param-1]=qq[param-1].to_f+rate*qq[@START-1].to_f+min.to_f end _scanStartEndTime() if param.to_i==@START.to_i end def repTimeInterp(param, min, max, stime=@startTime, etime=@endTime) dt=etime-stime dx=max-min rate=dx.to_f/dt.to_f print "dx=",dx," dt=",dt," rate=",rate,"\n" @score.each do |qq| qq[param-1]=rate*qq[@START-1].to_f+min.to_f end _scanStartEndTime() if param.to_i==@START.to_i end def addTimeInterpProc(param, proc) @score.each do |qq| qq[param-1]=qq[param-1].to_f + proc.call( qq[@START-1] ) end _scanStartEndTime() if param.to_i==@START.to_i end def repTimeInterpProc(param, proc) @score.each do |qq| qq[param-1]=proc.call( qq[@START-1] ) end _scanStartEndTime() if param.to_i==@START.to_i end def addTimeInterpBloc(param, &bloc) @score.each do |qq| qq[param-1]=qq[param-1].to_f + bloc.call( qq[@START-1] ) end _scanStartEndTime() if param.to_i==@START.to_i end def repTimeInterpBloc(param, &bloc) @score.each do |qq| qq[param-1]=bloc.call( qq[@START-1] ) end _scanStartEndTime() if param.to_i==@START.to_i end def _printline(q) formated="" formated=sprintf("i%-3s" ,q[0]) # instr, left- print formated, " " formated=sprintf("%8.3f" ,q[1].to_f) # start, 4,1,3 print formated, " " formated=sprintf("%8.3f" ,q[2].to_f) # dur 4,1,3 print formated, " " formated=sprintf("%9.3f" ,q[3].to_f) # amp print formated for i in 4..q.length-1 print " " formated=sprintf("%8.3f" ,q[i].to_f) # tone print formated end print "\n" end def _printscoreform(q) q.each do |qq| _printline(qq) end end def printScore(tail="") print ";p1 p2 p3 p4 p5 p6 p7\n" _printscoreform(@score) print tail end def repeatAdd(t,n) tmpScore=[] tmpLine=[] for i in 1..n startTime=i*t @score.each do |line| tmpLine=line.dup tmpLine.delete(nil) tmpLine[1]=line[1].to_f+startTime tmpScore<<tmpLine end #of each line end tmpScore.collect{|item| @score << item} _scanStartEndTime() end def setScoreFile(f) @scoreFile=f end def readScore() i=0 f=open(@scoreFile) while line = f.gets i=i+1 # remove comments if line =~ /;/ line=line[0..line.index(";")-1] line.strip end # handles lines which start with i if line =~ /^i/ p=[] line=line.gsub(/i/,'') p[0],p[1],p[2],p[3],p[4],p[5],p[6],p[7],p[8],p[9],p[10]=line.split(' ') p.delete(nil) if (p.length!=@column) then if (@column==0) then @column=p.length else print "ERROR: the number of column unmaches at line ", i,"\n" print " -> it changes from ",@column, " to ",p.length,"\n" print " -> ",line exit 1 end end @startTime=p[1].to_f if (p[1].to_f<@startTime)&&(p[1].to_f>=0.0) @endTime=p[1].to_f if (p[1].to_f>@endTime) @score<<p end end # each print "@startTime=", @startTime, "\n" if @@debug print "@endTime= ", @endTime, "\n" if @@debug end def exec(v) print v end end ############################################################################ # 今後したいこと #・パラメータ列の追加メソッド #・クオンタイズ用メソッド #・white/ping/blue/green...色つきノイズの実装 #・リズムをlaidbackしたりするフィルタ #・tendency maskを実装?? #・carryのサポート(.や省略) #・p2の+をサポートする #・負のp2(legarto)のサポート #・^+3前の値に3足すとか^-3前の値から3引くのサポート #・a,s,n,r,等のサポート #・np3をサポートする #・コメント行の保存するように、、 #・p1と書いているヘッダをまともに #・楽器番号の外部ファイル化 #・種となる曲から、作曲アルゴリズムを実装 #・複数楽器での楽曲の相互作用を実装 ############################################################################ # #ここから下にscoreで処理したい部分を書く。ことにしている。 # score=Score.new score.setScoreFile("scproc.sco") srand score.readScore() #score.printScore() #score.swapParam(6,7) # p6, p7を入れ替え #score.repeatAdd(32, 16) # 32.0毎に繰り返しを16回 #score.repString(1,"2") #score.addFract(2,-2,2) #score.addFract(3,-3,3) #score.addFract(4,-3,3) #score.addFract(5, 1,5) #score.repFract(6,-Math::PI/4.0,Math::PI/4.0) #score.timeShift(16.0*24*.0) #shift 16.0beat*24bars #score.repTimeInterp(3,10,100) #3-30 , start-end #score.addTimeInterp(3,30,10) #3-30 , startTime,endTime is default #psin=Proc.new do |t| Math.sin(t.to_f/4.0*Math::PI) end #score.repTimeInterpProc(3,psin) #3-30 , startTime,endTime is default #score.printScore() #score.addTimeInterpProc(3,psin) #3-30 , startTime,endTime is default #score.repTimeInterpBloc 3 do |t| Math.sin(t.to_f/4.0*Math::PI) end #score.addTimeInterpBloc 3 do |t| Math.sin(t.to_f/4.0*Math::PI) end score.opBloc 3 do |p,t| p.to_f*2.0+120+p.to_f*p.to_f-88.0 - Math.sin(t.to_f/4.0*Math::PI) end score.printScore()
これのスクリプト中でscproc.scoというファイルは結局スコアの「種」のようなもので、例えば以下のようなもの
;p1 p2 p3 p4 p5 p6 ;inst start dur note veloc rad i2 0 12 37 10 -0.7 i. 1 12 35 20 0 i. 2 12 88 30 0.7 i. 3 12 88 30 0.7 i. 4 12 37 10 -0.7 i. 5 12 35 20 0 i. 6 12 88 30 0.7 i. 7 12 88 30 0.7
引用部分を全部保存してruby scproc.rbとすれば動くと思います。多分。
rubyっぽいところは下記のopBlocと言うものではと「推測」。自信なし。
score.opBloc 3 do |p,t| p.to_f*2.0+120+p.to_f*p.to_f-88.0 - Math.sin(t.to_f/4.0*Math::PI) end
この例では、3番目のパラメータ(durationでしょうか、普通は)に対して元の値に2をかけて、120足して、二乗を加えて88を引いて、時間(普通はp2でしょうか)を見て8beatの周期を持つsinの値を足してあげるというようなものです。
Cmaskの中で持っているような関数は必要に応じて自分で作ってくださいというポリシのプログラムということで考えるのがいいです(ものぐさ)。
最初の目的はスコアファイルのエディティングの簡易化で、特定のパラメータに揺らぐ値を加えたり/置き換えたり、エディット中にパラメータ順序を置き換えたり、新たなパラメータを加えたり(これもinstrを作りながらだと良くあるのでは?)というあたりです。
今後は種となる複数のファイルからのアルゴリズックな作曲に持っていければと。
以下の対応を行った
硬いで出しですが、csoundで作曲を進めてゆくときに、見易さ、編集のしやすさ、メンテのしやすさから色々考えてみました。
- 結局ひとつの楽器にはひとつのスコアファイルを別立てにする。
- スコアファイルはプログラムを個別のスコアファイル用にカスタマイズして保存
- csdファイルはスコアはすべてincludeで
- 各楽器も今後の再利用を考えて個別のファイルに
でもその時に問題になる点も幾つか見えてきました。それは以下のとおりです。
- 楽器番号が再利用時にぶつかる
- fテーブルの番号が再利用時にぶつかる
- エフェクタ用のサブ楽器の番号がぶつかる
- エフェクタ用のグローバル変数の名前がぶつかる
これらの問題に大して具体的に以下の方法で各ファイルの記載方法を決めました。
- 曲中にある全楽器の番号は「define.h」のようなファイルの中で#defineを利用してマクロ定義する
- 「define.h」はcsdのorchestra部とscore部の両方で#includeする。
- fテーブルの番号は楽器番号+(一桁の)数字として楽器番号のマクロを利用して定義
- fテーブルの生成はscoreで行わずに楽器番号0のオーケストラ部でgitab ftgen $IFN_XXXX_YYYY, 0,65536,10,1のように行う。XXXは楽器名、YYYはfテーブルの固有名称。
- エフェクタ用サブ楽器番号も楽器番号のマクロを利用して定義1XYの3桁の番号とした
- グローバル変数も楽器番号を利用して定義
楽器OODAIKOについて以下のようにinstr_oodaiko.hを定義してオーケストラ部でインクルードする。
#define IFN_OODAIKO_SIN #$INSTR_OODAIKO1# gitab ftgen $IFN_OODAIKO_SIN, 0,65536,10,1, gamix$INSTR_KODAIKO init 0
このようにして整形されたcsdファイルは以下のようになる。
; ; shizukana natsu 2007/09 ; a9a9qq@gmail.com ; <CsoundSynthesizer> <CsOptions> -d -W -o shizukana_natsu.wav </CsOptions> <CsInstruments> sr = 44100 kr = 44100 ksmps = 1 nchnls = 2 #include "define.h" #include "instr_kaiten.h" #include "instr_konami.h" #include "instr_dora.h" #include "instr_heibon.h" #include "instr_hihat.h" #include "instr_noizdaiko.h" #include "instr_oodaiko.h" #include "instr_kodaiko.h" #include "instr_fmTomTom.h" #include "../../include/userDefinedOpcode/multiTapOnBeat.udo" #include "instr_kaiten.orc" #include "instr_konami.orc" #include "instr_dora.orc" #include "instr_heibon.orc" #include "instr_hihat.orc" #include "instr_noizdaiko.orc" #include "instr_oodaiko.orc" #include "instr_kodaiko.orc" #include "instr_fmTomTom.orc" </CsInstruments> <CsScore> #include "define.h" t 0 106 #include "instr_kaiten.sco" #include "instr_konami.sco" #include "instr_dora.sco" #include "instr_heibon.sco" #include "instr_hihat.sco" #include "instr_noizdaiko.sco" #include "instr_oodaiko.sco" #include "instr_kodaiko.sco" #include "instr_fmTomTom.sco" </CsScore> </CsoundSynthesizer>
ただし、
#define INSTR_OODAIKO #7# #define IFN_OODAIKO_SIN #$INSTR_OODAIKO1#
のような定義はバージョン5.06ではIFN_OODAIKO_SINを71のように解釈してくれるが、今後これが保証の限りではないかもしれない。