setInterval(), setTimeout() — не точны. Для музыки/ритма/игры лучше полагаться на абсолютные метки времени и часы компьютера.
// в относительных единицах от старта песни
const hits = [ { time: 0 }, { time: 24 }, { time: 36 }, ];
// on play start
const timeK = 1200; // в зависимости от темпа
const playStartTime = Date.now();
const events = hits.map(hit => {
hit.ts = playStartTime + timeK * hit.time;
return hit;
});
// теперь у каждой ноты есть время ts, когда она должна сыграть
// внутри цикла requestAnimationFrame, который часто-часто,
// сраниваем текущее время с нотами,
// если разница менее 100 мс, ноту можно "играть"
const now = Date.now();
events.filter(note => Math.abs(note.ts - now) < 100).forEach(note =>play(note));
Наверное, надо смотреть и «в прошлом» пропущенные ноты – мало ли, комп притормозил.. И убирать сыгранные ноты из массива, чтобы не возвращаться к ним.