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 タッチデバイスに対応しました。
指やマウスポインタの位置移動を考慮すると、触れている鍵盤の判定を座標で行うしかない(と思う)ので、面倒なコードになってしまった感があります・・・

コメント

  1. Mike より:

    お世話になります。ピアノの鍵盤で2行に渡り、文字を書き込む手立てと鍵盤を描く、CSSを参照させて頂きました。感謝です。

    音のフェードアウトの記事、多いに参考になりました。

    今後ともよろしく、お願い致します。

  2. Mike より:

    再度になります。

    1)main.jsも多いに参考になり、KeyboardEventプロパティの見直しを行いました。¥キーとバックスラッシュ(\)キーは両方とも¥を返すことが分かり、event.codeを使用して、キーを区別した方が良いような気がしております。

    2)ブラウザのfirefoxですが、独特な仕様になっており、ピアノの作成では悩まされています。?を押したとき、検索欄が出て、ピアノの演奏が中断します。最近、event.preventDefault();で対応しております。

    今までは、2種類のプログラムを作成していましたが、今後は、1個で対応できそうです。

    今後とも、宜しく、お願いします。

    • ichi3270 より:

      コメントありがとうございます。
      キーボードのキーの挙動について、考慮ができておらず、大変勉強になりました。
      今後修正などする際は参考にさせていただきます。

  3. bit より:

    音源がとても素晴らしいのでよろしければオーディオファイルをダウンロードして作りたいです。

  4. あんみかん より:

    ビープ音でピアノっぽい音を作りたくて検索していたらここに出会いました
    音が音源ファイルだったので少し残念ですが鍵盤の動きは素晴らしいですね
    ソースも公開ということなので音源ファイルの代わりにビープ音で動くように
    してみました。音の理論も実際のピアノの波形も正確には理解していないので
    公開によって音作りのアドバイスなどいただければと思っています
    許可頂ければレンタルサーバーを契約してアップしたいのですが如何でしょうか?