diff --git a/apps/common-app/src/examples/DrumMachine/DrumMachine.tsx b/apps/common-app/src/examples/DrumMachine/DrumMachine.tsx index c71b421d..9b7508c6 100644 --- a/apps/common-app/src/examples/DrumMachine/DrumMachine.tsx +++ b/apps/common-app/src/examples/DrumMachine/DrumMachine.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect, FC } from 'react'; -import { Text } from 'react-native'; +import { Text, View } from 'react-native'; import { AudioContext } from 'react-native-audio-api'; import Container from '../../components/Container'; @@ -7,58 +7,120 @@ import Steps from '../../components/Steps'; import PlayPauseButton from '../../components/PlayPauseButton'; import Spacer from '../../components/Spacer'; import Slider from '../../components/Slider'; -import { Sounds, SoundSteps } from '../../types'; -import { SoundEngine, Kick, Clap, HiHat } from '../SharedUtils'; -import { View } from 'react-native'; - -const STEPS: SoundSteps = { - kick: new Array(8).fill(false), - hihat: new Array(8).fill(false), - clap: new Array(8).fill(false), -}; +import { Sounds, SoundName } from '../../types'; +import { Kick, Clap, HiHat, Scheduler } from '../SharedUtils'; + +const STEPS: Sounds = [ + { name: 'kick', steps: new Array(8).fill(false) }, + { name: 'clap', steps: new Array(8).fill(false) }, + { name: 'hihat', steps: new Array(8).fill(false) }, +]; const DrumMachine: FC = () => { const audioContextRef = useRef(null); - const soundEngines = useRef>( - {} as Record - ); - const [sounds, setSounds] = useState(STEPS); + const schedulerRef = useRef(null); + const kickRef = useRef(null); + const hiHatRef = useRef(null); + const clapRef = useRef(null); + + const [sounds, setSounds] = useState(STEPS); const [isPlaying, setIsPlaying] = useState(false); const [bpm, setBpm] = useState(120); - const handleStepClick = (name: Sounds, idx: number) => { + const handleStepClick = (name: SoundName, idx: number) => { setSounds((prevSounds) => { - const newSounds = { ...prevSounds }; - const steps = newSounds[name]; + const newSounds = [...prevSounds]; + const steps = newSounds.find((sound) => sound.name === name)?.steps; if (steps) { - const newSteps = [...steps]; - newSteps[idx] = !newSteps[idx]; - newSounds[name] = newSteps; + steps[idx] = !steps[idx]; + } + if (schedulerRef.current) { + schedulerRef.current.steps = newSounds; } return newSounds; }); }; + const handleBpmChange = (newBpm: number) => { + handlePause(); + setBpm(newBpm); + if (schedulerRef.current) { + schedulerRef.current.bpm = newBpm; + } + }; + + const handlePause = () => { + setIsPlaying(false); + schedulerRef.current?.stop(); + }; + const handlePlayPause = () => { + if (!audioContextRef.current || !schedulerRef.current) { + return; + } + if (!isPlaying) { setIsPlaying(true); + schedulerRef.current.start(); + } else { + setIsPlaying(false); + schedulerRef.current.stop(); + } + }; + + const playSound = (name: SoundName, time: number) => { + if (!audioContextRef.current || !schedulerRef.current) { return; } - setIsPlaying(false); + if (!kickRef.current) { + kickRef.current = new Kick(audioContextRef.current); + } + + if (!hiHatRef.current) { + hiHatRef.current = new HiHat(audioContextRef.current); + } + + if (!clapRef.current) { + clapRef.current = new Clap(audioContextRef.current); + } + + switch (name) { + case 'kick': + kickRef.current.play(time); + break; + case 'hihat': + hiHatRef.current.play(time); + break; + case 'clap': + clapRef.current.play(time); + break; + default: + break; + } }; useEffect(() => { if (!audioContextRef.current) { audioContextRef.current = new AudioContext(); - soundEngines.current.kick = new Kick(audioContextRef.current); - soundEngines.current.hihat = new HiHat(audioContextRef.current); - soundEngines.current.clap = new Clap(audioContextRef.current); + } + + if (!schedulerRef.current) { + const scheduler = new Scheduler( + bpm, + 8, + audioContextRef.current, + STEPS, + playSound + ); + schedulerRef.current = scheduler; } return () => { + schedulerRef.current?.stop(); audioContextRef.current?.close(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( @@ -71,17 +133,17 @@ const DrumMachine: FC = () => { bpm: {bpm} - {Object.entries(sounds).map(([name, steps]) => ( + {sounds.map(({ name, steps }) => ( diff --git a/apps/common-app/src/examples/Metronome/Metronome.tsx b/apps/common-app/src/examples/Metronome/Metronome.tsx index bf92f946..b5db8b5f 100644 --- a/apps/common-app/src/examples/Metronome/Metronome.tsx +++ b/apps/common-app/src/examples/Metronome/Metronome.tsx @@ -6,19 +6,16 @@ import Slider from '../../components/Slider'; import Container from '../../components/Container'; import PlayPauseButton from '../../components/PlayPauseButton'; import Spacer from '../../components/Spacer'; - -const SCHEDULE_INTERVAL_MS = 25; -const SCHEDULE_AHEAD_TIME = 0.1; +import { Scheduler, MetronomeSound } from '../SharedUtils'; +import { Sounds, SoundName } from '../../types'; const DOWN_BEAT_FREQUENCY = 1000; const REGULAR_BEAT_FREQUENCY = 500; -const INITIAL_GAIN = 1; -const FADE_OUT_START_GAIN = 0.5; -const FADE_OUT_END_GAIN = 0.001; - -const FADE_OUT_START_TIME = 0.01; -const FADE_OUT_END_TIME = 0.03; +const STEPS: Sounds = [ + { name: 'downbeat', steps: [true, false, false, false] }, + { name: 'regularbeat', steps: [false, true, true, true] }, +]; const Metronome: FC = () => { const [bpm, setBpm] = useState(120); @@ -26,115 +23,102 @@ const Metronome: FC = () => { const [isPlaying, setIsPlaying] = useState(false); const audioContextRef = useRef(null); - const intervalRef = useRef(null); - const nextNoteTimeRef = useRef(0.0); - const currentBeatRef = useRef(0); - - const handlePlay = () => { - if (!audioContextRef.current) { - audioContextRef.current = new AudioContext(); - } - setIsPlaying(true); - currentBeatRef.current = 0; - nextNoteTimeRef.current = - audioContextRef.current.currentTime + SCHEDULE_INTERVAL_MS / 1000; - intervalRef.current = setInterval(scheduler, SCHEDULE_INTERVAL_MS); - }; + const downbeatSoundRef = useRef(null); + const regularbeatSoundRef = useRef(null); + const schedulerRef = useRef(null); const handlePause = () => { setIsPlaying(false); - - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } + schedulerRef.current?.stop(); }; const handlePlayPause = () => { + if (!audioContextRef.current || !schedulerRef.current) { + return; + } + if (isPlaying) { handlePause(); return; } - handlePlay(); + setIsPlaying(true); + schedulerRef.current.start(); }; const handleBpmChange = (newBpm: number) => { - setBpm(newBpm); handlePause(); + setBpm(newBpm); + if (schedulerRef.current) { + schedulerRef.current.bpm = newBpm; + } }; const handleBeatsPerBarChange = (newBeatsPerBar: number) => { - setBeatsPerBar(newBeatsPerBar); handlePause(); + setBeatsPerBar(newBeatsPerBar); + if (schedulerRef.current) { + schedulerRef.current.beatsPerBar = newBeatsPerBar; + const steps = new Array(newBeatsPerBar).fill(false); + steps[0] = true; + + schedulerRef.current.steps = [ + { name: 'downbeat', steps }, + { name: 'regularbeat', steps: steps.map((value) => !value) }, + ]; + } }; - const scheduler = () => { - if (!audioContextRef.current) { + const playSound = (name: SoundName, time: number) => { + if (!audioContextRef.current || !schedulerRef.current) { return; } - while ( - nextNoteTimeRef.current < - audioContextRef.current.currentTime + SCHEDULE_AHEAD_TIME - ) { - playNote(currentBeatRef.current, nextNoteTimeRef.current); - nextNote(); + if (!downbeatSoundRef.current) { + downbeatSoundRef.current = new MetronomeSound( + audioContextRef.current, + DOWN_BEAT_FREQUENCY + ); } - }; - const nextNote = () => { - const secondsPerBeat = 60.0 / bpm; - nextNoteTimeRef.current += secondsPerBeat; - - currentBeatRef.current += 1; - if (currentBeatRef.current === beatsPerBar) { - currentBeatRef.current = 0; + if (!regularbeatSoundRef.current) { + regularbeatSoundRef.current = new MetronomeSound( + audioContextRef.current, + REGULAR_BEAT_FREQUENCY + ); } - }; - const playNote = (beatNumber: number, time: number) => { - if (!audioContextRef.current) { - return; + switch (name) { + case 'downbeat': + downbeatSoundRef.current.play(time); + break; + case 'regularbeat': + regularbeatSoundRef.current.play(time); + break; + default: + break; } - - const oscillator = audioContextRef.current.createOscillator(); - const gain = audioContextRef.current.createGain(); - - oscillator.frequency.value = - beatNumber % beatsPerBar === 0 - ? DOWN_BEAT_FREQUENCY - : REGULAR_BEAT_FREQUENCY; - - gain.gain.setValueAtTime(INITIAL_GAIN, time); - gain.gain.linearRampToValueAtTime( - FADE_OUT_START_GAIN, - time + FADE_OUT_START_TIME - ); - gain.gain.linearRampToValueAtTime( - FADE_OUT_END_GAIN, - time + FADE_OUT_END_TIME - ); - - oscillator.connect(gain); - gain.connect(audioContextRef.current.destination); - - oscillator.start(time); - oscillator.stop(time + FADE_OUT_END_TIME); }; useEffect(() => { - if (!audioContextRef.current) { + if (!audioContextRef.current || !schedulerRef.current) { audioContextRef.current = new AudioContext(); } + if (!schedulerRef.current) { + schedulerRef.current = new Scheduler( + bpm, + beatsPerBar, + audioContextRef.current, + STEPS, + playSound + ); + } return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - + schedulerRef.current?.stop(); audioContextRef.current?.close(); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (