Realtime Servers¶
Supriya’s Server
provides a handle to a
scsynth
process, allowing you to control the process’s lifecycle, interact
with the entities it governs, and query its state.
Lifecycle¶
Instantiate a server with:
>>> server = supriya.Server()
Instantiated servers are initially offline:
>>> server
<Server OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
To bring an offline server online, boot the server:
>>> server.boot()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
Quit a running server:
>>> server.quit()
<Server OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
Booting without any additional options will use default settings for the
scsynth server process, e.g. listening on the IP address 127.0.0.1
and port 57110
, and will automatically attempt to detect the location of the
scsynth binary via supriya.scsynth.find()
.
You can override the IP address or port via keyword arguments:
>>> server.boot(ip_address="0.0.0.0", port=56666)
<Server ONLINE [/usr/local/bin/scsynth -B 0.0.0.0 -R 0 -l 1 -u 56666]>
Caution
Attempting to boot a server on a port where another server is already running will result in an error:
>>> server_one = supriya.Server()
>>> server_two = supriya.Server()
>>> server_one.boot()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
>>> server_two.boot()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/runner/work/supriya/supriya/supriya/contexts/realtime.py", line 675, in boot
raise ServerCannotBoot(self.process_protocol.error_text)
supriya.exceptions.ServerCannotBoot: *** ERROR: failed to open UDP socket: address in use.
Use find_free_port()
to grab a random unused port to
successfully boot:
>>> server_two.boot(port=supriya.osc.find_free_port())
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 39748]>
You can also explicitly select the server binary via the executable
keyword:
>>> server.boot(executable="scsynth")
<Server ONLINE [/usr/local/bin/scsynth -B 0.0.0.0 -R 0 -l 1 -u 56666]>
The executable
keyword allows you to boot with supernova
if you have it available:
>>> server.boot(executable="supernova")
<Server ONLINE [/usr/local/bin/supernova -B 0.0.0.0 -R 0 -l 1 -u 56666]>
Boot options¶
scsynth can be booted with a wide variety of command-line arguments,
which Supriya models via an Options
class:
>>> supriya.Options()
Options(audio_bus_channel_count=1024, block_size=64, buffer_count=1024, control_bus_channel_count=16384, executable=None, hardware_buffer_size=None, initial_node_id=1000, input_bus_channel_count=8, input_device=None, input_stream_mask='', ip_address='127.0.0.1', load_synthdefs=True, maximum_logins=1, maximum_node_count=1024, maximum_synthdef_count=1024, memory_locking=False, memory_size=8192, output_bus_channel_count=8, output_device=None, output_stream_mask='', password=None, port=57110, protocol='udp', random_number_generator_count=64, realtime=True, restricted_path=None, safety_clip=None, sample_rate=None, threads=6, ugen_plugins_path=None, verbosity=0, wire_buffer_count=64, zero_configuration=False)
Pass any of the named options found in Options
as
keyword arguments when booting:
>>> server.boot(input_bus_channel_count=2, output_bus_channel_count=2)
<Server ONLINE [/usr/local/bin/supernova -B 0.0.0.0 -R 0 -i 2 -l 1 -o 2 -u 56666]>
The server’s options describe how to generate the shell command for launching
the server process, and an Options
instance can be
iterated over like a list to generate that command:
>>> for x in supriya.Options():
... x
...
'/usr/local/bin/scsynth'
'-R'
'0'
'-l'
'1'
'-u'
'57110'
Multiple clients¶
SuperCollider support multiple users interacting with a single server simultaneously. One user boots the server and governs the underlying server process, and the remaining users simply connect to it.
Make sure that the server is booting with maximum_logins
set to the max
number of users you expect to log into the server at once, because the default
login count is 1:
>>> server_one = supriya.Server().boot(maximum_logins=2)
Connect to the existing server:
>>> server_two = supriya.Server().connect(
... ip_address=server_one.options.ip_address,
... port=server_one.options.port,
... )
Each connected user has their own client ID and default group:
>>> server_one.client_id
0
>>> server_two.client_id
1
>>> print(server_one.query_tree())
NODE TREE 0 group
1 group
2 group
Note that server_one
is owned, while server_two
isn’t:
>>> server_one.is_owner
True
>>> server_two.is_owner
False
Supriya provides some very limited guard-rails to prevent server shutdown by
non-owners, e.g. a force
boolean flag which non-owners can set to True
if they really want to quit the server. Without force
, quitting a non-owned
server will error:
>>> server_two.quit()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/runner/work/supriya/supriya/supriya/contexts/realtime.py", line 981, in quit
raise UnownedServerShutdown(
"Cannot quit unowned server without force flag."
)
supriya.exceptions.UnownedServerShutdown: Cannot quit unowned server without force flag.
Finally, disconnect:
>>> server_two.disconnect()
<Server OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
Disconnecting won’t terminate the server. It continues to run from wherever
server_one
was originally booted.
Lifecycle events¶
Supriya allows hooking into server lifecycle events via lifecycle callbacks. Use lifecycle callbacks to execute code before booting, once booting, before quitting, after quitting, or even on server panic:
>>> for event in supriya.ServerLifecycleEvent:
... print(repr(event))
...
<ServerLifecycleEvent.BOOTING: 1>
<ServerLifecycleEvent.PROCESS_BOOTED: 2>
<ServerLifecycleEvent.CONNECTING: 3>
<ServerLifecycleEvent.OSC_CONNECTED: 4>
<ServerLifecycleEvent.CONNECTED: 5>
<ServerLifecycleEvent.BOOTED: 6>
<ServerLifecycleEvent.OSC_PANICKED: 7>
<ServerLifecycleEvent.PROCESS_PANICKED: 8>
<ServerLifecycleEvent.QUITTING: 9>
<ServerLifecycleEvent.DISCONNECTING: 10>
<ServerLifecycleEvent.OSC_DISCONNECTED: 11>
<ServerLifecycleEvent.DISCONNECTED: 12>
<ServerLifecycleEvent.PROCESS_QUIT: 13>
<ServerLifecycleEvent.QUIT: 14>
Note
This is spiritually equivalent to sclang’s Server.doWhenBooted
.
Tip
Use lifecycle callbacks to load SynthDefs on server boot, and to perform cleanup just before server quit. Make sure to sync the server inside the boot callback procedure so your code blocks until the SynthDef loading completes.
Define a callback and register it against one or more events:
>>> def print_event(event):
... print(repr(event))
...
>>> callback = server.register_lifecycle_callback(
... event=list(supriya.ServerLifecycleEvent),
... procedure=print_event,
... )
Boot the server and watch the events print:
>>> server.boot()
<ServerLifecycleEvent.BOOTING: 1>
<ServerLifecycleEvent.PROCESS_BOOTED: 2>
<ServerLifecycleEvent.CONNECTING: 3>
<ServerLifecycleEvent.OSC_CONNECTED: 4>
<ServerLifecycleEvent.CONNECTED: 5>
<ServerLifecycleEvent.BOOTED: 6>
<Server ONLINE [/usr/local/bin/supernova -B 0.0.0.0 -R 0 -i 2 -l 1 -o 2 -u 56666]>
Quit the server and watch the events print:
>>> server.quit()
<ServerLifecycleEvent.QUITTING: 9>
<ServerLifecycleEvent.DISCONNECTING: 10>
<ServerLifecycleEvent.OSC_DISCONNECTED: 11>
<ServerLifecycleEvent.DISCONNECTED: 12>
<ServerLifecycleEvent.PROCESS_QUIT: 13>
<ServerLifecycleEvent.QUIT: 14>
<Server OFFLINE [/usr/local/bin/supernova -B 0.0.0.0 -R 0 -i 2 -l 1 -o 2 -u 56666]>
Unregister the callback:
>>> server.unregister_lifecycle_callback(callback)
You might want to …
Use
BOOTING
to run callbacks immediately afterboot()
starts, but before communication with the server is possible.Use
BOOTED
to run callbacks once the server has been completely setup, but just beforeboot()
returns,Use
QUITTING
to run callbacks oncequit()
has started safely, but before communication with the server shuts-down.Use
QUIT
to run callbacks once all communication with the server process has shutdown, but just beforequit()
returns.
See tests/contexts/test_Server_lifecycle.py for a variety of lifecycle scenarios and the resulting lifecycle events fired.
Inspection¶
Server
provides a number of methods and
properties for inspecting its state.
Let’s boot the server again:
>>> server = supriya.Server().boot()
Querying status¶
Inspect the “status” of audio processing:
>>> server.status
StatusInfo(actual_sample_rate=44105.116756221105, average_cpu_usage=0.04874815419316292, group_count=2, peak_cpu_usage=0.2565692365169525, synth_count=0, synthdef_count=0, target_sample_rate=44100.0, ugen_count=0)
Hint
Server status is a great way of tracking scsynth’s CPU usage.
Let’s add a SynthDef and a synth - explained soon - to increase the complexity of the status output:
>>> with server.at():
... with server.add_synthdefs(supriya.default):
... synth = server.add_synth(supriya.default)
...
>>> server.status
StatusInfo(actual_sample_rate=44105.00352740025, average_cpu_usage=0.12021782249212265, group_count=2, peak_cpu_usage=0.2565692365169525, synth_count=1, synthdef_count=33, target_sample_rate=44100.0, ugen_count=20)
Note that synth_count
, synthdef_count
and ugen_count
have gone up
after adding the synth to our server. We’ll discuss these concepts in
following sections.
Querying nodes¶
Querying the node tree with query()
will return a “query tree” representation, which you can print to generate
output similar to SuperCollider’s s.queryAllNodes
server method:
>>> server.query_tree()
QueryTreeGroup(node_id=0, annotation=None, children=[QueryTreeGroup(node_id=1, annotation=None, children=[QueryTreeSynth(node_id=1000, annotation=None, synthdef_name='supriya:default', controls=[QueryTreeControl(name_or_index='out', value=0.0), QueryTreeControl(name_or_index='amplitude', value=0.10000000149011612), QueryTreeControl(name_or_index='frequency', value=440.0), QueryTreeControl(name_or_index='gate', value=1.0), QueryTreeControl(name_or_index='pan', value=0.5)])])])
>>> print(_)
NODE TREE 0 group
1 group
1000 supriya:default
out: 0.0, amplitude: 0.1, frequency: 440.0, gate: 1.0, pan: 0.5
Querying default entities¶
Access the server’s root node and default group:
>>> server.root_node
RootNode(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, parallel=False)
>>> server.default_group
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1, parallel=False)
And access the input and output audio bus groups, which represent microphone inputs and speaker outputs:
>>> server.audio_input_bus_group
BusGroup(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=8, calculation_rate=<CalculationRate.AUDIO: 2>, count=8)
>>> server.audio_output_bus_group
BusGroup(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, calculation_rate=<CalculationRate.AUDIO: 2>, count=8)
Interaction¶
The server provides a variety of methods for interacting with it and modifying its state.
Sending OSC messages¶
You can send OSC messages via the
send()
method, either as
explicit OscMessage
or
OscBundle
objects, or as
Requestable
objects:
>>> from supriya.osc import OscMessage
>>> server.send(OscMessage("/g_new", 1000, 0, 1))
Syncing servers¶
Many interactions with scsynth don’t take effect immediately. In fact,
none of them really do, because the server behaves asynchronously. For
operations with significant delay, e.g. sending multiple SynthDefs or reading/writing buffers from/to disk, use
sync()
to block until all previously
initiated operations complete:
>>> server.sync()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
Note
See Open Sound Control for more information about OSC communication with the server, including OSC callbacks.
Mutating servers¶
The server provides methods for allocating nodes (groups and synths), buffers and buses, all of which are discussed in the sections following this one:
>>> server.add_group()
Group(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1000, parallel=False)
>>> server.add_synth(supriya.default, amplitude=0.25, frequency=441.3)
Synth(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1001, synthdef=<SynthDef: supriya:default>)
>>> server.add_buffer(channel_count=1, frame_count=512)
Buffer(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, completion=Completion(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, moment=Moment(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, seconds=None, closed=True, requests=[(AllocateBuffer(buffer_id=0, frame_count=512, channel_count=1, on_completion=None), ...)]), requests=[]))
>>> server.add_buffer_group(count=8, channel_count=2, frame_count=1024)
BufferGroup(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=1, count=8)
>>> server.add_bus()
Bus(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=0, calculation_rate=<CalculationRate.CONTROL: 1>)
>>> server.add_bus_group(count=2, calculation_rate="audio")
BusGroup(context=<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>, id_=16, calculation_rate=<CalculationRate.AUDIO: 2>, count=2)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1001 supriya:default
out: 0.0, amplitude: 0.25, frequency: 441.299988, gate: 1.0, pan: 0.5
1000 group
Resetting servers¶
Supriya supports resetting the state of the server, similar to
SuperCollider’s CmdPeriod
:
>>> server.reset()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
>>> print(server.query_tree())
NODE TREE 0 group
1 group
You can also just reboot the server, completely resetting all nodes, buses, buffers and SynthDefs:
>>> server.reboot()
<Server ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
Async servers¶
Supriya supports asyncio event loops via
AsyncServer
, which provides async
variants of many Server
’s methods. All
lifecycle methods (booting, quitting) are async, and all getter and query
methods are async as well.
>>> async def main():
... # Instantiate an async server
... print(async_server := supriya.AsyncServer())
... # Boot it on an arbitrary open port
... print(await async_server.boot(port=supriya.osc.find_free_port()))
... # Send an OSC message to the async server (doesn't require await!)
... async_server.send(["/g_new", 1000, 0, 1])
... # Query the async server's node tree
... print(await async_server.query_tree())
... # Quit the async server
... print(await async_server.quit())
...
>>> await main()
<AsyncServer OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
<AsyncServer ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 44523]>
NODE TREE 0 group
1 group
1000 group
<AsyncServer OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 44523]>
Note
Supriya’s documentation allows top-level await
constructs, but
depending on where you’re running async code, you may need to wrap it in a
call to asyncio.run(...)
or similar.
Use AsyncServer
with
AsyncClock
to integrate with
eventloop-driven libraries like aiohttp, python-prompt-toolkit and
pymonome.
Internals¶
Utilities¶
You can kill all running scsynth
processes via kill()
:
>>> supriya.scsynth.kill()
You can also find a random free port for use when booting multiple servers at once via find_free_port()
:
>>> supriya.osc.find_free_port()
44213
Process protocols¶
Get access to the server’s underlying process management subsystem via
process_protocol
:
>>> server.process_protocol
<supriya.scsynth.ThreadedProcessProtocol object at 0x7f296cc103d0>
Osc protocols¶
Get access to the server’s underlying OSC subsystem via
osc_protocol
:
>>> server.osc_protocol
<supriya.osc.threaded.ThreadedOscProtocol object at 0x7f291d3ec050>
Note
Server
manages its scsynth
subprocess and OSC communication via
SyncProcessProtocol
and
ThreadedOscProtocol
objects while the
AsyncServer
discussed later uses
AsyncProcessProtocol
and
AsyncOscProtocol
objects.