Jasmine Cheng
Gabriel Prado
Abstract
The goal of this project was to build a music visualizer, Headbanger, that would incorporate marbles and light to correspond with the mood and rhythm of songs. We built a two-axis pan-tilt machine, which included a face character with colored LED lights. We choreographed the machine to two contrasting songs via motorized movements and light sequences.
Objectives
We wanted to create a music visualizer to blend physical motion with creative expression. To achieve this effect, we made use of a pan/tilt mechanism and neopixels lights.
The machine could be choreographed to match diverse song types.
The combination of the pan/tilt movement and neopixels control expression of marble plate. Lights give the marble plate character and are refracted by the marbles. The pan tilt induces marble motion in sync with the music, and the marbles rhythmically roll across the plate and interact with each other.
Implementation
We built a two-axis pan tilt device that utilizes DC motors for each degree of freedom. The bottom layer is the pan motor assembly. There is a 20 tooth gear on the DC motor, connected to a 40 tooth gear attached to the ring bearing case. The top of the ring bearing case is mounted onto the panning plan. We chose to use the ring bearing case so that wires could be passed through from under the pan plate. The gearing helps us achieve slower pans.
The panning plate has tabs in it for struts for the tilt mechanism. The tilt mechanism is driven by a cam, which is powered by another DC motor with a 20 tooth (on motor) to 30 tooth (on cam) gear reduction.
We choreographed two songs to Headbanger: Tourette’s by Nirvana and To Build A Home by The Cinematic Orchestra. The former is a heavy noise rock song while the latter is a soft, melodic piece. We purposefully chose opposite song types in order to provoke diverse reactions by the audience at the show.
Outcomes
We constructed two prototypes prior to our final build.
For our first prototype, we incorporated all the planned fundamental functionality into the build. However, we had to use LEDs instead of neopixels because there was not enough space for the neopixel wires to go through the smile platform. We also included a return spring for the smile so that it would return to its original state when the cam activated.
Unfortunately, the pan mechanism of the first prototype malfunctioned often due to the ring bearing partially breaking during the build. The tilt mechanism was more reliable, but the return spring seemed to be too strong to have any realistic control of the tilt via code.
For our second prototype, we kept the fundamental mechanisms of the first prototype and mainly focused on making the motion more reliable and controllable. To do this, we used a second iteration of the ring bearing to provide better pan motion. We also removed the pullback spring and instead glued the smile platform such that its own weight would return it to the original state. We were also able to add the neopixels by giving more room in the smile to feed their wires through.
This prototype was a major improvement from the first one in terms of reliability and motion, and we were able to run code for a song on this prototype. However, there were still slight issues in its sturdiness. The glue that fixed the smile to the tilt mechanism was not strong enough, our cam was slightly oversized, and we had to improvise a counterweight for the entire assembly.
Taking the lessons learned from the creations of our previous prototypes, we approached the final build with further mechanical refinement. We also put a lot of focus on choreographing songs via our code. The major mechanical changes we made were adjusting the cam size and its attachment to the corresponding gear mechanism as well as using trapezoidal inserts to attach the smile platform to the tilt mechanism. These changes significantly improved the reliability and performance of Headbanger. Due to time constraints, we were only able to choreograph two songs. However, there was very little trouble mechanically in achieving this.
Here is our project video demonstrating the final build:
Future Work
To further improve upon this project, we could choreograph routines to more songs across different genres. We could add sensing such as an accelerometer that would allow for more fine tuned control of the pan and tilt, or photoelectric sensors that can allow the lights to change color when a marble triggers them.
We can also clean up the build in terms of loose wires and other aesthetic detail. In addition, the gear assembly could be more robust.
Photos
The following photos were taken by Professor Garth Zeglin at our final show:
Contribution
Jasmine wrote all of the software and designed the pan and tilt mechanisms while Gabriel designed the smile platform, conducted wiring testing, and made the project video.
Citations
Sections of software were taken from Professor Garth’s class website:
Fall 2021 Course Guide — 16-223 Creative Kinetic Systems (cmu.edu)
The crooked smile was inspired by rock band Nirvana’s smiley logo, The CAD for it was downloaded on GrabCad, and it was designed by user Javier López.
Nirvana | 3D CAD Model Library | GrabCAD
Supporting Material
Below is the source code. The files are:
code.py: Contains main event loop and input recorder
lights.py: Contains Light object that sets LED to various light sequences
motor.py: Contains motor objects that send power commands to two motors
timer.py: Contains timer object that gives elapsed time since start of the code running
motor_actions.py: Contains motor motion primitives and their respective key bindings, handles commanding motor actions, calls Motor object to set motor powers for a given primitive
code.py
import time import math import board import digitalio from motor_actions import MotorActions import neopixel_write import pwmio from lights import NeoPixelLights from motor import DRV8833 import remote import timer # ------------------------------------------------------------------- # Initialize motor_driver = DRV8833() lights = NeoPixelLights() remote = remote.RemoteSerial() timer = timer.Timer() class EventObject: def __init__(self, event_end): self.event_end = event_end event = EventObject(0) actions = MotorActions(motor_driver, event, timer) log_name = "log.txt" def log_input(): def keypress_handler(key_in): actions.command_action(key_in) remote.add_keypress_handler(keypress_handler) lights.sequence = lights.flash while True: lights.poll(timer.now()) remote.poll(0) actions.poll() def countdown(countdown_time): # Countdown countup_time = 0 while True: elasped_s = timer.elasped_s() if elasped_s > countup_time: print("Starting in", countdown_time - countup_time) countup_time += 1 if elasped_s > countdown_time: break def do_performance(log_name): f = open(log_name,'r',encoding = 'utf-8') event_list = [] for line in f: words = line.split() action_time = float(words[0]) action = words[1] event_list.append((action_time, action)) # ------------------------------------------------------------ # Main Event Loop timer.reset() i = 0 while True: if i % 10000 == 0: pass #print("Time elasped", elasped_s) i += 1 if timer.elasped_s() > event.event_end: motor_driver.set_pan_motor(0) motor_driver.set_tilt_motor(0) if len(event_list) > 0 and timer.elasped_s() > event_list[0][0]: next_event = event_list.pop(0) print("Event: ", next_event) # command_action will set the motor speeds and modify event.event_end actions.command_action(next_event[1]) # Lights lights.color_sequence(timer.now()) # Remote remote.poll(timer.elasped_s()) countdown(3) log_input() #do_performance(log_name)
motor.py
import time import board import digitalio import neopixel_write import math import pwmio class DRV8833(): def __init__(self, AIN1=board.GP18, AIN2=board.GP19, BIN2=board.GP20, BIN1=board.GP21, pwm_rate=20000): # Create a pair of PWMOut objects for each motor channel. self.ain1 = pwmio.PWMOut(AIN1, duty_cycle=0, frequency=pwm_rate) self.ain2 = pwmio.PWMOut(AIN2, duty_cycle=0, frequency=pwm_rate) self.bin1 = pwmio.PWMOut(BIN1, duty_cycle=0, frequency=pwm_rate) self.bin2 = pwmio.PWMOut(BIN2, duty_cycle=0, frequency=pwm_rate) def set_pan_motor(self, rate): # Pan motor is Motor A self.write(0, rate) def set_tilt_motor(self, rate): # Tilt motor is Motor B self.write(1, rate) def write(self, channel, rate): """Set the speed and direction on a single motor channel. :param channel: 0 for motor A, 1 for motor B :param rate: modulation value between -1.0 and 1.0, full reverse to full forward.""" # convert the rate into a 16-bit fixed point integer pwm = min(max(int(2**16 * abs(rate)), 0), 65535) if channel == 0: if rate < 0: self.ain1.duty_cycle = pwm self.ain2.duty_cycle = 0 else: self.ain1.duty_cycle = 0 self.ain2.duty_cycle = pwm else: if rate < 0: self.bin1.duty_cycle = pwm self.bin2.duty_cycle = 0 else: self.bin1.duty_cycle = 0 self.bin2.duty_cycle = pwm
motor_actions.py
import timer # import pdb class MotorActions: def __init__(self, motor_driver, event_obj, in_timer): self.motor_driver = motor_driver self.event_obj = event_obj self.timer = in_timer self.tilt_set_up = True self.action_lookup = { 'a': self.pan_right_slow, 'd': self.pan_left_slow, 'w': self.tilt_up, 's': self.tilt_down, 'h': self.tilt_up_left, 'j': self.tilt_up_right, 'n': self.tilt_down_left, 'm': self.tilt_down_right, 'l': self.swirl_movement } self.action_queue = [] self.action_queue_start = 0 self.action_timer = timer.Timer() self.current_action = None def poll(self): if self.current_action is not None: if self.action_timer.elasped_s() > self.current_action[2]: # Get next action if len(self.action_queue) > 0: self.set_next_action() else: self.motor_driver.set_pan_motor(0) self.motor_driver.set_tilt_motor(0) def set_next_action(self): if len(self.action_queue) > 0: self.current_action = self.action_queue.pop(0) pan, tilt, action_end = self.current_action self.motor_driver.set_pan_motor(pan) self.motor_driver.set_tilt_motor(tilt) self.action_timer.reset() def command_action(self, action_key): action = self.action_lookup.get(action_key) if action is not None: print(f"{self.timer.elasped_s()} {action_key}") action() self.set_next_action() def pan_right_slow(self): print('pan right') self.action_queue = [(-0.7, 0, 0.5)] def swirl_movement(self): if self.tilt_set_up: self.action_queue = [ (0.75, -0.8, 0.75), (0.75, 0.8, 0.75), (0.75, -0.8, 0.75) ] self.tilt_set_up = False def pan_left_slow(self): print('pan left') self.action_queue = [(0.7, 0, 0.5)] def tilt_up(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(0, 0.75, 0.6)] def tilt_down(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(0, -0.75, 0.5)] def tilt_up_left(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(0.7, 0.75, 0.7)] def tilt_down_right(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(-0.7, -0.75, 0.5)] def tilt_up_right(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(-0.7, 0.75, 0.7)] def tilt_down_left(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(0.7, -0.75, 0.5)] def set_action(self, pan_motor, tilt_motor, time): self.motor_driver.set_pan_motor(pan_motor) self.motor_driver.set_tilt_motor(tilt_motor) self.event_obj.event_end = self.timer.elasped_s() + time
lights.py
import digitalio import neopixel_write import board class Color(): def __init__(self, r, g, b): self.r = r self.g = g self.b = b def rgb(self): return self.r, self.g, self.b RED = Color(255, 0, 0) GREEN = Color(0, 255, 0) BLUE = Color(0, 0, 255) MAGENTA = Color(255, 0, 255) YELLOW = Color(255, 255, 0) CYAN = Color(0, 255, 255) class NeoPixelLights(): def __init__(self): self.pixels = digitalio.DigitalInOut(board.GP13) self.pixels.direction = digitalio.Direction.OUTPUT self.num_pixels = 40 self.frame_buffer = bytearray(3*self.num_pixels) self.dim_factor = 50 self.sequence = self.color_sequence def set_sequence(self, sequence): self.sequence = sequence def poll(self, now): self.sequence(now) def color_sequence(self, now): # generate a temporal color sequence with each component out of phase red = int((now//11000) % 256) grn = int((now//33000) % 256) blu = int((now//55000) % 256) # print(f"{red}, {grn}, {blu}") # update the entire frame buffer including an additional position-dependent term # to create spatial variation for p in range(self.num_pixels): self.frame_buffer[3*p] = (grn + 12*p) % 256 self.frame_buffer[3*p+1] = (red + 12*p) % 256 self.frame_buffer[3*p+2] = (blu + 12*p) % 256 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def eye_swirl(self, now): speed = 2 red = int((now//(speed * 11000)) % 256) grn = int((now//(speed * 33000)) % 256) blu = int((now//(speed * 55000)) % 256) for i in range(self.num_pixels * 3): self.frame_buffer[i] = 0 swirl_speed = 4 for p in range(12): self.frame_buffer[3*p] = (grn + swirl_speed*12*p) % 256 self.frame_buffer[3*p+1] = (red + swirl_speed*12*p) % 256 self.frame_buffer[3*p+2] = (blu + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)] = (grn + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)+1] = (red + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)+2] = (blu + swirl_speed*12*p) % 256 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def flash(self, now): flash_on = 1000 flash_off = 1000 on = now % (flash_on + flash_off) < flash_off grn = 0 red = 255 blu = 0 if on: for p in range(self.num_pixels): self.frame_buffer[3*p] = grn self.frame_buffer[3*p+1] = red self.frame_buffer[3*p+2] = blu else: for i in range(self.num_pixels * 3): self.frame_buffer[i] = 0 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def set_led_rgb(self, led_num, r, g, b): self.frame_buffer[3*led_num] = (g + 12*led_num) % 256 self.frame_buffer[3*led_num+1] = (r + 12*led_num) % 256 self.frame_buffer[3*led_num+2] = (b + 12*led_num) % 256 def set_led_color(self, led_num, color): r, g, b = color.rgb self.set_led_rgb(led_num, r, g, b) def write_to_neopixel(self): for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) neopixel_write.neopixel_write(self.pixels, self.frame_buffer)
timer.py
import time class Timer: def __init__(self): self.start_time = self.now_s() def reset(self): self.start_time = self.now_s() def elasped_s(self): return self.now_s() - self.start_time def now(self): return time.monotonic_ns() / 1000 def now_s(self): return self.now()/float(1000000)
Leave a Reply
You must be logged in to post a comment.