Search code examples
pythonaudioholoviewspyvizpanel-pyviz

Sync HoloViews VLine with PyViz Panel audio.time


I want to visualize in a HoloViews plot where the current audio is in the graph. This line should update automatically when PyViz's pn.pane.Audio.time value is changed (when audio is being played or Audio.time is changed).

My attempt:

# Python 3.7 in JupyterLab
import numpy as np
import holoviews as hv  # interactive plots
hv.notebook_extension("bokeh")
import panel as pn
pn.extension()
from holoviews.streams import Stream, param

# create sound
sps = 44100 # Samples per second
duration = 10 # Duration in seconds
modulator_frequency = 2.0
carrier_frequency = 120.0
modulation_index = 2.0

time = np.arange(sps*duration) / sps
modulator = np.sin(2.0 * np.pi * modulator_frequency * time) * modulation_index
carrier = np.sin(2.0 * np.pi * carrier_frequency * time)
waveform = np.sin(2. * np.pi * (carrier_frequency * time + modulator))
waveform_quiet = waveform * 0.3
waveform_int = np.int16(waveform_quiet * 32767)

# PyViz Panel's Audio widget to play sound
audio = pn.pane.Audio(waveform_int, sample_rate=sps)
# generated plotting data
x = np.arange(11.0)
y = np.arange(11.0, 0.0, -1) / 10
y[0::2] *= -1  # alternate positve-negative
# HoloViews line plot
line_plot = hv.Curve((x, y)).opts(width=500)

# should keep track of audio.time; DOES NOT WORK
Time = Stream.define('Time', t=param.Number(default=0.0, doc='A time parameter'))
time = Time(t=audio.time)

# callback to draw line when time value changes
def interactive_play(t):
    return hv.VLine(t).opts(color='green')

# dynamic map plot of line for current audio time
dmap_time = hv.DynamicMap(interactive_play, streams=[time])

# display Audio pane
display(audio)
# combine plot with stream of audio.time
line_plot * dmap_time

Why does this not work?

Since time is set as a param.Number(), I expect this to keep track of audio.time. Therefore, when the audio is being played, the callback to interactive_play() should constantly be called, resulting in a Line moving over the plot. This does not happen, and the line only stays at the default 0.0 (or any other value audio.time has at time of code execution).

How do I update the VLine to keep tracking audio.time?

Green line should match Audio pane's time

holoviews panel audio-sync


Solution

  • Since time is set as a param.Number(), I expect this to keep track of audio.time.

    In your example you are not linking the Panel Audio object to the stream in any way. All you're doing when you do this:

    time = Time(t=audio.time)
    

    is set the initial value of your Time stream to the current value of the Audio Pane. audio.time is not a reference to the parameter it is just the current value of that parameter.

    HoloViews DynamicMaps have supported the ability to listen to parameters on other objects for quite a while now. There are two main ways to go about this, either by doing something like this:

    @pn.depends(t=audio.param.time)
    def interactive_play(t):
        return hv.VLine(t).opts(color='green')
    
    dmap_time = hv.DynamicMap(interactive_play)
    

    Here you are decorating the interactive_play function with a dependency on the audio.time parameter so whenever it changes the DynamicMap is updated. The more explicit way of doing this and what actually happens internally is:

    from holoviews.streams import Params
    
    def interactive_play(t):
        return hv.VLine(t).opts(color='green')
    
    stream = Params(parameters=[audio.param.time], rename={'time': 't'})
    dmap_time = hv.DynamicMap(interactive_play, streams=[stream])
    

    If you need to update the Audio panes with new files I would strongly recommend you do that using callbacks instead of constantly creating new Audio panes using the interact or reactive APIs. That way you also don't have to deal with updating the stream to constantly changing Audio panes.