[134] MIDI - język nie tylko instrumentów cz. 10

[134] MIDI - język nie tylko instrumentów cz. 10

Kontynuując temat wykorzystania ATtiny jako małego Arduino w służbie MIDI przejdziemy do konkretu. Taki maluszek będzie się świetnie nadawał do wbudowywania w różne urządzenia, w szczególności w pedał fortepianowy.


Jest to manipulator posiadający w swojej najbardziej rozbudowanej formie trzy pedały, częściej jednak dwa albo tylko jeden. Obowiązkowym jest ten, który przedłuża brzmienie dźwięków tak, jak byśmy wciąż trzymali wciśnięte klawisze. Jest niezbędny podczas grania w stylu fortepianowym. Pozostałe dwa odpowiadają za wyciszenie instrumentu i pozostawienie młotków już uniesionych w tej pozycji – ale bez unoszenia pozostałych. Generalnie ten trzeci ma różne znaczenia, zwłaszcza w syntezatorach.

Zbudujemy więc manipulator wysyłający trzy, specjalnie do takich zadań zaprojektowane kontrolery. Nazywają się: sustain, soft i sostenuto. Oczywiście jeśli komuś wystarczy tylko jeden przełącznik, nie musi korzystać z pozostałych.

#include <avr/power.h>             // Dołącz bibliotekę zarządzającą energią.
const byte opoznienie = 32;        // Dobierz opóźnienie dla transmisji szeregowej.
const byte midiOut = 4;            // Adres portu MIDI OUT.
const byte przyciskSustain = 2;    // Adres przycisku sustain.
const byte przyciskSoft = 1;       // Adres przycisku soft.
const byte przyciskSostenuto = 0;  // Adres przycisku sostenuto.

void setup() {
  pinMode(midiOut, OUTPUT);                // Deklaruj port MIDI jako wyjściowy.
  digitalWrite(midiOut, HIGH);             // Ustaw wyskoki stan domyślny portu MIDI.
  pinMode(przyciskSustain, INPUT_PULLUP);  // Deklaruj piny przycisków jako wejście podciągnięte wewnętrznie.
  pinMode(przyciskSoft, INPUT_PULLUP);
  pinMode(przyciskSostenuto, INPUT_PULLUP);
  clock_prescale_set(clock_div_8);  // Ustaw zegar CPU na 1 MHz
}
void loop() {
  if (digitalRead(przyciskSustain) == LOW) {        // Jeśli sustain został wciśnięty...
    wyslijKomunikat(176, 64, 127);                  // Wyślij kontroler sustain on.
    while (digitalRead(przyciskSustain) == LOW) {}  // Czekaj aż przycisk zostanie puszczony.
    wyslijKomunikat(176, 64, 0);                    // Wyślij kontroler sustain off.
  }
  if (digitalRead(przyciskSoft) == LOW) {  // Jeśli sustain został wciśnięty...
    wyslijKomunikat(176, 67, 127);
    while (digitalRead(przyciskSoft) == LOW) {}
    wyslijKomunikat(176, 67, 0);
  }
  if (digitalRead(przyciskSostenuto) == LOW) {  // Jeśli sostenuto został wciśnięty...
    wyslijKomunikat(176, 66, 127);
    while (digitalRead(przyciskSostenuto) == LOW) {}
    wyslijKomunikat(176, 66, 0);
  }
}
void wyslijKomunikat(byte bajt1, byte bajt2, byte bajt3) {  // Wyślij kolejne trzy bajty.
  wyslijBajt(bajt1);
  wyslijBajt(bajt2);
  wyslijBajt(bajt3);
  delay(5);  // Zabezpieczenie na drgające styki.
}
void wyslijBajt(byte bajt) {          // Procedura wysyłająca bajt przez MIDI.
  clock_prescale_set(clock_div_1);    // Przywróć pełną szybkość procesora.
  digitalWrite(midiOut, LOW);         // Bit startu.
  delayMicroseconds(opoznienie);      // Opóźnienie wyznaczające szybkość transmisji.
  digitalWrite(midiOut, (bajt & 1));  // Kolejne bity danych.
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 2));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 4));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 8));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 16));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 32));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 64));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 128));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, HIGH);        // Bit stopu - stan domyślny portu MIDI.
  delayMicroseconds(opoznienie * 4);  // Dłuższe opóźnienie po wysłaniu ostatniego bitu.
  clock_prescale_set(clock_div_8);    // Zwolnij procesor celem zmniejszenia zużycia energii.
}

Program będzie tym razem napisany w opozycji do zasady, o której mówiłem ostatnio – żadnych eleganckich uproszczeń, piszemy jak przedszkolak. Dlaczego? Powody są dwa.

Po pierwsze: ten mały kontroler nie ma portu szeregowego. Można go napisać na przerwaniach, ale… po co? W dwa kilobajty nie będziemy wsadzać złożonych kodów, urządzenie jest wybitnie jednowątkowe. Procedura wysyłania komunikatu MIDI jest tutaj szeregowa: wysłanie bitu i 32 mikrosekundy przerwy. I tak dziesięć razy, bo oprócz ośmiu bitów danych na początku musi wyjść inicjujący bit startu, będący zerem, a na końcu – bit stopu – jedynka. Metoda nieelegancka ma jedną zaletę: wprowadza minimalne opóźnienia, czyniąc taką prymitywną transmisję asynchroniczną odporną na zakłócenia. Wprowadzenie pętli i warunków może być już odbierane z błędami, wszak Arduino, jeszcze z zegarem 8 MHz, nie jest sprinterem.

Po drugie, obsługując najwyżej cztery różne akcje – bo tyle mamy portów wejściowych po zarezerwowaniu jednego na transmisję i pozostawienie resetu w spokoju, nie ma sensu tworzyć tabel, zwłaszcza że w razie potrzeby można pod któreś z wejść wrzucić zupełnie inny komunikat czy całą akcję. Zaraz sobie przeanalizujemy ten program, najpierw jednak do układu dołóżmy pomoc naukową: amperomierz.

Przepuścimy przez niego napięcie zasilające nasz mały scalak. Miernik wskazuje 5 mA. To mało i dużo zarazem. Dużo, gdybyśmy chcieli zasilać układ z baterii. Zróbmy coś z tym. Wykorzystamy bibliotekę zarządzającą energią i skorzystamy z jednej opcji: zwolnienia wewnętrznego generatora z domyślnych 8 MHz do jednego. Pobór prądu spadnie wówczas do niecałych 2 mA. Układ jednak można zasilać z trzech woltów i wówczas układ skonsumuje około 700 uA, a to pozwoli zasilać całość pastylką litową przez około 300 godzin. Można pójść jeszcze dalej, usypiając kontroler, ale to pozostawię sobie na inną okazję, bo taka wartość do pełni szczęścia wystarczy, a program skomplikowałby się. Wszystko co istotne znajduje się w tej linii.

clock_prescale_set(clock_div_8);  // Ustaw zegar CPU na 1 MHz

Cyfra na końcu dzieli bazowy zegar. I teraz bardzo ważna rzecz: niech nas nie kusi schodzenie w niższe wartości! Nic już nie zyskamy, a zamordujemy nasz układ w takim sensie, że Uno nie da rady go już zaprogramować. Ósemka jest ostatnią bezpieczną wartością.

Ale zaraz: dlaczego w bloku transmisji z powrotem ustalamy pełną szybkość? Bo okazało się, że przy zwolnionej tak generowane komunikaty były od czasu do czasu odczytywane z błędami. To dla zużycia energii nie ma znaczenia, a stabilność rośnie znacząco.

Jak działa program? Deklarujemy dobra i w głównej pętli trzykrotnie sprawdzamy warunki na wciśnięcie przycisków. Jeśli to nastąpi, przekazujemy do pośredniego podprogramu trzybajtowe komunikaty, a stamtąd lecą już po bajcie, do procedury rozbioru ich na pojedyncze bity. Użyłem tu szybszej metody iloczynu logicznego zmiennych zamiast klasycznego wyłuskiwania bitów. Jak mówiłem, wysyłanie bajtu pracuje przy pełnej szybkości zegara, reszta – przy zegarze zwolnionym ośmiokrotnie, czyli przy 1 MHz.

Pozostaje omówić stałą opoznienie. 32 bierze się z definicji szybkości przesyłu MIDI, ale może się zdarzyć, że będzie trzeba tę wartość skrócić o jeden. Gdyby jakiś instrument czasem nie dogadywał się, wbrew zdrowemu rozsądkowi można tej wartości zrobić korektę. Jeśli chodzi o port MIDI, ponieważ jest programowy, może nim być dowolny pin od zerowego do czwartego.

A propos tych wyjątków, o których mówiłem: ten śliczny Roland nie ma wejścia na pedał sustain. Rzadko się to zdarza i mocno go brakuje, choć przy takiej małej klawiaturze nie byłby aż tak użyteczny. Co ciekawe, instrument nie reagował także na komunikat przesłany przez MIDI mimo deklaracji w opisie. Być może trzeba coś przestawić w menu, jednakże szybszą metodą było wysyłanie komunikaty relase, który ma numer 72.

#include <avr/power.h>           // Dołącz bibliotekę zarządzającą energią.
const byte opoznienie = 32;      // Dobierz opóźnienie dla transmisji szeregowej.
const byte midiOut = 4;          // Adres portu MIDI OUT.
const byte przyciskSustain = 2;  // Adres przycisku sustain.

void setup() {
  pinMode(midiOut, OUTPUT);                // Deklaruj port MIDI jako wyjściowy.
  digitalWrite(midiOut, HIGH);             // Ustaw wyskoki stan domyślny portu MIDI.
  pinMode(przyciskSustain, INPUT_PULLUP);  // Deklaruj piny przycisków jako wejście podciągnięte wewnętrznie.
  clock_prescale_set(clock_div_8);         // Ustaw zegar CPU na 1 MHz
}
void loop() {
  if (digitalRead(przyciskSustain) == LOW) {        // Jeśli sustain został wciśnięty...
    wyslijKomunikat(176, 72, 112);                  // Wyślij kontroler sustain on.
    while (digitalRead(przyciskSustain) == LOW) {}  // Czekaj aż przycisk zostanie puszczony.
    wyslijKomunikat(176, 72, 80);                   // Wyślij kontroler sustain off.
  }
}
void wyslijKomunikat(byte bajt1, byte bajt2, byte bajt3) {  // Wyślij kolejne trzy bajty.
  wyslijBajt(bajt1);
  wyslijBajt(bajt2);
  wyslijBajt(bajt3);
  delay(5);  // Zabezpieczenie na drgające styki.
}
void wyslijBajt(byte bajt) {          // Procedura wysyłająca bajt przez MIDI.
  clock_prescale_set(clock_div_1);    // Przywróć pełną szybkość procesora.
  digitalWrite(midiOut, LOW);         // Bit startu.
  delayMicroseconds(opoznienie);      // Opóźnienie wyznaczające szybkość transmisji.
  digitalWrite(midiOut, (bajt & 1));  // Kolejne bity danych.
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 2));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 4));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 8));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 16));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 32));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 64));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 128));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, HIGH);        // Bit stopu - stan domyślny portu MIDI.
  delayMicroseconds(opoznienie * 4);  // Dłuższe opóźnienie po wysłaniu ostatniego bitu.
  clock_prescale_set(clock_div_8);    // Zwolnij procesor celem zmniejszenia zużycia energii.
}

Tylko że w tym wypadku skrajne wartości były nieodpowiednie: zero zupełnie ucinało dźwięk, a 127 przedłużało go w nieskończoność. Metodą prób i błędów ustaliłem te wartości na 80 i 112. Teraz elektroniczne pianina brzmią jak należy. I to jest właśnie zaleta customizacji. Praktycznie nie ma szans na kupno fabrycznego pedału z możliwością aż tak zaawansowanego programowania, a tu chwila – i mamy dowolną kombinację.

Na koniec połączymy oba projekty powstałe dotąd projekty, czyli zrobimy przełącznik czterech programów używając małego Arduino. Dlaczego czterech, a nie pięciu? Piąty port to domyślnie reset. Można z niego zrobić port wejściowy, ale wówczas stracilibyśmy możliwość programowania układu naszym Uno. Dlatego reset pozostawimy w spokoju.

#include <avr/power.h>                          // Dołącz bibliotekę zarządzającą energią.
const byte opoznienie = 32;                     // Dobierz opóźnienie dla transmisji szeregowej.
const byte midiOut = 4;                         // Adres portu MIDI OUT.
const byte iloscKlawiszy = 4;                   // Liczba klawiszy.
const byte adresKlawiatury[] = { 0, 1, 2, 3 };  // Adresy klawiszy.
const byte numerProgramu[] = { 0, 1, 2, 3 };    // Numery programów przyporządkowane klawiszom w powyższej tablicy.

void setup() {
  pinMode(midiOut, OUTPUT);                     // Deklaruj port MIDI jako wyjściowy.
  digitalWrite(midiOut, HIGH);                  // Ustaw wyskoki stan domyślny portu MIDI.
  for (byte x = 0; x &lt; iloscKlawiszy; x++)      // Dla każdego pstryczka...
    pinMode(adresKlawiatury[x], INPUT_PULLUP);  // Deklaruj pin jako wejście podciągnięte wewnętrznie do wysokiego stanu.
  clock_prescale_set(clock_div_8);              // Ustaw zegar CPU na 1 MHz
}
void loop() {
  for (byte x = 0; x &lt; iloscKlawiszy; x++)               // Wybieraj kolejne klawisze.
    if (digitalRead(adresKlawiatury[x]) == LOW) {        // Jeśli kolejny przycisk został wciśnięty...
      wyslijBajt(192);                                   // Wyślij komunikat "zmień program" na kanale pierwszym.
      wyslijBajt(numerProgramu[x]);                      // Wyślij numer programu.
      delay(5);                                          // Zabezpieczenie na drgające styki.
      while (digitalRead(adresKlawiatury[x]) == LOW) {}  // Czekaj aż przycisk zostanie puszczony.
      delay(5);                                          // Zabezpieczenie na drgające styki.
    }
}
void wyslijBajt(byte bajt) {          // Procedura wysyłająca bajt przez MIDI.
  clock_prescale_set(clock_div_1);    // Przywróć pełną szybkość procesora.
  digitalWrite(midiOut, LOW);         // Bit startu.
  delayMicroseconds(opoznienie);      // Opóźnienie wyznaczające szybkość transmisji.
  digitalWrite(midiOut, (bajt & 1));  // Kolejne bity danych.
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 2));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 4));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 8));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 16));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 32));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 64));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, (bajt & 128));
  delayMicroseconds(opoznienie);
  digitalWrite(midiOut, HIGH);        // Bit stopu - stan domyślny portu MIDI.
  delayMicroseconds(opoznienie * 4);  // Dłuższe opóźnienie po wysłaniu ostatniego bitu.
  clock_prescale_set(clock_div_8);    // Zwolnij procesor celem zmniejszenia zużycia energii.
}

Skorzystamy z projektu z artykułu, w którym wystąpił przełącznik brzmień. Choć mówiłem: nie będzie tablic, jednak będą. Skoro tam były, bez sensu byłoby z nich rezygnować. Zatem mamy dwie tablice, z numerami przycisków i odpowiadającą z numerami programów. Tutaj mamy wartości kolejne, ale możemy wpisać co nam pasuje. Odwoływania do tablic napotkamy dwa razy: w części wstępnej, przy definiowaniu portów i w pętli głównej, przy odpytywaniu klawiatury. Jeśli któryś z przycisków będzie wciśnięty, wyślemy 192 – czyli zmień program na kanale pierwszym oraz ów numer programu pobrany z tablicy. Reszta nie różni się niczym od poprzedniego szkicu.

To oczywiście tylko przykłady, które można rozbudować. Pragnę zwrócić uwagę na fakt użycia takich małych układów nie tylko w tym projekcie. Często będą robić wszystko, czego potrzebujemy, a kosztują grosze i wymagają dosłownie zero elementów dodatkowych, nie licząc kondensatora na zasilaniu, który w dobrym tonie zawsze się zakłada.

Powiązane tematy

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2Sprawdź tutaj

Przeczytaj również

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2025 arduino.pl