Skip to main content

A library for representing input and output signals on a network and allowing arbitrary 'mappings' to be dynamically created between them.

Project description

libmapper

libmapper is an open-source cross-platform software library for participating in a distributed network of input and output signals which can be dynamically connected together ("mapped").

A "mapping" represents a data-streaming association between one or more source signals and a destination signal, in which data is transported using either shared memory or Open Sound Control (OSC) streams over a network. In addition to traffic management, libmapper automatically handles address and datatype translation, and enables the use of arbitrary mathematical expressions for conditioning, combining and transforming the source values as desired. This can be used for example to connect a set of sensors to a synthesizer's input parameters.

This document concerns the Python bindings. The library is written in C, and there are also bindings for C++, C#, Python, Java, Rust, SuperCollider, Max, and Pure Data.

Installation

Simply run pip install libmapper in your Python environment of choice.

Usage

Once you have libmapper installed, it can be imported into your program:

import libmapper as mpr

Overview of the API organization

The libmapper API is is divided into the following sections:

  • Graph
  • Devices
  • Signals
  • Maps

For this tutorial, the only sections to pay attention to are Devices and Signals. Graphs and Maps are mostly used when building user interfaces for designing mapping configurations.

Devices

Creating a device

To create a libmapper device, it is necessary to provide a device name to the constructor. There is an initialization period after a device is created where a unique ordinal is chosen to append to the device name. This allows multiple devices with the same name to exist on the network.

A second optional parameter of the constructor is a Graph object. It is not necessary to provide this, but can be used to specify different networking parameters, such as specifying the name of the network interface to use.

An example of creating a device:

mydev = mpr.Device("my_device")

Polling the device

The device lifecycle looks like this:

creation --> poll --+--> destruction
              |     |
              +--<--+

In other words, after a device is created, it must be continuously polled during its lifetime.

The polling is necessary for several reasons: to respond to requests on the admin bus; to check for incoming signals; to update outgoing signals. Therefore even a device that does not have signals must be polled. The user program must organize to have a timer or idle handler which can poll the device often enough. The polling interval is not extremely sensitive, but should be 100 ms or less. The more often it is polled, the faster it can handle incoming signals.

The poll function can be blocking or non-blocking, depending on how you want your application to behave. It takes a number of milliseconds during which it should do some work, or 0 if it should check for any immediate actions and then return without waiting:

mydev.poll(block_ms)

An example of calling it with non-blocking behaviour:

mydev.poll(0)

If your polling is in the middle of a processing function or in response to a GUI event for example, non-blocking behaviour is desired. On the other hand if you put it in the middle of a loop which reads incoming data at intervals or steps through a simulation for example, you can use poll() as your "sleep" function, so that it will react to network activity while waiting.

It returns the number of messages handled, so optionally you could continue to call it until there are no more messages waiting. Of course, you should be careful doing that without limiting the time it will loop for, since if the incoming stream is fast enough you might never get anything else done!

Note that an important difference between blocking and non-blocking polling is that during the blocking period, messages will be handled immediately as they are received. On the other hand, if you use your own sleep, messages will be queued up until you can call poll(); stated differently, it will "time-quantize" the message handling. This is not necessarily bad, but you should be aware of this effect.

Since there is a delay before the device is completely initialized, it is sometimes useful to be able to determine this using ready(). Only when ready() returns non-zero is it valid to use the device's name.

Signals

Now that we know how to create a device and poll it, we only need to know how to add signals in order to give our program some input/output functionality. While libmapper enables arbitrary connections between any declared signals, we still find it helpful to distinguish between two type of signals: inputs and outputs.

  • output signals are sources of data, updated locally by their parent device
  • input signals are consumers of data and are not generally updated locally by their parent device.

This can become a bit confusing, since the "reverb" parameter of a sound synthesizer might be updated locally through user interaction with a GUI, however the normal use of this signal is as a destination for control data streams so it should be defined as an input signal. Note that this distinction is to help with GUI organization and user-understanding – libmapper enables connections from input signals and to output signals if desired.

Creating a signal

We'll start with creating a "sender", so we will first talk about how to update output signals. A signal requires a bit more information than a device, much of which is optional:

  • a name for the signal (must be unique within a devices inputs or outputs)
  • the signal's vector length
  • the signal's data type: mpr.Type.INT32, mpr.Type.FLOAT, mpr.Type.DOUBLE
  • the signal's unit (optional)
  • the signal's minimum value (optional)
  • the signal's maximum value (optional)

for input signals there is an additional argument:

  • a function to be called when the signal is updated

examples:

sig_in = dev.add_signal(mpr.Signal.Direction.INCOMING, "my_input", 1,
                        mpr.Type.FLOAT, "m/s", -10, 10, None, h)

sig_out = dev.add_signal(mpr.Signal.Direction.OUTGOING, "my_output", 4,
                         mpr.Type.INT32, None, 0, 1000)

The only required parameters here are the signal "length", its name, and data type. Signals are assumed to be vectors of values, so for usual single-valued signals, a length of 1 should be specified. Finally, supported types are currently INT32, FLOAT, or DOUBLE.

The other parameters are not strictly required, but the more information you provide, the more libmapper can do some things automatically. For example, if minimum and maximum are provided, it will be possible to create linear-scaled connections very quickly. If unit is provided, libmapper will be able to similarly figure out a linear scaling based on unit conversion (centimeters to inches for example). Currently automatic unit-based scaling is not a supported feature, but will be added in the future. You can take advantage of this future development by simply providing unit information whenever it is available. It is also helpful documentation for users.

Lastly, it is usually necessary to be informed when input signal values change. This is done by providing a function to be called whenever its value is modified by an incoming message. It is passed in the handler parameter.

An example of creating a "barebones" int scalar output signal with no unit, minimum, or maximum information:

outA = mydev.add_signal(mpr.Signal.Direction.OUTGOING, "outA", 1,
                        mpr.Type.INT32, None, None, None)

or omitting some arguments:

outA = mydev.add_signal(mpr.Signal.Direction.OUTGOING, "outA", 1,
                        mpr.Type.INT32)

An example of a float signal where some more information is provided:

sensor1 = mydev.add_signal(mpr.Signal.Direction.OUTGOING, "sensor1",
                           1, mpr.Type.FLOAT, "V", 0.0, 5.0)

So far we know how to create a device and to specify an output signal for it. To recap, let's review the code so far:

import libmapper as mpr

mydev = mpr.Device("test_sender")
sensor1 = mydev.add_signal(mpr.Signal.Direction.OUTGOING, "sensor1",
                           1, mpr.Type.FLOAT, "V", 0.0, 5.0)
    
while 1:
    mydev.poll(50)
    # ... do stuff ...
    # ... update signals ...

It is possible to retrieve a device's inputs or outputs by name or by index at a later time using the functions get_signal_by_<name/index>.

Updating signals

We can imagine the above program getting sensor information in a loop. It could be running on an network-enabled ARM device and reading the ADC register directly, or it could be running on a computer and reading data from an Arduino over a USB serial port, or it could just be a mouse-controlled GUI slider. However it's getting the data, it must provide it to libmapper so that it will be sent to other devices if that signal is mapped.

This is accomplished by the set_value function:

mysig.set_value(value)

So in the "sensor 1 voltage" example, assuming in do_stuff() we have some code which reads sensor 1's value into a float variable called v1, the loop becomes:

while 1:
    mydev.poll(50)
    v1 = do_stuff()
    sensor1.set_value(v1)

This is about all that is needed to expose sensor 1's voltage to the network as a mappable parameter. The libmapper GUI can now be used to create a mapping between this value and a receiver, where it could control a synthesizer parameter or change the brightness of an LED, or whatever else you want to do.

Signal conditioning

Most synthesizers of course will not know what to do with "voltage"--it is an electrical property that has nothing to do with sound or music. This is where libmapper really becomes useful.

Scaling or other signal conditioning can be taken care of before exposing the signal, or it can be performed as part of the mapping. Since the end user can demand any mathematical operation be performed on the signal, they can perform whatever mappings between signals as they wish.

As a developer, it is therefore your job to provide information that will be useful to the end user.

For example, if sensor 1 is a position sensor, instead of publishing "voltage", you could convert it to centimeters or meters based on the known dimensions of the sensor, and publish a "sensor1/position" signal instead, providing the unit information as well.

We call such signals "semantic", because they provide information with more meaning than a relatively uninformative value based on the electrical properties of the sensing technique. Some sensors can benefit from low-pass filtering or other measures to reduce noise. Some sensor data may need to be combined in order to derive physical meaning. What you choose to expose as outputs of your device is entirely application-dependent.

You can even publish both "sensor1/position" and "sensor1/voltage" if desired, in order to expose both processed and raw data. Keep in mind that these will not take up significant processing time, and zero network bandwidth, if they are not mapped.

Receiving signals

Now that we know how to create a sender, it would be useful to also know how to receive signals, so that we can create a sender-receiver pair to test out the provided mapping functionality.

As mentioned above, the add_signal() function takes an optional handler. This is a function that will be called whenever the value of that signal changes. To create a receiver for a synthesizer parameter "pulse width" (given as a ratio between 0 and 1), specify a handler when calling add_signal(). We'll imagine there is some python synthesizer implemented as a class synthesizer which has functions setPulseWidth() which sets the pulse width in a thread-safe manner, and startAudioInBackground() which sets up the audio thread.

Let's use a real-world example using the pyo DSP library for Python to create a simple synth consisting of one sine wave. For now, we will only worry about controlling one parameter: the frequency of the sine.

We need to create a handler function for libmapper to update the pyo synth:

def frequency_handler(sig, event, inst, val, time):
    try:
        if event == mpr.Signal.Event.UPDATE:
            sine.setFreq(val)
    except:
        print('exception')
        print(sig, val)

Then our program will look like this:

from pyo import *
import libmapper as mpr

# Some pyo stuff
synth = Server().boot().start()
sine = Sine(freq=200, mul=0.5).out()

def freq_handler(sig, event, id, val, timetag):
    try:
        if event == mpr.Signal.Event.UPDATE:
            sine.setFreq(val)
    except:
        print('exception')
        print(sig, val)

mydev = mpr.Device('pyo_example')
mydev.add_signal(mpr.Signal.Direction.INCOMING, 'frequency', 1,
                 mpr.Type.FLOAT, 'Hz', 20, 2000, None, freq_handler)

while True:
    mydev.poll( 100 )

synth.stop()

Alternately, we can simplify our code by using a lambda expression instead of a separate handler:

from pyo import *
import libmapper as mpr

# Some pyo stuff
synth = Server().boot().start()
sine = Sine(freq=200, mul=0.5).out()

mydev = mpr.Device('pyo_example')
mydev.add_signal(mpr.Signal.Direction.INCOMING, 'frequency', 1,
                 mpr.Type.FLOAT, "Hz", 20, 2000, None,
                 lambda s, e, i, f, t: sine.setFreq(f),
                 mpr.Signal.Event.UPDATE)

while True:
    mydev.poll(100)

synth.stop()

Working with timetags

libmapper uses the mpr_time_t data structure internally to store NTP timestamps, but this value is represented using the Time class in the python bindings. For example, the handler function called when a signal update is received contains a time argument. This argument indicates the time at which the source signal was sampled (in the case of sensor signals) or generated (in the case of sequenced or algorithimically-generated signals).

Creating a new Time without arguments causes it to be initialized with the current system time:

now = mpr.Time()

Working with signal instances

libmapper also provides support for signals with multiple instances, for example:

  • control parameters for polyphonic synthesizers;
  • touches tracked by a multitouch surface;
  • "blobs" identified by computer vision systems;
  • objects on a tabletop tangible user interface;
  • temporal objects such as gestures or trajectories.

The important qualities of signal instances in libmapper are:

  • instances are interchangeable: if there are semantics attached to a specific instance it should be represented with separate signals instead.
  • instances can be ephemeral: signal instances can be dynamically created and destroyed. libmapper will ensure that linked devices share a common understanding of the relatonships between instances when they are mapped.
  • one mapping connection serves to map all of its instances.

All signals possess one instance by default. If you would like to reserve more instances you can use:

mysig.reserve_instances(num)

After reserving instances you can update a specific instance:

mysig.instance(id).set_value(value)

The id argument does not have to be considered as an array index - it can be any integer that is convenient for labelling your instance. libmapper will internally create a map from your id label to one of the preallocated instance structures.

Receiving instances

You might have noticed earlier that the handler function called when a signal update is received has a argument called id. Here is the function prototype again:

def frequency_handler(signal, event, id, value, time):

Remember that you will need to reserve instances for your input signal using reserve_instances() if you want to receive instanced updates.

Instance Stealing

For handling cases in which the sender signal has more instances than the receiver signal, the instance allocation mode can be set for an input signal to set an action to take in case all allocated instances are in use and a previously unseen instance id is received. Use the function:

mysig.set_property(mpr.Property.STEALING, mode);

The argument mode can have one of the following values:

  • mpr.Stealing.NONE Default value, in which no stealing of instances will occur;
  • mpr.Stealing.OLDEST Release the oldest active instance and reallocate its resources to the new instance;
  • mpr.Stealing.NEWEST Release the newest active instance and reallocate its resources to the new instance;

If you want to use another method for determining which active instance to release (e.g. the sound with the lowest volume), you can subscribe to "instance overflow" events and insert your own logic in the signal callback handler:

import random

def my_handler(sig, event, id, event, timetag):
    if event == mpr.Signal.Event.INST_OFLW:
        # user code chooses which instance to release, in this case randomly
        id = random.randint(0, sig.num_instances())

        # release the chosen instance
        sig.instance(id).release()

For this function to be called when instance stealing is necessary, we need to register it for mpr.Signal.Event.INST_OFLW events:

mysig.set_callback(my_handler, mpr.Signal.Event.UPDATE | mpr.Signal.Event.INST_OFLW)

Publishing metadata

Things like device names, signal units, and ranges, are examples of metadata--information about the data you are exposing on the network.

libmapper also provides the ability to specify arbitrary extra metadata in the form of name-value pairs. These are not interpreted by libmapper in any way, but can be retrieved over the network. This can be used for instance to label a device with its loation, or to perhaps give a signal some property like "reliability", or some category like "light", "motor", "shaker", etc.

Some GUI could then use this information to display information about the network in an intelligent manner.

Any time there may be extra knowledge about a signal or device, it is a good idea to represent it by adding such properties, which can be of any OSC-compatible type. (So, numbers and strings, etc.)

The property interface is through the functions,

myobject.set_property(key, value)
myobject.get_property(key, value)
myobject.remove_property(key, value)

where the key can be either a member of the Property enum class or a string specifying the name of the property, and the value can any OSC-compatible type. These functions can be called for any libmapper object, including Devices, Signals, Maps, and Graphs.

For example, to store a float indicating the X position of a device dev, you can call it like this:

mydev.set_property("x", 12.5)

To specify a string property of a signal:

mysig.set_property("sensingMethod", "resistive")

In the case of get_property() the key can also be a numerical index, enabling sequential recovery of all of an object's properties along with their keys. In this case it may be easier to simply retrieve all metadata as a Python dict:

# returns a dict containing all object properties
myobject.properties()

The return type of get_property() function depends on how it was called:

# if the property exists, returns the value associated with key 'min'
# otherwise returns None
myobj.get_property('min')

# if the property exists, returns the value associated with key Property.MIN
# otherwise returns None
myobj.get_property(Property.MIN)

# returns a tuple containing (key, value) for the 0th property
myobj.get_property(0)

Objects as Associative Arrays

Finally, you can also access object metadata by indexing the object itself as an associative array (dict in Python):

# set object property 'foo' to the value 'bar'
myobj['foo'] = bar

# print the object property 'foo'
print(myobj['foo'])

As before, retrieving a property using an index will return a tuple if the property exists.

Reserved keys

You can use any property name not already reserved by libmapper.

Object Reserved keys
All data, description, id, is_local, name, status, version
Device host, libversion, num_maps, num_maps_in, num_maps_out, num_sigs_in, num_sigs_out, ordinal, port, signal, synced
Signal device, direction, ephemeral, jitter, length, max, maximum, min, minimum, num_inst, num_maps, num_maps_in, num_maps_out, period, rate, steal, type, unit
Maps bundle, expr, muted, num_destinations, num_sources, process_loc, protocol, scope, signal, slot, use_inst

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

libmapper-2.5.2.tar.gz (39.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

libmapper-2.5.2-py3-none-win_amd64.whl (226.8 kB view details)

Uploaded Python 3Windows x86-64

libmapper-2.5.2-py3-none-manylinux_2_24_x86_64.whl (531.6 kB view details)

Uploaded Python 3manylinux: glibc 2.24+ x86-64

libmapper-2.5.2-py3-none-manylinux_2_24_i686.whl (496.9 kB view details)

Uploaded Python 3manylinux: glibc 2.24+ i686

libmapper-2.5.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (617.1 kB view details)

Uploaded Python 3manylinux: glibc 2.12+ x86-64

libmapper-2.5.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl (576.7 kB view details)

Uploaded Python 3manylinux: glibc 2.12+ i686

libmapper-2.5.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (626.3 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

libmapper-2.5.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl (588.1 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ i686

libmapper-2.5.2-py3-none-macosx_14_0_universal2.whl (321.9 kB view details)

Uploaded Python 3macOS 14.0+ universal2 (ARM64, x86-64)

File details

Details for the file libmapper-2.5.2.tar.gz.

File metadata

  • Download URL: libmapper-2.5.2.tar.gz
  • Upload date:
  • Size: 39.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for libmapper-2.5.2.tar.gz
Algorithm Hash digest
SHA256 5729851e3324168c8b51edc606ab64aafb4fde4e3bb26df19c29ec3e1dd08fec
MD5 40595e46f614b9f0037061f7c75d6b10
BLAKE2b-256 d51b9914cb9bb84941e7014cbda2e74f4e9bcab0b7e62bc3b9c7f62091e9c61d

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-win_amd64.whl.

File metadata

  • Download URL: libmapper-2.5.2-py3-none-win_amd64.whl
  • Upload date:
  • Size: 226.8 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for libmapper-2.5.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 82bd953a4330ef786a472cc7991faf053390a4c1bae8522cc547188b7cce3ffd
MD5 68c6a6e082e5399823dac28613789306
BLAKE2b-256 4af43bc7b46df9df63ea5cf0ac2ad36224582e6122c23066cd240ce826f7f2c5

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux_2_24_x86_64.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux_2_24_x86_64.whl
Algorithm Hash digest
SHA256 6552ee7092c9ec7f2a2222acb533d41e2cad5807072c3b989672a97f2a7084f6
MD5 9044dea521828a89fdbb4a0e14718198
BLAKE2b-256 8e7a7e2f1382e65585cfd2677c93e67ea3ec0730061ce610e824dfeb5ea6ee4b

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux_2_24_i686.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux_2_24_i686.whl
Algorithm Hash digest
SHA256 0c2e9fc548818c144fd8ebfa4ebba4cab6cb2e0abe4403097e56f931297894d4
MD5 64e787adaab2fc17fcfdced08cb0196d
BLAKE2b-256 3bbb548fea92cc9c2891a0bb40584b049065fdb6a3caa18aeac5d8e7524206b8

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 43807bab7196c5120919a38eab1b6c0ff49f69f525fadcf86b055feffae5e9d2
MD5 f56205c50c5afe4a0ec6ef1b95481f64
BLAKE2b-256 cab3a99d7685d0f9b3b86cb6c6c1a220820e07f2ca26b08a64c360c6483ca72d

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 9a0ee0a200088e516e6905e608ae0f3f5980865978d4c0d280692c5e29023ba5
MD5 72a404a298cd81832a33a3241ee9367b
BLAKE2b-256 135c66d3a282e814f456bdc1b54408103d99f5cde380a04a3d8dc522f9b7ee59

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 459b792b4b35568336e81ae139f83b6b514e09464d842ac345c9dc9fc75b6e43
MD5 1ca2c3086c8a8eb085952dd1a79c7b3c
BLAKE2b-256 3ce94d8956ef2be75824601eae9b34066b8118468c1b2def2abbc977f4609287

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl
Algorithm Hash digest
SHA256 5ff1e72c81f9d72cbc648233bafc00b3e25d96f587e0f04d3849257fd38c5744
MD5 900e18b38e0813147b3d365c18bf6fad
BLAKE2b-256 a1d02cd94948a8ef073319d85db348cdadf9c1c3ddf668752cd42bf26fe93aeb

See more details on using hashes here.

File details

Details for the file libmapper-2.5.2-py3-none-macosx_14_0_universal2.whl.

File metadata

File hashes

Hashes for libmapper-2.5.2-py3-none-macosx_14_0_universal2.whl
Algorithm Hash digest
SHA256 311982a385b3a66e4fdbee44902ca12056458b1f0cbf5acb3815aae872d2c4dc
MD5 eb08d66ced918d35ae2d4e081285e5d6
BLAKE2b-256 90c9b91791f3894301d23bfd2f5a57f90b92d6e359c8ed0791adc8eb1d9bef62

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page