前回はWeb Audio APIで音を出してみましたが、今回はこれをつかって「猫踏んじゃった」を自動演奏させてみることにします。
楽譜はここを参考にしました:【ドレミ付きあり無料楽譜】童謡_ねこふんじゃった 難易度別4楽譜 – ピアノ塾
Contents
HTML部分
HTML部分を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>「猫踏んじゃった」を自動演奏</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel = "stylesheet" href = "./style.css" type = "text/css" media = "all"> </head> <body> <div id = "container"> <div class = "float-left"> <div>音量:<span id="vol-label">5</span></div> <input type = "range" value = "5" max = "100" min = "1" step = "1" id = "vol"> </div> <div class = "float-left"> <button id = "start-playing">スタート</button> <button id = "stop-playing">ストップ</button> </div> <div class = "both"></div> <div id = "field"></div> </div> <script src = "./index.js"></script> </body> </html> |
style.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#container { margin-top: 10px; width: 360px; } .float-left { float: left; } .both { clear: both; height: 10px; } #start-playing { margin-left: 20px; margin-top: 10px; width: 120px; height: 40px; } #stop-playing { margin-left: 20px; margin-top: 10px; width: 120px; height: 40px; display: none; } #field { position: relative; height: 700px; } .white { width: 320px; height: 35px; position: absolute; background-color: #fff; } .black { width: 200px; height: 35px; position: absolute; background-color: #000; } .floor-name { margin-left: 200px; } |
グローバル変数と定数
グローバル変数と定数を示します。
音と周波数で連想配列を定義します。c4が普通の「ド」です。存在しない音(ミとファ、シとドの間は半音なのでここには黒鍵は存在しない)の部分も鍵盤の配置の都合上いれておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
const hzs = { 'c3': 130, // ひとつ低い「ド」 'c3#': 138, 'd3': 146, // 同「レ」 'd3#': 155, 'e3': 164, // 同「ミ」 'e3#': 0, // 存在しない音(鍵盤の配置の都合上いれておく) 'f3': 174, // 同「ファ」 'f3#': 184, 'g3': 195, // 同「ソ」 'g3#': 207, 'a3': 220, // 同「ラ」 'a3#': 233, 'b3': 246, // 同「シ」 'b3#': 0, // 存在しない音(鍵盤の配置の都合上いれておく) 'c4': 261, // 普通の「ド」 'c4#': 277, 'd4': 294, // 同「レ」 'd4#': 311, 'e4': 330, // 同「ミ」 'e4#': 0, // 存在しない音(鍵盤の配置の都合上いれておく) 'f4': 349, // 同「ファ」 'f4#': 369, 'g4': 392, // 同「ソ」 'g4#': 415, 'a4': 440, // 同「ラ」 }; // Web Audio API関連のグローバル変数 let audioCtx = null; let oscillator = null; let gainNode = null; let vol = 5; // ボリューム(0~100) let stoping = false; // このフラグがtrueになったら演奏中止 // ボタン要素 const $startPlaying = document.getElementById('start-playing'); const $stopPlaying = document.getElementById('stop-playing'); |
ページが読み込まれたときの処理
ページが読み込まれたら鍵盤を生成するとともに、レンジスライダーでボリューム変更ができるようにします。
1 2 3 4 5 6 7 8 |
window.onload = () => { initKeyboards(); initVolume(); $startPlaying.addEventListener('click', () => startPlaying()); $stopPlaying.addEventListener('click', () => stopPlaying()); // startPlaying関数とstopPlaying関数は後述 } |
鍵盤を生成する処理を示します。鍵盤はボタン要素としますが、前回のようにHTMLに直書きしないでこの関数内で動的に生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
function initKeyboards(){ // 連想配列のキーのうち、arr1には白鍵、arr2には黒鍵に対応したものを格納する const arr1 = []; const arr2 = []; for(let key in hzs){ if(key.indexOf('#') == -1) arr1.push(key); else arr2.push(key); } const $field = document.getElementById('field'); let maxBottom = 0; const keyboards = []; for(let i = 0; i < arr1.length; i++){ let top = 40 * i; // 配列のインデックスから白鍵のtopの座標を求める const $button = document.createElement('button'); $button.style.left = '0px'; $button.style.top = top + 'px'; $button.className = 'white'; $button.id = arr1[i]; $button.innerHTML = `<div class = "floor-name">${getName(arr1[i])}</div>`; // 階名を表示する(後述) $field.appendChild($button); keyboards.push($button); maxBottom = top + 40; // 鍵盤のbottomの座標を格納する(最後に格納されたものが最大値になる) } // fieldの高さを確定する(一番下の白鍵のbottom) $field.style.height = maxBottom + 'px'; for(let i = 0; i < arr2.length; i++){ // 存在しない黒鍵はスキップ(bX# eX#なら存在しない) if(arr2[i].indexOf('#') != -1 && (arr2[i].indexOf('b') != -1 || arr2[i].indexOf('e') != -1)) continue; const $button = document.createElement('button'); let top = 40 * i + 20; // 配列のインデックスから黒鍵のtopの座標を求める $button.style.left = '0px'; $button.style.top = top + 'px'; $button.id = arr2[i]; $button.className = 'black'; $field.appendChild($button); keyboards.push($button); } // 自動演奏をするだけなら必要ないがとりあえず押下したら音が出るようにする keyboards.forEach(keyboard => { const hz = hzs[keyboard.id]; if(hz == undefined) return; keyboard.addEventListener('mousedown', () => onKeyDown(keyboard)); keyboard.addEventListener('mouseup', () => onKeyUp(keyboard)); keyboard.addEventListener('touchstart', () => onKeyDown(keyboard)); keyboard.addEventListener('touchend', () => onKeyUp(keyboard)); }); document.addEventListener('mouseup', () => stop()); } |
getName関数は音名(id)から階名を取得します。
1 2 3 4 5 6 7 8 9 |
function getName(id){ if(id.indexOf('c') != -1) return 'ド'; if(id.indexOf('d') != -1) return 'レ'; if(id.indexOf('e') != -1) return 'ミ'; if(id.indexOf('f') != -1) return 'ファ'; if(id.indexOf('g') != -1) return 'ソ'; if(id.indexOf('a') != -1) return 'ラ'; if(id.indexOf('b') != -1) return 'シ'; } |
レンジスライダーでボリュームの調整ができるようにする処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function initVolume(){ const $vol = document.getElementById('vol'); const $volLabel = document.getElementById('vol-label'); if($vol == null || $volLabel == null) return; $vol.addEventListener('input',() => { $volLabel.innerHTML = $vol.value; vol = $vol.value; if(gainNode!=null){ gainNode.gain.value=vol/100; } }); $vol.value = 5; } |
鍵盤が押下されたら音を鳴らす
鍵盤が押下されたら音を鳴らします。
1 2 3 4 5 6 7 |
function onKeyDown(keyboard){ const hz = hzs[keyboard.id]; if(hz == undefined) return; keyboard.style.backgroundColor = '#ff0'; // 押下された鍵盤の色を変える playFromHz(hz); // 後述(実は前回のものと同じ) } |
鍵盤が離されたら音を止めます。
1 2 3 4 5 |
function onKeyUp(keyboard){ // 押下されていた鍵盤の色を元に戻す(白鍵なら白。黒鍵なら黒) keyboard.style.backgroundColor = keyboard.id.indexOf('#') == -1 ? '#fff' :'#000'; stop(); // 後述(実は前回のものと同じ) } |
playFromHz関数は引数で指定された周波数の音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function playFromHz(hz){ if(audioCtx == null) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if(gainNode == null){ gainNode = audioCtx.createGain(); gainNode.gain.value=vol/100; } if(oscillator == null){ oscillator = audioCtx.createOscillator(); oscillator.type = 'square'; oscillator.frequency.setValueAtTime(hz, audioCtx.currentTime); oscillator.connect(gainNode).connect(audioCtx.destination); oscillator.start(); } } function stop(){ if(oscillator!==null){ oscillator.stop(); oscillator=null; } } |
自動演奏をする
playTime関数は音名と拍数を指定すると、その間だけ音を鳴らします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
async function playTime(name, beat){ let elm = null; if(name != ""){ elm = document.getElementById(name); if(elm != null) elm.style.backgroundColor = '#f00'; // 鳴っている音に対応する鍵盤の色を変える const hz = hzs[name]; if(hz != undefined) playFromHz(hz); } await sleep(400 * beat); stop(); if(elm != null) elm.style.backgroundColor = elm.id.indexOf('#') == -1 ? '#fff' :'#000'; // 色を元に戻す await sleep(35); } async function sleep(ms){ return new Promise(resolve => setTimeout(resolve, ms)); } |
「猫踏んじゃった」の楽譜を探してみたら、ここにあったのでここから拝借します。
【ドレミ付きあり無料楽譜】童謡_ねこふんじゃった 難易度別4楽譜 – ピアノ塾
音名と拍数で配列をつくります。空文字の場合は休止です。
1 2 3 4 5 6 7 8 9 |
const data = [ 'd4', 0.5,'c4', 0.5,'f3', 1,'f4', 1,'f4', 1, // レ(半拍)ド(半拍)ファ(1拍)ファ(1拍)ファ(1拍) 'd4', 0.5,'c4', 0.5,'f3', 1,'f4', 1,'f4', 1, 'd4', 0.5,'c4', 0.5,'f3', 1,'f4', 1,'d3', 1, 'f4', 1,'c3', 1,'e4', 1,'e4',1, 'd4', 0.5,'c4', 0.5,'c3', 1,'e4', 1,'e4', 1, 'd4', 0.5,'c4', 0.5,'c3', 1,'e4', 1,'e4', 1, 'd4', 0.5,'c4', 0.5,'c3', 1,'e4', 1,'d3', 1, 'e4', 1,'f3', 1,'f4', 1,'f4',1,'',1, 'f4', 1,'c4', 0.5,'c4', 0.5,'c4#', 1,'c4', 1, '', 1,'e4', 1,'f4', 1,'', ]; |
自動演奏をするときはawaitしながらループを回します。途中でstopingがtrueになったら演奏中止です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function startPlaying(){ // スタートボタンを非表示にして、ストップボタンを表示する stoping = false; $startPlaying.style.display = 'none'; $stopPlaying.style.display = 'block'; for(let i = 0; i < data.length; i += 2){ if(stoping) break; await playTime(data[i], data[i+1]); } // ストップボタンを非表示にしてスタートボタンを表示する $startPlaying.style.display = 'block'; $stopPlaying.style.display = 'none'; } |
ストップボタンが押下されたらstopingフラグをセットします。
1 2 3 |
function stopPlaying(){ stoping = true; } |
東京フレンドパークのフール・オン・ザ・ヒルのようなゲームをつくる
『関口宏の東京フレンドパークII』は、1994年4月11日から2011年3月28日まで、TBS系列で毎週放送されたゲームバラエティ番組です。
フール・オン・ザ・ヒルは演奏者と解答者に分かれ、演奏者はヘッドホンで主旋律を聴きながら、ドラムを模したパッドの光るタイミングと位置と順番を覚えます。そのあと演奏者が覚えた通りに光ったパッドをたたいて主旋律を演奏するのですが、光っていないときに叩くと音は鳴りません。パッドの配置は頭の部分に左右2個ずつ、正面に6個、足で蹴る部分に3個の計13個。演奏を聴いた解答者が曲名を当てるというものです。
これを鍵盤をつかってやろうというわけです。
HTMLは上記のものをそのまま使います。JavaScript部分を加筆します。
1 2 3 4 5 6 7 8 9 10 11 |
function onKeyDown(keyboard){ const hz = hzs[keyboard.id]; if(hz == undefined) return; // 鍵盤が赤くなっていないと音は出ない if(keyboard.style.backgroundColor == 'rgb(255, 0, 0)'){ keyboard.style.backgroundColor = '#ff0'; playFromHz(hz); } } |
changeColor関数は指定された音名の鍵盤を指定された拍数だけ色を変えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async function changeColor(name, beat){ let elm = null; if(name != ""){ elm = document.getElementById(name); if(elm != null) elm.style.backgroundColor = '#f00'; const hz = hzs[name]; } await sleep(400 * 3 * beat); // 速いと無理ゲーになるので3倍の遅さにする stop(); if(elm != null) elm.style.backgroundColor = elm.id.indexOf('#') == -1 ? '#fff' :'#000'; await sleep(40 * 3); } |
ストップボタンが押下されたら1秒後に鍵盤が赤く変わります。タイミングよく押下すると音がなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async function startPlaying(){ stoping = false; $startPlaying.style.display = 'none'; $stopPlaying.style.display = 'block'; await sleep(1000); for(let i = 0; i < data.length; i += 2){ if(stoping) break; await changeColor(data[i], data[i+1]); } $startPlaying.style.display = 'block'; $stopPlaying.style.display = 'none'; } |
さいごのやつは鍵盤が今のままなら下が低い音のほうがよいかと、
音の配置を変えないのであれば黒鍵を右に寄せるのでもいいかも。
たしかにそうですね。