Electronic Tuner Design

This project aims to implement an electronic tuner which is able to analyze sound samples and display the notes contained in the sound. It utilizes a PIC32 microcontroller, a microphone circuit, and a TFT LCD to achieve that purpose.

The intuition of making this project came from the musical experiences of both of us. We are both musical instrument players in the real life, and we have to deal with tuners all the time to make sure our instruments could play the correct notes. Thus, we decided to give current commercial tuner devices a challenge and build a tuner of our own.

Figure 1. Actual System Setup

Initially, the state is set to be idle, and a prompt will be printed to the PuTTY terminal asking the user to select the mode, where mode ā€œ1ā€ indicates auto mode and mode ā€œ2ā€ indicate manual mode. If mode = 1, then the system will enable timer 2 for ADC reading, and the TFT display will be switched to display the note tuning meter. Frequency analysis will also be performed in the calculation thread, which will be discussed later. Also, the state machine will switch to running state, which is state 2. If mode = 2 instead, then the state machine will transit to MMWI state (state 1) to prompt the user to enter a note. If a note is correctly inputted, the note will be decoded, and the corresponding tone will be played by enabling Timer 3 interrupt. At the same time, the state machine will transit into running state ( state 2). Details of decoding and tone generation will be discussed in later sections. When the system is in running state, then whenever the user type in the command ā€œqā€, the program will quit and return to the idle state.

Tone generation with DDS

In the manual mode, the generation of the sound of reference note is achieved through Direct Digital Synthesis (DDS) technique. The basic idea of DDS is that, a 32-bit variable is used to represent the phasor of a sine wave, and incrementing this variable is equivalent to incrementing the phasor; to be more specific, incrementing the variable from 0 to overflow is the same as incrementing the phasor from 0 to 2?, and one cycle of variable overflow is just one cycle of the sine wave. That is to say, to generate a sine wave of desired frequency, f, using the timer interrupt at a frequency of 16 kHz, we could simply increment the 32-bit variable during each interrupt event, and let the variable overflow after every N interrupt events, where N is calculated by f/16kHz. The increment step that makes the 32-bit variable overflow after N increments is calculated by 2^32/N. To save memory, we decide to use a 256-entry sine table, which is created by dividing a cycle of sine wave into 256 samples and scale the samples to a range from -2048 to +2048 for future use of the DAC. Since 256 = 2^8, the top 8 bits of the 32-bit variable is then used as the index for the sine table to retrieve the sine sample.

Using this technique, we could generate the various frequencies by simply setting the increment step for the 32-bit variable to different values.

User interface and display aesthetics

For a tuner, the aesthetic traits are quite important to give the user both valuable information and an entertaining visual effect. Therefore, different from the previous labs in which the raw data are simply displayed on the TFT screen, for this project we spent considerable time designing the user interface to make it look more satisfying.

A real tuning meter like design is used for the TFT display interface. The appearance is shown in Figure 5 below.

Figure 7. Appearance of the user interface

This display involves three critical components: the panel consisted of an arc and 11 equally spaced tick marks, an arrow used to point for the precision, and the actual display of the note. Careful calculations are performed, and helper functions are created to build the display.

markSetup() drawMarks() and drawCircle(short x, short y, short r, short size, short color) are the helper functions to initialize the panel. arrowIndexSetup() and drawArrow() are the two helper functions used to draw the arrow.

All these drawings include the use of the provided TFT functions for drawing circles and triangles, with the calculated index points based on the Pythagorean theorem.

Optimization

After implementing the design described above, we werenā€™t satisfied with the execution speed of the program. Therefore, we have come up with the following optimizations to make the system run faster:

Sampling

Record samples into two arrays (arrayID)

In our design, two arrays dataArray0 and dataArray1 are used interchangeably to store the data, and a volatile global variable arrayID is used to keep track on which array is currently used for storing. Once a data array is filled, the other array will be switched on to store new data, while the filled array will be used to perform frequency analysis. Such use of the two arrays exhibits parallelism, which essentially means that new sample could be captured while the old samples are being analyzed.

Flags

In the sampling ISR, two flags are added for indicating if each of the two data arrays is full of newly recorded samples, thus ready for frequency analysis. By adding these flags and corresponding logic in the frequency analysis code, we can make sure that by no means the DFT could be calculating upon a data array mixed with newly recorded and old samples, which is highly likely to return a wrong note. By adding data array flags to the design, we could further increase the accuracy of output notes.

Thresholding

For efficiency of calculation, we decide to set a threshold on the captured data, and any signal below that threshold will be replaced with a value of 0. We set the threshold to be 0x3FC (1020). Based on our testing, we think it is safe to conclude that ADC data larger than this threshold is caused by background noise, and thus negligible. Therefore, applying the threshold cancels noise, making the analysis more accurate.

Frequency analysis

Reduce number of notes to be scanned

Originally, we planned to cover 84 notesā€™ pitch detection, which means Goertzel had to be called for 94 times in total, including the 10 times for finding steps (each step is 20 musical cents) around the closest note. However, we discovered that it took too long for each frequency analysis on a data array to finish. From our measurements, it took 560-592 milliseconds to finish computing on a data array, which was not ideal since it took only 250 milliseconds to record one such array.

Eventually, we set the number of notes to detect to be 36, starting from C3 to B5. During testing, we discovered that frequencies with ~120 Hz might not be picked up correctly by the microphone, even though the high-pass filter installed should only filter out frequencies lower than 31.83 Hz. We think the high noise level might be the cause. Therefore, it would not make the deviceā€™s performance worse by removing the first two octaves. The sixth octave is removed only for increasing the computation speed, and the reason for removing the seventh octave was stated in the previous section.

After this optimization, we found the time to complete frequency analysis for one data array to be always within 200 milliseconds, mostly from 133 to ~180 milliseconds, thus making real-time frequency analysis possible.

Thresholding

We also utilized thresholding to make the displayed note less frequently to change. We set a frequency response magnitude threshold of 10^9. If the maximal magnitude of all notesā€™ frequencies are smaller than this threshold, the displayed note would not be changed. During testing, we concluded that only magnitudes greater than this threshold could be meaningful, and lower magnitudes are caused by noise. By applying this threshold, we could achieve that the note displayed on the TFT, the note with the largest frequency response magnitude, could be preserved on the screen even though the user has stopped to give the microphone new sound inputs.

Transforming from floating point to fixed-point

Additionally, we attempted to utilize fixed-point numbers for execution of frequency analysis. Theoretically, the microcontroller should be able to perform fixed-point calculations faster than floating-point calculations. We expected a speedup of 30-50%, since the floating-point multiplication and divisions take a significant amount of time.

In order to preserve precision, we had to use unsigned long long data types (64-bit long) to replace 32-bit floats. The reason is that the integer part of the fixed-point could take up 42 bits, since the frequency response magnitude could reach up to 3*10^12, which is just within 2^42 (4*10^12). The rest 22 bits would be used for the fraction part.

However, it did not work well for us. It seems that the microcontroller and the debugger do not function normally with 64-bit data types. A ā€œData Size Error!ā€ would always appear for values of 64-bit variables. We spent a lot time debugging, but did not figure out a way to avoid this. More importantly, Professor Land told us that in reality, for our PIC32 microcontroller, 64-bit calculations might not be significantly faster than floating-point calculations, since the microcontroller is a 32-bit microcontroller. Thus, doing this floating-point to fixed-point conversion seemed pointless to us.

About The Author

Muhammad Bilal

I am a highly skilled and motivated individual with a Master's degree in Computer Science. I have extensive experience in technical writing and a deep understanding of SEO practices.