Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 116 additions & 16 deletions OSC
Original file line number Diff line number Diff line change
@@ -1,30 +1,130 @@
MapMap OSC Interface
====================

API Reference
-------------
MapMap can be controlled remotely over `Open Sound Control
<https://opensoundcontrol.stanford.edu/>`_ (OSC) messages sent over UDP. This
is handy for live performance: a sequencer, a hardware controller bridged to
OSC, or another program can drive MapMap while a show runs.

/mapmap/paint/media/load
~~~~~~~~~~~~
Change a media URI::

/mapmap/paint/media/load ,is <paintId> <path>
By default MapMap listens on UDP port **12345**. Change it from the
Preferences dialog, or at launch with ``mapmap --osc-port <port>``.

paintID: A number. Usually 0, or 1 or 2... depending on how many paints you have in your project.
path: Path to a file.

Examples
Conventions
-----------

Every address starts with ``/mapmap``. Commands that act on a source or a
layer take the **target as their first argument**:

* an **integer** selects a single element by its id, or
* a **string** selects every element whose name matches the given pattern
(shell-style wildcards, e.g. ``clip*``).

"layer" and "mapping" are accepted as synonyms (a layer *is* a mapping).

Coordinates (for ``move``, ``translate`` and ``vertex``) are expressed in
MapMap's internal coordinate space — the same numbers stored for each vertex
in the ``.mmp`` project file. These are pixel coordinates relative to the
canvas, with the origin at its top-left corner. Open a saved project to read
the actual values you want to target.


Global transport
-----------------

::

/mapmap/play Start playback of every source.
/mapmap/pause Pause playback of every source.
/mapmap/rewind Rewind every source.
/mapmap/quit Quit MapMap.


Sources
-------

``<target>`` is an integer id or a name pattern (see Conventions).

::

/mapmap/source/play ,i|s <target> Start playback of the source(s).
/mapmap/source/pause ,i|s <target> Pause the source(s).
/mapmap/source/rewind ,i|s <target> Rewind the source(s).
/mapmap/source/<prop> ,i|s ... <target> <val> Set a property (see below).

Settable source properties (``<prop>``):

::

opacity float 0.0 .. 1.0
name string
uri string path of a video/image source
rate float playback rate, in % (negative = reverse)
volume float audio volume, in % (video sources)
color string "#rrggbb" or a colour name (color sources)
locked int/bool

Examples::

/mapmap/source/opacity ,if 2 0.5
/mapmap/source/color ,is 3 #ff0000
/mapmap/source/uri ,is "Clip *" /home/vj/loops/a.mov
/mapmap/source/play ,i 2


Layers
------

``<target>`` is an integer id or a name pattern (see Conventions).

::

/mapmap/layer/<prop> ,i|s ... <target> <val> Set a property.
/mapmap/layer/move/xy ,iff <target> <x> <y> Move so the layer's centre is at (x, y).
/mapmap/layer/translate/xy ,iff <target> <dx> <dy> Translate the layer by (dx, dy).
/mapmap/layer/vertex/xy ,iiff <target> <index> <x> <y> Set a destination-shape vertex.
/mapmap/layer/vertex/destination/xy ,iiff <target> <index> <x> <y> Same, explicit.
/mapmap/layer/vertex/source/xy ,iiff <target> <index> <x> <y> Set a source-shape vertex (texture layers).

Settable layer properties (``<prop>``):

::

opacity float 0.0 .. 1.0
visible int/bool
solo int/bool
locked int/bool
depth int
name string

Examples::

/mapmap/layer/opacity ,if 0 0.8
/mapmap/layer/visible ,ii 0 1
/mapmap/layer/move/xy ,iff 0 640.0 360.0
/mapmap/layer/translate/xy ,iff 0 -10.0 0.0
/mapmap/layer/vertex/source/xy ,iiff 0 2 320.0 240.0


Security
--------

Change a media path::

osc-send osc.udp://localhost:12345 /mapmap/paint/media/load ,is 0 ~/Videos/clips_finaux_ok/lys_flou_net.mov
There is no authentication on the OSC port. Anyone able to reach it can
control MapMap. If you do not want incoming messages during a show, block the
port at the firewall (or run MapMap on an isolated network). For the same
reason, MapMap deliberately does NOT expose loading or saving project files
over OSC.


See also
--------

You might consider using:
* ``scripts/mapmap-osc.py`` — a small, dependency-free OSC client shipped with
MapMap. Run ``python3 scripts/mapmap-osc.py --help``.
* The ``oscsend`` utility from `liblo <https://github.com/radarsat1/liblo>`_.
* The ``python-osc`` library for Python.

* The txosc library for python. It contains osc-send
* The oscsend utility
Example with ``mapmap-osc.py``::

python3 scripts/mapmap-osc.py /mapmap/layer/opacity 0 0.5
python3 scripts/mapmap-osc.py --port 12345 /mapmap/source/color 3 '#ff0000'
36 changes: 20 additions & 16 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,33 @@ Fonctionnalités cool à faire plus tard
--------------------------------------
* lignes des quads assignable on/off, voire animales, en midi
* animation de stroke par groupe
* OSC pour controler les positions et l'alpha des quads
* DONE OSC pour controler les positions et l'alpha des quads (voir le fichier OSC)

OSC INTERFACE ADDITIONAL CALLBACKS
----------------------------------
Instead of using boolean values (true or false) we use numbers, where 0 means false, and any positive number means true.

You should make sure to block all incoming messages on that port if you don't want to be hacked during a show.

/mapmap/mapping/move/xy ,iff <mapping identifier> <x> <y>
/mapmap/mapping/vertex/source/xy ,iiff <mapping identifier> <vertex index> <x> <y>
/mapmap/mapping/vertex/destination/xy ,iiff <mapping identifier> <vertex index> <x> <y>
/mapmap/mapping/visible ,ii <mapping identifier> <enable>
/mapmap/mapping/highlight ,ii <mapping identifier> <enable>
/mapmap/mapping/vertex/highlight ,iii <mapping identifier> <vertex identifier> <enable>
/mapmap/project/load ,s <file>
/mapmap/project/save ,s <file>
/mapmap/paint/color/rgba ,iffff <paint identifier> <red> <green> <blue> <alpha> (each channel within [0,1])
/mapmap/paint/media/load ,is <paint identifier> <file>
/mapmap/paint/media/speed ,if <paint identifier> <speed ratio> (1.0 means 100% speed)
/mapmap/paint/media/seek ,il <paint identifier> <time position> (in milliseconds)
/mapmap/output/fullscreen ,i <enable>
/mapmap/output/size ,ii <width> <height>
/mapmap/output/position ,ii <x> <y>
The full, current address scheme lives in the `OSC` file at the root of the
repo. Status of the originally-wished-for callbacks below ("mapping" is now
spelled "layer", but kept as an alias; "paint" is now "source"):

DONE /mapmap/layer/move/xy ,iff <layer> <x> <y>
DONE /mapmap/layer/vertex/source/xy ,iiff <layer> <vertex index> <x> <y>
DONE /mapmap/layer/vertex/destination/xy ,iiff <layer> <vertex index> <x> <y>
DONE /mapmap/layer/visible ,ii <layer> <enable>
DONE /mapmap/source/color ,is <source> <#rrggbb> (replaces paint/color/rgba)
DONE /mapmap/source/uri ,is <source> <file> (replaces paint/media/load)
DONE /mapmap/source/rate ,if <source> <speed %> (replaces paint/media/speed)
TODO /mapmap/layer/highlight ,ii <layer> <enable>
TODO /mapmap/layer/vertex/highlight ,iii <layer> <vertex identifier> <enable>
TODO /mapmap/source/seek ,if <source> <time position> (no seek API exposed yet)
TODO /mapmap/output/fullscreen ,i <enable>
TODO /mapmap/output/size ,ii <width> <height>
TODO /mapmap/output/position ,ii <x> <y>
WONTDO /mapmap/project/load ,s <file> (file I/O over OSC is a security risk)
WONTDO /mapmap/project/save ,s <file> (file I/O over OSC is a security risk)


Roadmap (to do)
Expand Down
107 changes: 107 additions & 0 deletions scripts/mapmap-osc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Tiny, dependency-free OSC client for driving MapMap over UDP.

MapMap listens for OSC on UDP port 12345 by default (see the ``OSC`` file at
the root of the repository for the full address scheme). This script encodes a
single OSC message and sends it, with no third-party dependencies.

Argument types are inferred automatically:

* a token that parses as an integer is sent as an OSC int (``i``);
* a token that parses as a float is sent as an OSC float (``f``);
* anything else is sent as an OSC string (``s``).

Prefix a token with ``i:``, ``f:`` or ``s:`` to force its type, e.g. ``s:12``
sends the string "12" rather than the integer 12 (useful to select an element
by a numeric name).

Examples::

python3 scripts/mapmap-osc.py /mapmap/play
python3 scripts/mapmap-osc.py /mapmap/layer/opacity 0 0.5
python3 scripts/mapmap-osc.py /mapmap/layer/move/xy 0 640 360
python3 scripts/mapmap-osc.py /mapmap/source/color 3 '#ff0000'
python3 scripts/mapmap-osc.py --host 192.168.1.20 --port 9000 /mapmap/pause
"""

import argparse
import socket
import struct
import sys


def _osc_string(value: str) -> bytes:
"""Encode an OSC string: UTF-8 bytes, null-terminated, padded to 4 bytes."""
data = value.encode("utf-8") + b"\x00"
if len(data) % 4:
data += b"\x00" * (4 - len(data) % 4)
return data


def _encode_argument(token: str):
"""Return (type_tag, packed_bytes) for one command-line argument token."""
forced = None
if len(token) > 1 and token[1] == ":" and token[0] in "ifs":
forced, token = token[0], token[2:]

if forced == "s":
return "s", _osc_string(token)
if forced == "i":
return "i", struct.pack(">i", int(token))
if forced == "f":
return "f", struct.pack(">f", float(token))

# Auto-detect: int, then float, then fall back to string.
try:
return "i", struct.pack(">i", int(token))
except ValueError:
pass
try:
return "f", struct.pack(">f", float(token))
except ValueError:
pass
return "s", _osc_string(token)


def build_message(address: str, tokens) -> bytes:
"""Build a complete OSC message packet from an address and argument tokens."""
tags = ","
payload = b""
for token in tokens:
tag, packed = _encode_argument(token)
tags += tag
payload += packed
return _osc_string(address) + _osc_string(tags) + payload


def main(argv=None) -> int:
parser = argparse.ArgumentParser(
description="Send a single OSC message to MapMap.",
epilog=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("address", help="OSC address, e.g. /mapmap/layer/opacity")
parser.add_argument("args", nargs="*", help="OSC arguments (types auto-detected)")
parser.add_argument("--host", default="127.0.0.1", help="target host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=12345, help="target UDP port (default: 12345)")
parser.add_argument("-v", "--verbose", action="store_true", help="print what is sent")
options = parser.parse_args(argv)

if not options.address.startswith("/"):
parser.error("OSC address must start with '/' (e.g. /mapmap/play)")

packet = build_message(options.address, options.args)

if options.verbose:
print(f"-> {options.host}:{options.port} {options.address} {' '.join(options.args)}")

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(packet, (options.host, options.port))
finally:
sock.close()
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading