» React创建番茄工作法Pomodoro Web应用 » 2. 开发 » 2.7 添加音效

添加音效

如果你倾向于尽量少使用第三方库,可以尝试用 audio 标签来播放音效。

如下方例子所示,你可以通过 react 的 Ref 来控制音频标签。

import React, { useRef } from 'react';

const SoundPlayer = () => {
  const audioRef = useRef(null);

  const playSound = () => {
    audioRef.current.play();
  };

  return (
    <div>
      <button onClick={playSound}>Play Sound</button>
      <audio ref={audioRef}>
        <source src="your-sound-file.mp3" type="audio/mp3" />
        Your browser does not support the audio tag.
      </audio>
    </div>
  );
};

export default SoundPlayer;

如果你有很多音频文件要操弄的话,建议使用 howler

HOWLER.JS

App.js 中的修改:

@@ -2,11 +2,19 @@ import { useEffect, useRef, useState } from "react";
 import "./App.css";
 import Settings from "./components/Settings";
 import Timer from "./components/Timer";
+import { Howl, Howler } from "howler";
 
 const POMODORO_SECONDS = 25 * 60;
 const BREAK_SECONDS = 5 * 60;
 const PHASE_POMODORO = 0;
 const PHASE_BREAK = 1;
+
+// 音频来自 https://pixabay.com/sound-effects/search/tick-tock/
+const SOUNDS = {
+  tick: process.env.PUBLIC_URL + "/tick.mp3",
+  alarm: process.env.PUBLIC_URL + "/alarm.mp3",
+  button: process.env.PUBLIC_URL + "/button.mp3",
+};
 const DEFAULT_SETTING = {
   useCircle: true,
   soundOn: true,
@@ -22,12 +30,29 @@ function App() {
   useEffect(() => {
     if (seconds === 0) {
       stopTimer();
-      alarm();
+      // Howler.stop() 方法的特殊处理
+      setTimeout(() => {
+        // alarm
+        playShortSound(SOUNDS.alarm);
+      }, 10);
     }
   }, [seconds]);
 
+  useEffect(() => {
+    if (ticking) {
+      playLoopSound(SOUNDS.tick);
+    } else {
+      stopSound(tickSoundIdRef.current);
+    }
+  }, [ticking]);
+
+  useEffect(() => {
+    Howler.mute(!settings.soundOn);
+  }, [settings.soundOn]);
+
   // 使用 `useRef` hook 来创建一个可修改的 object,可在多次 render 中保持其值
   const intervalIdRef = useRef(null);
+  const tickSoundIdRef = useRef(null);
 
   const startTimer = () => {
     setTicking(true);
@@ -49,6 +74,7 @@ function App() {
   };
 
   const toggleTimer = () => {
+    playShortSound(SOUNDS.button);
     if (ticking) {
       // Clicked "Pause"
       stopTimer();
@@ -69,6 +95,11 @@ function App() {
     return seconds / duration;
   };
 
+  const skippable = () => {
+    const percentage = calcPercentage();
+    return percentage < 1 && percentage > 0;
+  };
+
   const pickPhase = (phase) => {
     const secBg = "secondary-bg";
     if (phase === PHASE_POMODORO) {
@@ -81,13 +112,30 @@ function App() {
   };
 
   const skipPhase = () => {
+    playShortSound(SOUNDS.button);
     const newPhase = (phase + 1) % 2;
     pickPhase(newPhase);
   };
 
-  const alarm = () => {
-    // TODO: 播放音效
-    console.log("Time's up!");
+  const playLoopSound = (url) => {
+    const sound = new Howl({
+      src: [url],
+      loop: true,
+    });
+    tickSoundIdRef.current = sound.play();
+  };
+
+  const playShortSound = (url) => {
+    const sound = new Howl({
+      src: [url],
+    });
+    sound.play();
+  };
+
+  const stopSound = (soundId) => {
+    if (soundId) {
+      Howler.stop(soundId);
+    }
   };
 
   return (
@@ -131,7 +179,7 @@ function App() {
             {ticking ? "Pause" : seconds === 0 ? "Next" : "Start"}
           </button>
         </div>
-        <span className="skip-btn" onClick={skipPhase}>
+        <span hidden={!skippable()} className="skip-btn" onClick={skipPhase}>
           skip
         </span>
       </div>