Quick and Easy 2D Spatial Audio with Howler.js
Tags: javascript, react, gamedev, ux
Published: 2021-01-11 20:58:21

Creating immersion within a web application is hard. Unless a lot of effort is put into the UI/UX design, these apps end up feeling very _flat_ and lifeless. As such, even the smallest feature that adds an organic touch can vastly improve the "feel" of the app.

If your application is very audio-centric (a game for instance), one such feature you can easily add is spatial audio. Giving your sound effect an _origin_ within the application can make the whole thing feel **bigger**. Let's take a quick look at how this can be achieved using a `JavaScript` audio library called `Howler`.

I won't go into details about how `Howler` itself works, but you can read up on the subject in their docs [here.](https://github.com/goldfire/howler.js#documentation) For now, all you need to know is that we use the `Howl` constructor to instantiate a simple sound effect object, and that this object takes an optional parameter called `stereo`.

The `stereo` parameter accepts a number ranging anywhere between -1 and 1, which corresponds to the left/right channel bias for the stereo sound (-1 being full left, 1 being full right). For this example, we simply want to play a sound effect when the mouse is clicked, and we want it to _feel_ as though that sound originates from the cursor.

Below is the basic setup for use in a `React` component. This will play the specified sound effect normally whenever the mouse is clicked within the component.

```javascript
import { useEffect } from "react";
import { Howl } from "howler";
import mySound from "./sounds/mySound.webm"; // our fictitious audio file, replace this with whatever sound you want to play

const MyComponent = () => {
  let component;

  useEffect(() => {
    const handleClick = (e) => {
      const sound = new Howl({ src: mySound }); // instantiate a new Howl here, passing it the path to our sound effect
      sound.play(); //  as soon as the object is created, we can play the sound effect
    };

    component && component.addEventListener("click", handleClick); //  once the component has been rendered and saved to a variable, add the EventListener

    return () => {
      component && component.removeEventListener("click", handleClick); //  if the component is removed, remove the EventListener
    };
  }, [component]);

  return (
    <div
      style={{ width: "100vw", height: "100vh" }} //  adding the styling ensures that our component will cover the entire viewport
      ref={(el) => (component = el)} // save the rendered element to a ref variable we can manipulate
    />
  );
};

export default MyComponent;

```

Now, to figure out where the sound is _coming from_, we need to do some simple calculations based on coordinates of the cursor in relation to the width of the component. We will do so by adding the following function to the top of the `useEffect` callback.

```javascript
const getStereoBias = (mouseX) => {
    const w = component.clientWidth; // grab the component's width
    const bias = -((Math.round(w / 2) - mouseX) / w) * 2; // calculate a value of -1 to 1 based on the cursor position within the component
    return bias;
  };
```

And finally, we will use this function whenever a sound effect is generated to tell `Howler` where the sound is _coming from_ by modifying the `handleClick` function as follows.

```javascript
 const handleClick = (e) => {
    const stereoBias = getStereoBias(e.clientX); //  calculate the "position" of the sound's origin

    const sound = new Howl({ src: mySound, stereo: stereoBias }); // instantiate a new Howl here, passing it the path to our sound effect and stereo bias "position"
    sound.play(); //  as soon as the object is created, we can play the sound effect
  };
```

And just like that, whenever our sound effect is played, it will follow the cursor around the screen (handy for things like particle effects in games)!

![mind blown](https://dev-to-uploads.s3.amazonaws.com/i/t4qm0c3eqh70igu6ydqz.gif)

To view a fleshed-out example of this concept in action, check out my [Domain Destroyer Demo](https://domain-destroyer-demo-95ini.ondigitalocean.app/) project.

If you make something cool like this, drop it in the comments, I'd love to see what you come up with!