def plot(self, pm: PrettyMIDI):
        """
        Plots the pretty midi object as a plot object.

          :param pm: the PrettyMIDI instance to plot
          :return: the bokeh plot layout
        """
        preset = self._preset

        # Calculates the QPM from the MIDI file, might raise exception if confused
        qpm = self._get_qpm(pm)

        # Initialize the tools, those are present on the right hand side
        plot = bokeh.plotting.figure(tools="reset,hover,save,wheel_zoom,pan",
                                     toolbar_location=preset.toolbar_location)

        # Setup the hover and the data dict for bokeh,
        # each property must match a property in the data dict
        plot.select(dict(type=bokeh.models.HoverTool)).tooltips = ({
            "program":
            "@program",
            "pitch":
            "@top",
            "velocity":
            "@velocity",
            "duration":
            "@duration",
            "start_time":
            "@left",
            "end_time":
            "@right"
        })
        data = dict(program=[],
                    top=[],
                    bottom=[],
                    left=[],
                    right=[],
                    duration=[],
                    velocity=[],
                    color=[])

        # Puts the notes in the dict for bokeh and saves first
        # and last note time, bigger and smaller pitch
        pitch_min = None
        pitch_max = None
        first_note_start = None
        last_note_end = None
        index_instrument = 0
        for instrument in pm.instruments:
            for note in instrument.notes:
                pitch_min = min(pitch_min or self._MAX_PITCH, note.pitch)
                pitch_max = max(pitch_max or self._MIN_PITCH, note.pitch)
                color = self._get_color(index_instrument, note)
                note_start = note.start
                note_end = note.start + (note.end - note.start)
                data["program"].append(instrument.program)
                data["top"].append(note.pitch)
                if self._show_velocity:
                    data["bottom"].append(note.pitch + (note.velocity / 127))
                else:
                    data["bottom"].append(note.pitch + 1)
                data["left"].append(note_start)
                data["right"].append(note_end)
                data["duration"].append(note_end - note_start)
                data["velocity"].append(note.velocity)
                data["color"].append(color)
                first_note_start = min(first_note_start or sys.maxsize,
                                       note_start)
                last_note_end = max(last_note_end or 0, note_end)
            index_instrument = index_instrument + 1

        # Shows an empty plot even if there are no notes
        if first_note_start is None or last_note_end is None or pitch_min is None or pitch_max is None:
            pitch_min = self._MIN_PITCH
            pitch_max = pitch_min + 5
            first_note_start = 0
            last_note_end = 0

        # Gets the pitch range (min, max) from either the provided arguments
        # or min and max values from the notes
        if self._plot_pitch_range_start is not None:
            pitch_min = self._plot_pitch_range_start
        else:
            pitch_min = min(self._MAX_PITCH, pitch_min)
        if self._plot_pitch_range_stop is not None:
            pitch_max = self._plot_pitch_range_stop
        else:
            pitch_max = max(self._MIN_PITCH, pitch_max)

        pitch_range = pitch_max + 1 - pitch_min

        # Draws the rectangles on the plot from the data
        source = ColumnDataSource(data=data)
        plot.quad(left="left",
                  right="right",
                  top="top",
                  bottom="bottom",
                  line_alpha=1,
                  line_color="black",
                  color="color",
                  source=source)

        # Draws the y grid by hand, because the grid has label on the ticks, but
        # for a plot like this, the labels needs to fit in between the ticks.
        # Also useful to change the background of the grid each line
        for pitch in range(pitch_min, pitch_max + 1):
            # Draws the background box and contours, on the underlay layer, so
            # that the rectangles and over the box annotations
            fill_alpha = (0.15 if pitch % 2 == 0 else 0.00)
            box = BoxAnnotation(bottom=pitch,
                                top=pitch + 1,
                                fill_color="gray",
                                fill_alpha=fill_alpha,
                                line_color="black",
                                line_alpha=0.3,
                                line_width=1,
                                level="underlay")
            plot.add_layout(box)
            label = Label(x=preset.label_y_axis_offset_x,
                          y=pitch + preset.label_y_axis_offset_y,
                          x_units="screen",
                          text=str(pitch),
                          render_mode="css",
                          text_font_size=preset.label_text_font_size,
                          text_font_style=preset.label_text_font_style)
            plot.add_layout(label)

        # Gets the time signature from pretty midi, or 4/4 if none
        if self._midi_time_signature:
            numerator, denominator = self._midi_time_signature.split("/")
            time_signature = TimeSignature(int(numerator), int(denominator), 0)
        else:
            if pm.time_signature_changes:
                if len(pm.time_signature_changes) > 1:
                    raise Exception(
                        "Multiple time signatures are not supported")
                time_signature = pm.time_signature_changes[0]
            else:
                time_signature = TimeSignature(4, 4, 0)

        # Gets seconds per bar and seconds per beat
        if len(pm.get_beats()) >= 2:
            seconds_per_beat = pm.get_beats()[1] - pm.get_beats()[0]
        else:
            seconds_per_beat = 0.5
        if len(pm.get_downbeats()) >= 2:
            seconds_per_bar = pm.get_downbeats()[1] - pm.get_downbeats()[0]
        else:
            seconds_per_bar = 2.0

        # Defines the end time of the plot in seconds
        if self._plot_bar_range_stop is not None:
            plot_end_time = self._plot_bar_range_stop * seconds_per_bar
        else:
            # Calculates the plot start and end time, the start time can start after
            # notes or truncate notes if the plot is too long (we left truncate the
            # plot with the bounds)
            # The plot start and plot end are a multiple of seconds per bar (closest
            # smaller value for the start time, closest higher value for the end time)
            plot_end_time = int(
                (last_note_end) / seconds_per_bar) * seconds_per_bar
            # If the last note end is exactly on a multiple of seconds per bar,
            # we don't start a new one
            is_on_bar = math.isclose(last_note_end % seconds_per_bar,
                                     seconds_per_bar)
            is_on_bar_exact = math.isclose(last_note_end % seconds_per_bar,
                                           0.0)
            if not is_on_bar and not is_on_bar_exact:
                plot_end_time += seconds_per_bar

        # Defines the start time of the plot in seconds
        if self._plot_bar_range_start is not None:
            plot_start_time = self._plot_bar_range_start * seconds_per_bar
        else:
            start_time = int(
                first_note_start / seconds_per_bar) * seconds_per_bar
            plot_max_length_time = self._plot_max_length_bar * seconds_per_bar
            plot_start_time = max(plot_end_time - plot_max_length_time,
                                  start_time)

        # Draws the vertical bar grid, with a different background color
        # for each bar
        if preset.show_bar:
            bar_count = 0
            for bar_time in pm.get_downbeats():
                fill_alpha_index = bar_count % len(self._bar_fill_alphas)
                fill_alpha = self._bar_fill_alphas[fill_alpha_index]
                box = BoxAnnotation(left=bar_time,
                                    right=bar_time + seconds_per_bar,
                                    fill_color="gray",
                                    fill_alpha=fill_alpha,
                                    line_color="black",
                                    line_width=2,
                                    line_alpha=0.5,
                                    level="underlay")
                plot.add_layout(box)
                bar_count += 1

        # Draws the vertical beat grid, those are only grid lines
        if preset.show_beat:
            for beat_time in pm.get_beats():
                box = BoxAnnotation(left=beat_time,
                                    right=beat_time + seconds_per_beat,
                                    fill_color=None,
                                    line_color="black",
                                    line_width=1,
                                    line_alpha=0.4,
                                    level="underlay")
                plot.add_layout(box)

        # Configure x axis
        plot.xaxis.bounds = (plot_start_time, plot_end_time)
        plot.xaxis.axis_label = "time (SEC)"
        plot.xaxis.axis_label_text_font_size = preset.axis_label_text_font_size
        plot.xaxis.ticker = bokeh.models.SingleIntervalTicker(interval=1)
        plot.xaxis.major_tick_line_alpha = 0.9
        plot.xaxis.major_tick_line_width = 1
        plot.xaxis.major_tick_out = preset.axis_x_major_tick_out
        plot.xaxis.minor_tick_line_alpha = 0
        plot.xaxis.major_label_text_font_size = preset.label_text_font_size
        plot.xaxis.major_label_text_font_style = preset.label_text_font_style

        # Configure y axis
        plot.yaxis.bounds = (pitch_min, pitch_max + 1)
        plot.yaxis.axis_label = "pitch (MIDI)"
        plot.yaxis.axis_label_text_font_size = preset.axis_label_text_font_size
        plot.yaxis.ticker = bokeh.models.SingleIntervalTicker(interval=1)
        plot.yaxis.major_label_text_alpha = 0
        plot.yaxis.major_tick_line_alpha = 0.9
        plot.yaxis.major_tick_line_width = 1
        plot.yaxis.major_tick_out = preset.axis_y_major_tick_out
        plot.yaxis.minor_tick_line_alpha = 0
        plot.yaxis.axis_label_standoff = preset.axis_y_label_standoff
        plot.outline_line_width = 1
        plot.outline_line_alpha = 1
        plot.outline_line_color = "black"

        # The x grid is deactivated because is draw by hand (see x grid code)
        plot.xgrid.grid_line_color = None

        # The y grid is deactivated because is draw by hand (see y grid code)
        plot.ygrid.grid_line_color = None

        # Configure the plot size and range
        plot_title_text = "Visual MIDI (%s QPM, %s/%s)" % (str(
            int(qpm)), time_signature.numerator, time_signature.denominator)
        plot.title = Title(text=plot_title_text,
                           text_font_size=preset.title_text_font_size)
        plot.plot_width = preset.plot_width
        if preset.row_height:
            plot.plot_height = pitch_range * preset.row_height
        else:
            plot.plot_height = preset.plot_height
        plot.x_range = Range1d(plot_start_time, plot_end_time)
        plot.y_range = Range1d(pitch_min, pitch_max + 1)
        plot.min_border_right = 50

        if self._live_reload and preset.stop_live_reload_button:
            callback = CustomJS(code="clearInterval(liveReloadInterval)")
            button = Button(label="stop live reload")
            button.js_on_click(callback)
            layout = column(button, plot)
        else:
            layout = column(plot)

        return layout
Beispiel #2
0
    def visualize(self):
        """
        Generates a plot using bokeh, which displays the initial trajectory and
        the optimized trajectory of the cutting tool.
        """

        # Tools that will be displayed on the plots
        tools = "pan,wheel_zoom,reset,save"

        # Plot displaying the optimized path
        result_plot = figure(plot_width=1000,
                             plot_height=500,
                             tools=tools,
                             active_scroll='wheel_zoom')
        result_plot.title.text = "Optimized Path"

        # Plot displaying the non optimized path
        initial_plot = figure(plot_width=1000,
                              plot_height=500,
                              tools=tools,
                              active_scroll='wheel_zoom')
        initial_plot.title.text = "Initial Path"

        # Add the data to the result plot
        result_plot = self.populate_plot(result_plot, self.result)
        result_plot.legend.location = "bottom_right"

        # Add the data to the initial plot
        initial_plot = self.populate_plot(initial_plot, self.initial)
        initial_plot.legend.location = "bottom_right"

        # Add cutting tool to plots
        # Generate the points on which the triangle should move on
        result_lines_x, result_lines_y = self.generate_tool_path(
            self.result, 1)
        initial_lines_x, initial_lines_y = self.generate_tool_path(
            self.initial, 1)

        # Add cutting tool triangle to optimized path
        result_triangle_position = ColumnDataSource(
            data=dict(x=[result_lines_x[0]], y=[result_lines_y[0]]))
        result_triangle = Triangle(x='x',
                                   y='y',
                                   line_color=Category10_4[3],
                                   line_width=3,
                                   size=20,
                                   fill_alpha=0)
        result_plot.add_glyph(result_triangle_position, result_triangle)

        # Add cutting tool triangle to initial path
        initial_triangle_position = ColumnDataSource(
            data=dict(x=[initial_lines_x[0]], y=[initial_lines_y[0]]))
        initial_triangle = Triangle(x='x',
                                    y='y',
                                    line_color=Category10_4[3],
                                    line_width=3,
                                    size=20,
                                    fill_alpha=0)
        initial_plot.add_glyph(initial_triangle_position, initial_triangle)

        # Add button to start moving the triangle
        button = Button(label='Start')
        result_num_steps = result_lines_x.shape[0]
        initial_num_steps = initial_lines_x.shape[0]
        num_steps = max(result_num_steps, initial_num_steps)

        # JavaScript callback which will be called once the button is pressed
        callback = CustomJS(args=dict(
            result_triangle_position=result_triangle_position,
            result_lines_x=result_lines_x,
            result_lines_y=result_lines_y,
            result_num_steps=result_num_steps,
            initial_triangle_position=initial_triangle_position,
            initial_lines_x=initial_lines_x,
            initial_lines_y=initial_lines_y,
            initial_num_steps=initial_num_steps,
            num_steps=num_steps),
                            code="""
            // Animate optimal path plot
            for(let i = 0; i < num_steps; i += 50) {
                setTimeout(function() {
                    if (i < result_num_steps) {
                        result_triangle_position.data['x'][0] = result_lines_x[i]
                        result_triangle_position.data['y'][0] = result_lines_y[i]
                    }

                    if (i < initial_num_steps) {
                        initial_triangle_position.data['x'][0] = initial_lines_x[i]
                        initial_triangle_position.data['y'][0] = initial_lines_y[i]
                    }

                    result_triangle_position.change.emit()
                    initial_triangle_position.change.emit()

                }, i)
            }
        """)
        # Add callback function to button, which starts the whole animation
        button.js_on_click(callback)

        # Save the plot
        result_plot = row([result_plot, button])
        plot = column([result_plot, initial_plot])
        output_file("visualization.html", title="CNC Path Optimization")
        save(plot)