
El reproductor propuesto no requiere una tarjeta de memoria; almacena un archivo MIDI de hasta 6,000 bytes de longitud directamente en el microcontrolador ATtiny85 (a diferencia de
este diseño clásico, que reproduce archivos WAV, naturalmente requiere una tarjeta de memoria). La reproducción de cuatro vías con atenuación mediante PWM se implementa en el software. Un ejemplo de sondeo está
aquí .
El dispositivo está hecho de acuerdo con el esquema:

El condensador electrolítico entre el microcontrolador y el cabezal dinámico no perderá el componente constante si aparece una unidad lógica en la salida de PB4 como resultado de una falla del software. La inductancia de la cabeza no pasa la frecuencia PWM. Si decide conectar el dispositivo al amplificador, para evitar sobrecargar el último con una señal PWM, debe agregar un filtro de paso bajo, como
aquí .
El archivo MIDI debe colocarse en la fuente de firmware como una matriz de la forma:
const uint8_t Tune[] PROGMEM = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 };
Hay una solución lista para convertir un archivo a este formato en sistemas operativos tipo UNIX: la utilidad xxd. Tomamos el archivo MIDI y pasamos por esta utilidad de esta manera:
xxd -i musicbox.mid
La consola mostrará algo como:
unsigned char musicbox_mid[] = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 }; unsigned int musicbox_mid_len = 2708;
2708 es la longitud en bytes. Resultó menos de 6000, eso significa que encaja. La secuencia de números hexadecimales a través del portapapeles se transfiere al boceto (solo recuerde:
en la consola, no Ctrl + C ) en lugar de la matriz predeterminada. O no hagas todo esto si deseamos dejarlo.
El contador de temporizador 1 funcionará a una frecuencia de 64 MHz desde el PLL:
PLLCSR = 1<<PCKE | 1<<PLLE;
Transferimos este temporizador al modo PWM para que funcione como DAC; el ciclo de trabajo dependerá del valor de OCR1B:
TIMSK = 0; // Timer interrupts OFF TCCR1 = 1<<CS10; // 1:1 prescale GTCCR = 1<<PWM1B | 2<<COM1B0; // PWM B, clear on match OCR1B = 128; DDRB = 1<<DDB4; // Enable PWM output on pin 4
La frecuencia de los pulsos rectangulares depende del valor de OCR1C, lo dejamos igual a 255 (por defecto), luego la frecuencia de 64 MHz se dividirá por 256, y obtenemos 250 kHz.
El contador 0 del temporizador generará interrupciones:
TCCR0A = 3<<WGM00; // Fast PWM TCCR0B = 1<<WGM02 | 2<<CS00; // 1/8 prescale OCR0A = 19; // Divide by 20 TIMSK = 1<<OCIE0A; // Enable compare match, disable overflow
Una frecuencia de reloj de 16 MHz se divide por un divisor entre 8, y luego por un valor OCR0A de 19 + 1, y se obtienen 100 kHz. El reproductor es de cuatro voces, se obtienen 25 kHz para cada voz. Tras la interrupción, se llama una rutina de procesamiento ISR (TIMER0_COMPA_vect), que calcula y emite sonidos.
El temporizador de vigilancia está configurado para generar una interrupción cada 16 ms, que se requiere para recibir frecuencias de nota:
WDTCR = 1<<WDIE | 0<<WDP0; // Interrupt every 16ms
Para obtener oscilaciones de una forma dada, se utiliza la síntesis digital directa. No hay multiplicación de hardware en ATtiny85, por lo que tomamos pulsos rectangulares y multiplicamos la amplitud de la envolvente por 1 o -1. La amplitud disminuye linealmente, y para calcularla en un momento dado, es suficiente disminuir linealmente la lectura del medidor.
Se proporcionan tres variables para cada canal: Freq [] - frecuencia, Acc [] - batería de fase, Amp [], valor de amplitud de envolvente. Se suman los valores de Freq [] y Acc []. El bit de orden superior Acc [] se utiliza para obtener pulsos rectangulares. Cuanto más Freq [], mayor es la frecuencia. La forma de onda terminada se multiplica por la envolvente Amp []. Los cuatro canales se multiplexan y se alimentan a la salida analógica.
Una parte importante del programa es el procedimiento para procesar la interrupción desde el temporizador-contador 0, que envía las oscilaciones a la salida analógica. Este procedimiento se llama a una frecuencia de aproximadamente 95 kHz. Para el canal actual c, actualiza los valores de Acc [c] y Amp [c], y también calcula el valor de la nota actual. El resultado se envía al registro de comparación OCR1B del contador-temporizador OCR1B para obtener una señal analógica en el pin 4:
ISR(TIMER0_COMPA_vect) { static uint8_t c; signed char Temp, Mask, Env, Note; Acc[c] = Acc[c] + Freq[c]; Amp[c] = Amp[c] - (Amp[c] != 0); Temp = Acc[c] >> 8; Temp = Temp & Temp<<1; Mask = Temp >> 7; Env = Amp[c] >> Volume; Note = (Env ^ Mask) + (Mask & 1); OCR1B = Note + 128; c = (c + 1) & 3; }
Cadena
Acc[c] = Acc[c] + Freq[c];
agrega la frecuencia freq [c] a la batería Acc [c]. Cuanto mayor sea Freq [c], más rápido cambiará el valor de Acc [c]. Luego línea
Amp[c] = Amp[c] - (Amp[c] != 0);
disminuye el valor de amplitud para un canal dado. El fragmento (Amp [c]! = 0) es necesario para que después de que la amplitud llegue a cero no disminuya más. Ahora línea
Temp = Acc[c] >> 8;
transfiere los 9 bits altos de Acc [c] a Temp. Y la linea
Temp = Temp & Temp<<1;
deja el bit de orden superior de esta variable igual a uno si dos bits de orden superior son iguales a uno, y establece el bit de orden superior a cero si no lo es. El resultado son pulsos rectangulares con una relación de encendido / apagado de 25/75. En una de las construcciones anteriores, el autor aplicó un meandro, mientras que con el nuevo método, los armónicos se obtienen un poco más. Cadena
Mask = Temp >> 7;
transfiere los valores de los bits más significativos a los bits restantes del byte, por ejemplo, si el bit más significativo fue 0, se obtendrá 0x00 y si 1 es 0xFF. Cadena
Env = Amp[c] >> Volume;
transfiere el bit Amp [c] especificado por el valor de Volumen a Env, por defecto el mayor, ya que Volumen = 8. Cadena
Note = (Env ^ Mask) + (Mask & 1);
Todo esto une. Si Máscara = 0x00, a Nota se le asigna el valor Env. Si Mask = 0xFF, a Note se le asigna un valor adicional a Env + 1, es decir, Env con un signo menos. Note ahora contiene la forma de onda actual, que cambia de valores positivos a negativos de la amplitud actual. Cadena
OCR1B = Note + 128;
agrega 128 a Note y escribe el resultado en OCR1B. Cadena
c = (c + 1) & 3;
emite cuatro canales de acuerdo con las interrupciones correspondientes, multiplexando las voces en la salida.
Doce frecuencias de nota se dan en la matriz:
unsigned int Scale[] = { 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715};
Las frecuencias de nota de otras octavas se obtienen dividiendo entre 2
n . Por ejemplo, dividimos 10973 por 2
4 y obtenemos 686. El bit superior Acc [c] cambiará con una frecuencia de 25000 / (65536/685) = 261.7 Hz.
Dos variables influyen en el sonido: Volumen - volumen, de 7 a 9 y Decadencia - atenuación, de 12 a 14. Cuanto mayor sea el valor de Decadencia, más lenta será la atenuación.
El intérprete MIDI más simple presta atención solo a los valores de la nota, el tempo y el coeficiente de división, e ignora otros datos. La rutina readIgnore () omite el número especificado de bytes en la matriz recibida del archivo:
void readIgnore (int n) { Ptr = Ptr + n; }
La rutina readNumber () lee un número de un número dado de bytes con una precisión de 4:
unsigned long readNumber (int n) { long result = 0; for (int i=0; i<n; i++) result = (result<<8) + pgm_read_byte(&Tune[Ptr++]); return result; }
La rutina readVariable () lee un número con precisión variable MIDI. El número de bytes en este caso puede ser de uno a cuatro:
unsigned long readVariable () { long result = 0; uint8_t b; do { b = pgm_read_byte(&Tune[Ptr++]); result = (result<<7) + (b & 0x7F); } while (b & 0x80); return result; }
Se toman siete bits de cada byte, y el octavo es igual a uno si necesita leer otro byte más, o cero si no.
El intérprete llama a la rutina noteOn () para reproducir la nota en el siguiente canal disponible:
void noteOn (uint8_t number) { uint8_t octave = number/12; uint8_t note = number%12; unsigned int freq = Scale[note]; uint8_t shift = 9-octave; Freq[Chan] = freq>>shift; Amp[Chan] = 1<<Decay; Chan = (Chan + 1) & 3; }
La variable Ptr indica el siguiente byte a leer:
void playMidiData () { Ptr = 0; // Begin at start of file
El primer bloque en el archivo MIDI es un encabezado que indica el número de pistas, tempo y relación de división:
// Read header chunk unsigned long type = readNumber(4); if (type != MThd) error(1); unsigned long len = readNumber(4); unsigned int format = readNumber(2); unsigned int tracks = readNumber(2); unsigned int division = readNumber(2); // Ticks per beat TempoDivisor = (long)division*16000/Tempo;
El coeficiente de división generalmente es igual a 960. Ahora leemos el número dado de bloques:
// Read track chunks for (int t=0; t<tracks; t++) { type = readNumber(4); if (type != MTrk) error(2); len = readNumber(4); EndBlock = Ptr + len;
Lea eventos secuenciales hasta el final del bloque:
// Parse track while (Ptr < EndBlock) { unsigned long delta = readVariable(); uint8_t event = readNumber(1); uint8_t eventType = event & 0xF0; if (delta > 0) Delay(delta/TempoDivisor);
En cada evento, se especifica delta: el retraso en unidades de tiempo determinado por el coeficiente de división, que debe ocurrir antes de este evento. Para los eventos que deberían ocurrir aquí de, delta es cero.
Los metaeventos son eventos de tipo 0xFF:
// Meta event if (event == 0xFF) { uint8_t mtype = readNumber(1); uint8_t mlen = readNumber(1); // Tempo if (mtype == 0x51) { Tempo = readNumber(mlen); TempoDivisor = (long)division*16000/Tempo; // Ignore other meta events } else readIgnore(mlen);
El único tipo de metaevento que nos interesa es el Tempo, el valor del tempo en microsegundos. Por defecto, son 500,000, es decir, medio segundo, que corresponde a 120 latidos por minuto.
Los eventos restantes son eventos MIDI definidos por el primer dígito hexadecimal de su tipo. Solo estamos interesados en 0x90 - Note On, tocando notas en el siguiente canal disponible:
// Note off - ignored } else if (eventType == 0x80) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); // Note on } else if (eventType == 0x90) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); noteOn(number); // Polyphonic key pressure } else if (eventType == 0xA0) readIgnore(2); // Controller change else if (eventType == 0xB0) readIgnore(2); // Program change else if (eventType == 0xC0) readIgnore(1); // Channel key pressure else if (eventType == 0xD0) readIgnore(1); // Pitch bend else if (eventType == 0xD0) readIgnore(2); else error(3); } } }
Ignoramos el valor de la velocidad, pero si lo desea, puede establecer la amplitud inicial de la nota en él. Nos saltamos el resto de los eventos, su duración puede ser diferente. Si se produce un error en el archivo MIDI, el LED se enciende.
El microcontrolador funciona a una frecuencia de 16 MHz, por lo que no se requiere cuarzo, debe configurar adecuadamente el PLL incorporado. Para que el microcontrolador sea compatible con Arduino, se aplica
esta experiencia de Spence Konde. En el menú Tablero, seleccione el submenú ATtinyCore y, a continuación, ATtiny25 / 45/85. En los siguientes menús, seleccione: Temporizador 1 Reloj: CPU, BOD deshabilitado, ATtiny85, 16 MHz (PLL). Luego seleccione Burn Bootloader, luego complete el programa. El programador se utiliza como la placa de programador Tiny AVR de SpinyFun.
El firmware para CC-BY 4.0, que ya tiene una fuga de Bach en re menor, está
aquí ,
aquí se toma el archivo MIDI original.