Source code for partitura.musicanalysis.key_identification

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module implements Krumhansl and Schmuckler key estimation method.

References
----------
.. [2] Krumhansl, Carol L. (1990) "Cognitive foundations of musical pitch",
       Oxford University Press, New York.
"""
import numpy as np
from scipy.linalg import circulant
from partitura.utils.music import ensure_notearray

__all__ = ["estimate_key"]

# List of labels for each key (Use enharmonics as needed).
# Each tuple is (key root name, mode, fifths)
# The key root name is equal to that with the smallest fifths in
# the circle of fifths.
KEYS = [
    ("C", "major", 0),
    ("Db", "major", -5),
    ("D", "major", 2),
    ("Eb", "major", -3),
    ("E", "major", 4),
    ("F", "major", -1),
    ("F#", "major", 6),
    ("G", "major", 1),
    ("Ab", "major", -4),
    ("A", "major", 3),
    ("Bb", "major", -2),
    ("B", "major", 5),
    ("C", "minor", -3),
    ("C#", "minor", 4),
    ("D", "minor", -1),
    ("D#", "minor", 6),
    ("E", "minor", 1),
    ("F", "minor", -4),
    ("F#", "minor", 3),
    ("G", "minor", -2),
    ("G#", "minor", 5),
    ("A", "minor", 0),
    ("Bb", "minor", -5),
    ("B", "minor", 2),
]

VALID_KEY_PROFILES = [
    "krumhansl_kessler",
    "kk",
    "temperley",
    "tp",
    "kostka_payne",
    "kp",
]


# Krumhansl--Kessler Key Profiles

# From Krumhansl's "Cognitive Foundations of Musical Pitch" pp.30
key_prof_maj_kk = np.array(
    [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
)

key_prof_min_kk = np.array(
    [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
)

# Temperley Key Profiles

# CBMS (from "Music and Probability" Table 6.1, pp. 86)
key_prof_maj_cbms = np.array(
    [5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0]
)

key_prof_min_cbms = np.array(
    [5.0, 2.0, 3.5, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 1.5, 4.0]
)

# Kostka-Payne (from "Music and Probability" Table 6.1, pp. 86)
key_prof_maj_kp = np.array(
    [0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400]
)

key_prof_min_kp = np.array(
    [0.712, 0.048, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330]
)


def build_key_profile_matrix(key_prof_maj, key_prof_min):
    """
    Generate Matrix of key profiles
    """
    # Normalize Key profiles
    key_prof_maj /= np.sum(key_prof_maj)
    key_prof_min /= np.sum(key_prof_min)

    # Create matrix of key profiles
    Key_prof_mat = np.vstack(
        (circulant(key_prof_maj).transpose(), circulant(key_prof_min).transpose())
    )

    return Key_prof_mat


# Key profile matrices
KRUMHANSL_KESSLER = build_key_profile_matrix(key_prof_maj_kk, key_prof_min_kk)
CMBS = build_key_profile_matrix(key_prof_maj_cbms, key_prof_min_cbms)
KOSTKA_PAYNE = build_key_profile_matrix(key_prof_maj_kp, key_prof_min_kp)


[docs]def estimate_key(note_info, method="krumhansl", *args, **kwargs): """ Estimate key of a piece by comparing the pitch statistics of the note array to key profiles [2]_, [3]_. Parameters ---------- note_info : structured array, `Part` or `PerformedPart` Note information as a `Part` or `PerformedPart` instances or as a structured array. If it is a structured array, it has to contain the fields generated by the `note_array` properties of `Part` or `PerformedPart` objects. If the array contains onset and duration information of both score and performance, (e.g., containing both `onset_beat` and `onset_sec`), the score information will be preferred. method : {'krumhansl'} Method for estimating the key. For now 'krumhansl' is the only supported method. args, kwargs Positional and Keyword arguments for the key estimation method Returns ------- str String representing the key name (i.e., Root(alteration)(m if minor)). See `partitura.utils.key_name_to_fifths_mode` and `partitura.utils.fifths_mode_to_key_name`. References ---------- .. [2] Krumhansl, Carol L. (1990) "Cognitive foundations of musical pitch", Oxford University Press, New York. .. [3] Temperley, D. (1999) "What's key for key? The Krumhansl-Schmuckler key-finding algorithm reconsidered". Music Perception. 17(1), pp. 65--100. """ if method not in ("krumhansl",): raise ValueError('For now the only valid method is "krumhansl"') if method == "krumhansl": kid = ks_kid if "key_profiles" not in kwargs: kwargs["key_profiles"] = "krumhansl_kessler" else: if kwargs["key_profiles"] not in VALID_KEY_PROFILES: raise ValueError( "Invalid key_profiles. " 'Valid options are "ks", "cmbs" or "kp"' ) note_array = ensure_notearray(note_info) return kid(note_array, *args, **kwargs)
def format_key(root, mode, fifths): return "{}{}".format(root, "m" if mode == "minor" else "") def ks_kid(note_array, key_profiles=KRUMHANSL_KESSLER, return_sorted_keys=False): """ Estimate key of a piece using the Krumhansl-Schmuckler algorithm. """ if isinstance(key_profiles, str): if key_profiles in ("ks", "krumhansl_kessler"): key_profiles = KRUMHANSL_KESSLER elif key_profiles in ("temperley", "cmbs"): key_profiles = CMBS elif key_profiles in ("kp", "kostka_payne"): key_profiles = KOSTKA_PAYNE else: raise ValueError( "Invalid key_profiles. " 'Valid options are "ks", "cmbs" or "kp"' ) corrs = _similarity_with_pitch_profile( note_array=note_array, key_profiles=key_profiles, similarity_func=corr ) if return_sorted_keys: return [format_key(*KEYS[i]) for i in np.argsort(corrs)[::-1]] else: return format_key(*KEYS[corrs.argmax()]) def corr(x, y): return np.corrcoef(x, y)[0, 1] def _similarity_with_pitch_profile( note_array, key_profiles=KRUMHANSL_KESSLER, similarity_func=corr, normalize_distribution=False, ): from partitura.utils.music import get_time_units_from_note_array _, duration_unit = get_time_units_from_note_array(note_array) # Get pitch classes pitch_classes = np.mod(note_array["pitch"], 12) # Compute weighted key distribution pitch_distribution = np.array( [ note_array[duration_unit][np.where(pitch_classes == pc)[0]].sum() for pc in range(12) ] ) if normalize_distribution: # normalizing is unnecessary for computing the correlation, but might # be necessary for other similarity metrics pitch_distribution = pitch_distribution / float(pitch_distribution.sum()) # Compute correlation with key profiles similarity = np.array( [similarity_func(pitch_distribution, kp) for kp in key_profiles] ) return similarity