[W] Własny kontroler MIDI, cz. 4

Przetestowałem już większość "normalnych" kontrolek, pora zatem przyjrzeć się innym, które być może nieco ubarwią czy uprzyjemnią tworzenie muzyki. Zanim jednak przejdę do sterowania MIDI, najpierw krótka prezentacja, jak można za pomocą Arduino rozwiązać problem komunikacji z użytkownikiem, np. wyświetlić mu jakieś wartości czy komunikat. Wprawdzie w części drugiej pokazywałem obsługę diód, którymi też można wiele zakomunikować, tym razem przyjdzie pora na bardziej zaawansowane elementy.

Mały ekran LCD

Początkowo myślałem o wyświetlaczach segmentowych, ale ostatecznie okazało się, że bardziej ekonomicznym i w dodatku prostszym w programowaniu będzie dwuwierszowy ekran LCD. Model HD44780, który jest w stanie wyświetlić 16 znaków w każdym wierszu, można dodatkowo kupić ze specjalnym konwerterem i2C 1602. W tej sytuacji cała obsługa sprowadza się do połączenia dwóch pinów SDA i SCL między konwerterem a Arduino (oczywiście, do konwertera przypinamy także zasilanie i masę):

Dwie linie po 16 znaków to niedużo, ale już coś można wyświetlić

Tak przygotowane ustrojstwo wygląda może dziwacznie w postaci prototypu, natomiast po zaimportowaniu biblioteki Bonezegei_LCD1602_I2C programowanie nie nastręcza żadnych problemów:

#include <Bonezegei_LCD1602_I2C.h>

Bonezegei_LCD1602_I2C lcd(0x27);

void setup() {
  lcd.begin();
  lcd.print("Gades Controller");
  lcd.setPosition(0, 1);      
  lcd.print("HD44780");
  delay(2000);
  lcd.setPosition(0, 0);
  lcd.print("GadesMusic      ");
}

void loop() {
  lcd.setPosition(0, 1);
  String str = "ms:";
  str += millis();
  lcd.print(str.c_str());
  delay(500);
}

Używanie wyświetlacza sprowadza się do ustalenia "pozycji" wstawiania znaków za po mocą metody setPosition() obiektu Bonezegei_LCD1602_I2C i wypisaniu tekstu metodą print(). Metoda setPosition() przyjmuje dwa parametry - współrzędne kolumny i wiersza. Warto tylko pamiętać, że print() wyrysuje/zmieni tylko tyle znaków na wyświetlaczu, ile jej wyślemy, ważne jest zatem, by kasować istniejące wcześniej znaki spacjami.

Światło na usługach muzyka

W całym morzu ciekawych czujników, możliwych do podłączenia do Arduino, wypatrzyłem płytkę VL6180X, czyli Time-of-flight Distance Sensor. Znający angielski już przeczuwają, o co może chodzić. Tak, to czujnik mierzący odległość za pomocą światła (niby laserowego). Prócz tego czujnik jest w stanie także mierzyć w ogóle jasność światła, chociaż zastosowania tego w muzyce nie potrafię sobie wyobrazić - chyba że oświetlać taki czujnik światłem mocnej latarki i tak wywoływać komunikaty MIDI? Hm...

Wracając do sprawy - czujnik nie jest skomplikowany, bo wykorzystamy tylko cztery piny: masę, +5V oraz sygnały SCL i SDA. Połączenie zatem nikomu nie powinno sprawić trudności: SCL, SDA i GND z płytki VL6180X do tak samo nazwanych pinów w Arduino, zaś VIN to VCC w Arduino. I w kwestii sprzętu to tyle, gorzej z programem.

Taka niepozorna płytka, a tyle radości z gry!

Próbowałem najpierw napisać coś swojego "na czuja", ale mimo korzystania z zalecanej biblioteki Adafruit_VL6180X nie szło mi to za dobrze. Kod się wprawdzie niby kompilował, ale przy wgrywaniu na płytkę coś szło nie tak i w efekcie traciłem bootloader, co z kolei zmuszało mnie do jego przywracania (procedurę przywracania opisałem w części drugiej arduinowego serialu).

W efekcie, aby szybko się przekonać, czy układ w ogóle działa, posłużyłem się po prostu przykładem dostarczanym z biblioteką i tu już wszystko poszło jak należy. Wzorując się na tym kodzie, napisałem wreszcie coś swojego i do tego działającego:

#include <MIDIUSB.h>
#include <Wire.h>
#include <Adafruit_VL6180X.h>

Adafruit_VL6180X vl = Adafruit_VL6180X();
int last_state = 0;
const byte CHANNEL = 0;
const byte CC = 1;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(1);
  }
  if (! vl.begin()) {
    while (1);
  }
}

void loop() {
  uint8_t range = vl.readRange();
  uint8_t status = vl.readRangeStatus();
  if (status == VL6180X_ERROR_NONE) {
    int curr_state = range;
    int diff = abs(curr_state - last_state);
    if (diff > 0) {
      int value = map(curr_state, 0, 200, 0, 127);
      last_state = curr_state;
      controlChange(value);
    }
  }
  delay(50);
}

void controlChange(byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | CHANNEL, CC, value};
  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}

Kluczowy okazał się prawdopodobnie kod w metodzie setup(), bo tego u siebie nie miałem w ogóle. W każdym razie całość zadziałała na tyle dobrze, że musiałem przerwać zabawę Arduino na rzecz zabawy w granie ręką niczym na Thereminie. Uruchomiłem w Reaperze syntezator Diva i przypisywałem różne parametry do komunikatu CC wysyłanego przez czujnik odległości. Frajdy było przy tym nie mniej, niż mam z gry na Breath Controller i coś czuję, że tego typu sterowanie musi się znaleźć w moim kontrolerze.

Ultradźwięki na usługach muzyka

Przed chwilą opisywałem sterowanie parametrem MIDI za pomocą mierzenia odległości laserem - tym razem pomierzymy odległość... ultradźwiękami. Tak, tak, zupełnie jak nietoperze. Jest do tego specjalny moduł o symbolu HC-SR04 i budowa prostego kontrolera korzystającego z ultradźwięków jest w sumie banalna:

Wygląda kozacko, ale raczej nie zostanie wbudowany w mój kontroler

Gorzej z programem, bo ten - mimo zastosowania biblioteki HCSR04 jest spory, a na dodatek niedoskonały:

#include <HCSR04.h>
#include <MIDIUSB.h>

const byte TRIGGER = 4;
const byte ECHO = 5;
int last_value = 127;
const int CHANNEL = 0;
const int CC = 1;
const int MAX_DIST = 60;
const int MIN_DIST = 5;

void setup() {
  Serial.begin(115200);
  HCSR04.begin(TRIGGER, ECHO);
}

void loop() {
  double* distance = HCSR04.measureDistanceCm();
  int curr_value = (int) distance[0];
  if (curr_value > MAX_DIST) {
    curr_value = MAX_DIST;
  } else if (curr_value < MIN_DIST) {
    curr_value = MIN_DIST;
  }
  curr_value = map(curr_value, MIN_DIST, MAX_DIST, 0, 135);
  int diff = abs(last_value - curr_value);
  if (diff > 3 && curr_value < 127) {
    controlChange(curr_value);
    last_value = curr_value;
  }
  delay(20);
}

void controlChange(byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | CHANNEL, CC, value};
  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}

Nie wiem, czy to wina sposobu pomiaru, biblioteki czy niedoskonałego działania kupionego czujnika, ale mierzone wartości zachowują się... dziwnie. To dlatego po pierwsze pojawiło się "kalibrowanie" odczytanej wartości na zakres 5-80 - kiedy za bardzo zbliżałem dłoń do czujnika, wartości potrafiły spaść poniżej 0 albo nagle wskoczyć na wartości bardzo duże, powyżej 200 na przykład. Z kolei ręką nie sięgam dalej niż 60-70 centymetrów od czujnika, więc uznałem, że to będzie użyteczny zakres.

Dziwić może skalowanie odczytu do zakresu 0-135 zamiast 0-127. To też wynika z kiepskiej stabilności odczytów czujnika - czasem pojawiają się odczyty bardzo duże, osiągające maksimum lub mu bardzo bliskie. Próbowałem z tym walczyć, badając różnicę między kolejnymi odczytami (zmienna diff), ale ograniczenie różnicy wpływało z kolei na niemożliwość szybkiej celowej zmiany parametru. Przeskalowałem zatem zakres tak, by te największe wartości przekroczyły zakres 127 i po prostu je "odfiltrowałem". Powyższa wersja działa jako tako, ale raczej nie planuję użyć jej na serio w swoim kontrolerze - czujnik laserowy daje dużo stabilniejsze i pewniejsze wyniki, a najważniejsze - nie "tyka". Bo niby to są ultradźwięki, a ja i tak słyszę delikatne "tykanie" czujnika. Ciekawa sprawa.

Grawitacja na usługach muzyka

Był laser, były ultradźwięki - czas na grawitację. Okazało się, że bardzo niedrogo można zakupić akcelerometry i żyroskopy, które umożliwią odczyt położenia czy ruchu w trzech osiach naraz. Czyż mogłem się oprzeć takiej pokusie? Wiadomo, że nie i nabyłem do testów od razu dwa takie czujniki: MPU6050 oraz ADXL345. Cel - modyfikacja jednocześnie trzech różnych parametrów CC podczas poruszania dłonią (no, w zasadzie podczas poruszania czujnikiem, ale oczami wyobraźni już widziałem specjalne rękawice z czujnikami na każdym z palców). Po zmontowaniu całość nie prezentuje się jakoś rewelacyjnie:

Czujnik w stanie spoczynkowym, za to widać nowy nabytek - większą płytkę stykową

Za to program już musiał być nieco dłuższy:

#include <MIDIUSB.h>
#include <Wire.h>
#include <MPU6050_light.h>

const int CHANNEL = 0;
const int CCx = 1;
const int CCy = 3;
const int CCz = 21;

MPU6050 mpu(Wire);
unsigned long timer = 0;

int last_x = 0;
int last_y = 0;
int last_z = 0;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  byte status = mpu.begin();
  while (status != 0) { }
  delay(1000);
  mpu.calcOffsets();
}

void loop() {
  mpu.update();
  if ((millis() - timer) > 10) {
    last_x = midiValue(mpu.getAngleX(), CCx, last_x);
    last_y = midiValue(mpu.getAngleY(), CCy, last_y);
    last_z = midiValue(mpu.getAngleZ(), CCz, last_z);
    timer = millis();  
  }
}

void controlChange(byte cc, byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | CHANNEL, cc, value};
  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}

int midiValue(float value, byte cc, int last_value) {
  int tempValue = (int) value;
  if (tempValue < -100) {
    tempValue = -100;
  } else if (tempValue > 100) {
    tempValue = 100;
  }
  tempValue = map(tempValue, -100, 100, 0, 127);
  if (last_value != tempValue) {
    controlChange(cc, tempValue);
  }
  return tempValue;
}

Metoda startowa setup() bada, czy czujnik jest w ogóle podłączony i jeśli tak, to dokonuje wstępnej kalibracji. Potem już wskakujemy do głównej pętli loop() i tam kluczowe jest wywoływanie update() na obiekcie czujnika, co powoduje każdorazowe odczytanie wszystkich parametrów. A jako że wartości parametrów bywają i ujemne, i dodatnie, tym razem dodałem całą metodę midiValue(), która przelicza zmiennoprzecinkową wartość parametru na wartość zgodną z MIDI (0-127), uprzednio trymując wartości z czujnika do zakresu od -100 do 100. I tradycyjnie, jeśli wartość uległa zmianie od poprzedniego odczytu, zostaje wysłana jako CC o odpowiednim kodzie (tutaj przyjąłem CC1 dla osi X, CC3 dla osi Y i CC21 dla osi Z).

I powiem Wam, że z tych trzech sposobów - laser, ultradźwięki, żyroskop - wybieram właśnie ten trzeci. Naprawdę, zabawa z tym przednia, nawet Perełka i Żona dały się skusić do zabawy i kręciły czujnikiem, modyfikując brzmienie z syntezatora u-he Diva. Teraz dumam tylko, jak rozwiązać połączenie czujnika z kontrolerem - musi to być jakiś przewód z czterema żyłami, ale jednocześnie na tyle elastyczny i wiotki, by nie przeszkadzał w "wywijaniu" czujnikiem. Pewnie wybebeszę po prostu jakiś kabelek USB w plecionce, one bywają odpowiednio elastyczne...

To jeszcze nie koniec

Mimo że obecny odcinek to już czwarta odsłona arduinowego serialu, wciąż daleko do końca. W piątej części jeszcze zaprezentuję to i owo w warunkach testowych, a potem przystąpię do budowy. Wydaje mi się, że konieczne będą dwa kontrolery - jeden bardziej klasyczny, czyli cztery suwaki i cztery potencjometry, a drugi bardziej "odjechany", z panelem XY czy właśnie czujnikiem żyroskopowym. Ten drugi będzie kontrolerem w rodzaju Breath Controller, czyli będzie podłączany na specjalne okazje, gdy potrzeba będzie "dziwnych manipulacji" dźwiękiem i w użyciu znajdą się syntezatory. Strasznie jestem ciekaw, co z tego wszystkiego wyniknie, bo robienie prostych prototypów to jedno, a połączenie tego w kontroler to zupełnie inna sprawa.

Komentarze