Skip to content

Classes and functions used for visualization

DualPianoRoll dataclass

Bases: PianoRoll

Extends the PianoRoll class to represent a dual-layer piano roll visualization.

The DualPianoRoll class enhances the basic piano roll visualization by allowing for the representation of additional information, such as masking in machine learning contexts, through the use of dual color mapping.

Attributes:

Name Type Description
base_cmap Union[str, ListedColormap]

The colormap for the base layer of the piano roll.

marked_cmap Union[str, ListedColormap]

The colormap for the marked layer of the piano roll.

mark_key str

The key used to determine markings in the MIDI data.

Methods:

Name Description
__post_init__

Initializes the dual-layer piano roll with specified colormaps.

_build_image

Builds the dual-layer piano roll image from the MIDI data, applying color mappings.

Source code in fortepyan/view/pianoroll/structures.py
@dataclass
class DualPianoRoll(PianoRoll):
    """
    Extends the PianoRoll class to represent a dual-layer piano roll visualization.

    The DualPianoRoll class enhances the basic piano roll visualization by allowing for the
    representation of additional information, such as masking in machine learning contexts, through
    the use of dual color mapping.

    Attributes:
        base_cmap (Union[str, ListedColormap]): The colormap for the base layer of the piano roll.
        marked_cmap (Union[str, ListedColormap]): The colormap for the marked layer of the piano roll.
        mark_key (str): The key used to determine markings in the MIDI data.

    Methods:
        __post_init__(): Initializes the dual-layer piano roll with specified colormaps.
        _build_image(): Builds the dual-layer piano roll image from the MIDI data, applying color mappings.
    """

    base_cmap: Union[str, ListedColormap] = field(default_factory=cm.devon_r)
    marked_cmap: Union[str, ListedColormap] = "RdPu"
    mark_key: str = "mask"

    def __post_init__(self):
        # Strings are for the standard set of colormaps
        # ListedColormap is for custom solutions (e.g.: cmcrameri)
        if isinstance(self.base_cmap, ListedColormap):
            self.base_colormap = self.base_cmap
        else:
            self.base_colormap = matplotlib.colormaps[self.base_cmap]

        if isinstance(self.marked_cmap, matplotlib.colors.ListedColormap):
            self.marked_colormap = self.marked_cmap
        else:
            self.marked_colormap = matplotlib.colormaps[self.marked_cmap]
        super().__post_init__()

    def _build_image(self):
        df = self.midi_piece.df_with_end
        if not self.time_end:
            # We don't really need a full second roundup
            self.time_end = np.ceil(df.end.max())

        if self.time_end < df.end.max():
            showwarning("Warning, piano roll is not showing everything!", UserWarning, "pianoroll.py", 164)

        # duration = time_end - time_start
        self.duration = self.time_end
        n_time_steps = self.RESOLUTION * int(np.ceil(self.duration))

        # Adjust velocity color intensity to be sure it's visible
        min_value = 20
        max_value = 160

        # Canvas to draw on
        background = np.zeros((self.N_PITCHES, n_time_steps), np.uint8)

        # Draw black keys with the base colormap
        for it in range(self.N_PITCHES):
            is_black = it % 12 in [1, 3, 6, 8, 10]
            if is_black:
                background[it, :] += min_value
        # This makes the array RGB
        background = self.base_colormap(background)

        # Draw notes
        for it, row in df.iterrows():
            note_on = row.start * self.RESOLUTION
            note_on = np.round(note_on).astype(int)

            note_end = row.end * self.RESOLUTION
            note_end = np.round(note_end).astype(int)
            pitch_idx = int(row.pitch)

            # This note is sounding right now
            if self.current_time and note_on <= self.current_time * self.RESOLUTION < note_end:
                color_value = max_value
            else:
                color_value = min_value + row.velocity

            # Colormaps are up to 255, but velocity is up to 127
            color_value += 90

            cmap = self.marked_colormap if row[self.mark_key] else self.base_colormap
            background[pitch_idx, note_on:note_end] = cmap(color_value)

            # pianoroll[pitch_idx, note_on:note_end] = color_value

        self.roll = background

FigureResolution dataclass

Represents the resolution configuration for a figure.

Attributes:

Name Type Description
w_pixels int

The width of the figure in pixels.

h_pixels int

The height of the figure in pixels.

dpi int

The dots per inch (resolution) of the figure.

Properties

w_inches (float): The width of the figure in inches, calculated from pixels and dpi. h_inches (float): The height of the figure in inches, calculated from pixels and dpi. figsize (tuple[float, float]): The size of the figure as a tuple of width and height in inches.

Source code in fortepyan/view/pianoroll/structures.py
@dataclass
class FigureResolution:
    """
    Represents the resolution configuration for a figure.

    Attributes:
        w_pixels (int): The width of the figure in pixels.
        h_pixels (int): The height of the figure in pixels.
        dpi (int): The dots per inch (resolution) of the figure.

    Properties:
        w_inches (float): The width of the figure in inches, calculated from pixels and dpi.
        h_inches (float): The height of the figure in inches, calculated from pixels and dpi.
        figsize (tuple[float, float]): The size of the figure as a tuple of width and height in inches.
    """

    w_pixels: int = 1920 // 2
    h_pixels: int = 1080 // 2
    dpi: int = 72

    @property
    def w_inches(self) -> float:
        return self.w_pixels / self.dpi

    @property
    def h_inches(self) -> float:
        return self.h_pixels / self.dpi

    @property
    def figsize(self) -> tuple[float, float]:
        return self.w_inches, self.h_inches

PianoRoll dataclass

Represents a piano roll visualization of a MIDI piece.

The PianoRoll class provides a visual representation of MIDI data as a traditional piano roll, which is often used in music software. This representation includes the ability to mark the current time, set start and end times for the visualization, and dynamically build the piano roll image based on the MIDI data.

Attributes:

Name Type Description
midi_piece MidiPiece

The MIDI piece to be visualized.

current_time float

The current time position in the MIDI piece.

time_start float

The start time for the piano roll visualization.

time_end float

The end time for the piano roll visualization.

roll array

The numpy array representing the piano roll image.

RESOLUTION int

The resolution of the piano roll image.

N_PITCHES int

The number of pitches to be represented in the piano roll.

Methods:

Name Description
__post_init__

Initializes the piano roll image and tick preparations.

lowest_pitch

Returns the lowest pitch present in the MIDI piece.

highest_pitch

Returns the highest pitch present in the MIDI piece.

_build_image

Builds the piano roll image from the MIDI data.

_prepare_ticks

Prepares the tick marks and labels for the piano roll visualization.

Source code in fortepyan/view/pianoroll/structures.py
@dataclass
class PianoRoll:
    """
    Represents a piano roll visualization of a MIDI piece.

    The PianoRoll class provides a visual representation of MIDI data as a traditional piano roll,
    which is often used in music software. This representation includes the ability to mark the current time,
    set start and end times for the visualization, and dynamically build the piano roll image based on the MIDI data.

    Attributes:
        midi_piece (MidiPiece): The MIDI piece to be visualized.
        current_time (float, optional): The current time position in the MIDI piece.
        time_start (float): The start time for the piano roll visualization.
        time_end (float, optional): The end time for the piano roll visualization.
        roll (np.array): The numpy array representing the piano roll image.
        RESOLUTION (int): The resolution of the piano roll image.
        N_PITCHES (int): The number of pitches to be represented in the piano roll.

    Methods:
        __post_init__(): Initializes the piano roll image and tick preparations.
        lowest_pitch(): Returns the lowest pitch present in the MIDI piece.
        highest_pitch(): Returns the highest pitch present in the MIDI piece.
        _build_image(): Builds the piano roll image from the MIDI data.
        _prepare_ticks(): Prepares the tick marks and labels for the piano roll visualization.
    """

    midi_piece: MidiPiece
    current_time: float = None
    time_start: float = 0.0
    time_end: float = None

    roll: np.array = field(init=False)

    RESOLUTION: int = 30
    N_PITCHES: int = 128

    def __post_init__(self):
        self._build_image()
        self._prepare_ticks()

    @property
    def lowest_pitch(self) -> int:
        return self.midi_piece.df.pitch.min()

    @property
    def highest_pitch(self) -> int:
        return self.midi_piece.df.pitch.max()

    def _build_image(self):
        df = self.midi_piece.df_with_end
        if not self.time_end:
            # We don't really need a full second roundup
            self.time_end = np.ceil(df.end.max())

        if self.time_end < df.end.max():
            print("Warning, piano roll is not showing everything!")

        # duration = time_end - time_start
        self.duration = self.time_end
        n_time_steps = self.RESOLUTION * int(np.ceil(self.duration))
        pianoroll = np.zeros((self.N_PITCHES, n_time_steps), np.uint8)

        # Adjust velocity color intensity to be sure it's visible
        min_value = 20
        max_value = 160

        for it, row in df.iterrows():
            note_on = row.start * self.RESOLUTION
            note_on = np.round(note_on).astype(int)

            note_end = row.end * self.RESOLUTION
            note_end = np.round(note_end).astype(int)
            pitch_idx = int(row.pitch)

            # This note is sounding right now
            if self.current_time and note_on <= self.current_time * self.RESOLUTION < note_end:
                color_value = max_value
            else:
                color_value = min_value + row.velocity
            pianoroll[pitch_idx, note_on:note_end] = color_value

        # Could be a part of "prepare empty piano roll"
        for it in range(self.N_PITCHES):
            is_black = it % 12 in [1, 3, 6, 8, 10]
            if is_black:
                pianoroll[it, :] += min_value

        self.roll = pianoroll

    def _prepare_ticks(self):
        self.y_ticks = np.arange(0, 128, 12, dtype=float)

        # Adding new line shifts the label up a little and positions
        # it nicely at the height where the note actually is
        self.pitch_labels = [f"{note_number_to_name(it)}\n" for it in self.y_ticks]

        # Move the ticks to land between the notes
        # (each note is 1-width and ticks by default are centered, ergo: 0.5 shift)
        self.y_ticks -= 0.5

        # Prepare x ticks and labels
        n_ticks = min(30, self.duration)
        step = np.ceil(self.duration / n_ticks)
        x_ticks = np.arange(0, step * n_ticks, step)
        self.x_ticks = np.round(x_ticks)
        self.x_labels = [round(xt) for xt in self.x_ticks]

draw_piano_roll(ax, piano_roll, time=0.0, cmap='GnBu')

Draws a piano roll visualization on a Matplotlib axis.

This function visualizes the piano roll of a MIDI piece on a given Matplotlib axis. It includes options to highlight notes played at a specific time and to customize the color mapping.

Parameters:

Name Type Description Default
ax Axes

The Matplotlib axis on which to draw the piano roll.

required
piano_roll PianoRoll

The PianoRoll object representing the MIDI piece.

required
time float

The specific time at which to highlight notes. Defaults to 0.0.

0.0
cmap str

The color map to use for the visualization. Defaults to "GnBu".

'GnBu'

Returns:

Type Description
Axes

plt.Axes: The modified Matplotlib axis with the piano roll visualization.

Source code in fortepyan/view/pianoroll/main.py
def draw_piano_roll(
    ax: plt.Axes,
    piano_roll: PianoRoll,
    time: float = 0.0,
    cmap: str = "GnBu",
) -> plt.Axes:
    """
    Draws a piano roll visualization on a Matplotlib axis.

    This function visualizes the piano roll of a MIDI piece on a given Matplotlib axis. It includes options to highlight
    notes played at a specific time and to customize the color mapping.

    Args:
        ax (plt.Axes): The Matplotlib axis on which to draw the piano roll.
        piano_roll (PianoRoll): The PianoRoll object representing the MIDI piece.
        time (float, optional): The specific time at which to highlight notes. Defaults to 0.0.
        cmap (str): The color map to use for the visualization. Defaults to "GnBu".

    Returns:
        plt.Axes: The modified Matplotlib axis with the piano roll visualization.
    """
    ax.imshow(
        piano_roll.roll,
        aspect="auto",
        vmin=0,
        vmax=138,
        origin="lower",
        interpolation="none",
        cmap=cmap,
    )

    ax.set_yticks(piano_roll.y_ticks)
    ax.set_yticklabels(piano_roll.pitch_labels, fontsize=15)

    # Show keyboard range where the music is
    y_min = piano_roll.lowest_pitch - 1
    y_max = piano_roll.highest_pitch + 1
    ax.set_ylim(y_min, y_max)

    ax.set_xticks(piano_roll.x_ticks * piano_roll.RESOLUTION)
    ax.set_xticklabels(piano_roll.x_labels, rotation=60)
    ax.set_xlabel("Time [s]")
    ax.set_xlim(0, piano_roll.duration * piano_roll.RESOLUTION)
    ax.grid()

    # Vertical position indicator
    if piano_roll.current_time:
        ax.axvline(piano_roll.current_time * piano_roll.RESOLUTION, color="k", lw=0.5)

    return ax

draw_pianoroll_with_velocities(midi_piece, time_end=None, title=None, cmap='GnBu', figres=None)

Draws a pianoroll representation of a MIDI piece with an additional plot for velocities.

This function creates a two-part plot with the upper part displaying the pianoroll and the lower part showing the velocities of the notes. Customizable aspects include the end time for the plot, the title, color mapping, and figure resolution.

Parameters:

Name Type Description Default
midi_piece MidiPiece

The MIDI piece to be visualized.

required
time_end float

End time for the plot. Defaults to None, meaning full duration is used.

None
title str

Title for the plot. Defaults to None.

None
cmap str

Color map for the pianoroll and velocities. Defaults to "GnBu".

'GnBu'
figres FigureResolution

Custom figure resolution settings. Defaults to None, which initiates a default FigureResolution.

None

Returns:

Name Type Description
fig Figure

A matplotlib figure object with the pianoroll and velocity plots.

Source code in fortepyan/view/pianoroll/main.py
def draw_pianoroll_with_velocities(
    midi_piece: MidiPiece,
    time_end: float = None,
    title: str = None,
    cmap: str = "GnBu",
    figres: FigureResolution = None,
):
    """
    Draws a pianoroll representation of a MIDI piece with an additional plot for velocities.

    This function creates a two-part plot with the upper part displaying the pianoroll and the lower part showing
    the velocities of the notes. Customizable aspects include the end time for the plot, the title, color mapping,
    and figure resolution.

    Args:
        midi_piece (MidiPiece): The MIDI piece to be visualized.
        time_end (float, optional): End time for the plot. Defaults to None, meaning full duration is used.
        title (str, optional): Title for the plot. Defaults to None.
        cmap (str): Color map for the pianoroll and velocities. Defaults to "GnBu".
        figres (FigureResolution, optional): Custom figure resolution settings. Defaults to None,
                                              which initiates a default `FigureResolution`.

    Returns:
        fig (plt.Figure): A matplotlib figure object with the pianoroll and velocity plots.
    """
    if not figres:
        figres = FigureResolution()

    fig, axes = plt.subplots(
        nrows=2,
        ncols=1,
        figsize=figres.figsize,
        dpi=figres.dpi,
        gridspec_kw={
            "height_ratios": [4, 1],
            "hspace": 0,
        },
    )
    piece = sanitize_midi_piece(midi_piece)
    piano_roll = PianoRoll(piece, time_end=time_end)
    draw_piano_roll(ax=axes[0], piano_roll=piano_roll, cmap=cmap)
    v_ax = axes[1]
    draw_velocities(ax=v_ax, piano_roll=piano_roll, cmap=cmap)

    if title:
        axes[0].set_title(title, fontsize=20)

    # Set the x-axis tick positions and labels, and add a label to the x-axis
    v_ax.set_xticks(piano_roll.x_ticks)
    v_ax.set_xticklabels(piano_roll.x_labels, rotation=60, fontsize=15)
    v_ax.set_xlabel("Time [s]")
    # Set the x-axis limits to the range of the data
    v_ax.set_xlim(0, piano_roll.duration)

    return fig

draw_velocities(ax, piano_roll, cmap='GnBu')

Draws a velocity plot for a MIDI piece on a Matplotlib axis.

This function visualizes the velocities of notes in a MIDI piece using a scatter and line plot. The color and style of the plot can be customized using a colormap.

Parameters:

Name Type Description Default
ax Axes

The Matplotlib axis on which to draw the velocity plot.

required
piano_roll PianoRoll

The PianoRoll object representing the MIDI piece.

required
cmap str

The color map to use for the visualization. Defaults to "GnBu".

'GnBu'

Returns:

Type Description
Axes

plt.Axes: The modified Matplotlib axis with the velocity plot.

Source code in fortepyan/view/pianoroll/main.py
def draw_velocities(
    ax: plt.Axes,
    piano_roll: PianoRoll,
    cmap: str = "GnBu",
) -> plt.Axes:
    """
    Draws a velocity plot for a MIDI piece on a Matplotlib axis.

    This function visualizes the velocities of notes in a MIDI piece using a scatter and line plot. The color and
    style of the plot can be customized using a colormap.

    Args:
        ax (plt.Axes): The Matplotlib axis on which to draw the velocity plot.
        piano_roll (PianoRoll): The PianoRoll object representing the MIDI piece.
        cmap (str): The color map to use for the visualization. Defaults to "GnBu".

    Returns:
        plt.Axes: The modified Matplotlib axis with the velocity plot.
    """
    df = piano_roll.midi_piece.df
    colormap = matplotlib.colormaps.get_cmap(cmap)
    color = colormap(125 / 127)

    ax.plot(df.start, df.velocity, "o", ms=7, color=color)
    ax.plot(df.start, df.velocity, ".", color="white")
    ax.vlines(
        df.start,
        ymin=0,
        ymax=df.velocity,
        lw=2,
        alpha=0.777,
        colors=color,
    )
    ax.set_ylim(0, 128)
    # Add a grid to the plot
    ax.grid()

    # Vertical position indicator
    if piano_roll.current_time:
        ax.axvline(piano_roll.current_time, color="k", lw=0.5)

    return ax

sanitize_midi_piece(piece)

Trims a MIDI piece to a maximum duration threshold for manageability.

If the duration of the MIDI piece exceeds a predefined threshold, it trims the piece to fit within this limit. This function is useful to avoid excessively long playtimes which might be impractical for visualization or analysis.

Parameters:

Name Type Description Default
piece MidiPiece

The MIDI piece to be sanitized.

required

Returns:

Name Type Description
MidiPiece MidiPiece

The sanitized MIDI piece, trimmed if necessary.

Source code in fortepyan/view/pianoroll/main.py
def sanitize_midi_piece(piece: MidiPiece) -> MidiPiece:
    """
    Trims a MIDI piece to a maximum duration threshold for manageability.

    If the duration of the MIDI piece exceeds a predefined threshold, it trims the piece to fit within this limit.
    This function is useful to avoid excessively long playtimes which might be impractical for visualization or analysis.

    Args:
        piece (MidiPiece): The MIDI piece to be sanitized.

    Returns:
        MidiPiece: The sanitized MIDI piece, trimmed if necessary.
    """
    duration_threshold = 1200
    if piece.duration > duration_threshold:
        # TODO Logger
        showwarning(
            message="playtime too long! Showing after trim",
            category=RuntimeWarning,
            filename="fortepyan/view/pianoroll/main.py",
            lineno=88,
        )
        piece = piece.trim(
            start=0,
            finish=duration_threshold,
        )

    return piece

sanitize_xticks(ax, piece)

Adjusts the x-axis ticks and labels for a MIDI piece plot for improved readability.

This function computes and sets appropriate tick marks and labels on the x-axis of a Matplotlib plot based on the duration of a MIDI piece. It ensures the plot is easy to read and interpret by adjusting the frequency and format of the x-axis ticks.

Parameters:

Name Type Description Default
ax Axes

The Matplotlib axes object to be modified.

required
piece MidiPiece

The MIDI piece based on which the axis ticks and labels are adjusted.

required
Source code in fortepyan/view/pianoroll/main.py
def sanitize_xticks(ax: plt.Axes, piece: MidiPiece):
    """
    Adjusts the x-axis ticks and labels for a MIDI piece plot for improved readability.

    This function computes and sets appropriate tick marks and labels on the x-axis of a Matplotlib plot based on the
    duration of a MIDI piece. It ensures the plot is easy to read and interpret by adjusting the frequency and format
    of the x-axis ticks.

    Args:
        ax (plt.Axes): The Matplotlib axes object to be modified.
        piece (MidiPiece): The MIDI piece based on which the axis ticks and labels are adjusted.
    """
    # Calculate the number of seconds in the plot
    n_seconds = np.ceil(piece.duration)
    # Set the maximum number of x-axis ticks to 30
    n_ticks = min(30, n_seconds)
    # Calculate the step size for the x-axis tick positions
    step = np.ceil(n_seconds / n_ticks)
    # Calculate the x-axis tick positions
    x_ticks = np.arange(0, step * n_ticks, step)
    # Round the x-axis tick positions to the nearest integer
    x_ticks = np.round(x_ticks)
    # Set the x-axis tick labels to the same values as the tick positions
    labels = [xt for xt in x_ticks]

    # Set the x-axis tick positions and labels, and add a label to the x-axis
    ax.set_xticks(x_ticks)
    ax.set_xticklabels(labels, rotation=60, fontsize=15)
    ax.set_xlabel("Time [s]")
    # Set the x-axis limits to the range of the data
    ax.set_xlim(0, n_seconds)
    # Add a grid to the plot
    ax.grid()

Classes used for animated visualizations of piano rolls.

PianoRollScene

A class for creating and managing the scene of a piano roll animation.

Attributes:

Name Type Description
piece MidiPiece

The MIDI piece to be visualized.

title str

Title of the piano roll scene.

cmap str

Color map used for the visualization, default is "GnBu".

axes list

List containing the matplotlib axes for the piano roll and velocity plots.

content_dir Path

Directory path for storing temporary files.

frame_paths list

List of paths where individual frame images are saved.

figure Figure

The matplotlib figure object for the scene.

roll_ax Axes

The axes for the piano roll plot.

velocity_ax Axes

The axes for the velocity plot.

Parameters:

Name Type Description Default
piece MidiPiece

The MIDI piece to be visualized.

required
title str

Title of the piano roll scene.

required
cmap str

Color map used for the visualization. Defaults to "GnBu".

'GnBu'
Source code in fortepyan/view/animation/pianoroll.py
class PianoRollScene:
    """
    A class for creating and managing the scene of a piano roll animation.

    Attributes:
        piece (MidiPiece): The MIDI piece to be visualized.
        title (str): Title of the piano roll scene.
        cmap (str): Color map used for the visualization, default is "GnBu".
        axes (list): List containing the matplotlib axes for the piano roll and velocity plots.
        content_dir (Path): Directory path for storing temporary files.
        frame_paths (list): List of paths where individual frame images are saved.
        figure (matplotlib.figure.Figure): The matplotlib figure object for the scene.
        roll_ax (matplotlib.axes.Axes): The axes for the piano roll plot.
        velocity_ax (matplotlib.axes.Axes): The axes for the velocity plot.

    Args:
        piece (MidiPiece): The MIDI piece to be visualized.
        title (str): Title of the piano roll scene.
        cmap (str, optional): Color map used for the visualization. Defaults to "GnBu".
    """

    def __init__(self, piece: MidiPiece, title: str, cmap: str = "GnBu"):
        self.axes = []
        self.content_dir = Path(tempfile.mkdtemp())

        self.frame_paths = []

        self.piece = piece
        self.title = title
        self.cmap = cmap

        figres = FigureResolution()
        f, axes = plt.subplots(
            nrows=2,
            ncols=1,
            figsize=figres.figsize,
            dpi=figres.dpi,
            gridspec_kw={
                "height_ratios": [4, 1],
                "hspace": 0,
            },
        )

        self.figure = f
        self.roll_ax = axes[0]
        self.velocity_ax = axes[1]
        self.axes = [self.roll_ax, self.velocity_ax]

    def draw_all_axes(self, time: float) -> None:
        """
        Draws both the piano roll and velocity plots at a specified time.

        Args:
            time (float): The time at which to draw the plots.
        """
        self.draw_piano_roll(time)
        self.draw_velocities(time)

    def draw_piano_roll(self, time: float) -> None:
        """
        Draws the piano roll plot at a specified time.

        Args:
            time (float): The time at which to draw the piano roll.
        """
        piano_roll = PianoRoll(self.piece, current_time=time)
        roll.draw_piano_roll(
            ax=self.roll_ax,
            piano_roll=piano_roll,
            cmap=self.cmap,
            time=time,
        )
        self.roll_ax.set_title(self.title, fontsize=20)

    def draw_velocities(self, time: float) -> None:
        """
        Draws the velocity plot at a specified time.

        Args:
            time (float): The time at which to draw the velocity plot.
        """
        piano_roll = PianoRoll(self.piece)
        roll.draw_velocities(
            ax=self.velocity_ax,
            piano_roll=piano_roll,
            cmap=self.cmap,
        )

        # Set the x-axis tick positions and labels, and add a label to the x-axis
        self.velocity_ax.set_xticks(piano_roll.x_ticks)
        self.velocity_ax.set_xticklabels(piano_roll.x_labels, rotation=60, fontsize=15)
        self.velocity_ax.set_xlabel("Time [s]")
        # Set the x-axis limits to the range of the data
        self.velocity_ax.set_xlim(0, piano_roll.duration)

    def save_frame(self, savepath: str = "tmp/tmp.png") -> None:
        """
        Saves the current state of the figure to a file.

        Args:
            savepath (str, optional): Path where the image should be saved. Defaults to "tmp/tmp.png".
        """
        self.figure.tight_layout()

        self.figure.savefig(savepath)

        self.clean_figure()

    def clean_figure(self) -> None:
        """
        Clears the content of all axes in the figure.
        """
        for ax in self.axes:
            ax.clear()

    def animate_part(self, part: pd.DataFrame) -> None:
        """
        Animates a part of the MIDI piece.

        Args:
            part (pd.DataFrame): DataFrame containing time and counter information for frames.
        """
        for it, row in part.iterrows():
            time = row.time
            frame_counter = int(row.counter)
            self.draw(time)
            savepath = self.content_dir / f"{100000 + frame_counter}.png"
            self.save_frame(savepath)
            self.frame_paths.append(savepath)

    def draw(self, time: float) -> None:
        """
        Prepares the figure for drawing and invokes drawing of all axes for a specific time.

        Args:
            time (float): The time at which to draw the figure.
        """
        self.clean_figure()
        self.figure.tight_layout()
        self.draw_all_axes(time)

    def prepare_animation_steps(self, framerate: int = 30) -> pd.DataFrame:
        """
        Prepare the data required for the animation.

        Parameters:
            framerate (int): Framerate for the animation (default is 30).

        Returns:
            pd.DataFrame: DataFrame containing time and counter for each frame.
        """
        # Calculate the maximum time required for the animation
        max_time = np.ceil(self.piece.df.end.max()).astype(int)
        # Calculate the number of frames required for the animation
        n_frames = max_time * framerate
        # Create an array of times that will be used to create the animation
        times = np.linspace(0, max_time - 1 / framerate, n_frames)
        # Create a DataFrame to store the time and counter for each frame
        df = pd.DataFrame({"time": times, "counter": range(n_frames)})

        return df

    def render(self, framerate: int = 30) -> Path:
        """
        Render the animation using a single process.

        Args:
            framerate (int): Framerate for the animation, defaults to 30.

        Returns:
            Path: Directory containing the generated animation content.
        """
        df = self.prepare_animation_steps(framerate)

        # Call the animate_part function with the entire DataFrame as an argument
        self.animate_part(df)

        # Return the directory containing the generated animation content
        return self.content_dir

    def render_mp(self, framerate: int = 30) -> Path:
        """
        Renders the animation using multi-processing to speed up the process.

        Args:
            framerate (int): Framerate for the animation, defaults to 30.

        Returns:
            Path: Directory containing the generated animation content.
        """
        df = self.prepare_animation_steps(framerate)

        # Step size for dividing the DataFrame into parts
        step = 50

        # Divide the DataFrame into parts based on the step size
        parts = [df[sta : sta + step] for sta in range(0, df.shape[0], step)]

        # Create a pool of processes using all available CPU cores
        with mp.Pool(mp.cpu_count()) as pool:
            # Map the animate_part function to each part of the DataFrame
            pool.map(self.animate_part, parts)

        # Return the directory containing the generated animation content
        return self.content_dir

animate_part(part)

Animates a part of the MIDI piece.

Parameters:

Name Type Description Default
part DataFrame

DataFrame containing time and counter information for frames.

required
Source code in fortepyan/view/animation/pianoroll.py
def animate_part(self, part: pd.DataFrame) -> None:
    """
    Animates a part of the MIDI piece.

    Args:
        part (pd.DataFrame): DataFrame containing time and counter information for frames.
    """
    for it, row in part.iterrows():
        time = row.time
        frame_counter = int(row.counter)
        self.draw(time)
        savepath = self.content_dir / f"{100000 + frame_counter}.png"
        self.save_frame(savepath)
        self.frame_paths.append(savepath)

clean_figure()

Clears the content of all axes in the figure.

Source code in fortepyan/view/animation/pianoroll.py
def clean_figure(self) -> None:
    """
    Clears the content of all axes in the figure.
    """
    for ax in self.axes:
        ax.clear()

draw(time)

Prepares the figure for drawing and invokes drawing of all axes for a specific time.

Parameters:

Name Type Description Default
time float

The time at which to draw the figure.

required
Source code in fortepyan/view/animation/pianoroll.py
def draw(self, time: float) -> None:
    """
    Prepares the figure for drawing and invokes drawing of all axes for a specific time.

    Args:
        time (float): The time at which to draw the figure.
    """
    self.clean_figure()
    self.figure.tight_layout()
    self.draw_all_axes(time)

draw_all_axes(time)

Draws both the piano roll and velocity plots at a specified time.

Parameters:

Name Type Description Default
time float

The time at which to draw the plots.

required
Source code in fortepyan/view/animation/pianoroll.py
def draw_all_axes(self, time: float) -> None:
    """
    Draws both the piano roll and velocity plots at a specified time.

    Args:
        time (float): The time at which to draw the plots.
    """
    self.draw_piano_roll(time)
    self.draw_velocities(time)

draw_piano_roll(time)

Draws the piano roll plot at a specified time.

Parameters:

Name Type Description Default
time float

The time at which to draw the piano roll.

required
Source code in fortepyan/view/animation/pianoroll.py
def draw_piano_roll(self, time: float) -> None:
    """
    Draws the piano roll plot at a specified time.

    Args:
        time (float): The time at which to draw the piano roll.
    """
    piano_roll = PianoRoll(self.piece, current_time=time)
    roll.draw_piano_roll(
        ax=self.roll_ax,
        piano_roll=piano_roll,
        cmap=self.cmap,
        time=time,
    )
    self.roll_ax.set_title(self.title, fontsize=20)

draw_velocities(time)

Draws the velocity plot at a specified time.

Parameters:

Name Type Description Default
time float

The time at which to draw the velocity plot.

required
Source code in fortepyan/view/animation/pianoroll.py
def draw_velocities(self, time: float) -> None:
    """
    Draws the velocity plot at a specified time.

    Args:
        time (float): The time at which to draw the velocity plot.
    """
    piano_roll = PianoRoll(self.piece)
    roll.draw_velocities(
        ax=self.velocity_ax,
        piano_roll=piano_roll,
        cmap=self.cmap,
    )

    # Set the x-axis tick positions and labels, and add a label to the x-axis
    self.velocity_ax.set_xticks(piano_roll.x_ticks)
    self.velocity_ax.set_xticklabels(piano_roll.x_labels, rotation=60, fontsize=15)
    self.velocity_ax.set_xlabel("Time [s]")
    # Set the x-axis limits to the range of the data
    self.velocity_ax.set_xlim(0, piano_roll.duration)

prepare_animation_steps(framerate=30)

Prepare the data required for the animation.

Parameters:

Name Type Description Default
framerate int

Framerate for the animation (default is 30).

30

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame containing time and counter for each frame.

Source code in fortepyan/view/animation/pianoroll.py
def prepare_animation_steps(self, framerate: int = 30) -> pd.DataFrame:
    """
    Prepare the data required for the animation.

    Parameters:
        framerate (int): Framerate for the animation (default is 30).

    Returns:
        pd.DataFrame: DataFrame containing time and counter for each frame.
    """
    # Calculate the maximum time required for the animation
    max_time = np.ceil(self.piece.df.end.max()).astype(int)
    # Calculate the number of frames required for the animation
    n_frames = max_time * framerate
    # Create an array of times that will be used to create the animation
    times = np.linspace(0, max_time - 1 / framerate, n_frames)
    # Create a DataFrame to store the time and counter for each frame
    df = pd.DataFrame({"time": times, "counter": range(n_frames)})

    return df

render(framerate=30)

Render the animation using a single process.

Parameters:

Name Type Description Default
framerate int

Framerate for the animation, defaults to 30.

30

Returns:

Name Type Description
Path Path

Directory containing the generated animation content.

Source code in fortepyan/view/animation/pianoroll.py
def render(self, framerate: int = 30) -> Path:
    """
    Render the animation using a single process.

    Args:
        framerate (int): Framerate for the animation, defaults to 30.

    Returns:
        Path: Directory containing the generated animation content.
    """
    df = self.prepare_animation_steps(framerate)

    # Call the animate_part function with the entire DataFrame as an argument
    self.animate_part(df)

    # Return the directory containing the generated animation content
    return self.content_dir

render_mp(framerate=30)

Renders the animation using multi-processing to speed up the process.

Parameters:

Name Type Description Default
framerate int

Framerate for the animation, defaults to 30.

30

Returns:

Name Type Description
Path Path

Directory containing the generated animation content.

Source code in fortepyan/view/animation/pianoroll.py
def render_mp(self, framerate: int = 30) -> Path:
    """
    Renders the animation using multi-processing to speed up the process.

    Args:
        framerate (int): Framerate for the animation, defaults to 30.

    Returns:
        Path: Directory containing the generated animation content.
    """
    df = self.prepare_animation_steps(framerate)

    # Step size for dividing the DataFrame into parts
    step = 50

    # Divide the DataFrame into parts based on the step size
    parts = [df[sta : sta + step] for sta in range(0, df.shape[0], step)]

    # Create a pool of processes using all available CPU cores
    with mp.Pool(mp.cpu_count()) as pool:
        # Map the animate_part function to each part of the DataFrame
        pool.map(self.animate_part, parts)

    # Return the directory containing the generated animation content
    return self.content_dir

save_frame(savepath='tmp/tmp.png')

Saves the current state of the figure to a file.

Parameters:

Name Type Description Default
savepath str

Path where the image should be saved. Defaults to "tmp/tmp.png".

'tmp/tmp.png'
Source code in fortepyan/view/animation/pianoroll.py
def save_frame(self, savepath: str = "tmp/tmp.png") -> None:
    """
    Saves the current state of the figure to a file.

    Args:
        savepath (str, optional): Path where the image should be saved. Defaults to "tmp/tmp.png".
    """
    self.figure.tight_layout()

    self.figure.savefig(savepath)

    self.clean_figure()