Exemple #1
0
    def emission_log_probability(self, emission, state):
        """
        Gives the probability P(emission | label). Returned as a base 2
        log.
        
        The emission should be a pair of (root,label), together defining a 
        chord.
        
        There's a special case of this. If the emission is a list, it's 
        assumed to be a I{distribution} over emissions. The list should 
        contain (prob,em) pairs, where I{em} is an emission, such as is 
        normally passed into this function, and I{prob} is the weight to 
        give to this possible emission. The probabilities of the possible 
        emissions are summed up, weighted by the I{prob} values.
        
        """
        if type(emission) is list:
            # Average probability over the possible emissions
            probs = []
            for (prob, em) in emission:
                probs.append(logprob(prob) + \
                             self.emission_log_probability(em, state))
            return sum_logs(probs)

        # Single chord label
        point, function = state
        chord_root, label = emission
        X, Y, x, y = point
        # Work out the chord substitution
        subst = (chord_root - coordinate_to_et_2d((x, y))) % 12

        # Generate the substitution given the chord function
        subst_prob = self.subst_emission_dist[function].logprob(subst)
        # Generate the label given the subst and chord function
        label_prob = self.type_emission_dist[(subst, function)].logprob(label)
        return subst_prob + label_prob
Exemple #2
0
 def emission_log_probability(self, emission, state):
     """
     Gives the probability P(emission | label). Returned as a base 2
     log.
     
     The emission should be a pair of (root,label), together defining a 
     chord.
     
     There's a special case of this. If the emission is a list, it's 
     assumed to be a I{distribution} over emissions. The list should 
     contain (prob,em) pairs, where I{em} is an emission, such as is 
     normally passed into this function, and I{prob} is the weight to 
     give to this possible emission. The probabilities of the possible 
     emissions are summed up, weighted by the I{prob} values.
     
     """
     if type(emission) is list:
         # Average probability over the possible emissions
         probs = []
         for (prob,em) in emission:
             probs.append(logprob(prob) + \
                          self.emission_log_probability(em, state))
         return sum_logs(probs)
     
     # Single chord label
     point,function = state
     chord_root,label = emission
     X,Y,x,y = point
     # Work out the chord substitution
     subst = (chord_root - coordinate_to_et_2d((x,y))) % 12
     
     # Generate the substitution given the chord function
     subst_prob = self.subst_emission_dist[function].logprob(subst)
     # Generate the label given the subst and chord function
     label_prob = self.type_emission_dist[(subst,function)].logprob(label)
     return subst_prob + label_prob
Exemple #3
0
def path_to_tones(path, tempo=120, chord_types=None, root_octave=0, 
        double_root=False, equal_temperament=False, timings=False):
    """
    Takes a tonal space path, given as a list of coordinates, and 
    generates the tones of the roots.
    
    @type path: list of (3d-coordinate,length) tuples
    @param path: coordinates of the points in the sequence and the length 
        of each, in beats
    @type tempo: int
    @param tempo: speed in beats per second (Maelzel's metronome)
    @type chord_types: list of (string,length)
    @param chord_types: the type of chord to use for each tone and the 
        time spent on that chord type, in beats. See 
        L{CHORD_TYPES} keys for possible values.
    @type equal_temperament: bool
    @param equal_temperament: render all the pitches as they would be 
        played in equal temperament.
    @rtype: L{ToneMatrix}
    @return: a tone matrix that can be used to render the sound
    
    """
    # Use this envelope for all notes
    envelope = piano_envelope()
    
    sample_rate = DEFAULT_SAMPLE_RATE
    
    beat_length = 60.0 / tempo
    if timings:
        root_times = path
    else:
        # Work out when each root change occurs
        time = Fraction(0)
        root_times = []
        for root,length in path:
            root_times.append((root,time))
            time += length
    def _root_at_time(time):
        current_root = root_times[0][0]
        for root,rtime in root_times[1:]:
            # Move through root until we get the first one that 
            #  occurs after the previous time
            if rtime > time:
                return current_root
            current_root = root
        # If we're beyond the time of the last root, use that one
        return current_root
    
    if chord_types is None:
        # Default to just pure tones
        chord_types = [('prime',length) for __,length in path]
        
    if equal_temperament:
        _pitch_ratio = tonal_space_et_pitch
    else:
        _pitch_ratio = tonal_space_pitch_2d
    
    # Build the tone matrix by adding the tones one by one
    matrix = ToneMatrix(sample_rate=sample_rate)
    time = Fraction(0)
    for ctype,length in chord_types:
        coord = _root_at_time(time)
        pitch_ratio = _pitch_ratio(coord)
        duration = beat_length * float(length)
        # We want all enharmonic equivs of I to come out close to I, 
        #  not an octave above
        if not equal_temperament and coordinate_to_et_2d(coord) == 0 \
                and pitch_ratio > 1.5:
            pitch_ratio /= 2.0
        # Use a sine tone for each note
        tone = SineChordEvent(220*pitch_ratio, chord_type=ctype, duration=duration, envelope=envelope, root_octave=root_octave, root_weight=1.2, double_root=double_root)
        matrix.add_tone(beat_length * float(time), tone)
        time += length
    return matrix
Exemple #4
0
class HmmPathNgram(NgramModel):
    """
    An ngram model that takes multiple chords (weighted by probability) as 
    input to its decoding. It is trained on labeled data.
    
    This is similar to 
    L{jazzparser.taggers.ngram_multi.model.MultiChordNgramModel}, but the 
    states represent points on a TS path, rather than categories.
    
    """
    def __init__(self,
                 order,
                 point_transition_counts,
                 fn_transition_counts,
                 type_emission_counts,
                 subst_emission_counts,
                 estimator,
                 backoff_model,
                 chord_map,
                 vector_dom,
                 point_dom,
                 history=""):
        self.order = order
        self.backoff_model = backoff_model

        chord_vocab = list(set(chord_map.keys()))
        self.chord_vocab = chord_vocab
        internal_chord_vocab = list(set(chord_map.values()))
        self.chord_map = chord_map
        self._estimator = estimator

        # Construct the domains by combining possible roots with
        #  the other components of the labels
        self.vector_dom = vector_dom
        self.point_dom = point_dom
        self.label_dom = [(point,function) for point in point_dom \
                                            for function in ["T","D","S"] ]
        self.num_labels = len(self.label_dom)
        self.emission_dom = [(root,label) for root in range(12) \
                                        for label in internal_chord_vocab]
        self.num_emissions = len(self.emission_dom)

        # Keep hold of the freq dists
        self.point_transition_counts = point_transition_counts
        self.fn_transition_counts = fn_transition_counts
        self.type_emission_counts = type_emission_counts
        self.subst_emission_counts = subst_emission_counts
        # Make some prob dists
        self.point_transition_dist = ConditionalProbDist(
            point_transition_counts, estimator, len(vector_dom))
        self.fn_transition_dist = ConditionalProbDist(
            fn_transition_counts, estimator, 4)  # Includes final state
        self.type_emission_dist = ConditionalProbDist(
            type_emission_counts, estimator, len(internal_chord_vocab))
        self.subst_emission_dist = ConditionalProbDist(subst_emission_counts,
                                                       estimator, 12)

        # Store a string with information about training, etc
        self.history = history
        # Initialize the various caches
        # These will be filled as we access probabilities
        self.clear_cache()

    def add_history(self, string):
        """ Adds a line to the end of this model's history string. """
        self.history += "%s: %s\n" % (datetime.now().isoformat(' '), string)

    @staticmethod
    def train(data,
              estimator,
              grammar,
              cutoff=0,
              logger=None,
              chord_map=None,
              order=2,
              backoff_orders=0,
              backoff_kwargs={}):
        """
        Initializes and trains an HMM in a supervised fashion using the given 
        training data. Training data should be chord sequence data (input 
        type C{bulk-db} or C{bulk-db-annotated}).
        
        """
        # Prepare a dummy logger if none was given
        if logger is None:
            logger = create_dummy_logger()
        logger.info(">>> Beginning training of ngram backoff model")

        training_data = []
        # Generate the gold standard data by parsing the annotations
        for dbinput in data:
            # Get a gold standard tonal space sequence
            try:
                parses = parse_sequence_with_annotations(dbinput, grammar, \
                                                        allow_subparses=False)
            except ParseError, err:
                # Just skip this sequence
                logger.error('Could not get a GS parse of %s: %s' %
                             (dbinput, err))
                continue
            # There should only be one of these now
            parse = parses[0]
            if parse is None:
                logger.error('Could not get a GS parse of %s' % (dbinput))
                continue

            # Get the form of the analysis we need for the training
            if chord_map is None:
                chords = [(c.root, c.type) for c in dbinput.chords]
            else:
                chords = [(c.root, chord_map[c.type]) for c in dbinput.chords]

            points, times = zip(
                *grammar.formalism.semantics_to_coordinates(parse.semantics))
            # Run through the sequence, transforming absolute points into
            #  the condensed relative representation
            ec0 = EnharmonicCoordinate.from_harmonic_coord(points[0])
            # The first point is relative to the origin and always in the
            #  (0,0) enharmonic space
            rel_points = [(0, 0, ec0.x, ec0.y)]
            for point in points[1:]:
                ec1 = EnharmonicCoordinate.from_harmonic_coord(point)
                # Find the nearest enharmonic instance of this point to the last
                nearest = ec0.nearest((ec1.x, ec1.y))
                # Work out how much we have to shift this by to get the point
                dX = ec1.X - nearest.X
                dY = ec1.Y - nearest.Y
                rel_points.append((dX, dY, ec1.x, ec1.y))
                ec0 = ec1
            funs, times = zip(
                *grammar.formalism.semantics_to_functions(parse.semantics))

            ### Synchronize the chords with the points and functions
            # We may need to repeat chords to match up with analysis
            #  points that span multiple chords
            analysis = iter(zip(rel_points, funs, times))
            rel_point, fun, __ = analysis.next()
            next_rel_point, next_fun, next_anal_time = analysis.next()
            # Keep track of how much time has elapsed
            time = 0
            training_seq = []
            reached_end = False
            for crd_pair, chord in zip(chords, dbinput.chords):
                if time >= next_anal_time and not reached_end:
                    # Move on to the next analysis point
                    rel_point, fun = next_rel_point, next_fun
                    try:
                        next_rel_point, next_fun, next_anal_time = analysis.next(
                        )
                    except StopIteration:
                        # No more points: keep using the same to the end
                        reached_end = True
                training_seq.append((crd_pair, (rel_point, fun)))
                time += chord.duration
            training_data.append(training_seq)

        # Create some empty freq dists
        subst_emission_counts = CutoffConditionalFreqDist(cutoff=cutoff)
        type_emission_counts = CutoffConditionalFreqDist(cutoff=cutoff)
        point_transition_counts = CutoffConditionalFreqDist(cutoff=cutoff)
        fn_transition_counts = CutoffConditionalFreqDist(cutoff=cutoff)

        seen_vectors = []
        seen_points = set()
        seen_XY = set()

        # Count all the stats from the training data
        for seq in training_data:
            # Keep track of the necessary history context
            history = []
            for (chord, (point, fun)) in seq:
                ### Counts for the emission distribution
                chord_root, label = chord
                # Work out the chord substitution
                X, Y, x, y = point
                subst = (chord_root - coordinate_to_et_2d((x, y))) % 12
                # Increment the counts
                subst_emission_counts[fun].inc(subst)
                type_emission_counts[(subst, fun)].inc(label)

                seen_points.add(point)
                seen_XY.add((X, Y))

                if order > 1:
                    ### Counts for the transition distribution
                    # Update the history fifo
                    history = [(point, fun)] + history[:order - 1]
                    if len(history) > 1:
                        points, functions = zip(*history)
                        #~ # Get the vectors between the points
                        #~ vectors = [vector(p0,p1) for (p1,p0) in \
                        #~ group_pairs([p for p in points if p is not None])]
                        # The function is conditioned on all previous functions
                        fn_context = tuple(functions[1:])
                        fn_transition_counts[fn_context].inc(functions[0])

                        #~ # The vector is conditioned on the function
                        #~ #  and the pairs of vector and function preceding that
                        #~ point_context = tuple([functions[:2]] +
                        #~ list(zip(vectors[1:], functions[2:])))
                        # The vector is conditioned on the function
                        #~ point_transition_counts[point_context].inc(vectors[0])
                        vect = vector(points[1], points[0])
                        point_transition_counts[functions[0]].inc(vect)

                        # Keep track of what vectors we've observed
                        #~ seen_vectors.append(vectors[0])
                        seen_vectors.append(vect)
                    else:
                        # For the first point, we only count the function prob
                        fn_transition_counts[tuple()].inc(fun)

            if order > 1:
                # Count the transition to the final state
                history = history[:order - 1]
                points, functions = zip(*history)
                fn_context = tuple(functions)
                fn_transition_counts[fn_context].inc(None)

        # Labels are (X,Y,x,y). We want all the points seen in the data
        #  and all (0,0,x,y)
        for X, Y in seen_XY:
            for x in range(4):
                for y in range(3):
                    seen_points.add((X, Y, x, y))
        point_dom = list(seen_points)

        if backoff_orders > 0:
            # Default to using the same params for the backoff model
            kwargs = {
                'cutoff': cutoff,
                'chord_map': chord_map,
            }
            kwargs.update(backoff_kwargs)

            logger.info("Training backoff model")
            # Train a backoff model
            backoff = HmmPathNgram.train(data,
                                         estimator,
                                         grammar,
                                         logger=logger,
                                         order=order - 1,
                                         backoff_orders=backoff_orders - 1,
                                         backoff_kwargs=backoff_kwargs,
                                         **kwargs)
        else:
            backoff = None

        # Get a list of every vector in the training set
        vector_dom = list(set(seen_vectors))

        return HmmPathNgram(order, point_transition_counts,
                            fn_transition_counts, type_emission_counts,
                            subst_emission_counts, estimator, backoff,
                            chord_map, vector_dom, point_dom)