Desenvolvimento de um sintetizador de música simples no ATMEGA8

Alguns anos atrás, fiz um despertador no microcontrolador ATmega8, onde implementei um sintetizador de melodia simples de tom único (voz única). Existem muitos artigos na Internet para iniciantes sobre esse tópico. Como regra, um temporizador de 16 bits é usado para gerar a frequência (notas), que é configurada de uma certa maneira, forçando no nível do hardware a emitir um sinal na forma de um meandro em um pino específico do MC. O segundo temporizador (8 bits) é usado para implementar a duração de uma nota ou pausa. As notas de acordo com fórmulas conhecidas são comparadas com frequências e, por sua vez, são comparadas com certos números de 16 bits, inversamente proporcionais às frequências que especificam os períodos de contagem do temporizador.

No meu projeto, forneci três melodias que foram escritas na mesma chave e escala. Portanto, tive que usar um número limitado e determinado de notas, o que facilitou a modelagem. Além disso, todas as três músicas foram tocadas no mesmo ritmo. O código da nota e seu código de duração cabem facilmente em um byte. A única desvantagem desse modelo foi a falta de versatilidade, a capacidade de editar, substituir ou complementar rapidamente a melodia. Para gravar uma melodia, primeiro a escrevi em um editor de música em um computador, depois copiei as notas e a duração delas, com a numeração que decidi previamente e, em seguida, formei os bytes resultantes. Eu fiz as últimas operações usando o programa Excel.

No futuro, eu queria eliminar a desvantagem acima mencionada, traindo o design com certa universalidade e reduzindo o tempo para implementar a melodia. Havia uma idéia que o programa MK lia os bytes de um dos famosos formatos de música. O mais popular e comum é o formato MIDI. Mais literalmente, esse não é tanto o formato de uma “ciência” que pode ser lida na Internet. A especificação MIDI define o protocolo para transmissão de mensagens em tempo real pela interface física correspondente e descreve como os arquivos midi são organizados em que essas mensagens podem ser armazenadas. O formato midi é orientado para a música e, portanto, encontra aplicação no campo relevante. Este é um controle síncrono de equipamentos de som, músicas coloridas, sintetizadores musicais e robôs, etc. Na esfera doméstica, o formato midi foi encontrado na era do início do desenvolvimento de telefones celulares. Nesse caso, as mensagens sobre a inclusão ou desativação de uma nota específica, informações sobre um instrumento musical, o volume das notas sonoras e assim por diante são gravadas no arquivo midi. O celular que reproduz esse arquivo contém um sintetizador que interpreta as mensagens midi nesse arquivo em tempo real e toca a melodia. Nos estágios iniciais, os telefones eram capazes de tocar apenas melodias de tom único. Com o tempo, a chamada polifonia apareceu.

Na Internet, conheci artigos sobre a implementação de um sintetizador polifônico no MK, que lê arquivos midi. Nesse caso, pelo menos, uma “tabela de ondas” pré-formada (uma lista de formas de ondas sonoras) é usada para cada instrumento musical armazenado na memória do MK. E, no meu caso particular, focaremos na implementação de um modelo mais simples: um sintetizador de tom único (voz única).

Para começar, estudei cuidadosamente a estrutura do arquivo midi e concluí que, além das informações necessárias sobre as notas, ele contém informações redundantes adicionais. Portanto, decidiu-se escrever um programa simples para converter um arquivo midi em seu próprio formato. O programa, trabalhando com muitos arquivos MIDI, não apenas converte formatos, mas também os organiza de uma certa maneira. Com antecedência, decidi organizar o armazenamento de muitas músicas na memória ROM (EEPROM 24XX512). Por conveniência de visualização no editor HEX, certifiquei-me de que cada melodia comece do início do setor. Ao contrário de um cartão SD (por exemplo), o conceito de setor não é aplicável à ROM usada, então eu me expresso condicionalmente. O tamanho do setor é de 512 bytes. E o primeiro setor da ROM é reservado para os endereços dos setores do início de cada melodia. Supõe-se que a melodia pode demorar vários setores.

Uma descrição completa do formato de arquivo midi, é claro, não vale a pena fazer aqui. Vou abordar apenas os pontos mais necessários e necessários. Um arquivo midi contém 16 canais, que, em regra, geralmente correspondem a um ou outro instrumento musical. No nosso caso, não importa que tipo de instrumento seja, e apenas um canal é necessário. O conteúdo de cada canal, juntamente com o cabeçalho, é elaborado em um arquivo midi, de acordo com um princípio muito semelhante à organização do armazenamento de fluxos de vídeo e áudio em um contêiner AVI. Eu escrevi sobre o último anteriormente em um dos meus artigos. O cabeçalho do arquivo midi é um conjunto de alguns parâmetros. Um desses parâmetros é a resolução do tempo. É expresso no número de "ticks" (um tipo de pixel) por trimestre (PPQN). Um quarto é um período de tempo durante o qual uma semínima é tocada. Dependendo do andamento da melodia, a duração do trimestre pode ser diferente. Portanto, a duração de um "pixel" (período de amostragem) depende do tempo e do PPQN. Todas as informações sobre a hora de um evento são determinadas com precisão nessa duração.

Além disso, o cabeçalho contém o tipo de arquivo MIDI (tipo 0 ou tipo 1) e o número de canais. Sem entrar em detalhes, trabalharemos com o tipo 1, o número de canais 2. Um arquivo midi com uma melodia de tom único, logicamente, contém um canal. Mas no arquivo midi do “tipo 1”, além do principal, existe outro canal “não musical” no qual são gravadas informações adicionais que não contêm notas. Estes são os chamados metadados. Também não há necessidade de entrar em detalhes. A única informação de que precisamos é de informações sobre o ritmo e em um formato incomum: microssegundos por trimestre. No futuro, será mostrado como usar essas informações, juntamente com o PPQN, para configurar o timer MK, responsável pelo andamento.

No bloco principal do canal com notas, estamos interessados ​​apenas em informações sobre os eventos de ativar e desativar as notas. Um evento de ativação de nota possui dois parâmetros: número e volume da nota. No total, são fornecidas 128 notas e 128 níveis de volume. Estamos interessados ​​apenas no primeiro parâmetro, porque não importa qual seja o volume da nota: todas as notas ao tocar a melodia MK soarão no mesmo volume. E, é claro, a melodia não deve conter notas "overdubbed", ou seja, a qualquer momento, mais de uma nota não deve soar ao mesmo tempo. O código do evento de obtenção (ativação) das anotações é 0x90. O código do evento de anotação é 0x80. No entanto, pelo menos o editor Cakewalk Pro Audio 9 não usa o evento com o código 0x80 ao exportar a composição para o formato midi. Em vez disso, o evento 0x90 ocorre em toda a parte musical e a nota de que a nota está desativada é seu volume zero. Ou seja, o evento "desativar a nota" é equivalente ao evento "ativar a nota com volume zero". Talvez isso seja feito por razões de economia. De acordo com a especificação, o código do evento não pode ser reescrito se esse evento for repetido. Entre os eventos, as informações sobre o intervalo de tempo são registradas em um formato de tamanho variável. Estes são os valores inteiros do número de "ticks" mencionados acima. Na maioria das vezes, um byte é suficiente para registrar o intervalo de tempo. Se dois eventos seguem um após o outro, então entre eles o intervalo de tempo é obviamente igual a zero. Isso, por exemplo, desativa a primeira e a inclusão da segunda nota a seguir, se não houver pausa (espaço) entre elas.

Vamos tentar escrever uma sequência de notas usando o programa "Cakewalk Pro Audio 9". Existem muitos editores, mas eu decidi pelo primeiro que apareceu.



Primeiro, você precisa definir as configurações do projeto. Neste editor, você pode definir a resolução no tempo (PPQN). Eu escolho o valor mínimo igual a 48. Um valor muito grande não faz sentido, pois você precisa trabalhar com números grandes que excedam 1 byte de tamanho. Mas o valor mínimo de 48 é bastante satisfatório. Em quase todas as melodias, notas menores que 1/32 não são encontradas. E se o número de "ticks" por trimestre for 48, a nota ou pausa 1/32 terá uma duração de 48 / (32/4) = 6 "ticks". Ou seja, existe a possibilidade teórica de dividir completamente a nota 1/32 por 2 e até por 3. Deixamos os parâmetros restantes na janela de propriedades do projeto por padrão.



Em seguida, abra a propriedade da primeira faixa e atribua a ele um número de canal igual a 1. A seu gosto, selecione um patch que corresponda a um instrumento musical ao tocar uma melodia no editor. O número do patch, é claro, não afetará o resultado final.



O andamento da melodia é definido no número de quartos por minuto na barra de ferramentas do editor. O valor do andamento padrão é 100 bpm.

O microcontrolador possui um timer de 8 bits, que, como já mencionado, será usado para controlar a duração das notas e pausas no som. Foi decidido que o intervalo de tempo entre operações adjacentes (interrupções) de um cronômetro corresponderia ao intervalo de um “tique”. Dependendo do andamento da melodia, o valor desse intervalo de tempo será diferente. Eu decidi usar interrupções do temporizador de estouro. E, dependendo do parâmetro de inicialização do temporizador inicial, é possível ajustar esse mesmo intervalo de tempo, que depende do andamento da melodia. Agora vamos aos cálculos.

Como regra, na prática, em média, o andamento das músicas fica na ordem de 50 a 200. Já foi dito que o andamento no arquivo midi é definido em microssegundos em um quarto. Para o tempo 50, esse valor é 60.000.000 / 50 = 1.200.000 e para o tempo 250 será 240.000. Como, de acordo com o projeto, um quarto contém 48 ticks, a duração do tick para o andamento mínimo será 1.200.000 / 48 = 25.000 μs. E para o ritmo máximo, se você calcular da mesma maneira, - 5000 μs. Para MK com uma frequência de quartzo de 8 MHz e um divisor de timer preliminar máximo de 1024, obtemos o seguinte. Para o ritmo mínimo, o temporizador precisa ser calculado 25000 / (1024/8) = 195 vezes. O resultado é arredondado para o valor inteiro mais próximo, o erro de arredondamento praticamente não afeta o resultado. Para o ritmo máximo - 5000 / (1024/8) = 39. Aqui, o erro de arredondamento não afeta mais, pois também é obtido um valor arredondado de 39 para valores de tempo vizinhos de 248 a 253. Portanto, o temporizador deve ser inicializado com um valor inverso: para o tempo mínimo - (256-195) = 61 e para o máximo - (256 -39) = 217. O ritmo mínimo no qual o timer será fornecido na configuração atual do MK é de 39 bpm. Com este valor, o temporizador deve ser contado 250 vezes. E com um valor de 38 - já 257, que ultrapassa os limites do temporizador. Decidi pegar o valor de 40 bpm no ritmo mínimo e 240 no máximo.

Para calcular o número de ticks, um temporizador virtual baseado no precedente será usado. É o número de ticks que define a duração de uma nota ou pausa, como já mencionado acima.

Para implementar a reprodução de notas, um segundo temporizador de 16 bits é usado. De acordo com a especificação MIDI, são fornecidas 128 notas. Mas, na prática, eles são usados ​​muito menos. Além disso, as notas das oitavas mais baixa (com frequências de cerca de 50 Hz) e mais alta (com frequências de cerca de 8 kHz) não serão reproduzidas harmoniosamente pelo microcontrolador. Mas, por tudo isso, um timer de 16 bits com um divisor fixo cobre quase toda a gama de notas fornecidas pelo midi, ou seja, sem as primeiras 35. Mas eu escolhi como início a nota com o número 37 (seu código é 36, pois a codificação vem do zero). Isso é feito por conveniência, pois esse número corresponde à nota “C”, como a primeira nota em uma escala tradicional. Corresponde a ele com uma frequência de 65,4 Hz e o meio-ciclo é - 1 / 65,4 / 2 = 0,00764 seg. Esse período com uma frequência MK de 8 MHz e um divisor 1 (ou seja, sem divisor) contará o cronômetro aproximadamente como um todo por 0,00764 / (1/8000000) = 61156 vezes. Para a 35ª nota, se você contar, esse valor será 68645, que está além do intervalo do timer de 16 bits. Mas, mesmo que houvesse a necessidade de tocar notas abaixo do 36º, você pode inserir o primeiro divisor de timer disponível, igual a 8. Mas não há necessidade prática disso, assim como não há nem mesmo para tocar as notas mais altas. No entanto, para a 128ª nota mais alta, nota “G” com uma frequência de 12.543,85 Hz, o valor do temporizador é, se contado de maneira semelhante, 319. As especificações de todos os cálculos acima são determinadas pela configuração específica do modo de temporizador, que será mostrado posteriormente.

Agora, tenho uma pergunta não menos importante: como obter a relação entre o número da nota e o código do temporizador? Existe uma fórmula conhecida para calcular a frequência de uma nota pelo seu número. E o código do temporizador para uma frequência conhecida é calculado facilmente, como mostrado acima nos exemplos. Mas a raiz do 12º grau aparece na fórmula da dependência da frequência na nota e, em geral, eu não gostaria de carregar o controlador com tais procedimentos computacionais. Por outro lado, a criação de uma matriz de códigos de timer para todas as notas também não é racional. E eu decidi fazer o seguinte, escolhendo um meio termo. Basta criar uma série de códigos de timer para as 12 primeiras notas, que são uma oitava. E as notas das oitavas a seguir devem ser obtidas multiplicando sequencialmente as frequências das notas da primeira oitava por 2. Ou, a mesma coisa, dividindo sequencialmente os valores dos códigos do temporizador por 2. Outra conveniência é que o número da oitava, por coincidência, é um argumento na operação de mudança de bit para a direita ( »), Que será usado como operação de divisão por potências de dois. Escolhi esse operador não por acaso, pois seu argumento reflete o expoente do poder do divisor (o número de divisões por 2). E este é o número da oitava. Para o meu conjunto de notas, um total de 8 oitavas está envolvido (a última oitava está incompleta). Uma nota em um arquivo midi é codificada com um byte, mais precisamente, 7 bits. Para reproduzir notas no MK, de acordo com a idéia acima, você deve primeiro calcular o número da oitava e o número da nota na oitava usando o código da nota. Esta operação é realizada no estágio de conversão do arquivo midi para um formato simplificado. Oito oitavas podem ser codificadas em três bits e 12 notas em uma oitava podem ser codificadas em quatro. No total, verifica-se que a nota é codificada nos mesmos sete bits que no arquivo midi, mas apenas em uma representação diferente, conveniente para o MK. Devido ao fato de que 16 bits podem ser codificados com 4 bits e notas em uma oitava de 12, existem bytes não utilizados.

O último oitavo bit pode ser usado como um marcador para ativar ou desativar as notas. No caso de MK, devido à unanimidade da melodia, as informações sobre a nota silenciada serão redundantes. Com uma mudança direta de nota na melodia, não há um "desligar-ligar-ligar", mas um "interruptor" da nota. E no caso de uma pausa, “o silêncio está ativado”, para o qual você pode selecionar um byte especial do conjunto de bytes não utilizados e não usar as informações sobre como desativar a nota. Essa ideia é boa, pois salva o tamanho da melodia resultante após a conversão, mas geralmente complica o modelo. Não segui essa ideia, pois já há bastante memória.

As informações sobre as notas da melodia no arquivo midi são armazenadas no bloco do canal correspondente na exibição "interval-event-interval-event ...". No formato convertido, exatamente o mesmo princípio se aplica. Para gravar um evento (ativar ou desativar uma nota), como mencionado acima, um byte é usado. O primeiro bit (o bit mais significativo 7) codifica o tipo de evento. O valor "1" é a nota ativada e o valor "0" é a nota desativada. Os próximos três bits codificam o número da oitava e os quatro bits mais baixos codificam o número da nota na oitava. Um byte também é usado para registrar o intervalo de tempo. No formato midi original, é usado um formato de tamanho variável. Sua pequena desvantagem é que apenas 7 bits codificam o intervalo de tempo (o número de "ticks"), e o oitavo bit é um sinal de continuação. Ou seja, com um byte, na verdade, você pode codificar um intervalo de até 128 ticks. Mas como os intervalos de tempo entre eventos em melodias reais e simples às vezes excedem 128, mas quase nunca excedem 256, abandonei o formato de tamanho variável e custou um byte. Ele codifica um intervalo de tempo de até 256 ticks. Como o projeto usa 48 ticks por trimestre ou 48 * 4 = 192 ticks por ciclo, um byte pode ser usado para codificar um intervalo de 256/192 = 1 duração. (3) (um todo e um terço) ciclos, que bastante.

No formato nativo para o qual o arquivo midi é convertido, também apliquei um cabeçalho pequeno, com 16 bytes de tamanho. Os primeiros 14 bytes contêm o nome da melodia. Naturalmente, o nome não deve exceder 14 caracteres. Então vem um espaço zero. O próximo último byte reflete o andamento da melodia em uma exibição conveniente para o MK. Este valor é calculado no estágio de conversão e serve para inicializar o timer MK, responsável pelo ritmo. Como é calculado é discutido em alguns parágrafos acima.

A partir do 17º byte, o conteúdo da melodia segue. Cada byte ímpar corresponde a um intervalo de tempo e cada byte par corresponde a um evento (nota).O primeiro byte será zero se a melodia começar com uma nota, desde o início do arquivo midi, sem uma pausa preliminar. Um sinal do final da melodia é um rótulo de dois bytes 0xFF. A tarefa envolve a reprodução cíclica de uma melodia por um microcontrolador. Para que a melodia no loop pareça harmoniosa do ponto de vista do ritmo, ela deve ser repetida corretamente. Para fazer isso, se necessário, após uma última nota, você precisará pausar um determinado comprimento, geralmente até que a última medida seja preenchida. E para isso você precisa desviar o evento correspondente. Eu usei o byte 0x0F, que não é usado nas notas de codificação. Corresponde a desabilitar a 16ª nota na primeira oitava, o que é um absurdo, uma vez que existem apenas 12 notas na oitava mencionamos acima sobre bytes não utilizados. Assim, esse byte codifica uma "nota silenciosa",o bit alto também pode servir como sinal de ativação ou desativação, apesar da redundância de informações também neste caso. Para definir esta nota no editor midi, tirei a primeira ou a segunda nota (qualquer uma delas). Deixe-me lembrá-lo de que as primeiras 36 notas não são usadas no modelo. Assim, a primeira (ou segunda) nota é usada conforme necessário para a conclusão correta da melodia, de modo que o ritmo não seja interrompido ao tocá-la em um loop.

Continuando a trabalhar no editor do "Cakewalk Pro Audio 9", comporemos uma melodia arbitrária. As figuras abaixo mostram as notas da melodia que reescrevi de uma das fotos na Internet. Imagens de notas são apresentadas em dois estilos: no estilo "Piano roll" e no estilo clássico. O primeiro é muito conveniente para escrever e editar melodias usando um mouse de computador. É isso que eu uso.





Como você pode ver na figura, no final, a nota mais baixa (primeira) é aplicada ao sinal de silêncio no intervalo de tempo certo para organizar corretamente o padrão cíclico. E no início da melodia, em vista da presença de um toque, há um quarto de recuo antes da primeira nota.

O editor fornece um modo para exibir eventos em forma de tabela.



Como você pode ver na figura, não há nada supérfluo na lista de eventos, exceto as anotações, como às vezes acontece com manipulações desnecessárias em um projeto musical. Se, no entanto, eventos desnecessários que não estão relacionados às notas estiverem incluídos por alguma razão na lista, eles poderão ser excluídos pressionando a tecla Del. Embora, no estágio de conversão, todos os eventos desnecessários sejam ignorados e o tempo delta "se acumule". A propósito, eu adicionei essa função ao programa no estágio de depuração. Como você pode imaginar, a tabela reflete o prazo e a duração de cada nota, além de outras propriedades que não precisamos. Ou seja, com uma linha na tabela, dois eventos midi são expressos ao mesmo tempo: ativar e desativar notas.

Salve a melodia no formato “midi 1”, como mostra a figura.



Abra o arquivo salvo no editor HEX. Deve-se notar imediatamente que, diferentemente dos mesmos arquivos avi (como escrevi anteriormente), bytes de valores numéricos em um arquivo midi são apresentados não na ordem inversa, mas de acordo com a antiguidade (big endian).



Na figura, marquei com marcadores apenas os bytes desejados. Primeiro, uma moldura vermelha em negrito descreve três grupos de dois bytes em cada um. Este é, respectivamente, o tipo de formato MIDI (1), o número de canais (2) e o número de ticks por trimestre (48). São esses valores que essas três constantes devem ter para o trabalho adicional do programa de transformação. Arcos roxos marcam o início de cada um dos dois canais. No primeiro canal, 6 bytes são marcados com uma moldura cinza, dentro da qual três bytes são realçados com uma moldura azul. Esses 6 bytes referem-se a um meta evento (marcador 0xFF) com um código de 0x51 e um comprimento de conteúdo de 0x03 bytes. Três bytes a mais - o conteúdo do evento. Este evento define o andamento da melodia com apenas esses três bytes em um quadro azul. O último byte baixo pode ser descartado com segurança, porque a super precisão não é importante. Não darei uma descrição detalhada e completa de todos os bytes no arquivo.Na segunda faixa - na faixa com notas - os valores dos intervalos de tempo são circulados em uma moldura azul. A propósito, neste exemplo em particular, eles não excederam um byte, exceto o único caso com a penúltima nota. É a penúltima nota da melodia (contando a pseudo nota extra do final) que dura três quartos de uma medida, que é 48 * 3 = 144 tiques e excede 128. E é para isso que você precisa usar dois bytes, de acordo com o formato de tamanho variável. E para representar o intervalo de tempo no formato convertido, o valor 144 é facilmente codificado com um byte. Eu circulei este caso especial em uma moldura azul dupla. As notas são circuladas em uma moldura verde, ou melhor, seus códigos. O volume de cada nota é circulado em uma moldura cinza. Como já mencionado, um volume zero é um sinal de mudo (liberação) da nota e, em toda a composição, há um evento:ligar as notas. O código para este evento, 0x90, está marcado em amarelo. Não esbocei todas as notas até o final da melodia. A única exceção é o quadro azul duplo por um único intervalo de tempo que excede o limite de 128 ticks.

Novamente, como mencionado acima, o programa para converter um arquivo midi em seu próprio formato para o MK realmente funciona com um grupo de vários arquivos midi e, na saída, cria um arquivo de imagem para a EEPROM. Considere um fragmento deste arquivo relacionado ao conteúdo da melodia convertida do exemplo acima. Abri em outro editor HEX para mostrar a imagem por setores e prestar atenção nela. Cada nova melodia começa com um novo setor.



O último byte da primeira linha (os primeiros 16 bytes), circulado em uma moldura vermelha, define o andamento da melodia. De acordo com os cálculos, o valor 0xC1 (193) cai nos tempos 154, 155 e 156. Apenas no projeto, defino o tempo da melodia para 155 bpm, o que foi visto em uma das capturas de tela anteriores. Os primeiros bytes (até o dia 14) circulados em uma moldura azul determinam o nome da composição. Neste exemplo, "Clássico". Para o MK, essas informações são desnecessárias, são necessárias apenas para orientação no editor HEX. Embora, se você fizer um projeto mais complexo no MK usando o display, poderá usar essas informações exibindo o nome da melodia tocada.

A segunda linha (do 17º byte) inicia o conteúdo da melodia. Como no arquivo midi original, não pintei todas as notas, mas pintei apenas uma parte. Os bytes ímpares destacados em azul são intervalos de tempo. Até os bytes marcados com uma moldura verde são notas e sinais de ativação / desativação. Por exemplo, os dois primeiros bytes verdes, 0xB4 e 0x34, referem-se à mesma nota com o código 0x34, e os bytes diferem em apenas um bit de ordem superior. No byte 0xB4 (0b10110100), o bit alto é um, que é um sinal de ativar uma nota, e no byte 0x34 (0b00110100), o bit alto é zero, que é um sinal para desativar uma nota. O byte 0x34 codificou uma nota com os seguintes parâmetros: código de oitava 0b011 e o código de nota em uma oitava - 0b0100. Ou, na forma decimal, 3 e 4, respectivamente. Se você não contar a partir de zero,acontece que a primeira nota na melodia pertence à quarta oitava e é a quinta nela. A numeração de oitavas aqui é escolhida arbitrariamente, sem levar em consideração a numeração padrão. A nota acordada, de acordo com minha tabela auxiliar de cálculo Excel, é a nota com o código 76 (0x4C) para o formato midi, ou seja, a nota E6 (nota “e” da 6ª oitava do meio). Assim é: a composição começa com esta nota.

Deve-se notar um caso especial na sequência musical, quando a mesma nota é repetida sem pausa. No nosso exemplo, todas as notas adjacentes sem pausa são diferentes. Mas há melodias em que a nota se repete sem pausa. Ou seja, o intervalo de tempo entre desligar uma e ativar a próxima nota exata é zero. Em vista da peculiaridade de uma síntese complexa da música, essa sequência soa familiar em qualquer sintetizador. Mas, no caso do MK, parecerá tão coeso que será difícil ouvir a diferença entre duas notas idênticas. Na prática, é claro, não haverá uma fusão clara devido aos cálculos intermediários que ocorrem no MC, mas, ainda assim, esse intervalo de tempo provavelmente será muito menor do que a duração de um único tick. Para casos especiais, o programa está na fase de conversão,esbarrar nessa combinação, introduz uma pausa entre as notas de 1 tiquetaque e reduz a duração de uma nota à esquerda da mesma no mesmo intervalo de tempo. Uma “diferença” mínima de 1 tick é suficiente, como a prática demonstrou.

Em um quadro azul duplo, circulei o valor do intervalo de tempo (0x90), que excede 128, e pelo qual tive que gastar dois bytes no arquivo midi, de acordo com o formato de tamanho variável. Círculos verdes são bytes circulados dentro e fora da mesma pseudo nota para alinhar a composição. O programa MK, ao ver esses bytes, os interpretará como ativando o silêncio. Por fim, dois bytes 0xFF rodeados por uma moldura azul em negrito marcam o final da melodia. Os valores de todos os seguintes bytes no setor de memória atual podem ser quaisquer, eles são ignorados.

Considere o primeiro setor do arquivo de imagem EEPROM de saída. Como já escrevi, serve como uma lista de endereços de setores do começo de melodias. O programa varreu com sucesso 8 músicas sem erros (no momento em que escrevi, eu gravei 8 músicas). O valor do número de melodias é registrado no último 512º byte do setor. E desde o início do setor, os endereços são escritos. Para a primeira melodia, o endereço é 0x01, que corresponde ao segundo setor (o primeiro, se você contar do zero). A terceira e quarta melodias (duas de oito) acabaram sendo longas e não se encaixavam em um setor. Portanto, são observadas lacunas na sequência de endereços. Se você contar 64kB de memória, não poderá gravar mais de 127 músicas; portanto, um setor para endereçamento é suficiente.



Todas as estimativas e cálculos preliminares refletidos no artigo, eu realizei no Excel. As capturas de tela abaixo mostram capturas de tela das tabelas resultantes (no modo de janela dupla).





Quem se importa, abaixo do spoiler, está o texto de um programa em C que converte arquivos midi em um arquivo para o microcontrolador. Do texto, removi as linhas extras usadas para depuração. O programa, até o momento, está funcionando, não pretende ser legível e instruído em escrever código.

Arquivo 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; } 


Anexo do arquivo 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); 


A parte básica do programa para o MK, de fato, é muito simples. Considere uma das opções para sua implementação, mais precisamente, sua parte principal.

O temporizador 1, usado para gerar o som das notas, é configurado da seguinte maneira. Para ativar e desativar notas, as seguintes substituições são usadas, respectivamente.

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

Antes de iniciar o timer, você precisa atribuir ao registro OCR1A um valor de 16 bits que corresponderá à frequência que está sendo reproduzida. Isso será mostrado mais tarde. Quando o timer é ativado, o registro TCCR1B recebe o Modo de Geração de Forma de Onda com um divisor de timer 1 e o registro TCCR1A é definido como Alternar OC1A na Comparação de Comparação. Nesse caso, o sinal é removido da saída especialmente designada do MK “OC1A”. No ATmega8 no pacote SMD, esse é o pino 13, que é o mesmo que PORTB.1. Quando o timer é desligado, os dois registradores são redefinidos e a saída de PORTB.1 é forçada a zero. Isso é necessário para impedir, durante o silêncio, a saída de uma tensão constante, o que seria indesejável para a entrada do VLF. No entanto, você pode colocar um capacitor no circuito, mas também pode desativar programaticamente a saída. Uma tensão constante pode ocorrer nesta saída se a nota for desativada no momento da fase correspondente do sinal, e isso ocorre em 50% dos casos.

Crie uma matriz de valores de timer para 12 notas da primeira oitava. Esses valores foram calculados antecipadamente.

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

As notas de outras oitavas, como eu disse, serão obtidas pela divisão em graus dois.

A configuração do temporizador 0 é ainda mais simples. Funciona constantemente, com uma interrupção de transbordamento, cada vez que é inicializado novamente com o valor que corresponde ao andamento da melodia. O divisor do temporizador é 5: TCCR0 = 0x05. Com base nesse timer, é criado um timer virtual que conta os tiques (vezes) na melodia. O processamento da resposta desse timer é colocado no ciclo principal do programa.

A função de interrupção do timer 0 é a seguinte.

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

Aqui, a variável ent01 é responsável por ativar o timer virtual. Por essa variável, ela pode ser ativada ou desativada, se necessário. A variável vt01 é a variável primária contável do timer virtual. A linha TCNT0 = top0 indica a inicialização do temporizador 0 para o valor desejado top0, que é lido no título da melodia antes de reproduzi-la.

O número da melodia a ser tocada corresponde à variável alm. Também serve como bandeira do início da reprodução. Ela precisa atribuir um número de melodia de uma das maneiras, dependendo da tarefa. Depois disso, o próximo bloco do ciclo principal ficará ativo.

 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; //        ,   ; } 

A mudança adicional de nota para nota é realizada na unidade de processamento do timer virtual, que também é colocado no loop 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; //   ; } } 

A partir dos comentários no texto do programa, tudo deve ser bem claro e compreensível.

Para parar a melodia, use a seguinte inserção do loop principal.

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

Há uma pequena observação sobre a implementação da reprodução da melodia. Antes de cada nova nota começar a soar, o microcontrolador gasta uma pequena quantidade de tempo convertendo o byte de leitura da nota em um valor de timer. Desta vez, como se viu na prática, é relativamente pequeno e não afeta a qualidade da reprodução. Mas eu tinha dúvidas de que essa operação permanecesse invisível. Nesse caso, pausas extras apareceriam antes de cada nota e o ritmo da melodia seria interrompido. Mas esse problema também é solucionável. Basta calcular os valores do timer da próxima nota com antecedência enquanto a nota atual soa. Este procedimento deve ser executado separadamente do processamento do timer virtual no loop principal do programa usando um sinalizador especialmente designado. Como é improvável que o tempo de cálculo exceda o tempo de reprodução da nota mais curta, essa solução é adequada.

Agora vamos testar o programa.

Além dos trechos de código acima, adicionei funções de processamento de botão ao programa MK, com as quais controlo a inclusão ou desativação de uma melodia específica. A EEPROM está conectada ao MK via barramento I2C, trabalho com o qual é implementado no nível do software. O projeto foi realizado com a ajuda do “CodeVisionAVR” junto com o “CodeWizardAVR”. Saída o MK do pino 13 para a placa de som do PC através do divisor e gravo o som da melodia no editor de som. Pisquei a memória EEPROM com a ajuda do firmware, sobre o qual escrevi em um dos artigos anteriores. Como nem todos os bytes do arquivo de imagem são úteis, o firmware da memória pode ser implementado apenas por bytes úteis (até os marcadores finais das melodias), a fim de economizar tempo de gravação e recursos de chip. Para fazer isso, você pode criar um programa separado ou gravar bytes no chip diretamente durante a conversão, adicionando ao programa principal.

Entre as oito músicas, há três de teste, com a qual avaliarei a faixa de frequência de ouvido, o som da mesclagem de notas idênticas, o som das notas mais curtas, transições rápidas etc. Deixe-me lembrá-lo de que a mesclagem das mesmas notas realmente soa com uma pausa de um tiquetaque, e a primeira nota na fusão dura menos um.

Uma das músicas de teste é uma sequência de notas do primeiro ao último, com duração de uma nota em um quarto e um tempo de melodia de 40 bpm.



Nesse cenário, uma nota soa um pouco mais de um segundo e, portanto, você pode ouvir em detalhes como soa todo o intervalo de notas. No espectro de frequências no editor de áudio "Adobe Audition", os principais componentes de frequência e seus harmônicos superiores são observados devido à forma de onda dente de serra correspondente. E a relação logarítmica entre o número da nota e a frequência é impressionante.



Analisando os intervalos de tempo, é claramente visto que a pausa real entre notas consecutivas é em média de cerca de 145 amostras (em uma frequência de amostragem da gravação de áudio 44100 Hz), que é de cerca de 3 ms. Este é o tempo durante o qual o MK executa os cálculos necessários. Essas inserções estão presentes regularmente antes de cada nota. Escrevi especificamente o significado nas amostras, pois essas informações são mais originais e mais precisas, embora isso não seja muito importante.



E a duração de um tique a um ritmo médio da melodia de 120 bpm é de cerca de 10 ms. Segue-se que, em princípio, seria possível não introduzir a mesma correção em 1 tick, quando duas notas idênticas forem uma após a outra sem pausa. Eu acho que a inserção regular de 3 ms entre as notas seria suficiente. Ao ouvir uma melodia, essas inserções regulares não são perceptíveis e as melodias soam uniformemente. Portanto, não há necessidade específica de calcular o valor do timer para a próxima nota enquanto a nota atual estiver sendo reproduzida.

Outra melodia de teste com um ritmo de 200 bpm contém sucessivamente as mesmas notas 1/32 da faixa intermediária, sem pausa. Nesse caso, após o processamento, ao tocar entre eles, há uma pausa de 1 tick, que neste ritmo rápido de 310 amostras (cerca de 6 ms) do sinal gravado.



A duração desta pausa, a propósito, é comparável ao período do sinal, o que indica um ritmo alto da melodia. E seu som lembra um trinado.

Em princípio, isso pode ser concluído. Fiquei satisfeito com o resultado do dispositivo, ele superou todas as expectativas. Na maioria das vezes, dediquei-me ao estudo do formato midi e à depuração do programa para conversão. Um dos seguintes artigos também dedicarei a um tópico relacionado ao MIDI, que abordará a aplicação desse formato em outras aplicações interessantes.

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


All Articles