"""
This module contains functions to compute features from melodies.
Features are the product of an input list and at least one algorithm.
"""
__author__ = "David Whyatt"
import warnings
from importlib import resources
from .feature_decorators import (
fantastic, idyom, midi_toolbox, melsim, jsymbolic, novel, simile, partitura, must,
FeatureType, feature_type, interval, pitch_class, contour, tonality, metre, absolute, timing,
lexical_diversity, expectation, complexity,
pitch, rhythm, both
)
from .feature_dispatch import collect_feature_values as _dispatch_collect_feature_values
from .feature_dispatch import invoke_feature as _dispatch_invoke_feature
from .feature_registry import get_features_by_domain as _registry_get_features_by_domain
from .feature_registry import (
get_features_by_domain_and_types as _registry_get_features_by_domain_and_types,
)
from .feature_registry import get_features_by_source as _registry_get_features_by_source
from .feature_registry import get_features_by_type as _registry_get_features_by_type
from .feature_registry import list_available_features
from .feature_definitions.absolute_pitch import (
pitch_range,
ambitus,
pitch_standard_deviation,
pitch_variability,
first_pitch,
last_pitch,
basic_pitch_histogram,
melodic_pitch_variety,
mean_pitch,
most_common_pitch,
number_of_unique_pitches,
number_of_common_pitches,
tessitura,
mean_tessitura,
tessitura_std,
prevalence_of_most_common_pitch,
relative_prevalence_of_top_pitches,
interval_between_most_prevalent_pitches,
pitch_skewness,
pitch_kurtosis,
importance_of_bass_register,
importance_of_middle_register,
importance_of_high_register,
pitch_spelling,
repeated_notes,
stepwise_motion,
)
from .feature_definitions.pitch_class import (
_pcdist1_vector,
_consecutive_fifths,
pitch_class_variability,
pitch_class_variability_after_folding,
pcdist1,
pcdist2,
first_pitch_class,
last_pitch_class,
dominant_spread,
mean_pitch_class,
most_common_pitch_class,
number_of_unique_pitch_classes,
number_of_common_pitch_classes,
number_of_common_pitches_classes,
prevalence_of_most_common_pitch_class,
relative_prevalence_of_top_pitch_classes,
interval_between_most_prevalent_pitch_classes,
folded_fifths_pitch_class_histogram,
pitch_class_skewness,
pitch_class_kurtosis,
pitch_class_skewness_after_folding,
pitch_class_kurtosis_after_folding,
strong_tonal_centres,
)
from .feature_definitions.pitch_interval import (
_ivdist1_vector,
pitch_interval,
absolute_interval_range,
mean_absolute_interval,
mean_melodic_interval,
standard_deviation_absolute_interval,
modal_interval,
most_common_interval,
ivdist1,
ivdist2,
ivdirdist1,
ivsizedist1,
interval_direction,
interval_direction_mean,
interval_direction_std,
average_length_of_melodic_arcs,
average_interval_span_by_melodic_arcs,
distance_between_most_prevalent_melodic_intervals,
melodic_interval_histogram,
melodic_large_intervals,
variable_melodic_intervals,
melodic_thirds,
melodic_perfect_fourths,
melodic_tritones,
melodic_perfect_fifths,
melodic_sixths,
melodic_sevenths,
melodic_octaves,
minor_major_third_ratio,
direction_of_melodic_motion,
number_of_common_melodic_intervals,
prevalence_of_most_common_melodic_interval,
relative_prevalence_of_most_common_melodic_intervals,
amount_of_arpeggiation,
chromatic_motion,
)
from .feature_definitions.expectation import (
_get_key_distances,
get_narmour_features,
_stability_distance,
_get_simonton_transition_matrix,
narmour_registral_direction,
narmour_proximity,
narmour_closure,
narmour_registral_return,
narmour_intervallic_difference,
melodic_embellishment,
mobility,
mean_mobility,
mobility_std,
melodic_attraction,
mean_melodic_attraction,
melodic_attraction_std,
melodic_accent,
mean_melodic_accent,
melodic_accent_std,
compltrans,
pitch_stm_mean_information_content,
pitch_ltm_mean_information_content,
rhythm_stm_mean_information_content,
rhythm_ltm_mean_information_content,
)
from .feature_definitions.metre import (
_meter_accent_mean,
metric_hierarchy,
meter_accent,
meter_numerator,
meter_denominator,
proportion_of_time_in_first_meter,
number_of_unique_time_signatures,
syncopation,
syncopicity,
)
from .feature_definitions.corpus import (
_fantastic_melody_tokens,
_fantastic_melody_tf_df,
_fantastic_log_normalized_tf_df,
_fantastic_melody_ngram_counts,
_fantastic_min_tie_ranks,
_compute_corpus_feature_bundle,
_setup_corpus_statistics,
get_ngram_document_frequency,
InverseEntropyWeighting,
tfdf_spearman,
tfdf_kendall,
mean_log_tfdf,
norm_log_dist,
max_log_df,
min_log_df,
mean_log_df,
mean_global_local_weight,
std_global_local_weight,
mean_global_weight,
std_global_weight,
get_corpus_features,
)
from .feature_utils import _get_durations
from .utils.warnings import suppress_common_melody_warnings
from .feature_definitions.contour import (
get_step_contour_features,
get_interpolation_contour_features,
comb_contour_matrix,
get_comb_contour_matrix,
get_polynomial_contour_features,
get_huron_contour_features,
get_contour_features,
)
from .feature_definitions.tonality import (
_normalize_key_root,
_canonical_key_string,
_resolve_key_for_melody,
_tonality_correlations_for_key,
infer_key_from_pitches,
key,
keyname,
tonalness,
tonal_clarity,
tonal_spike,
referent,
tonal_tension,
mean_cloud_diameter,
std_cloud_diameter,
mean_cloud_momentum,
std_cloud_momentum,
mean_tensile_strain,
std_tensile_strain,
tonalness_histogram,
inscale,
proportion_inscale,
longest_monotonic_conjunct_scalar_passage,
longest_conjunct_scalar_passage,
proportion_conjunct_scalar,
proportion_scalar,
mode,
get_tonality_features,
)
from .feature_definitions.timing import (
_durdist1_vector,
_get_tempo,
_rhythmic_run_lengths,
_rhythmic_value_offsets,
_silent_run_lengths_qn,
_calculate_thresholded_peak_table,
_get_beat_histogram_values_from_ticks,
_compute_beat_histogram_tables,
_count_strong_pulses,
_is_factor_or_multiple,
durdist1,
durdist2,
initial_tempo,
mean_tempo,
tempo_variability,
duration_range,
mean_duration,
average_note_duration,
duration_standard_deviation,
variability_of_note_durations,
modal_duration,
length,
total_number_of_notes,
number_of_unique_durations,
global_duration,
duration_in_seconds,
note_density,
note_density_variability,
note_density_per_quarter_note,
note_density_per_quarter_note_variability,
duration_histogram,
range_of_rhythmic_values,
number_of_different_rhythmic_values_present,
number_of_common_rhythmic_values_present,
prevalence_of_very_short_rhythmic_values,
prevalence_of_short_rhythmic_values,
prevalence_of_medium_rhythmic_values,
prevalence_of_long_rhythmic_values,
prevalence_of_very_long_rhythmic_values,
prevalence_of_dotted_notes,
shortest_rhythmic_value,
longest_rhythmic_value,
mean_rhythmic_value,
most_common_rhythmic_value,
prevalence_of_most_common_rhythmic_value,
relative_prevalence_of_most_common_rhythmic_values,
difference_between_most_common_rhythmic_values,
mean_rhythmic_value_run_length,
median_rhythmic_value_run_length,
variability_in_rhythmic_value_run_lengths,
mean_rhythmic_value_offset,
median_rhythmic_value_offset,
variability_of_rhythmic_value_offsets,
complete_rests_fraction,
longest_complete_rest,
mean_complete_rest_duration,
median_complete_rest_duration,
variability_of_complete_rest_durations,
strongest_rhythmic_pulse,
strongest_rhythmic_pulse_tempo_standardized,
second_strongest_rhythmic_pulse,
second_strongest_rhythmic_pulse_tempo_standardized,
harmonicity_of_two_strongest_rhythmic_pulses,
harmonicity_of_two_strongest_rhythmic_pulses_tempo_standardized,
strength_of_strongest_rhythmic_pulse,
strength_of_strongest_rhythmic_pulse_tempo_standardized,
strength_of_second_strongest_rhythmic_pulse,
strength_of_second_strongest_rhythmic_pulse_tempo_standardized,
strength_ratio_of_two_strongest_rhythmic_pulses,
strength_ratio_of_two_strongest_rhythmic_pulses_tempo_standardized,
combined_strength_of_two_strongest_rhythmic_pulses,
combined_strength_of_two_strongest_rhythmic_pulses_tempo_standardized,
rhythmic_variability,
rhythmic_variability_tempo_standardized,
rhythmic_looseness,
rhythmic_looseness_tempo_standardized,
polyrhythms,
polyrhythms_tempo_standardized,
number_of_strong_rhythmic_pulses,
number_of_strong_rhythmic_pulses_tempo_standardized,
number_of_moderate_rhythmic_pulses,
number_of_moderate_rhythmic_pulses_tempo_standardized,
number_of_relatively_strong_rhythmic_pulses,
number_of_relatively_strong_rhythmic_pulses_tempo_standardized,
minimum_note_duration,
maximum_note_duration,
equal_duration_transitions,
half_duration_transitions,
dotted_duration_transitions,
amount_of_staccato,
short_note_fraction,
npvi,
onset_autocorrelation,
onset_autocorr_peak,
)
from .feature_definitions.inter_onset_interval import (
ioi,
ioi_mean,
average_time_between_attacks,
ioi_standard_deviation,
variability_of_time_between_attacks,
ioi_ratio,
ioi_ratio_mean,
ioi_ratio_standard_deviation,
ioi_range,
ioi_contour,
ioi_contour_mean,
ioi_contour_standard_deviation,
ioi_histogram,
)
from .feature_definitions.complexity import (
_KK_MAJ_PROFILE,
_KK_MIN_PROFILE,
_kkcc_from_pcd,
_keymode_from_pcd,
_tonality_midi_toolbox,
_notedensity_seconds,
_complebm,
pitch_entropy,
interval_entropy,
duration_entropy,
duration_accent,
mean_duration_accent,
duration_accent_std,
tonal_entropy,
gradus,
complebm_pitch,
complebm_rhythm,
complebm_optimal,
bisect_unbalance,
center_mass_offset,
event_heterogeneity,
av_abs_interval,
mel_abruptness,
dur_abruptness,
rhythm_abruptness,
asym_total,
asym_index,
event_density,
av_local_p1_entropy,
p1_entropy,
p2_entropy,
p3_entropy,
i1_entropy,
i2_entropy,
d1_entropy,
d2_entropy,
d3_entropy,
wp_entropy,
pdist1,
pdist2,
pdist3,
idist1,
idist2,
ddist1,
ddist2,
ddist3,
)
from .feature_definitions.lexical_diversity import (
get_mtype_features,
get_lexical_diversity_features,
)
from .idyom.config import (
VALID_VIEWPOINTS,
IDyOMConfig,
_DEFAULT_CORPUS,
_DEFAULT_IDYOM_CONFIGS,
_default_idyom_configs,
_IDYOM_MEAN_INFORMATION_CONTENT_EXPORTS,
_resolve_idyom_corpus,
_validate_viewpoints,
)
from .idyom.interface import run_idyom
from .idyom.runners import (
_cleanup_idyom_temp_output,
_idyom_mean_information_content,
_melody_idyom_input_directory,
_run_idyom_analysis,
create_temp_midi_with_key_signature,
get_idyom_results,
to_mido_key_string,
)
from .pipeline.config import (
Config,
DEFAULT_MAX_NGRAM_ORDER,
FantasticConfig,
_setup_default_config,
_validate_config,
)
from .pipeline.loading import FeatureInput, _load_melody_data
from .pipeline.output import (
_get_category_display_name,
log_timing_statistics,
rename_feature_columns,
)
from .pipeline.processing import (
_process_melodies_parallel,
_setup_parallel_processing,
process_melody,
)
from .pipeline.timing import TIMING_STAT_CATEGORIES, _init_timing_stats
from .utils.logging import _setup_logger
from .utils.validation import _check_is_monophonic
suppress_common_melody_warnings()
import csv
import inspect
import json
import math
import os
import shutil
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
import glob
import logging
import time
from random import choices
from typing import Dict, List, Optional, Tuple, Union, Literal, Any
import mido
import numpy as np
import pandas as pd
import scipy
from functools import lru_cache
from natsort import natsorted
from tqdm import tqdm
from melody_features.algorithms import (
arpeggiation_proportion,
chromatic_motion_proportion,
circle_of_fifths,
compute_tonality_vector,
get_duration_ratios,
melodic_embellishment_proportion,
n_percent_significant_values,
rank_values,
repeated_notes_proportion,
stepwise_motion_proportion,
)
from melody_features.corpus import load_corpus_stats, make_corpus_stats
from melody_features.utils.distributional import (
distribution_proportions,
histogram_bins,
kurtosis,
skew,
)
from melody_features.io.midi import import_midi
from melody_features.melody_tokenizer import FantasticTokenizer
from melody_features.algorithms.narmour import (
closure,
intervallic_difference,
proximity,
registral_direction,
registral_return,
)
from melody_features.ngram_counter import NGramCounter
from melody_features.core.representations import Melody
from melody_features.feature_histogram import (
PitchHistogram,
PitchClassHistogram,
DurationHistogram,
RhythmicValueHistogram,
create_rhythmic_value_histogram,
create_beat_histogram,
create_melodic_interval_histogram,
)
from melody_features.utils.stats import (
distribution_entropy,
get_mode,
midi_toolbox_entropy,
range_func,
shannon_entropy,
standard_deviation,
)
from melody_features.algorithms.meter_estimation import (
compute_onset_autocorrelation,
duration_accent as _duration_accent,
melodic_accent as _melodic_accent,
metric_hierarchy as _metric_hierarchy,
)
from melody_features.algorithms.pitch_spelling import (
estimate_spelling_from_melody as _estimate_spelling_from_melody,
)
from melody_features.algorithms.tonal_tension import (
estimate_tonaltension,
SCALE_FACTOR,
DEFAULT_WEIGHTS,
ALPHA,
BETA
)
def _get_features_by_type(feature_type: str) -> dict:
"""Get all features of a specific type.
Parameters
----------
feature_type : str
The type of features to collect (e.g., 'absolute', 'contour', 'tonality', etc.)
Returns
-------
dict
Dictionary mapping feature names to functions
"""
current_module = sys.modules[__name__]
return _registry_get_features_by_type(current_module, feature_type)
def _get_features_by_domain(domain: str) -> dict:
"""Get all features of a specific domain.
Parameters
----------
domain : str
The domain of features to collect ('pitch', 'rhythm', or 'both')
Returns
-------
dict
Dictionary mapping feature names to functions
"""
current_module = sys.modules[__name__]
return _registry_get_features_by_domain(current_module, domain)
def _get_features_by_domain_and_types(domain: str, allowed_types: list[str]) -> dict:
"""Get all features of a specific domain that match any of the allowed types.
Parameters
----------
domain : str
The domain of features to collect ('pitch', 'rhythm', or 'both')
allowed_types : list[str]
List of allowed feature types (e.g., ['absolute', 'interval'])
Returns
-------
dict
Dictionary mapping feature names to functions
"""
current_module = sys.modules[__name__]
return _registry_get_features_by_domain_and_types(
current_module,
domain,
allowed_types,
)
def _invoke_feature(func, melody: Melody, **extra):
"""Call a feature function, binding `melody` fields and extras by parameter name."""
return _dispatch_invoke_feature(
func,
melody,
default_max_ngram_order=DEFAULT_MAX_NGRAM_ORDER,
**extra,
)
def _collect_feature_values(
feature_functions: Dict[str, callable],
melody: Melody,
*,
tuple_suffix: Optional[str] = None,
**extra,
) -> Dict[str, Any]:
"""Compute feature functions with shared melody argument dispatch."""
return _dispatch_collect_feature_values(
feature_functions,
melody,
default_max_ngram_order=DEFAULT_MAX_NGRAM_ORDER,
tuple_suffix=tuple_suffix,
**extra,
)
[docs]
def get_pitch_features(melody: Melody) -> Dict:
"""Dynamically collect all pitch features for a melody.
Collects features decorated with @pitch domain and @absolute type.
Parameters
----------
melody : Melody
The melody to analyze
Returns
-------
Dict
Dictionary of pitch feature values
"""
pitch_functions = _get_features_by_domain_and_types("pitch", ["absolute"])
return _collect_feature_values(pitch_functions, melody)
[docs]
def get_pitch_class_features(melody: Melody) -> Dict:
"""Dynamically collect all pitch class features for a melody.
Collects features decorated with @pitch domain and @pitch_class type.
Parameters
----------
melody : Melody
The melody to analyze
Returns
-------
Dict
Dictionary of pitch class feature values
"""
pitch_class_functions = _get_features_by_domain_and_types("pitch", ["pitch_class"])
return _collect_feature_values(pitch_class_functions, melody)
# I still think this is cool and I like it a lot, but it's not included in any of the software
# since this feature set is the result of a systematic review of toolboxes, we can't return it right now
# but it's here and it works
[docs]
@novel
def temperley_likelihood(pitches: list[int]) -> float:
"""
The likelihood of a melody using Bayesian reasoning,
according to David Temperley's model
(http://davidtemperley.com/wp-content/uploads/2015/11/temperley-cs08.pdf).
Parameters
----------
pitches : list[int]
List of MIDI pitch values
Returns
-------
float
Likelihood of the melody using Bayesian reasoning
"""
# represent all possible notes as int
notes_ints = np.arange(0, 120, 1)
# Calculate central pitch profile
central_pitch_profile = scipy.stats.norm.pdf(notes_ints, loc=68, scale=np.sqrt(5.0))
central_pitch = choices(notes_ints, central_pitch_profile)
range_profile = scipy.stats.norm.pdf(
notes_ints, loc=central_pitch, scale=np.sqrt(23.0)
)
# Get key probabilities
rpk_major = [
0.184,
0.001,
0.155,
0.003,
0.191,
0.109,
0.005,
0.214,
0.001,
0.078,
0.004,
0.055,
] * 10
rpk_minor = [
0.192,
0.005,
0.149,
0.179,
0.002,
0.144,
0.002,
0.201,
0.038,
0.012,
0.053,
0.022,
] * 10
# Calculate total probability
total_prob = 1.0
for i in range(1, len(pitches)):
# Calculate proximity profile centered on previous note
prox_profile = scipy.stats.norm.pdf(
notes_ints, loc=pitches[i - 1], scale=np.sqrt(10)
)
rp = range_profile * prox_profile
# Apply key profile based on major/minor
if "major" in compute_tonality_vector([p % 12 for p in pitches])[0][0]:
rpk = rp * rpk_major
else:
rpk = rp * rpk_minor
# Normalize probabilities
rpk_normed = rpk / np.sum(rpk)
# Get probability of current note
note_prob = rpk_normed[pitches[i]]
total_prob *= note_prob
return total_prob
[docs]
def get_complexity_features(
melody: Melody, phrase_gap: float = 1.5, max_ngram_order: int = DEFAULT_MAX_NGRAM_ORDER
) -> Dict:
"""Dynamically collect all complexity features for a melody."""
complexity_functions = _get_features_by_type(FeatureType.COMPLEXITY)
skip = frozenset({"InverseEntropyWeighting", "get_mtype_features"})
filtered = {
name: func for name, func in complexity_functions.items() if name not in skip
}
return _collect_feature_values(
filtered,
melody,
phrase_gap=phrase_gap,
max_ngram_order=max_ngram_order,
)
[docs]
def get_complexity_feature_bundle(
melody: Melody,
phrase_gap: float = 1.5,
max_ngram_order: int = DEFAULT_MAX_NGRAM_ORDER,
) -> Dict:
"""Return complexity and lexical-diversity features for export."""
return {
**get_complexity_features(
melody, phrase_gap=phrase_gap, max_ngram_order=max_ngram_order
),
**get_lexical_diversity_features(
melody, phrase_gap=phrase_gap, max_ngram_order=max_ngram_order
),
}
[docs]
def get_interval_features(melody: Melody) -> Dict:
"""Dynamically collect all interval features for a melody.
Collects features decorated with @pitch domain and @interval type.
Parameters
----------
melody : Melody
The melody to analyze
Returns
-------
Dict
Dictionary of interval feature values
"""
interval_functions = _get_features_by_domain_and_types("pitch", ["interval"])
return _collect_feature_values(interval_functions, melody, tuple_suffix="sd")
def _collect_rhythm_domain_features(melody: Melody, allowed_types: list[str]) -> Dict:
"""Collect @rhythm-domain features whose types intersect `allowed_types`."""
rhythm_functions = _get_features_by_domain_and_types("rhythm", allowed_types)
return _collect_feature_values(rhythm_functions, melody, tuple_suffix="std")
[docs]
def get_timing_features(melody: Melody) -> Dict:
"""Collect @rhythm-domain features decorated with @timing."""
return _collect_rhythm_domain_features(melody, ["timing"])
[docs]
def get_inter_onset_interval_features(melody: Melody) -> Dict:
"""Collect @rhythm-domain features decorated with @interval (IOI family)."""
return _collect_rhythm_domain_features(melody, ["interval"])
[docs]
def get_rhythm_features(melody: Melody) -> Dict:
"""Dynamically collect all rhythm features for a melody.
Combines timing and inter-onset interval features for backward-compatible
`rhythm_features` export. `metric_hierarchy` and `meter_accent` are
excluded here since they are already reported under `metre_features`
(via `get_metre_features`); including them in both would duplicate the
same values under two column prefixes (`timing.*` and `metre.*`).
"""
features: Dict[str, Any] = {}
features.update(get_timing_features(melody))
features.update(get_inter_onset_interval_features(melody))
return features
[docs]
def collect_rhythm_for_pipeline(melody: Melody) -> tuple[Dict[str, Any], Dict[str, float]]:
"""Collect rhythm features and per-subcategory timings for pipeline workers.
`timing` and `inter_onset_interval` are timed separately so pipeline
statistics keep them as discrete taxonomy categories. `metric_hierarchy`
and `meter_accent` are intentionally omitted here; they are collected
once, under `metre_features`, to avoid duplicating them under both
`timing.*` and `metre.*` column prefixes.
"""
rhythm_timings: Dict[str, float] = {}
start = time.time()
timing_features = get_timing_features(melody)
rhythm_timings["timing"] = time.time() - start
start = time.time()
ioi_features = get_inter_onset_interval_features(melody)
rhythm_timings["inter_onset_interval"] = time.time() - start
rhythm_features = {
**timing_features,
**ioi_features,
}
return rhythm_features, rhythm_timings
[docs]
def get_expectation_features(melody: Melody) -> Dict:
"""Dynamically collect all expectation features for a melody."""
expectation_functions = _get_features_by_type(FeatureType.EXPECTATION)
filtered = {
name: func
for name, func in expectation_functions.items()
if name not in _IDYOM_MEAN_INFORMATION_CONTENT_EXPORTS
}
return _collect_feature_values(
filtered, melody, tuple_suffix="std", numeric_tuple_only=True
)
[docs]
def get_metre_features(melody: Melody) -> Dict:
"""Dynamically collect all metre features for a melody."""
metre_functions = _get_features_by_type(FeatureType.METRE)
return _collect_feature_values(metre_functions, melody)
def _get_features_by_source(source: str) -> Dict[str, callable]:
"""Get all functions/classes decorated with a specific source."""
return _registry_get_features_by_source(sys.modules[__name__], source)
[docs]
def get_fantastic_features(
melody: Melody,
corpus_stats: Optional[dict] = None,
phrase_gap: float = 1.5,
max_ngram_order: int = DEFAULT_MAX_NGRAM_ORDER,
) -> Dict:
"""Get all FANTASTIC features for a melody.
Parameters
----------
melody : Melody
The melody to analyze
corpus_stats : Optional[dict], optional
Corpus statistics for distributional features (default: None)
phrase_gap : float, optional
Gap threshold for phrase segmentation (default: 1.5)
max_ngram_order : int, optional
Maximum inclusive n-gram length (default: 5)
Returns
-------
Dict
Dictionary containing all FANTASTIC features
"""
return _compute_features_by_source(
melody,
"fantastic",
corpus_stats=corpus_stats,
phrase_gap=phrase_gap,
max_ngram_order=max_ngram_order
)
[docs]
def get_jsymbolic_features(melody: Melody) -> Dict:
"""Get all jSymbolic features for a melody.
Parameters
----------
melody : Melody
The melody to extract features from
Returns
-------
Dict
Dictionary containing all jSymbolic features
"""
return _compute_features_by_source(melody, "jsymbolic")
[docs]
def get_must_features(melody: Melody) -> Dict:
"""Get all MUST features for a melody.
Parameters
----------
melody : Melody
The melody to extract features from
Returns
-------
Dict
Dictionary containing all MUST features
"""
return _compute_features_by_source(melody, "must")
[docs]
def get_idyom_features(melody: Melody) -> Dict:
"""Get all IDyOM features for a melody.
Parameters
----------
melody : Melody
The melody to extract features from
Returns
-------
Dict
Dictionary containing all IDyOM features
"""
return _compute_features_by_source(melody, "idyom")
[docs]
def get_simile_features(melody: Melody) -> Dict:
"""Get all SIMILE features for a melody.
Parameters
----------
melody : Melody
The melody to extract features from
Returns
-------
Dict
Dictionary containing all SIMILE features
"""
return _compute_features_by_source(melody, "simile")
[docs]
def get_novel_features(melody: Melody) -> Dict:
"""Get all novel/custom features for a melody.
Parameters
----------
melody : Melody
The melody to extract features from
Returns
-------
Dict
Dictionary containing all novel features
"""
return _compute_features_by_source(melody, "novel")
def _compute_features_by_source(
melody: Melody,
source: str,
corpus_stats: Optional[dict] = None,
phrase_gap: float = 1.5,
max_ngram_order: int = DEFAULT_MAX_NGRAM_ORDER,
) -> Dict:
"""Compute all features for a melody that are decorated with a specific source.
Parameters
----------
melody : Melody
The melody to extract features from
source : str
The source label to filter by
corpus_stats : Optional[dict], optional
Corpus statistics for FANTASTIC features (default: None)
phrase_gap : float, optional
Gap threshold for phrase segmentation (default: 1.5)
max_ngram_order : int, optional
Maximum n-gram order for FANTASTIC features (default: 6)
Returns
-------
Dict
Dictionary containing all features from the specified source
"""
import inspect
source_features = _get_features_by_source(source)
computed_features = {}
for name, func in source_features.items():
try:
result = _invoke_feature(
func,
melody,
corpus_stats=corpus_stats,
phrase_gap=phrase_gap,
max_ngram_order=max_ngram_order,
)
if hasattr(result, '__dict__') and not isinstance(result, (str, int, float, list, dict)):
computed_features.update(result.__dict__)
else:
computed_features[name] = result
except Exception as e:
logger = logging.getLogger("melody_features")
logger.warning(f"Could not compute {name}: {e}")
continue
return computed_features
[docs]
def get_all_features(
input: FeatureInput,
config: Optional[Config] = None,
log_level: int = logging.INFO,
skip_idyom: bool = False,
long_format: bool = False,
join_metadata: bool = True,
) -> Optional["pd.DataFrame"]:
"""Calculate a multitude of features from across the computational melody analysis field.
This function returns a pandas DataFrame with a row for every melody in the supplied input.
The input can be:
- A directory path containing MIDI files
- A list of MIDI file paths
- A single MIDI file path
- A list of :class:`~melody_features.core.representations.Melody` objects
If a path to a corpus of MIDI files is provided in the Config,
corpus statistics will be computed following FANTASTIC's n-gram document frequency
model (Müllensiefen, 2009). If not, this will be skipped.
This function will also run IDyOM (Pearce, 2005) on the input MIDI files.
If a corpus of MIDI files is provided in the Config, IDyOM will be run with
pretraining on the corpus. If not, it will be run without pretraining.
Parameters
----------
input : FeatureInput
Path to input MIDI directory, list of MIDI file paths, single MIDI file
path, or list of in-memory :class:`~melody_features.core.representations.Melody`
objects
config : Config
Configuration object containing corpus path, IDyOM configurations (as a dict), and FANTASTIC configuration.
If idyom.corpus or fantastic.corpus is set, those take precedence over config.corpus for their respective methods.
If multiple IDyOM configs are provided, IDyOM will run for each config and features for each
will be included with an identifier in the output.
log_level : int
Logging level (default: logging.INFO)
skip_idyom : bool
If True, skip IDyOM feature calculation (default: False)
long_format : bool
If True, return a tidy long-format DataFrame (one row per
melody/feature combination, with `feature_name` and `value` columns)
instead of the default wide format. See
:func:`melody_features.to_long_format`, which can also be applied to
an existing wide DataFrame (default: False)
join_metadata : bool
Only used when `long_format=True`. If True, join feature metadata
(family, source, domain, type, description, notes, references) onto
the long DataFrame by `feature_name`, so features can be filtered or
grouped by source/family without a separate join step. See
:func:`melody_features.get_feature_metadata` (default: True)
Returns
-------
pd.DataFrame or None
A pandas DataFrame with a row for every melody in the input, containing all extracted features.
You can save this to CSV using df.to_csv('filename.csv') if needed.
If `long_format=True`, one row per melody/feature combination instead.
Returns ``None`` if no valid melodies are loaded. If melodies load but
no features are extracted, an empty DataFrame is returned.
"""
suppress_common_melody_warnings()
# Set up logger
logger = _setup_logger(log_level)
# Clean up any existing IDyOM temporary output directory
_cleanup_idyom_temp_output()
config = _setup_default_config(config)
_validate_config(config)
logger.info("Starting feature extraction job...")
# Log configuration parameters
logger.info("Configuration Parameters:")
logger.info(f" Key Estimation Strategy: {config.key_estimation}")
logger.info(f" Key Finding Algorithm: {config.key_finding_algorithm}")
logger.info(f" Corpus Path: {config.corpus if config.corpus else 'None (corpus features disabled)'}")
logger.info(f" IDyOM Configurations: {len(config.idyom)} config(s)")
for idyom_name, idyom_cfg in config.idyom.items():
logger.info(f" [{idyom_name}]:")
logger.info(f" Models: {idyom_cfg.models}")
logger.info(f" Corpus: {idyom_cfg.corpus if idyom_cfg.corpus else 'Using Corpus Path from Config'}")
logger.info(f" Target Viewpoints: {idyom_cfg.target_viewpoints}")
logger.info(f" Source Viewpoints: {idyom_cfg.source_viewpoints}")
logger.info(f" PPM Order: {idyom_cfg.ppm_order}")
logger.info(f" FANTASTIC Configuration:")
logger.info(f" Max N-gram Order: {config.fantastic.max_ngram_order}")
logger.info(f" Corpus: {config.fantastic.corpus if config.fantastic.corpus else 'Using Corpus Path from Config'}")
# Keep internal corpus-stat caches out of the caller's working tree.
corpus_cache_dir = Path(tempfile.gettempdir()) / "melody_features" / "corpus_stats"
corpus_cache_dir.mkdir(parents=True, exist_ok=True)
temp_output_file = str(corpus_cache_dir / "temp_corpus_stats.csv")
corpus_stats = _setup_corpus_statistics(config, temp_output_file)
melody_data_list = _load_melody_data(input)
if not melody_data_list:
logger.warning("No valid monophonic melodies found to process.")
return None
if skip_idyom:
logger.info("Skipping IDyOM analysis...")
idyom_results_dict = {}
else:
# Add retry logic for IDyOM to handle database locking issues
max_retries = 3
retry_delay = 2
for attempt in range(max_retries):
try:
idyom_results_dict = _run_idyom_analysis(input, config)
break
except Exception as e:
if "database is locked" in str(e).lower() or "sqlite" in str(e).lower():
if attempt < max_retries - 1:
logger.warning(f"IDyOM database locked (attempt {attempt + 1}/{max_retries}). Retrying in {retry_delay} seconds...")
time.sleep(retry_delay)
retry_delay *= 2
else:
logger.error(f"IDyOM failed after {max_retries} attempts due to database locking. Skipping IDyOM analysis.")
idyom_results_dict = {}
else:
raise
start_time = time.time()
headers, melody_args, timing_stats = _setup_parallel_processing(
melody_data_list, corpus_stats, idyom_results_dict, config
)
all_features = _process_melodies_parallel(
melody_args,
headers,
melody_data_list,
idyom_results_dict,
timing_stats,
)
if not all_features:
logger.warning("No features were successfully extracted from any melodies")
return pd.DataFrame()
# Create DataFrame from results
# Sort results by melody_id
all_features.sort(key=lambda x: x[0])
# Create DataFrame
df = pd.DataFrame(all_features, columns=headers)
# Rename columns to use display names
df = rename_feature_columns(df)
# Log timing statistics
end_time = time.time()
log_timing_statistics(logger, timing_stats, end_time - start_time)
logger.info(f"Successfully extracted features for {len(df)} melodies")
if long_format:
from .reshape import to_long_format
df = to_long_format(df, join_metadata=join_metadata)
return df