Reproducir audio a través del DAC del Teensy 
El procesador ARM Cortex-M4 (Un MK20DX256 de Freescale) incluido en la placa de desarrollo Teensy 3.1 viene equipado con una salida analógica (DAC, no PWM) de 12 bits de resolución con la que es posible generar audio con una calidad razonable y sin apenas hardware externo.

Punto de partida

Se parte del compilador gcc, las binutils y la newlib compilados para el target “arm-none-eabi” detallados en este post y del trabajo realizado anteriormente en este otro post.

DAC

El DAC del microcontrolador Freescale MK20DX256 tiene una resolución de 12 bits (sin signo) y puede ser utilizado tanto de forma sencilla como mediante DMA. En este caso se va a optar por un uso sencillo sin DMA: La escritura de los datos de las muestras la hará el propio código del programa.

...
#define  SIM_SCGC2    *((uint32_t *) 0x4004802C)
#define  DACDAT       *((uint16_t *) 0x400CC000)
#define  DAC0_C0      *((uint8_t *)  0x400CC021)
#define  DAC0_C1      *((uint8_t *)  0x400CC022)
...
SIM_SCGC2 |= (1 << 12);   // habilitar el generador de reloj para el DAC
DAC0_C1 = 0x00;           // deshabilitar el modo DMA para el DAC
DAC0_C0 = 0xC0;           // habilitar el DAC para VREF2 (3.3v)
// a partir de ahora ya se puede escribir en el DAC (DACDAT)
...

Hay que tener en cuenta que el registro DACDAT es un registro de 12 bits sin signo (unsigned).

Systick

La interrupción “systick” es una de las interrupciones estándar del núcleo ARM Cortex-M4 (de hecho está presente en todos los procesadores ARM Cortex). Se trata de una interrupción que se dispara cuando un contador de 24 bits llega a cero, dicho contador está gobernado por el reloj del núcleo (cuidado, suele ser diferente al reloj del bus) y carece de divisores (es muy simple).

El vector de la interrupción se encuentra en la dirección de memoria 0x0000003C. En esta dirección de memoria debe alojarse la dirección de memoria donde se encuentre la función que se ejecutará cada vez que el systick llegue a cero y vuelva a cargarse (una indirección).

Para implementar esta funcionalidad con el GCC se modifica el linker script (teensy31.ld) para incluir el nuevo vector de interrupción:

...
. = 0x00000000 ;
.cortex_m4_vectors : {
    LONG(0x20007FFC);
    LONG(0x00000411);
}
. = 0x0000003C ;
.cortex_m4_vector_systick : {
    LONG(SYSTICK_ADDRESS + 1);
}
. = 0x00000400 ;
.flash_configuration : {
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFE);
}
...

Y para incluir una nueva sección dentro de la memoria de programa con una dirección de memoria prefijada:

...
SYSTICK_ADDRESS = . ;
.systick : {
    *(.systick)
}
...

Nótese que la dirección de memoria almacenada en 0x0000003C es la siguiente dirección impar después de SYSTICK_ADDRESS. Esto tiene una explicación y es muy sencilla:

Los procesadores ARM soportan dos repertorios de instrucciones: un repertorio muy amplio y potente en el que cada instrucción ocupa 32 bits (modo “arm”) y otro repertorio más reducido en el que cada instrucción ocupa 16 bits (modo “thumb”). El primero es más potente pero ocupa más, mientras que el segundo en menos potente pero ocupa mucho menos. Lo que se puede hacer en modo “arm” se puede hacer también en modo “thumb” aunque es posible que para hacer lo que hace una instrucción “arm” sean necesarias dos o tres instrucciones “thumb”.

La forma en que un procesador ARM sabe si una instrucción a la que apunta el PC forma parte de un repertorio de instrucciones u otro es mediante el bit 0 del PC. Si el bit 0 vale 0, se trata de una instrucción “arm”, mientras que si el bit vale 1 se trata de una instrucción “thumb” (nótese que sea cual sea el modo, todas las instrucciones se encuentran, como mínimo, en direcciones pares, en las direcciones impares nunca hay instrucciones).

Por otro lado según la especificación ARM, las excepciones (interrupciones) se deben ejecutar siempre en modo “thumb”. De todas formas en este caso no tenemos elección ya que la serie Cortex-M de ARM sólo soporta el repertorio de instrucciones “thumb” (http://infocenter.arm.com/help/index.js ... BIBGJ.html).

Con la nueva sección de código llamada “.systick” en el código fuente puede ahora definirse la función que va a manejar la interrupción:

...
#define  SYST_CSR  *((uint32_t *) 0xE000E010)
#define  SYST_RVR  *((uint32_t *) 0xE000E014)
#define  SYST_CVR  *((uint32_t *) 0xE000E018)
#define  SAMPLE_RATE  44100

// indicamos al compilador que queremos alojar el cuerpo de esta función en la sección “.systick”
void systick()  __attribute__ ((section(".systick")));

void systick() {
    // TODO
}
...

...
// configuramos el systick para que se ejecute SAMPLE_RATE veces por segundo
SYST_RVR = F_CPU / SAMPLE_RATE;
SYST_CVR = 0;
SYST_CSR |= 0x07;
...

F_CPU es la velocidad en Hz del núcleo (en este caso 96 MHz = 96000000 Hz) y hacemos que el systick se ejecute 44100 veces por segundo (la frecuencia de muestreo del sonido a reproducir).

No se debe utilizar el atributo “interrupt” al declarar la función “systick” ya que en ese caso el compilador intenta compilarla en modo “arm” en lugar de “thumb”.

Sonido

Partiendo de uno de los sonidos (un bucle de bateria) publicado con licencia Creative Commons Attribution-ShareAlike por el usuario de Soundcloud “Phantom Hack3r” (AKA Loop Studio, https://soundcloud.com/phantom-hack3r) se ha editado, se ha dejado sólo con un único compás (el inicial) y se ha exportado a WAV (“drum_loop_1.wav”).

A continuación, usando la herramienta de línea de comandos, “sox” se exporta a su vez este fichero WAV a un formato crudo de 8 bits, mono y sin signo:
sox drum_loop_1.wav -u -b 8 -c 1 -r 44100 drum_loop_1.raw
Luego el fichero drum_loop_1.raw se convierte a un fichero objeto para meterlo como si fuese código dentro del microcontrolador:
/opt/teensy/bin/arm-none-eabi-objcopy --input binary --output elf32-littlearm --binary-architecture arm --rename-section .data=.text drum_loop_1.raw drum_loop_1.o
La opción “--rename-section .data=.text” es muy importante ya que marca los datos generados para que se alojen en la sección “.text” del fichero de salida. Esta sección es la sección que será alojada en la memoria flash del Teensy.

Ahora en “drum_loop_1.o” hay definidas dos variables “_binary_drum_loop_1_raw_start” y “_binary_drum_loop_1_raw_end” cuya dirección de memoria es el inicio y el final respectivamente de los datos crudos convertidos (“drum_loop_1.raw”).

Circuito de salida

A la hora de conectar la salida analógica del DAC a unos altavoces hay que hacerlo siempre a través de un amplificador ya que la corriente máxima que soporta la salida DAC es muy baja. De entre todas las opciones de amplificación, la más sencilla es, sin duda, el uso de unos altavoces amplificados de PC (solución sugerida por el propio creador del Teensy, Paul Stoffregen, aquí).



Se trata de un sencillo condensador electrolítico (para el desacoplo de continua) entre la salida del DAC y la entrada del amplificador de altavoces.

Resultado final

El código fuente final de main.cc es el siguiente:

#include <stdint.h>

using namespace std;

#define  SYST_CSR  *((uint32_t *) 0xE000E010)
#define  SYST_RVR  *((uint32_t *) 0xE000E014)
#define  SYST_CVR  *((uint32_t *) 0xE000E018)

#define  SIM_SCGC2    *((uint32_t *) 0x4004802C)
#define  DACDAT       *((uint16_t *) 0x400CC000)
#define  DAC0_C0      *((uint8_t *)  0x400CC021)
#define  DAC0_C1      *((uint8_t *)  0x400CC022)
#define  SAMPLE_RATE  44100

extern char _binary_drum_loop_1_raw_start;
extern char _binary_drum_loop_1_raw_end;
volatile char *p;

void systick()  __attribute__ ((section(".systick")));

void systick() {
    DACDAT = ((uint16_t) *p) << 4;
    p++;
    if (p == &_binary_drum_loop_1_raw_end)
        p = &_binary_drum_loop_1_raw_start;
}

int main() {
    // configure DAC
    SIM_SCGC2 |= (1 << 12);    // enable DAC clock generator
    DAC0_C1 = 0x00;            // disable DAC DMA
    DAC0_C0 = 0xC0;            // enable DAC for VREF2 (3.3v)
    // configure SYSTICK
    p = &_binary_drum_loop_1_raw_start;
    SYST_RVR = F_CPU / SAMPLE_RATE;
    SYST_CVR = 0;
    SYST_CSR |= 0x07;
    while (1)
        ;
}

A continuación un vídeo donde puede verse (y oírse) el montaje en funcionamiento.



Todo el código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 695 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 9458 )
Detección de tonos en sistemas embebidos 
Cuando pensamos en detectar determinadas frecuencias o tonos en una señal lo primero que se nos viene a la cabeza suele ser la FFT, en concreto la implementación de Cooley-Tukey con N potencia de 2. La FFT está muy bien si lo que queremos es todo el espectro de una señal, pero si lo que necesitamos es detectar un único tono en una frecuencia concreta podemos recurrir al algoritmo de Goertzel, más rápido y muy fácil de implementar en sistemas embebidos.

El algoritmo de Goertzel permite calcular un coeficiente aislado de la DFT sobre un conjunto de N muestras con una complejidad temporal de O(n) y una complejidad espacial de O(1), además N no tiene por qué ser potencia de 2. Este algoritmo se basa en la aplicación de una ecuación en diferencias finitas (un filtro IIR).

A mayor N, mayor resolución en frecuencia y también mayor latencia en la detección de los tonos. Es necesario, por tanto encontrar un compromiso. La resolución en hercios del algoritmo de Goertzel viene dada por:
$$R = {f_s \over N}$$
Siendo $f_s$ la frecuencia de muestreo y $N$ el número de muestras que se evalúan en cada pasada.

1. Se calcula el índice del coeficiente correspondiente

Lo primero que hay que hacer es calcular el índice del coeficiente asociado a la DFT a partir de la frecuencia que queremos detectar:
$$k = N{f \over f_s}$$
Siendo $f$ la frecuencia que queremos detectar, $f_s$ la frecuencia de muestreo y $N$ el número de muestras que procesamos cada vez. La máxima frecuencia que podemos detectar será la mitad de la frecuencia de muestreo.

2. Para cada conjunto de N muestras

2.1. Se aplica la ecuación en diferencias

Para cada una de las muestras que van llegando, vamos aplicando la siguiente ecuación en diferencias finitas:
$$s[n] = x[n] + 2\cos\left({2 \pi k \over N}\right)s[n-1] - s[n-2]$$
$$n = 0..N$$
Con codiciones iniciales $s[-1] = s[-2] = 0$. Se trata, como se puede ver, de un sencillo filtro IIR de segundo orden.

2.2. Se obtiene del coeficiente k-esimo de la DFT

Se puede demostrar que el coeficiente k-ésimo de la DFT de tamaño N es:
$$X(k) = s[N] - W_N^ks[N-1]$$
Siendo:
$$W_N = e^{-j\left({2 \pi \over N}\right)}$$
Por tanto:
$$X(k) = s[N] - e^{-j\left({2 \pi \over N}\right)k}s[N-1]$$
Si desarrollamos la exponencial compleja mediante la fórmula de Euler tenemos que:
$$e^{-j\left({2 \pi \over N}\right)k} = \cos\left({2 \pi \over N}\right) - j \sin\left({2 \pi k \over N}\right)$$
Y, por tanto:
$$X(k)_{real} = s[N] - \cos\left({2 \pi k \over N}\right)s[N-1]$$
$$X(k)_{imag} = \sin\left({2 \pi k \over N}\right)s[N-1]$$
Para calcular la magnitud de la banda de frecuencia correspondiente calculamos el módulo de $X(k)$:
$$M^2 = \left|X(k)\right|^2 = X(k)_{real}^2 + X(k)_{imag}^2$$
De esta forma podemos medir la magnitud de la banda de frecuencia correspondiente al coeficiente k-ésimo de la DFT o, lo que es lo mismo, la magnitud de la banda de frecuencia correspondiente a la frecuencia $f$.
$$k = N{f \over f_s} \Rightarrow f = {k f_s \over N}$$

Implementación

A continuación se puede ver una implementación del algoritmo de Goertzel en Arduino:
const int ANALOG_INPUT = A0;
const int SAMPLE_RATE_HZ = 3000;
const int SAMPLE_PERIOD_US = 1000000 / SAMPLE_RATE_HZ;
const int F_HZ = 440;
const int N = 3000;
const int K = 440;  // N * F_HZ / SAMPLE_RATE_HZ;
unsigned long tPrev;

struct goertzelFilter {
  float s1, s2;
  int k, n;
  float sinv, cosv, cosv2;
  int nextIteration;
};

struct goertzelFilter filter;

void goertzelFilterReset(struct goertzelFilter &f) {
  f.s1 = 0;
  f.s2 = 0;
  f.nextIteration = 0;
}

void goertzelFilterInit(struct goertzelFilter &f, int k, int n) {
  f.k = k;
  f.n = n;
  f.cosv = cos(2 * PI * k / n);
  f.sinv = sin(2 * PI * k / n);
  f.cosv2 = 2 * f.cosv;
  goertzelFilterReset(f);
}

boolean goertzelFilterFinished(struct goertzelFilter &f) {
  return (f.nextIteration == (f.n + 1));
}

void goertzelFilterRun(struct goertzelFilter &f, float input) {
  float s = input + (f.cosv2 * f.s1) - f.s2;
  f.s2 = f.s1;
  f.s1 = s;
  f.nextIteration++;
}

float goertzelFilterGetMagnitude(struct goertzelFilter &f) {
  float real = f.s1 - (f.cosv * f.s2);
  float imag = f.sinv * f.s2;
  return ((real * real) + (imag * imag));
}

void setup() {
  Serial.begin(9600); 
  tPrev = micros();
  goertzelFilterInit(filter, K, N);
  goertzelFilterReset(filter);
}

void loop() {
  unsigned long t = micros();
  if ((t - tPrev) >= SAMPLE_PERIOD_US) {
    int v = analogRead(ANALOG_INPUT);
    if (goertzelFilterFinished(filter)) {
      float magnitude = goertzelFilterGetMagnitude(filter);
      Serial.println(magnitude);
      goertzelFilterReset(filter);
    }
    else
      goertzelFilterRun(filter, ((float) v - 512) / 512);
    tPrev = t;
  }
}

Se ha elegido una frecuencia de muestreo baja (3000Hz) para poder trabajar cómodamente con tipos float. Utilizando aritmética de punto fijo podríamos incremenentar la frecuencia de muestreo y que la detección sea más precisa.

La entrada de audio se toma de la entrada analógica A0 a la que se conecta un sencillo circuito amplificador:


Elegir el valor de N

A la hora de elegir la N lo ideal es escogerla lo más grande posible, que nos permita una latencia razonable y que se cumpla que:
$$k = N{f \over f_s} \in \mathbb{N}$$
Por ejemplo, para detectar un tono de 1Khz sobre una señal muestreada a 6KHz lo ideal sería que la N valiese: 96, 102, 114… Ya que para todos estos valores se cumple que $k$ es número natural.

Magnitudes medidas para diferentes tonos

En ausencia de señal de entrada: 0.30, 0.37, 0.30, 0.35, 0.44, 0.32, 0.37, 0.28...

Con una señal de entrada de 200Hz: 2.56, 1.73, 3.56, 0.81, 0.58, 1.67, 4.71, 6.81...

Con una señal de entrada de 440Hz (la frecuencia del detector): 138.41, 87.29, 441.14, 185.20, 233.03, 762.27, 80.62, 330.98...

Con una señal de entrada de 500Hz: 25.70, 9.60, 22.50, 2.76, 16.62, 18.75, 23.56, 35.58...



[ añadir comentario ] ( 1655 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3.1 / 2037 )
Algoritmo de detección de tempo 
Hace un tiempo encontré en el siguiente enlace un interesantísimo artículo explicando las diferentes técnicas que existen para la detección de BPM (tempo) sobre pistas de audio.

http://www.flipcode.com/misc/BeatDetect ... rithms.pdf

En particular me llamó la atención la técnica basada en el cálculo de la correlación de la señal con un tren de pulsos. En la sección soft puede descargarse una implementación sencilla en C de este algoritmo. Da muy buenos resultados con música pop, rock y electrónica. No he probado el código con otros estilos musicales.

Código fuente aquí.

[ añadir comentario ] ( 449 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 887 )
Detección de pitch con HPS 
El algoritmo HPS (Harmonic Product Spectrum) permite detectar la frecuencia fundamental (la altura) de una nota tocada por cualquier tipo de instrumento armónico.

Asume que el espectro generado por el instrumento está formado por frecuencias múltiplas enteras de la frecuencia fundamental, lo cual es cierto en el 99% de los instrumentos musicales convencionales, incluyendo la voz.

En la sección soft he colgado una sencilla implementación en C de este algoritmo y la he acompañado de dos ejemplos de tonos generados por un saxo. Uno de los ejemplos es una nota LA que el algoritmo detecta de 440.0848 Hz (440 Hz es la frecuencia de la nota LA en la 4ª octava) y el otro es de una nota RE que el algoritmo detecta de 293.3899 Hz (293.665 Hz es la frecuencia de la nota RE en la 4ª octava).

Más info sobre el HPS y otros algoritmos de detección de pitch en http://cnx.org/content/m11714/latest/.

EDITADO: He corregido un bug en el código que provocaba que la detección estuviera desplazada una octava.

Sección soft.

[ añadir comentario ] ( 471 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 824 )
Nueva versión de Sonority 
Sonority es un motor de síntesis de sonido mediante modelado analógico implementado en ANSI C que utiliza exclusivamente aritmética de punto fijo (formato Q16.16, 16 bits de parte entera y 16 bits de parte fraccionaria, 32 bits en total).

En esta nueva versión se han añadido dos modos de portamento (ALWAYS y LEGATTO), dos modos de disparo de envolvente (ALWAYS y STACATTO) y tablas precalculadas (tanto la tabla de frecuencias como las tablas de ondas).

El fichero test.c contiene un ejemplo de utilización del motor.

+info y descargas aquí.

[ añadir comentario ] ( 670 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 899 )

<< <Anterior | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Siguiente> >>