1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
|
TITLE:: Multi-client Setups
SUMMARY:: How to set up and shared servers for multiple clients in SuperCollider
CATEGORIES:: Server>Architecture, Tutorials
related:: Classes/ServerOptions, Classes/Server
KEYWORD:: server, multi-client
section:: Multi client-server setups - discussion and tests
OSC communication between SC and its sound server offers many options for network music: Multiple computers can run both supercollider and associated sound servers.
For clarity, the "server process" refers to a running scsynth or supernova process. The "server object" AKA "client" is the server's representation in sclang, such as code::Server.local, s, Server(\elsewhere, NetAddr("163.234.56.78"))::.
subsection:: What are clientIDs, and how do servers get them?
When more than one user plays on a given server,
some resources need to be shared between users/clients:
list::
## permanent and temporary nodeIDs (handled by Server:nodeAllocator),
## private control and audio bus channels (handled by Server:audioBusAllocator, Server:controlBusAllocator)
## buffer numbers (handled by Server:bufferAllocator).
::
This sharing is handled by declaring how many clients/users are expected to login - code::server.options.maxLogins:: - and scsynth will automatically give a different clientID to each client when it logs in.
The most common case is that there is only a single user/client, who always gets clientID 0, and control of all available resources (i.e. the full number range of every allocator).
When multiple clients log in, this is what happens:
list::
## On startup, scsynth (the server process) is started with a fixed limit of maxLogins.
## When a local or remote server object/client has no user-specified clientID, scsynth sends back the next free clientID, and the client uses that clientID.
## When a local or remote server object/client was created with specific clientID, scsynth sends back that number if it was free, or the next free clientID if not; the client should use the free number in any case, as the other may clash with a client already logged in.
## In case the client was already registered and tries to register again (after a reboot or network problem), scsynth sends back a failed message AND the clientID this client had earlier, and the client will use that clientID.
## [After pull request #3181] scsynth also sends back the maxLogins value it was started with, so clients can also adjust their internal allocator settings to it.
::
subsection:: Code examples and tests
Recommended usage for multiple clients on the same server is to use identical options settings for all clients, and logging into the scsynth process from different sclang instances, which are typically on different laptops.
code::
// on the machine where scsynth runs, it can be the default server.
// set the maximum number of client logins expected:
s.options.maxLogins = 8;
// now reboot the server, so that it will have maxLogins
s.reboot;
// from another sclang instance, log into scsynth:
s.options.maxLogins = 8;
// example NetAddr of the machine that runs scsynth on standard port
s.addr = NetAddr("168.192.1.20", 57110);
::
When fixed clientIDs for multiclient setups are desired, the recommended usage is to set every clientID on creation.
code::
s.options.maxLogins = 8;
r = Server(
\remote4,
// example NetAddr of the machine that runs scsynth on standard port
NetAddr("168.192.1.2", 57110),
s.options, // make sure all remote servers use the same options
4 // and when desired, set fixed client by hand
);
// now s knows it can change clientID from server login response
// (because userSpecifiedClientID is false)
s.userSpecifiedClientID;
// and z knows to keep its clientID
r.userSpecifiedClientID;
::
subsection:: Separate defaultGroups
For info on what a default group is, see link::Reference/default_group::.
Every client registering with a server has its own defaultGroup. All nodes belonging to one client are in its defaultGroup and can be specifically addressed, so that a client can release only one's own nodes, and leave those of other clients on this server untouched.
A client also knows the defaultGroups of all other clients that may login, so it can reason about other clients, and be as sharing-friendly as desired, see below in subsection CmdPeriod behavior.
code::s.defaultGroups;::
subsection:: Easy-to-trace nodeIDs
For details on node allocation, see NodeIDAllocator and ReadableNodeIDAllocator class and help files.
The scheme from NodeIDAllocator is also followed by many non-sclang clients allocation ranges; in networks with these, NodeIDAllocator will be the safe choice.
code::
// NodeIDAllocator uses a fixed binary prefix of (2 ** 26) * clientID:
Server.nodeAllocClass = NodeIDAllocator;
s.newAllocators;
r.newAllocators; // remake allocators
// clientID 0 has group 1:
s.clientID; // 0
s.defaultGroup; // Group(1)
s.defaultGroupID; // 1
// for server r:
r.clientID; // 4
r.nodeAllocator.idOffset; // lookup the offset: (2 ** 26 * 4)
r.defaultGroup; // Group(268435457) : idOffset + 1
r.defaultGroupID; // 268435457
r.options.maxLogins // 8
// calculate backwards to which clientID a node belongs
r.defaultGroupID mod: (2 ** 26); // 1 is the nodeID relative to idOffset relative ID
r.defaultGroupID >> 26;
r.nextNodeID ; // begin at defaultGroupID + 1000
r.nextNodeID >> 26; // get clientID from a long nodeID
// ReadableNodeIDAllocator uses a decimal prefix - to demonstrate:
Server.nodeAllocClass = ReadableNodeIDAllocator;
s.newAllocators;
r.newAllocators; // remake allocators
// for clientID 0, nothing changes:
s.clientID; // 0
s.defaultGroup; // Group(1)
s.defaultGroupID; // 1
// for server r:
r.clientID; // 4
r.nodeAllocator.idOffset; // decimal offset so clientID is prefix
r.defaultGroupID; // 400000001 - easy to identify nodeID source
r.defaultGroup; // Group(400000001)
r.options.maxLogins // 8
// s.defaultGroup can be looked up in many ways:
r.defaultGroupID; // 400000001
r.defaultGroup; // Group(400000001)
r.asGroup; // Group(400000001)
r.asTarget; // Group(400000001)
// temp nodeIDs readably belong to clientID 4, starting with 4...1000
5.do { r.nextNodeID.postln };
5.do { s.nextNodeID.postln };
// For demonstration, switch addr of r to point to local scsynth,
// so we can test the allocator numbers on a single machine:
r.addr = s.addr;
// whenever an accessible sound process is created, it gets a nodeID;
// here are four different ways to create sounds, and see their nodeIDs:
r.reboot;
r.plotTree;
Server.default = r;
// Synth
x = Synth(\default, nil);
x.release;
x = { Dust.ar(10!2).lag(0.002) }.play(r);
x.release(2);
Pbind(\degree, Pseq((0..7).mirror), \dur, 0.15, \server, r).play;
// JITLib nodeproxies
Ndef(\x, { Dust.ar(10 ! 2) });
Ndef(\x).play;
Ndef(\x).filter(10, { |in| Ringz.ar(in, [600, 800], 0.03) }).play;
Ndef(\x).end(3);
::
subsection:: Bus channel and buffer numbers
The allocators for audio and control busses and for buffers split the full number range of scsynth evenly for the number of clients expected.
code::
// default value for clientID is 0 and maxLogins is 1
Server.default = Server.local;
s.clientID; // 0
s.options.maxLogins; // default 1
// you can set maxLogins_ by hand - not recommended, only for testing here:
s.options.maxLogins_(1); // default 1
s.options.numAudioBusChannels;
// use newAllocators method to create allocator ranges accordingly
s.newBusAllocators;
s.audioBusAllocator.size;
// 1024 buses to allocate, starting past the hardware IO buses.
Bus.audio(s, 2);
// set maxLogins_ to 8 by hand - not recommended, only for demonstration here:
s.options.maxLogins_(8);
s.newBusAllocators; // 128 = 1024 / 8 buses to allocate per client.
s.audioBusAllocator.size;
3.collect { Bus.audio(s, 2) };
// 1024 control buses to allocate, starting at 0
s.controlBusAllocator.size;
3.collect { Bus.control(s, 2) };
r.options.dump
r.newBusAllocators;
// audio bus range starts at 512 of 1024 total range
3.collect { Bus.audio(r, 2) }; // 512, 514, 516
// control bus range starts at 8192 of 16384 total range
3.collect { Bus.control(r, 2) };
::
Buffer allocation uses the same class, ContiguousBlockAllocator, and thus works the same way.
code::
// show buffer allocation
Server.default = Server.local;
s.bufferAllocator.size;
3.collect { Buffer(s) }; // starts at 0
r.bufferAllocator.size;
3.collect { Buffer(r) }; // starts at 256
// more buffer alloc examples desirable here?
::
subsection:: Configuring CmdPeriod behavior
In networked performances, it is useful that clients have well-chosen
emergency access to 'their' sounds on a remote server; and it may make sense to give the local client more emergency power. Both can be configured easily.
strong::Default behavior::
By default, the local client will kill all sounds, and only reconstruct its defaultGroup; on remote clients, CmdPeriod does not affect remoter servers at all.
code::
// default - single client:
s.options.maxLogins_(1);
s.reboot;
s.plotTree; // watch to debug
s.defaultGroup; // Group(1)
s.defaultGroups; // only one client, so this is [ Group(1) ]
(dur: inf).play; // play a test sound
Group(0); // make another group at root level
s.freeAll; // sound and added group die, only Group(1) remains
// same with using CmdPeriod:
(dur: inf).play; // play a test sound
Group(0); // make another group at root level
CmdPeriod.run; // simulate CmdPeriod key action
// show behavior and support methods with a 4 client setup:
s.options.maxLogins_(4);
s.reboot;
s.defaultGroups; // 4 default groups, 1 for every possible client.
// send defaultGroups for specific clientIDs to scsynth : -> cf plotTree
s.sendDefaultGroupsForIDs([0, 1]); // adds second defaultGroup
// send all default groups
s.sendDefaultGroups; // four groups in plotTree
// s.tree gets evaluated after each CmdPeriod.
// make sure we have nothing in s.tree:
s.tree = nil;
s.freeAll; // <- by default, frees other client's defaultGroups too.
::
strong::Remote-friendly local parent setup::
Now, all sounds on the server are gone, as desired.
But the other clients cannot start new synths now,
because their defaultGroups are gone too!
Thus, the local client should reconstruct them,
so the other clients can pick up playing again:
code::
// - easiest option:
s.tree = { s.sendDefaultGroups; };
(dur: inf).play; // test sound
Group(0); // make second group at root level
CmdPeriod.run; // simulate CmdPeriod key action
// -> still stops all sounds, but then remakes all defaultGroups,
// so the other clients can continue.
::
strong::Give remote clients control of their sounds::
code::
// create a fake remote server
x = Server(\pseudoRem3, s.addr, s.options, 3);
// method to
~freeMyGroupX = { x.freeMyGroup };
CmdPeriod.add(~freeMyGroupX);
// fake playing a sound from it:
(dur: 10, group: x.defaultGroup).play;
(dur: 10, degree: 2).play; // and one on home client
~freeMyGroupX.value; // only frees the one in \pseudoRem3
CmdPeriod.remove(~freeMyGroupX);
strong::Symmetrical / democratic setups::
The home client on the machine where scsynth runs may want to be just like the others. It is simple to make the home client more polite:
code::
// first, disable general freeAll:
CmdPeriod.freeServers = false;
s.tree = nil;
s.sendDefaultGroups;
// and add a custom action to CmdPeriod:
~freeMyGroupS = { s.freeMyGroup };
CmdPeriod.add(~freeMyGroupS);
(dur: inf).play; // test sound
// simulate second sound from a different client:
(dur: inf, degree: 2, group: s.defaultGroups[3]).play;
CmdPeriod.run; // simulate CmdPeriod key action
// -> only stops s.defaultGroup, others continue untouched
CmdPeriod.remove(~freeMyGroupS); // cleanup
s.freeAll;
::
strong::More power to all::
In a less polite symmetrical setup, CmdPeriod stops all sounds on all clients, but keeps all defaultGroups running.
code::
~myserver = s; // s on home machine, remote client on others
~myserver.sendDefaultGroups;
~freeDefaultGroups = { ~myserver.freeDefaultGroups };
CmdPeriod.add(~freeDefaultGroups);
(dur: inf).play; // test sound
// fake sound from client 1:
(dur: inf, degree: 2, group: s.defaultGroups[1].nodeID).play;
CmdPeriod.run; // simulate CmdPeriod key action
// -> all clients stop sound in all groups, groups remain.
::
|