見出し画像

800円で作るユークリッドリズムシーケンサー-モジュラーシンセ自作

Arduino nanoを使って、モジュラーシンセサイザー のユークリッドリズムシーケンサ(Euclidean rhythm sequencer)を自作したので、その備忘録。

背景

自作モジュラーシンセの30作品目。
ユークリッドリズムを鳴らせることは、モジュラーシンセの特徴のひとつかもしれない。
世の中のリズムマシンには、ユークリッドリズムシーケンサが搭載されていない。しかし、ユーロラックには多くのユークリッドリズムシーケンサのラインナップがある。

ダンスミュージックとは違うリズムを作り出すために、過去に作成した「6CH トリガーシーケンサー」のハードウェアを流用して、ユークリッドリズムシーケンサの作成に至った。

制作物のスペック

ユーロラック規格 3U 6HPサイズ
電源:待機時37mA(at5V or 12V) 出力時100mA(at 5V or 12V)
5V単電源で動作可能。または12V単電源で動作可能。
OLED表示:128 * 64 size
ロータリーエンコーダ:パラメータの選択
PUSHボタン:パラメータの変更/決定
トリガー出力:6CH (0-5V)
トリガー入力:1CH (0-5V)

画像2

2種類のモード

マニュアルモード:各CHの各パラメータを任意に設定して再生する。
選択可能なパラメータは以下の通り。

HITS:16stepのうちの出力回数。

OFFSET:リズムの出だしをオフセット分ずらす

LIMIT:設定した値の回数トリガー入力があると、1step目に戻る。例えば5に設定すると、1~5step出力したら1step目に戻る。ポリリズム的な使い方ができる。

MUTE:選択されたCHのトリガー出力を無くす。

RESET:ボタンを押すと全CHの再生stepを1step目に戻す。

ランダムモード:各CHの各パラメータが、指定の拍に達するごとにランダムで切り替わる。完全なランダムではなく、各CHによって選出される値に傾向がある。

OCCURRENCE:ここで指定した回数のSTEPに達すると、各パラメータがランダムに切り替わる。選択肢は「2,4,8,16」で画面左下のバーで確認できる。

画像1

製作費

総額約800円
---------------------------------
Arduino nano 200円
OLED(SSD1306) 180円
パネル 150円
ロータリーエンコーダ80円

画像4

プログラミング

ユークリッドシーケンスについて:
ユークリッドリズムは高度な計算によって求められるが、プログラム上では計算は実施していない。16stepに限定してしまえば、リズムパターンは17種類(0hit~16hit)だけなので、リズムパターンはテーブルに格納している。

const static byte euc16[17][16] PROGMEM = {//euclidian rythm
 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 {1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0},
 {1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0},
 {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1},
 {1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1},
 {1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1},
 {1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1},
 {1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1},
 {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
 {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};

OLED表示の排他処理:
OLEDを画面上に表示すると、トリガー出力にレイテンシーが発生する。そのため、OLEDの表示更新はトリガー出力が終わった直後に行われる。トリガー入力が無い限り、画面の表示も更新されないので不便だが、ソース上のコメントアウトを有効にすることで、画面更新頻度を上げることができる。

  //    disp_reflesh = 1;//Enable while debugging.

OLEDの図形表示:
hitが有効なstepの頂点を結ぶ多角形を表示している。3hitなら三角形、4hitなら四角形。この多角形を表示するプログラムが以下の通り。

  for (k = 0; k <= 5; k++) { //ch count
   buf_count = 0;
   for (m = 0; m < 16; m++) {
     if (offset_buf[k][m] == 1) {
       line_xbuf[buf_count] = x16[m] + graph_x[k];//store active step
       line_ybuf[buf_count] = y16[m] + graph_y[k];
       buf_count++;
     }
   }

   for (j = 0; j < buf_count - 1; j++) {
     display.drawLine(line_xbuf[j], line_ybuf[j], line_xbuf[j + 1], line_ybuf[j + 1], WHITE);
   }
   display.drawLine(line_xbuf[0], line_ybuf[0], line_xbuf[j], line_ybuf[j], WHITE);
 }
 for (j = 0; j < 16; j++) {//line_buf reset
   line_xbuf[j] = 0;
   line_ybuf[j] = 0;
 }

線を引くためのバッファに頂点の座標を一時的に記憶させる。
その座標を繋ぐ線を描画したあと、バッファを削除する。
それを1~6CHで繰り返すという処理をしている。

メモリの使用量:
Arduino IDE上では、RAM領域を31%しか使用していないと表示されるが、これ以上プログラムの処理を重くすると動作が不安定になった。
例えば、OLEDの文字表示数を増やしたり、ifによる複雑な条件分岐をするとArduinoが操作を受け付けなくなる事が多発した。
ライブラリとの相性が悪かったのか?原因は不明。

画像3

ハードウェア

6chトリガーシーケンサーと回路は同じだ。
ソフトウェアを書き換えれば、機能を変更することができる。

宣伝:オープンソースプロジェクトの支援をお願いします

DIYモジュラーシンセのオープンソースプロジェクトを継続するために、patreonというサービスでパトロンを募集しています。
コーヒー一杯の支援をいただけると嬉しいです。
また、パトロン限定のコンテンツも配信しています。

ソースコード

粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
強制ではないが、このソースコードをベースに新しいプロダクトを作る場合、このブログ(又はYouTube)のリンクを記載してもらえると嬉しい。



//Encoder setting
#define  ENCODER_OPTIMIZE_INTERRUPTS //countermeasure of encoder noise
#include <Encoder.h>

//Oled setting
#include<Wire.h>
#include<Adafruit_GFX.h>
#include<Adafruit_SSD1306.h>

#define OLED_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

//rotery encoder
Encoder myEnc(3, 2);//use 3pin 2pin
int oldPosition  = -999;
int newPosition = -999;
int i = 0;

//push button
bool sw = 0;//push button
bool old_sw;//countermeasure of sw chattering
unsigned long sw_timer = 0;//countermeasure of sw chattering

//each channel param
byte hits[6] = { 4, 4, 5, 3, 2, 16};//each channel hits
byte offset[6] = { 0, 2, 0, 8, 3, 9};//each channele step offset
bool mute[6] = {0, 0, 0, 0, 0, 0}; //mute 0 = off , 1 = on
byte limit[6] = {16, 16, 16, 16, 16, 16};//eache channel max step

//Sequence variable
byte j = 0;
byte k = 0;
byte m = 0;
byte buf_count = 0;

const static byte euc16[17][16] PROGMEM = {//euclidian rythm
 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
 {1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
 {1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0},
 {1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0},
 {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0},
 {1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1},
 {1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1},
 {1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1},
 {1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1},
 {1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1},
 {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0},
 {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};
bool offset_buf[6][16];//offset buffer , Stores the offset result

bool trg_in = 0;//external trigger in H=1,L=0
bool old_trg_in = 0;
byte playing_step[6] = {0, 0, 0, 0, 0, 0}; //playing step number , CH1,2,3,4,5,6
unsigned long gate_timer = 0;//countermeasure of sw chattering

//display param
byte select_menu = 0;//0=CH,1=HIT,2=OFFSET,3=LIMIT,4=MUTE,5=RESET,
byte select_ch = 0;//0~5 = each channel -1 , 6 = random mode
bool disp_reflesh = 1;//0=not reflesh display , 1= reflesh display , countermeasure of display reflesh bussy

const byte graph_x[6] = {0, 40, 80, 15, 55, 95};//each chanel display offset
const byte graph_y[6] = {0, 0,  0,  32, 32, 32};//each chanel display offset

byte line_xbuf[17];//Buffer for drawing lines
byte line_ybuf[17];//Buffer for drawing lines

const byte x16[16] = {15,  21, 26, 29, 30, 29, 26, 21, 15, 9,  4,  1,  0,  1,  4,  9};//Vertex coordinates
const byte y16[16] = {0,  1,  4,  9,  15, 21, 26, 29, 30, 29, 26, 21, 15, 9,  4,  1};//Vertex coordinates


//random assign
byte hit_occ[6] = {0, 10, 20, 20, 40, 80}; //random change rate of occurrence
byte off_occ[6] = {10, 20, 20, 30, 40, 20}; //random change rate of occurrence
byte mute_occ[6] = {20, 20, 20, 20, 20, 20}; //random change rate of occurrence
byte hit_rng_max[6] = {0, 14, 16, 8, 9, 16}; //random change range of max
byte hit_rng_min[6] = {0, 13, 6, 1, 5, 10}; //random change range of max

byte bar_now = 1;//count 16 steps, the bar will increase by 1.
byte bar_max[4] = {2, 4, 8, 16} ;//selectable bar
byte bar_select = 1;//selected bar
byte step_cnt = 0;//count 16 steps, the bar will increase by 1.

void setup() {
 // OLED setting
 display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
 display.setTextSize(1);
 display.setTextColor(WHITE);
 OLED_display();

 //pin mode setting
 pinMode(12, INPUT_PULLUP); //BUTTON
 pinMode(5, OUTPUT); //CH1
 pinMode(6, OUTPUT); //CH2
 pinMode(7, OUTPUT); //CH3
 pinMode(8, OUTPUT); //CH4
 pinMode(9, OUTPUT); //CH5
 pinMode(10, OUTPUT); //CH6
}

void loop() {
 old_trg_in = trg_in;
 oldPosition = newPosition;
 //-----------------Rotery encoder read----------------------
 newPosition = myEnc.read();

 if ( newPosition   < oldPosition ) {//turn left
   oldPosition = newPosition;
   //    disp_reflesh = 1;//Enable while debugging.
   if (select_menu != 0) {
     select_menu --;
   }
 }

 else if ( newPosition    > oldPosition ) {//turn right
   oldPosition = newPosition;
   //    disp_reflesh = 1;//Enable while debugging.
   select_menu ++;
 }
 if (select_ch != 6) { // not random mode
   select_menu = constrain(select_menu, 0, 5);
 }
 else  if (select_ch == 6) { // random mode
   select_menu = constrain(select_menu, 0, 1);
 }

 //-----------------push button----------------------

 sw = 1;
 if ((digitalRead(12) == 0 ) && ( sw_timer + 300 <= millis() )) { //push button on ,Logic inversion , sw_timer is countermeasure of chattering
   sw_timer = millis();
   sw = 0;
   //    disp_reflesh = 1;//Enable while debugging.
 }

 if (sw == 0) { //push button on
   switch (select_menu) {
     case 0: //select chanel
       select_ch ++;
       if (select_ch >= 7) {
         select_ch = 0;
       }
       break;

     case 1: //hits
       if (select_ch != 6) { // not random mode
         hits[select_ch]++ ;
         if (hits[select_ch] >= 17) {
           hits[select_ch] = 0;
         }
       }
       else  if (select_ch == 6) { // random mode
         bar_select ++;
         if (bar_select >= 4) {
           bar_select = 0;
         }
       }
       break;

     case 2: //offset
       offset[select_ch]++ ;
       if (offset[select_ch] >= 16) {
         offset[select_ch] = 0;
       }
       break;

     case 3: //limit
       limit[select_ch]++ ;
       if (limit[select_ch] >= 17) {
         limit[select_ch] = 0;
       }

       break;

     case 4: //mute
       mute[select_ch] = !mute[select_ch] ;
       break;

     case 5: //reset
       for (k = 0; k <= 5; k++) {
         playing_step[k] = 0;
       }
       break;
   }
 }

 //-----------------offset setting----------------------
 for (k = 0; k <= 5; k++) { //k = 1~6ch
   for (i = offset[k]; i <= 15; i++) {
     offset_buf[k][i - offset[k]] = (pgm_read_byte(&(euc16[hits[k]][i]))) ;
   }

   for (i = 0; i < offset[k]; i++) {
     offset_buf[k][16 - offset[k] + i] = (pgm_read_byte(&(euc16[hits[k]][i])));
   }
 }

 //-----------------trigger detect & output----------------------
 trg_in = digitalRead(13);//external trigger in
 if (old_trg_in == 0 && trg_in == 1) {
   gate_timer = millis();
   for (i = 0; i <= 5; i++) {
     playing_step[i]++;      //When the trigger in, increment the step by 1.
     if (playing_step[i] >= limit[i]) {
       playing_step[i] = 0;  //When the step limit is reached, the step is set back to 0.
     }
   }
   for (k = 0; k <= 5; k++) {//output gate signal
     if (offset_buf[k][playing_step[k]] == 1 && mute[k] == 0) {
       switch (k) {
         case 0://CH1
           digitalWrite(5, HIGH);
           break;

         case 1://CH2
           digitalWrite(6, HIGH);
           break;

         case 2://CH3
           digitalWrite(7, HIGH);
           break;

         case 3://CH4
           digitalWrite(8, HIGH);
           break;

         case 4://CH5
           digitalWrite(9, HIGH);
           break;

         case 5://CH6
           digitalWrite(10, HIGH);
           break;
       }
     }
   }
   disp_reflesh = 1;//Updates the display where the trigger was entered.If it update it all the time, the response of gate on will be worse.

   if (select_ch == 6) {// random mode setting
     step_cnt ++;
     if (step_cnt >= 16) {
       bar_now ++;
       step_cnt = 0;
       if (bar_now > bar_max[bar_select]) {
         bar_now = 1;
         Random_change();
       }
     }
   }
 }

 if  (gate_timer + 10 <= millis()) { //off all gate , gate time is 10msec
   digitalWrite(5, LOW);
   digitalWrite(6, LOW);
   digitalWrite(7, LOW);
   digitalWrite(8, LOW);
   digitalWrite(9, LOW);
   digitalWrite(10, LOW);
 }


 if (disp_reflesh == 1) {
   OLED_display();//reflesh display
   disp_reflesh = 0;
 }
}

void Random_change() { // when random mode and full of bar_now ,
 for (k = 1; k <= 5; k++) {

   if (hit_occ[k] >= random(1, 100)) { //hit random change
     hits[k] = random(hit_rng_min[k], hit_rng_max[k]);
   }

   if (off_occ[k] >= random(1, 100)) { //hit random change
     offset[k] = random(0, 16);
   }

   if (mute_occ[k] >= random(1, 100)) { //hit random change
     mute[k] = 1;
   }
   else if (mute_occ[k] < random(1, 100)) { //hit random change
     mute[k] = 0;
   }
 }
}

void OLED_display() {
 display.clearDisplay();
 //-------------------------euclidean circle display------------------
 //draw setting menu
 display.setCursor(120, 0);
 if (select_ch != 6) { // not random mode
   display.print(select_ch + 1);
 }
 else if (select_ch == 6) { //random mode
   display.print("R");
 }
 display.setCursor(120, 9);
 if (select_ch != 6) { // not random mode
   display.print("H");
 }
 else if (select_ch == 6) { //random mode
   display.print("O");
 }
 display.setCursor(120, 18);
 if (select_ch != 6) { // not random mode
   display.print("O");
   display.setCursor(0, 36);
   display.print("L");
   display.setCursor(0, 45);
   display.print("M");
   display.setCursor(0, 54);
   display.print("R");
 }


 //random count square
 if (select_ch == 6) { //random mode
   //        display.drawRect(1, 32, 6, 32, WHITE);
   //    display.fillRect(1, 32, 6, 16, WHITE);
   display.drawRect(1, 62 - bar_max[bar_select] * 2, 6, bar_max[bar_select] * 2 + 2, WHITE);
   display.fillRect(1, 64 - bar_now * 2 , 6, bar_max[bar_select] * 2, WHITE);
 }
 //draw select triangle
 if ( select_menu == 0) {
   display.drawTriangle(113, 0, 113, 6, 118, 3, WHITE);
 }
 else  if ( select_menu == 1) {
   display.drawTriangle(113, 9, 113, 15, 118, 12, WHITE);
 }

 if (select_ch != 6) { // not random mode
   if ( select_menu == 2) {
     display.drawTriangle(113, 18, 113, 24, 118, 21, WHITE);
   }
   else  if ( select_menu == 3) {
     display.drawTriangle(12, 36, 12, 42, 7, 39, WHITE);
   }
   else  if ( select_menu == 4) {
     display.drawTriangle(12, 45, 12, 51, 7, 48, WHITE);
   }
   else  if ( select_menu == 5) {
     display.drawTriangle(12, 54, 12, 60, 7, 57, WHITE);
   }
 }

 //draw step dot
 for (k = 0; k <= 5; k++) { //k = 1~6ch
   for (j = 0; j <= limit[k] - 1; j++) { // j = steps
     display.drawPixel(x16[j] + graph_x[k], y16[j] + graph_y[k], WHITE);
   }
 }

 //draw hits line : 2~16hits
 for (k = 0; k <= 5; k++) { //ch count
   buf_count = 0;
   for (m = 0; m < 16; m++) {
     if (offset_buf[k][m] == 1) {
       line_xbuf[buf_count] = x16[m] + graph_x[k];//store active step
       line_ybuf[buf_count] = y16[m] + graph_y[k];
       buf_count++;
     }
   }

   for (j = 0; j < buf_count - 1; j++) {
     display.drawLine(line_xbuf[j], line_ybuf[j], line_xbuf[j + 1], line_ybuf[j + 1], WHITE);
   }
   display.drawLine(line_xbuf[0], line_ybuf[0], line_xbuf[j], line_ybuf[j], WHITE);
 }
 for (j = 0; j < 16; j++) {//line_buf reset
   line_xbuf[j] = 0;
   line_ybuf[j] = 0;
 }

 //draw hits line : 1hits
 for (k = 0; k <= 5; k++) { //ch count
   buf_count = 0;
   if (hits[k] == 1) {
     display.drawLine(15 + graph_x[k], 15 + graph_y[k], x16[offset[k]] + graph_x[k], y16[offset[k]] + graph_y[k], WHITE);
   }
 }

 //draw play step circle
 for (k = 0; k <= 5; k++) { //ch count
   if (mute[k] == 0) { //mute on = no display circle
     if (offset_buf[k][playing_step[k]] == 0) {
       display.drawCircle(x16[playing_step[k]] + graph_x[k], y16[playing_step[k]] + graph_y[k], 2, WHITE);
     }
     if (offset_buf[k][playing_step[k]] == 1) {
       display.fillCircle(x16[playing_step[k]] + graph_x[k], y16[playing_step[k]] + graph_y[k], 3, WHITE);
     }
   }
 }
 display.display();
}

この記事が気に入ったらサポートをしてみませんか?