ChucKで遊ぶ
ChucKとは以下のようなもの。
ChucK is a new (and developing) audio programming language for real-time synthesis, composition, and performance - fully supported on MacOS X, Windows, and Linux.
ChucK presents a new time-based, concurrent programming model that's highly precise and expressive (we call this strongly-timed), as well as dynamic control rates, and the ability to add and modify code on-the-fly.
つまりその場で書いてその場でVMに読ませてその場でプレイという、超インプロヴィゼーショナボーな音声プログラミング言語ということになる。で、細かいところを見ていくと
ChucK supports polymorphic inheritance (this is the same model used in Java, and also known as virtual inheritance in C++)
ということも書いてあった。VIVA多重継承! つかこんな機能はインプロヴィゼーションじゃない!!!!
とChucKを紹介したところで
自分でちょこちょこと遊んだり、ドキュメントおよびMLを読んだりして分かったのがwavとかaiffとかの音源ファイルを読んで出すことができること。これはおもろい。というわけでターミナルからリズムマシンを動かすことを思いついた。しかもChucKのソース書くのがめんどいので今っぽくYAMLで書いた音源ファイルをrubyで変換してChucKのソースにするスクリプトを書いた。
ChucK自体がC++で書かれているようなのでrubyの拡張ライブラリもがんばれば書けるだろうけれども、いかんせん早く遊びたいのでテキスト処理で叩く方針。
ちなみにChucKで音源ファイルを鳴らすスクリプトはこんなん。
sndbuf note => dac; "notes/kick.aiff" => note.read; fun void taktpos(dur module, dur offset){ module - ((now - offset) % module) => now; } 60::second / 120 => dur t4; t4 / 2.0 => dur t8; t4 / 4.0 => dur t16; t4 / 8.0 => dur t32; t4 * 2.0 => dur t2; t4 * 4.0 => dur t1; while(true){ 0 => note.gain; taktpos(t8, 0::samp); 0 => note.pos; 0.5 => note.gain; t8 => now; 0 => note.pos; 0 => note.gain; t4 => now; 0 => note.pos; 0.5 => note.gain; t8 => now; 0 => note.pos; 0.5 => note.gain; t8 => now; 0 => note.pos; 0 => note.gain; t4 => now; 0 => note.pos; }
ループの中身が発音。note.gainで音量を設定。音量が0の場合は休符。「t4」「t8」ってのはそれぞれ4分音符と8分音符になる。上のほうは音源ファイルの定義と音源ファイル内のスタート位置指定など。詳しい説明は要望があれば。
準備するトラック定義用YAMLは次のようなかんじ。
NOTE: FILE: po.aiff GAIN: 0.5 LOOP: - t8* - t8 - t4* - t8 - t8 - t4*
アスタがついてるとこは「t4ぶんの休符」ってこと。あとはなんとなく想像つくでしょ。
でスクリプトは以下。ChucKだけにruby内で「チャック・ウィルソン」と書きたいし、あとからmoduleとしてつかうために「Chuck::Wilson」というふうにした。しかしながら自分で遊ぶために20分ほどでダダ書きしたので美しくない部分もあったりすると思います。つかいながらリファクタリます。
require 'yaml' require 'pathname' data = <<WILSON #--sample data--# NOTE: FILE: po.aiff GAIN: 0.5 LOOP: - t8* - t8 - t4* - t8 - t8 - t4* WILSON module Chuck LENGTH = ["t4 / 2.0 => dur t8;","t4 / 4.0 => dur t16;","t4 / 8.0 => dur t32;","t4 * 2.0 => dur t2;","t4 * 4.0 => dur t1;"] FUNC = "fun void taktpos(dur module, dur offset){\n module - ((now - offset) % module) => now;\n}\n\n" class Wilson def initialize(str,*bpm) @data = YAML::load(str) @note_name = @data["NOTE"]["NAME"] @bpm = bpm.to_s @buf = Array.new end def set if @data["NOTE"] != nil note = Array.new note << "sndbuf note => dac;\n" if @data["NOTE"]["FILE"] != nil note.push('"notes/'+@data["NOTE"]["FILE"]+"\" => note.read;\n") end @buf << note.join('') @buf << FUNC @buf << "60::second / #{@bpm} => dur t4;\n"+LENGTH.join("\n")+"\n\n" if @bpm != '' end if @data["LOOP"] != nil ctrl = Array.new process = Array.new @data["LOOP"].each_with_index do |itm, idx| if itm.to_s =~ /\*$/ process << " 0 => note.gain;\n" else process << " "+@data["NOTE"]["GAIN"].to_s+" => note.gain;\n" end if idx == 0 process << " taktpos("+itm.to_s.gsub("*","")+", 0::samp);\n" else process << " "+itm.to_s.gsub("*","")+" => now;\n" end process << " 0 => note.pos;\n\n" end ctrl.push("while(true){\n\n",process.join(''),"}\n") @buf << ctrl.join('') end end def morikawa puts @buf.join('') end def yukari p @data.to_a end def notes Pathname.new("./notes/").find{|f| puts f} end end end if __FILE__ == $0 arg = ARGV.shift ck = Chuck::Wilson.new(data,120) ck.set if arg == "--list" ck.notes else ck.morikawa end end
なお「morikawa」「yukari」というメソッドはdebug用。以前見知らぬ外人にソースをパクられた経験上debug用メソッド「showme」は「morikawa」「yukari」をつかっている。外人が「morikawa」「yukari」とか書いてるソースは壮観ですな!!
単音の音源準備してYAML書いて(イマントコ)リダイレクトしてファイル吐いて「chuck + xxx.ck」で食わせてとてもおもしろいことになります。
ToDo
- chuckにckファイルを食わせるタイミングで動かしてるので「time-based」という特徴を生かしたい。
- 「0100100101010001」(16桁で1小節よ!)みたいにシーケンスを書けるようにしたい。つかacts_as_bitsを使ってrailsに組み込みたい。こちらも参考にさせていただきます!!
- ChucKで来年のLLRingに出たい。
よろすこ!!