Getting Started#

Summary

In this tutorial, we’ll see how to:

  • generate and shape sounds by creating audio nodes and connecting them together

  • control audio node attributes or parameters with Python code

  • link audio node attributes or parameters with other widgets

Note: you can download this tutorial as a Jupyter notebook.

import ipytone

First, let’s create a simple oscillator (i.e., a source audio node that generates an audio signal using a basic waveform).

osc = ipytone.Oscillator(volume=-5)

Like every other ipytone widget, an Oscillator has no visible output.

osc
Oscillator()

Audio nodes#

Like many other audio processing software, Tone.js and ipytone are built around the concept of audio nodes. These consist of logical audio processing units that can be connected to each other via their input / output anchors, letting the audio signal flow from source nodes to the destination node (i.e., the “speakers”).

Ipytone audio nodes input / output may be accessed via their input / output properties, which each either return another audio node object or None. For example, an Oscillator has no input (source node) and has a Volume node as output:

osc.input is None
True
osc.output
Volume(volume=Param(value=-5.0, units='decibels'), mute=False)

Connecting audio nodes#

The Oscillator object created above won’t make any sound until it is connected to the destination. To connect it directly to the destination, you can use the connect method like below or the more convenient to_destination method.

# connects the oscillator (output) to the destination (input)
osc.connect(ipytone.destination)
Oscillator()

Note that the oscillator still won’t make any sound. You need to start / stop it explicitly (all source nodes have start and stop methods):

osc.start()
Oscillator()
osc.stop()
Oscillator()

To disconnect the oscillator from the destination, you can use disconnect:

osc.disconnect(ipytone.destination)
Oscillator()

More advanced connections are possible. In addition to connect, the chain and fan convenience methods can be used to make multiple connections at once (resp. in serial and in parallel).

An example with a Filter node inserted between the oscillator and the destination:

filtr = ipytone.Filter(type="highpass", frequency=1000)
# connects in chain oscillator -> filter -> destination
osc.chain(filtr, ipytone.destination)
Oscillator()

Method chaining#

Most audio node methods return the node object itself, which allows chaining them for convenience, e.g.,

# start the oscillator now and stop it after 1 second
osc.start().stop("+1")
Oscillator()

Audio node controls#

An ipytone audio node may have several properties that are synchronized with the front-end and that can be used for a fine-grained control on the generated or processed audio.

The value of those properties generally corresponds to either:

  • a “simple” value of int, float or str, etc. basic type

  • an instance of Param or Signal, which holds some metadata in addition to the actual value (e.g., units) and provides convenient methods that can be used to schedule value changes (see below).

Basic node properties#

Basic audio node property values can be read/written directly with Python.

An example with the oscillator type (i.e., its waveform shape):

osc.type
'sine'
osc.type = "triangle"

osc.start().stop("+1")
Oscillator()

Param and Signal#

When an audio node property is a Param or Signal instance, the actual value can be read / written via the value property of that instance.

An example with the oscillator frequency:

osc.frequency
Signal(value=440.0, units='frequency')
osc.frequency.value
440.0
osc.frequency.value = 800

osc.start().stop("+1")
Oscillator()

Controlling audio nodes with Python#

“Pure-Python” example#

In the example below the frequency of the oscillator is gradually increased by directly setting the frequency value within a Python function.

import time

def linear_ramp_to(value, ramp_time):
    n = 100
    time_step = ramp_time / n
    freq_step = (value - osc.frequency.value) / n
    for i in range(n):
        time.sleep(time_step)
        osc.frequency.value += freq_step
osc.frequency.value = 440

osc.start()
linear_ramp_to(800, 3)
osc.stop()
Oscillator()

Although it is working, this solution is not optimal:

  • the Python interpreter is blocked while the frequency of the oscillator is updated (although there might be ways to make it non-blocking)

  • the actual frequency update steps in the front-end may not happen at an “audio-friendly” accuracy (slow data transfer between the Python kernel and the front-end can make things even worse)

Using ipytone (Tone.js) scheduling#

The same effect than in the example above can be achieved with ipytone method calls.

Those calls send a few messages in the front-end, which are then processed right-away to schedule a few events at specific times (via Tone.js and utlimately via the Web Audio API). This approach overcomes the limitations of the “pure-Python” solution above (i.e., non-blocking and more accurate scheduling).

osc.frequency.value = 440
osc.start().stop("+3")
osc.frequency.linear_ramp_to(800, 3)
osc.frequency.set_value_at_time(440, "+3")
Signal(value=440.0, units='frequency')

Important

When triggered in the front-end, scheduled events will not update and/or synchronize the value property of a Param or Signal in Python. This property thus won’t always return the current, actual value.

Ipytone provides other ways to track value updates from Python. See the Synchronizing Audio State tutorial.

Controling audio nodes with (ipy)widgets#

Ipytone audio nodes are widgets and can thus be integrated with other widgets for more interactive control, e.g., via widget events (i.e., observe, link, jslink).

Here is a basic example with a few UI widgets to control the oscillator type, frequency and playback state.

import ipywidgets

freq_slider = ipywidgets.FloatSlider(
    value=440,
    min=100,
    max=1000,
    step=1,
)

type_dropdown = ipywidgets.Dropdown(
    options=['sine', 'square', 'sawtooth', 'triangle'],
    value='sine',
)

toggle_play_button = ipywidgets.ToggleButton(
    value=False,
    description="Start/Stop"
)

ipywidgets.jslink((freq_slider, 'value'), (osc.frequency, 'value'))
ipywidgets.link((type_dropdown, 'value'), (osc, 'type'))

def start_stop_osc(change):
    if change['new']:
        osc.start()
    else:
        osc.stop()

toggle_play_button.observe(start_stop_osc, names='value')

ipywidgets.VBox([freq_slider, type_dropdown, toggle_play_button])

Dispose audio nodes#

If audio nodes are not used anymore, it is recommended to dispose it. Disposing a node instance means that all of its underlying Web Audio nodes are disconnected and freed for garbage collection in the front-end.

osc.dispose()
Oscillator(disposed=True)
osc.disposed
True
filtr.dispose()
Filter(disposed=True, type='highpass', frequency=Signal(disposed=True, value=1000.0, units='frequency'), q=Signal(disposed=True, value=1.0, units='positive'))

Note

When the close() method of an ipytone node widget is called, the node is automatically disposed.