def predict_song(song_start, scoring_function, frame_filter=None, length=100, order=1, variability=0):
    from copy import copy
    from random import randint

    song = copy(song_start)
    init_song_len = utils.song_length(song)
    possible_frames = [[int(note) for note in frame.split("|")] for frame in hmm.all_observations()]

    cache = dict()

    for i in range(0, length - init_song_len):

        print "SONG: %s" % (song)

        if i == 0:
            current_frame_obs = [[channel[i]] for channel in song]
        else:
            obs_to_pull = min(utils.song_length(song), order - 1)
            current_frame_obs = [channel[-1 * obs_to_pull:] for channel in song]

        scores = {}

        prev_frame = [channel[-1] for channel in song]

        if frame_filter is not None:
            filtered_frames = [frame for frame in possible_frames if frame_filter(prev_frame, frame)]
            # if we filtered everything out, easy back and let the full set back in
            if len(filtered_frames) > 0:
                local_possible_frames = filtered_frames
            else:
                local_possible_frames = possible_frames
        else:
            local_possible_frames = possible_frames

        for a_frame in local_possible_frames:
            score = scoring_function(current_frame_obs, a_frame, smooth=False, cache=cache)
            scores["|".join([str(note) for note in a_frame])] = score

        sorted_scores = sorted([(value, key) for (key, value) in scores.items() if value is not None], reverse=True)

        print current_frame_obs
        print sorted_scores[0:10]

        # If we don't have any valid transitions from this state, we just have
        # to guess a new one at random (not great!)  We just use semi popular
        # observations, to limit the damage
        if len(sorted_scores) == 0:
            print "WARNING, Random Transition!"
            random_frames = all_observations(cutoff=200)
            selected_observation = (0, random_frames[randint(0, len(random_frames) - 1)])
        elif variability == 0 or len(sorted_scores) == 1:
            selected_observation = sorted_scores[0]
        else:
            selected_observation = sorted_scores[randint(0, min(variability - 1, len(sorted_scores) - 1))]

        print selected_observation

        frame_parts = [int(part) for part in selected_observation[1].split("|")]

        for i in range(0, 3):
            song[i].append(frame_parts[i])
        print "Generated note {0}/{1}".format(utils.song_length(song), length)
    print song
    return song
    parser = argparse.ArgumentParser(description='Generate chiptunes, magically.')
    parser.add_argument('--file', default="", dest='file', help='The file to write the resulting midi song to.  Defaults to m-<model>_o-<order>_s-<shake>_l-<length>.mid')
    parser.add_argument('--length', default=20, dest='length', help="The number of notes to generate in the song", type=int)
    parser.add_argument('--model', default="hmm", dest='model', help="The model to use to generate the resulting file", choices=('hmm', 'bayes'))
    parser.add_argument('--order', dest='order', default=1, help="The order to use when generating predictions for the next note.  If using HMM, must be 2 or larger", type=int)
    parser.add_argument('--shake', dest='shake', default=1, help="How randomly to generate notes in the song", type=int)
    parser.add_argument('--filter', dest='filter', default='none', help="Custom filter to apply to the set of possible next notes considered for each step.  Options include: 'none', 'require_lead', 'no_repeats', 'no_empties', 'all', max_one_empty")
    parser.add_argument('--start', dest='start', default='', help="Song to start building a song off.  If not included, we'll just choose a random note.")

    args = parser.parse_args()

    scorer = hmm.score_transition if args.model == "hmm" else bayes_net.score_transition

    # Get a random starting note from the training data.
    possible_starts = all_observations(cutoff=200, include_starts=True)
    start_position = possible_starts[randint(0, len(possible_starts) - 1)]
    start_notes = start_position.split('|')

    def require_lead(pre_frame, post_frame):
        return post_frame[0] > 0

    def no_repeats(pre_frame, post_frame):
        return pre_frame != post_frame

    def no_empties(pre_frame, post_frame):
        return post_frame.count(0) == 0

    def max_one_empty(pre_frame, post_frame):
        return post_frame.count(0) <= 1