|
""" |
|
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. |
|
""" |
|
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(): |
|
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() |
|
|
|
|
|
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) |
|
|
|
|
|
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__": |
|
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) |
|
|