ソフトで作る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);
}
波形
指数関数的な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);
}
波形
所感
Fast PWM
// 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 は一度出力すると、それを頑固に続けるので、間違い個所を指摘できないと直せない。
exp関数のADSR特性
exp関数のロバスト化(差分方程式)
ADSRduinoというArduino Nano でADSRエンベロープジェネレータを実現しているプロジェクトがGithubで見つけた。 MCP4921 DAC を使っているが、Envelope特性は指数関数よりも負荷の軽い三角関数で実現しているし、特性の安定性も考慮されているようだ。
GitHub - m0xpd/ADSRduino: A simple ADSR for the Arduino
A simple ADSR for the Arduino. Contribute to m0xpd/ADSRduino development by creating an account on GitHub.
アタックフェーズ (Attack Phase)
- 式 :
- パラメータ :
- 初期値:
ディケイフェーズ (Decay Phase)
- 式 :
- パラメータ :
サステインフェーズ (Sustain Phase)
- 式 :
- パラメータ :
リリースフェーズ (Release Phase)
- 式 :
- パラメータ :
各フェーズの境界時間
- アタックフェーズ終了時間: 秒
- ディケイフェーズ終了時間: 秒
- サステインフェーズ終了時間: 秒
- リリースフェーズ終了時間: 秒
これらの式により、各フェーズでエンベロープがどのように変化するかを示すことができます。
元のコードでエンベロープの変化を表す式として、指数関数的な形式ではなく、以下のような差分方程式を使用している意図について考察します:
envelope = ((1.0 - alpha) * drive + alpha * envelope);
この式を選んでいる理由として、以下の点が推測されます:
1. 数値計算の簡便性
差分方程式は計算が簡単で、マイクロコントローラのようなリソースが限られた環境で効率的に実装できます。指数関数や対数関数を用いる場合に比べて、計算量が少なくなり、処理速度が向上します。
2. 直感的なパラメータ調整
alpha
を調整することで、エンベロープの変化速度を簡単に制御できます。alpha
が0に近いほど変化が速く、1に近いほど変化が遅くなります。このシンプルな調整方法は、エンベロープの制御において直感的で便利です。3. 安定性の確保
差分方程式を使用することで、数値的な安定性が向上します。特に、
alpha
の範囲を0から1の間に制限することで、エンベロープの変化が極端にならないようにできます。4. 簡単な実装
この形式の式は実装が簡単で、必要な計算も少ないため、開発の手間が少なくて済みます。また、ポテンショメータの値をそのまま
alpha
に反映させることで、ユーザーが直感的に操作できるようになります。5. 近似としての使用
この式は、エンベロープの変化をモデル化するための近似的な方法として機能します。完璧な指数関数的変化を求めるのではなく、エンベロープの滑らかな変化を再現するための実用的なアプローチとして使用されています。
これらの理由から、元のコードでは指数関数的な計算の代わりに、以下のような差分方程式が使用されていると推定されます:
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
// 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程度の変動であり、影響するほど変動していない。詳しい究明をしていないが、安定しない原因はノイズのような気がする。
関連リンク

Fast PWM on Arduino Leonardo
Today I will talk about PWM generation. The Arduino boards provide pseudo analog outputs using the analogWrite( ) function. This functio...