Module music_df.augmentations
Functions
def aug_by_trans(orig_data: pandas.DataFrame | Iterable[pandas.DataFrame],
n_keys: int,
hi: int | None = 108,
low: int | None = 21) ‑> Iterator[pandas.DataFrame]-
Expand source code
def aug_by_trans( orig_data: pd.DataFrame | Iterable[pd.DataFrame], n_keys: int, hi: int | None = MAX_PIANO_PITCH, low: int | None = MIN_PIANO_PITCH, ) -> Iterator[pd.DataFrame]: """ Augment a music_df by transposing its content to random keys. Calls music_df.transpose.transpose_to_key to perform the transpositions. See the docstring of that function for more details. Args: orig_data: a music_df or an iterable of music_dfs. All input dataframes must have a "global_key_sig" int attribute in df.attrs, which is used to determine the transpositions. n_keys: the number of keys to transpose to hi: the maximum pitch to allow low: the minimum pitch to allow Returns: An iterator of music_dfs with the pitches transposed to random keys. The original key may or may not be included among the yielded keys. """ if isinstance(orig_data, pd.DataFrame): orig_data = [orig_data] for df in orig_data: # we don't guarantee that original key will be included among the keys keys = _n_random_keys(n_keys, enh_unique_keys=True) for key in keys: out = transpose_to_key(df, key, inplace=False) if hi is not None: max_pitch = df.pitch.max() if max_pitch > hi: continue if low is not None: min_pitch = df.pitch.min() if min_pitch < low: continue yield outAugment a music_df by transposing its content to random keys.
Calls music_df.transpose.transpose_to_key to perform the transpositions. See the docstring of that function for more details.
Args
orig_data- a music_df or an iterable of music_dfs. All input dataframes must have a "global_key_sig" int attribute in df.attrs, which is used to determine the transpositions.
n_keys- the number of keys to transpose to
hi- the maximum pitch to allow
low- the minimum pitch to allow
Returns
An iterator of music_dfs with the pitches transposed to random keys. The original key may or may not be included among the yielded keys.
def aug_rhythms(orig_data: pandas.DataFrame | Iterable[pandas.DataFrame],
n_augs: int,
n_possibilities: int = 2,
threshold: float = 0.6547,
metadata: bool = True) ‑> Iterator[pandas.DataFrame]-
Expand source code
def aug_rhythms( orig_data: pd.DataFrame | Iterable[pd.DataFrame], n_augs: int, n_possibilities: int = 2, threshold: float = 0.6547, metadata: bool = True, ) -> Iterator[pd.DataFrame]: """ Augment one or more music_dfs rhythmically by scaling rhythms up or down. Example: if n_augs is 1 and n_possibilities is 2, then the returned values will be scaled by one of (1, 2) or (depending on the threshold) (0.5, 1), but only one of these values will be chosen. Args: orig_data: a music_df or an iterable of music_dfs n_augs: specifies the actual number of augmentations performed for each dataframe. Can be 1. n_possibilities: specifies the number of "scalings" from which the actual augmentations are (uniformly randomly) chosen. Must be >= n_augs. threshold: the threshold for the mean duration of a note to determine whether to scale up or down. The default value of 0.6547 was empirically calculated from a sample of 177 scores of Classical music. metadata: if True, a `rhythms_scaled_by` attribute will be added/updated to df.attrs Returns: An iterator of music_dfs with rhythmic values scaled. The original unscaled dataframe may be among the yielded values. """ if isinstance(orig_data, pd.DataFrame): orig_data = [orig_data] for df in orig_data: mean_dur = (df.release - df.onset).mean() possible_pows_of_2 = [ x - (n_possibilities // 2) + (mean_dur < threshold and n_possibilities % 2 == 0) for x in range(n_possibilities) ] actual_pows_of_2 = random.choices(possible_pows_of_2, k=n_augs) for pow_of_2 in actual_pows_of_2: if not pow_of_2: yield df else: scale_factor = 2.0**pow_of_2 yield scale_df(df, scale_factor, metadata=metadata)Augment one or more music_dfs rhythmically by scaling rhythms up or down.
Example: if n_augs is 1 and n_possibilities is 2, then the returned values will be scaled by one of (1, 2) or (depending on the threshold) (0.5, 1), but only one of these values will be chosen.
Args
orig_data- a music_df or an iterable of music_dfs
n_augs- specifies the actual number of augmentations performed for each dataframe. Can be 1.
n_possibilities- specifies the number of "scalings" from which the actual augmentations are (uniformly randomly) chosen. Must be >= n_augs.
threshold- the threshold for the mean duration of a note to determine whether to scale up or down. The default value of 0.6547 was empirically calculated from a sample of 177 scores of Classical music.
metadata- if True, a
rhythms_scaled_byattribute will be added/updated to df.attrs
Returns
An iterator of music_dfs with rhythmic values scaled. The original unscaled dataframe may be among the yielded values.
def aug_within_range(df_iter: Iterable[pandas.DataFrame],
n_keys: int,
hi: int = 108,
low: int = 21,
min_trans: int = -5,
max_trans: int = 6)-
Expand source code
def aug_within_range( df_iter: Iterable[pd.DataFrame], n_keys: int, hi: int = MAX_PIANO_PITCH, low: int = MIN_PIANO_PITCH, min_trans: int = -5, max_trans: int = 6, ): """ Augment a music_df by transposing its content to random keys within a range. Calls music_df.transpose.chromatic_transpose to perform the transpositions. See the docstring of that function for more details. Args: df_iter: an iterable of music_dfs n_keys: the number of keys to transpose to. If None, all keys within the range will be used. hi: the maximum pitch to allow low: the minimum pitch to allow min_trans: the minimum transposition to allow max_trans: the maximum transposition to allow Returns: An iterator of music_dfs with the pitches transposed to random keys within a range. """ # if n_keys is None, we transpose to every step within range avail_range = hi - low for df in df_iter: if "spelling" in df.columns: raise ValueError("need to use 'tranpose_to_key' with spelled data") actual_max = int(df.pitch.max()) actual_min = int(df.pitch.min()) actual_range = actual_max - actual_min n_trans = avail_range - actual_range + 1 if n_trans <= 0: continue trans = list( range( max(low - actual_min, min_trans), min(max_trans, low - actual_min + n_trans) + 1, ) ) if n_keys < n_trans: random.shuffle(trans) trans = trans[:n_keys] for t in trans: yield chromatic_transpose(df, t, inplace=False, label=True)Augment a music_df by transposing its content to random keys within a range.
Calls music_df.transpose.chromatic_transpose to perform the transpositions. See the docstring of that function for more details.
Args
df_iter- an iterable of music_dfs
n_keys- the number of keys to transpose to. If None, all keys within the range will be used.
hi- the maximum pitch to allow
low- the minimum pitch to allow
min_trans- the minimum transposition to allow
max_trans- the maximum transposition to allow
Returns
An iterator of music_dfs with the pitches transposed to random keys within a range.
def scale_df(df: pandas.DataFrame, factor: float, metadata: bool = True) ‑> pandas.DataFrame-
Expand source code
def scale_df(df: pd.DataFrame, factor: float, metadata: bool = True) -> pd.DataFrame: """ Scale all rhythmic values up/down by a power of 2. Besides altering all onsets/releases, this also scales the time signature appropriately. Args: df: a music_df factor: must be a power of 2 metadata: if True, a `rhythms_scaled_by` attribute will be added/updated to df.attrs Returns: A new music_df with the rhythmic values scaled. >>> nan = float("nan") # Alias to simplify below assignments >>> df = pd.DataFrame( ... { ... "pitch": [nan, 60, nan, 64], ... "onset": [0, 0, 16, 16.5], ... "release": [nan, 4, nan, 17], ... "type": ["time_signature", "note", "time_signature", "note"], ... "other": [ ... {"numerator": 4, "denominator": 1}, ... nan, ... {"numerator": 3, "denominator": 16}, ... nan, ... ], ... } ... ) >>> pd.set_option( ... "display.width", 200 ... ) # To avoid issues when the terminal is a different size >>> pd.set_option("display.max_columns", None) >>> df pitch onset release type other 0 NaN 0.0 NaN time_signature {'numerator': 4, 'denominator': 1} 1 60.0 0.0 4.0 note NaN 2 NaN 16.0 NaN time_signature {'numerator': 3, 'denominator': 16} 3 64.0 16.5 17.0 note NaN >>> scale_df(df, 0.5) pitch onset release type other 0 NaN 0.00 NaN time_signature {'numerator': 4, 'denominator': 2} 1 60.0 0.00 2.0 note NaN 2 NaN 8.00 NaN time_signature {'numerator': 3, 'denominator': 32} 3 64.0 8.25 8.5 note NaN >>> scale_df(df, 2.0) pitch onset release type other 0 NaN 0.0 NaN time_signature {'numerator': 8, 'denominator': 1} 1 60.0 0.0 8.0 note NaN 2 NaN 32.0 NaN time_signature {'numerator': 3, 'denominator': 8} 3 64.0 33.0 34.0 note NaN >>> scale_df(df, 0.2) # doctest: +SKIP Traceback (most recent call last): AssertionError: factor=0.2 must be a power of 2 """ assert not math.log2(factor) % 1, "factor must be a power of 2" aug_df = df.copy() aug_df["onset"] *= factor aug_df["release"] *= factor aug_df = scale_time_sigs(aug_df, factor) if metadata: if "rhythms_scaled_by" in aug_df.attrs: aug_df.attrs["rhythms_scaled_by"] *= factor else: aug_df.attrs["rhythms_scaled_by"] = factor return aug_dfScale all rhythmic values up/down by a power of 2.
Besides altering all onsets/releases, this also scales the time signature appropriately.
Args
df- a music_df
factor- must be a power of 2
metadata- if True, a
rhythms_scaled_byattribute will be added/updated to df.attrs
Returns
A new music_df with the rhythmic values scaled.
>>> nan = float("nan") # Alias to simplify below assignments >>> df = pd.DataFrame( ... { ... "pitch": [nan, 60, nan, 64], ... "onset": [0, 0, 16, 16.5], ... "release": [nan, 4, nan, 17], ... "type": ["time_signature", "note", "time_signature", "note"], ... "other": [ ... {"numerator": 4, "denominator": 1}, ... nan, ... {"numerator": 3, "denominator": 16}, ... nan, ... ], ... } ... ) >>> pd.set_option( ... "display.width", 200 ... ) # To avoid issues when the terminal is a different size >>> pd.set_option("display.max_columns", None) >>> df pitch onset release type other 0 NaN 0.0 NaN time_signature {'numerator': 4, 'denominator': 1} 1 60.0 0.0 4.0 note NaN 2 NaN 16.0 NaN time_signature {'numerator': 3, 'denominator': 16} 3 64.0 16.5 17.0 note NaN>>> scale_df(df, 0.5) pitch onset release type other 0 NaN 0.00 NaN time_signature {'numerator': 4, 'denominator': 2} 1 60.0 0.00 2.0 note NaN 2 NaN 8.00 NaN time_signature {'numerator': 3, 'denominator': 32} 3 64.0 8.25 8.5 note NaN>>> scale_df(df, 2.0) pitch onset release type other 0 NaN 0.0 NaN time_signature {'numerator': 8, 'denominator': 1} 1 60.0 0.0 8.0 note NaN 2 NaN 32.0 NaN time_signature {'numerator': 3, 'denominator': 8} 3 64.0 33.0 34.0 note NaN>>> scale_df(df, 0.2) # doctest: +SKIP Traceback (most recent call last): AssertionError: factor=0.2 must be a power of 2 def scale_time_sigs(music_df: pandas.DataFrame, factor: int | float) ‑> pandas.DataFrame-
Expand source code
def scale_time_sigs(music_df: pd.DataFrame, factor: int | float) -> pd.DataFrame: """ Scale a time signature up or down by a power of 2. Typically, this simply means multiplying or dividing the denominator. For example, if the time signature is 4/4, then - scaling by 2 gives 4/2 - scaling by 0.5 gives 4/8 However, if the denominator is too small, we may scale up the numerator instead. For example, scaling 4/1 by 2.0 gives 8/1. Note that this can change the metric implications of the time signature, since 3/1 implies three whole note beats per bar, while 6/1 implies two dotted breve beats per bar. >>> df = pd.DataFrame( ... { ... "pitch": [float("nan"), float("nan")], ... "onset": [0, 16], ... "type": ["time_signature", "time_signature"], ... "other": [ ... {"numerator": 4, "denominator": 1}, ... {"numerator": 3, "denominator": 16}, ... ], ... } ... ) >>> pd.set_option( ... "display.width", 200 ... ) # To avoid issues when the terminal is a different size >>> pd.set_option("display.max_columns", None) >>> df pitch onset type other 0 NaN 0 time_signature {'numerator': 4, 'denominator': 1} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 16} >>> scale_time_sigs(df, 0.5) pitch onset type other 0 NaN 0 time_signature {'numerator': 4, 'denominator': 2} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 32} If denominator would be fractional, we scale up the numerator (this won't always give totally correct results, e.g., 3/1 -> 6/1 with doubled note values which has different metric implications.) >>> scale_time_sigs(df, 2.0) pitch onset type other 0 NaN 0 time_signature {'numerator': 8, 'denominator': 1} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 8} >>> scale_time_sigs(df, 0.2) # doctest: +SKIP Traceback (most recent call last): AssertionError: factor=0.2 must be a power of 2 """ assert not math.log2(factor) % 1, f"{factor=} must be a power of 2" time_sig_mask = music_df.type == "time_signature" if not time_sig_mask.any(): return music_df music_df = music_df.copy() time_sigs = music_df[music_df.type == "time_signature"] time_sigs.loc[:, "other"] = time_sigs["other"].apply(_to_dict_if_necessary) updated_time_sigs = [] for _, time_sig in time_sigs.iterrows(): numerator = time_sig["other"]["numerator"] # The denominator should get bigger when the notes get shorter # and vice versa new_denominator = time_sig["other"]["denominator"] / factor while new_denominator % 1: numerator *= 2 new_denominator *= 2 assert not new_denominator % 1 new_dict = deepcopy(time_sig["other"]) new_dict["denominator"] = int(new_denominator) new_dict["numerator"] = int(numerator) time_sig["other"] = new_dict updated_time_sigs.append(time_sig) music_df.loc[time_sig_mask] = updated_time_sigs return music_dfScale a time signature up or down by a power of 2.
Typically, this simply means multiplying or dividing the denominator. For example, if the time signature is 4/4, then - scaling by 2 gives 4/2 - scaling by 0.5 gives 4/8
However, if the denominator is too small, we may scale up the numerator instead. For example, scaling 4/1 by 2.0 gives 8/1. Note that this can change the metric implications of the time signature, since 3/1 implies three whole note beats per bar, while 6/1 implies two dotted breve beats per bar.
>>> df = pd.DataFrame( ... { ... "pitch": [float("nan"), float("nan")], ... "onset": [0, 16], ... "type": ["time_signature", "time_signature"], ... "other": [ ... {"numerator": 4, "denominator": 1}, ... {"numerator": 3, "denominator": 16}, ... ], ... } ... ) >>> pd.set_option( ... "display.width", 200 ... ) # To avoid issues when the terminal is a different size >>> pd.set_option("display.max_columns", None) >>> df pitch onset type other 0 NaN 0 time_signature {'numerator': 4, 'denominator': 1} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 16}>>> scale_time_sigs(df, 0.5) pitch onset type other 0 NaN 0 time_signature {'numerator': 4, 'denominator': 2} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 32}If denominator would be fractional, we scale up the numerator (this won't always give totally correct results, e.g., 3/1 -> 6/1 with doubled note values which has different metric implications.)
>>> scale_time_sigs(df, 2.0) pitch onset type other 0 NaN 0 time_signature {'numerator': 8, 'denominator': 1} 1 NaN 16 time_signature {'numerator': 3, 'denominator': 8}>>> scale_time_sigs(df, 0.2) # doctest: +SKIP Traceback (most recent call last): AssertionError: factor=0.2 must be a power of 2 def shuffle_pitches(df: pandas.DataFrame, inplace=False)-
Expand source code
def shuffle_pitches(df: pd.DataFrame, inplace=False): """ Shuffle the pitches of the notes of a music_df. This can be used to get a random baseline for some tasks. >>> csv_table = ''' ... type,pitch,onset,release ... bar,,0.0,4.0 ... note,60,0.0,1.0 ... note,64,1.0,2.0 ... note,67,2.0,3.0 ... note,72,3.0,4.0 ... ''' >>> df = pd.read_csv(io.StringIO(csv_table.strip())) >>> df type pitch onset release 0 bar NaN 0.0 4.0 1 note 60.0 0.0 1.0 2 note 64.0 1.0 2.0 3 note 67.0 2.0 3.0 4 note 72.0 3.0 4.0 >>> shuffle_pitches(df) # doctest: +SKIP type pitch onset release 0 bar NaN 0.0 4.0 1 note 72.0 0.0 1.0 2 note 67.0 1.0 2.0 3 note 60.0 2.0 3.0 4 note 64.0 3.0 4.0 """ note_mask = df["type"] == "note" pitches = df.loc[note_mask, "pitch"].tolist() if not inplace: df = df.copy() random.shuffle(pitches) df.loc[note_mask, "pitch"] = pitches return dfShuffle the pitches of the notes of a music_df.
This can be used to get a random baseline for some tasks.
>>> csv_table = ''' ... type,pitch,onset,release ... bar,,0.0,4.0 ... note,60,0.0,1.0 ... note,64,1.0,2.0 ... note,67,2.0,3.0 ... note,72,3.0,4.0 ... ''' >>> df = pd.read_csv(io.StringIO(csv_table.strip())) >>> df type pitch onset release 0 bar NaN 0.0 4.0 1 note 60.0 0.0 1.0 2 note 64.0 1.0 2.0 3 note 67.0 2.0 3.0 4 note 72.0 3.0 4.0 >>> shuffle_pitches(df) # doctest: +SKIP type pitch onset release 0 bar NaN 0.0 4.0 1 note 72.0 0.0 1.0 2 note 67.0 1.0 2.0 3 note 60.0 2.0 3.0 4 note 64.0 3.0 4.0 def shuffle_slices(df: pandas.DataFrame, check: bool = False, include_rests: bool = True)-
Expand source code
def shuffle_slices(df: pd.DataFrame, check: bool = False, include_rests: bool = True): """ Shuffle the slices of a salami-sliced music_df. This can be used to get a random baseline for some tasks. Args: df: a music_df check: if True, will raise an AssertionError if the music_df is not salami-sliced. include_rests: if True, will include the rests in the shuffling. Returns: A new music_df with the slices shuffled. >>> csv_table = ''' ... type,pitch,onset,release ... bar,,0.0,4.0 ... note,60,0.0,0.5 ... note,64,0.0,0.5 ... note,60,2.0,3.0 ... note,65,2.0,3.0 ... bar,,4.0,8.0 ... note,60,4.75,5.0 ... note,67,4.75,5.0 ... ''' >>> df = pd.read_csv(io.StringIO(csv_table.strip())) >>> df type pitch onset release 0 bar NaN 0.00 4.0 1 note 60.0 0.00 0.5 2 note 64.0 0.00 0.5 3 note 60.0 2.00 3.0 4 note 65.0 2.00 3.0 5 bar NaN 4.00 8.0 6 note 60.0 4.75 5.0 7 note 67.0 4.75 5.0 >>> shuffle_slices(df) # doctest: +SKIP type pitch onset release 0 bar NaN 0.00 4.0 1 note 60.0 1.50 2.0 2 note 64.0 1.50 2.0 3 note 60.0 2.00 3.0 4 note 65.0 2.00 3.0 5 bar NaN 4.00 8.0 6 note 60.0 4.75 5.0 7 note 67.0 4.75 5.0 """ if check: assert appears_salami_sliced(df) note_mask = df["type"] == "note" note_df = df[note_mask] if include_rests: rests = return_rests(note_df) rest_df = pd.DataFrame( [ {"type": "rest", "onset": onset, "release": release} for (onset, release) in rests ] ) # I don't think it is necessary to sort the result so that the rests # occur in the correct location since we are shuffling immediately afterwards note_df = pd.concat([note_df, rest_df], axis=0) note_df["duration"] = note_df["release"] - note_df["onset"] note_groups = [group for _, group in note_df.groupby("onset")] random.shuffle(note_groups) onset = 0 for note_group in note_groups: note_group["onset"] = onset onset += note_group.iloc[0]["duration"] shuffled_note_df = pd.concat(note_groups) shuffled_note_df["release"] = ( shuffled_note_df["onset"] + shuffled_note_df["duration"] ) shuffled_note_df = shuffled_note_df.drop("duration", axis=1) if include_rests: # drop the added rests shuffled_note_df = shuffled_note_df[shuffled_note_df["type"] == "note"] out_df = pd.concat((shuffled_note_df, df[~note_mask])) return sort_df(out_df)Shuffle the slices of a salami-sliced music_df.
This can be used to get a random baseline for some tasks.
Args
df- a music_df
check- if True, will raise an AssertionError if the music_df is not salami-sliced.
include_rests- if True, will include the rests in the shuffling.
Returns
A new music_df with the slices shuffled.
>>> csv_table = ''' ... type,pitch,onset,release ... bar,,0.0,4.0 ... note,60,0.0,0.5 ... note,64,0.0,0.5 ... note,60,2.0,3.0 ... note,65,2.0,3.0 ... bar,,4.0,8.0 ... note,60,4.75,5.0 ... note,67,4.75,5.0 ... ''' >>> df = pd.read_csv(io.StringIO(csv_table.strip())) >>> df type pitch onset release 0 bar NaN 0.00 4.0 1 note 60.0 0.00 0.5 2 note 64.0 0.00 0.5 3 note 60.0 2.00 3.0 4 note 65.0 2.00 3.0 5 bar NaN 4.00 8.0 6 note 60.0 4.75 5.0 7 note 67.0 4.75 5.0 >>> shuffle_slices(df) # doctest: +SKIP type pitch onset release 0 bar NaN 0.00 4.0 1 note 60.0 1.50 2.0 2 note 64.0 1.50 2.0 3 note 60.0 2.00 3.0 4 note 65.0 2.00 3.0 5 bar NaN 4.00 8.0 6 note 60.0 4.75 5.0 7 note 67.0 4.75 5.0