0

I'm trying to get React Native to whistle 3 times before a timer, so for example, whistle 3 seconds in a row, then let the timer go, then whistle again, but for some reason it is only doing it twice, it's skipping the middle whistle and sometimes the last one.

I've tried mounting the sound before hand, reducing the sound duration to about .3 seconds, and it is still skipping some plays. I know I need to do some refactor on the timers, but I think at least playing the sound should work.

import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Dimensions,
  Vibration,
} from "react-native";
import React from "react";
import { StatusBar } from "expo-status-bar";
import { Audio } from "expo-av";

const screen = Dimensions.get("window");
let timeout: NodeJS.Timeout | undefined = undefined;

interface TimerComponentProps {
  timeInSeconds?: number;
}

export const TimerComponent: React.FC<TimerComponentProps> = ({
  timeInSeconds = 5,
}) => {
  const [remaningSeconds, setRemainingSeconds] = React.useState(timeInSeconds);
  const [isActive, setIsActive] = React.useState(false);
  const [sound, setSound] = React.useState<Audio.Sound | undefined>(undefined);
  const [shouldCount, setShouldCount] = React.useState(false);
  const [counter, setCounter] = React.useState(3);

  const { minutes, seconds } = React.useMemo(() => {
    const minutes = Math.floor(remaningSeconds / 60);
    const seconds = remaningSeconds % 60;
    return { minutes, seconds };
  }, [remaningSeconds]);

  async function mountSound() {
    try {
      const { sound } = await Audio.Sound.createAsync(
        require("../../assets/audio/Whistle.wav")
      );
      setSound(sound);
    } catch (error) {
      console.error(error);
    }
  }

  async function playWhistle() {
    if (sound) {
      try {
        await sound.playAsync();
      } catch (error) {
        console.error(error);
      }
    }
  }

  const endTimer = async () => {
    try {
      await playWhistle();
      setIsActive(false);
    } catch (error) {
      console.error(error);
    }
  };

  const startCounter = async () => {
    await mountSound();
    setShouldCount(true);
  };

  const resetTimer = () => {
    if (timeout) {
      clearTimeout(timeout);
    } else {
      timeout = setTimeout(() => {
        setRemainingSeconds(timeInSeconds);
        clearTimeout(timeout);
      }, 1000);
    }
  };

  React.useEffect(() => {
    let counterInterval: NodeJS.Timer | undefined = undefined;
    if (shouldCount) {
      counterInterval = setInterval(() => {
        try {
          if (counter === 1) {
            setCounter((counter) => counter - 1);
          }

          if (counter > 1) {
            playWhistle();
            Vibration.vibrate();
            setCounter((counter) => counter - 1);
          } else {
            // Plays the whistle sound and vibrates the device
            playWhistle();
            Vibration.vibrate();

            // Restarts the counter
            setCounter(3);
            setShouldCount(false);

            // Starts the timer
            setIsActive(true);

            // Stops the counter
            clearInterval(counterInterval);
          }
        } catch (error) {
          console.error(error);
        }
      }, 1000);
    } else if (!shouldCount && counter !== 0) {
      clearInterval(counterInterval);
    }

    return () => clearInterval(counterInterval);
  }, [shouldCount, counter]);

  React.useEffect(() => {
    let timerInterval: NodeJS.Timer | undefined = undefined;
    if (isActive) {
      timerInterval = setInterval(() => {
        if (remaningSeconds === 1) {
          setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
        }
        if (remaningSeconds > 1) {
          setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
        } else {
          Vibration.vibrate();
          endTimer();
          resetTimer();
        }
      }, 1000);
    } else if (!isActive && remaningSeconds === 0) {
      resetTimer();
      clearInterval(timerInterval);
    }
    return () => clearInterval(timerInterval);
  }, [isActive, remaningSeconds]);

  React.useEffect(() => {
    return sound
      ? () => {
          sound.unloadAsync();
          setSound(undefined);
        }
      : undefined;
  }, [sound]);

  const parseTime = (time: number) => {
    return time < 10 ? `0${time}` : time;
  };

  return (
    <View style={styles.container}>
      <StatusBar style="light" />
      <Text style={styles.timerText}>{`${parseTime(minutes)}:${parseTime(
        seconds
      )}`}</Text>
      <TouchableOpacity onPress={startCounter} style={styles.button}>
        <Text style={styles.buttonText}>{isActive ? "Pause" : "Start"}</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#07121B",
    alignItems: "center",
    justifyContent: "center",
  },
  button: {
    borderWidth: 10,
    borderColor: "#B9AAFF",
    width: screen.width / 2,
    height: screen.width / 2,
    borderRadius: screen.width / 2,
    alignItems: "center",
    justifyContent: "center",
  },
  buttonText: {
    color: "#B9AAFF",
    fontSize: 20,
  },
  timerText: {
    color: "#fff",
    fontSize: 90,
  },
});


Luis
  • 44
  • 1
  • 5
  • 17

1 Answers1

0

The issue was that expo-av leaves the audio file at it's end, so the next time you play it, nothing will sound because the file is already over, the way to fix it is pretty simple:

async function playWhistle() {
    if (sound) {
      try {
        await sound.playAsync();
        sound.setPositionAsync(0); // ADD THIS LINE
      } catch (error) {
        console.error(error);
      }
    }
  }
Luis
  • 44
  • 1
  • 5
  • 17