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オブジェクトの作り方チュートリアルはもうじき公開するよ。