Arduino Goes to Town, Part 6.


The 32 band version of the music visualiser works but a couple of things was bugging me. I don't like the square aspect ratio of the "pixels" or the way they kind of blend together. Most music "action" is in the first couple of bands so it feels a bit wasteful having 32 bands visualising ... nothing. If 32 bands is too much and 8 too little what about 16 bands? I could use two LEDs per band on the x-axis to get rid of the square looking "pixels". So what if I would use a FHT with N = 64, it would give me 30 useful bins if I skip the first and last. Plenty of bins for 16 bands, time to polish things up a bit.

I did some extra work this time to make sure to get everything as right as possible. First a ADC speed test to verify the sample rate. Result a constant 38.46 kHz, FHT N = 64 -> 38460 / 64 = 600.9375 centre frequency in bin 1. Next up was to set up a spread sheet with proper calculations of 5 dB / octave but what is max output out of FHT N = 64? Simulations using SimulIDE pointed at 11264 as max output for max input out of the useful bins. I used 500 Hz as the first octave and 0 dBFS with the new centre frequency of 601 Hz. Centre frequenzy out of bin 16 = 9615 Hz -> total range for the musical visualisation around 300 to 9900 Hz.

Bin

Hz

Oct

5 dB/Oct

20Log10

x AMP MAX

0 dBFS

500

1

5

1,78

11264


1000

2

10

3,16

35594


2000

3

15

5,62

63304


4000

4

20

10

112640


8000

5

25

17,78

200274


16000

6

30

31,62

356168


32000

7

35

56,23

633375

0

0

0

0

1

11264

1

600,94

1,27

6,35

2,08

23429

2

1201,88

2,27

11,35

3,69

41564

3

1802,81

2,85

14,25

5,16

58122

4

2403,75

3,27

16,35

6,57

74004

5

3004,69

3,59

17,95

7,9

88986

6

3605,63

3,85

19,25

9,17

103291

7

4206,56

4,07

20,35

10,41

117258

8

4807,5

4,27

21,35

11,68

131564

9

5408,44

4,44

22,2

12,88

145080

10

6009,38

4,59

22,95

14,04

158147

11

6610,31

4,72

23,6

15,14

170537

12

7211,25

4,85

24,25

16,31

183716

13

7812,19

4,97

24,85

17,48

196895

14

8413,13

5,07

25,35

18,51

208497

15

9014,06

5,17

25,85

19,61

220887

16

9615

5,27

26,35

20,77

233953

17

10215,94

5,35

26,75

21,75

244992

18

10816,88

5,44

27,2

22,91

258058

19

11417,81

5,51

27,55

23,85

268646

20

12018,75

5,59

27,95

24,97

281262

21

12619,69

5,66

28,3

26

292864

22

13220,63

5,72

28,6

26,92

303227

23

13821,56

5,79

28,95

28,02

315617

24

14422,5

5,85

29,25

29,01

326769

25

15023,44

5,91

29,55

30,03

338258

26

15624,38

5,97

29,85

31,08

350085

27

16225,31

6,02

30,1

31,99

360335

28

16826,25

6,07

30,35

32,92

370811

29

17427,19

6,12

30,6

33,88

381624

30

18028,13

6,17

30,85

34,87

392776

31

18629,06

6,22

31,1

35,89

404265

 
Next up was to update the LUT with a value for each bin and the EQ parts refactored into a new function:

// get 5dB/octave amplified bin
byte eqBin(byte bin) {
  int value;
  value = fht_lin_out[bin];  
  value = map(value, 0, BIN_EQ_LUT[0], 0, BIN_EQ_LUT[bin]) >> 6;
  return constrain(value, 0, 255);
}

I printed a new raster for the "display", old one on top and the new one below. The new one uses two less for each band and adds a frame between cells. I wanted a classic bar graph look and feel:


And this is what it looks like mounted (here in "rainbow mode"):


Compared to the original raster and 32 bands:


Both have their merits and I guess its a matter of taste but I prefer the new look. Using a FHT N = 64 is faster and consumes less resources so Im now left with almost 70% free resources on the Arduino Uno. Here is the complete source code for this new 16 band version (including a LUT for all 32 bins):

Note: the code below is subject to updates anytime I get inspired to make changes to it :)
Why does the code keep changing? First of all, it is not a spectrum analyzer, it is a music visualiser so I want it to visualise the music I am listening to in the best possible way. I also want the look and feel to be as analog as possible and last but not least, pleasing to the eye and not too obnoxious.

// 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 LIN_OUT 1
#define FHT_N 64
#include <FHT.h>
// LDR
#define LDR_MAX 600
#define LDR_MIN 100
#define LDR_FILTER_WEIGHT 6
// SND
#define SND_SAMPLES FHT_N
#define NOISE_THRESHOLD 3
#define NUM_BINS FHT_N/2
#define NUM_BANDS XRES/2
#define SILENCE_MS 10000
#define SND_FILTER_WEIGHT 2
// LED
#define NUM_LEDS XRES*YRES
#define BRIGHTNESS 64
#define YRES_HUE 65536/YRES
#define YRES_HUE_STEP YRES_HUE/YRES
#include <Adafruit_NeoPixel.h>
// GFX
#define UPDATE_MS 24
#define PEAK_HOLD 16
#define MAX_DIM 192
#define SATURATION 255
// used to amplify bins 5 dB / oct
const long BIN_EQ_LUT[NUM_BINS] = {
  11264,  23429,  41564,  58122,  74004,  88986, 103291, 117258,
  131564, 145080, 158147, 170537, 183716, 196895, 208497, 220887,
  233953, 244992, 258058, 268646, 281262, 292864, 303227, 315617,
  326769, 338258, 350085, 360335, 370811, 381624, 392776, 404265
};
const byte EQ_MAX = BIN_EQ_LUT[0] >> 6;
// used to hold data per band
struct BandStruct {
  byte avg;
  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 sndMax = 0;
byte dimValue = 0;
byte selectedInput = DEFAULT_INPUT;
bool altMode = false;
InputDebounce btnMode;
InputDebounce btnInput1;
InputDebounce btnInput2;
InputDebounce btnInput3;
InputDebounce btnInput4;
SoftTimer silenceTimer;
SoftTimer updateTimer;
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);
  altMode = btnMode.isPressed();
}

// 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 filter where weight factor is a power of two
int filter(int value, unsigned long avg, byte weight) {
  return ((avg << weight) - avg + value) >> weight;
}

// 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;
}

// init band data
void initBands() {
  for (byte i = 0; i < NUM_BANDS; i++) {
    bands[i].avg = 0;
    bands[i].level = 0;
    bands[i].peak = 0;
    bands[i].hold = 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 << 12;
}

// 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 dark) {
  byte x = band << 1;
  byte shade = 255 - dimValue;
  if (dark) {
    shade >>= 1;
  }
  for (byte i = 0; i < YRES; i++) { // bar
    if (bands[band].level > 0 && i < bands[band].level) {
      unsigned long normColor = leds.ColorHSV(hue, SATURATION, shade);
      leds.setPixelColor(pixel(x, i), normColor);
    }
    if (bands[band].peak > 0 && i < bands[band].peak) {
      unsigned long peakColor = leds.ColorHSV(hue + 2048, SATURATION, shade >> 1);
      leds.setPixelColor(pixel(x + 1, i), peakColor); 
    }
    hue += YRES_HUE_STEP;
  }
}

// draw alternative level bar for band
void drawAltBar(byte band, unsigned int hue, bool dark) {
  byte x = band << 1;
  byte shade = 255 - dimValue;
  if (dark) {
    shade >>= 1;
  }
  for (byte i = 0; i < YRES; i++) { // bar
    if (bands[band].peak > 0 && i == bands[band].peak - 1) {
      unsigned long peakColor = leds.ColorHSV(hue + 2048, SATURATION, shade);
      leds.setPixelColor(pixel(x, i), peakColor);
      leds.setPixelColor(pixel(x + 1, i), peakColor); 
    } else if (bands[band].level > 0 && i < bands[band].level && i < bands[band].peak - 1) {
      unsigned long normColor = leds.ColorHSV(hue, SATURATION, shade >> 1);
      leds.setPixelColor(pixel(x, i), normColor);
      leds.setPixelColor(pixel(x + 1, i), normColor);
    }
    hue += YRES_HUE_STEP;
  }
}

// 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;
    bool dark = (dimValue == MAX_DIM);
    for (byte i = 0; i < NUM_BANDS; i++) {
      hue = getNextHue();
      if (altMode) {
        drawAltBar(i, hue, dark);
      } else {
        drawBar(i, hue, dark);
      }
    }
  }
  leds.show();
}

// get 5dB/octave amplified bin
byte eqBin(byte bin) {
  int value;
  value = fht_lin_out[bin];
  value = map(value, 0, BIN_EQ_LUT[0], 0, BIN_EQ_LUT[bin]) >> 6;
  return constrain(value, 0, 255);
}

// eq and smoothen response, return updated avg value
byte processBand(byte band) {
  int value = eqBin(band + 1);                                    // skip first bin
  value = filter(value, bands[band].avg, SND_FILTER_WEIGHT);      // filter value
  bands[band].avg = constrain(value, 0, 255);                     // update avg
  return bands[band].avg;
}

// process sound sample
void processSnd() {
  bool sound = false;
  byte 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 = processBand(i);                                       // eq and filter band value 
    sndMax = max(value, sndMax);                                  // update max value
  }
  // scale, manage peaks and sound detection
  for (i = 0; i < NUM_BANDS; i++) {
    value = bands[i].avg;                                         // use filtered value
    value = scaleMinMax(value, 0, sndMax, YRES);                  // min-max scale to YRES
    if (value > 0) {
      sound = true;
    }
    bands[i].level = value;
    if (bands[i].level > bands[i].peak) {
      bands[i].peak = bands[i].level;
      bands[i].hold = PEAK_HOLD;
    } else if (bands[i].hold > 0) {
      bands[i].hold--;
    } else if (bands[i].peak > 0) {
      bands[i].peak--;
    }
  }
  if (sndMax > 0) {
    sndMax--;
  }
  if (sound) {
    silenceTimer.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 int avg = 0;
  int value;
  adcSet(LDR_ADC_CHANNEL);
  value = adcGet();                                               // read ldr value
  avg = filter(value, avg, LDR_FILTER_WEIGHT);                    // smoothen ldr reading
  value = constrain(avg, LDR_MIN, LDR_MAX);                       // clamp to ldr min-max
  value = map(value, LDR_MIN, LDR_MAX + 1, 0, MAX_DIM + 1);       // map value to min-max
  dimValue = MAX_DIM - value;                                     // update based on ldr value
}

void setup() {
  initBands();
  // 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();
  updateTimer.setTimeOutTime(UPDATE_MS);
  updateTimer.reset();
}

void loop() {
  processButtons();
  if (updateTimer.hasTimedOut()) {
    updateTimer.reset();
    sampleLdr();
    sampleSnd();
    processSnd();
    updateDisplay();
  }
}