見出し画像

2400円で作る高品質DSPマルチエフェクター-モジュラーシンセ自作

Spin semiconductor FV-1デジタルリバーブICとArduino nanoを使って、モジュラーシンセサイザー のDSPマルチエフェクターを自作したので、その備忘録。

紹介動画を公開しているので、以下のリンクも参照。https://youtu.be/X7DACIx0x40

画像1

背景

自作モジュラーシンセの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など

画像2

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種類

画像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, 010230150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
 POT1 = map(POT1, 010230150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
 POT2 = map(POT2, 010230150);//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ページですでに紹介されているので詳細は割愛する。非常にシンプルな回路だ。

画像4

エフェクトデータをArduinoを用いてEEPROMに書き込むプログラムは、FV-1のフォーラムに記述があるので参照されたし。"MyEEPROMBurner.ino"というファイルを使用させてもらった。感謝だ。

ハードウェア

画像5

部品点数が多くなってしまったが、それぞれの回路ブロックはシンプル。

電源回路
FV-1はデジタルICなので電流を多く消費する。また、ユーロラックの12Vから3.3Vに大幅な降圧をする必要がある。
通常ならばスイッチング電源を使いたいが、ノイズが懸念されるのでLDOを使用した。エネルギーの大部分が熱に変換されるので、LDOは大型のパッケージを使う必要がある。電力計算をした結果、ヒートシンクは不要と判断したが、気になる人はヒートシンクを付けたほうがいいかもしれない。

画像6

電源ソースは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から選択できるようにした。

画像7

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

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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst 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 charconst name25[] PROGMEM = {
 str25[0], str25[1], str25[2], str25[3], str25[4], str25[5], str25[6], str25[7],
};

//rotary encoder setting
Encoder myEnc(24);
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, 07);

 //PWM value calc
 POT0 = ( analogRead(0) + analogRead(3) );
 POT1 = ( analogRead(1) + analogRead(6) );
 POT2 = ( analogRead(2) + analogRead(7) );
 POT0 = map(POT0, 010230150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
 POT1 = map(POT1, 010230150);//150 : reduce voltage 5V to 3V (fv-1 voltage rate)
 POT2 = map(POT2, 010230150);//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(00);//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(016);//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(1200);
 display.print(i);

 display.setCursor(034);//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(044);//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(054);//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(7437, POT0 / 352, WHITE);
 display.fillRoundRect(7447, POT1 / 352, WHITE);
 display.fillRoundRect(7457, POT2 / 352, WHITE);

 display.display();
}

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