diff --git a/note_seq/notebook_utils.py b/note_seq/notebook_utils.py index 4570eed..52ab9e1 100644 --- a/note_seq/notebook_utils.py +++ b/note_seq/notebook_utils.py @@ -25,6 +25,7 @@ import bokeh.plotting from IPython import display from note_seq import midi_synth +from note_seq.protobuf import music_pb2 import numpy as np import pandas as pd from scipy.io import wavfile @@ -35,6 +36,8 @@ _DEFAULT_SAMPLE_RATE = 44100 _play_id = 0 # Used for ephemeral colab_play. +_CHORD_SYMBOL = music_pb2.NoteSequence.TextAnnotation.CHORD_SYMBOL + def colab_play(array_of_floats, sample_rate, ephemeral=True, autoplay=False): """Creates an HTML5 audio widget to play a sound in Colab. @@ -103,16 +106,23 @@ def play_sequence(sequence, display.display(display.Audio(array_of_floats, rate=sample_rate)) -def plot_sequence(sequence, show_figure=True): +def plot_sequence(sequence, + show_figure=True, + width=1000, + height=400, + show_chords=False): """Creates an interactive pianoroll for a NoteSequence. Example usage: plot a random melody. sequence = mm.Melody(np.random.randint(36, 72, 30)).to_sequence() - bokeh_pianoroll(sequence) + plot_sequence(sequence) Args: sequence: A NoteSequence. show_figure: A boolean indicating whether or not to show the figure. + width: An int indicating plot width in pixels. Default is 1000. + height: An int indicating plot height in pixels. Default is 400. + show_chords: If True, show chord changes on the x-axis. Default is False. Returns: If show_figure is False, a Bokeh figure; otherwise None. @@ -139,19 +149,32 @@ def _sequence_to_pandas_dataframe(sequence): return pd.DataFrame(pd_dict) - # These are hard-coded reasonable values, but the user can override them - # by updating the figure if need be. - fig = bokeh.plotting.figure( - tools='hover,pan,box_zoom,reset,save') - fig.plot_width = 500 - fig.plot_height = 200 - fig.xaxis.axis_label = 'time (sec)' - fig.yaxis.axis_label = 'pitch (MIDI)' + fig = bokeh.plotting.figure(tools='hover,pan,box_zoom,reset,previewsave') + if width: + fig.width = width + if height: + fig.height = height + fig.xaxis.axis_label = sequence.id + fig.yaxis.axis_label = 'pitch' fig.yaxis.ticker = bokeh.models.SingleIntervalTicker(interval=12) fig.ygrid.ticker = bokeh.models.SingleIntervalTicker(interval=12) # Pick indexes that are maximally different in Spectral8 colormap. spectral_color_indexes = [7, 0, 6, 1, 5, 2, 3] + if show_chords: + chords = [ + (ta.time, str(ta.text)) + for ta in sequence.text_annotations + if ta.annotation_type == _CHORD_SYMBOL + ] + fig.xaxis.ticker = bokeh.models.FixedTicker( + ticks=[time for time, _ in chords] + ) + fig.xaxis.formatter = bokeh.models.CustomJSTickFormatter(code=""" + var chords = %s; + return chords[tick]; + """ % dict(chords)) + # Create a Pandas dataframe and group it by instrument. dataframe = _sequence_to_pandas_dataframe(sequence) instruments = sorted(set(dataframe['instrument'])) @@ -162,22 +185,21 @@ def _sequence_to_pandas_dataframe(sequence): color = bokeh.palettes.Spectral8[color_idx] source = bokeh.plotting.ColumnDataSource(instrument_df) fig.quad(top='top', bottom='bottom', left='start_time', right='end_time', - line_color='black', fill_color=color, - fill_alpha='fill_alpha', source=source) - fig.select(dict(type=bokeh.models.HoverTool)).tooltips = ( # pylint: disable=use-dict-literal - {'pitch': '@pitch', - 'program': '@program', - 'velo': '@velocity', - 'duration': '@duration', - 'start_time': '@start_time', - 'end_time': '@end_time', - 'velocity': '@velocity', - 'fill_alpha': '@fill_alpha'}) + line_color=color, fill_color=color, source=source) + fig.select(dict(type=bokeh.models.HoverTool)).tooltips = { # pylint: disable=use-dict-literal + 'pitch': '@pitch', + 'program': '@program', + 'velo': '@velocity', + 'duration': '@duration', + 'start_time': '@start_time', + 'end_time': '@end_time' + } if show_figure: bokeh.plotting.output_notebook() bokeh.plotting.show(fig) return None + return fig diff --git a/setup.py b/setup.py index e7b5def..0288cec 100644 --- a/setup.py +++ b/setup.py @@ -14,8 +14,7 @@ """A setuptools based setup module for note-seq.""" -from setuptools import find_packages -from setuptools import setup +import setuptools with open('note_seq/version.py') as in_file: exec(in_file.read()) # pylint: disable=exec-used @@ -23,7 +22,7 @@ REQUIRED_PACKAGES = [ 'absl-py', 'attrs', - 'bokeh >= 0.12.0', + 'bokeh >= 3.0.3', 'intervaltree >= 2.1.0', 'IPython', 'librosa >= 0.6.2', @@ -35,7 +34,7 @@ 'scipy >= 0.18.1', ] -setup( +setuptools.setup( name='note-seq', version=__version__, # pylint: disable=undefined-variable description='Use machine learning to create art and music', @@ -57,7 +56,7 @@ 'Topic :: Software Development :: Libraries', ], keywords='note_seq note sequences', - packages=find_packages(), + packages=setuptools.find_packages(), package_data={'note_seq': ['*.pyi', '**/*.pyi']}, install_requires=REQUIRED_PACKAGES, setup_requires=['pytest-runner', 'pytest-pylint'],