Leitor MIDI de quatro partes minimalista



O player proposto não requer um cartão de memória; ele armazena um arquivo MIDI com até 6.000 bytes de comprimento diretamente no microcontrolador ATtiny85 (ao contrário do design clássico, que reproduz arquivos WAV, naturalmente requer um cartão de memória). A reprodução em quatro direções com atenuação usando PWM é implementada em software. Um exemplo de som está aqui .

O dispositivo é fabricado de acordo com o esquema:



O capacitor eletrolítico entre o microcontrolador e o cabeçote dinâmico não perderá o componente constante se uma unidade lógica aparecer na saída do PB4 como resultado de uma falha de software. A indutância da cabeça não passa na frequência PWM. Se você decidir conectar o dispositivo ao amplificador, para evitar sobrecarregar o último com um sinal PWM, você precisará adicionar um filtro passa-baixo, como aqui .

O arquivo MIDI deve ser colocado na fonte do firmware como uma matriz do formato:

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

Existe uma solução pronta para converter um arquivo neste formato em sistemas operacionais semelhantes ao UNIX - o utilitário xxd. Pegamos o arquivo MIDI e passamos por este utilitário assim:

 xxd -i musicbox.mid 

O console exibirá 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 é o comprimento em bytes. Acabou por menos de 6000 - isso significa que se encaixa. A sequência de números hexadecimais na área de transferência é transferida para o esboço (lembre-se: no console - sem Ctrl + C ) em vez da matriz padrão. Ou não faça tudo isso se quisermos deixá-lo.

O contador de temporizador 1 funcionará a uma frequência de 64 MHz a partir do PLL:

  PLLCSR = 1<<PCKE | 1<<PLLE; 

Transferimos esse timer para o modo PWM para funcionar como DAC; o ciclo de serviço dependerá do 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 

A frequência dos pulsos retangulares depende do valor de OCR1C, deixamos igual a 255 (por padrão), então a frequência de 64 MHz será dividida por 256 e obtemos 250 kHz.

O contador do temporizador 0 gerará interrupções:

  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 

Uma frequência de clock de 16 MHz é dividida por um divisor por 8 e, em seguida, por um valor de OCR0A de 19 + 1, e 100 kHz são obtidos. O aparelho tem quatro vozes e 25 kHz são obtidos para cada voz. Após a interrupção, uma rotina de processamento ISR é chamada (TIMER0_COMPA_vect), que calcula e emite sons.

O timer do watchdog está configurado para gerar uma interrupção a cada 16 ms, necessária para receber frequências de notas:

 WDTCR = 1<<WDIE | 0<<WDP0; // Interrupt every 16ms 

Para obter oscilações de uma determinada forma, é usada a síntese digital direta. Não há multiplicação de hardware no ATtiny85, portanto, pegamos pulsos retangulares e multiplicamos a amplitude do envelope por 1 ou -1. A amplitude diminui linearmente e, para calculá-la em um determinado momento, basta diminuir linearmente a leitura do medidor.

São fornecidas três variáveis ​​para cada canal: Freq [] - frequência, Acc [] - bateria de fase, Amp [], valor da amplitude do envelope. Os valores de Freq [] e Acc [] são somados. O bit de alta ordem Acc [] é usado para obter pulsos retangulares. Quanto mais Freq [], maior a frequência. A forma de onda finalizada é multiplicada pelo envelope Amp []. Todos os quatro canais são multiplexados e alimentados na saída analógica.

Uma parte importante do programa é o procedimento para processar a interrupção do contador de temporizador 0, que envia as oscilações para a saída analógica. Este procedimento é chamado a uma frequência de cerca de 95 kHz. Para o canal atual c, ele atualiza os valores de Acc [c] e Amp [c] e também calcula o valor da nota atual. O resultado é enviado ao registro de comparação OCR1B do contador de temporizador OCR1B para obter um sinal analógico no pino 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; } 

String

 Acc[c] = Acc[c] + Freq[c]; 

adiciona a frequência freq [c] à bateria Acc [c]. Quanto maior a Freq [c], mais rápido o valor de Acc [c] mudará. Então linha

 Amp[c] = Amp[c] - (Amp[c] != 0); 

diminui o valor da amplitude para um determinado canal. O fragmento (Amp [c]! = 0) é necessário para que após a amplitude chegue a zero, ela não diminua mais. Linha agora

 Temp = Acc[c] >> 8; 

transfere os 9 bits altos de Acc [c] para Temp. E a linha

 Temp = Temp & Temp<<1; 

deixa o bit de ordem superior dessa variável igual a um se dois bits de ordem superior forem iguais a um e define o bit de ordem superior a zero se não estiver. O resultado são pulsos retangulares com uma relação liga / desliga de 25/75. Em uma das construções anteriores, o autor aplicou um meandro, enquanto que com o novo método, as harmônicas são obtidas um pouco mais. String

 Mask = Temp >> 7; 

transfere os valores dos mais significativos para os bits restantes do byte, por exemplo, se o bit mais significativo for 0, serão obtidos 0x00 e se 1 - 0xFF. String

 Env = Amp[c] >> Volume; 

transfere o bit Amp [c] especificado pelo valor Volume para Env, por padrão o sênior, desde Volume = 8. String

 Note = (Env ^ Mask) + (Mask & 1); 

Tudo isso se une. Se Mask = 0x00, então Nota é atribuído ao valor Env. Se Mask = 0xFF, o Note recebe um valor adicional para Env + 1, ou seja, Env com um sinal de menos. A nota agora contém a forma de onda atual, mudando de valores positivos para negativos da amplitude atual. String

 OCR1B = Note + 128; 

adiciona 128 ao Note e grava o resultado no OCR1B. String

 c = (c + 1) & 3; 

emite quatro canais de acordo com as interrupções correspondentes, multiplexando as vozes na saída.

As frequências de doze notas são fornecidas no array:

 unsigned int Scale[] = { 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715}; 

As frequências das notas de outras oitavas são obtidas dividindo por 2 n . Por exemplo, dividimos 10973 por 2 4 e obtemos 686. O bit superior Acc [c] alternará com uma frequência de 25000 / (65536/685) = 261,7 Hz.

Duas variáveis ​​influenciam o som: Volume - volume, de 7 a 9 e Decay - atenuação, de 12 a 14. Quanto maior o valor de Decay, mais lenta será a atenuação.

O intérprete MIDI mais simples presta atenção apenas aos valores da nota, tempo e coeficiente de divisão e ignora outros dados. A rotina readIgnore () ignora o número especificado de bytes na matriz recebida do arquivo:

 void readIgnore (int n) { Ptr = Ptr + n; } 

A rotina readNumber () lê um número de um determinado número de bytes com uma precisão 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; } 

A rotina readVariable () lê um número com precisão de variável MIDI. O número de bytes nesse caso pode ser de um a quatro:

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

São retirados sete bits de cada byte e o oitavo é igual a um, se você precisar ler mais outro byte, ou zero, se não.

O intérprete chama a rotina noteOn () para reproduzir a nota no seguinte canal disponível:

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

A variável Ptr indica o próximo byte a ser lido:

 void playMidiData () { Ptr = 0; // Begin at start of file 

O primeiro bloco no arquivo MIDI é um cabeçalho que indica o número de faixas, tempo e taxa de divisão:

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

O coeficiente de divisão é geralmente igual a 960. Agora lemos o número determinado de blocos:

  // Read track chunks for (int t=0; t<tracks; t++) { type = readNumber(4); if (type != MTrk) error(2); len = readNumber(4); EndBlock = Ptr + len; 

Leia eventos sequenciais até o final do bloco:

  // 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); 

Em cada evento, o delta é especificado - o atraso em unidades de tempo determinado pelo coeficiente de divisão, que deve ocorrer antes desse evento. Para eventos que devem acontecer aqui de, delta é zero.

Metaeventos são eventos do 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); 

O único tipo de meta-evento que nos interessa é o Tempo, o valor do andamento em microssegundos. Por padrão, são 500.000, ou seja, meio segundo, o que corresponde a 120 batimentos por minuto.

Os eventos restantes são eventos MIDI definidos pelo primeiro dígito hexadecimal de seu tipo. Estamos interessados ​​apenas em 0x90 - Nota ativada, tocando notas no seguinte canal disponível:

  // 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 o valor da velocidade, mas, se desejar, você pode definir a amplitude inicial da nota. Ignoramos o restante dos eventos, sua duração pode ser diferente. Se ocorrer um erro no arquivo MIDI, o LED acenderá.

O microcontrolador opera a uma frequência de 16 MHz, para que o quartzo não seja necessário, você precisa configurar corretamente o PLL interno. Para que o microcontrolador se torne compatível com o Arduino, é aplicada essa experiência do Spence Konde. No menu Painel, selecione o submenu ATtinyCore e, em seguida, - ATtiny25 / 45/85. Nos seguintes menus, selecione: Relógio do timer 1: CPU, BOD desativado, ATtiny85, 16 MHz (PLL). Em seguida, selecione Gravar carregador de inicialização e preencha o programa. O programador é usado como o Tiny AVR Programmer Board da SpinyFun.

O firmware para o CC-BY 4.0, que já possui uma fuga de Bach em ré menor, está aqui , o arquivo MIDI original está aqui .

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


All Articles