ハノン練習曲 第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:SuperColliderでは、degreesは1からではなく0から始まる。
純正音程の完全四度堆積
純正音程の完全四度は振動比が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; }); }); )
参考
ERROR: syntax error, unexpected VAR, expecting $end を追う
きっかけ
次のコードを選択して実行するとエラーになる。
コード1
var aa; aa = 1; var bb; bb = 1 + aa;
エラー
ERROR: syntax error, unexpected VAR, expecting $end in file 'selected text' line 3 char 3: var bb; ^^^ bb = 1 + aa; ----------------------------------- ERROR: Command line parse failed -> nil
"シンタックスエラーなり。予期せぬVARがある。そこは終端記号があるべき。場所は4行目云々。"と読める。
一方、こちらの場合は問題なく通る。
コード2
var aa; var bb; aa = 1; bb = 1 + aa;
出力結果
-> 2
どうやら、var
宣言した後に「式」を書いて、その後にまたvar
宣言するとエラーになるらしい。斯くして、var
宣言はまとめて書いた後に「式」を書けば良いという教訓を得た。
のは良いが、どうせならということでSuperColliderのソースを追ってどの辺りでエラーになったのかを確かめてみよう。
コードを追う
まずは検索
githubのsupercolliderの中をsyntax error, unexpected
で検索する。すると、lang/LangSource/Bison/lang11d_tab.cppあたりで引っかかっているのを発見。
このファイルはどのフォルダに入っているのかを見ると、
supercollider/lang/LangSource/Bison/ lang11d lang11d_tab.cpp lang11d_tab.h make_parser.sh
となっている。make_parser.shというシェルスクリプトで何やら生成しているのだろう。また「make parser」、ということはパーサー(構文解析器)をメイク(作る)に違いない。このファイルを見ると、
#!/bin/sh bison -o lang11d_tab.cpp lang11d
と記述してある。意味的には「bisonを実行、出力ファイルはlang11d_tab.cppで入力ファイルはlang11d」であろう。ということなので、肝となるのは入力ファイルのlang11dに違いない。
bisonとは?
bisonとはWikipediaによると、
Bison(バイソン)とは構文解析器を生成するパーサジェネレータの一種であり、CコンパイラとしてのGCCのサポートのために開発されたフリーソフトウェアである。
とのこと。このことから、lang11dは構文解析機を生成するための入力ファイルであることが分かる。つまりSuperColliderのsyntaxはここで定義されているのだろう、と認識してみることにする。
該当箇所を追う
以下、lang11dから該当しそうな箇所を引用しながらコードを追う。
VARを探す
class宣言の方にもVARが見つかったけれど、そちらではなく以下の定義が怪しい。怪しいというのは、普段使っているVARの定義っぽいということである。
funcvardecl : VAR vardeflist ';' { $$ = (intptr_t)newPyrVarListNode((PyrVarDefNode*)$2, varLocal); } ;
これは、「funcvardeclとは、VARというtokenに、vardeflistと';'が続くものである」と読める。「VARに続く次の文字列はvardeflistである」とも読める。では、vardeflistは?というと、
vardeflist : vardef | vardeflist ',' vardef { $$ = (intptr_t)linkNextNode((PyrParseNode*)$1, (PyrParseNode*)$3); } ;
「vardeflistとは、vardefもしくはvardeflistに','とvardefと表されるものである」と読める。再帰的な定義である。vardefにカンマを打ちvardefを更に書いてもvardeflistと認める、ということだろうか?
では、vardefとはなんぞや?と探してみると、
vardef : name { $$ = (intptr_t)newPyrVarDefNode((PyrSlotNode*)$1, NULL, 0); } | name '=' expr { $$ = (intptr_t)newPyrVarDefNode((PyrSlotNode*)$1, (PyrParseNode*)$3, 0); } | name '(' exprseq ')' { PyrParseNode* node = (PyrParseNode*)$3; node->mParens = 1; $$ = (intptr_t)newPyrVarDefNode((PyrSlotNode*)$1, node, 0); } ;
「vardefとは、nameまたはname = exprまたはname(expreseq)である」とのこと。ここで、nameは
%token NAME INTEGER SC_FLOAT ACCIDENTAL SYMBOL STRING ASCII PRIMITIVENAME CLASSNAME CURRYARG
にあるようにtoken(終端記号)である。exprとexprseqはいわゆる式である。長いので引用は割愛する。
ひとまずまとめると、lang11dから普段使うVARのようなものを調べた結果、VAR v1(,v2,v3,...,vn);のうち、v1(,v2,v3,...,vn)の部分をfuncvardeclと呼ぶことが分かった。次にfuncvardeclを見てみる
funcvardeclを探す
funcvardeclのすぐ上に幾つか見つかる。
funcvardecls : { $$ = 0; } | funcvardecls funcvardecl { $$ = (intptr_t)linkNextNode((PyrParseNode*)$1, (PyrParseNode*)$2); } ; funcvardecls1 : funcvardecl | funcvardecls1 funcvardecl { $$ = (intptr_t)linkNextNode((PyrParseNode*)$1, (PyrParseNode*)$2); } ; funcvardecl : VAR vardeflist ';' { $$ = (intptr_t)newPyrVarListNode((PyrVarDefNode*)$2, varLocal); }
funcvardeclを並べたものをfuncvardecls・funcvardecls1と呼ぶ。では、funcvardeclsは・・・と更に辿ると面白い箇所を発見した。
cmdlinecode : '(' funcvardecls1 funcbody ')' { $$ = (intptr_t)newPyrBlockNode(NULL, (PyrVarListNode*)$2, (PyrParseNode*)$3, false); } | funcvardecls1 funcbody { $$ = (intptr_t)newPyrBlockNode(NULL, (PyrVarListNode*)$1, (PyrParseNode*)$2, false); } | funcbody { $$ = (intptr_t)newPyrBlockNode(NULL, NULL, (PyrParseNode*)$1, false); } ;
cmdlinecodeの定義である。中括弧内を省略して書くと、
cmdlinecode : '(' funcvardecls1 funcbody ')' | funcvardecls1 funcbody | funcbody ;
となる。cmdlinecodeは察するにコマンドラインコードであろう。IDEにコードを書いて選択して実行することを指すものだと思われる。そして、その定義というか受理される文法は
- '(' funcvardecls1 funcbody ')'
- funcvardecls1 funcbody
- funcbody
の3タイプしかない。念のためfuncbody(おそらく関数本体を意味する)を見ると、
funcbody : funretval | exprseq funretval { $$ = (intptr_t)newPyrDropNode((PyrParseNode*)$1, (PyrParseNode*)$2); } ;
funretval(おそらく関数戻り値を意味する)を見ると、
funretval : { $$ = (intptr_t)newPyrBlockReturnNode(); } | '^' expr optsemi { $$ = (intptr_t)newPyrReturnNode((PyrParseNode*)$2); } ;
となっている。つまり、funcbodyにVARは含まれていないことが分かる。
まとめ
VAR宣言はfuncbodyより前に書かれていないとcmdlinecodeとしては受理されず、よってsyntax error扱いされると思われる。斯くして、var宣言はまとめて書いた後に「式」を書けば良いという教訓は妥当であると言えよう。
文字列処理
電話番号のような文字列からハイフンを取り除き、一文字ずつ数字にしたものを要素として持つ配列を返す関数を作ろうとしたらエラーで泣いたので整理する。泣いている暇があったらドキュメントをしっかり読もう。
基本
.inspect
で型*1を調べることができる。'
で囲むとChar
ではなくSymbol
になるので要注意。
3.inspect;//Integer "3".inspect;//String '3'.inspect;//Symbol \3.inspect;//Symbol $3.inspect;//Char [3].inspect;//Array (3).inspect;//Interger {3}.inspect;//Function
==演算
型が違うと==
もfalse
となる。ここで、String
は
String represents an array of characters.
と説明されていることから(そして実際にも)、それぞれの要素はChar
である。なので、要素を参照する時は見た目でInteger
だと思ってはならない。
3 == 3;//true: Integer vs Integer 3 == "3";//false: Integer vs String 3 == '3';//false: Integer vs Symbol 3 == \3 ;//false: Integer vs Symbol '3' == "3";//false: Symbol vs String '3' == \3 ;//true: Symbol vs Symbol "3"[0] == 3;//false: Char vs Integer "3"[0] == $3;//true: Char vs Char "3".at(0) == $3;//true: Char vs Char [3][0] == 3;//true: Integer vs Integer [3].at(0) == 3;//true: Integer vs Integer {3} == 3;//false: Function vs Integer {3}.() == 3;//true: Integer vs Integer {3}.(3) == 3;//true: Integer vs Integer (3) == 3;//true: Integer vs Integer (3).() == 3;//true: Integer vs Integer
+演算と++演算
+
演算子は同一index同士で和を取るのに対して、++
演算子は直和である。使えそうな小技としては、[] ++ "123";//-> [ 1, 2, 3 ]
。この式で文字列から配列に変換できる。
[1,2,3] + [4,5,6];//-> [ 5, 7, 9 ] [1,2,3] ++ [4,5,6];//-> [ 1, 2, 3, 4, 5, 6 ] "123" + [];//-> 123 [ ] "123" ++ [];//-> 123[ ] [] + "123";//ERROR: Message '+' not understood. [] ++ "123";//-> [ 1, 2, 3 ] [1,2,3] + "";//ERROR: binary operator '+' failed. [1,2,3] ++ "";//-> [ 1, 2, 3 ] "" + [1,2,3];//-> [ 1, 2, 3 ] "" ++ [1,2,3];//-> [ 1, 2, 3 ] ("foo" + "bar");//-> foo bar "foo" ++ "bar";//-> foobar "foo" + $b;//-> foo b "foo" ++ $b;//-> foob $b + "foo";//ERROR: Message '+' not understood. $b ++ "foo";//-> bfoo
本題
やりたかったことは、「電話番号のような文字列からハイフンを取り除き、一文字ずつ数字にしたものを要素として持つ配列を返す」ことである。CharをIntegerにする、たとえば$5
を5
へと変換するには$5.digit
とすれば良い。以上を踏まえてコードを書くと以下の通り。
コード
( f = {|tel| var arrayfy = [] ++ _ ; var selecter = _.select( _ != $- ); var collecter = _.collect( _.digit ); (collecter <> selecter <> arrayfy).(tel); } ) f.("0190-2345-6789");//-> [ 0, 1, 9, 0, 2, 3, 4, 5, 6, 7, 8, 9 ] f.("881-902-22339-1");//-> [ 8, 8, 1, 9, 0, 2, 2, 2, 3, 3, 9, 1 ] f.("987-8765-4321");//-> [ 9, 8, 7, 8, 7, 6, 5, 4, 3, 2, 1 ]
参考
Char | SuperCollider 3.9dev Help
String | SuperCollider 3.9dev Help
*1:正式名称がよくわからない
幾つかの音を適当なタイミングで鳴らして適当なタイミングで止める
音を止めるには、Synth Nodeに対して
- gate=0を投げてreleaseさせる
- freeする
以外にもありそうだが、今のところ知っているのはこの二つである。
Synth NodeというかSynth NodeのNodeID
Synth Nodeには一意のIDが振られている。よって、NodeIDを指定してしまえばメッセージを送ることができる。
例
SynthDefで定義せず
{SinOsc.ar(440,0,0.1)}.play;
のように関数からplayした場合、
のように名無し楽器が作られ、NodeIDが振られる。(今回はid:1005)
この状態から音を止めるには、以下のようにSeverにメッセージを送る。
s.sendMsg('/n_free',s.nextNodeID - 1); //s.nextNodeIDは1005の「次の」1006となる
本題
「幾つかの音を適当なタイミングで鳴らして適当なタイミングで止める」ことを書いてみる。
コード
//meter and tree (s.meter(0,2);s.plotTree;) //Synth (SynthDef(\ins,{ |amp=0.2,freq=440,gate=1| var sig,e,env; sig = LFTri.ar(freq); e = Env.adsr(attackTime:2,decayTime:0.5,sustainLevel:0.3,releaseTime:3); env = EnvGen.kr(e,gate,doneAction:2); sig = sig * env; Out.ar(0,Pan2.ar(sig,0,amp)); }).add; ) //Routine ( var synth; var nodes = List.new; var n = 5; ~r = Routine({ n.do({ synth = Synth(\ins,[\freq,rrand(60,72).midicps]); nodes.add(synth.nodeID); 0.3.wait; }); n.do({ s.sendMsg('/n_set',nodes.pop,\gate,0); rrand(1,10).wait; }); }); ) //play ~r.reset; //~r.next; ~r.play(TempoClock(2));
学んだこと
- Synthの戻り値はオブジェクト*1であることから、
synth.nodeID
で作成したSynthのnodeIDを取得することができる。 nodes.add(synth.nodeID);
でnodeIDをnodesに格納する。ここで、nodesはArrayではなくListである。- ListはexpandableなCollectionに、ArrayはfixedなCollectionに使うと良い*2。
add
メソッドはListには存在する。Arrayにも存在はするがaddらしくない挙動をするので要注意。 - nodes.popはListの末尾をpopしてdeleteする。
参考
Node | SuperCollider 3.9dev Help
Server Command Reference | SuperCollider 3.9dev Help
gateを0にしてreleaseする
Envelopeにてgate=0でreleaseを作動させるには、SynthDefのargにgateを設定し、なおかつEnvGenにgateを渡す必要がある。これを怠ると、当然ながら.set(\gate,0)
と実行しても音は止まない。
doneAction:2
と設定していると、gateを0にしreleaseが終わったタイミングで、実行したSynthがGroupeから消える。なので、再び実行するときはSynthをServerにnewなりnewPausedなりする必要がある。
gate
This triggers the envelope and holds it open while > 0. If the Env is fixed-length (e.g. Env.linen, Env.perc), the gate argument is used as a simple trigger. If it is an sustaining envelope (e.g. Env.adsr, Env.asr), the envelope is held open until the gate becomes 0, at which point is released. If gate < 0, force release with time -1.0 - gate. See EnvGen: Forced%20release example.
作ったコード
(s.meter(0,2); s.plotTree; ) (SynthDef(\tri, {|out=0,gate=1,amp=0.1,freq=440| var sig,e,eg; e = Env.adsr(attackTime:1,decayTime:3,sustainLevel:0.5,releaseTime:3,peakLevel:1,curve:-8); eg = EnvGen.kr(e,gate,amp,0,1,2); sig = LFTri.ar(freq); sig = sig * eg; Out.ar(out,sig); } ).add; ) ~x = Synth(\tri,[\freq,500]); ~x.set(\gate,0);
参考
Busの練習
Busを介して音を出す、はずであった。作ったコードのoBus
やiBus
でそれらしいことをしたつもりである。しかし、どちらかというと
//use one bus ( ~b1 = Bus.audio(s,1); {Out.ar(0,In.ar(~b1,1))}.play; {Out.ar(~b1,SinOsc.ar(440))}.play; ) //use two busses ( ~b1 = Bus.audio(s,1); ~b2 = Bus.audio(s,1); {Out.ar(0,In.ar(~b1,1))}.play; {Out.ar(~b1,In.ar(~b2,1))}.play; {Out.ar(~b2,LFTri.ar(440))}.play; )
と明示的にBusを作った方が良かったと最後になって気付いた。
メモ
Synth.newPaused
で一時停止した状態のSynthを作ることが出来る。~x = Synth.newPaused(...)
を実行した後に~x.run
と実行すると再生することが出来る。Synth.after(aNode,defName)
でaNodeの後ろにSynthを作ることが出来る。source -> effect
のような流れを作る場合に使う。左右の信号に掛けるLFOの位相をそれぞれ変え(例:左は0, 右はpi/2)ておくと、位相のズレを体感することが出来る(論より証拠)。
作ったコード
s.meter(0,2); s.plotTree; //Triangle Osc ( SynthDef(\tri,{ |freq = 440,oBus = 10| var out,sig; sig = Mix.fill(4,{|i|LFTri.ar(freq/(1+i),0,1/4)}); out = sig; Out.ar(oBus,out); }).add; ) //Effector ( SynthDef(\eff,{ |iBus = 10,afreq=1| var in,in_r,in_l; in = In.ar(iBus); in_r = in * SinOsc.kr(afreq,0,0.2,0.4); in_l = in * SinOsc.kr(afreq,pi/2,0.2,0.4); Out.ar(0,[in_r,in_l]); }).add; ) //kick ( SynthDef(\kick,{ var env = EnvGen.ar(Env(levels:[100,200,60,0],times:[0.01,0.2,0.2,0.2],curve:-10)); var sig = SinOsc.ar(env,0,1,0) + WhiteNoise.ar(0.01); sig = sig * EnvGen.ar(Env.perc(attackTime:0,releaseTime:0.3),doneAction:2); Out.ar(0,sig!2); }).add; ) //Synth ( ~tri = Synth.newPaused(\tri,[\freq,200]); //~eff = Synth(\eff); ~eff = Synth.after(~tri,\eff); ) //run Synth ~tri.run; //Routine ( ~r = Routine({ var n = 10; loop{ n.do({|i| ~eff.set(\afreq,(i+1)); (i+1).postln; 4.wait; }); } }); ~rk = r({loop{Synth(\kick);1.wait;}}); ) ( ~rk.play(); ~r.play(); )
参考
Bus | SuperCollider 3.9dev Help
授業料無償化と発言したのは本当なのだろうか?
授業料無償化?
Twitterが授業料無償化(教育費無償化)で有象無象のごった煮となっていたので、ソースなどを辿る旅に出た記録。
According to NHK
大学在学中は授業料無償化 自民が検討案まとめる | NHKニュース
NHKニュースサイトの動画を見たが、ニュースキャスターの声が大きすぎて自民党教育再生実行本部?の生の声が聞こえなかった。果たして本当に「無償化」と発言したのだろうか?
教育再生実行本部
教育再生実行本部 第八次提言 | 政策 | ニュース | 自由民主党
教育再生実行本部の提言は、サイトを確認すると第八提言(2017年5月18日)は存在するものの(おそらく今回に相当する)第九提言はまだ存在しない。「無償化」発言のエビデンスを追うにはもう少し時間を待つ必要があるかもしれない。
自民党の政策パンフレット
「無償化」では無いが「卒業後拠出金方式」という単語は自民党の政策BANK2017のp.32に記述がある。オーストラリアのHECSを参考にした仕組みらしい。
ベネッセ
直接は関係ないが教育費無償化については、ベネッセのコラムが分かりやすかった。
考えたこと
完全無償化となると財源どうするの?となるし、卒業後拠出金方式だとすると大学の経営不安定になるような気がする。
いずれにしても現状維持より良い選択肢・現状維持より魅力的な解決策には見えないのが残念である。
全然関係ない余談
はてなブログのmarkdownモードにおいて、リンクを挿入する際[URL:title]
だとtitleが自動挿入される。
がしかし、URL参照先がpdfだとtitleを取得してくれないようである。
なので手動でタイトルを打ち込む必要がある。その方法は以下の2つである。
[URL:title=タイトル]
[タイトル](URL)