EzAudio / audiotools /core /playback.py
OpenSound's picture
Upload 33 files
71de706 verified
raw
history blame
8.1 kB
"""
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)