antonwilhelm
antonwilhelm

Reputation: 7493

Prevent Overlapping Sounds with use-sound / howler

I am transitioning to use-sound as my audio player right now, but I have a problem when having multiple components on the same page. My sounds are a bit longer than simple beeps, so the problem is that sounds will be overlapping if the user clicks on multiple components that trigger useSound.

The interrupt flag that useSound provides is very useful when using sprites or having multiple sounds in one component, but I am using multiple components, so the instances won't affect each other.

Here is my basic component:

import useSound from 'use-sound';

const Button = ({soundPath}) => {
  const [play] = useSound(soundPath, { interrupt: true });

  return <button onClick={play}>Boop!</button>;
};

and I am using it like so:

<div>
   <Button soundPath="https://...audio1.mp3" />
   <Button soundPath="https://...audio2.mp3" />
   <Button soundPath="https://...audio3.mp3" />
</div>

I had this problem before when using a different audio player, and I used to solve it by finding all html <audio> tags and pausing them, like so:

 document.addEventListener(
            "play",
            function (e) {
              var audios = document.getElementsByTagName("audio");
              for (var i = 0, len = audios.length; i < len; i++) {
                if (audios[i] != e.target) {
                  audios[i].pause();
                  audios[i].currentTime = 0;
                }
              }
            },
            true
          );

Not the most elegant solution, but it worked. This doesn't seem to work with use-sound and I am not sure why? It seems like it does not inject <audio> tags into the DOM.

Any ideas on how I could solve this?

Upvotes: 0

Views: 604

Answers (2)

Andria
Andria

Reputation: 5075

React Context for State and a Callback With a useSound Wrapper

We can utilize React's Context API to create a context that stores the last sound to be played and provides a callback that stops the last sound, starts the new sound, and stores it. Then we can make a hook that wraps useSound and its play function to interact with our context.

View this example live at CodeSandbox.

// src/App.js
import { SoundProvider } from "./components/SoundProvider.js";
import { SoundButton } from "./components/SoundButton.js";

export default function App() {
  return (
    <SoundProvider>
      <SoundButton src="/sounds/water-hose-gun.mp3">
        <span role="img" aria-label="Water gun">
          🔫
        </span>
      </SoundButton>
      <SoundButton src="/sounds/cool-jazz.mp3">
        <span role="img" aria-label="Trumpet">
          🎺
        </span>
      </SoundButton>
      <SoundButton src="/sounds/speaking.mp3">
        <span role="img" aria-label="Person speaking">
          🗣️
        </span>
      </SoundButton>
      <SoundButton src="/sounds/car-park-explosion.mp3">
        <span role="img" aria-label="Explosion">
          💥
        </span>
      </SoundButton>
    </SoundProvider>
  );
}

With this method, you can nest the <SoundButton> in as many other components as you want and in different sections of your app, and it will still be able to interact with the nearest <SoundProvider>.

// src/contexts/sound.js
import { createContext } from "react";
export const SoundContext = createContext(null);
// src/components/SoundProvider
import { useCallback, useState } from "react";
import { SoundContext } from "../contexts/sound";

export function SoundProvider(props) {
  const [sound, setSound] = useState(null);

  const playSound = useCallback(
    (newSound, ...args) => {
      sound?.[1].stop();
      setSound(newSound);
      newSound[0](...args);
    },
    [sound]
  );

  return <SoundContext.Provider {...props} value={{ sound, playSound }} />;
}
// src/hooks/useSound.js
import { useContext } from "react";
import { SoundContext } from "../contexts/sound";
import useSound from "use-sound";

function useSoundWrapper(src, hookOptions) {
  const sound = useSound(src, hookOptions);
  const { playSound } = useContext(SoundContext);
  return [(...args) => playSound(sound, ...args), sound[1]];
}
export { useSoundWrapper as useSound };

This wrapper implementation makes using this solution no different than using the original useSound. As you can see below, nothing really changes for the <SoundButton> besides the import.

// src/components/SoundButton.js
import { useSound } from "../hooks/useSound.js";

export function SoundButton({ src, ...props }) {
  const [play] = useSound(src, { interrupt: true });
  return <button onClick={play} {...props} />;
}

Upvotes: 0

Khairani F
Khairani F

Reputation: 448

This is what I did:

import {
  useState,
  useEffect,
} from "react"

import useSound from "use-sound";

const Button = ({
  id,
  soundPath,
  currentPlayButton,
  setCurrentPlayButton,
}) => {

  const [play, { stop }] = useSound(soundPath, {
    interrupt: true,  });

  const handleClick = () => {
    play();
    setCurrentPlayButton(id);
  }

  useEffect(() => {
    if (id !== currentPlayButton) {
      stop();
    }
  }, [currentPlayButton, id]);

  return <button onClick={handleClick}>Boop!</button>;
};

const AllButtons = () => {
  const [currentPlayButton, setCurrentPlayButton] = useState(0)

  return (
    <>
      <Button
        id={1}
        currentPlayButton={currentPlayButton}
        soundPath="https://...audio1.mp3"
        setCurrentPlayButton={setCurrentPlayButton}
      />
      <Button
        id={2}
        currentPlayButton={currentPlayButton}
        soundPath="https://...audio1.mp3"
        setCurrentPlayButton={setCurrentPlayButton}
      />
    </>
  );
};

export default AllButtons;
  1. The id prop is used to differentiate one button from another.
  2. The currentPlayButton prop is used to keep track of which button is currently clicked.
  3. The setCurrentPlayButton prop is a function used to change the value of currentPlayButton.
  4. The useEffect inside the <Button/> component is used to ensure that only one button can be played at a time.

Upvotes: 0

Related Questions