I’m playing around a bit with Pluto and web-audio. Right now I’m just creating an oscillator with the frequency controlled by a slider. The code I have to do this is:
htl"""
<script id="osctest">
const div = this == null ? document.createElement("div") : this
const ctx = this == null ? new AudioContext() : div.ctx
const osc = this == null ? new OscillatorNode(ctx) : div.osc
osc.frequency.setValueAtTime($freq, ctx.currentTime)
if(this == null) {osc.connect(ctx.destination); osc.start()}
// invalidation.then(() => {
// osc.stop()
// ctx.close()
// console.log("invalidated")
// })
div.osc = osc
div.ctx = ctx
return div
</script>
"""
This works well, but the problem is that if I re-run the cell, this is null again so I end up creating a brand new oscillator and context, and now both are playing, rather than replacing the original.
As you can see in the commented section, I tried using the invalidate promise, but then realized that it gets called on a value update, not just a user-generated re-run. One solution I think would be to attach the context upstream in the DOM or to a global variable or something, but then it can’t be a re-useable function.
If there were something like invalidate that only got triggered on a user update, I could do the cleanup there.
Here is a minimal working example I managed to make work using the MutationObserver API
@htl """
<script id="osctest">
let div = this ?? document.createElement("div")
if (this == null) {
div.classList.toggle('oscillator-container',true)
let id = $(join(rand("abcdefghilmnopqrstuvz",8)))
div.id = id
div.ctx = new AudioContext()
div.osc = new OscillatorNode(div.ctx)
div.osc.connect(div.ctx.destination); div.osc.start()
const observer = new MutationObserver((entries) => {
for (const entry of entries) {
for (const added of entry.addedNodes) {
if (added instanceof Element && added.classList.contains('oscillator-container') && added.id != id) {
console.log('Found a manual rerun! Disconnecting...')
div.osc.stop()
div.ctx.close()
observer.disconnect()
}
}
}
})
observer.observe(currentScript.closest('div.raw-html-wrapper'), {childList: true})
}
div.osc.frequency.setValueAtTime($freq, div.ctx.currentTime)
return div
</script>
"""
The main idea is to assign a unique id to the div you are generating in the script only upon manual re-run (when this == null) and then attach a MutationObserver to the pluto-output wrapper that monitors addition and deletion of children.
As soon as a new div with the oscillator-container class is added and has an id which is different from the one you generated together with the MutationObserver, I assume that a manual rerun has been triggered and I can disconnect everything.
Seems to work, don’t know if there are better ways to achieve this though.
Had a similar problem with plotting - wanted to have movable isosurfaces etc. My solution is here (Julia) and here (JS).
Essentially you create a div labeled with an uuid which you can later refer to in the javascript.
On the one hand it seems like it might be nice for Pluto to have some built-in support for this kind of thing, but also it probably is only coming up here because of the weird way that the lifetime of WebAudio objects works.