LightShow

Package for abstracting light shows (part of Master's Thesis)

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: