Nodes
Node inputs
A node has three different classes of input:
- Audio-rate inputs: Takes the output of another node as an input, for continuous modulation of synthesis parameters
- Trigger inputs: Used to trigger discrete control events — for example, restarting buffer playback
- Buffer inputs: Used to pass the contents of an audio buffer to a node — for example, as a source of audio samples, or an envelope shape
Audio-rate inputs
Virtually every node has one or more audio-rate inputs. Put simply, an audio-rate input is the output of another node. Let's look at a short example:
lfo = SineLFO()
signal = SquareOscillator(frequency=200, width=lfo)
In this case, we are passing the output of a SineLFO
as the pulse width of a SquareOscillator
. This is an audio-rate input.
Although it's not obvious, the frequency
parameter is also an audio-rate input. Any constant value (such as the 200
here) is behind the scenes implemented as a Constant
node, which continuously outputs the value at an audio rate.
All audio-rate inputs can be modified just like a normal Python property. For example:
signal.frequency = TriangleOscillator(0.5, 100, 1000)
Variable input nodes
Some nodes have a variable number of inputs, which can change over the Node's lifetime. For example, Sum()
takes an arbitrary number of input Nodes, and generates an output which is the sum of all of its inputs.
For variable-input nodes such as this, audio-rate inputs are added with add_input()
, and can be removed with remove_input()
.
a = Constant(1)
b = Constant(2)
c = Constant(3)
sum = Sum()
sum.add_input(a)
sum.add_input(b)
sum.add_input(c)
# sum will now generate an output of 6.0
It is possible to check whether a Node object takes variable inputs by querying node.has_variable_inputs
.
Triggers
When working with sequencing and timing, it is often useful be able to trigger discrete events within a node. This is where trigger inputs come in handy.
There are two different ways to handle trigger inputs:
- by calling the
trigger()
method on aNode
- by passing a Node to an input that corresponds to an audio-rate trigger
Calling trigger()
To generate trigger events at arbitrary times, call node.trigger()
. For example:
freq_env = Line(10000, 100, 0.5)
sine = SineOscillator(freq_env)
sine.play()
while True:
freq_env.trigger()
graph.wait(1)
This is useful because it can be done outside the audio thread. For example, trigger()
could be called each time a MIDI note event is received.
The trigger()
method takes an optional name
parameter, which is used by Node
classes containing more than one type of trigger. This example uses the set_position
trigger of BufferPlayer
to seek to a new location in the sample every second.
buffer = Buffer("../audio/stereo-count.wav")
player = BufferPlayer(buffer, loop=True)
player.play()
while True:
player.trigger("set_position", random_uniform(0, buffer.duration))
graph.wait(1)
Note
Because the trigger
method happens outside the audio thread, it will take effect at the start of the next audio block. This means that, if you are running at 44.1kHz with an audio buffer size of 1024 samples, this could introduce a latency of up to 1024/44100 = 0.023s
. For time-critical events like drum triggers, this can be minimised by reducing the hardware output buffer size.
This constraint also means that only one event can be triggered per audio block. To trigger events at a faster rate than the hardware buffer size allows, see Audio-rate triggers below.
Audio-rate triggers
It is often desirable to trigger events using the audio-rate output of another Node object as a source of trigger events, to give sample-level precision in timing. Most nodes that support trigger
inputs can also be triggered by a corresponding audio-rate input.
Triggers happen at zero-crossings — that is, when the output of the node passes above zero (i.e., from <= 0
to >0
). For example, to create a clock with an oscillating tempo to re-trigger buffer playback:
clock = Impulse(SineLFO(0.2, 1, 10))
buffer = Buffer("examples/audio/stereo-count.wav")
player = BufferPlayer(buffer, loop=True, clock=clock)
player.play()
This can be used to your advantage with the boolean operator nodes.
on_the_right = MouseX() > 0.5
envelope = ASREnvelope(0, 0, 0.5, clock=on_the_right)
square = SquareOscillator(100)
output = envelope * square * 0.1
output.play()
TODO: Should the name of the trigger() event always be identical to the trigger input name? So clock
for envelopes, buffer player, etc...?
Buffer inputs
The third type of input supported by nodes is the buffer. Nodes often take buffer inputs as sources of audio samples. They are also useful as sources of envelope shape data (for example, to shape the grains of a Granulator), or general control data (for example, recording motion patterns from a MouseX
input).
buffer = Buffer("audio/stereo-count.wav")
player = BufferPlayer(buffer, loop=True)
A buffer input cannot be set using the same property shorthand as audio-rate inputs; instead, the set_buffer
method should be used.
new_buffer = Buffer("audio/example.wav")
player.set_buffer("buffer", new_buffer)
TODO: Should this be set_input
for consistency with Patch?
Enumerating a node's inputs
To list the potential and actual inputs of a node, the .inputs
property returns a dict
of key-value pairs:
>>> player.inputs
{
'clock': <signalflow.Impulse at 0x107778eb0>,
'end_time': None,
'loop': <signalflow.Constant at 0x12a4cd4b0>,
'rate': <signalflow.Constant at 0x12a4cd330>,
'start_time': None
}
Any constant-valued inputs are wrapped inside a special Constant
node class. The value contained by a Constant
can be accessed with its .value
property.
>>> player.inputs["rate"].value
1.0
>>> player.inputs["rate"].value
1.0
Created: 2022-04-01