As mentioned in the title I'm trying to create a jQuery/JavaScript based metronome along with the HTML <audio />
tag to play the sound.
It works "okay", but it seems to me the setInterval
method is not working accurately enough. I searched some threads here, but for I am new to both jQuery and JavaScript and I haven't found a working solution. Same for the "open new tab and setInterval stops or lags" - problem. I tried to prevent that with stop(true,true)
but it didn't work as I expected.
I want the metronome to run "in background" without changing tempo when opening a new tab and doing something there. Also I want an exact metronome for sure ;)
Here's my testing environment located: http://nie-wieder.net/metronom/test.html
At the moment, JS-Code and HTML-markup are all in the test.html source, so you can look there.
Also, here's the concerned (as I think) js-code I use:
$(document).ready(function() {
//vars
var intervalReference = 0;
var currentCount = 1;
var countIncrement = .5;
var smin = 10;
var smax =240;
var svalue = 120;
//soundchkbox
$(".sndchck").attr("disabled", true);
//preload sound
$.ajax({
url: "snd/tick.ogg",
success: function() {
$(".sndchck").removeAttr("disabled");
}
});
// tick event
var met = $("#bpm").slider({
value: 120,
min: smin,
max: smax,
step: 1,
change: function( event, ui ) {
var delay = (1000*60/ui.value)/2
clearInterval(intervalReference);
//seems to be the Problem for me
intervalReference = setInterval(function(){
var $cur_sd = $('#sub_div_'+currentCount);
$cur_sd
.stop(true,true)
.animate({opacity: 1},15,
function() {
//Play HTML5 Sound
if($('#sound_check:checked').val()){
$('#tick')
.stop(true,true)
.trigger("play");
}
$(this).
stop(true,true).
animate({opacity:0});
}
);
currentCount += countIncrement;
if(currentCount > 4.5) currentCount = 1
}, delay);
createMusicTag(ui);
}
});
});
Any help would be great, I'm out of ideas for now.
setInterval is not accurate. what you can try doing is something like:
var timestamp = (new Date()).getTime();
function run() {
var now = (new Date()).getTime();
if( now - timestamp >= 1000 ) {
console.log( 'tick' );
timestamp = now;
}
setTimeout(run, 10);
}
run();
This will (every hundredth of a second) compare the 'timestamp' with the current time to see if the diff is a second or more (deviation is 0.01 seconds) and if it is logs 'tick' and resets the current timestamp.
http://jsfiddle.net/rlemon/UqbwT/
This is the best approach to something that needs to be time accurate (imo).
Update: if you change the setTimeout time setting... you get less deviation. http://jsfiddle.net/rlemon/UqbwT/1/
Second update: After reviewing this post I thought there must be a more accurate way to use timers in javascript.. so with a bit of research I came acrossed this article. I do suggest you read it.
After trying to coax requestAnimationFrame
and setTimeout
into accurate timing for a drum machine app and failing (a 3 year old could keep better tempo than my code), I gave up and switched to the Web Audio API and it instantly provided accurate audio timing for my purposes.
Here's a minimal example:
const scheduleBeep = time => {
const osc = audioContext.createOscillator();
osc.connect(audioContext.destination);
osc.frequency.value = 300;
osc.start(time);
osc.stop(time + 0.1);
};
window.AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
let timeBetweenSteps = 60 / 120;
let nextStepTime;
let interval;
const lookahead = 0.1;
const timeoutDelay = 30;
const schedule = () => {
while (nextStepTime < audioContext.currentTime + lookahead) {
nextStepTime += timeBetweenSteps;
scheduleBeep(nextStepTime);
}
};
document.querySelector("button")
.addEventListener("click", evt => {
clearInterval(interval);
if (evt.target.textContent === "Run") {
evt.target.textContent = "Stop";
nextStepTime = audioContext.currentTime;
interval = setInterval(schedule, timeoutDelay);
}
else {
evt.target.textContent = "Run";
}
})
;
<button>Run</button>
And a slightly more involved example that adds a sound file and a BPM slider:
<body>
<form class="metronome">
<button class="run">Run</button>
<input class="bpm" type="range" min="60" max="500" value="120">
<span class="bpm-readout">120</span>
</form>
<script>
const url = "https://upload.wikimedia.org/wikipedia/commons/e/e5/Abadie.jo-Marteau-1.ogg";
const $ = document.querySelector.bind(document);
const metroEls = {
run: $(".metronome .run"),
bpm: $(".metronome .bpm"),
bpmReadout: $(".metronome .bpm-readout"),
};
window.AudioContext =
window.AudioContext || window.webkitAudioContext
;
const audioContext = new AudioContext();
(async () => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext
.decodeAudioData(arrayBuffer)
;
const scheduleSample = time => {
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start(time);
};
let timeBetweenSteps = 60 / 120;
let nextStepTime;
let interval;
const lookahead = 0.1;
const timeoutDelay = 30;
const schedule = () => {
while (nextStepTime <
audioContext.currentTime + lookahead) {
nextStepTime += timeBetweenSteps;
scheduleSample(nextStepTime);
}
};
metroEls.run.addEventListener("click", () => {
if (metroEls.run.textContent === "Run") {
metroEls.run.textContent = "Stop";
nextStepTime = audioContext.currentTime;
clearInterval(interval);
interval = setInterval(schedule, timeoutDelay);
}
else {
metroEls.run.textContent = "Run";
clearInterval(interval);
}
});
metroEls.bpm.addEventListener("change", e => {
timeBetweenSteps = 60 / e.target.value;
metroEls.bpmReadout.innerText = e.target.value;
});
})();
</script>
</body>
Useful resources:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With