Google_Moogの遊び方 翔
C.まずRobert Moog's 78th Birthdayに行きます。
D.とりあえず演奏、録音をします。
E.そして短縮URLをゲットできます。
F.それを入力しますと自動的に再生します。
G.URLのアドレスを見ますと以下のようになっているはずです
https://www.google.com/doodles/robert-moogs-78th-birthday?doodle=6201726XdBA74MzMMlMzN0pmZthmZn8AAAMRmZnyAAANQAAB6pmZs3__-3-2E8aZmbTPP__9lPUzMz2n7___YzzpmZhymUWWGQYhiE4hhGQZhJQYhiGQVUGQZBkGIRkGQZBkGQZhkGQZBkEYhkGQZBBIZBmFlBiGQYhdQZRdIYxmmA..
A.さあリクエストパラメータのdoodle=6201726X以降の文字列を好きな文字に置き換えてみましょう。
B.予期せぬ音楽に出会えるかもしれませんよ。
C.何をいまさら。
CoffeeScriptでサウンドライブラリ
追記(実際にブラウザで音アプリを作りたい人はAudiolet.js がいいと思いますよ)
以前にjavascriptで書いたサウンドライブラリをCoffeeScriptで書きなおしたもの。
javascriptとcoffeescriptを相互変換するツールがあるのですがどうも望んだ感じにはならないので書いたというところ。
時間切れでfirefoxには対応していないし、奇妙なフィルタとかもなかったりする。あともう少し縮められたが気がする。
まあ名前はCoffeeSndってことで。簡易的なものであくまでネタです。
使い方。よくある感じです。
adsr = new ADSR(0, 0, 1, 0.25, 0) aseq = new Sequencer(adsr, 120, [1, 0, 1, 0, 1, 1, 1, 1]) noise = new Noise(aseq) f = new BandPass(noise, 8000, 0.9, 0, 0) out = new SndOut(f, f); out.play()
実際のライブラリ
class CoffeeSnd constructor: -> @sample_rate = 48000 @buffer_size = 2048 @timestamp = 0 @outtemp = 0 class Proxy extends CoffeeSnd constructor: (x) -> super @number = x update: (tm) -> @number note_on: -> Port = (n) -> if typeof (n) is "number" new Proxy(n) else n class Gen extends CoffeeSnd constructor: (frequency, amplitude, addfrequency, addamplitude) -> super @freq = Port(frequency) @amp = Port(amplitude) @addfreq = Port(addfrequency) @addamp = Port(addamplitude) @phase = 0 set_freq: (frequency) -> @freq = Port(frequency) set_amp: (amplitude) -> @amp = Port(amplitude) set_addfreq: (addfrequency) -> @addfreq = Port(addfrequency) set_addamp: (addamplitude) -> @addamp = Port(addamplitude) note_on: -> @phase = 0 class SinOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs( @freq.update(tm) + @addfreq.update(tm) )/@sample_rate @phase = (if @phase >= 1 then 0 else @phase) @outtemp = Math.sin(2*Math.PI*@phase)*@amp.update(tm) + @addamp.update(tm) else @outtemp class SqrOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm))*2/@sample_rate @phase = (if @phase >= 1 then -1 else @phase) d = @amp.update(tm) @outtemp = (if @phase < 0 then d * -1 + @addamp.update(tm) else d + @addamp.update(tm)) else @outtemp class SawOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs( @freq.update(tm) + @addfreq.update(tm) )*2/@sample_rate @phase = (if @phase >= 1 then -1 else @phase) @outtemp = @phase*@amp.update(tm) + @addamp.update(tm) else @outtemp class TriOsc extends Gen update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm)) * 2 / @sample_rate @phase = (if (@phase >= 1) then -1 else @phase) d = @phase * 2 d = (if (d > 1) then 1 - (d - 1) else d) d = (if (d < -1) then -2 - d else d) @outtemp = d * @amp.update(tm) + @addamp.update(tm) else @outtemp class TabOsc extends Gen constructor: (@wave_array, amplitude, addfrequency, addamplitude) -> super(amplitude, addfrequency, addamplitude) update: (tm) -> if @timestamp is tm @timestamp++ @phase += Math.abs(@freq.update(tm) + @addfreq.update(tm)) / @sample_rate @phase = (if (@phase >= 1) then 0 else @phase) @outtemp = @wave_array[Math.floor((@wave_array.length - 1) * @phase)] * @amp.update(tm) + @addamp.update(tm) else @outtemp class Noise extends Gen constructor: (amplitude) -> super(0, amplitude, 0, 0) update: (tm) -> Math.random() * @amp.update(tm) class ADSR extends CoffeeSnd constructor: (atc, dec, sus, sus_time, rel) -> super @start_value = new Array(4) @end_value = new Array(4) @sample_length = new Array(4) @reciprocal_sample_length = new Array(4) @current_value = 0 @count = 0 @stage = 5 @start_value[0] = @current_value @end_value[0] = 1 @sample_length[0] = atc * @sample_rate @reciprocal_sample_length[0] = (if @sample_length[0] is 0 then 0 else 1.0 / @sample_length[0]) @start_value[1] = 1 @end_value[1] = sus @sample_length[1] = dec * @sample_rate @reciprocal_sample_length[1] = (if @sample_length[1] is 0 then 0 else 1.0 / @sample_length[1]) @start_value[2] = sus @end_value[2] = sus @sample_length[2] = sus_time * @sample_rate @reciprocal_sample_length[2] = (if @sample_length[2] is 0 then 0 else 1.0 / @sample_length[2]) @start_value[3] = sus @end_value[3] = 0 @sample_length[3] = rel * @sample_rate @reciprocal_sample_length[3] = (if @sample_length[3] is 0 then 0 else 1.0 / @sample_length[3]) note_on: -> @stage = 0 @count = 0 @start_value[0] = @current_value update: (tm)-> if @timestamp is tm @timestamp++ if @stage < 4 @current_value = @start_value[@stage] + (@end_value[@stage] - @start_value[@stage]) * (@count * @reciprocal_sample_length[@stage]) if @sample_length[@stage] > @count @count = @count + 1 else @count = 0 @stage = @stage + 1 else @current_value = 0 @outtemp = @current_value else @outtemp class Sequencer extends CoffeeSnd constructor: (input, bpm, triger) -> super @input = Port(input) @bpm = (60.0 / bpm) * @sample_rate / 4.0 @triger = triger @triger_num = 0 @count = 0 @flag = true @range = 0 update: (tm) -> if @timestamp is tm @timestamp++ if @bpm < @count if @flag is true if @triger[@triger_num] @input.note_on() @range = @triger[@triger_num] @triger_num = (@triger_num + 1) % @triger.length @count = 0 @count = @count + 1 @outtemp = @input.update(tm) * @range else @outtemp class Filter extends CoffeeSnd constructor: (input, cf, q, addcf, addq) -> super @input = Port(input) @cf = Port(cf) @q = Port(q) @addcf = Port(addcf) @addq = Port(addq) @frame = 0 @b0 = 0 @b1 = 0 @b2 = 0 @a0 = 0 @a1 = 0 @a2 = 0 @forder = 0 @sorder = 0 @calc = (tm) -> update: (tm) -> if @frame > @sample_rate @calc() @frame = 0 @frame += 64 temp = (@b0 / @a0) * @input.update(tm) + (@b1 / @a0) * @forder + (@b2 / @a0) * @sorder - (@a1 / @a0) * @forder - (@a2 / @a0) * @sorder @sorder = @forder @forder = temp temp set_cf: (frequency) -> @cf = Port(cf) set_q: (amplitude) -> @q = Port(q) set_addcf: (addfrequency) -> @addcf = Port(addcf) set_addq: (addamplitude) -> @addq = Port(addq) class LowPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) / (2 * Math.abs((0.5 * @q.update(tm) + 0.5) + @addq.update(tm))) cs = Math.cos(omega) @b0 = (1 - cs) / 2.0 @b1 = 1 - cs @b2 = (1 - cs) / 2.0 @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class HighPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) / (2 * Math.abs((0.5 * @q.update(tm) + 0.5) + @addq.update(tm))) cs = Math.cos(omega) @b0 = (1 + cs) / 2 @b1 = -(1 + cs) @b2 = (1 + cs) / 2 @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class BandPass extends Filter constructor: (input, cf, q, addcf, addq) -> super(input, cf, q, addcf, addq) @calc = (tm) -> sinh = (arg) -> (Math.exp(arg) - Math.exp(-arg)) / 2 omega = 2 * Math.PI * Math.abs((0.5 * @cf.update(tm) + 0.5) + @addcf.update(tm)) / @sample_rate alpha = Math.sin(omega) * sinh(Math.log(2) / 2 * Math.abs(@q.update(tm) + @addq.update(tm)) * omega / Math.sin(omega)) cs = Math.cos(omega) @b0 = alpha @b1 = 0 @b2 = -alpha @a0 = 1 + alpha @a1 = -2 * cs @a2 = 1 - alpha @calc(0) class Operation constructor: (a, b) -> @a = Port(a) @b = Port(b) set_A: (a) -> @a = Port(a) set_B: (b) -> @b = Port(b) class Add extends Operation update: (tm) -> @a.update(tm) + @b.update(tm) class Mult extends Operation update: (tm) -> @a.update(tm) * @b.update(tm) class SndOut extends CoffeeSnd constructor: (input, input2) -> super @audiocontext = new webkitAudioContext(); @audiocontext.sampleRate = @sample_rate; @node = @audiocontext.createJavaScriptNode(@buffer_size, 0, 2); timestamp = 0 @node.onaudioprocess = (event) -> data = event.outputBuffer.getChannelData(0); data2 = event.outputBuffer.getChannelData(1); i = 0 while i < data.length data[i] = input.update(timestamp) data2[i] = input2.update(timestamp) i++ timestamp++ play: -> @node.connect(@audiocontext.destination) stop: -> @node.disconnect()
ブラウザとMIDIコントローラを繋げてみる。
今回はMIDIコントローラでブラウザで生成する矩形波を弾いてみる。
準備するもの
chromeブラウザと、pythonとライブラリpygameとAutobahnPythonと、なんらかのMIDI機器。
firefoxも可能なのですが音声部分のタイプ量が増えるので読みやすさ重視で今回は見送ります。
また残念ながらnode.jsは使ってません。
作るもの
ファイルは2つで、MIDI受信サーバーのpythonファイルとシンセエンジンのhtml。
おおまかな仕組み
サーバーは、MIDIコントローラから送られるデータを待ち、受け取ったらブラウザに渡します。
ブラウザは、受け取ったデータに従い矩形波の音をオンオフしたりピッチや音量を変えます。
MIDI入力
PythonでリアルタイムにMIDI機器と通信するには、一番手っ取り早いのPygameだと思います。
ちなみに1.9以上が必要なので古いの使ってる人はバージョン上げてください。
(僕の環境だと多少不安定で、気になるならpyoやpysndobjあたり使ってはどうでしょうか)
import pygame.midi pygame.midi.init() out = pygame.midi.Input(1) out.read(1)
必要なのはこの4行です。
3行目のInputの引数が1になっていますが、これはデバイスの番号です。
よって環境によって異なります、それを調べる方法が
for i in range(pygame.midi.get_count()): print pygame.midi.get_devie_info(i)
これで使用できるデバイスが全て表示されます。
ここから例えばKORGのnanoとかスタインバーグのCMCとか自分が持ってるMIDI機器を選びます。
持ってなくても仮想MIDIケーブルなどで音楽ソフトウェアからブラウザにアクセスしてみてもいいでしょう。
次に4行目のreadの引数は1になっています。
入力されたデータを1つだけ取り出すということです。
今回は鍵盤が押されたらなにかアクションをするという単純なものなのでヒトツデジュウブンデスヨ。
このreadはループの中で常に入力を待つような使い方をします。例えば
import time while 1: print output.read(1) time.sleep(1)
これだと無限ループですが雰囲気は伝わるでしょうか?。
入力がない場合は空の配列が返されます。
キーが入力された場合は、
[[[144,60,100],4753]]
144は鍵盤が押されたということを表します。
逆に鍵盤を離した場合は128になるはずです。
60は周波数(ノートナンバー)で100は音量、4753は初期化されてからの時間(タイムスタンプ)。
配列は環境によっていろいろあるでしょうからご自身のMIDI機器とにらめっこしてください。
サーバー
さて次はサーバーです。AutobahnPythonを使ってみます。
あまり有名ではないかもしれませんが名前が気に入りました。
わずか10行でエコーサーバーが決め手です。
本当はwindowsのバイナリがあるのが決め手だったかも。
さてこのAutobahnPythonはTwistedに依存しているので使用するためには
Zope Interface
Twisted
AutobahnPython
これらをインストールしてください。winユーザも安心のバイナリありで、ちょちょいです。
さてコードです。10行でエコーサーバーを少し改良した程度のものです
from twisted.internet import reactor from autobahn.websocket import WebSocketServerFactory, WebSocketServerProtocol, listenWS import pygame.midi class EchoServerProtocol(WebSocketServerProtocol): def sendMIDI(self): temp = self.out.read(1) if len(temp): # if self.out.poll():でもよい。入力があるとTrueを返すので。 self.sendMessage(str(temp)) reactor.callLater(1/30.0, self.sendMIDI) def onOpen(self): pygame.midi.init() self.out = pygame.midi.Input(1) self.sendMIDI() if __name__ == '__main__': factory = WebSocketServerFactory("ws://localhost:9000") factory.protocol = EchoServerProtocol listenWS(factory) reactor.run()
11行目のonOpenが最初に呼ばれるのでそこでpygame.midiを初期化し、sendMIDIを呼びます。
sendMIDIを再帰的にcallLaterで1/30秒ごとにMIDIの入力をチェックします。
次にブラウザにメッセージを送信するのは9行目のsendMessage。
ブラウザ
最後はブラウザです。
<html> <head> <script type="text/javascript"> function $(id){ return document.getElementById(id) } var sampleRate = 48000; var bufferSize = 2048; var freq = 440; var amp = 0.3; var audiocontext = new webkitAudioContext(); audiocontext.sampleRate = sampleRate; var node = audiocontext.createJavaScriptNode(bufferSize, 0, 1); var phase = 0; node.onaudioprocess = function (event) { var data = event.outputBuffer.getChannelData(0); // 以下は矩形波を生成する。 for (var i = 0; i < data.length; i++) { phase += freq/sampleRate; phase = phase >= 1 ? -1 : phase; data[i] = phase < 0 ? -1*amp : 1*amp; } }; //ノートナンバーを周波数に function midi2freq(x){ return 440*Math.pow(2, (x-69)/12); } window.onload = function() { $("console").innerHTML = "hoge"; node.connect(audiocontext.destination); //ここで音が出る. var ws_uri = "ws://localhost:9000"; if ("WebSocket" in window) { webSocket = new WebSocket(ws_uri); }else{ webSocket = new MozWebSocket(ws_uri); } webSocket.onmessage = function(e) { temp = eval(e.data); $("console").innerHTML = " freq: " + String(midi2freq(temp[0][0][1])); freq = midi2freq(temp[0][0][1]); //周波数を取り出す。 if(temp[0][0][0] !== 128){ $("console").innerHTML += " amp: " + String(temp[0][0][2]); amp = parseInt(temp[0][0][2])/127.0; $("console").innerHTML += " state: on "; }else{ $("console").innerHTML += " amp: 0 "; amp = 0; $("console").innerHTML += " state: off "; } } } </script> </head> <body> <h1>midi -> browser test</h1> <div id="console"></div> </body> </html>
関数midi2freqはその名の通りMidiのノートナンバーを周波数にしている。
音声処理の部分の説明は過去に何度も行なっているので省略します。
ブラウザARもどきテルミンもどき
chromeのabout:flagsでmedia-streamを有効化するとWebカメラが使える。
そこでjavascriptオンリーのブラウザARテルミンをやってみた。
仕組みはマーカーの位置を検出し、x軸とy軸をピッチと音量に割り当てた単純なものです。
マーカーを検出するライブラリはjs-arucoを使用しています。
音声処理はweb audio apiです。
やはり作ってみるとなかなか重いので、画面サイズを640*480から320*240にして、音で遊ぶものなので余計な描画は全てカットしました。
またモノフォニックなテルミンなので複数検出機能も捨てています。
すると、、、けっこうきれいに音が出て満足しています。工夫すればドラムマシーンのようなものもできるかも。
こんな感じになる写真(スマフォはマーカー印刷するのめんどくさかったから使ってる)
以下ソース
<html> <head> <title>AR Theremin</title> <script type="text/javascript" src="polyfill.js"></script> <script type="text/javascript" src="cv.js"></script> <script type="text/javascript" src="aruco.js"></script> <script> var video, canvas, context, imageData, detector; var sampleRate = 48000; var bufferSize = 2048; var freq = 440; var amp = 0.0; var audiocontext = new webkitAudioContext(); audiocontext.sampleRate = sampleRate; var node = audiocontext.createJavaScriptNode(bufferSize, 0, 1); var phase = 0; node.onaudioprocess = function (event) { var data = event.outputBuffer.getChannelData(0); for (var i = 0; i < data.length; i++) { phase += freq/sampleRate; phase = phase >= 1 ? -1 : phase; data[i] = phase < 0 ? -1*amp : 1*amp; } }; function freqampCorners(markers){ var corners,corner; corners = markers[0].corners; corner = corners[0]; freq = 8000 * (corner.x / parseFloat(canvas.width)); amp = corner.y / parseFloat(canvas.height); } function onLoad(){ video = document.getElementById("video"); canvas = document.getElementById("canvas"); context = canvas.getContext("2d"); canvas.width = parseInt(canvas.style.width); canvas.height = parseInt(canvas.style.height); navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; if (navigator.getUserMedia){ navigator.getUserMedia("video", successCallback, errorCallback); function successCallback(stream){ if (window.webkitURL) { video.src = window.webkitURL.createObjectURL(stream); } else { video.src = stream; } } function errorCallback(error){ } detector = new AR.Detector(); requestAnimationFrame(tick); } node.connect(audiocontext.destination); } function tick(){ requestAnimationFrame(tick); if (video.readyState === video.HAVE_ENOUGH_DATA){ snapshot(); var markers = detector.detect(imageData); freqampCorners(markers); } } function snapshot(){ context.drawImage(video, 0, 0, canvas.width, canvas.height); imageData = context.getImageData(0, 0, canvas.width, canvas.height); } window.onload = onLoad; </script> </head> <body> <center> <div>AR-Theremin.html</div> <video id="video" autoplay="true" style="display:none;"></video> <canvas id="canvas" style="width:320px; height:240px;"></canvas> </center> </body> </html>
AndroidでCsoundのコードを実行してみよう
この文書はVictor Lazzariniのブログを翻訳したものです。前のブログでCsoundをサウンドライブラリにして自分のAndroidアプリを作るみたいなことを紹介しましたが、今回はAndroid上でCsoundのコードをCsound Playerで実行するという話です。そう、AndroidだけでCsoundのコードを実行できます、色々注意しなければなりませんが。
>>>以下本文
皆さんによる叱咤で、私はAndroid版Csoundに、演奏するためのシンプルなUIを持たせることにした。基本的に、5つのスライダーと5つのボタンとトラックパッドと加速度計を配置している。このシンプルなアプリは、CSDファイルを走らせることができる、chgetオプコードと名付けたチャンネルでもって、コントローラーの値をタップで変えることができる。browseボタンで、利用者がファイルシステムからCSDファイルを読み込める。
備考は以下のように
1.コントローラは、加速度計を除いてすべて0から1のとっても狭い範囲なので、十分の範囲にするにはだいだい1000によって増幅しなければならない。
2.チャンネル名はUI(slide1のように)にあるようになる。ボタンは、余分に2つのタッチした座標である"channel_name.x"と"channel_name.y"のチャンネルを持つ。
またトラックパッドも、"trackpad.x"と"trackpad.y"。
3.ボタンはトリガーと言うよりはプレスダウンで、つまりダウンが1でそれいがいは0です。タッチしてないときは、ボタンのxとy座標も0になる(トラックパッドも同様)。
4.加速度センサーのチャンネル名は"accelerometerX"と"accelerometerY"と"accelerometerZ"だ。
5.私のタブレットで、UIは2つを同時に調整することができないようになっている(思うにAndroidの制限なのかもしれない)。ん、ボタンなんかはモノフォニックだよ。トラックパッドはモノフォニックなんだが、チャンネルの命名スキームが思いついたりしたら次のバージョンにおいて変更するかもしれない。
6.オンオフ・スイッチは演奏の終了時にオフにして。
コントローラにアクセスする基本的なCSDの書き方は以下のとおり。利用者はアプリで走らせる自身のCsoundコードを使える。
<CsoundSynthesizer> <CsOptions> -odac -+rtaudio=null -d </CsOptions> <CsInstruments> nchnls = 1 instr 1 idur = p3 iamp = p4*0dbfs icps = p5 iatt = p6 idec = p7 itab = p8 k3 init 1 k1 chnget "slider1" k1 port k1+1, 0.01, 1 a1 expseg 1,idur-idec,1,idec, 0.001 a2 linen a1, iatt, idur, 0.005 a3 oscili a2*iamp, icps*k1, itab k3 chnget "trackpad.x" k3 portk k3, 0.01 out a3*k3 chnmix a3*k3, "reverb" endin instr 100 asig chnget "reverb" a1,a2 freeverb asig,asig, 0.8, 0.7 out (a1+a2)*0.1 chnclear "reverb" endin </CsInstruments> <CsScore> ; 関数テーブルの例 f 1 0 16384 10 1 ; イベント例 ; ins st end amp cps att dec tab i 1 0 3600 0.5 440 0.01 .9 1 i 100 0 -1 </CsScore> </CsoundSynthesizer>
最後にアプリのリンクはここ。
CsoundApp.apk
ソースコードとEclipseプロジェクトはcsound5.gitで見つけることができる。
git://csound.git.sourceforge.net/gitroot/csound/csound
Pythonでヴォコーダー
これは、以前に紹介したPythonのサウンドライブラリPyoで、どのように自作クラス(ヴォコーダー)を作るかを説明した文章の翻訳です。
>>>以下本文
原文 http://code.google.com/p/pyo/wiki/createYourOwnAudioObject
自作オーディオ・オブジェクト
これは、小さいチュートリアル。サウンド処理のために、君自身のオブジェクトのつくりかた。
序文
この例では、ヴォコーダーを製作していくことにする。とてもシンプルなクラスから始めて、より洗練されたクラスを作るために、徐々に機能を加えていく。できあがったクラスはホンモノのpyoオブジェクトのように振る舞うだろう。
なにはともあれ、メインのスクリプトが読み込む、クラスの置き場所に"vocoder_lib.py"というファイルをつくろう。
ファイル"vocoder_lib.py"と"vocoder_main.py"はpyoのソースコードのexampleフォルダの中にある。
シンプルなヴォコーダー
自作クラスに、妙な振る舞いをさせないように覚えておくべきことがひとつある。すべてのpyoオブジェクトは、オーディオサンプルの計算に要する時間まで、生存させておかなければならない。initメソットの終了時に自作クラスが破壊されるのを防ぐために、常にpyoオブジェクトに接頭辞"self"をつけることをこころがけて。
自作クラス(最低でもPyo)にある、君が必要とするモジュールすべてが読み込まれることで開始する。
import math from pyo import *
そして、ここにクラスSimpleVocoderがある。
class SimpleVocoder: def __init__(self, in1, in2, num=32, base=50, spread=1.5, q=5): self._in1 = in1 self._in2 = in2 self._num = num self._base = base self._spread = spread self._q = q self._freqs = Sig([self._base * math.pow(i+1, self._spread) for i in range(self._num)]) self._clipped_freqs = Clip(self._freqs, 20, 200000) self._src = Biquadx(self._in1, freq=self._clipped_freqs, q=self._q, type=2, stages=4) self.envelope = Follower(self._src, freq=5, mul=self._q*30) self._exc = Biquadx(self._in2, freq=self._clipped_freqs, q=self._q, type=2, stages=4, mul=self._envelope)
おわかりのように、非常にシンプルなクラスですが、準備ができた。
ここで、なにが起こったのか確認してみよう。はじめに、あとで使うために、入力で与えられたpyoオブジェクトの参照を保持する。そのとき、フィルタの周波数を計算し、リストをシンプルなSig()オブジェクトに渡す。そこでは、フィルタ周波数をClip()されたオーディオ信号を浮動小数点に変更するために、Sig()オブジェクトを利用する(フィルタはナイキスト周波数によって一定ではない)。いったんこれがされると、最初の信号の帯域ごとに、エンベロープフォローワーを適用し、そして二番目の信号で、このエンベロープをフィルタの振幅として使うことによって、ヴォコーダーを作れる。
これで、われわれのメインのスクリプトで、このクラスを利用できる(vocoder.pyファイルをメインのスクリプトと同じフォルダに置くことを忘れないで)。
from pyo import * from vocoder_lib import SimpleVocoder s = Server(sr=44100, nchnls=2, buffersize=1024, duplex=0).boot() a = SFPlayer(SNDS_PATH + "/transparent.aif", loop=True, mul=3).play() b = Noise() voc = SimpleVocoder(in1=a, in2=2, num=32, base=50, spread=1.2, q=5) s.gui(locals())
これでいいでしょう、ただ扱いづらい、だって実行中にいろいろ変更するのがとっても面倒。
Vocoderをコントロールするためにメソッドを加えてみよう。
def setBase(self, x): self._base = x self._freq.value = [self._base * math.pow(i+1, self._spread) for i in range(self._num)] def setSpread(self, x): self._spread = x self._freqs.value = [self._base * math.pow(i+1, self._spread) for i in range(self._num)] def setQ(self, x): self._q = x self._envelope.mul = self._q * 30 self._src.q = self._exc.q = self._q
これで、再生中には、インタプリターで以下のように呼び出すことができる。
voc.setBase(60) voc.setSpread(1.5) voc.setQ(10)
また、自作クラスに以下の行を加えることで、タイプ量を減らすのに属性を利用することもできる。
@property def base(self): return self._base @base.setter def base(self, x): self.setBase(x) @property def spread(self): return self._spread @spread.setter def spread(self, x): self.setSpread(x) @property def q(self): return self._q @q.setter def q(self, x): self.setQ(x)
SimpleVocoderの親クラスとして"object"をあたえること忘れないで。
class SimpleVocoder(object):
属性を利用することで、このように呼び出しを置き換えることができる。
voc.base = 60 voc.spread = 1.5 voc.q = 10
以上です!。いま、われわれのプログラムで使用するヴォコーダの準備が整いました。
ヴォコーダ(さらなるpyo機能を加えて)
SimpleVocoderが、プロセスチェーンの最後の要素のときには役立つのだが。ふつうのpyoオブジェクトのように使えない。例えば、もしリバーブオブジェクトのサウンドを渡したいなら、クラスそれ自身にリバーブユニットをくわえて修正しないといけない。ライブラリにある、どんなオブジェクトでも他のオブジェクトに渡せるならとても便利だ。少しの工夫で、すべてのpyoの機能をもったクラスを作れる。今からこのようなことをしていく。
考慮する事項
・親のクラスはPyoObjectでなければならない
・PyoObjectはべつのPyoObjectを受けるときに、"self.base_objs"と名付けられたオブジェクトのリストを探す。
・"mul"や"add"の引数を加える(self._base_objsのオブジェクトを変更する)
・すべてのPyoObjectは"リストの展開"をサポートする。
・入力時のサウンドをもったPyoObjectは新旧のソースのクロスフェードをサポートする。
・おそらく.play()、.out()、そして.stop()メソッドを上書きしたくなるだろう。
・すべての関数ために、パラメータを修正する属性がある。
・__dir__メソッドは、使用可能な属性のリストを文字列のようにして返す。
・パラメータをコントロールするためのポップアップGUIを.ctrl()メソッドで定義できる。
クラスの宣言
PyoObjectを親クラスとするヴォコーダと呼ばれる新しいクラスを作っていく。もうひとつ習慣づけたほうが良いのは、クラスを作る際に __doc__ 文字列を置くことだ。そうすることで、標準のPythonのhelp()関数で、君以外にオブジェクトのドキュメントを検索できるようにする。
class Vocoder(PyoObject): """ ヴォコーダのエフェクト ヴォコーダは、分析とシンセシスのシステムだ。エンコードの際に、入力はマルチ・バンドフィルタを通過させられ、バンドごとにエンベロープフィルタを通過させ、そしてエンベロープフォロワーから作ったコントロール信号は、デコーダとして機能する。デコーダは、これら(音量)のコントロール信号を(再)シンセシスに応じるフィルタに使用する。 親クラス: PyoObject パラメータ in1: PyoObject スペクトルエンベロープを生じさせる音源 in2: PyoObject フィルタバンクを刺激する音源 base: 浮動小数点またはPyoObject、どちらでも良い 基本周波数は、ノッチフィルタの周波数を計算するのに使う 標準は50 spread: 浮動小数点とPyoObject ノッチフィルタの周波数の拡散。標準は1.5 q: 浮動小数点とPyoObject フィルタのQ(バンド帯域幅の逆数)。標準は5 num: 整数、オプション ヴォコーダのバンド(ノッチフィルタ)の数、初期化時のみ利用可能。標準は20 メソッド: setIn1(x): "in1"属性をかえる setIn2(x): "in2"属性をかえる setBase(x): "base"属性をかえる setSpread(x): "spread"属性をかえる serQ(x): "q"属性をかえる 属性 in1: PyoObject。スペクトルペンべロープを生じさせる音源 in2: PyoObject。フィルタバンクを刺激する音源 base: 浮動小数点とPyoObject、基本周波数 spread: 浮動小数点またはPyoObject、ノッチフィルタの周波数を拡散 q: 浮動小数点とPyoObject、フィルタのQ こちらも参照して:BandSplit、Phaser 例: >>>s = Server().boot() >>>s.start() >>>z = SFPlayer(SNDS_PATH + "/transparent.aif", loop=True) >>>b=Noise() >>>lfo = Sine(freq=.05, mul=50, add=100) >>>voc = Vocoder(in1=a, in2=b, num=20, base=lfo, sprea=[1,2,1.22]).out() """
__init__ メソッド
これが、pyoの一般的なふるまいを注意深く見ていかなければならないところだ。これだけは覚えておかなければいけない、PyoObjectが別のPyoObjectを入力として受け入れるとき、self._base_objsという属性をさがすこと。この属性はオブジェクトの基本クラスのリストで、オブジェクト(内部では、Sine_baseオブジェクトして利用されるSineオブジェクト)のオーディオ出力信号と考えられる。getBaseObject()メソッドは、あたえられたPyoObjecttのために基本クラスのリストを返す。われわれは、処理の出力信号を生成するオブジェクトでgetBaseObject()を呼ぶだろう。
またオブジェクトの定義に2つの属性を加えなければならない。そう"mul"と"add"だ。属性"self._mul"と"self._add"は、親クラスによってハンドルされ、自動的に"self._base_objs"のオブジェクトに当てられる。
最終的に、"リストの展開"という機能を考えなければならない、引数にあたえられたリストに、オブジェクトの複数のインスタンスを作成させることができ、複数のオーディオストリームを管理する。2つの関数が、これを成し遂げるのに役立つ。
・convertArgsToLists(*args):リストに変えられた引数とリストの最大サイズを返す
・wrap(list, i):len(list)でラップアラウンドした"list"のポジション"i"の値を返す
コードはこうなる。
def __init__(self, in1, in2, base=50, spread=1.5, q=5, num=20, mul=1, add=0): #すべての未処理の引数の参照を保持する self._in1 = in1 self._in2 = in2 self._base = base self._spread = spread self._q = q self._num = num self._mul = mul self._add = add #ノッチフィルタの周波数のリスト self._patrials = [i+1 for i in range(self._num)] #サウンド入力のためにInputFaderを使って、音源を交換するときにクロスフェードさせる self._in1_fader = InputFader(in1) self._in2_fader = InputFader(in2) #"リストの展開"で、すべての引数をリストに変換する #convertArgToListは、引数の内の変数をリストのように、リストの最大サイズを加えて返す in1_fader, in2_fader, base, spread, q, mul, add, lmax = convertArgToLists(self._in1_fader, self._in2_fader, base, spread, q, mul, add) #Initのリストは、生成されたオブジェクトを絶えず見失わないようにする self._pows = [] self._bases = [] self._freqs = [] self._srcs = [] self._amps = [] self._excs = [] self._outs = [] #self._base_objsは外側から見れるオーディオ出力 #.play()、.out()、.stop()、そして.mix()メソッドはこのリスト上でふるまう。 #"mul"や"add"属性は、またリストのオブジェクトに適用される。 self._base_objs = [] #ループの各サイクルは、サウンドのモノラルのストリームをつくる。 for i in range(lmax): self._pows.append(Pow(self._partials, wrap(spread,i))) self._bases.append(Sig(wrap(base,i))) self._freqs.append(Clip(self._pows[-1] * self._bases[-1], 20, 20000)) self._srcs.append(Biquadx(wrap(in1_fader,i), freq=self._freqs[-1], q=wrap(q,i), type=2, stages=2)) self._amps.append(Follower(self._srcs[-1], freq=5, mul=wrap(q,i)*30)) self._excs.append(Biquadx(wrap(in2_fader,i), freq=self._freqs[-1], q=wrap(q,i), type=2, stages=2, mul=self._amps[-1])) #ここで、ヴォコーダの"num"個の帯域によって作られた、モノラルのすべてのサブストリームをミックスする self._outs.append(Mix(input=self._excs[-1], voices=1, mul=wrap(mul,i), add=wrap(add,i))) # getBaseObjects()メソッドは、Object_Baseのリストを返し、self._base_objsのリストを要求した。 self._base_objs.extend(self._outs[-1].getBaseObjects())
メソッドと属性の設定
さあ、すべての制御可能なパラメータのために、メソッドと属性を加えてゆこう。気をつけなければいけないのは、入力ソース(setIn1とsetIn2)を交換するのに、InputFaderオブジェクトのsetInput()メソッドを使うこと。このオブジェクトは、クロスフェード時間の引数を伴って、古いソースと新しいソースの間をクロスフェードを実装する。
def setIn1(self, x, fadetime=0.05): """ "in1"属性を置き換える パラメータ: x : PyoObject 処理するための新しい信号 fadetime : float, オプション 古い入力から新しい入力の間をクロスフェードする時間。標準は0.05 """ self._in1 = x self._in1_fader.setInput(x, fadetime) def setIn2(self, x, fadetime=0.05): """ "in2"属性を置き換える パラメータ: x : PyoObject 処理するための新しい信号 fadetime : float, オプション 古い入力から新しい入力の間をクロスフェードする時間。標準は0.05 """ self._in2 = x self._in2_fader.setInput(x, fadetime) def setBase(self, x): """ "base"属性を置き換える パラメータ: x : float or PyoObject 新しい`base`属性 """ self._base = x x, lmax = convertArgsToLists(x) [obj.setValue(wrap(x,i)) for i, obj in enumerate(self._bases)] def setSpread(self, x): """ "spread"属性を置き換える Parameters: x : float or PyoObject 新しい"spread"属性 """ self._spread = x x, lmax = convertArgsToLists(x) [obj.setExponent(wrap(x,i)) for i, obj in enumerate(self._pows)] def setQ(self, x): """ "q"属性を置き換える Parameters: x : float or PyoObject 新しい"q"属性 """ self._q = x x, lmax = convertArgsToLists(x) [obj.setMul(wrap(x,i)*30) for i, obj in enumerate(self._amps)] [obj.setQ(wrap(x,i)) for i, obj in enumerate(self._srcs)] [obj.setQ(wrap(x,i)) for i, obj in enumerate(self._excs)] @property def in1(self): return self._in1 @in1.setter def in1(self, x): self.setIn1(x) @property def in2(self): return self._in2 @in2.setter def in2(self, x): self.setIn2(x) @property def base(self): return self._base @base.setter def base(self, x): self.setBase(x) @property def spread(self): return self._spread @spread.setter def spread(self, x): self.setSpread(x) @property def q(self): return self._q @q.setter def q(self, x): self.setQ(x)
__dir__ メソッド
オブジェクトのすべての制御可能な属性のリストを返すには、__dir__をオーバーライドすることになる。ユーザーはdir(obj)を呼び出すことで、この値を取得できる。
def __dir__(self): return ["in1", "in2", "base", "spread", "q", "mul", "add"]
ctrl() メソッド
PyoObjectのctrl()メソッドは、オブジェクトのパラメータをコントロールするために、ポップアップGUIを使用する。スライダーの初期化は、SLMapオブジェクトのリストで行う。そこで、スライダーの範囲、スケールの種類、スライダーに連結された属性の名前、初期値を設定できる。ユーザーが書き忘れた場合に備え、標準で"map_list"を定義している。
def ctrl(self, map_list=None, title=None, wxnoserver=False): # PyoObjectに何も渡されなかった場合、オブジェクトに標準のmap_listを定義する # map_listは、コントロールウィンドウで利用可能な属性ごとに、 # 定義されたSLMapオブジェクトのリスト self._map_list = [SLMap(20., 250., "lin", "base", self._base), SLMap(0.5, 2., "lin", "spread", self._spread), SLMap(1., 50., "log", "q", self._q), SLMapMul(self._mul)] PyoObject.ctrl(self, map_list, title, wxnoserver)
.play()、.stop()、 そして.output()メソッドをオーバーライド
最後に、カレントオブジェクトにあるような、すべての内部PyoObjectsが結果的に、self._base_objにリストしたオブジェクトだけを管理させるように.play()、.stop()、そして.out()メソッドを上書きしたほうがいいかもしれない。引数の意味を理解したいなら、マニュアルにあるPyoObjectのこれらのメソッドの定義を参照して。
def play(self, dur=0, delay=0): dur, delay, lmax = convertArgsToLists(dur, delay) [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._pows)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._bases)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._freqs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._srcs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._amps)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._excs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._outs)] self._base_objs = [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)] return self def stop(self): [obj.stop() for obj in self._pows] [obj.stop() for obj in self._bases] [obj.stop() for obj in self._freqs] [obj.stop() for obj in self._srcs] [obj.stop() for obj in self._amps] [obj.stop() for obj in self._excs] [obj.stop() for obj in self._outs] [obj.stop() for obj in self._base_objs] return self def out(self, chnl=0, inc=1, dur=0, delay=0): dur, delay, lmax = convertArgsToLists(dur, delay) [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._pows)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._bases)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._freqs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._srcs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._amps)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._excs)] [obj.play(wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._outs)] if type(chnl) == ListType: self._base_objs = [obj.out(wrap(chnl,i), wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)] else: if chnl < 0: self._base_objs = [obj.out(i*inc, wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(random.sample(self._base_objs, len(self._base_objs)))] else: self._base_objs = [obj.out(chnl+i*inc, wrap(dur,i), wrap(delay,i)) for i, obj in enumerate(self._base_objs)] return self
以上で完成です。サウンド処理のためホンモノのオブジェクトができました。もちろんピュアpythonで書いたオブジェクトでは、CPU使用率に若干のスパイクがかかってしまうかもしれない、だから次のステップではC言語で書くことにする。Cでのpyoオブジェクトの作り方チュートリアルはもうじき公開するよ。
Androidでシンプルなディレイ・エフェクト
この文章は前回と同様、Victor Lazzariniのブログを翻訳したものです。前回、マイクからの音をスピーカーで鳴らしました。今回はその間にエフェクトを挟んで音を変えます。
>>>それでは本文
前回の私の記事で書いたコードだが、音声処理アプリケーションをどう書いたらいいかと感じている人もいるかも知れない。この点について、明らかにしていく。実際、再利用を念頭において、OpenSLモジュールを設計しておいた。だから、モジュールに触れることなく、君のプロジェクトにそのまま加えればいい。
前回のブログから、NDKプロジェクトの置き方について知ってるはずです。君はそれをコピーし、コードを修正し始める。opensl_example.cとopensl_example.hファイルを編集することだけすればいい。このファイルにいくつかの処理能力を追加していく。
くし形フィルタをベースにした、エコー効果を追加しよう。そのためにインフラを構築しよう。データ構造(データメンバ)とエコーを操作する関数(メソッド)を基礎にして、C言語の簡単なクラスを作る。以下のコードを、C言語のソースの一番上に加えます。
typedef struct delayline_t { float *delay; // delayline int size; // length in samples int rp; // read pointer float fdb; // feedback amount } DELAYLINE; DELAYLINE *delayline_create(float delay, float fdb) { // allocate memory and set feedback parameter DELAYLINE *p = (DELAYLINE *) calloc(sizeof(float), delay*SR); p->fdb = fdb > 0.f ? (fdb < 1.f ? fdb : 0.99999999f) : 0.f ; return p; } void delayline_process(DELAYLINE *p,float *buffer, int size) { // process the delay, replacing the buffer float out, *delay = p->delay, fdb = p->fdb; int i, dsize = p->size, *rp = &(p->rp); for(i = 0; i < size; i++){ out = delay[*rp]; p->delay[(*rp)++] = buffer[i] + out*fdb; if(*rp == dsize) *rp = 0; buffer[i] = out; } }
決まったところにこいつを置いて、やるべきことは、ディレイラインを使うためにメインプロセスを修正することだけ。メイン処理関数(start_processからmain_processにリネーム)に2つの引数を加えることで、Javaのコードからフィードバックとディレイタイムの量をセットできるようにする。
void main_process(float delay, float fdb) { OPENSL_STREAM *p; int samps, i; float buffer[VECSAMPS]; DELAYLINE *d; p = android_OpenAudioDevice(SR,1,1,BUFFERFRAMES); if(p == NULL) return; d = delayline_create(delay, fdb); if(d == NULL) { android_CloseAudioDevice(p); return; } on = 1; while(on) { samps = android_AudioIn(p,buffer,VECSAMPS); delayline_process(d,buffer,samps); android_AudioOut(p,buffer,samps); } android_CloseAudioDevice(p); }
もう、やり残しているのは2つだけで、opensl_example.hヘッダーファイルを編集し、start_process()をmain_process(float delay, float fdb)のプロトタイプで置き換える。
#ifdef __cplusplus extern "C" { #endif void main_process(float delay, float fdb); void stop_process(); #ifdef __cplusplus }; #endif
そして、この新しい関数を呼ぶためにJavaコードを修正する。
t = new Thread() { public void run() { setPriority(Thread.MAX_PRIORITY); opensl_example.main_process(0.5f, 0.7f); } };
これでいいだろう!。
>>>以上本文、以下修正など
(3/10修正メンバ)変数ー>データメンバ