Source code for partitura.musicanalysis.note_array_to_score

from partitura.score import ScoreLike, Part
from partitura.utils import (
    estimate_symbolic_duration,
    estimate_clef_properties,
    key_name_to_fifths_mode,
    fifths_mode_to_key_name,
)
import warnings
import numpy as np
from typing import Union
import numpy.lib.recfunctions as rfn
from fractions import Fraction
import partitura.musicanalysis as analysis
import partitura.score as score


def create_divs_from_beats(note_array: np.ndarray):
    """
    Append onset_div and duration_div fields to the note array.
    Assumes beats are in uniform units across the whole array
    (no time signature change that modifies beat unit, e.g., 4/4 to 6/8).

    This function may result in an error if time signature changes that affect the ratio of beat/div are present.
    Parameters
    ----------
    note_array: np.ndarray
        The note array to which the divs fields will be added.
        Normally only beat onset and duration are provided.

    Returns
    -------
    note_array: np.ndarray
        The note array with the divs fields added.
    divs: int
        the divs per beat

    """
    duration_fractions = [
        Fraction(float(ix)).limit_denominator(256) for ix in note_array["duration_beat"]
    ]
    onset_fractions = [
        Fraction(float(ix)).limit_denominator(256) for ix in note_array["onset_beat"]
    ]
    divs = np.lcm.reduce(
        [
            Fraction(float(ix)).limit_denominator(256).denominator
            for ix in np.unique(note_array["duration_beat"])
        ]
    )
    onset_divs = list(
        map(lambda r: int(divs * r.numerator / r.denominator), onset_fractions)
    )
    min_onset_div = min(onset_divs)
    if min_onset_div < 0:
        onset_divs = list(map(lambda x: x - min_onset_div, onset_divs))
    duration_divs = list(
        map(lambda r: int(divs * r.numerator / r.denominator), duration_fractions)
    )
    na_divs = np.array(
        list(zip(onset_divs, duration_divs)),
        dtype=[("onset_div", int), ("duration_div", int)],
    )
    return rfn.merge_arrays((note_array, na_divs), flatten=True, usemask=False), divs


def create_beats_from_divs(note_array: np.ndarray, divs: int):
    """
    Append onset_beats and duration_beasts fields to the note array.
    Returns beats in quarters.

    Parameters
    ----------
    note_array: np.ndarray
        The note array to which the divs fields will be added.
        Normally only beat onset and duration are provided.
    divs: int
        Divs/ticks per quarter note.

    Returns
    -------
    note_array: np.ndarray
        The note array with the divs fields added.

    """
    onset_beats = list(note_array["onset_div"] / divs)
    duration_beats = list(note_array["duration_div"] / divs)
    na_beats = np.array(
        list(zip(onset_beats, duration_beats)),
        dtype=[("onset_beat", float), ("duration_beat", float)],
    )
    return rfn.merge_arrays((note_array, na_beats), flatten=True, usemask=False)


def create_part(
    ticks: int,
    note_array: np.ndarray,
    key_sigs: list = None,
    time_sigs: list = None,
    part_id: str = None,
    part_name: str = None,
    sanitize: bool = True,
    anacrusis_divs: int = 0,
    barebones: bool = False,
):
    """
    Create a part from a note array and a list of key signatures.

    Parameters
    ----------
    ticks: int
        The number of ticks per quarter note for the part creation.
    note_array: np.ndarray
        The note array from which the part will be created.
    key_sigs: list (optional)
        A list of key signatures. Each key signature is a tuple of the form (onset, key_name, offset).
    time_sigs: list (optional)
        A list of time signatures. Each time signature is a tuple of the form (onset, ts_num, ts_den, offset).
    part_id: str (optional)
        The id of the part.
    part_name: str (optional)
        The name of the part
    sanitize: bool (optional)
        If True, then measures, tied-notes and triplets will be sanitized.
    anacrusis_divs: int (optional)
        The number of divisions in the anacrusis. If 0, then there is no anacrusis measure.
    barebones: bool (optional)
        Returns a part with only notes, no measures

    Returns
    -------
    part : partitura.score.Part
        The part created from the note array and key signatures.
    """

    warnings.warn("create_part", stacklevel=2)

    part = Part(
        part_id,
        part_name=part_name,
    )
    part.set_quarter_duration(0, ticks)

    clef = score.Clef(staff=1, **estimate_clef_properties(note_array["pitch"]))
    part.add(clef, 0)

    # key sig
    if key_sigs is not None:
        for t_start, name, t_end in key_sigs:
            fifths, mode = key_name_to_fifths_mode(name)
            t_start, t_end = int(t_start), int(t_end)
            part.add(score.KeySignature(fifths, mode), t_start, t_end)
    else:
        warnings.warn("No key signatures added")

    # time sig
    if time_sigs is not None:
        for ts_start, num, den, ts_end in time_sigs:
            time_sig = score.TimeSignature(num.item(), den.item())
            part.add(time_sig, ts_start, ts_end)
    else:
        warnings.warn("No time signatures added")
        # without time signature, no measures
        barebones = True

    warnings.warn("add notes", stacklevel=2)
    # add the notes
    for n in note_array:
        if n["duration_div"] > 0:
            note = score.Note(
                step=n["step"],
                octave=n["octave"],
                alter=n["alter"],
                voice=int(n["voice"] or 0),
                id=n["id"],
                symbolic_duration=estimate_symbolic_duration(n["duration_div"], ticks),
            )
        else:
            note = score.GraceNote(
                grace_type="appoggiatura",
                step=n["step"],
                octave=n["octave"],
                alter=n["alter"],
                voice=int(n["voice"] or 0),
                id=n["id"],
                symbolic_duration=dict(type="quarter"),
            )

        part.add(note, n["onset_div"], n["onset_div"] + n["duration_div"])

    warnings.warn("add measures", stacklevel=2)

    if not barebones and anacrusis_divs > 0:
        part.add(score.Measure(0), 0, anacrusis_divs)

    if not barebones and sanitize:
        warnings.warn("Inferring measures", stacklevel=2)
        score.add_measures(part)

        warnings.warn("Find and tie notes", stacklevel=2)
        # tie notes where necessary (across measure boundaries, and within measures
        # notes with compound duration)
        score.tie_notes(part)

        warnings.warn("find and ensure tuplets", stacklevel=2)
        # apply simplistic tuplet finding heuristic
        score.find_tuplets(part)

        # clean up
        score.sanitize_part(part)

    warnings.warn("done create_part", stacklevel=2)
    return part


[docs]def note_array_to_score( note_array: Union[np.ndarray, list], name_id: str = "", divs: int = None, key_sigs: list = None, time_sigs: list = None, part_name: str = "", assign_note_ids: bool = True, estimate_key: bool = False, estimate_time: bool = False, sanitize: bool = True, return_part: bool = False, ) -> ScoreLike: """ A generic function to transform an enriched note_array to part or Score. The function can be used for many different occasions, i.e. part_from_graph, part from note_array, part from midi score import, etc. This function requires a note array that contains time signatures and key signatures (optional - can also estimate it automatically). Note array should contain the following fields: - onset_div or onset_beat - duration_div or duration_beat - pitch For time signature and key signature the arguments are processed in the following hierarchy: Key sig: (["key_fifths", "key_mode"] fields) overrides (key_sigs list) overrides (estimate_key bool) Time sig: (["ts_beats", "ts_beat_type"] fields) overrides (time_sigs list) overrides (estimate_time bool) If either times in divs or beats are missing, these cases are assumed: Only divs: divs/ticks need to be specified, beats are computed as quarters (not relative to time signature). Only beats: divs/ticks as well as times in divs are computed assuming the beat times are given in quarters. This function thus handles the following cases: 1) note_array fields ["onset_beat", "duration_beat", "pitch"] -> barebones part, divs estimated assuming uniform beats in quarters + estimate_time -> 4/4 time signature + estimate_key -> barebones + estimate key signature + time_sigs -> time signatures are added, times assumed in quarters (possible error against div/beat) + key_sigs -> key signatures are added, times assumed in quarters + ["ts_beats", "ts_beat_type"] -> time signatures are added (possible error against div/beat) + ["key_fifths", "key_mode"] -> key signatures are added 2) note_array fields ["onset_div", "duration_div", "pitch"] -> barebones part, uniform beats in quarters estimated from beats + estimate_time -> 4/4 time signature + estimate_key -> barebones + estimate key signature + time_sigs -> time signatures are added, times assumed in divs (possible error against div/beat) + key_sigs -> key signatures are added, times assumed in divs + ["ts_beats", "ts_beat_type"] -> time signatures are added (possible error against div/beat) + ["key_fifths", "key_mode"] -> key signatures are added 3) note_array fields ["onset_div", "duration_div", "onset_beat", "duration_beat", "pitch"] -> barebones part + estimate_time -> 4/4 time signature (possible error against div/beat) + estimate_key -> barebones + estimate key signature + time_sigs -> time signatures are added, times assumed in divs (possible error against div/beat) + key_sigs -> key signatures are added, times assumed in divs + ["ts_beats", "ts_beat_type"] -> time signatures are added (possible error against div/beat) + ["key_fifths", "key_mode"] -> key signatures are added Parameters ---------- note_array : structure array or list of structured arrays. A note array with the following fields: - onset_div or onset_beat - duration_div or duration_beat - pitch - ts_beats (optional) - ts_beat_type (optional) - key_mode(optional) - key_fifths(optional) - id (optional) divs : int (optional) Divs/ticks per quarter note. If not given, it is estimated assuming a beats in quarters. key_sigs: list (optional) A list of key signatures. Each key signature is a tuple of the form (onset, key_name, offset). Overridden by note_array fields "key_mode" and "key_fifths". Overrides estimate_key. time_sigs: list (optional) A list of time signatures. Each time signature is a tuple of the form (onset, ts_num, ts_den, offset). Overridden by note_array fields "key_mode" and "key_fifths". Overrides estimate_time. estimate_key: bool (optional) Estimate a single key signature. estimate_time: bool (optional) Add a default time signature. assign_note_ids: bool (optional) Assign note_ids. sanitize: bool (optional) sanitize the part by adding measures, tying notes, and finding tuplets. return_part: bool (optional) Return a Partitura part object instead of a score. Returns ------- part or score : Part or Score a Part object or a Score object, depending on return_part. """ if isinstance(note_array, list): parts = [ note_array_to_score( note_array=x, name_id=str(i), assign_note_ids=assign_note_ids, return_part=True, divs=divs, estimate_key=estimate_key, sanitize=sanitize, part_name=name_id + "_P" + str(i), ) for i, x in enumerate(note_array) ] return score.Score(partlist=parts) # Input validation if not isinstance(note_array, np.ndarray): raise TypeError("The note array does not have the correct format.") if len(note_array) == 0: raise ValueError("The note array is empty.") dtypes = note_array.dtype.names ts_case = ["ts_beats", "ts_beat_type"] ks_case = ["key_fifths", "key_mode"] case1 = ["onset_beat", "duration_beat", "pitch"] case1_ex = ["onset_div", "duration_div"] case2 = ["onset_div", "duration_div", "pitch"] case2_ex = ["onset_beat", "duration_beat"] # case3 = ["onset_div", "duration_div", "onset_beat", "duration_beat", "pitch"] if not (all([x in dtypes for x in case1]) or all([x in dtypes for x in case2])): raise ValueError("not all necessary note array fields are available") # sort the array onset_time = "onset_div" duration_time = "duration_div" if all([x not in dtypes for x in case1_ex]): onset_time = "onset_beat" duration_time = "duration_beat" # Order Lexicographically sort_idx = np.lexsort( (note_array[duration_time], note_array["pitch"], note_array[onset_time]) ) note_array = note_array[sort_idx] # case 1, estimate divs if all([x in dtypes for x in case1] and [x not in dtypes for x in case1_ex]): # estimate onset_divs and duration_divs, assumes all beat times as quarters note_array, divs_ = create_divs_from_beats(note_array) if divs is not None and divs != divs_: raise ValueError("estimated divs don't correspond to input divs") else: divs = divs_ # case 1: convert key sig times to divs if key_sigs is not None: key_sigs = np.array(key_sigs) if key_sigs.shape[1] == 2: key_sigs[:, 0] = (key_sigs[:, 0] / divs).astype(int) elif key_sigs.shape[1] == 3: key_sigs[:, 0] = (key_sigs[:, 0] / divs).astype(int) key_sigs[:, 2] = (key_sigs[:, 2] / divs).astype(int) else: raise ValueError("key_sigs is given in a wrong format") # case 1: convert time sig times to divs if time_sigs is not None: time_sigs = np.array(time_sigs) if time_sigs.shape[1] == 3: time_sigs[:, 0] = (time_sigs[:, 0] / divs).astype(int) elif time_sigs.shape[1] == 4: time_sigs[:, 0] = (time_sigs[:, 0] / divs).astype(int) time_sigs[:, 3] = (time_sigs[:, 3] / divs).astype(int) else: raise ValueError("time_sigs is given in a wrong format") # case 2, estimate beats if all([x in dtypes for x in case2] and [x not in dtypes for x in case2_ex]): # estimate onset_beats and duration_beats in quarters if divs is None: raise ValueError("Divs/ticks need to be specified") else: note_array = create_beats_from_divs(note_array, divs) if divs is None: # find first note with nonzero duration (in case score starts with grace_note). for idx, dur in enumerate(note_array["duration_beat"]): if dur != 0: break if all([x in dtypes for x in ts_case]): divs = int( (note_array[idx]["duration_div"] / note_array[idx]["duration_beat"]) / (4 / note_array[idx]["ts_beat_type"]) ) else: divs = int( note_array[idx]["duration_div"] / note_array[idx]["duration_beat"] ) # Test Note array for negative durations if not np.all(note_array["duration_div"] >= 0): raise ValueError("Note array contains negative durations.") if not np.all(note_array["duration_beat"] >= 0): raise ValueError("Note array contains negative durations.") # Test for negative divs if not np.all(note_array["onset_div"] >= 0): raise ValueError("Negative divs found in note_array.") # handle time signatures if all([x in dtypes for x in ts_case]): time_sigs = [[0, note_array[0]["ts_beats"], note_array[0]["ts_beat_type"]]] for n in note_array: if ( n["ts_beats"] != time_sigs[-1][1] or n["ts_beat_type"] != time_sigs[-1][2] ): time_sigs.append([n["onset_div"], n["ts_beats"], n["ts_beat_type"]]) global_time_sigs = np.array(time_sigs) elif time_sigs is not None: global_time_sigs = time_sigs elif estimate_time: global_time_sigs = [[0, 4, 4]] else: global_time_sigs = None if global_time_sigs is not None: global_time_sigs = np.array(global_time_sigs) if global_time_sigs.shape[1] == 3: # for convenience, we add the end times for each time signature ts_end_times = np.r_[ global_time_sigs[1:, 0], np.max(note_array["onset_div"] + note_array["duration_div"]), ] global_time_sigs = np.column_stack((global_time_sigs, ts_end_times)) elif global_time_sigs.shape[1] == 4: pass else: raise ValueError("time_sigs is given in a wrong format") # make sure there is a time signature from the beginning global_time_sigs[0, 0] = 0 # Note id creation or re-assignment if "id" not in dtypes: note_ids = ["{}n{:4d}".format(name_id, i) for i in range(len(note_array))] note_array = rfn.append_fields( note_array, "id", np.array(note_ids, dtype="<U256") ) elif assign_note_ids or np.all(note_array["id"] == note_array["id"][0]): note_ids = ["{}n{:4d}".format(name_id, i) for i in range(len(note_array))] note_array["id"] = np.array(note_ids) # estimate voice if "voice" in dtypes: estimate_voice_info = False part_voice_list = note_array["voice"] else: estimate_voice_info = True part_voice_list = np.full(len(note_array), np.inf) if estimate_voice_info: warnings.warn("voice estimation", stacklevel=2) # TODO: deal with zero duration notes in note_array. # Zero duration notes are currently deleted estimated_voices = analysis.estimate_voices(note_array) assert len(part_voice_list) == len(estimated_voices) for i, (part_voice, voice_est) in enumerate( zip(part_voice_list, estimated_voices) ): # Not sure if correct. if part_voice != np.inf: estimated_voices[i] = part_voice note_array = rfn.append_fields( note_array, "voice", np.array(estimated_voices, dtype=int) ) # estimate pitch spelling if not all(x in dtypes for x in ["step", "alter", "octave"]): warnings.warn("pitch spelling") spelling_global = analysis.estimate_spelling(note_array) note_array = rfn.merge_arrays((note_array, spelling_global), flatten=True) # handle or estimate key signature if all([x in dtypes for x in ks_case]): global_key_sigs = [ [ 0, fifths_mode_to_key_name( note_array[0]["ks_fifths"], note_array[0]["ks_mode"] ), ] ] for n in note_array: global_key_sigs.append( [n["onset_div"], fifths_mode_to_key_name(n["ks_fifths"], n["ks_mode"])] ) else: global_key_sigs = key_sigs elif key_sigs is not None: global_key_sigs = key_sigs elif estimate_key: k_name = analysis.estimate_key(note_array) global_key_sigs = [[0, k_name]] else: global_key_sigs = None if global_key_sigs is not None: global_key_sigs = np.array(global_key_sigs) if global_key_sigs.shape[1] == 2: # for convenience, we add the end times for each time signature ks_end_times = np.r_[ global_key_sigs[1:, 0], np.max(note_array["onset_div"] + note_array["duration_div"]), ] global_key_sigs = np.column_stack((global_key_sigs, ks_end_times)) elif global_key_sigs.shape[1] == 3: pass else: raise ValueError("key_sigs is given in a wrong format") # make sure there is a key signature from the beginning global_key_sigs[0, 0] = 0 # Steps for dealing with anacrusis measure. anacrusis_mask = np.zeros(len(note_array), dtype=bool) anacrusis_mask[note_array["onset_beat"] < 0] = True if np.all(anacrusis_mask == False): anacrusis_divs = 0 else: last_neg_beat = np.max(note_array[anacrusis_mask]["onset_beat"]) last_neg_divs = np.max(note_array[anacrusis_mask]["onset_div"]) if all([x in dtypes for x in ts_case]): beat_type = np.max(note_array[anacrusis_mask]["ts_beat_type"]) else: beat_type = 4 difference_from_zero = (0 - last_neg_beat) * divs * (4 / beat_type) anacrusis_divs = int(last_neg_divs + difference_from_zero) # Create the part part = create_part( ticks=divs, note_array=note_array, key_sigs=global_key_sigs, time_sigs=global_time_sigs, part_id=name_id, part_name=part_name, sanitize=sanitize, anacrusis_divs=anacrusis_divs, ) # Return Part or Score if return_part: return part else: return score.Score(partlist=[part], id=name_id)