クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【Tone.js】3コード楽器のコードをリファクタリング(ループとイベントと即時関数)

Tone.jsの続きになります。前回作った3コード楽器のコードに重複している部分があったのでリファクタリングしました。クリックイベントをfor文でループしたら思うようにうごかず手こずりました。目指せ全音階&全和音楽器!への布石になった回。それではどうぞ!

【目次】

※前回:【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'];

これによって和音が相対的な表記(配列scalen番目)でも音が出るはず。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から始まるので構わない。011までの番号にした。

//和音
//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]
];

残りの和音FGも同様に書き直す。

さあどうだ?

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]と配列codeMi番目を入れる。

//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番目と配列codeMj`番目を足した数。
  • 配列chordsi番目にnmbpush()で追加する。

これで配列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回
  • どのキーボードを押したときにクリックイベントを鳴らすか。配列Keysi番目を押した時に鳴らしたい。
  • どのコードを鳴らすか。配列chordsi番目を鳴らしたい

さっきと同じ要領で、共通の部分はそのままに、値が変わっているところだけを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番目という相対的な書き方になった。これによって全音階&全和音を無限増殖しなくて済む見込みが立ったと思う。

最後に

f:id:idr_zz:20190630233916j:plain

ということで、3コード楽器をリファクタリングしました。今回はアウトプットが何も変わっていないので地味な作業ですが、これをサナギ期間として、これから全音階&全和音楽器作成に向かっていきたいと思います♪それではまた!


※参考:Tone.jsを習得するためにやった事まとめ qiita.com