miércoles, 21 de octubre de 2015

Cosas que hacemos con cierta frecuencia

Qué encontrarás en esta entrada?
  • Análisis de Fourier desde Octave/Matlab para sacar las notas de una canción.
  • Comparación entre algoritmos para conseguir este objetivo.

En la última entrada hablamos de cómo sacar las notas de una canción con un sencillo algoritmo en código Octave/Matlab. Allí vimos que había casos en los que funcionaba medianamente bien, y otros en los que no demasiado.

El algoritmo en cuestión se basaba en una estimación de la frecuencia en base al número de nodos. Esto puede funcionar para una perfecta onda sinusoidal, pero todos los sonidos reales (desde el mismo instante en el que son finitos) son una superposición de ondas de distintas frecuencia. Aunque nos ha interesado desde el principio quedarnos con la frecuencia principal (despreciando los armónicos que resuenen a su son), se hacía indispensable un análisis de Fourier que nos diera más información sobre el espectro de la señal.

Mientras intentaba refrescar lo que dijo nuestro amigo Fourier (y que tenía bastante olvidado), he descubierto que Octave/Matlab cuentan con una función llamada "fft" (Fast Fourier Transform). Esta función nos da la gráfica del espectro, sin embargo, presenta tres complicaciones:

  • Los resultados están desordenados. Hay que ordenarlos con la función "fftshift"
  • Los resultados son complejos. Hay que hacerlos reales, para ello se puede tomar el módulo con la función "abs".
  • Una vez ordenados y pasados a reales los valores de la amplitud para cada frecuencia (lo que podríamos considerar gráficamente como el "eje y"), hay que escalar bien el eje de frecuencias ("eje x"). Si no, la relación entre la amplitud de cada frecuencia y el valor de la misma (que es lo que nos interesa) no sería correcta. Para ello hay que centrar el intervalo y escalarlo todo dividiéndolo entre el tiempo entre muestras y la longitud del intervalo.

Después de todo ello,  tendríamos dos vectores: uno que hemos llamado más abajo "yt", con las amplitudes de las frecuencias, y otro "f" con las frecuencias (ya desplazadas y escaladas para que sean las correctas). La nota que buscamos es la frecuencia que resuene con mayor intensidad, o lo que es lo mismo, hay que localizar el máximo valor de "yt", y ver qué valor de "f" le corresponde.

Este método no sólo es más limpio de programar y más exacto (luego veremos que da mejores resultados), sino que además es muchísimo más rápido.

El cambio que hemos realizado es básicamente el siguiente:

Script antiguo:

    % Recuento de los nodos:
    n=0;
    for i=1:(part-1)
        if (yt(i)*yt(i+1)) < 0
            n=n+1;
        else
            if (yt(i)*yt(i+1)) == 0
                n=n+1/2;
            end
        end
    end
    n=round(n);
    % Cálculo de la frecuencia original:
    freq(j)=((n+1)*fm)/(2*(fin-ini));

Script moderno:

    % FFT:
    [Fourmax,posmax]=max(abs(fftshift(fft(yt))));
    f=((1:fin-ini+1)-ceil((fin-ini+1)/2))*fm/(fin-ini+1);
    freq(j)=abs(f(posmax));

Siendo unas 6 veces más rápido el algoritmo nuevo con respecto al viejo. 

Pero, ¿es realmente más exacto este algoritmo? ¡Vamos a ponerlo a prueba! El caso más simple es el "La" puro generado por ordenador.

Detección de notas por el método de recuento de nodos

Detección de notas por el método Fourier

El resultado es parecido, pero constatamos que los tiempos de ejecución son muy distintos según qué algoritmo utilicemos.

Ejecución con el antiguo algoritmo

Ejecución con el nuevo algoritmo

Vale, pero éste era un caso muy fácil. ¿Qué pasa si tocamos un "La" con un órgano con efecto rotatorio? Aunque escuchemos el sonido modulado, la frecuencia principal viene dada por la nota ("La"), por lo que debería ser constante.

Detección de notas por el método de recuento de nodos

Detección de notas por el método Fourier

Como vemos, el método de los nodos se despista con las modulaciones mientras que el de Fourier resiste como debe.

Vamos un paso más allá: hagamos un arpegio en "Do mayor", es decir, las notas "Do", "Mi", "Sol" y "Do".

Detección de notas por el método de recuento de nodos

Detección de notas por el método Fourier

Viendo cómo reacciona el método de los nodos comprobamos que ni se acerca a identificar uno de los acordes más reconocibles que hay. El método de Fourier, sin embargo, lo hace a la perfección (e, insisto, muchísimo más rápido).

La última prueba es una escala que recorra todos los semitonos: "Do", "Do#", "Re", "Re#", "Mi", "Fa", "Fa#", "Sol", "Sol#", "La" y "Si".

Detección de notas por el método de recuento de nodos

Detección de notas por el método Fourier

Este es el único ejemplo donde falla el algoritmo de Fourier en una de las notas ("Re"), aunque todas las demás las saca exactas. El algoritmo de los nodos, sin embargo, parece mostrar notas casi al azar.

Por último, destacando la mucho mayor eficiencia del nuevo algoritmo, podemos procesar archivos mucho mayores y con un mayor número de particiones en un tiempo razonable. Valga de ejemplo el siguiente.


Se trata de una canción entera de unos dos minutos y medio grabada con una frecuencia de muestreo de 44.000Hz (6.613.200 valores), subdividida en 67 intervalos de 100.000 valores cada uno (2,27 segundos). Se tarda en procesar un tiempo similar a la duración de la canción, es decir, algo más de dos minutos. Además de ser más fiable este resultado, ésto no era procesable en un tiempo razonable con el algoritmo antiguo en un equipo como el mío.

Os dejo el código actualizado. ¡Espero que os sea útil!

function [freq]=notasFFT(p,part)
%
% Este script pretende dar una estimación de la nota que suena en un
%fragmento de audio WAVE. La sintaxtis sería:
%
% >>notasFFT('archivo.wav')
% >>notasFFT('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.
%
% Si no se especifica el tamaño de la partición (part), por defecto
%el script hace una división en 10 fragmentos.
%
% Creado por Astaroth (O.R.G.) el 17/10/2015.
% Actualizado el 21/10/2015.
%
% http://astarothsworld.blogspot.com
tic
clc


% 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);

Colores=[0,0,0;    %La
0,0,1;        %La#
0,1,0;        %Si
1,0,0;        %Do
1,0,1;        %Do#
1,1,0;        %Re
0,0,0;        %Re#
0,0,0.5;    %Mi
0,0.5,0;    %Fa
0.5,0,0;    %Fa#
0.5,0,0.5;    %Sol
0.5,0.5,0];    %Sol#
       

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));

% Control de argumentos de entrada:
if nargin <2
    % Ancho del intervalo:
    part=round(long/10)-1;    % Ancho del intervalo.

    % Ancho del intervalo (ligado al tempo):
%    tempo=150;
%    part=fm/(150/60);
end

% Representación incial:
x=linspace(1,long,long);
subplot(2,1,1)
plot(x,y)
hold on
xlabel('Registros muestreados')
ylabel('Amplitud de la onda')
plot([0,long],[0,0],'g')
axis([0,long,(-Y-Y/4),(Y+Y/4)])

subplot(2,1,2)
hold on
xlabel('Tiempo (s)')
ylabel('Notas')
axis([0,long/fm,0,14])

% Análisis de los datos:
tram=fix(long/part);
for j=1:(tram)
    ini=(j-1)*part+1;
    fin=min(j*part,long);
    yt(1:(fin-ini+1))=y(ini:fin);
    % FFT:
    [Fourmax,posmax]=max(abs(fftshift(fft(yt))));
    f=((1:fin-ini+1)-ceil((fin-ini+1)/2))*fm/(fin-ini+1);
    freq(j)=abs(f(posmax));

    % Cálculo de una frecuencia equivalente, reducida a un intervalo concreto:
    freqred=freq(j);
    while ( freqred < LA*r )
        freqred=freqred*2;
    end
    % Incluyo la octava superior por posible superación del intervalo por redondeo:
    while ( freqred > (LA*r^(13)) )
        freqred=freqred/2;
    end
    % Cálculo del exponente (distancia en semitonos desde LA):
    rcal=log(freqred/LA)/log(r);
    rcal=round(rcal+1);
    % Solución al rebosamiento por encima:
    if rcal >= 13
        rcal=rcal-12;
    end
    % Identificación de la nota:
    Nota(j,:)=NOTAS(rcal,:);

    % Representaciones gráficas de los resultados:
    subplot(2,1,1)
    plot(ini*ones(2),linspace(-Y,Y,2),'r-')
    text(ini+(fin-ini)/2,Y+Y/8,[num2str(freq(j)),' Hz'],'HorizontalAlignment','center')
    title(['Análisis del archivo: "',p,'" - Frecuencias'])

    subplot(2,1,2)
    plot([ini/fm,ini/fm],[0,12],'r--')
    plot([ini/fm,fin/fm],[rcal,rcal],'Color',Colores(rcal,:))
    text(ini/fm+(fin-ini)/(2*fm),13,Nota(j,:),'HorizontalAlignment','center')
    title(['Análisis del archivo: "',p,'" - Notas'])
    notes=gca;
    set(notes,'YTick',1:12)
    set(notes,'YTickLabel',{'La','La#','Si','Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#'})

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),'.'])
disp([' - Ancho del tramo: ',num2str(part),' muestras (',num2str(part/fm),' s).'])
disp(' - Análisis de los tramos:')
for j=1:(tram)
    disp(['    - Tramo ',num2str(j),': ',num2str(freq(j)),' Hz (  ',Nota(j,:),').'])
end
disp('')
disp('***Fin del programa***')
toc


Referencias:

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:

viernes, 9 de octubre de 2015

SC15: El Día de Cervantes

Qué encontrarás en esta entrada?
  • ¿Por qué el 9 de octubre es relevante para Alcalá de Henares?
  • Fotos del Mercadillo del Quijote durante este día.
  • Fotos del grupo Cornalusa

El 9 de octubre es el día grande de esta festividad medieval que celebramos en Alcalá de Henares cada año. Esto se debe a que aún conservamos un registro en el que figura que el 9 de octubre de 1547 fue bautizado Miguel de Cervantes en la iglesia de Santa María la Mayor.

En la actualidad podemos ver una copia de ese registro expuesta a diario en la conocida como Capilla del Oidor, pero sólo una vez al año tenemos la oportunidad de ver el documento original. Ese día es hoy, 9 de octubre.


En un pomposo acto, en el que no faltan los políticos que desean hacerse visibles en todas las fotos de los periódicos locales, se lleva el libro original desde el ayuntamiento a la Capilla del Oidor. Una vez allí, se recuerda el valor del documento: que en él aparece el registro del bautismo de Miguel de Cervantes, y que en él también se pueden ver referencias a bautismos de otros miembros de su familia, lo que indica que estaban establecidos en la localidad de forma estable.




El que este libro haya podido llegar hasta nuestros tiempos, no ha sido, sin embargo, algo trivial. Según podemos leer en el blog Efemérides Complutenses, la destrucción en 1936 de la iglesia de Santa María la Mayor, en el contexto de la Guerra Civil, pudo haber acabado con la existencia de tan preciado documento. El libro estuvo guardado en casas de vecinos, en comercios, e incluso en un pozo, dentro de una caja de galletas. Tras todo ello, el libro pudo ser recuperado, aunque con cierto deterioro.

Tras este acto, le siguen muchos otros durante el día. He aquí algunas fotos del ambiente del casco antiguo complutense en un día como hoy.



















Por otro lado, tenía cierta curiosidad por ver al grupo Cornalusa. Había visto algunos vídeos en YouTube y la verdad es que me llamaba la atención su música. Protagonizando varios pasacalles durante el día de hoy, he podido verlos y escucharlos tranquilamente.













Como otros muchos grupos invitados a estas fiestas, su música es principalmente popular celta, sin embargo no puedo evitar distinguir ciertos matices rock que a mi parecer lo hacen más interesante.

¡Y aún queda día! Así que salid, si no lo habéis hecho ya, a conocer el Mercadillo del Quijote. Y de haberlo hecho, repetid, pues nada os ha de prevenir de qué sorpresas os habréis de encontrar una vez inmersos en aquestas fiestas.