pwm audio on atmega/arduino

Started by g_u_e_s_t, January 15, 2013, 12:18:22 AM

Previous topic - Next topic

g_u_e_s_t

i just finished a tutorial on getting the most out of your pwm, and thought it might be of use to the folks around here.  it starts with a basic overview of pwm and some of the tradeoffs:
http://www.openmusiclabs.com/learning/digital/pwm-dac/
then it goes into how to setup a dual pwm:
http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/
and finally there is arduino/atmeg328 sample code for building a dual pwm that takes in audio from the onboad adc and plays it back out timer1 as a dac.  stuff could easily be thrown in the middle there to make a low-fi effects peda
http://wiki.openmusiclabs.com/wiki/PWMDAC

and for those who really want to dig deep, there is a full analysis of where all the distortion comes from, and ways to minimize it:
http://www.openmusiclabs.com/learning/digital/pwm-dac/pwm-distortion-analysis/

alparent

#1
Thanks for doing this.
I'm new to microcontroler and the digital side of this world.
So far I've only used my Arduino to do fancy switching.
But this is going to be interesting.

I'm building a shield with 2 jacks that will fit on my Arduino board.

Now you will have to teach us what can be dune with this other then outputing the input signal  ???

I told you I know nothing! :icon_redface:

slacker

Nice tutorial, thanks for taking the time to do that. I think I'll need to read it through a few times to fully understand the dual PWM concept though.

alparent

OK, as you will soon enough find out, I don't know what I'm talking about.
So my question is. I'm building a shield for my Arduino. I'm going to use the Input stage on your "Example Arduino Sketch" page. But for the Output stage....should I use the Output stage in the same schematic or use the one on your "Dual PWM Circuits" page, the one designed by Steve Woodward?

Would chose on over the other Output stage change anything to the example sketch you provided?

Thank you for taking the time to educate us  :)

g_u_e_s_t

i would use the one on the arduino tutorial / wiki page.  the steve woodward circuit is cool, but its a bit overkill, and really isnt any better for frequencies above 1khz or so.  one more thing you will want to add, is a 10uF capacitor after the mixing resistors.  there is a DC bias on the signal as it comes out of the arduino, and putting a capacitor there will get rid of it.  a lot of mixers and amps have a DC blocking capacitor already in them, but this will be just in case.

im working on another tutorial geared exclusively towards making a low cost effects pedal out of the arduino, which will use this basic circuit, but have a few modifications to make things easier.  also, since the ADC is occupied taking samples, there will need to be either rotary encoders or buttons for getting input in (like adjusting lfo times and such).

alparent

Thanks
So I should wait a bit before making my shield?

g_u_e_s_t

ill try to have something up by tomorrow night.
i wanted to add a wet/dry mix knob and a feedback knob

alparent

Hey! Don't stress yourself out for me.

I'm just thankfull someone is taking the time to teach this. So I'll take what ever you give ......... when you give it.

Just don't want to make a nice shield and remake a new one in a week, if you know something else is comming.

Thanks again

FiveseveN

I don't have anything useful to add, just wanted to congratulate you on being featured on HaD  :icon_mrgreen:
Quote from: R.G. on July 31, 2018, 10:34:30 PMDoes the circuit sound better when oriented to magnetic north under a pyramid?

g_u_e_s_t

thanks FiveseveN

so i dont think ill have time to do the really nice version for a little while, so i put up a quick example schematic and code to get things started.  the button interface is a bit wonky, but otherwise it doesnt sound half bad.

http://wiki.openmusiclabs.com/wiki/MiniArDSP

right now its a very bare bones arduino library.  as it grows, i will probably hide more things in the library, so the main code doesnt have to have as much in it.

earthtonesaudio


artifus


earthtonesaudio

What determines the rate (in Hz) of the tremolo in the MiniArDSP code?

artifus

// mini_tremolo.pde
// minimalisitic tremelo program
// guest - openmusiclabs.com - 1.13.13
// takes in audio data from the ADC and plays it out on
// Timer1 PWM.  16b, Phase Correct, 31.25kHz.  applies a variable
// frequency tremolo to the sound.

#include "MiniArDSP.h"

#define PWM_FREQ 0x00FF // pwm frequency - see table
#define PWM_MODE 0 // Fast (1) or Phase Correct (0)
#define PWM_QTY 2 // number of pwms, either 1 or 2

// create sinewave lookup table
// PROGMEM stores the values in the program memory
// it is automatically included with MiniArDSP.h
PROGMEM  prog_int16_t sinewave[]  = {
  // this file is stored in MiniArDSP and is a 1024 value
  // sinewave lookup table of signed 16bit integers
  // you can replace it with your own waveform if you like
  #include <sinetable.inc>
};
unsigned int location; // lookup table value location

byte rate = 5; // tremolo rate
byte rate_counter; // modifiable version of rate
unsigned int amplitude; // current tremolo amplitude
unsigned int button;

void setup() {
  // setup button pins
  PORTD |= 0x0c; // turn on pullups for pins 2 and 3
 
  // setup ADC
  ADMUX = 0x60; // left adjust, adc0, internal vcc
  ADCSRA = 0xe5; // turn on adc, ck/32, auto trigger
  ADCSRB =0x07; // t1 capture for trigger
  DIDR0 = 0x01; // turn off digital inputs for adc0
 
  // setup PWM
  TCCR1A = (((PWM_QTY - 1) << 5) | 0x80 | (PWM_MODE << 1)); //
  TCCR1B = ((PWM_MODE << 3) | 0x11); // ck/1
  TIMSK1 = 0x20; // interrupt on capture interrupt
  ICR1H = (PWM_FREQ >> 8);
  ICR1L = (PWM_FREQ & 0xff);
  DDRB |= ((PWM_QTY << 1) | 0x02); // turn on outputs
 
  TIMSK0 = 0; // turn of t0 - no delay() or millis()
  sei(); // turn on interrupts - not really necessary with arduino
}

void loop() {
  while(1); // gets rid of jitter
  // nothing happens up here.  if you want to put code up here
  // get rid of the ISR_NAKED and the reti(); below
}

ISR(TIMER1_CAPT_vect, ISR_NAKED) { // ISR_NAKED is used to save
// clock cycles, but prohibits code in the loop() section.
 
  // get ADC data
  byte temp1 = ADCL; // you need to fetch the low byte first
  byte temp2 = ADCH; // yes it needs to be done this way
  int input = (temp2 << 8) | temp1; // make a signed 16b value
 
  button--; // check buttons every so often
  if (button == 0) {
    byte temp3 = PIND & 0x0c; // get pin 2 and 3 values
    if (temp3 == 0) { // both buttons pressed
    } // do nothing
    else if (temp3 == 0x08) { // up button pressed
      if (rate == 1); // do nothing
      else rate--; // make tremolo faster
    }
    else if (temp3 == 0x04) { // down button pressed
      if (rate == 255); // do nothing
      else rate++; // make tremolo slower
    }
    button = 0x0400; // reset counter
  }

  rate_counter--; // decrement our counter
  if (rate_counter == 0) {
    // create a variable frequency and amplitude sinewave
    // fetch a sample from the lookup table
    amplitude = pgm_read_word_near(sinewave + location) + 0x8000;
    // the + 0x8000 turns the signed value to unsigned
    location++;
    // if weve gone over the table boundary -> loop back
    // around to the other side.
    location &= 0x03ff; // fast way of doing rollover for 2^n numbers
    rate_counter = rate; // reset rate counter
  }
 

  // multiply our input by the sinewave value
  // note the use of the special multiply macro
  // this is faster than the normal * method
  // this is a signed * unsigned, returning the high 16 bits
  int output; // define variable before using it
  MultiSU16X16toH16(output, input, amplitude);
 
  // output data
  OCR1AL = (output + 0x8000) >> 8; // convert to unsigned
  // and only output the top byte
  OCR1BL = output; // output the bottom byte
  reti(); // return from interrupt - required because of ISR_NAKED
}

SISKO

I think that you would have to sum all the instruction from line 58 to 109 (including those in the MultiSU16X16toH16 macro) and multiply them by the instruction cycle period to get the time it takes the code to finish one loop.
In every loop, the counter gets decremented untill it gets to zero.
Then based on the value of rate_counter you can do some more math to get your freq.

So if rate_counter = 255 and *supose* the time it takes to finish one loop is 1ms, then the period of the tremolo is 255*1ms = 0.255ms -> 4Hz

Keep in mind the some macros as the ones that gets the byte from the adc may or may not consume several cycles.

Hope it helps!
--Is there any body out there??--

earthtonesaudio

@sisko: That was my first guess, but I also was wondering if the capture/compare interrupt had something to do with the timing.

Specifically these parts:



  TIMSK1 = 0x20; // interrupt on capture interrupt
  ICR1H = (PWM_FREQ >> 8);
  ICR1L = (PWM_FREQ & 0xff);

g_u_e_s_t

everything is driven off the timer1 interrupt.  in the setup routine, the ADC is configured to take a sample everytime the timer1 capture event happens, so this just happens in the background, which makes everything easier and faster.  timer1 is set to interrput every 256*2 clock cycles.  the 2 comes from it running in phase correct mode, so it has to count up, and then back down for a full cycle.  since the arduino runs at 16MHz, this gives 16MHz/512 = 31.25kHz interrupt rate.  so this is the ADC sample rate, the PWM data playback rate, and the data processing rate.

in the timer1 interrupt, the data gets processed.  each time the interrupt happens, rate_counter gets decremented, and when it equals 0, the amplitude is modified to the next level of the LFO.  the LFO is stored as a 1k lookup table.  so, for rate = 1, it would take 31.25kHz/1k samples = 31.25Hz to go through the whole lookup table, and get a single cycle.  for rate = 2, this would take twice as long, and so forth.  in the example, rate is initially set to 5, so that gives 31.25Hz/5 = 6.2Hz.  so, as its currently setup, its good for longer LFO times, but starts to loose frequency resolution as you get above a few Hz.

earthtonesaudio


g_u_e_s_t

i just realized there might be a mistake in the code.  i was messing around with it tonight, and i had to change this line:

int input = (temp2 << 8) | temp1; // make a signed 16b value
to
int input = ((temp2 << 8) | temp1) + 0x8000; // make a signed 16b value

i also changed a bunch of other stuff, so who knows.  but if its giving you trouble, try making that change.  the first line doesnt actually give a signed value, so it probably was a mistake.  the second line does make a signed value

earthtonesaudio

One more question:
If I set the timer1 frequency as in the example, will later calls to analogWrite() be at that frequency?