import json
import uuid
from pathlib import Path
from IPython.display import HTML, display
from klotho.utils.playback.tonejs.cdn import (
cdn_scripts, INSTRUMENTS_JS_PATH, PLAYER_JS_PATH,
)
from klotho.utils.playback._helpers import convert_numpy_types
[docs]
class ToneEngine:
"""
Jupyter widget that renders Tone.js-based audio playback controls.
Generates an HTML/JS snippet with play/stop and loop buttons,
wiring the provided event list through Tone.js instruments.
Parameters
----------
events : list of dict
Audio event payload (typically from :func:`convert_to_events`).
custom_js_path : str or Path, optional
Path to a custom JavaScript file loaded before the player.
custom_js : str, optional
Inline JavaScript source embedded before the player.
"""
[docs]
def __init__(self, events, custom_js_path=None, custom_js=None):
self.events = convert_numpy_types(events)
self.custom_js_path = Path(custom_js_path) if custom_js_path else None
self.custom_js = custom_js
self.widget_id = f"klotho_{uuid.uuid4().hex[:8]}"
def _load_instruments_js(self):
"""Return the bundled instruments JavaScript source."""
return INSTRUMENTS_JS_PATH.read_text()
def _load_player_js(self):
"""Return the bundled player JavaScript source."""
return PLAYER_JS_PATH.read_text()
def _load_user_custom_js(self):
"""
Return user-supplied custom JavaScript, if any.
Returns
-------
str
Custom JS source, or an empty string when none is provided.
"""
if self.custom_js_path and self.custom_js_path.exists():
return self.custom_js_path.read_text()
if self.custom_js:
return self.custom_js
return ""
def _generate_html(self):
"""
Build the full HTML string for the playback widget.
Returns
-------
str
HTML containing inline styles, CDN scripts, instrument
definitions, and the player logic.
"""
payload_json = json.dumps(self.events)
user_custom_js = self._load_user_custom_js()
instruments_js = self._load_instruments_js()
player_js = self._load_player_js()
scripts = "\n".join([s for s in (user_custom_js, instruments_js, player_js) if s])
tone_script = cdn_scripts(include_tone=True)
html = f'''
<div id="{self.widget_id}" style="
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #1a1a2e;
border-radius: 6px;
user-select: none;
">
<button id="{self.widget_id}_toggle" style="
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
background: #16213e;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
">
<span id="{self.widget_id}_icon" style="
width: 0;
height: 0;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-left: 12px solid #4ade80;
margin-left: 3px;
"></span>
</button>
<button id="{self.widget_id}_loop" style="
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: #16213e;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
opacity: 0.5;
">
<svg id="{self.widget_id}_loop_svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a0a0a0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 2l4 4-4 4"></path>
<path d="M3 11v-1a4 4 0 0 1 4-4h14"></path>
<path d="M7 22l-4-4 4-4"></path>
<path d="M21 13v1a4 4 0 0 1-4 4H3"></path>
</svg>
</button>
</div>
{tone_script}
<script>
{scripts}
</script>
<script>
(() => {{
const toggleBtn = document.getElementById("{self.widget_id}_toggle");
const iconEl = document.getElementById("{self.widget_id}_icon");
const loopBtn = document.getElementById("{self.widget_id}_loop");
const loopSvg = document.getElementById("{self.widget_id}_loop_svg");
const payload = {payload_json};
const events = payload.events || [];
const instruments = globalThis.KLOTHO_BUILD_INSTRUMENTS(payload.instruments || {{}});
const player = KlothoPlayer.create();
let looping = false;
function setPlayIcon() {{
iconEl.style.width = "0";
iconEl.style.height = "0";
iconEl.style.borderTop = "7px solid transparent";
iconEl.style.borderBottom = "7px solid transparent";
iconEl.style.borderLeft = "12px solid #4ade80";
iconEl.style.borderRight = "none";
iconEl.style.marginLeft = "3px";
iconEl.style.background = "none";
}}
function setStopIcon() {{
iconEl.style.width = "12px";
iconEl.style.height = "12px";
iconEl.style.border = "none";
iconEl.style.borderRadius = "2px";
iconEl.style.marginLeft = "0";
iconEl.style.background = "#ef4444";
}}
loopBtn.onclick = () => {{
looping = !looping;
loopBtn.style.opacity = looping ? "1" : "0.5";
loopSvg.setAttribute("stroke", looping ? "#4ade80" : "#a0a0a0");
}};
toggleBtn.onclick = async () => {{
if (player.isPlaying()) {{
player.stop();
setPlayIcon();
}} else {{
setStopIcon();
await player.play(events, instruments, {{
loop: looping,
onFinish: () => setPlayIcon()
}});
}}
}};
}})();
</script>
'''
return html
[docs]
def display(self):
"""
Render the playback widget in the active Jupyter notebook.
Returns
-------
IPython.display.DisplayHandle
The display handle for the rendered HTML widget.
"""
html = self._generate_html()
return display(HTML(html))
def _repr_html_(self):
"""Return the widget HTML for automatic notebook rendering."""
return self._generate_html()