2400円で作る高品質DSPマルチエフェクター-モジュラーシンセ自作
Spin semiconductor FV-1デジタルリバーブICとArduino nanoを使って、モジュラーシンセサイザー のDSPマルチエフェクターを自作したので、その備忘録。
紹介動画を公開しているので、以下のリンクも参照。https://youtu.be/X7DACIx0x40
背景
自作モジュラーシンセの32作品目。
モジュラーシンセサイザーには、多くののDSPマルチエフェクターモジュールがあるが、その多くはSpin semiconductor FV-1というICを使用している。
私が調べる限りでは、下に記すモジュールが使用しているようだ。
Erica Synths Black Hole DSP, Dual FX Dual Effect, pico DSP
Happy Nerding FX AID
tiptop ZVERB, ECHOZ, Z5000など
Spin FV-1は日本で1200円で購入できる。比較的高額だが、少ない周辺部品を加えるだけで市販価格30000円相当のエフェクターを簡単に作ることができる。また、エフェクトは外部ROMに記録することでエフェクトもカスタマイズ可能だ。
テクノやアンビエントには、テーブエコーやシマーリバーブといったエフェクトが使われるので、それを手に入れるべくDIYの企画をした。
コンセプトは「汎用性を持たせること」
1.USB給電、9V電池、12Vで動作し、負電位は使わない。
2.音声入出力は、ラインレベルから、モジュラーシンセまで調整可能。
3.eurorackケースに搭載でき、デスクトップでも使用可能なこと。
制作物のスペック
ユーロラック規格 3U 12HPサイズ
電源:110mA ( at 5V or 9~12V)
Arduino nanoのUSBポートを使った5V単電源で動作可能。
又は、9V~12Vの電源で動作可能。
CV IN: 0V~+5V
Audio IN: -5V~+5Vの入力を想定。半固定抵抗でゲイン調整可。
エフェクト種類:23種類(プリセット7種類+任意16種類)
可変パラメータ:1エフェクトあたり3種類
マルチエフェクト
Spin FV-1はプリセット7種類+外付けEEPROM 8種類のエフェクトを選択可能だ。また、EEPROMの数を増やせば、使用できるエフェクト種類も増える。
今回、2個のEEPROMを使用することで、7+8+8=23種類のエフェクトを選択可能にした。
プリセットエフェクトは変更不可能だが、EEPROMのエフェクトは任意の物を記録できる。エフェクトはgithubで公開されており、気に入ったものをEEPROMに書き込み使用することができる。FV-1コミュニティに感謝だ。
製作費
総額約2400円
---------------------------------
Spin FV-1モジュール 1400円
Arduino nano 200円
OLED(SSD1306) 180円
フロントパネル 200円
EEPROM 50円*2pcs
オペアンプ TL072 30円
LDO 60円
ロータリーエンコーダモジュール100円
他
FV-1はIC単体で購入することもできるが、いくつかのFV-1モジュールが既に存在している。それを使えば周辺回路の部品点数を減らすことができて便利だ。
今までのDIYモジュールは6HPのフロントパネルを使用していたが、今回は12HPのフロントパネルをPCBWAYに発注した。
注文から到着まで5日間と短納期なのがありがたい。スポンサードもいただいている、ホント感謝。
プログラミング
FV-1はスタンドアロンのICなので外部のマイコンは必要しないが、今回は制御にArduinoを使用している。FV-1とArduinoの間で複雑な通信はしていない。各PINのhigh-low出力と、PWMによるアナログ出力をしているだけだ。
PWM出力
ArduinoのPWMは5V出力だが、FV-1の絶対電圧定格は3Vである。なので、Arduinoの出力をFV-1にそのまま入れると、破損の危険がある。
よってPWMのDUTY比率を3/5以下に制限することで、3Vの定格を超えないようにしている。
//PWM value calc
POT0 = ( analogRead(0) + analogRead(3) );
POT1 = ( analogRead(1) + analogRead(6) );
POT2 = ( analogRead(2) + analogRead(7) );
POT0 = map(POT0, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
POT1 = map(POT1, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
POT2 = map(POT2, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
//select fv1 effect number
digitalWrite(7, bitRead(i, 0)); //program LSB
digitalWrite(6, bitRead(i, 1)); //program
digitalWrite(5, bitRead(i, 2)); //program MSB
文字列のPROGMEM格納
PROGMEM領域に格納した数字は簡単に読み出しできるが、文字列の場合はバッファ処理が必要。 "strcpy_P"という見慣れない関数を使う必要がある。
公式のReferenceに記述があるので参照されたし。
EEPROMの書き込み
エフェクトデータのEEPROM書き込みは事前に行う必要がある。
EEPROM書き込みはArduinoを使用する。多くのwebページですでに紹介されているので詳細は割愛する。非常にシンプルな回路だ。
エフェクトデータをArduinoを用いてEEPROMに書き込むプログラムは、FV-1のフォーラムに記述があるので参照されたし。"MyEEPROMBurner.ino"というファイルを使用させてもらった。感謝だ。
ハードウェア
部品点数が多くなってしまったが、それぞれの回路ブロックはシンプル。
電源回路
FV-1はデジタルICなので電流を多く消費する。また、ユーロラックの12Vから3.3Vに大幅な降圧をする必要がある。
通常ならばスイッチング電源を使いたいが、ノイズが懸念されるのでLDOを使用した。エネルギーの大部分が熱に変換されるので、LDOは大型のパッケージを使う必要がある。電力計算をした結果、ヒートシンクは不要と判断したが、気になる人はヒートシンクを付けたほうがいいかもしれない。
電源ソースはUSB 5V給電か、9~12Vの給電で回路を分けている。9V~12Vの電源が5Vに流れ込むと5V電源(PCやモバイルバッテリー)を故障させる危険があるからだ。
ArduinoのUSB5V給電を使用する場合、9~12Vから給電されないよう絶縁する必要がある。回路図ではスイッチで示してあるが、ジャンパーピンを用いてもよい。
EEPROM
FV-1 はA0,01,02の全てがGNDに設定されたEEPROMのデータを読む設定になっている。Arduinoを用いてA0 pinのhigh-lowを制御することで、FV-1が読みに行くEEPROMを、ROM1又はROM2から選択できるようにした。
宣伝:オープンソースプロジェクトの支援をお願いします
DIYモジュラーシンセのオープンソースプロジェクトを継続するために、patreonというサービスでパトロンを募集しています。
コーヒー一杯の支援をいただけると嬉しいです。
また、パトロン限定のコンテンツも配信しています。
ソースコード
粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
強制ではないが、このソースコードをベースに新しいプロダクトを作る場合、このブログ(又はYouTube)のリンクを記載してもらえると嬉しい。
なお、EEPROMに書き込むエフェクトデータは私が作成したものでないので、ここではソースコードは公開していない。上述のFV-1 programsのgithubで公開されているので、そこからDLすべし。
#include <Encoder.h>
#include <avr/io.h>//for fast PWM
//encoder library setting
#define ENCODER_OPTIMIZE_INTERRUPTS //contermeasure of encoder noise
#include <Encoder.h>
//OLED display 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);
//OLED display data table
//PRESET 1st line
const char str01[8][12] PROGMEM = {//str1 is effect name of 1st line
"Chorus", "Flange", "Tremolo", "Pitch", "Pitch", "No effect", "Reverb", "Reverb"
};
const char* const name01[] PROGMEM = {
str01[0], str01[1], str01[2], str01[3], str01[4], str01[5], str01[6], str01[7],
};
//PRESET 2nd line
const char str02[8][12] PROGMEM = {//str1 is effect name of 1st line
"reverb", "reverb", "reverb", "shift", "echo", " ", "1", "2"
};
const char* const name02[] PROGMEM = {
str02[0], str02[1], str02[2], str02[3], str02[4], str02[5], str02[6], str02[7],
};
//PRESET param1
const char str03[8][12] PROGMEM = {//str1 is effect name of 1st line
"rev mix", "rev mix", "rev mix", "pitch", "pitch", "-", "rev time", "rev time"
};
const char* const name03[] PROGMEM = {
str03[0], str03[1], str03[2], str03[3], str03[4], str03[5], str03[6], str03[7],
};
//PRESET param2
const char str04[8][12] PROGMEM = {//str1 is effect name of 1st line
"cho rate", "flng rate", "trem rate", "-", "echo delay", "-", "HF filter", "HF filter"
};
const char* const name04[] PROGMEM = {
str04[0], str04[1], str04[2], str04[3], str04[4], str04[5], str04[6], str04[7],
};
//PRESET param3
const char str05[8][12] PROGMEM = {//str1 is effect name of 1st line
"cho mix", "flng mix", "trem mix", "-", "echo mix", "-", "LF filter", "LF filter"
};
const char* const name05[] PROGMEM = {
str05[0], str05[1], str05[2], str05[3], str05[4], str05[5], str05[6], str05[7],
};
//ROM1 1st line
const char str11[8][12] PROGMEM = {//str1 is effect name of 1st line
"Hall","echo","shimmer","Dual tape","Shingle","Echo","Star","Triple"
};
const char* const name11[] PROGMEM = {
str11[0], str11[1], str11[2], str11[3], str11[4], str11[5], str11[6], str11[7],
};
//ROM1 2nd line
const char str12[8][12] PROGMEM = {//str1 is effect name of 1st line
"reverb","reverb","reverb","reverb","tape echo"," ","field","echo"
};
const char* const name12[] PROGMEM = {
str12[0], str12[1], str12[2], str12[3], str12[4], str12[5], str12[6], str12[7],
};
//ROM1 param1
const char str13[8][12] PROGMEM = {//str1 is effect name of 1st line
"pre-delay","delay","shimmer","delay","time","rev level","delay","time1"
};
const char* const name13[] PROGMEM = {
str13[0], str13[1], str13[2], str13[3], str13[4], str13[5], str13[6], str13[7],
};
//ROM1 param2
const char str14[8][12] PROGMEM = {//str1 is effect name of 1st line
"rev time","repeat","rev level","feed back","feed back","delay","tremolo","time2"
};
const char* const name14[] PROGMEM = {
str14[0], str14[1], str14[2], str14[3], str14[4], str14[5], str14[6], str14[7],
};
//ROM1 param3
const char str15[8][12] PROGMEM = {//str1 is effect name of 1st line
"damping","reverb","rev time","damping","damping","echo level","mix","time3"
};
const char* const name15[] PROGMEM = {
str15[0], str15[1], str15[2], str15[3], str15[4], str15[5], str15[6], str15[7],
};
//ROM2 1st line
const char str21[8][12] PROGMEM = {//str1 is effect name of 1st line
"Dual","Chorus","Flanger","Wah","Distortion","octave","Digital","Sine"
};
const char* const name21[] PROGMEM = {
str21[0], str21[1], str21[2], str21[3], str21[4], str21[5], str21[6], str21[7],
};
//ROM2 2nd line
const char str22[8][12] PROGMEM = {//str1 is effect name of 1st line
"chorus","ring mod","","","","","fuzz","osc"
};
const char* const name22[] PROGMEM = {
str22[0], str22[1], str22[2], str22[3], str22[4], str22[5], str22[6], str22[7],
};
//ROM2 param1
const char str23[8][12] PROGMEM = {//str1 is effect name of 1st line
"cho level","blend","delay","reverb","gain","mix","rate","freq"
};
const char* const name23[] PROGMEM = {
str23[0], str23[1], str23[2], str23[3], str23[4], str23[5], str23[6], str23[7],
};
//ROM2 param2
const char str24[8][12] PROGMEM = {//str1 is effect name of 1st line
"rate1","offset","rate","sensitivity","tone","oct up","distortion","fine"
};
const char* const name24[] PROGMEM = {
str24[0], str24[1], str24[2], str24[3], str24[4], str24[5], str24[6], str24[7],
};
//ROM2 param3
const char str25[8][12] PROGMEM = {//str1 is effect name of 1st line
"rate2","chorus","width","Q/level","mix","oct down","volume","amp"
};
const char* const name25[] PROGMEM = {
str25[0], str25[1], str25[2], str25[3], str25[4], str25[5], str25[6], str25[7],
};
//rotary encoder setting
Encoder myEnc(2, 4);
int oldPosition = -999;
int newPosition = -999;
int i = 1;
int POT0 = 150;
int POT1 = 150;
int POT2 = 150;
bool old_sw = 0;
bool sw = 0;
byte bank = 0; //0=fv1 rom FX , 1 = EEPROM1 , 2 = EEPROM2
bool disp_reflesh = 0;
long disp_ref_count = 0;
void setup() {
//OLED library setting
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
//pin mode setting
pinMode(3, OUTPUT) ;//POT2 PWM
pinMode(5, OUTPUT) ;//S2
pinMode(6, OUTPUT) ;//S1
pinMode(7, OUTPUT) ;//S0
pinMode(8, OUTPUT) ;//EEPROM SW1
pinMode(9, OUTPUT) ;//EEPROM SW2
pinMode(10, OUTPUT);//POT2 PWM
pinMode(11, OUTPUT) ;//POT0 PWM
pinMode(12, INPUT_PULLUP); //external triger detect
pinMode(13, OUTPUT) ;//T0
//fast pwm setting
TCCR2B &= B11111000;
TCCR2B |= B00000001;
//fast pwm setting
TCCR1B &= B11111000;
TCCR1B |= B00000001;
disp_ref_count = millis();
}
void loop() {
//change bank(ROM) by push button
old_sw = sw;
sw = digitalRead(12);
if (sw == 1 && old_sw == 0) { //bank change
bank ++;
disp_reflesh = 1;
if (bank >= 3) {
bank = 0;
}
if (bank == 2) {
digitalWrite(9, LOW);
delay(5);
digitalWrite(8, HIGH);
delay(5);
digitalWrite(13, HIGH);
delay(5);
digitalWrite(13, LOW);
delay(5);
digitalWrite(13, HIGH);
}
else if (bank == 1) {
digitalWrite(8, LOW);
delay(5);
digitalWrite(9, HIGH);
delay(5);
digitalWrite(13, HIGH);
delay(5);
digitalWrite(13, LOW);
delay(5);
digitalWrite(13, HIGH);
}
else if (bank == 0) {
digitalWrite(13, LOW);
digitalWrite(8, LOW);
digitalWrite(9, LOW);
}
}
//rotary encoder
newPosition = myEnc.read();
if ( (newPosition - 3) / 4 > oldPosition / 4) {
oldPosition = newPosition;
i = i - 1;
disp_reflesh = 1;
if ( i <= -1) {
i = 7;
}
}
else if ( (newPosition + 3) / 4 < oldPosition / 4 ) {
oldPosition = newPosition;
i = i + 1;
disp_reflesh = 1;
if ( i >= 8) {
i = 0;
}
}
i = constrain(i, 0, 7);
//PWM value calc
POT0 = ( analogRead(0) + analogRead(3) );
POT1 = ( analogRead(1) + analogRead(6) );
POT2 = ( analogRead(2) + analogRead(7) );
POT0 = map(POT0, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
POT1 = map(POT1, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
POT2 = map(POT2, 0, 1023, 0, 150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
//select fv1 effect number
digitalWrite(7, bitRead(i, 0)); //program LSB
digitalWrite(6, bitRead(i, 1)); //program
digitalWrite(5, bitRead(i, 2)); //program MSB
//PWM output
analogWrite(3, POT2);
analogWrite(10, POT1);
analogWrite(11, POT0);
//dispray reflesh frequency
if ((disp_reflesh == 1) || (millis() >= disp_ref_count + 100)) { //reflesh rate is 100ms/once
disp_reflesh = 0;
disp_ref_count = millis();
display_out();
}
}
void display_out() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(0, 0);//effect name , 1st line
char buf1[30];
if (bank == 0) {
strcpy_P(buf1, pgm_read_word(&(name01[i])));
}
else if (bank == 1) {
strcpy_P(buf1, pgm_read_word(&(name11[i])));
}
else if (bank == 2) {
strcpy_P(buf1, pgm_read_word(&(name21[i])));
}
display.print(buf1);
display.setCursor(0, 16);//effect name , 2nd line
char buf2[30];
if (bank == 0) {
strcpy_P(buf2, pgm_read_word(&(name02[i])));
}
else if (bank == 1) {
strcpy_P(buf2, pgm_read_word(&(name12[i])));
}
else if (bank == 2) {
strcpy_P(buf2, pgm_read_word(&(name22[i])));
}
display.print(buf2);
display.setTextSize(0);
display.setCursor(120, 0);
display.print(i);
display.setCursor(0, 34);//effect param1
char buf3[30];
if (bank == 0) {
strcpy_P(buf3, pgm_read_word(&(name03[i])));
}
else if (bank == 1) {
strcpy_P(buf3, pgm_read_word(&(name13[i])));
}
else if (bank == 2) {
strcpy_P(buf3, pgm_read_word(&(name23[i])));
}
display.print(buf3);
display.setCursor(0, 44);//effect param2
char buf4[30];
if (bank == 0) {
strcpy_P(buf4, pgm_read_word(&(name04[i])));
}
else if (bank == 1) {
strcpy_P(buf4, pgm_read_word(&(name14[i])));
}
else if (bank == 2) {
strcpy_P(buf4, pgm_read_word(&(name24[i])));
}
display.print(buf4);
display.setCursor(0, 54);//effect param3
char buf5[30];
if (bank == 0) {
strcpy_P(buf5, pgm_read_word(&(name05[i])));
}
else if (bank == 1) {
strcpy_P(buf5, pgm_read_word(&(name15[i])));
}
else if (bank == 2) {
strcpy_P(buf5, pgm_read_word(&(name25[i])));
}
display.print(buf5);
//POT value round square
display.fillRoundRect(74, 37, POT0 / 3, 5, 2, WHITE);
display.fillRoundRect(74, 47, POT1 / 3, 5, 2, WHITE);
display.fillRoundRect(74, 57, POT2 / 3, 5, 2, WHITE);
display.display();
}
この記事が気に入ったらサポートをしてみませんか?