Beispiel #1
0
    def merge_tracks(
        self,
        track_indices=None,
        mode="sum",
        program=0,
        is_drum=False,
        name="merged",
        remove_merged=False,
    ):
        """
        Merge pianorolls of the tracks specified by `track_indices`. The merged
        track will have program number as given by `program` and drum indicator
        as given by `is_drum`. The merged track will be appended at the end of
        the track list.

        Parameters
        ----------
        track_indices : list
            The indices of tracks to be merged. Defaults to all the tracks.
        mode : {'sum', 'max', 'any'}
            A string that indicates the merging strategy to apply along the
            track axis. Default to 'sum'.

            - In 'sum' mode, the merged pianoroll is the sum of the collected
              pianorolls. Note that for binarized pianorolls, integer summation
              is performed.
            - In 'max' mode, for each pixel, the maximum value among the
              collected pianorolls is assigned to the merged pianoroll.
            - In 'any' mode, the value of a pixel in the merged pianoroll is
              True if any of the collected pianorolls has nonzero value at that
              pixel; False if all the collected pianorolls are inactive
              (zero-valued) at that pixel.

        program: int
            A program number according to General MIDI specification [1].
            Available values are 0 to 127. Defaults to 0 (Acoustic Grand Piano).
        is_drum : bool
            A boolean number that indicates whether it is a percussion track.
            Defaults to False.
        name : str
            A name to be assigned to the merged track. Defaults to 'merged'.
        remove_merged : bool
            True to remove the source tracks from the track list. False to keep
            them. Defaults to False.

        References
        ----------
        [1] https://www.midi.org/specifications/item/gm-level-1-sound-set

        """
        if mode not in ("max", "sum", "any"):
            raise ValueError("`mode` must be one of {'max', 'sum', 'any'}.")

        merged = self[track_indices].get_merged_pianoroll(mode)

        merged_track = Track(merged, program, is_drum, name)
        self.append_track(merged_track)

        if remove_merged:
            self.remove_tracks(track_indices)
Beispiel #2
0
    def load(self, filename):
        """
        Load a npz file. Supports only files previously saved by
        :meth:`pypianoroll.Multitrack.save`.

        Notes
        -----
        Attribute values will all be overwritten.

        Parameters
        ----------
        filename : str
            The name of the npz file to be loaded.

        """
        def reconstruct_sparse(target_dict, name):
            """Return a reconstructed instance of `scipy.sparse.csc_matrix`."""
            return csc_matrix(
                (
                    target_dict[name + "_csc_data"],
                    target_dict[name + "_csc_indices"],
                    target_dict[name + "_csc_indptr"],
                ),
                shape=target_dict[name + "_csc_shape"],
            ).toarray()

        with np.load(filename) as loaded:
            if "info.json" not in loaded:
                raise ValueError("Cannot find 'info.json' in the npz file.")
            info_dict = json.loads(loaded["info.json"].decode("utf-8"))
            self.name = info_dict["name"]
            self.beat_resolution = info_dict["beat_resolution"]

            self.tempo = loaded["tempo"]
            if "downbeat" in loaded.files:
                self.downbeat = loaded["downbeat"]
            else:
                self.downbeat = None

            idx = 0
            self.tracks = []
            while str(idx) in info_dict:
                pianoroll = reconstruct_sparse(loaded,
                                               "pianoroll_{}".format(idx))
                track = Track(
                    pianoroll,
                    info_dict[str(idx)]["program"],
                    info_dict[str(idx)]["is_drum"],
                    info_dict[str(idx)]["name"],
                )
                self.tracks.append(track)
                idx += 1

        self.check_validity()
Beispiel #3
0
    def append_track(self,
                     track=None,
                     pianoroll=None,
                     program=0,
                     is_drum=False,
                     name='unknown'):
        """
        Append a multitrack.Track instance to the track list or create a new
        multitrack.Track object and append it to the track list.

        Parameters
        ----------
        track : pianoroll.Track
            A :class:`pypianoroll.Track` instance to be appended to the track
            list.
        pianoroll : np.ndarray, shape=(n_time_steps, 128)
            A pianoroll matrix. The first and second dimension represents time
            and pitch, respectively. Available datatypes are bool, int and
            float. Only effective when `track` is None.
        program: int
            A program number according to General MIDI specification [1].
            Available values are 0 to 127. Defaults to 0 (Acoustic Grand Piano).
            Only effective when `track` is None.
        is_drum : bool
            A boolean number that indicates whether it is a percussion track.
            Defaults to False. Only effective when `track` is None.
        name : str
            The name of the track. Defaults to 'unknown'. Only effective when
            `track` is None.

        References
        ----------
        [1] https://www.midi.org/specifications/item/gm-level-1-sound-set

        """
        if track is not None:
            if not isinstance(track, Track):
                raise TypeError(
                    "`track` must be a pypianoroll.Track instance.")
            track.check_validity()
        else:
            track = Track(pianoroll, program, is_drum, name)
        self.tracks.append(track)
Beispiel #4
0
    def parse_pretty_midi(self,
                          pm,
                          mode='max',
                          algorithm='normal',
                          binarized=False,
                          skip_empty_tracks=True,
                          collect_onsets_only=False,
                          threshold=0,
                          first_beat_time=None):
        """
        Parse a :class:`pretty_midi.PrettyMIDI` object. The data type of the
        resulting pianorolls is automatically determined (int if 'mode' is
        'sum', np.uint8 if `mode` is 'max' and `binarized` is False, bool if
        `mode` is 'max' and `binarized` is True).

        Parameters
        ----------
        pm : `pretty_midi.PrettyMIDI` object
            A :class:`pretty_midi.PrettyMIDI` object to be parsed.
        mode : {'max', 'sum'}
            A string that indicates the merging strategy to apply to duplicate
            notes. Default to 'max'.
        algorithm : {'normal', 'strict', 'custom'}
            A string that indicates the method used to get the location of the
            first beat. Notes before it will be dropped unless an incomplete
            beat before it is found (see Notes for more information). Defaults
            to 'normal'.

            - The 'normal' algorithm estimates the location of the first beat by
              :meth:`pretty_midi.PrettyMIDI.estimate_beat_start`.
            - The 'strict' algorithm sets the first beat at the event time of
              the first time signature change. Raise a ValueError if no time
              signature change event is found.
            - The 'custom' algorithm takes argument `first_beat_time` as the
              location of the first beat.

        binarized : bool
            True to binarize the parsed pianorolls before merging duplicate
            notes. False to use the original parsed pianorolls. Defaults to
            False.
        skip_empty_tracks : bool
            True to remove tracks with empty pianorolls and compress the pitch
            range of the parsed pianorolls. False to retain the empty tracks
            and use the original parsed pianorolls. Deafault to True.
        collect_onsets_only : bool
            True to collect only the onset of the notes (i.e. note on events) in
            all tracks, where the note off and duration information are dropped.
            False to parse regular pianorolls.
        threshold : int or float
            A threshold used to binarize the parsed pianorolls. Only effective
            when `binarized` is True. Defaults to zero.
        first_beat_time : float
            The location (in sec) of the first beat. Required and only effective
            when using 'custom' algorithm.

        Notes
        -----
        If an incomplete beat before the first beat is found, an additional beat
        will be added before the (estimated) beat starting time. However, notes
        before the (estimated) beat starting time for more than one beat are
        dropped.

        """
        algorithm = 'custom'
        first_beat_time = 0

        if mode not in ('max', 'sum'):
            raise ValueError("`mode` must be one of {'max', 'sum'}.")
        if algorithm not in ('strict', 'normal', 'custom'):
            raise ValueError("`algorithm` must be one of {'normal', 'strict', "
                             " 'custom'}.")
        if algorithm == 'custom':
            if not isinstance(first_beat_time, (int, float)):
                raise TypeError("`first_beat_time` must be int or float when "
                                "using 'custom' algorithm.")
            if first_beat_time < 0.0:
                raise ValueError("`first_beat_time` must be a positive number "
                                 "when using 'custom' algorithm.")

        # Set first_beat_time for 'normal' and 'strict' modes
        if algorithm == 'normal':
            if pm.time_signature_changes:
                pm.time_signature_changes.sort(key=lambda x: x.time)
                first_beat_time = pm.time_signature_changes[0].time
            else:
                first_beat_time = pm.estimate_beat_start()
        elif algorithm == 'strict':
            if not pm.time_signature_changes:
                raise ValueError(
                    "No time signature change event found. Unable "
                    "to set beat start time using 'strict' "
                    "algorithm.")
            pm.time_signature_changes.sort(key=lambda x: x.time)
            first_beat_time = pm.time_signature_changes[0].time

        # get tempo change event times and contents
        tc_times, tempi = pm.get_tempo_changes()
        arg_sorted = np.argsort(tc_times)
        tc_times = tc_times[arg_sorted]
        tempi = tempi[arg_sorted]

        beat_times = pm.get_beats(first_beat_time)
        if not len(beat_times):
            raise ValueError("Cannot get beat timings to quantize pianoroll.")
        beat_times.sort()

        n_beats = len(beat_times)
        n_time_steps = self.beat_resolution * n_beats

        # Parse downbeat array
        if not pm.time_signature_changes:
            self.downbeat = None
        else:
            self.downbeat = np.zeros((n_time_steps, ), bool)
            self.downbeat[0] = True
            start = 0
            end = start
            for idx, tsc in enumerate(pm.time_signature_changes[:-1]):
                end += np.searchsorted(beat_times[end:],
                                       pm.time_signature_changes[idx + 1].time)
                start_idx = start * self.beat_resolution
                end_idx = end * self.beat_resolution
                stride = tsc.numerator * self.beat_resolution
                self.downbeat[start_idx:end_idx:stride] = True
                start = end

        # Build tempo array
        one_more_beat = 2 * beat_times[-1] - beat_times[-2]
        beat_times_one_more = np.append(beat_times, one_more_beat)
        bpm = 60. / np.diff(beat_times_one_more)
        self.tempo = np.tile(bpm, (1, 24)).reshape(-1, )

        # Parse pianoroll
        self.tracks = []
        for instrument in pm.instruments:
            if binarized:
                pianoroll = np.zeros((n_time_steps, 128), bool)
            elif mode == 'max':
                pianoroll = np.zeros((n_time_steps, 128), np.uint8)
            else:
                pianoroll = np.zeros((n_time_steps, 128), int)

            pitches = np.array([
                note.pitch for note in instrument.notes
                if note.end > first_beat_time
            ])
            note_on_times = np.array([
                note.start for note in instrument.notes
                if note.end > first_beat_time
            ])
            beat_indices = np.searchsorted(beat_times, note_on_times) - 1
            remained = note_on_times - beat_times[beat_indices]
            ratios = remained / (beat_times_one_more[beat_indices + 1] -
                                 beat_times[beat_indices])
            rounded = np.round((beat_indices + ratios) * self.beat_resolution)
            note_ons = rounded.astype(int)

            if collect_onsets_only:
                pianoroll[note_ons, pitches] = True
            elif instrument.is_drum:
                if binarized:
                    pianoroll[note_ons, pitches] = True
                else:
                    velocities = [
                        note.velocity for note in instrument.notes
                        if note.end > first_beat_time
                    ]
                    pianoroll[note_ons, pitches] = velocities
            else:
                note_off_times = np.array([
                    note.end for note in instrument.notes
                    if note.end > first_beat_time
                ])
                beat_indices = np.searchsorted(beat_times, note_off_times) - 1
                remained = note_off_times - beat_times[beat_indices]
                ratios = remained / (beat_times_one_more[beat_indices + 1] -
                                     beat_times[beat_indices])
                note_offs = ((beat_indices + ratios) *
                             self.beat_resolution).astype(int)

                for idx, start in enumerate(note_ons):
                    end = note_offs[idx]
                    velocity = instrument.notes[idx].velocity

                    if velocity < 1:
                        continue
                    if binarized and velocity <= threshold:
                        continue

                    if start > 0 and start < n_time_steps:
                        if pianoroll[start - 1, pitches[idx]]:
                            pianoroll[start - 1, pitches[idx]] = 0
                    if end < n_time_steps - 1:
                        if pianoroll[end, pitches[idx]]:
                            end -= 1

                    if binarized:
                        if mode == 'sum':
                            pianoroll[start:end, pitches[idx]] += 1
                        elif mode == 'max':
                            pianoroll[start:end, pitches[idx]] = True
                    elif mode == 'sum':
                        pianoroll[start:end, pitches[idx]] += velocity
                    elif mode == 'max':
                        maximum = np.maximum(
                            pianoroll[start:end, pitches[idx]], velocity)
                        pianoroll[start:end, pitches[idx]] = maximum

            if skip_empty_tracks and not np.any(pianoroll):
                continue

            track = Track(pianoroll, int(instrument.program),
                          instrument.is_drum, instrument.name)
            self.tracks.append(track)

        self.check_validity()
Beispiel #5
0
    def __init__(self,
                 filename=None,
                 tracks=None,
                 tempo=120.0,
                 downbeat=None,
                 beat_resolution=24,
                 name='unknown'):
        """
        Initialize the object by one of the following ways:
        - assigning values for attributes
        - loading a npz file
        - parsing a MIDI file

        Notes
        -----
        When `filename` is given, the arguments `tracks`, `tempo`, `downbeat`
        and `name` are ignored.

        Parameters
        ----------
        filename : str
            The file path to the npz file to be loaded or the MIDI file (.mid,
            .midi, .MID, .MIDI) to be parsed.
        beat_resolution : int
            The number of time steps used to represent a beat. Will be assigned
            to `beat_resolution` when `filename` is not provided. Defaults to
            24.
        tracks : list of :class:`pypianoroll.Track` objects
            The track object list to be assigned to `tracks` when `filename` is
            not provided.
        tempo : int or np.ndarray, shape=(n_time_steps,), dtype=float
            An array that indicates the tempo value (in bpm) at each time step.
            The length is the total number of time steps. Will be assigned to
            `tempo` when `filename` is not provided.
        downbeat : list or np.ndarray, shape=(n_time_steps,), dtype=bool
            An array that indicates whether the time step contains a downbeat
            (i.e., the first time step of a bar). The length is the total number
            of time steps.  Will be assigned to `downbeat` when `filename` is
            not given. If a list of indices is provided, it will be viewed as
            the time step indices of the down beats and converted to a numpy
            array. Default is None.
        name : str
            The name of the multitrack pianoroll. Defaults to 'unknown'.

        """
        # parse input file
        if filename is not None:
            if filename.endswith(('.mid', '.midi', '.MID', '.MIDI')):
                self.beat_resolution = beat_resolution
                self.name = name
                self.parse_midi(filename)
            elif filename.endswith('.npz'):
                self.load(filename)
            else:
                raise ValueError(
                    "Unsupported file type received. Expect a npz "
                    "or MIDI file.")
        else:
            if tracks is not None:
                self.tracks = tracks
            else:
                self.tracks = [Track()]
            if isinstance(tempo, (int, float)):
                self.tempo = np.array([tempo])
            else:
                self.tempo = tempo
            if isinstance(downbeat, list):
                self.downbeat = np.zeros((max(downbeat) + 1, ), bool)
                self.downbeat[downbeat] = True
            else:
                self.downbeat = downbeat
            self.beat_resolution = beat_resolution
            self.name = name
            self.check_validity()