ソフトで作るADSR特性

概要

エンベロープジェネレータのADSR特性をソフトウエアーで作る。
方法は、

  • Arduinoマイコンの高速PWM(50kHzのPWM周波数にするため、タイマーレジストリを書き換え)を使う。
  • 出力にLPFを設けて、PWMのデューティ比で波形をつくる。(ADCの役割)

要件は、

  • 一般的にはArduinoを使うが、Sparkfun Pro Micro (互換機)を使う。理由は、手持ちがあったから。
  • ソフトはChatGPTで作る。理由は、ニッチなPro Micro のタイマーレジストリの変更など、どこまでChatGPTはノウハウがあるのか興味があった。タイマーレジストリを使いこなすのが大変なことも、大きな理由。

本文

回路・配線

ChatGPT で書いた回路略図

Sparkfun Pro Micro

+-----------+
|           |
|    VCC    |
|    GND    |
|           |
|    A0  ---|--- ATTACK_SPEED_PIN
|    A1  ---|--- DECAY_SPEED_PIN
|    A2  ---|--- SUSTAIN_LEVEL_PIN
|    A3  ---|--- RELEASE_SPEED_PIN
|    A10 ---|--- ATTACK_MAX_PIN
|    2   ---|--- GATE_IN_PIN
|    7   ---|--- ATTACK_LED_PIN
|    8   ---|--- DECAY_LED_PIN
|    9   ---|--- ENVELOPE_OUT_PIN ---- R (3.3kΩ) ----+----> Output
|           |                                         |
|           |                                         +  
|           |                                      C (1μF) 
|           |                                         +  
|           |                                         |
|           |                                        GND
+-----------+

code作成(ChatGPT)

ChatGPT   に下記の内容で作成要請。

Pro Microを使って、ADSRの4つのフェーズを持つエンベロープジェネレター用のコードを書いてください。

要件は、

  • 2つのポットで、それぞれATTACK特性の最大値(電圧)とSustainのレベル(電圧)を決める。
  • 3つのポットそれぞれで、Attack, Dekay, Releaseの増減の速さを決める。(CR特性の時定数に相当)
  • ADSRの特性はCR(コンデンサ・抵抗)の特性に合わせて、指数関数とする。
  • エンベロープ特性の出力pinはA0、5つのポットの入力はPro Microのアナログピンを順番に割り当てる。
  • AttackとDecayのフェーズではそれぞれLEDを点灯する。
  • Attackフェーズは外部からのGate入力HIGHをきっかけに開始する。
  • Attack特性がATTACK特性の最大値(電圧)になると、Decay特性に移行する。その後、Sustain フェーズになる。
  • Gate入力がLOWになると、Sustain フェーズから、Releaseフェーズになる。
  • ADCは fast PWM とLPFを使う。PWM周波数は、50kHz。

リニアなADSR特性

code


// Pin definitions
#define ATTACK_MAX_PIN A10
#define SUSTAIN_LEVEL_PIN A2
#define ATTACK_SPEED_PIN A0
#define DECAY_SPEED_PIN A1
#define RELEASE_SPEED_PIN A3

#define GATE_IN_PIN 2
#define ATTACK_LED_PIN 7
#define DECAY_LED_PIN 8
#define ENVELOPE_OUT_PIN 9  // Use Timer1 for PWM

// Variables
float attackMax;
float sustainLevel;
float attackSpeed;
float decaySpeed;
float releaseSpeed;

float envelope;
float attackIncrement;
float decayDecrement;
float releaseDecrement;

unsigned long prevTime;
bool gateState;
int state;

void setup() {
pinMode(GATE_IN_PIN, INPUT);
pinMode(ATTACK_LED_PIN, OUTPUT);
pinMode(DECAY_LED_PIN, OUTPUT);
pinMode(ENVELOPE_OUT_PIN, OUTPUT);

Serial.begin(9600);

// Set PWM frequency to 50kHz on Timer1
TCCR1A = 0;             // Clear Timer/Counter Control Registers
TCCR1B = 0;
TCNT1 = 0;              // Clear Timer counter

ICR1 = 319;             // Set TOP value for 50kHz PWM (16MHz / (50kHz * 8) - 1)
TCCR1A |= (1 << WGM11); // Set fast PWM mode using ICR1 as TOP
TCCR1B |= (1 << WGM13) | (1 << WGM12); // Set fast PWM mode using ICR1 as TOP
TCCR1B |= (1 << CS11);  // Set prescaler to 8

// Enable output on pin 9 (OC1A)
TCCR1A |= (1 << COM1A1);
}

void loop() {
// Read potentiometer values
attackMax = map(analogRead(ATTACK_MAX_PIN), 0, 1023, 0, 255);
sustainLevel = map(analogRead(SUSTAIN_LEVEL_PIN), 0, 1023, 0, 255);
attackSpeed = map(analogRead(ATTACK_SPEED_PIN), 0, 1023, 1, 100);
decaySpeed = map(analogRead(DECAY_SPEED_PIN), 0, 1023, 1, 100);
releaseSpeed = map(analogRead(RELEASE_SPEED_PIN), 0, 1023, 1, 100);

// Calculate increments and decrements
attackIncrement = attackMax / (attackSpeed * 10.0);  // Adjust scale factor as needed
decayDecrement = (attackMax - sustainLevel) / (decaySpeed * 10.0);  // Adjust scale factor as needed
releaseDecrement = sustainLevel / (releaseSpeed * 10.0);  // Adjust scale factor as needed

// Read gate input
bool gateInput = digitalRead(GATE_IN_PIN);

// Update envelope based on gate input
if (gateInput == HIGH && gateState == LOW) {
  envelope = 0;
  state = 0;
  prevTime = millis(); // Reset time for next phase
  digitalWrite(ATTACK_LED_PIN, HIGH);
} else if (gateInput == LOW && gateState == HIGH) {
  state = 3;
  prevTime = millis(); // Reset time for next phase
  digitalWrite(ATTACK_LED_PIN, LOW);
  digitalWrite(DECAY_LED_PIN, LOW);
}
gateState = gateInput;

// Update envelope based on state
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - prevTime;

switch (state) {
  case 0: // Attack
    if (gateInput == LOW) {
      // If gate is LOW, reset envelope and go to Release state
      envelope = 0;
      state = 3; // Release
      prevTime = currentTime; // Reset time for next phase
      digitalWrite(ATTACK_LED_PIN, LOW);
      digitalWrite(DECAY_LED_PIN, LOW);
    } else {
      // If gate is HIGH, continue Attack phase
      envelope += attackIncrement * elapsedTime;
      if (envelope >= attackMax) {
        envelope = attackMax;
        state = 1; // Decay
        prevTime = currentTime; // Reset time for next phase
        digitalWrite(ATTACK_LED_PIN, LOW);
        digitalWrite(DECAY_LED_PIN, HIGH);
      }
    }
    break;
  case 1: // Decay
    envelope -= decayDecrement * elapsedTime;
    if (envelope <= sustainLevel) {
      envelope = sustainLevel;
      state = 2; // Sustain
      prevTime = currentTime; // Reset time for next phase
      digitalWrite(DECAY_LED_PIN, LOW);
    }
    break;
  case 2: // Sustain
    // Do nothing
    break;
  case 3: // Release
    envelope -= releaseDecrement * elapsedTime;
    if (envelope <= 0) {
      envelope = 0;
      state = 0; // Reset state to Attack
    }
    break;
}

// Constrain envelope value to 0-255 range
envelope = constrain(envelope, 0, 255);

// Output envelope as PWM
OCR1A = (int)envelope;

// Print envelope for debugging
Serial.print("Envelope: ");
Serial.print(envelope);
Serial.print(" gateInput: ");
Serial.print(gateInput);
Serial.print(" currentTime: ");
Serial.print(currentTime);
Serial.print(" elapsedTime: ");
Serial.print(elapsedTime);
Serial.print(" State: ");
Serial.println(state);

// Update previous time
prevTime = currentTime;

// Delay for stability
delay(10);
}

波形

Linear

指数関数的なADSR特性 (exp関数)

code

// Pin definitions
#define ATTACK_THR_PIN A10
#define ATTACK_SPEED_PIN A0
#define DECAY_SPEED_PIN A1
#define SUSTAIN_LEVEL_PIN A2
#define RELEASE_SPEED_PIN A3

#define GATE_IN_PIN 2
#define ATTACK_LED_PIN 7
#define DECAY_LED_PIN 8
#define ENVELOPE_OUT_PIN 9  // Use Timer1 for PWM

// Variables
float attackThres;
float sustainLevel;
float attackSpeed;
float decaySpeed;
float releaseSpeed;

float envelope;
float attackfunc;
float decayfunc;
float releasefunc;

unsigned long prevTime;
bool gateState;
int state;

void setup() {
pinMode(GATE_IN_PIN, INPUT);
pinMode(ATTACK_LED_PIN, OUTPUT);
pinMode(DECAY_LED_PIN, OUTPUT);
pinMode(ENVELOPE_OUT_PIN, OUTPUT);

Serial.begin(9600);

// Set PWM frequency to 50kHz on Timer1
TCCR1A = 0;             // Clear Timer/Counter Control Registers
TCCR1B = 0;
TCNT1 = 0;              // Clear Timer counter

ICR1 = 319;             // Set TOP value for 50kHz PWM (16MHz / (50kHz * 8) - 1)
TCCR1A |= (1 << WGM11); // Set fast PWM mode using ICR1 as TOP
TCCR1B |= (1 << WGM13) | (1 << WGM12); // Set fast PWM mode using ICR1 as TOP
TCCR1B |= (1 << CS11);  // Set prescaler to 8

// Enable output on pin 9 (OC1A)
TCCR1A |= (1 << COM1A1);
}

void loop() {
// Read potentiometer values
attackThres = map(analogRead(ATTACK_THR_PIN), 0, 1023, 0, 255);
sustainLevel = map(analogRead(SUSTAIN_LEVEL_PIN), 0, 1023, 0, 255);
attackSpeed = map(analogRead(ATTACK_SPEED_PIN), 0, 1023, 1, 100);
decaySpeed = map(analogRead(DECAY_SPEED_PIN), 0, 1023, 1, 100);
releaseSpeed = map(analogRead(RELEASE_SPEED_PIN), 0, 1023, 1, 100);

// Calculate funcs
float attackfunc = 1 - exp(-2 / (attackSpeed * 10.0) );
float decayfunc = 1 - exp(-2 / (decaySpeed * 10.0));
float releasefunc = 1 - exp(-2 / (releaseSpeed * 10.0));

// Read gate input
bool gateInput = digitalRead(GATE_IN_PIN);

// Update envelope based on gate input
if (gateInput == HIGH && gateState == LOW) {
  envelope = 0;
  state = 0;
  prevTime = millis();
  digitalWrite(ATTACK_LED_PIN, HIGH);
} else if (gateInput == LOW && gateState == HIGH) {
  state = 3;
  prevTime = millis();
  digitalWrite(ATTACK_LED_PIN, LOW);
  digitalWrite(DECAY_LED_PIN, LOW);
  gateState = LOW; // Update gateState to match gateInput
}
gateState = gateInput;

// Update envelope based on state
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - prevTime;
prevTime = currentTime;

switch (state) {
  case 0: // Attack
    //envelope += (attackThres - envelope) * attackfunc * elapsedTime;
    envelope += (attackThres  - envelope) * attackfunc * elapsedTime;
    if (envelope >= attackThres) {
      envelope = attackThres;
      state = 1; // Decay
      digitalWrite(ATTACK_LED_PIN, LOW);
      digitalWrite(DECAY_LED_PIN, HIGH);
    }
    break;
  case 1: // Decay
    envelope -= (envelope - sustainLevel) * decayfunc * elapsedTime;
    if (envelope <= sustainLevel) {
      envelope = sustainLevel;
      state = 2; // Sustain
      digitalWrite(DECAY_LED_PIN, LOW);
    }
    break;
  case 2: // Sustain
    // Do nothing
    break;
  case 3: // Release
    envelope -= envelope * releasefunc * elapsedTime;
    if (envelope <= 0) {
      envelope = 0;
      state = 0; // Reset state to Attack
    }
    break;
}

// Constrain envelope value to 0-255 range
envelope = constrain(envelope, 0, 255);

// Output envelope as PWM
OCR1A = (int)envelope;

// Print envelope for debugging
Serial.print("Envelope: ");
Serial.print(envelope);
Serial.print(" gateInput: ");
Serial.print(gateInput);
Serial.print(" currentTime: ");
Serial.print(currentTime);
Serial.print(" State: ");
Serial.println(state);

// Delay for stability
delay(10);
}

波形

リニアなADSR特性波形

所感

Fast PWM

Fast PWM は難解でデータシートを見ても、なかなか理解できないし、理解しても使うときには忘れてしまっている。まして、Pro Micro の fast PWM を扱うブログは、まず見かけない、これを考えると、ChatGPTに任せるのが賢明である。

しかし下記が、 ChatGPT   が出した50kHz の fast PWM のコードを抜粋したものであるが、間違いがある。そこに気づくまでに、数時間を費やした。

ChatGPT   を使いこなすには、知識が必要なことを痛感した。

// Pin definitions
      // Set PWM frequency to 50kHz on Timer1
  TCCR1A = 0;             // Clear Timer/Counter Control Registers
  TCCR1B = 0;
  TCNT1 = 0;              // Clear Timer counter

  OCR1A = 319;            // Set TOP value for 50kHz PWM (16MHz / (50kHz * 8) - 1)
  TCCR1A |= (1 << WGM11); // Set fast PWM mode using ICR1 as TOP
  TCCR1B |= (1 << WGM13) | (1 << WGM12); // Set fast PWM mode using ICR1 as TOP
  TCCR1B |= (1 << CS11);  // Set prescaler to 8

  // Enable output on pin 9 (OC1A)
  TCCR1A |= (1 << COM1A1);
    
  // Output envelope as PWM
  OCR1A = envelope; 

OCR1A = 319; が間違っており、正しくは、 ICR1 = 319; としなければならない。

// Set TOP value for 50kHz PWM (16MHz / (50kHz * 8) - 1)
この式も間違っている。分周比 1 なので、(16MHz / (50kHz * 1) - 1) になる。


ChatGPT   は一度出力すると、それを頑固に続けるので、間違い個所を指摘できないと直せない。

ChatGPT   それぞれの違いについて説明します。

OCR1A = 319; ICR1 = 319; の違い

  • OCR1A = 319;

    • OCR1A に設定された値はPWM出力のON時間を決定します。
  • ICR1 = 319;

    • ICR1 に設定された値はPWM信号の周期(周波数)を決定します。
つまり、上記のコードではON時間が319(50kHzでデューティ 1、常時最大値)になってしまう。

exp関数のADSR特性

リニアなPOTの特性と、exp関数の係数の合わせこみがむつかしく、POTの調整がうまくいかない。
具体的には、POTの抵抗値が低いときは特性の時定数が小さいときに相当するが、この領域が敏感過ぎて、上記に載せたADSR特性を得るには針の穴を通すほどの微細な調整が必要であった。
さらにPOT値に対して、敏感である故に特性が安定しないという問題もある。オシロで波形を見ていると、ADSRだったり、ARだったりする。

つまり、ADSRの特性にするのがむつかしく(ロバストでなく)、AR特性になってしまうという問題だ。

exp関数のロバスト化(差分方程式)


ADSRduinoというArduino Nano でADSRエンベロープジェネレータを実現しているプロジェクトがGithubで見つけた。 MCP4921 DAC を使っているが、Envelope特性は指数関数よりも負荷の軽い三角関数で実現しているし、特性の安定性も考慮されているようだ。




以下は、このプロジェクトのコードをChatGTPが解析した内容だ。

アタックフェーズ (Attack Phase)

  • : envelope [ i ] = ( 1.0 ? α 1 ) ? drive + α 1 ? envelope [ i ? 1 ] \text{envelope}[i] = (1.0 - \alpha_1) \cdot \text{drive} + \alpha_1 \cdot \text{envelope}[i-1]
  • パラメータ :
    • α 1 = 0.7 \alpha_1 = 0.7
    • drive = 1023 \text{drive} = 1023
    • 初期値: envelope [ 0 ] = 0 \text{envelope}[0] = 0

ディケイフェーズ (Decay Phase)

  • : envelope [ i ] = ( 1.0 ? α 2 ) ? sustain_level + α 2 ? envelope [ i ? 1 ] \text{envelope}[i] = (1.0 - \alpha_2) \cdot \text{sustain\_level} + \alpha_2 \cdot \text{envelope}[i-1]
  • パラメータ :
    • α 2 = 0.7 \alpha_2 = 0.7
    • sustain_level = 0.7 ? 1023 = 716.1 \text{sustain\_level} = 0.7 \cdot 1023 = 716.1

サステインフェーズ (Sustain Phase)

  • : envelope [ i ] = sustain_level \text{envelope}[i] = \text{sustain\_level}
  • パラメータ :
    • sustain_level = 0.7 ? 1023 = 716.1 \text{sustain\_level} = 0.7 \cdot 1023 = 716.1

リリースフェーズ (Release Phase)

  • : envelope [ i ] = ( 1.0 ? α 3 ) ? 0 + α 3 ? envelope [ i ? 1 ] \text{envelope}[i] = (1.0 - \alpha_3) \cdot 0 + \alpha_3 \cdot \text{envelope}[i-1]
  • パラメータ :
    • α 3 = 0.95 \alpha_3 = 0.95

各フェーズの境界時間

  • アタックフェーズ終了時間: t attack_end = attack_time = 0.1 t_{\text{attack\_end}} = \text{attack\_time} = 0.1
  • ディケイフェーズ終了時間: t decay_end = attack_time + decay_time = 0.2 t_{\text{decay\_end}} = \text{attack\_time} + \text{decay\_time} = 0.2
  • サステインフェーズ終了時間: t sustain_end = gate_time = 0.5 t_{\text{sustain\_end}} = \text{gate\_time} = 0.5
  • リリースフェーズ終了時間: t release_end = gate_time + release_time = 0.7 t_{\text{release\_end}} = \text{gate\_time} + \text{release\_time} = 0.7

これらの式により、各フェーズでエンベロープがどのように変化するかを示すことができます。


元のコードで alpha1 , alpha2 , alpha3 にかかっていた 0.999 0.99999 といった係数は、時間定数を調整するためのものであると考えられます。これらの係数は、ポテンショメータの読み取り値を利用して、 alpha 値を微調整し、エンベロープの変化速度を制御する役割を果たしています。

具体的には、以下のような意図があると推定されます:

  1. 時間定数の制御 :

    • alpha 値は、エンベロープの変化速度を決定する重要なパラメータです。 0.999 0.99999 といった係数を掛けることで、 alpha 値が1に近づくのを防ぎ、エンベロープの変化が無限に遅くなるのを防ぎます。
    • これらの係数は、エンベロープが適切な速度で変化するようにするための微調整です。具体的には、ポテンショメータの読み取り値が大きい場合でも、 alpha 値が1に近づきすぎないようにします。
  2. エンベロープの安定性の確保 :

    • alpha 値が1に近づくと、エンベロープの変化が非常に遅くなり、ほとんど変化しない状態になります。これを防ぐために、 0.999 0.99999 の係数を掛けることで、エンベロープが過度に遅くならないようにしています。
    • また、これらの係数はエンベロープのステップ応答の特性を調整し、システムの安定性を確保するために使用されます。
  3. 範囲の制限 :

    • alpha 値が1に近づくと、エンベロープの計算において数値的な不安定性が生じる可能性があります。これを防ぐために、係数を掛けて alpha 値の範囲を制限しています。

まとめると、 0.999 0.99999 といった係数は、エンベロープの時間定数を調整し、エンベロープの変化速度を適切に制御するための微調整として機能しています。これにより、エンベロープが期待通りの速度で変化し、システム全体の安定性が確保されます。


元のコードでエンベロープの変化を表す式として、指数関数的な形式ではなく、以下のような差分方程式を使用している意図について考察します:

envelope = ((1.0 - alpha) * drive + alpha * envelope);


この式を選んでいる理由として、以下の点が推測されます:

1. 数値計算の簡便性

差分方程式は計算が簡単で、マイクロコントローラのようなリソースが限られた環境で効率的に実装できます。指数関数や対数関数を用いる場合に比べて、計算量が少なくなり、処理速度が向上します。

2. 直感的なパラメータ調整

alpha を調整することで、エンベロープの変化速度を簡単に制御できます。 alpha が0に近いほど変化が速く、1に近いほど変化が遅くなります。このシンプルな調整方法は、エンベロープの制御において直感的で便利です。

3. 安定性の確保

差分方程式を使用することで、数値的な安定性が向上します。特に、 alpha の範囲を0から1の間に制限することで、エンベロープの変化が極端にならないようにできます。

4. 簡単な実装

この形式の式は実装が簡単で、必要な計算も少ないため、開発の手間が少なくて済みます。また、ポテンショメータの値をそのまま alpha に反映させることで、ユーザーが直感的に操作できるようになります。

5. 近似としての使用

この式は、エンベロープの変化をモデル化するための近似的な方法として機能します。完璧な指数関数的変化を求めるのではなく、エンベロープの滑らかな変化を再現するための実用的なアプローチとして使用されています。

これらの理由から、元のコードでは指数関数的な計算の代わりに、以下のような差分方程式が使用されていると推定されます:

code

ChatGTPは下記のようなこともできる。Code A、Bのロジックを解析して、envelope特性に該当する部分を入れ換えてみた。手作業でやると、ありがちなミスをすることなく、1,2行修正するだけで問題なく動くコードが一瞬でできる。

上記の指数関数のcode Aを置き換えたコード Code Bが、下記になる。

    // Pin definitions
#define ATTACK_THR_PIN A10
#define ATTACK_SPEED_PIN A0
#define DECAY_SPEED_PIN A1
#define SUSTAIN_LEVEL_PIN A2
#define RELEASE_SPEED_PIN A3

#define GATE_IN_PIN 2
#define ATTACK_LED_PIN 7
#define DECAY_LED_PIN 8
#define ENVELOPE_OUT_PIN 9  // Use Timer1 for PWM

// Constants
const float ADC_SCALE = 795.0;

// Variables
float attackThres;
float sustainLevel;
float attackSpeed;
float decaySpeed;
float releaseSpeed;

float envelope = 0.0;
float alpha1, alpha2, alpha3;

unsigned long prevTime;
bool gateState = false;
int state = 0;

void setup() {
  pinMode(GATE_IN_PIN, INPUT);
  pinMode(ATTACK_LED_PIN, OUTPUT);
  pinMode(DECAY_LED_PIN, OUTPUT);
  pinMode(ENVELOPE_OUT_PIN, OUTPUT);

  Serial.begin(9600);

  // Set PWM frequency to 50kHz on Timer1
  TCCR1A = 0;             // Clear Timer/Counter Control Registers
  TCCR1B = 0;
  TCNT1 = 0;              // Clear Timer counter

  ICR1 = 319;             // Set TOP value for 50kHz PWM (16MHz / (50kHz * 8) - 1)
  TCCR1A |= (1 << WGM11); // Set fast PWM mode using ICR1 as TOP
  TCCR1B |= (1 << WGM13) | (1 << WGM12); // Set fast PWM mode using ICR1 as TOP
  TCCR1B |= (1 << CS11);  // Set prescaler to 8

  // Enable output on pin 9 (OC1A)
  TCCR1A |= (1 << COM1A1);
}

void loop() {
  // Read potentiometer values
  // Calculate alpha values based on potentiometer readings
  alpha1 = sqrt(0.7 * cos((1023 - analogRead(ATTACK_SPEED_PIN)) / ADC_SCALE));
  alpha2 = sqrt(0.999 * cos((1023 - analogRead(DECAY_SPEED_PIN)) / ADC_SCALE));
  alpha3 = sqrt(0.99999 * cos((1023 - analogRead(RELEASE_SPEED_PIN)) / ADC_SCALE));

  // Read potentiometer values
  // Map sustain level to 0-255 range
  sustainLevel = map(analogRead(SUSTAIN_LEVEL_PIN), 0, 1023, 0, 255);
  attackThres = map(analogRead(ATTACK_THR_PIN), 0, 1023, 0, 255);

  // Read gate input
  bool gateInput = digitalRead(GATE_IN_PIN);

  // Update envelope based on gate input
  if (gateInput == HIGH && gateState == LOW) {
    envelope = 0;
    state = 0;
    prevTime = millis();
    digitalWrite(ATTACK_LED_PIN, HIGH);
  } else if (gateInput == LOW && gateState == HIGH) {
    state = 3;
    prevTime = millis();
    digitalWrite(ATTACK_LED_PIN, LOW);
    digitalWrite(DECAY_LED_PIN, LOW);
    gateState = LOW; // Update gateState to match gateInput
  }
  gateState = gateInput;

  // Update envelope based on state
  unsigned long currentTime = millis();
  unsigned long elapsedTime = currentTime - prevTime;
  prevTime = currentTime;

  switch (state) {
    case 0: // Attack
      envelope += (attackThres - envelope) * (1 - alpha1);
      if (envelope >= attackThres) {
        envelope = attackThres;
        state = 1; // Decay
        digitalWrite(ATTACK_LED_PIN, LOW);
        digitalWrite(DECAY_LED_PIN, HIGH);
      }
      break;
    case 1: // Decay
      envelope += (sustainLevel - envelope) * (1 - alpha2);
      if (envelope <= sustainLevel) {
        envelope = sustainLevel;
        state = 2; // Sustain
        digitalWrite(DECAY_LED_PIN, LOW);
      }
      break;
    case 2: // Sustain
      // Do nothing
      break;
    case 3: // Release
      envelope += (0 - envelope) * (1 - alpha3);
      if (envelope <= 0) {
        envelope = 0;
        state = 0; // Reset state to Attack
      }
      break;
  }

  // Constrain envelope value to 0-255 range
  envelope = constrain(envelope, 0, 255);

  // Output envelope as PWM
  OCR1A = (int)envelope;

  // Print envelope for debugging
  Serial.print("Envelope: ");
  Serial.print(envelope);
  Serial.print(" gateInput: ");
  Serial.print(gateInput);
  Serial.print(" currentTime: ");
  Serial.print(currentTime);
  Serial.print(" State: ");
  Serial.println(state);

  // Delay for stability
  delay(10);
}

    

このcodeにおいて、alpha1からalpha3の式に含まれるCOSにかかる係数が安定化のための抑制係数になる。

安定しないのはAttackフェーズなので、alpha1中の係数をいじってみたが、POT値の変更に対して、Attack特性の安定と変化のバランスをとることができなかった。

次の動画は、オリジナルの係数0.999に対して、より安定するように係数 0.7とした場合であるが、動画のとおり揺れ動いている。係数をさらに小さくすれば安定するが、POTを変えても特性が変わらなくなる。しばらく、試行錯誤を繰り返したが、満足する結果は得られなかった。

POTの出力電圧は0~5Vの可変範囲に対して、20mA程度の変動であり、影響するほど変動していない。詳しい究明をしていないが、安定しない原因はノイズのような気がする。


関連リンク

Next Post Previous Post