Implementación de un MIDI shuffler sobre Arduino 
El efecto "shuffle" o "swing" es un efecto muy utilizado en producción musical para humanizar y meter mas "groove" a canciones reproducidas por un secuenciador. El efecto consiste básicamente en adelantar o atrasar el disparo de determinadas notas durante algunos milisegundos para dar sensación de "humanidad" a la cadencia de la música. A lo largo de este post se abordará la implementación en C++ sobre Arduino de un "shuffler" MIDI para secuencias 4/4.

La forma más estándar de "shuffle" en secuencias musicales de 4/4 es la que consiste en retrasar una cantidad de tiempo determinada (milisegundos) la segunda y la cuarta semicorchea después de cada negra:

*----.----.----.----*----.----.----.----*----.----.----.----*----.----.----.---- Compás 4/4 estándar
*------.--.------.--*------.--.------.--*------.--.------.--*------.--.------.-- Compás de 4/4 con "shuffle"

Los asteriscos determinan las negras (4 negras por cada compás de 4/4) y los puntos determinan las semicorcheas (4 semicorcheas por cada negra). El concepto es muy sencillo, aunque a la hora de implementarlo en MIDI hay que tener en cuenta algunos aspectos importantes.

Protocolo MIDI

El protocolo MIDI es un protocolo muy sencillo por el que se envían eventos e información musical. No es objetivo de este post el explicar el protocolo ni los mensajes MIDI (cualquier búsqueda sobre "midi protocol" en la red nos dará acceso a centenares de páginas donde lo explican muy bien) aunque sí nos centraremos en los mensajes que más nos interesan de cara a implementar nuestro shuffler.

Dentro de los mensajes MIDI hay unos especiales denominados de tiempo real que son transmitidos por los secuenciadores cuando están reproduciendo una secuencia MIDI pregrabada:

0xF8: "timing clock" se envía 24 veces por cada negra.
0xFA: "start" indica que se va a iniciar la reproducción de una secuencia. Este mensaje es seguido de forma inmediata por el primer 0xF8.
0xFB: "continue" indica que se reanuda la secuencia por donde se paró.
0xFC: "stop" indica que se para la secuencia.

Por tanto, si en nuestro secuenciador musical tenemos una canción con un tempo de 120 negras por minuto, al emitir dicha secuencia por un cable MIDI, de forma intercalada con los mensaje de activación y desactivación de las notas y demás, irán entremezclados mensajes 0xF8 a razón de 24 por cada negra, es decir:

$${{120 \times 24} \over 60} = 48\;mensajes/segundo$$

Nótese que la cantidad de mensajes 0xF8 enviados por unidad de tiempo no depende de la velocidad de transmisión MIDI, sino del tempo de la secuencia musical que se esté reproduciendo. Si cada vez que nos llegue un mensaje 0xF8 desde el secuenciador vamos contando de 0 a 23 dando la vuelta de nuevo a 0 cada vez que llegamos a 24 tenemos que los mensaje 0xF8 coinciden en el tiempo con las negras y semicorcheas de la forma que indica la siguiente tabla:

n                 s                 s                 s
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23


En esta tabla se puede ver que la negra (el beat) coincide con el contador de mensajes 0xF8 recibidos a 0 mientras que las tres semicorcheas siguientes coinciden con ese mismo contador a 6, a 12 y a 18. Ahora tenemos una base de tiempo sólida que podemos aprovechar para implementar nuestro efecto shuffle: Lo que hay que hacer es atrasar en el tiempo los mensajes de "note on" y "note off" que lleguen entre el instante 6 y el 12 y entre el instante 18 y 0 de la siguiente negra.

n                 s                 s                 s
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
+--------> atrasar +--------> atrasar


Dicho atraso no puede ser tal que nuestro shuffler emita notas fuera de orden por lo que el retraso en el tiempo debe ser proporcional (una nota que llegue entre los instantes 6 y el 7 será atrasada más que una que llegue entre los instantes 9 y 10 pero la primera nunca debe emitirse depués de la segunda, debemos garantizar el orden de llegada de los eventos "note on" y "note off").

Algoritmo propuesto

El MIDI shuffler se plantea como un filtro MIDI, un dispositivo con una entrada MIDI y una salida MIDI que se intercala entre el secuenciador y los sintetizadores. La salida MIDI del secuenciador irá conectada a la entrada MIDI del shuffler y la salida MIDI del shuffler irá conectada a la entrada MIDI de los secuenciadores. A continuación se plantea una propuesta de pseudocódigo para el MIDI shuffler:

iniciarShuffler
estado := ESPERAR_START_MIDI
fin iniciarShuffler

getInstanteAtrasado(t)
ret := (tamSemicorchea - tamReducido) + ((t + tamReducido) / tamSemicorchea)
devolver ret
fin getInstanteAtrasado

byteMIDIRecibido(byte)
enviar := SÍ
si (estado = ESPERAR_START_MIDI) entonces
si (byte = 0xFA) entonces
colaRetraso.borrar()
estado := ESPERAR_PRIMER_CLOCK_MIDI
fin sin
en otro caso, si (estado = ESPERAR_PRIMER_CLOCK_MIDI) entonces
si (byte = 0xF8) entonces
estado := ESPERAR_CLOCK_MIDI
contadorReloj := 6
indiceSemicorchea := 0
timer.iniciar()
fin si
en otro caso, si (estado = ESPERAR_CLOCK_MIDI) entonces
si (byte = 0xF8) entonces
contadorReloj := contadorReloj - 1
si (contadorReloj = 0) entonces
si ((indiceSemicorchea = 0) ó (indiceSemicorchea = 2)) entonces
tamSemicorchea = timer.getValor()
tamReducido = (temSemicorchea * (100 - PERCENT)) / 100
fin si
contadorReloj := 6
indiceSemicorchea := (indiceSemicorchea + 1) mod 4
timer.parar()
timer.iniciar()
fin si
en otro caso, si (esEventoNota(byte) y ((indiceSemicorchea = 1) ó (indiceSemicorchea = 3)) entonces
t := getInstanteAtrasado(timer.getValor())
colaRetraso.meter({byte, t})
enviar := NO
en otro caso, si (byte = 0xFC)
estado := ESPERAR_START_MIDI
fin si
fin si
si (enviar = SÍ) entonces
enviar(byte)
fin si
fin byteMIDIRecibido

principal
siempre hacer
si ((indiceSemicorchea = 1) ó (indiceSemicorchea = 3)) entonces
t := timer.getValor()
mientras (colaRetraso.hayAlgo()) hacer
d := colaRetraso.getCabeza()
si (d.t <= t) entonces
colaRetraso.sacar()
enviar(d.byte)
en otro caso
salir del bucle
fin si
fin mientras
fin si
fin siempre
fin principal

Lo que hace el algoritmo es aprovechar el intervalo entre el midi clock 0 y el 5 para calcular el tiempo en unidades de timer que dura una semicorchea. El objeto "timer" es un timer de bastante resolución que se arranca en el instante 0 y se para en el instante 6. En ese instante 6, una vez parado el timer, se anota la cuenta del mismo como tamSemicorchea (para indicar que es el tamaño en ticks de nuestro contador de lo que dura una semicorchea) y se calcula tamReducido a partir del porcentaje de "shuffle" que queramos (un shuffle del 0% da un tamReducido = tamSemicorchea, mientras que un shuffle del 100% da un tamReducido = 0).

instante  semicorchea   acción
0 0 Iniciar timer de alta resolución
1
2
3
4
5
6 1 Anotar cuenta del timer, pararlo
7 y volver a iniciarlo. Encolar cualquier
8 evento "note on" o "note off" que llegue
9 en este intervalo calculando su instante
10 de emisión con una regla de tres.
11
12 2 La misma que la semicorchea 0
13
14
15
16
17
18 3 La misma que la semicorchea 1
19
20
21
22
23

Entre los instantes 6 y el 11 lo que se hace es encolar los eventos de "note on" y "note off" que vayan llegando calculándoles en el momento que llegan, en qué instante del tick del timer deben ser transmitidos haciendo una regla de tres (en getInstanteAtrasado) y metiendo cada una de estas parejas de valores (byte e instante que debe ser transmitido) en la cola "colaRetraso".

Lo mismo se hace para los instantes de tiempo 12 al 17 y 18 al 23, respectivamente.

Ya tenemos los eventos atrasados metidos en una cola (para garantizar que el orden de emisión sea el mismo que el de recepción), ahora lo que hay que hacer es emitirlos en el instante que corresponda. y de esto se encarga el procedimiento principal en su bucle infinito. Este procedimiento principal ejecuta un bucle infinito que lo que hace es inspeccionar si hay algo que enviar en la cola "colaRetraso", si hay algo que debe ser enviado (su instante de envío es menor o igual al valor actual del timer) lo envía y lo quita de la cola. El procedimiente byteMIDIRecibido es invocado cada vez que llega un byte por el puerto MIDI.

El circuito

El MIDI shuffler, como se comentó antes, hace de filtro MIDI con una entrada y una salida. La cantidad de efecto shuffle se controla mediante un potenciómetro conectado a una de las entradas analógicas del Arduino.

Con el potenciómetro al mínimo se aplica un efecto shuffle del 0% (sin efecto shuffle) mientras que con el potenciómetro al máximo se aplica un efecto shuffle del 50% (valores superiores al 50% genera unos resultados muy extremos).

Implementación en C++

A pesar de que en el algoritmo propuesto el procedimiento byteMIDIRecibido se supone que es invocado de forma asíncrona por el sistema cada vez que llega un byte por el puerto MIDI, lo cierto es que es más sencillo si en la rutina de interrupción de la UART encolamos los bytes MIDI que van llegando por la entrada MIDI y luego los vamos sirviendo en el bucle principal antes de comprobar el estado de la colaRetraso, haciéndolo de esta forma evitamos colisiones y la necesidad de hacer que colaRetraso sea reentrante.

int32_t MIDIShuffler::getDelayedInstant(int32_t sourceInstant) {
    return ((this->sixteenthNoteLength - this->reducedLength) + ((sourceInstant * this->reducedLength) / this->sixteenthNoteLength));
}


void MIDIShuffler::byteReceived(uint8_t byte) {
    this->rxQueue.push(byte);
}


void MIDIShuffler::processRxByte(uint8_t byte) {
    bool send = true;
    uint8_t noChannelByte = byte & 0xF0;
    if (this->status == STATUS_WAIT_START_MIDI_CLOCK) {
        if (byte == 0xFA) {
            this->delayQueue.clear();
            this->rxQueue.clear();
            this->status = STATUS_WAIT_FIRST_MIDI_CLOCK;
        }
    }
    else if (this->status == STATUS_WAIT_FIRST_MIDI_CLOCK) {
        if (byte == 0xF8) {
            this->status = STATUS_WAIT_MIDI_CLOCK;
            this->clockCounter = CLOCK_PER_SIXTEENTH_NOTE;
            this->sixteenthNoteIndex = 0;
            this->timeCounter->start();
        }
    }
    else if (this->status == STATUS_WAIT_MIDI_CLOCK) {
        if (byte == 0xF8) {
            this->clockCounter--;
            if (this->clockCounter == 0) {
                if ((this->sixteenthNoteIndex == 0) || (this->sixteenthNoteIndex == 2)) {
                    this->sixteenthNoteLength = this->timeCounter->getValue();
                    this->reducedLength = (this->sixteenthNoteLength * (100 - this->percentProvider->getPercent())) / 100;
                }
                this->clockCounter = CLOCK_PER_SIXTEENTH_NOTE;
                this->sixteenthNoteIndex = (this->sixteenthNoteIndex + 1) & 3;    // ... % 4
                this->timeCounter->stop();
                this->timeCounter->start();
            }
        }
        else if ((noChannelByte < 0xA0) && ((this->sixteenthNoteIndex == 1) || (this->sixteenthNoteIndex == 3)) && !this->byPass) {
            DelayedMIDIByte d(this->getDelayedInstant(this->timeCounter->getValue()), byte);
            this->delayQueue.push(d);
            send = false;
        }
        else if (byte == 0xFC)
            this->status = STATUS_WAIT_START_MIDI_CLOCK;
    }
    if (send && (this->sender != NULL))
        this->sender->sendByte(byte);
}


void MIDIShuffler::init(MIDISender &sender, PercentProvider &percentProvider, TimeCounter &timeCounter) {
    MIDIFilter::init(sender);
    this->percentProvider = &percentProvider;
    this->delayQueue.clear();
    this->rxQueue.clear();
    this->status = STATUS_WAIT_START_MIDI_CLOCK;
    this->timeCounter = &timeCounter;
    this->byPass = false;
    this->sixteenthNoteIndex = 0;
}


void MIDIShuffler::run() {
    if (this->rxQueue.hasElements()) {
        uint8_t byte = this->rxQueue.getHead();
        this->processRxByte(byte);
        this->rxQueue.pop();
    }
    if (((this->sixteenthNoteIndex == 1) || (this->sixteenthNoteIndex == 3)) && this->delayQueue.hasElements()) {
        int32_t t = this->timeCounter->getValue();
        while (this->delayQueue.hasElements()) {
            DelayedMIDIByte d = this->delayQueue.getHead();
            if (d.t <= t) {
                this->delayQueue.pop();
                if (this->sender != NULL)
                    this->sender->sendByte(d.byte);
            }
            else
                break;
        }
    }
}

A continuación puede verse un vídeo con el MIDI shuffler en acción (obviamente, hay que poner el audio para que se oiga :-) )



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

[ añadir comentario ] ( 2239 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 14072 )
Salida de audio de alta calidad con la placa Teensy 
La placa Teensy 3.1 (ARM Cortex M4) dispone de un puerto I2S para la transferencia de audio digital. Si combinamos esta salida con un buen DAC de alta fidelidad el resultado es espectacular :-)

El DAC de Texas Instruments PCM5102 es un DAC que soporta el estándar I2S de transferencia de audio digital y el estándar “left justified” (variante del I2S). Existen muchos otros DACs de audio en el mercado con soporte para estos formatos, sin embargo los más usados son el ES9023 y derivados, de ESS, y el PCM5102 y derivados, de Texas Instruments. En mi caso, adquirí una placa con un integrado PCM5102A y la circuitería mínima (componentes pasivos, espadines para conectar alimentación y las tres líneas del protocolo I2S y dos conectores RCA hembra de salida, uno para cada canal).



Unos 14€ por AliExpress (gastos de envío incluidos), aunque ahora creo que está incluso más barato.

I2S

El protocolo I2S es un protocolo muy sencillo de transferencia de audio digital. Aunque por su nombre puede parecer que es un protocolo derivado o parecido al protocolo I2C, lo cierto es que sólo se parecen en el nombre y, para nuestro alivio, es bastante más sencillo que el I2C.


(imagen extraida de Wikimedia, realizada por el usuario Wdwd y con licencia Creative Commons Attribution 3.0 Unported)

El protocolo, como se puede ver en el diagrama, solo necesita de tres hilos: uno para datos, otro para el reloj y otro para seleccionar la palabra o el frame (ponemos esta señal a 0 para enviar la muestra del canal izquierdo y a 1 para enviar la muestra del canal derecho).

Al tratarse de un protocolo de transferencia serie, si queremos emitir audio con calidad CD (16 bits a 44100 Hz estéreo) hace falta generar un reloj de:
$$44100 \times 16 \times 2 = 1411200 \thinspace Hz$$
Como se puede ver, si se quiere trabajar con frecuencias de muestreo lo suficientemente altas como para asegurar una mínima calidad de audio, es necesario hardware dedicado: generar esas señales por software es muy ineficiente. En nuestro caso el microcontrolador MK20 de Freescale (ARM Cortex-M4) que viene en la placa Teensy sí que viene equipado con un interface I2S totalmente programable.

El interface I2S en el microcontrolador MK20

El interface I2S tiene dos modos: directo y mediante DMA. En esta primera aproximación he implementado el modo directo (sin DMA). Es el modo que más CPU consume pero también es el más sencillo. Los pasos para configurar la interface de salida I2S en el MK20 son, grosso modo, los siguientes:

1. Configurar el multiplexor de pines para asignar las tres señales a pines reales.

2. Configurar el los divisores de frecuencia para obtener el “bit clock” de I2S a partir del reloj del sistema.

3. Configurar el tamaño de palabra (16 bits estéreo en nuestro caso).

4. Colgar de la IRQ 35 la función encargada de escribir las muestras en el registro de datos I2S.

5. Habilitar la IRQ 35 (vector de interrupción 16 + 35 = 51 del ARM Cortex-M4).

Configurar el multiplexor de pines es muy sencillo. En este caso he optado por usar la configuración “ALT6” para los pines PORTA.12, PORTA.13 y PORTC.3 que les dan la funcionalidad TX, FS (frame select, el equivalente a "word select") y BCLK (bit clock) respectivamente.



Para configurar el BCLK se dispone de un divisor de frecuencia fraccionario y de un divisor de frecuencia entero. Si quisiéramos usar una frecuencia de muestreo de 48KHz haríamos los siguiente:

1. Establecemos como fuente de reloj, el reloj del núcleo (SYSCLK) que, en nuestro caso, va a 96 MHz.

2. El divisor de frecuencia fraccionario lo configuramos con el valor: 16 / 125 (96 * 16 / 125 = 12.288 MHz).

3. El divisor de frecuencia entero lo configuramos a continuación con el valor 8: 12.288 / 8 = 1.536 MHz).

En este caso: 48 KHz * 2 * 16 = 1.536 MHz.

El resto de pasos es mejor verlos en el código:

bool i2sInit() {
	// configure i/o pins
	// (PTA12 = TX, PTA13 = FS, PTC3 = BCLK) --> ALT6
	PORTA_PCR12 = ((uint32_t) 6) << 8;
	PORTA_PCR13 = ((uint32_t) 6) << 8;
	PORTC_PCR3 = ((uint32_t) 6) << 8;
	// enable system clock for i2s module
	SIM_SCGC6 |= ((uint32_t) 1) << 15;
	// select input clock 0 and output enable
	I2S0_MCR = ((uint32_t) 1) << 30;
#if (I2S_SAMPLE_RATE == 48000)
	// divide to get the 12.2880 MHz from 96MHz (96 * (16/125))
	I2S0_MDR = (((uint32_t) 15) << 12) | ((uint32_t) 124);
#elif (I2S_SAMPLE_RATE == 44100)
	// divide to get the 11.2896 MHz from 96MHz (96 * (2/17))
	I2S0_MDR = (((uint32_t) 1) << 12) | ((uint32_t) 16);
#elif (I2S_SAMPLE_RATE == 32050)
	// divide to get the 8.2051 MHz from 96MHz (96 * (10/117))
	I2S0_MDR = (((uint32_t) 9) << 12) | ((uint32_t) 116);
#else
#error "I2S_SAMPLE_RATE must be 48000, 44100 or 32050"
#endif
	// re-enable system clock to the i2s module
	SIM_SCGC6 |= ((uint32_t) 1) << 15;
	// disable tx (TE=0) while configuring
	I2S0_TCSR &= ~(((uint32_t) 1) << 31);
	// transmitter remains enabled until (and TE set) the end of the current frame
	for (int i = 0; (i < 1000) && (I2S0_TCSR & (((uint32_t) 1) << 31)); i++)
		;
	if (I2S0_TCSR & (((uint32_t) 1) << 31))
		return false;
	// no word mask
	I2S0_TMR = 0;
	// set FIFO watermark
	I2S0_TCR1 = ((uint32_t) (I2S_FRAME_SIZE - 1));
	// use asynchronous mode (SYNC=0), BCLK polatiry active low (BCP=0), select master clock 1 (MSEL=1), bit clock divide (DIV=3), BCLK internally generated
	I2S0_TCR2 = (((uint32_t) 1) << 25) | (((uint32_t) 1) << 26) | ((uint32_t) 3) | (((uint32_t) 1) << 24);
	// transmit data channel is enabled (TCE=1)
	I2S0_TCR3 = (((uint32_t) 1) << 16);
	// frame size (FRSZ), bits per frame sync (SYWD), MSB (MF=1), I2S standard (not "left justified") (FSE=1), frame sync in master mode (FSD)
	I2S0_TCR4 = (((uint32_t) (I2S_FRAME_SIZE - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 8) | (((uint32_t) 1) << 4) | (((uint32_t) 1) << 3) | ((uint32_t) 1);
	// bits per word for first word in each frane (W0W), bits per word for rest of words in each frame (WNW), bit index for first bit tx (MSB, 15-th for 16 bit)
	//I2S0_TCR5 = (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 24) | (((uint32_t) 15) << 8);
	I2S0_TCR5 = (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 24) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 8);
	return true;
}


void i2sStart() {
	wavePtr = (int16_t *) &_binary_drum_loop_16_raw_start;
	NVIC_ENABLE_IRQ(IRQ_I2S0_TX);
	// tx enable (TE=1), bit clock enable (BCE=1), FIFO request interrupt enable, FIFO reset
	I2S0_TCSR |= (((uint32_t) 1) << 31) | (((uint32_t) 1) << 28) | (((uint32_t) 1) << 8) | (((uint32_t) 1) << 25);
}


void i2sStop() {
	NVIC_DISABLE_IRQ(IRQ_I2S0_TX);
}

Además de lo dicho, es necesario colgar de la IRQ 35 una rutina que será invocada tantas veces por segundo como indique la frecuencia de muestreo y que será la encargada de escribir en el registro de salida I2S las muestras de audio que se van a emitir por la interface I2S. Definimos la rutina de la siguiente manera dentro del codigo C++:

extern char _binary_drum_loop_16_raw_start;
extern char _binary_drum_loop_16_raw_end;
volatile char *p;


void i2sTx()  __attribute__ ((section(".i2s_tx")));


volatile int16_t *wavePtr;


void i2sTx() {
	// if FRF=0, return
	if (!(I2S0_TCSR & (((uint32_t) 1) << 16)))
		return;
	// write left and right sample
	I2S0_TDR0 = (uint32_t) *wavePtr;
	I2S0_TDR0 = (uint32_t) *wavePtr;
	wavePtr++;
	if (wavePtr >= ((int16_t *) &_binary_drum_loop_16_raw_end))
		wavePtr = (int16_t *) &_binary_drum_loop_16_raw_start;
	// if underrun, clear underrun
	if (I2S0_TCSR & (((uint32_t) 1) << 18))
		I2S0_TCSR |= (((uint32_t) 1) << 18);
	// if frame sync error, clear frame sync error flag
	if (I2S0_TCSR & (((uint32_t) 1) << 19))
		I2S0_TCSR |= (((uint32_t) 1) << 19);
}

Y en el linker script de nuestro proyecto incluimos una seccion especial a la que llamaremos “.cortex_m4_vector_i2s_tx” y que ubicamos en la direccion de memoria 0x000000CC (la correspondiente a la IRQ 35). En esta sección ponemos la dirección de memoria de nuestra rutina de servicio de interrupción (la encargada de escribir las muestras), es decir metemos la dirección de memoria I2S_TX_ADDRESS + 1 (recordar que al tratarse de un Cortex-M, el reportorio de instrucciones es siempre el reportorio “thumb” y, por lo tanto, los destinos de salto para subrutinas y para codigo siempre deben tener su bit 0 a 1).
SECTIONS {
	. = 0x00000000 ;
	.cortex_m4_vectors : {
		LONG(0x20007FFC);
		LONG(0x00000411);
	}
	. = 0x000000CC ;
	.cortex_m4_vector_i2s_tx : {
		LONG(I2S_TX_ADDRESS + 1);
	}
	. = 0x00000400 ;
	.flash_configuration : {
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFE);
	}
	.text : {
		_linker_code = . ;
		init.o (.text)
		*(.text)
		*(.text.*)
		*(.rodata*)
		*(.gnu.linkonce.t*)
		*(.gnu.linkonce.r*)
	}
	I2S_TX_ADDRESS = . ;
	.i2s_tx : {
		*(.i2s_tx)
	}
	.preinit_array : {
		__preinit_array_start = . ;
		*(.preinit_array)
		__preinit_array_end = . ;
	}

	...resto del linker script...

Audio de ejemplo

Se ha partido de un sample de dominio público consistente en dos golpes de bombo y caja con charles en medio, típicos del estilo de música house. La muestra se emite en 16 bits con una frecuencia de muestreo de 32050 Hz (Se ha usado este frecuencia por razones de espacio en la memoria flash: es una frecuencia que permite reproducir a una calidad buena manteniendo un tamaño lo suficientemente limitado como para caber en la memoria flash del microcontrolador).



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

[ añadir comentario ] ( 2212 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 14092 )
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.jsp?topic=/com.arm.doc.dui0553a/CHDBIBGJ.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 ] ( 1885 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 14767 )
Vúmetro LCD 
La posibilidad de redefinir una parte del juego de caracteres en los displays LCD alfanuméricos, en combinación con el uso de una de las entradas analógicas del AVR y un pequeño circuito analógico, nos va a permitir la implementación de un sencillo vúmetro en el Arduino.

Un vúmetro no es más que un medidor gráfico de intensidad de señal de audio. En este caso vamos a abordar una implementación muy sencilla y lineal (no logarítmica).



Entrada analógica

La entrada analógica del microcontrolador AVR en el Arduino permite medir entre 0 y 5 voltios con una precisión de 10 bits, de 0 a 1023. 0 se corresponde con 0 voltios y 1023 se corresponde con 5 voltios. Para obtener la intensidad de señal en una de las entradas analógicas, realizamos los siguientes pasos:

1. Amplificación
2. Detección de envolvente

La amplificación se ha implementado construyendo una sencilla etapa estándar de amplificación basada en un transistor NPN con configuración de emisor común:



La señal de entrada es una señal de audio en alterna (en este caso la he conectado a la salida de auriculares de mi móvil). Dicha señal, al pasar por el circuito de amplificación se invierte en fase (no nos importa) y, lo más importante, se amplifica. La señal de salida es también en alterna (el condensador de salida elimina la componente de continua) y pasa la siguiente parte del circuito: el detector de envolvente.



Un detector de envolvente es un circuito que, a partir de una señal de entrada, determina su envolvente:



(imagen extraida de Wikimedia Commons)

El circuito detector de envolvente es muy sencillo: La señal alterna, al hacerla pasar por un diodo, se rectifica y sólo deja pasar los semiciclos positivos. En cada uno de los semiciclos positivos de la señal se carga el condensador y, durante las pausas entre semiciclos, en ausencia de corriente que pase por el diodo, el condensador se descarga lentamente a través de la resistencia, así hasta el siguiente ciclo, que se repite el proceso. El resultado en la salida es una señal que “sigue” de forma aproximada a los picos de la señal de audio que hay en la entrada.

Hay que elegir correctamente los valores del condensador y la resistencia: Un valor de condensador muy bajo hará que se descargue rápidamente mientras que un valor de condensador muy alto hará que tarde excesivamente en cargarse. En el caso de la resistencia de descarga del condensador un valor muy alto hará que el condensador apenas se descargue en las pausas entre ciclos (lo que puede provocar que los ciclos se “sumen” a medida que llegan) y un valor muy bajo hará que el condensador se descargue muy rápido, perdiendo el efecto de seguimiento de envolvente.

Display LCD

En post anteriores de este blog publiqué varias clases C++ para la gestión de displays LCD. En este caso he reutilizado la clase “Lcd20x4” de proyectos anteriores, redefiniendo de forma estática 5 de los caracteres definibles por el usuario.

Para implementar una barra horizontal hay que tener en cuenta que tenemos 20 columnas y que cada carácter tiene 5 columnas de puntos: En total tenemos 100 pixels en horizontal para un display LCD de 20x4. Si definimos los caracteres de la siguiente forma:
* . . . .
* . . . .
* . . . .
* . . . . --> 1
* . . . .
* . . . .
* . . . .

* * . . .
* * . . .
* * . . .
* * . . . --> 2
* * . . .
* * . . .
* * . . .

...

* * * * *
* * * * *
* * * * *
* * * * * --> 5
* * * * *
* * * * *
* * * * *

Utilizando este método de visualización, la barra LCD podrá adoptar valores entre 0 y 100. El pseudocódigo para visualizar un valor “v” será:

procedimiento mostrar_valor(v)     // 0 <= v <= 100
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
ir_coordenada(0, 0)
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
escribir_carácter(5)
fin para
escribir_carácter(valorCaracterParcial)
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
escribir_carácter(0)
fin para
fin procedimiento

Como se puede ver, para cada valor “v” que se quiera visualizar en la barra, se escribe toda una fila horizontal del display LCD. La visualización puede optimizarse si partimos de la base de que la diferencia entre un valor “v” enviado en un instante “t” y el valor “v” enviado al display en un instante “t + d” no va a variar mucho si “d” es lo suficientemente pequeño. En nuestro caso vamos a hacer un muestreo de la entrada analógica cada 50ms por lo que la “v” no va a variar excesivamente entre un instante de muestreo y el siguiente.

Si asumimos que la “v” no va a variar mucho podemos enviar al LCD sólo los cambios:

barraLogica[20]
barraFisica[20]

procedimiento mostrar_valor(v)
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
j := 0
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
barraLogica[j] := 5
j := j + 1
fin para
barraLogica[j] := valorCaracterParcial
j := j + 1
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
barraLogica[j] := 0
j := j + 1
fin para
fin procedimiento

procedimiento actualiza
dentroCambio := NO
inicioCambio := -1
para j := 0 hasta 19 hacer
si (dentroCambio) entonces
si (barraFisica[j] = barraLogica[j]) entonces
escribir_barra_fisica(inicioCambio, j)
dentroCambio := NO
fin si
en otro caso
si (barraFisica[j] <> barraLogica[j]) entonces
inicioCambio := j
dentroCambio := SI
fin si
fin si
barraFisica[j] := barraLogica[j]
fin para
si (dentroCambio) entonces
escribir_barra_fisica(inicioCambio, 19)
fin si
fin procedimiento

El procedimiento “actualiza” se ejecutará de forma periódica y, como se puede ver, sólo envía al display LCD los caracteres que cambien. Esta forma de refresco del display LCD es más eficiente y nos permite tasas de muestreo mayores.

En el siguiente vídeo puede verse el circuito en acción:



El código en C++ puede descargarse de la sección soft.

[ añadir comentario ] ( 2028 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 15214 )
Minisintetizador basado en Arduino 
Versión iniciar y muy básica de un minisintetizador mononfónico de onda cuadrada con entrada MIDI y basado en Arduino. Por ahora sólo reconoce mensajes MIDI "NOTE ON" y "NOTE OFF".

El procesador del Arduino se encarga simplemente de parsear los mensajes MIDI: Genera los tonos y los silencios ante las tramas NOTE ON y NOTE OFF que detecta por la entrada MIDI.

#define  MIDI_NOTE_LOW   16
#define MIDI_NOTE_HIGH 107

// midi frequencies from C0 to B7
int freq[] = {
21, 22, 23, 24, 26, 28, 29, 31,
33, 35, 37, 39, 41, 44, 46, 48, 52, 55, 58, 62,
65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123,
131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247,
262, 277, 294, 311, 329, 349, 370, 392, 415, 440, 466, 494,
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988,
1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1864, 1976,
2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951
};

#define MIDI_STATUS_WAIT_STATUS 0
#define MIDI_STATUS_WAIT_NOTE 1
#define MIDI_STATUS_WAIT_VELOCITY 2
#define MIDI_STATUS_WAIT_NOTE_OR_STATUS 3

#define SPEAKER_PIN 13

int midiStatus = MIDI_STATUS_WAIT_STATUS;
int midiNote = 0;
int midiVelocity = 0;

void setup() {
Serial1.begin(31250);
}

void parseMidi(int b) {
if (midiStatus == MIDI_STATUS_WAIT_STATUS) {
if ((b & 0xF0) == 0x90)
midiStatus = MIDI_STATUS_WAIT_NOTE;
}
else if (midiStatus == MIDI_STATUS_WAIT_NOTE) {
midiNote = b;
midiStatus = MIDI_STATUS_WAIT_VELOCITY;
}
else if (midiStatus == MIDI_STATUS_WAIT_VELOCITY) {
midiVelocity = b;
midiStatus = MIDI_STATUS_WAIT_STATUS;
if (midiVelocity == 0)
noTone(SPEAKER_PIN);
else {
if ((midiNote >= MIDI_NOTE_LOW) && (midiNote <= MIDI_NOTE_HIGH))
tone(SPEAKER_PIN, freq[midiNote - MIDI_NOTE_LOW]);
}
midiStatus = MIDI_STATUS_WAIT_NOTE_OR_STATUS;
}
else if (midiStatus == MIDI_STATUS_WAIT_NOTE_OR_STATUS) {
if (b < 0x80) {
midiNote = b;
midiStatus = MIDI_STATUS_WAIT_VELOCITY;
}
else if ((b & 0xF0) == 0x90)
midiStatus = MIDI_STATUS_WAIT_NOTE;
else
midiStatus = MIDI_STATUS_WAIT_STATUS;
}
}

void loop() {
while (Serial1.available() > 0) {
int b = Serial1.read();
parseMidi(b);
}
}

Como se puede ver, el parseado de las tramas MIDI se realiza mediante un sencillo autómata finito (DFA) de 4 estados.



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

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Siguiente> >>