Audio Spectrum analyser - FFT using mbed-dsp - driving RGB(W) LED or PC python script.

Dependencies:   HSI2RGBW_PWM NVIC_set_all_priorities mbed-dsp mbed FastAnalogIn

Spectrum Analyzer

Code ported from Tony DiCola at AdaFruit FFT: Fun with Fourier Transforms.
Modifications were made to allow the use of the KL25Z on-board RGB LED or an external RGBW power LED.
See items marked with * in the next sections.

Main features

  • Uses mbed-dsp library.
  • Uses FastAnalogIn to allow a sample rate of 40kHz.
  • Display the audio spectrum on a single RGB(W) LED*.
  • Display the audio spectrum on your computer using an audio spectrogram tool (python script).
  • Change parameters using a terminal connection : Sample rate, min/max db, slowdown*.

Information

Detailed information and download of the python scripts is available here.

KL25Z wiring

Audio inputs
The software samples a single audio channel at 40kHz and applies a Fourier transform to return the frequency spectrum.
Analog input : PTC2.
A DC offset, and possibly some amplification or attenuation, is needed before the signal is fed into the analog inputs.
The opamp choice is not critical, just make sure it supports single supply operation.
Schematic
Currently, only one channel is used when the KL25Z is sampling at 40kHz.
/media/uploads/frankvnk/opamp_input_stage-tlc2264.png

External PWM outputs

PinColor
PTD4Red
PTA12Green
PTA4Blue
PTA5White

If you want to use a RGB LED, remove the last pin declaration (PTA5).
The conversion routine automatically switches from HSI/RGBW to HSI/RGB.
To use the external RGBW LED, enable following line in the code:

#define RGBW_ext // Disable this line when you want to use the KL25Z on-board RGB LED.

Commands

Parameters can be altered through the serial port Using a terminal program (eg : TeraTerm).
Communication settings : 38400 baud + local echo.
Each command needs to be terminated with a semicolon.
Use GET <command>; to read a parameter.
Use SET <command> <value>; to set a parameter.

CommandDescription
GET MAGNITUDES;Reads back the FFT magnitudes.
Number of magnitudes = PIXEL_COUNT.
GET SAMPLES;Reads back the current samples.
Number of samples = PIXEL_COUNT.
GET FFT_SIZE;The size of the FFT.
GET SAMPLE_RATE_HZ;Audio sample rate (Hz).
SET SAMPLE_RATE_HZ <value>;Change the audio sample rate (see also 'Limitations' below).
GET LEDS_ENABLED;LEDs enabled status.
SET LEDS_ENABLED <value>;Control if the LED's should display the spectrum or not.
1 is true, 0 is false.
GET SPECTRUM_MIN_DB;Audio intensity (in decibels) that maps to low LED brightness.
SET SPECTRUM_MIN_DB <value>;Change low sensitivity (0...100dB).
GET SPECTRUM_MAX_DB;Audio intensity (in decibels) that maps to high LED brightness.
SET SPECTRUM_MAX_DB <value>;Change high sensitivity (0...100dB).
GET SLOWDOWN;LED visualisation delay.
SET SLOWDOWN <value>;* Useful to visualize the spectrum using a single RGB(W) LED.
Without this command, the color values are shown too fast for the human eye.
This allows you to slow down the visualization without interfering in the FFT conversion.
Each frequency window is shown a little longer.
The number of frequency windows depends on the value of the PIXEL_COUNT variable.
[0...999] The larger the value, the longer each frequency window is shown - a good value is 4 when PIXEL_COUNT = 32 (choose a higher SLOWDOWN value when PIXEL_COUNT is lowered).
[1000...1000 + PIXEL_COUNT] Selecting a value within this range allows us to lock to a specific frequency window.

NOTE : PIXEL_COUNT is declared at compile time and determines the number of frequency windows (aka LED colors).

Limitations

The original code was written for a cortex-M4 processor.
For a cortex-M0 processor, following limitations apply:

SAMPLE_RATE_HZFFT_SIZE
1...40000max 64

Demo Videos

Parameter settings

SLOWDOWN16 (initial value - changed to 1000+ during the video to demonstrate the lock option).
SAMPLE_RATE_HZ40000
SPECTRUM_MIN_DB40.0
SPECTRUM_MAX_DB80.0
FFT_SIZE64
PIXEL_COUNT32

Using the on-board RGB LED
SLOWDOWN is set to different values (normal mode and locked mode).

Using an external 10W RGBW LED
SLOWDOWN is set to different values (normal mode and locked mode).

Spectrogram on Computer screen (serial input from KL25Z board

main.cpp

Committer:
frankvnk
Date:
2014-03-08
Revision:
2:035d551759a5
Parent:
1:736b34e0f484

File content as of revision 2:035d551759a5:

// Audio Spectrum Display
// Copyright 2013 Tony DiCola (tony@tonydicola.com)
// Code ported from the guide at http://learn.adafruit.com/fft-fun-with-fourier-transforms?view=all

#include "mbed.h"
#include "NVIC_set_all_priorities.h"
#include <ctype.h>
#include "arm_math.h"
#include "arm_const_structs.h"
#include "hsi2rgbw_pwm.h"
#include "FastAnalogIn.h"

Serial pc(USBTX, USBRX);

FastAnalogIn   Audio(PTC2);

//#define RGBW_ext // Disable this line when you want to use the KL25Z on-board RGB LED.

#ifndef RGBW_ext
// HSI to RGB conversion with direct output to PWM channels - on-board RGB LED
hsi2rgbw_pwm led(LED_RED, LED_GREEN, LED_BLUE);
#else
// HSI to RGBW conversion with direct output to external PWM channels - RGBW LED
hsi2rgbw_pwm led(PTD4, PTA12, PTA4, PTA5); //Red, Green, Blue, White
#endif

// Dummy ISR for disabling NMI on PTA4 - !! DO NOT REMOVE THIS !!
// More info at https://mbed.org/questions/1387/How-can-I-access-the-FTFA_FOPT-register-/
extern "C" void NMI_Handler() {
    DigitalIn test(PTA4);
}


////////////////////////////////////////////////////////////////////////////////
// CONFIGURATION
// These values can be changed to alter the behavior of the spectrum display.
// KL25Z limitations
// -----------------
// - When used with the Spectrogram python script :
//   There is a substantial time lag between the music and the screen output.
//   Max allowed SAMPLE_RATE_HZ is 40000
//   Max allowed FFT_SIZE is 64
////////////////////////////////////////////////////////////////////////////////

int SLOWDOWN = 4;                       // Create an optical delay in spectrumLoop - useful when only one RGB led is used.
                                        // Only active when nonzero.
                                        // A value >= 1000 and <= 1000 + PIXEL_COUNT fixes the output to a single frequency
                                        // window = a single color.
int SAMPLE_RATE_HZ = 40000;             // Sample rate of the audio in hertz.
float SPECTRUM_MIN_DB = 30.0;           // Audio intensity (in decibels) that maps to low LED brightness.
float SPECTRUM_MAX_DB = 80.0;           // Audio intensity (in decibels) that maps to high LED brightness.
int LEDS_ENABLED = 1;                   // Control if the LED's should display the spectrum or not.  1 is true, 0 is false.
                                        // Useful for turning the LED display on and off with commands from the serial port.
const int FFT_SIZE = 64;                // Size of the FFT.
const int PIXEL_COUNT = 32;             // Number of pixels.  You should be able to increase this without
                                        // any other changes to the program.
const int MAX_CHARS = 65;               // Max size of the input command buffer

////////////////////////////////////////////////////////////////////////////////
// INTERNAL STATE
// These shouldn't be modified unless you know what you're doing.
////////////////////////////////////////////////////////////////////////////////
const static arm_cfft_instance_f32 *S;
Ticker samplingTimer;
float samples[FFT_SIZE*2];
float magnitudes[FFT_SIZE];
int sampleCounter = 0;
char commandBuffer[MAX_CHARS];
float frequencyWindow[PIXEL_COUNT+1];
float hues[PIXEL_COUNT];
bool commandRecv = 0;
////////////////////////////////////////////////////////////////////////////////
// UTILITY FUNCTIONS
////////////////////////////////////////////////////////////////////////////////

void rxisr() {
    char c = pc.getc();
    // Add any characters that aren't the end of a command (semicolon) to the input buffer.
    if (c != ';') {
        c = toupper(c);
        strncat(commandBuffer, &c, 1);
    } else {
        // Parse the command because an end of command token was encountered.
        commandRecv = 1;
    }
}

// Compute the average magnitude of a target frequency window vs. all other frequencies.
void windowMean(float* magnitudes, int lowBin, int highBin, float* windowMean, float* otherMean)
{
    *windowMean = 0;
    *otherMean = 0;
    // Notice the first magnitude bin is skipped because it represents the
    // average power of the signal.
    for (int i = 1; i < FFT_SIZE/2; ++i) {
        if (i >= lowBin && i <= highBin) {
            *windowMean += magnitudes[i];
        } else {
            *otherMean += magnitudes[i];
        }
    }
    *windowMean /= (highBin - lowBin) + 1;
    *otherMean /= (FFT_SIZE / 2 - (highBin - lowBin));
}

// Convert a frequency to the appropriate FFT bin it will fall within.
int frequencyToBin(float frequency)
{
    float binFrequency = float(SAMPLE_RATE_HZ) / float(FFT_SIZE);
    return int(frequency / binFrequency);
}


////////////////////////////////////////////////////////////////////////////////
// SPECTRUM DISPLAY FUNCTIONS
///////////////////////////////////////////////////////////////////////////////

void spectrumSetup()
{
    // Set the frequency window values by evenly dividing the possible frequency
    // spectrum across the number of neo pixels.
    float windowSize = (SAMPLE_RATE_HZ / 2.0) / float(PIXEL_COUNT);
    for (int i = 0; i < PIXEL_COUNT+1; ++i) {
        frequencyWindow[i] = i*windowSize;
    }
    // Evenly spread hues across all pixels.
    for (int i = 0; i < PIXEL_COUNT; ++i) {
        hues[i] = 360.0*(float(i)/float(PIXEL_COUNT-1));
    }
}

void spectrumLoop()
{
    // Update each LED based on the intensity of the audio
    // in the associated frequency window.
    static int SLrpt = 0, SLpixcnt = 0;
    int SLpixend = 0;
    float intensity, otherMean;
    if(SLOWDOWN != 0)
    {
        if(SLOWDOWN >= 1000)
        {
            if(SLOWDOWN <= (1000 + PIXEL_COUNT-1))
            {
                SLpixcnt = SLOWDOWN - 1000;
                SLrpt = 0;
                SLpixend = SLpixcnt + 1;
            }
            else
                SLOWDOWN = 0;
        }
        else
        {
            SLrpt++;
            if (SLrpt >= SLOWDOWN)
            {
                SLrpt = 0;
                SLpixcnt = SLpixcnt < PIXEL_COUNT-1 ? ++SLpixcnt : 0;
            }
            SLpixend = SLpixcnt + 1;
        }
    }
    else
    {
        SLpixcnt = 0;
        SLrpt = 0;
        SLpixend = PIXEL_COUNT;
    }
    for (int i = SLpixcnt; i < SLpixend; ++i) {
        windowMean(magnitudes,
                   frequencyToBin(frequencyWindow[i]),
                   frequencyToBin(frequencyWindow[i+1]),
                   &intensity,
                   &otherMean);
        // Convert intensity to decibels.
        intensity = 20.0*log10(intensity);
        // Scale the intensity and clamp between 0 and 1.0.
        intensity -= SPECTRUM_MIN_DB;
        intensity = intensity < 0.0 ? 0.0 : intensity;
        intensity /= (SPECTRUM_MAX_DB-SPECTRUM_MIN_DB);
        intensity = intensity > 1.0 ? 1.0 : intensity;
        led.hsi2rgbw(hues[i], 1.0, intensity);
    }
}


////////////////////////////////////////////////////////////////////////////////
// SAMPLING FUNCTIONS
////////////////////////////////////////////////////////////////////////////////

void samplingCallback()
{
    // Read from the ADC and store the sample data
    samples[sampleCounter] = (1023 * Audio) - 511.0f;
    // Complex FFT functions require a coefficient for the imaginary part of the input.
    // Since we only have real data, set this coefficient to zero.
    samples[sampleCounter+1] = 0.0;
    // Update sample buffer position and stop after the buffer is filled
    sampleCounter += 2;
    if (sampleCounter >= FFT_SIZE*2) {
        samplingTimer.detach();
    }
}

void samplingBegin()
{
    // Reset sample buffer position and start callback at necessary rate.
    sampleCounter = 0;
    samplingTimer.attach_us(&samplingCallback, 1000000/SAMPLE_RATE_HZ);
}

bool samplingIsDone()
{
    return sampleCounter >= FFT_SIZE*2;
}


////////////////////////////////////////////////////////////////////////////////
// COMMAND PARSING FUNCTIONS
// These functions allow parsing simple commands input on the serial port.
// Commands allow reading and writing variables that control the device.
//
// All commands must end with a semicolon character.
//
// Example commands are:
// GET SAMPLE_RATE_HZ;
// - Get the sample rate of the device.
// SET SAMPLE_RATE_HZ 400;
// - Set the sample rate of the device to 400 hertz.
//
////////////////////////////////////////////////////////////////////////////////

void parseCommand(char* command)
{
    if (strcmp(command, "GET MAGNITUDES") == 0) {
        for (int i = 0; i < FFT_SIZE; ++i) {
            printf("%f\r\n", magnitudes[i]);
        }
    } else if (strcmp(command, "GET SAMPLES") == 0) {
        for (int i = 0; i < FFT_SIZE*2; i+=2) {
            printf("%f\r\n", samples[i]);
        }
    } else if (strcmp(command, "GET FFT_SIZE") == 0) {
        printf("%d\r\n", FFT_SIZE);
    } else if (strcmp(command, "GET SAMPLE_RATE_HZ") == 0) {
        printf("%d\r\n", SAMPLE_RATE_HZ);
    } else if (strstr(command, "SET SAMPLE_RATE_HZ") != NULL) {
        SAMPLE_RATE_HZ = (typeof(SAMPLE_RATE_HZ)) atof(command+(sizeof("SET SAMPLE_RATE_HZ")-1));
    } else if (strcmp(command, "GET LEDS_ENABLED") == 0) {
        printf("%d\r\n", LEDS_ENABLED);
    } else if (strstr(command, "SET LEDS_ENABLED") != NULL) {
        LEDS_ENABLED = (typeof(LEDS_ENABLED)) atof(command+(sizeof("SET LEDS_ENABLED")-1));
    } else if (strcmp(command, "GET SPECTRUM_MIN_DB") == 0) {
        printf("%f\r\n", SPECTRUM_MIN_DB);
    } else if (strstr(command, "SET SPECTRUM_MIN_DB") != NULL) {
        SPECTRUM_MIN_DB = (typeof(SPECTRUM_MIN_DB)) atof(command+(sizeof("SET SPECTRUM_MIN_DB")-1));
    } else if (strcmp(command, "GET SPECTRUM_MAX_DB") == 0) {
        printf("%f\r\n", SPECTRUM_MAX_DB);
    } else if (strstr(command, "SET SPECTRUM_MAX_DB") != NULL) {
        SPECTRUM_MAX_DB = (typeof(SPECTRUM_MAX_DB)) atof(command+(sizeof("SET SPECTRUM_MAX_DB")-1));
    } else if (strcmp(command, "GET SLOWDOWN") == 0) {
        printf("%d\r\n", SLOWDOWN);
    } else if (strstr(command, "SET SLOWDOWN") != NULL) {
        SLOWDOWN = (typeof(SLOWDOWN)) atoi(command+(sizeof("SET SLOWDOWN")-1));
    }

    // Update spectrum display values if sample rate was changed.
    if (strstr(command, "SET SAMPLE_RATE_HZ ") != NULL) {
        spectrumSetup();
    }

    // Turn off the LEDs if the state changed.
    if (LEDS_ENABLED == 0) {
    }
}

void parserLoop()
{
    // Process any incoming characters from the serial port
    while (pc.readable()) {
        char c = pc.getc();
        // Add any characters that aren't the end of a command (semicolon) to the input buffer.
        if (c != ';') {
            c = toupper(c);
            strncat(commandBuffer, &c, 1);
        } else {
            // Parse the command because an end of command token was encountered.
            parseCommand(commandBuffer);
            // Clear the input buffer
            memset(commandBuffer, 0, sizeof(commandBuffer));
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// MAIN FUNCTION
////////////////////////////////////////////////////////////////////////////////

int main()
{
    NVIC_set_all_irq_priorities(1);
    NVIC_SetPriority(UART0_IRQn, 0);
    // Set up serial port.
    pc.baud (38400);
    pc.attach(&rxisr);
#ifndef RGBW_ext
    led.invertpwm(1); //On-board KL25Z RGB LED uses common anode.
#endif 
    // Clear the input command buffer
    memset(commandBuffer, 0, sizeof(commandBuffer));

    // Initialize spectrum display
    spectrumSetup();

    // Begin sampling audio
    samplingBegin();

    // Init arm_ccft_32
    switch (FFT_SIZE)
    {
    case 16:
        S = & arm_cfft_sR_f32_len16;
        break;
    case 32:
        S = & arm_cfft_sR_f32_len32;
        break;
    case 64:
        S = & arm_cfft_sR_f32_len64;
        break;
    case 128:
        S = & arm_cfft_sR_f32_len128;
        break;
    case 256:
        S = & arm_cfft_sR_f32_len256;
        break;
    case 512:
        S = & arm_cfft_sR_f32_len512;
        break;
    case 1024:
        S = & arm_cfft_sR_f32_len1024;
        break;
    case 2048:
        S = & arm_cfft_sR_f32_len2048;
        break;
    case 4096:
        S = & arm_cfft_sR_f32_len4096;
        break;
    }

    while(1) {
        // Calculate FFT if a full sample is available.
        if (samplingIsDone()) {
            // Run FFT on sample data.
            // Run FFT on sample data.
            arm_cfft_f32(S, samples, 0, 1);
            // Calculate magnitude of complex numbers output by the FFT.
            arm_cmplx_mag_f32(samples, magnitudes, FFT_SIZE);

            if (LEDS_ENABLED == 1) {
                spectrumLoop();
            }

            // Restart audio sampling.
            samplingBegin();
        }

        // Parse any pending commands.
        if(commandRecv) {
//            pc.attach(NULL);
            parseCommand(commandBuffer);
            commandRecv = 0;
            // Clear the input buffer
            memset(commandBuffer, 0, sizeof(commandBuffer));
//            pc.attach(&rxisr);
        }
    }
}