/* Arduino Perfect Tuning Servoguitar Program * *single motor control that listens to dedicated pickup and corrects tuning inbetween note changes *taken from perfect tune 10.1--this adds midi input hopefully * * * * * Background Info: * ADC 8-Bit Mode * Analog input 0 (ADC0) is used to sample the audio signal * Timer2 drives interrupt and sample rate (~15 kHz) * this program mainlines avr-gcc on which Arduino IDE is based--but still compiles with Arduino IDE * the ISR set-up is based on code by * KHM 2008 / Lab3/ Martin Nawrath nawrath@khm.de * Kunsthochschule fuer Medien Koeln * Academy of Media Arts Cologne--thanks! */ //*******Defines/Includes //!!deprecated, consider replacing #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) #include //the eeprom library //*******Function prototypes int autocorrelate (); //does low-budget autocorrelation //int keyboardread (); //reads keypad void savekeymap(); //saves keymap (tunings) in EEPROM void loadkeymap(); //loads keymap (tunings) from EEPROM void loadhysteresismap(); //loads learning from EEPROM void savehysteresismap(); //saves learning in EEPROM int checkMidi(); //read Midi input //*******Global Variables //keyboard monitoring variables int pressedbutton=5; //the key button (0-11) pressed int previousbutton = 0; //variables to detect change in pressed button int lastbutton = 0; int sam=0; //slide variables float slidecommand; float slideincrement; int slidetimer=0; int lastcommand=5; //pin assignments for hex input int r0=2; int r1=3; int r2=4; int r3=5; //toggle switches int plusPin=7; int minusPin=6; int modePin=8; int keynumber=5; //number from 0-11 input from keypad; default to 5 // arrays for tensions, accumulated tuning corrections, and hysteresis byte keymap[] = { 70, 75, 80, 85, 90, 110, 130, 150, 170, 190, 210, 230, 250}; byte tunecorrectionmap[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; byte hysteresismap[145]; //errors caused by predictable hysteresis int indx = 0; //index for hysteresis map //motor control varibles int command = 50; //128; // tension value from keymap as indexed by keynumber int collectedcommand=0; //command plus tuning correction and hysteresis correction int errorsignal; // voltage to the motor //microcorrection variables byte do_once=HIGH; int lastsensor_in=0; //used to detect motor speed int stopcounter=0; byte motorhasstopped=LOW; int holdoff2 = -2000; //delay after motor stops for ramping gain and beginning autocorrellation int sharp_flat = 0; //output of autocorrelation int correctioncounter = 0; //integrator for tuning errors int tunecorrection = 0; //fractional tuning adjustment (saved between note changes) int perfectcounter=0; //hit note on center--used to stop tuning int stoptuning=0; //flag to stop tuning //autocorrelation subroutine variables--global for speed int pitchcorrection=0; //returned value of sharp (+1) or flat (-1) or no data (3) int flatvalue=0; //correlation value for a flat note int perfectvalue=0; int sharpvalue=0; int peakvalue=0; //detect non-zero amplitude to trigger autocorrellation int troughvalue=255; float averageflatvalue=0; //average autocorrellation over many cycles float averageperfectvalue=0; float averagesharpvalue=0; // autocorrrelation lags /*********************************************************** * for scale from E (164.8 Hz) to Eb (311.1 Hz) * actual periods (perfect lags) for these notes will be: * 94.81, 89.49, 84.46, 79.72, 75.23, 71.02, 67.03, 63.28, 59.73, 56.37, 53.20, 50.23 * of course, only integer lags are possible. *********************************************************** */ int flatlag[] = { 96, 90, 85, 81, 76, 71, 68, 65, 61, 57, 54, 51 }; int centerlag[] = { 95, 89, 84, 80, 75, 70, 67, 64, 60, 56, 53, 50 }; int sharplag[] = { 94, 88, 83, 79, 74, 69, 66, 63, 59, 55, 52, 49 }; //other I/O variables //int tuneup = 7; //+ tune up command //int tunedown = 8; //- tune down command int debounce = 0; int ledCenter = 13; // LED connected to digital pin 13 Note: Adruino not ATMEL numbering int ledLeft = 9; //pin 9 display note is flat int ledRight = 10; //pin 10 display note is sharp int motorpin = 11; //pin 11 is motor output int pin12 = 12; //pin 12 provides profiling etc // interrupt variables accessed globally volatile boolean fastsample=false; //turns on acquire audio buffer volatile boolean sensorsample=true; //turns on acquire tension sensor data volatile boolean half1; //frequency divider variables volatile boolean half2; volatile boolean half3; volatile int audio_in; //not used--for testing volatile byte mode_in; //mode button value (save data etc) volatile byte sensor_in; //potentiometer value volatile byte dummy; //is this needed? volatile byte audio_buffer[512]; // Audio Memory Array 8-Bit volatile int buffer_index; //Midi variables setup--thanks to Kuki for inspiration int midiNote=0; int remappedNote=0; int midiVolume=0; byte incomingByte=0; int midiValue=0; //********Functions (Subroutines) int checkMidi () { if (Serial.available() > 0) { // read the incoming byte: incomingByte = Serial.read(); if ((incomingByte & 0xF0) == 0x90) // note on message starting { digitalWrite(ledCenter, HIGH); //delay(50); digitalWrite(ledCenter, LOW); midiNote = Serial.read(); remappedNote= midiNote % 12; midiVolume = Serial.read(); /* for (int x=0; x0) //check to make sure EEPROM has previous data { for (int i = 0; i < 12; i++) //loop through array in EEPROM keymap[i] = int(EEPROM.read (i)); } } void savekeymap() { for (int i = 0; i < 12; i++) //loop through array in EEPROM { EEPROM.write(i, byte(keymap[i])); } } void loadhysteresismap() // load heuristics (best guesses as to overshoot) { for (int i = 0; i < 144; i++) //loop through array in EEPROM { hysteresismap[i] = int(EEPROM.read (i + 15))-128; //remove 128 offset to get plus and minus } } void savehysteresismap() { int tempholding; for (int j = 0; j < 144; j++) { tempholding=hysteresismap[j]+128; //to map plus and minus to byte if (tempholding > 255) tempholding = 255; //bound heuristics to byte if (tempholding < 0) tempholding = 0; //bound heuristics to byte EEPROM.write (j + 15, byte (tempholding)); } } int hexportread() { int midiInput=0; if (digitalRead(r0)==HIGH) midiInput=midiInput | 0x01; else midiInput=midiInput & 0xFE; if (digitalRead(r1)==HIGH) midiInput=midiInput | 0x02; else midiInput=midiInput & 0xFD; if (digitalRead(r2)==HIGH) midiInput=midiInput | 0x04; else midiInput=midiInput & 0xFB; if (digitalRead(r3)==HIGH) midiInput=midiInput | 0x08; else midiInput=midiInput & 0xF7; keynumber=midiInput; return keynumber; } int autocorrelate () //listen to signal to determine final correction { flatvalue=0; sharpvalue=0; peakvalue=0; troughvalue=255; //perform pseudocorrellation (uses subtraction not multiplication) for (int i = 1; i < 400; i++) //400=buffer-maximum lag { flatvalue += abs (audio_buffer[i] - audio_buffer[i + centerlag[pressedbutton]+1]); perfectvalue += abs (audio_buffer[i] - audio_buffer[i + centerlag[pressedbutton]]); sharpvalue += abs (audio_buffer[i] - audio_buffer[i + centerlag[pressedbutton]-1]); if (peakvalueaudio_buffer[i]) troughvalue=audio_buffer[i]; } //rolling averages !! .9 might be too big averageflatvalue=averageflatvalue*.4+flatvalue; averageperfectvalue=averageperfectvalue*.4+perfectvalue; averagesharpvalue=averagesharpvalue*.4+sharpvalue; //determine greatest correllation lag if ((troughvalue > 0) && (peakvalue < 255) && (peakvalue-troughvalue>25)) // was 25only if not clipping and high signal valu { digitalWrite(ledCenter, HIGH); if (averagesharpvalue > averageflatvalue) pitchcorrection = -1; if (averagesharpvalue < averageflatvalue) pitchcorrection = 1; if ((averagesharpvalue>averageperfectvalue)&& (averageflatvalue>averageperfectvalue)) pitchcorrection = -3; // was 25define correction deadzone } else { pitchcorrection = 0; digitalWrite(ledCenter, LOW); } return pitchcorrection; } //********Setup void setup () { loadkeymap(); //loadhysteresismap(); pinMode (r0, INPUT); //hex input 0 digitalWrite (r2, LOW); //turns off pull up resistor pinMode (r1, INPUT); //hex input 1 digitalWrite (r2, LOW); //turns off pull up resistor pinMode (r2, INPUT); //hex input 2 digitalWrite (r2, LOW); //turns off pull up resistor pinMode (r3, INPUT); //hex input 3 digitalWrite (r2, LOW); //turns off pull up resistor pinMode (plusPin, INPUT); //sets tuning buttons to inputs with pullup digitalWrite (plusPin, HIGH); pinMode (minusPin, INPUT); digitalWrite (minusPin, HIGH); pinMode (ledCenter, OUTPUT); // sets misc digital pins as outputs pinMode (ledLeft, OUTPUT); pinMode (ledRight, OUTPUT); pinMode (motorpin, OUTPUT); pinMode (pin12, OUTPUT); //set correction array to safe values for (int i = 0; i < 144; i++) hysteresismap[i] = 0; // *************Some Bit-Banging to get High Speed A/D operation********************* // This is basically assembly language accommodated by the Arduino IDE // The acronyms here are either (1) register names (e.g., ADCSRA is A/D register A) //(see Atmel168 datasheet for register names and use) //or (2) defines/constants (e.g., ADPS2=bit 2) set in the iom168p.h header file buried in the Arduino download //The defines are also described in the Atmel168 datasheet (do a search of the .pdf) // set adc prescaler to 32 for 33kHz sampling frequency (16mhz/32/15 instructions per conversion) sbi (ADCSRA, ADPS2); // This code sets the AD clock prescalar to /32 (16MHz/32=500kHz cbi (ADCSRA, ADPS1); // The A/D clock should be between 50-200 kHz (+ some only 8-bit conversions!) sbi (ADCSRA, ADPS0); // Normal conversion requires about 15 clock cycles so 33Khz conversion rate sbi (ADMUX, ADLAR); // Left justifies A/D output so it can be read in one swoop sbi (ADMUX, REFS0); // Selects VCC Reference for A/D cbi (ADMUX, REFS1); sbi (ADMUX, MUX0); // Set Input Multiplexer to Channel 0 cbi (ADMUX, MUX1); // MUX0-MUX3 select one of 8 A/D inputs Mux0=LSMB cbi (ADMUX, MUX2); // In this case channel 0 is the guitar pickup cbi (ADMUX, MUX3); // TCCR="timer counter control register" // This section sets Timer2 which will control the interrupt timing cbi (TCCR2A, COM2A0); // clear OC2A on Compare Match (output for PWM) pin PB3 ??what is this sbi (TCCR2A, COM2A1); sbi (TCCR2A, WGM20); //Timer2 PWM Mode set to fast PWM (not phase correct mode) sbi (TCCR2A, WGM21); cbi (TCCR2B, WGM22); // Timer2 Clock Prescaler to : 1 (timer driven direclty by I/O clock) sbi (TCCR2B, CS20); //interrupt at 256 countdown 16MHz/256=62kHz cbi (TCCR2B, CS21); // this is too fast, but next choice is /8 (too slow) cbi (TCCR2B, CS22); //so throw out every other ISR (e.g. don't convert in each ISR--see ISR below) //cli(); // disable interrupts to avoid distortion // "timer interrupt mask register" cbi (TIMSK0, TOIE0); // disable Timer0 interrupt !!! Arduino delay functions are turned off--be aware sbi (TIMSK2, TOIE2); // enable Timer2 Interrupt //Serial.print("ADC offset="); // Outputs A/D value once so input audio can be DC "trimmed" to a value of 127 //audio_display=audio_in; //Serial.println(audio_display); //start serial with Midi baudrate 31250 or 38400 for debugging //Serial.flush(); //midi !!!! //Serial.begin(31250); } //********************************************************************* //********************************************************************* //*******Main loop--motor contro and autotuning correctionl *********** //********************************************************************* //********************************************************************* void loop () { // *******************SET UP TIMERS ******************************* //PORTB = PORTB | 4; // Set pin 10 for code profiling // increment misc timers if (debounce < 250) debounce++; //if (holdoff1<1000) holdoff1++; // *******************READ SWITCHES ******************************* // read plus +/- toggle switches if ((digitalRead (plusPin) == LOW) && (debounce >= 100)) //+was 50 { debounce = 0; holdoff2=0; //ramp down gain keymap[pressedbutton] = keymap[pressedbutton] + 5; } // read minus toggle if ((digitalRead (minusPin) == LOW) && (debounce >= 100)) //- { debounce = 0; holdoff2=0; //ramp down gain keymap[pressedbutton] = keymap[pressedbutton] - 5; } // mode key (saves keymap in EEPROM) if ((digitalRead (modePin) == LOW) && (debounce >= 100)) { debounce = 0; savekeymap(); //savehysteresismap(); } // ******************************************************************** // *******************NEW KEYPRESS DETECTION*************************** // ******************************************************************** //get any pressed key pressedbutton=hexportread(); //read data from second arduino decoding midi if (pressedbutton != lastbutton) //new note triggers many actions { previousbutton = lastbutton; lastbutton = pressedbutton; digitalWrite(ledLeft,LOW); delay(50); digitalWrite(ledLeft,HIGH); delay(50); digitalWrite(ledLeft,LOW); delay(50); digitalWrite(ledLeft,HIGH); delay(50); digitalWrite(ledLeft,LOW); // store tune correction and countervalues accumulated so far if (tunecorrection>1) tunecorrection=1; //because of friction in static correction limit change note to 1 if (tunecorrection<-1) tunecorrection=-1; keymap[previousbutton]=keymap[previousbutton]+tunecorrection; //save autotuning effect //tunecorrectionmap[previousbutton]=correctioncounter; tunecorrection=0; //correctioncounter=tunecorrectionmap[pressedbutton]; correctioncounter=0; //reset lots of stuff do_once=HIGH; motorhasstopped=LOW; stopcounter=0; //holdoff1=0; holdoff2=0; averagesharpvalue=0; averageflatvalue=0; averageperfectvalue=0; perfectcounter=0; stoptuning=0; //correctioncounter = 0; //integrates autocorrelation values digitalWrite(pin12,LOW); digitalWrite (ledLeft, LOW); digitalWrite (ledRight, LOW); //slide variables lastcommand=command; slidetimer=0; //calculate new hysteresismap index indx = previousbutton + pressedbutton * 12; if (indx > 143) indx = 143; if (indx < 0) indx = 0; } // **************DETECT MOTOR HAS STOPPED AFTER RUNNING***************** //must be near final resting place for a given period of time sam=((command-int(sensor_in))/2+128); //check close to mark approach if ((sam>124)&&(sam<132)) stopcounter++; else stopcounter=0; if (stopcounter>2) { motorhasstopped=HIGH; //digitalWrite(pin12,HIGH); } else motorhasstopped=LOW; if (motorhasstopped==LOW) holdoff2=0; if ((holdoff2<1000)&& (motorhasstopped=HIGH)) holdoff2++; //start holdoff2 timer for gain if (stopcounter>900) stopcounter=900; // *********************************************************************** // ****************OUTPUT DATA TO MOTOR ********************************** // *********************************************************************** //add tuning correction and hysteresis correction to command command = keymap[pressedbutton]; //get tension command based on pressed button // slide stuff if (slidetimer<1000) slidetimer++; slideincrement=(command-lastcommand)/10; if (slideincrement<10) slideincrement=-10; //just jump to note for close notes if (slideincrement>10) slideincrement=10; if ((slideincrement<0)&& (slidecommand>command) && (slidetimer>50)) //was 100--pretty cool { slidecommand+=slideincrement; slidetimer=0; } if ((slideincrement<0)&& (slidecommand<=command)) slidecommand=command; //get rid of remainder errors if ((slideincrement>0) && (slidecommand50)) { slidecommand+=slideincrement; slidetimer=0; } if ((slideincrement>0)&& (slidecommand>=command)) slidecommand=command; //get rid of remainder errors collectedcommand=(slidecommand + tunecorrection); // + hysteresismap[indx]); if (collectedcommand>255) collectedcommand=255; if (collectedcommand<0) collectedcommand-1; //collectedcommand=command; // ramp up gain after a time delay if (holdoff2 >= 100) errorsignal = byte (((collectedcommand - sensor_in)*2.5)+128); else if (holdoff2 >= 90) errorsignal = byte (((collectedcommand - sensor_in)*2)+128); else if (holdoff2 >= 80) errorsignal = byte (((collectedcommand - sensor_in))+128); else errorsignal = byte (((collectedcommand - sensor_in)/2)+128); // trap error signal overflow if (errorsignal > 255) errorsignal=255; if (errorsignal < 1) errorsignal=1; //output error to motor OCR2A = errorsignal; //pin 11 // *********************************************************************** // ***********DO MICROCORRECTIONS***************************************** // *********************************************************************** if (holdoff2>80) //gain has settled out so can check frequency (loop update can slow because low motion) { // ********* GRAB HYSTERESIS ERROR if ((do_once=HIGH) && (command-sensor_in>0)) hysteresismap[indx]++; if ((do_once=HIGH) && (command-sensor_in<0)) hysteresismap[indx]--; do_once=LOW; // *******SET UP FOR AUTOCORRELLATION //switch A/D to audio input to get 512 samples of guitar for autocorrelation sensorsample=false; buffer_index=0; sbi (ADMUX, MUX0); // Set Input Multiplexer to Channel 1 for guitar input cbi (ADMUX, MUX1); // MUX0-3 select one of 8 A/D inputs Mux0=LSMB cbi (ADMUX, MUX2); cbi (ADMUX, MUX3); fastsample=true; //enable sampling in ISR while (fastsample==true); //spin until acquistion is complete //set back to normal sensor sampling for motor control half3=true; //reset to mode sampling sbi (ADMUX, MUX0); // Set Input Multiplexer to Channel 5 for sensor cbi (ADMUX, MUX1); // MUX0-3 select one of 8 A/D inputs Mux0=LSMB sbi (ADMUX, MUX2); cbi (ADMUX, MUX3); sensorsample=true; //get feedback back on line faster sharp_flat = autocorrelate (); // ************INTEGRATE/AVERAGE AUTOCORRELATION VALUES if ((sharp_flat!=0))// && (stoptuning==0)) //update tuning only if good data { //display tuning if (sharp_flat == 1) //sharp { digitalWrite (ledLeft, LOW); digitalWrite (ledRight, HIGH); perfectcounter=perfectcounter-1; correctioncounter = correctioncounter - 1; } if (sharp_flat == -3) //perfect { perfectcounter++; digitalWrite (ledLeft, LOW); digitalWrite (ledRight, LOW); } if (sharp_flat == -1) //flat { perfectcounter=perfectcounter-1; digitalWrite (ledLeft, HIGH); digitalWrite (ledRight, LOW); correctioncounter = correctioncounter + 1; } tunecorrection = (int) correctioncounter / 40; //adjsut this for tuning correction speed was 100 /* //stop tuning (may want to remove this?) if (perfectcounter<0) perfectcounter=0; if (perfectcounter>4) //!!was 10 { stoptuning=1; //digitalWrite (pin12,HIGH); digitalWrite (ledLeft, LOW); digitalWrite (ledRight, LOW); } //else digitalWrite(pin12,LOW); */ } //end of update tuning only if good data else { digitalWrite (ledLeft, LOW); digitalWrite (ledRight, LOW); } } //end of do autocorrellaton only if motor stopped //PORTB = PORTB ^ 4; // clear pin 10 for profiling }//main loop // **************************************************************** // ****************END OF MAIN LOOP****************************** // **************************************************************** // **************************************************************** // ****************INTERRUPT SERVICE ROUTINE*********************** // **************************************************************** /* ***************************************************************** * Timer2 Interrupt Service at 31.25 kHz KHz (too fast for ADC so divide by 2 here) *!!!ACTUALLY CLOCKING AT 62KhZ (per oscilliscope) and A/D CAN HANDLE 32KHZ AT 8 BITS-- * SO MAY SAMPLE AUDIO AT 15 AND ALTERNATE SENSORS AT 32--need to check math * here the audio is sampled in a rate of: 16Mhz / 256 / 4 = 15625 Hz ******************************************************************* */ ISR (TIMER2_OVF_vect) //how avr-gcc describes an interrupt { half1 = !half1; if (half1) //cut interrupt rate in half to 32kHz { half2=!half2; if (half2) //cut interrupt rate in half to 15kHz (Max A/D speed) { if (fastsample==true) { //PORTB = PORTB | 16 ; //set pin 12 to profile ISR audio_buffer[buffer_index] = ADCH; // store sample in audio buffer buffer_index++; if (buffer_index > 511) fastsample=false; //PORTB = PORTB ^ 16 ; //set pin 12 to profile ISR } } if (sensorsample==true) { half3 = !half3; if (half3) //get pin 0 (mode) { // measure second sensor here// mode_in = ADCH; !! sbi (ADMUX, MUX0); // Set Input Multiplexer to Channel 5 cbi (ADMUX, MUX1); // MUX0-3 select one of 8 A/D inputs Mux0=LSMB sbi (ADMUX, MUX2); cbi (ADMUX, MUX3); } else //get pin 5 (sensor) { sensor_in = ADCH; cbi (ADMUX, MUX0); // Set Input Multiplexer to Channel 0 cbi (ADMUX, MUX1); // MUX0-3 select one of 8 A/D inputs Mux0=LSMB cbi (ADMUX, MUX2); cbi (ADMUX, MUX3); } } dummy++; dummy--; dummy++; dummy--; // short delay before start conversion !!hmm why? sbi (ADCSRA, ADSC); // start next conversion } }