Adding ADSR to wavetable synth...?

Hi all. :slight_smile:
I am building an Android controlled Arduino synth using bluetooth as a transmission medium. In putting together my Bluetooth controlled synth engine I have successfully managed to output different waveshapes according to a button press on the android GUI I am building and I can change the frequency and volume crudely using sliders on the GUI as well.
I was hoping someone could help me with how to go about implementing an ADSR or envelope shaper in my code. The synthesis method I am using is timer interrupt PWM and wavetables. The code I am using is here:

This is the wavetable:

#define INTERRUPT_PERIOD 512
#define FINT (F_CPU / INTERRUPT_PERIOD) // 16kHz?
#define FS (FINT)
 #define STEPS 256
// sine lookup table pre-calculated
prog_uchar PROGMEM sinetable[256] = {
  128,131,134,137,140,143,146,149,152,156,159,162,165,168,171,174,
  176,179,182,185,188,191,193,196,199,201,204,206,209,211,213,216,
  218,220,222,224,226,228,230,232,234,236,237,239,240,242,243,245,
  246,247,248,249,250,251,252,252,253,254,254,255,255,255,255,255,
  255,255,255,255,255,255,254,254,253,252,252,251,250,249,248,247,
  246,245,243,242,240,239,237,236,234,232,230,228,226,224,222,220,
  218,216,213,211,209,206,204,201,199,196,193,191,188,185,182,179,
  176,174,171,168,165,162,159,156,152,149,146,143,140,137,134,131,
  128,124,121,118,115,112,109,106,103,99, 96, 93, 90, 87, 84, 81, 
  79, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, 49, 46, 44, 42, 39, 
  37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 18, 16, 15, 13, 12, 10, 
  9,  8,  7,  6,  5,  4,  3,  3,  2,  1,  1,  0,  0,  0,  0,  0,  
  0,  0,  0,  0,  0,  0,  1,  1,  2,  3,  3,  4,  5,  6,  7,  8,  
  9,  10, 12, 13, 15, 16, 18, 19, 21, 23, 25, 27, 29, 31, 33, 35, 
  37, 39, 42, 44, 46, 49, 51, 54, 56, 59, 62, 64, 67, 70, 73, 76, 
  79, 81, 84, 87, 90, 93, 96, 99, 103,106,109,112,115,118,121,124
};

this is the main code:

/*----------------------------------------------------
 functions to handle converting PCM to PWM and 
 outputting sound
 ----------------------------------------------------*/
 // Timer Interrupt 
// This is called at sampling freq to output 8-bit samples to PWM
ISR(TIMER1_COMPA_vect)
{
  static unsigned int phase0;
  static unsigned int sig0;
  static unsigned char flag = 0;
  static unsigned int tempphase;
 
  if (soundPWM)
  {
    tempphase = phase0 + frequencyCoef;
    sig0 = wavetable[phase0>>8];
    phase0 = tempphase;
    OCR2A = sig0>>vol; // output the sample
  } 
  else { //square wave 
    flag ^= 1;
    digitalWrite(speakerPin, flag);
  }
}      
 
 
 
void setupPWMSound()
{
  // Set up Timer 2 to do pulse width modulation on the speaker pin.
  // Use internal clock (datasheet p.160)
  ASSR &= ~(_BV(EXCLK) | _BV(AS2));
  // Set fast PWM mode  (p.157)
  TCCR2A |= _BV(WGM21) | _BV(WGM20);
  TCCR2B &= ~_BV(WGM22);
  // Do non-inverting PWM on pin OC2A (p.155)
  // On the Arduino this is pin 11.
  TCCR2A = (TCCR2A | _BV(COM2A1)) & ~_BV(COM2A0);
  TCCR2A &= ~(_BV(COM2B1) | _BV(COM2B0));
  // No prescaler (p.158)
  TCCR2B = (TCCR2B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
  // Set initial pulse width to the first sample.
  OCR2A = 0;
  // Set up Timer 1 to send a sample every interrupt.
  cli();
  // Set CTC mode (Clear Timer on Compare Match) (p.133)
  // Have to set OCR1A *after*, otherwise it gets reset to 0!
  TCCR1B = (TCCR1B & ~_BV(WGM13)) | _BV(WGM12);
  TCCR1A = TCCR1A & ~(_BV(WGM11) | _BV(WGM10));
  // No prescaler (p.134)
  TCCR1B = (TCCR1B & ~(_BV(CS12) | _BV(CS11))) | _BV(CS10);
  // Set the compare register (OCR1A).
  // OCR1A is a 16-bit register, so we have to do this with
  // interrupts disabled to be safe.
  OCR1A = INTERRUPT_PERIOD;
  // Enable interrupt when TCNT1 == OCR1A (p.136)
  TIMSK1 |= _BV(OCIE1A);
  sei();
  soundPWM = true;
}
 
void startSound()
{
  // Enable interrupt when TCNT1 == OCR1A (p.136)  
  cli();
  TIMSK1 |= _BV(OCIE1A);
  sei();
  soundOn = true;
} 
 
void stopSound()
{
  cli();  
  // Disable playback per-sample interrupt.
  TIMSK1 &= ~_BV(OCIE1A);
  sei();
  soundOn = false;
}
 
void setFrequency(unsigned int freq)
{
  if (soundPWM) {
    unsigned long templong = freq;
    frequencyCoef = templong * 65536 / FS;
  } 
  else {
    unsigned long periode = F_CPU/(2*freq); //multiply by 2, because its only toggled once per cycle
    cli();
    OCR1A = periode;
  }
}
 
/*----------------------------------------------------
 functions to determine the wavetable content
 ----------------------------------------------------*/
void loadVoice(int voice)
{
  if(soundOn) // if sound is on
  {
    stopSound(); // turn sound off
  }
  switch (voice)
  {
  // sine
  case 0:
    sineWave();
    break;
  // sawtooth
  case 1:
    sawtoothWave();
    break;
  // triangle
  case 2:
    triangleWave();
    break;
  // square
  case 3:
    squareWave();
    break;
  }
  if(!soundPWM) 
  {
    setupPWMSound();
  }
  startSound(); // start sound again
}
 
void sineWave()
{
  for (int i = 0; i < 256; ++i) {
    wavetable[i] = pgm_read_byte_near(sinetable + i);
  }
}
 
void sawtoothWave()
{
  for (int i = 0; i < 256; ++i) {
    wavetable[i] = i; // sawtooth
  }
}
 
 
void triangleWave()
{
  for (int i = 0; i < 128; ++i) {
    wavetable[i] = i * 2;
  }
  int value = 255;
  for (int i = 128; i < 256; ++i) {
    wavetable[i] = value;
    value -= 2;
  }
}
 
void squareWave()
{
  for (int i = 0; i < 128; ++i) {
    wavetable[i] = 187;
  }
  for (int i = 128; i < 256; ++i) {
    wavetable[i] = 0;
  }
}
 
/*----------------------------------------------------
 setup and loop functions
 ----------------------------------------------------*/
int prevButtonValue = 0;
 
void setup()
{ 
   pinMode(vcc,OUTPUT);
 // pinMode(gnd,OUTPUT);
  digitalWrite(vcc,HIGH);
   USART_init();        //Call the USART initialization code
 sei();  // global interrupts
  pinMode(speakerPin, OUTPUT); 
  pinMode(buttonPin, INPUT); 
  // Choose one signal type to initially load into wavetable
  // 0 - sine
  // 1 - sawtooth
  // 2 - triangle
  // 3 - square  
  loadVoice(3);
} 
 
 
void loop() 
{
  // read all sensor values
  
  //read from potentiometer
 
 
 
 
}

int slider1read( int value1)   // read the Android GUI's slider 1 and return the value
{
 int valuesend=value; // read the serial
  if (valuesend !='q' && valuesend !='s')
  {
     
    value1=valuesend;
 
  }
  return value1;
}
int slider2read (int value2)   // read the Android GUI slider 2 and return the value
{
 int valuesend=value;// read the serial
if(valuesend <114 && valuesend !=113)
{
 value2=valuesend;
}
return value2;
  
}
int slider3read (int value3)   // read the Android slider 3 and return the value
{
 int valuesend=value;// read the serial
if(valuesend !=117)
{
 value3=valuesend;
}
return value3;
  
}

void USART_init(void){
 
 UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
 UBRR0L = (uint8_t)(BAUD_PRESCALLER);
 UCSR0B = (1<<RXEN0)|(1<<TXEN0) | (1 << RXCIE0);
 UCSR0C = (3<<UCSZ00);
  pinMode(13,OUTPUT);
}
 
unsigned char USART_receive(void){
 
 while(!(UCSR0A & (1<<RXC0)));
 return UDR0;
 
}
 
void USART_send( unsigned char data){
 
 while(!(UCSR0A & (1<<UDRE0)));
 UDR0 = data;
 
}
 
void USART_putstring(char* StringPtr){
 
while(*StringPtr != 0x00){
 USART_send(*StringPtr);
 StringPtr++;}
 
}
int USART_putInt(int intData)
{
 
intData=  USART_receive();
  USART_send(intData);

  return intData;
}
// USART interrupt

ISR (USART0_RX_vect)    //USART recieve character interrupt. Much faster than polling and no clicks. :)
{

value = UDR0; // Fetch the received byte value into the variable "value"
UDR0 = value;   //Put the value to UDR
switch(UDR0)   // switch case for the characters sent from Android
{
  case 'q':
 val2=slider1read(f);     // change the wave frequency according to slider 1 value
 freq = map(slider1read(f), 0, 50, 440, 1320); 
  setFrequency(freq);
  break;
  case 'r':
  vol=map(slider2read(f2),0,100,8,0);     // change the volume according to slider 2 value
  //vol=slider2read(f2);
  break;
  case 's' :
  
   currentVoice = (currentVoice + 1) %4;    // change the wave shape when "wave" button on GUI is pressed. Switches between sine, square, tri and saw
     loadVoice(currentVoice);
     break;
  case 'u':
  cutoff=slider3read(f3);    // TODO: work out low pass filter in code.
 break;
  case 't' :
 
  break;
  case 'v':

  break;
 
  default:
  
  break;
// aSin.setFreq(slider1read((value*10)+100)); 
}
}

void decay(void) {     // experimenting with ADSR decay but how????
  int i, j;
  for (j=0; j<128; j++) {
    for (i=0; i<STEPS; i++) {
      if (wavetable[i]<127) {
        wavetable[i]++;
      }
      if (wavetable[i]>127) {
        wavetable[i]--;
      }
    }
    delay(10);
  }
 
}

I am wondering if it would be possible to generate an ADSR (or envelope shape) by making another wavetable of values and SOMEHOW combining the envelope wavetable with the wave wavetable... is it possible?
Could someone point me in the right direction in implementing this sort of thing using the code I have already?
I would, as per usual be really really grateful.

Steve.

Some explanation here -

PM Me your email address and I will send the code, but basically you multiply the output by the ADSR and thats it.

Duane B

rcarduino.blogspot.com

Hi Duane & thanks! I love it! I'd love to see the code. How are you generating more than one sound at a time???
I think I get the †heory but I'd love to see it in practice. Thanks for letting me see your code. My address is:
defacato@gmail.com

All the best! I'll let you know what evolves.
Steve.

http://elm-chan.org/works/mxb/report.html

With the 8 bit series you can implement a linear, proper ADSR without many problems rule of thumb, unless you already pushing it far); With DUE, you can take into the realms of looping inside the sustain stage, exponential attack stage, and a few more quirks not so easy with the 8 bit series, specially if you're aiming at a more complex synth module.
As Duane said , usually its done by one multiplication against a value based on an accumulator to keep track of where the ADSR is or whatever way you might find to do this).
Use the proverbial ADSR graph as a guide for a clearer understanding, if you havent already !

  • Duane's posts are brilliant to guide you into the basic building blocks, plus saves you some hassle with the intricacies of the hardware.
    I have been doing some posts on it, several different ideas, some of the classic methods, etc but not much of a finished working code posted as yet( just exemplifying, more on a reading style not optimized as was written for better understanding of the concepts i was trying to demonstrate in each of them; kind of building blocks stages, one by one).
    I been trying to document as much as i can, so i can share it and hopefully help others save some time, where i didnt :slight_smile:
    http://dubworks.blogspot.co.uk/