Music

レビュー : Fire and Frost Pattern / Andreas Bick

遅ればせながら新年明けましておめでとうございます。
ciqiのnakanoです。

前回まではストイックなSCブログをotonotrix君が書いてくれたので
私はCDレビューというか紹介的なのを半気楽にいってみようかと思います。

今回は割りとマニアックなフィールドレコーディング的作品です。
(この作品レビューしてるの、恐らく日本でここだけかも汗)
Fire and Frost Pattern  / Andreas Bick
fire_and_frost_pattern
試聴はこちらから

この作品を作ったAndreas Bick氏は1964年生まれドイツ出身の映画作曲家、
サウンドアーティスト、ラジオプロデューサーです。
で、このアルバムはタイトル名の通り、Fire と Frostをコンセプトにして作られており、様々な場所、方法での録音を組み合わせた作品となっています。
Fire_patternのトラックはバヌアツにあるヤスール山(世界で最も火口に近づける活火山)、コスタリカにある成層火山であるアレナル火山、シチリア島東部にある活火山、エトナ火山の噴火音、また、間欠泉(温泉みたいなもの)の音はアイスランドで録音されています。
一部、pyrophoneやwhoosh bottleなどの人工音を使っていて単なるフィールドレコーディングだけじゃなくて興味深かったです。

pyrophoneってこんな感じ。fire_organとも言うみたい。

whoosh bottleってこんな感じ。理科の実験思い出したり。


Frost_patternの方は南極海での巨大な氷山の衝突から起こる音、地震波音や
ベルリンにある凍結湖での氷床の音の波の分散などを録音。
凍った湖は気温の大きな変動時に氷が膨張したり収縮し、生じたテンションが、亀裂を引き起こし、ノイズを発することが知られているそうです。
また、雪が降る音をアルミホイルを敷いて録音するという変わった録音や、つららの砕ける音、雪解け水のしたたる音なども録音されていて面白かったです。
一聴した感じ、個人的には火と氷のイメージが少し変わった気がします。
それは一般的に火と氷って相反するもの(動と静みたいな)だというイメージがありますが、この作品の火山の噴火、氷山の衝突などから、自然の荒々しさ、ダイナミクスが全面的に伝わってきて、両者はやはり大自然の中のもので同種であると認識を改めさせられた気がします。

音響的に一番驚愕したのが凍結湖に穴をあけ、湖下を水中マイクによって録音した音ですね。ここで作品内で使われてる音が聴けますしダウンロードもできます。
まるでシンセをグリッサンドしたような音が鳴っていて、
スターウォーズみたいで面白いですね。なによりこのステレオ感!
これは、ここ数年でのビックリ音ランキングNo.1ですね。
いやはや、まだまだ自然界にも面白い音はたくさん溢れていると痛感しました。

ちなみにこの作品は5.1サラウンドにも対応しているファイルもセットで入っています。
5.1chの方がやはり音場も広いし、分離がよく作品をより楽しめました。ただ、こういった作品なのに、ファイルそのものは44.1khz,16bitのwav形式で少し残念な気もしました。

おまけ。ice fishing noise! おじさんの笑顔が良いw


最後にフィールドレコーディングによる作品というとそのままの録音を作品とする、あるいはそれをより加工して作品にするといった作品が多いですが、この作品は素材そのものに過度なエフェクト処理をせず、様々な録音法による素材と人工音の素材を活かし、それを巧妙に配置して1つの作品にしている様に思いました。また、個人的に、耳では聞こえない音や意識していない音なども聴こうとする姿勢次第でより音を探求できるんだという事をこの作品から学んだ気がします。

と、いうことで美味しい肉的な音楽もいいですが、たまにはこんな感じの音楽を食してみるのはいかがでしょうか?

SuperColliderコードリーディング 「Stacked Chord Loop by umbrella_process」 その2

どうも、ciqiの片一方のotonotrixです。

前半に引き続き、umbrella_processさんのコードを分析します。

コードはここから

今回は、sequenceです。SynthDefで作った楽器をどのように演奏するのかということです。
(SynthDefについての分析のページはこちら)

前回より少し長めですが、これで一つのまとまりです。
ArrayオブジェクトやTaskオブジェクト、loopメソッド等はよく使われるので、Helpやexampleでその機能を確認してください。
(条件文の範囲の明確化のため、日本語のコメントを追加してあります)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
(
// Stacked Chord Loop
 
var bpm = 80;
var nodeArray = Array.new();
var chordArray = Array.new();
var scaleArray = #[ 12, 14, 4, 5, 7, 9, 11];
var weightArray = #[ 0.1,0.05, 0.3, 0.1,0.05, 0.2, 0.2];
/**
* sequencers
*/
var sequencer = Task({
var note, node;
loop({
     if( 0.15.coin, {
         note = scaleArray.wchoose(weightArray) + 51;
         if( 0.2.coin, {
             note = note + 12;
         });
         while( { chordArray.includes(note) }, {
             note = scaleArray.wchoose(weightArray) + 51;
         });   // while文の終わり
         chordArray = chordArray.addFirst(note);
         node = Synth.tail( nil, "up-piano-20", [
              \freq, note.midicps,
              \amp, (0.05.rand2 + 0.1)*0.5,
              \pan, 0.8.rand2,
              \downRate, 10000.rand2 + 15000,
              \mix, 0.5.rand2 + 0.5
         ]);
         nodeArray = nodeArray.addFirst(node);
         if( chordArray.size > 5, {
             chordArray.pop;
             s.sendMsg( "/n_set", nodeArray.pop.nodeID, \gate, 0.0 );
      });
      chordArray.postln;
    }); // 最初のif文の終わり
    1.wait;
});
}, TempoClock(bpm/60));
 
/**
* music start
*/
SystemClock.sched(0,{sequencer.start});
)


12行目からの、Taskの内部にいきましょう。

簡単にすると、

1
2
3
4
5
6
Task( { 
       loop({処理;
             1.wait
           });
      },TempoClock(bpm/60)
     )


ですね。
1.waitは秒ではなく、clockで設定されたテンポのことで、1は四分音符一つ分のことです。

TempoClockのテンポは、BPM 60=1とした比ですので、使いたいテンポがあればそのテンポを60で割った値をTempoClockに渡せばいいわけですね。コードの通りTempoClock(bpm/60)と書いておけば、テンポ入力しやすいので便利です。

このコードの重要な部分は、
14行目の、loop( { 処理; 1.wait} )であり、メソッドの名の通り、loop内部の関数を延々evaluateし続けるということです。仮にBPM 60の場合では、「処理 → 1秒(四分音符分一つ分)待つ →処理 →1秒待つ…」が延々と続きます。

loop内部の処理がコード全体のコアといってもよいでしょう。
このことを念頭に、loop内部の処理を展開していきましょう。

15行目のifを見てみましょう。このifはloop処理の全体に渡ってますね。
簡潔化して、

1
2
3
4
loop({ if(0.15.coin~
         ~); 
     1.wait;
    })


ということですね。
このifはtrue処理しかありません。falseだとif内部の処理はまるまる無視されますので、何もせずに1.waitに行き、またloopの先頭から処理が始まります。ifの内部に0.15.coinというのがありますが、15%の確率でtrue(85%の確率でfalse)を返します。もちろん1.coinだと100% trueですね。
falseが無いので、15%の確率でifの内部が実行されるといえます。
(他のifもfalse処理がありませんね)

16行目、

note = scaleArray.wchoose(weightArray) + 51;

を見てみましょう。

wchooseメソッドというのは、weight chooseの略で、chooseメソッドは全くのランダムで配列の要素が選ばれるのに対し、wchooseは確率に偏りを持たせるメソッドです。weightArrayのindexと同じscaleArrayのindexの要素が確率的に選ばれます。12が選ばれるのは10%ということですね。その選ばれた値に51が足されます。鍵盤で表すとこうです。

グレーの部分が生成するMIDInoteです(オクターブは除いています)

upmidi
左から、30%, 10%, 5%, 20%, 20%, 10%, 5%です。右から四番目が60のドです。
ふむ、Gの出現率が一番高いのですな。しかもベース部分に。

17行目は、

if(0.2.coin,{note = note + 12;});

ですので、20%の確率で変数noteに12が足されます。変数noteの数値はMIDInoteとして扱われているので、1オクターブ上ということです。

20行目、

while( { chordArray.includes(note) },
{ note = scaleArray.wchoose(weightArray) + 51; });

最初の条件がtrueであるかぎり以下の処理を実行するので、ここでは、
「chordArrayという配列の内部に、変数noteに格納されているMIDInoteと同じのがあれば、(MIDInoteを)選び直す」ということです。
ユニゾンにならないようにということですね。音の多様性のためと、ユニゾンばかりが選ばれる続けることもありますし、その防止のwhile文でしょう。
その後、chordArrayに変数noteの値を格納します。

どんどんいきましょう。

23行目、

chordArray = chordArray.addFirst(note);
chordArrayの先頭(indexナンバー0)に変数noteの数値を入れます。

引数に値を渡し、生成したSynthを変数nodeに格納します。このときにifやwhileを駆使して得られたnoteを、MIDInoteから周波数に変換し、引数freqに渡します。
そして変数nodeに格納されたSynthをnodeArrayに入れます。ここでも配列の先頭に配置。

ここでchordArrayの要素数に対する条件文ですね。
32行目、

if( chordArray.size > 5, {chordArray.pop; s.sendMsg( “/n_set”, nodeArray.pop.nodeID, \gate, 0.0 );

「chordArrayの要素の数が5よりも大きくなれば、chordArrayの最後尾の要素からSynthを一つ取り出す。そのSynthに対し引数gateに0を渡し、音量をゼロに向かわせ、そののちメモリーから解放する」ということです。
(SynthDefで見かけた、gate, メモリの解放(doneAction: 2)については、EnvGen,Env.asrなどのHelpファイルを確認してください)

ここで、34行目の、

s.sendMsg( “/n_set”, nodeArray.pop.nodeID, \gate, 0.0 );

をみてみましょう。
簡単に言うと、「Serverの特定のSynth(Node)に、引数gateに0を渡すよう、OSCメッセージを送る」ということです。
特定のSynthにアクセスするには、nodeIDを使います。生成されるSynthはnodeIDで管理されているのです。背番号が付いているようなものですね。
.nodeIDはメソッドではなく、Synthにinstance variableとして格納されているnodeIDの取得という意味です。

ちなみに、nodeIDを渡さずにs.sendMsg( “/n_set”, nodeArray.pop, \gate, 0.0 );でコード全体を動かしてみましたが、こんな風にSynthが全く解放せずにエラいことになりました。

いつまでもSynthが残っているのがわかります。


背番号を送らなければ、どのSynth(Node)がメッセージの対象なのかわかりませんからね。
(てか、ServerにSynthをそのまま渡して、何がしたいねんっちゅう話ですな)
( s.sendMsg( “/n_set”, nodeArray.pop.defName, \gate, 0.0 );)でも無理でした。Synthの名前を渡してもダメですな)

実際にSynthが生成する時に、ServerにnodeIDは本当に渡されてるの?と思ったのでソースコードを見てみました。(Synthを選んでcommad + yでソースへ)

Synthのソースのすぐ下の*newがSynth生成のクラスメソッドですね。
このメソッド中の関数の一番下らへんに、

(Node.scのSynthの部分)

1
2
3
server.sendMsg(9, //"s_new"
              defName, synth.nodeID, addNum, inTarget.nodeID,
              *(args.asOSCArgArray)


というのがありますね。
なんのことはない、メソッドの内部でOSCメッセージが使われてますね。送っている内容を見ると、対象とするSynthDefの名前とnodeIDを送っているではありませんか。Serverに対してどのSynthDefを使うかと、生成するSynthに対して背番号を付与しているのですね。
(nodeIDをどのように取得しているかについても、是非ソースコードを追ってみてください)

これ以外にSynthを解放する方法を考えてみました。

◉一つ目。
chordArray.pop.set(\gate, 0);

取り出したSynthに対し、setメソッドで引数を渡します。
ん?これはnodeIDとは関係ない処理か?
確認のため、再びソースへ。
Synthにはsetメソッドは見当らないですが、親クラスのNodeにありました。メソッドの継承ですね。

(Node.scのNodeの部分)

1
2
3
set { arg ... args;
      server.sendMsg(15, nodeID, *(args.asOSCArgArray)); //"/n_set"
    }


やはり、ここでも内部でOSCメッセーが動いていて、ServerにnodeIDを渡していますね。
なんのことはない、umbrellaさんは、直接OSCメッセージを書いているのですね。

◉二つ目。
chordArray.pop.release(5);

取り出したSynthに対し、音を減衰させていき5秒後に音量を0にするメソッド。こちらも親クラスのNodeにメソッドがあります。ソースをば。

(Node.scのNodeの部分)

1
2
3
release { arg releaseTime;
          server.sendMsg(*this.releaseMsg(releaseTime))
         }


ここでも、sendMsgが活躍。内部にさらにreleaseMsgというメソッド。releaseメソッドのすぐ下にありますね。

(Node.scのNodeの部分)

1
2
3
4
5
6
7
8
releaseMsg { arg releaseTime;
        //assumes a control called 'gate' in the synth
             if(releaseTime.isNil, {
                     releaseTime = 0.0;
                   },{ releaseTime = -1.0 - releaseTime;
               });
            ^[15, nodeID, \gate, releaseTime]
           }


Serverにはメッセージを送ってなく、返り値[15, nodeID, \gate, releaseTime]を見るに、OSCメッセージと連携するタイプのようですね。ここでもやはりnodeIDが送られていますね。
この手法も有効ですが、SynthDef内部にrelease時間を設定してますし、それに任せましょう。

以上で、二つのやり方を考えてみましたが、どちらのメソッドも結局は、内部でs.sendMsg()をしているので、最初からOSCメッセージを使えばいいことが分かりました。

というわけで、

Synthが生成する際、SynthDefの名前と、背番号としてのnodeID等がServerに渡され、これらは生成したSynth(Node)のinstance variableに格納されている。

生成したSynthにメッセージを送る時の区別はnodeIDでする。
(Synthの名前やnodeIDはSynthのinstance variableから取得)

実際、nodeArray.pop.nodeIDを使って、配列から取り出したSynthからnodeIDを取得しています。
(Serverやgroupもわかるようですね)

少し脱線してしまいましたが、まとめると、
  • 生成されるSynthは最大5つまでで、6つ目が生成させれると一番古いSynth(すなわち配列の先頭)が減衰していき、やがて消滅する。
  • 候補の七つの音高(scaleArray)はそれぞれ重み付け(weightArray)がされている。
  • 音高がオクターブになる確率は20%ですが、各音が選ばれ、さらにオクターブ上になる確率は、各音の生成確率×0.2(オクターブになる確率)。一番高い可能性はMIDInoteナンバー55(G)の0.3×0.2で0.06、6%である。(個々のMIDInoteで見るとかなり低い確率なんですよね)
  • なお、最初のif文でtrueになる確率が15%なので、仮にBPM 60とすると、次の音は1~6.7秒の間に生成されますね。
テンポや各音の重み付け、オクターブになる確率など変更してみると面白いと思います。

umbrellaさんがコメントでご指摘くださったように、フリギア旋法ぽいですね。MIDInoteを変更して別の旋法を使うのも楽しいと思います。

確率的には、Ⅳ-Ⅴ-Ⅰがありえますね。といっても、仮にAbの響きの次の音がBb(最低音として)ならば、Ab/Bb(Bb7sus4 add 9th)となったりと響きが時間をかけて推移します。といっても、Gが30%の出現率ですからねえ。ベースがGである確率が低いと、フリギア旋法ぽくなりませんから。。。

他にも進行が生まれそうですが、中々巡り会えないでしょう。

以上で、「Stacked Chord Loop by umbrella_process」の分析を終了します。

読んでくださった方、コードを投稿されたumbrella_processさんに感謝いたします。

SuperColliderコードリーディング 「Stacked Chord Loop by umbrella_process」 その1

ciqiのかたわれのotonotrixです。

初回はSuperCollider Japanの運営もされている、umbrella_processさんのコードを紹介します。

コードはここから。

今回は前半部分に当たるSynthDefについて。
SynthDefの書き方や、SinOsc, EnvGen, Pan2などのよく使うUnit Generatorの機能については、SuperColliderのHelpファイルを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
(
SynthDef("up-piano-20", {
arg freq=440, gate=1, amp=1, pan=0, downRate=20000, mix=0.25, room=0.15, damp=0.5;
var x, y, env;
env = Env.asr(5,1,5, -3);
x = SinOsc.ar(freq,0,amp);
y = LFNoise2.ar(0.2,downRate/100,downRate);
x = Latch.ar(x, Impulse.ar(y));
x = FreeVerb.ar(x,mix,room,damp);
x = EnvGen.kr(env,gate,doneAction: 2) * x;
Out.ar(0, Pan2.ar(x,pan));
}).store;
)


7行目からいきましょう。見やすいように引数を渡した状態で、
y = LFNoise2.ar(0.2,200,20000);

LFNoiseはランダム値を出すUGenですが、1秒間に出す値(-1~1)の数を決めることができます。

ここでは、0.2となっているので10秒に二つ(1秒間に0.2つ)の値を出します。
そして第二、第三引数が、-1~1の値に対してそれぞれ乗算、加算なので、この場合、
「19800~20200の間の値を10秒に二つランダムに出す」ということです。

LFNoiseにはいくつか種類がありまして、大雑把にいうと、生み出された値同士を繋ぐ(補間)か繋がないかで分けられます。LFNoise2は後者で、値が滑らかに変化していきます。
「19800~20200の間の値を10秒に二つランダムに出す。次の値へは滑らかに変化する」です。

8行目は、
x = Latch.ar(SinOsc.ar(freq,0,amp), Impulse.ar(y));ですね。
Helpにも書いてありますが、LatchはSample & Holdという手法で、次のtriggerがあるまで値を保持するということです。試しに一つコードを。

{ Latch.ar( SinOsc.ar(440), Impulse.ar(5000)) }.scope; (要 internal server起動)



滑らかだったsin波がでこぼこになってますね。次のtrigger(Impulseが1を出す)まで前の値は維持されるから、Stepのようになるのです。波形が変わるので結果として音色が変わります。internal serverを起動した状態で{上のコード}.freqscopeでスペクトルを確認してください。

ん、ちょっと待ってください。triggerの数の分だけ値が検出されるって、何だかサンプリングレートみたいですね。 事実、Impulse.ar(5000)は値を5000回検出してるわけですし。そう考えるとですね、これはもともと44100回(44.1kHz)検出していたのを、5000回(5kHz)に変更すること、つまりサンプリングレートを下げていると考えられないでしょうか。下げるとどうなるか。

この場合だと、だいたい8~9分の1にレートが下がっているので、44.1kHzと比べて一回のtriggerごとに8~9個のサンプルを見逃していると考えられるのです。その結果、不完全なsin波が生まれ、音が歪むのです。

よく見たらSynthDefの引数がdownRateになってますね。

7行目のコードと合わせると、
「sin波をダウンサンプリングするが、そのレートは19800(19.8kHz)~20200(20.2kHz)の間をランダムにしかも滑らかに変化する、その結果としてsin波が変化しスペクトルが変化する」
ということですね。刺激的な音色になりそうですね。

しかし、Impulseは主役ではないのに、何故「kr」ではなく「ar」を使うのでしょう?controlするだけだし、krで十分ではないか?

そもそも、audio rate(ar)でsampleを生み出す時、一つ一つ処理するのではなく、64個をひとまとめにしています。このまとまりをblock sizeといいます。それに対してcontrol rate(kr)は、arのblock size一つの間に一つのsampleを生み出すだけなのです。つまりsample数は1/64なのです。
具体的に見ましょう。

{ Impulse.kr(200) }.scope;



Impulse.arとはえらい違いですね。
(64sampleごとに生み出される値間は直線(線形)で補間されています)
ImpulseというよりもTriangleっぽいですね。一つの値に対して64sampleですので、0から1までの間は64sample、1から0までも64sampleで、つまり一つのImpulseには128sample必要ということですね。
ということは、1秒間に出せるImpulseの最大数は44100/128でだいたい344といえます。

{ Impulse.kr(44100/128) }.scope;



ますますTriangleですね。
これ以上Impulseの数を増やしたら、Impulseとして成り立ちませんね。
ナイキストレートにより、最大周波数は検出レートの半分の周波数が望ましいので、44100/128/2でだいたい172Hzですね。

{ Latch.ar(SinOsc.ar(44100/128/2),Impulse.kr(44100/128)) }.scope;



Pulse波になりましたね。このくらいまでならばkrで対応出来るわけです。

というわけで、Impulse.kr(20200)なんかは、不可能(可能ではあるがImpulseとしての体をなさない)なんですね。だからarを使わなくてはならない。

9行以下も基本的なUGenを使ってますので、Helpで確認してください。

10、11行目の、
x = EnvGen.kr(env,gate,doneAction: 2) * SinOsc.ar(freq,0,amp);
Out.ar(0, Pan2.ar(x,pan));
についてですが、エンベロープをsin波に乗算してからPan2に入れるのは、CPUの負荷軽減になります。

Pan2.ar(signal*envelope)よりも、Pan2.ar(signal)*envelopeの方が負荷が大きいのです。
Pan2でsignalをステレオにする際、[signal,signal]と配列にしているので、前者だとenvelopeはsignal一つだけに使っていますが、後者だと、[signal,signal]*envelope、[signal*envelope,signal*envelope]となり、envelopeの乗算処理を一つ余計に使うことになり、これがCPUに負担をかけます。画面左下の、ServerのGUIのUGensが増えているのがわかります。

後半のsequencerの部分に話が及びますが、rand2などのmath operationをSynthDefには使用せず、language側で計算させてserverに計算結果を渡すのもCPUの負荷軽減になります。serverに計算させるということは、それだけ負担をかけるということですから。

アイデアと深い思索が効率よく詰めこまれた素晴らしいコードですね。

以上で前半のSynthDefを終わります。間違いや勘違いがあるかもしれませんので、ご指摘、ご教示いただけると幸いです。