900円で作るMIDI to CVモジュール-モジュラーシンセ自作
Arduinoプログラミングに挑戦しつつ、モジュラーシンセサイザー MIDI to CVモジュールを自作したので、その備忘録。
背景
コードの書けないシステムエンジニア脱却のために始めたプログラミングの11作品目。YAMAHAのPCM音源ICを使ったスタンドアローンシンセの制作を計画しており、それに向けてMIDIを理解する必要があった。
手始めに、MIDI to CVモジュールを作成し、MIDIの理解を深める。
世の中には既に、MIDI2CVの様に素晴らしいMIDI to CVモジュールがオープンソースで公開されている。
しかし、コードをコピーするだけではMIDIの理解が進まないので、車輪の再発明をする必要があった。
機能面はMIDI2CVに大きくインスパイアされつつも、コーディングは可能な限り自力で考えて作成した。
制作物のスペック
ユーロラック規格 3U 6HPサイズ
電源:39mA ( at 5V ) / 37mA ( at12V )
5V単電源で動作可能。または12V単電源で動作可能。
CV:MIDIの音階を1V/octで出力。Rangeは0 - 5V。
GATE:key onされている間だけ5Vを出力する。
MOD:モジュレーションホイールを出力。0 - 5V。
CLK:MIDI Clockを出力。ツマミで分周を24,12,6,3から選択可能。
GATEとCLKはLEDが連動して光る。
各出力には過電圧、負電圧、過電流の保護回路がある。
入力はCH1のMIDI信号だけ受けつけるが、ソースコードを編集すればCHを簡単に変更可能。
制作費
総額900円弱。
Arduino互換ボード:220円
12bit 2ch DAC:250円
パネル:150円
フォトカプラ:20円
DACとフォトカプラは秋月電子通称から入手。
それ以外の電子部品はAliexpressやJLCPCBから入手した。
プログラム
ArduinoのMIDIライブラリを使用。
複数の鍵盤の入力があった際のNOTE ON/OFFの処理に工夫を凝らしたが、他はいたってシンプル。
CVの出力はテーブルで値を持たせている。過去に作ったクオンタイザーのプログラムを流用している。
注意点としては、RX端子を使用するためシリアル通信が喧嘩をすること。
PCからArduinoにプログラムを書き込む際は、回路上のRXの接続を切ってやる必要がある。
また、PCとのシリアル通信もできないので、Arduino IDEのシリアルモニタも使用不可能である。
(もしかしたら、回避方法があるかもしれないが)
ハードウェア
MIDI入力周りは規格化されているので、世に出回っている回路をそのままコピーした。
フォトカプラは何を使っても問題ないと思う。一般的な20円くらいのフォトカプラでOK。
DACからの出力抵抗は200Ωに設定した。これはDACの電流定格を守るためだ。
理想をいうと、出力抵抗は0オームがベターだ。分圧によりピッチが若干ずれてしまうから。
世に出回っているモジュールには、出力抵抗が無いものが多い。受け側のインピーダンスがあるから悪い使い方をしなければ故障することはないだろうけど、変な接続をすると故障してしまう。
オープンソースとして公開する以上は、他の人のモジュールを壊すわけにはいけないので、保護部品を設けるようにしている。
EURORACKは規格化されているとはいえ、出力インピーダンスや入力インピーダンスが統一されていない。GATEなら出力が1kohm、入力が100kohmあたりが多いが、CVはまちまちだ。
ソースコード
粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
#include <MIDI.h>
#include <SPI.h>//DAC通信用
MIDI_CREATE_DEFAULT_INSTANCE(); //MIDIライブラリを有効
const int LDAC = 9;//SPI trans setting
int note_no = 0;//noteNo=21(A0)~60(A5) total 61,マイナスの値を取るのでint
int bend_range = 0;
int bend_msb = 0;
int bend_lsb = 0;
long after_bend_pitch = 0;
byte note_on_count = 0;//複数のノートがONかつ、いずれかのノートがOFFしたときに、最後のノートONが消えないようにする。
unsigned long trigTimer = 0;//for gate ratch
byte clock_count = 0;
byte clock_max = 24;//clock_max change by knob setting
byte clock_on_time = 0;
int clock_rate = 0;//knob CVin
// V/OCT LSB for DAC
const long cv[61] = {
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
};
void setup() {
pinMode(LDAC, OUTPUT) ;//DAC trans
pinMode(SS, OUTPUT) ;//DAC trans
pinMode(4, OUTPUT) ;//CLK_OUT
pinMode(5, OUTPUT) ;//GATE_OUT
MIDI.begin(1); // MIDI CH1をlisten
SPI.begin();
SPI.setBitOrder(MSBFIRST) ; // bit order
SPI.setClockDivider(SPI_CLOCK_DIV4) ;// クロック(CLK)をシステムクロックの1/4で使用(16MHz/4)
SPI.setDataMode(SPI_MODE0) ; // クロック極性0(LOW) クロック位相0
delay(50);
}
void loop() {
//-----------------------------clock_rate setting----------------------------
clock_rate = analogRead(1);//read knob voltage
if (clock_rate < 256) {
clock_max = 24;//slow
}
else if (clock_rate < 512 && clock_rate >= 256) {
clock_max = 12;
}
else if (clock_rate < 768 && clock_rate >= 512) {
clock_max = 6;
}
else if (clock_rate >= 768) {
clock_max = 3;//fast
}
//-----------------------------gate ratch----------------------------
if (note_on_count != 0) {
if ((millis() - trigTimer <= 20) && (millis() - trigTimer > 10)) {
digitalWrite(5, LOW);
}
if ((trigTimer > 0) && (millis() - trigTimer > 20)) {
digitalWrite(5, HIGH);
}
}
//-----------------------------midi operation----------------------------
if (MIDI.read()) { // チャンネル1に信号が入ってきたら
MIDI.setInputChannel(1);
switch (MIDI.getType()) {
case midi::NoteOn://NoteOnしたら
note_on_count ++;
trigTimer = millis();
note_no = MIDI.getData1() - 21 ;//note number
if (note_no < 0) {
note_no = 0;
}
else if (note_no >= 61) {
note_no = 60;
}
digitalWrite(5, HIGH); //GateをHIGH
OUT_CV(cv[note_no]);//V/OCT LSB for DACを参照
break;
case midi::NoteOff://NoteOffしたら
note_on_count --;
if (note_on_count == 0) {
digitalWrite(5, LOW); //GateをLOW
}
break;
case midi::ControlChange:
OUT_MOD( MIDI.getData2() << 5); //0-4095
break;
case midi::Clock:
clock_count ++;
if (clock_count >= clock_max) {
clock_count = 0;
}
if (clock_count == 1) {
digitalWrite(4, HIGH);
}
else if (clock_count != 1) {
digitalWrite(4, LOW);
}
break;
case midi::Stop:
clock_count = 0;
digitalWrite(5, LOW); //GateをLOW
break;
case midi::PitchBend:
bend_lsb = MIDI.getData1();//LSB
bend_msb = MIDI.getData2();//MSB
bend_range = bend_msb; //0 to 127
if (bend_range > 64) {
after_bend_pitch = cv[note_no] + cv[note_no] * (bend_range - 64) * 4 / 10000;
OUT_CV(after_bend_pitch);
}
else if (bend_range < 64) {
after_bend_pitch = cv[note_no] - cv[note_no] * (64 - bend_range) * 4 / 10000;
OUT_CV(after_bend_pitch);
}
break;
}
}
}
//DAC_CV output
void OUT_CV(int cv) {
digitalWrite(LDAC, HIGH) ;
digitalWrite(SS, LOW) ;
SPI.transfer((cv >> 8) | 0x30) ; // H0x30=OUTA/1x
SPI.transfer(cv & 0xff) ;
digitalWrite(SS, HIGH) ;
digitalWrite(LDAC, LOW) ;
}
//DAC_MOD output
void OUT_MOD(int mod) {
digitalWrite(LDAC, HIGH) ;
digitalWrite(SS, LOW) ;
SPI.transfer((mod >> 8) | 0xB0) ; // H0xB0=OUTB/1x
SPI.transfer(mod & 0xff) ;
digitalWrite(SS, HIGH) ;
digitalWrite(LDAC, LOW) ;
}