I made a metronome inspired by the famous Chris Wilson's article using React, Hooks, and the Web Audio API.
The metronome works but there's a delay between the moment I hit 'play' and the sound itself.
This is clearly noticeable if the BPM is very low (e.g. 40 BPM).
At first, I thought I needed to isolate the logic from the UI rendering using a Worker but now I start to think it's something else.
I think in the timer
function I need an else
calling sound
with a 0 value.
But I haven't found a solution yet.
Does anybody have an idea what's wrong and how to fix it?
Thanks!
import { useState } from 'react';
let ac;
let lastNote = 0;
let nextNote = 0;
let engine;
function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [bpm] = useState(40);
const oneBeatInSeconds = 60000 / bpm / 1000;
ac = new AudioContext();
const sound = (ac: AudioContext, time: number, dur: number) => {
// creates the sound, connects it and decides when it starts and stops
const osc = ac.createOscillator();
osc.connect(ac.destination);
osc.start(time);
osc.stop(time + dur);
};
const timer = () => {
// Calculates how long it was in ms from loading the browser to clicking the play button
const diff = ac.currentTime - lastNote;
// Schedules the next note if the diff is larger then the setInterval
if (diff >= oneBeatInSeconds) {
nextNote = lastNote + oneBeatInSeconds;
lastNote = nextNote;
sound(ac, lastNote, 0.025);
}
ac.resume();
};
if (isPlaying) {
// If the metronome is playing resumes the audio context
ac.resume();
clearInterval(engine);
engine = setInterval(timer, oneBeatInSeconds);
} else {
// If the metronome is stopped, resets all the values
ac.suspend();
clearInterval(engine);
lastNote = 0;
nextNote = 0;
}
const toggleButton = () =>
isPlaying === true ? setIsPlaying(false) : setIsPlaying(true);
return (
<div className="App">
<div className="Bpm">
<label className="Bpm_label" htmlFor="Bpm_input">
{bpm} BPM
</label>
<input type="range" min="40" max="200" step="1" value={bpm} />
</div>
<button type="button" className="PlayButton" onClick={toggleButton}>
{!isPlaying ? 'play' : 'stop'}
</button>
</div>
);
}
export default App;