DEV Community

Cover image for Let's Create a Song with Vanilla Javascript
Mojtaba Izadmehr
Mojtaba Izadmehr

Posted on

Let's Create a Song with Vanilla Javascript

But why does it matter?

First, it's cool, isn't it?

Secondly, something as simple as this can have huge security implications and use cases in fraud detection.

Are you interested to know what can be a more terrible invasion of our privacy than third-party cookies, a method that can follow you even when you use incognito or a VPN? The answer is browser fingerprinting, and using the browser Audio API is one of the techniques they can use to achieve this. In the first part, let's start easy and create a digital piano with JavaScript.

Javascript provides an API to create mathematically generated sound nodes and create a song from them.

The Web Audio API

The Web Audio API is the API supported by browsers which can create sounds or add effect to them. It can use different sources for audio nodes and link them together as chains to create audio graphs.
Different kinds of sources are:

  • Sound/Video files (MediaElementAudioSourceNodes)
  • Audio streams (MediaStreamAudioSourceNodes)
  • Mathematical calculations (OscillatorNodes)

Mathematical calculations are the part that I'm interested in. The most boring way to use it is to create a short beep. But, it is also possible to link enough of these nodes and create a song, but not any song; it's a completely digital and mathematically created song!

Let's talk a bit about how this can be used in browser fingerprinting!

These days, many people are s about 3rd party cookies, and how their usage as especially by advertisers is invading our privacy.

Browser fingerprinting is much worse. Some libraries out there can identify you even when using the incognito/VPN with a 99.5% accuracy rate. It can keep track of all the times you visited the website, and find out all your different IP addresses, locations, and session types.
They use many different techniques, but one of their techniques is using the browser AudioContext. By creating a mathematical sound oscillator in your browser and analyzing it, they can get good guesses about whether you have already visited the website or not.

image
source:https://fingerprintjs.com/demo/

Sound Creation

Let's not get ahead of ourselves, and let's start the journey by creating the most boring beep sound. We can use the AudioContext. Then we can use createOscillator to create a mathematically generated sound wave and connect it to the audio context we just created.

const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
oscillator.type = "sine";
oscillator.connect(audioCtx.destination);
oscillator.start();
Enter fullscreen mode Exit fullscreen mode

Sinusoidal sound waves are one of the famous sound waves out there. It has a well-rounded sound to it and by controlling its frequency we can control how low or high it sounds. As the human hearing range frequency is between 20 Hz to 20000 Hz (each Hz means 1 cycle second), so let's create a simple script to test our hearing.

We can change the frequency like this:

oscillator.frequency.setValueAtTime(frequency, audioCtx.currentTime);
Enter fullscreen mode Exit fullscreen mode

What was your hearing range? Could you hear all the way to 20 kHz? Or did the loud heavy metal songs already affected your hearing, like mine?

If you are interested to visualize the sound wave in the same range check this video out:

Digital Piano

Now that we could create a basic beep, let's try to create a Piano. The frequencies of an 88-key piano can be calculated with the following formula:
image
We can play around with our code from the previous section, and change the slider to a piano, with several keys.

Piano Frequency Range

If we use this formula for an 88-key piano (n=1 to n=88), the frequency can range from 27.5 to 4186. This range is too big, let's use a piano with fewer keys, which is in the more pleasant range. What about 15 keys, the notes range from C3 (130 Hz) to D4 (293 Hz). We will include thrid and a little bit of the fourth octave.
image
source: https://www.doomsquadmusic.com/piano-keys-explained/

Piano Design

Let's get started with the design, and let's start our design with a few simple keys.

<div class="piano">
   <div class="key white-key"/>
   <div class="key black-key"/>
   <div class="key white-key"/>
</div>
Enter fullscreen mode Exit fullscreen mode

And let's arrange them with flex, and split the space between them:

Let's start with basic styling with ugly rectangles:

.piano{
  display: flex;
  .white-key {
    flex: 1;
    height: 200px;
    border:1px solid #160801;
    background: white;
  }

  .black-key {
    flex: 0.5;
    height: 100px;
    z-index: 1;
    margin:0 -15px;
    border:1px solid #160801;
    background: black;
  }
}
Enter fullscreen mode Exit fullscreen mode

image
It works, it will add a black key with half-height, and a little bit of intersection with white keys. But it has a huge problem, it's so damn ugly. So, let's add a few shadows for normal and pressed cases.

.piano{
  background:#222;
  padding: 20px;
  display: flex;
  border-radius: 10px;

  .white-key {
    flex: 1;
    height: 200px;
    border:1px solid #121212;
    border-radius:10px 10px 20px 20px;
    box-shadow:0 0 50px rgba(0,0,0,0.5) inset,0 1px rgba(212,152,125,0.2) inset,0 5px 15px rgba(0,0,0,0.5);
    background: white;

    &:active {
        border:1px solid #777;
        border-left:1px solid #999;
        border-bottom:1px solid #999;
        box-shadow:2px 0 3px rgba(0,0,0,0.1) inset,-5px 5px 20px rgba(0,0,0,0.2) inset,0 0 3px rgba(0,0,0,0.2);
        background:linear-gradient(to bottom,#fff 0%,#e9e9e9 100%)
    }
  }

  .black-key {
    flex: 0.5;
    height: 100px;
    z-index: 1;
    margin:0 -20px;
    border:1px solid #121212;
    border-radius:10px 10px 20px 20px;
    box-shadow:0 0 50px rgba(0,0,0,0.5) inset,0 1px rgba(212,152,125,0.2) inset,0 5px 15px rgba(0,0,0,0.5);
    background:linear-gradient(45deg,#222 0%,#555 100%);

    &:active {
      box-shadow:-1px -1px 2px rgba(255,255,255,0.2) inset,0 -2px 2px 3px rgba(0,0,0,0.6) inset,0 1px 2px rgba(0,0,0,0.5);
      background: linear-gradient(to right,#444 0%,#222 100%)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

image

For simplicity we can create key elements dynamically with JavaScript, and append them to an empty <div class="piano" />, the return value of this snipped is an array of 15 keyElements.

const pianoEl = document.querySelector(".piano");
const startingKeyNumber = 28;

// Let's append the keys dynamically
const keyElements = Array(15)
  .fill()
  .map((item, index) => {
    const keyElement = document.createElement("button");
    const isBlackKey = index % 2;
    keyElement.className = `key ${isBlackKey ? "black-key" : "white-key"}`;
    keyElement.setAttribute("data-number", index + startingKeyNumber);
    pianoEl.appendChild(keyElement);
    return keyElement
  });
Enter fullscreen mode Exit fullscreen mode

Let's add an onClick to keys to add sounds. But instead of adding onClick, let's just add only one onclick event listener to the parent element. It can help with creating a smooth effect so that the sound continues between different piano keys.

// Let's create audio context and the oscillator

let isPlaying = false;
const audioCtx = new AudioContext();
let oscillator;
let currentKeyNumber;
let frequency; // We can calculate frequency based on key numbebr


pianoEl.onpointerdown = (e) => {
  if (currentKeyNumber && !isPlaying) {
    oscillator = audioCtx.createOscillator();
    oscillator.connect(audioCtx.destination);
    oscillator?.frequency.setValueAtTime(frequency, audioCtx.currentTime);
    // We can use the triangle type this time which sounds a little bit funky and party friendly
    oscillator.type = "triangle";
    oscillator.start();
    isPlaying = true;
  }
};

// Mathematical formula to calculate frequency
const getKeyFrequency = (keyNumber) => Math.pow(2, (keyNumber - 49) / 12) * 440;

Enter fullscreen mode Exit fullscreen mode

In this code, we just generated sound once, but we don't change the frequency when the mouse clicks other buttons. So there are two problems with it:

  • The sound doesn't stop:
    • We can add an event listener onmouseup and onmouseleave on piano parent element to stop the sound:
pianoEl.onmouseup = pianoEl.onmouseleave = (e) => {
  if (isPlaying) {
    oscillator?.stop();
    isPlaying = false;
  }
};
Enter fullscreen mode Exit fullscreen mode
  • There is no smooth way to switch between keys:
    • We can add on onmouseover event listener to every key, and change the frequency. We can add this logic to the end of keyElements chain.
const keyElements = Array(15)
  .fill()
  .map(
  ...
  )
  .forEach((keyElement) => {
    keyElement.onmouseover = (e) => {
      const currentKey = e.target;
      currentKeyNumber = currentKey.dataset.number;
      frequency = getKeyFrequency(currentKeyNumber);
      oscillator?.frequency.setValueAtTime(frequency, audioCtx.currentTime);
    };
  });
Enter fullscreen mode Exit fullscreen mode

Here is a working version of the piano:

Let's create a meaningful song, it's April now, so which song makes the most sense than Jingle Bells? Not really, but let's start easy! Here is a very basic melody for it. It shows the sequence of keys, the duration each one should be pressed, and the delay before pressing the next key.

const jingleBellKeys =[
  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 34, "duration": 300, "delay": 100 },

  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 34, "duration": 300, "delay": 100 },

  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 32, "duration": 200, "delay": 50 },
  { "key": 28, "duration": 200, "delay": 50 },
  { "key": 30, "duration": 200, "delay": 50 },
  { "key": 32, "duration": 300, "delay": 0 }
]
Enter fullscreen mode Exit fullscreen mode

Let's try to add a button that can do this automatically. First, we need a sleep method, which does the delay process. It creates a promise and does a setTimeout when setTimeout's time arrives, it resolves the promise.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
Enter fullscreen mode Exit fullscreen mode

Next, we need to create a button that will do a loop on this array and plays it:

playJingleBellButton.onclick = async () => {
  const notesList = jingleBellKeys;

  // let's do a map on all the keys
  for (let note of notesList) {
    // creating the oscillator object
    oscillator = audioCtx.createOscillator();
    oscillator.type = "triangle";
    oscillator.connect(audioCtx.destination);

    // calculate frequency from key number
    frequency = getKeyFrequency(note.key);
    oscillator?.frequency.setValueAtTime(frequency, audioCtx.currentTime);

    // start pressing the button
    oscillator.start();

    // wait for the duration the key is pressing
    await sleep(note.duration);

    // stop the oscillator when finished with pressing the key
    oscillator.stop();

    // wait before pressing the next key
    await sleep(note.delay);
  }
};
Enter fullscreen mode Exit fullscreen mode

We can do a little bit of style improvement to also show what is being pressed. Here is the final result:

Look cool, right?

In the next part, I'll dive a bit deeper into digital finger printing, and how it can be used to identify the user.

Discussion (0)