Spaces:
Running
on
Zero
Running
on
Zero
File size: 8,096 Bytes
9d3cb0a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
"""
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)
|