Reputation: 7493
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
Reputation: 5075
useSound
WrapperWe 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
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;
id
prop is used to differentiate one button from another.currentPlayButton
prop is used to keep track of which button is currently clicked.setCurrentPlayButton
prop is a function used to change the value of currentPlayButton
.useEffect
inside the <Button/>
component is used to ensure that only one button can be played at a time.Upvotes: 0