My goal is to make a workable piano. Once I’ve made the piano, I want to be able to build a database of playable songs, to be able to play along to. Nothing fancy, like rock band, but just a simple piano app with songs that you can play along with. If you want to follow along, here’s my workable prototype that we’ll be working toward: https://my-little-piano-app.herokuapp.com/
This is a fun project, and would be a fun little game to make for your kids. So let’s get started. I’m breaking this project up into 3 main components:
For the HTML, I originally wanted to make each key clickable, so the note would play on click. Later, I found that clicking each key achieved a much slower play, and an un-realistic sound. So I switched it up and assigned each sound to an individual key press. This allowed for more complex melodies and a more realistic sound. However, there are advantages to doing both, so in this tutorial, I’m going to show how you might go about doing either method, in case you want to follow along.
If you decide to do clickable keys, one problem you might run into very quickly is that keys are complex shapes. They fit around each other like puzzle pieces. In HTML and CSS, it’s easy to make squares and rectangles, but slightly harder to make more complex shapes. Since I wanted to have mouseover effects, I needed to have each key separate. Instead of trying to do simple rectangles and then figure out alignment and z-indexes, I went with a much harder, and unnecessary approach: svg polygons. The “SVG” tag allows you to create shapes that would otherwise be impossible. So I created each key with specific points in the shapes I wanted. So that you don’t have to figure that out yourself, I’m including those elements here:
<svg class="piano" height="230" width="1000"><polygon points="200,10 230,10 230,100 245,100 245,220 200,220 200,10" class="white" id="c" data-key="65"/>
<polygon points="245,100 260,100 260,10 275,10 275,100 290,100 290,220 245,220 245,100" class="white" data-key="83" id="d"/>
<polygon points="305,10 335,10 335,220 290,220 290,100 305,100 305,10" class="white" data-key="68" id="e"/>
<polygon points="335,10 365,10 365,100 380,100 380,220 335,220 335,10" class="white" data-key="70" id="f"/>
<polygon points="380,100 395,100 395,10 410,10 410,100 425,100 425,220 380,220 380,100" class="white" data-key="71" id="g"/>
<polygon points="425,100 440,100 440,10 455,10 455,100 470,100 470,220 425,220 425,100" class="white" data-key="72" id="a"/>
<polygon points="470,100 485,100 485,10 515,10 515,220 470,220 470,100" class="white" data-key="74" id="b"/>
<polygon points="515,10 545,10 545,100 560,100 560,220 515,220 515,10" class="white" data-key="82" id="key5"/>
<polygon points="560,100 575,100 575,10 590,10 590,100 605,100 605,220 560,220" class="white" data-key="84" id="key5"/>
<polygon points="605,100 620,100 620,10 650,10 650,220 605,220 605,100" class="white" data-key="89" id="key5"/>
<polygon points="650,10 680,10 680,100 695,100 695,220 650,220 650,10" class="white" data-key="85" id="key5"/>
<polygon points="695,100 710,100 710,10 725,10 725,100 740,100 740,220 695,220 695,100" class="white" data-key="73" id="key5"/>
<polygon points="740,100 755,100 755,10 770,10 770,100 785,100 785,220 740,220 740,100" class="white" data-key="79" id="key5"/>
<polygon points="785,100 800,100 800,10 830,10 830,220 785,220 785,100" class="white" data-key="80" id="key5"/>
<polygon points="230,10 260,10 260,100 230,100 230,10" class="black" data-key="49" id="c_sharp"/><polygon points="275,10 305,10 305,100 275,100 275,10" class="black" data-key="50" id="d_sharp"/><polygon points="365,10 395,10 395,100 365,100 365,10" class="black" data-key="51" id="f_sharp"/><polygon points="410,10 440,10 440,100 410,100 410,10" class="black" data-key="52" id="g_sharp"/><polygon points="455,10 485,10 485,100 455,100 455,10" class="black" data-key="53" id="a_sharp"/><polygon points="545,10 575,10 575,100 545,100 545,10" class="black" data-key="54" id="key4"/><polygon points="590,10 620,10 620,100 590,100 590,10" class="black" data-key="55" id="key4"/><polygon points="680,10 710,10 710,100 680,100 680,10" class="black" data-key="56" id="key4"/><polygon points="725,10 755,10 755,100 725,100 725,10" class="black" data-key="57" id="key4"/><polygon points="770,10 800,10 800,100 770,100 770,10" class="black" data-key="48" id="key4"/></svg>
Each polygon element is enclosed in the SVG tag and is defined by a series of points. Eacn polygon is also assigned a class, either black or white, corresponding to either black or white keys. Here’s the class styles I made for those:
.white {fill:white;stroke:black;stroke-width:1;cursor:pointer;margin:2px;}
.white:hover {fill:#9e9e9e;stroke:lightblue;cursor:pointer;stroke-width:1;outline: black solid 1px;}
.black {fill:black;stroke-width:1;cursor:pointer;margin:2px;}
.black:hover {fill:#515151;stroke:lightblue;stroke-width:1;outline: black solid 1px;}
This defines the color and border, as well as the hover effects for each of the keys. Even though the black and white keys are next to each other on a keyboard, for me it was easier to group the black and white keys separately in the HTML document. Each polygon also has an ID. Most of the IDs correspond to the note associated with that polygon, starting at middle “C” all the way up to “B” of the next octave. You will also notice that in addition to an ID, each polygon element also has a data-key, with looks like this:
data-key="80"
What this does, is it associates each element with key press. Each key on the keyboard has a number. So at this point we could either click the key on the screen, or press a keyboard key to get the program to play a sound for us. However, since the user won’t instinctively know which keyboard key corresponds to each sound, I’ve included a display in the HTML to show that to the user.
<div class="keysNotes"><h3>note</h3><h3>c</h3><h3>d</h3><h3>e</h3><h3>f</h3><h3>g</h3><h3>a</h3><h3>b</h3><h3>c</h3><h3>d</h3><h3>e</h3><h3>f</h3><h3>g</h3><h3>a</h3><h3>b</h3></div>
<div id="keysshow" class="keysNumbers"><h3>keys</h3><h3>A</h3><h3>S</h3><h3>D</h3><h3>F</h3><h3>G</h3><h3>H</h3><h3>J</h3><h3>R</h3><h3>T</h3><h3>Y</h3><h3>U</h3><h3>I</h3><h3>O</h3><h3>P</h3></div>
Then, in the CSS for these classes, I have the following:
.keysNumbers {color: lightgray;font-size: 40px;font-family: monospace;font-weight: bold;z-index: 10;width: 740px;display: flex;flex-flow: row nowrap;justify-content: space-between;margin-left: 80px;margin-top: -110px;}
.keysNotes {color: gray;font-size: 40px;font-family: monospace;font-weight: bold;z-index: 13;width: 740px;display: flex;flex-flow: row nowrap;justify-content: space-between;margin-left: 80px;margin-top: -150px;}
This perfectly lines up the number and key display overlayed ontop of the on screen keys. Now, we need to bring in our audio files. I searched the internet, and found piano notes for each key I needed, except for F sharp, A sharp and both Fs. Since I had the other key sounds, I took those keys into a free program called audacity, and changed the pitch to the higher or lower pitch, and resaved the file to get the sound I needed. Audacity is a great tool for that, especially if you wanted to make something like a guitar player instead of a piano, but had limited recordings available to make your initial sounds. In any case, I don’t want you to have to go through all that trouble, so I’m providing the sounds I gathered here for free: https://www.dropbox.com/sh/buuc6h937s62vw2/AADHdRxmmDCfcatwjxvdwAvNa?dl=0
Next, I’m putting each of these files into the HTML with audio tags, and also including the key press codes we used earlier in the polygon elements.
<audio data-key="65" id="c_octave1_audio" src="/middle_c.mp3"></audio><audio data-key="49" id="c_octave1_sharp_audio" src="/mid_c_sharp.mp3"></audio><audio data-key="83" id="d_octave1_audio" src="/middle_d.mp3"></audio><audio data-key="50" id="d_octave1_sharp_audio" src="/mid_d_sharp.mp3"></audio><audio data-key="68" id="e_octave1_audio" src="/middle_e.mp3"></audio><audio data-key="70" id="f_octave1_audio" src="/middle_f.mp3"></audio><audio data-key="51" id="f_octave1_sharp_audio" src="/mid_f_sharp.mp3"></audio><audio data-key="71" id="g_octave1_audio" src="/middle_g.mp3"></audio><audio data-key="52" id="g_octave1_sharp_audio" src="/mid_g_sharp.mp3"></audio><audio data-key="72" id="a_octave1_audio" src="/middle_a.mp3"></audio><audio data-key="53" id="a_octave1_sharp_audio" src="/mid_a_sharp.mp3"></audio><audio data-key="74" id="b_octave1_audio" src="/middle_b.mp3"></audio>
<audio data-key="82" id="c\_octave2\_audio" src="/high\_c.mp3"></audio>
<audio data-key="54" id="c\_octave2\_sharp\_audio" src="/high\_c\_sharp.mp3"></audio>
<audio data-key="84" id="d\_octave2\_audio" src="/high\_d.mp3"></audio>
<audio data-key="55" id="d\_octave2\_sharp\_audio" src="/high\_d\_sharp.mp3"></audio>
<audio data-key="89" id="e\_octave2\_audio" src="/high\_e.mp3"></audio>
<audio data-key="85" id="f\_octave2\_audio" src="/high\_f.mp3"></audio>
<audio data-key="56" id="f\_octave2\_sharp\_audio" src="/high\_f\_sharp.mp3"></audio>
<audio data-key="73" id="g\_octave2\_audio" src="/high\_g.mp3"></audio>
<audio data-key="57" id="g\_octave2\_sharp\_audio" src="/high\_g\_sharp.mp3"></audio>
<audio data-key="79" id="a\_octave2\_audio" src="/high\_a.mp3"></audio>
<audio data-key="48" id="a\_octave2\_sharp\_audio" src="/high\_a\_sharp.mp3"></audio>
<audio data-key="80" id="b\_octave2\_audio" src="/high\_b.mp3"></audio>
You’ll probably want to include the audio files somewhere in your project, so that your HTML actually has access to them. Now that we have our audio, we could either code the keypresses or the onclicks to trigger the sound, or both.
If we go the onclick route, our JavaScript could look something like this:
function play(){ var audio = document.getElementById("audio"); audio.play(); }
Of course, replacing “audio” with whichever element we want to trigger the function. One side note, if we do nothing but this, you will run into a problem, where, if you click one button, and then a second, the second one won’t play until the first one has finished. So It’s nice to include a little extra in the function to stop any previous sounds, if a button is clicked, and then play only the sound that has been clicked.
Which way is better? OnClick or KeyPress? That’s entirely up to you, but here are the arguments for both. OnClick is better if your goal is to help you or someone else learn the piano. And Here’s why: If your program relies on keypress, then the user is more often going to associate the key played with the letter displayed on the key, and not the letter associated with the note. So you could have the “C” key play the “middle C” and so forth, but that only works if you only have one octave. And then “C sharp” etc is also tricky. Eventually you’re going to have the user pressing “G” to play “high F” and “S” to play “A sharp”. You can see how this would get confusing for someone who is trying to learn the piano. OnClick allows the user to only associate the sound with the displayed letter, not the letter on the keyboard. However, clicking for each sound leads to a much clunkier piano sound, that isn’t very fluid or easy to play. In my opinion, neither is really a great tool for learning a musical instrument, which is why I went with the keypress approach, which is just more fun to play. I took some of the JavaScript for that from a drum Kit in one of Wes Bos’s projects, but it’s a great approach for this type of project. Here’s what it looks like:
window.addEventListener('keydown', function(e) {const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);if (!audio) return;audio.currentTime = 0;audio.play();key.classList.add('active')
Instead of having to get each element by ID, you simply listen for a keypress, and match it with the corresponding data-key, which we already have in our HTML. It plays the sound associated with that keypress, which we already have in our HTML as well. Finally, we’re saying if there isn’t any audio, play it, and we’re setting the audio.currentTime to 0, which helps with the wait I mentioned earlier, allowing the key pressed to play, and stopping any other sounds currently still playing.
At this point you should be able to get your piano working either with keypresses or onclicks, whichever you prefer, and your piano is playable, hopefully. The only other thing I did extra was start a database of “sheet music” , so that you could select a song to play, and it will give you the notes for that song. You can see that in my example earlier.
The hard part for this, is that, there isn’t an easy shortcut. If you create a similar database, you would have to manually add the notes for each song in there yourself. However, if anyone has a great way to transpose sheet music into JSON data…I’m all “ears”.
Once I had the notes saved in a database, I could just make an api call when the song is selected and retrieve the notes associated with the selected song. You could also take it a step further, and let the song be played first, or any number of additional add-ons. Anyway, I hope you enjoy, and have as much fun with it as I did. If you have any feedback, please let me know!