This page was generated from examples/osc-communication-examples.ipynb.
[1]:
import time
import numpy as np
[2]:
import sc3nb as scn
OSC communication
With the OSC communication module of sc3nb you can directly send and receive OSC packets.
Open Sound Control (OSC) is a networking protocol for sound and is used by SuperCollider to communicate between sclang and scsynth. sc3nb is itself a OSC client and server. This allows sc3nb to send and receive OSC traffic.
For more information on OSC and especially how Supercollider handles OSC packets please refer to the following links:
[3]:
sc = scn.startup()
Starting sclang process... [sclang | start reading ]
Done.
Registering OSC /return callback in sclang... Done.
Loading default sc3nb SynthDefs... Done.
Booting SuperCollider Server... [scsynth | start reading ]
Done.
sc3nb serves as OSC server and as client of the SuperCollider server scsynth
. You can also communicate with the SuperCollider interpreter sclang
via OSC.
You can see the current connection information with sc.server.connection_info()
[4]:
(sc3nb_ip, sc3nb_port), receivers = sc.server.connection_info()
This instance is at ('127.0.0.1', 57131),
Known receivers: "scsynth" at ('127.0.0.1', 57110)
"sclang" at ('127.0.0.1', 57120)
[5]:
(sc3nb_ip, sc3nb_port), receivers
[5]:
(('127.0.0.1', 57131),
{('127.0.0.1', 57110): 'scsynth', ('127.0.0.1', 57120): 'sclang'})
If you want to communicate via OSC with another receiver you could add its name via sc.server.add_receiver(name: str, ip: str, port: int)
, or you can pass a custom receiver when sending OSC
[6]:
sc.server.add_receiver("sc3nb", sc3nb_ip, sc3nb_port)
[7]:
sc.server.connection_info()
This instance is at ('127.0.0.1', 57131),
Known receivers: "scsynth" at ('127.0.0.1', 57110)
"sclang" at ('127.0.0.1', 57120)
"sc3nb" at ('127.0.0.1', 57131)
[7]:
(('127.0.0.1', 57131),
{('127.0.0.1', 57110): 'scsynth',
('127.0.0.1', 57120): 'sclang',
('127.0.0.1', 57131): 'sc3nb'})
Sending OSC
You can send OSC with
[8]:
help(sc.server.send)
Help on method send in module sc3nb.osc.osc_communication:
send(package: Union[sc3nb.osc.osc_communication.OSCMessage, sc3nb.osc.osc_communication.Bundler], *, receiver: Union[str, Tuple[str, int], NoneType] = None, bundle: bool = False, await_reply: bool = True, timeout: float = 5) -> Any method of sc3nb.sc_objects.server.SCServer instance
Sends OSC packet
Parameters
----------
package : OSCMessage or Bundler
Object with `dgram` attribute.
receiver : str or Tuple[str, int], optional
Where to send the packet, by default send to default receiver
bundle : bool, optional
If True it is allowed to bundle the package with bundling, by default False.
await_reply : bool, optional
If True ask for reply from the server and return it,
otherwise send the message and return None directly, by default True.
If the package is bundled None will be returned.
timeout : int, optional
timeout in seconds for reply, by default 5
Returns
-------
None or reply
None if no reply was received or awaited else reply.
Raises
------
ValueError
When the provided package is not supported.
OSCCommunicationError
When the handling of a package fails.
Messages
Use the OSCMessage
or the python-osc
package to build an OscMessage
[9]:
scn.OSCMessage?
[10]:
msg = scn.OSCMessage("/s_new", ["s1", -1, 1, 1,])
sc.server.send(msg)
A shortcut for sending Messages is
[11]:
help(sc.server.msg)
Help on method msg in module sc3nb.osc.osc_communication:
msg(msg_addr: str, msg_params: Optional[Sequence] = None, *, bundle: bool = False, receiver: Optional[Tuple[str, int]] = None, await_reply: bool = True, timeout: float = 5) -> Optional[Any] method of sc3nb.sc_objects.server.SCServer instance
Creates and sends OSC message over UDP.
Parameters
----------
msg_addr : str
SuperCollider address of the OSC message
msg_params : Optional[Sequence], optional
List of paramters of the OSC message, by default None
bundle : bool, optional
If True it is allowed to bundle the content with bundling, by default False
receiver : tuple[str, int], optional
(IP address, port) to send the message, by default send to default receiver
await_reply : bool, optional
If True send message and wait for reply
otherwise send the message and return directly, by default True
timeout : float, optional
timeout in seconds for reply, by default 5
Returns
-------
obj
reply if await_reply and there is a reply for this
[12]:
sc.server.msg("/s_new", ["s1", -1, 1, 1,])
a more complex example
[13]:
for p in [0,2,4,7,5,5,9,7,7,12,11,12,7,4,0,2,4,5,7,9,7,5,4,2,4,0,-1,0,2,-5,-1,2,5,4,2,4]:
freq = scn.midicps(60+p) # see helper fns below
sc.server.msg("/s_new", ["s1", -1, 1, 0, "freq", freq, "dur", 0.5, "num", 1])
time.sleep(0.15)
Note that the timing is here under python’s control, which is not very precise. The Bundler
class allows to do better.
Remarks:
note that the python code returns immediately and all events remain in scsynth
note that unfortunately scsynth has a limited buffer for OSC messages, so it is not viable to spawn thousends of events. scsynth will then simply reject OSC messages.
this sc3-specific problem motivated (and has been solved with) TimedQueue, see below.
Bundles
[14]:
from sc3nb.osc.osc_communication import Bundler
To send a single or multiple message(s) with a timetag as an OSC Bundle, you can use the Bundler
class
Bundlers allow to specify a timetag and thus let scsynth control the timing, which is much better, if applicable.
A Bundler can be created as documented here
[15]:
Bundler?
The prefered way of creating Bundlers for sending to the server is via
[16]:
help(sc.server.bundler)
Help on method bundler in module sc3nb.sc_objects.server:
bundler(timetag=0, msg=None, msg_params=None, send_on_exit=True) method of sc3nb.sc_objects.server.SCServer instance
Generate a Bundler with added server latency.
This allows the user to easly add messages/bundles and send it.
Parameters
----------
timetag : float
Time at which bundle content should be executed.
This servers latency will be added upon this.
If timetag <= 1e6 it is added to time.time().
msg_addr : str
SuperCollider address.
msg_params : list, optional
List of parameters to add to message.
(Default value = None)
Returns
-------
Bundler
bundler for OSC bundling.
This will add the sc.server.latency
time to the timetag. By default this is 0.0
but you can set it.
[17]:
sc.server.latency
[17]:
0.0
[18]:
sc.server.latency = 0.1
sc.server.latency
[18]:
0.1
A Bundler lets you add Messages and other Bundlers to the Bundler and accepts
an OSCMessage or Bundler
an timetag with an OSCMessage or Bundler
or Bundler arguments like (timetag, msg_addr, msg_params) (timetag, msg_addr) (timetag, msg)
Also see Nesting Bundlers for more details on how Bundlers are nested
[19]:
help(Bundler.add)
Help on function add in module sc3nb.osc.osc_communication:
add(self, *args) -> 'Bundler'
Add content to this Bundler.
Parameters
----------
args : accepts an OSCMessage or Bundler
or a timetag with an OSCMessage or Bundler
or Bundler arguments like
(timetag, msg_addr, msg_params)
(timetag, msg_addr)
(timetag, msg)
Returns
-------
Bundler
self for chaining
add
returns the Bundler for chaining
[20]:
msg1 = scn.OSCMessage("/s_new", ["s2", -1, 1, 1,])
msg2 = scn.OSCMessage("/n_free", [-1])
sc.server.bundler().add(1.5, msg1).add(1.9, msg2).send() # sound starts in 1.5s
An alternative is the usage of the context manager. This means you can use the with
statement for better handling as follows:
[21]:
with sc.server.bundler() as bundler:
bundler.add(0.0, msg1)
bundler.add(0.3, msg2)
Instead of declaring the time explicitly with add you can also use wait
.
[22]:
iterations = 3
with sc.server.bundler() as bundler:
for i in range(iterations):
bundler.add(msg1)
bundler.wait(0.3)
bundler.add(msg2)
bundler.wait(0.1)
This adds up the internal time passed of the Bundler
[23]:
bundler.passed_time
[23]:
1.2
[24]:
assert bundler.passed_time == iterations * 0.3 + iterations * 0.1, "Internal time seems wrong"
Here are some different styles of coding the same sound with the Bundler features
server Bundler with add and Bundler Arguments
[25]:
with sc.server.bundler(send_on_exit=False) as bundler:
bundler.add(0.0, "/s_new", ["s2", -1, 1, 1,])
bundler.add(0.3, "/n_free", [-1])
[26]:
dg1 = bundler.to_raw_osc(0.0) # we set the time_offset explicitly so all Bundle datagrams are the same
dg1
[26]:
b'#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x004#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x00 /s_new\x00\x00,siii\x00\x00\x00s2\x00\x00\xff\xff\xff\xff\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00$#bundle\x00\x83\xaa~\x80ffh\x00\x00\x00\x00\x10/n_free\x00,i\x00\x00\xff\xff\xff\xff'
Bundler with explict latency set and using add
[27]:
with Bundler(sc.server.latency, send_on_exit=False) as bundler:
bundler.add(0.0, "/s_new", ["s2", -1, 1, 1])
bundler.add(0.3, "/n_free", [-1])
[28]:
dg2 = bundler.to_raw_osc(0.0)
dg2
[28]:
b'#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x004#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x00 /s_new\x00\x00,siii\x00\x00\x00s2\x00\x00\xff\xff\xff\xff\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00$#bundle\x00\x83\xaa~\x80ffh\x00\x00\x00\x00\x10/n_free\x00,i\x00\x00\xff\xff\xff\xff'
server Bundler with implicit latency and using automatic bundled messages (See Automatic Bundling)
[29]:
with sc.server.bundler(send_on_exit=False) as bundler:
sc.server.msg("/s_new", ["s2", -1, 1, 1,], bundle=True)
bundler.wait(0.3)
sc.server.msg("/n_free", [-1], bundle=True)
[30]:
dg3 = bundler.to_raw_osc(0.0)
dg3
[30]:
b'#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x004#bundle\x00\x83\xaa~\x80\x19\x99\x98\x00\x00\x00\x00 /s_new\x00\x00,siii\x00\x00\x00s2\x00\x00\xff\xff\xff\xff\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00$#bundle\x00\x83\xaa~\x80ffh\x00\x00\x00\x00\x10/n_free\x00,i\x00\x00\xff\xff\xff\xff'
[31]:
# assert that all created raw OSC datagrams are the same
assert dg1 == dg2 and dg1 == dg3, "The datagrams are not the same"
Note: You can use the Bundler with the Synth and Group classes for easier Message creation. This also removes the burden of managing the IDs for the different commands.
Also make sure to look at the Automatic Bundling Feature which is using the bundled messages (msg(..., bundle=True
)
[32]:
t0 = time.time()
with sc.server.bundler() as bundler:
for i, r in enumerate(np.random.randn(100)):
onset = t0 + 3 + r
freq = 500 + 5 * i
bundler.add(onset, scn.Synth("s1",
{"freq": freq, "dur": 1.5, "num": abs(r)+1}, new=False
).new(return_msg=True))
Bundler Timestamp
Small numbers (<1e6) are interpreted as times in seconds relative to time.time()
, evaluated at the time of sending
[33]:
sc.server.bundler(0.5, "/s_new", ["s1", -1, 1, 0, "freq", 200, "dur", 1]).send() # a tone starts in 0.5s
sc.server.bundler(1.0, "/s_new", ["s1", -1, 1, 0, "freq", 300, "dur", 1]).send() # a tone starts in 1.0s
Attention:
Sending bundles with relative times could lead to unprecise timings. If you care about precision
use a bundler with multiple messages (if you care about the timings relative to each other in one Bundler)
because all relative times of the inner messages are calculated on top of the outermost bundler timetag
or provide an explict timetag (>1e6) to specify absolute times (see the following examples)
A single Bundler with multiple messages
[34]:
bundler = sc.server.bundler()
bundler.add(0.5, "/s_new", ["s1", -1, 1, 0, "freq", 200, "dur", 1])
bundler.add(1.0, "/s_new", ["s1", -1, 1, 0, "freq", 300, "dur", 1])
bundler.send() # second tone starts in 1.0s
using time.time()+timeoffset
for absolute times
[35]:
t0 = time.time()
sc.server.bundler(t0 + 0.5, "/s_new", ["s1", -1, 1, 0, "freq", 200, "dur", 1]).send() # a tone starts in 0.5s
sc.server.bundler(t0 + 1.0, "/s_new", ["s1", -1, 1, 0, "freq", 300, "dur", 1]).send() # a tone starts in 1.0s
[36]:
t0 = time.time()
with sc.server.bundler() as bundler:
for i, r in enumerate(np.random.randn(100)): # note: 1000 will give: msg too long
onset = t0 + 3 + r
freq = 500 + 5 * i
msg_params = ["s1", -1, 1, 0, "freq", freq, "dur", 1.5, "num", abs(r)+1]
bundler.add(onset, "/s_new", msg_params)
[37]:
sc.server.free_all()
Nesting Bundlers
You can nest Bundlers: this will recalculate the time relative to the sending time of the outermost bundler
[38]:
with sc.server.bundler() as bundler_outer:
with sc.server.bundler(0.2) as bundler:
sc.server.msg("/s_new", ["s2", -1, 1, 1,], bundle=True)
bundler.wait(0.3)
sc.server.msg("/n_free", [-1], bundle=True)
bundler_outer.wait(0.8)
bundler_outer.add(bundler)
[39]:
bundler_outer
[39]:
<Bundler {0.4: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 0.7: [<OSCMessage("/n_free", [-1])>], 1.2000000000000002: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 1.5000000000000002: [<OSCMessage("/n_free", [-1])>]}>
Or you can nest using Bundler.add
and get the same results
[40]:
bundler_outer_add = sc.server.bundler()
bundler_outer_add.add(bundler)
bundler_outer_add.add(0.8, bundler)
[40]:
<Bundler {0.4: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 0.7: [<OSCMessage("/n_free", [-1])>], 1.2000000000000002: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 1.5000000000000002: [<OSCMessage("/n_free", [-1])>]}>
[41]:
assert bundler_outer.to_raw_osc(0.0) == bundler_outer_add.to_raw_osc(0.0), "Bundler contents are not the same"
Notice that using relative timetags with wait will delay the relative timetags aswell.
[42]:
bundler_outer_add = sc.server.bundler()
bundler_outer_add.wait(1) # delays both bundles
bundler_outer_add.add(bundler)
bundler_outer_add.add(0.8, bundler)
[42]:
<Bundler {1.4000000000000001: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 1.7000000000000002: [<OSCMessage("/n_free", [-1])>], 2.2: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 2.5: [<OSCMessage("/n_free", [-1])>]}>
[43]:
bundler_outer_add = sc.server.bundler()
bundler_outer_add.add(bundler)
bundler_outer_add.wait(1) # delays the 2nd bundle
bundler_outer_add.add(0.8, bundler)
[43]:
<Bundler {0.4: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 0.7: [<OSCMessage("/n_free", [-1])>], 2.2: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 2.5: [<OSCMessage("/n_free", [-1])>]}>
This can be helpful in loops where the relative time then can be seen as relative for this iteration
[44]:
with sc.server.bundler() as nested_bundler_loop:
for i in range(3):
nested_bundler_loop.add(0.5, bundler)
nested_bundler_loop.wait(1)
nested_bundler_loop
[44]:
<Bundler {0.9: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 1.2: [<OSCMessage("/n_free", [-1])>], 1.9000000000000001: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 2.2: [<OSCMessage("/n_free", [-1])>], 2.9: [<OSCMessage("/s_new", ['s2', -1, 1, 1])>], 3.1999999999999997: [<OSCMessage("/n_free", [-1])>]}>
Managing IDs
The OSC commands often require IDs for the different OSC commands. These should be manged by the SCServer
to ensure not accidentally using wrong IDs.
See Allocating IDs in the Server guide for more information about correctly using IDs manually.
Or use Automatic Bundling and let sc3nb do the work for you!
Automatic Bundling
Probably the most convenient way of sending OSC is by using the Automatic Bundling Feature.
This allows you to simply use the SuperCollider Objects Synth and Group in the Context Manager of a Bundler and they will be automatically captured and stored.
[45]:
with sc.server.bundler() as bundler:
synth = scn.Synth("s2")
bundler.wait(0.3)
synth.set("freq", 1000)
bundler.wait(0.1)
synth.free()
synth.wait()
Note that it is important that to only wait on the Synth after the context of the Bundler has been closed.
If you’d call synth.wait()
in the Bundler context, it would wait before sending the /s_new Message to the server and then wait forever (or until timeout) for the /n_end notification.
[46]:
try:
with sc.server.bundler() as bundler:
synth = scn.Synth("s2")
bundler.wait(0.3)
synth.set("freq", 1000)
bundler.wait(0.1)
synth.free()
synth.wait(timeout=2) # without a timeout this would hang forever
except RuntimeError as error:
print(error)
Error at SCServer('127.0.0.1', 57110) pid=14078 from scsynth: ('/fail', '/n_free', 'Node -1 not found')
Aborting. Exception raised in bundler:
Traceback (most recent call last):
File "/var/folders/g1/m1whjjvd7pq3yz_w9s0rj64r0000gn/T/ipykernel_14067/1927839133.py", line 8, in <module>
synth.wait(timeout=2) # without a timeout this would hang forever
File "/Users/dreinsch/dev/soni/sc3nb/.tox/docs/lib/python3.10/site-packages/sc3nb/sc_objects/node.py", line 682, in wait
raise TimeoutError("Timed out waiting for synth.")
TimeoutError: Timed out waiting for synth.
Receiving OSC packets
sc3nb is receiving OSC messages with the help of queues, one AddressQueue for each OSC address for which we want to receive messages.
[47]:
sc.server.msg_queues
[47]:
{<MasterControlReply.STATUS_REPLY: '/status.reply'>: AddressQueue /status.reply : [],
<MasterControlReply.SYNCED: '/synced'>: AddressQueue /synced : [],
<MasterControlReply.VERSION_REPLY: '/version.reply'>: AddressQueue /version.reply : [],
<NodeCommand.SET: '/n_set'>: AddressQueue /n_set : [],
<NodeCommand.SETN: '/n_setn'>: AddressQueue /n_setn : [],
<GroupReply.QUERY_TREE_REPLY: '/g_queryTree.reply'>: AddressQueue /g_queryTree.reply : [],
<NodeReply.INFO: '/n_info'>: AddressQueue /n_info : [],
<BufferReply.INFO: '/b_info'>: AddressQueue /b_info : [],
<BufferCommand.SET: '/b_set'>: AddressQueue /b_set : [],
<BufferCommand.SETN: '/b_setn'>: AddressQueue /b_setn : [],
<ControlBusCommand.SET: '/c_set'>: AddressQueue /c_set : [],
<ControlBusCommand.SETN: '/c_setn'>: AddressQueue /c_setn : [],
<ReplyAddress.RETURN_ADDR: '/return'>: AddressQueue /return : [],
'/done/quit': AddressQueue /quit : [],
'/done/notify': AddressQueue /notify : [],
'/done/d_recv': AddressQueue /d_recv : [],
'/done/d_load': AddressQueue /d_load : [],
'/done/d_loadDir': AddressQueue /d_loadDir : [],
'/done/b_alloc': AddressQueue /b_alloc : [],
'/done/b_allocRead': AddressQueue /b_allocRead : [],
'/done/b_allocReadChannel': AddressQueue /b_allocReadChannel : [],
'/done/b_read': AddressQueue /b_read : [],
'/done/b_readChannel': AddressQueue /b_readChannel : [],
'/done/b_write': AddressQueue /b_write : [],
'/done/b_free': AddressQueue /b_free : [],
'/done/b_zero': AddressQueue /b_zero : [],
'/done/b_gen': AddressQueue /b_gen : [],
'/done/b_close': AddressQueue /b_close : []}
To see more information what messages are sent and received, set the logging level to INFO as demonstrated below.
[48]:
import logging
logging.basicConfig(level=logging.INFO)
# even more verbose logging is avaible via
# logging.basicConfig(level=logging.DEBUG)
Getting replies
For certain outgoing OSC messages an incoming Message is defined.
This means that on sending such a message sc3nb automatically waits for the incoming message at the corresponding Queue and returns the result.
An example for this is /sync {sync_id} -> /synced {sync_id}
[49]:
sc.server.msg("/sync", 12345)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/synced', 12345)
[49]:
12345
See all (outgoing message, incoming message)
pairs:
[50]:
sc.server.reply_addresses
[50]:
{<MasterControlCommand.STATUS: '/status'>: <MasterControlReply.STATUS_REPLY: '/status.reply'>,
<MasterControlCommand.SYNC: '/sync'>: <MasterControlReply.SYNCED: '/synced'>,
<MasterControlCommand.VERSION: '/version'>: <MasterControlReply.VERSION_REPLY: '/version.reply'>,
<SynthCommand.S_GET: '/s_get'>: <NodeCommand.SET: '/n_set'>,
<SynthCommand.S_GETN: '/s_getn'>: <NodeCommand.SETN: '/n_setn'>,
<GroupCommand.QUERY_TREE: '/g_queryTree'>: <GroupReply.QUERY_TREE_REPLY: '/g_queryTree.reply'>,
<NodeCommand.QUERY: '/n_query'>: <NodeReply.INFO: '/n_info'>,
<BufferCommand.QUERY: '/b_query'>: <BufferReply.INFO: '/b_info'>,
<BufferCommand.GET: '/b_get'>: <BufferCommand.SET: '/b_set'>,
<BufferCommand.GETN: '/b_getn'>: <BufferCommand.SETN: '/b_setn'>,
<ControlBusCommand.GET: '/c_get'>: <ControlBusCommand.SET: '/c_set'>,
<ControlBusCommand.GETN: '/c_getn'>: <ControlBusCommand.SETN: '/c_setn'>,
<MasterControlCommand.QUIT: '/quit'>: '/done/quit',
<MasterControlCommand.NOTIFY: '/notify'>: '/done/notify',
<SynthDefinitionCommand.RECV: '/d_recv'>: '/done/d_recv',
<SynthDefinitionCommand.LOAD: '/d_load'>: '/done/d_load',
<SynthDefinitionCommand.LOAD_DIR: '/d_loadDir'>: '/done/d_loadDir',
<BufferCommand.ALLOC: '/b_alloc'>: '/done/b_alloc',
<BufferCommand.ALLOC_READ: '/b_allocRead'>: '/done/b_allocRead',
<BufferCommand.ALLOC_READ_CHANNEL: '/b_allocReadChannel'>: '/done/b_allocReadChannel',
<BufferCommand.READ: '/b_read'>: '/done/b_read',
<BufferCommand.READ_CHANNEL: '/b_readChannel'>: '/done/b_readChannel',
<BufferCommand.WRITE: '/b_write'>: '/done/b_write',
<BufferCommand.FREE: '/b_free'>: '/done/b_free',
<BufferCommand.ZERO: '/b_zero'>: '/done/b_zero',
<BufferCommand.GEN: '/b_gen'>: '/done/b_gen',
<BufferCommand.CLOSE: '/b_close'>: '/done/b_close'}
You can get the reply address via
[51]:
sc.server.get_reply_address("/sync")
[51]:
<MasterControlReply.SYNCED: '/synced'>
or
[52]:
sc.server.reply_addresses["/sync"]
[52]:
<MasterControlReply.SYNCED: '/synced'>
If we specify await_reply=False
the message will be kept in the queue
[53]:
sc.server.msg("/sync", 1, await_reply=False)
[54]:
sc.server.msg_queues[sc.server.get_reply_address("/sync")]
[54]:
AddressQueue /synced : []
[55]:
sc.server.msg("/sync", 2, await_reply=False)
[56]:
sc.server.msg_queues[sc.server.reply_addresses["/sync"]]
[56]:
AddressQueue /synced : []
[57]:
sc.server.msg_queues["/synced"]
[57]:
AddressQueue /synced : [1]
You can see how many values were hold.
[58]:
sc.server.msg_queues["/synced"].skips
[58]:
2
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/synced', 2)
Notice that these hold messages will be skipped.
[59]:
sc.server.msg("/sync", 3, await_reply=True)
WARNING:sc3nb.osc.osc_communication:AddressQueue MasterControlReply.SYNCED: skipped value 1
WARNING:sc3nb.osc.osc_communication:AddressQueue MasterControlReply.SYNCED: skipped value 2
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/synced', 3)
[59]:
3
[60]:
sc.server.msg_queues["/synced"]
[60]:
AddressQueue /synced : []
Therefore you should retrieve them with get
and set skip=False
if you care for old values in the queue and dont want them to be skipped.
[61]:
sc.server.msg("/sync", 42, await_reply=False)
sc.server.msg_queues["/synced"].get(skip=False)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/synced', 42)
[61]:
42
Custom Message Queues
If you want to get additional OSC Messages you need to create a custom MessageQueue
[62]:
from sc3nb.osc.osc_communication import MessageQueue
[63]:
help(MessageQueue)
Help on class MessageQueue in module sc3nb.osc.osc_communication:
class MessageQueue(MessageHandler)
| MessageQueue(address: str, preprocess: Optional[Callable] = None)
|
| Queue to retrieve OSC messages send to the corresponding OSC address
|
| Method resolution order:
| MessageQueue
| MessageHandler
| abc.ABC
| builtins.object
|
| Methods defined here:
|
| __init__(self, address: str, preprocess: Optional[Callable] = None)
| Create a new AddressQueue
|
| Parameters
| ----------
| address : str
| OSC address for this queue
| preprocess : function, optional
| function that will be applied to the value before they are enqueued
| (Default value = None)
|
| get(self, timeout: float = 5, skip: bool = True) -> Any
| Returns a value from the queue
|
| Parameters
| ----------
| timeout : int, optional
| Time in seconds that will be waited on the queue, by default 5
| skip : bool, optional
| If True the queue will skip as many values as `skips`, by default True
|
| Returns
| -------
| obj
| value from queue
|
| Raises
| ------
| Empty
| If the queue has no value
|
| put(self, address: str, *args) -> None
| Add a message to MessageQueue
|
| Parameters
| ----------
| address : str
| message address
|
| show(self) -> None
| Print the content of the queue.
|
| skipped(self)
| Skipp one queue value
|
| ----------------------------------------------------------------------
| Readonly properties defined here:
|
| map_values
| Values needed for dispatcher map call
|
| Returns
| -------
| tuple
| (OSC address pattern, callback function)
|
| size
| How many items are in this queue
|
| skips
| Counts how many times this queue was not synced
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __abstractmethods__ = frozenset()
|
| ----------------------------------------------------------------------
| Data descriptors inherited from MessageHandler:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
[64]:
mq = MessageQueue("/test")
[65]:
sc.server.add_msg_queue(mq)
[66]:
sc.server.msg("/test", ["Hi!"], receiver="sc3nb")
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from sc3nb: ('/test', 'Hi!')
[67]:
sc.server.msg_queues["/test"]
[67]:
AddressQueue /test : ['Hi!']
[68]:
sc.server.msg("/test", ["Hello!"], receiver="sc3nb")
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from sc3nb: ('/test', 'Hello!')
[69]:
sc.server.msg_queues["/test"]
[69]:
AddressQueue /test : ['Hi!', 'Hello!']
[70]:
sc.server.msg_queues["/test"].get()
[70]:
'Hi!'
[71]:
sc.server.msg_queues["/test"].get()
[71]:
'Hello!'
If you want to create a pair of an outgoing message that will receive a certain incomming message you need to specify it via the out_addr
arugment of add_msg_queue
or you could use the shortcut for this add_msg_pairs
[72]:
help(sc.server.add_msg_pairs)
Help on method add_msg_pairs in module sc3nb.osc.osc_communication:
add_msg_pairs(msg_pairs: Dict[str, str]) -> None method of sc3nb.sc_objects.server.SCServer instance
Add the provided pairs for message receiving.
Parameters
----------
msg_pairs : dict[str, str], optional
dict containing user specified message pairs.
{msg_addr: reply_addr}
[73]:
sc.server.add_msg_pairs({"/hi": "/hi.reply"})
Let’s use OSCdef
in sclang to send us replies.
[74]:
%%sc
OSCdef.newMatching("say_hi", {|msg, time, addr, recvPort| addr.sendMsg("/hi.reply", "Hello there!")}, '/hi');
-> OSCdef(say_hi, /hi, nil, nil, nil)
[75]:
sc.server.msg("/hi", receiver="sclang")
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from sclang: ('/hi.reply', 'Hello there!')
[75]:
'Hello there!'
There is also the class MessageQueueCollection, which allows to create multiple MessageQueues for a multiple address/subaddresses
combination
[76]:
from sc3nb.osc.osc_communication import MessageQueueCollection
[77]:
help(MessageQueueCollection)
Help on class MessageQueueCollection in module sc3nb.osc.osc_communication:
class MessageQueueCollection(MessageHandler)
| MessageQueueCollection(address: str, sub_addrs: Optional[Sequence[str]] = None)
|
| A collection of MessageQueues that are all sent to one and the same first address.
|
| Method resolution order:
| MessageQueueCollection
| MessageHandler
| abc.ABC
| builtins.object
|
| Methods defined here:
|
| __contains__(self, item) -> bool
|
| __getitem__(self, key)
|
| __init__(self, address: str, sub_addrs: Optional[Sequence[str]] = None)
| Create a collection of MessageQueues under the same first address
|
| Parameters
| ----------
| address : str
| first message address that is the same for all MessageQueues
| sub_addrs : Optional[Sequence[str]], optional
| secound message addresses with seperate queues, by default None
| Additional MessageQueues will be created on demand.
|
| put(self, address: str, *args) -> None
| Add a message to the corresponding MessageQueue
|
| Parameters
| ----------
| address : str
| first message address
|
| ----------------------------------------------------------------------
| Readonly properties defined here:
|
| map_values
| Values needed for dispatcher map call
|
| Returns
| -------
| tuple
| (OSC address pattern, callback function)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __abstractmethods__ = frozenset()
|
| ----------------------------------------------------------------------
| Data descriptors inherited from MessageHandler:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
[78]:
mqc = MessageQueueCollection("/collect", ["/address1", "/address2"])
sc.server.add_msg_queue_collection(mqc)
[79]:
mqc = MessageQueueCollection("/auto_collect")
sc.server.add_msg_queue_collection(mqc)
[80]:
sc.server.reply_addresses
[80]:
{<MasterControlCommand.STATUS: '/status'>: <MasterControlReply.STATUS_REPLY: '/status.reply'>,
<MasterControlCommand.SYNC: '/sync'>: <MasterControlReply.SYNCED: '/synced'>,
<MasterControlCommand.VERSION: '/version'>: <MasterControlReply.VERSION_REPLY: '/version.reply'>,
<SynthCommand.S_GET: '/s_get'>: <NodeCommand.SET: '/n_set'>,
<SynthCommand.S_GETN: '/s_getn'>: <NodeCommand.SETN: '/n_setn'>,
<GroupCommand.QUERY_TREE: '/g_queryTree'>: <GroupReply.QUERY_TREE_REPLY: '/g_queryTree.reply'>,
<NodeCommand.QUERY: '/n_query'>: <NodeReply.INFO: '/n_info'>,
<BufferCommand.QUERY: '/b_query'>: <BufferReply.INFO: '/b_info'>,
<BufferCommand.GET: '/b_get'>: <BufferCommand.SET: '/b_set'>,
<BufferCommand.GETN: '/b_getn'>: <BufferCommand.SETN: '/b_setn'>,
<ControlBusCommand.GET: '/c_get'>: <ControlBusCommand.SET: '/c_set'>,
<ControlBusCommand.GETN: '/c_getn'>: <ControlBusCommand.SETN: '/c_setn'>,
<MasterControlCommand.QUIT: '/quit'>: '/done/quit',
<MasterControlCommand.NOTIFY: '/notify'>: '/done/notify',
<SynthDefinitionCommand.RECV: '/d_recv'>: '/done/d_recv',
<SynthDefinitionCommand.LOAD: '/d_load'>: '/done/d_load',
<SynthDefinitionCommand.LOAD_DIR: '/d_loadDir'>: '/done/d_loadDir',
<BufferCommand.ALLOC: '/b_alloc'>: '/done/b_alloc',
<BufferCommand.ALLOC_READ: '/b_allocRead'>: '/done/b_allocRead',
<BufferCommand.ALLOC_READ_CHANNEL: '/b_allocReadChannel'>: '/done/b_allocReadChannel',
<BufferCommand.READ: '/b_read'>: '/done/b_read',
<BufferCommand.READ_CHANNEL: '/b_readChannel'>: '/done/b_readChannel',
<BufferCommand.WRITE: '/b_write'>: '/done/b_write',
<BufferCommand.FREE: '/b_free'>: '/done/b_free',
<BufferCommand.ZERO: '/b_zero'>: '/done/b_zero',
<BufferCommand.GEN: '/b_gen'>: '/done/b_gen',
<BufferCommand.CLOSE: '/b_close'>: '/done/b_close',
'/hi': '/hi.reply',
'/address1': '/collect/address1',
'/address2': '/collect/address2'}
[81]:
sc.server.msg_queues
[81]:
{<MasterControlReply.STATUS_REPLY: '/status.reply'>: AddressQueue /status.reply : [],
<MasterControlReply.SYNCED: '/synced'>: AddressQueue /synced : [],
<MasterControlReply.VERSION_REPLY: '/version.reply'>: AddressQueue /version.reply : [],
<NodeCommand.SET: '/n_set'>: AddressQueue /n_set : [],
<NodeCommand.SETN: '/n_setn'>: AddressQueue /n_setn : [],
<GroupReply.QUERY_TREE_REPLY: '/g_queryTree.reply'>: AddressQueue /g_queryTree.reply : [],
<NodeReply.INFO: '/n_info'>: AddressQueue /n_info : [],
<BufferReply.INFO: '/b_info'>: AddressQueue /b_info : [],
<BufferCommand.SET: '/b_set'>: AddressQueue /b_set : [],
<BufferCommand.SETN: '/b_setn'>: AddressQueue /b_setn : [],
<ControlBusCommand.SET: '/c_set'>: AddressQueue /c_set : [],
<ControlBusCommand.SETN: '/c_setn'>: AddressQueue /c_setn : [],
<ReplyAddress.RETURN_ADDR: '/return'>: AddressQueue /return : [],
'/done/quit': AddressQueue /quit : [],
'/done/notify': AddressQueue /notify : [],
'/done/d_recv': AddressQueue /d_recv : [],
'/done/d_load': AddressQueue /d_load : [],
'/done/d_loadDir': AddressQueue /d_loadDir : [],
'/done/b_alloc': AddressQueue /b_alloc : [],
'/done/b_allocRead': AddressQueue /b_allocRead : [],
'/done/b_allocReadChannel': AddressQueue /b_allocReadChannel : [],
'/done/b_read': AddressQueue /b_read : [],
'/done/b_readChannel': AddressQueue /b_readChannel : [],
'/done/b_write': AddressQueue /b_write : [],
'/done/b_free': AddressQueue /b_free : [],
'/done/b_zero': AddressQueue /b_zero : [],
'/done/b_gen': AddressQueue /b_gen : [],
'/done/b_close': AddressQueue /b_close : [],
'/test': AddressQueue /test : [],
'/hi.reply': AddressQueue /hi.reply : [],
'/collect/address1': AddressQueue /address1 : [],
'/collect/address2': AddressQueue /address2 : []}
[82]:
%%scv
OSCdef.newMatching("ab", {|msg, time, addr, recvPort| addr.sendMsg('/collect', '/address1', "toast".scramble)}, '/address1');
-> OSCdef(ab, /address1, nil, nil, nil)
[83]:
%%scv
OSCdef.newMatching("ab", {|msg, time, addr, recvPort| addr.sendMsg('/collect', '/address2', "sonification".scramble)}, '/address2');
-> OSCdef(ab, /address2, nil, nil, nil)
[84]:
sc.server.msg("/address1", receiver="sclang")
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from sclang: ('/collect', '/address1', 'tstao')
[84]:
'tstao'
[85]:
sc.server.msg("/address2", receiver="sclang")
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from sclang: ('/collect', '/address2', 'isnfocitaoni')
[85]:
'isnfocitaoni'
Examples
Creating an OSC responder and msg to sclang for synthesis
[86]:
%%sc
OSCdef(\dinger, { | msg, time, addr, recvPort |
var freq = msg[2];
{Pulse.ar(freq, 0.04, 0.3)!2 * EnvGen.ar(Env.perc, doneAction:2)}.play()
}, '/ding')
-> OSCdef(dinger, /ding, nil, nil, nil)
[87]:
with scn.Bundler(receiver=sc.lang.addr):
for i in range(5):
sc.server.msg("/ding", ["freq", 1000-5*i], bundle=True)
INFO:sc3nb.osc.osc_communication:send to sclang : <Bundler {0.0: [<OSCMessage("/ding", ['freq', 1000])>, <OSCMessage("/ding", ['freq', 995])>, <OSCMessage("/ding", ['freq', 990])>, <OSCMessage("/ding", ['freq', 985])>, <OSCMessage("/ding", ['freq', 980])>]}> contents size 5
[88]:
for i in range(5):
sc.server.msg("/ding", ["freq", 1000-5*i], receiver=sc.lang.addr)
[89]:
%scv OSCdef.freeAll()
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1000, 1, -1, -368, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1001, 1, -1, 1000, 0)
-> OSCdef
[90]:
sc.exit()
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1002, 1, -1, 1001, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1003, 1, -1, 1002, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1004, 1, -1, 1003, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1005, 1, -1, 1004, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1006, 1, -1, 1005, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1007, 1, -1, 1006, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1008, 1, -1, 1007, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/n_go', 1009, 1, -1, 1008, 0)
INFO:sc3nb.sc_objects.server:SCServer('127.0.0.1', 57110) pid=14078 got OSC msg from scsynth: ('/done', '/quit')
Quitting SCServer... [scsynth | reached EOF ]
Done.
Exiting sclang... [sclang | reached EOF ]
Done.