Ejemplo de metrónomo en Javascript (VueJS)

5 minutos

Metrónomo

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.

Tal vez también te interese...