def melodic_arch(score: stream.Stream, phrase_length: int):
    total_phrases = 0  #total number of phrases
    sum_pitch_height = [
        0 for i in range(phrase_length)
    ]  #sum of heights for each note position, measured in semitones above middle C
    phrase = []  #phrase is empty at beginning of piece
    for n in score.recurse().notesAndRests:
        if isinstance(n, chord.Chord):
            n = max(n)
        if n.tie and (n.tie.type == 'stop' or n.tie.type
                      == 'continue'):  #do not count a tied note more than once
            continue
        if n.isRest or (len(n.expressions) != 0
                        and 'fermata' == n.expressions[0].name):
            if len(phrase) == phrase_length:
                total_phrases += 1
                for i in range(phrase_length):
                    sum_pitch_height[
                        i] += phrase[i].pitch.ps - 60  #60 is middle C
            phrase = []
        else:
            phrase.append(n)
    #if reached end of score, check the last phrase is of desired length
    if len(phrase) == phrase_length:
        total_phrases += 1
        for i in range(phrase_length):
            sum_pitch_height[i] += phrase[i].pitch.ps - 60

    if total_phrases == 0:
        return None
    return [height / total_phrases for height in sum_pitch_height]
def avg_phrase_length(score: stream.Stream):
    total_phrases = 0  #total number of phrases
    length = 0  #phrase is empty at beginning of piece
    phrase_lengths = []  #all the phrase lengths
    for n in score.recurse().notesAndRests:
        if isinstance(n, chord.Chord):
            n = max(n)
        if n.tie and (n.tie.type == 'stop' or n.tie.type
                      == 'continue'):  #do not count a tied note more than once
            continue
        if n.isRest or (len(n.expressions) != 0
                        and 'fermata' == n.expressions[0].name):
            phrase_lengths.append(length)
            total_phrases += 1
            length = 0
        else:
            length += 1
    #reached end
    if length != 0:
        phrase_lengths.append(length)
        total_phrases += 1

    if total_phrases == 0:
        return None
    return sum(phrase_lengths) / total_phrases
def get_note_lengths(score: stream.Stream):
    note_lengths = dict()
    note_count = 0.0
    for n in score.recurse().notesAndRests:
        if isinstance(n, chord.Chord):
            n = max(n)
        if n.quarterLength not in note_lengths:
            note_lengths[n.quarterLength] = 1
        else:
            note_lengths[n.quarterLength] += 1
        note_count += 1.0
    i = 0
    vector = []
    while i < 4:
        i += 0.25
        if i in note_lengths:
            vector.append(note_lengths[i] / note_count)
        else:
            vector.append(0)

    return vector
def nonharmonic_notes(score: stream.Stream):
    key_sig = score.analyze('key')
    certainty = key_sig.tonalCertainty()
    notes_within_key = [p.name for p in key_sig.pitches]
    for pitch in key_sig.pitches:
        notes_within_key.extend(
            [p.name for p in pitch.getAllCommonEnharmonics()])
    total_notes = 0  #total number of notes
    num_nonharmonic = 0  #total number of nonharmonic notes
    for n in score.recurse().notes:
        if isinstance(n, chord.Chord):
            n = max(n)
        if n.tie and (n.tie.type == 'stop' or n.tie.type
                      == 'continue'):  #do not count a tied note more than once
            continue
        else:
            if n.pitch.name not in notes_within_key:
                num_nonharmonic += 1
            total_notes += 1

    if total_notes == 0:
        return None

    return (certainty, 1 - num_nonharmonic / total_notes)
def get_key(score: stream.Stream):
    first_key = score.recurse().getElementsByClass(key.KeySignature)
    if not first_key:
        return 0
    else:
        return first_key[0].sharps
def get_meter(score: stream.Stream):
    first_meter = score.recurse().getElementsByClass(meter.TimeSignature)
    if not first_meter:
        return (0, 0)
    else:
        return (first_meter[0].numerator, first_meter[0].denominator)