WEB Piano – HTML5 Audioでピアノを作る

HTML5 Audioを使ってWEB Pianoを作ってみました。

  • PCキーボードでの演奏
  • マウスでの演奏
  • タッチでの演奏

ができます。ぜひ遊んでみてください。

Web Piano

Q
 
2
 
W
 
E
 
4
 
R
 
5
 
T
 
Y
 
7
 
U
 
8
 
I
Z
9
S
O
X
P
C

F
@
V
^
G
[
B
N
J
M
K
,
L
.
/
:
\

ソースコード

html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Piano</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="piano-container">
        <div id="piano-wrap">
            <div class="piano-key white-key" data-key-num="0"><span class="key-label">Q<br> </span></div><!-- ラ -->
            <div class="piano-key black-key" data-key-num="1"><span class="key-label">2<br> </span></div><!-- ラ# -->
            <div class="piano-key white-key" data-key-num="2"><span class="key-label">W<br> </span></div><!-- シ -->
            <div class="piano-key white-key" data-key-num="3"><span class="key-label">E<br> </span></div><!-- ド -->
            <div class="piano-key black-key" data-key-num="4"><span class="key-label">4<br> </span></div><!-- ド# -->
            <div class="piano-key white-key" data-key-num="5"><span class="key-label">R<br> </span></div><!-- レ -->
            <div class="piano-key black-key" data-key-num="6"><span class="key-label">5<br> </span></div><!-- レ# -->
            <div class="piano-key white-key" data-key-num="7"><span class="key-label">T<br> </span></div><!-- ミ -->
            <div class="piano-key white-key" data-key-num="8"><span class="key-label">Y<br> </span></div><!-- ファ -->
            <div class="piano-key black-key" data-key-num="9"><span class="key-label">7<br> </span></div><!-- ファ# -->
            <div class="piano-key white-key" data-key-num="10"><span class="key-label">U<br> </span></div><!-- ソ -->
            <div class="piano-key black-key" data-key-num="11"><span class="key-label">8<br> </span></div><!-- ソ# -->
            <div class="piano-key white-key" data-key-num="12"><span class="key-label">I<br>Z</span></div><!-- ラ -->
            <div class="piano-key black-key" data-key-num="13"><span class="key-label">9<br>S</span></div><!-- ラ# -->
            <div class="piano-key white-key" data-key-num="14"><span class="key-label">O<br>X</span></div><!-- シ -->
            <div class="piano-key white-key" data-key-num="15"><span class="key-label">P<br>C</span></div><!-- ド -->
            <div class="piano-key black-key" data-key-num="16"><span class="key-label">-<br>F</span></div><!-- ド# -->
            <div class="piano-key white-key" data-key-num="17"><span class="key-label">@<br>V</span></div><!-- レ -->
            <div class="piano-key black-key" data-key-num="18"><span class="key-label">^<br>G</span></div><!-- レ# -->
            <div class="piano-key white-key" data-key-num="19"><span class="key-label">[<br>B</span></div><!-- ミ -->
            <div class="piano-key white-key" data-key-num="20"><span class="key-label">N</span></div><!-- ファ -->
            <div class="piano-key black-key" data-key-num="21"><span class="key-label">J</span></div><!-- ファ# -->
            <div class="piano-key white-key" data-key-num="22"><span class="key-label">M</span></div><!-- ソ -->
            <div class="piano-key black-key" data-key-num="23"><span class="key-label">K</span></div><!-- ソ# -->
            <div class="piano-key white-key" data-key-num="24"><span class="key-label">,</span></div><!-- ラ -->
            <div class="piano-key black-key" data-key-num="25"><span class="key-label">L</span></div><!-- ラ# -->
            <div class="piano-key white-key" data-key-num="26"><span class="key-label">.</span></div><!-- シ -->
            <div class="piano-key white-key" data-key-num="27"><span class="key-label">/</span></div><!-- ド -->
            <div class="piano-key black-key" data-key-num="28"><span class="key-label">:</span></div><!-- ド# -->
            <div class="piano-key white-key" data-key-num="29"><span class="key-label">\</span></div><!-- レ -->
        </div>
    </div>
    <script src="main.js"></script>
</body>
</html>

style.css

#piano-container {
    margin: 0 auto;
    width: calc(100% - 40px);
    height: 400px;
    overflow: auto;
}

#piano-wrap * {            
    box-sizing: border-box;
    font-family: Arial;
    user-select: none;
}

#piano-wrap {
    margin: 0 auto;
    height: 330px;
    width: calc(46px * 18);
    display: flex;
    justify-content: center;
}

#piano-wrap > div {
    position: relative;
}

.white-key {
    width: 46px;
    height: 320px;
    background-color: white;
    border: solid 1px black;
    z-index: 1;
    border-bottom: solid rgb(230, 230, 230) 20px;
    box-shadow: 0 7px 3px 0 rgba(0, 0, 0, 0.3);
    transition: 100ms;
    color: black;
}

.white-key.pressing {
    border-bottom: solid rgb(230, 230, 230) 5px;
    border-left:solid rgb(109, 109, 76) 2px;
    border-right:solid rgb(109, 109, 76) 2px;
    box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.3);
}

.black-key {
    width: 24px;
    height: 190px;
    background: linear-gradient(to bottom, rgb(24, 24, 24) 97%, white);
    margin-left: -12px;
    margin-right: -12px;
    z-index: 2;
    border-bottom: solid rgb(54, 54, 54) 10px;
    border-left: solid black 3px;
    border-right: solid black 3px;
    box-shadow: 5px 1px 2px 0 rgba(0, 0, 0, 0.4);
    transition: 100ms;
    color: white;
    text-align: center;
}

.black-key.pressing {
    border-bottom: solid rgb(54, 54, 54) 4px;
    box-shadow: 2px 1px 2px 0 rgba(0, 0, 0, 0.4);
    background: linear-gradient(to bottom, rgb(24, 24, 24) 100%, white);
}

.key-label {
    position: absolute;
    display: block;
    bottom: 10px;
    width: 100%;
    text-align: center;
}

main.js

// 変数宣言
const path = "./audio/"             // オーディオファイルのパス
const keyMap = [
    { pcKey: "q", pianoKey: 0 },{ pcKey: "2", pianoKey: 1 },{ pcKey: "w", pianoKey: 2 },{ pcKey: "e", pianoKey: 3 },{ pcKey: "4", pianoKey: 4 },{ pcKey: "r", pianoKey: 5 },{ pcKey: "5", pianoKey: 6 },{ pcKey: "t", pianoKey: 7 },{ pcKey: "y", pianoKey: 8 },{ pcKey: "7", pianoKey: 9 },{ pcKey: "u", pianoKey: 10 },{ pcKey: "8", pianoKey: 11 },{ pcKey: "i", pianoKey: 12 },{ pcKey: "9", pianoKey: 13 },{ pcKey: "o", pianoKey: 14 },{ pcKey: "p", pianoKey: 15 },{ pcKey: "-", pianoKey: 16 },{ pcKey: "@", pianoKey: 17 },{ pcKey: "^", pianoKey: 18 },{ pcKey: "[", pianoKey: 19 },{ pcKey: "z", pianoKey: 12 },{ pcKey: "s", pianoKey: 13 },{ pcKey: "x", pianoKey: 14 },{ pcKey: "c", pianoKey: 15 },{ pcKey: "f", pianoKey: 16 },{ pcKey: "v", pianoKey: 17 },{ pcKey: "g", pianoKey: 18 },{ pcKey: "b", pianoKey: 19 },{ pcKey: "n", pianoKey: 20 },{ pcKey: "j", pianoKey: 21 },{ pcKey: "m", pianoKey: 22 },{ pcKey: "k", pianoKey: 23 },{ pcKey: ",", pianoKey: 24 },{ pcKey: "l", pianoKey: 25 },{ pcKey: ".", pianoKey: 26 },{ pcKey: "/", pianoKey: 27 },{ pcKey: ":", pianoKey: 28 },{ pcKey: "\\", pianoKey: 29 }
]                                   // PCキーとピアノ鍵盤番号の紐づけ
const pianoSounds = []              // Audioオブジェクト        
const touchKeyNumlist = []          // タッチ中の鍵盤番号リスト
let clickedKeyNum = null            // クリック中の鍵盤番号リスト
const isKeyPressing = new Array(30) // ピアノ鍵盤ごとの押下状態
isKeyPressing.fill(false)           // 初期値 = false            
const intervalIds = new Array(30)   // 各オーディオフェードアウトのインターバルID
intervalIds.fill(null)              // 初期値 = null           
const pianoWrap = document.getElementById("piano-wrap")     // 鍵盤全体
const whiteKeys = document.querySelectorAll(".white-key")   // 白鍵
const blackKeys = document.querySelectorAll(".black-key")   // 黒鍵

// 初期処理
// Audioオブジェクトを作成セット
for ( i = 0; i <= 29; i++ ){
    let sound = new Audio( path + i + ".mp3" )
    sound.volume = 0
    pianoSounds.push(sound)
}
// タッチ対応判定
if (window.ontouchstart === null) {
    // タッチ対応:タッチイベントのリスナーをセット
    pianoWrap.addEventListener("touchstart", function(){ handleTouchEvents(event) })
    pianoWrap.addEventListener("touchmove", function(){ handleTouchEvents(event) })
    pianoWrap.addEventListener("touchend", function(){ handleTouchEvents(event) })
    pianoWrap.addEventListener("touchcancel", function(){ handleTouchEvents(event) }) 
} else {
    // タッチ非対応:マウスイベントのリスナーをセット
    pianoWrap.addEventListener("mousedown", function(){ handleMouseEvents(event) })
    pianoWrap.addEventListener("mouseup", function(){ handleMouseEvents(event) })
    window.addEventListener("mousemove", function(){ handleMouseEvents(event) })
} 

// 座標(x,y)に応じた鍵盤番号を取得
function getKeyNum(x, y){
    // 黒鍵とタッチ箇所が重なるかチェック
    for ( let j = 0; j < blackKeys.length; j++ ){
        const KeyRect = blackKeys[j].getBoundingClientRect()
        if ( x >= window.pageXOffset + KeyRect.left  &&
             x <= window.pageXOffset + KeyRect.right &&
             y >= window.pageYOffset + KeyRect.top   &&
             y <= window.pageYOffset + KeyRect.bottom ){
            // タッチした鍵盤番号をセット
            return Number( blackKeys[j].dataset.keyNum )
        }
    } 
    // 白鍵とタッチ箇所が重なるかチェック
    for ( let j = 0; j < whiteKeys.length; j++ ){
        const KeyRect = whiteKeys[j].getBoundingClientRect()
        if ( x >= window.pageXOffset + KeyRect.left  &&
             x <= window.pageXOffset + KeyRect.right &&
             y >= window.pageYOffset + KeyRect.top   &&
             y <= window.pageYOffset + KeyRect.bottom ){
            // タッチした鍵盤番号をセット
            return Number( whiteKeys[j].dataset.keyNum )
        }
    }
    // ピアノ外のタッチの場合
    return null
}

// タッチイベント発生時の処理
function handleTouchEvents(event){
    if (typeof event.cancelable !== 'boolean' || event.cancelable) {
        event.preventDefault();
    }
    const BeforeKeyNumlist = JSON.parse(JSON.stringify(touchKeyNumlist)) 
    touchKeyNumlist.length = 0
    // 各接触ポイントから押下中の鍵盤番号リストを作成
    for ( let i = 0; i < event.touches.length; i++ ){
        const x = event.touches[i].pageX 
        const y = event.touches[i].pageY 
        let keyNum = getKeyNum(x, y)
        if ( keyNum !== null ){
            if ( !touchKeyNumlist.includes(keyNum) ){
                // リストに存在しなければ鍵盤番号をセット
                touchKeyNumlist.push(keyNum)
            }
        }
    } 
    // 新リストのみに存在 => 鍵盤を押下した処理
    for ( let i = 0; i < touchKeyNumlist.length; i++ ){
        if ( !BeforeKeyNumlist.includes(touchKeyNumlist[i]) ){ 
            pressPianoKey(touchKeyNumlist[i]) 
        }
    }
    // 旧リストのみに存在 => 鍵盤をはなした処理
    for ( let i = 0; i < BeforeKeyNumlist.length; i++ ){
        if ( !touchKeyNumlist.includes(BeforeKeyNumlist[i]) ){
            releasePianoKey(BeforeKeyNumlist[i]) 
        }
    }
}

// マウスイベント発生時の処理
function handleMouseEvents(event){
    // 左クリック以外は対象外
    if ( event.which !== 1 ){ return }
    const x = event.pageX 
    const y = event.pageY 
    let keyNum
    switch ( event.type ){
        case "mousedown":
            keyNum = getKeyNum(x, y)
            if ( keyNum !== null ){ pressPianoKey(keyNum) }
            clickedKeyNum = keyNum
            break
        case "mouseup":
            if ( clickedKeyNum !== null ){
                keyNum = getKeyNum(x, y)
                if ( keyNum !== null ){ releasePianoKey(keyNum) }
                clickedKeyNum = null
            }
            break
        case "mousemove":
            keyNum = getKeyNum(x, y)
            if ( keyNum !== null ){
                // マウスポインタ位置が直前の鍵盤以外の鍵盤上の場合
                if ( keyNum !== clickedKeyNum ){ 
                    releasePianoKey(clickedKeyNum)
                    pressPianoKey(keyNum) 
                    clickedKeyNum = keyNum
                }
            } else {
                // マウスポインタ位置が鍵盤外の場合
                releasePianoKey(clickedKeyNum)
                clickedKeyNum = null
            }
            break
    }
}

// PCkeydown時の処理
document.onkeydown = function(event) {
    // 鍵盤番号を取得
    const obj = keyMap.find( (item) => item.pcKey === event.key )
    if ( typeof obj !== "undefined" ){
        // keyMapに含まれるキーの場合は後続処理実行
        pressPianoKey(obj.pianoKey)
    } 
}

// PCkeyup時の処理
document.onkeyup = function(event) {
    // 鍵盤番号を取得
    const obj = keyMap.find( (item) => item.pcKey === event.key )
    if ( typeof obj !== "undefined" ){
        // keyMapに含まれるキーの場合は後続処理実行
        releasePianoKey(obj.pianoKey)
    } 
}

// ピアノ鍵盤を押下した時の処理
function pressPianoKey(keyNum){
    if ( !isKeyPressing[keyNum] ){
        // 鍵盤を離している場合のみ続行(長押しによる連打防止)
        isKeyPressing[keyNum] = true
        document.querySelector(`[data-key-num="${keyNum}"]`).classList.add("pressing")
        soundPlay(keyNum)
    }
}

// ピアノ鍵盤をはなした時の処理
function releasePianoKey(keyNum){
    if ( isKeyPressing[keyNum] ){
        // 鍵盤を押している場合のみ続行
        isKeyPressing[keyNum] = false
        document.querySelector(`[data-key-num="${keyNum}"]`).classList.remove("pressing")
        soundStop(keyNum)
    }
}

// オーディオ再生
function soundPlay(soundNum){
    clearInterval( intervalIds[soundNum] )
    intervalIds[soundNum] = null
    pianoSounds[soundNum].volume = 1
    pianoSounds[soundNum].currentTime = 0
    pianoSounds[soundNum].play()
}

// オーディオ停止(フェードアウト)
function soundStop(soundNum){       
    // 20msごとに音量を下げる
    intervalIds[soundNum] = setInterval( function(){
        if ( pianoSounds[soundNum].volume <= 0.05 ){
            // 音量が0.05以下の場合、Interval停止・オーディオ停止
            clearInterval( intervalIds[soundNum] )
            intervalIds[soundNum] = null
            pianoSounds[soundNum].volume = 0
            pianoSounds[soundNum].pause()
            pianoSounds[soundNum].currentTime = 0
        } else {
            // 音量が0.05より大きい場合、音量を0.05下げる
            pianoSounds[soundNum].volume -= 0.05
        }
    }, 20 )
}

制作メモ

自分なりにこだわったつもりのポイント
  • デザインや動きをCSSでちょっとリアルにした
  • 音源はKontakt 5(Native Instruments)を使って録音
  • 鍵盤から手を離した時の音が自然になるようフェードアウトにした
  • タッチ操作・マウス操作は押す・離すだけでなく移動に対応した

・・・とはいえ、連打したりした時の音の切れ方はまだ不自然なので、いつか改善したい。

最初は、同じ鍵盤を2連打した時、
audio.pause() → audio.currentTime = 0 → audio.play()
となるようにしていたのですが、それだけだとプツプツプツというノイズがかなり目立ちました。

pause()する前に、volume=0にしても結果は変わらず。
なので、できる限りフェードアウトしてvolume=0までなめらかに下げる、フェードアウト完了までに同じ鍵盤が押された場合は、やむを得ずvolume=0,currentTime=0にしてplayする という方針でコーディングしました。

そのため、高速で連打すると少しプツプツノイズが出ます・・・

いい対策をご存じの方いらっしゃったら是非教えてください。
(1つの鍵盤につきAudioオブジェクトを複数用意してうまくコントロールすれば改善はできるかも・・・?と思ったけど試せていません)

スマートフォン・タブレット対応はマルチタッチ等も考えないといけないようで、いつか実装できればいいなぁ・・・
2020.07.21 タッチデバイスに対応しました。
指やマウスポインタの位置移動を考慮すると、触れている鍵盤の判定を座標で行うしかない(と思う)ので、面倒なコードになってしまった感があります・・・

コメント

タイトルとURLをコピーしました