domingo, 18 de octubre de 2015

¿Notas algo?

Qué encontrarás en esta entrada?
  • Script en MATLAB/OCTAVE para sacar las notas de una canción.


Los músicos suelen identificar sin dificultad las notas que suenan en una canción, pero ¿y si no se tuviera tan desarrollada esa capacidad? ¿Y si hubiera situaciones en las que, ya sea por el timbre del instrumento o por cualquier otro motivo, nos costase más hacerlo o nos resultara imposible? En esos casos, amigos míos, aún nos quedarían las máquinas para ayudarnos.

Hace poco me enfrenté con este problema y pensé: ¡no tiene que ser tan difícil hacerlo! Lo primero que se me ocurrió fue un análisis de frecuencias vía una transformada rápida de Fourier (FFT), que supongo que es el método que utiliza Audacity.

Análisis de frecuencias del programa Adacity

Sin embargo, yo no estaba interesado en sacar todo un espectro de frecuencias, sino en sacar la dominante por intervalos de tiempo, lo que viene a ser sacar la partitura de una canción.


Para ello, de manera más o menos gráfica, he escrito un script, que detallaré más abajo, el cual subdivide una canción en fragmentos (de duración configurable) y nos dice qué nota suena en cada uno de ellos.

El funcionamiento del script no es complicado: cuenta los ceros (nodos) de la onda por intervalo (como un cambio en el signo de la función de la gráfica) y divide entre el tiempo del intervalo, obteniendo así la frecuencia. De manera aproximada:

f ~ (n+1)/(2*t)

f: frecuencia.
n: número de nodos.
t: tiempo del intervalo.

Hay que tener en cuenta que en nuestro caso, por lo general, "n" va a ser un número lo suficientemente grande como para que el uno que le acompaña no sea demasiado importante. Por otra parte, los múltiplos de una frecuencia son la misma nota en distintas escalas, por lo que el dos que divide tampoco es demasiado relevante.

Con este sencillo método, contando ceros sacamos frecuencias. Para relacionarlas con las notas tenemos que tener en cuenta la siguiente expresión:

f(nota 1) = f(nota 2) * r^d

f: frecuencia.
r: constante con valor 2^(1/12).
d: separación en semitonos entre "nota 1" y "nota 2".

Es decir:

f(La) = 440 Hz.
f(La#) = f(La) * r = 440 * 2^(1/12) ~ 466,16 Hz.
f(Si) = f(La) * r^2 = 440 * 2^(2/12) ~ 493,88 Hz.
f(Do) = f(La) * r^3 = 440 * 2^(3/12) ~ 523,25 Hz.
f(Do#) = f(La) * r^4 = 440 * 2^(4/12) ~ 554,37 Hz
.
...

Lo que podemos hacer, sabiendo la frecuencia, es despejar:

f(nota incógnita) = f(nota conocida) * r^d 
ln [ f(nota incógnita) ] = ln [ f(nota conocida) * r^d ]
ln [ f(nota incógnita) ] = ln [ f(nota conocida) ] +d ln [ r ]
d = { ln [ f(nota incógnita) ] - ln [ f(nota conocida) ] } / ln [ r ]
d = { ln [ f(nota incógnita) / f(nota conocida) ] } / ln [ r ] 

Con esto, sacamos la distancia en semitonos entre ambas notas. Como origen suele ser bastante estándar utilizar el "La" (440 Hz).

Una vez en este punto llega el momento de probarlo. Empecemos por el caso más sencillo: una nota "La" pura, generada por ordenador, con un espectro bastante limpio.



Como vemos, aunque la frecuencia sufre pequeñas variaciones, oscila siempre cerca de la nota "La". Parece que de momento no va mal.

A continuación, le pido a mi hermano pequeño que toque una nota "Sol" en una flauta dulce, con el siguiente resultado.



En este caso las fluctuaciones son más fuertes, pero sí creo que se puede decir que se ve una predominante en torno a "Sol", que baja (o sube) un semitono hasta "Fa#" (o hasta "Sol#"), y en un momento dado se dispara a "Si" (que es una tercera mayor desde "Sol"). Me parece interesante destacar que realmente estemos viendo dos notas "Sol": la que debería estar en torno a los 783,99 Hz y la de la octava superior, a 1567,98 Hz. Tengamos en cuenta que la flauta dulce es un instrumento que puede pasar a una octava superior si se sopla con mayor intensidad.

Seguimos aumentando en dificultad, y le pasamos un arpegio en "Do mayor" con un teclado grabado al aire. Esto es: primero la nota "Do", luego la nota "Mi", y por último la nota "Sol". Haciendo una primera pasada el resultado es un desastre.


Hay que irse a una partición más fina para poder ver las notas del acorde, y no son predominantes entre las notas que extrae el análisis.


En los tres casos se obtiene la estabilidad en la nota después del golpe inicial, cuando están resonando, de la mitad hacia el final del golpe.

La última prueba es lanzarlo directamente contra nuestra canción incógnita, la que queríamos sacar desde un principio. He aquí el resultado:


¿Será fiable? Mientras tanto, os dejo el script por si os veis con ganas de jugar con él.

notas.m

function [freq]=notas(p,part)
%
% Este script pretende dar una estimación de la nota que suena en un
%fragmento de audio WAVE. La sintaxtis sería:
%
% >>notas('archivo.wav')
% >>notas('archivo.wav',1000)
%
% Donde 'archivo.wav' es el archivo a analizar y 1000 es el ancho de
%la partición. Una partición con 1000 registros significa que de los
%miles de registros que se han muestreado a una tasa de muestreo dada
%se cogen bloques de 1000 para ser analizados. La relación entre el
%número de registros y la duración del intervalo viene dada por la
%tasa de muestreo, la cual se lee directamente del archivo WAV.
%
% Creado por Astaroth (O.R.G.) el 17/10/2015.
%astarothsworld.blogspot.com

clc

% Control de argumentos de entrada:
if nargin <2
    part=50000;    % Ancho del intervalo.
end

% Tabla de relaciones "Notas vs Frecuencias".
r=2^(1/12);

LA=440;
%LAs=LA*r;
%SI=LA*r^2;
%DO=LA*r^(3);
%DOs=LA*r^(4);
%RE=LA*r^(5);
%REs=LA*r^(6);
%MI=LA*r^(7);
%FA=LA*r^(8);
%FAs=LA*r^(9);
%SOL=LA*r^(10);
%SOLs=LA*r^(11);

NOTAS=['La';'La#';'Si';'Do';'Do#';'Re';'Re#';'Mi';'Fa';'Fa#';'Sol';'Sol#';'La';'La#'];

close all

% Adquisición de datos:
[y,fm]=wavread(p);
y=y(:,1);
long=max(size(y));
Y=max(abs(y));

% Representación incial:
for i=1:long
    x(i)=i;
end
plot(x,y)
hold on
xlabel('Registros muestreados')
ylabel('Amplitud de la onda')
plot(x,zeros(size(y)),'g')

% Análisis de los datos:
tram=fix(long/part);
for j=1:(tram+1)
    ini=(j-1)*part+1;
    fin=min(j*part,long);
    yt(1:(fin-ini+1),j)=y(ini:fin);
    % Recuento de los nodos:
    n(j)=0;
    for i=1:(part-1)
        if (yt(i,j)*yt(i+1,j)) < 0
            n(j)=n(j)+1;
        end
    end
    % Cálculo de la frecuencia original:
    freq(j)=((n(j)+1)*fm)/(2*(fin-ini));
    % Cálculo de una frecuencia equivalente,
    %reducida a un intervalo concreto:
    freqred=freq;
    while ( freqred(j) < LA*r )
        freqred(j)=freqred(j)*2;
    end
    while ( freqred(j) > (LA*r^(13)) )
        freqred(j)=freqred(j)/2;
    end
    % Cálculo del exponente (distancia en semitonos desde LA):
    rcal(j)=log(freqred(j)/LA)/log(r);
    rcal(j)=round(rcal(j)+1);
    % Identificación de la nota:
    Nota(j,:)=NOTAS(rcal(j),:);
    % Representaciones gráficas de los resultados:
    plot(ini*ones(100),linspace(-Y,Y),'r-')
    text(ini+(fin-ini)/2,Y,[num2str(freq(j)),' Hz'],'HorizontalAlignment','center')
    text(ini+(fin-ini)/2,-Y,Nota(j,:),'HorizontalAlignment','center')
    title(['Análisis del archivo: "',p,'"'])
end

% Presentación de los resultados:
disp ('Este script intentará determinar las notas de un fragmento de audio.')
disp('')
disp('')
disp('Datos del audio original:')
disp('')
disp([' - Número de muestras: ',num2str(long),'.'])
disp([' - Frecuencia de muestreo: ',num2str(fm),' Hz.'])
disp([' - Duración del audio: ',num2str(long/fm),' s.'])
disp('')
disp('')
disp('Datos del análisis:')
disp('')
disp([' - Número de tramos: ',num2str(tram+1),'.'])
disp([' - Ancho del tramo: ',num2str(part),' muestras (',num2str(part/fm),' s).'])
disp(' - Análisis de los tramos:')
for j=1:(tram+1)
    disp(['    - Tramo ',num2str(j),': ',num2str(freq(j)),' Hz ( ',Nota(j,:),').'])
end
disp('')
disp('')

Referencias:

No hay comentarios:

Publicar un comentario

Querido astarothista!,

Si te ha gustado la entrada y quieres dejar constancia de ello, tienes alguna sugerencia para completarla o corregirla, quieres mostrar tu opinión respecto a algo de lo que se haya hablado en esta entrada (con respeto) o simplemente quieres dejarme un mensaje a mi o a la comunidad, no dudes en comentar ;)!

Recuerda que también estamos en Facebook y en Google+.