Skip to content

Instantly share code, notes, and snippets.

@bmcfee
Created September 7, 2017 19:02
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bmcfee/1f66825cef2eb34c839b42dddbad49fd to your computer and use it in GitHub Desktop.
Save bmcfee/1f66825cef2eb34c839b42dddbad49fd to your computer and use it in GitHub Desktop.
Krumhansl-Schmuckler key estimation
import numpy as np
import scipy.linalg
import scipy.stats
def ks_key(X):
'''Estimate the key from a pitch class distribution
Parameters
----------
X : np.ndarray, shape=(12,)
Pitch-class energy distribution. Need not be normalized
Returns
-------
major : np.ndarray, shape=(12,)
minor : np.ndarray, shape=(12,)
For each key (C:maj, ..., B:maj) and (C:min, ..., B:min),
the correlation score for `X` against that key.
'''
X = scipy.stats.zscore(X)
# Coefficients from Kumhansl and Schmuckler
# as reported here: http://rnhart.net/articles/key-finding/
major = np.asarray([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88])
major = scipy.stats.zscore(major)
minor = np.asarray([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17])
minor = scipy.stats.zscore(minor)
# Generate all rotations of major
major = scipy.linalg.circulant(major)
minor = scipy.linalg.circulant(minor)
return major.T.dot(X), minor.T.dot(X)
@devanshugupta
Copy link

how is the correlation score greater than 1?

@AndreyPikunov
Copy link

I've slightly adapted this code and fixed "greater than 1" issue mentioned by @devanshugupta.

import numpy as np
import scipy.linalg
from scipy.stats import zscore

from typing import List


@dataclass
class KeyEstimator:

    # adapted from:
    # https://gist.github.com/bmcfee/1f66825cef2eb34c839b42dddbad49fd

    major = np.asarray(
        [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
    )
    minor = np.asarray(
        [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
    )

    def __post_init__(self):
        self.major = zscore(self.major)
        self.major_norm = scipy.linalg.norm(self.major)
        self.major = scipy.linalg.circulant(self.major)

        self.minor = zscore(self.minor)
        self.minor_norm = scipy.linalg.norm(self.minor)
        self.minor = scipy.linalg.circulant(self.minor)

    def __call__(self, x: np.array) -> List[np.array]:

        x = zscore(x)
        x_norm = scipy.linalg.norm(x)

        coeffs_major = self.major.T.dot(x) / self.major_norm / x_norm
        coeffs_minor = self.minor.T.dot(x) / self.minor_norm / x_norm

        return coeffs_major, coeffs_minor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment