Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added patel features (nPVI and MIV) as custom_features #60

Draft
wants to merge 16 commits into
base: develop
Choose a base branch
from
Binary file added musif/.DS_Store
Binary file not shown.
Binary file added musif/extract/.DS_Store
Binary file not shown.
Empty file.
1 change: 1 addition & 0 deletions musif/extract/custom_features/patel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .constants import *
3 changes: 3 additions & 0 deletions musif/extract/custom_features/patel/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MIV = "MIV"
NPVI="nPVI"

60 changes: 60 additions & 0 deletions musif/extract/custom_features/patel/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ipdb

from music21.analysis.patel import nPVI, melodicIntervalVariability

import musif.extract.constants as C
from musif.extract.features.prefix import get_part_feature
from musif.config import ExtractConfiguration
from typing import List


from musif.extract.custom_features.patel.constants import *


def update_part_objects(
score_data: dict, part_data: dict, cfg: ExtractConfiguration, part_features: dict
):
# "update_part_objects from module inside a package given its parent package (score)!"

for measure in part_data["measures"]:
try:
_nPVI = nPVI(measure)
_melodicIntervalVariability = melodicIntervalVariability(measure)
except Exception:
_nPVI = 0.0
_melodicIntervalVariability = 0.0

print('_nPVI: ', _nPVI)
part_features['NPVI'] = _nPVI

print('_melodicIntervalVariability: ', _melodicIntervalVariability)
part_features['MIV'] = _melodicIntervalVariability


def update_score_objects(
score_data: dict,
parts_data: List[dict],
cfg: ExtractConfiguration,
parts_features: List[dict],
score_features: dict,
):
# We need to add the data to score_features,
# the dictionary where all final info is stored.
# Otherwise it will not be reflected in the final dataframe.
# "Updating stuffs from module inside a package given its parent package (part)!"

features = {}
for part_data, part_features in zip(parts_data, parts_features):
part_abbreviation = part_data[C.DATA_PART_ABBREVIATION]
# get NPVI
feature_name = get_part_feature(part_abbreviation, NPVI)
features[feature_name] = part_features['NPVI']
# get MIV
feature_name = get_part_feature(part_abbreviation, MIV)
features[feature_name] = part_features['MIV']

score_features.update(features)




126 changes: 126 additions & 0 deletions musif/extract/custom_features/prefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from musif.extract.constants import DATA_PART_ABBREVIATION, DATA_SOUND_ABBREVIATION


def get_part_prefix(part_abbreviation: str) -> str:
"""
Returns prefix name for a specific part given instrument's abbreviation

Example
'vnI' -> 'PartVnI_'

Parameters
----------
part_abbreviation: str
String that represents the abbreviated name of an instrument
"""
if part_abbreviation is None or len(part_abbreviation) == 0:
return "Part"
return f"Part{part_abbreviation[0].upper() + part_abbreviation[1:]}_"


def get_sound_prefix(sound_abbreviation: str) -> str:
"""
Returns prefix name for a specific part given sound's abbreviation

Example
'vnI' -> 'SoundVn_'

Parameters
----------
part_abbreviation: str
String that represents the abbreviated name of a sound
"""
if sound_abbreviation is None or len(sound_abbreviation) == 0:
return "Sound"
return f"Sound{sound_abbreviation[0].upper() + sound_abbreviation[1:]}_"


def get_family_prefix(family_abbreviation: str) -> str:
"""
Returns prefix name for a specific part given sound's abbreviation

Example
'vnI' -> 'SoundVn_'

Parameters
----------
part_abbreviation: str
String that represents the abbreviated name of a sound
"""

if family_abbreviation is None or len(family_abbreviation) == 0:
return "Family"
return f"Family{family_abbreviation[0].upper() + family_abbreviation[1:]}_"


def get_score_prefix() -> str:
return "Score_"


def get_corpus_prefix() -> str:
return "Corpus_"


def get_part_feature(part: str, feature: str) -> str:
"""
It builds the name of a feature with part scope.
For instance, if the feature is "NumberOfIntervals" and the part is "VnI",
this class would return: "PartVnI_NumberOfIntervals".
Args:
part (str): The part name.
feature (str): Name of the feature to be prefixed.
Returns:
str: The feature properly prefixed for the part passed as argument.
"""

return get_part_prefix(part) + feature


def get_sound_feature(sound: str, feature: str) -> str:
"""
It builds the name of a feature with sound scope.

For instance, if the feature is "NumberOfIntervals" and the sound is "Ob",
this class would return: "SoundOb_NumberOfIntervals".

Args:
sound (str): The sound name.
feature (str): Name of the feature to be prefixed.

Returns:
str: The feature properly prefixed for the sound passed as argument.
"""
return get_sound_prefix(sound) + feature


def get_family_feature(family: str, feature: str) -> str:
"""
It builds the name of a feature with family scope.

For instance, if the feature is "NumberOfIntervals" and the family is "Str",
this class would return: "FamilyStr_NumberOfIntervals".

Args:
family (str): The family name.
feature (str): Name of the feature to be prefixed.

Returns:
str: The feature properly prefixed for the family passed as argument.
"""
return get_family_prefix(family) + feature


def get_score_feature(feature: str) -> str:
"""
It builds the name of a feature with Score scope.

For instance, if the feature is "NumberOfIntervals",
this class would return: "Score_NumberOfIntervals".

Args:
feature (str): Name of the feature to be prefixed.

Returns:
str: The feature properly prefixed for score scope.
"""
return get_score_prefix() + feature
2 changes: 1 addition & 1 deletion musif/extract/features/ambitus/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def update_score_objects(
parts_data = _filter_parts_data(parts_data, cfg.parts_filter)
if len(parts_data) == 0:
return

for part_data, part_features in zip(parts_data, parts_features):
part = part_data[DATA_PART_ABBREVIATION]
for feature_name in SCORE_FEATURES:
Expand Down
2 changes: 2 additions & 0 deletions musif/extract/features/melody/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections import Counter
from statistics import mean, stdev
from typing import List, Tuple
import ipdb
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this debug line


import numpy as np
from music21.interval import Interval
Expand Down Expand Up @@ -37,6 +38,7 @@ def update_part_objects(
part_features.update(get_interval_stats_features(intervals))



def update_score_objects(
score_data: dict,
parts_data: List[dict],
Expand Down
75 changes: 75 additions & 0 deletions musif/extract/features/rhythm/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def update_part_objects(
notes_duration = [
i for i in notes_duration if i != 0.0
] # remove notes with duration equal to 0


part_features.update(
{
Expand Down Expand Up @@ -167,3 +168,77 @@ def update_score_objects(
score_features.update(features)


def get_motion_features(part_data) -> dict:
notes_midi = []
notes_duration = []
for note in part_data["notes_and_rests"]:
if hasattr(note, "pitch"):
notes_midi.append(note.pitch.midi)
notes_duration.append(note.duration.quarterLength)

notes_midi = np.asarray(notes_midi)
notes_duration = np.asarray(notes_duration)

if len(notes_midi) == 0:
return {
SPEED_AVG_ABS: 0,
ACCELERATION_AVG_ABS: 0,
ASCENDENT_AVERAGE: 0,
DESCENDENT_AVERAGE: 0,
ASCENDENT_PROPORTION: 0,
DESCENDENT_PROPORTION: 0
}

step = 0.125
midis_raw = np.repeat(notes_midi, [i / step for i in notes_duration], axis=0)
spe_raw = np.diff(midis_raw) / step
acc_raw = np.diff(spe_raw) / step

# Absolute means of speed and acceleration
spe_avg_abs = np.mean(abs(spe_raw))
acc_avg_abs = np.mean(abs(acc_raw))

# Rolling mean to smooth the midis by +-1 compasses -- not required for
# statistics based on means but important for detecting increasing sequences
# with a tolerance.
measure = 4
midis_smo_series = pd.Series(midis_raw)
midis_smo = [
np.mean(i.to_list())
for i in midis_smo_series.rolling(2 * measure + 1, center=True)
]

# midis_smo = np.rollmean(midis_raw, k = 2 * compass + 1, align = "center")

# spe_smo = np.diff(midis_smo) / step
# acc_smo = np.diff(spe_smo) / step

# Prolonged ascent/descent chunks in smoothed midis of the aria (allows for
# small violations in the form of decrements/increments that do not
# decrease/increase the rolling mean).

dife = np.diff(midis_smo)

asc = [(k, sum(1 for i in g)) for k, g in groupby(dife > 0)]
dsc = [(k, sum(1 for i in g)) for k, g in groupby(dife < 0)]

asc = [i for b, i in asc if b]
dsc = [i for b, i in dsc if b]

# Average length of ascent/descent chunks of the aria
asc_avg = mean(asc) if asc else np.nan
dsc_avg = mean(dsc) if dsc else np.nan

# Proportion of ascent/descent chunks over the total of the aria
asc_prp = sum(asc) / (len(dife) - 1) if asc else np.nan
dsc_prp = sum(dsc) / (len(dife) - 1) if dsc else np.nan

return {
SPEED_AVG_ABS: spe_avg_abs,
ACCELERATION_AVG_ABS: acc_avg_abs,
ASCENDENT_AVERAGE: asc_avg,
DESCENDENT_AVERAGE: dsc_avg,
ASCENDENT_PROPORTION: asc_prp,
DESCENDENT_PROPORTION: dsc_prp
}