デジアナな Envelope Generator を作る
概要
素人ゆえに、オーソドックスなADSR特性のエンベロープジェネレターを作りたいと思った。 HAGIWO作品 を揃えていこうと始めたモジュラーシンセ作りだが、氏の作品にはオーソドックスなADSR特性のエンベロープ ジェネレータがない。そこで、オーソドックスなADSR特性の製作事例を探してみると、発振回路とCRで作るアナログなエンベロープジェネレターが一般的だ。
エンベロープのADSR特性をソフト的に作ってみた がAttackからDecayへの遷移が安定しなかったり、特性を調整しずらい問題などがあり、満足のいく結果にはならなかった。

ソフトで作るADSR特性 - 私の図書館
概要 エンベロープジェネレータのADSR特性をソフトウエアーで作る。 方法は、 Arduinoマイコンの高速PWM(50kHzのPWM周波数にするため、タイマーレジストリを書き換え)を使う。 出力にLPFを設けて、PWMのデューティ比で波形をつくる。(ADCの役割)...
Googleでアナログとデジタルを組み合わせてオーソドックスなADSR特性を得ている事例を見つけた。Githubに回路図やArduinoスケッチも上がっているので、これをまねて作ることにした。

Arduino EGの構想
Arduinoとアナログ処理部の組み合わせでエンベロープ・ジェネレーターを製作することにしました。 シミュレーション回路図 前回のシミュレーション では「InvAttack」のスイッチング動作をAVRの3-State Bufferで行いましたが、一応MOS-FETでス...
GitHub - ryood/Arduino_EG
Contribute to ryood/Arduino_EG development by creating an account on GitHub.
本文
ARDUINO EG を解析
このエンベロープジェネレータの特徴は、次のようになる。
- 抵抗とコンデンサのLPFで、アナログなエンベロープ特性を作る。
- 外部からのGate信号をきっかけとして、Attack, Decay, Sustain, Release 4フェーズでArduinoが5VをON/OFFし、その電圧でコンデンサの充放電をで行う。
動作をもう少し具体的に示すと、
- Attack段階: Gate信号がONになると、エンベロープはAttack段階に移行し、ArduinoのAttackPinをHIGHにする。
- Decay段階: Attack段階のしきい値に達すると、エンベロープはDecay段階に移行し、ArduinoのAttackPinをLOWにして、InvAttackPinをHIGHにする。
- release段階: Gate信号がOFFになると、エンベロープはrelease段階に移行し、ArduinoのGateOutPinをLOWにする。
LTspiceでシミュレーションすると、下図のようにきれいなADSR特性が得られることが確認できた。
回路図
動作解析
ChatGPTで回路図の解析をしてもらうと、次のようになる。(すばらしい、感心する。)説明がちょっとくどいが、そのまま載せる。
調整用ポテンショメータ
ChatGPT調整用ポテンショメータ(RV2、RV3、RV5、RV6)はエンベロープジェネレータの特性に次のように影響します。
RV2 (Decay Time) :
- 役割 : Decay段階の時間を調整します。
- 影響 : ポテンショメータを調整することで、Attack段階の後に信号がピークから減衰する速度を変更できます。RV2の抵抗値が高いほど、減衰時間が長くなります。
RV3 (Sustain Level) :
- 役割 : サステインレベルを調整します。
- 影響 : Decay段階の後に信号が保持されるレベルを設定します。RV3の抵抗値が高いほど、サステインレベルが低くなり、逆に低いほどサステインレベルが高くなります。
RV5 (Attack Time) :
- 役割 : Attack段階の時間を調整します。
- 影響 : Gate信号がONになったときに信号が0からピークまで上昇する速度を変更します。RV5の抵抗値が高いほど、Attack時間が長くなります。
RV6 (Release Time) :
- 役割 : release段階の時間を調整します。
- 影響 : Gate信号がOFFになったときに信号がピークから0に減衰する速度を変更します。RV6の抵抗値が高いほど、release時間が長くなります。
2つのトランジスタ(Q1とQ2)の機能と動作
ChatGPTQ1 (2N7000)
- Q1はNチャネルMOSFETであり、6_Inv_Attackピンの信号に応じて動作します。
- Decayおよびrelease段階で、6_Inv_Attack信号がHIGHになると、Q1はON状態になります。これにより、信号が減衰し始めます。
Q2 (2SA1015)
- Q2はPNPトランジスタであり、Attackピンの信号に応じて動作します。
- Attack段階で、Attack信号がHIGHになると、Q2はON状態になります。これにより、信号がピークまで上昇します。
トランジスタの動作
- Q1とQ2の比較 :
- Q1 (2N7000) : NチャネルMOSFETであり、 負の信号をスイッチング するために使用されます。 Decayおよびrelease段階での信号の減衰を制御します。
- Q2 (2SA1015) : PNPトランジスタであり、 正の信号をスイッチングする ために使用されます。 Attack段階での信号の上昇を制御します。
時間軸での特性線図
ChatGPT
ゼロドリフトとインバース特性
お手本にしたArduino_EGは出力波形がGNDまで下がりきらないという問題がある模様。
音源とオーディオの電子工作(予定)
ソフト屋がハードにはまっていく軌跡
この対策にオペアンプを使うことにした。元々、電源は±12Vを考えているのでオペアンプにバイアスをかけてゼロ点調整ができる。
ついでに、
DOEPFERのA-140-1 ADSR ENVELOPE GEN.
に備わっているinverse Output(反転出力)ポートを真似てみる。
この回路として、下記の2ケースを考えた。ゼロ点調整に反転増幅回路を使う案と非反転増幅回路を使う案が考えられる。いづれでも同じようにゼロ点調整ができる。入力をほんのちょっとだけ増幅して、5V程度の出力にしたかったので、非反転増幅回路でゼロ点調整をすることにしたのが冒頭の回路図だ。
DOEPFERのA-140-1 ADSR ENVELOPE GEN. には、inverse Output以外に正Outputが2ポートあるので、それも真似てみた。4回路のオペアンプでこれらを構成する。
製作
12HP x 3U サイズにPOTとジャックを並べるために、下図形状のPOTを基板にハンダ固定する方法をとった。
例によってKicadと連携して搭載設計をFusion360で行う。
Fusion360で丁寧にモデルづくりを行う。出来上がりが楽しみだ。
基板の2層化に初めて挑戦。固定ネジの位置決めのためFusion360の3Dモデルと、各層基板のスケッチが役立った。
配線は結構手間で、4,5時間かかったと思う。
スケッチ
/*
* Arduino EG - Envelope Generator with LED Indicators
*
* Updated on 2024.05.31
*/
#define UART_TRACE (0)
#define PIN_CHECK (0)
#define TITLE_STR1 ("Arduino EG - Envelope Generator")
#define TITLE_STR2 ("20240531")
// Pin definitions
const int ThresholdPin = A0;
const int AttackLevelPin = A1;
const int GateInPin = 3;
const int AttackPin = 5;
const int InvAttackPin = 6;
const int GateOutPin = 7;
const int A_LedPin = 8;
const int D_LedPin = 9;
// Envelope Generator states
enum EG_STATE {
ST_ATTACK,
ST_DECAY,
ST_RELEASE
};
volatile EG_STATE state = ST_RELEASE;
volatile bool isStateChanged = true;
volatile EG_STATEprevState = ST_RELEASE;
// Attack time threshold: (2/3) * 1024
int attackThreshold = 683;
// Variables to store previous pin states
int prevGateInState = LOW;
int prevAttackState = LOW;
int prevInvAttackState = LOW;
int prevGateOutState = LOW;
int prevALedState = LOW;
int prevDLedState = LOW;
void gateIn()
{
bool isGateOn = digitalRead(GateInPin);
#if (UART_TRACE)
Serial.println(isGateOn);
#endif
state = isGateOn ? ST_ATTACK : ST_RELEASE;
isStateChanged = true;
}
void setup()
{
pinMode(AttackPin, OUTPUT);
pinMode(InvAttackPin, OUTPUT);
pinMode(GateOutPin, OUTPUT);
pinMode(A_LedPin, OUTPUT);
pinMode(D_LedPin, OUTPUT);
pinMode(GateInPin, INPUT);
attachInterrupt(digitalPinToInterrupt(GateInPin), gateIn, CHANGE);
#if (PIN_CHECK)
pinMode(10, OUTPUT);
#endif
#if (UART_TRACE)
Serial.begin(115200);
Serial.println(TITLE_STR1);
Serial.println(TITLE_STR2);
delay(1000);
#endif
}
void updateState()
{
// Update the state of pins and add comments to show state changes
switch (state) {
case ST_ATTACK:
// Changing state to ST_ATTACK
digitalWrite(A_LedPin, HIGH); // Turn on A_LED
digitalWrite(D_LedPin, LOW); // Turn off D_LED
digitalWrite(AttackPin, HIGH); // Turn on Attack
digitalWrite(InvAttackPin, LOW); // Turn off InvAttack
digitalWrite(GateOutPin, HIGH); // Turn on GateOut
break;
case ST_DECAY:
// Changing state to ST_DECAY
digitalWrite(A_LedPin, LOW); // Turn off A_LED
digitalWrite(D_LedPin, HIGH); // Turn on D_LED
digitalWrite(AttackPin, LOW); // Turn off Attack
digitalWrite(InvAttackPin, HIGH); // Turn on InvAttack
// GateOutPin remains unchanged
break;
case ST_RELEASE:
// Changing state to ST_RELEASE
digitalWrite(A_LedPin, LOW); // Turn off A_LED
digitalWrite(D_LedPin, LOW); // Turn off D_LED
digitalWrite(AttackPin, LOW); // Turn off Attack
digitalWrite(InvAttackPin, HIGH); // Turn on InvAttack
digitalWrite(GateOutPin, LOW); // Turn off GateOut
break;
}
}
void loop()
{
#if (PIN_CHECK)
digitalWrite(10, HIGH);
#endif
attackThreshold = analogRead(AttackLevelPin);
int th = analogRead(ThresholdPin);
if (state == ST_ATTACK && th > attackThreshold) {
state = ST_DECAY;
isStateChanged = true;
}
#if (UART_TRACE)
// Check for pin state changes and print if any
bool pinStateChanged = false;
int currentGateInState = digitalRead(GateInPin);
int currentAttackState = digitalRead(AttackPin);
int currentInvAttackState = digitalRead(InvAttackPin);
int currentGateOutState = digitalRead(GateOutPin);
int currentALedState = digitalRead(A_LedPin);
int currentDLedState = digitalRead(D_LedPin);
if (currentGateInState != prevGateInState ||
currentAttackState != prevAttackState ||
currentInvAttackState != prevInvAttackState ||
currentGateOutState != prevGateOutState ||
currentALedState != prevALedState ||
currentDLedState != prevDLedState) {
pinStateChanged = true;
}
if (pinStateChanged) {
Serial.print("AtkLvl: ");
Serial.print(attackThreshold);
Serial.print("\tTh: ");
Serial.print(th);
Serial.print("\t\tGIn: ");
Serial.print(currentGateInState);
Serial.print(" Atk: ");
Serial.print(currentAttackState);
Serial.print(" InvAtk: ");
Serial.print(currentInvAttackState);
Serial.print(" GOut: ");
Serial.print(currentGateOutState);
Serial.print(" A_LED: ");
Serial.print(currentALedState);
Serial.print(" D_LED: ");
Serial.print(currentDLedState);
Serial.print(" State: ");
Serial.println(state);
// Update previous states
prevGateInState = currentGateInState;
prevAttackState = currentAttackState;
prevInvAttackState = currentInvAttackState;
prevGateOutState = currentGateOutState;
prevALedState = currentALedState;
prevDLedState = currentDLedState;
}
#endif
if (isStateChanged) {
isStateChanged = false;
updateState();
}
#if (PIN_CHECK)
digitalWrite(10, LOW);
#endif
}