Hi!
I’m working on an AVR-based octaver project. It’s not a standard octaver that can do fuzz octave up and down (I would just have gone for classic CMOS circuits). This one has modulation (using an internally generated LFO), and also follows the level of the input signal. It’s similar to the “Into the Unknown” pedal by Parasit Studio, but using a practical, cheap, small-footprint, all-in-one MCU, instead of a combination of CD4046 + CD4015 + LFO and VCA circuitry.
I’m using the ATtiny84 as it has an Input Capture Pin. The idea is to count clock cycles between input captures, calculate the frequency of the input signal, and then use that calculated frequency value to generate an octave up/down signal, which can be modulated and have its amplitude adjusted. Another option would be to count the number of rising edges for a given interval, but for low frequencies that would be too inaccurate I believe.
I have got the whole thing to work, but when I start to get into the high frequency range (above 1kHz), the signal becomes very noisy. I have tried out a few things, but I haven’t been able to find out the reason for that noise. I'm not sure if calculating the frequency is the best approach, but I can't think of another suitable one. For my particular purpose, the input and output of the octaver will always be square waves, so sampling the input and using DSP seems a bit overkill. Do you have any idea what might be the reason for that noise in the high-pitched tones? And do you know of any other projects out there similar to what I'm trying to achieve? It seems like all octaver/pitch-shifter/harmonizer projects are either CMOS-based or involve complicated signal processing…
In case it is useful, I briefly describe the timers and interrupts setup:
Timer0 runs at 20/8=2.5Mhz (using a 20Mhz crystal) and has a 44kh OVF interrupt to output the values of the audio output signal, as well as the values of the LFO (the latter are actually calculated at 44/4=11kHz, to save CPU time).
Timer1 runs at 20Mhz and has a 78kHz OVF interrupt for keeping track of overflows in between each input capture. The capture interrupt basically reads the counter and calculates the frequency. Timer1 is also running the PWM for outputting the samples at 78kHz.
At the moment, I do the octave up / down selection directly in the code and the LFO only outputs a square wave, but these are features that are very easy to implement and I want to get the sound right before moving on. BTW, this is my second ATtiny84 project, I hope that the code is not too terrible...
/*
ATtiny84 pin layout:
___ ___
VCC -| U |- GND
XTAL1 (PB0) -| |- (PA0) ADC0 <- Rate LFO
XTAL2 (PB1) -| |- (PA1) ADC1 <- Depth LFO
RESET (PB3) -| |- (PA2)
(PB2) -| |- (PA3)
Signal input -> ICP (PA7) -| |- (PA4) ADC4 <- Enveloppe
Output <- OC1A (PA6) -|_______|- (PA5)
*/
#include <avr/io.h> // for port commands.
//-------------------------------- I/O PORT PINS & ADC INPUTS ----------------------------------//
// PWM output:
#define OUTPUT_PIN PA6 // OC1A PWM output - Pin 7 of the IC.
// Analog input pins:
#define INPUTCAPT_PIN PA7 // ICP - Pin 6 of the IC.
#define ENVELOPE_PIN PA4 // ADC4 - Pin 8 of the IC.
#define rateLFO_PIN PA0 // ADCO - Pin 13 of the IC.
#define depthLFO_PIN PA1 // ADC1 - Pin 12 of the IC.
// Analog inputs:
const uint8_t ENVELOPE_READ = 4; // ADC4 - Pin 8 of the IC.
const uint8_t rateLFO_READ = 0; // ADCO - Pin 13 of the IC.
const uint8_t depthLFO_READ = 1; // ADC1 - Pin 12 of the IC.
//----------------------- VARIABLE DEFINITION AND INITIALISATION -------------------------------//
volatile uint16_t ovfCount0, ovfCount1; // Input signals no lower than 20Hz. For 20Hz Timer/Counter1 overflows (20Mhz/256)/20=3906.25 times.
volatile uint16_t ovfCount; // ovfCount is the difference between ovfCount1 and ovfCount0.
volatile uint8_t captCount0, captCount1; // Limited by Timer/Counter1 resolution, which is 8bit (OCR1A = 255).
volatile int16_t captCount; // captCount = captCount1 - captCount0, can be negative and reach +/-255.
volatile uint32_t count; // For 20Hz, count = 1,000,000.
volatile uint32_t freq; // Highest value, for 20kHz, is 29,000.
volatile uint8_t amplitude; // Only read 8-bit.
volatile uint8_t rateLFO; // Only read 8-bit.
volatile uint8_t depthLFO; // Only read 8-bit.
volatile uint8_t outputLFO; // 8-bit PWM.
//------------------ I/O CONFIGURATION, TIMER/COUNTERS & INTERRUPTS SETUP ---------------------//
void setup() {
// I/O configuration on the Data Direction Register (DDRB):
DDRA = 1<<OUTPUT_PIN; // Configure PWM output pin as output.
DDRA &= ~(1<<ENVELOPE_PIN); // Configure analog and digital input pins as inputs.
DDRA &= ~(1<<INPUTCAPT_PIN);
DDRA &= ~(1<<rateLFO_PIN);
DDRA &= ~(1<<depthLFO_PIN);
// Set up Timer/Counter0 & interrupts:
// COM0A1:0 = 0 and COM0B1:0 = 0 for normal port operation, both OC0A and OC0B disconnected.
// WGM02:0 = 2 for Clear Timer on Compare (CTC) match mode.
// CS02:0 = 2 for prescaler of 8 (timer running at 2.5MHz).
TCCR0A = 2<<WGM00;
TCCR0B = 2<<CS00;
// Timer/Counter0 interrupts:
// OCIE0A = 1 to enable Output Compare Match A interrupts.
// OCR0A = 55 to generate interrupts at ~44kHz (2.5MHz/55+1 = 44.6kHz).
TIMSK0 = 1<<OCIE0A;
OCR0A = 55;
// Set up Timer/Counter1 & interrupts:
// COM1A1:0 = 2 to clear OC1A on Compare Match (non-inverting mode).
// COM1B1:0 = 0 for normal port operation, OC1B disconnected.
// WGM13:10 = 5 for 8-bit Fast PWM.
// CS12:0 = 1 for no prescaling, 20Mhz clock.
TCCR1A = 2<<COM1A0;
TCCR1A |= 1<<WGM10;
TCCR1B = 1<<WGM12;
TCCR1B |= 1<<CS10;
// Input capture set-up:
// ICNC1 = 1 to activate noise cancelling.
// ICES1 = 1 to trigger capture on raising edge.
TCCR1B |= 1<<ICNC1;
TCCR1B |= 1<<ICES1;
// Timer/Counter1 interrupts:
// ICIE1 = 1 to enable Input Capture Interrupts.
// TOIE1 = to enable Overflow Interrupts.
TIMSK1 = 1<<ICIE1;
TIMSK1 |= 1<<TOIE1;
// Set up ADC:
ADCSRA = 1<<ADEN | 7<<ADPS0; // enable ADC, 156.25kHz ADC clock (128 prescaler).
ADCSRB = 1<<ADLAR; // Right adjusted (8-bit readings).
DIDR0 = 1<<ADC0D | 1<<ADC1D | 1<<ADC4D; // Disable digital input buffer to save power.
}
//--------------------------------- INTERRUPT SERVICE ROUTINES ---------------------------------//
// Timer/Counter0 Compare Match Interrupt - runs at (20Mhz/8)/(OCR0A+1) = 44.6kHz
ISR(TIM0_COMPA_vect) {
// ### LFO ###
// We only want to compute LFO samples at a rate of ~10kHz (which is more than enough)
static uint8_t timerLFO;
static uint16_t accLFO;
uint8_t indexLFO = (accLFO >> 8);
static uint8_t outputLFO;
if ((timerLFO & 3) == 3) {
// Square wave:
if (indexLFO < 128) {
outputLFO = ((uint16_t) (256) * depthLFO) >> 8;
} else {
outputLFO = 0;
}
accLFO += rateLFO;
}
timerLFO += 1;
// ### OUTPUT ###
static uint16_t acc;
uint8_t index = (acc >> 8);
uint8_t output;
uint32_t temp;
if (count == 0) {
output = 0;
} else {
// Square wave:
if (index < 128) {
output = ((uint16_t) (256) * amplitude) >> 8;
} else {
output = 0;
}
}
OCR1A = output;
//Increment the accumulator:
if (depthLFO < 10) {
// If LFO depth is near 0, just add the frequency.
acc += freq;
} else {
// If LFO != 0, add the LFO output
acc += (freq + (((uint32_t) (outputLFO) * freq) >> 8));
}
}
// Timer/Counter1 Overflow Interrupt - runs at 20Mhz/256 = 78kHz.
ISR(TIM1_OVF_vect) {
// Update ovfCount1:
if (amplitude > 50) {
ovfCount1++;
} else {
// If the level of the input signal is not high enough, basically restart all variables.
ovfCount0 = ovfCount1 = ovfCount = 0;
captCount0 = captCount1 = captCount = 0;
count = 0;
}
}
// Timer/Counter1 Input Capture Interrupt:
ISR(TIM1_CAPT_vect) {
uint8_t temp = ICR1; // clock is cleared when it gets to 255;
// We first compute the difference between input captures:
captCount1 = temp;
captCount = captCount1 - captCount0;
captCount0 = captCount1;
// We compute the difference between the number of overflows:
ovfCount = ovfCount1 - ovfCount0 + 65535 * (ovfCount1 < ovfCount0);
ovfCount0 = ovfCount1;
// The total clock cycles that went through a whole period of the input signal is
// the difference between input captures (either positive or negative) and the number of
// OVF interrupts multiplied by the number of clock cycles between OVF interrupts (256)
count = captCount + (uint32_t (ovfCount) * 256);
// Compute the frequency:
freq = (uint32_t) 29360128 / count; // number calculated so that freq added to acc in Timer0 gives the input frequency.
freq = freq * 2; // Octave up.
//freq = freq / 2; // Octave down.
freq &= 0xFFFFFFFC; // remove last bits to have more stable output.
}
//-------------------------------------- SAMPLING INPUTS --------------------------------------//
void loop() {
GetEnvelope(ENVELOPE_READ);
GetRateLFO (rateLFO_READ);
GetDepthLFO (depthLFO_READ);
}
//---------------------- WAVEFORM, FREQUENCY AND AMPLITUDE READ FUNCTIONS ----------------------//
void GetEnvelope(uint8_t pin) {
amplitude = ReadAnalog(pin);
}
void GetRateLFO(uint8_t pin) {
rateLFO = ReadAnalog(pin);
}
void GetDepthLFO(uint8_t pin) {
depthLFO = ReadAnalog(pin);
}
// 8-bit ADC analog read
uint8_t ReadAnalog(uint8_t ADC_VALUE) {
ADMUX = ADC_VALUE; // Select ADC, Vcc as voltage reference
ADCSRA |= 1<<ADSC; // Start conversion
while(ADCSRA & 1<<ADSC); // Wait until complete
return ADCH;
}