見出し画像

800円で作るArduinoOLEDオシロスコープ&スペクトルアナライザー-モジュラーシンセ自作

ArduinoとSSD1306OLEDを用いて、モジュラーシンセサイザー のオシロスコープ&スペクトルアナライザーを自作したので、その備忘録。

背景

自作モジュラーシンセの25作品目。
最近、ビデオシンセサイザーが種類を増やしてきている。ブラウン管に投影するものや、モジュール自体がディスプレイを持つものもある。

音楽が聴くものから見るもの・体験するものに変化している現在、視覚的に音楽を楽しむ流れが起こるのは、必然ともいえる。

今回、音やシーケンスを視覚で楽しむために、そして理解するためのモジュールを作成しようと思った。
目的は「見て楽しむこと」「理解すること」であり、「測定すること」ではない。正確な周波数、正確な電圧を測定することは目的ではない。そのため、部品は表示を大胆に減らしている。
まずはオシロスコープとスペクトルアナライザーを実装したが、将来的にはVUメーターやビデオシンセサイザーに機能拡張をする予定だ。

画像2

制作物のスペック

ユーロラック規格 3U 6HPサイズ
電源:50mA ( at 5V ) / 49mA ( at12V )
5V単電源で動作可能。または12V単電源で動作可能。

OLED : 128*64 pixel 0.96inch
knob1:選択用ロータリーエンコーダ(push sw含む)
knob2:GAIN
toggle SW : AC /DC切り替え
TRIG IN : 外部トリガーIN
INPUT : 観察対象信号IN
OUTPUT : 観察対象信号OUT(INPUTと短絡)
観察可能電圧は5Vp-p

基本的な操作はOLEDの表示を見ながら、ロータリーエンコーダとプッシュボタンで操作する。
一定時間操作が無いと、操作に関わる表示は隠れ、操作を再開すると表示が現れる。

ゲインは入力信号をオペアンプで増幅する。波形を見る事が目的なので、非反転増幅だ。AC入力の場合は作動増幅、DC入力の場合は単なる増幅。
「増幅したら正しい電圧が測定できない」と思うかもしれないが、前述の通り「見て楽しむこと」が目的なので、そもそも正しい電圧を入力する機能を省略している。つまりは、波形をでっかく表示したいだけだ。

トグルSWで入力のACカップリング、DCカップリングを切り替える。ACカップリングを用いる場面では、Arduinoが2.5Vを作り出し、中間電位をオフセットしてくれる。

TRIGはHIGH,LOWの信号入力端子。
INPUT端子とOUTPUT端子は短絡している。単なるマルチプルだ。

画像1

表示モード

現時点で4種類のモードを搭載している。将来的に拡張予定。

"mode1 : LFO / EG モード"
LFOやEGなどの、低周波数を観察するためのモード。
DCカップリングでの使用を想定している。
時間軸と波形表示のオフセットのパラメータを制御できる。
TRIG INは使用しない。

"mode2 : LFO / EG モード"
音声波形などの、連続した高周波数を観察するためのモード。
ACカップリングでの使用を想定している。
時間軸と表示切替頻度を制御できる。
TRIG INは使用しない。

"mode3 : shot モード"
パーカッションなどの、瞬間的な波形を観察するためのモード。
ACカップリングでの使用を想定している。
時間軸のみ制御できる。波形表示のトリガー時に"TRIG"がハイライトする。
TRIG IN入力と同時に波形を読み込む。(波形の取り込み、表示に時間がかかるので早いテンポだとトリガーが間に合わない事がある)

"mode4 : スペクトルアナライザーモード"
音声波形や曲などの、連続した高周波数のスペクトルを表示する。
ACカップリングでの使用を想定している。
高周波数のゲインと、ノイズフィルターを制御できる。
TRIG INは使用しない。
観察可能な周波数は4.5kHzまで。精度も悪いが、「見て楽しむこと」が目的である。倍音が増えると棒の数が増え、kickのリズムに合わせてスペクトルが揺れるのを楽しめれば良い。

画像5

製作費

総額800円
---------------------------------
Arduino nano 互換品200円
OLED(SSD1306) 180円
op-amp 30円
パネル 100円
ロータリーエンコーダモジュール 100円etc

OLEDはSPI通信のものを使う。I2Cと比較して、通信速度が速いからだ。

画像3

ロータリーエンコーダモジュールはaliexpressから調達した。
そのままではSWが5Vにプルアップされているが、写真のR5を取り外せばOPEN/GNDのボタンとして(INTERNAL PULLUP用として)使う事ができる。
ロータリーエンコーダモジュールの5Vは未使用、OPENのまま使用する。

プログラミング/ハードウェア

複数のモードがあり、各モードで求められるハードの性能が異なる。そのため、モードに合わせてハード仕様を変えている。

        pinMode(6, INPUT);//no active 2pole filter
       analogWrite(3, 0); //offset = 0V
       ADCSRA = ADCSRA & 0xf8;//fast ADC *8 speed
       ADCSRA = ADCSRA | 0x04;//fast ADC *8 speed

1.ACカップリング/DCカップリング切り替え
AC/DC回路の切り替えはトグルスイッチで手動で行うが、オフセットはArduinoで行っている。
analogWrite(3, **); が該当。
ACカップリングを用いるモードでは、Arduinoの高速PWMでオフセット電圧の2.5Vを作り出している。
DCカップリングを用いるモードでは、Arduinoは0Vを出力することでオフセット電圧は0Vとなっている。

2.ADCレートの切り替え
モード1~3はADCの読み込み速度を上げるためにレジスタを変更している。
ADCSRA = ADCSRA & 0xf8;
ADCSRA = ADCSRA | 0x04;を実行することで、デフォルトのセッティングからADC読み込み頻度を8倍に増やしている。
これにより音声の様な数kHzの信号の波形も読み取れるようになる。

逆にスペクトルアナライザーモードではADC読み込み頻度をデフォルトに戻している。理由は、使用しているfix_fftライブラリの動作を有効にするためだ。(ADCを高速にしていると、fix_fftが正しく動作しなかった)

3.ローパスフィルタの切り替え
モード1~3ではA0pin直前のローパスフィルタは不要である。
一方、モード4のスペクトルアナライザはローパスフィルタを用いて9kHz以上の波形を除去してやる必要がある。「サンプリングの定理」により9kHz以上の波形が含まれると、正しいスペクトルが表示されないからだ。

pinMode(6, INPUT);にすると、6pinはハイインピーダンス、すなわちオープンとなるため、2極のローパスフィルタが無効となる。

pinMode(6, OUTPUT);
digitalWrite(6, LOW);にすると、6pinはGNDに接続されるため、2極のローパスフィルタが有効になる。

画像4

スペクトルアナライザではfix_fftライブラリを使用している。
Arduino IDEのライブラリマネージャから入手可能だ。
測定周波数が4.5kHzまでと小さいが、高速PWMやOLEDと併用が出来る強みがある。

他にもFHTライブラリというものがあり、これを用いればより高い周波数のスペクトルまで測定可能だ。しかし、OLEDとの併用で問題が発生したため使用しなかった。

電子部品も可能な限り少なくしている。
例えばオペアンプの2.5V出力にはVccとGNDの間にコンデンサをいれて電圧を安定させるのが理想だが、割愛している。OLEDのy軸は64 pixelしかないので分解のは6bitしかない。ノイズの影響はほぼ受けない。

1kohmと22nFによる2極のローパスフィルタも、高周波を完全にカットは出来ない。スペクトルアナライザモードでは、カットしきれなかった高い周波数により誤測定が発生している。4極にすれば改善するけど、面倒くさいから放置している。

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

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

ソースコード

粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
開発スピードを優先してるため、結構適当なプログラム。


#include <avr/io.h>//for fast PWM
#include "fix_fft.h"//spectrum analyze

//OLED display setting
#include <SPI.h>//for OLED display
#include <Adafruit_GFX.h>//for OLED display
#include <Adafruit_SSD1306.h> //for OLED display
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_MOSI   9
#define OLED_CLK   10
#define OLED_DC    11
#define OLED_CS    12
#define OLED_RESET 13
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT,
                        OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

//rotery encoder setting
#define  ENCODER_OPTIMIZE_INTERRUPTS //contermeasure of rotery encoder noise
#include <Encoder.h>
Encoder myEnc(2, 4);//rotery encoder digitalRead pin
float oldPosition  = -999;//rotery encoder counter
float newPosition = -999;


byte mode = 1;//1=low freq oscilo , 2=high freq oscilo , 3 = mid freq oscilo with external trig , 4 = spectrum analyze
byte old_mode = 1;//for initial setting when mode change.
byte param_select = 0;
byte param = 0;
byte param1 = 1;
bool param1_select = 0;
byte param2 = 1;
bool param2_select = 0;
int rfrs = 0;//display refresh rate

bool trig = 0;//external trig
bool old_trig = 0;//external trig , OFF -> ON detect
bool old_SW = 0;//push sw, OFF -> ON detect
bool SW = 0;//push sw

unsigned long trigTimer = 0;
unsigned long hideTimer = 0;//hide parameter count
bool hide = 0; //1=hide,0=not hide

char data[128], im[128] , cv[128]; //data and im are used for spectrum , cv is used for oscilo.

void setup() {
 //display setting
 display.begin(SSD1306_SWITCHCAPVCC);
 display.clearDisplay();
 display.setTextSize(0);
 display.setTextColor(WHITE);
 analogReference(DEFAULT);

 //pin mode setting
 pinMode(3, OUTPUT) ;//offset voltage
 pinMode(5, INPUT_PULLUP);//push sw
 pinMode(6, INPUT) ;//input is high impedance -> no active 2pole filter , output is active 2pole filter
 pinMode(7, INPUT); //external triger detect

 //fast pwm setting
 TCCR2B &= B11111000;
 TCCR2B |= B00000001;

 //fast ADC setting
 ADCSRA = ADCSRA & 0xf8;//fast ADC *8 speed
 ADCSRA = ADCSRA | 0x04;//fast ADC *8 speed
};

void loop() {
 old_SW = SW;
 old_mode = mode;

 SW = digitalRead(5);
 //select mode by push sw
 if (old_SW == 0 && SW == 1 && param_select == param) {
   param_select = 0;
   hideTimer = millis();
 }
 else if (old_SW == 0 && SW == 1 && param == 1) {
   param_select = param;
   hideTimer = millis();
 }
 else   if (old_SW == 0 && SW == 1 && param == 2) {
   param_select = param;
   hideTimer = millis();
 }
 else   if (old_SW == 0 && SW == 1 && param == 3) {
   param_select = param;
   hideTimer = millis();
 }
 mode = constrain(mode, 1, 4);
 param = constrain(param, 1, 3);

 //rotery encoder input
 newPosition = myEnc.read();
 if ( (newPosition - 3) / 4  > oldPosition / 4) {
   oldPosition = newPosition;
   hideTimer = millis();
   switch (param_select) {
     case 0:
       param --;
       break;

     case 1:
       mode --;
       break;

     case 2:
       param1 --;
       break;

     case 3:
       param2 --;
       break;
   }
 }

 else if ( (newPosition + 3) / 4  < oldPosition / 4 ) {
   oldPosition = newPosition;
   hideTimer = millis();
   switch (param_select) {
     case 0:
       param ++;
       break;

     case 1:
       mode ++;
       break;

     case 2:
       param1 ++;
       break;

     case 3:
       param2 ++;
       break;

   }
 }

 //initial settin when mode change.
 if (old_mode != mode) {
   switch (mode) {
     case 1:
       param1 = 2; //time
       param2 = 1; //offset
       pinMode(6, INPUT);//no active 2pole filter
       analogWrite(3, 0); //offset = 0V
       ADCSRA = ADCSRA & 0xf8;//fast ADC *8 speed
       ADCSRA = ADCSRA | 0x04;//fast ADC *8 speed
       break;

     case 2:
       param1 = 3; //time
       param2 = 3; //offset
       analogWrite(3, 127); //offset = 2.5V
       pinMode(6, INPUT);//no active 2pole filter
       ADCSRA = ADCSRA & 0xf8;//fast ADC *8 speed
       ADCSRA = ADCSRA | 0x04;//fast ADC *8 speed
       break;

     case 3:
       param1 = 2; //time
       analogWrite(3, 127); //offset = 2.5V
       pinMode(6, INPUT);//no active 2pole filter
       ADCSRA = ADCSRA & 0xf8;//fast ADC *8 speed
       ADCSRA = ADCSRA | 0x04;//fast ADC *8 speed
       break;

     case 4:
       param1 = 2; //high freq amp
       param2 = 3; //noise filter
       analogWrite(3, 127); //offset = 2.5V
       pinMode(6, OUTPUT);//active 2pole filter
       digitalWrite(6, LOW);//active 2pole filter
       ADCSRA = ADCSRA & 0xf8;//standard ADC speed
       ADCSRA = ADCSRA | 0x07;//standard ADC speed
       break;
   }
 }

 //OLED parameter hide while no operation
 if ( hideTimer + 5000 >= millis() ) {//
   hide = 1;
 }
 else {
   hide = 0;
 }

 //LFO mode--------------------------------------------------------------------------------------------
 if (mode == 1) {
   param = constrain(param, 1, 3);
   param1 = constrain(param1, 1, 8);
   param2 = constrain(param2, 1, 8);

   display.clearDisplay();

   //store data
   for (int i = 126 / (9 - param1); i >= 0; i--) {
     display.drawLine(127 - (i * (9 - param1)) , 63 - cv[i] - (param2 - 1) * 4, 127 - (i + 1) * (9 - param1) , 63 - cv[(i + 1 )] - (param2 - 1) * 4, WHITE); //right to left
     cv[i + 1] = cv[i];
     if (i == 0) {
       cv[0] = analogRead(0) / 16;
     }
   }

   //display
   if (hide == 1) {
     display.drawLine((param - 1) * 42, 8,   (param - 1) * 42 + 36, 8, WHITE);

     display.setTextColor(WHITE);
     if (param_select == 1) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(0, 0);
     display.print("Mode:");
     display.setCursor(30, 0);
     display.print(mode);

     display.setTextColor(WHITE);
     if (param_select == 2) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(42, 0);
     display.print("Time:");
     display.setCursor(72, 0);
     display.print(param1);

     display.setTextColor(WHITE);
     if (param_select == 3) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(84, 0);
     display.print("Offs:");
     display.setCursor(114, 0);
     display.print(param2);
   }
 }

 //wave mode---------------------------------------------------------------------------------------------
 else if (mode == 2) {
   param = constrain(param, 1, 3);
   param1 = constrain(param1, 1, 8);
   param2 = constrain(param2, 1, 6);

   //store data
   if (param1 > 5) {//for mid frequency
     for (int i = 127 ; i >= 0; i--) {
       cv[i] = analogRead(0) / 16;
       delayMicroseconds((param1 - 5) * 20);
     }
     rfrs++;
     if (rfrs >= (param2 - 1) * 2) {
       rfrs = 0;
       display.clearDisplay();
       for (int i = 127 ; i >= 1; i--) {
         display.drawLine(127-i , 63 - cv[i - 1], 127-(i + 1)  , 63 - cv[(i)], WHITE);
       }
     }
   }

   else if (param1 <= 5) {//for high frequency
     for (int i = 127 / (6 - param1); i >= 0; i--) {
       cv[i] = analogRead(0) / 16;
     }
     rfrs++;
     if (rfrs >= (param2 - 1) * 2) {
       rfrs = 0;
       display.clearDisplay();
       for (int i = 127 / (6 - param1); i >= 1; i--) {
         display.drawLine(127-i * (6 - param1), 63 - cv[i - 1], 127-(i + 1) * (6 - param1)  , 63 - cv[(i)], WHITE);
       }
     }
   }

   //display
   if (hide == 1) {
     display.drawLine((param - 1) * 42, 8,   (param - 1) * 42 + 36, 8, WHITE);
     display.setTextColor(WHITE);
     if (param_select == 1) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(0, 0);
     display.print("Mode:");
     display.setCursor(30, 0);
     display.print(mode);

     display.setTextColor(WHITE);
     if (param_select == 2) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(42, 0);
     display.print("Time:");
     display.setCursor(72, 0);
     display.print(param1);

     display.setTextColor(WHITE);
     if (param_select == 3) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(84, 0);
     display.print("Rfrs:");
     display.setCursor(114, 0);
     display.print(param2);
   }
 }

 //shot mode---------------------------------------------------------------------------------------------
 else if (mode == 3) {
   param = constrain(param, 1, 2);
   param1 = constrain(param1, 1, 4);
   old_trig = trig;
   trig = digitalRead(7);

   //    trig detect
   if (old_trig == 0 && trig == 1  ) {
     for (int i = 10 ; i <= 127; i++) {
       cv[i] = analogRead(0) / 16;
       delayMicroseconds(100000 * param1);//100000 is magic number
     }
     for (int i = 0 ; i < 10; i++) {
       cv[i] = 32;
     }
   }

   display.clearDisplay();
   for (int i = 126 ; i >= 1; i--) {
     display.drawLine(i , 63 - cv[i], (i + 1)  , 63 - cv[(i + 1)], WHITE);
   }

   //display
   if (hide == 1) {
     display.drawLine((param - 1) * 42, 8,   (param - 1) * 42 + 36, 8, WHITE);
     display.setTextColor(WHITE);
     if (param_select == 1) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(0, 0);
     display.print("Mode:");
     display.setCursor(30, 0);
     display.print(mode);

     display.setTextColor(WHITE);
     if (param_select == 2) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(42, 0);
     display.print("Time:");
     display.setCursor(72, 0);
     display.print(param1);
     display.setTextColor(WHITE);
     if (trig == 1) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(84, 0);
     display.print("TRIG");
   }
 }

 //spectrum analyze mode---------------------------------------------------------------------------------------------
 else if (mode == 4) {
   param = constrain(param, 1, 3);
   param1 = constrain(param1, 1, 4);//high freq sence
   param2 = constrain(param2, 1, 8);//noise filter

   for (byte i = 0; i < 128; i++) {
     int spec = analogRead(0);
     data[i] = spec / 4 - 128;
     im[i] = 0;
   };
   fix_fft(data, im, 7, 0);
   display.clearDisplay();
   for (byte i = 0; i < 64; i++) {
     int level = sqrt(data[i] * data[i] + im[i] * im[i]);;
     if (level >= param2) {
       display.fillRect(i * 2, 63 - (level + i * (param1 - 1) / 8), 2, (level + i * (param1 - 1) / 8), WHITE); // i * (param1 - 1) / 8 is high freq amp
     }
   }

   //display
   if (hide == 1) {
     display.drawLine((param - 1) * 42, 8,   (param - 1) * 42 + 36, 8, WHITE);
     display.setTextColor(WHITE);
     if (param_select == 1) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(0, 0);
     display.print("Mode:");
     display.setCursor(30, 0);
     display.print(mode);

     display.setTextColor(WHITE);
     if (param_select == 2) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(42, 0);
     display.print("High:");
     display.setCursor(72, 0);
     display.print(param1);

     display.setTextColor(WHITE);
     if (param_select == 3) {
       display.setTextColor(BLACK, WHITE);
     }
     display.setCursor(84, 0);
     display.print("Filt:");
     display.setCursor(114, 0);
     display.print(param2);
   }
 }

 display.display();
};
​

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