1

Newbie here, I'm trying to make a Javascript metronome, but I'm stuck on making the sound play according to the current bpm of the project

Here's where I think the problem lies, when I press play, it sounds, but in a fixed bpm, that is not the one being inputted in the bpm variable:

//VARIABLES
let bpm = 150;
let soundInterval = (60/bpm)*1000;


//FUNCTIONS

//START METRONOME
let startStopMetronome = startButton.addEventListener('click', () => {
    startMetronome(soundInterval);
})


function startMetronome(si) {
    setTimeout(() => {
        primaryBeat.play();
        startMetronome(si); 
    },si);
}

UPDATE: I´ve tried making the soundInterval update with a function, but it still does the same, and plays the same fixed interval no matter the bpm changes

//VARIABLES
let bpm = 150;
let soundInterval;




//FUNCTIONS

//START METRONOME
let startStopMetronome = startButton.addEventListener('click', () => {
    soundInterval = calculateSoundInterval();
    startMetronome(soundInterval);
})


function startMetronome(si) {
    setTimeout(() => {
        primaryBeat.play();
        startMetronome(si); 
    },si);
}

let calculateSoundInterval = () => {
    return (60/bpm)*1000;
}

let updateBpmInDisplay = display.addEventListener('change', ()=> {
    soundInterval = calculateSoundInterval();
})
Usitha Indeewara
  • 870
  • 3
  • 10
  • 21
Zeke Cachi
  • 49
  • 4
  • `setTimeout` only runs once after the passed interval. Did you mean to use `setInterval`? And what is `primaryBeat`? Is it a single blip, or a repeating series of tones itself? – pilchard Mar 03 '23 at 22:36
  • 3
    Javascript is not the right language for this. Due to the [event loop](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) you can't rely on your timeout being called in exactly x milliseconds. This is going to cause your BPM to drift over time and drive the entire band crazy. As soon as any heavy lifting or thread blocking code is executed you will delay your next 'tick'. – Adam H Mar 03 '23 at 22:52
  • @AdamH I wonder if wasm timer would be more accurate :) might be a good use case if looking for accuracy. However, for a pet project, 50-100ms delay might not be that big of a deal for this metronome. – async await Mar 04 '23 at 00:20
  • @AdamH 150 bpm is pretty slow, I doubt a few ms of drift over time would be noticed (but I'm no musician). That said, it's [fairly easy to prevent the drifting](https://stackoverflow.com/q/29971898/1048572). – Bergi Mar 04 '23 at 00:45
  • Possible dupe: [HTML5/jQuery metronome - performance problems](https://stackoverflow.com/questions/10211567/html5-jquery-metronome-performance-problems/68249713#68249713). You can use JS, but don't use `setTimeout` directly if you care at all about accuracy (and the point of a metronome is accuracy, unless you're doing a toy project). – ggorlen Mar 06 '23 at 04:30
  • See also [Accurately Timing Sounds In-Browser For A Metronome](https://stackoverflow.com/questions/62512755/accurately-timing-sounds-in-browser-for-a-metronome) – ggorlen Mar 06 '23 at 04:38

2 Answers2

1

I thought it might be fun to look over how I would accomplish this. I used an AudioContext and oscillator to make the sound, and instead of using setTimeout directly I used a helper function to model async practice. If I wanted to improve upon this, I might hold onto my timeout in a higher scope, so when stop is pressed I could clear the timeout and prevent any bugs when pressing stop and start quickly to have two loops running at the same time. I hope my take on things has some value was fun to look at.

const get = str => document.querySelector(str);
const wait = seconds => new Promise(r => setTimeout(r, seconds * 1e3));

const context = new AudioContext();   
let cntr = 0;
const makeSound = () => {
  const sound = context.createOscillator();
  const fourthBeat = cntr++ % 4 === 0;
  sound.frequency.value = fourthBeat ? 400 : 440;
  sound.connect(context.destination);
  sound.start(context.currentTime);
  sound.stop(context.currentTime + .1);
};

let bpm = 60;
get("input").addEventListener("input", e => {
  bpm = e.target.value;
  get(".display").innerText = bpm;
});

let running = true;
get(".start").addEventListener("click", async () => {
  running = true;
  get(".start").disabled = true;
  while (running) {
    makeSound();    
    await wait(60 / bpm);
  }
});

get(".stop").addEventListener("click", () => {
  running = false
  get(".start").disabled = false;
  cntr = 0;
});
<div class="display">60</div>
<input min="30" max="200" value="60" type="range" />
<button class="start">start</button>
<button class="stop">stop</button>
async await
  • 1,967
  • 1
  • 8
  • 19
0

Thanks everyone for the help. At the end, the issue was that I had not configured the inputs to reset the sound on click or input, so the sound just played until it stopped naturally. I added currentTime = 0, and problem solved, now it works as intended:

function startMetronome(si) {
    timerId = setInterval(() => {
        primaryBeat.play();
        primaryBeat.currentTime = 0;
    },si);
}
Zeke Cachi
  • 49
  • 4