Digital Piano with Web Audio API (4) : white and black key with hover and active effects

When I changed the keyboard layout of the digital piano on this page, I removed the hover (color changes when the mouse hovers over a key) and active (color changes when a key is pressed) effects from the white and black keys. This is because it was impossible to separate the hover and active effects of the child elements (black keys) from those of parent keys (white keys).

Referring to this page, I changed the relationship between the white and black keys from “parent-child” to “sibling”. The hover and active effects of the white keys were separated from those of the black keys.

Volume:
Current waveform:

1. main modification in JavaScript (modified code for generating keyboard HTML)

I prepared pairs of white and black keys as child elements of a div element whose class attribute is “key-set-parent”. The piano keys are arranged from the left keyboard key. When the white keys are A, C, D, F and G where the black key is to the right of the white key, both white and black keys are added as child elements of the div element whose class attribute is “key-set-parent”. When the white keys are B and E, only the white key is added as a child element of the div element whose class attribute is “key-set-parent”.

        for (let i = 0; i < keyList.length; i++) {

            const keySetElem = document.createElement("div");
            keySetElem.className = "key-set-parent";

            const whiteKey = keyList[i];
            const whiteKeyName = whiteKey[0];

            const whiteKeyElem = createKey(whiteKeyName, idx, whiteKey[1], 'white-key');
            keySetElem.appendChild(whiteKeyElem);

            if ( whiteKeyName === 'ド' || whiteKeyName === 'レ' || whiteKeyName === 'ファ' ||
                 whiteKeyName === 'ソ' || whiteKeyName === 'ラ') {

                const blackKey = keyList[++i];

                if (blackKey != undefined) {
                    const blackKeyName = blackKey[0];
                    const blackKeyElem = createKey(blackKeyName, idx, blackKey[1], 'black-key');
                    keySetElem.appendChild(blackKeyElem);
                }
            }

            octaveElem.appendChild(keySetElem);
        }

        keyboard.appendChild(octaveElem);
    });

2. main modifications in css

I prepared a class selector “key-set-parent” for the parent element that will contain the white and black key pairs. I specified “display: inline-block;” so that HTML elements of the specified size are positioned horizontally. The width and height of “key-set-parent” are the same as the size of the white keys.

.key-set-parent {
    position: relative;
    display: inline-block;
    width: 40px;
    height: 120px;
}

3. entire code after modification

3.1. css

<style type="text/css">
.container {
    overflow-x: scroll;
    overflow-y: hidden;
    width: 100%;
    height: 180px;
    white-space: nowrap;
    margin: 10px;
}

.keyboard {
    width: auto;
    padding: 0;
    margin: 0;
}

.key-set-parent {
    position: relative;
    display: inline-block;
    width: 40px;
    height: 120px;
}

.key {
    position: relative;
    cursor: pointer;
    font: 10px "Open Sans", "Lucida Grande", "Arial", sans-serif;
    border: 1px solid black;
    border-radius: 5px;
    width: 40px;
    height: 120px;
    text-align: center;
    box-shadow: 2px 2px darkgray;
    display: inline-block;
    margin-right: 3px;
    user-select: none;
    -moz-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
}

.key.black-key {
    position: absolute;
    background-color: #000;
    color: #fff;
    width: 36px;
    height: 80px;
    top: 0px;
    left: 22px;
    z-index: 1;
    pointer-events: auto;
    vertical-align: top;
}

.key div {
    position: absolute;
    bottom: 0;
    text-align: center;
    width: 100%;
    pointer-events: none;
}

.key div sub {
    font-size: 8px;
    pointer-events: none;
}

.key:hover {
    background-color: #eef;
}

.key.black-key:hover {
    background-color: #778;
}

.key:active {
    background-color: #000;
    color: #fff;
}

.key.black-key:active {
    background-color: #fff;
    color: #000;
}

.octave {
    display: inline-block;
    padding: 0 6px 0 0;
}

.settingsBar {
    padding-top: 8px;
    font: 14px "Open Sans", "Lucida Grande", "Arial", sans-serif;
    position: relative;
    vertical-align: middle;
    width: 100%;
    height: 60px;
}

.left {
    width: 50%;
    position: absolute;
    left: 0;
    display: table-cell;
    vertical-align: middle;
}

.left span, .left input {
    vertical-align: middle;
}

.right {
    width: 50%;
    position: absolute;
    right: 0;
    display: table-cell;
    vertical-align: middle;
}

.right span {
    vertical-align: middle;
}

.right input {
    vertical-align: baseline;
}
</style>

3.2. HTML

<div class="container">
    <div class="keyboard"></div>
</div>

<div class="settingsBar">
    <div class="left">
        <span>Volume: </span>
        <input type="range" min="0.0" max="1.0" step="0.01" value="0.5" list="volumes" name="volume">
        <datalist id="volumes">
            <option value="0.0" label="Mute">
            <option value="1.0" label="100%">
        </datalist>
    </div>

    <div class="right">
        <span>Current waveform: </span>
        <select name="waveform">
            <option value="sine">Sine</option>
            <option value="square">Square</option>
            <option value="sawtooth">Sawtooth</option>
            <option value="triangle" selected>Triangle</option>
            <option value="custom">Custom</option>
        </select>
    </div>
</div>

3.3. JavaScript

<script>
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
let oscList = [];

const keyboard = document.querySelector(".keyboard");
const wavePicker = document.querySelector("select[name='waveform']");
const volumeControl = document.querySelector("input[name='volume']");

let noteFreq = null;
let customWaveform = null;
let sineTerms = null;
let cosineTerms = null;

const note_names =
[
    ["ラ", "", "A", ""],
    ["ラ#", "シ$\\flat$", "A#", "B$\\flat$"],
    ["シ", "","B", ""],
    ["ド", "","C", ""],
    ["ド#", "レ$\\flat$","C#", "D$\\flat$"],
    ["レ", "","D", ""],
    ["レ#", "ミ$\\flat$","D#", "E$\\flat$"],
    ["ミ", "","E", ""],
    ["ファ", "","F", ""],
    ["ファ#", "ソ$\\flat$","F#", "G$\\flat$"],
    ["ソ", "", "G", ""],
    ["ソ#", "ラ$\\flat$", "G#", "A$\\flat$"]
];

setup();

// -------------------------------------------------------
// functions
// -------------------------------------------------------

function createNoteTable() {

    let noteFreq = [];
    for (let octave = 0; octave < 9; octave++) {
        noteFreq[octave] = [];
    }

    for (let n = 0; n < 88; n++) {

        const frequency = getAudioFrequency(n);

        let octave = parseInt(n/12);
        if (n % 12 >= 3) {
            octave++;
        }

        const note_name_sharp_english = note_names[n % 12][2];
        noteFreq[octave][note_name_sharp_english] = frequency;
    }

    return noteFreq;
}

function getAudioFrequency(n) {
    return 27.5 * ( Math.pow( Math.pow(2, 1/12), n) );
}

function setup() {
    noteFreq = createNoteTable();

    noteFreq.forEach(function(keys, idx) {

        const keyList = Object.entries(keys);
        const octaveElem = document.createElement("div");
        octaveElem.className = "octave";

        for (let i = 0; i < keyList.length; i++) {

            const keySetElem = document.createElement("div");
            keySetElem.className = "key-set-parent";

            const whiteKey = keyList[i];
            const whiteKeyName = whiteKey[0];

            const whiteKeyElem = createKey(whiteKeyName, idx, whiteKey[1], 'white-key');
            keySetElem.appendChild(whiteKeyElem);

            if (whiteKeyName === 'A' || whiteKeyName === 'C' || whiteKeyName === 'D' ||
                whiteKeyName === 'F' || whiteKeyName === 'G') {

                const blackKey = keyList[++i];

                if (blackKey != undefined) {
                    const blackKeyName = blackKey[0];
                    const blackKeyElem = createKey(blackKeyName, idx, blackKey[1], 'black-key');
                    keySetElem.appendChild(blackKeyElem);
                }
            }

            octaveElem.appendChild(keySetElem);
        }

        keyboard.appendChild(octaveElem);
    });

    document.querySelector("div[data-note='F'][data-octave='5']").scrollIntoView(false);

    sineTerms = new Float32Array([0, 0, 1, 0, 1]);
    cosineTerms = new Float32Array(sineTerms.length);
    customWaveform = audioContext.createPeriodicWave(cosineTerms, sineTerms);

    for (i = 0; i < 9; i++) {
        oscList[i] = {};
    }
}

function createKey(note, octave, freq, keyColor) {
    const keyElement = document.createElement("div");
    const labelElement = document.createElement("div");

    if (keyColor === 'black-key') {
        keyElement.className = "key black-key";
    } else {
        keyElement.className = "key";
    }
    keyElement.dataset["octave"] = octave;
    keyElement.dataset["note"] = note;
    keyElement.dataset["frequency"] = freq;

    labelElement.innerHTML = note + "<sub>" + octave + "</sub>";
    keyElement.appendChild(labelElement);

    keyElement.addEventListener("mousedown", notePressed, false);
    keyElement.addEventListener("touchstart", notePressed, false);

    return keyElement;
}

function playTone(freq) {
    const osc = audioContext.createOscillator();
    const volume = volumeControl.value;

    const gainNode = audioContext.createGain();
    gainNode.connect(audioContext.destination);
    gainNode.gain.setValueAtTime(0, audioContext.currentTime);
    gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.05);
    gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1.00);

    osc.connect(gainNode);

    const type = wavePicker.options[wavePicker.selectedIndex].value;

    if (type == "custom") {
        osc.setPeriodicWave(customWaveform);
    } else {
        osc.type = type;
    }

    osc.frequency.value = freq;
    osc.start();

    osc.stop(audioContext.currentTime + 1.00);

    return osc;
}

function notePressed(event) {
    const dataset = event.target.dataset;
    const octave = +dataset["octave"];
    oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
}
</script>

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA