Por necesidad de un cliente necesité crear un rápido y sencillo metrónomo en Javascript. Con la ayuda de VueJS para gestionar los eventos y el renderizado, la etiqueta <audio>
para reproducir el sonido y un poco de Javascript lo conseguí casi sin darme cuenta. Comparto el código para todo aquel que lo necesite rápidamente sin calentarse mucho la cabeza.
DEMO
A continuación puedes jugar un poco con el metrónomo.
HTML
En el ejemplo he utilizado SVGs para los iconos de play
y pause
, aunque podría ser cualquier otra cosa como una imagen.
Los audios los he nombrado tick.ogg
y tick.mp3
, necesitarás que ambos reproduzcan el mismo sonido con su formato correspondiente para ampliar la compatibilidad.
<section class="metronome">
<!--Boton para reproducir o parar -->
<button @click.prevent="play = !play">
<!--Icono de parado -->
<svg v-if="play" x="0px" y="0px" viewBox="0 0 42 42" style="enable-background:new 0 0 42 42;" xml:space="preserve"> <g> <path d="M14.5,0c-0.552,0-1,0.447-1,1v40c0,0.553,0.448,1,1,1s1-0.447,1-1V1C15.5,0.447,15.052,0,14.5,0z"/> <path d="M27.5,0c-0.552,0-1,0.447-1,1v40c0,0.553,0.448,1,1,1s1-0.447,1-1V1C28.5,0.447,28.052,0,27.5,0z"/> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> </svg>
<!--Icono de reproducir -->
<svg v-else x="0px" y="0px" viewBox="0 0 41.999 41.999" style="enable-background:new 0 0 41.999 41.999;" xml:space="preserve"> <path d="M36.068,20.176l-29-20C6.761-0.035,6.363-0.057,6.035,0.114C5.706,0.287,5.5,0.627,5.5,0.999v40 c0,0.372,0.206,0.713,0.535,0.886c0.146,0.076,0.306,0.114,0.465,0.114c0.199,0,0.397-0.06,0.568-0.177l29-20 c0.271-0.187,0.432-0.494,0.432-0.823S36.338,20.363,36.068,20.176z M7.5,39.095V2.904l26.239,18.096L7.5,39.095z"/> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> </svg>
</button>
<!-- Reproductor (será invisible) -->
<audio class="player">
<!-- Para conseguir la mayor compatibilidad, se usa tanto OGG como MP3 -->
<source src="tick.ogg" type="audio/ogg">
<source src="tick.mp3" type="audio/mpeg">
</audio>
<!-- Muestra las pulsaciones por minuto -->
<p>
{{ ppm }}
</p>
<!-- Rango para cambiar las PPM (pulsaciones por minuto) -->
<input @change="togglePlayer(play)" v-model="ppm" type="range" min="40" max="218" step="1">
</section>
<!-- VueJS -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- Código Javascript -->
<script>
...
</script>
Javascript
Descubrirás que he comentado cada apartado. No contiene ninguna dificultad destacable.
document.addEventListener("DOMContentLoaded", () => {
//======================================================================
// METRÓNOMO
//======================================================================
//-----------------------------------------------------
// Variables
//-----------------------------------------------------
// DOM reproductor
let player = document.querySelector(".metronome .player");
// Lugar donde se almacenará la ID del timeout para poder destruirlo en el momento indicado
let timeout = undefined;
///-----------------------------------------------------
// VueJS
//-----------------------------------------------------
let appMetronome = new Vue({
el: ".metronome",
data: {
ppm: 60,
play: false
},
watch: {
play: function(state) {
// Lanza toggler si cambia la variable de play, utilizado para centralizar la lógica
this.togglePlayer(state);
}
},
methods: {
togglePlayer: function(state) {
/**
* Método que activa o desactiva el metrónomo
* @param {bool} state - Activar o desactivar
*/
// Detiene intervalo
clearInterval(timeout);
// Empieza interval si el estado es cierto
if (state) {
timeout = setInterval(function() {
// Reproduce audio
player.play();
}, appMetronome.ppmToMiliseconds(appMetronome.ppm));
}
},
ppmToMiliseconds: function(ppm) {
/**
* Método que transforma los PPM (pulsaciones por minuto) a Milisegundos
* @param {int} ppm - pulsamociones por minuto
* @return {int} milisegundos
*/
return (60 / ppm) * 1000;
}
}
});
});
Completo
Todo unido quedaría de la siguiente forma.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<style>
.metronome {
display: flex;
flex-direction: column;
padding: 2rem;
text-align: center;
border: 1px solid orange;
align-items: center;
}
.metronome p {
font-size: 1.3rem;
font-weight: bold;
font-family: arial;
}
.metronome button {
border: 2px solid orange;
background: #ffce73;
width: 3rem;
height: 3rem;
}
.metronome input[type=range] {
-webkit-appearance: none;
width: 100%;
margin: 13.2px 0;
}
.metronome input[type=range]:focus {
outline: none;
}
.metronome input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 12.6px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #ffa500;
border-radius: 6.2px;
border: 0.7px solid #ffa500;
}
.metronome input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 1px #ffa500, 0px 0px 1px #ffae1a;
border: 2.2px solid #000000;
height: 39px;
width: 19px;
border-radius: 2px;
background: #ffa500;
cursor: pointer;
-webkit-appearance: none;
margin-top: -13.9px;
}
.metronome input[type=range]:focus::-webkit-slider-runnable-track {
background: #ffae1a;
}
.metronome input[type=range]::-moz-range-track {
width: 100%;
height: 12.6px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #ffa500;
border-radius: 6.2px;
border: 0.7px solid #ffa500;
}
.metronome input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 1px #ffa500, 0px 0px 1px #ffae1a;
border: 2.2px solid #000000;
height: 39px;
width: 19px;
border-radius: 2px;
background: #ffa500;
cursor: pointer;
}
.metronome input[type=range]::-ms-track {
width: 100%;
height: 12.6px;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
.metronome input[type=range]::-ms-fill-lower {
background: #e69500;
border: 0.7px solid #ffa500;
border-radius: 12.4px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
.metronome input[type=range]::-ms-fill-upper {
background: #ffa500;
border: 0.7px solid #ffa500;
border-radius: 12.4px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
.metronome input[type=range]::-ms-thumb {
box-shadow: 1px 1px 1px #ffa500, 0px 0px 1px #ffae1a;
border: 2.2px solid #000000;
height: 39px;
width: 19px;
border-radius: 2px;
background: #ffa500;
cursor: pointer;
height: 12.6px;
}
.metronome input[type=range]:focus::-ms-fill-lower {
background: #ffa500;
}
.metronome input[type=range]:focus::-ms-fill-upper {
background: #ffae1a;
}
</style>
</head>
<body>
<section class="metronome">
<!--Boton para reproducir o parar -->
<button @click.prevent="play = !play">
<!--Icono de parado -->
<svg v-if="play" x="0px" y="0px" viewBox="0 0 42 42" style="enable-background:new 0 0 42 42;" xml:space="preserve"> <g> <path d="M14.5,0c-0.552,0-1,0.447-1,1v40c0,0.553,0.448,1,1,1s1-0.447,1-1V1C15.5,0.447,15.052,0,14.5,0z"/> <path d="M27.5,0c-0.552,0-1,0.447-1,1v40c0,0.553,0.448,1,1,1s1-0.447,1-1V1C28.5,0.447,28.052,0,27.5,0z"/> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> </svg>
<!--Icono de reproducir -->
<svg v-else x="0px" y="0px" viewBox="0 0 41.999 41.999" style="enable-background:new 0 0 41.999 41.999;" xml:space="preserve"> <path d="M36.068,20.176l-29-20C6.761-0.035,6.363-0.057,6.035,0.114C5.706,0.287,5.5,0.627,5.5,0.999v40 c0,0.372,0.206,0.713,0.535,0.886c0.146,0.076,0.306,0.114,0.465,0.114c0.199,0,0.397-0.06,0.568-0.177l29-20 c0.271-0.187,0.432-0.494,0.432-0.823S36.338,20.363,36.068,20.176z M7.5,39.095V2.904l26.239,18.096L7.5,39.095z"/> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> </svg>
</button>
<!-- Reproductor (será invisible) -->
<audio class="player">
<!-- Para conseguir la mayor compatibilidad, se usa tanto OGG como MP3 -->
<source src="tick.ogg" type="audio/ogg">
<source src="tick.mp3" type="audio/mpeg">
</audio>
<!-- Muestra las pulsaciones por minuto -->
<p>
{{ ppm }}
</p>
<!-- Rango para cambiar las PPM (pulsaciones por minuto) -->
<input @change="togglePlayer(play)" v-model="ppm" type="range" min="40" max="218" step="1">
</section>
<!-- VueJS -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- Código Javascript -->
<script>
document.addEventListener("DOMContentLoaded", () => {
//======================================================================
// METRÓNOMO
//======================================================================
//-----------------------------------------------------
// Variables
//-----------------------------------------------------
// DOM reproductor
let player = document.querySelector(".metronome .player");
// Lugar donde se almacenará la ID del timeout para poder destruirlo en el momento indicado
let timeout = undefined;
///-----------------------------------------------------
// VueJS
//-----------------------------------------------------
let appMetronome = new Vue({
el: ".metronome",
data: {
ppm: 60,
play: false
},
watch: {
play: function(state) {
// Lanza toggler si cambia la variable de play, utilizado para centralizar la lógica
this.togglePlayer(state);
}
},
methods: {
togglePlayer: function(state) {
/**
* Método que activa o desactiva el metrónomo
* @param {bool} state - Activar o desactivar
*/
// Detiene intervalo
clearInterval(timeout);
// Empieza interval si el estado es cierto
if (state) {
timeout = setInterval(function() {
// Reproduce audio
player.play();
}, appMetronome.ppmToMiliseconds(appMetronome.ppm));
}
},
ppmToMiliseconds: function(ppm) {
/**
* Método que transforma los PPM (pulsaciones por minuto) a Milisegundos
* @param {int} ppm - pulsamociones por minuto
* @return {int} milisegundos
*/
return (60 / ppm) * 1000;
}
}
});
});
</script>
</body>
</html>
Si crees que puede mejorarse, por favor deja un comentario.
{{ comments.length }} comentarios