Typowe moduły do syntezatorów to układy w pełni analogowe. Nie powinno to dziwić – jest to kluczem do uniwersalności tych układów. Połączenia pomiędzy nimi przesyłają napięcia sterujące oraz oczywiście sygnał analogowy. Istnieje ogromna liczba typów modułów, jakich używa się w takich konstrukcjach. Moduły syntezatorów analogowych można podzielić (z grubsza) na kilka klas:
- generatory (moduły, które generują pojedynczy, ciągły przebieg o kontrolowanej częstotliwości i amplitudzie),
- filtry (analogowe filtry o różnej charakterystyce, także takiej, która może być kontrolowana np. napięciem sterującym),
- modulatory (AM, FM i tzw. modulatory obwiedni, które z ciągłego przebiegu robią pojedynczy „impuls” dźwiękowy, przypominający dźwięk instrumentu – pojedyncze naciśnięcie klawisza pianina, uderzenie werbla, szarpnięcie struny gitary itd.),
- do sterowania systemem używa się modułów zwanych sekwencerami. Są to układy, które taktują cały syntezator bądź jego system, generując zmieniające się krokowo napięcie sterujące lub impulsy wyzwalające, które służą następnie do sterowania innymi modułami.
Autor projektu sięgnął po syntezator analogowy jako pomoc naukową. Na co dzień uczy on elektroniki, inżynierii i robotyki. Dziedziny te są trudne w zademonstrowaniu w łatwy i zrozumiały sposób, szczególnie ze względu na leżącą u ich podstaw matematykę. Syntezator analogowy jest tutaj idealnym narzędziem, gdyż pozwala na zademonstrowanie (w słyszalny sposób) wielu zjawisk, takich jak rezonans, filtry itp. Modułowy syntezator pozwala na zestawianie układu w taki sposób, żeby w czytelny sposób demonstrować szerokie spektrum zjawisk, zachodzących pomiędzy sygnałami i układami elektronicznymi.
Jednym z modułów, które autor uważał za konieczne w swoim systemie, był sekwencer. To kluczowy element wielu syntezatorów. Jednak autor postanowił skonstruować nie byle jaki sekwencer – jego konstrukcja miała realizować tzw. rytm euklidesowski. Termin ten (lub rytm Euklidesa) został po raz pierwszy użyty przez informatyka Godfrieda Toussainta w jego pracy z 2005 roku. Rytmy euklidesowe to zupełnie nowe zjawisko – pomysł tej koncepcji pojawił się u Toussainta w 2004 roku. Inspiracją była hiszpańska muzyka flamenco, która do wykonywania i opisywania rytmów wykorzystuje okrąg. Toussaint odkrył, że można użyć algorytmu Euklidesa (algorytmu do obliczania największego wspólnego dzielnika dwóch liczb, znanego od starożytności i przypisywanego właśnie Euklidesowi) do równego dzielenia dowolnej kombinacji rytmów i że algorytm Euklidesa może być użyty do wyjaśnienia nawet bardzo złożonych rytmów od afrykańskich etnicznych pieśni przez bossa novę czy standardy jazzowe.
Rytmy Euklidesa składają się z N zdarzeń równomiernie rozmieszczonych w M możliwych pozycjach w powtarzającym się wzorze. Alokację zdarzeń określa się za pomocą algorytmu Euklidesa, stosowanego do wyznaczania największych wspólnych dzielników. Takie algorytmiczne podejście czyni system bardzo użytecznym do automatycznego generowania takich rzeczy, jak wzorce perkusyjne. Na rynku dostępnych jest wiele wirtualnych i fizycznych implementacji tego rodzaju generatorów rytmu, które były inspiracją do zbudowania opisanego poniżej urządzenia.
Moduł zaprezentowany w tym artykule bazuje na Arduino Nano i może sterować maksymalnie czterema głosami (wyzwalaczami perkusji, modulatorami obwiedni lub generatorami), wykorzystując przy tym rytm z podziałem na 16 lub mniej. Rytm może być generowany za pomocą wewnętrznego lub zewnętrznego zegara. Liczbę zdarzeń i ich pozycji (N i M) można niezależnie kontrolować, uzyskując interesujące rytmiczne interakcje między poszczególnymi głosami. Projekt panelu przedniego modułu bazuje na formacie Kosmo, bardzo często używanym w modułowych syntezatorach dźwięku.
Elementy potrzebne do budowy sekwencera
Do zestawienia prezentowanego modułu potrzebne są:
- moduł Arduino Nano (lub dowolne inne Arduino, Nano wybrano z uwagi na niewielki rozmiar),
- 16-bitowy Ring LED NeoPixel lub kompatybilny moduł,
- pięć przycisków chwilowych (microswitch),
- miniaturowy przełącznik SPDT,
- potencjometr 100 kΩ o charakterystyce liniowej,
- enkoder obrotowy z przyciskiem,
- pięć monofonicznych złączy duży jack,
- poczwórny wzmacniacz operacyjny TL074 i podstawka DIL14,
- 10-wyprowadzeniowe złącze do zasilania w standardzie Eurorack,
- cztery diody LED 5 mm (czerwona, niebieska, zielona i żółta),
- opornik 220 Ω, sześć oporników 1 kΩ, opornik 2,2 kΩ, opornik 4,7 kΩ, dwa oporniki 10 kΩ,
- przewody połączeniowe, płytka uniwersalna, goldpiny (męskie i żeńskie),
- materiały do budowy panelu (blacha aluminiowa, pleksi itd.).
Ponadto potrzebne będą podstawowe narzędzia – multimetr, lutownica itp. Dodatkowo do wycięcia panelu autor użył plotera laserowego, jednak nie jest to jedyna opcja.
Budowa układu
Projekt rozpoczął się od nowego szkicu Arduino i obwodu testowego, zbudowanego do przeanalizowania pomysłów autora. Prototypowanie układu najlepiej jest przeprowadzić, dodając do systemu na płytce stykowej poszczególne elementy, krok po kroku. Taktyka ta przydaje się szczególnie w przypadku nieznanych nam dotychczas komponentów. W przypadku autora był to moduł NeoPixel oraz enkoder obrotowy. Dlatego też one w pierwszej kolejności trafiły na płytkę stykową (fotografia 1).
Linia danych NeoPixel jest podłączona do cyfrowego pinu 2 w Arduino Nano przez rezystor 220 Ω. Enkoder obrotowy ma dwie cyfrowe linie wyjściowe, które są dołączone do pinów 3 i 4 modułu Arduino, w celu zliczania impulsów wychodzących z enkodera. Pin 5 jest używany wraz z wewnętrznym rezystorem podciągającym w Nano do odczytu stanu przycisku w enkoderze, który zostaje zwarty, gdy pokrętło enkodera zostaje naciśnięte. Przycisk ten zostanie użyty do przełączania się między różnymi trybami pracy w module. Należy pamiętać dodatkowo, że moduł NeoPixel musi być zasilany napięciem 5 V, więc aby go zasilić, należy podłączyć wyjście 5 V i pin masy do Nano. Enkoder ma dwa piny, które należy podłączyć do masy Nano. Są one pokazane na schemacie obwodu na rysunku 1.
Po uruchomieniu tych dwóch komponentów autor dodał przyciski chwilowe do przełączania między czterema wyzwalaczami. Przypisanie każdemu przyciskowi osobnego pinu cyfrowego może być kuszące, ale wyjścia układu są bardzo cenne w takim projekcie. Kiedy mamy do czynienia z kilkoma przyciskami, można połączyć je szeregowo z rezystorami, które stają się dzielnikiem napięcia. Napięcie wyjściowe z dzielnika różni się w zależności od tego, który przycisk jest wciśnięty i można je łatwo zdekodować za pomocą jednego z wejść analogowych w module Arduino Nano. To zgrabna sztuczka, która pomoże wycisnąć więcej z dostępnych w Arduino linii wejścia/wyjścia. Jeśli sam opis tego połączenia nie jest zrozumiały, to warto spojrzeć na schemat na rysunku 1, gdzie jest to czytelniej narysowane.
Finalnym dodatkiem do systemu był potencjometr, który pozwala na zmianę szerokości impulsu wyjściowego. To ważna funkcja do dodania, gdyż zwiększa elastyczność łączenia modułu z innymi modułami w syntezatorze. W ten sposób można wyzwalać inne urządzenia krótkim impulsem albo użyć dłuższego impulsu bramki jako np. obwiedni dla sygnału audio. Potencjometr zapewnia taką elastyczność.
Aby przetestować płytki prototypowe, autor po prostu podłączył zworki do odpowiednich pinów wyjścia cyfrowego w module Nano. To pozwoliło szybko sprawdzić za pomocą oscyloskopu prawidłowe ustawienia wyjść cyfrowych (fotografia 2).
Oprogramowanie
Dzięki dodawaniu poszczególnych elementów do systemu po kolei, debugowanie programu i pisanie kodu poszło autorowi dosyć łatwo. Pełny kod projektu znajduje się w materiałach dodatkowych do artykułu. Aby poprawnie skomplikować ten szkic, potrzebne będzie zainstalowanie dwóch bibliotek za pomocą Manage Libraries w menu Tools. Są to biblioteki Encoder i Adafruit_NeoPixel, które są potrzebne do obsługi, odpowiednio, enkodera obrotowego oraz modułu NeoPixel z diodami LED.
Aby załadować oprogramowanie do modułu Nano, wystarczy podłączyć go do komputera za pomocą kabla USB i z poziomu Arduino IDE skomplikować i wgrać szkic do pamięci mikrokontrolera. Kluczowe funkcje oprogramowania obejmują:
- Obsługę pierścienia LED NeoPixel, który wyświetla aktywny obecnie wzór, wybrany za pomocą jednego z czterech przycisków wyboru wzoru:
- aktywny wzór jest wyświetlany w jednym z czterech kolorów (czerwony, zielony, niebieski lub żółty),
- migający piksel obraca się wokół wzoru, wskazując tempo i tryb pracy enkodera:
- biały piksel oznacza tryb tempa,
- fioletowy piksel oznacza tryb euklidesowy,
- żółty piksel oznacza tryb rotacji.
- Wciśnięcie pokrętła enkodera (naciśnięcie przełącznika) powoduje przejście pomiędzy trzema trybami działania:
- tryb tempo – pokrętło enkodera przyspiesza i spowalnia wewnętrzny zegar,
- tryb euklidesowy – pokrętło enkodera zwiększa i zmniejsza liczbę impulsów w aktywnym wzorze,
- tryb rotacji – pokrętło enkodera obraca aktywny wzór zgodnie z ruchem wskazówek zegara lub przeciwnie do ruchu wskazówek zegara.
- Przestawienie przełącznika wyboru zegara umożliwia taktowanie modułu wewnętrznie lub z zewnętrznego źródła wyzwalania:
- w trybie tempo nie mamy wpływu na szybkość taktowania modułu, gdy używany jest zegar zewnętrzny.
- Potencjometr szerokości impulsu zmienia szerokość impulsów generowanych przez moduł:
- ustawienie go całkowicie przeciwnie do ruchu wskazówek zegara wygeneruje krótkie impulsy,
- ustawienia go w pozycji skrajnej zgodne z ruchem wskazówek zegara zwiększa szerokość impulsów, aby nadawały się do bramkowania.
- Moduł ma cztery niezależne wyjścia, które odpowiadają każdemu z czterech rytmów euklidesowych i tempu:
- wyjścia cyfrowe mogą być używane do wyzwalania diod LED lub innych modułów syntezatora.
Płytka główna i przedni panel
Gdy oprogramowanie jest gotowe i przetestowane na płytce prototypowej, nadszedł czas na rozpoczęcie pracy nad samym modułem. Płytki stykowe są dobre do tymczasowych obwodów i testowania, ale wyprodukowanie trwalszego modułu, który można by zainstalować w szafie syntezatora, wymaga pewnego przylutowania elementów do płytki drukowanej. Autor zastosował dwie płytki uniwersalne o wymiarach 4×6 cm do instalacji komponentów. Jedna płytka zawiera Arduino Nano, rezystory, złącze zasilania i poczwórny wzmacniacz operacyjny do buforowania sygnałów wyjściowych z modułu. Druga płytka to miejsce instalacji pierścienia diod NeoPixel i enkodera. Jest ona ponadto punktem, do którego podłączone są wszystkie złącza, przełączniki i tym podobne elementy z przedniego panelu.
Płytki pokazano na fotografii 3 (moduł z Arduino Nano) oraz fotografii 4 (moduł przyłączeniowy).
Obie płytki zaplanowane są tak, aby łączyć się ze sobą za pomocą zestawu męskich/żeńskich goldpinów po lewej i prawej stronie płytki. Złącza te przenoszą zasilanie, masę i sygnały między płytkami.
Montaż należy rozpocząć od ułożenia elementów na płytce, aby rozplanować lokalizację poszczególnych elementów, odstępy pomiędzy nimi i ich wzajemną pozycję. Najwięcej miejsca zajmuje na płytce oczywiście moduł Nano. Dodatkowo, musi być on tak zorientowany, aby wtyczka USB była dostępna do programowania z zewnątrz. Jeśli chcemy zasilać moduł z zasilacza w stylu Eurorack (jeden z popularnych standardów zasilania w syntezatorach modułowych), konieczne będzie dodanie odpowiedniego złącza 2×5 pinów, co pozwoli pobrać napięcia 12 V oraz –12 V (schemat wyprowadzeń złącza pokazany na rysunku 2).
Na płytce z mikrokontrolerem znajduje się również wzmacniacz operacyjny TL074 do buforowania wyjść z Arduino. Bufor zapewnia sygnałom wyjściowym modułu nieco wyższy prąd wyjściowy i zapobiega nadmiernemu obciążeniu linii GPIO. Jeśli nie planujemy podłączać modułu do wielu innych modułów, można zrezygnować z tego elementu, ale nie jest to rekomendowane rozwiązanie.
Arduino Nano i wzmacniacz operacyjny nie powinny być lutowane bezpośrednio do płytki. W przypadku Arduino należy przylutować parę żeńskich złączy goldpin do płytki i podłączyć Nano do tego złącza. Układ scalony z kolei korzysta z podstawki, którą należy wlutować w płytkę drukowaną, a dopiero w niej umieścić wzmacniacz operacyjny. Zapobiegnie to przypadkowemu uszkodzeniu modułu Arduino i wzmacniacza operacyjnego podczas lutowania. W przyszłości, jeśli Nano lub wzmacniacz operacyjny ulegną uszkodzeniu, można po prostu wyjąć stare komponenty z gniazd i wymienić je na nowe, sprawne.
Montując wtyczkę zasilania, należy zwrócić uwagę na orientację wycięcia wtyczki, dzięki któremu można podłączyć ją tylko w jednej orientacji. Napięcie Vin Arduino Nano podłączone jest do linii 12 V, więc niepoprawne podłączenie gniazda może spowodować podanie tam –12 V i w konsekwencji uszkodzenie modułu.
Korzystając ze schematu z rysunku 1, należy polutować wszystkie połączenia między złączami Nano, wzmacniaczem operacyjnym, rezystorami i złączami krawędziowymi płyty. Po zakończeniu montażu dobrze jest użyć multimetru z funkcją testu ciągłości, aby upewnić się, że elementy połączone są poprawnie i nie występują żadne przypadkowo powstałe mostki między sąsiednimi stykami. Staranne planowanie, cierpliwość i konsekwentne testowanie naprawdę opłacają się na etapie lutowania.
Przedni panel, pokazany na fotografii 5, wycięty został na ploterze laserowym. Odpowiednie pliki dostępne są na stronie projektu. Można je pobrać i samodzielnie wyciąć taki panel, jeśli ma się dostęp do plotera laserowego lub innego podobnego urządzenia.
Ostatnia część budowy modułu obejmuje okablowanie gniazd, przycisków, przełącznika, potencjometru i diod LED. Najłatwiej jest zamontować elementy na panelu przed przylutowaniem do nich kabli. Pomoże to w określeniu długości i miejsca prowadzenia przewodów.
Połączenia najlepiej jest wykonywać kablem typu linka, a nie drutem, z uwagi na zwiększoną wytrzymałość na zginanie. Korzystając ze schematu z rysunku 1, należy przylutować odcinki przewodu do płytki w celu przygotowania połączenia z elementami montowanymi na panelu. Większość komponentów wymaga połączenia z masą – wykonuje się je za pomocą krótkich odcinków przewodów, które łączą się ze sobą (wszystkie gniazda, potencjometr, przełącznik z masą, szeregowo). Dodatkowy przewód łączy jeden punkt połączenia z płytką. W podobny sposób można postępować z elementami, które wymagają napięcia zasilania (5 V).
Lutując poszczególne elementy, nie można zapomnieć o dodaniu rezystorów ograniczających prąd diody LED (1 kΩ). Są one lutowane do dłuższego wyprowadzenia diody. Rezystor jest następnie lutowany do złącza gniazda wyjściowego, a drugi koniec diody LED jest lutowany do tulei – masy złącza. Po zlutowaniu elementów należy ostrożnie zagiąć wyprowadzenia LED, aby umieścić element w otworze panelu. Dobrym pomysłem jest zastosowanie rurek termokurczliwych do osłonięcia wyprowadzeń, aby uniknąć przypadkowych zwarć.
Po wykonaniu wszystkich połączeń między elementami panelu a płytką z mikrokontrolerem można przymocować ją do panelu. Enkoder powinien być wyposażony w podkładkę i nakrętkę. Należy je wykorzystać, aby dokręcić enkoder do panelu. Cztery mniejsze otwory w panelu powinny pokrywać się z otworami w płytce. Używając małych śrubek i nakrętek, można przykręcić PCB do panelu. Ostatnim krokiem montażu jest zamontowanie głównej płytki na płycie panelu. Należy ostrożnie wyrównać wyprowadzenia i złącze i docisnąć je do siebie. Powinno to zapewnić dopasowane i ciasne połączenie pomiędzy elementami. Po zmontowaniu obu płytek można, jeśli nie zrobiło się tego wcześniej, zainstalować Arduino Nano i wzmacniacz operacyjny w podstawkach. Należy pamiętać o orientacji elementów w ich gniazdach.
Zastosowanie modułu
Większość funkcjonalności modułu można przetestować za pomocą kabla USB, którym programowaliśmy Nano – brak zasilania dostarczanego przez złącze Eurorack nie powinno być problemem, gdyż elementy zasilane są z USB. Moduł NeoPixel powinien uruchomić się z czterema czerwonymi diodami LED i migającą białą diodą LED pędzącą wokół pierścienia. Moduł uruchamia się w trybie tempa i można przyspieszyć lub spowolnić tempo, obracając pokrętło enkodera. Jeśli widoczne są cztery czerwone kontrolki, ale migająca biała dioda LED nie porusza się, należy upewnić się, że przełącznik wyboru zegara jest ustawiony na zegar wewnętrzny. Naciśnięcie czterech przycisków wyboru impulsu wyzwalania powinno zmienić kolor NeoPixel, a także domyślne wzory, które są prezentowane. Jeśli przyciski nie działają niezawodnie, można dokonać zmian w kodzie w funkcji CheckButtons (komentarze znajdują się na listingu programu).
#include <Adafruit_NeoPixel.h> // Biblioteka do obsługi Adafruit NeoPixel
#include <Encoder.h> // Biblioteka do obsługi enkodera
Encoder myEnc(3, 4); // Obiekt enkodera, dołączonego do pinów cyfrowych 3 i 4
#define PixelPin 2 // Moduł NeoPixel dołączony do pinu cyfrowego 2
#define ProgPin 5 // Przełącznike enkodera dołączony do pinu cyfrowego 5
#define ClkPin 6 // Zewnętrzny zegar dołączony do pinu cyfrowego 6
#define Trig1Pin 10 // Cztery sygnały wyzwalacza, dołączone do pinów 7-10
#define Trig2Pin 8
#define Trig3Pin 9
#define Trig4Pin 7
#define TapPin 11 // Przycisk tempa dołączony do pinu cyfrowego 11
#define ButtonPin A0 // Wybór wyzalania (dielnika napięcia) do pinu analogowego A0
#define PotPin A1 // Potencjometr czasu trwania impulsu do pinu analogowego A1
#define ExtClkPin A2 // Zewnętrzne napięcie kontrolne zegtara do pinu analogowego A2
#define NumSteps 16 // Predefiniowana liczba kroków w sekwencji
int Part = 1; // Aktualny wyzwalacz ustawiony początkowo na 1
Adafruit_NeoPixel pixels(NumSteps, PixelPin, NEO_GRB + NEO_KHZ800); // Konfiguracja modułu NeoPixel
unsigned int Euclid(int NumPulses) {
unsigned int Number = 0; // Number to liczba impulsów, które już zostaly zaalokowane
for (int i=0; i<NumSteps; i++) { // Przejście przez wszystkie kroki w sekwencji
int bucket = bucket + NumPulses; // Wypełnia bucket liczbą impulsów do alokacji
if (bucket >= NumSteps) { // Jeśli bucket > liczba kroków to opróźnia bucket
bucket = bucket - NumSteps; // ustawiający i-ty bit w sekwencju
Number |= 1 << i; // i wpisując do bucket liczbę pozostałych impulsów.
}
}
return(Number);
}
unsigned int RotateLeft(unsigned int Number) {
int DROPPED_MSB;
DROPPED_MSB = (Number >> NumSteps-1) & 1;
Number = (Number << 1) | DROPPED_MSB;
return(Number);
}
unsigned int RotateRight(unsigned int Number) {
int DROPPED_LSB;
DROPPED_LSB = Number & 1;
Number = (Number >> 1) & (~(1 << NumSteps-1));
Number = Number | (DROPPED_LSB << NumSteps-1);
return(Number);
}
int CheckButtons() {
int Buttons = analogRead(ButtonPin); // Odczytuje napięcie z dzielnika napięciowego
if ((Buttons>480)&&(Buttons<520)) Part = 1; // Wybierz wyjście 1, gdy naciśnięto pierwszy przycisk
if ((Buttons>580)&&(Buttons<620)) Part = 2; // Wybierz wyjście 2, gdy naciśnięto pierwszy drugi
if ((Buttons>680)&&(Buttons<720)) Part = 3; // Wybierz wyjście 3, gdy naciśnięto pierwszy trzeci
if (Buttons>800) Part = 4; // Wybierz wyjście 4, gdy naciśnięto pierwszy czwarty
}
void ClearPattern() {
for (int i=0; i<NumSteps; i++) // Przejdź przez każdy piksel w pierścieniu LED
pixels.setPixelColor(i, pixels.Color(0,0,0)); // Wyłącz każdy piksel
pixels.show(); // Aktualizuj stan wyświetlacza
}
void BitPattern(int Chan, unsigned int Number) {
int R;
int G;
int B;
switch (Chan) {
case 1: R = 20; G = 0; B = 0; break; // Wzór 1 jest czerwony
case 2: R = 0; G = 20; B = 0; break; // Wzór 2 jest zielony
case 3: R = 0; G = 0; B = 20; break; // Wzór 3 jest niebieski
case 4: R = 20; G = 20; B = 0; break; // Wzór 4 jest żółty
}
for (int i=0; i<NumSteps; i++)
if (Number & (1<<i))
pixels.setPixelColor(i, pixels.Color(R,G,B));
else
pixels.setPixelColor(i, pixels.Color(0,0,0));
pixels.show();
}
void setup() {
Serial.begin(9600); // Uruchom konsolę szeregową
pinMode(ProgPin,INPUT_PULLUP); // Przypisz piny enkodera i włącz podciągnięcie do zasilania
pinMode(ClkPin,INPUT); // Przypisz pin selektroa zewnętrznego zegara
pinMode(Trig1Pin,OUTPUT); // Przypisz wyjście 1
pinMode(Trig2Pin,OUTPUT); // Przypisz wyjście 2
pinMode(Trig3Pin,OUTPUT); // Przypisz wyjście 3
pinMode(Trig4Pin,OUTPUT); // Przypisz wyjście 4
pinMode(TapPin,INPUT); // Przypisz przycisk wyboru tępa
pixels.begin(); // Uruchom NeoPixel
pixels.clear(); // Wyczyść NeoPixel
}
void loop() {
static unsigned int Ch1 = 0x8888; // Domyślne wzory dla wyjść 1-4
static unsigned int Ch2 = 0x4444;
static unsigned int Ch3 = 0x2222;
static unsigned int Ch4 = 0xEEEE;
static int Step = 0; // Aktywny krok jest zapisany w pamieci pomiędzy pętlami, ale startuje od 0
static int Delay = 80; // Czas pomiędzy krokami też jest zapisany w pamięci, startuje od 80 ms
static int Mode = 0; // Tryb zapisywany jest pomiędzy pętlami
static long Mode1Pos = 0; // Ostatnia pozycja enkodera w trybie 1
static int PrevExtClk = 0; // Wartość zewnętrznego zegara do detekcji zbocza
bool Triggered = false; // Załóż, że nie będzie nowych kroków w tej iteracji pętli
static unsigned long Time; // Przygotowanie do pobrania danych z zegara Nano
static unsigned long PrevTime = 0; // Zlicza czas pomiędzy pętlami
static long oldPosition; // Poprzednia wartość licznika enkodera
static int Pulses; // Liczba impulsów generowana dla aktywnego wyjścia
static bool PrevProg; // Poprzedni stan przycisku enkodera
static bool Prog; // Przygotowanie do odczytania obecnego stanu przycisku enkodera
long newPosition = myEnc.read(); // Odczytaj licznik enkodera
int PotV = analogRead(PotPin); // Odczytaj wartość potencjometra długości impulsu
int ExtClkV = analogRead(ExtClkPin); // Odczytaj wejście zewnętrznego zegara
CheckButtons(); // Odczytaj wartość przycisku wyjścia i aktualizuj aktywne wyjście
PrevProg = Prog; // Zapis wartość przełącznika enkodera
Prog = digitalRead(ProgPin); // Pobierz wartość przełącznika enkodera
Time = millis(); // Pobierz wartość czasu systemowego z Nano (w ms)
if ((Prog==true) && (PrevProg==false)) { // Jeśli naciśnięto przycisk enkodera trzeba zmienić tryb
if (Mode==1) Mode1Pos = newPosition; // Zapisz pozycję enkodera, aby wrócić do trybu
Mode++; // Przejdź do następnego trybu
if (Mode>3) Mode=1; // Przejdź do trybu 1 z trybu 3
if (Mode==1) { myEnc.write(Mode1Pos); newPosition = Mode1Pos; } // Odtwórz pozycję enkodera w tym trybie
oldPosition = newPosition;
}
ClearPattern();
switch(Mode) {
case 1: // Tryb 1 - Manualna regulacja tempa sekwencji
if (newPosition!=oldPosition) {
Delay = newPosition*2; // Wewnętrzne opóźnienie jest proporcjonalne do pozycji enkodera
if (Delay<10) Delay = 10; // Najmniejszy czas opóźnienia = 10 ms
if (Delay>1000) Delay = 1000; // Największy czas opóźnienia = 1 s
oldPosition = newPosition;
}
break;
case 2: // Tryb 2 - Zmiana liczby impulsów w trybie Euklidesowskim
if (newPosition < oldPosition-3) {
Pulses++; // Zwięsz liczbę impulsów
if (Pulses>NumSteps) Pulses=NumSteps; // Ograniczenie liczby impulsów <= NumSteps
oldPosition = newPosition;
}
else if (newPosition > oldPosition+3) {
Pulses--; // Zmniejsz liczbę impulsów
if (Pulses<0) Pulses=0; // Ograniczenie liczby impulsów => 0
oldPosition = newPosition;
}
Serial.println(Pulses);
switch (Part) { // Obliczenia Rytmu Euklidesowskiego dla obecnego układu
case 1: Ch1 = Euclid(Pulses); break;
case 2: Ch2 = Euclid(Pulses); break;
case 3: Ch3 = Euclid(Pulses); break;
case 4: Ch4 = Euclid(Pulses); break;
}
break;
case 3: // Tryb 3 - Przekręcanie Rotate the Euclidean Rhythm of the active pattern
if (newPosition > oldPosition+3) {
oldPosition = newPosition;
switch (Part) {
case 1: Ch1 = RotateRight(Ch1); break;
case 2: Ch2 = RotateRight(Ch2); break;
case 3: Ch3 = RotateRight(Ch3); break;
case 4: Ch4 = RotateRight(Ch4); break;
}
}
else if (newPosition < oldPosition-3) {
oldPosition = newPosition;
switch (Part) {
case 1: Ch1 = RotateLeft(Ch1); break;
case 2: Ch2 = RotateLeft(Ch2); break;
case 3: Ch3 = RotateLeft(Ch3); break;
case 4: Ch4 = RotateLeft(Ch4); break;
}
}
break;
}
switch(Part) { // Pokaż aktywny wzór wraz z aktualizacjami w obecnym trybie
case 1: BitPattern(1,Ch1); break;
case 2: BitPattern(2,Ch2); break;
case 3: BitPattern(3,Ch3); break;
case 4: BitPattern(4,Ch4); break;
}
if (digitalRead(ClkPin)==true) { // Jeżeli układ korzysta z zewnętrznego zegara
if ((ExtClkV>512) && (PrevExtClk<128)) { // Szuka zbocza narastającego
Delay = Time-PrevTime; // Moment wystąpienia zbocza
Triggered = true;
}
PrevExtClk = ExtClkV;
}
else { // Jeżeli układ korzysta z wewnętrznego zegara
if ((Time-PrevTime)>Delay) { // Sprawdź czy minęło dostatczni dużo, aby przejść do kolejnego kroku sekwencji
Triggered = true;
}
}
int TrigWidth = map(PotV, 0, 1023, Delay/2, Delay/50); // Oblicz szerokość impulsu
if ((Time-PrevTime)>TrigWidth) { // Wyłącz wyjścia sterujące, jeśli minął czas impulsu
digitalWrite(Trig1Pin,LOW);
digitalWrite(Trig2Pin,LOW);
digitalWrite(Trig3Pin,LOW);
digitalWrite(Trig4Pin,LOW);
}
if (Triggered) { // Gdy uruchomiono kolejny cykl
PrevTime = Time;
Step++;
if (Step>NumSteps-1) Step = 0; // Reset licznika kroków, jeśi sekwencja zakończyła się
if (Mode==1) pixels.setPixelColor(Step, pixels.Color(40,40,40)); // Zapal obecny piksel na kolor wynikający z trybu
if (Mode==2) pixels.setPixelColor(Step, pixels.Color(40,0,40));
if (Mode==3) pixels.setPixelColor(Step, pixels.Color(20,20,0));
pixels.show();
if (Ch1 & (1<<Step)) digitalWrite(Trig1Pin, HIGH);
if (Ch2 & (1<<Step)) digitalWrite(Trig2Pin, HIGH);
if (Ch3 & (1<<Step)) digitalWrite(Trig3Pin, HIGH);
if (Ch4 & (1<<Step)) digitalWrite(Trig4Pin, HIGH);
}
}
Testowanie należy rozpocząć od naciśnięcia raz pokrętła enkodera. Migające światło powinno zmienić kolor na fioletowy, a czerwone światła dookoła znikną. Obracanie enkoderem zwiększa lub zmniejsza liczbę impulsów we wzorze. Naciśnięcie przycisków wyboru wyzwalacza umożliwia określenie impulsów dla każdego z wyjść niezależnie. Po naciśnięciu pokrętła enkodera po raz drugi migające światło zmieni kolor na żółty. Obracanie pokrętłem enkodera obraca wybrany wzór. Naciskając przyciski wyboru wyjścia, przełącza się wzory impulsów i obraca je względem siebie. Trzecie naciśnięcie pokrętła enkodera powoduje powrót do pierwszego trybu działania układu.
W trakcie tego procesu można zauważyć, że diody LED dołączone do wyjść wyzwalających w module migają sporadycznie lub wcale. Dzieje się tak, ponieważ układ zasilany jest z USB – wyjściowe op-ampy zasilane są ze złącza na tyle modułu. Na czas testów zasilanie ±12 V zastąpić można ±9 V z dwóch baterii.
Po włączeniu, jeśli cały system działa prawidłowo, każda z diod LED wyjścia powinna migać w rytmie z pierścieniem diod NeoPixel. Każda dioda LED powinna zaświecić się, gdy lampka wskaźnika trybu przecina punkt w rytmie euklidesowym dla swojego wzoru.
Podkręcenie potencjometru szerokości impulsu spowoduje, że dioda LED będzie świecić nieco dłużej. Jest to bardziej zauważalne przy wolniejszych przebiegach. Jeśli diody LED migają prawidłowo, sygnały wyzwalające również powinny być prawidłowe. Aby to przetestować, można się posłużyć oscyloskopem lub po prostu podłączyć sekwencer do innych modułów syntezatora. Złożony moduł został pokazany na fotografii 6.
Podsumowanie
Arduino ma nadal wolne cyfrowe i analogowe wyprowadzenia. Można dalej rozszerzyć możliwości tego sekwencera. Autor ma kilka pomysłów na przyszłość, które mogłyby rozszerzyć funkcjonalność tego modułu:
- dodanie większej liczby triggerów/wyjść,
- zwiększenie zakresu kroków, jakie mogą być w sekwencji,
- dodanie przycisku do manualnego „wystukiwania” rytmu,
- dodanie czwartego trybu działania, pozwalające na redukcję okresu rytmów euklidesowych,
- dodanie wejścia dla napięcia kontrolnego, które sterować będzie:
- liczbą uderzeń na cykl,
- szerokością impulsów,
- translacją wzorów.
Moduł ten sprawdzi się doskonale w każdym analogowym syntezatorze. Dzięki automatycznej generacji rytmów z użyciem predefiniowanego algorytmu można mieć pewność, że generowana sekwencja zawsze będzie dobrze brzmiąca i wpadająca w ucho.
Nikodem Czechowski, EP
Źródło:
https://bit.ly/3F25WQB
https://bit.ly/3qS4GdV (próbki dźwięku w systemie)