Skip to content

Commit 1ddce0c

Browse files
committed
Document the OSC interface and add a test client
- Rewrite the OSC file: the old reference described addresses that no longer exist (/mapmap/paint/media/load). Document the actual address scheme — global transport, per-source and per-layer commands, the id/name selector convention, the coordinate space, and the security stance (no project file I/O over OSC). - Add scripts/mapmap-osc.py, a small dependency-free OSC client with automatic argument typing, for testing MapMap over OSC. - Update TODO: mark the implemented OSC callbacks as DONE, the ones still open as TODO, and the project load/save callbacks as WONTDO (security).
1 parent 6b27c2c commit 1ddce0c

3 files changed

Lines changed: 243 additions & 32 deletions

File tree

OSC

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,130 @@
11
MapMap OSC Interface
22
====================
33

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

7-
/mapmap/paint/media/load
8-
~~~~~~~~~~~~
9-
Change a media URI::
10-
11-
/mapmap/paint/media/load ,is <paintId> <path>
9+
By default MapMap listens on UDP port **12345**. Change it from the
10+
Preferences dialog, or at launch with ``mapmap --osc-port <port>``.
1211

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

16-
Examples
13+
Conventions
14+
-----------
15+
16+
Every address starts with ``/mapmap``. Commands that act on a source or a
17+
layer take the **target as their first argument**:
18+
19+
* an **integer** selects a single element by its id, or
20+
* a **string** selects every element whose name matches the given pattern
21+
(shell-style wildcards, e.g. ``clip*``).
22+
23+
"layer" and "mapping" are accepted as synonyms (a layer *is* a mapping).
24+
25+
Coordinates (for ``move``, ``translate`` and ``vertex``) are expressed in
26+
MapMap's internal coordinate space — the same numbers stored for each vertex
27+
in the ``.mmp`` project file. These are pixel coordinates relative to the
28+
canvas, with the origin at its top-left corner. Open a saved project to read
29+
the actual values you want to target.
30+
31+
32+
Global transport
33+
-----------------
34+
35+
::
36+
37+
/mapmap/play Start playback of every source.
38+
/mapmap/pause Pause playback of every source.
39+
/mapmap/rewind Rewind every source.
40+
/mapmap/quit Quit MapMap.
41+
42+
43+
Sources
44+
-------
45+
46+
``<target>`` is an integer id or a name pattern (see Conventions).
47+
48+
::
49+
50+
/mapmap/source/play ,i|s <target> Start playback of the source(s).
51+
/mapmap/source/pause ,i|s <target> Pause the source(s).
52+
/mapmap/source/rewind ,i|s <target> Rewind the source(s).
53+
/mapmap/source/<prop> ,i|s ... <target> <val> Set a property (see below).
54+
55+
Settable source properties (``<prop>``):
56+
57+
::
58+
59+
opacity float 0.0 .. 1.0
60+
name string
61+
uri string path of a video/image source
62+
rate float playback rate, in % (negative = reverse)
63+
volume float audio volume, in % (video sources)
64+
color string "#rrggbb" or a colour name (color sources)
65+
locked int/bool
66+
67+
Examples::
68+
69+
/mapmap/source/opacity ,if 2 0.5
70+
/mapmap/source/color ,is 3 #ff0000
71+
/mapmap/source/uri ,is "Clip *" /home/vj/loops/a.mov
72+
/mapmap/source/play ,i 2
73+
74+
75+
Layers
76+
------
77+
78+
``<target>`` is an integer id or a name pattern (see Conventions).
79+
80+
::
81+
82+
/mapmap/layer/<prop> ,i|s ... <target> <val> Set a property.
83+
/mapmap/layer/move/xy ,iff <target> <x> <y> Move so the layer's centre is at (x, y).
84+
/mapmap/layer/translate/xy ,iff <target> <dx> <dy> Translate the layer by (dx, dy).
85+
/mapmap/layer/vertex/xy ,iiff <target> <index> <x> <y> Set a destination-shape vertex.
86+
/mapmap/layer/vertex/destination/xy ,iiff <target> <index> <x> <y> Same, explicit.
87+
/mapmap/layer/vertex/source/xy ,iiff <target> <index> <x> <y> Set a source-shape vertex (texture layers).
88+
89+
Settable layer properties (``<prop>``):
90+
91+
::
92+
93+
opacity float 0.0 .. 1.0
94+
visible int/bool
95+
solo int/bool
96+
locked int/bool
97+
depth int
98+
name string
99+
100+
Examples::
101+
102+
/mapmap/layer/opacity ,if 0 0.8
103+
/mapmap/layer/visible ,ii 0 1
104+
/mapmap/layer/move/xy ,iff 0 640.0 360.0
105+
/mapmap/layer/translate/xy ,iff 0 -10.0 0.0
106+
/mapmap/layer/vertex/source/xy ,iiff 0 2 320.0 240.0
107+
108+
109+
Security
17110
--------
18111

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

23119
See also
24120
--------
25121

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

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

129+
python3 scripts/mapmap-osc.py /mapmap/layer/opacity 0 0.5
130+
python3 scripts/mapmap-osc.py --port 12345 /mapmap/source/color 3 '#ff0000'

TODO

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,29 +68,33 @@ Fonctionnalités cool à faire plus tard
6868
--------------------------------------
6969
* lignes des quads assignable on/off, voire animales, en midi
7070
* animation de stroke par groupe
71-
* OSC pour controler les positions et l'alpha des quads
71+
* DONE OSC pour controler les positions et l'alpha des quads (voir le fichier OSC)
7272

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

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

79-
/mapmap/mapping/move/xy ,iff <mapping identifier> <x> <y>
80-
/mapmap/mapping/vertex/source/xy ,iiff <mapping identifier> <vertex index> <x> <y>
81-
/mapmap/mapping/vertex/destination/xy ,iiff <mapping identifier> <vertex index> <x> <y>
82-
/mapmap/mapping/visible ,ii <mapping identifier> <enable>
83-
/mapmap/mapping/highlight ,ii <mapping identifier> <enable>
84-
/mapmap/mapping/vertex/highlight ,iii <mapping identifier> <vertex identifier> <enable>
85-
/mapmap/project/load ,s <file>
86-
/mapmap/project/save ,s <file>
87-
/mapmap/paint/color/rgba ,iffff <paint identifier> <red> <green> <blue> <alpha> (each channel within [0,1])
88-
/mapmap/paint/media/load ,is <paint identifier> <file>
89-
/mapmap/paint/media/speed ,if <paint identifier> <speed ratio> (1.0 means 100% speed)
90-
/mapmap/paint/media/seek ,il <paint identifier> <time position> (in milliseconds)
91-
/mapmap/output/fullscreen ,i <enable>
92-
/mapmap/output/size ,ii <width> <height>
93-
/mapmap/output/position ,ii <x> <y>
79+
The full, current address scheme lives in the `OSC` file at the root of the
80+
repo. Status of the originally-wished-for callbacks below ("mapping" is now
81+
spelled "layer", but kept as an alias; "paint" is now "source"):
82+
83+
DONE /mapmap/layer/move/xy ,iff <layer> <x> <y>
84+
DONE /mapmap/layer/vertex/source/xy ,iiff <layer> <vertex index> <x> <y>
85+
DONE /mapmap/layer/vertex/destination/xy ,iiff <layer> <vertex index> <x> <y>
86+
DONE /mapmap/layer/visible ,ii <layer> <enable>
87+
DONE /mapmap/source/color ,is <source> <#rrggbb> (replaces paint/color/rgba)
88+
DONE /mapmap/source/uri ,is <source> <file> (replaces paint/media/load)
89+
DONE /mapmap/source/rate ,if <source> <speed %> (replaces paint/media/speed)
90+
TODO /mapmap/layer/highlight ,ii <layer> <enable>
91+
TODO /mapmap/layer/vertex/highlight ,iii <layer> <vertex identifier> <enable>
92+
TODO /mapmap/source/seek ,if <source> <time position> (no seek API exposed yet)
93+
TODO /mapmap/output/fullscreen ,i <enable>
94+
TODO /mapmap/output/size ,ii <width> <height>
95+
TODO /mapmap/output/position ,ii <x> <y>
96+
WONTDO /mapmap/project/load ,s <file> (file I/O over OSC is a security risk)
97+
WONTDO /mapmap/project/save ,s <file> (file I/O over OSC is a security risk)
9498

9599

96100
Roadmap (to do)

scripts/mapmap-osc.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python3
2+
"""Tiny, dependency-free OSC client for driving MapMap over UDP.
3+
4+
MapMap listens for OSC on UDP port 12345 by default (see the ``OSC`` file at
5+
the root of the repository for the full address scheme). This script encodes a
6+
single OSC message and sends it, with no third-party dependencies.
7+
8+
Argument types are inferred automatically:
9+
10+
* a token that parses as an integer is sent as an OSC int (``i``);
11+
* a token that parses as a float is sent as an OSC float (``f``);
12+
* anything else is sent as an OSC string (``s``).
13+
14+
Prefix a token with ``i:``, ``f:`` or ``s:`` to force its type, e.g. ``s:12``
15+
sends the string "12" rather than the integer 12 (useful to select an element
16+
by a numeric name).
17+
18+
Examples::
19+
20+
python3 scripts/mapmap-osc.py /mapmap/play
21+
python3 scripts/mapmap-osc.py /mapmap/layer/opacity 0 0.5
22+
python3 scripts/mapmap-osc.py /mapmap/layer/move/xy 0 640 360
23+
python3 scripts/mapmap-osc.py /mapmap/source/color 3 '#ff0000'
24+
python3 scripts/mapmap-osc.py --host 192.168.1.20 --port 9000 /mapmap/pause
25+
"""
26+
27+
import argparse
28+
import socket
29+
import struct
30+
import sys
31+
32+
33+
def _osc_string(value: str) -> bytes:
34+
"""Encode an OSC string: UTF-8 bytes, null-terminated, padded to 4 bytes."""
35+
data = value.encode("utf-8") + b"\x00"
36+
if len(data) % 4:
37+
data += b"\x00" * (4 - len(data) % 4)
38+
return data
39+
40+
41+
def _encode_argument(token: str):
42+
"""Return (type_tag, packed_bytes) for one command-line argument token."""
43+
forced = None
44+
if len(token) > 1 and token[1] == ":" and token[0] in "ifs":
45+
forced, token = token[0], token[2:]
46+
47+
if forced == "s":
48+
return "s", _osc_string(token)
49+
if forced == "i":
50+
return "i", struct.pack(">i", int(token))
51+
if forced == "f":
52+
return "f", struct.pack(">f", float(token))
53+
54+
# Auto-detect: int, then float, then fall back to string.
55+
try:
56+
return "i", struct.pack(">i", int(token))
57+
except ValueError:
58+
pass
59+
try:
60+
return "f", struct.pack(">f", float(token))
61+
except ValueError:
62+
pass
63+
return "s", _osc_string(token)
64+
65+
66+
def build_message(address: str, tokens) -> bytes:
67+
"""Build a complete OSC message packet from an address and argument tokens."""
68+
tags = ","
69+
payload = b""
70+
for token in tokens:
71+
tag, packed = _encode_argument(token)
72+
tags += tag
73+
payload += packed
74+
return _osc_string(address) + _osc_string(tags) + payload
75+
76+
77+
def main(argv=None) -> int:
78+
parser = argparse.ArgumentParser(
79+
description="Send a single OSC message to MapMap.",
80+
epilog=__doc__,
81+
formatter_class=argparse.RawDescriptionHelpFormatter,
82+
)
83+
parser.add_argument("address", help="OSC address, e.g. /mapmap/layer/opacity")
84+
parser.add_argument("args", nargs="*", help="OSC arguments (types auto-detected)")
85+
parser.add_argument("--host", default="127.0.0.1", help="target host (default: 127.0.0.1)")
86+
parser.add_argument("--port", type=int, default=12345, help="target UDP port (default: 12345)")
87+
parser.add_argument("-v", "--verbose", action="store_true", help="print what is sent")
88+
options = parser.parse_args(argv)
89+
90+
if not options.address.startswith("/"):
91+
parser.error("OSC address must start with '/' (e.g. /mapmap/play)")
92+
93+
packet = build_message(options.address, options.args)
94+
95+
if options.verbose:
96+
print(f"-> {options.host}:{options.port} {options.address} {' '.join(options.args)}")
97+
98+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
99+
try:
100+
sock.sendto(packet, (options.host, options.port))
101+
finally:
102+
sock.close()
103+
return 0
104+
105+
106+
if __name__ == "__main__":
107+
sys.exit(main())

0 commit comments

Comments
 (0)