いらないモノ、ひつようなモノ

書籍、音楽、そして若干のテクノロジー

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}

で、以下のようなものを作った*1ruby 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で作曲を進めてゆくときに、見易さ、編集のしやすさ、メンテのしやすさから色々考えてみました。

  1. 結局ひとつの楽器にはひとつのスコアファイルを別立てにする。
  2. スコアファイルはプログラムを個別のスコアファイル用にカスタマイズして保存
  3. csdファイルはスコアはすべてincludeで
  4. 各楽器も今後の再利用を考えて個別のファイルに

でもその時に問題になる点も幾つか見えてきました。それは以下のとおりです。

  1. 楽器番号が再利用時にぶつかる
  2. fテーブルの番号が再利用時にぶつかる
  3. エフェクタ用のサブ楽器の番号がぶつかる
  4. エフェクタ用のグローバル変数の名前がぶつかる

これらの問題に大して具体的に以下の方法で各ファイルの記載方法を決めました。

  1. 曲中にある全楽器の番号は「define.h」のようなファイルの中で#defineを利用してマクロ定義する
  2. 「define.h」はcsdのorchestra部とscore部の両方で#includeする。
  3. fテーブルの番号は楽器番号+(一桁の)数字として楽器番号のマクロを利用して定義
  4. fテーブルの生成はscoreで行わずに楽器番号0のオーケストラ部でgitab ftgen $IFN_XXXX_YYYY, 0,65536,10,1のように行う。XXXは楽器名、YYYはfテーブルの固有名称。
  5. エフェクタ用サブ楽器番号も楽器番号のマクロを利用して定義1XYの3桁の番号とした
  6. グローバル変数も楽器番号を利用して定義

楽器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のように解釈してくれるが、今後これが保証の限りではないかもしれない。

*1:かなり駄目駄目コードかもしれませんが、初めてのrubyということで、、、