600円で作るノイズ&ハイハットモジュール-モジュラーシンセ自作
Arduinoプログラミングに挑戦しつつ、モジュラーシンセサイザー デジタルノイズ&ハイハットモジュールを自作したので、その備忘録。
背景
コードの書けないシステムエンジニア脱却のために始めたプログラミングの13作品目。前回、Arduinoのレジスタを直接叩くことで、高い周波数のPWMを作成することができた。それを応用し、音の出るモジュールを作りたかった。
いきなり音程のあるVCOモジュールを作るのも難しいので、まずは制御が簡単なノイズモジュールを作るに至る。
モジュラーシンセは高額だ。特にドラム音源のモジュールは汎用性が低いうえに、2万円以上の出費になる。(たとえそれがAndroidの無料アプリで出せる音だとしても)
かといって、ホワイトノイズからハイハットを作るとなると、VCO、EG、VCF、VCAが必要で、3万円以上の出費になる。
モジュラーシンセをドラムマシンとして使う為に、安いドラム音源モジュールが欲しかった。
制作物のスペック
ユーロラック規格 3U 6HPサイズ
電源:29mA ( at 5V ) / 30mA ( at12V )
5V単電源で動作可能。または12V単電源で動作可能。
入出力
Knob1:Tone、ノイズ音域の広がりを制御する。
Knob2:Tune、ノイズのピッチ(PWM周波数)を制御する。
Knob3:Decay、ディケイの長さを制御する。
MAXにすると、常時出力となる。
TUNE_CVin , DECAY_CVin:それぞれのCV制御
TRIGGER IN:入力があると、DECAYの長さだけノイズを出力する。
OUT:音声出力。
ノイズの生成
ノイズはPWMで生成している。周期とパルス幅をランダムに設定することで、疑似的なホワイトノイズとなる。
パルス幅は完全にランダムだが、周期はToneとTuneでコントロールが可能。
Toneの設定が大きいほど、周期のバラツキが大きくなる。逆に小さいと、狭い周期の範囲でランダムな値をとるため、ピッチ感が生まれる。
Tuneでは、そのピッチ感の高さを制御する。
そのため、ホワイトノイズではあるが、ピッチ感のあるノイズが出せる。
ディケイ出力
Arduinoは全部で3種類のPWMソースを持つ。
うち1つ目はノイズの生成に使い、もう一つをディケイ波形の生成に使った。
ディケイはexpカーブのため、自然な減衰感がある。
減衰波形はPWMのパルス制御で生成後、ローパスフィルターを通してVCA回路を制御する。
ノイズ波形のパルス幅を小さくすることで、VCA回路を使わなくても音量を小さくすることも可能だが、ノイズの音色も変化してしまう。
そのため、ノイズ波形と、ディケイ波形は別々のPWMで生成した。
制作費
600円弱。VCA回路をトランジスタ1個で構成したため、回路は安価。
Arduino nano 互換:220円
パネル:150円
可変抵抗:20円*3個
他
ノイズモジュールなので、AD入力のバイパスコンデンサも一部削除した。
AD入力にノイズが入っても、問題ないからだ。ノイズモジュールだし。
プログラム
特筆すべきはPWM制御だが、前回の記事に書いてあるので割愛。
10pinだけでなく、3pinも高速PWM出力をしている。
3pinの高速PWMについては、10pinと比べて技術資料がすくないため、手探りで作成した。アカデミックなプログラムではない。トライアンドエラーの産物だ。賢い方法ではない。
処理の中で音声を生成しているので、シリアル通信をすると音色が劣化してしまう。Serial.printを実行している間は、まともな音が出ないことに注意。
ハードウェア
VCA回路に工夫を凝らした。
トランジスタのベースにはDECAY波形が入力され、コレクタ側のノイズ波形の音量をコントロールしている。
トランジスタは2SC1815を使用、手元にあったから。NPN型の一般的なトランジスタならば、何を使っても問題ない。
また、OUT端子から過電圧が印加された場合、トランジスタで受けることでArduinoを保護する役目も果たしている。
ノイズのローパスフィルタのカットオフ周波数は約15kHz。
20kHzに設定したところ、不快な高周波ノイズが聞こえたため、一段下げて15kHzとした。
DECAYのローパスフィルタは、PWMのリプルが残っているが、ノイズを増幅するので無視した。多少のリプル(VCA増幅の周期的変化)は、ノイズの音量に影響がなかったからだ。
ソースコード
粗末だが公開する。悪い点があれば教えてもらえると勉強になる。
#include <avr/io.h>//for custom pwm setting
//noise setting
unsigned int frq = 1000; // 周波数
float duty = 0.5;//pin10 for noise
unsigned int knob_tune = 1;//knob AD
unsigned int knob_tone = 1;//knob AD
unsigned int CV_tone = 1;//CV AD
//decay setting
unsigned int knob_decay = 0;//knob AD
unsigned int CV_decay = 0;//CV AD
unsigned int decay_time = 0;
unsigned int d_duty = 100;//pin3 for decay
int i = 100;//decay wavetable reference
bool before_gate = 0;//0=no signal,1=gate in
bool gate = 0;//0=decay out end , 1=during decay out
long trigTimer = 0;//for generate decay time
//decay wavetable
const static word decay_table[100] PROGMEM = {
100, 96, 94, 92, 91, 89, 87, 86, 84, 82, 81, 79, 78, 76, 74, 73, 71, 70, 68, 67, 65, 64, 63, 61, 60, 58, 57, 56, 54, 53, 52, 51, 49, 48, 47, 46, 44, 43, 42, 41, 40, 39, 38, 37, 36, 34, 33, 32, 31, 31, 30, 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 19, 19, 18, 17, 17, 16, 15, 15, 14, 13, 13, 12, 12, 11, 11, 10, 10, 9, 9, 8, 8, 8, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4, 3, 3, 2, 1
};
void setup() {
pinMode(3, OUTPUT);//decay out
pinMode(5, INPUT);//gate in
pinMode(10, OUTPUT);//noise out
//10pin pwm setting
TCCR1A = 0b00100001;
TCCR1B = 0b00010001;//分周比1
//3pin pwm setting
TCCR2A = 0b00100001;
TCCR2B = 0b00001001;//分周比1
//for development
// Serial.begin(9600);
}
void loop() {
before_gate = gate;//for gate signal detect
//------------------decay time setting---------------
knob_decay = analogRead(3);
CV_decay = analogRead(5);
decay_time = (knob_decay + CV_decay ) * 5;
//------------------noise setting-----------
knob_tune = analogRead(0);
knob_tone = analogRead(1);
CV_tone = analogRead(6);
//------------------detect gate signal--------------
gate = digitalRead(5);
if (before_gate == 0 && gate == 1) {
i = 0;
}
//--------------10pin pwm setting-----------------
//MAKE SOME NOISE!!
frq = random(1 + knob_tone * 2 + CV_tone * 2, 2 + knob_tone * 2 + CV_tone * 2 + knob_tune * 2);
duty = (float)random(1, 999) / 1000;
// TOP値指定
OCR1A = (unsigned int)(8000000 / frq );
// Duty比指定
OCR1B = (unsigned int)(8000000 / frq * duty);
//--------------3pin pwm setting-----------------
// TOP値指定
OCR2A = (unsigned int)(8000000 / 20000 );
if (knob_decay <= 970) {//make decay wave
if (i < 99 && (micros() - trigTimer >= decay_time)) {
i++;
d_duty = (pgm_read_word(&(decay_table[i]))) ;
trigTimer = micros();
}
}
else {//decay knob max = constant output ( no decay )
d_duty = 100;
}
// Duty比指定
OCR2B = (unsigned int)(8000000 / 20000 * d_duty / 300);//300 is magic number
// for development(Serial communication affects noise generation!)
// Serial.print(d_duty);
// Serial.print(",");
// Serial.print(gate);
// Serial.println("");
}
この記事が気に入ったらサポートをしてみませんか?