This page was generated from examples/timedqueue-examples.ipynb.

TimedQueue

Motivation:

  • for sonifications (or any sound composition) with precise timing, usually a large number of events need to be spawned at the exact time.

  • doing this with bundles doesn’t work as the OSC buffer of scsynth is limited

  • it needs a TimedQueue where events can be added for time-precise dispatching

  • In TimedQueue, a thread then simply checks what items are due and executes them

  • allowing arbitrary functions as objects to be queued for execution enables both sound-specific usecases (e.g. sending OSC messages/bundles) and also other things such as visualization

  • However, the functions should complete really quickly as otherwise the queue would run late and fail to process due events

  • hence, it remains in the user’s responsibility to be careful

  • If, however, longer programs are needed, functions can be spawned as threads on execution

Basic Demo of TimedQueue

The following demo illustrate the core functionality with console print and sound. Please check the console / shell from which you have launched jupyter-notebook (i.e. your stdout)

[ ]:
import sys, os, time, random
import numpy as np

import sc3nb as scn
[ ]:
sc = scn.startup()
[ ]:
queue = scn.TimedQueue()
[ ]:
def myfun(x):
    os.write(1, "{}\n".format(x).encode())
    sys.stderr.flush()

def myblip(freq):
    sc.server.msg("/s_new", ["s1", -1, 1, 0, "freq", freq, "num", 3])
[ ]:
myfun(4)  # the number is written to stdout
[ ]:
myblip(700) # a tone should play
[ ]:
t0 = time.time()
for i in range(50):
    queue.put(t0+i*0.04, myblip, 800+1*7*i)
    queue.put(t0+i*0.04, myfun,  400+30*i)  # plots on stderr = console
print(time.time()-t0)

Note that the code returns immediately, allowing you to interact with jupyter. All executions are then done by means of the TimedQueue

[ ]:
queue.close()

TimedQueueSC

To allow easy and fast usage of SC messages and bundles TimedQueueSC was created

  • put_msg(onset, address, params)allows to send a message from python at onset.

  • put_bundler(onset, bundler)allows to send a bundler from python at onset.

TimedQueueSC example with synchronized sound and mpl plot

  • This example shows how to highlight data points as they are played.

  • However, the marking is reset for every new data point, i.e. data points are not highlighted as long as the corresponding sound lasts

  • to achieve that, see code example below

Note that there are sometimes some strange effects with matplotlib event loop hickups in Mac, it is untested with Linux or Windows, any problem reports or suggested solutions are welcome.

[ ]:
import matplotlib
import matplotlib.pyplot as plt
%matplotlib qt5
[ ]:
# create some test data
data = np.vstack((np.random.randn(50, 5), np.random.randn(100, 5)+3.5))
[ ]:
# create figure, axis, plots -> a window should open depicting two clusters
fig, ax = plt.subplots(1)  # create figure
mngr = plt.get_current_fig_manager(); mngr.window.setGeometry(1200, 0, 500, 400)
pldata, = ax.plot(data[:,1], data[:,2], ".", ms=5) # create plots
plmarked, = ax.plot([], [], "ro", ms=5, lw=0.5)
plt.show(block=False); plt.ion(); fig.canvas.draw() # not needed if plot shows

# create the queue
queue = scn.TimedQueueSC()
[ ]:
def update_plot(x, y):
    global fig, ax, pldata, plmarked
    plmarked.set_data([x], [y])
    ax.draw_artist(ax.patch)
    ax.draw_artist(pldata)
    ax.draw_artist(plmarked)
    fig.canvas.update() # additional fig.canvas.flush_events() not needed?
[ ]:
t0 = time.time()
for i, r in enumerate(data):
    onset = t0 + scn.linlin(r[1], data[:,1].min(), data[:,1].max(), 0.1, 4) + random.random()*0.2 + 0.2
    freq = scn.midicps(scn.linlin(r[2], 2, 5, 60, 80))
    pos = scn.linlin(r[4], 0, 2, -1, 1)
    queue.put_bundler(onset-0.2, scn.Bundler(onset, "/s_new", ["s1", -1, 1, 0, "freq", freq, "amp", 0.05, "dur", .52, "pos", pos]))
    queue.put(onset, update_plot, (r[1], r[2]), spawn=False)
print(f'time used: {time.time() - t0}')

Notice that any data point is turned red the moment its sound starts.

TimedQueueSC PMSon with matplotlib highlights

The following example illustrates how to use TimedQueues to maintain a ‘currently playing selection’ of data points, so that the GUI highlighting is deactivated when the corresponding sound stops

  • this is achieved by scheduling a select and an unselect function at corresponding sound onset and stop time

  • Note that here the plot update is done within a second loop of scheduled ‘update_plot’ invocations, at an frame rate independent of the sound events.

[ ]:
data = np.vstack((np.random.randn(300, 7), np.random.randn(300, 7)+5))
[ ]:
# create figure
fig, ax = plt.subplots(1)  # create figure
mngr = plt.get_current_fig_manager()
mngr.window.setGeometry(1200, 0, 500, 400)
plt.show()

# create the queue
queue = scn.TimedQueueSC()
[ ]:
def mapcol(row, stats, col, val_from, val_to):  # helper for mapping
    return scn.linlin(row[col], stats[col, 0], stats[col, 1], val_from, val_to)

def select(i):  #  highlight selection
    selected[i] = True

def unselect(i): # lowlight selection
    selected[i] = False

def update_plot(xs, ys):
    global fig, ax, pldata, plmarked, selected
    plmarked.set_data(xs[selected], ys[selected])
    ax.draw_artist(ax.patch)
    ax.draw_artist(pldata)
    ax.draw_artist(plmarked)
    fig.canvas.flush_events()
    fig.canvas.update()

# parameter mapping sonification with GUI
tot_dur = 5  # total duration of the sonification
max_ev_dur = 5.5  # maximal event duration
delay = 1  # offset

stats = np.vstack((np.min(data, 0), np.max(data, 0))).T
selected = np.zeros(np.shape(data)[0], bool)

# create axis, plots
ax.clear()
plmarked, = ax.plot([], [], "ro", ms=4, lw=0.5)
pldata, = ax.plot(data[:,1], data[:,2], ".", ms=2) # create plots

t0 = time.time()

for i, r in enumerate(data):
    onset = t0 + delay + 5* i/800 # mapcol(r, stats, 3, 0, tot_dur)
    freq  = scn.midicps( mapcol(r, stats, 2, 60, 90))
    ev_dur = mapcol(r, stats, 4, 0.2, max_ev_dur)
    # sonification
    synth_args = ["s1", -1, 1, 0, "freq", freq, "amp", 0.05, "dur", ev_dur, "pos", pos]
    bundler = scn.Bundler(onset, "/s_new", synth_args)
    queue.put_bundler(onset-delay, bundler)
    # on/off events of marker highlight
    queue.put(onset, select, i)
    queue.put(onset + ev_dur, unselect, i)

# update plot at given rate from earliest to latext time
for t in np.arange(t0, t0+delay+tot_dur+ev_dur+1, 1/10):  # 1 / update rate
    queue.put(t, update_plot, (data[:,1], data[:,2]))

TimedQueueSC PMSon with timeseries data and matplotlib

The following example illustrates howto create a continuous sonification with concurrent plotting the time in a plot

  • This presumes time-indexable data

  • a ‘maximum onset’ variable is maintained to shutdown the continuously playing synths when done

  • note that the highlight will only replot the marker, required time is thus independent of the amount of data plotted in the other plot.

[ ]:
ts = np.arange(0, 20, 0.01)
data = np.vstack((ts,
                  np.sin(2.5*ts) + 0.01*ts*np.random.randn(np.shape(ts)[0]),
                  0.08*ts[::-1]*np.cos(3.5*ts)**2)).T
[ ]:
# create figure
fig, ax = plt.subplots(1)  # create figure
mngr = plt.get_current_fig_manager(); mngr.window.setGeometry(1200, 0, 500, 400)

# create axis, plots
ax.clear()
plmarked, = ax.plot([], [], "r-", lw=1)
pldata1, = ax.plot(data[:,0], data[:,1], "-", ms=2) # create plot 1
pldata2, = ax.plot(data[:,0], data[:,2], "-", ms=2) # create plot 2
[ ]:
# create the queue
queue = scn.TimedQueueSC()

def mapcol(row, stats, col, val_from, val_to):  # helper for mapping
    return scn.linlin(row[col], stats[col, 0], stats[col, 1], val_from, val_to)

def update_plot(t):
    global fig, ax, pldata1, pldata2, plmarked, selected
    plmarked.set_data([t,t], [-10000, 10000])
    ax.draw_artist(ax.patch)
    ax.draw_artist(pldata1)
    ax.draw_artist(pldata2)
    ax.draw_artist(plmarked)
    fig.canvas.update()
    # fig.canvas.flush_events()

stats = np.vstack((np.min(data, 0), np.max(data, 0))).T
selected = np.zeros(np.shape(data)[0], bool)

# parameter mapping sonification with GUI
delay = 0.5
rate = 2

t0 = time.time()
queue.put_msg(t0, "/s_new", ["s2", 1200, 1, 0, "amp", 0])
queue.put_msg(t0, "/s_new", ["s2", 1201, 1, 0, "amp", 0])

max_onset = 0
latest_gui_onset = 0
gui_frame_rate = 60

ts = []
for i, r in enumerate(data[::2, :]):
    ts.append(time.time()-t0)
    if i==0: tmin = r[0]
    onset = (r[0]-tmin)/rate
    freq  = scn.midicps( mapcol(r, stats, 1, 60, 70))
    freqR = 0.5 * scn.midicps( mapcol(r, stats, 2, 70, 80))

    # sonification
    tt = t0 + delay + onset
    if tt > max_onset: max_onset = tt
    bundler = scn.Bundler(tt)
    bundler.add(0, "/n_set", [1200, "freq", freq, "num", 4, "amp", 0.2, "pan", -1, "lg", 0])
    bundler.add(0, "/n_set", [1201, "freq", freqR, "num", 1, "amp", 0.1, "pan", 1])
    queue.put_bundler(tt-0.2, bundler)
    if tt > latest_gui_onset + 1/gui_frame_rate:  # not more than needed gui updates
        latest_gui_onset = tt
        queue.put(tt, update_plot, (r[0],), spawn=False)
queue.put_msg(max_onset, "/n_free", [1200])
queue.put_msg(max_onset, "/n_free", [1201])

# queue.join()
print(time.time()-t0)
[ ]:
sc.exit()