Source code for partitura.musicanalysis.note_features

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains methods to compute note-level features.
"""
import sys
import warnings
import numpy as np
from scipy.interpolate import interp1d
import partitura.score as score

import types
from typing import List, Union, Tuple
from partitura.utils import ensure_notearray, ensure_rest_array
from partitura.score import ScoreLike

__all__ = [
    "list_note_feats_functions",
    "list_note_feature_functions",
    "print_note_feats_functions",
    "print_note_feature_functions",
    "make_note_feats",
    "make_note_features",
    "make_rest_feats",
    "make_rest_features",
    "compute_note_array",
    "full_note_array",
]


class InvalidNoteFeatureException(Exception):
    pass


def print_note_feats_functions():
    """Print a list of all featurefunction names defined in this module,
    with descriptions where available.

    """
    module = sys.modules[__name__]
    doc_indent = 4
    for name in list_note_feats_functions():
        print("* {}".format(name))
        member = getattr(sys.modules[__name__], name)
        if member.__doc__:
            print(
                " " * doc_indent + member.__doc__.replace("\n", " " * doc_indent + "\n")
            )


[docs]def list_note_feats_functions(): """Return a list of all feature function names defined in this module. The feature function names listed here can be specified by name in the `make_note_features` and `make_rest_features` functions. For example: >>> feature, names = make_note_feats(part, ['metrical_feature', 'articulation_feature']) Returns ------- list A list of strings """ module = sys.modules[__name__] bfs = [] exclude = {"make_feature"} for name in dir(module): if name in exclude: continue member = getattr(sys.modules[__name__], name) if isinstance(member, types.FunctionType) and name.endswith("_feature"): bfs.append(name) return bfs
[docs]def make_note_features( part: ScoreLike, feature_functions: Union[List, str], add_idx: bool = False, include_empty_features: bool = True, force_fixed_size: bool = False, ) -> Tuple[np.ndarray, List]: """Compute the specified feature functions for a part. The function returns the computed feature functions as a N x M array, where N equals `len(part.notes_tied)` and M equals the total number of descriptors of all feature functions that occur in part. Furthermore, the function returns the names of the feature functions. A list of strings of size M. The names have the name of the function prepended to the name of the descriptor. For example if a function named `abc_feature` returns descriptors `a`, `b`, and `c`, then the list of names returned by `make_feature(part, ['abc_feature'])` will be ['abc_feature.a', 'abc_feature.b', 'abc_feature.c']. Parameters ---------- part : ScoreLike A partitura scoreLike object, can be Score, Part, or PartGroup. feature_functions : list or str A list of feature functions. Elements of the list can be either the functions themselves or the names of a feature function as strings (or a mix), or the keywork "all". The feature functions specified by name are looked up in the `featuremixer.featurefunctions` module. add_idx : bool (default: False) If True, the index of the note in the part is added as a feature. This is useful for debugging. include_empty_features : bool (default: True) If True, features that are empty are included in the output. Otherwise, they are omitted. force_fixed_size : bool (default: False) If True, the output array uses only features that have a fixed size with no new entries added. Returns ------- feature : ndarray The feature functions names : list The feature names """ if isinstance(part, score.Score): part = score.merge_parts(part.parts) else: part = score.merge_parts(part) na = ensure_notearray( part, include_metrical_position=True, include_grace_notes=True, include_time_signature=True, ) acc = [] if isinstance(feature_functions, str) and feature_functions == "all": feature_functions = list_note_feats_functions() elif not isinstance(feature_functions, list): raise TypeError( "feature_functions variable {} needs to be list or all".format( feature_functions ) ) for bf in feature_functions: # skip time_signature_feature if force_fixed_size is True if force_fixed_size and ( bf == "time_signature_feature" or bf == time_signature_feature ): continue # skip metrical_feature if force_fixed_size is True if force_fixed_size and (bf == "metrical_feature" or bf == metrical_feature): continue if isinstance(bf, str): # get function by name from module func = getattr(sys.modules[__name__], bf) elif isinstance(bf, types.FunctionType): func = bf else: warnings.warn("Ignoring unknown feature function {}".format(bf)) bf, bn = func( na, part, include_empty_features=( True if force_fixed_size else include_empty_features ), ) # check if the size and number of the feature function are correct if bf.size != 0: if bf.shape[1] != len(bn): msg = ( "number of feature names {} does not equal " "number of feature {}".format(len(bn), bf.shape[1]) ) raise InvalidNoteFeatureException(msg) n_notes = len(part.notes_tied) if len(bf) != n_notes: msg = ( "length of feature {} does not equal " "number of notes {}".format(len(bf), n_notes) ) raise InvalidNoteFeatureException(msg) if np.any(np.logical_or(np.isnan(bf), np.isinf(bf))): problematic = np.unique( np.where(np.logical_or(np.isnan(bf), np.isinf(bf)))[1] ) msg = "NaNs or Infs found in the following feature: {} ".format( ", ".join(np.array(bn)[problematic]) ) raise InvalidNoteFeatureException(msg) # prefix feature names by function name bn = ["{}.{}".format(func.__name__, n) for n in bn] acc.append((bf, bn)) if add_idx: _data, _names = zip(*acc) feature_data = np.column_stack(_data) feature_data_list = [list(f) + [i] for f, i in zip(feature_data, na["id"])] feature_names = [n for ns in _names for n in ns] + ["id"] feature_names_dtypes = list( zip(feature_names, ["f4"] * (len(feature_names) - 1) + ["U256"]) ) feature_data_struct = np.array( [tuple(f) for f in feature_data_list], dtype=feature_names_dtypes ) return feature_data_struct else: _data, _names = zip(*acc) feature_data = np.column_stack(_data) feature_names = [n for ns in _names for n in ns] return feature_data, feature_names
[docs]def make_rest_features( part: Union[score.Part, score.PartGroup, List], feature_functions: Union[List, str], add_idx: bool = False, ) -> Tuple[np.ndarray, List]: """Compute the specified feature functions for a part. The function returns the computed feature functions as a N x M array, where N equals `len(part.rests)` and M equals the total number of descriptors of all feature functions that occur in part. Parameters ---------- part : Part The score as a Part instance feature_functions : list or str A list of feature functions. Elements of the list can be either the functions themselves or the names of a feature function as strings (or a mix), or the keywork "all". The feature functions specified by name are looked up in the `featuremixer.featurefunctions` module. Returns ------- feature : ndarray The feature functions names : list The feature names """ if isinstance(part, score.Score): part = score.merge_parts(part.parts) else: part = score.merge_parts(part) na = ensure_rest_array( part, include_metrical_position=True, include_grace_notes=True, include_time_signature=True, ) if na.size == 0: return np.array([]) acc = [] if isinstance(feature_functions, str) and feature_functions == "all": feature_functions = list_note_feats_functions() elif not isinstance(feature_functions, list): raise TypeError( "feature_functions variable {} needs to be list or all".format( feature_functions ) ) for bf in feature_functions: if isinstance(bf, str): # get function by name from module func = getattr(sys.modules[__name__], bf) elif isinstance(bf, types.FunctionType): func = bf else: warnings.warn("Ignoring unknown feature function {}".format(bf)) bf, bn = func(na, part) # check if the size and number of the feature function are correct if bf.size != 0: if bf.shape[1] != len(bn): msg = ( "number of feature names {} does not equal " "number of feature {}".format(len(bn), bf.shape[1]) ) raise InvalidNoteFeatureException(msg) n_notes = len(part.rests) if len(bf) != n_notes: msg = ( "length of feature {} does not equal " "number of notes {}".format(len(bf), n_notes) ) raise InvalidNoteFeatureException(msg) if np.any(np.logical_or(np.isnan(bf), np.isinf(bf))): problematic = np.unique( np.where(np.logical_or(np.isnan(bf), np.isinf(bf)))[1] ) msg = "NaNs or Infs found in the following feature: {} ".format( ", ".join(np.array(bn)[problematic]) ) raise InvalidNoteFeatureException(msg) # prefix feature names by function name bn = ["{}.{}".format(func.__name__, n) for n in bn] acc.append((bf, bn)) if add_idx: _data, _names = zip(*acc) feature_data = np.column_stack(_data) feature_data_list = [list(f) + [i] for f, i in zip(feature_data, na["id"])] feature_names = [n for ns in _names for n in ns] + ["id"] feature_names_dtypes = list( zip(feature_names, ["f4"] * (len(feature_names) - 1) + ["U256"]) ) feature_data_struct = np.array( [tuple(f) for f in feature_data_list], dtype=feature_names_dtypes ) return feature_data_struct else: _data, _names = zip(*acc) feature_data = np.column_stack(_data) feature_names = [n for ns in _names for n in ns] return feature_data, feature_names
# alias make_note_feats = make_note_features make_rest_feats = make_rest_features list_note_feature_functions = list_note_feats_functions print_note_feature_functions = print_note_feats_functions
[docs]def compute_note_array( part: ScoreLike, include_pitch_spelling=False, include_key_signature=False, include_time_signature=False, include_metrical_position=False, include_grace_notes=False, feature_functions=None, force_fixed_size=False, ): """ Create an extended note array from this part. 1) Without arguments this returns a structured array of onsets, offsets, pitch, and ID information: equivalent to part.note_array() 2) With any of the flag arguments set to true, a column with the specified information will be added to the array: equivalent t0 part.note_array(*flags) 3) With a list of strings or functions as feature_functions argument, a column (or multiple columns) with the specified information will be added to the array. See also: >>> make_note_features(part) For a list of features see: >>> list_note_feats_functions() Parameters ---------- include_pitch_spelling : bool (optional) If `True`, includes pitch spelling information for each note. Default is False include_key_signature : bool (optional) If `True`, includes key signature information, i.e., the key signature at the onset time of each note (all notes starting at the same time have the same key signature). Default is False include_time_signature : bool (optional) If `True`, includes time signature information, i.e., the time signature at the onset time of each note (all notes starting at the same time have the same time signature). Default is False include_metrical_position : bool (optional) If `True`, includes metrical position information, i.e., the position of the onset time of each note with respect to its measure (all notes starting at the same time have the same metrical position). Default is False include_grace_notes : bool (optional) If `True`, includes grace note information, i.e. if a note is a grace note and the grace type "" for non grace notes). Default is False feature_functions : list or str A list of feature functions. Elements of the list can be either the functions themselves or the names of a feature function as strings (or a mix). The feature functions specified by name are looked up in the `featuremixer.featurefunctions` module. force_fixed_size : bool (default: False) If True, the output array uses only features that have a fixed size with no new entries added. Returns: note_array : structured array """ if isinstance(part, score.Score): part = score.merge_parts(part.parts) else: part = score.merge_parts(part) na = ensure_notearray( part, include_pitch_spelling=include_pitch_spelling, include_key_signature=include_key_signature, include_time_signature=include_time_signature, include_metrical_position=include_metrical_position, include_grace_notes=include_grace_notes, ) if feature_functions is not None: feature_data_struct = make_note_feats( part, feature_functions, add_idx=True, force_fixed_size=force_fixed_size ) note_array_joined = np.lib.recfunctions.join_by("id", na, feature_data_struct) note_array = note_array_joined.data sort_idx = np.lexsort( (note_array["duration_div"], note_array["pitch"], note_array["onset_div"]) ) note_array = note_array[sort_idx] else: note_array = na return note_array
[docs]def full_note_array(part): """ Create a note array with all available information. """ return compute_note_array( part, include_pitch_spelling=True, include_key_signature=True, include_time_signature=True, include_metrical_position=True, include_grace_notes=True, feature_functions="all", )
def polynomial_pitch_feature(na, part, **kwargs): """Normalize pitch feature.""" pitches = na["pitch"].astype(float) feature_names = ["pitch"] max_pitch = 127 W = pitches / max_pitch return np.expand_dims(W, axis=1), feature_names def duration_feature(na, part, **kwargs): """Duration feature. Parameters ---------- na : structured array The Note array for Unified part. """ feature_names = ["duration"] durations_beat = na["duration_beat"] W = durations_beat W.shape = (-1, 1) return W, feature_names def onset_feature(na, part, **kwargs): """Onset feature Returns: * onset : the onset of the note in beats * score_position : position of the note in the score between 0 (the beginning of the piece) and 1 (the end of the piece) TODO: * rel_position_repetition """ feature_names = ["onset", "score_position"] onsets_beat = na["onset_beat"] rel_position = normalize(onsets_beat, method="minmax") W = np.column_stack((onsets_beat, rel_position)) return W, feature_names def relative_score_position_feature(na, part, **kwargs): W, names = onset_feature(na, part, **kwargs) return W[:, 1:], names[1:] def grace_feature(na, part, **kwargs): """Grace feature. Returns: * grace_note : 1 when the note is a grace note, 0 otherwise * n_grace : the length of the grace note sequence to which this note belongs (0 for non-grace notes) * grace_pos : the (1-based) position of the grace note in the sequence (0 for non-grace notes) """ feature_names = ["grace_note", "n_grace", "grace_pos"] W = np.zeros((len(na), 3)) W[:, 0] = na["is_grace"] grace_notes = na[np.nonzero(na["is_grace"])] notes = ( {n.id: n for n in part.notes_tied} if not np.all(na["pitch"] == 0) else {n.id: n for n in part.rests} ) indices = np.nonzero(na["is_grace"])[0] for i, index in enumerate(indices): grace = grace_notes[i] n_grace = np.count_nonzero(grace_notes["onset_beat"] == grace["onset_beat"]) W[index, 1] = n_grace W[index, 2] = ( n_grace - sum(1 for _ in notes[grace["id"]].iter_grace_seq()) + 1 if grace["id"] not in (None, "None", "") else 0 ) return W, feature_names def loudness_direction_feature(na, part, **kwargs): """The loudness directions in part. This function returns a varying number of descriptors, depending on which directions are present. Some directions are grouped together. For example 'decrescendo' and 'diminuendo' are encoded together in a descriptor 'loudness_decr'. The descriptor names of textual directions such as 'adagio' are the verbatim directions. Some possible descriptors: * p : piano * f : forte * pp : pianissimo * loudness_incr : crescendo direction * loudness_decr : decrescendo or diminuendo direction """ onsets = na["onset_div"] N = len(onsets) constant = ["ppp", "pp", "p", "mp", "mf", "f", "ff", "fff", "unknown_constant"] impulsive = ["fp", "sf", "sfp", "sfz", "unknown_impulsive"] names = constant + impulsive + ["loudness_incr", "loudness_decr"] directions = list(part.iter_all(score.LoudnessDirection, include_subclasses=True)) if "include_empty_features" in kwargs.keys(): force_size = kwargs["include_empty_features"] else: force_size = False if force_size: def to_name(d): if isinstance(d, score.ConstantLoudnessDirection): if d.text in constant: return d.text else: return "unknown_constant" elif isinstance(d, score.ImpulsiveLoudnessDirection): if d.text in impulsive: return d.text else: return "unknown_impulsive" elif isinstance(d, score.IncreasingLoudnessDirection): return "loudness_incr" elif isinstance(d, score.DecreasingLoudnessDirection): return "loudness_decr" else: def to_name(d): if isinstance(d, score.ConstantLoudnessDirection): return d.text elif isinstance(d, score.ImpulsiveLoudnessDirection): return d.text elif isinstance(d, score.IncreasingLoudnessDirection): return "loudness_incr" elif isinstance(d, score.DecreasingLoudnessDirection): return "loudness_decr" feature_by_name = {} for d in directions: j, bf = feature_by_name.setdefault( to_name(d), (len(feature_by_name), np.zeros(N)) ) bf += feature_function_activation(d)(onsets) if not force_size: M = len(feature_by_name) if len(feature_by_name) > 0 else 1 names = [None] * M W = np.zeros((len(onsets), len(names))) for name, (j, bf) in feature_by_name.items(): if force_size: j = names.index(name) else: names[j] = name W[:, j] = bf return W, names def tempo_direction_feature(na, part, **kwargs): """The tempo directions in part. This function returns a varying number of descriptors, depending on which directions are present. Some directions are grouped together. For example 'adagio' and 'molto adagio' are encoded together in a descriptor 'adagio'. Some possible descriptors: * adagio : directions like 'adagio', 'molto adagio' """ onsets = na["onset_div"] N = len(onsets) constant = [ "adagio", "largo", "lento", "grave", "larghetto", "adagietto", "andante", "andantino", "moderato", "allegretto", "allegro", "vivace", "presto", "prestissimo", "unknown_constant", ] names = constant + ["tempo_incr", "tempo_decr"] directions = list(part.iter_all(score.TempoDirection, include_subclasses=True)) if "include_empty_features" in kwargs.keys(): force_size = kwargs["include_empty_features"] else: force_size = False if force_size: def to_name(d): if isinstance(d, score.ResetTempoDirection): ref = d.reference_tempo if ref: if ref.text in constant: return ref.text else: return "unknown_constant" else: if d.text in constant: return d.text else: return "unknown_constant" elif isinstance(d, score.ConstantTempoDirection): if d.text in constant: return d.text else: return "unknown_constant" elif isinstance(d, score.IncreasingTempoDirection): return "tempo_incr" elif isinstance(d, score.DecreasingTempoDirection): return "tempo_decr" else: def to_name(d): if isinstance(d, score.ResetTempoDirection): ref = d.reference_tempo if ref: return ref.text else: return d.text elif isinstance(d, score.ConstantTempoDirection): return d.text elif isinstance(d, score.IncreasingTempoDirection): return "tempo_incr" elif isinstance(d, score.DecreasingTempoDirection): return "tempo_decr" feature_by_name = {} for d in directions: j, bf = feature_by_name.setdefault( to_name(d), (len(feature_by_name), np.zeros(N)) ) bf += feature_function_activation(d)(onsets) if not force_size: M = len(feature_by_name) if len(feature_by_name) > 0 else 1 names = [None] * M W = np.zeros((len(onsets), len(names))) for name, (j, bf) in feature_by_name.items(): if force_size: j = names.index(name) else: names[j] = name W[:, j] = bf return W, names def articulation_direction_feature(na, part, **kwargs): """ """ onsets = na["onset_div"] N = len(onsets) directions = list( part.iter_all(score.ArticulationDirection, include_subclasses=True) ) constant_names = ["staccato", "tenuto", "accent", "marcato", "unknown_articulation"] if "include_empty_features" in kwargs.keys(): force_size = kwargs["include_empty_features"] else: force_size = False if force_size: def to_name(d): if d.text in constant_names: return d.text else: return "unknown_articulation" else: def to_name(d): return d.text feature_by_name = {} for d in directions: j, bf = feature_by_name.setdefault( to_name(d), (len(feature_by_name), np.zeros(N)) ) bf += feature_function_activation(d)(onsets) if force_size: W = np.zeros((len(onsets), len(constant_names))) names = constant_names else: M = len(feature_by_name) if len(feature_by_name) > 0 else 1 W = np.zeros((len(onsets), M)) names = [None] * M for name, (j, bf) in feature_by_name.items(): if force_size: j = names.index(name) else: names[j] = name W[:, j] = bf return W, names def feature_function_activation(direction): epsilon = 1e-6 if isinstance( direction, (score.DynamicLoudnessDirection, score.DynamicTempoDirection) ): # a dynamic direction will be encoded as a ramp from d.start.t to d.end.t # if d.end is None (e.g. just a ritardando without dashes) if direction.end: direction_end = direction.end.t else: # assume the end of d is the end of the measure: measure = next(direction.start.iter_prev(score.Measure, eq=True), None) if measure: direction_end = measure.start.t else: # no measure, unlikely, but not impossible. direction_end = direction.start.t + 1 x = [direction.start.t, direction_end, direction_end + epsilon] y = [0, 1, 0] elif isinstance( direction, ( score.ConstantLoudnessDirection, score.ConstantArticulationDirection, score.ConstantTempoDirection, ), ): x = [ direction.start.t - epsilon, direction.start.t, direction.end.t - epsilon, direction.end.t, ] y = [0, 1, 1, 0] else: # impulsive x = [ direction.start.t - epsilon, direction.start.t, direction.start.t + epsilon, ] y = [0, 1, 0] return interp1d(x, y, bounds_error=False, fill_value=0) def slur_feature(na, part, **kwargs): """Slur feature. Returns: * slur_incr : a ramp function that increases from 0 to 1 over the course of the slur * slur_decr : a ramp function that decreases from 1 to 0 over the course of the slur """ names = ["slur_incr", "slur_decr"] onsets = na["onset_div"] slurs = part.iter_all(score.Slur) W = np.zeros((len(onsets), 2)) for slur in slurs: if not slur.end: continue x = [slur.start.t, slur.end.t] y_inc = [0, 1] y_dec = [1, 0] W[:, 0] += interp1d(x, y_inc, bounds_error=False, fill_value=0)(onsets) W[:, 1] += interp1d(x, y_dec, bounds_error=False, fill_value=0)(onsets) # Filter out NaN values W[np.isnan(W)] = 0.0 return W, names def articulation_feature(na, part, **kwargs): """Articulation feature. This feature returns articulation-related note annotations, such as accents, legato, and tenuto. Possible descriptors: * accent : 1 when the note has an annotated accent sign * legato : 1 when the note has an annotated legato sign * staccato : 1 when the note has an annotated staccato sign ... """ names = [ "accent", "strong-accent", "staccato", "tenuto", "detached-legato", "staccatissimo", "spiccato", "scoop", "plop", "doit", "falloff", "breath-mark", "caesura", "stress", "unstress", "soft-accent", ] if "include_empty_features" in kwargs: force_size = kwargs["include_empty_features"] else: force_size = False feature_by_name = {} notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests N = len(notes) for i, n in enumerate(notes): if n.articulations: for art in n.articulations: if art in names: j, bf = feature_by_name.setdefault( art, (len(feature_by_name), np.zeros(N)) ) bf[i] = 1 if force_size: M = len(names) else: M = len(feature_by_name) if len(feature_by_name) > 0 else 1 names = [None] * M W = np.zeros((N, M)) for name, (j, bf) in feature_by_name.items(): if force_size: j = names.index(name) else: names[j] = name W[:, j] = bf return W, names def ornament_feature(na, part, **kwargs): """Ornament feature. This feature returns ornamentation note annotations,such as trills. Possible descriptors: * trill : 1 when the note has an annotated trill * mordent : 1 when the note has an annotated mordent ... """ names = [ "trill-mark", "turn", "delayed-turn", "inverted-turn", "delayed-inverted-turn", "vertical-turn", "inverted-vertical-turn", "shake", "wavy-line", "mordent", "inverted-mordent", "schleifer", "tremolo", "haydn", "other-ornament", ] feature_by_name = {} notes = part.notes_tied N = len(notes) for i, n in enumerate(notes): if n.ornaments: for art in n.ornaments: if art in names: j, bf = feature_by_name.setdefault( art, (len(feature_by_name), np.zeros(N)) ) bf[i] = 1 if "include_empty_features" in kwargs.keys(): fix_size = kwargs["include_empty_features"] else: fix_size = False if fix_size: M = len(names) else: M = len(feature_by_name) if len(feature_by_name) > 0 else 1 names = [None] * M W = np.zeros((N, M)) for name, (j, bf) in feature_by_name.items(): if fix_size: j = names.index(name) else: names[j] = name W[:, j] = bf return W, names def staff_feature(na, part, **kwargs): """Staff feature""" names = ["staff"] notes = {n.id: n.staff for n in part.notes_tied} N = len(na) W = np.zeros((N, 1)) for i, n in enumerate(na): W[i, 0] = notes[n["id"]] if n["id"] not in (None, "None", "") else 0 return W, names # # for a subset of the articulations do e.g. # def staccato_feature(part): # W, names = articulation_feature(part) # if 'staccato' in names: # i = names.index('staccato') # return W[:, i:i + 1], ['staccato'] # else: # return np.empty(len(W)), [] def fermata_feature(na, part, **kwargs): """Fermata feature. Returns: * fermata : 1 when the note coincides with a fermata sign. """ names = ["fermata"] onsets = na["onset_div"] W = np.zeros((len(onsets), 1)) for ferm in part.iter_all(score.Fermata): W[onsets == ferm.start.t, 0] = 1 return W, names def metrical_feature(na, part, **kwargs): """Metrical feature This feature encodes the metrical position in the bar. For example the first beat in a 3/4 meter is encoded in a binary descriptor 'metrical_3_4_0', the fifth beat in a 6/8 meter as 'metrical_6_8_4', etc. Any positions that do not fall on a beat are encoded in a feature suffixed '_weak'. For example a note starting on the second 8th note in a bar of 4/4 meter will have a non-zero value in the 'metrical_4_4_weak' descriptor. """ notes = part.notes_tied if not np.all(na["pitch"] == 0) else part.rests ts_map = part.time_signature_map bm = part.beat_map feature_by_name = {} eps = 10**-6 for i, n in enumerate(notes): beats, beat_type, mus_beats = ts_map(n.start.t).astype(int) measure = next(n.start.iter_prev(score.Measure, eq=True), None) if measure: measure_start = measure.start.t else: measure_start = 0 pos = bm(n.start.t) - bm(measure_start) if pos % 1 < eps: name = "metrical_{}_{}_{}".format(beats, beat_type, int(pos)) else: name = "metrical_{}_{}_weak".format(beats, beat_type) j, bf = feature_by_name.setdefault( name, (len(feature_by_name), np.zeros(len(notes))) ) bf[i] = 1 W = np.zeros((len(notes), len(feature_by_name))) names = [None] * len(feature_by_name) for name, (j, bf) in feature_by_name.items(): W[:, j] = bf names[j] = name return W, names def metrical_strength_feature(na, part, **kwargs): """Metrical strength feature This feature encodes the beat phase (relative position of a note within the measure), as well as metrical strength of common time signatures. """ names = [ "beat_phase", "metrical_strength_downbeat", "metrical_strength_secondary", "metrical_strength_weak", ] relod = na["rel_onset_div"].astype(float) totmd = na["tot_measure_div"].astype(float) W = np.zeros((len(na), len(names))) W[:, 0] = np.divide(relod, totmd) # Onset Phase W[:, 1] = na["is_downbeat"].astype(float) W[:, 2][W[:, 0] == 0.5] = 1.00 W[:, 3][np.nonzero(np.add(W[:, 1], W[:, 0]) == 1.00)] = 1.00 return W, names def time_signature_feature(na, part, **kwargs): """TIme Signature feature This feature encodes the time signature of the note in two sets of one-hot vectors, a one hot encoding of number of beats and a one hot encoding of beat type """ ts_map = part.time_signature_map possible_beats = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, "other"] possible_beat_types = [1, 2, 4, 8, 16, "other"] W_beats = np.zeros((len(na), len(possible_beats))) W_types = np.zeros((len(na), len(possible_beat_types))) names = ["time_signature_num_{0}".format(b) for b in possible_beats] + [ "time_signature_den_{0}".format(b) for b in possible_beat_types ] for i, n in enumerate(na): beats, beat_type, mus_beats = ts_map(n["onset_div"]).astype(int) if beats in possible_beats: W_beats[i, beats - 1] = 1 else: W_beats[i, -1] = 1 if beat_type in possible_beat_types: W_types[i, possible_beat_types.index(beat_type)] = 1 else: W_types[i, -1] = 1 W = np.column_stack((W_beats, W_types)) return W, names def vertical_neighbor_feature(na, part, **kwargs): """Vertical neighbor feature. Describes various aspects of simultaneously starting notes. Returns: * n_total : * n_above : * n_below : * highest_pitch : * lowest_pitch : * pitch_range : """ # the list of descriptors names = [ "n_total", "n_above", "n_below", "highest_pitch", "lowest_pitch", "pitch_range", ] W = np.zeros((len(na), len(names))) for i, n in enumerate(na): neighbors = na[np.where(na["onset_beat"] == n["onset_beat"])]["pitch"] max_pitch = np.max(neighbors) min_pitch = np.min(neighbors) W[i, 0] = len(neighbors) - 1 W[i, 1] = np.sum(neighbors > n["pitch"]) W[i, 2] = np.sum(neighbors < n["pitch"]) W[i, 3] = max_pitch W[i, 4] = min_pitch W[i, 5] = max_pitch - min_pitch return W, names def normalize(data, method="minmax"): """ Normalize data in one of several ways. The available normalization methods are: * minmax Rescale `data` to the range `[0, 1]` by subtracting the minimum and dividing by the range. If `data` is a 2d array, each column is rescaled to `[0, 1]`. * tanh Rescale `data` to the interval `(-1, 1)` using `tanh`. Note that if `data` is non-negative, the output interval will be `[0, 1)`. * tanh_unity Like "soft", but rather than rescaling strictly to the range (-1, 1), following will hold: normalized = normalize(data, method="tanh_unity") np.where(data==1) == np.where(normalized==1) That is, the normalized data will equal one wherever the original data equals one. The target interval is `(-1/np.tanh(1), 1/np.tanh(1))`. Parameters ---------- data: ndarray Data to be normalized method: {'minmax', 'tanh', 'tanh_unity'}, optional The normalization method. Defaults to 'minmax'. Returns ------- ndarray Normalized copy of the data """ """Normalize the data in `data`. There are several normalization """ if method == "minmax": vmin = np.min(data, 0) vmax = np.max(data, 0) if np.isclose(vmin, vmax): # Return all values as 0 or as 1? return np.zeros_like(data) else: return (data - vmin) / (vmax - vmin) elif method == "tanh": return np.tanh(data) elif method == "tanh_unity": return np.tanh(data) / np.tanh(1)