Introduction: Making a Guitar Tuner and Audio Spectrum Analyser Using the Pimoroni Galactic Unicorn

The Pimoroni Galactic Unicorn is a 53x11 RGB LED display from Pimoroni's Unicorn series based on a Pi Pico W board. This article shows how it can be used as an audio spectrum analyser with various displays including a waterfall spectrogram and a tuner suitable for stringed instruments like a guitar.

The display doesn't use the common WS2812 protocol RGB pixels like the ones sold as NeoPixels by Adafruit. It's driven by an array of FM6047 constant current drivers using the RP2040's PIO providing a high ~300fps update rate. The display doesn't have the extremely bright peak illumination of NeoPixels but does have much finer control over low brightness levels, better diffusion and draws a very reasonable 1A current at maximum brightness.

There is no onboard microphone and it is unfortunately not trivial to connect one using the two Qwiic/STEMMA QT connectors on the Space Unicorns. Instructables: Adding a Microphone to Pi Pico W on Pimoroni Galactic Unicorn explains this in more detail and shows how to add an analogue microphone, a SparkFun Sound Detector.

The program used on the Unicorn is an evolution of Phil Howard's Blunicorn code which has additions by Mike Bell and contains V. Hunter Adams's fixed-point FFT code and parts of the BlueKitchen Bluetooth A2DPaudio example code. The autocorrelation code for efficientpitch detection for the instrument tuner is based on Joel de Guzman's code which was aided by Gerry Beauregard's prior work.

Supplies

Step 1: Adding a Microphone

The background and steps to add an electret microphone, a SparkFun Sound Detector, are described in Instructables: Adding a Microphone to Pi Pico W on Pimoroni Galactic Unicorn. A high-level, concise summary follows.

  1. Select a suitable analogue microphone with an approximate 0-3.3V output range and one that can be powered by 3.3V.
  2. Select a cable/connector to solder to the Pi Pico W. A connector either on the Pi Pico W or as part of the lead is useful to allow the Galactic Unicorn to also be used without the microphone.
  3. Carefully and briefly solder pins or wires to the following pads on the Unicorn's Pi Pico W:
  4. pin 36 3V3(OUT) (red),
  5. pin 33 GND (black),
  6. pin 32 GP27 (another colour).
  7. Connect those to the sound module.

The default gain on the SparkFun Sound Detector is at a reasonable level for typical use in the home. Their Hookup Guide describes how the gain can be lowered by adding a through-hole resistor at R17 or increased by desoldering the R3 surface mount resistor.

Step 2: Installing the Program

There are two pre-built uf2 files in the release in the GitHub repository compiled for the Galactic Unicorn.

  1. Plug the Galactic Unicorn into a computer using a (non-charge-only) USB cable.
  2. Hold down the BOOTSEL button on the Pi Pico W board on the reverse side of the Unicorn and press the RESET button. An RPI-RP2 drive will appear (/media/USER/RPI-RP2 on Linux on a Raspberry Pi).
  3. Download the uf2 files from the v1.4.0 release or the latest release:
  4. galactic-unicornaudiodisplay-dmaadc.uf2
  5. galactic-unicornaudiodisplay-bluetooth.uf2 (this appears to mostly work but has not been thoroughly tested)
  6. Copy the uf2 file onto the RPI-RP2 drive.

A large scrolling welcome message will appear as the program starts. If you have a terminal connected to the serial console over USB then this will appear.

unicornaudiodisplay 1.3.0
More information: https://www.instructables.com/member/kevinjwalters/

EffectSpectrogramFFT: 53x11
EffectRainbowFFT: 53x11
EffectClassicFFT: 53x11
EffectClassicTuner: 53x11

The v1.4.0 program hasn't been tested on the Cosmic Unicorn but should work with minor bugfixes and modifications to the C++ code.

Some short notes on how to compile the program are at the bottom of the repository page.

The development and testing used Pi Pico SDK version 1.5.1.

Step 3: Using the Guitar Tuner

This display mode is called Tuner and can be selected by pressing button A repeatedly until it appears. The note appears when the program thinks it has detected a note being played - it can take a fraction of a second to stabilise. The error is shown by the red bar on the display. Each pixel represents 2 cents. If the bar is centred then the note is within +/- 1 cent (1/100th of a semitone) and bar becomes green.

The video above shows some recorded notes from a 4-string electric bass and an acoustic guitar played sequentially in ascending order.

  1. 0:12 Bass fourth string: E1 (misidentified as E2)
  2. 0:20 Bass third string: A1
  3. 0:28 Bass second string: D2
  4. 0:35 Bass first string: G2


  1. 0:44 Acoustic sixth string: E2
  2. 0:50 Acoustic fifth string: A2
  3. 0:56 Acoustic fourth string: D3
  4. 1:03 Acoustic third string: G3
  5. 1:10 Acoustic second string: B3
  6. 1:16 Acoustic first string: E4

Step 4: Using the Spectrum Analyser

There are three display modes showing the a section of the audible frequency spectrum in various ways. These can be selected by pressing button A repeatedly until the name appears. The button can be pressed again to skip the scrolling display of the name.

Spectrogram

This displays the volume of the frequency bands on the top line represented by the luminance of blue. The average value is scrolled downwards after a few updates.

Rainbow Spectrum Analyser

This displays the volume of the frequency bands using the vertical pixels like a y-axis plot with 11 levels. The peak value is shown and held for a while.

The colours are mainly for fun but could be useful to indicate frequency bands if the user knows which colours map to which frequency bands.

Spectrum Analyser

This is the same as the above analyser with a different colour scheme. The colours vary only vertically in the style of a digital VU meter. Red is used to indicate the peaks regardless of the level.

Step 5: Program Evolution

The original Blunicorn program had two display modes, the rainbow and the normal (classic) spectrum analyser, where the choice was made at compile time yielding a single program with just the chosen mode. The audio source was Bluetooth only. The Galactic Unicorn appeared to other devices in the same was as a Bluetooth speaker. The audio was played on the Unicorn's small 1W speaker.

At a high level the changes and additions to the code are:

  • Added an ADC input using DMA to read an analogue input for a microphone.
  • The build is now configured to create two UF2 programs, one for Bluetooth, one for DMA ADC. The need for two programs is solely due to an issue with the Bluetooth code not coexisting well with the DMA ADC code.
  • For Display / GalacticUnicorn class
  • Using the latest Pimoroni Galactic and Cosmic libraries to replace the cut-down, modified Display class.
  • Added basic scrolling functionality.
  • Enhanced init member function to allow control over
  • audio rate and enablement to prevent multiple initialisations of I2S/PIO/DMA/IRQ) and
  • PIO selection.
  • Supporting all display modes selectable via button A.
  • Added a waterfall-style spectrogram.
  • Added a tuner mode using an efficient autocorrelation algorithm.
  • Enhanced code to cater for multiple sample rates.
  • Using some of the Pimoroni Picographics libraries like bitmap fonts. This is needed for the tuner to show the detected note name and to announce each display mode.
  • The support for parallel processing using the additional core has been removed for now.
  • Slowly moving the code towards a more modern C++17 style. The Pi Pico SDK build system defaults to the C++17 standard.

The image above is the output from DALL-E 3 given the challenging task of rendering an image based on the input: An image representing the evolution of a c++ program on a Pimoroni Galactic Unicorn to include a microphone, guitar tuner and waterfall spectrogram.

Step 6: Continuous ADC Using DMA

The Pi Pico W's RP2040 (system diagram above on the left) microcontroller has a flexible Direct Memory Access (DMA) controller. This can read and write from/to peripherals and memory concurrently and independently of the two processors. This can be a very useful feature for reading from the analogue to digital converter (ADC) at a constant (jitter-free) rate into an array of samples in memory (a buffer).

The DMA system features chaining which allows one transfer to start when another finishes. This can be used to read data continuously into one buffer and then seamlessly into a second buffer when the first is full allowing the data in the first to be extracted and processed. When the second buffer is full it returns to the first in a round-robin style using DMA "ping-pong". The image above on the right shows how this works on another ARM microcontroller, the TI TM4C1294NCPDT.

There is a detailed explanation of DMA in Pi Pico ADC input using DMA and MicroPython. There's a description in Realtime Audio FFT to VGA Display with RP2040 with code that uses chaining in a different way. This stepper motor analyzer code adc_dma.cpp uses the ping-pong style of chained DMA channels to populate two buffers.

DMA code can be difficult to debug - developing a small DMA only program and getting that to work correctly before integrating the code into a large program may help.

There is a clash between DMA ADC and Bluetooth on the Pi Pico W which is not fully understood at the moment.

Step 7: RP2040 Features and Limitations

The features of the RP2040 microcontroller on the Pi Pico W that are relevant to this project are listed below.

  1. No floating-point unit (FPU) since it's based on the Cortex-M0+ design.
  2. High clock speed for a low-cost microcontroller and over-clocking options on Pi Pico (W) board.
  3. Dual core.
  4. 12bit ADC with sample rates up to 500ksps which can be read via DMA.

For algorithms used in real-time applications or ones which have time critical sections some of the following techniques for the implementation may be useful in places:

  1. maximising compile-time calculations, avoiding unnecessary calculations, type conversions and copying/movement of data;
  2. using lookup tables;
  3. approximations for computationally intensive steps;
  4. loop fusion, either manual or via compiler optimiser;
  5. caution with use of (32bit) float as operations will be slow due to software implementation;
  6. only use (64bit) double when absolutely necessary;
  7. consider fixed-point maths for heavy use;
  8. use of inline functions (or macros);
  9. high compiler optimisation levels.

Profiling computationally intensive sections can help to observe the effect of code changes.

Step 8: How Does the Guitar Tuner Work?

The Spectrum Analyser uses the Fast Fourier Transform (FFT) which will show the constitutent frequencies in a signal. As can be seen later in this article the resolution is typically coarse and even with techniques to enhance the resolution it is not the optimal way to accurately find the pitch (fundamental frequency) of a single note on a typical microcontroller. Cycfi Research: Fast and Efficient Pitch Detection: Bitstream Autocorrelation by Joel de Guzman has a good description of how the pitch of a singe note can be determined. The steps are:

  1. Create a single bit representation of the 16bit audio signal** using a 1 for positive and 0 for negative with some hysteresis to reduce the effect of low-level noise.
  2. Use XORautocorrelation to compare the first half of the audio buffer with an offset (often referred to as lag) section of the buffer. A limited range of lag offsets is used to cover a specific frequency range to make the algorithm run quicker. The code currently uses 30Hz-500Hz, this range includes notes B0 to B4.
  3. Check the periods where there are high correlations and examine harmonics to determine the likely fundamental frequency.
  4. Improve the precision of the integer period by interpolation at zero-crossing points yielding a real value.

The fourth step is not used in the Unicorn code. It doesn't work well in this real world application due to the presence of fairly high degree of noise especially on quiet sources which will have a low signal-to-noise ratio (SNR).

The image above shows a simulation of the autocorrelation values being calculated in (the computer language) R with real data from a Galactic Unicorn and Sound Detector. A duplicate of the audio signal is shown below it which slides along with the signals in the yellow regions being compared to look for a match. The classic mathematical approach is shown as the normalised correlation (in green) where a peak near 1.0 indicates the signal matches.

An XNOR operation indicates the sign of the result of multiplication - this allows it to be used to correlate single bit values. Using XOR is more efficient as it's implemented in hardware in the ALU. The bitstream XOR correlation approach used by the program produces a count where a trough near 0 indicates the signal matching. The frequency of 82.4Hz is a match but there will also be matches with multiple waveforms at 41.2, 27.5, 20.6, etc.

The Unicorn code adds the following steps.

  1. Detection of the degree of certainty of correlation to try to prevent false detection. If no significant pitch is detected then this is represented internally in the code by a frequency of 0.0.
  2. Storage of the last five frequency values.
  3. A more reliable estimate of the frequency is achieved by discarding the minimum and maximum values and averaging the remaining values ignoring any 0.0 values.

The calculated frequency is then quantised to an equal temperament note and the difference between the value and the quantised value is the error shown by the moving bar. On the Galactic Unicorn a linear scale of 2 cents (1/50th of a semitone) per pixel is used.

Sampling Rate

The first implementation used the same rate as the Bluetooth source, 44.1ksps**. The note A4 (440Hz) has a period of 100.23 samples. If the autocorrelation is out by 1 then the error will be considerable for the purpose of tuning. For this note in the fourth octave, at 44.1ksps:

  • period 101 = 436.63Hz, error is -13.2 cents;
  • period 100 = 441Hz, error is +3.9 cents;
  • period 99 = 445.45Hz, error is +21.3 cents.

The (abandoned) zero-crossing interpolation code is an attempt to deal with this issue but empirically it doesn't work well at low SNR. The high sample rates which would not normally be used for audio on offer by the RP2040 microcontroller's ADC are helpful here. The rate of 96ksps with a 6144 element sample buffer was eventually chosen to give a good balance of precision, frequency range and performance. For A4 at 96ksps:

  • period 219 = 438.36Hz, error is -6.5 cents;
  • period 218 = 440.37Hz, error is +1.4 cents;
  • period 217 = 442.40Hz, error is +9.4 cents.

The precision improves with lower notes, e.g. A2 (110Hz):

  • period 874 = 109.84Hz, error is -2.5 cents;
  • period 873 = 109.97Hz, error is -0.5 cents;
  • period 872 = 110.09Hz, error is +1.4 cents.

** The ADC is 12bit (with ENOB around 9) but the samples are converted to a signed 16bit representation for processing.

*** The closest sample rate the ADC clock divider on the RP2040 can provide is 44.117647ksps.

Step 9: How Does the Spectrum Analyser Work?

The samples of the signal (two different ones are shown in blue above) have time on the x axis and being discrete samples they only form a continuous line due to the way they are plotted with lines rather than dots. To look at the frequencies which make up this signal it needs to be transformed from the time domain into the frequency domain. The Fourier Transform can be used for this, the Discrete Fourier Transform (DFT) is used for digital signals and the Fast Fourier Transform (FFT) is an efficient implementation of that. The FFT produces a sequence of complex numbers as an output, the magnitude of those complex values represents the amplitude of the constituent sine waves, the angle represents the phase which isn't of interest here. These values are sometimes referred to as bins as each represents a small range of frequencies.

The image above shows the FFT values being calculated in R with real data from a Galactic Unicorn and Sound Detector. Each of the signals has 80000 samples and for this particular example 9 FFTs are performed on subsets using a windowing function. The resulting chart shows how the constituent frequencies change over time. The colours represent the magnitude of the FFT in decibels (dB). The y axis is zoomed in to show only 0-1kHz frequencies, the data will include values up to 48kHz due to the unusually high 96ksps sample rate. The green lines are the exact frequencies for the expected notes with the fundamental as a dotted line and seven potential overtones as dashed lines. The particular guitar will determine the ratio of the magnitudes of the overtones to the fundamental.

The C++ implementation on the Galactic Unicorn is similar to the R one with the following differences.

  1. The calculations are performed in 16.15bit fixed-point.
  2. The subset of the signal is 2048 samples (with no overlap). This equates to 21.33ms.
  3. The algorithm is implemented to allow the calculation to be performed in-place.
  4. The sine and cosine functions are implemented with a table lookup on precomputed values.
  5. The magnitude function uses an approximation for the square root part of the calculation.
  6. A 20 phon loudness contour is applied.

The program displays the first 53 bins on the display's 53 horizontal pixels skipping the first one which holds the DC component of the signal. The visible frequency ranges from 96000/2048*1 = 46.88Hz to 96000/2048*53 = 2484Hz. The maximum frequency of 96000/2048*1024 = 48kHz which is not shown. These frequencies are the centre frequencies of the bins which span +/- 23.44Hz.

A more detailed explanation can be found in Understanding the Cooley_Tukey FFT.

Step 10: Spectrum Analyser Example With Sine Wave Sweep

The video above shows the spectrum analyser's output for a sine wave which increases slowly in frequency between 20Hz and 2973Hz. For comparison, here are some frequency ranges:

  • 20Hz - 20kHz - commonly cited human hearing range but upper limit will be higher for young people and lower for old people.
  • 300Hz – 3400Hz - traditional bandwidth for analogue telephones (landlines).
  • 130Hz (C3) - 1865Hz Bb6 - Cyndi Lauper's four-octave singing vocal range.
  • 85Hz - 155Hz - typical male speaking vocal range, fundamental frequency only.
  • 165Hz - 255Hz - typical female speaking vocal range, fundamental frequency only.
  • 440Hz - A4 reference frequency.

A single peak might be expected but it ends up having a width of 3-4 pixels (FFT bins) due to spectral leakage. The variation in level could be caused by a variety of factors including wave interference from the two speakers and subwoofer emitting the mono sine wave and any reflections of that sound in the room.

Step 11: Fixing a Bug

In an earlier pre-release version of the code the autocorrelation was working sporadically. It wasn't clear whether this was simply due to pitch detection from the microphone being challenging for this approach/algorithm or something else.

Some short audio samples at 44.1ksps were exported from the Galactic Unicorn and can be seen above. There are two different samples:

  • the top one is a louder sample with the microphone 15cm from the speaker playing the (decaying) guitar note;
  • the bottom one is quieter as the microphone is now 1m from the speaker playing the same note.

At a glance they look like a regular, repeating sequence of waveforms but on closer inspection and with the aid of markers (green lines) showing the period of the fundamental frequency it becomes more obvious there's a problem. The red arrows highlight what appears to be a difference in one of the waveforms on both the top and bottom signals.

A first guess was there was a occasional, brief pause in the ADC or DMA operation. The actual problem turned out to be some incorrect logic in selecting the inactive audio buffer for reading/processing from the pair of audio buffers. The active buffer was being read which would contain half new data as the DMA writes to it but would still have half of the previous data too creating a messy mix of two samples.

Step 12: Going Further

Ideas for areas to explore:

  • Easy
  • Add a basic VU or loudness meter.
  • Offer various x scales for the spectrum analyser and spectrogram selectable via a button.
  • Calculate the DC offset of the microphone rather than assuming it is exactly at the ADC midpoint (~1.65V) to facilitate conversion to a signed AC representation of the audio without DC bias.
  • Measure the noise level from the microphone to set an appropriate level for the zero-crossing threshold.
  • Automatic detection of the DC offset for the microphone board's audio output.
  • Finish the internal profiling code to aid real-time performance monitoring and debugging.
  • Increase clock speed from 125MHz (Pi Pico default) to133MHz (datasheet recommended maximum).
  • Intermediate
  • Check how the well the current algorithm works with low guitar notes like E1. The video shows the recorded bass guitar's fourth string (E1) being misidentified as E2 although this could be due to the use of a satellite speaker system with the microphone being placed away from the subwoofer.
  • Investigate the high degree of noise when using the SparkFun Sound Detector with the Pi Pico W.
  • Look at Pimoroni's PIO-based display driver code to see if there's value in a mode where it updates on demand rather than the current continuous approach.
  • Test the Bluetooth uf2 to ensure it still works. Fix likely issues
  • redesign the text display code as its delays and lengthy operation don't coexist well with the BTstack main loop;
  • fix any 44.1k vs 96k issues.
  • Evaluate potential optimisations and refinements for the tuner's autocorrelation code:
  • computing only values near the last (reliable) note's harmonics first;
  • adjusting the sample rate dynamically;
  • check to see if the period from the integer lag can be made more accurate by applying a small adjustment based on the lag values either side of it.
  • Add some unit tests!
  • Expert
  • Work out why Bluetooth stack and CYW43439 driver combination are incompatible with DMA ADC.
  • Find an elegant approach for making use of the second core for all/most modes, possibly with a variation based on the audio source.
  • Cautiously experiment with over-clocking (i.e. beyond 133MHz) with single and dual core use. A wise approach would be using a separate, disposable Pi Pico board, monitoring the RP2040 temperature, profiling power use and checking application does not crash/misbehave over long duration runs.

Related projects:

Related smartphone/tablet apps:

Background reading:

Step 13: Appendix: More Autocorrelation Examples

G3 sine wave

A perfect 16bit resolution sine wave. This signal has no noise and no DC offset.

Korg Volca Bass sawtooth E2

An amplified Korg Volca Bass playing an E2 note using one sawtooth** oscillator with an open filter recorded by the Galactic Unicorn and Sound Detector.

Room noise from microphone

A real world recording of a quiet room from the Galactic Unicorn and Sound Detector. The sample sounds very noisy when this is played and the nearby fans in a desktop computer cannot be perceived. This is probably dominated by electrical noise. The bitstream version in red with many values that are high rather than low suggests the signal could have a small positive DC offset.

** The electrical waveform looks like a crisper sawtooth directly from the Volca Bass's line/headphone output.

Anything Goes Contest

Participated in the
Anything Goes Contest