Audio 101 — Sampling
En este post nos volveremos un poco más técnicos y veremos algo de matemáticas. Sé que a muchos no les gustan mucho las matemáticas, pero les prometo que es más fácil de lo que parece. Ahora entremos en materia, estás creando un grabador de audio y estás revisando el proceso para detener y guardar la grabación que sería un código similar al siguiente:
// ...
mediaRecorder.onstop = async() => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const ctx = new AudioContext();
const audioBuffer = await ctx.decodeAudioData(
await audioBlob.arrayBuffer()
);
const samples = audioBuffer.getChannelData(0); // Float32Array, -1.0 a 1.0
console.log(audioBuffer.sampleRate, audioBuffer.duration, samples);
// Detener el track del micrófono
stream.getTracks().forEach(track => track.stop());
};
// ...Y en console.log ves los siguientes números:
El primero sería el Sample Rate, el segundo la duración de ese audio y el último los samples de uno de los canales (izquierdo). ¿Qué representa el primer número? ¿De dónde viene? ¿Existen más?
Sampling
Sampling es el proceso en el que, a partir de una señal analógica continua, extraemos ciertos valores o instantáneas de la misma y estos valores son almacenados en un stream, arreglo o alguna colección para posteriormente procesarlos digitalmente.
En el post anterior vimos que “Sample” es la amplitud instantánea de la señal — Fundamental post, esa definición es importante y hacía referencia a cada valor de cada sample.
¿Por qué esto es importante? Recordemos que las computadoras contienen recursos limitados y no pueden almacenar una señal completa continua, ya que estas son infinitas. Aún tomando un segmento, podemos extraer infinitos valores de ella y esto dejaría sin recursos a una computadora. Por lo que con el proceso de sampling, podemos “aproximar” una señal usando los valores de los samples.
Sample Rate
Este es un valor que hemos visto en muchas cosas de nuestra vida cotidiana: lectores de disco, algunos CDs, reproductores de audio, Digital Audio Workstation (DAWs)… Este valor hace referencia a cuántos samples se extraen de una señal en una unidad de tiempo, generalmente un segundo.
Donde samples es cada una de las instantáneas de la señal, t es el tiempo, en segundos. Si tomamos el valor inicial de 44100, significaría que en 1 segundo se toman 44100 valores (¡ES UN MONTÓN DE DATA EN UN SEGUNDO!).
La unidad de hertz aquí no es lo mismo que la unidad de hertz de la señal. Ambas cuentan “eventos por segundo”, solo que una cuenta mediciones tomadas y la otra cuenta ciclos de cada onda. Misma unidad, distintas cosas contadas.
Nosotros, como programadores, dependiendo del lenguaje que estemos utilizando, podemos decirle o configurar el ADC(Analog-to-Digital Converter) cuántos samples queremos leer por segundo.
A continuación veremos gráficamente cómo se extraen los valores. Para ello, usaremos valores más manejables, ya que 44100 sería poco didáctico para nuestros propósitos. Para eso, usaremos una señal de 1 Hz (representada por sin(2πt)) sin desfase ni nada en especial y un sample rate de 8 Hz.
La señal analógica vendría a verse de la siguiente forma:
En donde el eje x es el tiempo en segundos y el eje y, la amplitud [-1, 1].
Vamos a sacar los samples entre 0 segs y 1 seg, recordando que estamos trabajando a un sample rate de 8 Hz, veríamos lo siguiente:
Si vemos los puntos, podemos ver que siguen la forma de la señal original, puesto que de allí es de donde se sacan los valores:
Ahora bien, podemos llegar a pensar que esos puntos se sacan al azar y que nosotros tenemos el control de ello, en parte sí, pero requeriría de técnicas mucho más avanzadas. Sin embargo, nosotros podemos calcular el tiempo que existe entre samples con la siguiente fórmula:
La cual Fs sería el valor de nuestro sample rate y Ts será el valor de tiempo entre samples en segundos:
Eso significa que el ADC toma un valor de la señal, sea cual sea, cada 0.125 s. Si nosotros usamos 44100 Hz, tendríamos que la distancia entre samples sería de 0.00002267 s.
Get Jesus Herrera’s stories in your inbox
Join Medium for free to get updates from this writer.
De qué nos podría servir conocer estas fórmulas, bueno, imaginemos el caso en el que necesitamos grabar una cantidad de tiempo precisa, por ejemplo, 0.75 segs, entonces nosotros leemos el stream [0.0f, 0.707, 1.0f, 0.707, 0.0f, -0.707, -1.0f, -0.707, 0.0f, …] y sabiendo que la distancia entre samples es de 0.125 segs, resultado de la formula de Ts, necesitaríamos contar samples en lugar de contar tiempo, necesitamos contar 6 samples para llegar a 0.75 segs.
Esa fórmula nos ayuda a identificar cuántos samples ocupamos para alcanzar cierta cantidad de tiempo.
Se estarán preguntando: ¿por qué no usar un SetTimeOut, SetInterval o alguna solución similar en su lenguaje de preferencia para grabar una cantidad de tiempo específica? Es una pregunta válida; sin embargo, en términos de precisión y performance no son la mejor solución. Implementar una solución de esa forma hace que sea muy difícil y poco mantenible. Si quieres grabar 1 segundo usando estos métodos, no va a ser preciso y va a tocar ajustar el tiempo del timeout a 1.1 segs o números similares, y si metes complejidad de procesamiento posteriormente, como filtros o condiciones, obtienes más tiempo de overhead, y toca ajustar el timeout a valores que llegarían a ser poco autodescriptivos. Y ni hablar de que cada computadora tiene tiempos de carga de recursos diferentes… En resumen, un dolor de cabeza a futuro.
También podemos extraer la duración de un audio en base a la cantidad de samples que tenemos y conociendo el sample rate, solo reajustando la fórmula a t = N / Fs, por ejemplo usaremos los valores proporcionados en el console.log de la primera imagen N=12224 y Fs = 44100
Con eso obtendríamos el valor de tiempo que nos arroja el código específicamente en el log de la imagen 1. (Niceeee! 🧙♂️)
¿Qué sucede en el proceso inverso?
Imaginemos que queremos reproducir de vuelta nuestra grabación. A nivel teórico, podemos decir que reconstruir de vuelta una señal a partir de nuestro sample daría como resultado nuestra señal original. En realidad, casi, pero no. Cuando nosotros enviamos nuestro audio a los parlantes, pasa por el DAC (Digital to Analog Converter) y este lo que hace es reconstruir, rellenando los espacios entre samples con valores, ¿cuáles?, lo hace generando pulsos que son pasados por medio de un “low-pass filter” y este filtro devuelve la curva o señal.
Unir los puntos con líneas rectas es una forma de dibujar una interpretación, no lo que hace el DAC. El DAC entrega escalones; luego el filtro suaviza el resultado. Y si la señal no tiene frecuencias por encima de la mitad del sample rate, ese filtro reconstruye la curva original, no una aproximación, la original. Por qué esto funciona, lo veremos en un próximo post acerca del Nyquist limit.
Ahora bien, ¿cuándo comenzamos a perder información? Esto lo comenzamos a perder cuando una señal excede la mitad de nuestro sample rate. Si nosotros tenemos un sample rate de 8 Hz, entonces nuestra señal solo puede tener 4 Hz como frecuencia máxima. Si nuestra señal fuera de 5 Hz, la reconstrucción de la misma haría que nuestra señal sonara “sucia” y cierta información sería procesada como parte de una señal de 4 Hz, lo cual no es correcto.
Para ello existen valores de sample rates medidos en kilohertz que de acuerdo con el caso de uso son mejores:
- 44.1 kHz: audio, cd, algunas grabaciones
- 48 kHz: películas, televisión y video broadcasting.
- 96 kHz y 192 kHz: Diseño de sonido, transformación de tonos o time stretching y procesamientos.
Estos son utilizados por la industria ya que resuelven problemas de precisión en la reconstrucción y dejan más arriba el “límite de Nyquist”, límite auditivo del ser humano.
Existe otro problema de precisión que no hemos discutido todavía y está en cada sample como tal. Solo podemos guardar una cantidad finita de valores distintos después del punto decimal, en caso de que se estén usando valores flotantes, así que cada sample se redondea al valor más cercano disponible. Eso significa que aún los valores que obtuvimos no son del todo exactos, y es un problema que resolveremos con el “Bit depth”.
Si hacemos super zoom a nuestra señal, vemos que el valor que usamos para este post, que es 0.707 (punto rojo), queda fuera de los valores exactos de la señal, mientras que si usamos más valores del punto decimal, nos aproximamos más a los valores de la señal, 0.70710678118655 (punto morado).
Veremos qué involucra esto en el próximo post de Bit Depth.










