Desarrollo de un sintetizador musical simple en ATMEGA8

Hace unos años, hice una alarma en el microcontrolador ATmega8, donde implementé un sintetizador de melodía simple de un solo tono (una sola voz). Hay muchos artículos en Internet para principiantes sobre este tema. Como regla general, se utiliza un temporizador de 16 bits para generar la frecuencia (notas), que se configura de cierta manera, lo que obliga al nivel de hardware a emitir una señal en forma de meandro en un pin específico del MC. El segundo temporizador (8 bits) se usa para implementar la duración de una nota o pausa. Las notas de acuerdo con fórmulas bien conocidas se comparan con las frecuencias y, a su vez, se comparan con ciertos números de 16 bits, inversamente proporcionales a las frecuencias que especifican los períodos de conteo del temporizador.

En mi diseño, proporcioné tres melodías que fueron escritas en la misma clave y escala. Por lo tanto, tuve que usar un número limitado y cierto de notas, lo que facilitó el modelado. Además, las tres canciones se tocaron al mismo ritmo. El código de nota y su código de duración caben fácilmente en un byte. El único inconveniente de este modelo era la falta de versatilidad, la capacidad de editar, reemplazar o complementar rápidamente la melodía. Para grabar una melodía, primero la dibujé en un editor de música en una computadora, luego copié las notas y su duración, con la numeración que decidí de antemano, y luego formé los bytes resultantes. Hice las últimas operaciones usando el programa Excel.

En el futuro, quería eliminar el inconveniente antes mencionado, traicionar el diseño de una cierta universalidad y reducir el tiempo para implementar la melodía. Hubo tal idea que el programa MK leyó los bytes de uno de los formatos de música famosos. El más popular y común es el formato MIDI. Más literalmente, este no es tanto un formato como una "ciencia" completa sobre la que se puede leer en Internet. La especificación MIDI define el protocolo para transmitir mensajes en tiempo real a través de la interfaz física correspondiente y describe cómo se organizan los archivos midi en los que se pueden almacenar estos mensajes. El formato midi está orientado a la música, por lo que encuentra aplicación en el campo relevante. Este es un control sincrónico de equipos de sonido, música en color, sintetizadores musicales y robots, etc. En el ámbito doméstico, el formato midi se encontró en la era del comienzo del desarrollo de los teléfonos móviles. En este caso, los mensajes sobre la inclusión o desactivación de una nota en particular, información sobre un instrumento musical, el volumen de las notas sonoras, etc., se graban en el archivo midi. El teléfono móvil que reproduce dicho archivo contiene un sintetizador que interpreta los mensajes midi en este archivo en tiempo real y reproduce la melodía. En las primeras etapas, los teléfonos solo podían reproducir melodías de un solo tono. Con el tiempo, apareció la llamada polifonía.

En Internet, conocí artículos sobre la implementación de un sintetizador polifónico en MK, que lee archivos midi. En este caso, al menos, se utiliza una "tabla de ondas" preformada (una lista de formas de ondas de sonido) para cada instrumento musical almacenado en la memoria de MK. Y en mi caso particular, nos centraremos en la implementación de un modelo más simple: un sintetizador de un solo tono (una sola voz).

Para empezar, estudié cuidadosamente la estructura del archivo midi y llegué a la conclusión de que, además de la información necesaria sobre las notas, contiene información redundante adicional. Por lo tanto, se decidió escribir un programa simple para convertir un archivo midi a su propio formato. El programa, que trabaja con muchos archivos MIDI, no solo convierte formatos, sino que también los organiza de cierta manera. De antemano, decidí organizar el almacenamiento de muchas melodías en la memoria ROM (EEPROM 24XX512). Para facilitar la visualización en el editor HEX, me aseguré de que cada melodía comience desde el principio del sector. A diferencia de una tarjeta SD (por ejemplo), el concepto de un sector no es aplicable a la ROM utilizada, por lo que me expreso condicionalmente. El tamaño del sector es de 512 bytes. Y el primer sector de ROM está reservado para las direcciones de los sectores de los comienzos de cada melodía. Se supone que la melodía puede tomar varios sectores.

Una descripción completa del formato de archivo midi, por supuesto, no vale la pena hacerlo aquí. Me referiré solo a los puntos más necesarios y necesarios. Un archivo midi contiene 16 canales, que, por regla general, corresponden con mayor frecuencia a uno u otro instrumento musical. En nuestro caso, no importa qué tipo de instrumento sea, y solo se necesita un canal. El contenido de cada canal, junto con el encabezado, se redacta en un archivo midi de acuerdo con un principio que es muy similar a organizar el almacenamiento de transmisiones de video y audio en un contenedor AVI. Escribí sobre esto último anteriormente en uno de mis artículos. El encabezado del archivo midi es un conjunto de algunos parámetros. Uno de esos parámetros es la resolución de tiempo. Se expresa en el número de "ticks" (una especie de píxel) por trimestre (PPQN). Un cuarto es un lapso de tiempo durante el cual se toca un cuarto de nota. Dependiendo del tempo de la melodía, la duración del trimestre puede ser diferente. Por lo tanto, la duración de un "píxel" (período de muestreo) depende del tempo y PPQN. Toda la información sobre la hora de un evento se determina con precisión en esta duración.

Además, el encabezado contiene el tipo de archivo MIDI (tipo 0 o tipo 1) y la cantidad de canales. Sin entrar en detalles, trabajaremos con el tipo 1, el número de canales 2. Un archivo midi con una melodía de tono único, lógicamente, contiene un canal. Pero en el archivo midi "tipo 1" hay, además del principal, otro canal "no musical" en el que se graba información adicional que no contiene notas. Estos son los llamados metadatos. Tampoco hay necesidad de entrar en detalles. La única información que necesitamos allí es que hay información sobre el ritmo, y en un formato inusual: microsegundos por trimestre. En el futuro, se mostrará cómo usar esta información, junto con PPQN, para configurar el temporizador MK, que es responsable del tempo.

En el bloque del canal principal con notas, solo nos interesa la información sobre los eventos de activación y desactivación de notas. Un evento de habilitación de nota tiene dos parámetros: número de nota y volumen. En total, se proporcionan 128 notas y 128 niveles de volumen. Solo nos interesa el primer parámetro, porque no importa cuál sea el volumen de la nota: todas las notas al tocar la melodía MK sonarán al mismo volumen. Y, por supuesto, la melodía no debe contener notas "sobregrabadas", es decir, en cualquier momento, no debe sonar más de una nota al mismo tiempo. El código del evento de tomar (encender) las notas es 0x90. La nota del código de evento es 0x80. Sin embargo, al menos el editor Cakewalk Pro Audio 9 no utiliza el evento con el código 0x80 al exportar la composición a formato midi. En cambio, el evento 0x90 tiene lugar en toda la parte musical, y la nota de que la nota está desactivada es su volumen cero. Es decir, el evento "apagar la nota" es equivalente al evento "encender la nota con volumen cero". Quizás esto se hace por razones de economía. De acuerdo con la especificación, el código de evento no puede reescribirse si este evento se repite. Entre eventos, la información sobre el intervalo de tiempo se registra en un formato de longitud variable. Estos son los valores enteros del número de "ticks" mencionados anteriormente. Muy a menudo, un byte es suficiente para registrar el intervalo de tiempo. Si dos eventos siguen uno tras otro, entonces, entre ellos, el intervalo de tiempo es obviamente igual a cero. Esto, por ejemplo, deshabilita la primera y la inclusión de la segunda nota que le sigue, si no hay pausa (espacio) entre ellas.

Tratemos de escribir una secuencia de notas usando el programa "Cakewalk Pro Audio 9". Hay muchos editores, pero me decidí por el primero que apareció.



Primero debe configurar los ajustes del proyecto. En este editor puede establecer la resolución en el tiempo (PPQN). Elijo el valor mínimo igual a 48. Un valor demasiado grande no tiene sentido, ya que debe trabajar con números grandes que excedan 1 byte de tamaño. Pero el valor mínimo de 48 es bastante satisfactorio. En casi todas las melodías, no se encuentran notas más cortas que 1/32. Y si el número de "ticks" por trimestre es 48, entonces la nota o pausa 1/32 tendrá una duración de 48 / (32/4) = 6 "ticks". Es decir, existe la posibilidad teórica de dividir por completo 1/32 nota por 2, e incluso por 3. Dejamos los parámetros restantes en la ventana de propiedades del proyecto por defecto.



A continuación, abra la propiedad de la primera pista y asígnele un número de canal igual a 1. A su gusto, seleccione un parche que corresponda a un instrumento musical cuando toque una melodía en el editor. El número de parche, por supuesto, no afectará el resultado final.



El tempo de la melodía se establece en la cantidad de trimestres por minuto en la barra de herramientas del editor. El valor de tempo predeterminado es de 100 bpm.

El microcontrolador tiene un temporizador de 8 bits que, como ya se mencionó, se usará para controlar la duración de las notas y pausas. Se decidió que el intervalo de tiempo entre operaciones adyacentes (interrupciones) de dicho temporizador correspondería al intervalo de un "tic". Dependiendo del tempo de la melodía, el valor de este intervalo de tiempo será diferente. Decidí usar interrupciones de temporizador de desbordamiento. Y dependiendo del parámetro de inicialización del temporizador inicial, es posible ajustar este mismo intervalo de tiempo, que depende del tempo de la melodía. Ahora pasemos a los cálculos.

Como regla, en la práctica, en promedio, el tempo de las canciones se encuentra en el rango del orden de 50 a 200. Ya se ha dicho que el tempo en el archivo midi se establece en microsegundos por un cuarto. Para el tempo 50, este valor es 60,000,000 / 50 = 1,200,000, y para el tempo 250 será 240,000. Dado que, según el proyecto, un cuarto contiene 48 ticks, la longitud del tick para el tempo mínimo será 1,200,000 / 48 = 25,000 μs. Y para el ritmo máximo, si calcula de la misma manera, - 5000 μs. Para MK con una frecuencia de cuarzo de 8 MHz y un divisor de temporizador preliminar máximo de 1024, obtenemos lo siguiente. Para el ritmo mínimo, el temporizador debe calcularse 25000 / (1024/8) = 195 veces. El resultado se redondea al valor entero más cercano, el error de redondeo prácticamente no afecta el resultado. Para el ritmo máximo - 5000 / (1024/8) = 39. Aquí, el error de redondeo no afecta aún más, ya que también se obtiene un valor redondeado de 39 para los valores de tempo vecinos de 248 a 253. En consecuencia, el temporizador debe inicializarse con un valor inverso: para el tempo mínimo - (256-195) = 61, y para el máximo - (256 -39) = 217. El ritmo mínimo al que se proporcionará el temporizador en la configuración MK actual es de 39 bpm. Con este valor, el temporizador debe contarse 250 veces. Y con un valor de 38, ya 257, que va más allá de los límites del temporizador. Decidí tomar el valor de 40 lpm para el ritmo mínimo y 240 para el máximo.

Para calcular la cantidad de ticks, se utilizará un temporizador virtual basado en lo anterior. Es el número de tics que establece la duración de una nota o pausa, como ya se mencionó anteriormente.

Para implementar la reproducción de notas, se utiliza un segundo temporizador de 16 bits. Según la especificación MIDI, se proporcionan un total de 128 notas. Pero en la práctica se usan mucho menos. Además, las notas de las octavas más bajas (con frecuencias de aproximadamente 50 Hz) y más altas (con frecuencias de aproximadamente 8 kHz) no serán reproducidas armoniosamente por el microcontrolador. Pero por todo esto, un temporizador de 16 bits con un divisor fijo cubre casi todo el rango de notas proporcionadas por midi, es decir, sin los primeros 35. Pero elegí al principio la nota con el número 37 (su código es 36, ya que la codificación proviene de cero). Esto se hace por conveniencia, ya que este número corresponde a la nota "C", como la primera nota en una escala tradicional. Le corresponde con una frecuencia de 65.4 Hz, y el semiciclo es - 1 / 65.4 / 2 = 0.00764 seg. Este período de tiempo a una frecuencia MK de 8 MHz y un divisor 1 (es decir, sin un divisor) contará el temporizador aproximadamente en su totalidad por 0.00764 / (1/8000000) = 61156 veces. Para la nota 35, si cuenta, este valor será 68645, que está más allá del rango del temporizador de 16 bits. Pero, incluso si era necesario tocar notas por debajo del 36, puede ingresar el primer divisor de temporizador disponible, igual a 8. Pero no hay necesidad práctica de esto, así como tampoco hay ninguna para tocar las notas más altas. Sin embargo, para la nota 128 más alta, nota "G" con una frecuencia de 12,543.85 Hz, el valor del temporizador es, si se cuenta de manera similar, 319. Los detalles de todos los cálculos anteriores están determinados por la configuración específica del modo de temporizador, que se mostrará más adelante.

Ahora tengo una pregunta no menos importante: ¿cómo obtener la relación entre el número de nota y el código del temporizador? Existe una fórmula bien conocida para calcular la frecuencia de una nota por su número. Y el código del temporizador para una frecuencia conocida se calcula fácilmente, como se muestra arriba en los ejemplos. Pero la raíz del grado 12 aparece en la fórmula para la dependencia de la frecuencia de la nota, y en general, no quisiera cargar el controlador con tales procedimientos computacionales. Por otro lado, crear una matriz de códigos de temporizador para todas las notas tampoco es racional. Y decidí hacer lo siguiente, eligiendo un término medio. Es suficiente crear una matriz de códigos de temporizador para las primeras 12 notas, que son una octava. Y las notas de las siguientes octavas deben obtenerse multiplicando secuencialmente las frecuencias de las notas de la primera octava por 2. O, lo mismo, dividiendo secuencialmente los valores de los códigos del temporizador por 2. Otra conveniencia es que el número de octava, por coincidencia, es un argumento en la operación de desplazamiento bit a la derecha ( »), Que se utilizará como la operación de dividir por potencias de dos. Elegí este operador no por casualidad, ya que su argumento refleja el exponente de la potencia del divisor (el número de divisiones por 2). Y este es el número de octava. Para mi conjunto de notas, está involucrado un total de 8 octavas (la última octava está incompleta). Una nota en un archivo midi está codificada con un byte, más precisamente, 7 bits. Para tocar notas en MK, de acuerdo con la idea anterior, primero debe calcular el número de octava y el número de nota en la octava usando el código de nota. Esta operación se realiza en la etapa de convertir el archivo midi a un formato simplificado. Se pueden codificar ocho octavas en tres bits, y 12 notas en una octava se pueden codificar en cuatro. En total, resulta que la nota está codificada en los mismos siete bits que en el archivo midi, pero solo en una representación diferente conveniente para MK. Debido al hecho de que 16 bits pueden codificarse con 4 bits, y las notas en una octava de 12, hay bytes no utilizados.

El último octavo bit se puede usar como marcador para habilitar o deshabilitar las notas. En el caso de MK, debido a la unanimidad de la melodía, la información sobre la nota silenciada será redundante. Con un cambio directo de nota en la melodía, no hay un "encendido-encendido-encendido", sino un "cambio" de la nota. Y en el caso de una pausa, "el silencio está activado", para lo cual puede seleccionar un byte especial del conjunto de bytes no utilizados, y no utilizar la información sobre cómo desactivar la nota. Tal idea es buena porque ahorra el tamaño de la melodía resultante después de la conversión, pero generalmente complica el modelo. No seguí esta idea, ya que hay mucha memoria ya.

La información sobre las notas de la melodía en el archivo midi se almacena en el bloque del canal correspondiente en la vista "intervalo-evento-intervalo-evento ...". En el formato convertido, se aplica exactamente el mismo principio. Para grabar un evento (activar o desactivar una nota), como se mencionó anteriormente, se utiliza un byte. El primer bit (el bit más significativo 7) codifica el tipo de evento. El valor "1" es la nota activada, y el valor "0" es la nota desactivada. Los siguientes tres bits codifican el número de octava, y los cuatro bits más bajos codifican el número de nota en la octava. Un byte también se usa para registrar el intervalo de tiempo. En el formato midi original, se utiliza un formato de longitud variable para esto. Su pequeño inconveniente es que solo 7 bits codifican el intervalo de tiempo (el número de "ticks"), y el octavo bit es un signo de continuación. Es decir, con un byte, de hecho, puede codificar un intervalo de hasta 128 tics. Pero dado que los intervalos de tiempo entre eventos en melodías reales y simples a veces exceden 128, pero casi nunca exceden 256, abandoné el formato de longitud variable y lo manejé con un byte. Codifica un intervalo de tiempo de hasta 256 ticks. Como el proyecto usa 48 ticks por trimestre, o 48 * 4 = 192 ticks por ciclo, se puede usar un byte para codificar un intervalo de 256/192 = 1 duración. (3) (un entero y un tercio) ciclos, que Bastante

En el formato nativo al que se convierte el archivo midi, también apliqué un encabezado pequeño, de 16 bytes de tamaño. Los primeros 14 bytes contienen el nombre de la melodía. Naturalmente, el nombre no debe exceder los 14 caracteres. Luego viene un espacio cero. El siguiente último byte refleja el tempo de la melodía en una vista conveniente para MK. Este valor se calcula en la etapa de conversión y sirve para inicializar el temporizador MK, que es responsable del ritmo. Cómo se calcula se discute en algunos párrafos anteriores.

A partir del byte 17, siguen los contenidos de la melodía. Cada byte impar corresponde a un intervalo de tiempo, y cada byte par corresponde a un evento (nota).El primer byte será cero si la melodía comienza con una nota, desde el comienzo del archivo midi, sin una pausa preliminar. Una señal del final de la melodía es una etiqueta de dos bytes 0xFF. La tarea implica la reproducción cíclica de una melodía por un microcontrolador. Para que la melodía en el bucle suene armoniosa desde el punto de vista del ritmo, debe estar colocada correctamente. Para hacer esto, si es necesario, después de una última nota, debe pausar una cierta longitud, generalmente hasta que se complete la última medida. Y para esto necesitas desviar el evento correspondiente. Usé el byte 0x0F, que no se usa para codificar notas. Corresponde a deshabilitar la nota 16 en la primera octava, lo cual es absurdo, ya que solo hay 12 notas en la octava. Mencionamos anteriormente sobre los bytes no utilizados. Por lo tanto, este byte codifica una "nota silenciosa",la parte alta de la cual también puede servir como un signo de encendido o apagado, a pesar de la redundancia de información en este caso. Para configurar esta nota en el editor midi, tomé la primera o segunda nota (cualquiera de ellas). Permítame recordarle que las primeras 36 notas no se usan en el modelo. Por lo tanto, la primera (o segunda) nota se usa según sea necesario para completar correctamente la melodía, de modo que el ritmo no se rompa cuando se toca en un bucle.

Continuando trabajando en el editor de "Cakewalk Pro Audio 9", compondremos una melodía arbitraria. Las siguientes figuras muestran las notas de la melodía que reescribí de una de las imágenes en Internet. Las imágenes de las notas se presentan en dos estilos: en el estilo de "Piano roll" y en el estilo clásico. El primero es muy conveniente para escribir y editar melodías usando un mouse de computadora. Eso es lo que yo uso.





Como puede ver en la figura, al final se aplica la nota más baja (primera) para el signo de silencio en el intervalo de tiempo correcto para organizar correctamente el patrón cíclico. Y al comienzo de la melodía, en vista de la presencia de un toque, hay un cuarto de sangría antes de la primera nota.

El editor proporciona un modo para mostrar eventos en forma de tabla.



Como puede ver en la figura, no hay nada superfluo en la lista de eventos, excepto tomar notas, como a veces sucede con manipulaciones innecesarias en un proyecto musical. Sin embargo, si por alguna razón se incluyen en la lista eventos innecesarios que no están relacionados con las notas, se pueden eliminar presionando la tecla Supr. Aunque, en la etapa de conversión, todos los eventos innecesarios se ignoran y el tiempo delta "se acumula". Por cierto, agregué esta función al programa en la etapa de depuración. Como puede suponer, la tabla refleja el tiempo y la duración de cada nota junto con otras propiedades que no necesitamos. Es decir, con una línea en la tabla, dos eventos midi se expresan a la vez: activar y desactivar notas.

Guarde la melodía en el formato "midi 1", como se muestra en la figura.



Abra el archivo guardado en el editor HEX. Cabe señalar de inmediato que, a diferencia de los mismos archivos avi (como escribí anteriormente), los bytes de valores numéricos en el archivo midi se presentan no en orden inverso, sino por antigüedad (big endian).



En la figura, marqué con marcadores solo los bytes deseados. Primero, un marco rojo en negrita describe tres grupos de dos bytes en cada uno. Este es, respectivamente, el tipo de formato MIDI (1), el número de canales (2) y el número de ticks por trimestre (48). Son estos valores los que estas tres constantes deben tener para el trabajo posterior del programa de transformación. Los arcos morados marcan el comienzo de cada uno de los dos canales. En el primer canal, 6 bytes están marcados con un marco gris, dentro del cual se resaltan tres bytes con un marco azul. Estos 6 bytes se refieren a un metaevento (marcador de marcador 0xFF) con un código de 0x51 y una longitud de contenido de 0x03 bytes. Tres bytes más: el contenido del evento. Este evento establece el tempo de la melodía con solo estos tres bytes en un marco azul. El último byte bajo se puede descartar de forma segura, porque la súper precisión no es importante. No daré una descripción detallada y exhaustiva de todos los bytes en el archivo.En la segunda pista, en la pista con notas, los valores de los intervalos de tiempo se encierran en un círculo azul. Ellos, por cierto, en este ejemplo particular, no excedieron un byte, excepto por el único caso con la penúltima nota. Es la penúltima nota de la melodía (contando la pseudo nota adicional del final) que dura tres cuartos de una medida, que es 48 * 3 = 144 tics y excede 128. Y para eso debes usar dos bytes, de acuerdo con el formato de longitud variable. Y para representar el intervalo de tiempo en el formato convertido, el valor 144 se codifica fácilmente con un byte. Rodeé este caso especial en un marco azul doble. Las notas están encerradas en un marco verde, o más bien, sus códigos. El volumen de cada nota se encierra en un círculo en un marco gris. Como ya se mencionó, un volumen cero es un signo de silencio (lanzamiento) de la nota, y en toda la composición hay un evento:Encendiendo las notas. El código para este evento, 0x90, está marcado en amarillo. No describí todas las notas hasta el final de la melodía. La única excepción es el doble marco azul para un único intervalo de tiempo que excede el umbral de 128 tics.

Nuevamente, como se mencionó anteriormente, el programa para convertir un archivo midi a su propio formato para MK en realidad funciona con un grupo de varios archivos midi y crea un archivo de imagen para EEPROM en la salida. Considere un fragmento de este archivo que se relaciona con el contenido de la melodía convertida del ejemplo anterior. Lo abrí en otro editor HEX para mostrar la imagen por sectores y prestarle atención. Cada nueva melodía comienza con un nuevo sector.



El último byte de la primera línea (los primeros 16 bytes), encerrado en un cuadro rojo, establece el tempo de la melodía. Según los cálculos, el valor 0xC1 (193) cae en el tempo 154, 155 y 156. Justo en el proyecto configuré el tempo de la melodía en 155 bpm, que se vio en una de las capturas de pantalla anteriores. Los primeros bytes (hasta el 14) encerrados en un marco azul determinan el nombre de la composición. En este ejemplo, "Clásico". Para MK, esta información es innecesaria, solo se necesita para orientarse en el editor HEX. Sin embargo, si realiza un proyecto más complejo en el MK usando la pantalla, puede usar esta información mostrando el nombre de la melodía reproducida.

La segunda línea (desde el byte 17) comienza el contenido de la melodía. Al igual que con el archivo midi original, no pinté todas las notas, sino que pinté solo una parte. Los bytes impares resaltados en azul son intervalos de tiempo. Incluso los bytes marcados con un marco verde son notas junto con signos de su activación / desactivación. Por ejemplo, los primeros dos bytes verdes, 0xB4 y 0x34, se refieren a la misma nota con el código 0x34, y los bytes difieren en un solo bit de orden superior. En el byte 0xB4 (0b10110100), el bit alto es uno, lo cual es un signo de activar una nota, y en el byte 0x34 (0b00110100), el bit alto es cero, lo cual es un signo de desactivar una nota. El byte 0x34 codificó una nota con los siguientes parámetros: código de octava 0b011, y el código de nota en una octava - 0b0100. O, en forma decimal, 3 y 4, respectivamente. Si no cuentas desde cero,Resulta que la primera nota en la melodía pertenece a la cuarta octava y es la quinta en ella. La numeración de octavas aquí se elige arbitrariamente sin tener en cuenta la numeración estándar. La nota acordada, según mi tabla auxiliar de cálculo Excel, es la nota con el código 76 (0x4C) para el formato midi, es decir, la nota E6 (nota "e" de la sexta octava media). Así es: la composición comienza con esta nota.

Debe notarse un caso especial en la secuencia musical, cuando la misma nota se repite sin pausa. En nuestro ejemplo, todas las notas adyacentes que están libres de pausa son diferentes. Pero hay melodías donde la nota se repite sin pausa. Es decir, el intervalo de tiempo entre apagar uno y encender la siguiente nota exacta es cero. En vista de la peculiaridad de una síntesis compleja de música, dicha secuencia sonará familiar en cualquier sintetizador. Pero en el caso de MK, sonará tan cohesivo que será difícil escuchar la diferencia entre dos notas idénticas. En la práctica, por supuesto, no habrá una fusión clara debido a los cálculos intermedios que ocurren en el MC, pero aún así, es muy probable que este intervalo de tiempo sea mucho menor que la duración de incluso un tic. Para tales casos especiales, el programa se encuentra en la fase de conversión,tropezar con tal combinación, introduce una pausa entre notas de 1 tick de longitud y reduce la duración de una nota a la izquierda de la nota en el mismo intervalo de tiempo. Un "espacio" mínimo de 1 tic es suficiente, como lo ha demostrado la práctica.

En un marco azul doble, marqué con un círculo ese valor del intervalo de tiempo (0x90), que excede 128, y para el cual tuve que gastar dos bytes en el archivo midi, de acuerdo con el formato de longitud variable. Los círculos verdes son bytes encerrados dentro y fuera de la misma pseudo nota para alinear la composición. El programa MK, al ver estos bytes, los interpretará como activando el silencio. Finalmente, dos bytes 0xFF encerrados en un marco azul en negrita marcan el final de la melodía. Los valores de todos los siguientes bytes dentro del sector de memoria actual pueden ser cualquiera, se ignoran.

Considere el primer sector del archivo de imagen EEPROM de salida. Como ya escribí, sirve como una lista de direcciones de sectores del comienzo de las melodías. El programa escaneó con éxito 8 melodías sin errores (al momento de escribir, había grabado 8 melodías). El valor de la cantidad de melodías se registra en el último byte 512 del sector. Y desde el comienzo del sector, se escriben las direcciones. Para la primera melodía, la dirección es 0x01, que corresponde al segundo sector (el primero, si cuenta desde cero). Las melodías tercera y cuarta (dos de ocho) resultaron ser largas y no encajaban en un sector. Por lo tanto, se observan lagunas en la secuencia de direcciones. Si cuenta, 64kB de memoria, puede grabar no más de 127 melodías, por lo que un sector para el direccionamiento es suficiente.



Todas las estimaciones preliminares y cálculos reflejados en el artículo, lo realicé en Excel. Las capturas de pantalla a continuación muestran capturas de pantalla de las tablas resultantes (en modo de doble ventana).





A quién le importa, debajo del spoiler se encuentra el texto de un programa en C que convierte archivos midi en un archivo para el microcontrolador. Del texto, eliminé las líneas adicionales que se usaron para la depuración. El programa, hasta ahora, está funcionando, no pretende ser legible y alfabetizado al escribir código.

Archivo principal 1.cpp
#include <stdio.h> #include <windows.h> #include <string.h> #define SPACE 1 HANDLE openInputFile(const char * filename) { return CreateFile ( filename, // Open Two.txt. GENERIC_READ, // Open for writing 0, // Do not share NULL, // No security OPEN_ALWAYS, // Open or create FILE_ATTRIBUTE_NORMAL, // Normal file NULL); // No template file } HANDLE openOutputFile(const char * filename) { return CreateFile ( filename, // Open Two.txt. GENERIC_WRITE, // Open for writing 0, // Do not share NULL, // No security OPEN_ALWAYS, // Open or create FILE_ATTRIBUTE_NORMAL, // Normal file NULL); // No template file } void filepos(HANDLE f, unsigned int p){ LONG LPos; LPos = p; SetFilePointer (f, LPos, NULL, FILE_BEGIN); //FILE_CURRENT //https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-setfilepointer } DWORD wr; DWORD ww; unsigned long int read32(HANDLE f){ unsigned char b3,b2,b1,b0; ReadFile(f, &b3, 1, &wr, NULL); ReadFile(f, &b2, 1, &wr, NULL); ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b3<<24|b2<<16|b1<<8|b0; } unsigned long int read24(HANDLE f){ unsigned char b2,b1,b0; ReadFile(f, &b2, 1, &wr, NULL); ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b2<<16|b1<<8|b0; } unsigned int read16(HANDLE f){ unsigned char b1,b0; ReadFile(f, &b1, 1, &wr, NULL); ReadFile(f, &b0, 1, &wr, NULL); return b1<<8|b0; } unsigned char read8(HANDLE f){ unsigned char b0; ReadFile(f, &b0, 1, &wr, NULL); return b0; } void message(unsigned char e){ printf("Error %d: ",e); switch(e){ case 1: // -   -; printf("In track0 event is not FF\n"); break; case 2: // -  127 printf("Len of FF >127\n"); break; case 3: //  ; printf("Midi is incorrect\n"); break; case 4: //   ; printf("Delta>255\n"); break; case 5: //    RPN  NRPN; printf("RPN or NRPN is detected\n"); break; case 6: //   ; printf("Note in 1...35 range\n"); break; case 7: //    ; printf("Long of name of midi file >18\n"); break; } system("PAUSE"); } int main(){ HANDLE in; HANDLE out; unsigned int i,j; unsigned int inpos; unsigned int outpos=0; unsigned char byte; // ; unsigned char byte1; //  1  ; unsigned char byte2; //  2  ; unsigned char status; //- ( ); unsigned char sz0; // -; unsigned long int bsz0; //    -; unsigned short int format, ntrks, ppqn; //  ; unsigned long int bsz1; //    ; unsigned long int bpm; // ( .  ); unsigned long int time=0; //    ( ); unsigned char scale; //    ,  ; unsigned char oct; //    ; unsigned char nt; // ; unsigned char outnote; //      ; unsigned char prnote=0; //  ; unsigned char tdt; // ()   ; unsigned int dt; //    ( ); unsigned int outdelta=0; //    ( ); unsigned char prdelta=0; //  ; char fullname[30]; //    ; char name[16]; // ; WIN32_FIND_DATA fld; //   mid; HANDLE hf; unsigned short int csz; //  ; unsigned char nfile=0; // ; unsigned char adr[128]; //    ; out=openOutputFile("IMAGE.out"); outpos=512; //   ; filepos(out,outpos); hf=FindFirstFile(".\\midi\\*.mid",&fld); do{ printf("\n***** %s *****\n",fld.cFileName); if(strlen(fld.cFileName)>18){ //   ; message(7); } sprintf(name,"%s",fld.cFileName); name[strlen(fld.cFileName)-4]=0; // ; sprintf(fullname,".\\midi\\%s",fld.cFileName); //    ; WriteFile(out, name, strlen(name), &ww, NULL); //    ; in=openInputFile(fullname); //    ; #include "process.cpp" //     ; outpos+=((csz/512)+1)*512; //    ; adr[nfile]=(outpos/512)-((csz/512)+1); //  ()   ; filepos(out,outpos); CloseHandle(in); nfile+=1; }while(FindNextFile(hf,&fld)); //   ,    ; FindClose(hf); WriteFile(out, &outnote, 1, &ww, NULL); outpos=0; //   ; filepos(out,outpos); WriteFile(out, adr, nfile, &ww, NULL); outpos=511; //  ; filepos(out,outpos); WriteFile(out, &nfile, 1, &ww, NULL); CloseHandle(out); system("PAUSE"); return 0; } 


Archivo adjunto Process.cpp
 time=0; inpos=8; //  ; filepos(in,inpos); format=read16(in); ntrks=read16(in); ppqn=read16(in); if(format!=1 || ntrks!=2 || ppqn!=48){ message(3); } inpos+=10; filepos(in,inpos); //    -; bsz0=read32(in); inpos+=4; while(inpos<22+bsz0){ //      ; tdt=read8(in); inpos+=1; //   ; dt=(unsigned int)(tdt&0x7F); while(tdt&0x80){ tdt=read8(in); inpos+=1; dt=(dt<<7)|(tdt&0x7F); } byte=read8(in); inpos+=1; if(byte==0xFF){ //  ,  -    -; byte=read8(in); //  -; sz0=read8(in); //  , ,     127 ( ); if(sz0&0x80){ message(2); } inpos+=2; switch(byte){ case 0x51: //   "Set Tempo"; bpm=read24(in); scale=256-(bpm/(ppqn*128)); printf("scale=%d\n",scale); filepos(out,outpos+15); // ; WriteFile(out, &scale, 1, &ww, NULL); csz=16; break; default: break; } inpos+=sz0; filepos(in,inpos); // ,     0x51; }else{ message(1); } } //    ; outdelta=0; inpos+=4; filepos(in,inpos); bsz1=read32(in); inpos+=4; while(inpos<30+bsz0+bsz1){ tdt=read8(in); inpos+=1; //   ; dt=(unsigned int)(tdt&0x7F); while(tdt&0x80){ tdt=read8(in); inpos+=1; dt=(dt<<7)|(tdt&0x7F); } outdelta+=dt; //  ; // ,      , ; time+=dt; //  ; byte=read8(in); //    ,  ; inpos+=1; if(byte&0x80){ //  ; status=byte; // ; if(byte==0xFF){ //   -; byte=read8(in); //    ,    ; sz0=read8(in); inpos+=(2+sz0); filepos(in,inpos); }else{ //    ; byte1=read8(in); inpos+=1; } }else{ //    ,        ; byte1=byte; } switch(status&0xF0){ // ,      ; case 0xF0: //   ,  -; break; case 0x80: // ; byte2=read8(in); //     ( ); inpos+=1; //     ,    ; if(byte1>1&&byte1<36){ //         ; message(6); } if(byte1>1){ // ; oct=((byte1-36)/12); //  ; nt=(byte1-36)%12; //    ; }else{ //   ; oct=0; nt=15; } outnote=(oct<<4)|nt; //  ; prnote=outnote; prdelta=outdelta; if(outdelta>255){ //     255 (  ); message(4); } WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; outdelta=0; //  ; break; case 0x90: //   ; byte2=read8(in); //    ( ); inpos+=1; //     ,    ; if(byte1>1&&byte1<36){ //         ; message(6); } if(byte1>1){ // ; oct=((byte1-36)/12); //  ; nt=(byte1-36)%12; //    ; }else{ //   ; oct=0; nt=15; } if(byte2){ //  ,   ; outnote=0x80|(oct<<4)|nt; //  = 1; //   ; if(!outdelta && (outnote&0x7F)==prnote){ //     ; prdelta-=SPACE; // -; filepos(out,outpos+csz-2); //    ; WriteFile(out, &prdelta, 1, &ww, NULL); // ; filepos(out,outpos+csz); outdelta=SPACE; //  -  ; } }else{ //  ,    ; outnote=(oct<<4)|nt; prnote=outnote; //  ; prdelta=outdelta; //  -; } if(outdelta>255){ //   -    ; message(4); } WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; outdelta=0; // -   ; break; //   () ; case 0xA0: // ; byte2=read8(in); inpos+=1; break; case 0xB0: //   ; if(byte1>=98&&byte1>=101){ //     NRPN  RPN; message(5); //  ; } byte2=read8(in); inpos+=1; break; case 0xC0: //  (.  ); // , ,    ; break; case 0xD0: //; break; case 0xE0: // ; byte2=read8(in); inpos+=1; break; default: //  (   ); break; } } //     0xFFFF,    ; outdelta=255; outnote=255; WriteFile(out, &outdelta, 1, &ww, NULL); WriteFile(out, &outnote, 1, &ww, NULL); csz+=2; //   ,     ; printf("Length: %i (%i:%02i)\n",time,time/192,time%192); 


La parte básica del programa para MK, de hecho, es muy simple. Considere una de las opciones para su implementación, más precisamente, su parte principal.

El temporizador 1, utilizado para generar el sonido de las notas, se configura de la siguiente manera. Para habilitar y deshabilitar las notas, se utilizan las siguientes sustituciones, respectivamente.

 #define ENT1 TCCR1B=0x09;TCCR1A=0x40 #define DIST1 TCCR1B=0x00;TCCR1A=0x00;PORTB.1=0 

Antes de iniciar el temporizador, debe asignar al registro OCR1A un valor de 16 bits que corresponderá a la frecuencia que se está reproduciendo. Esto se mostrará más adelante. Cuando se enciende el temporizador, al registro TCCR1B se le asigna el modo de generación de forma de onda con un divisor de temporizador de 1, y el registro TCCR1A se establece en Toggle OC1A en Comparar coincidencia. En este caso, la señal se elimina de la salida especialmente designada de MK "OC1A". En el ATmega8 en el paquete SMD, este es el pin 13, que es lo mismo que PORTB.1. Cuando se apaga el temporizador, ambos registros se reinician y la salida de PORTB.1 se fuerza a cero. Esto es necesario para evitar, durante el silencio, la salida de un voltaje constante, que sería indeseable para la entrada del VLF. Aunque, puede poner un condensador en el circuito, pero también puede desactivar la salida mediante programación. Puede producirse un voltaje constante en esta salida si la nota se apaga en el momento de la fase correspondiente de la señal, y esto es en el 50% de los casos.

Cree una matriz de valores de temporizador para 12 notas de la primera octava. Estos valores se calcularon por adelantado.

 freq[]={61156,57724,54484,51426,48540,45815,43244,40817,38526,36364,34323,32396}; 

Las notas de otras octavas, como dije, se obtendrán dividiendo por grados dos.

La configuración del temporizador 0 es aún más simple. Funciona constantemente, con una interrupción por desbordamiento, cada vez que se inicializa de nuevo con el valor que corresponde al tempo de la melodía. El divisor del temporizador es 5: TCCR0 = 0x05. Basado en este temporizador, se crea un temporizador virtual que cuenta los tics (veces) en la melodía. El procesamiento de la respuesta de este temporizador se coloca en el ciclo principal del programa.

La función de interrupción del temporizador 0 es la siguiente.

 interrupt [TIM0_OVF] void timer0_ovf_isr(void){ if(ent01){ vt01+=1; } TCNT0=top0; } 

Aquí la variable ent01 es responsable de activar el temporizador virtual. Mediante esta variable, se puede activar o desactivar si es necesario. La variable vt01 es la variable primaria contable del temporizador virtual. La línea TCNT0 = top0 indica la inicialización del temporizador 0 al valor deseado top0, que se lee del título de la melodía antes de reproducirla.

El número de la melodía a tocar corresponde a la variable alm. También sirve como la bandera del comienzo de la reproducción. Ella necesita asignar un número de melodía en una de las formas, dependiendo de la tarea. Después de eso, el próximo bloque del ciclo principal se activará.

 if(alm){ //     ; adr=eepr(alm-1)<<9; //     (<<9    512); adr+=15; //   ,      ; top0=eepr(adr); //  ; adr+=1; //     ; adr0=adr; //      (  ); top01=eepr(adr); //      " "  ; adr+=1; //   ; note=eepr(adr); // ; adr+=1; //    -; vt01=0; //    ; ent01=1; //  ; TCNT0=0; //  ; alm=0; //        ,   ; } 

El cambio adicional de nota a nota se lleva a cabo en la unidad de procesamiento del temporizador virtual, que también se coloca en el bucle principal.

 if(vt01>=top01){ //   ,    ; vt01=0; //  ; if(note&0x80){ //     ""; nt=note&15; //    ; oct=(note&0x7F)>>4; //  ; if(nt!=15){ //       15,   ; OCR1A=freq[nt]>>oct; //     ; //         ; ENT1; // ; }else{ //  " "   ; DIST1; // ; } }else{ //     ""; DIST1; // ; } top01=eepr(adr); //      " "; adr+=1; //   ; note=eepr(adr); //   ; adr+=1; // ; if(note==255 && top01==255){ //      ; top01=eepr(adr0); //   ,   ; note=eepr(adr0+1); //   ; adr=adr0+2; //   ; } } 

De los comentarios en el texto del programa, todo debería ser bastante claro y comprensible.

Para detener la melodía, use la siguiente inserción del bucle principal.

 if(stop){ //  ; DIST1; //  ; ent01=0; //  ; vt01=0; //  ; } 

Hay un pequeño comentario sobre la implementación de la reproducción de la melodía. Antes de que cada nueva nota comience a sonar, el microcontrolador pasa una pequeña cantidad de tiempo convirtiendo el byte de lectura de la nota en un valor de temporizador. Esta vez, como resultó en la práctica, es relativamente pequeño y no afecta la calidad de la reproducción. Pero tenía dudas de que esta operación seguiría siendo invisible. En este caso, aparecerían pausas adicionales antes de cada nota, y el ritmo de la melodía se rompería. Pero este problema también es solucionable. Es suficiente calcular los valores del temporizador de la siguiente nota por adelantado mientras suena la nota actual. Este procedimiento debe realizarse por separado del procesamiento del temporizador virtual en el bucle principal del programa utilizando una bandera especialmente designada. Debido al hecho de que es poco probable que el tiempo de cálculo exceda el tiempo de reproducción de la nota más corta, tal solución es apropiada.

Ahora pasemos a probar el programa.

Además de los fragmentos de código anteriores, agregué funciones de procesamiento de botones al programa MK, con el cual controlo la inclusión o desactivación de una melodía particular. EEPROM está conectado a MK a través del bus I2C, cuyo trabajo se implementa a nivel de software. El proyecto se realizó con la ayuda de "CodeVisionAVR" junto con "CodeWizardAVR". Saco MK desde el pin 13 a la tarjeta de sonido de la PC a través del divisor y grabo el sonido de la melodía en el editor de sonido. Actualicé la memoria EEPROM con la ayuda del firmware, sobre el que escribí en uno de los artículos anteriores. Debido al hecho de que no todos los bytes del archivo de imagen son útiles, el firmware de la memoria solo se puede implementar en bytes útiles (para los marcadores finales de las melodías) para ahorrar tiempo de grabación y recursos de chip. Para hacer esto, puede hacer un programa separado o escribir bytes en el chip directamente durante la conversión, agregando al programa principal.

Entre las ocho melodías, hay tres de prueba, con la ayuda de las cuales evaluaré el rango de frecuencia por oído, el sonido de fusionar notas idénticas, el sonido de las notas más cortas, transiciones rápidas, etc. Permítame recordarle que fusionar las mismas notas realmente suena con una pausa de un tic, y la primera nota en la fusión dura un tic menos.

Una de las melodías de prueba es una secuencia de notas de la primera a la última con una duración de una nota en un cuarto y un tempo de melodía de 40 lpm.



En este escenario, una nota suena un poco más de un segundo y, por lo tanto, puede escuchar en detalle cómo suena todo el rango de notas. En el espectro de frecuencia en el editor de audio "Adobe Audition", se observan los principales componentes de frecuencia y sus armónicos superiores debido a la forma de onda de diente de sierra correspondiente. Y la relación logarítmica entre el número de nota y la frecuencia es sorprendente.



Analizando los intervalos de tiempo, se ve claramente que la pausa real entre notas consecutivas promedia aproximadamente 145 muestras (a una frecuencia de muestreo de la grabación de audio 44100 Hz), que es de aproximadamente 3 ms. Este es el tiempo durante el cual el MK realiza los cálculos necesarios. Estos insertos están presentes regularmente antes de cada nota. Escribí específicamente el significado en las muestras, ya que esta información es más original y más precisa, aunque esto no es muy importante.



Y la duración de una marca a un ritmo promedio de la melodía de 120 lpm es de aproximadamente 10 ms. De ello se deduce que, en principio, sería posible no introducir la misma corrección en 1 tic, cuando dos notas idénticas van una tras otra sin pausa. Creo que la inserción regular de 3 ms entre notas sería suficiente. Al escuchar una melodía, estas inserciones regulares no se notan en absoluto y las melodías suenan de manera uniforme. Por lo tanto, no hay necesidad particular de calcular el valor del temporizador para la siguiente nota mientras se reproduce la nota actual.

Otra melodía de prueba con un tempo de 200 lpm contiene sucesivamente las mismas notas de 1/32 del rango medio sin pausa. En este caso, después del procesamiento, cuando se reproduce entre ellos, hay una pausa de 1 tic, que a este ritmo rápido de 310 muestras (aproximadamente 6 ms) de la señal grabada.



La duración de esta pausa, por cierto, es comparable al período de la señal, lo que indica un tempo alto de la melodía. Y su sonido recuerda un trino.

En principio, esto se puede terminar. Estaba satisfecho con el resultado del dispositivo, superó todas las expectativas. La mayor parte del tiempo me dediqué a estudiar el formato midi y depurar el programa para la conversión. Uno de los siguientes artículos también lo dedicaré a un tema relacionado con MIDI, que hablará sobre la aplicación de este formato en otras aplicaciones interesantes.

Source: https://habr.com/ru/post/454514/


All Articles