def update_lights(matrix, mean, std):
    """Update the state of all the lights

    Update the state of all the lights based upon the current
    frequency response matrix

    :param matrix: row of data from cache matrix
    :type matrix: list

    :param mean: standard mean of fft values
    :type mean: list

    :param std: standard deviation of fft values
    :type std: list
    """
    for pin in xrange(0, hc.GPIOLEN):
        # Calculate output pwm, where off is at some portion of the std below
        # the mean and full on is at some portion of the std above the mean.
        brightness = matrix[pin] - mean[pin] + 0.5 * std[pin]
        brightness /= 1.25 * std[pin]
        if brightness > 1.0:
            brightness = 1.0

        if brightness < 0:
            brightness = 0

        if not hc.is_pin_pwm[pin]:
            # If pin is on / off mode we'll turn on at 1/2 brightness
            # TODO(mdietz): Configurable per channel threshold!
            if brightness > 0.5:
                hc.turn_on_light(pin, True)
            else:
                hc.turn_off_light(pin, True)
        else:
            hc.turn_on_light(pin, True, brightness)
Example #2
0
def update_lights(matrix, mean, std):
    """Update the state of all the lights

    Update the state of all the lights based upon the current
    frequency response matrix

    :param matrix: row of data from cache matrix
    :type matrix: list

    :param mean: standard mean of fft values
    :type mean: list

    :param std: standard deviation of fft values
    :type std: list
    """
    for pin in xrange(0, hc.GPIOLEN):
        # Calculate output pwm, where off is at some portion of the std below
        # the mean and full on is at some portion of the std above the mean.
        brightness = matrix[pin] - mean[pin] + 0.5 * std[pin]
        brightness /= 1.25 * std[pin]
        if brightness > 1.0:
            brightness = 1.0

        if brightness < 0:
            brightness = 0

        if not hc.is_pin_pwm[pin]:
            # If pin is on / off mode we'll turn on at 1/2 brightness
            # TODO(mdietz): Configurable per channel threshold!
            if brightness > 0.5:
                hc.turn_on_light(pin, True)
            else:
                hc.turn_off_light(pin, True)
        else:
            hc.turn_on_light(pin, True, brightness)
Example #3
0
def main():
    """
    Test pattern2

    Unlights one channel at a time in order
    """
    # this is a list of all the channels you have access to
    lights = hc._GPIO_PINS

    # initialize your hardware for use
    hc.initialize()

    # start with all the lights off
    hc.turn_off_lights()

    # pause for 1 second
    time.sleep(2)

    # working loop
    for _ in range(50):
        # try except block to catch keyboardinterrupt by user to stop
        try:
            # here we just loop over the gpio pins and do something with them
            for light in lights:
                # turn on all the lights
                hc.turn_on_lights()

                # then turn off one
                hc.turn_off_light(light)

                # wait a little bit before the for loop
                # starts again and turns off the next light
                time.sleep(.4)

        # if the user pressed <CTRL> + C to exit early break out of the loop
        except KeyboardInterrupt:
            print "\nstopped"
            break

    # This ends and cleans up everything
    hc.clean_up()
def update_lights(matrix, mean, std):
    """
    Update the state of all the lights

    Update the state of all the lights based upon the current
    frequency response matrix
    """
    for i in range(0, hc.GPIOLEN):
        # Calculate output pwm, where off is at some portion of the std below
        # the mean and full on is at some portion of the std above the mean.
        brightness = matrix[i] - mean[i] + 0.5 * std[i]
        brightness = brightness / (1.25 * std[i])
        if brightness > 1.0:
            brightness = 1.0
        if brightness < 0:
            brightness = 0
        if not hc.is_pin_pwm(i):
            # If pin is on / off mode we'll turn on at 1/2 brightness
            if (brightness > 0.5):
                hc.turn_on_light(i, True)
            else:
                hc.turn_off_light(i, True)
        else:
            hc.turn_on_light(i, True, brightness)
def main():
    """
    Random flashing lights
    """
    # this is a list of all the channels you have access to
    # I'm also tracking the time here so that I know when I turned a light off
    # So I'm putting everything in a dict
    gpio_pins = hc._GPIO_PINS
    lights = dict.fromkeys(range(0, len(gpio_pins)), [True, time.time()])

    # get a number that is about 40% the length of your gpio's
    # this will be use to make sure that no more then 40% of
    # the light will be off at any one time
    max_off = int(round(len(lights) * .4))

    # initialize your hardware for use
    hc.initialize()
    print "Press <CTRL>-C to stop"

    # start with all the lights on
    hc.turn_on_lights()

    # lets run for 2 minutes
    end = time.time() + 120

    # working loop will run as long as time.time() is less then "end"
    while time.time() < end:
        # try except block to catch keyboardinterrupt by user to stop
        try:
            # here we just loop over the gpio pins
            for light in lights:
                # this is where we check to see if we have any light
                # that are turned off
                # if they are off we will check the time to see if we
                # want to turn them back on yet, if we do then turn it on
                if not lights[light][0]:
                    if lights[light][1] < time.time():
                        lights[light][0] = True
                        hc.turn_on_light(light)

            # count the number of lights that are off
            off = [k for (k, v) in lights.iteritems() if v.count(1) == False]

            # if less then out max count of light that we chose
            # we can turn one off
            if len(off) < max_off:
                # pick a light at random to turn off
                choice = random.randrange(0, len(gpio_pins))
                # if it's on then lets turn it off
                if lights[choice][0]:
                    # pick a duration for that light to be off
                    # default times are between 1/2 and secong and 1.8 seconds
                    duration = random.uniform(0.5, 1.8)

                    # store this informatin in our dict
                    lights[choice] = [False, time.time() + duration]
                    # and turn that light off then continue with the main loop
                    # and do it all over again
                    hc.turn_off_light(choice)

        # if the user pressed <CTRL> + C to exit early break out of the loop
        except KeyboardInterrupt:
            print "\nstopped"
            break

    # This ends and cleans up everything
    hc.clean_up()
def main():
    '''main'''
    song_to_play = int(cm.get_state('song_to_play', 0))
    play_now = int(cm.get_state('play_now', 0))

    # Arguments
    parser = argparse.ArgumentParser()
    filegroup = parser.add_mutually_exclusive_group()
    filegroup.add_argument('--playlist', default=_PLAYLIST_PATH,
                           help='Playlist to choose song from.')
    filegroup.add_argument('--file', help='path to the song to play (required if no'
                           'playlist is designated)')
    parser.add_argument('--readcache', type=int, default=1,
                        help='read light timing from cache if available. Default: true')
    args = parser.parse_args()

    # Log everything to our log file
    # TODO(todd): Add logging configuration options.
    logging.basicConfig(filename=cm.LOG_DIR + '/music_and_lights.play.dbg',
                        format='[%(asctime)s] %(levelname)s {%(pathname)s:%(lineno)d}'
                        ' - %(message)s',
                        level=logging.DEBUG)

    # Make sure one of --playlist or --file was specified
    if args.file == None and args.playlist == None:
        print "One of --playlist or --file must be specified"
        sys.exit()

    # Initialize Lights
    hc.initialize()

    # Only execute preshow if no specific song has been requested to be played right now
    if not play_now:
        execute_preshow(cm.lightshow()['preshow'])

    # Determine the next file to play
    song_filename = args.file
    if args.playlist != None and args.file == None:
        most_votes = [None, None, []]
        current_song = None
        with open(args.playlist, 'rb') as playlist_fp:
            fcntl.lockf(playlist_fp, fcntl.LOCK_SH)
            playlist = csv.reader(playlist_fp, delimiter='\t')
            songs = []
            for song in playlist:
                if len(song) < 2 or len(song) > 4:
                    logging.error('Invalid playlist.  Each line should be in the form: '
                                 '<song name><tab><path to song>')
                    sys.exit()
                elif len(song) == 2:
                    song.append(set())
                else:
                    song[2] = set(song[2].split(','))
                    if len(song) == 3 and len(song[2]) >= len(most_votes[2]):
                        most_votes = song
                songs.append(song)
            fcntl.lockf(playlist_fp, fcntl.LOCK_UN)

        if most_votes[0] != None:
            logging.info("Most Votes: " + str(most_votes))
            current_song = most_votes

            # Update playlist with latest votes
            with open(args.playlist, 'wb') as playlist_fp:
                fcntl.lockf(playlist_fp, fcntl.LOCK_EX)
                writer = csv.writer(playlist_fp, delimiter='\t')
                for song in songs:
                    if current_song == song and len(song) == 3:
                        song.append("playing!")
                    if len(song[2]) > 0:
                        song[2] = ",".join(song[2])
                    else:
                        del song[2]
                writer.writerows(songs)
                fcntl.lockf(playlist_fp, fcntl.LOCK_UN)

        else:
            # Get random song
            if _RANDOMIZE_PLAYLIST:
                current_song = songs[random.randint(0, len(songs) - 1)]
            # Get a "play now" requested song
            elif play_now > 0 and play_now <= len(songs):
                current_song = songs[play_now - 1]
            # Play next song in the lineup
            else:
                song_to_play = song_to_play if (song_to_play <= len(songs) - 1) else 0
                current_song = songs[song_to_play]
                next_song = (song_to_play + 1) if ((song_to_play + 1) <= len(songs) - 1) else 0
                cm.update_state('song_to_play', next_song)

        # Get filename to play and store the current song playing in state cfg
        song_filename = current_song[1]
        cm.update_state('current_song', songs.index(current_song))

    song_filename = song_filename.replace("$SYNCHRONIZED_LIGHTS_HOME", cm.HOME_DIR)

    # Ensure play_now is reset before beginning playback
    if play_now:
        cm.update_state('play_now', 0)
        play_now = 0

    # Initialize FFT stats
    matrix = [0 for _ in range(hc.GPIOLEN)]
    offct = [0 for _ in range(hc.GPIOLEN)]

    # Build the limit list
    if len(_LIMIT_LIST) == 1:
        limit = [_LIMIT_LIST[0] for _ in range(hc.GPIOLEN)]
    else:
        limit = _LIMIT_LIST

    # Set up audio
    if song_filename.endswith('.wav'):
        musicfile = wave.open(song_filename, 'r')
    else:
        musicfile = decoder.open(song_filename)

    sample_rate = musicfile.getframerate()
    num_channels = musicfile.getnchannels()
    output = aa.PCM(aa.PCM_PLAYBACK, aa.PCM_NORMAL)
    output.setchannels(num_channels)
    output.setrate(sample_rate)
    output.setformat(aa.PCM_FORMAT_S16_LE)
    output.setperiodsize(CHUNK_SIZE)

    # Output a bit about what we're about to play
    song_filename = os.path.abspath(song_filename)
    logging.info("Playing: " + song_filename + " (" + str(musicfile.getnframes() / sample_rate)
                 + " sec)")

    cache = []
    cache_found = False
    cache_filename = os.path.dirname(song_filename) + "/." + os.path.basename(song_filename) \
        + ".sync.gz"
    # The values 12 and 1.5 are good estimates for first time playing back (i.e. before we have
    # the actual mean and standard deviations calculated for each channel).
    mean = [12.0 for _ in range(hc.GPIOLEN)]
    std = [1.5 for _ in range(hc.GPIOLEN)]
    if args.readcache:
        # Read in cached fft
        try:
            with gzip.open(cache_filename, 'rb') as playlist_fp:
                cachefile = csv.reader(playlist_fp, delimiter=',')
                for row in cachefile:
                    cache.append([0.0 if np.isinf(float(item)) else float(item) for item in row])
                cache_found = True
                # TODO(todd): Optimize this and / or cache it to avoid delay here
                cache_matrix = np.array(cache)
                for i in range(0, hc.GPIOLEN):
                    std[i] = np.std([item for item in cache_matrix[:, i] if item > 0])
                    mean[i] = np.mean([item for item in cache_matrix[:, i] if item > 0])
                logging.debug("std: " + str(std) + ", mean: " + str(mean))
        except IOError:
            logging.warn("Cached sync data song_filename not found: '" + cache_filename
                         + ".  One will be generated.")

    # Process audio song_filename
    row = 0
    data = musicfile.readframes(CHUNK_SIZE)
    frequency_limits = calculate_channel_frequency(_MIN_FREQUENCY,
                                                   _MAX_FREQUENCY,
                                                   _CUSTOM_CHANNEL_MAPPING,
                                                   _CUSTOM_CHANNEL_FREQUENCIES)

    while data != '' and not play_now:
        output.write(data)

        # Control lights with cached timing values if they exist
        matrix = None
        if cache_found and args.readcache:
            if row < len(cache):
                matrix = cache[row]
            else:
                logging.warning("Ran out of cached FFT values, will update the cache.")
                cache_found = False

        if matrix == None:
            # No cache - Compute FFT in this chunk, and cache results
            matrix = calculate_levels(data, sample_rate, frequency_limits)
            cache.append(matrix)


        # blank out the display
        led.fill(Color(0,0,0),0,151)
        for i in range(0, hc.GPIOLEN):
            if hc.is_pin_pwm(i):
                # Output pwm, where off is at 0.5 std below the mean
                # and full on is at 0.75 std above the mean.

                display_column(i,matrix[i])

                #brightness = matrix[i] - mean[i] + 0.5 * std[i]
                #brightness = brightness / (1.25 * std[i])
                #if brightness > 1.0:
                    #brightness = 1.0
                #if brightness < 0:
                    #brightness = 0
                #hc.turn_on_light(i, True, int(brightness * 60))
            else:
                if limit[i] < matrix[i] * _LIMIT_THRESHOLD:
                    limit[i] = limit[i] * _LIMIT_THRESHOLD_INCREASE
                    logging.debug("++++ channel: {0}; limit: {1:.3f}".format(i, limit[i]))
                # Amplitude has reached threshold
                if matrix[i] > limit[i]:
                    hc.turn_on_light(i, True)
                    offct[i] = 0
                else:  # Amplitude did not reach threshold
                    offct[i] = offct[i] + 1
                    if offct[i] > _MAX_OFF_CYCLES:
                        offct[i] = 0
                        limit[i] = limit[i] * _LIMIT_THRESHOLD_DECREASE  # old value 0.8
                    logging.debug("---- channel: {0}; limit: {1:.3f}".format(i, limit[i]))
                    hc.turn_off_light(i, True)

        # send out data to RGB LED Strip
        led.update()
        # Read next chunk of data from music song_filename
        data = musicfile.readframes(CHUNK_SIZE)
        row = row + 1

        # Load new application state in case we've been interrupted
        cm.load_state()
        play_now = int(cm.get_state('play_now', 0))

    if not cache_found:
        with gzip.open(cache_filename, 'wb') as playlist_fp:
            writer = csv.writer(playlist_fp, delimiter=',')
            writer.writerows(cache)
            logging.info("Cached sync data written to '." + cache_filename
                         + "' [" + str(len(cache)) + " rows]")

    # We're done, turn it all off ;)
    hc.clean_up()
Example #7
0
def main():
    """
    Random flashing lights
    """
    # this is a list of all the channels you have access to
    # I'm also tracking the time here so that I know when I turned a light off
    # So I'm putting everything in a dict
    gpio_pins = hc._GPIO_PINS
    lights = dict.fromkeys(range(0, len(gpio_pins)), [True, time.time()])

    # get a number that is about 40% the length of your gpio's
    # this will be use to make sure that no more then 40% of
    # the light will be off at any one time
    max_off = int(round(len(lights) * .4))

    # initialize your hardware for use
    hc.initialize()
    print "Press <CTRL>-C to stop"

    # start with all the lights on
    hc.turn_on_lights()

    # lets run for 2 minutes
    end = time.time() + 120

    # working loop will run as long as time.time() is less then "end"
    while time.time() < end:
        # try except block to catch keyboardinterrupt by user to stop
        try:
            # here we just loop over the gpio pins
            for light in lights:
                # this is where we check to see if we have any light
                # that are turned off
                # if they are off we will check the time to see if we
                # want to turn them back on yet, if we do then turn it on
                if not lights[light][0]:
                    if lights[light][1] < time.time():
                        lights[light][0] = True
                        hc.turn_on_light(light)

            # count the number of lights that are off
            off = [k for (k, v) in lights.iteritems() if v.count(1) == False]

            # if less then out max count of light that we chose
            # we can turn one off
            if len(off) < max_off:
                # pick a light at random to turn off
                choice = random.randrange(0, len(gpio_pins))
                # if it's on then lets turn it off
                if lights[choice][0]:
                    # pick a duration for that light to be off
                    # default times are between 1/2 and secong and 1.8 seconds
                    duration = random.uniform(0.5, 1.8)

                    # store this informatin in our dict
                    lights[choice] = [False, time.time() + duration]
                    # and turn that light off then continue with the main loop
                    # and do it all over again
                    hc.turn_off_light(choice)

        # if the user pressed <CTRL> + C to exit early break out of the loop
        except KeyboardInterrupt:
            print "\nstopped"
            break

    # This ends and cleans up everything
    hc.clean_up()
Example #8
0
    def execute(self):
        """
        Execute the pre/post show as defined by the current config

        Returns the exit status of the show, either done if the
        show played to completion, or play_now_interrupt if the
        show was interrupted by a play now command.
        """
        # Is there a show to launch?
        if self.config == None:
            return PrePostShow.done

        # Is the config a script or a transition based show
        # launch the script if it is
        if not isinstance(self.config, dict) and os.path.exists(self.config):
            logging.debug("Launching external script " + self.config + " as " \
                + self.show)
            return self.start_script()

        # start the audio if there is any
        self.start_audio()

        if 'transitions' in self.config:
            try:
                # display transition based show
                for transition in self.config['transitions']:
                    start = time.time()
                    if transition['type'].lower() == 'on':
                        hc.turn_on_lights(True)
                    else:
                        hc.turn_off_lights(True)
                    logging.debug('Transition to ' + transition['type'] + ' for ' \
                        + str(transition['duration']) + ' seconds')

                    if 'channel_control' in transition:
                        channel_control = transition['channel_control']
                        for key in channel_control.keys():
                            mode = key
                            channels = channel_control[key]
                            for channel in channels:
                                if mode == 'on':
                                    hc.turn_on_light(int(channel) - 1, 1)
                                elif mode == 'off':
                                    hc.turn_off_light(int(channel) - 1, 1)
                                else:
                                    logging.error("Unrecognized channel_control mode "
                                                "defined in preshow_configuration " \
                                                    + str(mode))
                    # hold transition for specified time
                    while transition['duration'] > (time.time() - start):
                        # check for play now
                        if check_state():
                            # kill the audio playback if playing
                            if self.audio:
                                os.killpg(self.audio.pid, signal.SIGTERM)
                                self.audio = None
                            return PrePostShow.play_now_interrupt
                        time.sleep(0.1)
            except:
                pass

        # hold show until audio has finished if we have audio
        # or audio is not finished
        return_value = self.hold_for_audio()

        return return_value
Example #9
0
    def execute(self):
        """
        Execute the pre/post show as defined by the current config

        Returns the exit status of the show, either done if the
        show played to completion, or play_now_interrupt if the
        show was interrupted by a play now command.
        """
        # Is there a show to launch?
        if self.config == None:
            return PrePostShow.done

        # Is the config a script or a transition based show
        # launch the script if it is
        if not isinstance(self.config, dict) and os.path.exists(self.config):
            logging.debug("Launching external script " + self.config + " as " + self.show)
            return self.start_script()

        # start the audio if there is any
        self.start_audio()

        if "transitions" in self.config:
            try:
                # display transition based show
                for transition in self.config["transitions"]:
                    start = time.time()
                    if transition["type"].lower() == "on":
                        hc.turn_on_lights(True)
                    else:
                        hc.turn_off_lights(True)
                    logging.debug(
                        "Transition to " + transition["type"] + " for " + str(transition["duration"]) + " seconds"
                    )

                    if "channel_control" in transition:
                        channel_control = transition["channel_control"]
                        for key in channel_control.keys():
                            mode = key
                            channels = channel_control[key]
                            for channel in channels:
                                if mode == "on":
                                    hc.turn_on_light(int(channel) - 1, 1)
                                elif mode == "off":
                                    hc.turn_off_light(int(channel) - 1, 1)
                                else:
                                    logging.error(
                                        "Unrecognized channel_control mode "
                                        "defined in preshow_configuration " + str(mode)
                                    )
                    # hold transition for specified time
                    while transition["duration"] > (time.time() - start):
                        # check for play now
                        if check_state():
                            # kill the audio playback if playing
                            if self.audio:
                                os.killpg(self.audio.pid, signal.SIGTERM)
                                self.audio = None
                            return PrePostShow.play_now_interrupt
                        time.sleep(0.1)
            except:
                pass

        # hold show until audio has finished if we have audio
        # or audio is not finished
        return_value = self.hold_for_audio()

        return return_value
def main():
    '''main'''
    song_to_play = int(cm.get_state('song_to_play', 0))
    play_now = int(cm.get_state('play_now', 0))

    # Arguments
    parser = argparse.ArgumentParser()
    filegroup = parser.add_mutually_exclusive_group()
    filegroup.add_argument('--playlist',
                           default=_PLAYLIST_PATH,
                           help='Playlist to choose song from.')
    filegroup.add_argument('--file',
                           help='path to the song to play (required if no'
                           'playlist is designated)')
    parser.add_argument(
        '--readcache',
        type=int,
        default=1,
        help='read light timing from cache if available. Default: true')
    args = parser.parse_args()

    # Log everything to our log file
    # TODO(todd): Add logging configuration options.
    logging.basicConfig(
        filename=cm.LOG_DIR + '/music_and_lights.play.dbg',
        format='[%(asctime)s] %(levelname)s {%(pathname)s:%(lineno)d}'
        ' - %(message)s',
        level=logging.DEBUG)

    # Make sure one of --playlist or --file was specified
    if args.file == None and args.playlist == None:
        print "One of --playlist or --file must be specified"
        sys.exit()

    # Initialize Lights
    hc.initialize()

    # Only execute preshow if no specific song has been requested to be played right now
    if not play_now:
        execute_preshow(cm.lightshow()['preshow'])

    # Determine the next file to play
    song_filename = args.file
    if args.playlist != None and args.file == None:
        most_votes = [None, None, []]
        current_song = None
        with open(args.playlist, 'rb') as playlist_fp:
            fcntl.lockf(playlist_fp, fcntl.LOCK_SH)
            playlist = csv.reader(playlist_fp, delimiter='\t')
            songs = []
            for song in playlist:
                if len(song) < 2 or len(song) > 4:
                    logging.error(
                        'Invalid playlist.  Each line should be in the form: '
                        '<song name><tab><path to song>')
                    sys.exit()
                elif len(song) == 2:
                    song.append(set())
                else:
                    song[2] = set(song[2].split(','))
                    if len(song) == 3 and len(song[2]) >= len(most_votes[2]):
                        most_votes = song
                songs.append(song)
            fcntl.lockf(playlist_fp, fcntl.LOCK_UN)

        if most_votes[0] != None:
            logging.info("Most Votes: " + str(most_votes))
            current_song = most_votes

            # Update playlist with latest votes
            with open(args.playlist, 'wb') as playlist_fp:
                fcntl.lockf(playlist_fp, fcntl.LOCK_EX)
                writer = csv.writer(playlist_fp, delimiter='\t')
                for song in songs:
                    if current_song == song and len(song) == 3:
                        song.append("playing!")
                    if len(song[2]) > 0:
                        song[2] = ",".join(song[2])
                    else:
                        del song[2]
                writer.writerows(songs)
                fcntl.lockf(playlist_fp, fcntl.LOCK_UN)

        else:
            # Get random song
            if _RANDOMIZE_PLAYLIST:
                current_song = songs[random.randint(0, len(songs) - 1)]
            # Get a "play now" requested song
            elif play_now > 0 and play_now <= len(songs):
                current_song = songs[play_now - 1]
            # Play next song in the lineup
            else:
                song_to_play = song_to_play if (
                    song_to_play <= len(songs) - 1) else 0
                current_song = songs[song_to_play]
                next_song = (song_to_play + 1) if (
                    (song_to_play + 1) <= len(songs) - 1) else 0
                cm.update_state('song_to_play', next_song)

        # Get filename to play and store the current song playing in state cfg
        song_filename = current_song[1]
        cm.update_state('current_song', songs.index(current_song))

    song_filename = song_filename.replace("$SYNCHRONIZED_LIGHTS_HOME",
                                          cm.HOME_DIR)

    # Ensure play_now is reset before beginning playback
    if play_now:
        cm.update_state('play_now', 0)
        play_now = 0

    # Initialize FFT stats
    matrix = [0 for _ in range(hc.GPIOLEN)]
    offct = [0 for _ in range(hc.GPIOLEN)]

    # Build the limit list
    if len(_LIMIT_LIST) == 1:
        limit = [_LIMIT_LIST[0] for _ in range(hc.GPIOLEN)]
    else:
        limit = _LIMIT_LIST

    # Set up audio
    if song_filename.endswith('.wav'):
        musicfile = wave.open(song_filename, 'r')
    else:
        musicfile = decoder.open(song_filename)

    sample_rate = musicfile.getframerate()
    num_channels = musicfile.getnchannels()
    output = aa.PCM(aa.PCM_PLAYBACK, aa.PCM_NORMAL)
    output.setchannels(num_channels)
    output.setrate(sample_rate)
    output.setformat(aa.PCM_FORMAT_S16_LE)
    output.setperiodsize(CHUNK_SIZE)

    # Output a bit about what we're about to play
    song_filename = os.path.abspath(song_filename)
    logging.info("Playing: " + song_filename + " (" +
                 str(musicfile.getnframes() / sample_rate) + " sec)")

    cache = []
    cache_found = False
    cache_filename = os.path.dirname(song_filename) + "/." + os.path.basename(song_filename) \
        + ".sync.gz"
    # The values 12 and 1.5 are good estimates for first time playing back (i.e. before we have
    # the actual mean and standard deviations calculated for each channel).
    mean = [12.0 for _ in range(hc.GPIOLEN)]
    std = [1.5 for _ in range(hc.GPIOLEN)]
    if args.readcache:
        # Read in cached fft
        try:
            with gzip.open(cache_filename, 'rb') as playlist_fp:
                cachefile = csv.reader(playlist_fp, delimiter=',')
                for row in cachefile:
                    cache.append([
                        0.0 if np.isinf(float(item)) else float(item)
                        for item in row
                    ])
                cache_found = True
                # TODO(todd): Optimize this and / or cache it to avoid delay here
                cache_matrix = np.array(cache)
                for i in range(0, hc.GPIOLEN):
                    std[i] = np.std(
                        [item for item in cache_matrix[:, i] if item > 0])
                    mean[i] = np.mean(
                        [item for item in cache_matrix[:, i] if item > 0])
                logging.debug("std: " + str(std) + ", mean: " + str(mean))
        except IOError:
            logging.warn("Cached sync data song_filename not found: '" +
                         cache_filename + ".  One will be generated.")

    # Process audio song_filename
    row = 0
    data = musicfile.readframes(CHUNK_SIZE)
    frequency_limits = calculate_channel_frequency(
        _MIN_FREQUENCY, _MAX_FREQUENCY, _CUSTOM_CHANNEL_MAPPING,
        _CUSTOM_CHANNEL_FREQUENCIES)

    while data != '' and not play_now:
        output.write(data)

        # Control lights with cached timing values if they exist
        matrix = None
        if cache_found and args.readcache:
            if row < len(cache):
                matrix = cache[row]
            else:
                logging.warning(
                    "Ran out of cached FFT values, will update the cache.")
                cache_found = False

        if matrix == None:
            # No cache - Compute FFT in this chunk, and cache results
            matrix = calculate_levels(data, sample_rate, frequency_limits)
            cache.append(matrix)

        # blank out the display
        led.fill(Color(0, 0, 0), 0, 151)
        for i in range(0, hc.GPIOLEN):
            if hc.is_pin_pwm(i):
                # Output pwm, where off is at 0.5 std below the mean
                # and full on is at 0.75 std above the mean.

                display_column(i, matrix[i])

                #brightness = matrix[i] - mean[i] + 0.5 * std[i]
                #brightness = brightness / (1.25 * std[i])
                #if brightness > 1.0:
                #brightness = 1.0
                #if brightness < 0:
                #brightness = 0
                #hc.turn_on_light(i, True, int(brightness * 60))
            else:
                if limit[i] < matrix[i] * _LIMIT_THRESHOLD:
                    limit[i] = limit[i] * _LIMIT_THRESHOLD_INCREASE
                    logging.debug("++++ channel: {0}; limit: {1:.3f}".format(
                        i, limit[i]))
                # Amplitude has reached threshold
                if matrix[i] > limit[i]:
                    hc.turn_on_light(i, True)
                    offct[i] = 0
                else:  # Amplitude did not reach threshold
                    offct[i] = offct[i] + 1
                    if offct[i] > _MAX_OFF_CYCLES:
                        offct[i] = 0
                        limit[i] = limit[
                            i] * _LIMIT_THRESHOLD_DECREASE  # old value 0.8
                    logging.debug("---- channel: {0}; limit: {1:.3f}".format(
                        i, limit[i]))
                    hc.turn_off_light(i, True)

        # send out data to RGB LED Strip
        led.update()
        # Read next chunk of data from music song_filename
        data = musicfile.readframes(CHUNK_SIZE)
        row = row + 1

        # Load new application state in case we've been interrupted
        cm.load_state()
        play_now = int(cm.get_state('play_now', 0))

    if not cache_found:
        with gzip.open(cache_filename, 'wb') as playlist_fp:
            writer = csv.writer(playlist_fp, delimiter=',')
            writer.writerows(cache)
            logging.info("Cached sync data written to '." + cache_filename +
                         "' [" + str(len(cache)) + " rows]")

    # We're done, turn it all off ;)
    hc.clean_up()