Digital Piano with Web Audio API

I prepared a page for a digital piano using Web Audio API based on this page.

This code is just a modification of the sample code. I applied the following modifications: i. added black keys, ii. used a function to calculate the frequencies of the notes and iii. made it work with smartphone touch operations. (This digital piano is not stable for operations with smartphones.)

Volume:
Current waveform:

The code used on this page is described below. The code is a slight modification of the code on this page.

1. css code

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

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

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

.key.black-key {
    background-color: #000;
    color: #fff;
    height: 80px;
    vertical-align: top;
}

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

.key div sub {
    font-size: 10px;
    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>

2. HTML code

<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">Triangle</option>
            <option value="custom" selected>Custom</option>
        </select>
    </div>
</div>

3. JavaScript code

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

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();

    volumeControl.addEventListener("change", changeVolume, false);

    mainGainNode = audioContext.createGain();
    mainGainNode.connect(audioContext.destination);
    mainGainNode.gain.value = volumeControl.value;

    noteFreq.forEach(function(keys, idx) {
        const keyList = Object.entries(keys);
        const octaveElem = document.createElement("div");
        octaveElem.className = "octave";

        keyList.forEach(function(key) {
            const key_name = key[0];
            if (key_name === 'A' || key_name === 'B' || key_name === 'C' || key_name === 'D' ||
                key_name === 'E' || key_name === 'F' || key_name === 'G') {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'white-key'));
            } else {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'black-key'));
            }
        });

        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);
<style type="text/css">
.container {
    overflow-x: scroll;
    overflow-y: hidden;
    width: 100%;
    height: 160px;
    white-space: nowrap;
    margin: 10px;
}

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

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

.key.black-key {
    background-color: #000;
    color: #fff;
    height: 80px;
    vertical-align: top;
}

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

.key div sub {
    font-size: 10px;
    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>



<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">Triangle</option>
            <option value="custom" selected>Custom</option>
        </select>
    </div>
</div>



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

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();

    volumeControl.addEventListener("change", changeVolume, false);

    mainGainNode = audioContext.createGain();
    mainGainNode.connect(audioContext.destination);
    mainGainNode.gain.value = volumeControl.value;

    noteFreq.forEach(function(keys, idx) {
        const keyList = Object.entries(keys);
        const octaveElem = document.createElement("div");
        octaveElem.className = "octave";

        keyList.forEach(function(key) {
            const key_name = key[0];
            if (key_name === 'A' || key_name === 'B' || key_name === 'C' || key_name === 'D' ||
                key_name === 'E' || key_name === 'F' || key_name === 'G') {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'white-key'));
            } else {
                octaveElem.appendChild(createKey(key_name, idx, key[1], 'black-key'));
            }
        });

        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("mouseup", noteReleased, false);
    keyElement.addEventListener("mouseleave", noteReleased, false);

    keyElement.addEventListener("touchstart", notePressed, false);
    keyElement.addEventListener("touchend", noteReleased, false);
    keyElement.addEventListener("touchmove", noteReleased, false);
    keyElement.addEventListener("touchcancel", noteReleased, false);

    return keyElement;
}

function playTone(freq) {
    const osc = audioContext.createOscillator();
    osc.connect(mainGainNode);

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

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

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

    return osc;
}

function notePressed(event) {
    const dataset = event.target.dataset;

    if (!dataset["pressed"]) {
        const octave = +dataset["octave"];
        oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
        dataset["pressed"] = "yes";
    }
}

function noteReleased(event) {
    const dataset = event.target.dataset;

    if (dataset && dataset["pressed"]) {
        const octave = +dataset["octave"];
        oscList[octave][dataset["note"]].stop();
        delete oscList[octave][dataset["note"]];
        delete dataset["pressed"];
    }
}

function changeVolume(event) {
    mainGainNode.gain.value = volumeControl.value
}
</script>

    return keyElement;
}

function playTone(freq) {
    const osc = audioContext.createOscillator();
    osc.connect(mainGainNode);

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

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

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

    return osc;
}

function notePressed(event) {
    const dataset = event.target.dataset;

    if (!dataset["pressed"]) {
        const octave = +dataset["octave"];
        oscList[octave][dataset["note"]] = playTone(dataset["frequency"]);
        dataset["pressed"] = "yes";
    }
}

function noteReleased(event) {
    const dataset = event.target.dataset;

    if (dataset && dataset["pressed"]) {
        const octave = +dataset["octave"];
        oscList[octave][dataset["note"]].stop();
        delete oscList[octave][dataset["note"]];
        delete dataset["pressed"];
    }
}

function changeVolume(event) {
    mainGainNode.gain.value = volumeControl.value
}
</script>

I searched for similar pages on the Web and found a page that made the sound closer to that of a piano, a page that displayed the white and black keys like the actual piano, a page that made it possible to create tones by overlapping multiple OscillatorNodes and a page that made it possible to produce sounds with a computer keyboard instead of a mouse, etc. I would like to prepare other versions of digital pianos when I have time.

I don’t know if it will make interesting sounds, but I’m thinking a bit about making custom waveforms from solutions of Schrödinger equations.

Leave a Reply

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

CAPTCHA