Complete: Arduino Tremolo w/ tap tempo, expr pedal

Started by harmless, January 15, 2012, 04:37:00 PM

Previous topic - Next topic

harmless

With many thanks to all the helpful people on this forum, I'm happy to say that I've finished my Arduino Tremolo.  This is the first build I've done where I didn't just download a vero, though nearly all of the schematic is borrowed from the PIC based trem done by .Mike here http://www.diystompboxes.com/smfforum/index.php?topic=77471.0.  I've simplified a few things (.Mike's build is a bit more long term bulletproof) and used a vactrol instead of an LED/LDR pair.  I also added expression pedal support.  If an expression pedal is detected at power on, the first knob you turn gets bound to the expression pedal instead.   This works for Wave, Rate, and Depth, but the Gain knob is in the analog path so it doesn't work on that one.

I've got wave shapes for sine, square, triangle, ramp up, and ramp down as well as 2 kill switch modes (on unless tapped and off unless tapped).

I got a pre poweder coated enclosure from Mammoth that was already drilled but I had to drill a hole for the expression pedal jack (and also use a mini stereo jack just to get it to fit).  As suggested here, slow drill speed, pilot hole and then unibit worked fantastically.  The expression pedal jack is a closed circuit jack with the ring shunt wired through a pull down resistor to the Arduino for detection.

I used a full face water slide decal for the graphics (bought directly from papilio).  The laser printed stuff doesn't even need to be clear coated before applying, just print and go.  I put a few coats of rustoleum laquer clear coat on top and it looks great.  I had no bubbles until I put the LED bezels in and then when tightening one or two tiny ones showed up, but not bad for my first attempt.

I opted to use a 5v Arduino Pro Mini inside the box rather than laying out all the Arduino components on my vero to save time.  If I were doing a PCB I wouldn't take this approach.  I put a 6 pin header on the pro mini and took care to leave it accessible so I can upgrade the software later if I want.

Here's the vero (the numbers match the schematic of .Mike's):


Here's a few pics:




Here's the Arduino code...please take and enjoy and use (if you find it helpful give a bit of credit)
The code is usable but not tuned right now....I alternate polling the analog pots and have limited how often that happens because I was worried about analog read times.  I think simply going round robin and reading one per loop cycle is fine and the artificial delay on reading can be removed.   The depth range of values could probably be tweaked too.  It's linear but in practice it'd probably be more useful to fade from full depth to some partial but noticeable value because as it is right now the lower values are useless since the trem is so subtle as to be imperceptible.

//arduino tremolo v1.0
//harmless machines
//
//feel free to use this code, but please credit if you do thanks

const float twoPi = 2.0 * 3.141592654;

const int outPin = 10;
const int ledPin = 13;
const int periodPin = A0;
const int depthPin = A1;
const int wavePin = A2;
const int exprPin = A3;
const int exprDetectPin = 11;

const int tapPin = 2; //interrupt 0 is pin 2

const int minPeriod = 10;
const int maxPeriod = 1000;

const int minDepth = 255; //255 = full off

const int blinkDivisor = 10;

const int tapDown = 1;
const int tapUp = 0;

const int potCount = 3;
const int waveCount = 7;

volatile int tapState = tapUp;

int waveform = -10; //must be negative enough for 0 to cross all change thresholds
int period = -1;
int rawPeriod = -10;
int depth = -10;

unsigned long firstPeriod = 0;
unsigned int blinkDuration = 0;
unsigned int halfPeriod = 0;

boolean exprPedal = false;
boolean firstChange = true;
int replacedByExpr = -1;

//waveform
//period
//depth
const int potPins[potCount] = { wavePin, periodPin, depthPin }; //hack for expr pedal
int* pots[potCount] = { &waveform, &rawPeriod, &depth };
int (*potReaders[potCount])(void) = { &readWaveform, &readRawPeriod, &readDepth };
void (*potChanged[potCount])(int,int,unsigned long,unsigned int) =  { &waveformChanged, &rawPeriodChanged, &nop };
const int changeThresholds[potCount] = { 0, 1, 1 }; //0 = no wiggle allowed

void nop(int oldVal, int newVal, unsigned long currentMillis, unsigned int offset) { }

int readAnalogPot(int pin) {
 int pinToRead = pin;
 if(exprPedal && (pin == replacedByExpr)) {
   pinToRead = exprPin;
 }
 return analogRead(pinToRead);
}

int readWaveform() { return map(readAnalogPot(wavePin), 0, 1023, 0, waveCount - 1); }
int readRawPeriod() { return map(readAnalogPot(periodPin), 0, 1023, minPeriod, maxPeriod); }
int readDepth() { return map(readAnalogPot(depthPin), 0, 1023, minDepth, 0);}

void waveformChanged(int oldVal, int newVal, unsigned long currentMillis, unsigned int offset) {
 if(newVal >= 5) {
   attachInterrupt(0, changeTapState, CHANGE);
 } else {
   attachInterrupt(0, tapped, RISING);
 }
}

void setPeriod(int newVal, unsigned long currentMillis, unsigned int offset) {
 int oldVal = period;  //can't use old raw value as current period may be tapped

 //adjust first period so that we are the same % of the way through the period
 //so there aren't any jarring transitions
 int effectiveNewOffset = map(offset, 0, oldVal - 1, 0, newVal - 1);
 firstPeriod = currentMillis - effectiveNewOffset;
 
 blinkDuration = newVal / blinkDivisor;
 halfPeriod = newVal / 2;
 
 period = newVal; //override any tapped period  
}

void rawPeriodChanged(int oldVal, int newVal, unsigned long currentMillis, unsigned int offset) {
 //oldVal is old raw val and is only useful to detect that the raw period has changed
 //moving the knob means override tapped period and force set to knob value
 setPeriod(newVal, currentMillis, offset);
}

//sin, square, tri, ramp up, ramp down, play if down, play if up
int (*waveFns[waveCount])(unsigned int) = { &waveSin, &waveSquare, &waveTri, &waveRampUp, &waveRampDown, &waveIfDown, &waveIfUp };

//actually cos so we're on at period start
int waveSin(unsigned int offset) {
 float rads = ((float)offset / (float)period) * twoPi;
 float amplitude = (255.0 - depth);
 return constrain((cos(rads) + 1.0) * amplitude / 2.0 + depth, 0, 255); //cap at 255 instead of 256
}

int waveSquare(unsigned int offset) {
 if(offset < halfPeriod) return 255;
 else return depth;
 
}

int waveTri(unsigned int offset) {
 if(offset < halfPeriod) return map(offset, 0, halfPeriod, 255, depth);
 else return map(offset, halfPeriod + 1, period - 1, depth, 255);
}

int waveRampUp(unsigned int offset) {
 return map(offset, 0, period - 1, depth, 255);
}

int waveRampDown(unsigned int offset) {
 return map(offset, 0, period - 1, 255, depth);
}

int waveIf(int tapIf) {
 if(tapState == tapIf) return 255;
 else return 0;
}

int waveIfDown(unsigned int offset) {
 return waveIf(tapDown);  
}

int waveIfUp(unsigned int offset) {
 return waveIf(tapUp);
}

volatile boolean tapPending = false;
volatile int tappedPeriod = -1;
const int maxTaps = 5;
int tapCount = 0;
int nextTap = 0;
int taps[maxTaps] = { 0, 0, 0, 0, 0 };
unsigned long lastTap = -maxPeriod;

void tapped() {
 unsigned long current = millis();
 if(current - lastTap > (2 * maxPeriod)) {
   tapCount = 0;
   nextTap = 0;
 } else {
   taps[nextTap] = current - lastTap;    
   nextTap = (nextTap + 1) % maxTaps;
   tapCount = constrain(tapCount + 1, 0, maxTaps);    
 }  
 lastTap = current;
 
 if(tapCount > 0) {
   int total = 0;
   for(int i = 0; i < tapCount; i++) {
     int tapIndex = nextTap - 1 - i;
     if(tapIndex < 0) tapIndex += maxTaps;
     total += taps[tapIndex];
   }
   tapPending = true;
   tappedPeriod = total / tapCount;
 }
}

void changeTapState() {
 if(tapState == tapUp) tapState = tapDown;
 else tapState = tapUp;
}

void setup(){
//  Serial.begin(9600);
 pinMode(outPin, OUTPUT);
 pinMode(ledPin, OUTPUT);
 pinMode(periodPin, INPUT);
 pinMode(depthPin, INPUT);
 pinMode(wavePin, INPUT);
 pinMode(exprDetectPin, INPUT);
 pinMode(exprPin, INPUT);

 //set prescaler for pin 10 to 1 for fastest pwm freq
 TCCR1B = _BV(CS10);
   
 unsigned long current = millis();
 firstPeriod = current;
 for(int i=0;i<potCount;i++) updatePot(i, current, 0);
 
 exprPedal = digitalRead(exprDetectPin) == LOW;
}

int statusLedValue(unsigned int offset) {
 if(offset < blinkDuration) return HIGH;
 else return LOW;
}

void digitalWriteIfChanged(int pin, int *lastValue, int newValue) {
 if(*lastValue != newValue) {
   digitalWrite(pin, newValue);
   *lastValue = newValue;
 }
}

void analogWriteIfChanged(int pin, int *lastValue, int newValue) {
 if(*lastValue != newValue) {
   analogWrite(pin, newValue);
   *lastValue = newValue;
 }
}

int nextPot = 0;
unsigned long lastPotRead = millis();
unsigned int readEvery = 200;

boolean updatePot(int pot, unsigned long currentMillis, unsigned int offset) {
 int newVal = (*potReaders[pot])();
 int oldVal = *pots[pot];
 int delta = oldVal - newVal;
 if(abs(delta) > changeThresholds[pot]) {
   (*potChanged[pot])(oldVal, newVal, currentMillis, offset);
//    Serial.print(pot);Serial.print(" changed from ");Serial.print(oldVal);Serial.print(" to ");Serial.println(newVal);
   *pots[pot] = newVal;
   return true;
 }else return false;
}

void updatePots(unsigned long currentMillis, unsigned int offset) {
 if(currentMillis > lastPotRead + readEvery) {
   boolean updated = updatePot(nextPot, currentMillis, offset);
   if(exprPedal && firstChange && updated) {
     firstChange = false;
     replacedByExpr = potPins[nextPot];
//      Serial.print("Expr pedal is assigned to pot ");Serial.println(nextPot);
   }
   nextPot = (nextPot + 1) % potCount; //don't keep reading the same pot if it's changing - expression pedals wobble and thus other pots wont ever get polled
   lastPotRead = currentMillis;
 }  
}

int lastLedValue = LOW;
int lastOutputValue = -1;

void loop() {    
 unsigned long currentMillis = millis();
 unsigned int offset = (currentMillis - firstPeriod) % period;

 digitalWriteIfChanged(ledPin, &lastLedValue, statusLedValue(offset));
 analogWriteIfChanged(outPin, &lastOutputValue, (*waveFns[waveform])(offset));
 updatePots(currentMillis, offset);
 
 if(tapPending) {
   setPeriod(tappedPeriod, currentMillis, offset);
   tapPending = false;
 }
}


Again thanks to everyone on this forum who helped me out (especially R.G. who helped me figure out that the PWM frequency was causing the whine the pedal had at first).  Hopefully this is first of many :)

nexekho

If you avoid using analogRead and handle the ADC manually you can perform operations while the ADC works.  Have a look at the source code for analogRead() - it should be a matter of taking everything from it, and putting your code in the busy wait loop if you don't fancy reading up on the full workings of the ADC; IIRC it waits for some flag in ADCSRA.
I made the transistor angry.

harmless

Quote from: nexekho on January 15, 2012, 06:11:44 PM
If you avoid using analogRead and handle the ADC manually you can perform operations while the ADC works.  Have a look at the source code for analogRead() - it should be a matter of taking everything from it, and putting your code in the busy wait loop if you don't fancy reading up on the full workings of the ADC; IIRC it waits for some flag in ADCSRA.

Thanks.  I think my read worries were way overblown, considering how little processing it takes to generate an LFO wave.

That will come in handy for my next project though where I'm planning to put the Arduino in the signal path.

RonaldB

Looking good.

Thanks for sharing this project.

RonaldB

egasimus

Cool! Now, do it in ASM using a single ATtiny, and put your Arduino to better use.  ;)

earthtonesaudio

Quote from: egasimus on January 16, 2012, 02:51:33 AM
Cool! Now, do it in ASM using a single ATtiny, and put your Arduino to better use.  ;)

I'm just now learning ASM and Arduino, and this is something I would like to learn more about.  Is this sort of info on the Arduino forums?

musimedia

I know this is an old thread.. but I have a little question.

I see on the veroboard layout that the PWM output from Arduino is not going straight to the LED but to a transistor first then to the LED. Does this take care of removing the PWM noise on the audio side ?

Thanks a bunch!
Mike.




harmless

Quote from: musimedia on December 18, 2012, 12:43:27 PM
I know this is an old thread.. but I have a little question.

I see on the veroboard layout that the PWM output from Arduino is not going straight to the LED but to a transistor first then to the LED. Does this take care of removing the PWM noise on the audio side ?

Thanks a bunch!
Mike.


It's been a while, but I don't think it's noise related (more in a sec).  It's just a basic LED driver so that we don't have to pull all the current from the arduino output pin (which has some limit on the amount of current it can source).

Noise wise it doesn't even matter since the PWM is never in the signal path (the LED/LDR totally isolates one side of the circuit from the other).  We don't even need an LPF to get rid of the PWM carrier frequency because the reaction time of the LDR naturally smooths things for us.  What is important for noise is grounding.  It took quite a while and a few ground reroutes to remove the noise from the pedal.  A suggestion I've seen since then is to ground the signal path to the jacks and then have a single connection back to ground at the power supply.  Then have a single connection to the power supply feeding the digital ground.

musimedia

Thanks for your reply.

I have a LED/LDR circuit (not a tremolo) on a bredboard with arduino taking care of the LED part,  the PWM signal is louder than the guitar signal!  I'll try boxing it up properly after the holidays to see if it helps.


Cheers!
Mike




harmless

Quote from: musimedia on December 19, 2012, 11:10:09 AM
Thanks for your reply.

I have a LED/LDR circuit (not a tremolo) on a bredboard with arduino taking care of the LED part,  the PWM signal is louder than the guitar signal!  I'll try boxing it up properly after the holidays to see if it helps.


Cheers!
Mike

The default arduino PWM frequency is something stupidly low like 440Hz (ie, nicely in the audio range).  You can set something to increase it to something in the MHz range which will help a lot.

From my code:
//set prescaler for pin 10 to 1 for fastest pwm freq
  TCCR1B = _BV(CS10);

I was using pin 10.  If you're using a different pin the code will be different.

gittela

Great idea!
I've built it, but omitted the expression pedal as I didn't need it. It sounds good and is being used every week at rehearsal.
Could you upload your wiring of the arduino, though? I'm having a bit of a hard time getting the tap tempo to work properly...

:-)
Howard