Autor: daniellos
Redakcja: Dondu
Zobacz inne artykułu z cyklu: Arduiono ... czyli prościej się nie da :-)
Kontrolery MIDI używane są głównie przez muzyków do przesyłania informacji pomiędzy urządzeniami (np. jaką nutę zagrać w danej chwili, jaki przycisk został naciśnięty).
Przykładem może być klawiatura sterująca, która wygląda jak keyboard, ale służy do komunikowania się z innym urządzeniem, np. komputerem, który przyjmuje sygnały MIDI i odtwarza zadane przez nią dźwięki. Zatem sama klawiatura nie wydobywa dźwięku, ale robi to za pomocą komputera.
W tym artykule chcę przedstawić kontroler MIDI domowej roboty oparty na Arduino. Moim celem było zrobienie kontrolera efektów do programu Traktor Pro (program do miksowania muzyki), ale może być dowolnie programowany i używany przez inne programy obsługujące standard MIDI. W tym przypadku urządzenie nie wysyła informacji dotyczących wciśnięcia danej nuty, ale powiadamia o zmianie stanu poszczególnych elementów na urządzeniu (CC, Control Change).
Budowa
Kontroler zawiera 7 programowalnych przycisków (B1-B7), 2 potencjometry (P1, P2) i panel dotykowy. Ósmy przycisk służy do zmiany trybu kontrolera (Shift mode on\off), więc można zaprogramować 2 razy więcej zdarzeń niż mamy dostępnych fizycznie elementów w urządzeniu. Oto rysunek poglądowy urządzenia (numery w nawiasach oznaczają piny w Arduino, do których podłączony jest dany element):
Rysunek poglądowy |
Zielona dioda shift świeci się w zależności od stanu trybu shift, a dwie niebieskie diody pod panelem dotykowym służą do wizualizacji miejsca naciśnięcia panelu na osi X. Odpowiednio ściemniając się lub rozjaśniając - taki bajer :)
Jako obudowy użyłem drewnianego pudełka, które znalazłem w domu.
Pudełko i płytka uniwersalna |
Pudełko z otworami |
Urządzenie jest właściwie modułem podłączanym pod płytkę Arduino Uno R3, zlutowanym na płytce uniwersalnej, z gniazdami goldpin umożliwiającymi szybki montaż i demontaż Arduino.
Moduł na płytce uniwersalnej |
Moduł, tył |
Moduł, gniazda |
Panel dotykowy posiada złącze ZIFF, a więc potrzebna była przejściówka na goldpin, której zrobienie zleciłem osobie zajmującej się takimi rzeczami, gdyż nie znalazłem sklepu, w którym mógłbym ją kupić.
Przejściówka |
Wszystkie elementy umieszczone są w pudełku z wyjątkiem panelu, który postanowiłem położyć na pudełku i docisnąć odpowiednio pozaginaną blachą. Pod panelem umieściłem piankę (która z resztą była w opakowaniu z panelem). Dystansuje ona panel od powierzchni pudełka, dzięki czemu można umieścić diody pod spodem, a światło rozchodzi się dając całkiem fajny efekt.
Profil z blachy |
Profil i panel |
Wszystkie części |
Na koniec pudełko okleiłem folią carbon (używaną do oklejania samochodów), żeby "jakoś to wyglądało" :)
Gotowy kontroler |
Gotowy kontroler |
USB |
Wnętrze |
Wnętrze |
Oświetlenie panelu dotykowego |
Lista użytych części
- Arduino Uno R3
- Panel dotykowy LCD-AG-TP-240128S
- Przejściówka ZIFF <-> goldpin
- 2 potencjometry 10k
- 8 przycisków tact switch 12mm z klawiszem
- 3 diody LED (zielona, 2 niebieskie)
- Rezystory: 8x 10K, 3x150R
- Gniazda i wtyczki goldpin + styki do wtyków (męskie i żeńskie)
- 1mb taśmy 10-żyłowej do zrobienia przewodów
- 8 pinów z listwy kołkowej goldpin
- Trochę kabla telefonicznego do połączenia lutów na płytce uniwersalnej
- Śruby, nakładki
- Gumowe nóżki samoprzylepne pod obudowę (aby się nie ślizgała)
- Drewniane pudełko
- Folia carbon
Schemat
Na schemacie przedstawiony jest sposób podłączenia poszczególnych elementów do płytki Arduino. Proszę zwrócić uwagę, że potencjometry podłączone są do innego pinu uziemienia ze względu na zakłócenia jakie powstawały przy jednym, wspólnym pinie GND. Podłączenie przycisków na schemacie nie jest proste jak można by się spodziewać, taka kolejność była korzystniejsza ze względu na sposób lutowania połączeń na płytce uniwersalnej.
Schemat |
Kod źródłowy
Przejdźmy teraz do ciekawszej kwestii jaką jest kod źródłowy. W programie korzystam z osobnego modułu TouchScreen.h oraz biblioteki MIDI.h
TouchScreen.h - obsługa panelu dotykowego
Klasa TouchScreen zawiera konstruktor, który jako parametry przyjmuje 4 wejścia analogowe, funkcję read() do odczytu współrzędnych (X,Y) oraz touched() do sprawdzenia czy panel jest w ogóle dotknięty.
Rezystancyjny panel dotykowy zbudowany jest z dwóch folii rezystancyjnych ułożonych jedna pod drugą. Na jednej folii, na przeciwległych brzegach, umieszczone są elektrody wzdłuż OX, a na drugiej wzdłuż OY.
Źródło: Texas Instruments |
Odczyt współrzędnych polega na użyciu prądu stałego na jednej osi i odczyt spadku napięcia używając elektrody drugiej osi. Jako, że musimy odczytać wartości współrzędne z dwóch osi, trzeba zmieniać tryby pracy odpowiednich pinów. Przydatna jest tutaj informacja, że analogowe piny w Arduino mogą pełnić rolę pinów cyfrowych.
#include "TouchScreen.h" TouchScreen::TouchScreen(int pinX1, int pinX2, int pinY1, int pinY2) { PIN_X1 = pinX1 + OFFSET; PIN_X2 = pinX2 + OFFSET; PIN_Y1 = pinY1 + OFFSET; PIN_Y2 = pinY2 + OFFSET; } void TouchScreen::read(int *coordinates) { //Konfiguracja do odczytu współrzędnej X pinMode(PIN_X1, INPUT); pinMode(PIN_X2, INPUT); pinMode(PIN_Y1, OUTPUT); digitalWrite(PIN_Y1, LOW); // Y1 jako GND pinMode(PIN_Y2, OUTPUT); digitalWrite(PIN_Y2, HIGH); //Y2 jako +5V delay(1); coordinates[0] = analogRead(PIN_X2 - OFFSET); //odczyt X //Konfiguracja do odczytu współrzędnej Y pinMode(PIN_Y1, INPUT); pinMode(PIN_Y2, INPUT); pinMode(PIN_X2, OUTPUT); digitalWrite(PIN_X2, LOW); pinMode(PIN_X1, OUTPUT); digitalWrite(PIN_X1, HIGH); delay(1); coordinates[1] = analogRead(PIN_Y1 - OFFSET); //odczyt Y } boolean TouchScreen::touched() { //Konfiguracja do wykrycia dotknięcia pinMode(PIN_X1, INPUT); pinMode(PIN_Y2, INPUT); pinMode(PIN_X2, OUTPUT); digitalWrite(PIN_X2, LOW); pinMode(PIN_Y1, OUTPUT); digitalWrite(PIN_Y1, HIGH); return analogRead(PIN_X1) > 0; }MIDI.h
Biblioteka MIDI służy do wysyłania i odbierania komunikatów MIDI za pomocą portu szeregowego.
Dokumentacja: http://arduinomidilib.sourceforge.net/index.html
Program główny
Na początku nazywam numery pinów bardziej intuicyjnymi nazwami, definiuję zmienne globalne i deklaruję funkcje pomiarowe.
setup() - wykonywana raz przy starcie urządzenia
MIDI.begin() odpowiada za uruchomienie komunikacji MIDI na porcie szeregowym dlatego też nie możemy wtedy używać portu szeregowego do wypisywania jakiegoś tekstu w monitorze portu za pomocą np. Serial.print(). Dalej mamy ustawienie pinów do odpowiednich trybów i efekt rozjaśniania LEDów pod panelem dotykowym przy włączaniu.
Funkcja loop() - główna pętla, wykonywana cały czas
Tutaj następuje sprawdzenie każdego elementu urządzenia pod kątem zmian i ewentualne wysłanie komunikatu.
Jak wiadomo, kod w loop() to nieskończona pętla, która w czasie od naciśnięcia przycisku do jego zwolnienia może wykonać się kilka razy! Tak więc samo sprawdzenie czy dany przycisk jest naciśnięty i wysłanie komunikatu MIDI spowoduje, że zostanie on wysłany nawet kilkadziesiąt razy, a nie chcemy tego.
Można to rozwiązać dodając pustą pętlę zaraz po wysłaniu sygnału, która wykonuje się dopóki przycisk jest trzymany.
Pojawia się następujący problem: Nie możemy wtedy jednocześnie użyć innego przycisku, potencjometru ani panelu. Program nie robi nic innego jak czeka na zwolnienie przycisku.
Rozwiązanie: Tablica active[] typu boolean zawierająca stany przyciśnięcia danego elementu. Jeżeli przyciśniemy przycisk to zostanie mu przypisana wartość true, wysłany będzie komunikat MIDI i nastąpi dalsze wykonywanie programu. Przy ponownym pomiarze tego przycisku, jeśli będzie on ciągle wciśnięty to wiemy, żeby nie wysyłać ponownie komunikatu. Jeśli natomiast okaże się, że zwolniliśmy przycisk to przypiszemy mu wartość false. Dla potencjometrów zapamiętujemy wartość ostatniego pomiaru i sygnał MIDI jest wysyłany, gdy pomiar różni się od poprzedniego.
Powracając do struktury funkcji loop(), najpierw mamy czytanie współrzędnych XY jeśli panel jest dotknięty. Dalej pomiary potencjometrów, przycisków mapowalnych i trybu shift. Na koniec sprawdzane jest czy chcemy przejść do trybu mapowania.
Tryb mapowania
Zazwyczaj programy obsługujące interfejs MIDI posiadają możliwość ręcznego wprowadzenia który przycisk na urządzeniu odpowiada przyciskowi w programie lub mapowania poprzez uczenie, czyli wybieramy element w programie, a potem naciskamy przycisk na urządzeniu, który ma być przypisany do wybranego elementu.
Druga opcja jest praktycznie niemożliwa do wykonania w przypadku panelu dotykowego, ponieważ (w moim programie jak i zapewne w większości przypadków) sygnały dla współrzędnych wysyłane są bezpośrednio po sobie, cały czas do momentu zwolnienia (wtedy też wysyłany jest sygnał informujący o tym), a więc w ten sposób mamy możliwość zaprogramowania tylko zdarzenia dotknięcia/zwolnienia panelu.
Dlatego też kontroler posiada tryb mapowania, służący do wysłania pojedynczego komunikatu dla osi X lub Y. Działa to w następujący sposób: Przez 3 sekundy trzeba jednocześnie trzymać wciśnięte przyciski B1, B2, B5, B6. Kiedy diody pod panelem zaczną mrugać, wtedy naciśnięcie B3 spowoduje wysłanie sygnału dla OX, a B7 dla OY. Po naciśnięciu któregoś tych dwóch przycisków następuje wyjście z trybu mapowania.
Kod (plik główny - XYcontroller.ino):
//********************************** // Daniel Baczyński // Kontroler XY z panelem dotykowym //********************************** #include <TouchScreen.h> #include <MIDI.h> //potencjometry #define pinK1 A4 #define pinK2 A5 //przyciski #define pinB1 6 #define pinB2 5 #define pinB3 3 #define pinB4 12 #define pinB5 7 #define pinB6 4 #define pinB7 2 #define pinBShift 8 //diody LED #define pinLEDshift 9 #define pinLEDright 11 #define pinLEDleft 10 #define LEDstandby //stan trybu shift boolean shiftMode = false; #define SHIFT_OFFSET 50 unsigned long long pressTime; //tablica active zawiera stany przyciśnięcia dla poszczególnych przycisków (patrz funkcja measureButton) oraz panelu //true - przyciśnięty (jeszcze nie zwolniony) //false - zwolniony boolean active[10] = {0,0,0,0,0,0,0,0,0,0}; #define panel 0 //indeks dla panelu dotykowego to 0 #define shiftIndex 8 #define mapIndex 9 //zmienne pomocnicze dla pomiarów potencjometrów int valueKnob1 = -1; int valueKnob2 = -1; //panel dotykowy //wartosci min i max oznaczają sprawdzone doświadczalnie pomiary przy krawędziach panelu przy użyciu palca TouchScreen ts(3,1,0,2); int minX=50, maxX=965; int minY=120, maxY=880; //deklaracje funkcji pomiarowych void measureButton (int pin, int note, int index); void measureKnob (int pin, int note, int &lastMeas); void setup() { MIDI.begin(1); //nadajemy na pierwszym kanale //ustawienie pinów pinMode(pinB1, INPUT); pinMode(pinB2, INPUT); pinMode(pinB3, INPUT); pinMode(pinB4, INPUT); pinMode(pinB5, INPUT); pinMode(pinB6, INPUT); pinMode(pinB7, INPUT); pinMode(pinBShift, INPUT); pinMode(pinLEDshift, OUTPUT); pinMode(pinLEDright, OUTPUT); pinMode(pinLEDleft, OUTPUT); digitalWrite(pinLEDshift, LOW); //efekt LEDów for (int i=0; i<=255; i++) { analogWrite(pinLEDright, i); analogWrite(pinLEDleft, i); delay(10); } } void loop() { //panel dotykowy - obsługa if (ts.touched()) //jeśli jest dotknięty... { //...i nie wchodzimy w ten blok drugi raz podczas jednego dotknięcia... if (!active[panel]) { //wyślij sygnał, że panel jest dotknięty if (shiftMode) MIDI.sendControlChange(0 + SHIFT_OFFSET, 127, 1); else MIDI.sendControlChange(0, 127, 1); } active[panel] = true; //odczyt współrzędnych int coords[2]; ts.read(coords); coords[0] = map(coords[0], minX, maxX, 0, 127); coords[1] = map(coords[1], minY, maxY, 0, 127); //wyślij sygnał MIDI if (shiftMode) { MIDI.sendControlChange(1 + SHIFT_OFFSET, coords[0], 1); //wsp. X MIDI.sendControlChange(2 + SHIFT_OFFSET, coords[1], 1); //wsp. Y } else { MIDI.sendControlChange(1, coords[0], 1); MIDI.sendControlChange(2, coords[1], 1); } //uaktualnij diody int coordsX = coords[0]*2; analogWrite(pinLEDleft, 255-coordsX); analogWrite(pinLEDright, coordsX); delay (5); } else { if (active[panel]) { //jeśli panel zostanie zwolniony to wyślij sygnał, że panel zwolniony :) if (shiftMode) MIDI.sendControlChange(0 + SHIFT_OFFSET, 0, 1); else MIDI.sendControlChange(0, 0, 1); active[panel] = false; //ustaw poprawnie LEDy digitalWrite(pinLEDleft, HIGH); digitalWrite(pinLEDright, HIGH); } }//koniec obsługi panelu //pomiary potencjometrów delay(5); measureKnob(pinK1, 4, valueKnob1); delay(5); measureKnob(pinK2, 5, valueKnob2); //pomiary przycisków measureButton(pinB1, 11, 1); measureButton(pinB2, 12, 2); measureButton(pinB3, 13, 3); measureButton(pinB4, 14, 4); measureButton(pinB5, 15, 5); measureButton(pinB6, 16, 6); measureButton(pinB7, 17, 7); //przycisk trybu shift if (digitalRead(pinBShift) == HIGH) { if (!active[shiftIndex]) //jeśli pierwsze wejście podczas pomiaru... { //zmień tryb shiftMode = !shiftMode; digitalWrite(pinLEDshift, shiftMode); active[shiftIndex] = true; } } else if (active[shiftIndex]) active[shiftIndex] = false; //sprawdzenie 4 przycisków - tryb mapowania if (digitalRead(pinB1) && digitalRead(pinB2) && digitalRead(pinB5) && digitalRead(pinB6)) { if (!active[mapIndex]) { //zapisujemy czas naciśnięcia pressTime = millis(); active[mapIndex] = true; } else { //jeśli przyciski są trzymane 3 sekundy if ( (millis() - pressTime) >= 3000) { //wysyłanie sygnału MIDI dla OX lub OY unsigned long lightTime = millis(); digitalWrite(pinLEDright, HIGH); digitalWrite(pinLEDleft, HIGH); boolean lights = true; boolean pushed = false; while (!pushed) { if (digitalRead(pinB3)) //OX { pushed = true; if (shiftMode) MIDI.sendControlChange(1 + SHIFT_OFFSET, 0, 1); else MIDI.sendControlChange(1, 0, 1); active[mapIndex] = false; while (digitalRead(pinB3)) {} // aby po wyjściu nie został od razu "naciśnięty" przycisk B3 } else if (digitalRead(pinB7)) //OY { pushed = true; if (shiftMode) MIDI.sendControlChange(2 + SHIFT_OFFSET, 0, 1); else MIDI.sendControlChange(2, 0, 1); active[mapIndex] = false; while (digitalRead(pinB7)) {} // aby po wyjściu nie został od razu "naciśnięty" przycisk B7 } //zmień stan diód if ( millis() - lightTime >= 200 ) { lights = !lights; digitalWrite(pinLEDright, lights); digitalWrite(pinLEDleft, lights); lightTime = millis(); } } // koniec while (!pushed) } } } // koniec trybu mapowania } //koniec loop ------------------------------------------------------------ //funkcja do odczytu stanu potencjometrów //pin - który pin sprawdzić //note - jaką nutę MIDI wysłać //lastMeas - ostatni pomiar przesyłany przez referencję, aby móc go nadpisać void measureKnob (int pin, int note, int &lastMeas) { int meas = 0; //odczytujemy wartość na potencjometrze jednocześnie mapując do zakresu 0-127 //jeśli pomiar jest różny od ostatniego pomiaru (z dokładnością do 2) to powinniśmy wysłać sygnał MIDI analogRead(pin); //pomijamy pierwszy pomiar meas = analogRead(pin); if (abs(meas - lastMeas*8) >=2 ) { if ( (meas = map(meas, 0, 1023, 0, 127)) != lastMeas ) { lastMeas = meas; if (shiftMode) MIDI.sendControlChange(note + SHIFT_OFFSET, meas, 1); else MIDI.sendControlChange(note, meas, 1); } } } //funkcja do odczytu stanu przycisku //pin - który pin sprawdzić //note - jaką nutę MIDI wysłać //index - indeks przycisku w tablicy active void measureButton (int pin, int note, int index) { //jeśli przycisk jest przyciśnięty... if (digitalRead(pin) == HIGH) { //..to sprawdź czy nie jest to drugi pomiar podczas jednego przyciśnięcia if (!active[index]) { //wyślij sygnał przyciśnięta przycisku uwzględniając tryb shift if (shiftMode) MIDI.sendControlChange(note + SHIFT_OFFSET, 127, 1); else MIDI.sendControlChange(note, 127, 1); //oznaczmy przycisk jako naciśnięty, aby zapobiec ponownemu wejściu do tego bloku i wysłaniu sygnału jeszcze raz active[index] = true; } } else if (active[index]) active[index] = false; }
Komunikacja z programem
Nasz program na Arduino komunikuje się z komputerem za pomocą portu szeregowego. Programy obsługujące MIDI nie odczytują danych z takiego portu, ale wybierają kontroler z listy podpiętych urządzeń MIDI.
Jednym z rozwiązań jest utworzenie wirtualnego portu MIDI i wysyłanie do niego danych z portu szeregowego. Za pomocą LoopBe utworzymy "niewidzialny przewód", którego jeden koniec podłączymy do sterowanego programu, a drugi do Hairless MIDI-Serial, który przyjmuje dane z portu szeregowego i wysyła komunikaty MIDI połączeniem utworzonym przez LoopBe.
Należy się upewnić, że stała MIDI_BAUDRATE (domyślnie 31250) w nagłówku MIDI.h jest taka sama jak ustawienie w Hairless MIDI-Serial (domyślnie 115200).
Hairless MIDI - serial |
Do pobrania
Kod źródłowy z bibliotekami:
XYController.rar (kopia)
Dla zainteresowanych umieszczam także ustawienia mapowania do programu Traktor:
XYControler_mapping.tsi (kopia)
Zobacz inne artykułu z cyklu: Arduiono ... czyli prościej się nie da :-)
ile ma mieć wtyka goldpin
OdpowiedzUsuńjesteś w stanie wykonać taki dla mnie prywatnie? tylko obudowe juz zrobie sam:)
OdpowiedzUsuńWitam
OdpowiedzUsuńCzy mógłbyś napisać kod dla controllera,
1. który ma: 10 faderów i 20 przycisków (przy każdym przycisku dioda sygnalizująca jego włączenie.
Z góry dzięki za wielką POMOC w tym artykule.
Adam
djstonka2@gmail.com