[127] MIDI - język nie tylko instrumentów cz. 3

[127] MIDI - język nie tylko instrumentów cz. 3

O teoretycznych aspektach MIDI pisałem w poprzednich artykułach. Wiele zawartej tam wiedzy mogło wydawać się niezrozumiałej i zagmatwanej, dlatego od teraz będzie już tylko praktycznie. Przypomnę, że niezbędna będzie platforma sprzętowa, czyli dowolne Arduino, dwa oporniki i gniazdko DIN bo MIDI używa takiego starożytnego gniazdka do porozumiewania się między kolejnymi urządzeniami.


Ścieżka sygnału wygląda następująco: z pinu pierwszego, czyli portu TX, przez rezystor 220Ω do piątego styku gniazdka MIDI OUT. Czwarty styk tego gniazdka, przez taki sam rezystor 220Ω połączony jest z pięcioma woltami. Gdyby ktoś używał Arduino standardu trzywoltowego, rezystory powinny mieć 33Ω. I to całe lutowanie, prościej chyba się nie da.

Polecam jeszcze pewien dodatek: równolegle do styków dobrze dołożyć ultrajasną diodę czerwoną przez rezystor 3,3kΩ. Nie wpłynie ona zupełnie na pracę urządzeń, a uwidoczni przepływające komunikaty mruganiem. Bywa to przydatne, gdy coś nie gra i nie wiemy gdzie jest problem.

Jak zwykle nasze ćwiczenia będę wykonywał, używając płytki edukacyjnej TME, ale to ze względu na wyposażenie jej w wyświetlacz, przyciski i resztę elementów. Każdy może sobie takie peryferia dołączyć do własnych rozwiązań. Zacznijmy więc od rzeczy najprostszej: klawiaturki MIDI. Tutaj powrócę do artykułu, w którym prezentowałem ideę prościutkiego instrumentu wykorzystującego funkcję tone, która służy do wydawania dźwięków przez Arduino. Mając pięć przycisków, możemy zbudować bardzo prostą klawiaturę, ale da się na niej zagrać na przykład krakowski hejnał.

const byte g3 = 5;  // Adres pstryczka włączającego dźwięk g3
const byte c4 = 4;  // c2
const byte e4 = 8;  // e2
const byte g4 = 7;  // g2
const byte c5 = 6;  // c3

void setup() {
  Serial.begin(31250);        // Inicjuj port z nietypową szybkością 31250 bps
  pinMode(g3, INPUT_PULLUP);  // Deklaruj linie pstryczków jako wejścia podciągnięte wewnętrznie do wysokiego stanu.
  pinMode(c4, INPUT_PULLUP);
  pinMode(e4, INPUT_PULLUP);
  pinMode(g4, INPUT_PULLUP);
  pinMode(c5, INPUT_PULLUP);
}
void loop() {
  while (digitalRead(g3) == HIGH) {      // Jeśli przycisk został wciśnięty...
    noteOn(55);                          // Włącz nutę.
    while (digitalRead(g3) == HIGH) {};  // Czekaj aż przycisk zostanie puszczony.
    noteOff(55);                         // Wyłącz nutę.
  }
  while (digitalRead(c4) == HIGH) {
    noteOn(60);
    while (digitalRead(c4) == HIGH) {};
    noteOff(60);
  }
  while (digitalRead(e4) == HIGH) {
    noteOn(64);
    while (digitalRead(e4) == HIGH) {};
    noteOff(64);
  }
  while (digitalRead(g4) == HIGH) {
    noteOn(67);
    while (digitalRead(g4) == HIGH) {};
    noteOff(67);
  }
  while (digitalRead(c5) == HIGH) {
    noteOn(72);
    while (digitalRead(c5) == HIGH) {};
    noteOff(72);
  }
}
void noteOn(byte wysokoscNuty) {  // Włącz nutę o wysokości note.
  Serial.write(144);              // Wyślij komunikat "włącz nutę" na kanale pierwszym.
  Serial.write(wysokoscNuty);     // Wyślij wysokość nuty.
  Serial.write(64);               // Wyślij głośność nuty.
}
void noteOff(byte wysokoscNuty) {  // Wyłącz nutę o wysokości note.
  Serial.write(128);               // Wyślij komunikat "wyłącz nutę" na kanale pierwszym.
  Serial.write(wysokoscNuty);      // Wyślij wysokość nuty.
  Serial.write(0);                 // Wyślij głośność nuty.
}

Tak więc na początku ponazywamy kolejne klawisze zgodnie ze skalą muzyczną. Początek programu zainicjuje port z nietypową szybkością wymaganą przez MIDI i zadeklaruje klawiaturę.

W głównej pętli spotkamy pięć bliźniaczych bloków. W każdym najpierw będziemy badać, czy dany przycisk nie został wciśnięty. Jeśli tak będzie, przejdziemy do podprogramu obsługującego komunikat włączenia nuty noteOn, przekazując jej wysokość. Tam przechwycimy wartość wysokości i wyślemy trzy bajty: komunikat włączenia nuty na kanale pierwszym, dane o tym która nuta ma być włączona i bajt głośności, czy raczej szybkości wciskania klawisza. Nasze przyciski są prościutkie, więc tutaj szybkość zawsze będzie interpretowana tak samo. Przyjąłem wartość środkową, czyli 64.

Następnie, już w głównej pętli, będziemy oczekiwać na puszczenie przycisku i jeśli to nastąpi, udamy się do podprogramu obsługującego wyłączenie grającego dźwięku. Jest on bliźniaczy, tylko ma przesunięty adres o 16 w dół, bo taki offset mają komunikaty Note Off. No i wartość głośności jest ignorowana – przyjęło się wysyłać zero.

Oczywiście jeśli klawisz będzie pozostawiony w spokoju, program przejdzie do kolejnego bloku i tak do końca. Właściwie to moglibyśmy ten program już skompilować, ale… No właśnie, chyba już najwyższy czas przejść na wyższy poziom programowania. Ten szkic jest prosty do analizy przez początkujących, ale ma mnóstwo powtarzalnych elementów. Spróbujmy wyeliminować dłużyzny na rzecz tablic i indeksów.

const byte iloscKlawiszy = 5;                        // Liczba klawiszy.
const byte adresKlawiatury[] = { 5, 4, 8, 7, 6 };    // Adresy klawiszy.
const byte wysokoscNuty[] = { 55, 60, 64, 67, 72 };  // Wysokości nut przyporządkowane klawiszom w powyższej tablicy.
byte indeks;                                         // Zmienna licznika kolejnych pozycji tablic.

void setup() {
  Serial.begin(31250);                                // Inicjuj port z nietypową szybkością 31250 bps
  for (indeks = 0; indeks < iloscKlawiszy; indeks++)  // Dla każdego pstryczka...
    pinMode(adresKlawiatury[indeks], INPUT_PULLUP);   // Deklaruj pin jako wejście podciągnięte wewnętrznie do wysokiego stanu.
}
void loop() {
  for (indeks = 0; indeks < iloscKlawiszy; indeks++)           // Wybieraj kolejne klawisze.
    if (digitalRead(adresKlawiatury[indeks]) == HIGH) {        // Jeśli kolejny przycisk został wciśnięty...
      noteOn();                                                // Włącz nutę.
      while (digitalRead(adresKlawiatury[indeks]) == HIGH) {}  // Czekaj aż przycisk zostanie puszczony.
      noteOff();                                               // Wyłącz nutę.
    }
}
void noteOn() {                        // Włącz nutę.
  Serial.write(144);                   // Wyślij komunikat "włącz nutę" na kanale pierwszym.
  Serial.write(wysokoscNuty[indeks]);  // Wyślij wysokość nuty.
  Serial.write(64);                    // Wyślij głośność nuty.
}
void noteOff() {  // Wyłącz nutę.
  Serial.write(128);
  Serial.write(wysokoscNuty[indeks]);
  Serial.write(0);
}

Tablice oczywiście już pojawiały wielokrotnie i zawierały elementy, które były wyciągane po kolei – zwykle były to składniki obrazu czy dźwięku. Tymczasem możemy zeń korzystać częściej, wstawiając tam także zmienne innego charakteru. Na przykład – adresy klawiszy. Zwróćmy uwagę na to, że w naszym programie zachodzi ścisła zależność między adresem klawisza i wysokością nuty. Ale zarówno jedno jak i drugie nie da się opisać żadnym prostym wzorem, gdyż są to wartości niekolejne. Innymi słowy, i przyciski nie mają sensownej kolejności, i wysokości dźwięków, które mają wydawać. I właśnie do takich spraw najlepsze są tablice. Przeanalizujmy program.

const byte iloscKlawiszy = 5;                        // Liczba klawiszy.
const byte adresKlawiatury[] = { 5, 4, 8, 7, 6 };    // Adresy klawiszy.
const byte wysokoscNuty[] = { 55, 60, 64, 67, 72 };  // Wysokości nut przyporządkowane klawiszom w powyższej tablicy.
byte indeks;                                         // Zmienna licznika kolejnych pozycji tablic.

Gdzie tablice, tam indeks, czyli wskaźnik pozycji bieżącej. A gdzie indeks, tam maksimum, czyli ilość przycisków. Indeks się zmienia, maksimum jest stałe. W tablicy adresów klawiatury zamieściłem te same wartości, co poprzednio. Zauważmy, że nie mają już muzycznych nazw, bo nie ma takiej potrzeby. W drugiej tablicy siedzą wysokości nut, w kolejności wpisanych wcześniej adresów przycisków.

for (indeks = 0; indeks < iloscKlawiszy; indeks++)  // Dla każdego pstryczka...
  pinMode(adresKlawiatury[indeks], INPUT_PULLUP);   // Deklaruj pin jako wejście podciągnięte wewnętrznie do wysokiego stanu.

We wstępnej części programu – nowość: zamiast wstawiać pięć bliźniaczych deklaracji, mamy pętlę wykonującą się pięć razy, wyłuskującą kolejne adresy przycisków i ustawiające je jako porty wejściowe.

for (indeks = 0; indeks &lt; iloscKlawiszy; indeks++)           // Wybieraj kolejne klawisze.
  if (digitalRead(adresKlawiatury[indeks]) == HIGH) {        // Jeśli kolejny przycisk został wciśnięty...
    noteOn();                                                // Włącz nutę.
    while (digitalRead(adresKlawiatury[indeks]) == HIGH) {}  // Czekaj aż przycisk zostanie puszczony.
    noteOff();                                               // Wyłącz nutę.
  }

W głównej pętli mamy bliźniaczą pętlę, tylko teraz sprawdzamy każdy przycisk według indeksu, czy aby nie został wciśnięty. Jeśli tak, przechodzimy do podprogramu wysyłającego komunikat włączenia nuty. Jest identyczny, z tym że nie przekazujemy tym razem wysokości jak poprzednio, a jest ona pamiętana w globalnej zmiennej indeks. Po to właśnie uczyniłem ją globalną na początku.

Następnie, dopóki klawisz nie zostanie puszczony, program będzie trwał w pętli i gdy to nastąpi, zostanie uwolniony i oddelegowany do obsługi wyłączenia nuty. I tak dalej, dla wszystkich indeksów, aż dojdziemy do maksimum, czyli ilości klawiszy. A potem od początku. Zauważmy jeszcze, że ubyło nawiasów wąsatych. W pętlach for nie są one niezbędne, jeśli mamy w niej tylko jedną instrukcję, przy czym kolejny for albo if możemy tak traktować. Do tej pory tego nie robiłem, bo zaciemnia to logikę, lecz jesteśmy już pro, więc możemy czynić skróty.

Jakie mamy zalety takiego potraktowania problemu? Po pierwsze: program jest krótszy, znacznie. Zajmuje też mniej zasobów, nie jakoś bardzo, bo około pięciu procent, co jest zasługą świetnego kompilatora, ale zawsze to coś. Po drugie, dane o adresach i wysokościach nut siedzą w jednym tylko miejscu. Raz, dwa – można je zmienić. W pierwszym przypadku były porozrzucane i multiplikowane, więc edycja wymagała więcej pracy. No i w końcu pozbyliśmy się nazw własnych klawiatury i powoływania do życia nadmiarowych zmiennych. W tym prostym przypadku nie jest tego dużo, ale dobrze przywyknąć do oszczędności.

A wady jakieś są? Owszem. Idea jest nieodporna na złożone zadania. Jeśli każde zdarzenie będzie inne, np. będzie zawierać innego typu komunikat, system bierze w łeb i często metoda „na piechotę” będzie jedyną sensowną. Można też stosować sposób hybrydowy, czyli bliźniaki – jak tu, a wyjątki w dodatkowych warunkach.

Skompilujmy kod i pobawmy się trochę. Hejnał możemy zagrać, zwłaszcza jak dołożymy trochę efektów. Ale po chwili odkryjemy, że oba szkice miały wady w założeniach: realizują pracę monofoniczną. Po wciśnięciu któregoś z klawiszy program zamarzał w nieskończonej pętli, do chwili jego puszczenia. Jeśli interesuje nas taka właśnie klawiatura, to oczywiście wszystko jest w porządku, ale to są przypadki rzadkie. Najczęściej potrzeba, by każdy klawisz niezależnie od innych włączał i wyłączał nuty. Zatem czekania w pętlach bez końca są tu wykluczone. Ale za naprawę tego zabierzemy się już w kolejnym artykule.

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