Metronome

by YH Park

HTML

<h1>Metronome</h1>

<h3>Tempo</h3>
<input type="range" min="10" max="200" step="1" style="width: 500px" oninput="fnObj.rangeChange()">
<br>
<input type="button" name="tempo-up" value="+" onclick="fnObj.addTempo(true)">
<input type="button" name="tempo-down" value="-" onclick="fnObj.addTempo(false)">
<input type="button" name="taptap" value="tap tempo" onclick="fnObj.tap()">
<br>
<form action="javascript: fnObj.tempoChange(Number($('#cur-tempo').val()))">
  <input type="text" id="cur-tempo" autocomplete="off">
  <input type="submit" value="change">
</form>

<h3>Beat</h3>
<select name="beat" onchange="fnObj.changeBeat()">
  <option>1</option>
  <option>2</option>
  <option>3</option>
  <option selected>4</option>
  <option>5</option>
  <option>6</option>
  <option>7</option>
  <option>8</option>
</select>
1<input type="radio" name="divide" value="1" onchange="fnObj.split = 1" checked>
2<input type="radio" name="divide" value="2" onchange="fnObj.split = 2">
3<input type="radio" name="divide" value="3" onchange="fnObj.split = 3">
4<input type="radio" name="divide" value="4" onchange="fnObj.split = 4">
6<input type="radio" name="divide" value="6" onchange="fnObj.split = 6">

<br><br>
<div id="visualization-area"></div>
<br><br>

<input type="button" name="begin" value="begin" onclick="fnObj.begin()">
<input type="button" name="stop" value="stop" onclick="fnObj.stop()">

CSS

#visualization-area {
  width: max-content;
}

.circle {
  width: 50px;
  height: 50px;
  display: inline-block;
  margin-right: 50px;
  border-radius: 50%;
}

.circle.dis {
  background: #555;
  opacity: 0.2;
  position: relative;
}

.circle.act {
  background: #0f0;
  position: absolute;
  transition-property: margin-left;
  transition-timing-function: ease-out;
  transition-delay: 0s;
}

JavaScript

var beep = [
	new Audio('https://www.soundjay.com/buttons/beep-29.wav'),
  new Audio('https://www.soundjay.com/buttons/beep-07a.wav'),
	new Audio('https://www.soundjay.com/buttons/beep-21.wav'),
	new Audio('https://www.soundjay.com/buttons/beep-21.wav'),
	new Audio('https://www.soundjay.com/buttons/beep-21.wav'),
	new Audio('https://www.soundjay.com/buttons/beep-21.wav'),
	new Audio('https://www.soundjay.com/buttons/beep-21.wav')
]
beep[1].volume = 0.5;

var fnObj = {
	tempo: 100, //metronome tempo
  cnt: 0, //beep counter
  intv: 4, //loud beep interval
  split: 1, //beat split number
  // init before begin
  init: function() {
  	//set tempo
  	this.tempoChange(this.tempo);
    
    //set visual area (circle)
  	$('#visualization-area').empty();
    $('#visualization-area').append('<div class="circle act"></div>');
  	for (var i = 0; i < this.intv; i++)
  		$('#visualization-area').append('<div class="circle dis"></div>');
  },
  //metronome begin
  begin: function() {
  	this.stop();
  	this.recur();
  },
  //function for each beep sound
  recur: function() {
  	//play standard beep sound
  	if ((this.cnt %= this.intv) == 0)
    	beep[1].play();
    else
    	beep[0].play();
    
    //move green circle to next gray circle
    $('.circle.act').css('transition-duration', Math.round(60000/this.tempo)+'ms').css('margin-left', (100*this.cnt)+'px');
    
    this.cnt++;
    
    //call next recursive function
    this.timeoutFunc = setTimeout(function() {
    	fnObj.recur()
    }, 60000 / this.tempo);
    
    //if there is a bit split, plays a small beep
    for (let i = 1; i < this.split; i++)
    	setTimeout(function() {
    		beep[i+1].play();
    	}, 60000 / this.tempo / this.split * i);
    
  },
  //metronome end
  stop: function() {
  	this.cnt = 0;
  	clearInterval(this.timeoutFunc);
  },
  //change tempo between 10 ~ 200
  tempoChange: function(val) {
  	if (val < 10)
      	val = 10;
      else if (val > 200)
      	val = 200;
    
    this.tempo = val;
...