Arduino VU meter and spectrum analyser

Here is my latest creation, an arduino based spectrum analyser runing the awesome FFT engine by elm-chan and VU meter, all in a simple 16x2 char display.

It was not really made using the Arduino IDE, but AvrStudio, but its still and Arduino :slight_smile:

Its still an open project, because there is still left a simple menu to adjust contrast, brightness and change between the spectrum analyser and vu meter modes, but here it is, its faster in real life, between 3 and 6 times faster than in the videos:

very nice!

do you have more details (src, schematic) on how you did it?

I purchased some 'helper chips' that would do a lot of offload and send in a single analog signal that would correspond to each of the 7 or so bands. are you using one of those chips or is this all in atmel space?

looks good, at any rate.

also, are you being strict to the VU meter specs? rise and fall time decays and such?

cool, man. nice stuff.

@Senso, good work man! Keep us informed. Would be nice to see what do you have for the audio input stage.

@linux-works: if he is doing FFT I doubt he would be using external ICs. But which are the helper chips you are talking about? I know the MSEG7 but it has a digital serial output, not analog.

yes, that's the chip.

check again - you clock out the ANALOG values to get the seven histogram display entries. its analog based in its 'levels' to the arduino. wish it WAS serial data in pure digital form, would be easier, overall.

regarding VU meters, my interest is more along the lines of having the arduino front-end an actual set of meter movements, real cheap ones, like these from ebay:

the idea would be the arduino would sample the audio and do the correct VU rise/fall time decays and send the 'math output' to the real meter movement in the form of a dc signal (maybe filtered pwm, maybe something better). you could also show peak or average and its 'just math' and the meter does not really even matter anymore.

I have not gotton around to writing the stuff to really emulate a true VU meter. that's what I was hoping to see. its rare to find VU meter code, for some reason, and even good hardware schematics are few and far between (again, for true-to-spec VU meters).

Argh! You are right, had a brain fart ;D

I'm using an op-amp with a full wave rectifier and some gain to tranform the audio signal into a 0-5v signal to feed into the arduino/atmega adc.
Then for the VU meter its a simple sampling and mapping for the 75 different values that I can show with the lcd, altough I know that a real VU meter as a logaritmic scale, when I have some time I will try to implement a real VU meter algorithm, for the FFT/spectrum analyzer I'm using elm-chan FFT engine, for the 64 points FFT and all the others variables used in the fft and vu meter functions I'm using less than 512bytes of ram and still under 3,5k of code.
The lcd is updated at about 14Hz using a timer interrupt routine.

Yes, I can show the source code, but almost all the comentaries are in portuguese, but I can translate it if some one requests.

This is the schematic for the input filter:

The filter was done with help from Macetech that show the schematic from its Shifty VU shield, as I also read this:
http://sound.westhost.com/appnotes/an001.htm

The lcd lib that I used is this one:
http://www.jump.to/fleury

And this is the FFT lib:
http://elm-chan.org/works/akilcd/report_e.html

And here is the source code, it didnt fit in the other post:

/*
*****************************************************************************
**   LCD VU meter and FFT spectrum analyser                           *
**   Using Peter Fleury lcd lib and el-chan fft engine for avr            *
**   Made for Atmega328p/Arduino Duemilanove                           *
**   Tiago Angelo   12/01/2011                                    *
**   V0.6                                                   *
**                                                         *
*****************************************************************************

****************************************************************************
**
**   Pinos do lcd - 16x2
**   1   2   3   4   5   6   7   8   9   10    11   12   13   14    15   16
**   Gnd   Vcc   Ctr   RS   RW   En   D0   D1   D2   D3    D4     D5   D6   D7    An     Cat
**PB         4   5                   0     1    2    3
**PD               7
**
**   Ctr - Contrast
**   An - Anode(+)
**   Cat - Cathode(-)
****************************************************************************
*/

#include <avr/io.h>
#include <stdlib.h>
#include <avr/interrupt.h>
#define F_CPU 16000000UL
#include <util/delay.h>
#include <math.h>
#include <inttypes.h>
#include <avr/pgmspace.h>

#include "lcd.h"
#include "ffft.h"

/*
** Defines usados no programa
*/
#define NUM_SAMPLES 64      //Samples usadas para calcular o FFT
#define FFT_SIZE (64/2)      //Numero de valores devolvidos pelo FFT

#define FULL 0xFF   //Caracter "cheio", consultar datasheet para perceber
#define BLANK 0xFE   //Caracter em branco

/*
***********************************************************************
** Constantes globais usadas no programa
***********************************************************************
*/

static const PROGMEM unsigned char vuChars[] = {   //Dados na flash que não são precisos na Ram para nada
   0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,   // 1 linha
   0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,   // 2 linhas
   0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,   // 3 linhas
   0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,   // 4 linhas
   0x00, 0x00, 0x1F, 0x10, 0x10, 0x10, 0x00, 0x00,   //simbolo L
   0x00, 0x00, 0x1F, 0x05, 0x0D, 0x12, 0x00, 0x00,   //Simbolo R
   };
static const PROGMEM unsigned char fftChars[] = {   //Dados na flash que não são precisos na Ram para nada
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,   //1 coluna
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F,   //2 coluna
   0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F,   //3 coluna
   0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F,   //4 coluna
   0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F,   //5 coluna
   0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F,   //6 coluna
   0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F,   //7 coluna
   0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F,   //7 coluna
   };

uint8_t i,k;                  //Variaveis de iterações
uint8_t sector2 = 0;            //Numero de colunas para o vu meter, linha 2
uint8_t sectorRest2 = 0;         //Numero de colunas para o vu meter, linha 2
uint8_t sector1 = 0;            //Numero de colunas para o vu meter, linha 1
uint8_t sectorRest1 = 0;         //Numero de colunas para o vu meter, linha 1
uint8_t count = 0;
volatile uint8_t j=0;            //variavel de iterações (só para a ISR)
volatile uint8_t lcd_linha1[16];   //Dados da linha 1 do lcd
volatile uint8_t lcd_linha2[16];   //Dados da linha 2 do lcd
uint16_t newReading1 = 0;         //Variavel para guardar o valor lido pelo ADC
uint16_t newReading2 = 0;         //Variavel para guardar o valor lido pelo ADC
uint16_t lastReading1 = 0;
uint16_t lastReading2 = 0;
uint16_t adcVal= 0;               //Usado para guardar o valor lido pelo adc no modo fft
uint32_t mapped1 = 0;            //Variavel para guardar o valor de adc_var_1*map
uint32_t mapped2 = 0;            //Variavel para guardar o valor de adc_var_2*map

//Estas 3 são especificas para o FFT
int16_t capture[FFT_N];         //Buffer de captura
complex_t bfly_buff[FFT_N];      //Buffer do FFT
uint16_t spectrum[(FFT_N/2)];   //Buffer de saida do FFT


/*
***********************************************************************
** Declarações dos protótipos das funções
***********************************************************************
*/

int adc_read(char channel);      //Função usada para ler um canal arbitrário do ADC
void adc_init(void);         //Função para inicializar o ADC
void vu_mode(void);
void vu_mode_init(void);      //Inicialização do modo VU meter
void fft_mode_init(void);
void fft_mode(void);
void timer1_init(void);         //Inicialização do Timer1
void lcd_test(void);

/*
***********************************************************************
** Inicio do main
***********************************************************************
*/

int main(void){

   adc_init();
   lcd_init(LCD_DISP_ON);         //Inicializa o LCD, sem cursor visivel
   lcd_clrscr();               //Limpa o lcd e coloca o cursor em (0,0)
   fft_mode_init();            //Inicialização do modo fft
   //vu_mode_init();            //Inicialização do modo vu meter
   timer1_init();               //Inicialização/configuração do timer para gerar as interrupções
   sei();                     //Inicia as interrupções

   while(1){                  //Loop infinito
      
      //vu_mode();            //Modo vu meter
      fft_mode();               //Modo fft
      //lcd_test();            //Modo de teste do lcd
      }

   return 1;
}

/*
***********************************************************************
** ISR
** Corre todo o vu_mode e faz o refresh do display.
** Todo o trabalho é feito por interrupção, deixando o CPU livre
** entre interrupções.
** Actualiza as duas linhas na totalidade.
***********************************************************************
*/

ISR(TIMER1_COMPA_vect){

   lcd_gotoxy(0,0);
   for(j=0; j<16; j++){
      lcd_putc(lcd_linha1[j]); }

   lcd_gotoxy(0,1);
   for(j=0; j<16; j++){
      lcd_putc(lcd_linha2[j]); }
   }

/*
***********************************************************************
**                     Funções usadas
***********************************************************************
*/

/*
***********************************************************************
** Inicializa o ADC no modo 10bits a 125Khz
***********************************************************************
*/

void adc_init(void){

   ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));   //16Mhz/128 = 125Khz
   ADMUX |= (1<<REFS0);                     //Referencia de 5v, com condensador no pino Aref
   ADCSRA |= (1<<ADEN);                     //Adc ligada
   ADCSRA |= (1<<ADSC);                     //Fazer uma primeira conversão para iniciar o circuito e porque é a mais lenta
}

/*
***********************************************************************
** Passa-se o canal a ler e devolve um valor de 10bits do ADC
***********************************************************************
*/

int adc_read(char channel){

   ADMUX &= 0xF0;                  //Limpa o canal anterior
   ADMUX |= channel;               //Define o novo canal a ler do ADC
   ADCSRA |= (1<<ADSC);            //Inicia uma nova conversão
   while(ADCSRA & (1<<ADSC));         //Espera que a conversão seja feita
   return ADCW;                  //Retorna o valor do ADC, em modo 10 bits
}

/*
***********************************************************************
** Le o canal 0 e 1 do adc, faz uma detecção de pico e depois mapeia
** o valor 0..1023 do adc para 0..75 barras no display 16x2
***********************************************************************
*/

void vu_mode(void){
   
   newReading1 = adc_read(0);
   newReading2 = adc_read(1);
   
   if(newReading1 > lastReading1){
       lastReading1 = newReading1; }
   else{
      lastReading1 = (lastReading1*3 + newReading1)/4; }   //Decaimento "suave"

   mapped1 = ((lastReading1 * 75)/1024);                  //Pega nos 0..1023 e devolve 0..75
   sector1 = mapped1/5;                              //Segmentos FULL na linha 0
   sectorRest1 = mapped1 % 5;                            //Segmento final da linha 0

   if(newReading2 > lastReading2){
       lastReading2 = newReading2; }
   else{
      lastReading2 = (lastReading2*3 + newReading2)/4; }   //Decaimento "suave"

   mapped2 = ((lastReading2 * 75)/1024);                  //Pega nos 0..1023 e devolve 0..75
   sector2 = mapped2/5;                              //Segmentos FULL na linha 1
   sectorRest2 = mapped2 % 5;                            //Segmento final da linha 1
   

   //Linha 0
   for(i=0; i<(sector1); i++){
      lcd_linha1[i+1] = FULL; }
   if(sectorRest1>=1){
      lcd_linha1[i+1] = ((sectorRest1-1)); }
   for(i=(sector1 + 1);i<15; i++){
      lcd_linha1[i+1] = BLANK; }

   //Linha 1
   for(i=0; i<(sector2); i++){
      lcd_linha2[i+1] = FULL; }
   if(sectorRest2>=1){
      lcd_linha2[i+1] = ((sectorRest2-1)); }
   for(i=(sector2 + 1);i<15; i++){
      lcd_linha2[i+1] = BLANK; }

}

And the rest, 10000 char limit per post is a little short :confused:

/*
***********************************************************************
** Le o canal 0 do adc, ao subtrair 512 á sample de 1023 bits cria um
** sinal positivo ou negativo centrado em 0, é preciso para o fft
** usando o FFT feito pelo elm-chan calcula um FFT de 64 pontos
** e preenche as duas linhas do lcd com barras
***********************************************************************
*/

void fft_mode(void){
   count = 0;
   adc_read(0);
   cli();
   while(count != NUM_SAMPLES){
      ADCSRA |= (1<<ADSC);
      while((ADCSRA & (1<<ADSC))){};
      adcVal = ADCW;
      capture[count] = ((int16_t)(adcVal)-512);
      count++;
      }
   sei();

   fft_input(capture,bfly_buff);
   fft_execute(bfly_buff);
   fft_output(bfly_buff,spectrum);
   
   k=0;
   for(i=1; i<17; i++){
      sector1 = spectrum[i]/16;

   if(sector1>7){
      lcd_linha2[k]=FULL;
      lcd_linha1[k]=(sector1-8);
      }
   else{
      lcd_linha2[k]=sector1;
      lcd_linha1[k]=BLANK;
      }

      k++;

   }
}

/*
***********************************************************************
** Função de teste usada para afinar o gerador de barras verticais
***********************************************************************
*/

void lcd_test(void){

   for(i=0; i<16; i++){
   
   sector1=i;

   if(sector1>7){
      lcd_linha2[i]=FULL;
      lcd_linha1[i]=(sector1-8);
      }
   else{
      lcd_linha2[i]=sector1;
      lcd_linha1[i]=BLANK;
      }

   }


}

/*
***********************************************************************
** Carrega da flash os caracteres especiais para fazer as barras e as
** letras L e R na CGRAM do display
***********************************************************************
*/

void vu_mode_init(void){

   lcd_command(_BV(LCD_CGRAM));                  //Coloca CG RAM no endereço 0
   for(i=0; i<48; i++){
      lcd_data(pgm_read_byte_near(&vuChars[i])); }   //Lê os dados da flash e carrega na Ram do LCD

   lcd_gotoxy(0,0);   //Linha 0 coluna 0
   lcd_putc(4);      //Escreve L na esquerda
   lcd_gotoxy(0,1);   //Linha 1 coluna 0
   lcd_putc(5);      //Escreve R na direita
   lcd_linha1[0]=4;
   lcd_linha2[0]=5;
}

/*
***********************************************************************
** Carrega da flash os caracteres especiais para fazer as barras e as
** na CGRAM do display
***********************************************************************
*/

void fft_mode_init(void){
   
   lcd_command(_BV(LCD_CGRAM));                  //Coloca CG RAM no endereço 0
   for(i=0; i<64; i++){
      lcd_data(pgm_read_byte_near(&fftChars[i]));   }   //Lê os dados da flash e carrega na Ram do LCD

   lcd_clrscr();
}

/*
***********************************************************************
** Inicializa o timer1(16 bits) no modo CTC com prescaller de 1024
***********************************************************************
*/

void timer1_init(void){

   TCCR1B |= (1 << WGM12);         // Configure timer 1 for CTC mode
   OCR1A = 1100;               //Para gerar interrupções a 14Hz para o refresh do display, valor obtido experimentalmente
   TIMSK1 |= (1 << OCIE1A);       // Enable CTC interrupt
   TCCR1B |= ((1<<CS12)|(1<<CS10));//Inicia timer 1 com clock div de 1024
}