クモのようにコツコツと

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

【tone.js】一音鍵盤を1オクターブに拡張したらコードが冗長だったので書き直した

ブラウザで音を鳴らせるJSライブラリ「tone.js」の続きになります。
前回は一音鍵盤を作りましたが、一音だと少々飽きてくるので1オクターブに拡張しました!
鍵盤を増やしながらコードが冗長になっていくことに気づき、このままではフルキーボードの頃にはすごいことになるのでメンテしやすい書き方を検討しました。では、どうぞ。

※目次:

※参考:前回はこちら
はじめの一音!tone.js事始める - クモのようにコツコツと

前回のおさらい!一音鍵盤

まずは前回の成果物である一音鍵盤を振り返る。こんな楽器です。え、ええ。楽器ですってこれは…。

HTMLには一音鍵盤。うむ。潔き。

<div id="piano">
   <div id="do"></div>
</div>

JSはこんなです。

//DOM
var btn = document.getElementById("do");

//イベントリスナ
btn.addEventListener("click", function () { 
//シンセ生成
var synth = new Tone.Synth().toMaster();
//ドが8分音符の長さ鳴る
synth.triggerAttackRelease("C4", "8n"); 
}, false);
  • まずDOMでHTMLの鍵盤#doを取得。
  • イベントリスナで#doのクリックイベントを作成。
  • 変数synthにtone.jsに用意されたシンセSynth()をインスタンス化し、シンセ生成。音色はtoMaster()
  • #doをクリックするとtriggerAttackRelease()で音がなる。ド(C4)を8分音符(8n)の長さ。

ではこの鍵盤を増やしていきましょう。

まずは3音に増やしてみる!

まずHTMLの鍵盤を3つに増やす。今度はドミソのCメジャーコードを弾き放題の楽器を作る!
ちなみにID名は、tone.jsの音階がドレミではなくCDEのため、c4などに変えました。

<div id="piano">
   <div id="c4" class="w_key"></div>
   <div id="e4" class="w_key"></div>
   <div id="g4" class="w_key"></div>
</div>

次にJSでDOMに三つの鍵盤を読み出す。

//DOM
var btnDo = document.getElementById("c4");
var btnMi = document.getElementById("e4");
var btnSo = document.getElementById("g4");

前回はシンセの生成をイベントリスナの中で行ってましたが、この生成は最初にやればいいのでブロック文の外に出しました。

//シンセ生成
var synth = new Tone.Synth().toMaster();

//イベントリスナ(ド)
btnDo.addEventListener("click", function () { 
//ドが8分音符の長さ鳴る
synth.triggerAttackRelease("C4", "8n"); 
}, false);

さあ、残り二つの楽器も作ってみよう。「ド」ベースで音階部分を打ち替え。

//イベントリスナ(ミ)
btnMi.addEventListener("click", function () { 
//ミが8分音符の長さ鳴る
synth.triggerAttackRelease("E4", "8n"); 
}, false);

//イベントリスナ(ソ)
btnSo.addEventListener("click", function () { 
//ソが8分音符の長さ鳴る
synth.triggerAttackRelease("G4", "8n"); 
}, false);

できたものがこれ!

おお!鳴ってます。Cメジャー弾き放題です。明るい響き。ドとミでラッパのマークの正露丸のメロディが作れることを発見。

1オクターブに拡張!

乗ってきたのでどんどんいきませう。1オクターブに拡張!ドレミ各音階と次のオクターブのド(C5)まで。

<ul id="piano">
   <li id="C4" class="w_key"></li>
   <li id="D4" class="w_key"></li>
   <li id="E4" class="w_key"></li>
   <li id="F4" class="w_key">ファ</li>
   <li id="G4" class="w_key"></li>
   <li id="A4" class="w_key"></li>
   <li id="B4" class="w_key"></li>
   <li id="C5" class="w_key"></li>
</ul>

DOMも拡張!鍵盤のイベントリスナも拡張!

//DOM
var btnC4 = document.getElementById("C4");
var btnD4 = document.getElementById("D4");
var btnE4 = document.getElementById("E4");
var btnF4 = document.getElementById("F4");
var btnG4 = document.getElementById("G4");
var btnA4 = document.getElementById("A4");
var btnB4 = document.getElementById("B4");
var btnC5 = document.getElementById("C5");


//シンセ生成
var synth = new Tone.Synth().toMaster();

//ド(下)が8分音符の長さ鳴る
btnC4.addEventListener("click", function () { 
synth.triggerAttackRelease("C4", "8n"); 
}, false);

//レが8分音符の長さ鳴る
btnD4.addEventListener("click", function () { 
synth.triggerAttackRelease("D4", "8n"); 
}, false);

//ミが8分音符の長さ鳴る
btnE4.addEventListener("click", function () { 
synth.triggerAttackRelease("E4", "8n"); 
}, false);

//ファが8分音符の長さ鳴る
btnF4.addEventListener("click", function () { 
synth.triggerAttackRelease("F4", "8n"); 
}, false);

//ソが8分音符の長さ鳴る
btnG4.addEventListener("click", function () { 
synth.triggerAttackRelease("G4", "8n"); 
}, false);

//ラが8分音符の長さ鳴る
btnA4.addEventListener("click", function () { 
synth.triggerAttackRelease("A4", "8n"); 
}, false);

//シが8分音符の長さ鳴る
btnB4.addEventListener("click", function () { 
synth.triggerAttackRelease("B4", "8n"); 
}, false);

//ド(上)が8分音符の長さ鳴る
btnC5.addEventListener("click", function () { 
synth.triggerAttackRelease("C5", "8n"); 
}, false);

でけた。

おお!1オクターブ、なってます!正直さっきのCメジャーはもどかしさがあったので開放感。
でも、見た目、まだ物足りなさがありますよね?そう。一オクターブは本来12音階あり、「黒鍵」にある半音がまだ抜けています。さあ、ここからさらに黒鍵も足してピアノらしい鍵盤を完成させましょう〜。

っと思ってコードを見ていて気がついたのでが……これってすっごく…

じょ、冗長やないかぁ〜〜〜〜〜い!!!!(号泣)

確かにオクターブの音階を鳴らすことは実現できているのだが、コードを見るとドレミファ…の音階名以外のところが同じような内容で何回も書かれている。今後のメンテナンスを考えると使い勝手が悪そうです。この調子で数オクターブのグランドピアノを再現したらどんどんコードが肥大化するな。うーむ、なんとかしたい。。

querySelectorAll()でliタグを配列に入れる

もっとこう共通部分が汎用的にまとまってメンテナンスや拡張や変更がしやすい書き方に改変できないかなぁ。

//DOM
var btnC4 = document.getElementById("C4");

//ド(下)が8分音符の長さ鳴る
synth.triggerAttackRelease("C4", "8n"); 

DOMとtone.jsのメソッドを比較。ID名C4を文字列として取得してtriggerAttackRelease()の第一引数に代入できれば解決するような気がするなぁ。

まず、肥大化したDOMを書き換えてみる。getElementById()はIDしか取得できないが、JSには他にもCSSやjQueryみたいな書式でセレクタを取得できる「クエリセレクタ」というDOMがありました。クエリセレクタはquerySelector()querySelectorAll()の二種類あります。

  • querySelector()は単一のセレクタを取得したいときに使う。
  • querySelectorAll()は同じ種類のセレクタが複数あるときに全部取得して配列[]に入れる。

今回はliが8つなのでquerySelectorAll()ですね。よし、これでリストliを取得してみよう。

//DOM
var Key = document.querySelectorAll('#piano li');

これで#pianoの中のliを全部取得できたはず!

for文で鍵盤と配列を紐づける。

次にこの配列Keyforループで数えて実行したい。

//鍵盤数分ループ
for (var i = 0; i < Key.length; i++) {
Key[i].addEventListener("click", function () { 
    //何か処理をする
}, false);
}
  • 条件1:変数i0で開始する。
  • 条件2:lengthで配列Keyの件数を取得。iKeyの件数になるまで繰り返す。
  • 条件 3:++で一回実行するごとにiに+1ずつ加算する。
  • 配列Keyi番目のキーをクリックしたときに処理を実行する。

for文に入れることで鍵盤をクリックするとその鍵盤が配列Keyの何番目かを見に行き、その数をiに代入してくれるわけです。

liタグ内のID名を取得する

次はliの中のID名を取得したい。ちゃんと文字列として取得できるか、まずはアラートalert()で確かめる。
えーっと、IDの取得はgetElementById()かな?変数idName作ってthis(=クリックした要素)に繋げてみる。どうだ!

//鍵盤数分ループ
for (var i = 0; i < Key.length; i++) {
Key[i].addEventListener("click", function () { 
    //ID名取得
    var idName = this.getElementById();
    //アラート表示
    alert(idName);
}, false);
}

結果は…シーーーン。へんじがない。ただのしかばねのようだ。

そりゃそうだよね。getできそうな名前であはあるが、thisはもともとKeyであり、Keyはもともとdocument.querySelectorAll('#piano li')document.querySelectorAll('#piano li').getElementById()とDOMを二つ繋げてるようなもんだからな。

ID名の取得の他の方法をついて調べたところidプロパティを繋げるといいようです!シンプル!この書き方はclass名や他の属性もいろいろ取得できそうだな。覚えておきましょう。

※参考:Element.id - id属性を取得、変更する

さっそくthis.idに書き換えてみる。

//鍵盤数分ループ
for (var i = 0; i < Key.length; i++) {
Key[i].addEventListener("click", function () { 
    //ID名取得
    var idName = this.id;
    //アラート表示
    alert(idName);
}, false);
}

どうかな?

おお!!「C4」「D4」などID名のアラートが現れた!

triggerAttackRelease()メソッドにID名を入れる

このID名idNameをtone.jsのtriggerAttackRelease()メソッドの第一引数に入れることができれば音が鳴りそうな気がするぞ!なんだかいけそうな気がする〜(ドキドキ)。

//鍵盤数分ループ
for (var i = 0; i < Key.length; i++) {
Key[i].addEventListener("click", function () { 
    //ID名取得
    var idName = this.id;
    //音階を代入
    synth.triggerAttackRelease(idName, "8n"); 
}, false);
}

さあどうだ!!?

やた!鳴った!!!

先ほどの冗長コードと挙動は何も変わらないのですが、コード量は大幅に減ったのではなかろうか。音階名をいちいち打たなくていいのが素敵すぎです。

黒鍵増やしてもJSは手付かずでOK!

さあ、こうなると理論上はさらに黒鍵を増やしてもJSは何も手を加えないまま音が鳴ってくれるはずです。
さっそくHTMLに黒鍵を増やしてみた。

<ul id="piano">
   <li id="C4" class="w_key"></li>
   <li id="C#4" class="b_key"></li> 
   <li id="D4" class="w_key"></li>
   <li id="D#4" class="b_key"></li> 
   <li id="E4" class="w_key"></li>
   <li id="F4" class="w_key">ファ</li>
   <li id="F#4" class="b_key"></li>
   <li id="G4" class="w_key"></li>
   <li id="G#4" class="b_key"></li>
   <li id="A4" class="w_key"></li>
   <li id="A#4" class="b_key"></li>
   <li id="B4" class="w_key"></li>
   <li id="C5" class="w_key"></li>
</ul>
  • tone.jsの黒鍵はC#4など、音階とオクターブ数の間にシャープ#を入れる書き方だったので、それをそのままID名に入れました。
  • CSSで鍵盤色や位置を変えるため、黒鍵には.b_keyというclass名をつけました。

これでJSは手付かずのまま黒鍵の音は鳴ってくれるはず。どうだ!

おお!やた!鳴った鳴った〜黒鍵が鳴った!
最終的なJSコードはこんなです。

//DOM
var Key = document.querySelectorAll('#piano li');

//シンセ生成
var synth = new Tone.Synth().toMaster();

//鍵盤数分ループ
for (var i = 0; i < Key.length; i++) {
Key[i].addEventListener("click", function () { 
    //ID名取得
    var idName = this.id;
    //音階を代入
    synth.triggerAttackRelease(idName, "8n"); 
}, false);
}

ね?コンパクトになったでしょ! やろうと思えばここからさらにグランドピアノ並みに鍵盤を増やしたとしてもJSは何も変える必要がないってことです。これは嬉しい!!

最後に

ということで、鍵盤を1オクターブに増やしつつ、メンテしやすいコードに書き直しました。満足できた。
完成してから音が同時に発声できない(和音を鳴らせない)ことに気づいた。スマホでもダメでした。今後の課題です。
さて次回は、このままグランドピアノばりに鍵盤を増やしてもいいのだが、シンセの音色を変えたり、メロディを奏でるのも興味あるのでそっちに進もうかな、と考え中。


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