Synth ADSR Envelope Generation

Go To Last Post
12 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I am working on a project to make a Mini-Synth. I am using ATmega32. Nothing too fancy. It should play one Octave( C4 to C5 ). Basic waveforms like Sine, Square, Triangular, Sawtooth with select buttons.

Hardware was pretty simple. Just set up a DAC: R2R and PWM. Settled with PWM (could do with the other as well) on TIMER1 in CTC mode, generating an interrupt every 1 by 8000th of a second (Fs: 8000 KHz). So at 16Mhz and Prescalar of 64, OCR1A = 30.

I found two ways of playing the notes.

1. From Flash Program Memory LUT
2. Generating the table dynamically and storing in SRAM

Obviously, SRAM is not a good idea. So I implemented the first one for a Sine Wave. All good and working well, should be easy for the other waveforms. Tried the second method and that worked as well. Two versions. So now I have a basic DDS Generator Player for one octave ( can be extended for others) but not a synth!

There are many ways to synthesize :

1. Karplus-Strong
2. Additive Synthesis
3. Subtractive Synthesis,
4. Frequency Modulation

https://ccrma.stanford.edu/~sdill/220A-project/drums.html

Instead of using the various methods I decided to generate a basic "amplitude" envelope over the notes I was playing. One simple way is to have an ADSR envelope: Attack, Decay, Sustain, Release.

http://en.wikiaudio.org/ADSR_envelope

So I started with the first method i.e. LUT. The best way to change the amplitude for example in the Attack time ( ramp signal ) would be to multiply the LUT sample value with ( CountTime / AttackTime ) where CountTime can be every ms ( Attack time also in ms ). The problem I am facing is that this calculation (in float) in the ISR slows down the time and I get lot of noise or nothing. What is the best way to do the computation?

I know the ISR should be free from heavy calculations and should be quick but then how do I generate the envelope?

Sine Wave with LUT

/* ---------------------------------------------------------------------
                        MAIN
------------------------------------------------------------------------ */

int main(void)
{

    Init_Ports();    // Initialize PORTS
    Init_Timer();    // Start Timer
    Init_PWM();      // Start PWM
    sei();           // Global interrupt enable

#ifdef LCD_USE
    lcd_init(LCD_DISP_ON);  //Initialize the LCD
    lcd_clrscr();
#endif

    while(1){

        note = GetKeyPressed();  // Check key Pressed

        if( note != NOT_PRSD ){

            soundlen = pgm_read_word( &SineLen[note] ); // Get One Cycle Sample Count
            NotePtr = (uint8_t*) pgm_read_word( &SineNotes[note] ); // Point to Note
        }

        else{
            soundlen = 0;
            MilliCount = 0;
        }

#ifdef LCD_USE
        lcd_gotoxy(0,0);
        lcd_put_int();
#endif

	}


    return 0;
}

/*--------------------------------------------------------------
                  FUNCTIONS, ISR DEFINITIONS
---------------------------------------------------------------*/

ISR( TIMER1_COMPA_vect )
{
    IsrCount++;

    if( ( IsrCount >= 8 ) ){
        IsrCount = 0;
        MilliCount++;
    }

    if( sample >= soundlen ){           // Reset to zero the Sample Sequence
        sample = 0;
    }

    amp = pgm_read_word( NotePtr + sample );
    sample++;

    if( MilliCount < Attack ){

        amp  = (uint8_t) ( (float)amp * ( (float) MilliCount / (float) Attack ) ); 
    }

    else if( MilliCount > Attack ){
        MilliCount = Attack + 1; //Just to stay out
    }

    //Set Duty
    SetDuty( amp );
}
}

Generating the Table in SRAM: Very heavy and messy. I would avoid this method if the the above works.

/* ---------------------------------------------------------------------
                        MAIN
------------------------------------------------------------------------ */

int main(void)
{

    Init_Ports();    // Initialize PORTS
    Init_Timer();    // Start Timer
    Init_PWM();      // Start PWM
    sei();           // Global interrupt enable

#ifdef LCD_USE
    lcd_init(LCD_DISP_ON);  //Initialize the LCD
    lcd_clrscr();
#endif

    while(1){

        note = GetKeyPressed();  // Check key Pressed


        // Key Pressed for First Time
        if( ( note != NOT_PRSD ) && ( Gen_State == YES ) && (CurNote!= note) ){

            CurNote = note;
            GenerateWave( GetWaveSel() , note ); // Generate Samples
            MilliCount = 0;
            continue;
        }

        // When Key is kept pressed
        else if( ( note != NOT_PRSD ) && ( Gen_State == NO ) ){

           if( CurNote != note ){
               Gen_State = YES;
            }
        }

        // When Key is released
        else if( ( note == NOT_PRSD ) && ( Gen_State == NO ) ){
            Gen_State = YES;
            CurNote = note;
            MilliCount = Attack_time + 1;
        }

        // Other Cases
        else{

            soundlen = 0;
            MilliCount = Attack_time + 1;

        }

#ifdef LCD_USE
        lcd_gotoxy(0,0);
        lcd_put_int();
#endif

	}


    return 0;
}

/*--------------------------------------------------------------
                  FUNCTIONS, ISR DEFINITIONS
---------------------------------------------------------------*/

ISR( TIMER1_COMPA_vect )
{
    IsrCount++;  // Count ISR Visits

    // Sound Reproduction
    if( sample > soundlen ){ // Reset to zero the Sample Pointer
        sample = 0;
    }

    amp = y[sample];  // Get Sample

    /* ISR */
    if( ( IsrCount > 8 ) ){ // Count Milliseconds

        IsrCount = 0; // Reset ISR Count
        MilliCount++; // Increment Millisecond count

        // DEBUG
        if( (MilliCount < Attack_time) ) {

            SET( PORT_DEBUG, 1<<PIN_DEBUG );
        }

        else if (MilliCount > Attack_time ) {

            MilliCount = Attack_time+1;
            CLR( PORT_DEBUG, 1<< PIN_DEBUG );
        }
    }

    SetDuty( amp );    // Set Duty
    sample++;
}

void GenerateWave( enum WAVE var_wave, enum KEY var_key )
{

    uint16_t i, len;
    float SampleCount, f0;

    f0 = (float) var_key;
    SampleCount = F_SAMPLING / f0 ;
    len = (uint16_t) SampleCount;   //
    soundlen = len;                 // Length of Sine Table

    // Generate SINE WAVE
    if( (var_wave == SINE) ){

        for( i = 0; i
Last Edited: Sun. Mar 25, 2012 - 12:02 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Will your synth be polyphonic? You have time to look up 2 or 3 notes and add them up. I have done volume control by rightshift. 6 db per bit. You can take it down 36 db in 6 steps. And its logarithmic just like you want.

Imagecraft compiler user

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm taking it one step at time. Monophonic with ADSR Controls and then I can add the notes for polyphonic. There a lot of other things I can add as well: LFO, the other algorithms. Completion of each gives confidence to the next. Some questions pop-up. Adding two or three 8-bit values, I might have to increase the resolution to 10-bit.

For the amplitude control, are you suggesting:

amp = ( amp >> step_down ) 

and I can get a divide by 2 at every step ( roughly ). I am assuming that this is a lot faster than using a heavy floating point division.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Everytime a music synthesizer project appears here I am obliged to repeat the same advice.

Before spending tens of hours and $50 on parts for a simple sine-wave synth, go to eBay's section of musical instruments:synthesizers and search for any used tone modules selling (nearing the end of the bid period) for $80 or less.
For example, an EMu Proteus 2000 from 1998, 128 voices, 50 12-pole programmable filters, 1028 samples of real instruments, on and on, with a general bid range of $85 to $125. And you can ALWAYs resell it for the price that you paid for it.
Any used commercial synth that you buy there will be miles above in music creating potential than anything that you could design with an AVR.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I wrote an Arduino demo a little while ago that might be of use (YouTube video here). It's the ending song to the game 'Portal'. It's got 2-channel music that uses wave-tables and envelopes. Here's the code: http://www.batsocks.co.uk/downloads/tms_StillAlive_003.ino

I've got the waveforms and envelopes defined in flash memory.
The classes EnvelopeState, WaveformState and Channel are probably worth looking at (at least for ideas), as well as the interrupt handler.

I used fixed point (8:8) maths for the amplitude control. It's fast and gives a reasonable result.

ISR( TIMER1_OVF_vect ){
  // Get the waveform values...
  int8_t A = channel1.getWaveformValue() ;
  int8_t B = channel2.getWaveformValue() ;

  // and now scale it by the volume according to the envelope...
  A = ((A * channel1.getVolume()) >> 8) & 0xff ;
  B = ((B * channel2.getVolume()) >> 8) & 0xff ;
  
  // and finally mix the results into OCR2A
  // (which controls the fast PWM output on pin 11)
  OCR2A = 128 + (A + B) ;

  channel1.tick();  
  channel2.tick();
};

I cheated with the channel mixing - all my waveforms are half volume, so I can just add them.

Cost of parts: 1 Arduino.
Value of music created with it: possibly negative.
Value of knowing I made it myself: Priceless.

(I do understand Simonetta's point though. Do you want to make music, or do you want to make a musical instrument?)

Nigel Batten
www.batsocks.co.uk

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
Any used commercial synth that you buy there will be miles above in music creating potential than anything that you could design with an AVR.

Yes, true. But we are using AVR.

Quote:
Do you want to make music, or do you want to make a musical instrument?

Music comes first. Thanks for sharing your code. Trying the 8:8 fixed point way. Should work.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Can you use Bresenham's integer line algorithm for ADSR? It's very simple and fast.

The Dark Boxes are coming.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

When I programmed my polyphonic module tracker player on AVR I just multiplied signed 8-bit samples by unsigned 0-64 volume and got 13-bit sample. Then I add 4 channels together and got full 16-bit resolution for an I2S stereo DAC. No shifting needed.
Resampling of module waveforms was done in fixed point simply by adding 8.8 resampling coeff. to sample address and of course including linear interpolation since it's just following sample load + 6 cycles in 8-bit resolution.
Amplitude and frequency ramps were made by periodical incrementation/decrementation of volume byte and resampling coeff. every tick (0.02s for 125bpm MOD I think).
To ensure good sound quality audio data were calculated in main loop and stored into FIFO. Only I2S audio output was placed in ISR so it had almost equidistant sampling no matter how long every sample is calculated. This also allowed usage of slow division routines and another time consuming operations in generator code.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The trick is not to use floating point at all, just fixed point.

To get rid of multiplications, real FM synth chips like Yamaha OPL series do not use multiplication or division at all. Multiplication and division are just simple sum and addition operations when values are logarithms. An exponential table is used when converting to PCM before output.

But AVRs have hardware multiplier so having an exponential table might not be worth it.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Finally, some headway. Got the Decay and Sustain working. It sounds a lot better( only relatively: 8-bit! ). So, now I can work on getting the remaining envelopes(A & R). Should be simple enough. The next stage i'll put POT's to the ADC and vary the envelope times.

On polyphonic:

Using TIMER1, so I can set ICR1 upto 16-bit TOP; as of now on 8-bit FAST PWM. Issue is that PWM frequency = 16Mhz / ( 1 + TOP) * N. Prescalar N=1, TOP if 255( for 8 bit ) gives me f(PWM) = 62.5 KHz. Ok, satisfying Nyquist(Fs = 8 Khz ). If I add two/three notes I should take 10-bit minimum ( ICR1 = 0x3FF ). This time f(PWM) = 15.62 Khz and with my sampling frequency brings aliasing. So I should reduce my sampling rate to 4Khz? Or weirdly I can mix the amplitude from two pins OC1A and OC1B?

Quote:
Can you use Bresenham's integer line algorithm for ADSR? It's very simple and fast.

I will check it up. Don't have much idea on that front.

What is working till now:

/* ---------------------------------------------------------------------
                        MAIN
------------------------------------------------------------------------ */

int main(void)
{

    Init_Ports();    // Initialize PORTS
    Init_Timer();    // Start Timer
    Init_PWM();      // Start PWM
    sei();           // Global interrupt enable

#ifdef LCD_USE
    lcd_init(LCD_DISP_ON);  //Initialize the LCD
    lcd_clrscr();
#endif

    while(1){

        note = GetKeyPressed();  // Check key Pressed

        if( (note != NOT_PRSD) ){

            soundlen = pgm_read_word( &SineLen[note] ); // Get length of LUT
            NotePtr = (uint8_t*) pgm_read_word( &SineNotes[note] ); // Point to LUT

        }

        else{
            soundlen = 0; // LUT length reset
            sample = 0;   // Sample pointer reset
            Decay_shift = 0;   // Decay reset
        }

#ifdef LCD_USE
        lcd_gotoxy(0,0);
        lcd_put_int();
#endif

	}


    return 0;
}

/*--------------------------------------------------------------
                  FUNCTIONS, ISR DEFINITIONS
---------------------------------------------------------------*/

ISR( TIMER0_COMP_vect )
{


    IsrCount++; // 1/8000 sec ticker

    // Reset Sample Count if reaches end
    if( sample > soundlen ){
        sample = 0;
    }

    Amp = pgm_read_word( NotePtr + sample ); // Read Sample Amplitude

    // Count ms: 1 MilliCount = 8 * IsrCount
    if( IsrCount > 8 ){

        IsrCount = 0;
        MilliCount++;
    }

    // Check Decay Time: Decay_time = Decay_tick * ( 8 - Sustain_amp )
    if( MilliCount == Decay_tick ){

        TOG( PORTD, 1<<6 ); // DEBUG LED
        MilliCount = 0;     // Reset ms count

        // Shift val increment
        if( Decay_shift < ( 8 - Sustain_amp ) ){
            Decay_shift++;
        }
    }

    // Play if note Pressed
    if( note != NOT_PRSD ){

        Amp = ( Amp >> Decay_shift ) ; // Decay
        SetAmp( Amp ); // Speaker OUT
        sample++;      // Next Sample

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I don't know C.

And I know even less about music than I do about C.

That said, The April 2012, Nuts & Volts magazine has an Arduino based project for a DDS based, polyphonic, (3 voices), music player with ADSR envelope control.

You may wish to compare methodologies.

JC

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

There is one aspect of sound generation that is a natural for the AVR but I have never seen discussed anywhere.

When you put an electric guitar signal into a fuzz tone, the output waveform is a pulse-width modulated square wave. A very complex PWM wave. The fuzz box first amplifies the signal (using a 20x-50x gain op-amp) so that distant harmonics of the guitar's waveform are brought forward. Then the signal is clipped at a low line-level amplitude (about 0.5 to 0.7 volts peak-to-peak). Finally the pulse wave is fed into a tone-control to round off the rising and falling edges of the pulse-wave and make the final sound less 'fizzy'.

The AVR could use its comparator to directly input the guitar signal. Set one input at 1/2 Vcc and the other at 1/2 Vcc + 0.02V. Then feed the guitar (through a capacitor) into the 1/2 Vcc terminal. The comparator will change state with the fundamental and first few harmonics. Then measure the time length of each high and low state. Output on a different pin waveforms that are mathematically related to these state measurements. Scale the outputs down to 1v peak-to-peak and feed them a guitar amp.

You get very strange sounds that directly follow the notes played on the guitar. From a chip that costs less than a dollar. Several guitar effects boxes use the similar waveshapes between the fuzz output and the CPU serial port. For example the BOSS Feedbacker/Distortion and most Octave Divider boxes. But there is much to explore in this area without having to mess with envelopes, filters, and synthetic waveshapes.