Spaces:
Running
on
Zero
Running
on
Zero
""" | |
These are utilities that allow one to embed an AudioSignal | |
as a playable object in a Jupyter notebook, or to play audio from | |
the terminal, etc. | |
""" # fmt: skip | |
import base64 | |
import io | |
import random | |
import string | |
import subprocess | |
from tempfile import NamedTemporaryFile | |
import importlib_resources as pkg_resources | |
from . import templates | |
from .util import _close_temp_files | |
from .util import format_figure | |
headers = pkg_resources.files(templates).joinpath("headers.html").read_text() | |
widget = pkg_resources.files(templates).joinpath("widget.html").read_text() | |
DEFAULT_EXTENSION = ".wav" | |
def _check_imports(): # pragma: no cover | |
try: | |
import ffmpy | |
except: | |
ffmpy = False | |
try: | |
import IPython | |
except: | |
raise ImportError("IPython must be installed in order to use this function!") | |
return ffmpy, IPython | |
class PlayMixin: | |
def embed(self, ext: str = None, display: bool = True, return_html: bool = False): | |
"""Embeds audio as a playable audio embed in a notebook, or HTML | |
document, etc. | |
Parameters | |
---------- | |
ext : str, optional | |
Extension to use when saving the audio, by default ".wav" | |
display : bool, optional | |
This controls whether or not to display the audio when called. This | |
is used when the embed is the last line in a Jupyter cell, to prevent | |
the audio from being embedded twice, by default True | |
return_html : bool, optional | |
Whether to return the data wrapped in an HTML audio element, by default False | |
Returns | |
------- | |
str | |
Either the element for display, or the HTML string of it. | |
""" | |
if ext is None: | |
ext = DEFAULT_EXTENSION | |
ext = f".{ext}" if not ext.startswith(".") else ext | |
ffmpy, IPython = _check_imports() | |
sr = self.sample_rate | |
tmpfiles = [] | |
with _close_temp_files(tmpfiles): | |
tmp_wav = NamedTemporaryFile(mode="w+", suffix=".wav", delete=False) | |
tmpfiles.append(tmp_wav) | |
self.write(tmp_wav.name) | |
if ext != ".wav" and ffmpy: | |
tmp_converted = NamedTemporaryFile(mode="w+", suffix=ext, delete=False) | |
tmpfiles.append(tmp_wav) | |
ff = ffmpy.FFmpeg( | |
inputs={tmp_wav.name: None}, | |
outputs={ | |
tmp_converted.name: "-write_xing 0 -codec:a libmp3lame -b:a 128k -y -hide_banner -loglevel error" | |
}, | |
) | |
ff.run() | |
else: | |
tmp_converted = tmp_wav | |
audio_element = IPython.display.Audio(data=tmp_converted.name, rate=sr) | |
if display: | |
IPython.display.display(audio_element) | |
if return_html: | |
audio_element = ( | |
f"<audio " | |
f" controls " | |
f" src='{audio_element.src_attr()}'> " | |
f"</audio> " | |
) | |
return audio_element | |
def widget( | |
self, | |
title: str = None, | |
ext: str = ".wav", | |
add_headers: bool = True, | |
player_width: str = "100%", | |
margin: str = "10px", | |
plot_fn: str = "specshow", | |
return_html: bool = False, | |
**kwargs, | |
): | |
"""Creates a playable widget with spectrogram. Inspired (heavily) by | |
https://sjvasquez.github.io/blog/melnet/. | |
Parameters | |
---------- | |
title : str, optional | |
Title of plot, placed in upper right of top-most axis. | |
ext : str, optional | |
Extension for embedding, by default ".mp3" | |
add_headers : bool, optional | |
Whether or not to add headers (use for first embed, False for later embeds), by default True | |
player_width : str, optional | |
Width of the player, as a string in a CSS rule, by default "100%" | |
margin : str, optional | |
Margin on all sides of player, by default "10px" | |
plot_fn : function, optional | |
Plotting function to use (by default self.specshow). | |
return_html : bool, optional | |
Whether to return the data wrapped in an HTML audio element, by default False | |
kwargs : dict, optional | |
Keyword arguments to plot_fn (by default self.specshow). | |
Returns | |
------- | |
HTML | |
HTML object. | |
""" | |
import matplotlib.pyplot as plt | |
def _save_fig_to_tag(): | |
buffer = io.BytesIO() | |
plt.savefig(buffer, bbox_inches="tight", pad_inches=0) | |
plt.close() | |
buffer.seek(0) | |
data_uri = base64.b64encode(buffer.read()).decode("ascii") | |
tag = "data:image/png;base64,{0}".format(data_uri) | |
return tag | |
_, IPython = _check_imports() | |
header_html = "" | |
if add_headers: | |
header_html = headers.replace("PLAYER_WIDTH", str(player_width)) | |
header_html = header_html.replace("MARGIN", str(margin)) | |
IPython.display.display(IPython.display.HTML(header_html)) | |
widget_html = widget | |
if isinstance(plot_fn, str): | |
plot_fn = getattr(self, plot_fn) | |
kwargs["title"] = title | |
plot_fn(**kwargs) | |
fig = plt.gcf() | |
pixels = fig.get_size_inches() * fig.dpi | |
tag = _save_fig_to_tag() | |
# Make the source image for the levels | |
self.specshow() | |
format_figure((12, 1.5)) | |
levels_tag = _save_fig_to_tag() | |
player_id = "".join(random.choice(string.ascii_uppercase) for _ in range(10)) | |
audio_elem = self.embed(ext=ext, display=False) | |
widget_html = widget_html.replace("AUDIO_SRC", audio_elem.src_attr()) | |
widget_html = widget_html.replace("IMAGE_SRC", tag) | |
widget_html = widget_html.replace("LEVELS_SRC", levels_tag) | |
widget_html = widget_html.replace("PLAYER_ID", player_id) | |
# Calculate width/height of figure based on figure size. | |
widget_html = widget_html.replace("PADDING_AMOUNT", f"{int(pixels[1])}px") | |
widget_html = widget_html.replace("MAX_WIDTH", f"{int(pixels[0])}px") | |
IPython.display.display(IPython.display.HTML(widget_html)) | |
if return_html: | |
html = header_html if add_headers else "" | |
html += widget_html | |
return html | |
def play(self): | |
""" | |
Plays an audio signal if ffplay from the ffmpeg suite of tools is installed. | |
Otherwise, will fail. The audio signal is written to a temporary file | |
and then played with ffplay. | |
""" | |
tmpfiles = [] | |
with _close_temp_files(tmpfiles): | |
tmp_wav = NamedTemporaryFile(suffix=".wav", delete=False) | |
tmpfiles.append(tmp_wav) | |
self.write(tmp_wav.name) | |
print(self) | |
subprocess.call( | |
[ | |
"ffplay", | |
"-nodisp", | |
"-autoexit", | |
"-hide_banner", | |
"-loglevel", | |
"error", | |
tmp_wav.name, | |
] | |
) | |
return self | |
if __name__ == "__main__": # pragma: no cover | |
from audiotools import AudioSignal | |
signal = AudioSignal( | |
"tests/audio/spk/f10_script4_produced.mp3", offset=5, duration=5 | |
) | |
wave_html = signal.widget( | |
"Waveform", | |
plot_fn="waveplot", | |
return_html=True, | |
) | |
spec_html = signal.widget("Spectrogram", return_html=True, add_headers=False) | |
combined_html = signal.widget( | |
"Waveform + spectrogram", | |
plot_fn="wavespec", | |
return_html=True, | |
add_headers=False, | |
) | |
signal.low_pass(8000) | |
lowpass_html = signal.widget( | |
"Lowpassed audio", | |
plot_fn="wavespec", | |
return_html=True, | |
add_headers=False, | |
) | |
with open("/tmp/index.html", "w") as f: | |
f.write(wave_html) | |
f.write(spec_html) | |
f.write(combined_html) | |
f.write(lowpass_html) | |