デジアナな Envelope Generator を作る

概要

素人ゆえに、オーソドックスなADSR特性のエンベロープジェネレターを作りたいと思った。 HAGIWO作品 を揃えていこうと始めたモジュラーシンセ作りだが、氏の作品にはオーソドックスなADSR特性のエンベロープ ジェネレータがない。そこで、オーソドックスなADSR特性の製作事例を探してみると、発振回路とCRで作るアナログなエンベロープジェネレターが一般的だ。

エンベロープのADSR特性をソフト的に作ってみた がAttackからDecayへの遷移が安定しなかったり、特性を調整しずらい問題などがあり、満足のいく結果にはならなかった。

Googleでアナログとデジタルを組み合わせてオーソドックスなADSR特性を得ている事例を見つけた。Githubに回路図やArduinoスケッチも上がっているので、これをまねて作ることにした。

本文

ARDUINO EG を解析

このエンベロープジェネレータの特徴は、次のようになる。

  • 抵抗とコンデンサのLPFで、アナログなエンベロープ特性を作る。
  • 外部からのGate信号をきっかけとして、Attack, Decay, Sustain, Release 4フェーズでArduinoが5VをON/OFFし、その電圧でコンデンサの充放電をで行う。

動作をもう少し具体的に示すと、

  1. Attack段階: Gate信号がONになると、エンベロープはAttack段階に移行し、ArduinoのAttackPinをHIGHにする。
  2. Decay段階: Attack段階のしきい値に達すると、エンベロープはDecay段階に移行し、ArduinoのAttackPinをLOWにして、InvAttackPinをHIGHにする。
  3. release段階: Gate信号がOFFになると、エンベロープはrelease段階に移行し、ArduinoのGateOutPinをLOWにする。

LTspiceでシミュレーションすると、下図のようにきれいなADSR特性が得られることが確認できた。

sim

回路図


動作解析

ChatGPTで回路図の解析をしてもらうと、次のようになる。(すばらしい、感心する。)説明がちょっとくどいが、そのまま載せる。

調整用ポテンショメータ

ChatGPT

調整用ポテンショメータ(RV2、RV3、RV5、RV6)はエンベロープジェネレータの特性に次のように影響します。

  1. RV2 (Decay Time) :

    • 役割 : Decay段階の時間を調整します。
    • 影響 : ポテンショメータを調整することで、Attack段階の後に信号がピークから減衰する速度を変更できます。RV2の抵抗値が高いほど、減衰時間が長くなります。
  2. RV3 (Sustain Level) :

    • 役割 : サステインレベルを調整します。
    • 影響 : Decay段階の後に信号が保持されるレベルを設定します。RV3の抵抗値が高いほど、サステインレベルが低くなり、逆に低いほどサステインレベルが高くなります。
  3. RV5 (Attack Time) :

    • 役割 : Attack段階の時間を調整します。
    • 影響 : Gate信号がONになったときに信号が0からピークまで上昇する速度を変更します。RV5の抵抗値が高いほど、Attack時間が長くなります。
  4. RV6 (Release Time) :

    • 役割 : release段階の時間を調整します。
    • 影響 : Gate信号がOFFになったときに信号がピークから0に減衰する速度を変更します。RV6の抵抗値が高いほど、release時間が長くなります。

2つのトランジスタ(Q1とQ2)の機能と動作

ChatGPT

Q1 (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

時間軸での特性線図を描くために、エンベロープジェネレータの動作を時間の経過とともにシミュレーションします。具体的には、Attack、Decay、サステイン、releaseの各フェーズでの信号の変化を示します。

まず、エンベロープジェネレータの各フェーズを説明します:

  1. Attack (Attack) :

    • Gate信号がHIGHになると、Attackフェーズが開始されます。
    • このフェーズでは、信号が急速に最大値に達します。
    • Q2 (2SA1015)がONになり、AttackピンをHIGHにします。
  2. Decay (Decay) :

    • 信号が最大値に達すると、Decayフェーズに移行します。
    • このフェーズでは、信号がサステインレベルに向かって減少します。
    • Q1 (2N7000)がONになり、InvAttackピンをHIGHにします。
  3. サステイン (Sustain) :

    • 信号がサステインレベルに達すると、一定のレベルで保持されます。
    • このフェーズでは、信号が一定の電圧で維持されます。
  4. release (Release) :

    • Gate信号がLOWになると、releaseフェーズが開始されます。
    • このフェーズでは、信号がゼロに向かって減少します。
    • Q1 (2N7000)がONになり、InvAttackピンをHIGHにします。

以下に、時間軸での信号変化を示す特性線図を描きます。

ゼロドリフトとインバース特性

お手本にした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回路のオペアンプでこれらを構成する。

INV

製作

12HP x 3U サイズにPOTとジャックを並べるために、下図形状のPOTを基板にハンダ固定する方法をとった。

POT

例によってKicadと連携して搭載設計をFusion360で行う。

KICAD レイアウト


Fusion360で丁寧にモデルづくりを行う。出来上がりが楽しみだ。


3D Models


基板の2層化に初めて挑戦。固定ネジの位置決めのためFusion360の3Dモデルと、各層基板のスケッチが役立った。

実装基板


フロントパネル


配線は結構手間で、4,5時間かかったと思う。





スケッチ

ほぼ Arduino_EG のままである。LEDを付けただけ。

/*
 * 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
}
    
    

ADSR特性

ブレッドボードで組んだ試験回路では、ちゃんとADSRの個々の特性が出来上がっていたのに、基板にもってくるとなぜがARになってしまった。



POTをいじくり、ソフトを見直ししているときにPro Microを逆刺ししてしまい。特性が安定せず、おかしな特性がときどき出たりする。Pro Microがやられたのだと思う。








関連リンク

Next Post Previous Post