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.)
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.