diff --git a/apps/common-app/src/App.tsx b/apps/common-app/src/App.tsx index 5b4937ab..7ad39b2a 100644 --- a/apps/common-app/src/App.tsx +++ b/apps/common-app/src/App.tsx @@ -40,7 +40,7 @@ const App: FC = () => { { - return ; + const [bpm, setBpm] = useState(120); + const [beatsPerBar, setBeatsPerBar] = useState(4); + 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 handlePause = () => { + setIsPlaying(false); + + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + const handlePlayPause = () => { + if (isPlaying) { + handlePause(); + + return; + } + + handlePlay(); + }; + + const handleBpmChange = (newBpm: number) => { + setBpm(newBpm); + handlePause(); + }; + + const handleBeatsPerBarChange = (newBeatsPerBar: number) => { + setBeatsPerBar(newBeatsPerBar); + handlePause(); + }; + + const scheduler = () => { + if (!audioContextRef.current) { + return; + } + + while ( + nextNoteTimeRef.current < + audioContextRef.current.currentTime + SCHEDULE_AHEAD_TIME + ) { + playNote(currentBeatRef.current, nextNoteTimeRef.current); + nextNote(); + } + }; + + const nextNote = () => { + const secondsPerBeat = 60.0 / bpm; + nextNoteTimeRef.current += secondsPerBeat; + + currentBeatRef.current += 1; + if (currentBeatRef.current === beatsPerBar) { + currentBeatRef.current = 0; + } + }; + + const playNote = (beatNumber: number, time: number) => { + if (!audioContextRef.current) { + return; + } + + 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(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + return ( + +