javascriptreactjsaudiovolume

cant change the volume of music after it starts playing


I am trying to make a simple component that when you press a button, will start playing a looping song which you could control the volume of using a range input.

This is my code so far:

import { useState, useEffect } from "react";
import music from "./assets/music.mp3";

function MusicPlay() {
  const [volume, setVolume] = useState(20);
  const testMusic = new Audio(music);
  const playMusic = () => {
    testMusic.play();
    testMusic.volume = volume / 100;
    testMusic.loop = true;
    console.log("started music", testMusic, testMusic.volume);
  };

  useEffect(() => {
    testMusic.volume = volume / 100;
    console.log("changed volume", testMusic, testMusic.volume);
  }, [volume]);

  return (
    <div className="App">
      <button onClick={playMusic}>Play</button>
      <input
        type="range"
        min="0"
        max="100"
        step="1"
        value={volume}
        onChange={(e) => {
          setVolume(Number(e.target.value));
        }}
      />
    </div>
  );
}

export default MusicPlay;

right now you can press the button to start music, and it will start at the volume you specified, but when you move the slider it doesnt update the volume


Solution

  • Every render of this component will create a new instance of Audio. Changing the volume will cause a rerender. This means that you lost the reference to your playing Audio quite a while ago. If you store the audio in a ref, you'll always refer to the same audio and be able to change it's volume:

    import React, { useState, useEffect, useRef } from "react";
    import music from "./assets/music.mp3";
    
    function MusicPlay() {
      const [volume, setVolume] = useState(20);
      const testMusicRef = useRef();
      React.useEffect(() => {
        if (!testMusicRef.current) {
          testMusicRef.current = new Audio(music);
        }
      }, []);
      const playMusic = () => { // consider wrapping this in useCallback too
        if (!testMusicRef.current) {
          return;
        }
        testMusicRef.current.play();
        testMusicRef.current.volume = volume / 100;
        testMusicRef.current.loop = true;
        console.log(
          "started music",
          testMusicRef.current,
          testMusicRef.current.volume
        );
      };
    
      useEffect(() => {
        if (!testMusicRef.current) {
          return;
        }
        testMusicRef.current.volume = volume / 100;
        console.log(
          "changed volume",
          testMusicRef.current,
          testMusicRef.current.volume
        );
      }, [volume]);
    
      return (
        <div className="App">
          <button onClick={playMusic}>Play</button>
          <input
            type="range"
            min="0"
            max="100"
            step="1"
            value={volume}
            onChange={(e) => {
              setVolume(Number(e.target.value));
            }}
          />
        </div>
      );
    }
    
    export default MusicPlay;