見出し画像

900円で作るリボンコントローラー&キーボード-モジュラーシンセ自作

NSX-39の印刷鍵盤とArduinoを使って、モジュラーシンセサイザー のリボンコントローラー&キーボードを自作したので、その備忘録。

背景

自作モジュラーシンセの34作品目。
シンセサイザーの良いところは、制御装置の選択肢が豊富にあることだ。
最もメジャーなのが鍵盤だが、シーケンサーで自動演奏もできるし、ブレスコントローラー(息)で演奏もできる、テルミンの様に手をかざして演奏もできる。
そんな豊富な制御装置の一つにリボンコントローラーがある。

初期のMOOG モジュラーシンセサイザーはリボンコントローラーを備えていた。ユーロラックではリボンコントローラーを滅多に見る機会が無いので、自作を試みることにした。

画像5

制作物のスペック

スタンドアロン型デスクトップ
電源:USB5V又は9V電池

CV出力:0~5V
Gate出力:0 / 5V
電源スイッチ:9V電池のON/OFF切り替え
オクターブ切替スイッチ:-1 / 0 / +1 oct
モード切替スイッチ:ribbon / key without LFO / key with LFO

制御はスタイラスペンで行う。
タッチしてる間はGate出力がON、ペンを鍵盤から離すとOFFになる。

モード1:リボンコントローラー
鍵盤上部のリボンコントローラーを使って演奏する。
CVのレンジはオクターブ切替スイッチで3種類から選択できる。
クオンタイズはされていないので、正確な音階を演奏することは難しいが、ドローンの様な演奏に向いている。

モード2:キーボード without LFO
鍵盤をタッチして演奏する。オクターブ切替スイッチでCV出力のオクターブを変更できる。出力電圧はクロマチックスケールの1V/octでクオンタイズされている。

モード3:キーボード with LFO
動作はモード2と同じだが、ノートONが継続するとピッチにLFOが掛かる(ビブラート)。LFOの周波数、振幅、ノートONからLFOが掛かるまでの時間はソースコードから簡単に変更ができる。

製作費

総額約900円
---------------------------------
arduino nano互換品 200円
印刷鍵盤 300円
MPC4725モジュール 100円

印刷鍵盤とスタイラスペンはNSX-39(通称:ポケットミク)から分解して手に入れた。NSX-39はミントコンディションの中古品を、ネットオークションで1300円で購入した。

画像1


音源基板と印刷鍵盤を取り出すことができる。音源基板を1000円、印刷鍵盤とスタイラスペンを300円と見積もっている。
分解は非常に簡単で、ネジを外すだけでよい。

画像2

ハードウェア

印刷鍵盤はカーボンが印刷されていて、約100kΩの抵抗値をもっている。この鍵盤の片側に5V、もう片側をGNDに接続し、スタイラスペンでタッチしている場所の電圧値をA7pin読み出している。

画像4

A7 pinの電圧がそのままになるので、ノイズを低減する必要がある。
VrefとGND間に接続された2.2uFと、68nFのコンデンサはADCのノイズを小さくする役割がある。

リボンコントローラモードでは、D2をOUTPUT設定しLOW出力にしている。つまり100kΩのプルダウンが有効になる。
スタイラスペンを鍵盤から離すと、プルダウン抵抗によりA7の電圧は0Vになる。その0Vを検出することで、ノートON/OFF(ペンがタッチしている/していない)を検出することが出来る。

これより難解な話をする。
キーボードモードでは、D2をINPUT設定している。つまりD2はハイインピーダンスとなり、100kΩは機能しない。100kΩでプルダウンすると印刷鍵盤の抵抗値により分圧が発生し、A7 pinの読み取り精度が悪化するためだ。鍵盤のタッチしている場所(抵抗値)のわずかな変化の影響が発生し、隣り合う鍵盤と見分けることが出来ない。
100kΩのプルダウンを無くすことで分圧の影響を無くし、精度よくADCが電圧を読むことができる。しかし、ペンを離したときにノートOFFを検出できない背反がある。プルダウン抵抗が無いと、68nFコンデンサに溜まった電荷を抜く事が出来ないからだ。

コンデンサに溜まった電荷を抜くために、マルチプレクサのメモリ効果を利用している。メモリ効果とは、簡単に言うと「他のADCのpinを通じて電荷が移動して正しい電圧を読めない」という事だ。この「他のADCのpinを通じて電荷が移動」する原理を用いて、A7 pinのコンデンサの電荷を抜いている。

A6 pinがGNDに設置しているのは、A7 pinの電荷をマイコン内部経由でA6 pinからGNDに逃がすためにある。もっと賢い解決方法があるかもしれないが、私にはこれしか思いつかなかった。
A6 pin経由でA7 pinの電荷が逃げるので、ペンを離すとA7 pinの電圧は緩やかに0Vへ向かう。その電圧の変化を検出して、ノートOFF(ペンが離れた)を検出している。

画像4

ソフトウェア

先述の「他のADCのpinを通じて電荷が移動」をするために、ADCの6pinと7pinを交互に複数回読み込んでいる。繰り返し回数が増えるほど、電荷が抜けるスピードが速くなる。

この制御には欠点がある。電荷を抜くスピード次第では、ノートONを検出できない音階が出来てしまう。理由は、リボンコントローラの電圧が大きく変化した瞬間をノートONと認識させているため、電圧の変化量が小さい場合にノートONを検出できない。

    for (int j = 0; j < 30; j++) {//dumy reading for memory effect of ADC
     dumy = analogRead(6);
     dumy = analogRead(7);
   }

電荷を抜いた後に、本命の電圧を読み取る。ノイズ対策として100回読み取りをして、その平均値を算出している。

    for (int j = 0; j < 100; j++) {//To improve the reading accuracy, measure multiple times and calculate the average value.
     rbn += analogRead(7);
   }
   rbn = rbn / 100 * calb;
画像6

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

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

ソースコード

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

#include <Wire.h>

long rbn, old_rbn, out_rbn, timer;
int dumy = 0;//for memory effect
float calb = 1.01;//Carbon keyboard calibration
int cmp1, cmp2, CV_out, i;
byte mode = 1; //0=ribbon cont,1=key without lfo,2=key with lfo

//----LFO setting----
int lfo_CV = 0;
int lfo_rate = 3;//
int lfo_amp = 25;//
int lfo_delay = 360;//
long lfo_rad = 0;//make sin wave
long lfo_delay_timer = 0;
long lfo_timer = 0;

bool oct_sw1, oct_sw2;//SW read
bool mode_sw1, mode_sw2; //SW read
int oct = 0;//0~2 , octave UP

int old_note_on = 0;
int note_on = 0;
int note_off = 0;
int note_on_int = 5;

int dac[61] = {//CV out pre quantize
 0,  68, 137,  205,  273,  341,  410,  478,  546,  614,  683,  751,
 819,  887,  956,  1024, 1092, 1161, 1229, 1297, 1365, 1434, 1502, 1570,
 1638, 1707, 1775, 1843, 1911, 1980, 2048, 2116, 2185, 2253, 2321, 2389,
 2458, 2526, 2594, 2662, 2731, 2799, 2867, 2935, 3004, 3072, 3140, 3209,
 3277, 3345, 3413, 3482, 3550, 3618, 3686, 3755, 3823, 3891, 3959, 4028, 4095
};
int note[28] = {//touching key number
 27,  26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9,  8,  7,  6,  5,  4,  3,  2,  1, 1
};

int rbn_table[28] = {//detect touching key number
 0, 90,  133,  172,  214,  247,  279,  310,  340,  370,  400,  433,  465,  495,  525,  555,  585,  615,  645,  680,  709,  743,  769,  805,  840,  880,  920,  950
};

void setup() {
 pinMode(11, OUTPUT);//gate and LED out
 pinMode(7, INPUT_PULLUP); //mode sw
 pinMode(8, INPUT_PULLUP); //mode sw
 pinMode(5, INPUT_PULLUP); //oct sw
 pinMode(4, INPUT_PULLUP); //oct sw
 pinMode(2, OUTPUT);//pull down resistor valid
 timer = millis();
 lfo_timer = millis();
 lfo_delay_timer = millis();
 note_off = 1;
 Wire.begin();//DAC communication
}

void loop() {
 old_note_on = note_on;
 //------------------LFO READING--------------------------
 if (lfo_timer + 1 <= millis()) {
   lfo_rad = lfo_rad + lfo_rate;
   lfo_CV = (int) (sin(lfo_rad / PI) * lfo_amp);
   lfo_timer = millis();
 }

 //------------------MODE and OCTAVE SW--------------------------
 if (timer + 300 <= millis()) {//Reduce frequency to improve reading accuracy of ribbon controller
   mode_sw1 = digitalRead(8);
   mode_sw2 = digitalRead(7);
   if (mode_sw1 == 1 && mode_sw2 == 0) {
     mode = 0;
   }
   else if (mode_sw1 == 1 && mode_sw2 == 1) {
     mode = 1;
   }
   else if (mode_sw1 == 0 && mode_sw2 == 1) {
     mode = 2;
   }

   oct_sw1 = digitalRead(4);
   oct_sw2 = digitalRead(5);
   if (oct_sw1 == 1 && oct_sw2 == 0) {
     oct = 0;
   }
   else if (oct_sw1 == 1 && oct_sw2 == 1) {
     oct = 1;
   }
   else if (oct_sw1 == 0 && oct_sw2 == 1) {
     oct = 2;
   }
   timer = millis();
 }

 //------------------MODE=0 RIBBON CONTROL--------------------------
 if (mode == 0) {
   old_rbn = rbn;
   //By enabling the pull-down resistor of the ADC, the detection accuracy of note on / off is improved.
   pinMode(2, OUTPUT);//pull down resistor valid
   digitalWrite(2, LOW);//pull down resistor valid

   delay(5);

   for (int j = 0; j < 10; j++) {//100 noteon
     rbn += analogRead(7);
   }
   rbn = rbn / 10 * calb;


   if (rbn >= 70 && abs(old_rbn - rbn) <= 5) {
     digitalWrite(11, HIGH);
         switch (oct) {
     case 0://short range
       dacout(1024 - rbn);
       break;
     case 1://mid range
       dacout((1024 - rbn) * 2 * 1.33);//1.33 is the correction value for scaling
       break;
     case 2://long range
       dacout((1024 - rbn) * 4);
       break;
   }
   }
   else if (rbn < 70 ) {
     digitalWrite(11, LOW);
   }
   else if (abs(old_rbn - rbn) > 5) {
     digitalWrite(11, LOW);
   }
 }

 //------------------MODE=1,2 KEYBOARD--------------------------
 else if (mode == 1 || mode == 2) {
   //By disabling the pull-down resistor, you can measure the voltage of the keyboard accurately.
   pinMode(2, INPUT);

   //If the pull-down resistor is disabled, the charge accumulated in the ADC cannot be removed.
   //By reading the 0V voltage of ADC 6pin, the memory effect of the multiplexer can be generated and the charge of ADC 7pin can be slowly depleted.
   for (int j = 0; j < 30; j++) {//dumy reading for memory effect of ADC
     dumy = analogRead(6);
     dumy = analogRead(7);
   }

   for (int j = 0; j < 100; j++) {//To improve the reading accuracy, measure multiple times and calculate the average value.
     rbn += analogRead(7);
   }
   rbn = rbn / 100 * calb;
   note_on_int--;

   if (abs(out_rbn - rbn) > 7 && note_off == 1 ) {//note on sence
     note_on = 1;
     note_off = 0;
     out_rbn = rbn;
     note_on_int = 1000;
   }
   if (note_on_int >= 997) {
     out_rbn = rbn;
   }

   if (abs(out_rbn - rbn) > 3 && note_on == 1 && note_on_int < 997) {//note off sence
     note_off = 1;
     note_on = 0;
   }

   if (note_on == 0) { //counter measure of re-note
     out_rbn = rbn;
   }

   for (int j = 0; j < 27; j++) {//Detects touched keyboard note
     if ( rbn >= rbn_table[j] && rbn < rbn_table[j + 1]) {
       cmp1 = rbn - rbn_table[j];//Detect closest note
       cmp2 = rbn_table[j + 1] - rbn; //Detect closest note
       i = j - 1;
       break;
     }
   }

   if (cmp1 >= cmp2) {//Detect closest note
     CV_out = note[i + 1];
   }
   else if (cmp2 > cmp1) {//Detect closest note
     CV_out = note[i];
   }

   //------OUTPUT CV & gate-----
   //without lfo
   if (mode == 1 && note_on == 1 && (lfo_delay_timer + lfo_delay) > millis()) {
     dacout(dac[CV_out + 12 * oct ]); //output DAC , oct range = 2(keyboard) + 3(octave by vr)
   }
   else if (mode == 1 && note_on == 1 && (lfo_delay_timer + lfo_delay) <= millis()) {
     dacout(dac[CV_out + 12 * oct ]); //output DAC , oct range = 2(keyboard) + 3(octave by vr)
   }

   //with lfo
   else if (mode == 2 && note_on == 1 && (lfo_delay_timer + lfo_delay) > millis()) {
     dacout(dac[CV_out + 12 * oct ]); //output DAC , oct range = 2(keyboard) + 3(octave by vr)
   }
   else if (mode == 2 && note_on == 1 && (lfo_delay_timer + lfo_delay) <= millis()) {
     dacout(dac[CV_out + 12 * oct ] + lfo_CV); //output DAC , oct range = 2(keyboard) + 3(octave by vr)
   }

   if (old_note_on == 0 && note_on == 1) {
     lfo_delay_timer = millis();
     digitalWrite(11, HIGH);//gate on
   }
   if (old_note_on == 1 && note_on == 0) {
     digitalWrite(11, LOW);//gate off
   }
   
 }
}

void dacout(int MPC) {
 Wire.beginTransmission(0x60);
 Wire.write((MPC >> 8) & 0x0F);
 Wire.write(MPC);
 Wire.endTransmission();
}

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