Super basic piano. Sounds are being generated with the oscillator node from the Web Audio API.
Created
September 25, 2025 18:34
-
-
Save mhekel/44cac154cef6da76e4eeca240a97c40c to your computer and use it in GitHub Desktop.
Basic piano with Web Audio API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <p><button type="button">Enable sound</button></p> | |
| <!-- Waveform selector --> | |
| <p> | |
| <label for="soundType">Select a waveform:</label> <select name="soundType" id="soundType"> | |
| <option value="triangle" selected>Triangle</option> | |
| <option value="sine">Sine</option> | |
| <option value="square">Square</option> | |
| <option value="sawtooth">Sawtooth</option> | |
| </select> | |
| </p> | |
| <!-- Keyboard placeholder --> | |
| <div id="keyboard"> | |
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * NOTE: | |
| * Some of the features of the Web Audio API currently only work on Chrome, hence the 'webkit' prefix! | |
| */ | |
| (function() { | |
| // Create audio (context) container | |
| var audioCtx = new (AudioContext || webkitAudioContext)(); | |
| // Audio context must be enabled via user gesture | |
| document.querySelector('button').addEventListener('click', function(event) { | |
| audioCtx.resume().then(() => { | |
| event.target.innerText = "Sound enabled"; | |
| console.log('Playback resumed successfully'); | |
| }); | |
| }); | |
| // Table of notes with correspending keyboard codes. Frequencies are in hertz. | |
| // The notes start from middle C | |
| var notesByKeyCode = { | |
| 65: { noteName: 'c4', frequency: 261.6, keyName: 'a' }, | |
| 83: { noteName: 'd4', frequency: 293.7, keyName: 's' }, | |
| 68: { noteName: 'e4', frequency: 329.6, keyName: 'd' }, | |
| 70: { noteName: 'f4', frequency: 349.2, keyName: 'f' }, | |
| 71: { noteName: 'g4', frequency: 392, keyName: 'g' }, | |
| 72: { noteName: 'a4', frequency: 440, keyName: 'h' }, | |
| 74: { noteName: 'b4', frequency: 493.9, keyName: 'j' }, | |
| 75: { noteName: 'c5', frequency: 523.3, keyName: 'k' }, | |
| 76: { noteName: 'd5', frequency: 587.3, keyName: 'l' }, | |
| 186: { noteName: 'e5', frequency: 659.3, keyName: ';' } | |
| }; | |
| function Key(keyCode, noteName, keyName, frequency) { | |
| var keyHTML = document.createElement('div'); | |
| var keySound = new Sound(frequency, 'triangle'); | |
| /* Cheap way to map key on touch screens */ | |
| keyHTML.setAttribute('data-key', keyCode); | |
| /* Style the key */ | |
| keyHTML.className = 'key'; | |
| keyHTML.innerHTML = noteName + '<br><span>' + keyName + '</span>'; | |
| return { | |
| html: keyHTML, | |
| sound: keySound | |
| }; | |
| } | |
| function Sound(frequency, type) { | |
| this.osc = audioCtx.createOscillator(); // Create oscillator node | |
| this.pressed = false; // flag to indicate if sound is playing | |
| /* Set default configuration for sound */ | |
| if(typeof frequency !== 'undefined') { | |
| /* Set frequency. If it's not set, the default is used (440Hz) */ | |
| this.osc.frequency.value = frequency; | |
| } | |
| /* Set waveform type. Default is actually 'sine' but triangle sounds better :) */ | |
| this.osc.type = type || 'triangle'; | |
| /* Start playing the sound. You won't hear it yet as the oscillator node needs to be | |
| piped to output (AKA your speakers). */ | |
| this.osc.start(0); | |
| }; | |
| Sound.prototype.play = function() { | |
| if(!this.pressed) { | |
| this.pressed = true; | |
| this.osc.connect(audioCtx.destination); | |
| } | |
| }; | |
| Sound.prototype.stop = function() { | |
| this.pressed = false; | |
| this.osc.disconnect(); | |
| }; | |
| function createKeyboard(notes, containerId) { | |
| var sortedKeys = []; // Placeholder for keys to be sorted | |
| var waveFormSelector = document.getElementById('soundType'); | |
| for(var keyCode in notes) { | |
| var note = notes[keyCode]; | |
| /* Generate playable key */ | |
| note.key = new Key(keyCode, note.noteName, note.keyName, note.frequency); | |
| /* Add new key to array to be sorted */ | |
| sortedKeys.push(notes[keyCode]); | |
| } | |
| /* Sort keys by frequency so that they'll be added to the DOM in the correct order */ | |
| sortedKeys = sortedKeys.sort(function(note1, note2) { | |
| if (note1.frequency < note2.frequency) return -1; | |
| if (note1.frequency > note2.frequency) return 1; | |
| return 0; | |
| }); | |
| // Add those sorted keys to DOM | |
| for(var i = 0; i < sortedKeys.length; i++) { | |
| document.getElementById(containerId).appendChild(sortedKeys[i].key.html); | |
| } | |
| var playNote = function(event) { | |
| event.preventDefault(); | |
| var keyCode = event.keyCode || event.target.getAttribute('data-key'); | |
| if(typeof notesByKeyCode[keyCode] !== 'undefined') { | |
| // Pipe sound to output (AKA speakers) | |
| notesByKeyCode[keyCode].key.sound.play(); | |
| // Highlight key playing | |
| notesByKeyCode[keyCode].key.html.className = 'key playing'; | |
| } | |
| }; | |
| var endNote = function(event) { | |
| var keyCode = event.keyCode || event.target.getAttribute('data-key'); | |
| if(typeof notesByKeyCode[keyCode] !== 'undefined') { | |
| // Kill connection to output | |
| notesByKeyCode[keyCode].key.sound.stop(); | |
| // Remove key highlight | |
| notesByKeyCode[keyCode].key.html.className = 'key'; | |
| } | |
| }; | |
| var setWaveform = function(event) { | |
| for(var keyCode in notes) { | |
| notes[keyCode].key.sound.osc.type = this.value; | |
| } | |
| // Unfocus selector so value is not accidentally updated again while playing keys | |
| this.blur(); | |
| }; | |
| // Check for changes in the waveform selector and update all oscillators with the selected type | |
| waveFormSelector.addEventListener('change', setWaveform); | |
| window.addEventListener('keydown', playNote); | |
| window.addEventListener('keyup', endNote); | |
| window.addEventListener('touchstart', playNote); | |
| window.addEventListener('touchend', endNote); | |
| } | |
| window.addEventListener('load', function() { | |
| createKeyboard(notesByKeyCode, 'keyboard'); | |
| }); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| * { | |
| box-sizing: border-box; | |
| } | |
| html { | |
| font-size: 62.5%; | |
| } | |
| body { | |
| font-size: 3rem; | |
| font-family: Arial, sans-serif; | |
| background: #222; | |
| } | |
| select, button { | |
| font-size: 3rem; | |
| } | |
| p { | |
| background: #fff; | |
| border-radius: 0.7rem; | |
| padding: 1rem 1.5rem; | |
| } | |
| #keyboard { | |
| width: 100%; | |
| overflow: hidden; | |
| } | |
| .key { | |
| border: 1px solid #ccc; | |
| background: #fff; | |
| border-radius: 0.7rem; | |
| margin: 0 0.5%; | |
| width: 9%; | |
| padding: 3.5rem 0; | |
| text-align: center; | |
| float: left; | |
| text-transform: uppercase; | |
| transition: margin 150ms ease-in-out; | |
| font-weight: bold; | |
| } | |
| .key span { | |
| font-size: 1.8rem; | |
| display: inline-block; | |
| padding: 1rem; | |
| margin-top: 1rem; | |
| background: #ccc; | |
| border-radius: 0.4rem; | |
| } | |
| .playing { | |
| background: #ddd; | |
| margin-top: 2rem; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment