# Copyright 2023 The Magenta Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Python functions which run only within a Jupyter or Colab notebook.""" import base64 import collections import functools import io import os import urllib import bokeh 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 makedirs = functools.partial(os.makedirs, exist_ok=True) _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. This function should only be called from a Colab notebook. Args: array_of_floats: A 1D or 2D array-like container of float sound samples. Values outside of the range [-1, 1] will be clipped. sample_rate: Sample rate in samples per second. ephemeral: If set to True, the widget will be ephemeral, and disappear on reload (and it won't be counted against realtime document size). autoplay: If True, automatically start playing the sound when the widget is rendered. """ from google.colab.output import _js_builder as js # pylint:disable=import-outside-toplevel,g-import-not-at-top,protected-access normalizer = float(np.iinfo(np.int16).max) array_of_ints = np.array( np.asarray(array_of_floats) * normalizer, dtype=np.int16) memfile = io.BytesIO() wavfile.write(memfile, sample_rate, array_of_ints) html = """""" html = html.format( autoplay='autoplay' if autoplay else '', base64_wavfile=base64.b64encode(memfile.getvalue()).decode('ascii')) memfile.close() global _play_id _play_id += 1 if ephemeral: element = 'id_%s' % _play_id display.display(display.HTML('
' % element)) js.Js('document', mode=js.EVAL).getElementById(element).innerHTML = html else: display.display(display.HTML(html)) def play_sequence(sequence, synth=midi_synth.synthesize, sample_rate=_DEFAULT_SAMPLE_RATE, colab_ephemeral=True, **synth_args): """Creates an interactive player for a synthesized note sequence. This function should only be called from a Jupyter or Colab notebook. Args: sequence: A music_pb2.NoteSequence to synthesize and play. synth: A synthesis function that takes a sequence and sample rate as input. sample_rate: The sample rate at which to synthesize. colab_ephemeral: If set to True, the widget will be ephemeral in Colab, and disappear on reload (and it won't be counted against realtime document size). **synth_args: Additional keyword arguments to pass to the synth function. """ array_of_floats = synth(sequence, sample_rate=sample_rate, **synth_args) try: import google.colab # pylint:disable=import-outside-toplevel,g-import-not-at-top,unused-import colab_play(array_of_floats, sample_rate, colab_ephemeral) except ImportError: display.display(display.Audio(array_of_floats, rate=sample_rate)) 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() 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. """ def _sequence_to_pandas_dataframe(sequence): """Generates a pandas dataframe from a sequence.""" pd_dict = collections.defaultdict(list) for note in sequence.notes: pd_dict['start_time'].append(note.start_time) pd_dict['end_time'].append(note.end_time) pd_dict['duration'].append(note.end_time - note.start_time) pd_dict['pitch'].append(note.pitch) pd_dict['bottom'].append(note.pitch - 0.4) pd_dict['top'].append(note.pitch + 0.4) pd_dict['velocity'].append(note.velocity) pd_dict['fill_alpha'].append(note.velocity / 128.0) pd_dict['instrument'].append(note.instrument) pd_dict['program'].append(note.program) # If no velocity differences are found, set alpha to 1.0. if np.max(pd_dict['velocity']) == np.min(pd_dict['velocity']): pd_dict['fill_alpha'] = [1.0] * len(pd_dict['fill_alpha']) return pd.DataFrame(pd_dict) 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'])) grouped_dataframe = dataframe.groupby('instrument') for counter, instrument in enumerate(instruments): instrument_df = grouped_dataframe.get_group(instrument) color_idx = spectral_color_indexes[counter % len(spectral_color_indexes)] 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=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 def download_bundle(bundle_name, target_dir, force_reload=False): """Downloads a Magenta bundle to target directory. Target directory target_dir will be created if it does not already exist. Args: bundle_name: A string Magenta bundle name to download. target_dir: A string local directory in which to write the bundle. force_reload: A boolean that when True, reloads the bundle even if present. """ makedirs(target_dir) bundle_target = os.path.join(target_dir, bundle_name) if not os.path.exists(bundle_target) or force_reload: response = urllib.request.urlopen( 'http://download.magenta.tensorflow.org/models/%s' % bundle_name) data = response.read() local_file = open(bundle_target, 'wb') local_file.write(data) local_file.close()