Final Documentation – Mikayla Fischler

Abstract and Introduction

To complete my HUA requirement for WPI, I decided to take Light Art as it caught my interest due to my passion for electronics and LEDs. The project I came up with is an LED audio visualizer in the shape of a cloud that is covered in cotton balls. This became known as the “Sound Cloud”.

This project took a few weeks to complete and I am very happy with the result. Through different modes, there is a effective visualizer for almost every song and every genre. This provides a wonderful light show to go along with your music, and can even light up a room if it is dark enough.

I had a lot of fun (and a lot of challenges) when creating this project, and look forward to future iterations as I plan to keep, maintain, and improve this project as it is very enjoyable to have running.

This documentation likely extends far beyond the requirements and expectations, but I felt as though I wanted to explain all aspects of my project in addition to sharing the results. I will begin with sharing the results.

Gallery

Simple Animations

Simple Bass Pulse
Bass Range Pulse
Bass Treble Pulse

Advanced Animations

Demonstrations

Sound Cloud Project – Bass Pulse – Stepper
Electronic/House
Sound Cloud Project – Bass Range Pulse – First of the Year (Equinox)
Dubstep
Sound Cloud Project – Bass Treble Pulse – Mammoth
Electronic/House
[EXPLICIT] Sound Cloud Project – Bass Treble Pulse – Multiple Songs
Explicit songs (rap/hip-hop) until 0:43
Clean song (electronic) starting at 0:44
Sound Cloud Project – Rainbow Linear Visualizer – Immigrant Song
Classic Rock
Sound Cloud Project – Rainbow Linear Visualizer – Lake Shore Drive
Classic Rock
Sound Cloud Project – Rainbow Linear Visualizer – Ventura Highway
Classic Rock
Sound Cloud Project – Rainbow Linear Visualizer – Matilda
Piano
Sound Cloud Project – Rainbow Linear Visualizer – Welcome to the Black Parade
Rock (Alternative/Emo?)
Sound Cloud Project – Fire Linear Visualizer – Slip
Electronic/House
Sound Cloud Project – Rainbow Linear Visualizer – Emerald Princess Part 1
Orchestral/Movie Scores
Sound Cloud Project – Fire Linear Visualizer – Emerald Princess Part 2
Orchestral/Movie Scores

Structure

The structure of the cloud consists of 4 key materials and a lot of hot glue. The base is a set of cut-up cardboard boxes hot glued together into the desired base size, which was sculpted before hand by hand using wire mesh. The overall structure was defined by using wire mesh, which was reinforced by the aforementioned cardboard in the base. The base also has 4 rubber feet mounted to both prevent the cloud from sliding and provide space to reach under when picking it up.

On the sides and top of the cloud, a layer of thermoplastic was applied, giving a moderately strong plastic shell to the project. This plastic is slightly flexible, therefore not at all brittle, and it takes a lot of force to deform or fracture it, which is good as I want this project to last.

Layered on the front, top, and sides are over 300 cotton balls. These are mostly glued onto LED strips (which will be mentioned later), but in some cases they are just directly mounted to the thermoplastic shell. They are all attached with hot glue both to the shell and to the adjacent cotton balls.

Electrical Hardware

Picture of the internals

The electrical hardware of this project consists of inputs, outputs, assistive circuits, and processing and control boards.

Input and Output (I/O)

I/O Panel on the rear of the cloud

On the rear of the cloud is the I/O panel. To the left we have power related things, specifically the 5v DC barrel jack connector and the power switch. This is the source of power for the whole system, which runs through a 10 Amp fuse that can be seen in the first hardware picture all the way to the right middle of the box. In the case of a short, this should prevent an unexpected amount of current from being drawn. This may be lowered in the future, but in theory at max brightness, 8-9 Amps could be drawn.

Next, there is the speaker selector switch, which is deprecated. This has labels int and ext, corresponding to internal and external speaker selection. The circuit for this did not work, and instead of replacing it with something I knew would work, I instead opted for having both outputs live at once, since the internal speakers could be completely silenced with the volume knob.

Two very important ports are the two holes near the bottom. These are the 3.5mm audio jacks, left being input and right being output (for external speaker/headphone output). The input is split to go to the audio amplifier for the internal speakers, the audio processing board, and the output line.

Internal view of the I/O panel

The blue knob on the back controls volume, and the white knob currently controls nothing, but may instead soon have some sort of purpose. The volume knob is a dual (separate signals for left and right but controlled equally) 10K ohm logarithmic potentiometer. Audio systems use logarithmic control since humans perceive audio on a logarithmic scale.

Mode button de-bouncer circuit

Finally, the mode button and status LED are the on the right most side of the panel. The mode button switches the current animation with each press, and the status LED describes the system state, which is described later in the Software section. The mode button also has a hardware de-bouncer circuit which is shown above.This consists of a 1K ohm resistor from de-bounced signal to controller input, a 470 ohm resistor from Vcc to the de-bounce signal, and a 1uF capacitor from the signal to ground. This setup forms a de-bouncer circuit, with hysteresis implemented in code (to be discussed in the Software section).

Assistive Circuits

Power distribution, regulation, and audio amplifier

Power Distribution and Regulation

The rightmost soldered board consists of multiple circuits, but in the end is purely for power distribution. It also contains an orange LED which lights up when power is available, and will dim if too much current is being drawn, which was helpful for debugging.

The breadboard section at the top continues this, providing for the ground connection of numerous boards and the power to the LED logic level shifter. Both the power distribution board and the bar have their own 1000uF capacitors to stabilize the power rails and reduce audio noise due to power supply stress.

Audio Amplification

At the bottom left of the above image is the 3.1 Watt audio amplifier board. This board is class D so it generates very little heat, which is good as it is sitting in an insulated box. This board amplifies the audio input signal and provides enough current to drive the two speakers, which are connected over 14 AWG stranded copper wire for better audio fidelity (even though the speakers and general acoustics are not exactly professional grade).

Processing and Control Boards

Teensy Audio Board

To start off, this board is one of the best devices I have ever worked with. Its audio capabilities are endless, and I would highly recommend it to anyone who wants to do anything with audio. Playback, generation, analysis? It has all of that.

For this application, it is hooked up to the micro-controller (the next sub-section) and provides a 1024-bit Fast Fourier Transform (discussed in the software section). The audio line input is connected to the line input, and data lines connect it to the micro-controller.

Teensy 3.6 Micro-controller

Teensy 3.6 board with its own 1000uF capacitor across Vcc and GND

The Teensy 3.6 is a beast of a micro-controller when compared to its Arduino cousins. At 180 MHz and a dedicated FPU (floating point unit, for processing floating point math), it is able to execute any floating point math and complex conditional array accesses that I throw at it with ease. The only slow downs I have encountered are the time it takes to actually update the strip (which can be eliminated with DMA) and the delay to receive FFT data, which is all discussed more in depth in the software section.

This board runs at 3.3 volts, and to ensure proper signal transmission to the LEDs, a 3.3v to 5v logic level shifter is used (shown in the top right of the above image).

Software

Overview of the Code

The software for this project was developed in C/C++ using the Arduino IDE for compilation and Visual Studio Code for development. In order to work with the Teensy, the Teensyduino software was installed, providing the Teensy program loader which loaded the compiled files to the Teensy.

The basis for this entire program is the usage of the Teensy Audio Board, mentioned in the prior Hardware section. Connecting to this board from the Teensy using I2C communication allowed the core program to receive Fast Fourier Transforms (FFTs) about every 10ms, resulting in a very smooth visualization.

The program runs in a minimally blocking state, only blocking when waiting for FFT data (after performing all necessary calculations and writing the output to the LEDs, the latter taking up 99% of the 4-6ms that this step takes). Since it is minimally blocking, it is able to check for mode changes about every 8-10ms, which is often enough.

Mode changing as mentioned before is done with a button on the back, which is attached to an interrupt. When the button is pressed and the signal edge falls, it calls a function in the code to increment the mode (or wrap around to the beginning). Due to some slight noise after hardware de-bouncing, a simple software de-bouncer is implemented, which will be described later.

Fast Fourier Transform

A Fast Fourier Transform (FFT) is a method of quickly performing a Fourier Transform to analyze signals such as audio signals. Different bits of resolution provide different frequency precision.

In this case, a 1024-bit FFT provides 512 single-precision floating point values from 0.0 to 1.0. This spans 0 Hz to 22000 KHz, providing a frequency resolution of 43 Hz per frequency bin. This allows the code to average together or sum ranges of frequencies and use those to accurately display components of audio onto the LEDs.

LED Mapping

LED Map

Mapping the three LED strips onto a 3-dimensional surface was one of the biggest challenges of this project. After brainstorming and some failed ideas, I decided to throw everything into a spreadsheet and work something out. In the graphic above, each color represents a different LED strip and each number represents the index of the LED in that location of the specified strip. -1 correlates to spots that have no mapped LED, which are ignored when writing to the arrays. The end result of creating this graphical representation was the development of a 4-zone system of 2D arrays.

  • Zone 0: Right
  • Zone 1: Front
  • Zone 2: Left
  • Zone 3: Top

Each zone can be written to in 1 of 3 ways. Firstly, the entire zone can be set to one color, which is used for the simpler animations. Secondly, an entire row or an entire column can be set to a specific color. And finally, any specific LED can be set by its row/column coordinate pair, which is defined along the axes of the above map.

Converting this map into software was tedious but not as challenging as it could have been. I created a class to represent the full cloud, and once initialized, the developer can create segments out of the three existing strips and then map them to specified coordinates with specified orientations. Due to the organization of the strips, this resulted in a lot of configuration-related lines of code.

x segmentation

// levels
led_segment_t* level_0 = led_ctrl->createSegment(strip_t::S_R12, 0, 23);
led_segment_t* level_1 = led_ctrl->createSegment(strip_t::S_R12, 23, 23);
led_segment_t* level_2 = led_ctrl->createSegment(strip_t::S_R34, 0, 21);
led_segment_t* level_3 = led_ctrl->createSegment(strip_t::S_R34, 21, 19);
led_ctrl->mapSegmentToLevel(level_0, 0);
led_ctrl->mapSegmentToLevel(level_1, 1);
led_ctrl->mapSegmentToLevel(level_2, 2);
led_ctrl->mapSegmentToLevel(level_3, 3);

// right zone
led_segment_t* z_0__0 = led_ctrl->createSegment(strip_t::S_R12, 0, 5);
led_segment_t* z_0__1 = led_ctrl->createSegment(strip_t::S_R12, 41, 5);
led_segment_t* z_0__2 = led_ctrl->createSegment(strip_t::S_R34, 0, 1);
led_ctrl->mapSegmentToZone(z_0__0, zone_t::RIGHT, 0, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_0__1, zone_t::RIGHT, 1, 4, seg_o_t::ROW_NEG);
led_ctrl->mapSegmentToZone(z_0__2, zone_t::RIGHT, 2, 4, seg_o_t::ROW_POS);

// front zone
led_segment_t* z_1__0 = led_ctrl->createSegment(strip_t::S_R12, 5, 12);
led_segment_t* z_1__1 = led_ctrl->createSegment(strip_t::S_R12, 28, 13);
led_segment_t* z_1__2 = led_ctrl->createSegment(strip_t::S_R34, 1, 13);
led_segment_t* z_1__3 = led_ctrl->createSegment(strip_t::S_R34, 24, 11);
led_segment_t* z_1__4 = led_ctrl->createSegment(strip_t::S_R34, 35, 1);
led_ctrl->mapSegmentToZone(z_1__0, zone_t::FRONT, 0, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_1__1, zone_t::FRONT, 1, 12, seg_o_t::ROW_NEG);
led_ctrl->mapSegmentToZone(z_1__2, zone_t::FRONT, 2, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_1__3, zone_t::FRONT, 3, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_1__4, zone_t::FRONT, 3, 12, seg_o_t::ROW_POS);

// left zone
led_segment_t* z_2__0 = led_ctrl->createSegment(strip_t::S_R12, 17, 6);
led_segment_t* z_2__1 = led_ctrl->createSegment(strip_t::S_R12, 23, 5);
led_segment_t* z_2__2 = led_ctrl->createSegment(strip_t::S_R34, 14, 7);
led_ctrl->mapSegmentToZone(z_2__0, zone_t::LEFT, 0, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_2__1, zone_t::LEFT, 1, 4, seg_o_t::ROW_NEG);
led_ctrl->mapSegmentToZone(z_2__2, zone_t::LEFT, 2, 0, seg_o_t::ROW_POS);

// top zone
led_segment_t* z_3__0 = led_ctrl->createSegment(strip_t::S_TOP, 0, 11);
led_segment_t* z_3__1 = led_ctrl->createSegment(strip_t::S_TOP, 11, 12);
led_segment_t* z_3__2 = led_ctrl->createSegment(strip_t::S_TOP, 23, 12);
led_segment_t* z_3__3 = led_ctrl->createSegment(strip_t::S_TOP, 35, 13);
led_segment_t* z_3__4 = led_ctrl->createSegment(strip_t::S_TOP, 48, 12);
led_segment_t* z_3__5 = led_ctrl->createSegment(strip_t::S_R34, 21, 3);
led_segment_t* z_3__6 = led_ctrl->createSegment(strip_t::S_R34, 36, 6);
led_segment_t* z_3__7 = led_ctrl->createSegment(strip_t::S_R34, 42, 1);
led_ctrl->mapSegmentToZone(z_3__0, zone_t::TOP, 0, 2, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_3__1, zone_t::TOP, 1, 12, seg_o_t::ROW_NEG);
led_ctrl->mapSegmentToZone(z_3__2, zone_t::TOP, 2, 1, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_3__3, zone_t::TOP, 3, 12, seg_o_t::ROW_NEG);
led_ctrl->mapSegmentToZone(z_3__4, zone_t::TOP, 4, 0, seg_o_t::ROW_POS);
led_ctrl->mapSegmentToZone(z_3__5, zone_t::TOP, 2, 0, seg_o_t::COLUMN_NEG);
led_ctrl->mapSegmentToZone(z_3__6, zone_t::TOP, 0, 13, seg_o_t::COLUMN_POS);
led_ctrl->mapSegmentToZone

Animations

In order to actually visualize music, the LEDs need to have patterns made and written to them. This is all done using the Animations class. At the root of this class, there are two basic functions, then three sub-classes contain all the other animations. The two basic functions consist of an allOff() that shuts off all LEDs and a diagnostics() that runs a diagnostics animation on all the LED pixels to ensure that mapping was done correctly.

The first sub-class is not used as of now. This is the Animations::Standby class. This consists of sparsely implemented functions that can do basic ambient animations for when no audio is detected, but this will be done in future iterations.

The second sub-class contains the simpler animations. The Animations::Simple class consists of the simple bass side pulse, the bass range pulse, and the bass treble pulse animations. They are titled with “pulse” as they correlate directly to signal strength in the specified signal band. These are the first three accessible animations when using the mode button.

The third and final sub-class contains the more advanced animations. The Animations::Advanced class contains the historical linear visualizers that are the 4th and 5th modes of operation. This is the rainbow and fire visualizers. These require a lot more math and some data persistence, so they were put in this Advanced child class.

List of All Animations

  1. Bass Pulse (Default at Start-UP) – The left and right sides pulse red with the bass frequencies of each side respectively
  2. Bass Range Pulse – Bass range and low mids broken down into different regions for each side and controlled by the respective frequencies
  3. Bass Treble Pulse – Bass and Treble ranges are mapped to different regions of the cloud which are illuminated by their associated frequencies
  4. Linear Visualizer [Rainbow] – A visualizer for a subset of frequency ranges providing a pleasant scrolling animation, with each range having its own color, forming a rainbow
  5. Linear Visualizer [Fire] – A visualizer for a subset of frequency ranges providing a pleasant scrolling animation, with the current data being white, and the recent data being red, then orange, then yellow, then a soft white

Control

De-bouncing and Use of the Mode Button

In order to prevent a single press from registering as multiple press due to the mechanical reality of buttons, de-bouncing is needed. This can be done electrically using a resistor/capacitor circuit as done here. Adding hysteresis can help smooth things even more. Even with the best R/C circuit I could design with what I have, one click would register as 2 or 3.

To fix this in software, I created a single byte of history. If a press is registered, the first bit is set to 1. If all other bits are 0, this is registered as a valid click. If any are 1, it is ignored. History is created by shifting the bits left every 1ms using a timer interrupt. Therefore, only 1 click every 8ms can be registered as valid.

uint8_t led_mode = 0;
uint8_t hysteresis = 0x00;

/**
 * @brief Initialize the mode button control
 * 
 */
void io_init_mode_control(void) {
	pinMode(LED_MODE_PIN, INPUT);
	attachInterrupt(digitalPinToInterrupt(LED_MODE_PIN), io_change_mode, FALLING);

	shift_timer.begin(__io_mode_shift, 1000); 
}

/**
 * @brief Shift the hysteresis varaible to add a software debounce to the existing hardware debounce
 * 
 */
void __io_mode_shift(void) { hysteresis <<= 1; }

/**
 * @brief Update the LED mode when the button is pressed, taking into account the last 8000us of readings to software debounce the input
 * 
 */
void io_change_mode(void) {
	if (!hysteresis) {
		if (++led_mode == LED_NUM_MODES) { led_mode = 0; }
		hysteresis |= 0x1;
	}
}

/**
 * @brief Get the current mode to be running in
 * 
 * @return uint8_t The mode
 */
uint8_t io_get_mode(void) { return led_mode; }

This eliminated all cases of button bounce. Now, with each valid press, a global mode variable is either incremented or wrapped back to 0 if it exceeds the highest mode that is present.

Status LED

/**
 * @brief Set the Common Anode RGB status LED color
 * 
 * @param r Red component (0 - 255)
 * @param g Green component (0 - 255)
 * @param b Blue component (0 - 255)
 */
void __io_write_status_led(uint8_t r, uint8_t g, uint8_t b) {
	analogWrite(S_LED_R, 255 - r);
	analogWrite(S_LED_G, 255 - g);
	analogWrite(S_LED_B, 255 - b);
}

In order to indicate status, a common-anode RGB LED is used. This is controlled by three different signals using analogWrite, one for each color component. System states are defined in the table below.

ColorState
YellowSystem start up initiated and in progress
GreenSystem is ready and running
BlueSystem is in a state-transition between LED animation modes

Final Source and External Resources Used

Final Source Code
The project repository can be found here.

The release (v1.1) for the demo can be found here. More details can be found on the prior release (v1.0) which is here.

Software Used
Arduino
Teensyduino

Libraries Used
Adafruit NeoPixel Library
Teensy Audio Library
Teensy I2C Libray

Thanks

I want to say thank you to Professor Rosenstock for having this course and providing very useful guidance to me and the rest of the class and I would like to thank the other students who helped me solve some hurdles I encountered in the physical design of the cloud.



Leave a Reply