python-osc 1.9.3__tar.gz → 1.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_osc-1.9.3/python_osc.egg-info → python_osc-1.10.2}/PKG-INFO +30 -31
- {python_osc-1.9.3 → python_osc-1.10.2}/README.rst +2 -2
- {python_osc-1.9.3 → python_osc-1.10.2}/pyproject.toml +17 -3
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/dispatcher.py +113 -61
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_bundle_builder.py +4 -2
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_message.py +6 -2
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_message_builder.py +18 -10
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_packet.py +1 -2
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_server.py +24 -2
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_tcp_server.py +24 -4
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/parsing/osc_types.py +12 -18
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/tcp_client.py +61 -23
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/parsing/test_osc_types.py +5 -5
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_dispatcher.py +61 -1
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle.py +4 -10
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_message.py +4 -12
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_message_builder.py +1 -1
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_packet.py +1 -1
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_server.py +25 -1
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_tcp_server.py +9 -9
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_udp_client.py +41 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/udp_client.py +36 -10
- python_osc-1.9.3/MANIFEST.in +0 -3
- python_osc-1.9.3/PKG-INFO +0 -198
- python_osc-1.9.3/python_osc.egg-info/SOURCES.txt +0 -38
- python_osc-1.9.3/python_osc.egg-info/dependency_links.txt +0 -1
- python_osc-1.9.3/python_osc.egg-info/top_level.txt +0 -1
- python_osc-1.9.3/setup.cfg +0 -4
- {python_osc-1.9.3 → python_osc-1.10.2}/LICENSE.txt +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/__init__.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/osc_bundle.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/parsing/__init__.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/parsing/ntp.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/py.typed +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/slip.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/__init__.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/parsing/__init__.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/parsing/test_ntp.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle_builder.py +0 -0
- {python_osc-1.9.3 → python_osc-1.10.2}/pythonosc/test/test_tcp_client.py +0 -0
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: python-osc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.2
|
|
4
4
|
Summary: Open Sound Control server and client implementations in pure Python
|
|
5
|
+
Keywords: osc,sound,midi,music
|
|
6
|
+
Author: attwad
|
|
5
7
|
Author-email: attwad <tmusoft@gmail.com>
|
|
6
8
|
License: This is free and unencumbered software released into the public domain.
|
|
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
|
-
Project-URL: Repository, https://github.com/attwad/python-osc
|
|
32
|
-
Keywords: osc,sound,midi,music
|
|
9
|
+
|
|
10
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
11
|
+
distribute this software, either in source code form or as a compiled
|
|
12
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
13
|
+
means.
|
|
14
|
+
|
|
15
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
16
|
+
of this software dedicate any and all copyright interest in the
|
|
17
|
+
software to the public domain. We make this dedication for the benefit
|
|
18
|
+
of the public at large and to the detriment of our heirs and
|
|
19
|
+
successors. We intend this dedication to be an overt act of
|
|
20
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
21
|
+
software under copyright law.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
24
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
25
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
26
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
27
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
28
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
29
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
30
|
+
|
|
31
|
+
For more information, please refer to <http://unlicense.org/>
|
|
33
32
|
Classifier: Development Status :: 5 - Production/Stable
|
|
34
33
|
Classifier: Intended Audience :: Developers
|
|
35
34
|
Classifier: License :: Freely Distributable
|
|
@@ -37,8 +36,8 @@ Classifier: Programming Language :: Python :: 3
|
|
|
37
36
|
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
38
37
|
Classifier: Topic :: System :: Networking
|
|
39
38
|
Requires-Python: >=3.10
|
|
39
|
+
Project-URL: Repository, https://github.com/attwad/python-osc
|
|
40
40
|
Description-Content-Type: text/x-rst
|
|
41
|
-
License-File: LICENSE.txt
|
|
42
41
|
|
|
43
42
|
==========
|
|
44
43
|
python-osc
|
|
@@ -115,7 +114,7 @@ Simple client
|
|
|
115
114
|
help="The port the OSC server is listening on")
|
|
116
115
|
args = parser.parse_args()
|
|
117
116
|
|
|
118
|
-
client = udp_client.SimpleUDPClient(args.ip, args.port)
|
|
117
|
+
client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
|
|
119
118
|
|
|
120
119
|
for x in range(10):
|
|
121
120
|
client.send_message("/filter", random.random())
|
|
@@ -159,7 +158,7 @@ Simple server
|
|
|
159
158
|
dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
|
|
160
159
|
|
|
161
160
|
server = osc_server.ThreadingOSCUDPServer(
|
|
162
|
-
(args.ip, args.port), dispatcher)
|
|
161
|
+
(args.ip, args.port), dispatcher, timeout=10)
|
|
163
162
|
print("Serving on {}".format(server.server_address))
|
|
164
163
|
server.serve_forever()
|
|
165
164
|
|
|
@@ -73,7 +73,7 @@ Simple client
|
|
|
73
73
|
help="The port the OSC server is listening on")
|
|
74
74
|
args = parser.parse_args()
|
|
75
75
|
|
|
76
|
-
client = udp_client.SimpleUDPClient(args.ip, args.port)
|
|
76
|
+
client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
|
|
77
77
|
|
|
78
78
|
for x in range(10):
|
|
79
79
|
client.send_message("/filter", random.random())
|
|
@@ -117,7 +117,7 @@ Simple server
|
|
|
117
117
|
dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
|
|
118
118
|
|
|
119
119
|
server = osc_server.ThreadingOSCUDPServer(
|
|
120
|
-
(args.ip, args.port), dispatcher)
|
|
120
|
+
(args.ip, args.port), dispatcher, timeout=10)
|
|
121
121
|
print("Serving on {}".format(server.server_address))
|
|
122
122
|
server.serve_forever()
|
|
123
123
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["
|
|
3
|
-
build-backend = "
|
|
2
|
+
requires = ["uv-build"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-osc"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.10.2"
|
|
8
8
|
description = "Open Sound Control server and client implementations in pure Python"
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,10 +21,24 @@ classifiers = [
|
|
|
21
21
|
"Topic :: Multimedia :: Sound/Audio",
|
|
22
22
|
"Topic :: System :: Networking",
|
|
23
23
|
]
|
|
24
|
+
dependencies = []
|
|
24
25
|
|
|
25
26
|
[project.urls]
|
|
26
27
|
Repository = "https://github.com/attwad/python-osc"
|
|
27
28
|
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest",
|
|
32
|
+
"mypy",
|
|
33
|
+
"ruff",
|
|
34
|
+
"pytest-cov",
|
|
35
|
+
"pre-commit",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.uv.build-backend]
|
|
39
|
+
module-name = "pythonosc"
|
|
40
|
+
module-root = "."
|
|
41
|
+
|
|
28
42
|
[tool.mypy]
|
|
29
43
|
# Would be great to turn this on, however there's too many cases it would break
|
|
30
44
|
# right now.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Maps OSC addresses to handler functions
|
|
2
|
-
"""
|
|
1
|
+
"""Maps OSC addresses to handler functions"""
|
|
3
2
|
|
|
3
|
+
import asyncio
|
|
4
4
|
import collections
|
|
5
5
|
import inspect
|
|
6
6
|
import logging
|
|
@@ -81,6 +81,46 @@ class Handler(object):
|
|
|
81
81
|
else:
|
|
82
82
|
return self.callback(message.address, *message)
|
|
83
83
|
|
|
84
|
+
async def async_invoke(
|
|
85
|
+
self, client_address: Tuple[str, int], message: OscMessage
|
|
86
|
+
) -> Union[None, AnyStr, Tuple[AnyStr, ArgValue]]:
|
|
87
|
+
"""Invokes the associated callback function (asynchronously)
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
client_address: Address match that causes the invocation
|
|
91
|
+
message: Message causing invocation
|
|
92
|
+
Returns:
|
|
93
|
+
The result of the handler function can be None, a string OSC address, or a tuple of the OSC address
|
|
94
|
+
and arguments.
|
|
95
|
+
"""
|
|
96
|
+
cb = self.callback
|
|
97
|
+
is_async = inspect.iscoroutinefunction(cb)
|
|
98
|
+
|
|
99
|
+
if self.needs_reply_address:
|
|
100
|
+
if self.args:
|
|
101
|
+
if is_async:
|
|
102
|
+
return await cb(
|
|
103
|
+
client_address, message.address, self.args, *message
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
return cb(client_address, message.address, self.args, *message)
|
|
107
|
+
else:
|
|
108
|
+
if is_async:
|
|
109
|
+
return await cb(client_address, message.address, *message)
|
|
110
|
+
else:
|
|
111
|
+
return cb(client_address, message.address, *message)
|
|
112
|
+
else:
|
|
113
|
+
if self.args:
|
|
114
|
+
if is_async:
|
|
115
|
+
return await cb(message.address, self.args, *message)
|
|
116
|
+
else:
|
|
117
|
+
return cb(message.address, self.args, *message)
|
|
118
|
+
else:
|
|
119
|
+
if is_async:
|
|
120
|
+
return await cb(message.address, *message)
|
|
121
|
+
else:
|
|
122
|
+
return cb(message.address, *message)
|
|
123
|
+
|
|
84
124
|
|
|
85
125
|
class Dispatcher(object):
|
|
86
126
|
"""Maps Handlers to OSC addresses and dispatches messages to the handler on matched addresses
|
|
@@ -88,9 +128,20 @@ class Dispatcher(object):
|
|
|
88
128
|
Maps OSC addresses to handler functions and invokes the correct handler when a message comes in.
|
|
89
129
|
"""
|
|
90
130
|
|
|
91
|
-
def __init__(self) -> None:
|
|
131
|
+
def __init__(self, strict_timing: bool = True) -> None:
|
|
132
|
+
"""Initialize the dispatcher.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
strict_timing: Whether to automatically schedule messages with future timetags.
|
|
136
|
+
If True (default), the dispatcher will wait (using sleep) until the specified
|
|
137
|
+
timetag before invoking handlers.
|
|
138
|
+
If False, messages are dispatched immediately regardless of their timetag.
|
|
139
|
+
Disabling this can prevent memory/thread accumulation issues when receiving
|
|
140
|
+
many future-dated messages.
|
|
141
|
+
"""
|
|
92
142
|
self._map: DefaultDict[str, List[Handler]] = collections.defaultdict(list)
|
|
93
143
|
self._default_handler: Optional[Handler] = None
|
|
144
|
+
self._strict_timing = strict_timing
|
|
94
145
|
|
|
95
146
|
def map(
|
|
96
147
|
self,
|
|
@@ -183,32 +234,66 @@ class Dispatcher(object):
|
|
|
183
234
|
) -> Generator[Handler, None, None]:
|
|
184
235
|
"""Yields handlers matching an address
|
|
185
236
|
|
|
186
|
-
|
|
187
237
|
Args:
|
|
188
238
|
address_pattern: Address to match
|
|
189
239
|
|
|
190
240
|
Returns:
|
|
191
241
|
Generator yielding Handlers matching address_pattern
|
|
192
242
|
"""
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
# Convert OSC Address Pattern to a Python regular expression.
|
|
244
|
+
# Spec: https://opensoundcontrol.stanford.edu/spec-1_0.html#osc-address-patterns
|
|
245
|
+
|
|
246
|
+
pattern = "^"
|
|
247
|
+
i = 0
|
|
248
|
+
while i < len(address_pattern):
|
|
249
|
+
c = address_pattern[i]
|
|
250
|
+
if c == "*":
|
|
251
|
+
pattern += "[^/]*"
|
|
252
|
+
elif c == "?":
|
|
253
|
+
pattern += "[^/]"
|
|
254
|
+
elif c == "[":
|
|
255
|
+
pattern += "["
|
|
256
|
+
i += 1
|
|
257
|
+
if i < len(address_pattern) and address_pattern[i] == "!":
|
|
258
|
+
pattern += "^"
|
|
259
|
+
i += 1
|
|
260
|
+
while i < len(address_pattern) and address_pattern[i] != "]":
|
|
261
|
+
if address_pattern[i] in r"\^$.|()+*?":
|
|
262
|
+
pattern += "\\"
|
|
263
|
+
pattern += address_pattern[i]
|
|
264
|
+
i += 1
|
|
265
|
+
pattern += "]"
|
|
266
|
+
elif c == "{":
|
|
267
|
+
pattern += "("
|
|
268
|
+
i += 1
|
|
269
|
+
while i < len(address_pattern) and address_pattern[i] != "}":
|
|
270
|
+
char = address_pattern[i]
|
|
271
|
+
if char == ",":
|
|
272
|
+
pattern += "|"
|
|
273
|
+
elif char in r"\^$.|()[]+*?":
|
|
274
|
+
pattern += "\\" + char
|
|
275
|
+
else:
|
|
276
|
+
pattern += char
|
|
277
|
+
i += 1
|
|
278
|
+
pattern += ")"
|
|
279
|
+
elif c in r"\^$.|()[]+?":
|
|
280
|
+
pattern += "\\" + c
|
|
281
|
+
else:
|
|
282
|
+
pattern += c
|
|
283
|
+
i += 1
|
|
284
|
+
pattern += "$"
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
patterncompiled = re.compile(pattern)
|
|
288
|
+
except re.error:
|
|
289
|
+
# If the pattern is invalid, it won't match anything.
|
|
290
|
+
return
|
|
207
291
|
|
|
292
|
+
matched = False
|
|
208
293
|
for addr, handlers in self._map.items():
|
|
209
294
|
if patterncompiled.match(addr) or (
|
|
210
|
-
|
|
211
|
-
and re.match(addr.replace("*", "
|
|
295
|
+
"*" in addr
|
|
296
|
+
and re.match(addr.replace("*", ".*?") + "$", address_pattern)
|
|
212
297
|
):
|
|
213
298
|
yield from handlers
|
|
214
299
|
matched = True
|
|
@@ -239,7 +324,7 @@ class Dispatcher(object):
|
|
|
239
324
|
if not handlers:
|
|
240
325
|
continue
|
|
241
326
|
# If the message is to be handled later, then so be it.
|
|
242
|
-
if timed_msg.time > now:
|
|
327
|
+
if self._strict_timing and timed_msg.time > now:
|
|
243
328
|
time.sleep(timed_msg.time - now)
|
|
244
329
|
for handler in handlers:
|
|
245
330
|
result = handler.invoke(client_address, timed_msg.message)
|
|
@@ -276,48 +361,15 @@ class Dispatcher(object):
|
|
|
276
361
|
if not handlers:
|
|
277
362
|
continue
|
|
278
363
|
# If the message is to be handled later, then so be it.
|
|
279
|
-
if timed_msg.time > now:
|
|
280
|
-
|
|
364
|
+
if self._strict_timing and timed_msg.time > now:
|
|
365
|
+
await asyncio.sleep(timed_msg.time - now)
|
|
281
366
|
for handler in handlers:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
timed_msg.message.address,
|
|
287
|
-
handler.args,
|
|
288
|
-
*timed_msg.message,
|
|
289
|
-
)
|
|
290
|
-
elif handler.args:
|
|
291
|
-
result = await handler.callback(
|
|
292
|
-
timed_msg.message.address,
|
|
293
|
-
handler.args,
|
|
294
|
-
*timed_msg.message,
|
|
295
|
-
)
|
|
296
|
-
else:
|
|
297
|
-
result = await handler.callback(
|
|
298
|
-
timed_msg.message.address, *timed_msg.message
|
|
299
|
-
)
|
|
300
|
-
else:
|
|
301
|
-
if handler.needs_reply_address:
|
|
302
|
-
result = handler.callback(
|
|
303
|
-
client_address,
|
|
304
|
-
timed_msg.message.address,
|
|
305
|
-
handler.args,
|
|
306
|
-
*timed_msg.message,
|
|
307
|
-
)
|
|
308
|
-
elif handler.args:
|
|
309
|
-
result = handler.callback(
|
|
310
|
-
timed_msg.message.address,
|
|
311
|
-
handler.args,
|
|
312
|
-
*timed_msg.message,
|
|
313
|
-
)
|
|
314
|
-
else:
|
|
315
|
-
result = handler.callback(
|
|
316
|
-
timed_msg.message.address, *timed_msg.message
|
|
317
|
-
)
|
|
318
|
-
if result:
|
|
367
|
+
result = await handler.async_invoke(
|
|
368
|
+
client_address, timed_msg.message
|
|
369
|
+
)
|
|
370
|
+
if result is not None:
|
|
319
371
|
results.append(result)
|
|
320
|
-
except osc_packet.ParseError
|
|
372
|
+
except osc_packet.ParseError:
|
|
321
373
|
pass
|
|
322
374
|
return results
|
|
323
375
|
|
|
@@ -25,9 +25,11 @@ class OscBundleBuilder(object):
|
|
|
25
25
|
seconds since the epoch in UTC or IMMEDIATELY.
|
|
26
26
|
"""
|
|
27
27
|
self._timestamp = timestamp
|
|
28
|
-
self._contents: List[osc_bundle.OscBundle] = []
|
|
28
|
+
self._contents: List[osc_bundle.OscBundle | osc_message.OscMessage] = []
|
|
29
29
|
|
|
30
|
-
def add_content(
|
|
30
|
+
def add_content(
|
|
31
|
+
self, content: osc_bundle.OscBundle | osc_message.OscMessage
|
|
32
|
+
) -> None:
|
|
31
33
|
"""Add a new content to this bundle.
|
|
32
34
|
|
|
33
35
|
Args:
|
|
@@ -34,8 +34,12 @@ class OscMessage(object):
|
|
|
34
34
|
|
|
35
35
|
# Get the parameters types.
|
|
36
36
|
type_tag, index = osc_types.get_string(self._dgram, index)
|
|
37
|
-
if type_tag.startswith(","):
|
|
38
|
-
|
|
37
|
+
if not type_tag.startswith(","):
|
|
38
|
+
raise ParseError(
|
|
39
|
+
f"OSC Type Tag String must start with a comma, got: {type_tag}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
type_tag = type_tag[1:]
|
|
39
43
|
|
|
40
44
|
params = [] # type: List[Any]
|
|
41
45
|
param_stack = [params]
|
|
@@ -5,7 +5,11 @@ from typing import Any, Iterable, List, Optional, Tuple, Union
|
|
|
5
5
|
from pythonosc import osc_message
|
|
6
6
|
from pythonosc.parsing import osc_types
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Represents a single OSC argument value.
|
|
9
|
+
# Can be a primitive type, a MIDI packet, or a list/tuple for nested OSC arrays.
|
|
10
|
+
ArgValue = Union[
|
|
11
|
+
str, bytes, bool, int, float, osc_types.MidiPacket, List[Any], Tuple[Any, ...], None
|
|
12
|
+
]
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
class BuildError(Exception):
|
|
@@ -68,9 +72,9 @@ class OscMessageBuilder(object):
|
|
|
68
72
|
"""Returns the (type, value) arguments list of this message."""
|
|
69
73
|
return self._args
|
|
70
74
|
|
|
71
|
-
def _valid_type(self, arg_type: str) -> bool:
|
|
72
|
-
if arg_type
|
|
73
|
-
return
|
|
75
|
+
def _valid_type(self, arg_type: Union[str, List[Any]]) -> bool:
|
|
76
|
+
if isinstance(arg_type, str):
|
|
77
|
+
return arg_type in self._SUPPORTED_ARG_TYPES
|
|
74
78
|
elif isinstance(arg_type, list):
|
|
75
79
|
for sub_type in arg_type:
|
|
76
80
|
if not self._valid_type(sub_type):
|
|
@@ -78,7 +82,9 @@ class OscMessageBuilder(object):
|
|
|
78
82
|
return True
|
|
79
83
|
return False
|
|
80
84
|
|
|
81
|
-
def add_arg(
|
|
85
|
+
def add_arg(
|
|
86
|
+
self, arg_value: ArgValue, arg_type: Optional[Union[str, List[Any]]] = None
|
|
87
|
+
) -> None:
|
|
82
88
|
"""Add a typed argument to this message.
|
|
83
89
|
|
|
84
90
|
Args:
|
|
@@ -96,7 +102,7 @@ class OscMessageBuilder(object):
|
|
|
96
102
|
arg_type = self._get_arg_type(arg_value)
|
|
97
103
|
if isinstance(arg_type, list):
|
|
98
104
|
self._args.append((self.ARG_TYPE_ARRAY_START, None))
|
|
99
|
-
for v, t in zip(arg_value, arg_type): # type: ignore[
|
|
105
|
+
for v, t in zip(arg_value, arg_type): # type: ignore[arg-type]
|
|
100
106
|
self.add_arg(v, t)
|
|
101
107
|
self._args.append((self.ARG_TYPE_ARRAY_STOP, None))
|
|
102
108
|
else:
|
|
@@ -121,7 +127,7 @@ class OscMessageBuilder(object):
|
|
|
121
127
|
elif arg_value is False:
|
|
122
128
|
arg_type = self.ARG_TYPE_FALSE
|
|
123
129
|
elif isinstance(arg_value, int):
|
|
124
|
-
if arg_value.bit_length() >
|
|
130
|
+
if arg_value.bit_length() > 31:
|
|
125
131
|
arg_type = self.ARG_TYPE_INT64
|
|
126
132
|
else:
|
|
127
133
|
arg_type = self.ARG_TYPE_INT
|
|
@@ -134,7 +140,7 @@ class OscMessageBuilder(object):
|
|
|
134
140
|
elif arg_value is None:
|
|
135
141
|
arg_type = self.ARG_TYPE_NIL
|
|
136
142
|
else:
|
|
137
|
-
raise ValueError("
|
|
143
|
+
raise ValueError("Inferred arg_value type is not supported")
|
|
138
144
|
return arg_type
|
|
139
145
|
|
|
140
146
|
def build(self) -> osc_message.OscMessage:
|
|
@@ -193,9 +199,11 @@ class OscMessageBuilder(object):
|
|
|
193
199
|
raise BuildError(f"Could not build the message: {be}")
|
|
194
200
|
|
|
195
201
|
|
|
196
|
-
def build_msg(
|
|
202
|
+
def build_msg(
|
|
203
|
+
address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
|
|
204
|
+
) -> osc_message.OscMessage:
|
|
197
205
|
builder = OscMessageBuilder(address=address)
|
|
198
|
-
values:
|
|
206
|
+
values: Iterable[Any]
|
|
199
207
|
if value == "":
|
|
200
208
|
values = []
|
|
201
209
|
elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
@@ -72,8 +72,7 @@ class OscPacket(object):
|
|
|
72
72
|
else:
|
|
73
73
|
# Empty packet, should not happen as per the spec but heh, UDP...
|
|
74
74
|
raise ParseError(
|
|
75
|
-
"OSC Packet should at least contain an OscMessage or an "
|
|
76
|
-
"OscBundle."
|
|
75
|
+
"OSC Packet should at least contain an OscMessage or an OscBundle."
|
|
77
76
|
)
|
|
78
77
|
except (osc_bundle.ParseError, osc_message.ParseError) as pe:
|
|
79
78
|
raise ParseError(f"Could not parse packet {pe}")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""OSC Servers that receive UDP packets and invoke handlers accordingly.
|
|
2
|
-
"""
|
|
1
|
+
"""OSC Servers that receive UDP packets and invoke handlers accordingly."""
|
|
3
2
|
|
|
4
3
|
import asyncio
|
|
5
4
|
import os
|
|
5
|
+
import socket
|
|
6
6
|
import socketserver
|
|
7
7
|
from socket import socket as _socket
|
|
8
8
|
from typing import Any, Coroutine, Tuple, Union, cast
|
|
@@ -64,6 +64,8 @@ class OSCUDPServer(socketserver.UDPServer):
|
|
|
64
64
|
server_address: Tuple[str, int],
|
|
65
65
|
dispatcher: Dispatcher,
|
|
66
66
|
bind_and_activate: bool = True,
|
|
67
|
+
timeout: float | None = None,
|
|
68
|
+
family: socket.AddressFamily | None = None,
|
|
67
69
|
) -> None:
|
|
68
70
|
"""Initialize
|
|
69
71
|
|
|
@@ -71,9 +73,29 @@ class OSCUDPServer(socketserver.UDPServer):
|
|
|
71
73
|
server_address: IP and port of server
|
|
72
74
|
dispatcher: Dispatcher this server will use
|
|
73
75
|
(optional) bind_and_activate: default=True defines if the server has to start on call of constructor
|
|
76
|
+
(optional) timeout: Default timeout in seconds for socket operations
|
|
77
|
+
(optional) family: socket.AF_INET or socket.AF_INET6. If None, it will be inferred from server_address.
|
|
74
78
|
"""
|
|
79
|
+
if family is not None:
|
|
80
|
+
self.address_family = family
|
|
81
|
+
else:
|
|
82
|
+
# Try to infer address family from server_address
|
|
83
|
+
try:
|
|
84
|
+
infos = socket.getaddrinfo(
|
|
85
|
+
server_address[0],
|
|
86
|
+
server_address[1],
|
|
87
|
+
type=socket.SOCK_DGRAM,
|
|
88
|
+
family=socket.AF_UNSPEC,
|
|
89
|
+
)
|
|
90
|
+
if infos:
|
|
91
|
+
self.address_family = infos[0][0]
|
|
92
|
+
except (socket.gaierror, IndexError):
|
|
93
|
+
# Fallback to default if resolution fails
|
|
94
|
+
pass
|
|
95
|
+
|
|
75
96
|
super().__init__(server_address, _UDPHandler, bind_and_activate)
|
|
76
97
|
self._dispatcher = dispatcher
|
|
98
|
+
self.timeout = timeout
|
|
77
99
|
|
|
78
100
|
def verify_request(
|
|
79
101
|
self, request: _RequestType, client_address: _AddressType
|
|
@@ -35,6 +35,7 @@ loop.run_forever()
|
|
|
35
35
|
import asyncio
|
|
36
36
|
import logging
|
|
37
37
|
import os
|
|
38
|
+
import socket
|
|
38
39
|
import socketserver
|
|
39
40
|
import struct
|
|
40
41
|
from typing import List, Tuple
|
|
@@ -75,7 +76,7 @@ class _TCPHandler1_0(socketserver.BaseRequestHandler):
|
|
|
75
76
|
)
|
|
76
77
|
# resp = _call_handlers_for_packet(data, self.server.dispatcher)
|
|
77
78
|
for r in resp:
|
|
78
|
-
if not isinstance(r,
|
|
79
|
+
if not isinstance(r, tuple):
|
|
79
80
|
r = [r]
|
|
80
81
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
81
82
|
b = struct.pack("!I", len(msg.dgram))
|
|
@@ -117,7 +118,7 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
|
|
|
117
118
|
p, self.client_address
|
|
118
119
|
)
|
|
119
120
|
for r in resp:
|
|
120
|
-
if not isinstance(r,
|
|
121
|
+
if not isinstance(r, tuple):
|
|
121
122
|
r = [r]
|
|
122
123
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
123
124
|
self.request.sendall(slip.encode(msg.dgram))
|
|
@@ -146,11 +147,30 @@ class OSCTCPServer(socketserver.TCPServer):
|
|
|
146
147
|
server_address: Tuple[str | bytes | bytearray, int],
|
|
147
148
|
dispatcher: Dispatcher,
|
|
148
149
|
mode: str = MODE_1_1,
|
|
150
|
+
family: socket.AddressFamily | None = None,
|
|
149
151
|
):
|
|
150
152
|
self.request_queue_size = 300
|
|
151
153
|
self.mode = mode
|
|
152
154
|
if mode not in [MODE_1_0, MODE_1_1]:
|
|
153
155
|
raise ValueError("OSC Mode must be '1.0' or '1.1'")
|
|
156
|
+
|
|
157
|
+
if family is not None:
|
|
158
|
+
self.address_family = family
|
|
159
|
+
elif isinstance(server_address[0], str):
|
|
160
|
+
# Try to infer address family from server_address
|
|
161
|
+
try:
|
|
162
|
+
infos = socket.getaddrinfo(
|
|
163
|
+
server_address[0],
|
|
164
|
+
server_address[1],
|
|
165
|
+
type=socket.SOCK_STREAM,
|
|
166
|
+
family=socket.AF_UNSPEC,
|
|
167
|
+
)
|
|
168
|
+
if infos:
|
|
169
|
+
self.address_family = infos[0][0]
|
|
170
|
+
except (socket.gaierror, IndexError):
|
|
171
|
+
# Fallback to default if resolution fails
|
|
172
|
+
pass
|
|
173
|
+
|
|
154
174
|
if self.mode == MODE_1_0:
|
|
155
175
|
super().__init__(server_address, _TCPHandler1_0)
|
|
156
176
|
else:
|
|
@@ -284,7 +304,7 @@ class AsyncOSCTCPServer:
|
|
|
284
304
|
buf, client_address
|
|
285
305
|
)
|
|
286
306
|
for r in result:
|
|
287
|
-
if not isinstance(r,
|
|
307
|
+
if not isinstance(r, tuple):
|
|
288
308
|
r = [r]
|
|
289
309
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
290
310
|
b = struct.pack("!I", len(msg.dgram))
|
|
@@ -319,7 +339,7 @@ class AsyncOSCTCPServer:
|
|
|
319
339
|
p, client_address
|
|
320
340
|
)
|
|
321
341
|
for r in result:
|
|
322
|
-
if not isinstance(r,
|
|
342
|
+
if not isinstance(r, tuple):
|
|
323
343
|
r = [r]
|
|
324
344
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
325
345
|
writer.write(slip.encode(msg.dgram))
|
|
@@ -71,24 +71,21 @@ def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
|
|
|
71
71
|
raise ParseError("start_index < 0")
|
|
72
72
|
offset = 0
|
|
73
73
|
try:
|
|
74
|
-
if (
|
|
75
|
-
len(dgram) > start_index + _STRING_DGRAM_PAD
|
|
76
|
-
and dgram[start_index + _STRING_DGRAM_PAD] == _EMPTY_STR_DGRAM
|
|
77
|
-
):
|
|
78
|
-
return "", start_index + _STRING_DGRAM_PAD
|
|
79
74
|
while dgram[start_index + offset] != 0:
|
|
80
75
|
offset += 1
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
|
|
77
|
+
# OSC spec: "followed by a null, followed by 0-3 additional null characters
|
|
78
|
+
# to make the total number of bits a multiple of 32"
|
|
79
|
+
# This means the total length (including the first null) must be a multiple of 4.
|
|
80
|
+
total_len = offset + 1
|
|
81
|
+
if total_len % 4 != 0:
|
|
82
|
+
total_len += 4 - (total_len % 4)
|
|
83
|
+
|
|
84
|
+
if start_index + total_len > len(dgram):
|
|
89
85
|
raise ParseError("Datagram is too short")
|
|
86
|
+
|
|
90
87
|
data_str = dgram[start_index : start_index + offset]
|
|
91
|
-
return data_str.
|
|
88
|
+
return data_str.decode("utf-8"), start_index + total_len
|
|
92
89
|
except IndexError as ie:
|
|
93
90
|
raise ParseError(f"Could not parse datagram {ie}")
|
|
94
91
|
except TypeError as te:
|
|
@@ -214,11 +211,8 @@ def get_timetag(dgram: bytes, start_index: int) -> Tuple[Tuple[datetime, int], i
|
|
|
214
211
|
timetag, _ = get_uint64(dgram, start_index)
|
|
215
212
|
seconds, fraction = ntp.parse_timestamp(timetag)
|
|
216
213
|
|
|
217
|
-
hours, seconds = seconds // 3600, seconds % 3600
|
|
218
|
-
minutes, seconds = seconds // 60, seconds % 60
|
|
219
|
-
|
|
220
214
|
utc = datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) + timedelta(
|
|
221
|
-
|
|
215
|
+
seconds=seconds
|
|
222
216
|
)
|
|
223
217
|
|
|
224
218
|
return (utc, fraction), start_index + _TIMETAG_DGRAM_LEN
|