Group 10: BEAT BUCKET 🎶

¸

BeatBucket is a fashion-forward bucket hat with 4 rows of LED lights that respond to music. The lights on the hat can change color, brightness, and configuration according to the rhythm and volume of the music. Wear it to light up any party, concert, and club!

Design Blog

1 – Research

We started by making a decision about what type of textile to use. Our target users are people who need a way to elevate their style and stand out in the crowd i.e. at parties with dark dance floors. Hats are appropriate given their hands-free nature for users and visibility on the body. 

Next, we researched lighting effects and hardware components. Addressable LED light strips can be programmed to display a variety of patterns, making them suitable for our project. 

Finally, we decided which type of sensing to support: the audio of the music and/or the wearer’s movement. We would need a microphone and accelerometer/gyroscope sensor, respectively. To allow for greater creative possibilities in programming the LEDs, we decided to attempt incorporating both. 

2 – Low-fidelity prototyping

What style of hat can fit a battery pack, microcontroller, multiple sensors, and still look good? We prototyped using real hats and found the bucket hat style to be appropriate. It allows for room at the inside above the wearer’s head for discreetly attached components. However it still calls for a smaller, wearable-specific microcontroller and battery pack. The outside surfaces of the hat are also sturdy enough for attaching lights.

Our next question was how to arrange lights on the hat. In order to create lighting effects that correspond to the motion of the dancer, we needed a dense arrangement of many lights. Initially, we envisioned using multiple LED strips attached in vertical columns around the hat, however we realized individually wiring and controlling each strip may not be feasible with limited microcontroller pins. We switched to a single long LED strip with addressable lights, which we represented in our prototype with a string.

 

Finally, we visualized different dance movements using our prototype. We realized that moves with subtle motions (e.g. head nodding) could be troublesome to detect accurately with the sensor. We decided to focus on moves associated with distinct patterns in sensor values, such as jumps, spins, and resting states. 

3- Coding & building

Once we received our components, we programmed and tested them individually before attempting to connect them. This was when we implemented examples using open source libraries for LEDs, music beat detection, and gyroscope readings. 

We ran into difficulties when attempting to connect the components. The wearable microcontroller did not have enough pins compatible with all our sensors (the gyroscope used the I2C protocol). As a result, we made the decision to proceed only with the audio sensor. We attempted using alternative microcontrollers, however ran into difficulties with the physical build. Soldering was not available as an option without lab access (our components did not arrive until Dec 9). We were able to connect the audio sensor to control the lights:

The audio sensor was too sensitive! Additionally without the gyroscope readings, the LED patterns needed more variety. Troubleshooting involved experimenting with the following in code:

  • Different methods of detecting music volume (amplitude) and frequency to determine BPM
  • Detecting clapping as a way to change the “mode” of the light display
  • Delays/timeouts so that animations can complete

The final light patterns programmed into the hat in response to music:

  • Mode 0 (Resting state): fading lights transitioning from red, green, and blue
  • Mode 1: Solid-colour light that becomes brighter at higher volumes
  • Mode 2: Transitions to random new colour on beats
  • Mode 3: Sparkling lights with higher frequency at higher volumes
  • Mode 4: Single spiraling light that changes direction on claps

Tasks


Maker Manual

Overview

The Beat Bucket is a hat with an affixed LED strip that uses a microphone to make the lights on the hat react to the sounds in the room. 

Tools and supplies

  • Addressable LED strip (WS2812B)
  • Adafruit GEMMA M0 Wearable Microcontrolller
  • 6DOF Motion Sensor Breakout (LSM303D)
  • Microphone Amplifier with Adjustable Gain (ADA1063)
  • 500 mAh LiPo Battery Pack and charger
  • Cotton Bucket Hat
  • Wires and wirestripper

Circuit Layout

Build

To construct the hat, start by cutting a small hole just above the brim. Feed the led strip through the whole so that the wires are inside the hat and the LED strip is outside the hat. Wrap the LED strip around the hat, using the adhesive on the back of the strip to attach it to the fabric of the hat. When the body of the hat is covered, simply cut the LED strip to the desired length. Connect the LED strip, sound sensor and button to the Arduino as shown in the circuit diagram.

Code

// Reference for LED library and code design:  https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
// Reference for microphone sensor library and code design: https://learn.adafruit.com/adafruit-microphone-amplifier-breakout/measuring-sound-levels

#include <Adafruit_NeoPixel.h>

#define LED_PIN 6  //Pin connected to LED Strip

#define LED_COUNT 67  // Number of LEDS in the strip

// Declare our NeoPixel strip object:
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

// Variable for current light show
const int NUM_MODES = 5;
int mode = 0;

const int buttonPin = 2;  // the number of the pushbutton pin
int buttonState = HIGH;   // the current reading from the input pin

int direction = 1;  // variable to use in the 'direction loop'

const int sampleWindow = 25;  // Sample window width in mS (50 mS = 20Hz)
unsigned int sample;

const int SAMPLE_SIZE = 6; // Number of readings to use to determine the average brightness
int prev_readings[SAMPLE_SIZE]; // Array to hold previous volume readings
int stale_index = 0;  // The index of the oldest volume measurement

const int CHANGE_THRESHOLD = 450;  // Volume threshold for colour and direction change

void setup() {
  for (int i = 0; i < SAMPLE_SIZE; i++) {
    prev_readings[i] = 0;
  }
  randomSeed(analogRead(2));

  // Initialize all LEDs to off
  strip.begin();
  strip.show();

  // initialize the pushbutton pin as an input:
  pinMode(buttonPin, INPUT);
}

void loop() {
  switch (mode) {
    case 0:
      resting();
      break;
    case 1:
      brightness_loop();
      break;
    case 2:
      sparkle_loop();
      break;
    case 3:
      colour_loop();
      break;
    case 4:
      direction_loop();
      break;
    default:
      break;
  }

  buttonState = digitalRead(buttonPin);  // Read button state
  // If button is pressed, change the current mode
  if (buttonState == LOW) {
    Serial.println("button");
    mode = (mode + 1) % NUM_MODES;
    // Flash lights three times to indicate mode has changed
    setAll(255, 255, 255);
    delay(50);
    setAll(0, 0, 0);
    delay(50);
    setAll(255, 255, 255);
    delay(50);
    setAll(0, 0, 0);
    delay(50);

    if (mode == 3) {  // For the Colour change mode, keep the lights on
      setAll(255, 255, 255);
    } else {  // For the all other modes, keep the lights off
      setAll(255, 255, 255);
      delay(50);
      setAll(0, 0, 0);
    }
    delay(500);
  }
}

// [MODE 1] Resting Loop iterates through red, green, and blue-- fading in and out as it transitions
void resting() {
  for (int j = 0; j < 3; j++) {
    // Fade IN
    for (int k = 0; k < 256; k++) {
      switch (j) {
        case 0: setAll(k, 0, 0); break;
        case 1: setAll(0, k, 0); break;
        case 2: setAll(0, 0, k); break;
      }
      strip.show();
      delay(3);
      // If button is pressed, break out of loop
      buttonState = digitalRead(buttonPin);
      if (buttonState == LOW) {
        Serial.println("button");
        break;
      }
    }
    // Fade OUT
    for (int k = 255; k >= 0; k--) {
      switch (j) {
        case 0: setAll(k, 0, 0); break;
        case 1: setAll(0, k, 0); break;
        case 2: setAll(0, 0, k); break;
      }
      strip.show();
      delay(3);
      // If button is pressed, break out of loop
      buttonState = digitalRead(buttonPin);
      if (buttonState == LOW) {
        Serial.println("button");
        break;
      }
    }
  }
}

// [MODE 2] Lights get brighter as volume increases
void brightness_loop() {
  unsigned long startMillis = millis();  // Start of sample window
  unsigned int peakToPeak = 0;           // peak-to-peak level

  unsigned int signalMax = 0;
  unsigned int signalMin = 1024;

  while (millis() - startMillis < sampleWindow) {
    sample = analogRead(0);
    if (sample < 1024)  // toss out spurious readings
    {
      if (sample > signalMax) {
        signalMax = sample;  // save just the max levels
      } else if (sample < signalMin) {
        signalMin = sample;  // save just the min levels
      }
    }
  }
  peakToPeak = signalMax - signalMin;             // max - min = peak-peak amplitude
  int volume = map(peakToPeak, 0, 1023, 0, 255);  // map volume levels to brightness levels
  int avg_volume = calculate_average_volumes(volume);
  setAll(avg_volume, avg_volume, avg_volume);
  strip.show();
}

// [MODE 3] Lights 'sparkle', with higher frequency as the volume increases
void sparkle_loop() {
  unsigned long startMillis = millis();  // Start of sample window
  unsigned int peakToPeak = 0;           // peak-to-peak level

  unsigned int signalMax = 0;
  unsigned int signalMin = 1024;

  while (millis() - startMillis < sampleWindow) {
    sample = analogRead(0);
    if (sample < 1024)  // toss out spurious readings
    {
      if (sample > signalMax) {
        signalMax = sample;  // save just the max levels
      } else if (sample < signalMin) {
        signalMin = sample;  // save just the min levels
      }
    }
  }
  peakToPeak = signalMax - signalMin;  // max - min = peak-peak amplitude
  int volume = map(peakToPeak, 0, 1023, 0, 255);
  int avg_volume = calculate_average_volumes(volume);

  // Set a random pixel to light up
  int i = random(LED_COUNT);
  strip.setPixelColor(i, 0xff, 0xff, 0xff);
  strip.show();
  // Delay correlated with volume-- the higher the volume, the less delay
  int del = map(avg_volume, 0, 255, 0, 1000);
  if (del > 300) {
    delay(0);
  } else {
    delay(300 - del);
  }
  // Turn pixel off after
  strip.setPixelColor(i, 0, 0, 0);
}

// [MODE 4] LEDS change to a random colour on the beat
void colour_loop() {
  unsigned long startMillis = millis();  // Start of sample window
  unsigned int peakToPeak = 0;           // peak-to-peak level

  unsigned int signalMax = 0;
  unsigned int signalMin = 1024;

  while (millis() - startMillis < sampleWindow) {
    sample = analogRead(0);
    if (sample < 1024)  // toss out spurious readings
    {
      if (sample > signalMax) {
        signalMax = sample;  // save just the max levels
      } else if (sample < signalMin) {
        signalMin = sample;  // save just the min levels
      }
    }
  }
  peakToPeak = signalMax - signalMin;  // max - min = peak-peak amplitude
  // Change Color, chosen randomly
  if (peakToPeak > CHANGE_THRESHOLD) {
    int red = random(0, 255);
    int green = random(0, 255);
    int blue = random(0, 255);

    setAll(red, green, blue); 
    strip.show();
  }
}

// [MODE 5] LED moves in one direction and switches directions on the beat
void direction_loop() {

  // Iterate over all the pixels in the specified direction
  for (int i = 0; i < LED_COUNT; i += direction) {
    buttonState = digitalRead(buttonPin);
    if (buttonState == LOW) {
      Serial.println("button");
      break;
    }
    if (i < 0) {
      i = 67;
    }
    // Set the pixel color
    strip.setPixelColor(i, 255, 255, 255);
    // Show the pixels
    strip.show();
    // Delay for better visibility
    delay(10);

    strip.setPixelColor(i, 0, 0, 0);
    unsigned long startMillis = millis();  // Start of sample window
    unsigned int peakToPeak = 0;           // peak-to-peak level

    unsigned int signalMax = 0;
    unsigned int signalMin = 1024;

      while (millis() - startMillis < sampleWindow) {
      sample = analogRead(0);
      if (sample < 1024)  // toss out spurious readings
      {
        if (sample > signalMax) {
          signalMax = sample;  // save just the max levels
        } else if (sample < signalMin) {
          signalMin = sample;  // save just the min levels
        }
      }
    }
    peakToPeak = signalMax - signalMin;  // max - min = peak-peak amplitude
    if (peakToPeak > CHANGE_THRESHOLD) {
      // Update the direction
      direction *= -1;
    }
  }
}

// Helper Methods:

// Method to mitigate the effort of spurious readings and make visual changes smooth
int calculate_average_volumes(int volume) {
  prev_readings[stale_index] = volume;
  stale_index = (stale_index + 1) % SAMPLE_SIZE;

  int sum = 0;
  for (int i = 0; i < SAMPLE_SIZE; i++) {
    sum += prev_readings[i];
  }
  return sum / SAMPLE_SIZE;
}

// Method to set the entire LED strip to the same color
void setAll(byte red, byte green, byte blue) {
  for (int i = 0; i < LED_COUNT; i++) {
    strip.setPixelColor(i, red, green, blue);
  }
  strip.show();
}

Testing and shortcomings

  • Audio sensor sensitivity: certain styles of music with prominent bass performs more consistently, however this also depends on the music source (from a phone would be different than a speaker system). We adjusted threshold values in our code to minimize response to quieter sounds.
  • Microcontroller choice: The Gemma M0 wearable microcontroller did not have enough input pins to support all our sensors and lights, so our current build relies on the Uno board. If soldering was possible we would use the Arduino Micro so that the electrical components can fit inside in the hat.