You're right, the code is almost exactly the same as the current one on my laptop. I'll try to change the buffer to 1000 samples. I agree with you that the results are quite good, but I noticed that you've set the readData function to D4. Wouldn't it be optimized for frequencies from 320 to 350 Hz if you'd choose E1? Asside from that, I believe that with all the help I got from you guys I have a fair change of succeeding in the project
As for the weird error that I've written about in my previous post, do you have any idea what could be the cause?
So, I've been thinking about the results and their stability. It's not really good. I can optimize it for certain frequencies by playing around with the signal capture time, but then the results for other frequencies are heavily influenced by errors. Could the combination of continuous sampling and the use of interrupts be the cause of instability? As i mentioned before, the resolution is quite good, but there are errors that show up at a constant rate and mess things up. I figured that a solution (or a way to isolate the cause of error) could be to use continuous sampling with interrupts that are generated on the reading of a new value, instead of using interrupts to control the sample frequency. The question that I want to ask about this is the following: Can I alter the samplefrequency on an analog port as I did with the OCR timer value (during a program)? Next to that I'm not sure how to implement a change of samplefrequency on a port that is continuously sampling. I'm looking forward to your comments.
I've had a bit more time to play with this. There are two significant changes in this version. First the data capture uses the ADC in the continuous mode with an interrupt service routine to move the data into the buffer. Sample rate is fixed at 9615.4 samples per second. The second change is a correction to the interpolation routine which previously had the peak correlation value as one of the endpoints rather than the center of the three points.
I've changed the peak detection algorithm implementation to use "case" statements rather than a cascaded "if" statements. It should be functionally the same, but I think peak detection could be made more robust and this was a first step toward a revision. Eventually I'd like peak detection to evaluate multiple correlation peaks rather than stopping at the first it finds over the threshold.
Finally I opened up the frequency window test to 100-2000 Hz, largely because the phone app doesn't put out much audio power below about 200 Hz, so it gives me more range to test over. In limited testing this implementation seems to mostly put out accurate values if it puts out anything at all.
// Arduino Pitch Detection on A0 with autocorrelation and peak detection
// Original author(s): akellyirl, revised by robtillaart, MrMark, barthulsen
// http://forum.arduino.cc/index.php?topic=540969.15
// Continuous ADC ala http://www.instructables.com/id/Arduino-Audio-Input/ "Step 7"
#define sampleFrequency 9615.4
#define bufferSize 1024
volatile byte rawData[bufferSize] ; // Buffer for ADC capture
volatile int sampleCnt ; // Pointer to ADC capture buffer
long currentSum, previousSum, twoPreviousSum ;
int threshold = 0;
float frequency = 0;
byte pdState = 0;
void setup() {
Serial.begin(115200) ;
pinMode(LED_BUILTIN, OUTPUT);
cli();//disable interrupts
//set up continuous sampling of analog pin 0
//clear ADCSRA and ADCSRB registers
ADCSRA = 0 ;
ADCSRB = 0 ;
ADMUX |= (1 << REFS0) ; //set reference voltage
ADMUX |= (1 << ADLAR) ; //left align the ADC value- so we can read highest 8 bits from ADCH register only
ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // ADC clock 128 prescaler- 16mHz/128=125kHz->9615 sps
ADCSRA |= (1 << ADATE); //enable auto trigger
ADCSRA |= (1 << ADIE) ; //enable interrupts when measurement complete
ADCSRA |= (1 << ADEN) ; //enable ADC
ADCSRA |= (1 << ADSC) ; //start ADC measurements
}
ISR(ADC_vect) { // When ADC sample ready, put in buffer if not full
if (sampleCnt < bufferSize)
{
rawData[sampleCnt] = ADCH ;
sampleCnt++ ;
}
}
void readData() {
sampleCnt = 0 ;
sei() ; // Enable interrupts, samples placed in buffer by ISR
while (sampleCnt < bufferSize) ; // Spin until buffer is full
cli() ; // Disable interrupts
}
void findFrequency() {
// Calculate mean to remove DC offset
long meanSum = 0 ;
for (int k = 0; k < bufferSize; k++) {
meanSum += rawData[k] ;
}
char mean = meanSum / bufferSize ;
// Remove mean
for (int k = 0; k < bufferSize; k++) {
rawData[k] -= mean ;
}
// Autocorrelation
currentSum = 0 ;
pdState = 0 ;
for (int i = 0; i < bufferSize && (pdState != 3); i++) {
// Autocorrelation
float period = 0 ;
twoPreviousSum = previousSum ;
previousSum = currentSum ;
currentSum = 0 ;
for (int k = 0; k < bufferSize - i; k++) {
currentSum += char(rawData[k]) * char(rawData[k + i]) ;
}
// Peak detection
switch (pdState) {
case 0: // Set threshold based on zero lag autocorrelation
threshold = currentSum / 2 ;
pdState = 1 ;
break ;
case 1: // Look for over threshold and increasing
if ((currentSum > threshold) && (currentSum - previousSum) > 0) pdState = 2 ;
break ;
case 2: // Look for decreasing (past peak over threshold)
if ((currentSum - previousSum) <= 0) {
// quadratic interpolation
float interpolationValue = 0.5 * (currentSum - twoPreviousSum) / (2 * previousSum - twoPreviousSum - currentSum) ;
period = i - 1 + interpolationValue ;
pdState = 3 ;
}
break ;
default:
pdState = 3 ;
break ;
}
// Frequency identified in Hz
if (threshold > 100) {
frequency = sampleFrequency / period;
if (frequency < 2000) {
Serial.println(frequency);
}
}
}
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
readData();
digitalWrite(LED_BUILTIN, LOW);
findFrequency();
}
Thanks MrMark for your help. I'll test the code today. I've got only one question. Is it possible and usefull to change to constant samplefrequency that you've implemented to a higher one for the high frequencies / strings?
Edit:
So I've tested the code. The problem with my code was that I had implemented interpolation wrong, and the problem with yours was that the sample buffer was too large. I don't know why, but when I changed it to 512 samples (instead of 1024) it worked unbelievably well! With the larger amount of samples it puts out almost nothing, like you said.
The sample frequency can be adjusted in the code of post #42 by setting the values of the A/D pre-scale bits (ADPS0,1,2) in the A/D control/status register A (ADCSRA) register. The A/D clock is FCPU (16 MHz) divided by the pre-scale (128 in code above) divided by the clocks per A/D output (13). The pre-scale is a power of two so control of the A/D sample rate frequency is pretty coarse (9615.4 Hz, 19231 Hz, 38462 Hz, ...) .
Interesting that you got better results with a shorter sample length, I hadn't explored that yet, but was wondering if 1k samples was excessive. In particular, when this is extended to do multiple peak detection, it will have to calculate the correlation at more lags, not just until it finds the first peak, and that's going to be slow with an excessively long sample buffer.
In the limited testing that I did, I was seeing frequency reports that were within 1% of what I expected. Most of the error looked like bias rather than random error, which would be consistent with the Arduino Uno clone's ceramic resonator being the dominant error source.
Nice explanation on the changing of the sample frequency. Actually, I don't think it has to (or can) be more accurate. While testing with the MyDAQ in a range of 80 to 330 Hz the error didn't exceed 1 Hz. With a microphone it is a bit inaccurate in the 330 Hz region, but I think this is caused by hardware, since the software proved to work well. I'll play around with it and try to find the optimum buffer size. Next to that I'll take the average of multiple measurements and adapt the peak state machine to find the next correlation peak as well. As for the next step in the project, I'd like to add an OLED screen (with the Adafruit SSD1306 and GFX libraries). This requires I2C. Since I've read that the other analog ports are unusable during free running mode on one of them, I'd like to ask the following question: Do you expect that this will cause any problems? Or can I just simply turn the continuous sampling on and off in the readData function to use I2C on other ports?
barthulsen:
Since I've read that the other analog ports are unusable during free running mode on one of them, I'd like to ask the following question: Do you expect that this will cause any problems? Or can I just simply turn the continuous sampling on and off in the readData function to use I2C on other ports?
You can't use analogRead() to read another Analog input pin while doing continuous conversion, because analogRead() will reconfigure the ADC registers such that the continuous conversion stops. I don't think this applies to using one of those pins (A4, A5 in this case) for alternate functions, such as GPIO or I2C. Thus I don't expect there's any issue with I2C to your display while the ADC is running, but it's not something I've tried.
The code below shows the changes that I made to find not only the first but also the second correlation peak. Both the frequencies that belong to these periods are calculated (not in the code fragment).