# 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()