見出し画像

800円で作るタッチコントローラー-モジュラーシンセ自作

Arduino nanoとMPR121を用いて、モジュラーシンセサイザー のタッチコントローラーを自作したので、その備忘録。

背景

自作モジュラーシンセの38作品目。
モジュラーシンセにはタッチコントローラーの種類が豊富だ。古くはBuchlaや、最近ではMake Noise Rene等で使われている。
タッチセンサーの良いところは、メカニカルなボタンと違い複雑な機構をもたず、安価なことだ。

タッチセンサーはSeeduino xiaoが内蔵しており、私もかつてドラムサンプルプレイヤーのインターフェースとして採用している。

今回はSeeeduino xiaoは用いず、Arduino nanoベースでタッチセンサーを使用する。

画像1

制作物のスペック

ユーロラック規格 3U 6HPサイズ
電源:50mA ( at 5V ) 
ゲート4出力、CV1出力。

タッチセンサーA,B,C,D:ON-OFF検出
モード切替:モーメンタリ/オルタネイト/バースト
POT:出力設定
Gate out A,B,C,D:ゲート出力(0-5V)
CV out:CV出力(5Vp-p、プリクオンタイズ可能、10bit)
LED

モード:モーメンタリ
タッチセンサーに触れている間、ゲートがONになる。

モード:オルタネイト
タッチセンサに触れるとゲートがON。タッチセンサから指を離してもON状態が継続する。再度タッチセンサに触れるとゲートがOFFする。

モード:バースト
タッチセンサに触れている間、ゲート出力を連打する。連打の周期はABCDの各チャンネルで個別に設定可能。

CV出力
タッチセンサーに触れると、A,B,C,Dそれぞれに割り当てたCV出力を行う。
CV出力は12音階にクオンタイズすることも出来る。

出力設定
ABCDすべてのセンサーを同時に触れてしばらくすると、出力設定モードに入る。セッティングはPOTを使用する。全部で5種類のセッティングができ、センサーAを押すとセッティング対象を切り替える事ができる。

設定1:クオンタイズ設定
POT左側でクオンタイズOFF、POT右側で12音階クオンタイズONする。

設定2~5:ABCD各チャンネルのCV出力
POTで0~5V(0~5oct)の範囲でCV出力電圧を設定できる。バーストモードでは、ここで設定した値が連打の周期に反映される。

出力設定モードでないとき、POTはLOW出力インピーダンスの選択ができる。POT左側はハイインピーダンスLOW出力、右側はローインピーダンスLOW出力。詳細は後述する。

画像2

製作費

総額約800円
---------------------------------
Arduino nano 200円
フロントパネル 100円
MCP4911 (DAC) 120円
MPR121モジュール 100円
他(汎用部品は下記リンク先参照)

MPR121モジュールはAliexpressから買える。1セットが100円以下。

画像3

ハードウェア

画像4

タッチセンサー MPR121
今回のコアとなる部品。全12チャンネルのタッチセンサー入力を検出できる。注意点としては、3.3V電源が必要であること。
3.3V電源はArduinoから供給可能だが、I2C通信は3.3Vと5Vのレベルシフトが必要だ。

レベルシフト
ArduinoとMPR121のI2C通信で3.3Vと5Vのレベルシフトを行う。汎用トランジスタの2SC1815を使えば、簡単な回路でレベルシフト回路を組むことが出来る。

ハイインピーダンスLow出力回路
地味だけど、今回最も思考した重要な回路。ゲート出力回路にある47kohm抵抗が重要な部品。

そもそも、なぜハイインピーダンスLow出力回路が必要か。
モジュラーシンセでは、複数のゲート出力をOR出力したい場合、出力回路が干渉してゲート出力電圧が低下してしまう欠点がある。

例えば、Aが5Vのhigh出力、Bが0Vのlow出力で、AとBをパッシブマルチで合成した場合、Aの出力は2.5Vまで低下してしまう。
対策としてバッファードマルチモジュールを使用するのだが、貴重なスペースを圧迫し、パッチングも複雑化してしまう。


電圧の干渉が発生するのは、low出力する側が強烈に電流を引き込んでいるから。つまり、ローインピーダンス状態でGNDに電流を引き込んでいる。
逆に、ハイインピーダンスでGNDに電流を引き込めば、電圧の干渉は最小限に抑えることが出来る。

Arduinoを用いて、どのようにハイインピーダンスで電流を引き込むか。それはLow出力の際に、出力端子をINPUTに切り替えてやり、外部に47kohmのプルダウン抵抗を接続すればよい。

出力端子がOUTPUTのままLow出力すると、強烈に電流を引き込んでしまい、電圧干渉が発生する。一方、出力端子をINPUTに変更すると、INPUT端子はOPEN状態(正確には100Mohmのハイインピーダンス)になり、電流を全く引き込まなくなる。しかし、電流を全く引き込まないと0Vに落とすことも出来ないので、47kohmの外部抵抗により、ある程度の電流を引き込むことで0Vに落とすことができる。

画像5


ソフトウェア

タッチセンサーMPR121はAdafruitのライブラリを使用している。12ch分のタッチ状況を0/1で読み取ることができる。ライブラリはArduino IDEからdownloadできる。

DAC通信用にSPI通信も使用している。SPIのライブラリを有効状態だと、12pinをdigitalWriteで内部プルアップに抵抗があるような挙動があったため、12pinをGATE出力として採用することを見送った。
5V出力はできるが、電流が絞られているようだ。詳細は未検証だが、SPIライブラリ有効中はSPIで使用するpinをSPI以外に使わないほうが良いかもしれない。

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

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

ソースコード

粗末だが公開する。悪い点があれば指摘を貰えると嬉しい。

#include <SPI.h>
#include <EEPROM.h>
#include <Wire.h>
#include "Adafruit_MPR121.h"  //touch sensor library

Adafruit_MPR121 cap = Adafruit_MPR121();
uint16_t touch_data = 0;  //use for touch sensor library
const int LDAC = 8;                       //spi LDAC pin number
bool touch[4] = {0, 0, 0, 0}; //detect touching
long touch_chat[4] = {0, 0, 0, 0}; //chattering countermeasure
long burst_timer[4] = {0, 0, 0, 0}; //use for burst mode
bool old_touch[4] = {0, 0, 0, 0};
bool gate[4] = {0, 0, 0, 0};
int CV[5] = {0, 0, 0, 0, 0};
byte mode = 0; //0=Momentary,1=Alternate,2=burst,3=CV setting
int CVmode_count = 0;//CV mode enter count
long CVmode_timer = 0;//CV mode enter count
byte CVmode = 0; //0=quantize set,1=A set,2=B set,3=C set,4=D set
long LED_timer = 0;
long timer;
int LED_LFO = 0;
bool quant = 0;//0= no quantize , 1 = pre quantize
bool output_mode = 0;//gate out impedance , 0=high impedance low out, 1=low impedance low out

const long cv_qnt[61] = {//10bit pre quantizer table
 0,  17, 34, 51, 68, 85, 102,  119,  136,  153,  170,  187,  204,  221,  239,  256,
 273,  290,  307,  324,  341,  358,  375,  392,  409,  426,  443,  460,  477,  495,  512,
 529,  546,  563,  580,  597,  614,  631,  648,  665,  682,  699,  716,  733,  751,  768,
 785,  802,  819,  836,  853,  870,  887,  904,  921,  938,  955,  972,  989,  1007, 1023
};

void setup() {
 pinMode(LDAC, OUTPUT) ;//spi
 pinMode(SS, OUTPUT) ;//spi
 pinMode(9, OUTPUT) ;//gateA
 pinMode(7, OUTPUT) ;//gateB
 pinMode(6, OUTPUT) ;//gateC
 pinMode(3, OUTPUT) ;//gateD
 pinMode(5, OUTPUT) ;//LED
 pinMode(2, INPUT_PULLUP) ;//toggle SW
 pinMode(4, INPUT_PULLUP) ;//toggle SW
 cap.begin(0x5A);//start mpr121
 timer = millis();//
 EEPROM.get(0x00, CV);

 SPI.begin();
 SPI.setBitOrder(MSBFIRST) ;
 SPI.setClockDivider(SPI_CLOCK_DIV8) ;
 SPI.setDataMode(SPI_MODE0) ;
 delay(50);
}

void loop() {
 for (int i = 0; i < 4; i++) {
   old_touch[i] =  touch[i];
 }

 if (mode != 3) {
   mode_select();  //read toggle SW

   if (analogRead(0) <= 512) {
     output_mode = 0;
   }
   else if (analogRead(0) > 512) {
     output_mode = 1;
   }
 }

 if (CV[0] <= 512) {
   quant = 0;
 }
 else if (CV[0] > 512) {
   quant = 1;
 }

 //  ---------------------------read touch sensor------------------------------------------
 touch_data = cap.touched();
 for (int i = 0; i < 4; i++) {
   if (mode != 2) {
     if (touch_chat[i] + 30 <= millis()) {//chattering countermeasure , 30msec interval
       touch[i] =  bitRead(touch_data, i);
       touch_chat[i] = millis();
     }
   }
   else if (mode == 2) {//burst interval
     touch[i] =  bitRead(touch_data, i);
   }
 }

 //  ---------------------------mode=0 Momentary------------------------------------------
 if (mode == 0) {
   for (int i = 0; i < 4; i++) {
     if (old_touch[i] == 0 &&  touch[i] == 1) {
       DACout(CV[i + 1]);
     }
   }
   trig_out();
 }

 //  ---------------------------mode=1 Alternate------------------------------------------
 if (mode == 1) {
   for (int i = 0; i < 4; i++) {
     if (old_touch[i] == 0 &&  touch[i] == 1) {
       DACout(CV[i + 1]);
     }
   }
   for (int i = 0; i < 4; i++) {
     if (old_touch[i] == 0 &&  touch[i] == 1) {
       gate[i] = !gate[i];
     }
   }

   if (gate[0] == 1) {
     pinMode(9, OUTPUT) ;
     digitalWrite(9, HIGH);
   }
   else if (gate[0] == 0) {
     if (output_mode == 0) {
       pinMode(9, INPUT) ;
     }
     else if (output_mode == 1) {
       digitalWrite(9, LOW);
     }
   }
   if (gate[1] == 1) {
     pinMode(7, OUTPUT) ;
     digitalWrite(7, HIGH);
   }
   else if (gate[1] == 0) {
     if (output_mode == 0) {
       pinMode(7, INPUT) ;
     }
     else if (output_mode == 1) {
       digitalWrite(7, LOW);
     }
   }
   if (gate[2] == 1) {
     pinMode(6, OUTPUT) ;
     digitalWrite(6, HIGH);
   }
   else if (gate[2] == 0) {
     if (output_mode == 0) {
       pinMode(6, INPUT) ;
     }
     else if (output_mode == 1) {
       digitalWrite(6, LOW);
     }
   }
   if (gate[3] == 1) {
     pinMode(3, OUTPUT) ;
     digitalWrite(3, HIGH);
   }
   else if (gate[3] == 0) {
     if (output_mode == 0) {
       pinMode(3, INPUT) ;
     }
     else if (output_mode == 1) {
       digitalWrite(3, LOW);
     }
   }
 }

 //  ---------------------------mode=2 burst------------------------------------------
 if (mode == 2) {
   for (int i = 0; i < 4; i++) {
     if (old_touch[i] == 0 &&  touch[i] == 1) {
       DACout(CV[i + 1]);
     }
   }

   for (int i = 0; i < 5; i++) {
     if (burst_timer[i] + CV[i + 1] / 3 + 3  <= millis()) {
       touch[i] = 0;
       burst_timer[i] = millis();
     }
   }
   trig_out();
 }

 //  ---------------------------CV setting mode------------------------------------------

 if (mode != 3) {
   if (touch_data == 15 && CVmode_timer + 100 <= millis()) {
     CVmode_count++;
     CVmode_timer = millis();
   }
   else if (touch_data = !15) {
     CVmode_count = 0;
     CVmode_timer = millis();
   }

   if (CVmode_count >= 10) {
     mode = 3;
     LED_timer = millis();
     digitalWrite(9, LOW);//all gate off
     digitalWrite(7, LOW);
     digitalWrite(6, LOW);
     digitalWrite(3, LOW);
     LEDflash(3);//flash 3times
     delay(500);
   }
 }

 if (mode == 3) {
   switch (CVmode) {
     case 0:
       CV[CVmode] = analogRead(0);
       if (old_touch[0] == 0 && touch[0] == 1) {
         CVmode++;
         LEDflash(CVmode + 1);
       }
       break;

     case 1:
       CV[CVmode] = analogRead(0);
       DACout(CV[CVmode]);
       if (old_touch[0] == 0 && touch[0] == 1) {
         CVmode++;
         LEDflash(CVmode + 1);
       }
       break;

     case 2:
       CV[CVmode] = analogRead(0);
       DACout(CV[CVmode]);
       if (old_touch[0] == 0 && touch[0] == 1) {
         CVmode++;
         LEDflash(CVmode + 1);
       }
       break;

     case 3:
       CV[CVmode] = analogRead(0);
       DACout(CV[CVmode]);
       if (old_touch[0] == 0 && touch[0] == 1) {
         CVmode++;
         LEDflash(CVmode + 1);
       }
       break;

     case 4:
       CV[CVmode] = analogRead(0);
       DACout(CV[CVmode]);
       if (old_touch[0] == 0 && touch[0] == 1) {
         CVmode++;
         LEDflash(CVmode + 1);
         CVmode = 0;
         mode_select();
         CVmode_count = 0;
         eeprom(); //save setting data
       }
       break;
   }
 }

 //  ---------------------------LED turn on/off------------------------------------------

 if (mode != 3) {
   if (touch[0] == 1 || touch[1] == 1 || touch[2] == 1 || touch[3] == 1) {
     LED(touch[0] * 63 + touch[1] * 63 + touch[2] * 63 + touch[3] * 63);
   }
   else if (touch[0] == 0 && touch[1] == 0 && touch[2] == 0 && touch[3] == 0) {
     LED(0) ;
   }
 }

 else if (mode == 3) {
   if (LED_timer + 1 <= millis()) {
     LED_LFO = LED_LFO+1;
     if (LED_LFO >= 255) {
       LED_LFO = 0;
       if (CVmode != 0) {//gate on for confirm CV setting output
         touch[CVmode - 1] = 1;
         trig_out();
         delay(10);
         touch[CVmode - 1] = 0;
         trig_out();
       }
     }
     LED(LED_LFO);
     LED_timer = millis();
   }
 }
}

void mode_select() {
 if (digitalRead(2) == 0 && digitalRead(4) == 1) {
   mode = 2;
 }
 if (digitalRead(2) == 1 && digitalRead(4) == 1) {
   mode = 1;
 }
 if (digitalRead(2) == 1 && digitalRead(4) == 0) {
   mode = 0;
 }
}

void LEDflash(byte times) {
 for (int i = 0; i < times; i++) {
   LED(0);
   delay(50);
   LED(255);
   delay(50);
 }
}

void trig_out() {
 if (touch[0] == 1) {
   pinMode(9, OUTPUT) ;
   digitalWrite(9, HIGH);
 }
 else if (touch[0] == 0) {
   if (output_mode == 0) {
     pinMode(9, INPUT) ;
   }
   else if (output_mode == 1) {
     digitalWrite(9, LOW);
   }
 }
 if (touch[1] == 1) {
   pinMode(7, OUTPUT) ;
   digitalWrite(7, HIGH);
 }
 else if (touch[1] == 0) {
   if (output_mode == 0) {
     pinMode(7, INPUT) ;
   }
   else if (output_mode == 1) {
     digitalWrite(7, LOW);
   }
 }
 if (touch[2] == 1) {
   pinMode(6, OUTPUT) ;
   digitalWrite(6, HIGH);
 }
 else if (touch[2] == 0) {
   if (output_mode == 0) {
     pinMode(6, INPUT) ;
   }
   else if (output_mode == 1) {
     digitalWrite(6, LOW);
   }
 }
 if (touch[3] == 1) {
   pinMode(3, OUTPUT) ;
   digitalWrite(3, HIGH);
 }
 else if (touch[3] == 0) {
   if (output_mode == 0) {
     pinMode(3, INPUT) ;
   }
   else if (output_mode == 1) {
     digitalWrite(3, LOW);
   }
 }
}

void DACout(int dat) {
 if (quant == 1) {
   dat = cv_qnt[map(dat, 0, 1023, 0, 60)];//quantize
 }
 digitalWrite(LDAC, HIGH) ;//spi communication
 digitalWrite(SS, LOW) ;
 SPI.transfer((dat >> 6) | 0x30) ;
 SPI.transfer((dat << 2) & 0xff) ;
 digitalWrite(SS, HIGH) ;
 digitalWrite(LDAC, LOW) ;
}

void LED(byte R) {
 analogWrite(5, R);//PWM output
}

void eeprom() {
 EEPROM.put(0x00, CV);//save setting data
}

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