ADSR envelope generator - PWM change over time

Hi Everyone!

I’m new here but I’ll try to keep this short

I want to use 4 potentiometers And a ‘gate pin’ on a Nano to control how quickly the PWM value changes over four sequential periods of time to attempt an ADSR envelope generator.
ie.
A ~ 0-255
D ~ 255-<sustain pot/pwm value>
S ~ (Hold this value until ‘gate’ falls to LOW state)
R ~ <sustain pot/pwm value>-0

Is this possible without using the delay() command or using the ‘hidden pwm’ functionality?

I’m planning on using RC filter(s) to smooth it out to a more consistent signal before amplifying the signal to 10v for eurorack application.

Cheers

Kempy

You can using state machines

loop()
{
	if(stateA) {
		stateA = false;
		
		// do stateA stuff
		
		stateB = true;
	}
	
	if(stateB) {
		stateB = false;
		
		// do stateB stuff
		
		stateC = true;
	}
	
	if(stateC) {
		stateC = false;
		
		// do stateC stuff
		
		stateD = true;
	}
	
	if(stateD) {
		stateD = false;
		
		// do stateD stuff

	}
}

The example above is a sequential state machine. stateA --> stateB --> stateC --> stateD

A more general approach would look like

loop()
{
	if(stateA) {
		stateA = false;
		
		// do stateA stuff

	}
	
	if(stateB) {
		stateB = false;
		
		// do stateB stuff

	}
	
	if(stateC) {
		stateC = false;
		
		// do stateC stuff

	}
	
	if(stateD) {
		stateD = false;
		
		// do stateD stuff

	}
}

where each state is independent of each other, and each has it's own trigger source i.e. a push button make stateC true, a sensor reading make stateA true, etc.

Awesome, thanks! That seems like it would definitely makes it quick without holdups.

Is there a way to automate the PWM sweep over time? Or would I have to make a table? Or code all 256 steps for the attack phase?

What are you changing during the PWM sweep? Is it the duty cycle, the frequency, or both? And how long is the sweep (a few seconds to go through all the values or a few hours, etc)

I want to change the duty cycle of the pwm of the Attack, Decay, and Release Phases over a period of no more than 10 seconds each.

I don’t know exact time values yet because of the nature of the limited pwm resolution.

Edit: If possible I would like to double up this code to be able to output two ADSR envelopes to utilize all 8 analog pins. The ultimate goal is just trying to make the most robust arduino based lofi eurorack setup that I can.

I don't know exact time values yet because of the nature of the limited pwm resolution

The Arduino's PWM library may provide you with limited resolution, but you can certainly achieve resolution in the hundred thousand Hz using direct register manipulation

I’m not overly concerned with the limitation of the resolution as it currently stands...

and reprogramming any of the default values is a little beyond the intended scope of this project and the scope my experience with coding hahaha.

For the state machine, you really want to transition directly from one to the other because they are always consecutive:

loop()
{
  if(state == ATTACK) {
	// do ATTACK stuff
                // when we decide we're finished:
		state = DECAY;
		}
	else if(state == DECAY) {...

Hey everyone!

that took longer than intended. Excuses aside, here's the code I came up with:

class EnvelopeGenerator
{
    //pins
    byte triggerPin, attackPin, attackLengthPin, decayPin, decayLengthPin, sustainPin, releasePin, releaseLengthPin, outputPin;

    //state variables
    bool trigger = 0;
    bool sustain = 0;
    bool note_on = 0;
    bool attack_length = 0;
    bool decay_length = 0;
    bool release_length = 0;

    byte attack_state = 0;
    byte decay_state = 0;
    byte release_state = 0;

    unsigned long last_time_of_attack_sample;
    unsigned long last_time_of_decay_sample;
    unsigned long last_time_of_release_sample;
    int attack_sample_rate = 0;
    int decay_sample_rate = 0;
    int release_sample_rate = 0;

  public:
    EnvelopeGenerator(byte pin1, byte pin2, byte pin3, byte pin4, byte pin5, byte pin6, byte pin7, byte pin8, byte pin9)
    {
      triggerPin = pin1;
      attackPin = pin2;
      attackLengthPin = pin3;
      decayPin = pin4;
      decayLengthPin = pin5;
      sustainPin = pin6;
      releasePin = pin7;
      releaseLengthPin = pin8;
      outputPin = pin9;
    }

    int attack_time_map(int input) { //2ms per sample to 20ms per sample
      return (input * attack_sample_rate);
    }
    int decay_time_map(int input) { //2ms per sample to 20ms per sample
      return (input * decay_sample_rate);
    }
    int release_time_map(int input) { //2ms per sample to 20ms per sample
      return (input * release_sample_rate);
    }


    void update_controls() {
      attack_sample_rate = attack_time_map(analogRead(attackPin));
      attack_length = digitalRead(attackLengthPin);
      decay_sample_rate = decay_time_map(analogRead(decayPin));
      decay_length = digitalRead(decayLengthPin);
      sustain = map(analogRead(sustainPin), 0, 1023, 0, 255);
      release_sample_rate = release_time_map(analogRead(releasePin));
      release_length = digitalRead(releaseLengthPin);
    }

    void read_trigger() {
      if (digitalRead(triggerPin) == true and trigger == false) {
        trigger = true;
        attack_state = 0;
        note_on = true;
        digitalWrite(13, HIGH);
      }
      else if (digitalRead(triggerPin) == false and trigger == true) {
        trigger = false;
        release_state = sustain;
        note_on = false;
        digitalWrite(13, LOW);
      }
    }

    void loopFunc() {
      //read trigger pin and set boolean variables depending on state
      read_trigger();

      if (note_on == true) {
        analogWrite(outputPin, attack_state);
        if (attack_state >= 255) {
          decay_state = 255;
        }
        else ((micros() - last_time_of_attack_sample >= attack_sample_rate) and (attack_state < 255)); {
          attack_state++;
          if (attack_length = LOW) {
            last_time_of_attack_sample = micros();
            attack_sample_rate = 50;
          }
          else {
            attack_sample_rate = 200;
            last_time_of_attack_sample = micros();
          }
        }
      }

      else if ((decay_state >= 255) and note_on == true) {
        analogWrite(outputPin, decay_state);

        if ((micros() - last_time_of_decay_sample >= decay_sample_rate) and (decay_state > sustain)) {
          decay_state--;
          attack_state = 0;
          if (decay_length == LOW) {
            last_time_of_decay_sample = micros();
            decay_sample_rate = 50;
          }
          else {
            last_time_of_decay_sample = micros();
            decay_sample_rate = 200;
          }
        }
      }
      else if ((sustain >= decay_state) and note_on == true) {
        analogWrite(outputPin, sustain);
      }
      else {
        analogWrite(outputPin, release_state);
        if ((micros() - last_time_of_release_sample >= release_sample_rate)) {
          release_state--;
          sustain = 0;
          if (release_length == LOW) {
            last_time_of_release_sample = micros();
            decay_sample_rate = 50;
          }
          else {
            last_time_of_release_sample = micros();
            decay_sample_rate = 200;
          }
        }
      }
    }
};

EnvelopeGenerator adsr1(2, A7, 3, A6, 4, A5, A4, 5, 6);
//EnvelopeGenerator adsr2(7, A3, 8, A2, 9, A1, A0, 10, 11);

unsigned long last_time_of_update;
int update_controls_delta = 15; //every 512ms update control parameters

void setup() {
  Serial.begin(300);

}

void loop() {
  //update control parameters periodically based on update_controls_delta variable
  if (millis() - last_time_of_update >= update_controls_delta) {
    adsr1.update_controls();
  //  adsr2.update_controls();
  }

  adsr1.loopFunc();
  //adsr2.loopFunc();


}

I've got this uploaded to my Nano but I don't seem to have any control over the time parameter of each state (Attack, Decay, Sustain, Release)

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.