800円で作るArduinoOLEDオシロスコープ&スペクトルアナライザー-モジュラーシンセ自作
ArduinoとSSD1306OLEDを用いて、モジュラーシンセサイザー のオシロスコープ&スペクトルアナライザーを自作したので、その備忘録。
背景
自作モジュラーシンセの25作品目。
最近、ビデオシンセサイザーが種類を増やしてきている。ブラウン管に投影するものや、モジュール自体がディスプレイを持つものもある。
音楽が聴くものから見るもの・体験するものに変化している現在、視覚的に音楽を楽しむ流れが起こるのは、必然ともいえる。
今回、音やシーケンスを視覚で楽しむために、そして理解するためのモジュールを作成しようと思った。
目的は「見て楽しむこと」「理解すること」であり、「測定すること」ではない。正確な周波数、正確な電圧を測定することは目的ではない。そのため、部品は表示を大胆に減らしている。
まずはオシロスコープとスペクトルアナライザーを実装したが、将来的にはVUメーターやビデオシンセサイザーに機能拡張をする予定だ。
制作物のスペック
ユーロラック規格 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端子は短絡している。単なるマルチプルだ。
表示モード
現時点で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のリズムに合わせてスペクトルが揺れるのを楽しめれば良い。
製作費
総額800円
---------------------------------
Arduino nano 互換品200円
OLED(SSD1306) 180円
op-amp 30円
パネル 100円
ロータリーエンコーダモジュール 100円etc
OLEDはSPI通信のものを使う。I2Cと比較して、通信速度が速いからだ。
ロータリーエンコーダモジュールは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極のローパスフィルタが有効になる。
スペクトルアナライザでは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();
};
この記事が気に入ったらサポートをしてみませんか?