1. Usage

1.1. Introduction

1.1.1. What is osc4py3?

It is a fresh implementation of the Open Sound Control (OSC) protocol [1] for Python3. It tries to have a “pythonic” programming interface at top layers, and to support different scheduling schemes in lower layers to have efficient processing.

[1]See http://opensoundcontrol.org/ for complete OSC documentation.

1.1.2. What Python?

The package targets Python3.2 or greater, see other OSC implementations for older Python versions (OSC.py, simpleosc, TxOSC).

1.1.3. Why?

It was initially developed for use in a larger software as a background system communicating via OSC in separate threads, spending less possible time in main thread: sending OSC is just building messages/bundles objects and identifying targets, receiving OSC is just calling identified functions with OSC messages. All other operations, encoding and decoding, writing and reading, monitoring for communication channels availability, pattern matching messages addresses, etc, can be realized in other threads. On a multi-core CPU with multi-threading, these optimizations allow C code to run in parallel with communication operations, we gain milliseconds which are welcome in the project context.

1.1.4. What’s new?

The whole package is really bigger and the underlying system is complex to understand. But, two base modules, oscbuildparse and oscmethod (see the modules documentation), are autonomous and can be used as is to provide: encoding and decoding of OSC messages and bundles, pattern matching and functions calling.

As we start from scratch, we use latest information about OSC with support for advanced data types (adding new types is easy), and insert support for OSC extensions like address pattern compression or messages control.

1.1.5. Complicated to use?

No. See the “Simple use” chapter below. It may be even easier to use than other Python OSC implementations. Don’t mistake, the underlying processing is complex, but the high level API hide it.

1.2. Quick OSC

Open Sound Control is a simple way to do remote procedure calls between applications, triggered on string pattern matching, transported by any way inside binary packets. It mainly defines packets encoding format to transmit data and pattern matching to select functions to call. Using network broadcast or multicast, it allows to send data in one shot to several systems at same time.

1.2.1. Messages

Messages structure only contains: an address string (to be matched by patterns for routing to processing functions), description of data content (using type codes as defined in Supported atomic data types), and data itself (as packed binary).

For basic OSC address pattern, see Address patterns.

Data in OSC messages are generally simple, like can be function parameters, some int or float numbers, string, boolean, an “impulse” or “bang” (OSC come from MIDI [2] musical world where a kind of “start” event is necessary). It could be used to transmit complex structures, but it’s not its main purpose. See Supported atomic data types to see types allowed by osc4py3.

[2]See https://en.wikipedia.org/wiki/MIDI

1.3. Simple use

1.3.1. Examples

We will first start by small commented examples, then give some explanations and precisions.

Note

Even if examples have been splitted in two parts, sending and receiving OSC messages can be realized in same program ; osc_startup(), osc_process() and osc_terminate() are client & server ready.

1.3.1.1. Receiving OSC messages

 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
# Import needed modules from osc4py3
from osc4py3.as_eventloop import *
from osc4py3 import oscmethod as osm

def handlerfunction(s, x, y):
    # Will receive message data unpacked in s, x, y
    pass

def handlerfunction2(address, s, x, y):
    # Will receive message address, and message data flattened in s, x, y
    pass

# Start the system.
osc_startup()

# Make server channels to receive packets.
osc_udp_server("192.168.0.0", 3721, "aservername")
osc_udp_server("0.0.0.0", 3724, "anotherserver")

# Associate Python functions with message address patterns, using default
# argument scheme OSCARG_DATAUNPACK.
osc_method("/test/*", handlerfunction)
# Too, but request the message address pattern before in argscheme
osc_method("/test/*", handlerfunction2, argscheme=osm.OSCARG_ADDRESS + osm.OSCARG_DATAUNPACK)

# Periodically call osc4py3 processing method in your event loop.
finished = False
while not finished:
    # …
    osc_process()
    # …

# Properly close the system.
osc_terminate()

1.3.1.2. Sending OSC messages

 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
# Import needed modules from osc4py3
from osc4py3.as_eventloop import *
from osc4py3 import oscbuildparse

# Start the system.
osc_startup()

# Make client channels to send packets.
osc_udp_client("192.168.0.4", 2781, "aclientname")

# Build a simple message and send it.
msg = oscbuildparse.OSCMessage("/test/me", ",sif", ["text", 672, 8.871])
osc_send(msg, "aclientname")

# Build a message with autodetection of data types, and send it.
msg = oscbuildparse.OSCMessage("/test/me", None, ["text", 672, 8.871])
osc_send(msg, "aclientname")

# Buils a complete bundle, and postpone its executions by 10 sec.
exectime = time.time() + 10   # execute in 10 seconds
msg1 = oscbuildparse.OSCMessage("/sound/levels", None, [1, 5, 3])
msg2 = oscbuildparse.OSCMessage("/sound/bits", None, [32])
msg3 = oscbuildparse.OSCMessage("/sound/freq", None, [42000])
bun = oscbuildparse.OSCBundle(oscbuildparse.unixtime2timetag(exectime),
                    [msg1, msg2, msg3])
osc_send(bun, "aclientname")

# Periodically call osc4py3 processing method in your event loop.
finished = False
while not finished:
    # You can send OSC messages from your event loop too…
    # …
    osc_process()
    # …

# Properly close the system.
osc_terminate()

You can take a look at the rundemo.py script in demos/ directory. It is a common demo for the different scheduling options, where main osc4py3 functions are highlighted by surrounding comments. You can also look at speedudpsrv.py and speedudpcli.py in the demos/ directory.

1.3.1.3. General layout

So, the general layout for using osc4py3 is

  1. Import osc4py3.as_xxx module upon desired threading model.

  2. Import osc4py3.oscbuildparse if you plan to send messages.

  3. Call osc_startup function to start the system.

  4. Create a set of client/server identified via channels names.

  5. Register some handler functions with address matching.

  6. Enter the processing loop.

    1. Send created messages via client channels.
    2. Call osc_process function to run the system.
    3. Received messages from server channels, have their address matching handlers called for you.
  7. Call osc_terminate to properly release resources (communications, threads…).

1.3.2. Sending messages

As seen in Examples, you need to import oscbuildparse module (or directly some of its content) to build a new OSCMessage object. Then, simply and send it via osc_send() specifying target client channel names (see Client channels).

msg = oscbuildparse.OSCMessage("/test/me", ",sif", ["text", 672, 8.871])
osc_send(msg, "aclientname")

You can see supported message data types in Supported atomic data types.

It is also possible to send multiple OSC messages, or to request an immediate or on the contrary differed execution time using OSCBundle (see OSC Bundles).

bun = oscbuildparse.OSCBundle(unixtime2timetag(time.time()+30),
        [oscbuildparse.OSCMessage("/test/me", ",sif", ["none", -45, 11.34]),
         oscbuildparse.OSCMessage("/test/him", ",sif", ["two", 15, 11.856])])
osc_send(bun, "aclientname")
bun = oscbuildparse.OSCBundle(.oscbuildparse.OSC_IMMEDIATELY,
        [oscbuildparse.OSCMessage("/test/me", ",sif", ["three", 92, 454]),
         oscbuildparse.OSCMessage("/test/him", ",sif", ["four", 107, -3e2])])
osc_send(bun, "aclientname")

Warning

Note that when running the system with osc4py3.as_eventloop, several calls to osc_process() may be necessary to achieve complete processing of message sending (the message itself is generally sent / received in one osc_process() call if possible, but some monitoring status on sockets may need other calls to be stopped, and several sent messages may take time to be processed on sockets).

1.3.3. Message handlers

A message handler is a Python callable (function or bound method, or Python partial), which will be called when an OSCMessage’s addresspattern is matched by a specified addrpattern filter. By default it is called with the (unpacked) OSC message data as parameters.

You can build message handlers with an interface specifying each independent argument, like:

def handler_set_location(x, y, z):
    # Wait for x,y,z of new location.

Or use of *args variable arguments count.

def handler_set_location(*args):
    # Wait for x,y,z of new location.

Hint

If you want your method to be called even if sender gives an invalid parameters count, you may prefer the variable arguments count solution or the OSCARG_DATA argscheme see below), else with an invalid argument count message, your handler call will fail immediately with a ValueError exception.

1.3.3.1. Installing message handlers

The normal way is to use osc_method() function, where you associate your handler function with an OSC address pattern filter string expression (see Address patterns).

def osc_method(addrpattern, function [,argscheme[,extra]])

There is by default a simple mapping of the OSC message data to your function parameters, but you can change this using the argscheme parameter (see Arguments schemes).

You can provide an extra parameter(s) for your handler function, given as extra (you must require this parameter in your argscheme too).

1.3.3.2. Address patterns

When calling osc_method function you specify the message address pattern filter as first parameter.

def osc_method(addrpattern, function [,argscheme[,extra=None]])

By default the addrpattern follows OSC pattern model [3] , which looks like unix/dos globbing for filenames:

  • use / separators between “levels”
  • * match anything, operator stop at / boundary
  • // at beginning allow any level deep from start
  • ? match any single char
  • [abcdef] match any any char in abcdef
  • [a-z] match any char from a to z
  • [!a-z] match any char non from a to z
  • {truc,machin,chose} match truc or machin or chose

Note

There is no unbind function at this level of osc4py3 use.

[3]It is internally rewritten as Python re pattern to directly use Python regular expressions engine for matching.

1.3.3.3. Arguments schemes

By default message data are unpacked as handler parameters.

You can change what and how parameters are passed to handler’s arguments with the argscheme parameter of osc_method function. This parameter uses some tuple constant from oscmethod module, which can be added to build your handler function required arguments list.

  • OSCARG_DATA — OSC message data tuple as one parameter.
  • OSCARG_DATAUNPACK — OSC message data tuple as N parameters (default).
  • OSCARG_MESSAGE — OSC message as one parameter.
  • OSCARG_MESSAGEUNPACK — OSC message as three parameters (addrpattern, typetags, arguments).
  • OSCARG_ADDRESS — OSC message address as one parameter.
  • OSCARG_TYPETAGS — OSC message typetags as one parameter.
  • OSCARG_EXTRA — Extra parameter you provide as one parameter - may be None.
  • OSCARG_EXTRAUNPACK — Extra parameter you provide (unpackable) as N parameters.
  • OSCARG_METHODFILTER — Method filter object as one parameter. This argument is a MethodFilter internal structure containing informations about the message filter and your handler (argument scheme, filter expression…).
  • OSCARG_PACKOPT — Packet options object as one parameter - may be None. This argument is a PacketOption internal structure containing information about packet processing (source, time, etc).
  • OSCARG_READERNAME — Name of transport channel which receive the OSC packet - may be None.
  • OSCARG_SRCIDENT — Indentification information about packet source (ex. (ip,port)) - may be None.
  • OSCARG_READTIME — Time when the packet was read - may be None.

By combining these constants with addition operator, you can change what arguments your handler function will receive, and in what order. You can then adapt your handler function and/or the arguments scheme to fit. Examples:

# Import one of osc4py3 top level functions, and argument schemes definitions
from osc4py3.as_allthreads import *     # does osc_method
from osc4py3.oscmethod import *         # does OSCARG_XXX

# Request the unpacked data (default).
def fct(arg0, arg1, arg2, argn): pass
def fct(*args): pass
osc_method("/test/*", fct)

# Request the message address pattern and the unpacked data.
def fct(address, arg0, arg1, arg2, argn): pass
def fct(address, *args): pass
osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_DATAUNPACK)

# Request the message address pattern and the packed data.
def fct(address, args): pass
osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_DATA)

# Request the message address pattern, its type tag and the unppacked data.
def fct(address, typetag, arg0, arg1, arg2, argn): pass
def fct(address, typetag, *args): pass
osc_method("/test/*", fct, argscheme=OSCARG_ADDRESS + OSCARG_TYPETAGS + OSCARG_DATAUNPACK)

# Request the message address pattern, its type tag and the packed data… the OSCMessage parts.
def fct(address, typetag, args): pass
osc_method("/test/*", fct, argscheme=OSCARG_MESSAGEUNPACK)

# Request the OSCMessage as a whole.
def fct(msg): pass
osc_method("/test/*", fct, argscheme=OSCARG_MESSAGE)

# Request the message source identification, the whole message, and some extra data.
def fct(srcident, msg, extra): pass
osc_method("/test/*", fct, argscheme=OSCARG_SRCIDENT + OSCARG_MESSAGE + OSCARG_EXTRA,
           extra=dict(myoption1=4, myoption2="good"))

Hint

Informations made available via argument scheme constants should be enough for large common usage. But, if some handler function need even more information from received messages, other members of internal objects coming with OSCARG_METHODFILTER and OSCARG_PACKOPT argscheme can be of interest.

1.3.4. Threading model

From osc4py3 package, you must import functions from one of the as_eventloop, as_comthreads or as_allthreads modules (line 2 in examples). Each of these modules publish the same set of osc_… functions for different threading models (star import is safe with as_xxx modules, importing only the osc_fctxxx functions).

1.3.4.1. All threads

Using as_allthreads module, threads are used in several places, including message handlers calls - by default a pool of ten working threads are allocated for message handlers calls.

To control the final execution of methods, you can add an execthreadscount named parameter to osc_startup(). Setting it to 0 disable executions of methods in working threads, and all methods are called in the context of the raw packets processing thread, or if necessary in the context of the delayed bundle processing thread. Setting it to 1 build only one working thread to process methods calls, which are then all called in sequence in the context of that thread.

If you want to protect your code against race conditions between your main thread and message handlers calls, you should better use as_comthreads.

1.3.4.2. Communication threads

Using as_comthreads module, threads are used for communication operations (sending and receiving, intermediate processing), and messages are stored in queues between threads.

Message handler methods are called only when you call yourself osc_process(). This is optimal if you want to achieve maximum background processing of messages transmissions, but final messages processing must occur within an event loop.

1.3.4.3. No thread

Using as_eventloop module, there is no multi-threading, no background processing. All operations (communications and message handling) take place only when you call osc_process().

Warning

Multi-threading add some overhead in the whole processing (due to synchronization between threads). By example, on my computer, transmitting 1000 messages each executing a simple method call, speedudpsrv.py goes from 0.2 sec with eventloop scheduling to 0.245 sec with comthreads scheduling and 0.334 sec with allthreads scheduling.

But multi-threading can remain interesting if your main thread does C computing while osc4py3 process messages in background (which is the first use case of this development).

Important

If you choose to run the system with as_eventloop or with as_comthreads, you must periodically call the function osc_process() sometime in your event loop.

This call all code needed in the context of the event loop (all transmissions and dispatching code for as_eventloop, and only dispatching for as_comthreads).

If you choose to run the system with as_allthreads, you don’t need to call osc_process() (this function is defined in this module too, but does nothing). But you should have checked that your code will not have problems with concurrent access to resources (if needed there is a way to simply install a message handler to be called with an automatic Lock).

1.3.5. Servers channels

Server channel are identified by names, but you should not have to use these names once the server channel has been created (unless you do advanced internal processing on incoming OSC packets).

In the example server channels are simple UDP ports which will be listened to read some incoming datagram, first server listen for data from a specific IPV4 network, second server listen on all IPV4 networks.

You can also create servers via osc_broadcast_server() or osc_multicast_server().

1.3.6. Client channels

Client channels are identified by names, allowing light coupling in your code when sending OSC messages.

In the example client channels is a simple UDP transport. It will transmit data to server channels listening on the other side.

You can also create clients via osc_broadcast_client() or osc_multicast_client() (providing ad-hoc broadcast/multicast addresses).

1.3.7. Light coupling

Client and server channels being identified via names, where name resolution is only done when communication activity is required, creation order of the different parts is not important.

In this way, in your code you send a message to a named channel, without knowing how and where this channel connection has been established. You can change transport protocol with no impact on your communication code. If you target multiple channels via a list or tuple or set, eventually nested — this allow to easily define groups of channels as target for sending.

If you use the special reserved "_local" name target, you send a message to be processed directly by your own program without network communication (no need to create a channel, it’s a bypass name).

1.3.8. Logging OSC operations

Once imported, use of the package begin by initializing the system with a call to osc_startup() function.

This function allows an optional logger named parameter, accepting a logging.logger object [4] . Example:

import logging
logging.basicConfig(format='%(asctime)s - %(threadName)s ø %(name)s - '
    '%(levelname)s - %(message)s')
logger = logging.getLogger("osc")
logger.setLevel(logging.DEBUG)
osc_startup(logger=logger)
[4]This is highly recommended in case of problems with multi-threads usage.

1.3.9. Advanced pattern/handler

You may setup your pattern / handler binding using low level functions, giving access to some more advanced options.

from osc4py3 import oscmethod, oscdispatching
mf = oscmethod.MethodFilter(...)
oscdispatching.register_method(mf)

By creating the osc4py3.oscmethod.MethodFilter object directly, you can provide some extra construction parameters which are not available using high level osc_method() function (we don’t list parameters which can be specified using argscheme):

  • patternkind default to "osc", indicates to use OSC syntax for pattern. You can use here "re" to specify that you provide in addrpattern a Python regular expression.
  • workqueue allows you to specify a processing WorkQueue associated to threads to call the handler with matched incoming messages — by default it’s None and indicate to use the common work queue.
  • lock can be used to specify a thread.Lock (or thread.RLock) object to acquire before processing matched messages with this handler function — by default it’s None.
  • logger is a specific logger to use to specifically trace that handler calling on matched messages reception. By default top level osc_method() functions use the logger given as osc_startup() logger parameter.

1.4. User main functions

See OSCMessage or OSCBundle for their respective documentation.

We document using the common as_xxx module

Note

Startup and termination functions (osc_startup() and osc_terminate()) are intended to be called once only, at begining of program and at end of program. They install/uninstall a set of communication and filtering tools which normally stay active during program execution.

Terminating then restarting osc4py3 may work… but has not been extensively tested.

Each as_eventloop, as_allthreads and as_comthreads module define the same set of functions documented here.

osc4py3.as__common.osc_startup(**kwargs)

Once call startup function for all osc processing in the event loop.

Create the global dispatcher and register it for all packets and messages. Create threads for background processing when used with as_allthreads or as_comthreads scheduling.

Parameters:
  • logger (logging.Logger) – Python logger to trace activity. Default to None
  • execthreadscount (int) – number of execution threads for methods to create. Only used with as_allthreads scheduling. Default to 10.
  • writethreadscount (int) – number of write threads for packet sending to create. Only used with as_allthreads scheduling. Default to 10.
osc4py3.as__common.osc_terminate()

Once call termination function to clean internal structures before exiting process.

osc4py3.as__common.osc_process()

Function to call from your event loop to receive/process OSC messages.

osc4py3.as__common.osc_method(addrpattern, function, argscheme=('data_unpack', ), extra=None)

Add a method filter handler to automatically call a function.

Note

There is no unregister function at this level of osc4py use, but module oscdispatching provides MethodFilter object and functions to register and unregister them.

Parameters:
  • addrpattern (str) – OSC pattern to match
  • function (callable) – code to call with the message arguments
  • argscheme (tuple) – scheme for handler function arguments. By default message data are transmitted, flattened as N parameters.
  • extra (anything) – extra parameters for the function (must be specified in argscheme too).
osc4py3.as__common.osc_send(packet, names)

Send the packet using channels via names.

Parameters:
  • packet (OSCMessage or OSCBundle) – the message or bundle to send.
  • names (str or list or set) – name of target channels (can be a string, list or set).
osc4py3.as__common.osc_udp_server(name, address, port)

Create an UDP server channel to receive OSC packets.

Parameters:
  • name (str) – internal identification of the channel server.
  • address (str) – network address for binding UDP socket
  • port (int) – port number for binding UDP port
osc4py3.as__common.osc_udp_client(name, address, port)

Create an UDP client channel to send OSC packets.

Parameters:
  • name (str) – internal identification of the channel client.
  • address (str) – network address for binding UDP socket
  • port (int) – port number for binding UDP port
osc4py3.as__common.osc_multicast_server(name, address, port)

Create a multicast server to receive OSC packets.

Parameters:
  • name (str) – internal identification of the channel server.
  • address (str) – network address for binding socket
  • port (int) – port number for binding port
osc4py3.as__common.osc_multicast_client(name, address, port, ttl)

Create a multicast client channel to send OSC packets.

Parameters:
  • name (str) – internal identification of the channel client.
  • address (str) – multicast network address for binding socket
  • port (int) – port number for binding port
  • ttl (int) – time to leave for multicast packets. Default to 1 (one hop max).
osc4py3.as__common.osc_broadcast_server(name, address, port)

Create a broadcast server channel to receive OSC packets.

Parameters:
  • name (str) – internal identification of the UDP server.
  • address (str) – network address for binding UDP socket
  • port (int) – port number for binding UDP port
osc4py3.as__common.osc_broadcast_client(name, address, port, ttl)

Create a broadcast client channel to send OSC packets.

Parameters:
  • name (str) – internal identification of the channel client.
  • address (str) – broadcast network address for binding socket
  • port (int) – port number for binding port
  • ttl (int) – time to leave for broadcast packets. Default to 1 (one hop max).