axjack's blog

### axjack is said to be an abbreviation for An eXistent JApanese Cool Klutz ###

Xmas Liveを終えて

去る12/10(日)、Xmas Live@赤坂B♭にて演奏してきました。メンバー含めお客さまや色んな方から「ドラム凄かった」「ドラムのビートやべぇ」などお褒めの言葉を頂き、とても嬉しく存じ上げます。ありがとうございます。 ただ、全身全霊で叩いたせいもあり、そして歳を重ねた事もあり、ライブの翌日から右腕左腕が嬉しい悲鳴という名の筋肉痛に襲われて居る次第です。私は今年で齢三十を迎えました。

扨、演奏中の記憶は全く以って吹き飛んでおりますのでここでは先日右手中指を痛めてからのドラムプレイングの変化などを書き残し今後に生かそうと思います。

右手中指を痛めて

最悪の場合もうドラムは叩けないのかなぁ、としょんぼりしてました。幼い頃からピアノを弾いていた事もあり、指の怪我は絶対に避けなければならないと思っていたので、指の痛み以上に精神的なショックが大きかったです。

突き指では無く腱鞘炎

怪我をして一ヶ月。痛みが完全には取れなかったので病院へ。すると医者曰く「腱鞘炎かもね。」と。腱鞘炎は一番聞きたくない怪我の名前です。。手術か注射か"動かさないようにする"のどれかで回復を待つしかなく、聞くたびにうんざりしてしまう。

それでもドラムを叩きたい

とはいえ、1.5ヶ月ぐらいで痛みもかなりデクレッシェンド。また痛くなるのは怖いけどそろそろドラムが恋しくなる時期。しかし右手中指はあまり使いたくない。さてどうするか?

フレンチグリップ

普段はアメリカングリップなので久しぶりにフレンチグリップにしてみました。この握り方は、親指と人差し指がメインで他の指は添える感じなので中指への負担が少ないのが特徴。

しかしフレンチグリップはバウンスを殺してしまいがちな握り方でもある。バウンスは命。でも中指はもっと命。命と命を比較すると中指の方が大事。ということで、フレンチグリップでバウンスの感覚を掴むことに専念する。

一度叩くと自信がつく

怪我をして最初のドラムはかなりヒヤヒヤしたのだけれど、2回目からは少し慣れてしまった。3回目からは、実はフレンチグリップでずっとやってきたのでは?ぐらいの気持ちにまで回復。自信がつく、自信を取り戻すのは自分にとってこれほど重要なことだったのか、と改めて気付く。

メンバーも、「怪我してるとは思えないプレイだった」と言ってくれたりもして、一安心を得る。メンバーに受け入れられるのは心強い。

そして本番

お酒を飲んだこともあり、怪我のことはだいたい吹っ飛んでしまった。と書いてしまうと元も子もないが、「怪我であると思っている自分」がイイ感じに吹っ飛んでしまったのはありがたい。

あと、いつもの音量・いつも以上の音量をフレンチグリップで叩いた結果、腕を多く使った。アメリカングリップだと手首のスナップで音量が出せるのに対して、フレンチグリップは手首のスナップは使えないからである。というのは知識としてはその時知らなくて、本番叩きながら「腕使うしか選択肢ないなー」と思った。

今後

腱鞘炎はしぶとく残るであろうから、フレンチグリップをメインに叩こうかと思う。そうすると、腕も使うことになるので腕力もつけないといけない?筋トレは苦手なり。

指・手首・腕に負担をかけないように心掛けながら、出したい音を表現出来るよう模索します。

releaseの方法 in depth

では、OSCコマンド(Server Command)でreleaseさせるにはどうすればよいのだろう?

前回書いたのだが、もう少しまとめる。というよりもっと前にも書いていた。今回含め3回も書くぐらいだし、releaseは重要なのだ。

releaseするには、大きく分けると、

  • setメソッド
  • releaseメソッド
  • sendMsgメソッド

の3つ方法がある。このうち、set/releaseメソッドはnodeインスタンスの、sendMsgメソッドはserverインスタンスのメソッドである。どれをいつ使うかは状況次第。

コード

//SynthDef
(
SynthDef(\h,{
    |amp=0.3,gate=1,rT=1,freq=440|
    var sig,e,eg;
    sig = SinOsc.ar(freq) + SinOsc.ar(freq + 1);
    e   = Env.adsr(releaseTime:rT);
    eg  = EnvGen.kr(e,gate,doneAction:2);
    sig = sig * eg;
    
    Out.ar(0,Pan2.ar(sig,0,amp));
}).add;

)

/* how to release */
//set
~x = Synth(\h,[\amp,0.1,\rT,0]);
~x.set(\rT,5);
~x.set(\gate,0);

//set gate: negative value
~x = Synth(\h,[\amp,0.1,\rT,0]);
~x.set(\gate,-5-1);

//release
~x = Synth(\h,[\amp,0.1,\rT,0]);
~x.release(5);

//sendMsg release -> gate: 0
~x = Synth(\h,[\amp,0.1,\rT,0]);
s.sendMsg('/n_set',~x.nodeID,\rT,5);
s.sendMsg('/n_set',~x.nodeID,\gate,0);

//sendMsg gate: negative value
~x = Synth(\h,[\amp,0.1,\rT,0]);
s.sendMsg('/n_set',~x.nodeID,\gate,-5-1);

メモ

/* how to release */以降は全て5秒でreleaseする方法である。

saw stringsを作って余韻(release)の伸びを確かめる

saw strings

その名の通りSaw.arを使えばできるだろうと思いググると、

まずオシレーターには通常のこぎり波や細いパルス波など倍音成分が多い波形を用います。最初に述べた通りこれらのオシレーター波形はストリングス系の楽器に近い倍音分布を持っています。これをデチューンするかオクターブ違いにするのがストリングス音の基本的なオシレーター設定です。

チュートリアル:シンセサイザー入門

と書いてある。なので、2つのSaw.arを素直にデチューンして作ってみることにした。

releaseの伸び

ストリングスと言えばreleaseの伸びが印象的。releaseの値を0から0.05ずつ加算して、混ざり合う音がどんな心地になるのかを確かめる。

コード

(
SynthDef(\saw_strings,{
    |freq=440,amp=0.3,gate=1,detune=2,rate=1,rT=0|
    var sig,e,eg;

    sig = Saw.ar(freq) + Saw.ar(freq * rate + detune);
    e   = Env.adsr(0.5,5,0.9,rT,curve:\sin);
    eg  = EnvGen.ar(e,gate,doneAction:2);
    sig = RLPF.ar(sig,freq*2,0.1);
    sig = sig * eg;

    Out.ar(0,Pan2.ar(sig,0,amp*SinOsc.kr(6.5,0,0.1,0.8)));
}).add;

)


(
~r = r({
    var rt = 0;
    var f = [1,1/2,3/2,4/3,5/4];
    var freq = 440!f.size * f;
    inf.do({
        "releaseTime: %\n".postf(rt);
        ~x = Synth(\saw_strings,[\freq,freq.choose,\detune,1,\rT,rt]);
        1.wait;
        ~x.release;
        rt = rt + 0.05;
        1.wait;
    });
});

)

~r.reset;
~r.play(TempoClock( 144/60 ));
~r.stop;


/*
http://danielnouri.org/docs/SuperColliderHelp/ServerArchitecture/SynthDef.html

    // if value is negative, it overrides the release time, to -1 - gate
        x = Synth(\synthDefTest);
        x.set(\gate, -5); // 4 second release
*/

~x = Synth(\saw_strings,[\freq,440,\detune,1,\rT,0]);
s.sendMsg('/n_set',~x.nodeID,\gate,-5);

メモ

感想

  • Synth面
    • デチューンしただけの割には、シンセサイザのプリセットにあるようなsaw stringsの音に聞こえる。
  • Routine面
    • releaseTimeが1を超えた辺りから音のオーバーラップが始まる。音楽的作品には全く以てほど遠いものの、時間が経つにつれてコーラル(合唱)のような響きを感じた。

releaseの方法

Synth(~xとする。)にEnvGenを噛ましてgateをaugとして持っている場合、~x.releaseを実行すればreleaseされる。また~x.release(3)と実行すれば3秒後にreleaseされる(release値の上書き)。

では、OSCコマンド(Server Command)でreleaseさせるにはどうすればよいのだろう?*1それは、

s.sendMsg('/n_set',~x.nodeID,\gate,-5);

のように、sendMsgコマンドの引数に\gate, <負の値>をセットすれば良い。そうすると、-1 - <負の値>後にreleaseされる。

動画に撮ってみた

画面キャプチャではなくiPhoneで撮ったものをiPhoneからYouTubeにアップロードしてみた。こちらより。録音や録画はコードを再実行する手間も省けて便利。

参考

チュートリアル:シンセサイザー入門

Server-Command-Reference

SynthDef

*1:これを書くきっかけ:s.sendMsg('/n_release',node_id)やs.sendMsg('/n_set',node_id,\release)ではreleaseできなかった故。

do-loopのネスト

yoppaさんのなかなか良いサンプルができたのSynthDefがとてもきれいで感動した。RLPFのcutoff frequencyをLFNoiseで動かしているのだけど、右と左でcutoffの周期が違うので立体感が増すように感じる。こんな風に作れたらなぁ。。

今回作ったもの

  • SynthDef

    • yoppaさんがSawを3つステレオで重ねていたのをパkインスパイアして、4つほど重ねてみる。
    • freq * 2 => 1オクターブ
    • freq * 4.5 = freq * 3 * 3/2 = freq * (3/2) * (3/2) * 2 => (完全五度上の完全五度 = 長9度?)の1オクターブ
    • freq * 6 = freq * 2 * 2 * (3/2) => 完全五度上の2オクターブ
  • Routine

    • i = 0の時, 2**(i+1) = 2, (2**i).reciprocal = 1 => 四分音符を2拍
    • i = 1の時, 2**(i+1) = 4, (2**i).reciprocal = 1/2 => 八分音符を4拍
    • i = 2の時, 2**(i+1) = 8, (2**i).reciprocal = 1/4 => 十六分音符を8拍

コード

(
SynthDef(\h,
{
        arg freq = 48;
        Out.ar(0,(
            LFTri.ar([freq,freq-5]) + 
            LFTri.ar([freq * 2 + 1,freq * 2 - 1]) +
            LFTri.ar([freq * 4.5 + 1,freq * 4.5 + 2]) +
            LFTri.ar([freq * 6 - 2,freq * 6 + 5])
        ) * 0.1 * EnvGen.ar(Env.perc,doneAction:2)
        );
}).add;

)


(
~r = r({

    inf.do({
        3.do({|i|
            ( 2**(i+1) ).do({
                Synth(\h,[\freq,48*(i+1)]);
                (2**i).reciprocal.wait;
            });
        });
    });

});

)


~r.reset;
~r.play(TempoClock(144/60));

Envのblendと録音

難しいやり方はググった方が早い。ひとまずざっくりとblendと録音をしてみる。

blend

blend (argAnotherEnv, argBlendFrac: 0.5)ブレンドされたEnvが返る。実際に使うときはEnvGenenvelopeに入れる。

~e         = Env.perc(2,level:0.5);
~e_delay2  = ~e.delay(2);
~e_blend   = blend(~e,~e_delay2,0.9);

[~e,~e_delay2,~e_blend].plot(maxval:0.75);

plotで確認すると以下のようになる。上から順に~e~e_delay2~e_blend。センスよく混ぜれば素敵なエンベロープが得られる、のではないかと思う。

f:id:axjack:20171117233536p:plain

録音

最低限やるべきなのは以下の3つで良い。

  • s.prepareForRecord;
  • s.record;
  • s.stopRecording;

.prepareForRecordでバッファなどを確保し、.recordで録音開始。そして.stopRecordingで録音終了。.prepareForRecordをやっておかないと

slight delay before recording starts for real

となるらしい。録音前にしれっと実行しておこう。

録音が終わるとどこに保存されるかというと、Post Windowに表示されるRecording: /...Recording Stopped: /...である。Macだと/Users/<username>/Music/SuperCollider Recordingsに保存されるのだと思われる。

f:id:axjack:20171117235603p:plain

難しいやり方はググった方が早い。ひとまずざっくりの方針なので、これで一応録音ができる。

作ったコード

(
SynthDef(\h,{
    |amp=0.1,freq=440,gate=1,aT=2|
    var sig,e,eg,y;

    y   = SinOsc;
    sig = Mix.ar(y.ar([freq,freq*(4/3),freq*(5/4)]));
    e   = Env.perc(aT,level:0.5);
    e   = blend(e,e.delay(2),0.5);
    eg  = EnvGen.ar(e,gate,doneAction:2);
    sig = sig * eg;
    sig = FreeVerb.ar(sig,0.3,0.9,0.7);

    Out.ar(0,Pan2.ar(sig,0,amp));
}).add;
)

//~x = Synth(\h,[\amp,0.5,\freq,400,\aT,5]);

(
~r = r({
    var f  = [400,400*1.2,400/1.2,400*1.5,400*1.9,400];
    //var w  = Array.rand(f.size,5,15);
    var w  = [11,5,8,6,15,8];
    var wi;

    f.do({|item,i|
        wi = w[i].postln;
        Synth(\h,[\amp,[0.3,0.15,0.4].choose,\freq,item,\aT,wi]);
        (wi-2).wait;
    })

});
)


s.prepareForRecord;
~r.reset;

(
s.record;
~r.play;
)

s.stopRecording;

解説や補足

  • freq*(4/3)はfreqに対して純正完全4度、freq*(5/4)はfreqに対して純正長3度である。
    • 例えば、freq = 60.midicps = ドなら上記はドミファを表す。
  • 録音したものはsoundcloudにアップロードしてみた。

参考

バブルソートの音を聞く

var note = [63,61,69,64]; note.sort;と書けばnoteはソートされるのであるが、ソート中のnoteは見ることも聞くこともできない。聞く価値があるのか・ソート中の配列に芸術的な価値があるのか、は神棚に上げておくとし、ひとまずバブルソートしているときの音を聞いてみたい。

コードを実行すると音がどんどん昇順になることが分かる。ただ、あまり綺麗ではないな...というのが率直な感想である。

コード

/*
    Bubble Sort
*/
(
~r = r({
    //var note = [ 68, 71, 70, 61, 60, 64, 67, 65, 63, 66, 69, 62 ];
    //var note = (60 .. 71).scramble;
    var note = Array.rand(8, 50, 50+24);

    var note_size = note.size;
    var x;//temp variable for Synth

    "-----START-----\n".post;
    note.postln;
    "---------------\n".post;

    (note_size - 1).do({|item,i|
        (0 .. note_size - 2 - i).do({|jtem,j|

            //"compare: (%, %)\n".postf(jtem,jtem+1);


            if(note[jtem] > note[jtem+1],{
                note.swap(jtem,jtem+1);
                note.postln;

                note.do({|ktem,k|
                    x = Synth(\default,[\freq,ktem.midicps]);
                    (1/8).wait;
                    x.release(0.1);
                });
            });//end of if


        });//end of j in do-loop
    });//end of i in do-loop

    "-----END-----\n".post;
    note.postln;
});
)

~r.reset;
~r.play;

コード実行例

-----START-----
[ 60, 65, 70, 53, 69, 52, 56, 55 ]
---------------
[ 60, 65, 53, 70, 69, 52, 56, 55 ]
[ 60, 65, 53, 69, 70, 52, 56, 55 ]
[ 60, 65, 53, 69, 52, 70, 56, 55 ]
[ 60, 65, 53, 69, 52, 56, 70, 55 ]
[ 60, 65, 53, 69, 52, 56, 55, 70 ]
[ 60, 53, 65, 69, 52, 56, 55, 70 ]
[ 60, 53, 65, 52, 69, 56, 55, 70 ]
[ 60, 53, 65, 52, 56, 69, 55, 70 ]
[ 60, 53, 65, 52, 56, 55, 69, 70 ]
[ 53, 60, 65, 52, 56, 55, 69, 70 ]
[ 53, 60, 52, 65, 56, 55, 69, 70 ]
[ 53, 60, 52, 56, 65, 55, 69, 70 ]
[ 53, 60, 52, 56, 55, 65, 69, 70 ]
[ 53, 52, 60, 56, 55, 65, 69, 70 ]
[ 53, 52, 56, 60, 55, 65, 69, 70 ]
[ 53, 52, 56, 55, 60, 65, 69, 70 ]
[ 52, 53, 56, 55, 60, 65, 69, 70 ]
[ 52, 53, 55, 56, 60, 65, 69, 70 ]
-----END-----
[ 52, 53, 55, 56, 60, 65, 69, 70 ]

学んだことなど

  • noteが配列のとき、note.swap(i , j)note[i]note[j]を交換する。
  • do-loopの反復回数に悩んだ。n.do(0 .. n).doだと、前者はn回反復し後者はn+1回反復する。

(
var x = 0;
(8).do({|item,i|
    /* "(8).do({" is same as "8.do({" */
    x = x+1;
    "(item,i) = (%, %)\n".postf(item,i);
});
x;//8
)

/* 実行結果
(item,i) = (0, 0)
(item,i) = (1, 1)
(item,i) = (2, 2)
(item,i) = (3, 3)
(item,i) = (4, 4)
(item,i) = (5, 5)
(item,i) = (6, 6)
(item,i) = (7, 7)
*/

(
var x = 0;
(0 .. 8).do({|item,i|
    x = x+1;
    "(item,i) = (%, %)\n".postf(item,i);
});

x;//9
)

/* 実行結果
(item,i) = (0, 0)
(item,i) = (1, 1)
(item,i) = (2, 2)
(item,i) = (3, 3)
(item,i) = (4, 4)
(item,i) = (5, 5)
(item,i) = (6, 6)
(item,i) = (7, 7)
(item,i) = (8, 8)
*/
  • 前述のdo-loop例を頭に入れて、今回のコードのループ構造を見ると、以下の通りとなる。
(
var note = Array.rand(8,50,50+12+12);
var note_size = note.size;

"-------------------".postln;
" - note is %\n".postf(note);
" - note_size is %\n".postf(note_size);
"-------------------".postln;

(note_size - 1).do({|item,i|
    (0 .. note_size - 2 - i).do({|jtem,j|
        "item, i : jtem, j = %, % : %, %\n".postf(item,i,jtem,j);
    });
});
)

//実行結果
/*
-------------------
 - note is [ 54, 50, 52, 61, 55, 50, 70, 65 ]
 - note_size is 8
-------------------
item, i : jtem, j = 0, 0 : 0, 0
item, i : jtem, j = 0, 0 : 1, 1
item, i : jtem, j = 0, 0 : 2, 2
item, i : jtem, j = 0, 0 : 3, 3
item, i : jtem, j = 0, 0 : 4, 4
item, i : jtem, j = 0, 0 : 5, 5
item, i : jtem, j = 0, 0 : 6, 6
item, i : jtem, j = 1, 1 : 0, 0
item, i : jtem, j = 1, 1 : 1, 1
item, i : jtem, j = 1, 1 : 2, 2
item, i : jtem, j = 1, 1 : 3, 3
item, i : jtem, j = 1, 1 : 4, 4
item, i : jtem, j = 1, 1 : 5, 5
item, i : jtem, j = 2, 2 : 0, 0
item, i : jtem, j = 2, 2 : 1, 1
item, i : jtem, j = 2, 2 : 2, 2
item, i : jtem, j = 2, 2 : 3, 3
item, i : jtem, j = 2, 2 : 4, 4
item, i : jtem, j = 3, 3 : 0, 0
item, i : jtem, j = 3, 3 : 1, 1
item, i : jtem, j = 3, 3 : 2, 2
item, i : jtem, j = 3, 3 : 3, 3
item, i : jtem, j = 4, 4 : 0, 0
item, i : jtem, j = 4, 4 : 1, 1
item, i : jtem, j = 4, 4 : 2, 2
item, i : jtem, j = 5, 5 : 0, 0
item, i : jtem, j = 5, 5 : 1, 1
item, i : jtem, j = 6, 6 : 0, 0
*/

参考

バブルソート : アルゴリズム

Array | SuperCollider 3.9dev Help

ハノン練習曲 第1番

Scaleを使ってハノン練習曲 第1番(=ドミファソラソファミ〜♪)をプログラミングしてみる。

分析

degrees(度数)*1[0,2,3,4,5,4,3,2]に固定してプログラミングしたい。ということでどのスケールを使うか検討する。

まず「ドミファソラソファミ」はメジャースケールに見える。そこで「レファソラシラソファ」以降も全てメジャースケールである、と早合点してはならぬ。例えば「ドミファソラソファミ」をハ長調と見なしてニ長調に移調すると、「レファソラシラソファ」ではなく「レファ#ソラシラソファ#」となる。これだとずいぶんと明るめなハノンになってしまう。

では、全てがピアノの白鍵になるような都合の良いスケールと言えば・・・教会旋法(チャーチモード)である。なので、「ドミファソラソファミ」がionian、「レファソラシラソファ」がdorian、と教科書通りチャーチモードを並べていけばdegreesを[0,2,3,4,5,4,3,2]に固定してハノンを奏でることができる。

以上を表にまとめるとこのようになる。

実音 Scale root(MIDI note) degrees
ドミファソラソファミ ionian 60 [0,2,3,4,5,4,3,2]
レファソラシラソファ dorian 62 [0,2,3,4,5,4,3,2]
ミソラシドシラソ phrygian 64 [0,2,3,4,5,4,3,2]
ファラシドレドシラ lydian 65 [0,2,3,4,5,4,3,2]
ソシドレミレドシ mixolydian 67 [0,2,3,4,5,4,3,2]
ラドレミファミレド aeolian 69 [0,2,3,4,5,4,3,2]
シレミファソファミレ locrian 71 [0,2,3,4,5,4,3,2]

この表に基づいてプログラミングしてみる。

コード

//Instrument
(
SynthDef(\h,{
    |amp=0.3,gate=1,out=0,freq=440|
    var sig,e,eg;

    sig = SinOsc.ar(freq);
    e = Env.adsr;
    eg = EnvGen.ar(e,gate,doneAction:2);
    sig = sig * eg;
    Out.ar(out,Pan2.ar(sig,0,amp));
}).add;
);


//Score
(
~r = r({
    var root  = [60,62,64,65,67,69,71];//C D E F G A H
    var hanon_up   = [0,2,3,4,5,4,3,2];//Hanon No.1 going up
    var hanon_down = [5,3,2,1,0,1,2,3];//Hanon No.1 going down
    var mode  = #[
        ionian,dorian,phrygian,
        lydian,mixolydian,
        aeolian,locrian
    ];

    var x;


    /* Hanon Up */
    mode.do({|i,ix|
        hanon_up.do({|j|
            x = Synth(\h,
                [\freq,Scale.at(i).degreeToFreq(j,root[ix].midicps,0)]
            );
            (1/4).wait;
            x.release(0.1);
        });
    });


    /* Hanon Down */
    mode = mode.reverse ++ #[locrian];
    root = root.reverse ++ [59];
    mode.do({|i,ix|
        hanon_down.do({|j|
            x = Synth(\h,
                [\freq,Scale.at(i).degreeToFreq(j,root[ix].midicps,0)]
            );
            (1/4).wait;
            x.release(0.1);
        });
    });


    /* Ending */
    x = Synth(\h,[\freq,60.midicps]);
    2.wait;
    x.release(0.1);

});
);


//Exec
~r.play;
~r.reset;

解説

  • Scale.major == Scale.at(\major);//true

  • #[ionian,dorian,/*略*/ locrian]と書くと、配列の中身はリテラルとなる。リテラルになると何が利点になるのか、はよく分からないが「In literal Arrays names are interpreted as symbols.」*2とのことなので、配列の中身はシンボル扱いされるらしい。シンボルとキーの違いは・・・。(おそらく言葉の使い方としては、シンボルをキーとして扱う、と呼称すると思われる。それ以上の意味は分からない。。)

  • mode.reverse ++ #[locrian];はmodeを逆順にした上でlocrianを最後に追加している。この前学習した通り++と書くと配列は直和される。なお、これを+と書くと#[locrian]は無視される。

参考

ハノン練習曲 第1番

教会旋法

*1:SuperColliderでは、degreesは1からではなく0から始まる。

*2:http://doc.sccode.org/Reference/Literals.html

純正音程の完全四度堆積

純正音程の完全四度は振動比が3:4である。たとえば、MIDIノートの60(振動数およそ261[Hz]、中央ド)の純正音程版の完全四度は、261×(4/3) = 348となる。なので、完全四度をずっと堆積することは基準の振動数(周波数)に4/3を乗算し続ければ良い。良いのではあるが、4/3 > 1なので3回ぐらい掛け算すると積が2を超えてしまう。2を超えるとオクターブ上がってしまう。

//(4/3)の0乗,1乗,...,12乗を小数点2桁まで求める
( (4/3)**(0..12) ).round(0.01);
-> [ 1, 1.33, 1.78, 2.37, 3.16, 4.21, 5.62, 7.49, 9.99, 13.32, 17.76, 23.68, 31.57 ]

ということで、2を超えたら2で割ってオクターブ下げる処理を施しながら完全四度堆積をしてみることにする。

(
~r = r({
    var f=1;
    f.yield;
    loop({
        f = f * (4/3);
        if(f > 2,{ f = f/2 },{ f });
        f.yield;
    });
});
)

~r.reset;
~r.nextN(12).round(0.01);

-> [ 1, 1.33, 1.78, 1.19, 1.58, 1.05, 1.4, 1.87, 1.25, 1.66, 1.11, 1.48 ]

中央ドから完全四度堆積するコード

(
SynthDef(\h,{
    |amp=0.1,freq=440|
    var sig,e,eg;
    
    e = Env.perc;
    eg = EnvGen.kr(e,doneAction:2);
    sig = SinOsc.ar(freq);
    sig = sig * eg;
    
    Out.ar(0,Pan2.ar(sig,0,amp));
}).add;
)


(
~r = r({
    var f = 1,c = 60.midicps;
    Synth(\h,[\freq,c*f]);
    1.wait;

    inf.do({
        f = f * (4/3);
        if( f > 2, {f = f/2});
        (c*f).postln;
        Synth(\h,[\freq,c*f]);
        1.wait;
    });
});
)

参考

Wikipedia:完全四度

axjack is said to be an abbreviation for An eXistent JApanese Cool Klutz.