Module music_df.split_notes

Functions for splitting notes.

Functions

def split_notes_at_barlines(df: pandas.DataFrame, min_overhang_dur: float | None = None)
Expand source code
def split_notes_at_barlines(
    df: pd.DataFrame,
    min_overhang_dur: float | None = None,
):
    """
    >>> csv_table = '''
    ... type,pitch,onset,release,tie_to_next,tie_to_prev
    ... bar,,0.0,4.0,,
    ... note,60,0.0,4.0,,
    ... note,64,0.0,4.001,,
    ... note,67,0.0,12.0,,
    ... bar,,4.0,8.0,,
    ... note,72,7.999,12.0,,
    ... bar,,8.0,12.0,,
    ... note,76,9.0,9.001,,
    ... bar,,12.0,16.0,,
    ... '''
    >>> df = pd.read_csv(io.StringIO(csv_table.strip()))
    >>> df["tie_to_next"] = df["tie_to_next"].fillna(False)
    >>> df["tie_to_prev"] = df["tie_to_prev"].fillna(False)
    >>> df
       type  pitch   onset  release  tie_to_next  tie_to_prev
    0   bar    NaN   0.000    4.000        False        False
    1  note   60.0   0.000    4.000        False        False
    2  note   64.0   0.000    4.001        False        False
    3  note   67.0   0.000   12.000        False        False
    4   bar    NaN   4.000    8.000        False        False
    5  note   72.0   7.999   12.000        False        False
    6   bar    NaN   8.000   12.000        False        False
    7  note   76.0   9.000    9.001        False        False
    8   bar    NaN  12.000   16.000        False        False

    >>> split_notes_at_barlines(df)
        type  pitch   onset  release  tie_to_next  tie_to_prev
    0    bar    NaN   0.000    4.000        False        False
    1   note   60.0   0.000    4.000        False        False
    2   note   64.0   0.000    4.000         True        False
    3   note   67.0   0.000    4.000         True        False
    4    bar    NaN   4.000    8.000        False        False
    5   note   64.0   4.000    4.001        False         True
    6   note   67.0   4.000    8.000         True         True
    7   note   72.0   7.999    8.000         True        False
    8    bar    NaN   8.000   12.000        False        False
    9   note   67.0   8.000   12.000        False         True
    10  note   72.0   8.000   12.000        False         True
    11  note   76.0   9.000    9.001        False        False
    12   bar    NaN  12.000   16.000        False        False

    >>> split_notes_at_barlines(df, min_overhang_dur=0.025)
        type  pitch  onset  release  tie_to_next  tie_to_prev
    0    bar    NaN    0.0    4.000        False        False
    1   note   60.0    0.0    4.000        False        False
    2   note   64.0    0.0    4.000        False        False
    3   note   67.0    0.0    4.000         True        False
    4    bar    NaN    4.0    8.000        False        False
    5   note   67.0    4.0    8.000         True         True
    6    bar    NaN    8.0   12.000        False        False
    7   note   67.0    8.0   12.000        False         True
    8   note   72.0    8.0   12.000        False        False
    9   note   76.0    9.0    9.001        False        False
    10   bar    NaN   12.0   16.000        False        False

    """
    if df.loc[df.type == "bar", "release"].isna().any():
        df = add_bar_durs(df)
    bars = df[df.type == "bar"].reset_index()
    bars_i = 0
    row_accumulator = []
    for _, row in df.iterrows():
        overhang = False
        if row.type != "note":
            row_accumulator.append(row)
            continue
        else:
            while (bars_i < len(bars) - 1) and (bars.loc[bars_i + 1].onset < row.onset):
                bars_i += 1
            temp_row = row.copy()
            for final_bars_i in range(bars_i, len(bars)):
                bar_release = bars.loc[final_bars_i].release
                if row.release > bar_release:
                    overhang = True
                    truncated_row = temp_row.copy()
                    truncated_row.release = bar_release
                    if (
                        min_overhang_dur is None
                        or truncated_row.release - truncated_row.onset
                        >= min_overhang_dur
                    ):
                        row_accumulator.append(truncated_row)
                        temp_row.tie_to_prev = True

                    temp_row.onset = bar_release
                    if (
                        min_overhang_dur is None
                        or temp_row.release - temp_row.onset >= min_overhang_dur
                    ):
                        truncated_row.tie_to_next = True
                else:
                    break
            if (
                (not overhang)
                or (min_overhang_dur is None)
                or (temp_row.release - temp_row.onset >= min_overhang_dur)
            ):
                row_accumulator.append(temp_row)

    out_df = pd.DataFrame(row_accumulator)
    out_df = sort_df(out_df)
    return out_df
>>> csv_table = '''
... type,pitch,onset,release,tie_to_next,tie_to_prev
... bar,,0.0,4.0,,
... note,60,0.0,4.0,,
... note,64,0.0,4.001,,
... note,67,0.0,12.0,,
... bar,,4.0,8.0,,
... note,72,7.999,12.0,,
... bar,,8.0,12.0,,
... note,76,9.0,9.001,,
... bar,,12.0,16.0,,
... '''
>>> df = pd.read_csv(io.StringIO(csv_table.strip()))
>>> df["tie_to_next"] = df["tie_to_next"].fillna(False)
>>> df["tie_to_prev"] = df["tie_to_prev"].fillna(False)
>>> df
   type  pitch   onset  release  tie_to_next  tie_to_prev
0   bar    NaN   0.000    4.000        False        False
1  note   60.0   0.000    4.000        False        False
2  note   64.0   0.000    4.001        False        False
3  note   67.0   0.000   12.000        False        False
4   bar    NaN   4.000    8.000        False        False
5  note   72.0   7.999   12.000        False        False
6   bar    NaN   8.000   12.000        False        False
7  note   76.0   9.000    9.001        False        False
8   bar    NaN  12.000   16.000        False        False
>>> split_notes_at_barlines(df)
    type  pitch   onset  release  tie_to_next  tie_to_prev
0    bar    NaN   0.000    4.000        False        False
1   note   60.0   0.000    4.000        False        False
2   note   64.0   0.000    4.000         True        False
3   note   67.0   0.000    4.000         True        False
4    bar    NaN   4.000    8.000        False        False
5   note   64.0   4.000    4.001        False         True
6   note   67.0   4.000    8.000         True         True
7   note   72.0   7.999    8.000         True        False
8    bar    NaN   8.000   12.000        False        False
9   note   67.0   8.000   12.000        False         True
10  note   72.0   8.000   12.000        False         True
11  note   76.0   9.000    9.001        False        False
12   bar    NaN  12.000   16.000        False        False
>>> split_notes_at_barlines(df, min_overhang_dur=0.025)
    type  pitch  onset  release  tie_to_next  tie_to_prev
0    bar    NaN    0.0    4.000        False        False
1   note   60.0    0.0    4.000        False        False
2   note   64.0    0.0    4.000        False        False
3   note   67.0    0.0    4.000         True        False
4    bar    NaN    4.0    8.000        False        False
5   note   67.0    4.0    8.000         True         True
6    bar    NaN    8.0   12.000        False        False
7   note   67.0    8.0   12.000        False         True
8   note   72.0    8.0   12.000        False        False
9   note   76.0    9.0    9.001        False        False
10   bar    NaN   12.0   16.000        False        False
def subdivide_notes(df: pandas.DataFrame, grid_size, onset_col='onset', release_col='release')
Expand source code
def subdivide_notes(
    df: pd.DataFrame, grid_size, onset_col="onset", release_col="release"
):
    """
    Subdivides notes into smaller intervals of size grid_size.

    Parameters:
        df: pandas DataFrame with onset and release times
        grid_size: size of subdivisions
        onset_col: name of onset column
        release_col: name of release column

    Returns:
        DataFrame with subdivided intervals


    >>> csv_table = '''
    ... type,pitch,onset,release
    ... bar,,0.0,4.0
    ... note,60,0.0,4.0
    ... '''
    >>> df = pd.read_csv(io.StringIO(csv_table.strip()))
    >>> subdivide_notes(df, 1.0)
       type  pitch  onset  release
    0   bar    NaN    0.0      4.0
    1  note   60.0    0.0      1.0
    2  note   60.0    1.0      2.0
    3  note   60.0    2.0      3.0
    4  note   60.0    3.0      4.0
    """
    new_rows = []

    for _, row in df.iterrows():
        if row["type"] != "note":
            new_rows.append(row)
            continue

        # Get start and end times
        start = row[onset_col]
        end = row[release_col]

        # Find the first interval boundary after start
        first_boundary = np.ceil(start / grid_size) * grid_size

        # Generate all interval boundaries
        boundaries = np.arange(first_boundary, end, grid_size)

        # Create subdivided intervals
        if len(boundaries) == 0:
            # Case where interval is smaller than grid_size
            new_rows.append(pd.Series({**row, onset_col: start, release_col: end}))
        else:
            # First interval (from start to first boundary)
            if start < boundaries[0]:
                new_rows.append(
                    pd.Series(
                        {**row, onset_col: start, release_col: min(boundaries[0], end)}
                    )
                )

            # Middle intervals
            for i in range(len(boundaries)):
                if boundaries[i] >= end:
                    break

                new_rows.append(
                    pd.Series(
                        {
                            **row,
                            onset_col: boundaries[i],
                            release_col: min(boundaries[i] + grid_size, end),
                        },
                    )
                )

    return pd.DataFrame(new_rows).reset_index(drop=True)

Subdivides notes into smaller intervals of size grid_size.

Parameters

df: pandas DataFrame with onset and release times grid_size: size of subdivisions onset_col: name of onset column release_col: name of release column

Returns

DataFrame with subdivided intervals

>>> csv_table = '''
... type,pitch,onset,release
... bar,,0.0,4.0
... note,60,0.0,4.0
... '''
>>> df = pd.read_csv(io.StringIO(csv_table.strip()))
>>> subdivide_notes(df, 1.0)
   type  pitch  onset  release
0   bar    NaN    0.0      4.0
1  note   60.0    0.0      1.0
2  note   60.0    1.0      2.0
3  note   60.0    2.0      3.0
4  note   60.0    3.0      4.0