Analizador de sonido (3)

facebook-svg gplus-svg twitter-svg

Podemos crear un analizador de sonido que utiliza la señal audio de un micrófono. La mala noticia es que por ahora esto no funciona en Opera, Safari e IE. En Edge, Firefox y Chrome se le pide permiso para utilizar el micrófono.

Veamos un ejemplo.

En el HTML tenemos un solo elemento: el <canvas>.

En el JavaScript empezamos declarando algunas variables útiles e inicializamos el <canvas>.


  // variables para el audio
  // crea un nuevo contexto audio
  var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  var analizador = audioCtx.createAnalyser();// crea un nodo analizador
  analizador.fftSize = 128; // [32, 64, 128, 256, 512, 1024, 2048]
  var dataArray = new Uint8Array(analizador.frequencyBinCount);
  //  Inicializa el canvas
  var canvas = document.querySelector("canvas");
  ctx = canvas.getContext("2d");
  var cw = canvas.width = 700; // la anchura del canvas
  var ch = canvas.height = 350;// la altura del canvas
  ctx.fillStyle = "hsla(210,95%,45%,.75)";// el color de relleno

Lea más acerca del nodo analizador en este articulo: Un analizador de sonido

También creamos un array de puntos. Vamos a utilizar estos puntos para visualizar la frecuencia de la señal de sonido.


  // crea el array de los puntos
  var puntos = [];
  var pNum = 25;// número de puntos
  var puntoW = cw / pNum;// calcula la distribución de los puntos
    for (var i = 0; i < pNum + 1; i++) {
      var punto = {};
      punto.x = i * puntoW;
      punto.y = ch;
      puntos.push(punto);
    }

El método MediaDevices.getUserMedia

A continuación viene lo importante:
El método MediaDevices.getUserMedia() solicita permiso para utilizar un dispositivo de entrada, en este caso el micrófono.

navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then . . .
 

Si el usuario autoriza el uso del micrófono, entonces ( then ) una función anónima puede utilizar la señal audio ( stream ) que viene del micrófono.
De lo contrario, si el usuario prohíbe el uso del micrófono, la señal audio no está disponible, y un bloque catch captura el error y puede hacer algo con el, como por ejemplo, sacar un mensaje en pantalla.


navigator.mediaDevices
    .getUserMedia({ audio: true, video: false })
    .then(function(stream) {
      /* utilice la señal audio (stream) */
      var fuenteDeSonido = audioCtx.createMediaStreamSource(stream);
      fuenteDeSonido.connect(analizador);
      Animacion();
  
    }).catch(function(err) {
    /* gestionar errores */
    txt("E R R O R")
  });

Lea más acerca del método AudioContext.createMediaStreamSource()
Lea más acerca del método MediaDevices.getUserMedia()

Analicemos un poco que pasa si el usuario autoriza el uso del micrófono: para capturar la fuente de sonido utilizamos el método createMediaStreamSource. Después conectamos la fuenteDeSonido con el analizador y llamamos la función que genera la animación.

var fuenteDeSonido = audioCtx.createMediaStreamSource(stream);
fuenteDeSonido.connect(analizador);
Animacion();

La animación

La animación es muy parecida a la de un ejemplo anterior: Un analizador de sonido solo que esta vez, en lugar de dibujar rectángulos, conectamos los puntos con curvas cuadráticas de Bézier.


  function Animacion() {
    requestId = window.requestAnimationFrame(Animacion);
    //el método getByteFrequencyData() toma como argumento un array de tipo Uint8Array
    analizador.getByteFrequencyData(dataArray);
    ctx.clearRect(0, 0, cw, ch);
    // la doble tilde (~~) es un operador equivalente a Math.floor() o casi
    var n = ~~(analizador.frequencyBinCount / pNum);
    // un bucle for calcula la coordenada en y de cada punto del array de puntos.
    for (var i = 0; i < puntos.length; i++) {
      puntos[i].y = ch - dataArray[i * n] - 50;
    }
    trazarCurvas(puntos);
    txt("Haz algo de ruido");
  }

Lea más acerca del método requestAnimationFrame, un método que genera animaciones fluidas, que paran en pestañas ( tabs ) inactivas, y es mucho más eficiente en los navegadores.

La función que se encarga de trazar las curvas toma como atributo el array de los puntos:


function trazarCurvas(puntos) {
    ctx.beginPath();
    // mueve el puntero al primer punto del array
    ctx.moveTo(puntos[0].x, puntos[0].y);
    // une los puntos con curvas de Bézier
    for (var i = 1; i < puntos.length - 2; i++) {
      var x = (puntos[i].x + puntos[i + 1].x) / 2;
      var y = (puntos[i].y + puntos[i + 1].y) / 2;
      ctx.quadraticCurveTo(puntos[i].x, puntos[i].y, x, y);
    }
    // dibuja la última curva de Bézier
    ctx.quadraticCurveTo(
      puntos[i].x,
      puntos[i].y,
      puntos[i + 1].x,
      puntos[i + 1].y
    ); 
    // cierra el trazado 
    ctx.lineTo(cw, ch);
    ctx.lineTo(0, ch);
    ctx.closePath();
    // y lo rellena de color
    ctx.fill();
  }

Lea más acerca de cómo dibujar Curvas cuadráticas de Bézier en canvas.

También hay una función que se encarga de dibujar el mensaje de texto.


function txt(mensaje) {
    var t = mensaje.split("").join(" ");
    ctx.font = "1.5em Lucida Console";
    ctx.textAlign="center";  
    ctx.fillText(t, cw*.5, ch*.5);
  }

Lea más acerca de cómo dibujar texto en canvas

Puede ver este ejemplo en codepen: Haz algo de ruido (Web audio api)*

También puede consultar aquí el código completo:


// variables para el audio
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var analizador = audioCtx.createAnalyser();
analizador.fftSize = 128; // [32, 64, 128, 256, 512, 1024, 2048]
var dataArray = new Uint8Array(analizador.frequencyBinCount);
//  Inicializa el canvas
var canvas = document.querySelector("canvas");
ctx = canvas.getContext("2d");
var cw = canvas.width = 700; // la anchura del canvas
var ch = canvas.height = 350;// la altura del canvas
ctx.fillStyle = "hsla(210,95%,45%,.75)";

// crea el array de los puntos
var puntos = [];
var pNum = 25;// número de puntos
var puntoW = cw / pNum;// calcula la distribución de los puntos 

  for (var i = 0; i < pNum + 1; i++) {
    var punto = {};
    punto.x = i * puntoW;
    punto.y = ch;
    puntos.push(punto);
  }


navigator.mediaDevices
  .getUserMedia({ audio: true, video: false })
  .then(function(stream) {
    var fuenteDeSonido = audioCtx.createMediaStreamSource(stream);
    fuenteDeSonido.connect(analizador);
    Animacion();
  }).catch(function(err) {
  txt("E R R O R")
});

function Animacion() {
  requestId = window.requestAnimationFrame(Animacion);
  /*el método getByteFrequencyData() toma como argumento un array de tipo Uint8Array*/
  analizador.getByteFrequencyData(dataArray);
  ctx.clearRect(0, 0, cw, ch);
  // la doble tilde (~~) es un operador equivalente a Math.floor() o casi
  var n = ~~(analizador.frequencyBinCount / pNum);
  // un bucle for calcula la coordenada en y de cada punto del array de puntos.
  for (var i = 0; i < puntos.length; i++) {
    puntos[i].y = ch - dataArray[i * n] - 50;
  }
  trazarCurvas(puntos);
  txt("Haz algo de ruido");
}


function trazarCurvas(puntos) {
  ctx.beginPath();
  ctx.moveTo(puntos[0].x, puntos[0].y);
  
  for (var i = 1; i < puntos.length - 2; i++) {
     var x = (puntos[i].x + puntos[i + 1].x) / 2;
     var y = (puntos[i].y + puntos[i + 1].y) / 2;
    ctx.quadraticCurveTo(puntos[i].x, puntos[i].y, x, y);
  }
  ctx.quadraticCurveTo(
    puntos[i].x,
    puntos[i].y,
    puntos[i + 1].x,
    puntos[i + 1].y
  );  
  ctx.lineTo(cw, ch);
  ctx.lineTo(0, ch);
  ctx.closePath();
  ctx.fill();
}


function txt(mensaje) {
  var t = mensaje.split("").join(" ");
  ctx.font = "1.5em Lucida Console";
  ctx.textAlign="center";  
  ctx.fillText(t, cw*.5, ch*.5);
}