Lecteur MIDI minimaliste en quatre parties



Le lecteur proposé ne nécessite pas de carte mémoire; il stocke un fichier MIDI jusqu'à 6000 octets directement dans le microcontrôleur ATtiny85 (contrairement à cette conception classique, qui lit des fichiers WAV, il nécessite naturellement une carte mémoire). La lecture à quatre voies avec atténuation utilisant PWM est implémentée dans le logiciel. Un exemple de sondage est ici .

L'appareil est fabriqué selon le schéma:



Le condensateur électrolytique entre le microcontrôleur et la tête dynamique ne manquera pas la composante constante si une unité logique apparaît à la sortie du PB4 à la suite d'une panne logicielle. L'inductance de la tête ne dépasse pas la fréquence PWM. Si vous décidez de connecter l'appareil à l'amplificateur, afin d'éviter de surcharger ce dernier avec un signal PWM, vous devez ajouter un filtre passe-bas, comme ici .

Le fichier MIDI doit être placé dans la source du firmware sous la forme d'un tableau de la forme:

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

Il existe une solution prête à l'emploi pour convertir un fichier dans ce format sur des systèmes d'exploitation de type UNIX - l'utilitaire xxd. Nous prenons le fichier MIDI et passons par cet utilitaire comme ceci:

 xxd -i musicbox.mid 

La console affichera quelque chose comme:

 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 est la longueur en octets. Il s'est avéré moins de 6000 - ce qui signifie qu'il convient. La séquence de nombres hexadécimaux à travers le presse-papiers est transférée vers l'esquisse (rappelez-vous juste: dans la console - pas Ctrl + C ) au lieu du tableau par défaut. Ou ne faites pas tout cela si nous voulons le quitter.

Le compteur de minuterie 1 fonctionnera à une fréquence de 64 MHz à partir de la PLL:

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

Nous transférons cette minuterie en mode PWM pour fonctionner comme un DAC; le rapport cyclique dépendra de la valeur de OCR1B:

  TIMSK = 0; // Timer interrupts OFF TCCR1 = 1<<CS10; // 1:1 prescale GTCCR = 1<<PWM1B | 2<<COM1B0; // PWM B, clear on match OCR1B = 128; DDRB = 1<<DDB4; // Enable PWM output on pin 4 

La fréquence des impulsions rectangulaires dépend de la valeur de OCR1C, nous la laissons égale à 255 (par défaut), puis la fréquence de 64 MHz sera divisée par 256, et nous obtenons 250 kHz.

Le compteur de minuterie 0 générera des interruptions:

  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 

Une fréquence d'horloge de 16 MHz est divisée par un diviseur par 8, puis par une valeur OCR0A de 19 + 1, et 100 kHz sont obtenus. Le lecteur est à quatre voix, 25 kHz est obtenu pour chaque voix. Lors d'une interruption, un sous-programme de traitement ISR est appelé (TIMER0_COMPA_vect), qui calcule et émet des sons.

Le temporisateur du chien de garde est configuré pour générer une interruption toutes les 16 ms, ce qui est nécessaire pour recevoir les fréquences des notes:

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

Pour obtenir des oscillations d'une forme donnée, une synthèse numérique directe est utilisée. Il n'y a pas de multiplication matérielle dans ATtiny85, nous prenons donc des impulsions rectangulaires et multiplions l'amplitude de l'enveloppe par 1 ou -1. L'amplitude diminue linéairement, et pour la calculer à un instant donné, il suffit de diminuer linéairement la lecture du compteur.

Trois variables sont fournies pour chaque canal: Fréq [] - fréquence, Acc [] - batterie de phase, Amp [], valeur d'amplitude d'enveloppe. Les valeurs de Freq [] et Acc [] sont additionnées. Le bit de poids fort Acc [] est utilisé pour obtenir des impulsions rectangulaires. Plus Freq [] est élevé, plus la fréquence est élevée. La forme d'onde finie est multipliée par l'enveloppe Amp []. Les quatre canaux sont multiplexés et envoyés à la sortie analogique.

Une partie importante du programme est la procédure de traitement de l'interruption à partir du compteur de temporisation 0, qui délivre les oscillations à la sortie analogique. Cette procédure est appelée à une fréquence d'environ 95 kHz. Pour le canal actuel c, il met à jour les valeurs de Acc [c] et Amp [c], et calcule également la valeur de la note actuelle. Le résultat est envoyé au registre de comparaison OCR1B du compteur de temps OCR1B pour obtenir un signal analogique à la broche 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]; 

ajoute la fréquence freq [c] à la batterie Acc [c]. Plus Freq [c] est élevé, plus la valeur Acc [c] changera rapidement. Puis ligne

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

diminue la valeur d'amplitude pour un canal donné. Le fragment (Amp [c]! = 0) est nécessaire pour qu'après que l'amplitude ait atteint zéro, elle ne diminue plus. Now line

 Temp = Acc[c] >> 8; 

transfère les 9 bits de Acc [c] élevés à Temp. Et la ligne

 Temp = Temp & Temp<<1; 

laisse le bit de poids fort de cette variable égal à un si deux bits de poids fort sont égaux à un et met le bit de poids fort à zéro s'il ne l'est pas. Le résultat est des impulsions rectangulaires avec un rapport marche / arrêt de 25/75. Dans l'une des constructions précédentes, l'auteur a appliqué un méandre, tandis qu'avec la nouvelle méthode, les harmoniques sont un peu plus obtenues. String

 Mask = Temp >> 7; 

transfère les valeurs des bits les plus significatifs aux bits restants de l'octet, par exemple, si le bit le plus significatif était 0, alors 0x00 sera obtenu, et si 1 - alors 0xFF. String

 Env = Amp[c] >> Volume; 

transfère le bit Amp [c] spécifié par la valeur Volume à Env, par défaut le bit principal, puisque Volume = 8. String

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

Tout cela unit. Si Mask = 0x00, la valeur Env est attribuée à Note. Si Mask = 0xFF, alors Note reçoit une valeur supplémentaire à Env + 1, c'est-à-dire Env avec un signe moins. Remarque contient maintenant la forme d'onde actuelle, passant de valeurs positives à des valeurs négatives de l'amplitude actuelle. String

 OCR1B = Note + 128; 

ajoute 128 à Note et écrit le résultat dans OCR1B. String

 c = (c + 1) & 3; 

produit quatre canaux en fonction des interruptions correspondantes, multiplexant les voix à la sortie.

Douze fréquences de notes sont données dans le tableau:

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

Les fréquences de note des autres octaves sont obtenues en divisant par 2 n . Par exemple, nous divisons 10973 par 2 4 et nous obtenons 686. Le bit supérieur Acc [c] commutera avec une fréquence de 25000 / (65536/685) = 261,7 Hz.

Deux variables influencent le son: Volume - volume, de 7 à 9 et Decay - atténuation, de 12 à 14. Plus la valeur de Decay est élevée, plus l'atténuation est lente.

L'interprète MIDI le plus simple ne fait attention qu'aux valeurs de la note, du tempo et du coefficient de division, et ignore les autres données. La routine readIgnore () ignore le nombre spécifié d'octets dans le tableau reçu du fichier:

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

La routine readNumber () lit un nombre à partir d'un nombre donné d'octets avec une précision de 4:

 unsigned long readNumber (int n) { long result = 0; for (int i=0; i<n; i++) result = (result<<8) + pgm_read_byte(&Tune[Ptr++]); return result; } 

La routine readVariable () lit un nombre avec une précision variable MIDI. Le nombre d'octets dans ce cas peut être de un à quatre:

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

Sept bits sont extraits de chaque octet, et le huitième est égal à un si vous avez besoin de lire un autre octet plus loin, ou zéro sinon.

L'interpréteur appelle la routine noteOn () pour jouer la note dans le canal disponible suivant:

 void noteOn (uint8_t number) { uint8_t octave = number/12; uint8_t note = number%12; unsigned int freq = Scale[note]; uint8_t shift = 9-octave; Freq[Chan] = freq>>shift; Amp[Chan] = 1<<Decay; Chan = (Chan + 1) & 3; } 

La variable Ptr indique l'octet suivant à lire:

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

Le premier bloc du fichier MIDI est un en-tête qui indique le nombre de pistes, le tempo et le rapport de division:

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

Le coefficient de division est généralement égal à 960. Nous lisons maintenant le nombre donné de blocs:

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

Lisez les événements séquentiels jusqu'à la fin du bloc:

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

Dans chaque événement, le delta est spécifié - le retard en unités de temps déterminé par le coefficient de division, qui doit se produire avant cet événement. Pour les événements qui devraient se produire ici, delta est nul.

Les méta-événements sont des événements de type 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); 

Le seul type de méta-événement qui nous intéresse est Tempo, la valeur du tempo en microsecondes. Par défaut, c'est 500 000, soit une demi-seconde, ce qui correspond à 120 battements par minute.

Les événements restants sont des événements MIDI définis par le premier chiffre hexadécimal de leur type. Nous ne sommes intéressés que par 0x90 - Note On, jouant des notes sur le canal disponible suivant:

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

Nous ignorons la valeur de vélocité, mais si vous le souhaitez, vous pouvez définir l'amplitude initiale de la note dessus. Nous sautons le reste des événements, leur durée peut être différente. Si une erreur se produit dans le fichier MIDI, la LED s'allume.

Le microcontrôleur fonctionne à une fréquence de 16 MHz, de sorte que le quartz n'est pas nécessaire, vous devez configurer correctement la PLL intégrée. Pour que le microcontrôleur devienne compatible Arduino, cette expérience de Spence Konde est appliquée. Dans le menu Board, sélectionnez le sous-menu ATtinyCore, et là - ATtiny25 / 45/85. Dans les menus suivants, sélectionnez: Timer 1 Clock: CPU, BOD Disabled, ATtiny85, 16 MHz (PLL). Sélectionnez ensuite Graver Bootloader, puis remplissez le programme. Le programmateur est utilisé comme le Tiny AVR Programmer Board de SpinyFun.

Le firmware pour CC-BY 4.0, qui a déjà une fugue de Bach en ré mineur, est ici , le fichier MIDI d'origine est pris ici .

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


All Articles