""" 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" " ) 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)