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 -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 603, in boot
raise ServerCannotBoot
supriya.exceptions.ServerCannotBoot
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 43439]>
You can also explicitly select the server binary via the executable
keyword:
>>> server.boot(executable="scsynth")
<Server ONLINE [/usr/local/bin/scsynth -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 -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,
remote_control_volume=False,
restricted_path=None,
sample_rate=None,
threads=None,
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 -R 0 -i 2 -l 1 -o 2 -u 56666]>
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 894, in quit
raise UnownedServerShutdown(
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.
Inspection¶
Server
provides a number of methods and
properties for inspecting its state.
>>> server = supriya.Server().boot()
Inspect the “status” of audio processing:
>>> server.status
StatusInfo(actual_sample_rate=44103.75804874041, average_cpu_usage=0.06354143470525742, group_count=2, peak_cpu_usage=0.334428608417511, 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=44103.644826895725, average_cpu_usage=0.156699538230896, group_count=2, peak_cpu_usage=0.334428608417511, 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 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='default', controls=[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), QueryTreeControl(name_or_index='out', value=0.0)])])])
>>> print(_)
NODE TREE 0 group
1 group
1000 default
amplitude: 0.1, frequency: 440.0, gate: 1.0, pan: 0.5, out: 0.0
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, 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, count=8)
Interaction¶
The server provides a variety of methods for interacting with it and modifying its state.
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))
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.
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: 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)
>>> 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, count=2)
>>> print(server.query_tree())
NODE TREE 0 group
1 group
1001 default
amplitude: 0.25, frequency: 441.299988, gate: 1.0, pan: 0.5, out: 0.0
1000 group
Resetting¶
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¶
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.
>>> import asyncio
>>> 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())
...
>>> asyncio.run(main())
<AsyncServer OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 57110]>
<AsyncServer ONLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 42295]>
NODE TREE 0 group
1 group
1000 group
<AsyncServer OFFLINE [/usr/local/bin/scsynth -R 0 -l 1 -u 42295]>
Use AsyncServer
with
AsyncClock
to integrate with
eventloop-driven libraries like aiohttp, python-prompt-toolkit and
pymonome.
Lower level APIs¶
You can kill all running scsynth
processes via supriya.scsynth.kill()
:
>>> supriya.scsynth.kill()
Get access to the server’s underlying process management subsystem via
process_protocol
:
>>> server.process_protocol
<supriya.scsynth.SyncProcessProtocol object at 0x7f61e53709e0>
Get access to the server’s underlying OSC subsystem via
osc_protocol
:
>>> server.osc_protocol
<supriya.osc.threaded.ThreadedOscProtocol object at 0x7f61e5370410>
Note
Server
manages its scsynth
subprocess and OSC communication via
SyncProcessProtocol
and
ThreadedOscProtocol
objects while the
AsyncServer
discussed later uses
AsyncProcessProtocol
and
AsyncOscProtocol
objects.