Source code for partitura.musicanalysis.pitch_spelling

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains methods for estimation pitch spelling using the ps13 algorithm.

References
----------
.. [4] Meredith, D. (2006). "The ps13 Pitch Spelling Algorithm". Journal
       of New Music Research, 35(2):121.
"""
import numpy as np
from collections import namedtuple
from partitura.utils import ensure_notearray, get_time_units_from_note_array

# from partitura.musicanalysis.utils import prepare_notearray


__all__ = ["estimate_spelling"]

ChromamorpheticPitch = namedtuple(
    "ChromamorpheticPitch", "chromatic_pitch morphetic_pitch"
)

STEPS = np.array(["A", "B", "C", "D", "E", "F", "G"])
UND_CHROMA = np.array([0, 2, 3, 5, 7, 8, 10], dtype=int)
ALTER = np.array(["n", "#", "b"])


[docs]def estimate_spelling(note_info, method="ps13s1", **kwargs): """Estimate pitch spelling using the ps13 algorithm [4]_, [5]_. 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 : {'ps13s1'} Pitch spelling algorithm. More methods will be added. **kwargs Keyword arguments for the algorithm specified in `method`. Returns ------- spelling : structured array Array with pitch spellings. The fields are 'step', 'alter' and 'octave' References ---------- .. [4] Meredith, D. (2006). "The ps13 Pitch Spelling Algorithm". Journal of New Music Research, 35(2):121. .. [5] Meredith, D. (2019). "RecurSIA-RRT: Recursive translatable point-set pattern discovery with removal of redundant translators". 12th International Workshop on Machine Learning and Music. Würzburg, Germany. """ if method == "ps13s1": ps = ps13s1 step, alter, octave = ps(ensure_notearray(note_info), **kwargs) spelling = np.empty( len(step), dtype=[("step", "U1"), ("alter", int), ("octave", int)] ) spelling["step"] = step spelling["alter"] = alter spelling["octave"] = octave return spelling
def ps13s1(note_array, K_pre=10, K_post=40): """ ps13s1 Pitch Spelling Algorithm """ onset_unit, _ = get_time_units_from_note_array(note_array) pitch_sort_idx = note_array["pitch"].argsort() onset_sort_idx = np.argsort( note_array[pitch_sort_idx][onset_unit], kind="mergesort" ) sort_idx = pitch_sort_idx[onset_sort_idx] re_idx = sort_idx.argsort() # o_idx[sort_idx] sorted_ocp = np.column_stack( ( note_array[sort_idx][onset_unit], chromatic_pitch_from_midi(note_array[sort_idx]["pitch"]), ) ) # n = len(sorted_ocp) # ChromaList chroma_array = compute_chroma_array(sorted_ocp=sorted_ocp) # ChromaVectorList chroma_vector_array = compute_chroma_vector_array( chroma_array=chroma_array, K_pre=K_pre, K_post=K_post ) morph_array = compute_morph_array( chroma_array=chroma_array, chroma_vector_array=chroma_vector_array ) morphetic_pitch = compute_morphetic_pitch(sorted_ocp, morph_array) step, alter, octave = p2pn( sorted_ocp[:, 1], morphetic_pitch.reshape( -1, ), ) # sort back pitch names step = step[re_idx] alter = alter[re_idx] octave = octave[re_idx] return step, alter, octave def chromatic_pitch_from_midi(midi_pitch): return midi_pitch - 21 def chroma_from_chromatic_pitch(chromatic_pitch): return np.mod(chromatic_pitch, 12) def pitch_class_from_chroma(chroma): return np.mod(chroma - 3, 12) def compute_chroma_array(sorted_ocp): return chroma_from_chromatic_pitch(sorted_ocp[:, 1]).astype(int) def compute_chroma_vector_array(chroma_array, K_pre, K_post): """ Computes the chroma frequency distribution within the context surrounding each note. """ n = len(chroma_array) chroma_vector = np.zeros(12, dtype=int) for i in range(np.minimum(n, K_post)): chroma_vector[chroma_array[i]] = 1 + chroma_vector[chroma_array[i]] chroma_vector_list = [chroma_vector.copy()] for i in range(1, n): if i + K_post <= n: chroma_vector[chroma_array[i + K_post - 1]] = ( 1 + chroma_vector[chroma_array[i + K_post - 1]] ) if i - K_pre > 0: chroma_vector[chroma_array[i - K_pre - 1]] = ( chroma_vector[chroma_array[i - K_pre - 1]] - 1 ) chroma_vector_list.append(chroma_vector.copy()) return np.array(chroma_vector_list) def compute_morph_array(chroma_array, chroma_vector_array): n = len(chroma_array) # Line 1: Initialize morph array morph_array = np.empty(n, dtype=int) # Compute m0 # Line 2 init_morph = np.array([0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6], dtype=int) # Line 3 c0 = chroma_array[0] # Line 4 m0 = init_morph[c0] # Line 5 morph_int = np.array([0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6], dtype=int) # Lines 6-8 tonic_morph_for_tonic_chroma = np.mod( m0 - morph_int[np.mod(c0 - np.arange(12), 12)], 7 ) # Line 10 tonic_chroma_set_for_morph = [[] for i in range(7)] # Line 11 morph_strength = np.zeros(7, dtype=int) # Line 12 for j in range(n): # Lines 13-15 (skipped line 9, since we do not need to # initialize morph_for_tonic_chroma) morph_for_tonic_chroma = np.mod( morph_int[np.mod(chroma_array[j] - np.arange(12), 12)] + tonic_morph_for_tonic_chroma, 7, ) # Lines 16-17 tonic_chroma_set_for_morph = [[] for i in range(7)] # Line 18 for m in range(7): # Line 19 for ct in range(12): # Line 20 if morph_for_tonic_chroma[ct] == m: # Line 21 tonic_chroma_set_for_morph[m].append(ct) # Line 22 for m in range(7): # Line 23 morph_strength[m] = sum( [chroma_vector_array[j, ct] for ct in tonic_chroma_set_for_morph[m]] ) # Line 24 morph_array[j] = np.argmax(morph_strength) return morph_array def compute_ocm_chord_list(sorted_ocp, chroma_array, morph_array): # Lines 1-3 ocm_array = np.column_stack((sorted_ocp[:, 0], chroma_array, morph_array)).astype( int ) # Alternative implementation of lines 4--9 unique_onsets = np.unique(ocm_array[:, 0]) unique_onset_idxs = [np.where(ocm_array[:, 0] == u) for u in unique_onsets] ocm_chord_list = [ocm_array[uix] for uix in unique_onset_idxs] return ocm_chord_list def compute_morphetic_pitch(sorted_ocp, morph_array): """ Compute morphetic pitch Parameters ---------- sorted_ocp : array Sorted array of (onset in beats, chromatic pitch) morph_array : array Array of morphs Returns ------- morphetic_pitch : array Morphetic pitch of the notes """ n = len(sorted_ocp) chromatic_pitch = sorted_ocp[:, 1] morph = morph_array.reshape(-1, 1) morph_oct_1 = np.floor(chromatic_pitch / 12.0).astype(int) morph_octs = np.column_stack((morph_oct_1, morph_oct_1 + 1, morph_oct_1 - 1)) chroma = np.mod(chromatic_pitch, 12) mps = morph_octs + (morph / 7) cp = (morph_oct_1 + (chroma / 12)).reshape(-1, 1) diffs = abs(cp - mps) best_morph_oct = morph_octs[np.arange(n), diffs.argmin(1)] morphetic_pitch = ( morph.reshape( -1, ) + 7 * best_morph_oct ) return morphetic_pitch def p2pn(c_pitch, m_pitch): """ Chromamorphetic pitch to pitch name Parameters ---------- c_pitch : int or array Chromatic pitch. m_pitch : int or array Morphetic pitch. Returns ------- step : str or array Note name (step) alter : int or array Alteration(s) of the notes. 1 is sharp, -1 is flat and 0 is natural octave : int or array Octave """ morph = np.mod(m_pitch, 7) step = STEPS[morph] undisplaced_chroma = UND_CHROMA[morph] # displacement in paper alter = c_pitch - 12 * np.floor(m_pitch / 7.0) - undisplaced_chroma asa_octave = np.floor(m_pitch / 7) if isinstance(morph, (int, float)): if morph > 1: asa_octave += 1 else: asa_octave[morph > 1] += 1 return step, alter, asa_octave