Module music_df.humdrum_export.humdrum_export

Functions

def df2clef(df: pandas.DataFrame)
Expand source code
def df2clef(df: pd.DataFrame):
    spines = df_to_spines(df)
    with _get_temp_paths(len(spines)) as paths:
        for path, spine in zip(paths, spines):
            _write_spine(spine, path)
        collated = collate_spines(paths)
    return collated
def df2hum(df: pandas.DataFrame,
split_point: int = 60,
label_col: str | None = None,
label_mask_col: str | None = None,
label_color_col: str | None = None,
color_col: str | None = None,
color_mask_col: str | None = None,
color_mapping: Mapping[Any, str] | None = None,
color_transparency_col: str | None = None,
n_transparency_levels: int | None = None,
uncolored_val: str | None = None,
number_every_nth_note: int | None = None,
number_specified_notes: List[int] | None = None,
number_notes_offset: int = 0,
quantize: int | None = None) ‑> str
Expand source code
def df2hum(
    df: pd.DataFrame,
    # n_clefs: int = 2, # TODO
    split_point: int = 60,
    label_col: t.Optional[str] = None,
    label_mask_col: t.Optional[str] = None,
    label_color_col: t.Optional[str] = None,
    color_col: t.Optional[str] = None,
    color_mask_col: t.Optional[str] = None,
    color_mapping: t.Optional[t.Mapping[t.Any, str]] = None,
    color_transparency_col: t.Optional[str] = None,
    n_transparency_levels: t.Optional[int] = None,
    uncolored_val: t.Optional[str] = None,
    number_every_nth_note: t.Optional[int] = None,
    number_specified_notes: t.Optional[t.List[int]] = None,
    number_notes_offset: int = 0,
    quantize: None | int = None,
) -> str:
    """
    Args:
        df: pandas DataFrame containing a musical score (see `music_df`
            package).
        split_point: int at which to split output into different staves (for
            treble and bass clefs). Default 60.
        label_col: an optional column name. The contents of the columns will be
            used to label the notes.
        color_col: if provided, notes will be colored based on this column.
            The unique values in this column will be mapped to different
            colors, up to 7 different colors, at which point they will
            repeat in modular fashion. If there are values that shouldn't
            be colored (i.e., should be left black), this can be indicated
            with a boolean mask in the column pointed to by the `color_mask_col`
            argument.
        color_transparency_col: if provided, the transparency of notes will
            be indicated with this column. Must be scalar, transparency will be linearly
            interpolated from its low value to its high value.
        color_mapping: optional dictionary mapping values (from color_col) to colors.
            The colors can be indicated w/ hex colors (strings beginning with a "#"
            character). Missing colors are given default values.
            There can be at most 7 distinct colors.
    """
    df = spell_df(df)
    if quantize:
        df = quantize_df(df, tpq=quantize)
    df = add_bar_durs(df)
    df = split_notes_at_barlines(
        df, min_overhang_dur=(1 / 16) if quantize is None else (1 / quantize)
    )

    if number_every_nth_note:
        df["note_index"] = -1
        df.loc[df.type == "note", "note_index"] = range(  # type:ignore
            number_notes_offset,
            (df.type == "note").sum() + number_notes_offset,
        )
        df["nth_note_labels"] = df.note_index.astype(str)
        df.loc[df["note_index"] % number_every_nth_note != 0, "nth_note_labels"] = ""

    if number_specified_notes:
        if "nth_note_labels" not in df.columns:
            df["nth_note_labels"] = ""
        offset_indices = [n + number_notes_offset for n in number_specified_notes]
        mask = df.index.isin(df[df.type == "note"].index[offset_indices])
        df.loc[mask, "nth_note_labels"] = [str(n) for n in number_specified_notes]

    if label_mask_col is not None:
        # (Malcolm 2023-10-20) I'm not sure why we constrain label_color_col to have at
        #   most one color. I think it is so that there is no risk of simultaneous
        #   labels having conflicting colors (since we only have one label
        #   per-time-point and only one color per-label)
        if label_color_col is not None:
            assert len(df.loc[df[label_mask_col], label_color_col].unique()) == 1
        # TODO: (Malcolm 2023-10-20) what do we do if label_color_col is None?
    if color_col is not None:
        color_mapping_inst = ColorMapping(
            df=df,
            color_col=color_col,
            color_mask_col=color_mask_col,
            color_mapping=color_mapping,
            n_alpha_levels=n_transparency_levels,
            uncolored_val=uncolored_val,
        )
        # internal_color_mapping, val_to_color_char = process_color_mapping(
        #     df, color_col, color_mask_col, color_mapping
        # )
        df = color_df(df, color_col, color_transparency_col, color_mapping_inst)
    else:
        color_mapping_inst = None
    postscript = _check_colors(df, color_mapping_inst, uncolored_val)
    # if the last item in the df is not a barline, there can be problems if
    #   after we split the df by pitch, one or more of the dfs is empty in
    #   the last measure (making the final barline seem 1 measure sooner
    #   in those parts)
    if df.iloc[-1].type != "bar":
        last_bar_vals = {"type": ["bar"], "onset": [df.release.max()]}
        if label_mask_col is not None:
            last_bar_vals[label_mask_col] = False
        if color_mask_col is not None:
            last_bar_vals[color_mask_col] = False
        last_bar = pd.DataFrame(last_bar_vals)
        df = pd.concat([df, last_bar])
    dfs = split_df_by_pitch(df, split_point)
    staves = []
    for split_df in dfs:
        if label_mask_col is not None:
            assert label_color_col is not None
            assert (
                len(split_df.loc[split_df[label_mask_col], label_color_col].unique())
                == 1
            )
        spines = df_to_spines(
            split_df,
            label_col=label_col,
            label_mask_col=label_mask_col,
            label_color_col=label_color_col,
            nth_note_label_col=(
                "nth_note_labels"
                if (number_every_nth_note or number_specified_notes)
                else None
            ),
        )
        if not spines:
            continue
        with _get_temp_paths(len(spines)) as paths:
            for path, spine in zip(paths, spines):
                _write_spine(spine, path)
            collated = collate_spines(paths)
            merged = merge_spines(collated)
        staves.append(merged)
    assert staves
    with _get_temp_paths(len(staves)) as paths:
        for path, stave in zip(paths, staves):
            with open(path, "w") as outf:
                outf.write("\n".join(stave))
        collated = collate_spines(paths)
    collated = number_measures(collated)
    return collated.strip() + "\n" + "\n".join(postscript)

Args

df
pandas DataFrame containing a musical score (see music_df package).
split_point
int at which to split output into different staves (for treble and bass clefs). Default 60.
label_col
an optional column name. The contents of the columns will be used to label the notes.
color_col
if provided, notes will be colored based on this column. The unique values in this column will be mapped to different colors, up to 7 different colors, at which point they will repeat in modular fashion. If there are values that shouldn't be colored (i.e., should be left black), this can be indicated with a boolean mask in the column pointed to by the color_mask_col argument.
color_transparency_col
if provided, the transparency of notes will be indicated with this column. Must be scalar, transparency will be linearly interpolated from its low value to its high value.
color_mapping
optional dictionary mapping values (from color_col) to colors. The colors can be indicated w/ hex colors (strings beginning with a "#" character). Missing colors are given default values. There can be at most 7 distinct colors.
def number_measures(humdrum_contents: str)
Expand source code
def number_measures(humdrum_contents: str):
    """Note: this function assumes that the input has no measure numbers.

    >>> humdrum_contents = '''*C:\\t*C:\\t*C:\\t*C:
    ... *M2/2\\t*M2/2\\t*M2/2\\t*M2/2
    ... *met(c|)\\t*met(c|)\\t*met(c|)\\t*met(c|)
    ... =\\t=\\t=\\t=
    ... 1C\\t1c\\t1G\\t1e
    ... =\\t=\\t=\\t=
    ... 2.B\\t2.d\\t2.e\\t2.g#
    ... 4E\\t4B\\t4d\\t4g
    ... =\\t=\\t=\\t=
    ... 2F\\t2A\\t2c\\t2f
    ... 2F#\\t2B\\t2A\\t2d#
    ... =\\t=\\t=\\t=
    ... 2G\\t2c\\t2G\\t2e
    ... 4C\\t4c\\t4G\\t4e
    ... 4C\\t4c\\t4G\\t4e
    ... =\\t=\\t=\\t=
    ... 4D\\t4c\\t4A\\t4f#
    ... 4D\\t4c\\t4A\\t4f#
    ... 4D\\t4c\\t4A\\t4f#
    ... 4D\\t4c\\t4A\\t4f#
    ... ==\\t==\\t==\\t==
    ... *-\\t*-\\t*-\\t*-
    ... '''

    Visually this doctest appears to be working but I haven't figured out the whitespace
    normalization to get it to pass yet.
    >>> number_measures(humdrum_contents)  # doctest: +SKIP
    '''*C:      *C:     *C:     *C:
    *M2/2        *M2/2   *M2/2   *M2/2
    *met(c|)     *met(c|)        *met(c|)        *met(c|)
    =1   =1      =1      =1
    1C   1c      1G      1e
    =2   =2      =2      =2
    2.B  2.d     2.e     2.g#
    4E   4B      4d      4g
    =3   =3      =3      =3
    2F   2A      2c      2f
    2F#  2B      2A      2d#
    =4   =4      =4      =4
    2G   2c      2G      2e
    4C   4c      4G      4e
    4C   4c      4G      4e
    =5   =5      =5      =5
    4D   4c      4A      4f#
    4D   4c      4A      4f#
    4D   4c      4A      4f#
    4D   4c      4A      4f#
    ==   ==      ==      ==
    *-   *-      *-      *-
    '''
    """
    output_lines = []
    bar_n = 1
    for line in humdrum_contents.split("\n"):
        if not line.startswith("="):
            output_lines.append(line)
            continue
        else:
            bar_tokens = line.split("\t")
        if bar_tokens == ["=="] * len(bar_tokens):
            # Not sure if there should ever be numbered double bars
            output_lines.append(line)
            continue

        assert bar_tokens == ["="] * len(bar_tokens), (
            "bar numbers etc. are not implemented"
        )

        numbered_bars = "\t".join([f"={bar_n}"] * len(bar_tokens))
        output_lines.append(numbered_bars)
        bar_n += 1
    return "\n".join(output_lines)

Note: this function assumes that the input has no measure numbers.

>>> humdrum_contents = '''*C:\t*C:\t*C:\t*C:
... *M2/2\t*M2/2\t*M2/2\t*M2/2
... *met(c|)\t*met(c|)\t*met(c|)\t*met(c|)
... =\t=\t=\t=
... 1C\t1c\t1G\t1e
... =\t=\t=\t=
... 2.B\t2.d\t2.e\t2.g#
... 4E\t4B\t4d\t4g
... =\t=\t=\t=
... 2F\t2A\t2c\t2f
... 2F#\t2B\t2A\t2d#
... =\t=\t=\t=
... 2G\t2c\t2G\t2e
... 4C\t4c\t4G\t4e
... 4C\t4c\t4G\t4e
... =\t=\t=\t=
... 4D\t4c\t4A\t4f#
... 4D\t4c\t4A\t4f#
... 4D\t4c\t4A\t4f#
... 4D\t4c\t4A\t4f#
... ==\t==\t==\t==
... *-\t*-\t*-\t*-
... '''

Visually this doctest appears to be working but I haven't figured out the whitespace normalization to get it to pass yet.

>>> number_measures(humdrum_contents)  # doctest: +SKIP
'''*C:      *C:     *C:     *C:
*M2/2        *M2/2   *M2/2   *M2/2
*met(c|)     *met(c|)        *met(c|)        *met(c|)
=1   =1      =1      =1
1C   1c      1G      1e
=2   =2      =2      =2
2.B  2.d     2.e     2.g#
4E   4B      4d      4g
=3   =3      =3      =3
2F   2A      2c      2f
2F#  2B      2A      2d#
=4   =4      =4      =4
2G   2c      2G      2e
4C   4c      4G      4e
4C   4c      4G      4e
=5   =5      =5      =5
4D   4c      4A      4f#
4D   4c      4A      4f#
4D   4c      4A      4f#
4D   4c      4A      4f#
==   ==      ==      ==
*-   *-      *-      *-
'''