2017年1月28日土曜日

5. Introduction

フィルターはDSPアプリの重要なツールです。
そしてフィジカルモデリングでの中心的な役割であり,サウンドマニピュレーションとなります。

これまでに信号をスムーズに処理するために用いたSmoothや「Universal Pitch Tracker(2.2.4)」で解析箇所に用いられたハイパスフィルター,ローパスフィルターなどの多くのフィルターを見てきました。

これから他にもサウンドを加工するために用いるフィルターを見ていきます。
例えば「Auto-Wha」です。そこで,不正確な箇所を解決するためにはどうするかを5.1で見ていきます。
また,より直接的な使い方として「Signal Sideband Modulation」を5.2で扱います。

さらに「Adaptive FM Synthesis」を扱うことで,ヘテロダイニングフィルターを5.3で学びます。
最後には,5.4にてフィジカルモデリングを「circular spazialisator」で扱います。

そして,いつものように5.5において計算コストを考え,5.6で簡潔なまとめをおこない,この章の終わりとします。

2017年1月15日日曜日

2.2.4 Complex sounds Pitch Tracker

複雑な信号のピッチ判定


Sin波だけのピッチ判定については先ほど紹介した通りです。
次に,複数の波が重なった音に対して,この処理を行ってみましょう。

Fig.2.9.をみてください。


[Fig.2.9.]

100Hzの3和音の信号です。
(2つの段階が表れることが分かります)
どの段階でも,波形が上昇する部分で,
3つの0の値を持つことが分かります。
それぞれの0の値の箇所にギリシャ文字をふっています。

もし,この波形を先のピッチトラッカーで分析したなら,
3つの重要なゼロの値をどの段階でも判定するでしょう。
よって,計算結果は,100Hzではなく,300Hzになるはずです。

解決方法として,ローパスフィルターを用いることが挙げられます。
このフィルターは波形を分析して,
周波数を遮断しますが,これがピッチを判定する手法として使えます。

例えば,「first-order Butterworth」を使ってみます。
このアルゴリズムを,今回の波形に用いてみましょう。


[1st step]


まず,先の信号を分析して,だいたい300Hzだということを計算しておきます。
フィルターを適用することで,次のように波形を遮断できます。
Fig.2.10.をみてください。


[Figure 2.10: First step:]
100Hzの3和音の波形に,300Hzのローパスフィルターを当てたところ,
重要な0の値はたった2つになりました。
フィルターのおかげで波形が滑らかになったことがわかります。

[2nd step]

滑らかになった新しい波形を再び Pitch Tracker で分析します。
フィルターの滑らかにする効果によって,
0の値は排除されていきます。

そして新しく返ってきた値は,
全体の数に依存しますが,
200Hz付近になるはずです(正確には200Hzです)。

次に,ローパスフィルターをこの新しい値にセットして,周波数をカットします。
Fig.2.11.をみてください。


[Figure 2.11: Second step:]
100Hzの3和音の波形を,200Hzのローパスフィルターに通した結果です。
重要な0の値はたった1つだけになりました。

[3rd step]

重要な0の値はそれぞれの段階で1つだけになりました。
よって, sinusoid pitch tracker は正しい値を返すことになります。

しかしながら,
安定した結果を計算するためには,もう1つ試行が必要になります。
ローパスフィルターに100Hzをセットして,波形を編集します。

これで,これ以上0の値が変わらないことが明らかです。
そして, pitch tracker は正しい周波数値を返し続けることができます。
Fig.2.12.を見てください。


[Figure 2.12: Third step: ]
100Hzのローパスフィルターで,100Hzの3和音の波形を分析した結果です。
安定した結果を出力するようになります。
(次の段階は,信号が同じである限り変わらないということになります)


[Timing overview. ]


このように,すでに2つのステップを経ることで,
pitch trackerは,毎回の計算で値を更新していき,
正しいピッチを判定することができるとわかりました。
この判別は 0.019 * a 秒以下で実行できます。

実際に,最初に何もフィルターを通さなかった場合の解析区域の時間は 1/300秒でした。
よって, a/300 秒後に値が返ってくることになります。

2つ目のステップはだいたい1/200秒です。
よって,値が返ってくるのは a/200 秒後になります。

そして,値を決定する3つめの段階は1/100秒です。
よって,値が返されるのは a / 100 秒後です。

これら全ての時間を合計したら,
どれくらいかかるか,わかると思います。

これは,そんなに早い処理ではありません。

実際,aの値は10ほどになるでしょう。
(2.2.5.をみてください)
そうすると,処理にかかる時間はだいたい0.2秒ぐらいだと分かります。
それは,75BPMの16分音符の間隔です。
音楽ではよくある間隔になります。


本当に実行されるコードは,
あなたがみてきたように,
Sinusoid Pitch Tracker が解析を始める前に,
100Hzの周波数をカットオフしたフィルターが適応されます。

よって,実際に実行する際には,
たった 0.01 * a秒で解析されるだろうと考えられます。
これは先ほど計算した時間の半分です。


[Changing signal. ]


では,信号のピッチが変化するとどうなるでしょうか。
もしも,下がった場合,
同じように少しの計算を経て,低い値を検出できるでしょう。


もし,ピッチが上がった場合,
新しい波形の基本周波数が,カットオフする周波数よりも高いため,
フィルターは基本周波数の波形を遮断してしまうのではないでしょうか。

しかし,実際には,フィルターはそうは働かず,
信号の”Fundamental’s zero-crossing rate”情報には影響しません。

例のFig.2.13.をみてください。


[Figure 2.13: ]
入力された信号により,ピッチが上がりました。
3和音の波が今200Hzになっています。
そして,ローパスフィルターは古い100Hzの値にセットされています。
情報に損失はなく「sinusoid pitch tracker」は新しい基本周波数を捉えることができています。


[Faust code.]

FAUSTでのこの新しい”pitch tracker”のコードは,
“sinusoid”のものと比較して,
[proces]の定義とライブラリの追加があるだけです。


====================

import("math.lib");
import("filter.lib");
SH(trig,x) = (*(1 - trig) + x * trig) ~ _;
a = hslider("n cycles", 1, 1, 100, 1);

Pitch(a,x) = a * SR / max(M, 1) - a * SR * ( M == 0)
with {
  U = (x' < 0) & ( x >= 0);
  V = +(U) ~ %(int(a));
  W = U & (V == a);
  N = (+(1) : *(1 - W)) ~_;
  M = SH(N == 0, N' + 1);
  };
process=dcblockerat(80) : (lowpass1 : Pitch(a)) ~ max(100);

====================


“filter.lib”ライブラリを,フィルターを用いるために定義し,付け加えています。
その後,[process]の定義において,まず[dcblockerat]を呼び出しました。
これは,ハイパスフィルターで,カットオフする値を80Hzに指定しています。

これはDCオフセットの信号を除去し,
ピッチ判定の影響となるノイズを除去するために機能します。

その後,用いている[lowpass1]フィルターは,
バターワースフィルタのローパスフィルターのことです。

これは[dcblockeart]から信号を受け取り,
Pitch関数から周波数を除去しています。

また[~]演算子を付け加えることで,
入力された値を100の値を比較して選択された値を,
ピッチ関数は max関数を経て値を出力します。

適用したフィルターはカットオフした100Hz以下の値を受け取らないということに気をつけて下さい。

これが動作すること使いやすくなります。
フィルターの初期値は0Hzにもかかわらず,
値がこれに到達する前に,直接出力することができるからです。

最後にSVGダイアグラムをFig.2.14.をみて確認してください。



[Figure 2.14:]
“Universal Pitch Tracker”の[process]のブロックダイアグラムです。


筆者注:
1)OS X 10.11で試しているのですが,lowpass1が見つからないとのエラーがでます。
  別環境で動作確認する必要あり。
  ERROR : undefined symbol : lowpass1
  2行目で"filter.lib"を読み込んでいるはずなのですが。

2)OSX 10.12ではprocess自体が見つからないとのエラーになります。
  対処法がわかり次第更新します。

2017年1月12日木曜日

2.2.3 How to set “a” inside a code

Faustのバージョンが0.9.9.4j-par以上のものを用いているか,注意してください。
もし,コード内に定数を入力した際に,
“Pitch Tracker”は正しく動きません。
常に0を返すと思います。

生成されたC++のコードを見れば,その理由がわかると思います。
先に用いたPitch Trackerのコードを次のように修正しました。


process = Pitch(8), Pitch(a);


この”Process”は,2つのPitch関数を並行して計算します。
1つめは引数に8をとり,2つ目はスライダーの値を引数に持ちます。

そうすると,2つ目の関数だけが正しく動作します。
図2.7にC++が生成したコードを掲載しました。

for ( int i = 0; i < count; i++){
    […]
    output0[i] = (fSamplingFreq * (( 8 / max(iRec0[0], 1)) - (8 *(iRec0[0] === 0))));
    […]
    output1[i] = (fSlow2 * ((1.0f / max(iRec3[0], 1)) - (iRec3[0] == 0)));
    […]
}


Forループの中で,サンプリングレートが計算に使われているのが分かります。

コード内には2つのoutputがあります。
1つ目がoutput0[1]で,これは Pitch(8) の結果に相当します。
(8をコード内に見つけられるでしょうか?)

output1[i]は2つ目のoutputで,これが,Pitch(a)の結果を表しています。
これだけが,正しく動くものです。

1.0fという数値がその理由です。
この箇所は,output0のコードの8の部分を置き換えたところになります。

C++では,1.0fは浮動小数点を表します。
よって,全ての計算結果は浮動小数点になります。
(それが正しい結果です。)

1行目は反対に,整数値(int)で扱われています。
そのため,出力される結果が間違ってしまうのです。

また,今回は8の値を置き換えましたが,
Faustの関数では,浮動小数点の係数は1.0と指定するだけで十分です。
よって,単純に8.0と入力しても,最初の関数は正しく動きます。

process = Pitch(8 * 1.0), Pitch(a);
process = Pitch(8 : float), Pitch(a);
process = Pitch(8.0), Pitch(a);

これらの行は全て,同じ,正しいC++のコードを生成します。

for ( int i = 0; i < count; i++){
    […]
    output0[i] = (fSamplingFreq * (( 8.0f / max(iRec0[0], 1)) - (8.0f * (iRec0[0] === 0))));
    […]
    output1[i] = (fSlow2 * ((1.0f / max(iRec3[0], 1)) - (iRec3[0] === 0 )));
    […]
}

Figure 2.8:
  正しく生成されたC++のコード。
  両方の出力が正しく動作しています。
  1つ目のoutputが8の値の代わりに8.0f(浮動小数点)を用いているのがわかると思います。

この注意点は,3.5で扱うAdaptive FM synthesis でまた出てきます。

2016年5月22日日曜日

2.2.2 Sinusoid Pitch Tracker - several periods measurement

先ほど作成したsinusoid pitch trackerには、問題があります。

周波数の計算に用いた M(xt)という関数ですが、
これはサンプルごとにカウントするもので、
Time Quantization(時間量子化)のために、
明らかないくつかのエラーが存在するということです。

解決方法として、いくつかに分けられたを範囲をカウントし、
その後、この結果を想定される間隔の数で割るというものです。


幸いなことに、
普通の音(ピッチ)では、音の高さはこの小さい間隔では変化がありません。
それでは、想定される間隔の数を a として、
この値をスライダーに割り当ててみましょう。


V 関数


先ほどは、N (x) 関数を用いて、
カウンターを  U(x) = 1 というイベントが起こるたびに、
(例えば、信号が上昇して0の値になるときに)
リセットしました。

そして、 今回は、N(x t)関数を、 a U(xt ) = 0 となる後のときだけ、
0にリセットしなければなりません。

もちろん、その U(x t) = 0 イベントのために、カウンターが必要です。

このカウンターを V (x t)と呼びます。
それでは、FAUST での定義を見てみましょう。


V(a, x) = +( U (x ) ) ~ % (int(a));


カウンターの基本形は次のようになります。


count = +(1) ~ _;


+(1)  の代わりに、 + U(x)) を用いています。
これは 0 の間 1 を返します。
よって、毎サンプルごとに1増加するカウンターの代わりに、
U(x t) = 0 となるごとに1増加するカウンターを用いています。

modulo 関数(%)は、int(a)の値になったときに、
0にリセットを行う関数です。

想定した間隔のサンプリング数である a という値は、すでに整数なので、
int(値を整数にして返す) 関数は、使う価値がないように思えますが、
しかしながら、コンパイラはこの背景を知らないので、
int (a ) とはっきり述べる必要があります。


V (a, x t )関数が毎 U(x t ) = 0 となるごとに1ずつ増加する関数だとすると、
a の値に達したときに値は再び0にもどります。



W 関数


さて、私たちは、N(x t)関数の部分を、書き換えていかなければなりません。
なぜなら、 a U(x t)= 1 かつ V(a, x t )=a という状態になるまで
値が上げ続けるのを止めないようにしないといけないからです。

例えば、信号が0の値をとる間、
最後の V (a, x t) 値がリセットさせるまで、
(N (x t) 値も同じです)
それらの0の a が正しく生成されなければなりません。

新しく作る関数 はシンプルなものです。
W(a, x t)と表し、この要求された2つの状態を表しています。




FAUST では次のように表されます。


W(a,x) = (U(x) == 1) & (V(a,x) == a);




FAUSTでは真は1、偽は0が返るために、簡略化したコードは次のようになります。


W(a,x) = U(x) & (V(a,x) == a);




最終調整


さて、それでは、U(x)関数をW(a, xt)関数を用いることで、
N(xt)関数を新しくしましょう。

N(a,x) = (+(1) : *(1 - W(a,x))) ~ _;



最後に、Pitch(a, x)関数にて、
a を M(a, xt)関数で除算し、また0の時にnullとなるようにもしておきましょう。


Pitch(a,x) = a * SR / max(M(a,x),1) - a * SR * (M == 0);


コードの全体は次のようになります。



  1 import("math.lib");
  2 
  3 SH(trig,x) = (*(1 - trig) + x * trig) ~ _;
  4 a = hslider("n cycles", 1, 1, 100, 1);
  5 
  6 Pitch(a,x)= a * SR / max(M,1) - a * SR * (M==0)
  7 with {
  8   U = (x' < 0) & (x>=0);
  9   V = +(U) ~ %(int(a));
 10   W = U & (V==a);
 11   N = (+(1) : *(1-W)) ~_;
 12   M = SH(N==0, N'+1);
 13   };
 14 process = Pitch(a);


a の値を高くすれば、正確さもより増していきます。
しかし、その分、解析にかかる時間にも遅れが生じることを忘れないでください。


SVGダイアグラム図



DSP ファイル実行 



2.2.1 Sinusoid Pitch Tracker - one period measurement

では、周波数解析について、
まずはシンプルな機能を持つものを作っていきましょう。
後々には、より長い周期の解析もできるようになるはずです。

-----

U 関数

まずこれらの機能を作るにあたって、
0の値をつきとめる必要があります。

より最適なものを選択するために、
信号が上昇している部分の0の値だけを取得します。
(全ての0ではありません)

サイン波では、
波が上昇を描く際には2つの0の値をとります。

この関数を U ( x)関数と呼びます。
xt はt 地点での信号を表します。

-----





最初に書いた、 xt >= 0 and not x= 0 という状況に注意してください。
別々の領域(状態)で、この関数がうまく動くようにしていかなければなりません。
xt = 0 のときに、即座に正確な値を取得できる保証はありません。

このように、2つの正負に位置する場合それぞれにて値を探すことで、
この間に、少なくともひとつの0の値が生じることがわかるでしょう。


この U 関数を FAUSTでは次のように表せます。




 ' は1サンプル遅れているところを表しています。
よって、U(x)が U t (x) ならば、
x' は xt-1 になります。

& 演算子は論理ANDのことで、
2つの状態が True(1)ならば、1( True )を返し、
そうでなければ、0( False ) を返します。

U(xt)関数については、Fig.2.3. のグラフや
Fig.2.4.の .svgブロックダイアグラムからもわかるでしょう。



Fig. 2.3.
100Hzのサイン波(青線)のとき、赤の線がU(xt)関数の値です。
横軸は時間軸を表しています。
U(xt)関数が1の値をとる間に、
サイン波が1周期を刻んでいることがわかります。

SVGブロックダイアグラム(Fig.2.4.)





-----


N 関数


U(xt)関数が2つの1の値を刻む間に、
どれくらいの時間がかかるか測ることに、
興味が持てたと思います。

よって、次はその時間をカウントする関数が必要ですね。

もちろん、デジタル値で、サンプル数を数えていくことになります。

この関数を N ( xt) 関数と呼び、
もし、 U (tx) = 1 ならば、0 の値をとり、
そうでなければ、値は増え続けます。

このようにして、
U(tx)が最後に1を刻んだ後のサンプル数を数えていきます。





Fig.2.5

U(xt) 関数が赤を示し、緑線はそれぞれの N(xt)関数を示しています。
横軸は時間軸(サンプル数)を表し、縦軸は取得したサンプル数を表しています。

U(xt)関数が2つの1の値をとる間の時間は、
0になる直前のN(xt)の値に1を加算することで、
読み取ることができます。


N (xt) 関数を、FAUST では、
U(xt) が1となるときに0となる値を掛け合わせた、
普通のカウンターとして定義しています。


SVG ブロックダイアグラム




M 関数


1周期のサンプル数をカウントすることの最初の課題は、
どのようにして、N(x) = 0となる前の値のN(x)に1を加算するかということです。

この問題は、 S & H オブジェクトを用いれば簡単にクリアできます。

Triggerが N(xt) = 0 のときに、
サンプルを行う関数が N(x t-1) + 1 を取得するようにイベントを設定しておけば、
前の値のN(x)の値に1を加算した数を取得できます。

よって、もし、 N(x t) = 0 なら、
次にNの値が 0 の値をとるまでN ( xt - 1 ) の値を、保持させておけば良いのです。

この新しい関数を M (x t )関数と呼びます。



 SH関数の最初の引数は トリガーになっているということを思い出してください。
そして二つ目の引数は、取得・保持される信号になります。

トリガーの引数には == 演算子を用いました。
これは、 N (x t ) =0となったときに1を返し、
それ以外のときには、0を返すというものです。


Pitch 関数


さて、それでは、入力した信号を M(x t)関数に入れてみましょう。
もし代わりに、周波数を取得したいならば、
サンプリングレートを M(x t )の値で割りましょう。


SR は保持しているサンプリングレートを返す関数です。
そしてこれは、math.lib ライブラリをインポートすることで使用できます。

0で割ることを避けるために、
例えば、 max ( M(xt), 1)のように
分母に max 関数を用いています。

よって、M(xt)=0 のときでも、
max関数は1の値を選んで返してきます。
(この場合では、1 > M(xt) となるので)

そうすることで、分数はエラーなく定義することができます。

また、これは演算を始めたときにのみ必要なことですが、
値を初期化しておくために、
Pitch(xt) 関数の値をSR/1のままにしておくより、
null としておきましょう。

そのため、もし、M(x t) = 0 ならば、 SRを引くようにしておけば大丈夫です。

結果として FAUSTのコードは次のようになります。



コード全体


では最後に Sinusolid Pitch Tracker のソースコードを見てみましょう。
次の.dsp コードを実行してみてください。


import("math.lib");
SH(trig,x) = (*(1 - trig) + x * trig) ~ _;

U(x) = (x' < 0) & (x >= 0);
N(x) = (+(1) : *(1 - U(x))) ~ _;
M(x) = SH(N(x) == 0, N(x)' + 1);
Pitch(x) = SR / max(M(x), 1) - SR * (M(x) == 0);
process = Pitch;



process は Pitch関数を引数なしで呼び出しています。
必要とされる x 引数は、入力信号の値になります。

では、同じコードを新しい構文で書いてみましょう。


import("math.lib");

SH(trig,x) = (*(1 - trig) + x * trig) ~_;
Pitch(x)=SR/max(M,1)-SR*(M==0)
with{
  U = (x' < 0) & (x >= 0);
  N = (+(1) : *(1 - U)) ~_;
  M = SH(N == 0, N' + 1);
};
process = Pitch;



違いに気づいたでしょうか。
今回は  with { ... } という構文を用いました。

主要な関数を定義した後、セミコロンを文末に書かず、
with {...} 構文を用い、そしてその構文の最後に、
セミコロンを書きました。


with {... }構文の中には、
必要とする全ての他の関数を定義しています。
またその関数は、以前のコードのように
U(x)  といった引数を取らず、 U だけ宣言するといった書き方をしています。

with を用いた書き方には2つの利点があります。

1つは、メイン関数の引数をwith 構文の中に引き継がせることができ、
with 構文内では毎回引数に用いる必要がなくなることです。

もう1つは、with内部で宣言された関数は、
withの外では宣言されていないということです。
よって、ユーザーは同じ名前の関数を別の場所で、
構文エラーになることなく用いることができます。

2.2 Pitch tracker

'pitch tracker' のゴールはすぐに入力された信号のピッチを返すことです。
FFTやautocorrelationといった様々な手法があります。

残念ながら、FAUSTでは、これらの手法は計算することができません。
なぜなら、正確には、ユーザーがサンプルごとにそれらを計算すると、
必要でないにもかかわらず、
かなり膨大な処理コストが結果としてかかるからです。
将来的には、新しいバージョンの FAUSTではこの制限はなくなるでしょう。


現状では、つまり、処理コストの少なく、
即座に信号のzero-crossing rate を解析して、
ピッチを計算するpitch trackerを使っていくことになります。

実際、サイン曲線の波は、
1度の周期で3つの0を必ず通ります。
(1つめは最初の0で、2つめは正の値から負の値に移る際の0、
そして、最後に0を、また次の新しい波が始まる0の値をとります。)



Fig.2.2
100Hz のサイン波で、時間は横軸になります。
1周期には0.01秒かかり、
毎回3度0の値をとることがわかります。


よって、サイン曲線を用いる間、
私たちは3つの0を数え、
そして経過したサンプル数を数えればよいのです。

その後、簡単に関連するサイン波の周波数を得ることができます。

しかし、入力値がより複雑な波形だった場合には、どうでしょうか。

波形は毎周期、より多くの0の値をとるので、
より多くの0をカウントしなければならないでしょう。

信号にローパスフィルターを使えば良いかもしれませんが、
しかし、この解決方法は後にまわしましょう。


まずは、サイン波の周波数分析をする関数を作成してみましょう。

2.1 S&H


2.1 S&H

"Sample & Hold" は 、
指定されたタイミングで信号を保持出力し(sample)、
その信号を新しい指令が来るまで出力し続ける(hold)関数です。

入力された新しい信号はすぐに出力されるようになります。
これはより複雑なものを開発していくのに不可欠な機能です。

それでは FAUST のソースコードを見てみましょう。


  1 but = button("Hold!") ;
  2 SH(trig,x) = (*(1 - trig) + x * trig) ~ _;
  3 process = SH(but) ; 



UIは非常に単純です。


"Hold!" とラベルされたボタンがあるだけです。
これは `but` 変数に格納されています。

もし、ボタンが押されると、ボタンは1、または0を返します。
それらが、`but` 変数のもつ値です。

その後、`SH` 関数が宣言されています。
最初の引数である、`trig` は、イベントを始める信号です。

第二引数の x 秒の間、サンプルが続けられます。


そして、 `process` にて `SH(but)` 関数が実行されています。
独特な引数を持っていることがわかります。
(イベントを起こす信号を SH 関数では、`trig`と呼んでいます)

割り当てがない間は、待ち受け状態になります。

`but`、ボタンの値は、SH関数が信号を受け入れ始める信号を送る役割を持ちます。



SH(trig, x )関数について、もう少し説明します。

もし、 trig = 1 ならば、 SH は x  と同じ値になり、
そうでなければ、その前の値を持ちます。
(t はその時の時間を表しています。)




この関数はこのように書き表せます。



`trig` が1または0の値を持ち続ける間、
1を持っていた場合、
最初に加えられるのは空白で、そして、2つめに与えられた値を残していきます。
0ならば、逆のことを行います。

もし、`trig`に間の値をもたせたならば、2つの加えられた値は凸結合となります。
例えば、SH t-1 (trig, x) と x の間では、線形の補完が行われます。

しかし今、不幸なことに、FAUST では、この書き方は意味がなく、
ユーザーは関数をこのように呼び出すことはできません。


ですが、"~"演算子を用いて、
右の出力を左の入力に送るといったことができます。
もちろん、 1 sampleの遅れが生じます。

SH関数を書いた行を見てみると、
先に表したSH t- 1 という部分を、
同じ値で終わるものにつなげていることが想像できます。

次のような具合です。





矢印は、ループする線を表しています。
この表現は、とてもFAUST Likeで、
時間の t に注意する必要がないばかりか、
FAUST の表現に置き換えてしまいます。

このように、正確なコードに適することができます。

SH( trig, x ) = ( * ( 1 - trig ) + x * trig ) ~ _;

それでは最後に、.svg ブロックダイアグラムを見てみましょう(fig2.1)。
"Looping Cable"が、Outputのケーブルからきて、
内部の * ブロックに戻っていくのがわかります。