DEV Community

Cover image for Rethinking web audio feedback with the useSound Hook
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Rethinking web audio feedback with the useSound Hook

Written by Olasunkanmi John Ajiboye✏️

Historically, sounds on the web have gotten a bad rap — and rightly so. They can be jarring, distracting, and sometimes startling to users. However, proper use of sound on an app can provide helpful cues to end users, enriching the user interaction overall.

Sound can be used to highlight specific user actions and accentuate important feedback. When handled elegantly, sound can give life to an otherwise dull user experience. There are many use cases in which sound can enrich user experience. Games and mobile apps may immediately come to mind, but web can also benefit from this enriching user experience.

One golden rule to keep in mind is accessibility, which we will dive into in greater detail later on. A user must have the ability to opt out, and sounds should never auto-play without explicit user consent. With this in mind, the possibilities are endless.

Consider important notifications, new messages in chats when a user has navigated away from the tab or browser, and so on. This is where the useSound Hook becomes really useful. It helps to seamlessly integrate sound into your React-based UI.

Overview

useSound is a React Hook that allows you to easily add sound to your React projects. It comes with many options for most of the common use cases. It also extends the howler.js library, which enables you to extend the functionality it already provides.

At ~1KB gzipped, and asynchronously loading about 10KB of howler.js, it is small enough that it won’t significantly impact your app’s performance. According to the announcement blog, you get the following functionalities out of the box, and many more:

  • Prematurely stop the sound, or pause/resume the sound
  • Load an audio sprite and split it up into many individual sounds
  • Tweak playback speed to speed up/slow down sounds
  • Tons of event listeners
  • Lots of other advanced stuff, made possible by howler.js

LogRocket Free Trial Banner

Getting started

Installation

The package can be installed via either yarn or npm:

#  yarn
yarn add use-sound
 # npm
npm install use-sound
Enter fullscreen mode Exit fullscreen mode

Imports

This package exports a single default value: the useSound Hook.

import useSound from 'use-sound';
Enter fullscreen mode Exit fullscreen mode

This is all you need to start using the Hook. Of course, you will need to import the sound to be used as well. With create-react-app, you can import this like any other arbitrary file (e.g., an image). You can easily get free sound from resources like Freesound or ZapSplat.

For example:

import ping from '../../sounds/ping.mp3';
const [play, { stop }] = useSound(ping);
Enter fullscreen mode Exit fullscreen mode

Core concepts

As you might have noticed from the imports and usage example above, we destructured play and stop from the Hook, which accepts the ping sound.

These are the two basic methods that can be used for playing and pausing sound. By default, sound doesn’t play until the user interacts with an element or it is intentionally triggered. This is great for accessibility and allows us to lazy-load sound and third-party libraries.

Additionally, the useSound Hook can accept the path to the sound directly as the first argument. You can also add a config object consisting of the hookOptions for more control and flexibility — for instance, the playbackRate, volume, interrupt, etc. This is reactive and syncs with the state of the component.

const [volume, setVolume] = React.useState(0.75);
const [play] = useSound('/path/to/sound', { volume });
Enter fullscreen mode Exit fullscreen mode

hookOptions

When calling useSound, you can pass it a variety of options referred to as hookOptions. The charts below, along with additional details and an exhaustive API list, are available in the useSound API documentation:

Name Value
volume Number
playbackRate Number
interrupt Boolean
soundEnabled Boolean
sprite spriteMap
[delegated]

Besides the play method, you also have access to the exposedData object, extending your UI control possibilities:

Name Value
stop Function – (id?: string) => void
pause Function – (id?: string) => void
isPlaying Boolean
duration Number (or null)
sound Howl (or null)

Escape hatches with howler.js

howler.js is an audio library that makes working with audio in JavaScript easy and reliable across all platforms. Any unrecognized option you pass to hookOptions will be delegated to howler.js. You can see the full list of options in the howler.js docs.

Here’s an example of how we can use onPlayError to fire a function when there is an error:

const [play] = useSound('/beep.mp3', {
  onPlayError: () => {
    console.error('Error occured!');
  },
})
Enter fullscreen mode Exit fullscreen mode

Or fire a callback when the sound is muted:

const [play] = useSound('/thong.mp3', {
  onmute: () => {
    myCallback()
  },
})
Enter fullscreen mode Exit fullscreen mode

We will go into use cases with concrete examples of all the core concepts in the next section.

Use cases and examples

In this section, we will explore some use cases with code samples and recipes. All examples can be explored or edited directly on CodeSandbox.

Popups and notifications

Two of the more common use case scenarios are popups and notifications. Think something similar to a Facebook notification tab; you want to get the user’s attention when they have a new notification, friend request, message, or a like on their posts.

To simulate this scenario, we will build a simple lookalike navbar with notification icons. We will then have a setInterval logic that randomly sets notification. I won’t go into the implementation details of the setInterval, which is available in full in the CodeSandbox. We will instead focus on handling this particular scenario with useSound.

First, create the AppBar component. Note that I have also added a checkbox toggle to demonstrate giving the user the ability to permanently turn off or turn on the sound if they so wish. This is important for good user experience and for accessibility.

import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBell } from "@fortawesome/free-solid-svg-icons";
import useSound from "use-sound";

import CheckBox from "./CheckBox";
import useInterval from "../hooks/useInterval";
import sound1 from "../assets/sound1.mp3";

const AppBar = () => {
  const [isRunning, setIsRunning] = useState(true);
  const [checked, setChecked] = useState(false);
  const [count, setCount] = useState(0);
  const [play] = useSound(sound1, { volume: 0.2 });

  useInterval(
    () => {
      setCount(count + 1);
      if (checked) {
        play();
      }
    },
    isRunning ? 3000 : null
  );

  const reset = () => {
    setIsRunning(false);
  };

  const toggle = () => {
    setChecked(!checked);
  };

  return (
    <nav className="appbar">
      <div className="toggle">
        <CheckBox handleChange={toggle} checked={checked} />
      </div>
      <span className="notification">
        <FontAwesomeIcon icon={faBell} onClick={() => reset()} />
        {!!count &&amp; <span className="badge">{count}</span>}
      </span>
    </nav>
  );
};
export default AppBar;
Enter fullscreen mode Exit fullscreen mode

And the CSS:

.appbar {
  display: flex;
  justify-content: space-between;
  background-color: blue;
  align-items: center;
  color: white;
  height: 50px;
}

.toggle {
  margin-left: 5px;
}
.icons * {
  margin: 0 5px;
}
Enter fullscreen mode Exit fullscreen mode

First, let’s review what we intend to achieve. We want to keep sounding the notification each x seconds until the user checks the notification. This is useful when a user navigates away from the tab or the browser but we would like to keep their attention.

Here we have simply called the play() method for as long as our condition is true. To reset or cancel the playing, we simply opt out of playing when isRunning or notification is false.

Play/pause button

Another common example is playing, pausing, and then resuming sound. Think Spotify or any other audio streaming app. Let’s quickly build this component (the full code is available in the CodeSandbox).

import React from "react";
import useSound from "use-sound";

const Pause = ({ stop }) => {
  return (
    <svg className="button" viewBox="0 0 60 60" onClick={()=>stop()}>
      <polygon points="0,0 15,0 15,60 0,60" />
      <polygon points="25,0 40,0 40,60 25,60" />
    </svg>
  );
};

const Play = ({ play }) => {
  return (
    <svg className="button" viewBox="0 0 60 60" onClick={play}>
      <polygon points="0,0 50,30 0,60" />
    </svg>
  );
};

const Player = () => {
  const [play, { stop, isPlaying }] = useSound(sound3);
  return (
    <div className="player">
      {isPlaying ? <Pause stop={stop} /> : <Play play={play} />}
    </div>
  );
};

export default Player;
Enter fullscreen mode Exit fullscreen mode

Let’s take a stab at the code above. The Player component toggles between play and stop. Just like with the previous example, we have delegated the play() and stop() method, respectively, to handle these cases on click.

The other useful piece of data here is the isPlaying property. This is a Boolean that tells us whether the sound is currently playing. For this use case, we have employed this property to toggle between play or stop.

Increasing pitches/volume

Another fun example is increasing pitch or volume.

To demonstrate this, we’d use a simple progress bar. We will increase the length of the progress bar with each click. This example is common in displaying health bars, game status, progress, etc. We will also increase the volume and pitch as the bar grows.

You will notice that the playbackRate and volume passed to useSound are reactive and automatically sync with state. Manipulating any of the exposedData is as easy as binding it to a state in the component.

import React, { useState } from "react";
import Progress from "react-progressbar";
import useSound from "use-sound";

import sound from "./sound3.mp3";

const ProgressBar = () => {
  const [status, setStatus] = useState(10);
  const [playbackRate, setPlaybackRate] = useState(0.75);
  const [ volume, setVolume]=  useState(0.4);

  const [play] = useSound(sound, {
    playbackRate,
    volume
  });

  const handleIncrease = () => {
    setPlaybackRate(playbackRate => playbackRate + 0.1);
    setStatus(status => status + 10);
    setVolume(volume=>volume+1)
    play();
  };

  return (
    <div>
      <Progress completed={status} onClick={handleIncrease} />
    </div>
  );
};

export default ProgressBar;
Enter fullscreen mode Exit fullscreen mode

Again, the full code is available on CodeSandbox.

Sprites

Sprites come in handy when we have to deal with a larger number of sounds in our app. Sprites combine many little sound files into one. This decreases files size, and most importantly, it is better for performance as it avoids many parallel HTTP trips to fetch different sound files.

We will build a simple set of buttons and bind the ID to the sound in the sprite such that each button is responsible for playing different sounds in the sprite.

import React from "react";
import useSound from "use-sound";

import sound from "./sound3.mp3";


function SpriteDemo() {
  const [play] = useSound(sound, {
    sprite: {
      kick: [0, 350],
      pong: [374, 160],
      bell: [666, 290],
      cowbell: [968, 200]
    }
  });

  const playSound = (e) => {
    e.preventDefault();
    play(e.target.id);
  };

  return (
    <>
      <button id="kick" onClick={e => playSound(e)}>
        Kick
      </button>
      <button id="pong" onClick={e => playSound(e)}>
        Pong
      </button>
      <button id="bell" onClick={e => playSound(e)}>
        Bell
      </button>
      <button id="cowbell" onClick={e => playSound(e)}>
        Cowbell
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

There are many more options and possibilities — you are only limited by your creativity. The documentation has more recipes for usage.

Accessibility concerns

A number of users would disagree that sound enhances UX on the web. And this isn’t just an auditory preference — it can be a cause of serious annoyance and accessibility issues if not handled properly.

Many visually impaired users rely on screen readers to parse the text on the web into sounds, which is then narrated to them. Stuffing the web with confusing sounds could be jarring for them and produce the opposite effect we had in mind. Hence, it is crucial to think critically about sound on the web. There are a few golden rules to keep in mind to ensure wider usability and accessibility.

It is necessary that all users must opt in to sound — that is, the user can decide whether they want to receive sound at all. Users must have the ability to easily mute or stop the sound, and the must be able to permanently disable sound until they decide otherwise. The control to do this should be readily keyboard accessible, e.g., with Tab key.

More importantly, the web application should be 100 percent usable without sound. For users who are hearing impaired, sound would be all but useless; if there is no other way of meaningfully interacting with the site without sound, that renders the website itself useless. In case of longer audio, attempts should be made to provide alternatives, such as a transcript.

The takeaway is to think about all users and the end goal of using sounds in the first place. For instance, in the notification example above, the user can still see the notifications with or without audio; a badge, color change, count, etc. would make that functionality 100 percent usable without audio.

Conclusion

Audio on the web is under-explored and under-utilized. An elegant, well thought-out use of sound on the web can deeply enrich user experience.

In the above examples, we have barely begun to scratch the surface when it comes to the possibilities. Almost all modern browsers support audio, but the native HTML solution can be hard to configure. The combination of third-party libraries like useSound and howler.js, along with some creativity, can produce amazing results.

While keeping accessibility in mind, I would implore product designers and developers to experiment and give audio enhancement a second look. Have a resounding time experimenting.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post Rethinking web audio feedback with the useSound Hook appeared first on LogRocket Blog.

Top comments (1)

Collapse
 
javaarchive profile image
Raymond

Nice! I like how you talked about user experience and letting them turn off sound. I recently saw this online os called Aurora OS popup. Using themes users were able to create something looking like Windows Longhorn. However there was a startup sound that was quite annoying to have during testing because the code editor refreshes the page on changes. One user said, "Could of made a warning that there’s a startup sound. It’s 11.49pm and I had my speaker on max". I agree on your point of view on usage of audio on the web.
support.glitch.com/t/auroraos-an-o...
Yes that really happended: support.glitch.com/t/windows-longh...