Tone.jsの続きになります。前回作った3コード楽器のコードに重複している部分があったのでリファクタリングしました。クリックイベントをfor文でループしたら思うようにうごかず手こずりました。目指せ全音階&全和音楽器!への布石になった回。それではどうぞ!
【目次】
- 前回のコードのままだと完成(全音階&全和音)まで果てしない
- 音階を配列に入れ、和音にも配列の値を入れる
- ルート音を連想配列に入れ、ルート音に相対的に和音を足す
- 和音を中身をループに入れる
- 二重for文で3コードを一つにまとめる
- イベントをループ化するために即時関数に入れる
- 最後に
※前回:【Tone.js】PolySynth()で和音を鳴らす(3コード楽器を作る) - クモのようにコツコツと
前回のコードのままだと完成(全音階&全和音)まで果てしない
前回、3コードを演奏する楽器を作った。
See the Pen tone.js-3cord by イイダリョウ (@i_ryo) on CodePen.
JSコード
//DOM var Key_C = document.querySelector('#Key_C'); var Key_F = document.querySelector('#Key_F'); var Key_G = document.querySelector('#Key_G'); //和音 var C_chord = ['C4', 'E4', 'G4']; var F_chord = ['F4', 'A4', 'C5']; var G_chord = ['G4', 'B4', 'D5']; //シンセ生成 var synth = new Tone.PolySynth().toMaster(); //イベントリスナ(C) Key_C.addEventListener('click', function () { //ドが8分音符の長さ鳴る synth.triggerAttackRelease(C_chord, '4n'); }, false); //イベントリスナ(F) Key_F.addEventListener('click', function () { //ミが8分音符の長さ鳴る synth.triggerAttackRelease(F_chord, "4n"); }, false); //イベントリスナ(G) Key_G.addEventListener('click', function () { //ソが8分音符の長さ鳴る synth.triggerAttackRelease(G_chord, "4n"); }, false);
コード(和音)のところを見ていただくと、一つ一つのコードに['C4', 'E4', 'G4']
などの固有の音符を打っているのがわかる。
このままだと全音階(ド、ミ、ソ以外の音階もある)や全和音(上の例は○メジャーコードだが和音は他にも○マイナー、○セブンス、など無数の種類がある)の楽器を作ろうとしたときに果てしないことになってしまう。。
ルート音から相対的に和音の位置を加算する書き方に変えたいっ!
音階を配列に入れ、和音にも配列の値を入れる
書き直した。(見た目や挙動は変化なし)
//音階 var scale = [ //休符 'null', //1オクターブ目 'C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4', //2オクターブ目 'C5', 'C#5', 'D5', 'D#5', 'E5', 'F5', 'F#5', 'G5', 'G#5', 'A5', 'A#5', 'B5']
まず変数scale
にあらかじめ音階を入れる。ルート音が上の方にいくと和音は次のオクターブに差し掛かるはずなので、2オクターブ分入れている。頭に入れている休符null
の出番があるかはまだわからないが、JSはカウントが0
からのため、ルート音から直感的にカウントできるよう頭にnull
を入れることでルート音が1
になった。
//和音 //var C_chord = ['C4', 'E4', 'G4']; var C_chord = [ scale[1], scale[5], scale[8]]; var F_chord = ['F4', 'A4', 'C5']; var G_chord = ['G4', 'B4', 'D5'];
これによって和音が相対的な表記(配列scale
のn
番目)でも音が出るはず。1番目のCコードC_chord
だけ配列にしてみた。
さあどうだ?
See the Pen tone.js-3cord-2 by イイダリョウ (@i_ryo) on CodePen.
おお!音がちゃんと出ている!
ルート音を連想配列に入れ、ルート音に相対的に和音を足す
次のコードを作りたいが、Cコードからルート音が平行移動して、その上に相対的に和音が足されているので、ルート音を基準にしたい。
//ルート音 var root = {C: 0, Cs: 1, D: 2, Ds: 3, E: 4, F: 5, Fs: 6, G: 7, Gs: 8, A: 9, As: 10, B: 11}
まずルート音を連想配列に入れる。キーは音階名だがシャープ#
は変数名に使えないっぽいので一文字目のs
とした。キーはCを1番目にするにあたり+1
で表現したいため、0
から始まるので構わない。0
〜11
までの番号にした。
//和音 //var C_chord = ['C4', 'E4', 'G4']; var C_chord = [ scale[root['C']+1], scale[root['C']+5], scale[root['C']+8] ];
和音部分の修正。配列scale[]
の中に連想配列root['音階名'+n]
が入っている。n
はルート音から足される和音の音程数。Cメジャーはルート音が1番目+1
とすると5番目+5
、8番目+8
になる。
//var F_chord = ['F4', 'A4', 'C5']; var F_chord = [ scale[root['F']+1], scale[root['F']+5], scale[root['F']+8] ]; //var G_chord = ['G4', 'B4', 'D5']; var G_chord = [ scale[root['G']+1], scale[root['G']+5], scale[root['G']+8] ];
残りの和音F
、G
も同様に書き直す。
さあどうだ?
See the Pen tone.js-3cord-3 by イイダリョウ (@i_ryo) on CodePen.
やた!ちゃんと鳴った!
和音を中身をループに入れる
和音の中身が3行ともよく似ているのでループにしたい。すべてに一致しているのは+1
、+5
、+8
の部分。
//メジャースケール var codeM =[1,5,8];
あらかじめメジャースケースの音程1,5,8
を配列codeM
に入れておく。
//和音 //var C_chord = ['C4', 'E4', 'G4']; var C_chord = []; for (var i = 0; i < codeM.length; i++){ var nmb = scale[root['C']+codeM[i]]; C_chord.push(nmb); }
for文の条件、ループ回数は配列codeM
の数length
(3個なので3回)。ループの中はscale[root['C']+codeM[i]
と配列codeM
のi
番目を入れる。
//var F_chord = ['F4', 'A4', 'C5']; var F_chord = []; for (var i = 0; i < codeM.length; i++){ var nmb = scale[root['F']+codeM[i]]; F_chord.push(nmb); } //var G_chord = ['G4', 'B4', 'D5']; var G_chord = []; for (var i = 0; i < codeM.length; i++){ var nmb = scale[root['G']+codeM[i]]; G_chord.push(nmb); }
残りのFコード、Gコードも同じように書き直す。
さあ、どうだ?
See the Pen tone.js-3cord-4 by イイダリョウ (@i_ryo) on CodePen.
鳴りました♪
二重for文で3コードを一つにまとめる
さて、コードの中身がすっきりしたところで、3コード自体も似たようなfor文が並んでいることに気づく。これを一つにまとめたい。
//3コード var threeCode = [root['C'],root['F'],root['G']]
まず配列threeCode
に3コードのルート音を入れる。
var C_chord = []; var F_chord = []; var G_chord = []; var chords = [C_chord,F_chord,G_chord]
次にコードも配列化したいが、中身はfor文で入れるので、空っぽの配列でいい。最後に配列chords
にコードを入れる。これでchords
のn番目、という呼び出し方ができる。
和音を作るfor文。
//和音 for (var i = 0 ; i < codeM.length; i++ ) { //var C_chord, F_chord, G_chord for (var j = 0; j < codeM.length; j++){ var nmb = scale[threeCode[i]+codeM[j]]; chords[i].push(nmb); } }
- 先ほどのfor文の外側をfor文で囲み二重for文にする。条件の変数は識別のため外側は
i
、内側はj
としておく。 - 外側も内側も
codeM
の数(3和音なので3回)だけループ。 - 内側で変数
nmb
に配列scale
を入れる。何番目の値を呼び出すかは、配列threeCodeの
i番目と配列
codeMの
j`番目を足した数。 - 配列
chords
のi
番目にnmb
をpush()
で追加する。
これで配列cords
の中の3コードにルート音と和音が追加されていく。
//イベントリスナ(C) Key_C.addEventListener('click', function () { //ドが8分音符の長さ鳴る synth.triggerAttackRelease(chords[0], '4n'); }, false);
クリックイベントのtriggerAttackRelease()
の第一引数を先ほどのループで作られた和音chords[]
の1番目(0
)を入れる。
//イベントリスナ(F) Key_F.addEventListener('click', function () { //ミが8分音符の長さ鳴る synth.triggerAttackRelease(chords[1], "4n"); }, false); //イベントリスナ(G) Key_G.addEventListener('click', function () { //ソが8分音符の長さ鳴る synth.triggerAttackRelease(chords[2], "4n"); }, false);
同様に他の和音もchords[]
の2番目(1
)、3番目(2
)を入れる。
See the Pen tone.js-3cord-5 by イイダリョウ (@i_ryo) on CodePen.
はい、無事に鳴りました。
イベントをループ化するために即時関数に入れる
さて最後。演奏するクリックイベントも似たようなコードが並んでいるため、for文に入れてひとまとめにしたい。
//DOM var Key_C = document.querySelector('#Key_C'); var Key_F = document.querySelector('#Key_F'); var Key_G = document.querySelector('#Key_G'); var Keys = [Key_C, Key_F, Key_G]
冒頭のキーボードのDOMを配列Keys
に入れてみる。これによってKeys
のn番目、という表現になると思い。
イベントリスナをループ化する。
//イベントリスナ for (var i = 0; i < Keys.length; i++) { Keys[i].addEventListener('click', function () { //3コードが8分音符の長さ鳴る synth.triggerAttackRelease(chords[i], '4n'); }, false); }
- for文の条件、ループ回数は配列
Keys
の値の数。3個なので3回 - どのキーボードを押したときにクリックイベントを鳴らすか。配列
Keys
のi
番目を押した時に鳴らしたい。 - どのコードを鳴らすか。配列
chords
のi
番目を鳴らしたい
さっきと同じ要領で、共通の部分はそのままに、値が変わっているところだけをi
番目、に置き換える感じですね。OKOK。
…と思ったら、鳴らない!!ウンともスンとも言わない!デベロッパーツールのコンソールでも特にエラーは見つからず摩訶不思議。
for文の中にデバッグのconsole.log(i)
を仕掛けてみたところ3
となっている。ループの最後の1回しか実行されていない。それにしても、その3番目の音すら鳴らないんだけどなー。。
調べると、for文の中のクリックイベントはハマりどころのようで、変数スコープやクロージャを理解せずに書くとうまく動かない。
これはうまく動きません。なぜなら、iを含めたfor文内の各変数はグローバルスコープになるため、それぞれのiに対する値になっていないからです。これも即時関数で囲ってしまいましょう。
この記事がとても詳しくてわかりやすかった。
※参考:JavaScriptから即時関数を排除する - Qiita
この記事で対応策としてあげられている「即時関数」はJS独特の記法で最終的には排除するのが望ましいようだが、今回はこの即時関数で解決できた。
//即時関数 (function(i) { //処理 })(i);
無名関数の入ったカッコのすぐ後にカッコが続く不思議な書き方。
for文でのそれぞれのループにおいてiを含めたそれぞれの変数は別々になります。これでうまく動作することでしょう。
//イベントリスナ for (var i = 0; i < Keys.length; i++) { (function(i) { Keys[i].addEventListener('click', function () { //3コードが8分音符の長さ鳴る synth.triggerAttackRelease(chords[i], '4n'); }, false); })(i); }
クリックイベントを即時関数で囲うとこうなる。
さあどうだ?
See the Pen tone.js-3cord-6 by イイダリョウ (@i_ryo) on CodePen.
やった!鳴りました!!
最終的なコードはこうなった。
//DOM var Key_C = document.querySelector('#Key_C'); var Key_F = document.querySelector('#Key_F'); var Key_G = document.querySelector('#Key_G'); var Keys = [Key_C, Key_F, Key_G] //音階 var scale = [ //休符 'null', //1オクターブ目 'C4', 'C#4', 'D4', 'D#4', 'E4', 'F4', 'F#4', 'G4', 'G#4', 'A4', 'A#4', 'B4', //2オクターブ目 'C5', 'C#5', 'D5', 'D#5', 'E5', 'F5', 'F#5', 'G5', 'G#5', 'A5', 'A#5', 'B5']; //ルート音 var root = {C: 0, Cs: 1, D: 2, Ds: 3, E: 4, F: 5, Fs: 6, G: 7, Gs: 8, A: 9, As: 10, B: 11}; //メジャースケール var codeM =[1,5,8]; //3コード var threeCode = [root['C'],root['F'],root['G']] var C_chord = []; var F_chord = []; var G_chord = []; var chords = [C_chord,F_chord,G_chord] //和音 for (var i = 0 ; i < codeM.length; i++ ) { //var C_chord, F_chord, G_chord for (var j = 0; j < codeM.length; j++){ var nmb = scale[threeCode[i]+codeM[j]]; chords[i].push(nmb); } } //シンセ生成 var synth = new Tone.PolySynth().toMaster(); //イベントリスナ for (var i = 0; i < Keys.length; i++) { (function(i) { Keys[i].addEventListener('click', function () { //3コードが8分音符の長さ鳴る synth.triggerAttackRelease(chords[i], '4n'); }, false); })(i); }
記述量はあまり減っていないというかむしろ増えているのだが(汗)、重複したような書き方は減って、ルート音からの和音もi
番目という相対的な書き方になった。これによって全音階&全和音を無限増殖しなくて済む見込みが立ったと思う。
最後に
ということで、3コード楽器をリファクタリングしました。今回はアウトプットが何も変わっていないので地味な作業ですが、これをサナギ期間として、これから全音階&全和音楽器作成に向かっていきたいと思います♪それではまた!
※参考:Tone.jsを習得するためにやった事まとめ qiita.com