1Keys – How I Made a Piano in only 1kb of JavaScript

Digit Dilemma

The legendary JavaScript competition JS1k was thought to have ended, but it has risen from it’s ashes in the form of JS1024! This competition for making tiny JavaScript programs has been going on for 10 years and will now be continuing under a new name. Thank you to the organizers and other participants, you should definitely check out some of the other entries.

Figuring out how make something cool with such a small amount of code is quite the challenge, but it’s not my first rodeo. This year I submitted two programs, so check out my other entry, Digit Dilemma Plus, a logic puzzle game with procedural levels. The contest provides “shims”, html starter code for canvas and WebGL programs, but neither of my entries use them.

JavaScript provides a powerful though simple audio library, the Web Audio API. I first explored it while developing ZzFX, a tiny sound effect system. For while I’ve been itching to make some musical instruments. This demo works much differently from ZzFX, mostly using the oscillator and gain nodes.

In this post I will go through every single line of my 1 kilobyte piano and explain how some of the trickier parts work. The whole thing is open source on GitHub and has a some improvements since my JS1024 submission. So please continue reading and let’s make sweet music together!

Here’s 1Keys running live in a CodePen for you to mess around with. It can be played with either the mouse or keyboard.

See the Pen 🎹 1Keys – 1k Version by Frank Force (@KilledByAPixel) on CodePen.

Source Code on GitHub

Official JS1024 Entry

Initialize

This program is almost entirely JavaScript, but there is a bit of HTML necessary to set things up. Normally the body is accessed using “document.body”, but to save space we set the body id to B. The body style is set in the JavaScript code so it can be compressed along with everything else. In the final version, all the code is moved to the body’s onload rather then using the script tag to squeeze out a few more bytes.

All the variables and functions are single letters, but there are only a few. To help with organization, global variables use capital letters while locals are lower case. This is not my usual style of writing code, but kind of a shorthand that I use for extremely small programs.

The audio context is created and stored in C, which will be used for all audio calls. The D function gets the piano key html element for a given index. Each of the keys will be created with an unique id of K+i so they can be accessed in this way. Instead of calling document.getElementById(), we can use eval to convert an id like “K3” to the variable K3.

<body id=B><script>

// body style
B.style = `background:#112;color:#fff;user-select:none;text-align:center`;

C = new AudioContext;  // audio context
D = i=> eval(`K` + i); // get piano key div

Create Instrument Radio Buttons

All the HTML content for this program is built dynamically to save space. First we add the radio buttons for the four different instruments. I used emojis because they are smaller then words!

To create the buttons we convert the emoji string to an array and use map to iterate through the icons, adding radio buttons for each one. The input’s onmousedown event handles setting the instrument when it changes.

All the instrument buttons are set to checked, but since they are radio buttons only the last one will end up being checked, which is the default instrument. Just another trick to save space.

// instrument select
[...`∿🎻🎷🎹`].map((i,j)=> // instrument icons
 B.innerHTML += i +        // icon
  `<input type=radio name=I checked onmousedown=I=${ // radio
 3 - j}> `);
);

Create Piano Key Divs

Like the instrument buttons, the keyboard itself is also created dynamically, composed mainly of divs. There are 3 rows of 12 keys each that form the piano. To save space the instrument I and active sound array A are also initialized here.

The formula 24 + i%12 – (i/12|0)*12 handles reordering the keys so lower keys are on the bottom. The result is stored in the variable k to be used several more times.

The piano key’s CSS code takes up a large chunk of space. The display must be set to inline-block for with and height settings to work. Keys are separated using the margin setting but they look a bit better with an outline like “outline:3px solid #000” .

// piano keys
for( I = i = 0; i < 36; A = [])      // 36 keys & init 
 `B.innerHTML += ${i%12 ? `` : `<br>` // new row
 }<div id=K${                        // create key
 k = 24 + i%12 - (i/12|0)*12         // reorder
 } style=display:inline-block;margin:2;background:${ // style

To check if a key is black or white we look up the key index in a list that corresponds to the black keys. The black keys are shifted into the correct position using the margin-left setting.

 `02579`.indexOf(i++%12 - 1) < 0 ?       // b or w
 `#fff;color:#000;width:60;height:180` : // w
 `#000;position:absolute;margin-left:-17;width:33;height:99` // b

To allow mouse input we hook up the mouse events for each piano key triggering either the play function P() or cancel function X(). Each of these callbacks is very similar aside from an extra check if the button is down in onmouseover.

The div will automatically be closed because we are appending to the the body’s innerHTML, so there is no need to add a closing tag.

 } onmouseover=event.buttons&&P(${ k // mouse over
 }) onmousedown=P(${ k               // mouse down
 }) onmouseup=X(${ k                 // mouse up
 }) onmouseout=X(${ k                // mouse out
 })>`,                               // end key div

Play Note

The play note function is the largest part of this program so I split it up into a few parts. First we check if it’s a valid note that isn’t already being played. While a key is held, onkeydown down will trigger repeatedly so we need to prevent the note from playing on subsequent calls.

Each of the instruments is composed of harmonic sin waves with multiples of the base frequency defined by a string of digits. With this simple system we can produce a variety of interesting sounds. For example the organ is a root note and the 3 octaves above it to make a fuller sounding note. The other instruments work the same way, try playing around with the numbers and see what kinds of sounds you can make.

To create the overlapping waves we convert the instrument frequency multiplier string to arrays and iterate through the active instrument I.

// play note
P = i=> i < 0 || A[i] || // is valid and note not playing 
( 
 A[i] = [        // instruments 
  [...`1248`],   // 🎹 organ 
  [...`3579`],   // 🎷 brass 
  [...`321`],    // 🎻 strings 
  [...`3`],      // ∿ sine 
 ][ I ].map(j=>( // for each harmonic
Wave shapes from top to bottom: Piano, Sax, Strings, and Wave.

Finally some audio code. Here is where we create the sine wave oscillator and set it’s volume, also known as gain.

Some of these functions like createOscillator do not take parameters, so to save space, other unrelated stuff is shoved in. This technique cuts down the need for extra ;’s that would otherwise be necessary.

To get the div that corresponds to a key we just use the D function defined earlier. There is a trick here to reset the transition. D(i).style.transition needs to be unset, but that alone wouldn’t do it. You need need to access one of any specific variables in the element which triggers a “DOM Reflow” and reset the css transition. In this case we are using innerHTML.

There is a simple formula that converts a semi tone offset to a frequency for the oscillator. The equation uses an equal temperament scale with a root of A1 or 55 hz. The root frequency is multiplied by each of the instrument’s harmonics to create the overlapping sound. It has always been amazing to me that our system music is built on math!

The oscillator and gain connect calls are strung together because the connect function returns the value passed in to allow for this type of chaining.

  o = C.createOscillator(      // create oscillator
   D(i).style.transition = 
    D(i).innerHTML),           // reset transition
   o.connect(                  // oscillator to gain
    o.g = C.createGain(        // create gain node
     o.frequency.value =       // set frequency
      j * 55 * 2**((i+3)/12))) // A 55 root note
   .connect(C.destination),    // gain to destination

Each harmonic of the instrument has a it’s volume scaled to balance the fact that lower notes carry less energy, making them sound quieter then higher notes. Also, to prevent the clipping, we scale the volume by .2 since there are up to 4 overlapping sounds, this limits the combined amplitude from exceeding 1.

After everything has been set up the sound can be played by calling start. The last part returns o as the result of the map function to create an array of oscillators that is stored in A[i].

  o.g.gain.value = .2/(1+Math.log2(j)), // set gain
  o.start(),                            // start audio
  o)                                    // return sound

To give some visual feedback that a note is being played, the key color is set to red. But first we must save the original color in a data member called b so after being released it can be set back to it’s original color..

  D(i).b = D(i).style.background, // save original color
  D(i).style.background = `#f00`  // set key color red
);

Cancel Note

We could simply call stop immediately but that would cause a pop. To prevent this from happening we can taper off the sounds by using linearRampToValueAtTime.

Tapering off a note versus stopping it instantly.

Each note is composed of multiple oscillators that need to be stopped by iterating over A[i]. The key’s background is also set back to it’s original color with a transition time of .5s so it animates gradually for a nicer presentation.

The actual stop event is delayed by 350 milliseconds (.35 seconds) using setTimeout to cover the ramp off time.

// cancel note
X = i=> A[i] &&                       // is already playing?
 A[i].map(o=>                         // for each oscilator
 setTimeout(i=>o.stop(), 350,         // stop sound after delay
  o.g.gain.linearRampToValueAtTime(   // set gain start ramp
   o.g.gain.value, C.currentTime),    // set gain
    o.g.gain.linearRampToValueAtTime( // ramp off gain
     A[i] = 0, C.currentTime + .3),   // clear note
    D(i).style.transition = `.5s`,    // set transition
    D(i).style.background = D(i).b    // reset original color
 )
);

Keyboard Control

The piano can be played with either a mouse or keyboard, though the keyboard requires a bit more code it allows for much more nuanced control. This pair of functions trigger the play and cancel sound functions for the onkeydown and onkeyup events.

To map a keyboard key to a piano key we use a string that represents the keyboard to piano key mapping and look up the lower case key. This could could be smaller but there is some repetition because it compresses better this way. In most cases for optimal compression it is better to copy and paste code if it is an exact match though it can get difficult to work with.

// keyboard to piano key mapping
K = `zsxdcvgbhnjm,l.;/q2w3er5t6y7ui9o0p[=]`;

// play note on key down
onkeydown = i=> P(
 K.indexOf(i.key.toLowerCase())               // map key to note
  - 5 * (K.indexOf(i.key.toLowerCase()) > 16) // overlap 2nd row
);

// release note on key up
onkeyup = i=> X(
 K.indexOf(i.key.toLowerCase())               // map key to note
  - 5 * (K.indexOf(i.key.toLowerCase()) > 16) // overlap 2nd row
);

On Blur

That is nearly everything except one small bug fix. If a user clicks away from the program while playing a note it will get stuck because the onkeyup event will never happen! To fix this, we can hook up the onblur event to cancel all the active notes.

We also need a closing script tag, though as I mentioned earlier the final version will use the onload event.

// stop all sounds if focus lost
onblur = e=> A.map((e,i)=> X(i));

</script>

Minification

The code is mostly minified already but there are a few more tricks necessary to break the 1 kilobyte barrier. First we need to remove all the white space, I used xem’s terser online. You may not have noticed but every string in the code uses the template string ` character. This is important because we will need to nest all 3 string types. Of course we could use escape characters but that requires a bit more space. Unfortunately most JavaScript minifiers will replace ` with ” if the template functionality is not being used. So, we need to do a replace on all ” back to `.

Now we need to compress the code using the ingenious algorithm called JSCrush. It works a little bit like a zip to create self uncompromising JavaScript. I use siorki’s amazing regpack tool which has great features for this sort of thing. To prevent the global B body variable from being used, add B to the “Reassign variable names… … except for variables” input. Pack the code with the default settings and verify that it still works the same.

Regpack replaces repeated strings with unused characters. Normally this is fine but ” is a replacement character and we need to use it. So replace ” with Q or any other unused character.

The reason we need the ” is to wrap the code in <body onload=””> to eliminate the need for a script tag. The single quote is required by the uncompressor itself, so that’s why we need to go through all this trouble with replacing quotes.

Final Result 1020 Bytes

Here’s a look a the final code, isn’t it beautiful? Well, maybe not to everyone, but they say beauty is in the eye of the beholder!

<body onload="for(='onWtiW~keyQo.!${ZutYin;marg_^=>FerE;width:Ddown.map((e,o)F.cWnect(.dexOf(;height:;color:#12(Ke.Q.toLowECase())-5*>16)=C.create[…],3tTimestylealue$(e)..transi~=),A[e] =eF,C.curren(Zk})!g.ga..nEHTML Wmouse,l_earRampToVA(backgroundfor(B.=:#1fff;usE-select:nWe;text-align:centE,C=new AudioCWtext,$=iFeval(K+i∿🎻🎷🎹]B+=e+<_pY type=radio name=I checked=I=Z3-o}> I=i=0;i<36;A=[])B+=Zi%?`:
}<div id=KZk=24+i%-*(i/|0)} =display:_l_e-block^:2;:Z02579i++%-1)<0?#fff000D60180:#000;posi~:absolYe^-left:-17D3399}ovE=event.bYtWs&&P=Pup=XoY=X>,Pe<0|| ||( =[4857921]][I]nF(oOscillator(o!gGa_(!frequency.v=55*n*2**((e+3)/)))C.dest_a~v=.2/(1+Math.log2(n)!start(o).b=,=#f00);X && oFseoY(eF!stop(350v) =0+.3.5s¨C12Czsxdcvgbhnjm,l.;/q2w3E5t6y7ui9o0p[=]`,WQPWQupX)';G=/[-D-F^_YZ!Q~W]/.exec();)with(.split(G))=join(shift());eval(_)"id=B>

Thank you for reading or maybe just skimming this very long post! As a small bonus here’s a short Bach piece played by 1Keys. Rodrigo Siqueira made this Bach demo and also wrote the original prototype.

Follow me on Twitter for the latest updates on all my coding adventures. Until next time, be kind, smart, creative, and for crying out loud, wear a mask!

This entry was posted in JavaScript, Music and tagged , , , . Bookmark the permalink.

1 Response to 1Keys – How I Made a Piano in only 1kb of JavaScript

  1. Pingback: 1Keys – How I Made a Piano in only 1kb of JavaScript > Seekalgo

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.