How to play audio and dynamically change src

i’d like to have a bokeh button play a wav file that the user selects with a file dialog. i can get this to work statically, but not dynamically.

in “templates/index.html” i put <audio id="audio_context" controls></audio>.

then i create a button and add a javascript callback to it which sets the source from a python variable:

    play = Button(label='play')
    play.js_on_event(ButtonClick, \
      CustomJS(args=dict(context_audio=context_audio), code="""
var aud = document.getElementById("audio_context");
aud.src="data:audio/wav;base64,"+context_audio;
"""))

this works when play.js_on_event is called before doc.add_root(), but not afterwords. so if i have a way for context_audio to change, say via a file dialog, and then call play.js_on_event again, the new callback seems to not be registered.

i searched for a solution and found a merged PR which nominally claims to have made it possible to change CustomJS callbacks dynamically. but this does not work for me with bokeh 1.4.

is there another way to do this? thanks.

@bjarthur70 Sometimes it’s just really difficult to say much without seeing/running real code. If you can provide a minimal, complete, reproducer, I would be happy to take a closer look.

thanks for the offer to help! here is an explicit example:

$ tree audio
audio
├── main.py
└── templates
    └── index.html

1 directory, 2 files

$ cat main.py 
from bokeh.plotting import curdoc
from bokeh.layouts import row, widgetbox
from bokeh.models.widgets import Button
from bokeh.events import ButtonClick
from bokeh.models.callbacks import CustomJS
import logging 

bokehlog = logging.getLogger("deepsong") 
bokehlog.setLevel(logging.INFO)

play_audio = Button(label='play audio')
play_audio.js_on_event(ButtonClick, \
                       CustomJS(args=dict(fakedata="hide-and-seek"),
                                code="""
var aud = document.getElementById("audio_tag");
aud.src="data:audio/wav;base64,"+fakedata;
aud.play();
"""))

def change_audio_callback():
    bokehlog.info("entering `change_audio_callback`") 
    play_audio.js_on_event(ButtonClick, \
                           CustomJS(args=dict(fakedata="peek-a-boo"),
                                    code="""
var aud = document.getElementById("audio_tag");
aud.src="data:audio/wav;base64,"+fakedata;
aud.play();
"""))

change_audio = Button(label='change audio')
change_audio.on_click(change_audio_callback)

curdoc().add_root(row(play_audio, change_audio))

$ cat templates/index.html 
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    {{ bokeh_css }}
    {{ bokeh_js }}
    <link rel="stylesheet" href="gui/static/css/styles.css"/>
  </head>
  <body>
  <audio id="audio_tag" controls></audio>
  {{ plot_div|indent(8) }}
  {{ plot_script|indent(8) }}
  </body>
</html>

to serve i use:

# bokeh serve --allow-websocket-origin=`hostname`:5006 --allow-websocket-origin=localhost:5006 --port 5006 --show audio

i then open chrome, navigate to “localhost:5006”, open the javascript console, click on Elements, and examine the DOM of the audio HTML tag. initially it has no src field. when i click on the bokeh “play audio” button, the audio src field appears and is set to data:audio/wav;base64,hide-and-seek, as expected. i then click “change audio” and confirm that "entering change_audio_callback" is logged. then i click on “play audio” again, and though the audio tag line in the Elements window blinks to indicate is has been updated, the src field remains “hide-and-seek” instead of changing to “peek-a-boo”.

1 Like

There’s at least one definite issue, and a suggestion for a different approach that I would take.

The first issue is that the args dict cannot pass arbitrary values [1]. The only thing that it can synchronize is other Bokeh models. So you can’t pass the encoded audio data that way (but also don’t need to, see next point).

The other comment I would make is that it is always the best practice with Bokeh to make the smallest change possible. In concrete terms that usually mean: change a property on an existing model, don’t replace models wholesale. And very specifically here, that means updating the code property of the existing callback, not making a new callback. (I’m not actually sure if swapping out the CustomJS callback works, I changed to how I would actually write things in practice before even trying). Below is a complete updated example:

from bokeh.plotting import curdoc
from bokeh.layouts import row
from bokeh.models.widgets import Button
from bokeh.events import ButtonClick
from bokeh.models.callbacks import CustomJS
import logging

bokehlog = logging.getLogger("deepsong")
bokehlog.setLevel(logging.INFO)

CODE = """
const aud = document.getElementById("audio_tag")
aud.src="data:audio/wav;base64,"+%r
console.log(aud.src)
aud.play();
"""

callback = CustomJS(code=CODE % "hide-and-seek")

play_audio = Button(label='play audio')
play_audio.js_on_event(ButtonClick, callback)

def change_audio_callback():
    bokehlog.info("entering `change_audio_callback`")
    callback.code = CODE % "peek-a-boo"

change_audio = Button(label='change audio')
change_audio.on_click(change_audio_callback)

curdoc().add_root(row(play_audio, change_audio))

  1. Though note there is an open issue, hopefully implemented soon, to add a “Namespace” model that would make it simple to send fairly arbitrary data via args. Once that is available, the code can remain fixed, more like your original code, and you could just update a property you define on the namespace model for the audio data. ↩︎

thanks so much!

1 Like