Module music_df.transpose

Functions for transposing music either by pitch-class or along the circle of fifths.

Functions

def chromatic_transpose(df: pandas.DataFrame,
interval: int,
inplace: bool = True,
label: bool = False,
metadata=True)
Expand source code
def chromatic_transpose(
    df: pd.DataFrame,
    interval: int,
    inplace: bool = True,
    label: bool = False,
    metadata=True,
):
    """
    Transpose the pitches of a music_df by a given chromatic interval.

    Note that this will change the "pitch" column but not any other columns that may
    be pitch-related such as those that may indicate the spelling or key signature.

    Args:
        df: a music_df
        interval: the interval to transpose by
        inplace: if True, will modify the music_df in place
        label: if True, will add a "transposed_by_n_semitones" column to the music_df
        metadata: if True, will add a "chromatic_transpose" attribute to the music_df

    Returns:
        A new music_df with the pitches transposed by the given interval.
    """
    out_df = df if inplace else df.copy()
    out_df.pitch += interval
    if metadata:
        if "chromatic_transpose" in out_df.attrs:
            out_df.attrs["chromatic_transpose"] += interval
        else:
            out_df.attrs["chromatic_transpose"] = interval
    if label:
        out_df.loc[:, "transposed_by_n_semitones"] = interval
    return out_df

Transpose the pitches of a music_df by a given chromatic interval.

Note that this will change the "pitch" column but not any other columns that may be pitch-related such as those that may indicate the spelling or key signature.

Args

df
a music_df
interval
the interval to transpose by
inplace
if True, will modify the music_df in place
label
if True, will add a "transposed_by_n_semitones" column to the music_df
metadata
if True, will add a "chromatic_transpose" attribute to the music_df

Returns

A new music_df with the pitches transposed by the given interval.

def transpose_to_key(df: pandas.DataFrame, new_key_sig: int, inplace: bool = True)
Expand source code
def transpose_to_key(
    df: pd.DataFrame,
    new_key_sig: int,
    inplace: bool = True,
):
    """
    Transpose a music_df to a new key signature.

    The dataframe must have a "global_key_sig" int attribute in df.attrs which is used
    to determine the transposition. This attribute indicates the number of sharps/flats
    in the key signature. E.g., 4 indicates 4 sharps, -4 indicates 4 flats.

    Different metadata values in df.attrs are used to determine which columns to
    transpose in which way:

    - "pitch_columns" tuple of str: columns containing midi pitches to transpose. By
        default, only the "pitch" column is used.
    - "spelled_columns" tuple of str: columns containing spelled pitches (e.g., D, Cb,
        F##) to transpose. By default, no spelled columns are used.
    - "pc_columns" tuple of str: columns containing pitch classes (e.g., 0, 8, 11) to
        transpose. By default, no pitch class columns are used.
    - "key_sig_columns" tuple of str: columns containing key signatures to transpose.
        By default, no key signature columns are used.
    - "key_sig_class_columns" tuple of str: columns containing key signature classes.
        By default, no key signature class columns are used.

    Args:
        df: a music_df
        new_key_sig: the new key signature to transpose to
        inplace: if True, will modify the music_df in place

    Returns:
        A new music_df with the pitches transposed to the new key signature.

    >>> df = pd.DataFrame(
    ...     {
    ...         "pitch": [60, 62, 64],
    ...         "spelling": ["C", "D", "E"],
    ...         "pc": [0, 2, 4],
    ...         "key": ["C", "C", "a"],
    ...     }
    ... )
    >>> df.attrs["global_key_sig"] = 0

    In order for spelled columns and pc columns to be transposed correctly,
    they need to be included in sequences in df.attrs:
    >>> df.attrs["spelled_columns"] = ("spelling", "key")
    >>> df.attrs["pc_columns"] = ("pc",)

    >>> new_df = transpose_to_key(df, 2, inplace=False)
    >>> new_df
       pitch spelling  pc key
    0     62        D   2   D
    1     64        E   4   D
    2     66       F#   6   b
    >>> new_df.attrs["global_key_sig"]
    2
    >>> new_df.attrs["transposed_by_n_sharps"]
    2
    >>> newer_df = transpose_to_key(new_df, -7, inplace=False)
    >>> newer_df
       pitch spelling  pc key
    0     59       Cb  11  Cb
    1     61       Db   1  Cb
    2     63       Eb   3  ab
    >>> newer_df.attrs["global_key_sig"]
    -7
    >>> newer_df.attrs["transposed_by_n_sharps"]
    -7
    """
    orig_key = df.attrs["global_key_sig"]
    transposed_by = df.attrs.get("transposed_by_n_sharps", 0)
    interval = new_key_sig - orig_key
    out_df = df if inplace else df.copy()

    if not interval:
        return out_df

    for column in df.attrs.get("pitch_columns", ("pitch",)):
        out_df[column] = df[column].apply(
            functools.partial(MIDI_NUM_TRANSPOSER, interval=interval)
        )
    for column in df.attrs.get("spelled_columns", ()):
        out_df[column] = df[column].apply(
            functools.partial(SPELLING_TRANSPOSER, interval=interval)
        )
    for column in df.attrs.get("pc_columns", ()):
        out_df[column] = df[column].apply(
            functools.partial(MIDI_NUM_TRANSPOSER, interval=interval, pc=True)
        )
    for column in df.attrs.get("key_sig_columns", ()):
        df[column] = df[column] + interval
    for column in df.attrs.get("key_sig_class_columns", ()):
        df[column] = (df[column] + interval) % 12
        df.loc[df[column] > 6, column] -= 12

    out_df.attrs.pop("global_key", None)  # global_key will no longer be accurate
    out_df.attrs["global_key_sig"] = new_key_sig
    out_df.attrs["transposed_by_n_sharps"] = transposed_by + interval
    return out_df

Transpose a music_df to a new key signature.

The dataframe must have a "global_key_sig" int attribute in df.attrs which is used to determine the transposition. This attribute indicates the number of sharps/flats in the key signature. E.g., 4 indicates 4 sharps, -4 indicates 4 flats.

Different metadata values in df.attrs are used to determine which columns to transpose in which way:

  • "pitch_columns" tuple of str: columns containing midi pitches to transpose. By default, only the "pitch" column is used.
  • "spelled_columns" tuple of str: columns containing spelled pitches (e.g., D, Cb, F##) to transpose. By default, no spelled columns are used.
  • "pc_columns" tuple of str: columns containing pitch classes (e.g., 0, 8, 11) to transpose. By default, no pitch class columns are used.
  • "key_sig_columns" tuple of str: columns containing key signatures to transpose. By default, no key signature columns are used.
  • "key_sig_class_columns" tuple of str: columns containing key signature classes. By default, no key signature class columns are used.

Args

df
a music_df
new_key_sig
the new key signature to transpose to
inplace
if True, will modify the music_df in place

Returns

A new music_df with the pitches transposed to the new key signature.

>>> df = pd.DataFrame(
...     {
...         "pitch": [60, 62, 64],
...         "spelling": ["C", "D", "E"],
...         "pc": [0, 2, 4],
...         "key": ["C", "C", "a"],
...     }
... )
>>> df.attrs["global_key_sig"] = 0

In order for spelled columns and pc columns to be transposed correctly, they need to be included in sequences in df.attrs:

>>> df.attrs["spelled_columns"] = ("spelling", "key")
>>> df.attrs["pc_columns"] = ("pc",)
>>> new_df = transpose_to_key(df, 2, inplace=False)
>>> new_df
   pitch spelling  pc key
0     62        D   2   D
1     64        E   4   D
2     66       F#   6   b
>>> new_df.attrs["global_key_sig"]
2
>>> new_df.attrs["transposed_by_n_sharps"]
2
>>> newer_df = transpose_to_key(new_df, -7, inplace=False)
>>> newer_df
   pitch spelling  pc key
0     59       Cb  11  Cb
1     61       Db   1  Cb
2     63       Eb   3  ab
>>> newer_df.attrs["global_key_sig"]
-7
>>> newer_df.attrs["transposed_by_n_sharps"]
-7

Classes

class MidiNumAlongLineOfFifthsTransposer
Expand source code
class MidiNumAlongLineOfFifthsTransposer:
    """
    A simple class for transposing MIDI numbers along the line of fifths.

    Caches results to avoid recalculating them.

    >>> transposer = MidiNumAlongLineOfFifthsTransposer()
    >>> transposer(60, 2)
    62
    >>> transposer(60, 7)
    61

    Tranposition is always to the nearest pitch:
    >>> transposer(60, 12 * 7)
    60

    We take +6 semitones rather than -6 semitones:
    >>> transposer(60, 6)
    66
    >>> transposer(60, -6)
    66

    >>> transposer(60, -6, pc=True)
    6

    Nan values are returned unchanged (this is likely to occur when transposing an
    entire column of a DataFrame, where e.g., a key signature does not have a pitch):
    >>> transposer(float("nan"), -3)
    nan
    """

    def __init__(self):
        self._memo = defaultdict(dict)

    def __call__(
        self, midi_num: int | float, interval: int, pc: bool = False
    ) -> int | float:
        if isnan(midi_num):
            return midi_num
        if midi_num in self._memo[interval]:
            out = self._memo[interval][midi_num]
        else:
            chromatic_int = (interval * 7) % 12
            if chromatic_int > 6:
                chromatic_int -= 12
            new_midi_num = midi_num + chromatic_int
            self._memo[interval][midi_num] = new_midi_num
            out = new_midi_num
        if pc:
            return out % 12
        return out

A simple class for transposing MIDI numbers along the line of fifths.

Caches results to avoid recalculating them.

>>> transposer = MidiNumAlongLineOfFifthsTransposer()
>>> transposer(60, 2)
62
>>> transposer(60, 7)
61

Tranposition is always to the nearest pitch:

>>> transposer(60, 12 * 7)
60

We take +6 semitones rather than -6 semitones:

>>> transposer(60, 6)
66
>>> transposer(60, -6)
66
>>> transposer(60, -6, pc=True)
6

Nan values are returned unchanged (this is likely to occur when transposing an entire column of a DataFrame, where e.g., a key signature does not have a pitch):

>>> transposer(float("nan"), -3)
nan
class SpellingAlongLineOfFifthsTransposer
Expand source code
class SpellingAlongLineOfFifthsTransposer:
    """
    A simple class for transposing spellings along the line of fifths.

    Caches results to avoid recalculating them.

    >>> transposer = SpellingAlongLineOfFifthsTransposer()
    >>> transposer("C", 2)
    'D'
    >>> transposer("C", 7)
    'C#'
    >>> transposer("C", -9)
    'Bbb'

    We preserve case so that this class can also be used to transpose minor keys:
    >>> transposer("c", 3)
    'a'
    >>> transposer("c", -3)
    'eb'

    Nan values are returned unchanged (this is likely to occur when transposing an
    entire column of a DataFrame, where e.g., a key signature does not have a pitch):
    >>> transposer(float("nan"), -3)
    nan

    Likewise, "na" values are returned unchanged for a similar reason:
    >>> transposer("na", -3)
    'na'

    """

    def __init__(self):
        self._memo = defaultdict(dict)

    def __call__(self, spelling: str | float, interval: int) -> str | float:
        if not isinstance(spelling, str) and isnan(spelling):
            return spelling
        if spelling == "na":
            return spelling
        if spelling in self._memo[interval]:
            return self._memo[interval][spelling]
        else:
            assert isinstance(spelling, str)
            new_spelling = line_of_fifths_to_spelling(
                spelling_to_line_of_fifths(spelling.capitalize()) + interval
            )
            if spelling[0].islower():
                new_spelling = new_spelling[0].lower() + new_spelling[1:]
            self._memo[interval][spelling] = new_spelling
            return new_spelling

A simple class for transposing spellings along the line of fifths.

Caches results to avoid recalculating them.

>>> transposer = SpellingAlongLineOfFifthsTransposer()
>>> transposer("C", 2)
'D'
>>> transposer("C", 7)
'C#'
>>> transposer("C", -9)
'Bbb'

We preserve case so that this class can also be used to transpose minor keys:

>>> transposer("c", 3)
'a'
>>> transposer("c", -3)
'eb'

Nan values are returned unchanged (this is likely to occur when transposing an entire column of a DataFrame, where e.g., a key signature does not have a pitch):

>>> transposer(float("nan"), -3)
nan

Likewise, "na" values are returned unchanged for a similar reason:

>>> transposer("na", -3)
'na'