Arduino Goes to Town, Part 5.

 
Using a microcontroller for button debouce and input selection might be overkill but it can also be used for things like a spectrum analyser. The final result turned out better than expected. This is what it looks like in normal mode:


The colour is slowly cycling so it doesn't get too boring. I had a mode button on the frontpanel to switch between VU and PPM mode when I had VU-meters on the front. I reused the mode button to switch between the normal mode and Rainbow mode:


I think this switch could be utilised a bit better. I have tried all kinds of display modes. Eight band from left to right, from the middle and out, bars four pixels wide with a bit of shading Mario style. I wish I thought about using it to switch to a VU meter mode because I would then have made the sound interface support two channels. It could actually have been super useful to measure the input signal making sure it doesn't clip.

The sound interface is now connected in parallel with the source instead of parallel to the DSP output as when I had VU-meters on the front. The min-max scaling is normalising the response so it is not input level dependent. It makes sure its alway busy on the front panel. It would be quite obnoxious if the LDR didn't adjust the LED panel intensity based on the ambient light. I also choose to only use peak values instead of also redraw the bars based on current level. It is totally doable but it just got to busy for my taste.

The code below is loaded with defines used to tweak the output. Like max and min values of LDR for dimming level, decay and peak hold thresholds, matrix x and y values, pin assignments etc. Its all tweaked to my amplifier and likings. Your milage may differ but it might serve as an inspiration. I sometimes force a bitwise operation even though the compiler probably would have done it anyway. Its more for my own mental house keeping than premature optimisation. The button logics has already been covered and also some of the main parts of the code. Whats added is the mode button, LDR, the SoftTimers library used to handle visualisation after a time of silence and the peak decay. 

You find the complete source code below. It is not full of comments but hopefully straight forward enough to be read as is. I only wish Blogger had a nice source code listing widget. You probably have to cut and paste it to a proper editor. This is probably not the final version. This is DIY after all ;)

Note: the source code has been updated. The initial approach using the FHT function fht_mag_lin8() with a scaling factor of 128 works but it left me with the nagging feeling of too little resolution for the upper bands. The 8 bit FHT output attenuated 25.9 dB as for bin 32 only represents an analog value of 13. That is not much. So how to increase the resolution? I had enough resources left to use the function fht_mag_lin() instead. The output covers the full 16 bit range but only has 8 bit of precision at any point in that range. Simulating a perfect sine sweep through this FHT resulted in the max output value of 11392 for all bins except bin 0 = 19712, bin 1 = 11776 and bin 63 = 11776. Amplifying 11392 25.9 dB -> 224698 , a pretty big number. So I recalculated the LUT with 11392 as the value for 0 dBFS. Worst case scenario would be 11776 in bin 1, amplified 0.9 dB -> 13062. Divide it by 64 and you get 204, still within a byte. So my new approach is to use a 16 bit linear FHT, use the new LUT and map() function to scale it to a long and right shift it back to byte resolution truncating any excess with constrain() as before. The struct holding data for each band can still be 8 bit since the result has to be further down scaled to the max y resolution of 0..7. The code below is updated using this new approach. It still uses only about half of the resources available on the Arduino Uno and the result is much better response in all bands and especially the upper bands. 

Note: the final version uses only 16 bands and the full source code is in this post.

// Some input management and music visualisation code
#define SND_ADC_CHANNEL 0
#define LDR_ADC_CHANNEL 1
#define RND_ADC_CHANNEL 2
#define DEFAULT_INPUT 1
#define LED_PIN 2
#define INPUT1_PIN 3
#define INPUT2_PIN 4
#define INPUT3_PIN 5
#define INPUT4_PIN 6
#define MODE_PIN 7
#define OUTPUT1_PIN 8
#define OUTPUT2_PIN 9
#define OUTPUT3_PIN 10
#define OUTPUT4_PIN 11
#define XRES 32
#define YRES 8
#include <SoftTimers.h>
#include <InputDebounce.h>
// MEM
#define BUFFER_LEN 10
#define BUFFER_ADR 0x10
#include <eewl.h>
// FHT
#define FHT_MAX 11392
#define LIN_OUT 1
#define FHT_N 128
#include <FHT.h>
// LDR
#define LDR_MAX 600
#define LDR_MIN 100
// SND
#define SND_SAMPLES FHT_N
#define NOISE_THRESHOLD 2
#define NUM_BINS FHT_N/2
#define NUM_BANDS XRES
#define SILENCE_MS 10000
// LED
#define NUM_LEDS XRES*YRES
#define BRIGHTNESS 64
#include <Adafruit_NeoPixel.h>
// GFX
#define DECAY_MS 20
#define PEAK_HOLD 1
#define MAX_DIM 192
#define SATURATION 255
// Used to amplify bands 5 dB / oct
const long BAND_EQ_LUT[NUM_BANDS] = { 
   12643,  22471,  31719,  39939,  47489,  56424,  65538,  71032,
   77912,  84435,  92610, 100384, 108782, 116556, 122051, 127814,
  133845, 138536, 145058, 151893, 159086, 166591, 174409, 182629,
  189063, 197953, 202599, 207290, 212115, 214572, 219575, 224713
};
// used to hold data per band
struct BandStruct {
  byte level;
  byte peak;
  byte hold;
} bands[NUM_BANDS];
// used for silence indicator 
struct StarStruct {
  byte x;
  byte y;
  unsigned int hue;
  byte sat;
  byte val;
} star;
byte dimValue = 0;
byte selectedInput = DEFAULT_INPUT;
InputDebounce btnMode;
InputDebounce btnInput1;
InputDebounce btnInput2;
InputDebounce btnInput3;
InputDebounce btnInput4;
SoftTimer silenceTimer;
SoftTimer decayTimer;
EEWL eewl(selectedInput, BUFFER_LEN, BUFFER_ADR);
Adafruit_NeoPixel leds(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

// return input selection for pin
byte pinToSelection(byte pin) {
  byte selection;
  switch (pin) {
    case INPUT2_PIN:
      selection = 2;
      break;
    case INPUT3_PIN:
      selection = 3;
      break;
    case INPUT4_PIN:
      selection = 4;
      break;
    default:
      selection = 1;
      break;
  }
  return selection;
}

// return pin for input selection
byte selectionToPin(byte selection) {
  byte pin;
  switch (selection) {
    case 2:
      pin = INPUT2_PIN;
      break;
    case 3:
      pin = INPUT3_PIN;
      break;
    case 4:
      pin = INPUT4_PIN;
      break;
    default:
      pin = INPUT1_PIN;
      break;
  }
  return pin;
}

// set selected output and save to eeprom
void setOutput(byte selection) {
  (selection == 1) ? digitalWrite(OUTPUT1_PIN, HIGH) : digitalWrite(OUTPUT1_PIN, LOW);
  (selection == 2) ? digitalWrite(OUTPUT2_PIN, HIGH) : digitalWrite(OUTPUT2_PIN, LOW);
  (selection == 3) ? digitalWrite(OUTPUT3_PIN, HIGH) : digitalWrite(OUTPUT3_PIN, LOW);
  (selection == 4) ? digitalWrite(OUTPUT4_PIN, HIGH) : digitalWrite(OUTPUT4_PIN, LOW);
  if (selection != selectedInput) {
    selectedInput = selection;
    eewl.put(selectedInput);
  }
}

// input selection callback
void btnInputCallback(byte pin) {
  byte selection = pinToSelection(pin);
  if (selection != selectedInput) {
    setOutput(selection);
  }
}

// handle button debouce
void processButtons() {
  unsigned long now = millis();
  btnInput1.process(now);
  btnInput2.process(now);
  btnInput3.process(now);
  btnInput4.process(now);
  btnMode.process(now);
}

// map x,y to pixel offset
int pixel(byte x, byte y) {
  int i = XRES * (YRES - y);
  return (y & 0x01) ? i - XRES + x : i - 1 - x;
}

// integer min-max scaling to resolution
byte scaleMinMax(byte value, byte minValue, byte maxValue, byte resolution) {
  return (minValue < maxValue) ? map(value, minValue, maxValue, 0, resolution) : 0;
}

// generate new random star
void nova() {
  star.x = random(XRES);
  star.y = random(YRES);
  star.hue = random(0xffff);
  star.sat = 32;
  star.val = 255 - dimValue;
}

// get rainbow hue for band
unsigned int getRainbowHue(byte band) {
  return band << 11;
}

// get hue from color cycle
unsigned int getNextHue() {
  static unsigned int hue = 0;
  hue--;
  return hue;
}

// draw level bar for band
void drawBar(byte band, unsigned int hue) {
  bool peak;
  byte shade = 255 - dimValue;
  unsigned long hiColor = leds.ColorHSV(hue, SATURATION, shade);
  unsigned long loColor = leds.ColorHSV(hue, SATURATION, shade >> 1);
  for (byte i = 0; i < YRES; i++) {
    if (bands[band].peak > 0 && bands[band].peak > i) {
      peak = (i == bands[band].peak - 1);
      leds.setPixelColor(pixel(band, i), peak ? hiColor : loColor);
    }
  }
}

// update led display
void updateDisplay() {
  leds.clear();
  if (silenceTimer.hasTimedOut()) {                               // no sound, indicate it with a "twinkeling star"
    if (star.sat < 255) {
      star.sat++;
    } else if (star.val > 0) {
      star.val--;
    } else {
      nova();
    }
    unsigned long color = leds.ColorHSV(star.hue, star.sat, star.val);
    leds.setPixelColor(pixel(star.x, star.y), color);
  } else {
    unsigned int hue;
    for (byte i = 0; i < NUM_BANDS; i++) {
      if (btnMode.isPressed()) {
        hue = getRainbowHue(i);
      } else {
        hue = getNextHue();
      }
      drawBar(i, hue);
    }
  }
  leds.show();
}

// process sound sample
void processSnd() {
  bool sound = false;
  int minValue = -1;
  int maxValue = -1;
  int value;
  byte i;
  fht_window();                                                   // window data for better frequency response
  fht_reorder();                                                  // reorder data before FHT
  fht_run();                                                      // process FHT data
  fht_mag_lin();                                                  // get FHT output
  for (i = 0; i < NUM_BANDS; i++) {  
    value = fht_lin_out[i + 1];                                   // skip first bin
    value = map(value, 0, FHT_MAX, 0, BAND_EQ_LUT[i]) >> 6;       // amplify 5 dB/octave and shift back to byte
    value = constrain(value, 0, 255);                             // clamp to byte resolution                                                     
    minValue = (minValue < 0) ? value : min(value, minValue);
    maxValue = (maxValue < 0) ? value : max(value, maxValue);
    bands[i].level = value; 
  }
  // scale, manage peaks and sound detection
  for (i = 0; i < NUM_BANDS; i++) {
    value = bands[i].level;
    value = scaleMinMax(value, minValue, maxValue, YRES);         // min-max scale to YRES
    bands[i].level = value;
    if (decayTimer.hasTimedOut()) {
      if (bands[i].hold > 0) {
        bands[i].hold--;
      } else if (bands[i].peak > 0) {
        bands[i].peak--;
      }
    }
    if (value > bands[i].peak) {
      bands[i].peak = value;
      bands[i].hold = PEAK_HOLD;
    } 
    if (bands[i].peak > 0) {
      sound = true;
    }
  }
  if (sound) {
    silenceTimer.reset();
  }
  if (decayTimer.hasTimedOut()) {
    decayTimer.reset();
  }
}

// set adc channel
void adcSet(byte channel) {
  static byte adcChannel = 0xFF;
  if (adcChannel == 0xFF) {
    adcChannel = channel;
    DIDR0 |= B00111111;                                           // disable digital input pins
    ADMUX = B00000000 | adcChannel;                               // AREF, right-adjust, channel
    ADCSRA = B10100101;                                           // enable ADC, auto-trigger, prescaler 32
  } else if (channel != adcChannel) {
    adcChannel = channel;
    ADMUX = B00000000 | adcChannel;                               // AREF, right-adjust, channel
    ADCSRA |= B01000000;                                          // start ADC conversion
    while (!(ADCSRA & B00010000));                                // wait for adc to be ready                                
    ADCL | (ADCH << 8);                                           // skip sample from old channel
  }
}

// return one adc sample
int adcGet() {
  ADCSRA |= B01000000;                                            // start ADC conversion
  while (!(ADCSRA & B00010000));                                  // wait for adc to be ready
  return ADCL | (ADCH << 8);                                      // must read low first
}

// take sound samples
void sampleSnd() {
  int i;
  int value;
  long avg = 0;
  adcSet(SND_ADC_CHANNEL);
  for (i = 0; i < SND_SAMPLES; i++) {
    value = adcGet();                                             // get sample
    fht_input[i] = value;                                         // put into FHT input
    avg += value;                                                 // add for avg
  }
  avg /= SND_SAMPLES;                                             // avg -> voltage offset
  for (i = 0; i < SND_SAMPLES; i++) {
    value = fht_input[i];                                         // get from FHT input
    value -= avg;                                                 // remove voltage offset
    value = abs(value) > NOISE_THRESHOLD ? value : 0;             // filter some noise
    value <<= 6;                                                  // to FHT max resolution
    fht_input[i] = value;                                         // put into FHT input
  }
}

// take ldr sample
void sampleLdr() {
  static unsigned int avg = 0;
  int value;
  adcSet(LDR_ADC_CHANNEL);
  value = adcGet();                                               // read ldr value
  avg = ((avg << 3) - avg + value) >> 3;                          // smoothen ldr reading
  value = constrain(avg, LDR_MIN, LDR_MAX);                       // clamp to ldr min/max
  dimValue = MAX_DIM - map(value, LDR_MIN, LDR_MAX, 0, MAX_DIM);  // update dim value based on ldr value
}

void setup() {
  // init output pins
  pinMode(OUTPUT1_PIN, OUTPUT);
  pinMode(OUTPUT2_PIN, OUTPUT);
  pinMode(OUTPUT3_PIN, OUTPUT);
  pinMode(OUTPUT4_PIN, OUTPUT);
  // get persisted data
  eewl.get(selectedInput);
  // output selected input
  setOutput(selectedInput);
  // init buttons
  btnMode.setup(MODE_PIN);
  btnInput1.registerCallbacks(btnInputCallback, NULL);
  btnInput2.registerCallbacks(btnInputCallback, NULL);
  btnInput3.registerCallbacks(btnInputCallback, NULL);
  btnInput4.registerCallbacks(btnInputCallback, NULL);
  btnInput1.setup(INPUT1_PIN);
  btnInput2.setup(INPUT2_PIN);
  btnInput3.setup(INPUT3_PIN);
  btnInput4.setup(INPUT4_PIN);
  // init LEDs
  leds.begin();
  leds.show();
  leds.setBrightness(BRIGHTNESS);
  // init silence display
  adcSet(RND_ADC_CHANNEL);
  randomSeed(adcGet());                                           // seed with adc noise
  nova();                                                         // create new star ;)
  // init timers
  silenceTimer.setTimeOutTime(SILENCE_MS);
  silenceTimer.reset();
  decayTimer.setTimeOutTime(DECAY_MS);
  decayTimer.reset();
}

void loop() {
  processButtons();
  sampleLdr();
  sampleSnd();
  processSnd();
  updateDisplay();
}