Links/Code
The thesis paper can be viewed here, and the code can be seen on GitHub.
Summary
From spring of 2021 to fall of 2022, I worked on getting my Master’s from MIT. Fortunately, I was able to explore my own interests for my thesis, as my funding came from being a teaching assistant for one of the courses.
I had the opportunity to work with Prof. Joseph A. Paradiso’s Responsive Environments group (part of MIT’s Media Lab). Joe and many of the other members of the group were incredibly supportive and helpful throughout the entire process, and I am super appreciative for all of their help.
Thesis Deliverable
While much of my time with the thesis was spent working on various explorative mini-projects, I eventually landed on making a nice abstraction for scripting high-level, abstract lighting shows. The GitHub repo has the high level documentation, but this post has some examples (all of which are included in the thesis paper) to illustrate some of the things that the package can do.
Overall, the package has proved relatively useful, and I plan on using it to sync some more elaborate lights with music once I settle in to living somewhere. The final example (at the bottom of this post), illustrates how the package can be used to create a pretty neat litte light show, with a relatively small amount of code.
Examples
In the following examples, there is a visualization of some lights that are rendered in a Three.js web environment. One nice thing about LightShow objects is that they just output information about lights, so that information can be used to light any kind of light, real or virtual (virtual in this case). Here are some examples, with the code that is used to run them. More examples with further explanation can be found in the thesis paper.
Note that videos with audio have been kept short for copywrite reasons.
Simple Examples
A Simple Fade
Here is the code:
def a_simple_fade() -> LightShow:
"""
Simple Fade Over 2 seconds from Red to Blue on lights 0 and 3
"""
lights = {Light(light_number=0), Light(3)}
return fade(start_value=RED,
end_value=BLUE,
length=2 * ONE_SECOND,
lights=lights)
And the output:
Simple Fade Repeated at Times
def repeated_fade() -> LightShow:
"""
Same as a simple_fade, but repeated every 3 seconds, 5 times
"""
fade_show = a_simple_fade()
timestamps = [3*ONE_SECOND*i for i in range(5)]
return repeat_at(timestamps, fade_show)
Together With Another Fade Delayed By 4 Seconds
def together_and_delayed() -> LightShow:
"""
Make lights 1 and 2 do another longer fade together with
repeated_fade, and delay the start by 4 seconds
"""
repeated_fade_show = repeated_fade()
new_fade = fade(start_value=GREEN,
end_value=HSV(
h=GREEN.h,
s=GREEN.s,
v=0), # fade to black
length=10 * ONE_SECOND,
lights={Light(1), Light(2)})
delayed_new_fade = at(4 * ONE_SECOND, new_fade)
return together([repeated_fade_show, delayed_new_fade])
Geometry Aware Examples
This is the function that returns some geometry for our LightShow to run on in this section. It essentially sets up components for a panel of lights, as shown in the videos in the examples.
def setup_light_components() -> tuple[
List[LightingComponent],
LightingComponent,
List[LightingComponent],
LightingComponent]:
"""
Returns all_strips, panel, extra_lights, all_lights
"""
# Set up all the strips in the panel
all_strips: List[LightingComponent] = []
for row in range(8):
all_strips.append(
LightStrip([Light(col + row * 20) for col in range(20)],
start_location=Point(-5, row * .5, -2),
end_location=Point(5, row * .5, -2)))
# The panel is just made up of all of the strips
panel = LightingComponentGroup(all_strips)
single_cube_positions = [
[-4.75, 3.5, -.5],
[-4.35, 3, -.7],
[-4.75, 3.25, 0],
[-5.75, 3.5, -.5],
[4.75, 3.5, -.5],
[4.35, 3, -.7],
[4.75, 3.25, 0],
[5.75, 3.5, -.5],
]
extra_lights = []
for i, [x, y, z] in enumerate(single_cube_positions):
extra_lights.append(
SingleLight(
Light(i+len(panel.all_lights_in_component())), Point(x, y, z))
)
all_lights = LightingComponentGroup(
[panel, LightingComponentGroup(extra_lights)])
return all_strips, panel, extra_lights, all_lights
Lighting Entire Component
def constant_lights_on_components() -> LightShow:
"""
Lights up the whole panel red, the extra lights green,
but overrides the 3rd strip to be blue after 5 seconds
"""
all_strips, panel, extra_lights, all_lights = setup_light_components()
panel_red = on_component(component=panel,
lightshow=constant(RED, 10 * ONE_SECOND))
extra_lights_green = on_component(LightingComponentGroup(
extra_lights), constant(GREEN, 10 * ONE_SECOND))
third_strip_blue_after_5_seconds = at(
5 * ONE_SECOND,
on_component(all_strips[2], constant(BLUE, 5 * ONE_SECOND)))
return together([
panel_red,
extra_lights_green,
with_importance(1, third_strip_blue_after_5_seconds)
])
Lighting a Shape
def lights_on_spheres() -> LightShow:
"""
Lights up a sphere on the panel to be blue,
followed up by a sphere on only every other strip to be red
"""
all_strips, panel, extra_lights, all_lights = setup_light_components()
sphere = Sphere(radius=5, origin=Point(0, 1, -2))
# make a sphere
blue_sphere_on_panel = on_shape(
shape=sphere,
lighting_component=panel,
lightshow=fade(BLUE, HSV(BLUE.h, BLUE.s, 0), 4000),
)
red_sphere_on_every_other_strip = on_shape(
shape=sphere,
# every other strip should be affected by the red sphere
lighting_component=LightingComponentGroup(all_strips[::2]),
lightshow=fade(RED, HSV(RED.h, RED.s, 0), 4000)
)
return concat([blue_sphere_on_panel, red_sphere_on_every_other_strip])
Moving Spheres
def moving_spheres() -> LightShow:
"""
Two spheres that are red, moving left and right and
up and down in a sinusoidal manner
"""
all_strips, panel, extra_lights, all_lights = setup_light_components()
spheres = CompositeShape([
Sphere(radius=2, origin=Point(0, 1, -2)),
Sphere(radius=1, origin=Point(3, 2.5, -2))
])
def position_controller(t: float) -> Point:
"""x goes from +4 to -4, y from -1 to 1"""
return Point(4 * math.sin(t * 2 * math.pi / 2250),
math.cos(t * 2 * math.pi / 500),
0)
return Mover(all_lights,
shape=spheres,
lightshow=constant(RED, 10000),
position_controller=position_controller)
Music Aware Examples
Simple Usage of on_midi()
def on_midi_beats_1():
"""
Simple single color fades on lights 0,1,2 that go with the beat
"""
fade_time = 400
bass_drum = fade(GREEN, HSV(GREEN.h, GREEN.s, 0),
fade_time, lights={Light(0)})
snare_drum = fade(BLUE, HSV(BLUE.h, BLUE.s, 0),
fade_time, lights={Light(1)})
hihat_drum = fade(RED, HSV(RED.h, RED.s, 0), fade_time, lights={Light(2)})
midi_file_kwarg = "drum_midi_location"
return together([on_midi(midi_file_kwarg=midi_file_kwarg,
light_show_on_midi=bass_drum,
pitch=35),
on_midi(midi_file_kwarg,
snare_drum,
38),
on_midi(midi_file_kwarg,
hihat_drum,
42)])
Calling
on_midi_beats_1().with_audio(0,drum_midi_location="./SpoonITurnMyCameraOnDrums.mid")
and playing the result gives
Final show with a bunch of output
This is the final show from the thesis examples. Code:
def on_midi_beats_5():
"""
Similar to midi_beats_4, but now two spheres move towards each other
and affect different strips. The hihat also only affects extra lights
"""
fade_time = 400
generic_fade = fade(GREEN, HSV(GREEN.h, GREEN.s, 0), fade_time)
all_strips, _, extra_lights, _ = setup_light_components()
midi_file_kwarg = "drum_midi_location"
common_shape = GrowingAndShrinkingSphere(
5, 0, fade_time * 2, Point(0, 1.5, -2))
bass_drum = back_and_forth(start_location=Point(4, 0),
end_location=Point(-8, 0),
time_to_move=fade_time * 2,
shape=common_shape,
lighting_component=LightingComponentGroup(
all_strips[::2]),
lightshow=generic_fade
)
snare_drum = back_and_forth(start_location=Point(-4, 0),
end_location=Point(8, 0),
time_to_move=fade_time * 2,
shape=common_shape,
lighting_component=LightingComponentGroup(
all_strips[1::2]),
lightshow=generic_fade
)
hihat_drum = on_component(
LightingComponentGroup(extra_lights), generic_fade)
return WithAlbumArtColors(album_url_kwarg="album_art_url",
lightshows=[
on_midi(midi_file_kwarg=midi_file_kwarg,
light_show_on_midi=bass_drum,
pitch=35),
on_midi(midi_file_kwarg,
snare_drum,
38),
on_midi(midi_file_kwarg,
with_importance(1, hihat_drum),
42)])
Output: